diff --git a/.changeset/curvy-poems-camp.md b/.changeset/curvy-poems-camp.md new file mode 100644 index 0000000..c52caeb --- /dev/null +++ b/.changeset/curvy-poems-camp.md @@ -0,0 +1,5 @@ +--- +"create-better-t-stack": minor +--- + +Add automatic prisma postgres setup diff --git a/apps/cli/package.json b/apps/cli/package.json index 4fb8abd..4fcce20 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -7,10 +7,7 @@ "bin": { "create-better-t-stack": "dist/index.js" }, - "files": [ - "dist", - "template" - ], + "files": ["dist", "template"], "keywords": [ "typescript", "scaffold", diff --git a/apps/cli/src/constants.ts b/apps/cli/src/constants.ts index e3334aa..0f55cb4 100644 --- a/apps/cli/src/constants.ts +++ b/apps/cli/src/constants.ts @@ -19,6 +19,7 @@ export const DEFAULT_CONFIG: ProjectConfig = { packageManager: getUserPkgManager(), noInstall: false, turso: false, + prismaPostgres: false, backend: "hono", runtime: "bun", }; @@ -62,6 +63,8 @@ export const dependencyVersionMap = { ai: "^4.2.8", "@ai-sdk/google": "^1.2.3", + + "@prisma/extension-accelerate": "^1.3.0", } as const; export type AvailableDependencies = keyof typeof dependencyVersionMap; diff --git a/apps/cli/src/helpers/create-project.ts b/apps/cli/src/helpers/create-project.ts index cb2e068..e3cc380 100644 --- a/apps/cli/src/helpers/create-project.ts +++ b/apps/cli/src/helpers/create-project.ts @@ -53,6 +53,7 @@ export async function createProject(options: ProjectConfig): Promise { projectDir, options.database, options.orm, + options.packageManager, options.turso ?? options.database === "sqlite", ); diff --git a/apps/cli/src/helpers/create-readme.ts b/apps/cli/src/helpers/create-readme.ts index 9fea16b..0730123 100644 --- a/apps/cli/src/helpers/create-readme.ts +++ b/apps/cli/src/helpers/create-readme.ts @@ -75,7 +75,7 @@ The API is running at [http://localhost:3000](http://localhost:3000). ${ addons.includes("pwa") && hasReactRouter - ? "\n## PWA Support with React Router v7\n\nThere is a known compatibility issue between VitePWA and React Router v7.\nSee: https://github.com/vite-pwa/vite-plugin-pwa/issues/809\n\nIf you encounter problems with the PWA functionality, you may need to manually modify\nthe service worker registration or consider waiting for a fix from VitePWA.\n" + ? "\n## PWA Support with React Router v7\n\nThere is a known compatibility issue between VitePWA and React Router v7.\nSee: https://github.com/vite-pwa/vite-plugin-pwa/issues/809\n" : "" } diff --git a/apps/cli/src/helpers/db-setup.ts b/apps/cli/src/helpers/db-setup.ts index 2498745..5ecb529 100644 --- a/apps/cli/src/helpers/db-setup.ts +++ b/apps/cli/src/helpers/db-setup.ts @@ -2,14 +2,20 @@ import path from "node:path"; import { log, spinner } from "@clack/prompts"; import fs from "fs-extra"; import pc from "picocolors"; -import type { ProjectDatabase, ProjectOrm } from "../types"; +import type { + ProjectDatabase, + ProjectOrm, + ProjectPackageManager, +} from "../types"; import { addPackageDependency } from "../utils/add-package-deps"; +import { setupPrismaPostgres } from "./prisma-postgres-setup"; import { setupTurso } from "./turso-setup"; export async function setupDatabase( projectDir: string, databaseType: ProjectDatabase, orm: ProjectOrm, + packageManager: ProjectPackageManager, setupTursoDb = true, ): Promise { const s = spinner(); @@ -52,6 +58,10 @@ export async function setupDatabase( devDependencies: ["prisma"], projectDir: serverDir, }); + + if (databaseType === "postgres" && orm === "prisma") { + await setupPrismaPostgres(projectDir, true, packageManager); + } } } } catch (error) { diff --git a/apps/cli/src/helpers/post-installation.ts b/apps/cli/src/helpers/post-installation.ts index 5b6306e..ed19006 100644 --- a/apps/cli/src/helpers/post-installation.ts +++ b/apps/cli/src/helpers/post-installation.ts @@ -89,7 +89,7 @@ function getDatabaseInstructions( if (runtime === "bun") { instructions.push( - `${pc.yellow("NOTE:")} Prisma with Bun may require additional configuration. If you encounter errors, follow the guidance provided in the error messages`, + `${pc.yellow("NOTE:")} Prisma with Bun may require additional configuration. If you encounter errors,\nfollow the guidance provided in the error messages`, ); } diff --git a/apps/cli/src/helpers/prisma-postgres-setup.ts b/apps/cli/src/helpers/prisma-postgres-setup.ts new file mode 100644 index 0000000..b3e307e --- /dev/null +++ b/apps/cli/src/helpers/prisma-postgres-setup.ts @@ -0,0 +1,178 @@ +import path from "node:path"; +import { cancel, isCancel, log, password } from "@clack/prompts"; +import { execa } from "execa"; +import fs from "fs-extra"; +import pc from "picocolors"; +import type { ProjectPackageManager } from "../types"; +import { addPackageDependency } from "../utils/add-package-deps"; + +type PrismaConfig = { + databaseUrl: string; +}; + +async function initPrismaDatabase( + serverDir: string, + packageManager: ProjectPackageManager, +): Promise { + try { + log.info(pc.blue("Initializing Prisma PostgreSQL")); + + const prismaDir = path.join(serverDir, "prisma"); + await fs.ensureDir(prismaDir); + + const initCmd = + packageManager === "npm" + ? "npx" + : packageManager === "pnpm" + ? "pnpm dlx" + : "bunx"; + + await execa(initCmd, ["prisma", "init", "--db"], { + cwd: serverDir, + stdio: "inherit", + }); + + log.info( + pc.yellow( + "Please copy the Prisma Postgres URL from the output above.\nIt looks like: prisma+postgres://accelerate.prisma-data.net/?api_key=...", + ), + ); + + const databaseUrl = await password({ + message: "Paste your Prisma Postgres database URL:", + validate(value) { + if (!value) return "Please enter a database URL"; + if (!value.startsWith("prisma+postgres://")) { + return "URL should start with prisma+postgres://"; + } + }, + }); + + if (isCancel(databaseUrl)) { + cancel("Database setup cancelled"); + return null; + } + + return { + databaseUrl: databaseUrl as string, + }; + } catch (error) { + if (error instanceof Error) { + log.error(pc.red(error.message)); + } + return null; + } +} + +async function writeEnvFile(projectDir: string, config?: PrismaConfig) { + const envPath = path.join(projectDir, "apps/server", ".env"); + let envContent = ""; + + if (await fs.pathExists(envPath)) { + envContent = await fs.readFile(envPath, "utf8"); + } + + const databaseUrlLine = config + ? `DATABASE_URL="${config.databaseUrl}"` + : `DATABASE_URL="postgresql://postgres:postgres@localhost:5432/mydb?schema=public"`; + + if (!envContent.includes("DATABASE_URL=")) { + envContent += `\n${databaseUrlLine}`; + } else { + envContent = envContent.replace( + /DATABASE_URL=.*(\r?\n|$)/, + `${databaseUrlLine}$1`, + ); + } + + await fs.writeFile(envPath, envContent.trim()); +} + +function displayManualSetupInstructions() { + log.info(`Manual Prisma PostgreSQL Setup Instructions: + +1. Visit https://console.prisma.io and create an account +2. Create a new PostgreSQL database from the dashboard +3. Get your database URL +4. Add the database URL to the .env file in apps/server/.env + +DATABASE_URL="your_database_url"`); +} + +export async function setupPrismaPostgres( + projectDir: string, + shouldSetupPrisma: boolean, + packageManager: ProjectPackageManager = "npm", +) { + const serverDir = path.join(projectDir, "apps/server"); + + if (!shouldSetupPrisma) { + await writeEnvFile(projectDir); + log.info( + pc.blue( + "Using default Postgres configuration. You'll need to provide your own database.", + ), + ); + return; + } + + try { + const config = await initPrismaDatabase(serverDir, packageManager); + + if (config) { + await writeEnvFile(projectDir, config); + await addPrismaAccelerateExtension(serverDir); + log.success( + pc.green("Prisma PostgreSQL database configured successfully!"), + ); + } else { + await writeEnvFile(projectDir); + displayManualSetupInstructions(); + } + } catch (error) { + log.error(pc.red(`Error during Prisma PostgreSQL setup: ${error}`)); + await writeEnvFile(projectDir); + displayManualSetupInstructions(); + log.info("Setup completed with manual configuration required."); + } +} + +async function addPrismaAccelerateExtension(serverDir: string) { + try { + addPackageDependency({ + dependencies: ["@prisma/extension-accelerate"], + projectDir: serverDir, + }); + + const prismaIndexPath = path.join(serverDir, "prisma/index.ts"); + const prismaIndexContent = ` +import { PrismaClient } from '@prisma/client'; +import { withAccelerate } from "@prisma/extension-accelerate"; + +const prisma = new PrismaClient().$extends(withAccelerate()); + +export default prisma; +`; + await fs.writeFile(prismaIndexPath, prismaIndexContent.trim()); + + const dbFilePath = path.join(serverDir, "src/db/index.ts"); + if (await fs.pathExists(dbFilePath)) { + let dbFileContent = await fs.readFile(dbFilePath, "utf8"); + + if (!dbFileContent.includes("@prisma/extension-accelerate")) { + dbFileContent = `import { withAccelerate } from "@prisma/extension-accelerate";\n${dbFileContent}`; + + dbFileContent = dbFileContent.replace( + "export const db = new PrismaClient();", + "export const db = new PrismaClient().$extends(withAccelerate());", + ); + + await fs.writeFile(dbFilePath, dbFileContent); + } + } + } catch (error) { + log.warn( + pc.yellow("Could not add Prisma Accelerate extension automatically"), + ); + } +} diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index 2274e91..8edd50c 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -58,6 +58,8 @@ async function main() { .option("--no-install", "Skip installing dependencies") .option("--turso", "Set up Turso for SQLite database") .option("--no-turso", "Skip Turso setup") + .option("--prisma-postgres", "Set up Prisma Postgres") + .option("--no-prisma-postgres", "Skip Prisma Postgres setup") .option("--backend ", "Backend framework (hono, elysia)") .option("--runtime ", "Runtime (bun, node)") .parse(); @@ -198,6 +200,20 @@ function validateOptions(options: CLIOptions): void { process.exit(1); } + if ("prismaPostgres" in options && options.prismaPostgres === true) { + if ( + (options.database && options.database !== "postgres") || + (options.orm && options.orm !== "prisma") + ) { + cancel( + pc.red( + "Prisma PostgreSQL setup requires PostgreSQL database with Prisma ORM. Cannot use --prisma-postgres with incompatible database or ORM options.", + ), + ); + process.exit(1); + } + } + if ( options.packageManager && !["npm", "pnpm", "bun"].includes(options.packageManager) @@ -384,16 +400,36 @@ function processFlags( } } - const database = options.database as ProjectDatabase | undefined; + let database = options.database as ProjectDatabase | undefined; let orm: ProjectOrm | undefined; if (options.orm) { orm = options.orm as ProjectOrm; } + if ("prismaPostgres" in options && options.prismaPostgres === true) { + if (!database) { + database = "postgres" as ProjectDatabase; + } + if (!orm) { + orm = "prisma" as ProjectOrm; + } + } + let auth: boolean | undefined = "auth" in options ? options.auth : undefined; let tursoOption: boolean | undefined = "turso" in options ? options.turso : undefined; + let prismaPostgresOption: boolean | undefined = + "prismaPostgres" in options ? options.prismaPostgres : undefined; + + if ( + database === "none" || + (database === "sqlite" && database !== undefined) || + (orm !== undefined && orm !== "prisma") + ) { + prismaPostgresOption = false; + } + if (database === "none") { orm = "none"; auth = false; @@ -473,21 +509,25 @@ function processFlags( | ProjectPackageManager | undefined; - return { - ...(projectDirectory && { projectName: projectDirectory }), - ...(database !== undefined && { database }), - ...(orm !== undefined && { orm }), - ...(auth !== undefined && { auth }), - ...(packageManager && { packageManager }), - ...("git" in options && { git: options.git }), - ...("install" in options && { noInstall: !options.install }), - ...(tursoOption !== undefined && { turso: tursoOption }), - ...(backend && { backend }), - ...(runtime && { runtime }), - ...(frontend !== undefined && { frontend }), - ...(addons !== undefined && { addons }), - ...(examples !== undefined && { examples }), - }; + const config: Partial = {}; + + if (projectDirectory) config.projectName = projectDirectory; + if (database !== undefined) config.database = database; + if (orm !== undefined) config.orm = orm; + if (auth !== undefined) config.auth = auth; + if (packageManager) config.packageManager = packageManager; + if ("git" in options) config.git = options.git; + if ("install" in options) config.noInstall = !options.install; + if (tursoOption !== undefined) config.turso = tursoOption; + if (prismaPostgresOption !== undefined) + config.prismaPostgres = prismaPostgresOption; + if (backend) config.backend = backend; + if (runtime) config.runtime = runtime; + if (frontend !== undefined) config.frontend = frontend; + if (addons !== undefined) config.addons = addons; + if (examples !== undefined) config.examples = examples; + + return config; } main().catch((err) => { diff --git a/apps/cli/src/prompts/config-prompts.ts b/apps/cli/src/prompts/config-prompts.ts index 61b33ca..db98d98 100644 --- a/apps/cli/src/prompts/config-prompts.ts +++ b/apps/cli/src/prompts/config-prompts.ts @@ -21,6 +21,7 @@ import { getGitChoice } from "./git"; import { getNoInstallChoice } from "./install"; import { getORMChoice } from "./orm"; import { getPackageManagerChoice } from "./package-manager"; +import { getPrismaSetupChoice } from "./prisma-postgres"; import { getProjectName } from "./project-name"; import { getRuntimeChoice } from "./runtime"; import { getTursoSetupChoice } from "./turso"; @@ -36,6 +37,7 @@ type PromptGroupResults = { packageManager: ProjectPackageManager; noInstall: boolean; turso: boolean; + prismaPostgres: boolean; backend: ProjectBackend; runtime: ProjectRuntime; frontend: ProjectFrontend[]; @@ -65,6 +67,10 @@ export async function gatherConfig( results.database === "sqlite" && results.orm !== "prisma" ? getTursoSetupChoice(flags.turso) : Promise.resolve(false), + prismaPostgres: ({ results }) => + results.database === "postgres" && results.orm === "prisma" + ? getPrismaSetupChoice(flags.prismaPostgres) + : Promise.resolve(false), addons: ({ results }) => getAddonsChoice(flags.addons, results.frontend), examples: ({ results }) => getExamplesChoice( @@ -97,6 +103,7 @@ export async function gatherConfig( packageManager: result.packageManager, noInstall: result.noInstall, turso: result.turso, + prismaPostgres: result.prismaPostgres, backend: result.backend, runtime: result.runtime, }; diff --git a/apps/cli/src/prompts/prisma-postgres.ts b/apps/cli/src/prompts/prisma-postgres.ts new file mode 100644 index 0000000..9cc30e5 --- /dev/null +++ b/apps/cli/src/prompts/prisma-postgres.ts @@ -0,0 +1,21 @@ +import { cancel, confirm, isCancel } from "@clack/prompts"; +import pc from "picocolors"; +import { DEFAULT_CONFIG } from "../constants"; + +export async function getPrismaSetupChoice( + prismaSetup?: boolean, +): Promise { + if (prismaSetup !== undefined) return prismaSetup; + + const response = await confirm({ + message: "Set up Prisma Postgres database?", + initialValue: DEFAULT_CONFIG.prismaPostgres, + }); + + if (isCancel(response)) { + cancel(pc.red("Operation cancelled")); + process.exit(0); + } + + return response; +} diff --git a/apps/cli/src/types.ts b/apps/cli/src/types.ts index 59ff438..85079a8 100644 --- a/apps/cli/src/types.ts +++ b/apps/cli/src/types.ts @@ -20,6 +20,7 @@ export interface ProjectConfig { packageManager: ProjectPackageManager; noInstall?: boolean; turso?: boolean; + prismaPostgres: boolean; frontend: ProjectFrontend[]; } @@ -35,6 +36,7 @@ export type CLIOptions = { packageManager?: string; install?: boolean; turso?: boolean; + prismaPostgres?: boolean; backend?: string; runtime?: string; }; diff --git a/apps/cli/src/utils/display-config.ts b/apps/cli/src/utils/display-config.ts index 6465ca4..eb2a6d3 100644 --- a/apps/cli/src/utils/display-config.ts +++ b/apps/cli/src/utils/display-config.ts @@ -64,5 +64,11 @@ export function displayConfig(config: Partial) { configDisplay.push(`${pc.blue("Turso Setup:")} ${config.turso}`); } + if (config.prismaPostgres !== undefined) { + configDisplay.push( + `${pc.blue("Prisma Postgres Setup:")} ${config.prismaPostgres ? "Yes" : "No"}`, + ); + } + return configDisplay.join("\n"); } diff --git a/apps/cli/template/with-auth/apps/web-tanstack/src/components/header.tsx b/apps/cli/template/with-auth/apps/web-tanstack-router/src/components/header.tsx similarity index 100% rename from apps/cli/template/with-auth/apps/web-tanstack/src/components/header.tsx rename to apps/cli/template/with-auth/apps/web-tanstack-router/src/components/header.tsx diff --git a/apps/cli/template/with-auth/apps/web-tanstack/src/components/sign-in-form.tsx b/apps/cli/template/with-auth/apps/web-tanstack-router/src/components/sign-in-form.tsx similarity index 100% rename from apps/cli/template/with-auth/apps/web-tanstack/src/components/sign-in-form.tsx rename to apps/cli/template/with-auth/apps/web-tanstack-router/src/components/sign-in-form.tsx diff --git a/apps/cli/template/with-auth/apps/web-tanstack/src/components/sign-up-form.tsx b/apps/cli/template/with-auth/apps/web-tanstack-router/src/components/sign-up-form.tsx similarity index 100% rename from apps/cli/template/with-auth/apps/web-tanstack/src/components/sign-up-form.tsx rename to apps/cli/template/with-auth/apps/web-tanstack-router/src/components/sign-up-form.tsx diff --git a/apps/cli/template/with-auth/apps/web-tanstack/src/components/user-menu.tsx b/apps/cli/template/with-auth/apps/web-tanstack-router/src/components/user-menu.tsx similarity index 100% rename from apps/cli/template/with-auth/apps/web-tanstack/src/components/user-menu.tsx rename to apps/cli/template/with-auth/apps/web-tanstack-router/src/components/user-menu.tsx diff --git a/apps/cli/template/with-auth/apps/web-tanstack/src/routes/dashboard.tsx b/apps/cli/template/with-auth/apps/web-tanstack-router/src/routes/dashboard.tsx similarity index 100% rename from apps/cli/template/with-auth/apps/web-tanstack/src/routes/dashboard.tsx rename to apps/cli/template/with-auth/apps/web-tanstack-router/src/routes/dashboard.tsx diff --git a/apps/cli/template/with-auth/apps/web-tanstack/src/routes/login.tsx b/apps/cli/template/with-auth/apps/web-tanstack-router/src/routes/login.tsx similarity index 100% rename from apps/cli/template/with-auth/apps/web-tanstack/src/routes/login.tsx rename to apps/cli/template/with-auth/apps/web-tanstack-router/src/routes/login.tsx diff --git a/apps/web/src/app/(home)/_components/CustomizableSection.tsx b/apps/web/src/app/(home)/_components/CustomizableSection.tsx index 2c3e836..d9b9698 100644 --- a/apps/web/src/app/(home)/_components/CustomizableSection.tsx +++ b/apps/web/src/app/(home)/_components/CustomizableSection.tsx @@ -4,7 +4,7 @@ import StackArchitect from "./StackArchitech"; export default function CustomizableSection() { return ( -
+
{ } }, [stack.frontend, stack.auth]); + useEffect(() => { + if (stack.database === "none" && stack.orm !== "none") { + setStack((prev) => ({ ...prev, orm: "none" })); + } + + if (stack.database !== "postgres" || stack.orm !== "prisma") { + if (stack.prismaPostgres === "true") { + setStack((prev) => ({ ...prev, prismaPostgres: "false" })); + } + } + + if (stack.database !== "sqlite" || stack.orm === "prisma") { + if (stack.turso === "true") { + setStack((prev) => ({ ...prev, turso: "false" })); + } + } + }, [stack.database, stack.orm, stack.prismaPostgres, stack.turso]); + useEffect(() => { const cmd = generateCommand(stack); setCommand(cmd); @@ -387,13 +425,26 @@ const StackArchitect = () => { "Turso integration is only available with SQLite database.", ); } + if (stack.orm === "prisma") { + notes.turso.push("Turso is not compatible with Prisma ORM."); + } + + notes.prismaPostgres = []; + if (stack.database !== "postgres" || stack.orm !== "prisma") { + notes.prismaPostgres.push( + "Prisma PostgreSQL setup requires PostgreSQL database with Prisma ORM.", + ); + } notes.examples = []; if (!hasWebFrontend) { notes.examples.push( - "Todo and Ai example are only available with React Web.", + "Todo and AI examples are only available with React Web.", ); } + if (stack.backendFramework === "elysia") { + notes.examples.push("AI example is only compatible with Hono backend."); + } setCompatNotes(notes); }, [stack]); @@ -438,6 +489,10 @@ const StackArchitect = () => { flags.push("--turso"); } + if (stackState.prismaPostgres === "true") { + flags.push("--prisma-postgres"); + } + if (stackState.backendFramework !== "hono") { flags.push(`--backend ${stackState.backendFramework}`); } @@ -481,18 +536,14 @@ const StackArchitect = () => { ...prev, frontend: ["none"], auth: "false", - examples: prev.examples.filter( - (ex) => ex !== "todo" && ex !== "ai", - ), + examples: [], addons: prev.addons.filter( (addon) => addon !== "pwa" && addon !== "tauri", ), }; } - // Handle web router types (tanstack-router or react-router) if (webTypes.includes(techId)) { - // If clicking on an already selected web router, do nothing if ( currentSelection.includes(techId) && currentSelection.length === 1 @@ -500,7 +551,6 @@ const StackArchitect = () => { return prev; } - // If selecting a web router while another one is active, replace it if (currentSelection.some((id) => webTypes.includes(id))) { const nonWebSelections = currentSelection.filter( (id) => !webTypes.includes(id), @@ -508,11 +558,10 @@ const StackArchitect = () => { return { ...prev, frontend: [...nonWebSelections, techId], - auth: prev.auth, // Keep existing auth setting + auth: prev.auth, }; } - // If no web router was selected before if (currentSelection.includes("none")) { return { ...prev, @@ -531,11 +580,10 @@ const StackArchitect = () => { }; } - // Handle native selection if (techId === "native") { if (currentSelection.includes(techId)) { if (currentSelection.length === 1) { - return prev; // Don't allow removing the last frontend + return prev; } return { ...prev, @@ -571,11 +619,20 @@ const StackArchitect = () => { } else { if ( category === "examples" && - techId === "todo" && + (techId === "todo" || techId === "ai") && !hasWebFrontend ) { return prev; } + + if ( + category === "examples" && + techId === "ai" && + prev.backendFramework === "elysia" + ) { + return prev; + } + if ( category === "addons" && (techId === "pwa" || techId === "tauri") && @@ -583,6 +640,15 @@ const StackArchitect = () => { ) { return prev; } + + if ( + category === "addons" && + techId === "husky" && + !currentArray.includes("biome") + ) { + currentArray.push("biome"); + } + currentArray.push(techId); } @@ -597,8 +663,10 @@ const StackArchitect = () => { return { ...prev, database: techId, - orm: null, + orm: "none", turso: "false", + prismaPostgres: "false", + auth: hasWebFrontend(prev.frontend) ? prev.auth : "false", }; } @@ -607,25 +675,63 @@ const StackArchitect = () => { ...prev, database: techId, orm: "drizzle", + turso: techId === "sqlite" ? prev.turso : "false", + prismaPostgres: + techId === "postgres" && prev.orm === "prisma" + ? prev.prismaPostgres + : "false", }; } - if (techId === "sqlite") { - return { - ...prev, - database: techId, - turso: prev.turso, - }; - } - - return { + const updatedState = { ...prev, database: techId, - turso: "false", }; + + if (techId === "sqlite") { + updatedState.prismaPostgres = "false"; + } else if (techId === "postgres" && prev.orm === "prisma") { + } else { + updatedState.turso = "false"; + } + + return updatedState; } - if (category === "turso" && prev.database !== "sqlite") { + if (category === "orm") { + if (prev.database === "none") { + return prev; + } + + const updatedState = { + ...prev, + orm: techId, + }; + + if (techId === "prisma") { + updatedState.turso = "false"; + if (prev.database === "postgres") { + } else { + updatedState.prismaPostgres = "false"; + } + } else if (techId === "drizzle" || techId === "none") { + updatedState.prismaPostgres = "false"; + } + + return updatedState; + } + + if ( + category === "turso" && + (prev.database !== "sqlite" || prev.orm === "prisma") + ) { + return prev; + } + + if ( + category === "prismaPostgres" && + (prev.database !== "postgres" || prev.orm !== "prisma") + ) { return prev; } @@ -638,6 +744,13 @@ const StackArchitect = () => { [], ); + const hasWebFrontend = useCallback((frontendOptions: string[]) => { + return ( + frontendOptions.includes("tanstack-router") || + frontendOptions.includes("react-router") + ); + }, []); + const copyToClipboard = useCallback(() => { navigator.clipboard.writeText(command); setCopied(true); @@ -742,32 +855,40 @@ const StackArchitect = () => { stack[activeTab as keyof StackState] === tech.id; } - const hasWebFrontend = + const hasWebFrontendSelected = stack.frontend.includes("tanstack-router") || stack.frontend.includes("react-router"); + const isDisabled = (activeTab === "orm" && stack.database === "none") || - (activeTab === "turso" && stack.database !== "sqlite") || - (activeTab === "auth" && !hasWebFrontend) || + (activeTab === "turso" && + (stack.database !== "sqlite" || + stack.orm === "prisma")) || + (activeTab === "prismaPostgres" && + (stack.database !== "postgres" || + stack.orm !== "prisma")) || + (activeTab === "auth" && !hasWebFrontendSelected) || (activeTab === "examples" && - ((tech.id === "todo" && !hasWebFrontend) || - (tech.id === "ai" && !hasWebFrontend))) || + (((tech.id === "todo" || tech.id === "ai") && + !hasWebFrontendSelected) || + (tech.id === "ai" && + stack.backendFramework === "elysia"))) || (activeTab === "addons" && (tech.id === "pwa" || tech.id === "tauri") && - !hasWebFrontend); + !hasWebFrontendSelected); return ( @@ -868,25 +989,52 @@ const StackArchitect = () => { } - {stack.orm && ( + {stack.orm && stack.database !== "none" && ( {TECH_OPTIONS.orm.find((t) => t.id === stack.orm)?.icon}{" "} {TECH_OPTIONS.orm.find((t) => t.id === stack.orm)?.name} )} - - {TECH_OPTIONS.auth.find((t) => t.id === stack.auth)?.icon}{" "} - {TECH_OPTIONS.auth.find((t) => t.id === stack.auth)?.name} - - - {stack.turso === "true" && stack.database === "sqlite" && ( - - {TECH_OPTIONS.turso.find((t) => t.id === stack.turso)?.icon}{" "} - {TECH_OPTIONS.turso.find((t) => t.id === stack.turso)?.name} + {hasWebFrontend(stack.frontend) && ( + + {TECH_OPTIONS.auth.find((t) => t.id === stack.auth)?.icon}{" "} + {TECH_OPTIONS.auth.find((t) => t.id === stack.auth)?.name} )} + {stack.turso === "true" && + stack.database === "sqlite" && + stack.orm !== "prisma" && ( + + { + TECH_OPTIONS.turso.find((t) => t.id === stack.turso) + ?.icon + }{" "} + { + TECH_OPTIONS.turso.find((t) => t.id === stack.turso) + ?.name + } + + )} + + {stack.prismaPostgres === "true" && + stack.database === "postgres" && + stack.orm === "prisma" && ( + + { + TECH_OPTIONS.prismaPostgres.find( + (t) => t.id === stack.prismaPostgres, + )?.icon + }{" "} + { + TECH_OPTIONS.prismaPostgres.find( + (t) => t.id === stack.prismaPostgres, + )?.name + } + + )} + {stack.addons.map((addonId) => { const addon = TECH_OPTIONS.addons.find( (a) => a.id === addonId, @@ -925,13 +1073,13 @@ const StackArchitect = () => { type="button" key={category} className={` - py-2 px-4 text-xs font-mono whitespace-nowrap transition-colors - ${ - activeTab === category - ? "bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 border-t-2 border-blue-500" - : "text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 hover:bg-gray-300 dark:hover:bg-gray-800" - } - `} + py-2 px-4 text-xs font-mono whitespace-nowrap transition-colors + ${ + activeTab === category + ? "bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 border-t-2 border-blue-500" + : "text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 hover:bg-gray-300 dark:hover:bg-gray-800" + } + `} onClick={() => setActiveTab(category)} > {category} diff --git a/apps/web/src/app/(home)/_components/Testimonials.tsx b/apps/web/src/app/(home)/_components/Testimonials.tsx index 6613d0f..f8712de 100644 --- a/apps/web/src/app/(home)/_components/Testimonials.tsx +++ b/apps/web/src/app/(home)/_components/Testimonials.tsx @@ -6,6 +6,8 @@ import { useEffect, useState } from "react"; import { Tweet } from "react-tweet"; const TWEET_IDS = [ + "1907728148294447538", + "1907723601731530820", "1904144343125860404", "1904215768272654825", "1904233896851521980",