From 77368a6975db932582ed0a72c2914a6ff1ca3e61 Mon Sep 17 00:00:00 2001 From: flipvanhaaren Date: Thu, 9 Jan 2025 17:27:13 +0100 Subject: [PATCH 1/2] improvements: dev onboarding --- backend/package.json | 2 +- backend/scripts/drizzle-studio.ts | 26 +++++++-------- backend/scripts/quick.ts | 2 ++ backend/scripts/seeds/organizations/seed.ts | 10 ++++-- backend/scripts/seeds/user/seed.ts | 6 +++- backend/src/index.ts | 9 ++--- backend/src/lib/mailer.ts | 11 ++++++- cli/create-cella/src/create.ts | 5 ++- frontend/package.json | 3 +- frontend/src/alert-config.tsx | 33 ++++++++++++++++--- frontend/src/modules/auth/sign-up-form.tsx | 2 +- frontend/src/modules/common/public-layout.tsx | 2 ++ .../{legals-text.tsx => legal-texts.tsx} | 6 ++-- .../marketing/{legals.tsx => legal.tsx} | 6 ++-- frontend/src/routes/marketing.tsx | 4 +-- frontend/vite.config.ts | 1 + info/ARCHITECTURE.md | 11 +++++-- info/QUICKSTART.md | 4 +-- pnpm-lock.yaml | 23 +++++++++++++ 19 files changed, 119 insertions(+), 47 deletions(-) rename frontend/src/modules/marketing/{legals-text.tsx => legal-texts.tsx} (99%) rename frontend/src/modules/marketing/{legals.tsx => legal.tsx} (91%) diff --git a/backend/package.json b/backend/package.json index a282ee632..6b67f1b95 100644 --- a/backend/package.json +++ b/backend/package.json @@ -8,7 +8,7 @@ "scripts": { "quick": "cross-env PGLITE=true tsx scripts/quick.ts && cross-env PGLITE=true pnpm dev", "start": "tsx dist/index.js", - "dev": "cross-env NODE_ENV=development tsx --watch src/index.ts", + "dev": "tsx scripts/drizzle-studio.ts && cross-env NODE_ENV=development tsx --watch src/index.ts", "check:types": "tsc", "build": "cross-env NODE_ENV=production tsup", "build:dev": "tsup", diff --git a/backend/scripts/drizzle-studio.ts b/backend/scripts/drizzle-studio.ts index e2aba418e..2babb5f25 100644 --- a/backend/scripts/drizzle-studio.ts +++ b/backend/scripts/drizzle-studio.ts @@ -1,21 +1,19 @@ import { spawn } from 'node:child_process'; +import chalk from 'chalk'; -// Function to start Drizzle Studio -export const startDrizzleStudio = () => { +const startDrizzleStudioInBackground = () => { const studioProcess = spawn('npx', ['drizzle-kit', 'studio'], { - stdio: 'inherit', - shell: true, + detached: true, // Detach the process + stdio: 'ignore', // Ignore its output to let the parent process exit cleanly + shell: true, // Use shell for compatibility }); - studioProcess.on('close', (code) => { - if (code === 0) { - console.log('Drizzle Studio exited successfully.'); - } else { - console.error(`Drizzle Studio exited with code ${code}.`); - } - }); + // Detach the child process from the parent + studioProcess.unref(); - studioProcess.on('error', (err) => { - console.error('Failed to start Drizzle Studio:', err); - }); + console.log(' '); + console.log(`${chalk.greenBright.bold('✔')} Drizzle Studio started in the background`); + console.log(' '); }; + +startDrizzleStudioInBackground(); diff --git a/backend/scripts/quick.ts b/backend/scripts/quick.ts index 832c519f7..1ff47d6ca 100644 --- a/backend/scripts/quick.ts +++ b/backend/scripts/quick.ts @@ -11,7 +11,9 @@ await migrate(db, { migrationsFolder: 'drizzle', migrationsSchema: 'drizzle-back const res = await db.execute(sql`SELECT * FROM users`); if (res.rows.length > 0) { + console.info(' '); console.info(`${chalk.greenBright.bold('✔')} Database is already seeded`); + console.info(' '); process.exit(0); } diff --git a/backend/scripts/seeds/organizations/seed.ts b/backend/scripts/seeds/organizations/seed.ts index ea92e0492..a4e123def 100644 --- a/backend/scripts/seeds/organizations/seed.ts +++ b/backend/scripts/seeds/organizations/seed.ts @@ -20,7 +20,8 @@ const SYSTEM_ADMIN_MEMBERSHIP_COUNT = 10; // Seed organizations with data export const organizationsSeed = async () => { - console.info('Seeding organizations...'); + console.info(' '); + console.info('◔ Seeding organizations...'); const organizationsInTable = await db.select().from(organizationsTable).limit(1); @@ -53,7 +54,8 @@ export const organizationsSeed = async () => { }); await db.insert(organizationsTable).values(organizations).onConflictDoNothing(); - console.info('Seeding members and memberships, this can take a while...'); + console.info(' '); + console.info('◔ Seeding members and memberships, this can take a while...'); const hashedPassword = await hashPasswordWithArgon('12345678'); @@ -138,5 +140,7 @@ export const organizationsSeed = async () => { await db.insert(membershipsTable).values(memberships).onConflictDoNothing(); } - console.info(`${chalk.greenBright.bold('✔')} Created ${ORGANIZATIONS_COUNT} organizations with ${MEMBERS_COUNT} members each.`); + console.info(' '); + console.info(`${chalk.greenBright.bold('✔')} Created ${ORGANIZATIONS_COUNT} organizations with ${MEMBERS_COUNT} members each`); + console.info(' '); }; diff --git a/backend/scripts/seeds/user/seed.ts b/backend/scripts/seeds/user/seed.ts index 73850ae25..bac1bf7ab 100644 --- a/backend/scripts/seeds/user/seed.ts +++ b/backend/scripts/seeds/user/seed.ts @@ -36,5 +36,9 @@ export const userSeed = async () => { }) .onConflictDoNothing(); - console.info(`${chalk.greenBright.bold('✔')} Created admin user with verified email ${adminUser.email} and password ${adminUser.password}.`); + console.info(' '); + console.info( + `${chalk.greenBright.bold('✔')} Created admin user with verified email ${chalk.greenBright.bold(adminUser.email)} and password ${chalk.greenBright.bold(adminUser.password)}.`, + ); + console.info(' '); }; diff --git a/backend/src/index.ts b/backend/src/index.ts index 111365eb2..49d15d42a 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -6,7 +6,6 @@ import { migrate as pgliteMigrate } from 'drizzle-orm/pglite/migrator'; import { db } from '#/db/db'; import ascii from '#/utils/ascii'; import { env } from '../env'; -import { startDrizzleStudio } from '../scripts/drizzle-studio'; import docs from './lib/docs'; import app from './routes'; @@ -33,9 +32,6 @@ const main = async () => { await pgMigrate(db, migrateConfig); } - // Start Drizzle Studio in development mode - if (config.mode === 'development') startDrizzleStudio(); - // Start server serve( { @@ -45,10 +41,11 @@ const main = async () => { }, () => { ascii(); + console.info(' '); console.info( - `Open ${chalk.greenBright.bold(config.name)} on ${chalk.cyanBright(config.frontendUrl)}. Backend on ${chalk.cyanBright(config.backendUrl)}`, + `${chalk.greenBright.bold(config.name)} (Frontend) runs on ${chalk.cyanBright.bold(config.frontendUrl)}. Backend: ${chalk.cyanBright.bold(config.backendUrl)}. Docs: ${chalk.cyanBright(`${config.backendUrl}/docs`)}`, ); - console.info(`Read API docs on ${chalk.cyanBright(`${config.backendUrl}/docs`)}`); + console.info(' '); }, ); }; diff --git a/backend/src/lib/mailer.ts b/backend/src/lib/mailer.ts index 04f5ebca9..175c28820 100644 --- a/backend/src/lib/mailer.ts +++ b/backend/src/lib/mailer.ts @@ -3,12 +3,21 @@ import { config } from 'config'; import { env } from '../../env'; const sendgrid = new MailService(); +// Check if the API key is set +const hasApiKey = !!env.SENDGRID_API_KEY; -sendgrid.setApiKey(env.SENDGRID_API_KEY ?? ''); +if (hasApiKey) { + sendgrid.setApiKey(env.SENDGRID_API_KEY ?? ''); +} // Send email, currently hardcoded to use SendGrid but can be changed to any other service export const emailSender = { send: async (to: string, subject: string, html: string, replyTo?: string) => { + if (!hasApiKey) { + console.info(`Email to ${to} is not sent because API key is missing.`); + return; + } + await sendgrid.send({ to: env.SEND_ALL_TO_EMAIL || to, replyTo: replyTo ? replyTo : config.supportEmail, diff --git a/cli/create-cella/src/create.ts b/cli/create-cella/src/create.ts index 6994b56b2..9e2f77b59 100644 --- a/cli/create-cella/src/create.ts +++ b/cli/create-cella/src/create.ts @@ -168,7 +168,6 @@ export async function create({ if (needsCd) { // Calculate the relative path between the original working directory and the target folder - console.info('now go to your project using:'); console.info(colors.cyan(` cd ${relativePath}`)); // Adding './' to make it clear it's a relative path console.info(); @@ -184,6 +183,10 @@ export async function create({ console.info(colors.cyan(` ${packageManager} seed`)); console.info(); + console.info(`You can directly sign in using:`); + console.info(`email: ${colors.greenBright('admin-test@cellajs.com')}`); + console.info(`password: ${colors.greenBright('12345678')}`); + console.info(); console.info(`For more info, check out: ${relativePath}/README.md`); console.info(`Enjoy building ${projectName} using cella! 🎉`); console.info(); diff --git a/frontend/package.json b/frontend/package.json index c058f44ed..cb2e93e43 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,9 +7,9 @@ }, "scripts": { "quick": "cross-env VITE_QUICK=true pnpm dev", + "dev": "cross-env NODE_ENV=development vite --mode development && node --trace-warnings", "start": "cross-env NODE_ENV=production pnpm preview", "check:types": "tsc", - "dev": "cross-env NODE_ENV=development vite --mode development && node --trace-warnings", "build": "tsc && vite build", "build:dev": "cross-env NODE_ENV=development vite build --mode development", "preview": "vite preview --port 3000", @@ -141,6 +141,7 @@ "@vitejs/plugin-react": "^4.3.4", "autoprefixer": "^10.4.20", "cross-env": "^7.0.3", + "kill-port": "^2.0.1", "postcss": "^8.4.49", "postcss-import": "^16.1.0", "postcss-preset-env": "^10.1.3", diff --git a/frontend/src/alert-config.tsx b/frontend/src/alert-config.tsx index 35401fb2d..635372833 100644 --- a/frontend/src/alert-config.tsx +++ b/frontend/src/alert-config.tsx @@ -1,10 +1,30 @@ +import { config } from 'config'; import { t } from 'i18next'; import { Info } from 'lucide-react'; import type { MainAlert } from '~/modules/common/main-alert'; -// Here you can set app-specific global alerts -export const alertsConfig: MainAlert[] = [ - { +const alerts = []; + +// Explain how to sign in using test account +if (config.mode === 'development') { + alerts.push({ + id: 'test-credentials', + Icon: Info, + className: 'rounded-none border-0 border-t z-10 fixed bottom-0 left-0 right-0', + children: ( + <> + Testing credentials +

+ Hi there! New developer? Welcome to Cella! Sign in using admin-test@cellajs.com and password 12345678. +

+ + ), + }); +} + +// In production mode, show a notice that the app is a pre-release version +if (config.mode === 'production') { + alerts.push({ id: 'prerelease', Icon: Info, className: 'rounded-none border-0 border-b', @@ -14,5 +34,8 @@ export const alertsConfig: MainAlert[] = [ {t('common:experiment_notice.text')} ), - }, -]; + }); +} + +// Here you can set app-specific global alerts +export const alertsConfig: MainAlert[] = alerts; diff --git a/frontend/src/modules/auth/sign-up-form.tsx b/frontend/src/modules/auth/sign-up-form.tsx index d961f786d..2c02c4976 100644 --- a/frontend/src/modules/auth/sign-up-form.tsx +++ b/frontend/src/modules/auth/sign-up-form.tsx @@ -17,7 +17,7 @@ import { Input } from '~/modules/ui/input'; import type { TokenData } from '.'; const PasswordStrength = lazy(() => import('~/modules/auth/password-strength')); -const LegalText = lazy(() => import('~/modules/marketing/legals-text')); +const LegalText = lazy(() => import('~/modules/marketing/legal-texts')); const formSchema = authBodySchema; diff --git a/frontend/src/modules/common/public-layout.tsx b/frontend/src/modules/common/public-layout.tsx index 09a8b4079..8c7177676 100644 --- a/frontend/src/modules/common/public-layout.tsx +++ b/frontend/src/modules/common/public-layout.tsx @@ -2,11 +2,13 @@ import { Outlet } from '@tanstack/react-router'; import { Dialoger } from '~/modules/common/dialoger'; import { DropDowner } from '~/modules/common/dropdowner'; import { Sheeter } from '~/modules/common/sheeter'; +import AlertRenderer from './main-alert/alert-render'; // Also in public routes, some components need to be initialized. function PublicLayout() { return ( <> + diff --git a/frontend/src/modules/marketing/legals-text.tsx b/frontend/src/modules/marketing/legal-texts.tsx similarity index 99% rename from frontend/src/modules/marketing/legals-text.tsx rename to frontend/src/modules/marketing/legal-texts.tsx index e18f5bc65..ec087fdc5 100644 --- a/frontend/src/modules/marketing/legals-text.tsx +++ b/frontend/src/modules/marketing/legal-texts.tsx @@ -1,8 +1,8 @@ import { Link } from '@tanstack/react-router'; import { config } from 'config'; -import type { LegalTypes } from './legals'; +import type { LegalTypes } from './legal'; -const LegalText = ({ textFor }: { textFor: LegalTypes }) => { +const LegalTexts = ({ textFor }: { textFor: LegalTypes }) => { const appName = config.name; const companyFull = config.company.name; const companyShort = config.company.shortName; @@ -566,4 +566,4 @@ const LegalText = ({ textFor }: { textFor: LegalTypes }) => { ); }; -export default LegalText; +export default LegalTexts; diff --git a/frontend/src/modules/marketing/legals.tsx b/frontend/src/modules/marketing/legal.tsx similarity index 91% rename from frontend/src/modules/marketing/legals.tsx rename to frontend/src/modules/marketing/legal.tsx index 2fbb79163..bed14b689 100644 --- a/frontend/src/modules/marketing/legals.tsx +++ b/frontend/src/modules/marketing/legal.tsx @@ -7,7 +7,7 @@ import { SimpleHeader } from '~/modules/common/simple-header'; import StickyBox from '~/modules/common/sticky-box'; import PublicPage from '~/modules/marketing/page'; -const LegalText = lazy(() => import('~/modules/marketing/legals-text')); +const LegalTexts = lazy(() => import('~/modules/marketing/legal-texts')); export type LegalTypes = 'privacy' | 'terms'; @@ -15,7 +15,7 @@ const Legal = ({ type }: { type: LegalTypes }) => { return (
- +
); @@ -26,7 +26,7 @@ const tabs = [ { id: 'privacy', label: 'common:privacy_policy' }, ] as const; -export const LegalsMenu = () => { +export const LegalMenu = () => { const { t } = useTranslation(); return ( diff --git a/frontend/src/routes/marketing.tsx b/frontend/src/routes/marketing.tsx index 51eef6ae8..f1fb26503 100644 --- a/frontend/src/routes/marketing.tsx +++ b/frontend/src/routes/marketing.tsx @@ -2,7 +2,7 @@ import { createRoute } from '@tanstack/react-router'; import About from '~/modules/marketing/about'; import Accessibility from '~/modules/marketing/accessibility'; import Contact from '~/modules/marketing/contact'; -import { LegalsMenu } from '~/modules/marketing/legals'; +import { LegalMenu } from '~/modules/marketing/legal'; import { PublicRoute, rootRoute } from './general'; export const AboutRoute = createRoute({ @@ -23,7 +23,7 @@ export const LegalRoute = createRoute({ path: '/legal', staticData: { pageTitle: 'Legal', isAuth: false }, getParentRoute: () => rootRoute, - component: () => , + component: () => , }); export const AccessibilityRoute = createRoute({ diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index f23f59f96..d8b46fb74 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -21,6 +21,7 @@ export default defineConfig(() => { server: { host: '0.0.0.0', port: Number(frontendUrl.port), + strictPort: true, }, build: { rollupOptions: { diff --git a/info/ARCHITECTURE.md b/info/ARCHITECTURE.md index 3bf644374..9b751715a 100644 --- a/info/ARCHITECTURE.md +++ b/info/ARCHITECTURE.md @@ -8,8 +8,10 @@ This document describes the high-level architecture of Cella. 5. Open standards. Our long term vision is that each Cella - as in each cell - can speak fluently with other cells. ### Backend -- [Hono](https://hono.dev) + [NodeJS](https://nodejs.org) -- [Postgres](https://www.postgresql.org) / [PGLite](https://pglite.dev/) + [Drizzle ORM](https://orm.drizzle.team/) +- [NodeJS](https://nodejs.org) +- [Hono](https://hono.dev) +- [Postgres](https://www.postgresql.org) +- [Drizzle ORM](https://orm.drizzle.team/) - [Zod](https://github.com/colinhacks/zod) - [OpenAPI](https://www.openapis.org) - [Lucia Auth](https://lucia-auth.com/) @@ -29,10 +31,13 @@ This document describes the high-level architecture of Cella. - [Lucide icons](https://lucide.dev) ### Build tools -- [Vite](https://vitejs.dev) + [Vite-PWA](https://github.com/antfu/vite-plugin-pwa) - [pnpm](https://pnpm.io) +- [Vite](https://vitejs.dev) +- [Vite-PWA](https://github.com/antfu/vite-plugin-pwa) - [Biome](https://biomejs.dev) - [Lefthook](https://github.com/evilmartians/lefthook) +- [PGLite](https://pglite.dev/) + ## File structure ``` diff --git a/info/QUICKSTART.md b/info/QUICKSTART.md index e1b37ef45..3a60ed04f 100644 --- a/info/QUICKSTART.md +++ b/info/QUICKSTART.md @@ -23,8 +23,8 @@ pnpm seed ## Customize 1. Customize your config in `/config/default.ts` 2. Update package.json with your own data -3. Look at you .env file to understand what is required and update accordingly -4. There are many config files, which end with '-config.ts'. Here you can set for example your entity structure or your navigation structure. +3. Look at your .env file to understand what is required, for example to send emails you will need an API key. +4. There are many config files with filenames like `-config.ts`. For example for entities or navigation structure. ## Cella CLI diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cbeb9f4c7..1428270f1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -675,6 +675,9 @@ importers: cross-env: specifier: ^7.0.3 version: 7.0.3 + kill-port: + specifier: ^2.0.1 + version: 2.0.1 postcss: specifier: ^8.4.49 version: 8.4.49 @@ -2844,6 +2847,7 @@ packages: '@evilmartians/lefthook@1.10.1': resolution: {integrity: sha512-G1NPLB4yLYYEyz8oH7yWgHsxUiF546aS1ChSRPNFPSosLxZFM8wqDxek/J9sT447v83gJbKdrnstxeQW/9CIRA==} + cpu: [x64, arm64, ia32] os: [darwin, linux, win32] hasBin: true @@ -7558,6 +7562,9 @@ packages: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} + get-them-args@1.3.2: + resolution: {integrity: sha512-LRn8Jlk+DwZE4GTlDbT3Hikd1wSHgLMme/+7ddlqKd7ldwR6LjJgTVWzBnR01wnYGe4KgrXjg287RaI22UHmAw==} + get-tsconfig@4.8.1: resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} @@ -8261,6 +8268,10 @@ packages: jws@3.2.2: resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + kill-port@2.0.1: + resolution: {integrity: sha512-e0SVOV5jFo0mx8r7bS29maVWp17qGqLBZ5ricNSajON6//kmb7qqqNnml4twNE8Dtj97UQD+gNFOaipS/q1zzQ==} + hasBin: true + kind-of@3.2.2: resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==} engines: {node: '>=0.10.0'} @@ -10194,6 +10205,9 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shell-exec@1.0.2: + resolution: {integrity: sha512-jyVd+kU2X+mWKMmGhx4fpWbPsjvD53k9ivqetutVW/BQ+WIZoDoP4d8vUMGezV6saZsiNoW2f9GIhg9Dondohg==} + shiki@1.26.1: resolution: {integrity: sha512-Gqg6DSTk3wYqaZ5OaYtzjcdxcBvX5kCy24yvRJEgjT5U+WHlmqCThLuBUx0juyxQBi+6ug53IGeuQS07DWwpcw==} @@ -18907,6 +18921,8 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.2.7 + get-them-args@1.3.2: {} + get-tsconfig@4.8.1: dependencies: resolve-pkg-maps: 1.0.0 @@ -19867,6 +19883,11 @@ snapshots: jwa: 1.4.1 safe-buffer: 5.2.1 + kill-port@2.0.1: + dependencies: + get-them-args: 1.3.2 + shell-exec: 1.0.2 + kind-of@3.2.2: dependencies: is-buffer: 1.1.6 @@ -22162,6 +22183,8 @@ snapshots: shebang-regex@3.0.0: {} + shell-exec@1.0.2: {} + shiki@1.26.1: dependencies: '@shikijs/core': 1.26.1 From 87be0a7c4d5ecb144d30a5dde970e3a465598a5f Mon Sep 17 00:00:00 2001 From: flipvanhaaren Date: Thu, 9 Jan 2025 17:28:15 +0100 Subject: [PATCH 2/2] bump create cella --- cli/create-cella/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/create-cella/package.json b/cli/create-cella/package.json index b98fafde3..c97423636 100644 --- a/cli/create-cella/package.json +++ b/cli/create-cella/package.json @@ -1,6 +1,6 @@ { "name": "@cellajs/create-cella", - "version": "0.1.0", + "version": "0.1.1", "private": false, "license": "MIT", "description": "Cella is a TypeScript template to create web apps with sync and offline capabilities.",