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/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.", 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 ( <> +