"use client"; import { DEFAULT_STACK, PRESET_TEMPLATES, type StackState, TECH_OPTIONS, } from "@/lib/constant"; import { Check, Circle, CircleCheck, ClipboardCopy, HelpCircle, InfoIcon, RefreshCw, Settings, Star, Terminal, } from "lucide-react"; import { motion } from "motion/react"; import { useCallback, useEffect, useMemo, useRef, 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 CATEGORY_ORDER: Array = [ "frontend", "runtime", "backendFramework", "api", "database", "orm", "dbSetup", "auth", "packageManager", "addons", "examples", "git", "install", ]; const hasWebFrontend = (frontend: string[]) => frontend.some((f) => ["tanstack-router", "react-router", "tanstack-start", "next"].includes(f), ); const hasPWACompatibleFrontend = (frontend: string[]) => frontend.some((f) => ["tanstack-router", "react-router"].includes(f)); const hasNativeFrontend = (frontend: string[]) => frontend.includes("native"); const StackArchitect = () => { const [stack, setStack] = useState(DEFAULT_STACK); const [command, setCommand] = useState(""); const [copied, setCopied] = useState(false); const [compatNotes, setCompatNotes] = useState< Record >({}); const [projectNameError, setProjectNameError] = useState( undefined, ); const [showPresets, setShowPresets] = useState(false); const [showHelp, setShowHelp] = useState(false); const [lastSavedStack, setLastSavedStack] = useState(null); const [activeCategory, setActiveCategory] = useState( CATEGORY_ORDER[0], ); const sectionRefs = useRef>({}); const contentRef = useRef(null); const currentHasWebFrontend = useMemo( () => hasWebFrontend(stack.frontend), [stack.frontend], ); const currentHasPWACompatibleFrontend = useMemo( () => hasPWACompatibleFrontend(stack.frontend), [stack.frontend], ); const currentHasNativeFrontend = useMemo( () => hasNativeFrontend(stack.frontend), [stack.frontend], ); useEffect(() => { const savedStack = localStorage.getItem("betterTStackPreference"); if (savedStack) { try { const parsedStack = JSON.parse(savedStack) as StackState; setLastSavedStack(parsedStack); } catch (e) { console.error("Failed to parse saved stack", e); localStorage.removeItem("betterTStackPreference"); } } }, []); // biome-ignore lint/correctness/useExhaustiveDependencies: Dependencies are logically required for validation inside updater useEffect(() => { setStack((currentStack) => { const nextStack = { ...currentStack }; let changed = false; const isWeb = hasWebFrontend(nextStack.frontend); const isPWACompat = hasPWACompatibleFrontend(nextStack.frontend); const isNative = hasNativeFrontend(nextStack.frontend); if (nextStack.database === "none") { if (nextStack.orm !== "none") { nextStack.orm = "none"; changed = true; } if (nextStack.auth === "true") { nextStack.auth = "false"; changed = true; } if (nextStack.dbSetup !== "none") { nextStack.dbSetup = "none"; changed = true; } } else if (nextStack.database === "mongodb") { if (nextStack.orm !== "prisma") { nextStack.orm = "prisma"; changed = true; } } if (nextStack.dbSetup === "turso") { if (nextStack.database !== "sqlite") { nextStack.database = "sqlite"; changed = true; } if (nextStack.orm !== "drizzle") { nextStack.orm = "drizzle"; changed = true; } } else if (nextStack.dbSetup === "prisma-postgres") { if (nextStack.database !== "postgres") { nextStack.database = "postgres"; changed = true; } if (nextStack.orm !== "prisma") { nextStack.orm = "prisma"; changed = true; } } else if (nextStack.dbSetup === "mongodb-atlas") { if (nextStack.database !== "mongodb") { nextStack.database = "mongodb"; changed = true; } if (nextStack.orm !== "prisma") { nextStack.orm = "prisma"; changed = true; } } else if (nextStack.dbSetup === "neon") { if (nextStack.database !== "postgres") { nextStack.database = "postgres"; changed = true; } } if (isNative && nextStack.api !== "trpc") { nextStack.api = "trpc"; changed = true; } const incompatibleAddons: string[] = []; if (!isPWACompat) { incompatibleAddons.push("pwa", "tauri"); } const originalAddonsLength = nextStack.addons.length; nextStack.addons = nextStack.addons.filter( (addon) => !incompatibleAddons.includes(addon), ); if (nextStack.addons.length !== originalAddonsLength) { changed = true; } if ( nextStack.addons.includes("husky") && !nextStack.addons.includes("biome") ) { nextStack.addons.push("biome"); nextStack.addons = [...new Set(nextStack.addons)]; changed = true; } const incompatibleExamples: string[] = []; if (!isWeb) { incompatibleExamples.push("todo", "ai"); } if (nextStack.database === "none") { incompatibleExamples.push("todo"); } if (nextStack.backendFramework === "elysia") { incompatibleExamples.push("ai"); } const originalExamplesLength = nextStack.examples.length; nextStack.examples = nextStack.examples.filter( (ex) => !incompatibleExamples.includes(ex), ); if (nextStack.examples.length !== originalExamplesLength) { changed = true; } return changed ? nextStack : currentStack; }); }, [ stack.database, stack.orm, stack.auth, stack.dbSetup, stack.frontend, stack.api, stack.addons, stack.examples, stack.backendFramework, ]); const generateCommand = useCallback((stackState: StackState) => { let base: string; switch (stackState.packageManager) { case "npm": base = "npx create-better-t-stack@latest"; break; case "pnpm": base = "pnpm create better-t-stack@latest"; break; default: base = "bun create better-t-stack@latest"; break; } const projectName = stackState.projectName || "my-better-t-app"; const flags: string[] = []; const isDefault = ( key: K, value: StackState[K], ) => { const defaultValue = DEFAULT_STACK[key]; if (Array.isArray(defaultValue) && Array.isArray(value)) { return ( defaultValue.length === value.length && defaultValue.every((item) => value.includes(item)) && value.every((item) => defaultValue.includes(item)) ); } return defaultValue === value; }; if (!isDefault("frontend", stackState.frontend)) { if ( stackState.frontend.length === 0 || stackState.frontend[0] === "none" ) { flags.push("--frontend none"); } else { flags.push(`--frontend ${stackState.frontend.join(" ")}`); } } if (!isDefault("database", stackState.database)) { flags.push(`--database ${stackState.database}`); } if (stackState.database !== "none" && !isDefault("orm", stackState.orm)) { flags.push(`--orm ${stackState.orm}`); } if (!isDefault("auth", stackState.auth)) { if (stackState.auth === "false") { flags.push("--no-auth"); } } if (!isDefault("dbSetup", stackState.dbSetup)) { flags.push(`--db-setup ${stackState.dbSetup}`); } if (!isDefault("backendFramework", stackState.backendFramework)) { flags.push(`--backend ${stackState.backendFramework}`); } if (!isDefault("runtime", stackState.runtime)) { flags.push(`--runtime ${stackState.runtime}`); } if (!isDefault("api", stackState.api)) { flags.push(`--api ${stackState.api}`); } if (!isDefault("packageManager", stackState.packageManager)) { flags.push(`--package-manager ${stackState.packageManager}`); } if (!isDefault("git", stackState.git)) { if (stackState.git === "false") { flags.push("--no-git"); } } if (!isDefault("install", stackState.install)) { if (stackState.install === "false") { flags.push("--no-install"); } } if (!isDefault("addons", stackState.addons)) { if (stackState.addons.length > 0) { flags.push(`--addons ${stackState.addons.join(" ")}`); } } if (!isDefault("examples", stackState.examples)) { if (stackState.examples.length > 0) { flags.push(`--examples ${stackState.examples.join(" ")}`); } } return `${base} ${projectName}${flags.length > 0 ? ` ${flags.join(" ")}` : ""}`; }, []); useEffect(() => { const cmd = generateCommand(stack); setCommand(cmd); const notes: Record = {}; for (const cat of CATEGORY_ORDER) { notes[cat] = { notes: [], hasIssue: false }; } const isWeb = currentHasWebFrontend; const isPWACompat = currentHasPWACompatibleFrontend; const isNative = currentHasNativeFrontend; if (isNative && stack.frontend.length > 1) { notes.frontend.notes.push( "React Native requires the tRPC API when used with other frontends. oRPC will be disabled.", ); if (stack.api !== "trpc") notes.frontend.hasIssue = true; } if ( !isPWACompat && stack.addons.some((a) => ["pwa", "tauri"].includes(a)) ) { notes.frontend.notes.push( "PWA/Tauri addons require TanStack or React Router.", ); notes.frontend.hasIssue = true; notes.addons.hasIssue = true; } if (!isWeb && stack.examples.length > 0) { notes.frontend.notes.push("Examples require a web frontend."); notes.frontend.hasIssue = true; notes.examples.hasIssue = true; } if (isNative && stack.api !== "trpc") { notes.api.notes.push( "React Native requires tRPC. It will be selected automatically.", ); notes.api.hasIssue = true; } if (stack.database === "mongodb" && stack.orm !== "prisma") { notes.database.notes.push( "MongoDB requires Prisma ORM. It will be selected automatically.", ); notes.database.hasIssue = true; notes.orm.hasIssue = true; } if (stack.database === "none") { if (stack.orm !== "none") notes.database.hasIssue = true; if (stack.auth === "true") notes.database.hasIssue = true; if (stack.dbSetup !== "none") notes.database.hasIssue = true; if (stack.examples.includes("todo")) notes.database.hasIssue = true; } if (stack.database === "none" && stack.orm !== "none") { notes.orm.notes.push( "ORM requires a database. It will be set to 'None'.", ); notes.orm.hasIssue = true; } if (stack.database === "mongodb" && stack.orm !== "prisma") { notes.orm.notes.push("MongoDB requires Prisma ORM. It will be selected."); notes.orm.hasIssue = true; } if (stack.dbSetup === "turso" && stack.orm !== "drizzle") { notes.orm.notes.push( "Turso DB setup requires Drizzle ORM. It will be selected.", ); notes.orm.hasIssue = true; } if (stack.dbSetup === "prisma-postgres" && stack.orm !== "prisma") { notes.orm.notes.push( "Prisma PostgreSQL setup requires Prisma ORM. It will be selected.", ); notes.orm.hasIssue = true; } if (stack.dbSetup === "mongodb-atlas" && stack.orm !== "prisma") { notes.orm.notes.push( "MongoDB Atlas setup requires Prisma ORM. It will be selected.", ); notes.orm.hasIssue = true; } if (stack.database === "none" && stack.dbSetup !== "none") { notes.dbSetup.notes.push( "DB Setup requires a database. It will be set to 'Basic Setup'.", ); notes.dbSetup.hasIssue = true; } else if (stack.dbSetup !== "none") { const db = stack.database; const orm = stack.orm; if (stack.dbSetup === "turso" && (db !== "sqlite" || orm !== "drizzle")) { notes.dbSetup.notes.push( "Turso requires SQLite & Drizzle. They will be selected.", ); notes.dbSetup.hasIssue = true; if (db !== "sqlite") notes.database.hasIssue = true; if (orm !== "drizzle") notes.orm.hasIssue = true; } else if ( stack.dbSetup === "prisma-postgres" && (db !== "postgres" || orm !== "prisma") ) { notes.dbSetup.notes.push( "Requires PostgreSQL & Prisma. They will be selected.", ); notes.dbSetup.hasIssue = true; if (db !== "postgres") notes.database.hasIssue = true; if (orm !== "prisma") notes.orm.hasIssue = true; } else if ( stack.dbSetup === "mongodb-atlas" && (db !== "mongodb" || orm !== "prisma") ) { notes.dbSetup.notes.push( "Requires MongoDB & Prisma. They will be selected.", ); notes.dbSetup.hasIssue = true; if (db !== "mongodb") notes.database.hasIssue = true; if (orm !== "prisma") notes.orm.hasIssue = true; } else if (stack.dbSetup === "neon" && db !== "postgres") { notes.dbSetup.notes.push( "Neon requires PostgreSQL. It will be selected.", ); notes.dbSetup.hasIssue = true; if (db !== "postgres") notes.database.hasIssue = true; } } if (stack.database === "none" && stack.auth === "true") { notes.auth.notes.push( "Authentication requires a database. It will be disabled.", ); notes.auth.hasIssue = true; } if ( !isPWACompat && stack.addons.some((a) => ["pwa", "tauri"].includes(a)) ) { notes.addons.notes.push( "PWA/Tauri require TanStack/React Router. They will be removed.", ); notes.addons.hasIssue = true; } if (stack.addons.includes("husky") && !stack.addons.includes("biome")) { notes.addons.notes.push( "Husky automatically enables Biome. It will be added.", ); } if (!isWeb && stack.examples.length > 0) { notes.examples.notes.push( "Examples require a web frontend. They will be removed.", ); notes.examples.hasIssue = true; } if (stack.database === "none" && stack.examples.includes("todo")) { notes.examples.notes.push( "Todo example requires a database. It will be removed.", ); notes.examples.hasIssue = true; } if (stack.backendFramework === "elysia" && stack.examples.includes("ai")) { notes.examples.notes.push( "AI example is not compatible with Elysia. It will be removed.", ); notes.examples.hasIssue = true; notes.backendFramework.hasIssue = true; } setCompatNotes(notes); }, [ stack, generateCommand, currentHasWebFrontend, currentHasPWACompatibleFrontend, currentHasNativeFrontend, ]); const handleTechSelect = useCallback( (category: keyof typeof TECH_OPTIONS, techId: string) => { setStack((prev) => { const currentStack = { ...prev }; const catKey = category as keyof StackState; if ( catKey === "frontend" || catKey === "addons" || catKey === "examples" ) { const currentArray = [...(currentStack[catKey] as string[])]; let nextArray = [...currentArray]; const isSelected = currentArray.includes(techId); if (catKey === "frontend") { const webTypes = [ "tanstack-router", "react-router", "tanstack-start", "next", ]; if (techId === "none") { nextArray = ["none"]; } else if (isSelected) { nextArray = nextArray.filter((id) => id !== techId); if (nextArray.length === 0) { return prev; } } else { nextArray = nextArray.filter((id) => id !== "none"); if (webTypes.includes(techId)) { nextArray = nextArray.filter((id) => !webTypes.includes(id)); } nextArray.push(techId); } } else { if (isSelected) { nextArray = nextArray.filter((id) => id !== techId); } else { nextArray.push(techId); } } return { ...currentStack, [catKey]: [...new Set(nextArray)] }; } if (currentStack[catKey] === techId) { return prev; } return { ...currentStack, [catKey]: techId }; }); }, [], ); const getDisabledReason = useCallback( (category: keyof typeof TECH_OPTIONS, techId: string): string | null => { const catKey = category as keyof StackState; if (catKey === "api" && techId !== "trpc" && currentHasNativeFrontend) { return "Only tRPC API is supported with React Native."; } if (catKey === "orm") { if (stack.database === "none") return "Select a database to enable ORM options."; if (stack.database === "mongodb" && techId === "drizzle") return "MongoDB requires the Prisma ORM."; if (stack.dbSetup === "turso" && techId === "prisma") return "Turso DB setup requires the Drizzle ORM."; } if (catKey === "dbSetup" && techId !== "none") { if (stack.database === "none") return "Select a database before choosing a cloud setup."; if (techId === "turso") { if (stack.database !== "sqlite") return "Turso requires SQLite database."; if (stack.orm === "prisma") return "Turso requires Drizzle ORM."; } else if (techId === "prisma-postgres") { if (stack.database !== "postgres") return "Requires PostgreSQL database."; if (stack.orm !== "prisma") return "Requires Prisma ORM."; } else if (techId === "mongodb-atlas") { if (stack.database !== "mongodb") return "Requires MongoDB database."; if (stack.orm !== "prisma") return "Requires Prisma ORM."; } else if (techId === "neon") { if (stack.database !== "postgres") return "Requires PostgreSQL database."; } } if (catKey === "auth" && techId === "true" && stack.database === "none") { return "Authentication requires a database."; } if (catKey === "addons") { if ( (techId === "pwa" || techId === "tauri") && !currentHasPWACompatibleFrontend ) { return "Requires TanStack Router or React Router frontend."; } } if (catKey === "examples") { if ((techId === "todo" || techId === "ai") && !currentHasWebFrontend) { return "Requires a web frontend (TanStack Router, React Router, etc.)."; } if (techId === "todo" && stack.database === "none") { return "Todo example requires a database."; } if (techId === "ai" && stack.backendFramework === "elysia") { return "AI example is not compatible with Elysia backend."; } } return null; }, [ stack.database, stack.orm, stack.dbSetup, stack.backendFramework, currentHasNativeFrontend, currentHasPWACompatibleFrontend, currentHasWebFrontend, ], ); const copyToClipboard = useCallback(() => { navigator.clipboard.writeText(command); setCopied(true); setTimeout(() => setCopied(false), 2000); }, [command]); const resetStack = useCallback(() => { setStack(DEFAULT_STACK); setProjectNameError(validateProjectName(DEFAULT_STACK.projectName || "")); setShowHelp(false); setShowPresets(false); setActiveCategory(CATEGORY_ORDER[0]); contentRef.current?.scrollTo(0, 0); }, []); useEffect(() => { setProjectNameError(validateProjectName(stack.projectName || "")); }, [stack.projectName]); const saveCurrentStack = useCallback(() => { localStorage.setItem("betterTStackPreference", JSON.stringify(stack)); setLastSavedStack(stack); }, [stack]); const loadSavedStack = useCallback(() => { if (lastSavedStack) { setStack(lastSavedStack); setProjectNameError( validateProjectName(lastSavedStack.projectName || ""), ); setShowHelp(false); setShowPresets(false); setActiveCategory(CATEGORY_ORDER[0]); contentRef.current?.scrollTo(0, 0); } }, [lastSavedStack]); const applyPreset = useCallback((presetId: string) => { const preset = PRESET_TEMPLATES.find( (template) => template.id === presetId, ); if (preset) { setStack(preset.stack); setProjectNameError(validateProjectName(preset.stack.projectName || "")); setShowPresets(false); setShowHelp(false); setActiveCategory(CATEGORY_ORDER[0]); contentRef.current?.scrollTo(0, 0); } }, []); const handleSidebarClick = (category: string) => { setActiveCategory(category); const element = sectionRefs.current[category]; if (element) { element.scrollIntoView({ behavior: "smooth", block: "start" }); } }; const getCategoryDisplayName = (categoryKey: string): string => { const result = categoryKey.replace(/([A-Z])/g, " $1"); return result.charAt(0).toUpperCase() + result.slice(1); }; return (
Stack Architect Terminal
{showHelp && (

How to Use Stack Architect

  • Use the sidebar to navigate between configuration sections.
  • Select your preferred technologies in the main area.
  • Some selections may disable or automatically change other options based on compatibility (check notes within each section!).
  • The command below updates automatically based on your selections.
  • Click the copy button ( ) next to the command to copy it.
  • Use presets () for quick setup or reset () to defaults.
  • Save () your preferences to load () them later.
)} {showPresets && (

Quick Start Presets

{PRESET_TEMPLATES.map((preset) => ( ))}
)}
{lastSavedStack && ( )}
$ {command}

Selected Stack Summary

{CATEGORY_ORDER.flatMap((category) => { const categoryKey = category as keyof StackState; const options = TECH_OPTIONS[category as keyof typeof TECH_OPTIONS]; const selectedValue = stack[categoryKey]; if (!options) return []; if (Array.isArray(selectedValue)) { if (selectedValue.length === 0 || selectedValue[0] === "none") return []; return selectedValue .map((id) => options.find((opt) => opt.id === id)) .filter((tech): tech is NonNullable => Boolean(tech), ) .map((tech) => ( {tech.icon} {tech.name} )); } const tech = options.find((opt) => opt.id === selectedValue); if ( !tech || tech.id === "none" || tech.id === "false" || ((category === "git" || category === "install" || category === "auth") && tech.id === "true") ) { return []; } return ( {tech.icon} {tech.name} ); })}
{CATEGORY_ORDER.map((categoryKey) => { const categoryOptions = TECH_OPTIONS[categoryKey as keyof typeof TECH_OPTIONS] || []; const categoryDisplayName = getCategoryDisplayName(categoryKey); const notesInfo = compatNotes[categoryKey]; return (
{ sectionRefs.current[categoryKey] = el; }} key={categoryKey} id={`section-${categoryKey}`} className="mb-8 scroll-mt-4" >

{categoryDisplayName}

{notesInfo?.notes && notesInfo.notes.length > 0 && (
{notesInfo.hasIssue ? "Compatibility Issues / Auto-Adjustments" : "Notes"}
    {notesInfo.notes.map((note, index) => ( // biome-ignore lint/suspicious/noArrayIndexKey: Static notes per render
  • {note}
  • ))}
)}
{categoryOptions.map((tech) => { let isSelected = false; const category = categoryKey as keyof StackState; if ( category === "addons" || category === "examples" || category === "frontend" ) { isSelected = ( (stack[category] as string[]) || [] ).includes(tech.id); } else { isSelected = stack[category] === tech.id; } const disabledReason = getDisabledReason( categoryKey as keyof typeof TECH_OPTIONS, tech.id, ); const isDisabled = !!disabledReason; return ( !isDisabled && handleTechSelect( categoryKey as keyof typeof TECH_OPTIONS, tech.id, ) } >
{isSelected ? ( ) : ( )}
{tech.icon} {tech.name}

{tech.description}

{tech.default && !isSelected && !isDisabled && ( Default )}
); })}
); })}
); }; const getBadgeColors = (category: string): string => { switch (category) { case "frontend": return "border-blue-300 bg-blue-100 text-blue-800 dark:border-blue-700/30 dark:bg-blue-900/30 dark:text-blue-300"; case "runtime": return "border-amber-300 bg-amber-100 text-amber-800 dark:border-amber-700/30 dark:bg-amber-900/30 dark:text-amber-300"; case "backendFramework": return "border-sky-300 bg-sky-100 text-sky-800 dark:border-sky-700/30 dark:bg-sky-900/30 dark:text-sky-300"; case "api": return "border-indigo-300 bg-indigo-100 text-indigo-800 dark:border-indigo-700/30 dark:bg-indigo-900/30 dark:text-indigo-300"; case "database": return "border-emerald-300 bg-emerald-100 text-emerald-800 dark:border-emerald-700/30 dark:bg-emerald-900/30 dark:text-emerald-300"; case "orm": return "border-cyan-300 bg-cyan-100 text-cyan-800 dark:border-cyan-700/30 dark:bg-cyan-900/30 dark:text-cyan-300"; case "auth": return "border-green-300 bg-green-100 text-green-800 dark:border-green-700/30 dark:bg-green-900/30 dark:text-green-300"; case "dbSetup": return "border-pink-300 bg-pink-100 text-pink-800 dark:border-pink-700/30 dark:bg-pink-900/30 dark:text-pink-300"; case "addons": return "border-violet-300 bg-violet-100 text-violet-800 dark:border-violet-700/30 dark:bg-violet-900/30 dark:text-violet-300"; case "examples": return "border-teal-300 bg-teal-100 text-teal-800 dark:border-teal-700/30 dark:bg-teal-900/30 dark:text-teal-300"; case "packageManager": return "border-orange-300 bg-orange-100 text-orange-800 dark:border-orange-700/30 dark:bg-orange-900/30 dark:text-orange-300"; case "git": case "install": return "border-gray-300 bg-gray-100 text-gray-700 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400"; default: return "border-gray-300 bg-gray-100 text-gray-800 dark:border-gray-700/30 dark:bg-gray-900/30 dark:text-gray-300"; } }; export default StackArchitect;