From b3ae7e280bc05e76a0d074f49deff085420addc2 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Sun, 2 Mar 2025 12:34:58 +0530 Subject: [PATCH] update customizable stack --- .../(home)/_components/CustomizableStack.tsx | 462 ++++++++++++------ .../app/(home)/_components/TechSelector.tsx | 103 +++- apps/web/src/lib/constant.ts | 18 +- 3 files changed, 398 insertions(+), 185 deletions(-) diff --git a/apps/web/src/app/(home)/_components/CustomizableStack.tsx b/apps/web/src/app/(home)/_components/CustomizableStack.tsx index 97bde5a..6eb9178 100644 --- a/apps/web/src/app/(home)/_components/CustomizableStack.tsx +++ b/apps/web/src/app/(home)/_components/CustomizableStack.tsx @@ -14,11 +14,12 @@ import { initialNodes } from "@/lib/constant"; import { CommandDisplay } from "./CommandDisplay"; import { TechNodeComponent } from "./TechNodeComponent"; +// Define initial edges with proper connections const initialEdges = [ { id: "bun-hono", source: "bun", target: "hono", animated: true }, { id: "bun-tanstack", source: "bun", target: "tanstack", animated: true }, - { id: "hono-libsql", source: "hono", target: "sqlite", animated: true }, - { id: "libsql-drizzle", source: "sqlite", target: "drizzle", animated: true }, + { id: "hono-sqlite", source: "hono", target: "sqlite", animated: true }, + { id: "sqlite-drizzle", source: "sqlite", target: "drizzle", animated: true }, { id: "hono-better-auth", source: "hono", @@ -38,16 +39,38 @@ const nodeTypes = { techNode: TechNodeComponent, }; +interface ActiveNodes { + backend: string; + database: string; + orm: string; + auth: string; + packageManager: string; + features: { + docker: boolean; + githubActions: boolean; + seo: boolean; + git: boolean; + }; +} + const CustomizableStack = () => { const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); - const [activeNodes, setActiveNodes] = useState({ + const [activeNodes, setActiveNodes] = useState({ backend: "hono", database: "sqlite", orm: "drizzle", auth: "better-auth", + packageManager: "npm", + features: { + docker: false, + githubActions: false, + seo: false, + git: true, + }, }); const [windowSize, setWindowSize] = useState("lg"); + const [command, setCommand] = useState("npx create-better-t-stack my-app -y"); useEffect(() => { const handleResize = () => { @@ -66,32 +89,54 @@ const CustomizableStack = () => { }, []); // biome-ignore lint/correctness/useExhaustiveDependencies: - const cleanupConnectionsByCategory = useCallback((category: string) => { - setEdges((eds) => - eds.filter((edge) => { - if (category === "database") { - return !( - ["postgres", "sqlite"].includes(edge.target) || - ["postgres", "sqlite"].includes(edge.source) || - edge.target === "drizzle" || - edge.target === "prisma" - ); - } - if (category === "orm") { - return !(edge.target === "drizzle" || edge.target === "prisma"); - } - if (category === "auth") { - return !["better-auth", "no-auth"].includes(edge.target); - } - return true; - }), - ); - }, []); + useEffect(() => { + // Generate command whenever activeNodes changes and update the command state + setCommand(generateCommand()); + }, [activeNodes]); + + // Function to remove connections related to specific category + const removeConnectionsByCategory = useCallback( + (category: string) => { + setEdges((eds) => { + return eds.filter((edge) => { + // Find source and target nodes + const sourceNode = nodes.find((n) => n.id === edge.source); + const targetNode = nodes.find((n) => n.id === edge.target); + + if (!sourceNode || !targetNode) return true; + + // Remove edges connected to the category being changed + if (targetNode.data.category === category) return false; + + // Remove edges that connect from the category being changed + if (sourceNode.data.category === category) return false; + + // For database changes, also remove ORM connections + if (category === "database" && targetNode.data.category === "orm") + return false; + + return true; + }); + }); + }, + [nodes, setEdges], + ); const handleTechSelect = useCallback( (category: string, techId: string) => { - setActiveNodes((prev) => ({ ...prev, [category]: techId })); + // Update active nodes state + setActiveNodes((prev) => ({ + ...prev, + [category]: techId, + ...(category === "features" && { + features: { + ...prev.features, + [techId]: !prev.features[techId as keyof typeof prev.features], + }, + }), + })); + // Update node active states setNodes((nds) => nds.map((node) => ({ ...node, @@ -106,32 +151,84 @@ const CustomizableStack = () => { })), ); - cleanupConnectionsByCategory(category); + // Remove old connections for this category + removeConnectionsByCategory(category); - if (category === "database") { - const honoNode = nodes.find((n) => n.id === "hono"); - const ormNode = nodes.find( - (n) => n.data.category === "orm" && n.data.isActive, - ); + // Create new connections based on the selected tech + if (category === "backend") { + // Connect backend to database, auth, and other core components + const database = activeNodes.database; + const auth = activeNodes.auth; - if (honoNode && ormNode) { + setEdges((eds) => [ + ...eds, + { + id: `bun-${techId}`, + source: "bun", + target: techId, + animated: true, + }, + { + id: `${techId}-${database}`, + source: techId, + target: database, + animated: true, + }, + { + id: `${techId}-${auth}`, + source: techId, + target: auth, + animated: true, + }, + { + id: `${techId}-tanstack`, + source: techId, + target: "tanstack", + animated: true, + }, + ]); + } else if (category === "database") { + // Connect backend to database and database to ORM + const orm = activeNodes.orm; + + setEdges((eds) => [ + ...eds, + { + id: `hono-${techId}`, + source: "hono", + target: techId, + animated: true, + }, + // Only add ORM connection if database is not "no-database" + ...(techId !== "no-database" + ? [ + { + id: `${techId}-${orm}`, + source: techId, + target: orm, + animated: true, + }, + ] + : []), + ]); + } else if (category === "orm") { + // Connect database to ORM + const database = activeNodes.database; + + // Only add connection if database is not "no-database" + if (database !== "no-database") { setEdges((eds) => [ ...eds, { - id: `hono-${techId}`, - source: "hono", + id: `${database}-${techId}`, + source: database, target: techId, animated: true, }, - { - id: `${techId}-${ormNode.id}`, - source: techId, - target: ormNode.id, - animated: true, - }, ]); } } else if (category === "auth") { + // Connect backend to auth setEdges((eds) => [ ...eds, { @@ -141,24 +238,9 @@ const CustomizableStack = () => { animated: true, }, ]); - } else if (category === "orm") { - const dbNode = nodes.find( - (n) => n.data.category === "database" && n.data.isActive, - ); - if (dbNode) { - setEdges((eds) => [ - ...eds, - { - id: `${dbNode.id}-${techId}`, - source: dbNode.id, - target: techId, - animated: true, - }, - ]); - } } }, - [nodes, setNodes, setEdges, cleanupConnectionsByCategory], + [activeNodes, setNodes, setEdges, removeConnectionsByCategory], ); const isValidConnection = useCallback( @@ -168,8 +250,9 @@ const CustomizableStack = () => { if (!sourceNode || !targetNode) return false; + // Define valid connection patterns if (sourceNode.id === "hono" && targetNode.data.category === "database") { - return ["postgres", "sqlite"].includes(targetNode.id); + return ["postgres", "sqlite", "no-database"].includes(targetNode.id); } if (sourceNode.id === "hono" && targetNode.data.category === "auth") { @@ -180,7 +263,7 @@ const CustomizableStack = () => { ["postgres", "sqlite"].includes(sourceNode.id) && targetNode.data.category === "orm" ) { - return true; + return ["drizzle", "prisma"].includes(targetNode.id); } return false; @@ -188,137 +271,196 @@ const CustomizableStack = () => { [nodes], ); - const cleanupPreviousConnections = useCallback( - (connection: Connection) => { - const sourceNode = nodes.find((n) => n.id === connection.source); - const targetNode = nodes.find((n) => n.id === connection.target); - if (!targetNode || !sourceNode) return; - - cleanupConnectionsByCategory(targetNode.data.category); - }, - [nodes, cleanupConnectionsByCategory], - ); - const onConnect = useCallback( (connection: Connection) => { if (!isValidConnection(connection)) return; - cleanupPreviousConnections(connection); + const targetNode = nodes.find((n) => n.id === connection.target); - if (!targetNode) return; + if (!targetNode || !targetNode.data.category) return; - setEdges((eds) => { - const newEdges = [ - ...eds, - { - id: `${connection.source}-${connection.target}`, - source: connection.source, - target: connection.target, - animated: true, + // Remove existing connections for the category we're connecting to + removeConnectionsByCategory(targetNode.data.category); + + // Update active nodes state + setActiveNodes((prev) => ({ + ...prev, + [targetNode.data.category]: connection.target, + })); + + // Update node active states + setNodes((nds) => + nds.map((node) => ({ + ...node, + data: { + ...node.data, + isActive: node.data.isStatic + ? true + : node.data.category === targetNode.data.category + ? node.id === connection.target + : node.data.isActive, }, - ]; + })), + ); - if (targetNode.data.category === "database") { - const activeOrm = nodes.find( - (n) => n.data.category === "orm" && n.data.isActive, - ); - if (activeOrm) { - newEdges.push({ + // Add the new connection + setEdges((eds) => [ + ...eds, + { + id: `${connection.source}-${connection.target}`, + source: connection.source, + target: connection.target, + animated: true, + }, + ]); + + // If connecting to database, also connect to the active ORM + if ( + targetNode.data.category === "database" && + targetNode.id !== "no-database" + ) { + const activeOrm = nodes.find( + (n) => n.data.category === "orm" && n.data.isActive, + ); + if (activeOrm) { + setEdges((eds) => [ + ...eds, + { id: `${connection.target}-${activeOrm.id}`, source: connection.target, target: activeOrm.id, animated: true, - }); - } - } - - return newEdges; - }); - - if (targetNode.data.category) { - setActiveNodes((prev) => ({ - ...prev, - [targetNode.data.category]: connection.target, - })); - - setNodes((nds) => - nds.map((node) => ({ - ...node, - data: { - ...node.data, - isActive: node.data.isStatic - ? true - : node.data.category === targetNode.data.category - ? node.id === connection.target - : node.data.isActive, }, - })), - ); + ]); + } } }, - [nodes, setEdges, setNodes, cleanupPreviousConnections, isValidConnection], + [nodes, setEdges, setNodes, removeConnectionsByCategory, isValidConnection], ); const generateCommand = useCallback(() => { - const flags: string[] = ["-y"]; + // Start with the base command + const command = "npx create-better-t-stack my-app"; + const flags: string[] = []; - if (activeNodes.database !== "sqlite") { - flags.splice(flags.indexOf("-y"), 1); - flags.push(`--${activeNodes.database}`); + // Check if all defaults are being used + const isAllDefaults = + activeNodes.database === "sqlite" && + activeNodes.auth === "better-auth" && + activeNodes.orm === "drizzle" && + activeNodes.packageManager === "npm" && + activeNodes.features.git === true && + !activeNodes.features.docker && + !activeNodes.features.githubActions && + !activeNodes.features.seo; + + // If using all defaults, just use -y flag + if (isAllDefaults) { + return `${command} -y`; } - if (activeNodes.auth !== "better-auth") { - if (flags.includes("-y")) { - flags.splice(flags.indexOf("-y"), 1); - } + // Database options + if (activeNodes.database === "postgres") { + flags.push("--postgres"); + } else if (activeNodes.database === "sqlite") { + flags.push("--sqlite"); + } else if (activeNodes.database === "no-database") { + flags.push("--no-database"); + } + + // Authentication options + if (activeNodes.auth === "better-auth") { + flags.push("--auth"); + } else if (activeNodes.auth === "no-auth") { flags.push("--no-auth"); } - return `npx create-better-t-stack my-app ${flags.join(" ")}`; + // ORM options + if (activeNodes.orm === "drizzle") { + flags.push("--drizzle"); + } else if (activeNodes.orm === "prisma") { + flags.push("--prisma"); + } + + // Package manager options + if (activeNodes.packageManager !== "npm") { + flags.push(`--${activeNodes.packageManager}`); + } + + // Feature flags + if (activeNodes.features.docker) { + flags.push("--docker"); + } + + if (activeNodes.features.githubActions) { + flags.push("--github-actions"); + } + + if (activeNodes.features.seo) { + flags.push("--seo"); + } + + if (!activeNodes.features.git) { + flags.push("--no-git"); + } + + return flags.length > 0 ? `${command} ${flags.join(" ")}` : command; }, [activeNodes]); return (
- -
- + {/* Command Display - Fixed at top with proper centering */} +
+
-
-
- Select technologies from the left panel to customize your stack. The - graph will automatically update connections. + + {/* Main container with proper layout */} +
+
+ + {/* Tech selector fixed to the left side */} +
+ +
+ + {/* Help text */} +
+
+ Select technologies from the left panel to customize your stack. The + graph will automatically update connections. +
+
+ + {/* Flow container with proper spacing from the selector */} +
+ + +
-
-
-
- - -
); diff --git a/apps/web/src/app/(home)/_components/TechSelector.tsx b/apps/web/src/app/(home)/_components/TechSelector.tsx index 6ba0835..07642e4 100644 --- a/apps/web/src/app/(home)/_components/TechSelector.tsx +++ b/apps/web/src/app/(home)/_components/TechSelector.tsx @@ -1,3 +1,17 @@ +interface ActiveNodes { + backend: string; + database: string; + orm: string; + auth: string; + packageManager: string; + features: { + docker: boolean; + githubActions: boolean; + seo: boolean; + git: boolean; + }; +} + type TechOption = { id: string; label: string; @@ -6,48 +20,89 @@ type TechOption = { const techOptions: Record = { database: [ - { id: "sqlite", label: "Sqlite", category: "database" }, + { id: "sqlite", label: "SQLite", category: "database" }, { id: "postgres", label: "PostgreSQL", category: "database" }, + { id: "no-database", label: "No DB", category: "database" }, ], orm: [ { id: "drizzle", label: "Drizzle", category: "orm" }, { id: "prisma", label: "Prisma", category: "orm" }, ], auth: [ - { id: "better-auth", label: "Better-Auth", category: "auth" }, + { id: "better-auth", label: "Auth", category: "auth" }, { id: "no-auth", label: "No Auth", category: "auth" }, ], + packageManager: [ + { id: "npm", label: "NPM", category: "packageManager" }, + { id: "pnpm", label: "PNPM", category: "packageManager" }, + { id: "yarn", label: "Yarn", category: "packageManager" }, + { id: "bun", label: "Bun", category: "packageManager" }, + ], + features: [ + { id: "docker", label: "Docker", category: "features" }, + { id: "githubActions", label: "GitHub Actions", category: "features" }, + { id: "seo", label: "SEO", category: "features" }, + { id: "git", label: "Git", category: "features" }, + ], }; interface TechSelectorProps { onSelect: (category: string, techId: string) => void; - activeNodes: Record; + activeNodes: ActiveNodes; } export function TechSelector({ onSelect, activeNodes }: TechSelectorProps) { return ( -
-
Customize Stack
- {Object.entries(techOptions).map(([category, options]) => ( -
-
{category}
-
- {options.map((option) => ( - onSelect(category, option.id)} - > - {option.label} - - ))} +
+
+ Options +
+ + {/* Regular tech options */} + {Object.entries(techOptions) + .filter(([category]) => category !== "features") + .map(([category, options]) => ( +
+
{category}
+
+ {options.map((option) => ( + + ] === option.id && "bg-blue-600 text-white" + }`} + onClick={() => onSelect(category, option.id)} + > + {option.label} + + ))} +
+ ))} + + {/* Feature toggles */} +
+
Features
+
+ {techOptions.features.map((option) => ( + onSelect("features", option.id)} + > + {option.label} + + ))}
- ))} +
); } @@ -65,7 +120,7 @@ const Badge = ({ return (