"use client"; import { motion } from "framer-motion"; import { Check, Circle, CircleCheck, ClipboardCopy, InfoIcon, Terminal, } from "lucide-react"; import { useCallback, useEffect, useState } from "react"; const validateProjectName = (name: string): string | undefined => { const INVALID_CHARS = ["<", ">", ":", '"', "|", "?", "*"]; const MAX_LENGTH = 255; if (name === ".") return undefined; if (!name) return "Project name cannot be empty"; if (name.length > MAX_LENGTH) { return `Project name must be less than ${MAX_LENGTH} characters`; } if (INVALID_CHARS.some((char) => name.includes(char))) { return "Project name contains invalid characters"; } if (name.startsWith(".") || name.startsWith("-")) { return "Project name cannot start with a dot or dash"; } if ( name.toLowerCase() === "node_modules" || name.toLowerCase() === "favicon.ico" ) { return "Project name is reserved"; } return undefined; }; const TECH_OPTIONS = { frontend: [ { id: "web", name: "React Web", description: "React with TanStack Router", icon: "🌐", color: "from-blue-400 to-blue-600", default: true, }, { id: "native", name: "React Native", description: "Expo with NativeWind", icon: "📱", color: "from-purple-400 to-purple-600", default: false, }, { id: "none", name: "No Frontend", description: "API-only backend", icon: "⚙️", color: "from-gray-400 to-gray-600", default: false, }, ], runtime: [ { id: "bun", name: "Bun", description: "Fast JavaScript runtime & toolkit", icon: "🥟", color: "from-amber-400 to-amber-600", default: true, }, { id: "node", name: "Node.js", description: "JavaScript runtime environment", icon: "🟩", color: "from-green-400 to-green-600", }, ], backendFramework: [ { id: "hono", name: "Hono", description: "Ultrafast web framework", icon: "⚡", color: "from-blue-500 to-blue-700", default: true, }, { id: "elysia", name: "Elysia", description: "TypeScript web framework", icon: "🦊", color: "from-purple-500 to-purple-700", }, ], database: [ { id: "sqlite", name: "SQLite", description: "File-based SQL database", icon: "🗃️", color: "from-blue-400 to-cyan-500", default: true, }, { id: "postgres", name: "PostgreSQL", description: "Advanced SQL database", icon: "🐘", color: "from-indigo-400 to-indigo-600", }, { id: "none", name: "No Database", description: "Skip database integration", icon: "🚫", color: "from-gray-400 to-gray-600", }, ], orm: [ { id: "drizzle", name: "Drizzle", description: "TypeScript ORM", icon: "💧", color: "from-cyan-400 to-cyan-600", default: true, }, { id: "prisma", name: "Prisma", description: "Next-gen ORM", icon: "◮", color: "from-purple-400 to-purple-600", }, ], auth: [ { id: "true", name: "Better Auth", description: "Simple authentication", icon: "🔐", color: "from-green-400 to-green-600", default: true, }, { id: "false", name: "No Auth", description: "Skip authentication", icon: "🔓", color: "from-red-400 to-red-600", }, ], turso: [ { id: "true", name: "Turso", description: "SQLite cloud database", icon: "☁️", color: "from-pink-400 to-pink-600", default: false, }, { id: "false", name: "No Turso", description: "Skip Turso integration", icon: "🚫", color: "from-gray-400 to-gray-600", default: true, }, ], packageManager: [ { id: "npm", name: "npm", description: "Default package manager", icon: "📦", color: "from-red-500 to-red-700", }, { id: "pnpm", name: "pnpm", description: "Fast, disk space efficient", icon: "🚀", color: "from-orange-500 to-orange-700", }, { id: "bun", name: "bun", description: "All-in-one toolkit", icon: "🥟", color: "from-amber-500 to-amber-700", default: true, }, ], addons: [ { id: "pwa", name: "PWA", description: "Progressive Web App", icon: "📱", color: "from-blue-500 to-blue-700", default: false, }, { id: "tauri", name: "Tauri", description: "Desktop app support", icon: "🖥️", color: "from-amber-500 to-amber-700", default: false, }, { id: "biome", name: "Biome", description: "Linting & formatting", icon: "🌿", color: "from-green-500 to-green-700", default: false, }, { id: "husky", name: "Husky", description: "Git hooks & lint-staged", icon: "🐶", color: "from-purple-500 to-purple-700", default: false, }, ], examples: [ { id: "todo", name: "Todo Example", description: "Simple todo application", icon: "✅", color: "from-indigo-500 to-indigo-700", default: false, }, { id: "ai", name: "AI Example", description: "AI integration example using AI SDK", icon: "🤖", color: "from-purple-500 to-purple-700", default: false, }, ], git: [ { id: "true", name: "Git", description: "Initialize Git repository", icon: "📝", color: "from-gray-500 to-gray-700", default: true, }, { id: "false", name: "No Git", description: "Skip Git initialization", icon: "🚫", color: "from-red-400 to-red-600", }, ], install: [ { id: "true", name: "Install Dependencies", description: "Install packages automatically", icon: "📥", color: "from-green-400 to-green-600", default: true, }, { id: "false", name: "Skip Install", description: "Skip dependency installation", icon: "⏭️", color: "from-yellow-400 to-yellow-600", }, ], }; interface StackState { projectName: string; frontend: string[]; runtime: string; backendFramework: string; database: string; orm: string | null; auth: string; turso: string; packageManager: string; addons: string[]; examples: string[]; git: string; install: string; } const DEFAULT_STACK: StackState = { projectName: "my-better-t-app", frontend: ["web"], runtime: "bun", backendFramework: "hono", database: "sqlite", orm: "drizzle", auth: "true", turso: "false", packageManager: "bun", addons: [], examples: [], git: "true", install: "true", }; const StackArchitect = () => { const [stack, setStack] = useState(DEFAULT_STACK); const [command, setCommand] = useState( "npx create-better-t-stack@latest my-better-t-app --yes", ); const [activeTab, setActiveTab] = useState("frontend"); const [copied, setCopied] = useState(false); const [compatNotes, setCompatNotes] = useState>({}); const [projectNameError, setProjectNameError] = useState( undefined, ); useEffect(() => { if (!stack.frontend.includes("web") && stack.auth === "true") { setStack((prev) => ({ ...prev, auth: "false", })); } }, [stack.frontend, stack.auth]); useEffect(() => { const cmd = generateCommand(stack); setCommand(cmd); const notes: Record = {}; notes.frontend = []; notes.auth = []; if (!stack.frontend.includes("web") && stack.auth === "true") { notes.auth.push("Authentication is only available with React Web."); } notes.addons = []; if (!stack.frontend.includes("web")) { notes.addons.push("PWA and Tauri are only available with React Web."); } notes.database = []; notes.orm = []; if (stack.database === "none") { notes.orm.push( "ORM options are only available when a database is selected.", ); } notes.turso = []; if (stack.database !== "sqlite") { notes.turso.push( "Turso integration is only available with SQLite database.", ); } notes.examples = []; if (!stack.frontend.includes("web")) { notes.examples.push( "Todo and Ai example are only available with React Web.", ); } setCompatNotes(notes); }, [stack]); const generateCommand = useCallback((stackState: StackState) => { let base: string; if (stackState.packageManager === "npm") { base = "npx create-better-t-stack@latest"; } else if (stackState.packageManager === "pnpm") { base = "pnpm create better-t-stack@latest"; } else { base = "bun create better-t-stack@latest"; } const projectName = stackState.projectName || "my-better-t-app"; const flags: string[] = ["--yes"]; if (stackState.frontend.length === 1 && stackState.frontend[0] === "none") { flags.push("--frontend none"); } else if ( !(stackState.frontend.length === 1 && stackState.frontend[0] === "web") ) { flags.push(`--frontend ${stackState.frontend.join(" ")}`); } if (stackState.database !== "sqlite") { flags.push(`--database ${stackState.database}`); } if (stackState.database !== "none" && stackState.orm !== "drizzle") { flags.push(`--orm ${stackState.orm}`); } if (stackState.auth === "false") { flags.push("--no-auth"); } if (stackState.turso === "true") { flags.push("--turso"); } if (stackState.backendFramework !== "hono") { flags.push(`--backend ${stackState.backendFramework}`); } if (stackState.runtime !== "bun") { flags.push(`--runtime ${stackState.runtime}`); } if (stackState.packageManager !== "bun") { flags.push(`--package-manager ${stackState.packageManager}`); } if (stackState.git === "false") { flags.push("--no-git"); } if (stackState.install === "false") { flags.push("--no-install"); } if (stackState.addons.length > 0) { flags.push(`--addons ${stackState.addons.join(" ")}`); } if (stackState.examples.length > 0) { flags.push(`--examples ${stackState.examples.join(" ")}`); } return `${base} ${projectName} ${flags.join(" ")}`; }, []); const handleTechSelect = useCallback( (category: keyof typeof TECH_OPTIONS, techId: string) => { setStack((prev) => { if (category === "frontend") { const currentSelection = [...prev.frontend]; if (techId === "none") { return { ...prev, frontend: ["none"], auth: "false", examples: prev.examples.filter( (ex) => ex !== "todo" && ex !== "ai", ), addons: prev.addons.filter( (addon) => addon !== "pwa" && addon !== "tauri", ), }; } if (currentSelection.includes(techId)) { if (currentSelection.length === 1) { return prev; } const newFrontend = currentSelection.filter((id) => id !== techId); if (techId === "web") { return { ...prev, frontend: newFrontend, auth: "false", examples: prev.examples.filter( (ex) => ex !== "todo" && ex !== "ai", ), addons: prev.addons.filter( (addon) => addon !== "pwa" && addon !== "tauri", ), }; } return { ...prev, frontend: newFrontend, }; } if (currentSelection.includes("none")) { return { ...prev, frontend: [techId], ...(techId === "web" && { auth: "true" }), }; } return { ...prev, frontend: [...currentSelection, techId], ...(techId === "web" && { auth: "true" }), }; } if (category === "addons" || category === "examples") { const currentArray = [...(prev[category] || [])]; const index = currentArray.indexOf(techId); if (index >= 0) { currentArray.splice(index, 1); } else { if ( category === "examples" && techId === "todo" && !prev.frontend.includes("web") ) { return prev; } if ( category === "addons" && (techId === "pwa" || techId === "tauri") && !prev.frontend.includes("web") ) { return prev; } currentArray.push(techId); } return { ...prev, [category]: currentArray, }; } if (category === "database") { if (techId === "none") { return { ...prev, database: techId, orm: null, turso: "false", }; } if (prev.database === "none") { return { ...prev, database: techId, orm: "drizzle", }; } if (techId === "sqlite") { return { ...prev, database: techId, turso: prev.turso, }; } return { ...prev, database: techId, turso: "false", }; } if (category === "turso" && prev.database !== "sqlite") { return prev; } return { ...prev, [category]: techId, }; }); }, [], ); const copyToClipboard = useCallback(() => { navigator.clipboard.writeText(command); setCopied(true); setTimeout(() => setCopied(false), 2000); }, [command]); return (
Stack Architect Terminal
$ {command}
{compatNotes[activeTab] && compatNotes[activeTab].length > 0 && (
Compatibility Notes
    {compatNotes[activeTab].map((note) => (
  • {note}
  • ))}
)}
Configure{" "} {activeTab.charAt(0).toUpperCase() + activeTab.slice(1)}
{TECH_OPTIONS[activeTab as keyof typeof TECH_OPTIONS].map( (tech) => { let isSelected = false; if (activeTab === "addons" || activeTab === "examples") { isSelected = stack[activeTab].includes(tech.id); } else if (activeTab === "frontend") { isSelected = stack.frontend.includes(tech.id); } else { isSelected = stack[activeTab as keyof StackState] === tech.id; } const isDisabled = (activeTab === "orm" && stack.database === "none") || (activeTab === "turso" && stack.database !== "sqlite") || (activeTab === "auth" && !stack.frontend.includes("web")) || (activeTab === "examples" && ((tech.id === "todo" && !stack.frontend.includes("web")) || (tech.id === "ai" && !stack.frontend.includes("web")))) || (activeTab === "addons" && (tech.id === "pwa" || tech.id === "tauri") && !stack.frontend.includes("web")); return ( !isDisabled && handleTechSelect( activeTab as keyof typeof TECH_OPTIONS, tech.id, ) } >
{isSelected ? ( ) : ( )}
{tech.icon} {tech.name}

{tech.description}

{tech.default && !isSelected && ( Default )}
); }, )}
Selected Stack
{stack.frontend.map((frontendId) => { const frontend = TECH_OPTIONS.frontend.find( (f) => f.id === frontendId, ); return frontend ? ( {frontend.icon} {frontend.name} ) : null; })} { TECH_OPTIONS.runtime.find((t) => t.id === stack.runtime) ?.icon }{" "} { TECH_OPTIONS.runtime.find((t) => t.id === stack.runtime) ?.name } { TECH_OPTIONS.backendFramework.find( (t) => t.id === stack.backendFramework, )?.icon }{" "} { TECH_OPTIONS.backendFramework.find( (t) => t.id === stack.backendFramework, )?.name } { TECH_OPTIONS.database.find((t) => t.id === stack.database) ?.icon }{" "} { TECH_OPTIONS.database.find((t) => t.id === stack.database) ?.name } {stack.orm && ( {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} )} {stack.addons.map((addonId) => { const addon = TECH_OPTIONS.addons.find( (a) => a.id === addonId, ); return addon ? ( {addon.icon} {addon.name} ) : null; })} {stack.examples.length > 0 && stack.examples.map((exampleId) => { const example = TECH_OPTIONS.examples.find( (e) => e.id === exampleId, ); return example ? ( {example.icon} {example.name} ) : null; })}
{Object.keys(TECH_OPTIONS).map((category) => ( ))}
); }; export default StackArchitect;