"use client"; import { DEFAULT_STACK, PRESET_TEMPLATES, type StackState, TECH_OPTIONS, } from "@/lib/constant"; import { motion } from "framer-motion"; import { Check, Circle, CircleCheck, ClipboardCopy, HelpCircle, InfoIcon, RefreshCw, Settings, Star, 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 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, ); const [showPresets, setShowPresets] = useState(false); const [showHelp, setShowHelp] = useState(false); const [lastSavedStack, setLastSavedStack] = useState(null); useEffect(() => { const savedStack = localStorage.getItem("betterTStackPreference"); if (savedStack) { try { const parsedStack = JSON.parse(savedStack); setLastSavedStack(parsedStack); } catch (e) { console.error("Failed to parse saved stack", e); } } }, []); 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" })); } } if (stack.database === "none" && stack.auth === "true") { setStack((prev) => ({ ...prev, auth: "false" })); } }, [ stack.database, stack.orm, stack.prismaPostgres, stack.turso, stack.auth, ]); useEffect(() => { const cmd = generateCommand(stack); setCommand(cmd); const notes: Record = {}; const hasWebFrontend = stack.frontend.includes("tanstack-router") || stack.frontend.includes("react-router"); notes.frontend = []; notes.addons = []; if (!hasWebFrontend) { 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.auth = []; if (stack.database === "none") { notes.auth.push("Authentication requires a database."); } notes.turso = []; if (stack.database !== "sqlite") { notes.turso.push( "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 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]); 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] === "tanstack-router" ) ) { 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.prismaPostgres === "true") { flags.push("--prisma-postgres"); } 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]; const webTypes = ["tanstack-router", "react-router"]; if (techId === "none") { return { ...prev, frontend: ["none"], examples: [], addons: prev.addons.filter( (addon) => addon !== "pwa" && addon !== "tauri", ), }; } if (currentSelection.includes(techId)) { if (currentSelection.length === 1) { return prev; } return { ...prev, frontend: currentSelection.filter((id) => id !== techId), }; } let newSelection = [...currentSelection]; if (newSelection.includes("none")) { newSelection = []; } if (webTypes.includes(techId)) { newSelection = newSelection.filter((id) => !webTypes.includes(id)); } newSelection.push(techId); return { ...prev, frontend: newSelection, }; } if (category === "addons" || category === "examples") { const currentArray = [...(prev[category] || [])]; const index = currentArray.indexOf(techId); const hasWebFrontend = prev.frontend.includes("tanstack-router") || prev.frontend.includes("react-router"); if (index >= 0) { currentArray.splice(index, 1); } else { if ( category === "examples" && (techId === "todo" || techId === "ai") && !hasWebFrontend ) { return prev; } if ( category === "examples" && techId === "ai" && prev.backendFramework === "elysia" ) { return prev; } if ( category === "addons" && (techId === "pwa" || techId === "tauri") && !hasWebFrontend ) { return prev; } if ( category === "addons" && techId === "husky" && !currentArray.includes("biome") ) { currentArray.push("biome"); } currentArray.push(techId); } return { ...prev, [category]: currentArray, }; } if (category === "database") { if (techId === "none") { return { ...prev, database: techId, orm: "none", turso: "false", prismaPostgres: "false", auth: "false", }; } if (prev.database === "none") { return { ...prev, database: techId, orm: "drizzle", turso: techId === "sqlite" ? prev.turso : "false", prismaPostgres: techId === "postgres" && prev.orm === "prisma" ? prev.prismaPostgres : "false", auth: hasWebFrontend(prev.frontend) || prev.frontend.includes("native") ? "true" : "false", }; } const updatedState = { ...prev, database: techId, }; if (techId === "sqlite") { updatedState.prismaPostgres = "false"; } else if (techId === "postgres" && prev.orm === "prisma") { } else { updatedState.turso = "false"; } return updatedState; } 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; } if ( category === "auth" && prev.database === "none" && techId === "true" ) { return prev; } return { ...prev, [category]: techId, }; }); }, [], ); const hasWebFrontend = useCallback((frontendOptions: string[]) => { return ( frontendOptions.includes("tanstack-router") || frontendOptions.includes("react-router") || frontendOptions.includes("native") ); }, []); const copyToClipboard = useCallback(() => { navigator.clipboard.writeText(command); setCopied(true); setTimeout(() => setCopied(false), 2000); }, [command]); const resetStack = useCallback(() => { setStack(DEFAULT_STACK); setActiveTab("frontend"); }, []); const saveCurrentStack = useCallback(() => { localStorage.setItem("betterTStackPreference", JSON.stringify(stack)); setLastSavedStack(stack); const saveMessage = document.createElement("div"); saveMessage.textContent = "Stack preferences saved!"; saveMessage.className = "fixed bottom-4 right-4 bg-green-500 text-white py-2 px-4 rounded-md shadow-lg z-50"; document.body.appendChild(saveMessage); setTimeout(() => { document.body.removeChild(saveMessage); }, 3000); }, [stack]); const loadSavedStack = useCallback(() => { if (lastSavedStack) { setStack(lastSavedStack); } }, [lastSavedStack]); const applyPreset = useCallback((presetId: string) => { const preset = PRESET_TEMPLATES.find( (template) => template.id === presetId, ); if (preset) { setStack(preset.stack); setShowPresets(false); } }, []); return (
Stack Architect Terminal
{showHelp && (

How to Use Stack Architect

  • Select your preferred technologies from each category using the tabs below
  • The command will automatically update based on your selections
  • Click the copy button to copy the command to your clipboard
  • You can reset to defaults or choose from presets for quick setup
  • Save your preferences to load them later when you return
)} {showPresets && (

Quick Start Presets

{PRESET_TEMPLATES.map((preset) => ( ))}
)}
{lastSavedStack && ( )}
$ {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 hasWebFrontendSelected = stack.frontend.includes("tanstack-router") || stack.frontend.includes("react-router"); const isDisabled = (activeTab === "orm" && stack.database === "none") || (activeTab === "turso" && (stack.database !== "sqlite" || stack.orm === "prisma")) || (activeTab === "prismaPostgres" && (stack.database !== "postgres" || stack.orm !== "prisma")) || (activeTab === "examples" && (((tech.id === "todo" || tech.id === "ai") && !hasWebFrontendSelected) || (tech.id === "ai" && stack.backendFramework === "elysia"))) || (activeTab === "addons" && (tech.id === "pwa" || tech.id === "tauri") && !hasWebFrontendSelected) || (activeTab === "auth" && tech.id === "true" && stack.database === "none"); const compatNote = isDisabled ? compatNotes[activeTab]?.find((note) => note.toLowerCase().includes(tech.name.toLowerCase()), ) : null; 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 && stack.database !== "none" && ( {TECH_OPTIONS.orm.find((t) => t.id === stack.orm)?.icon}{" "} {TECH_OPTIONS.orm.find((t) => t.id === stack.orm)?.name} )} {stack.frontend[0] !== "none" && stack.database !== "none" && stack.auth === "true" && ( {TECH_OPTIONS.auth.find((t) => t.id === "true")?.icon}{" "} {TECH_OPTIONS.auth.find((t) => t.id === "true")?.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, ); 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;