mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
update customizable stack
This commit is contained in:
@@ -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<ActiveNodes>({
|
||||
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: <explanation>
|
||||
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 (
|
||||
<div className="relative w-full max-w-5xl mx-auto z-50 mt-24">
|
||||
<TechSelector onSelect={handleTechSelect} activeNodes={activeNodes} />
|
||||
<div className="absolute -top-16 left-1/2 max-sm:left-[60%] -translate-x-1/2 z-50 w-96">
|
||||
<CommandDisplay command={generateCommand()} />
|
||||
{/* Command Display - Fixed at top with proper centering */}
|
||||
<div className="absolute -top-16 left-0 right-0 mx-auto flex justify-center z-50">
|
||||
<CommandDisplay command={command} />
|
||||
</div>
|
||||
<div className="max-sm:hidden bg-gray-950/10 lg:p-4 p-1 absolute lg:top-4 top-2 lg:right-4 right-2 z-50 w-80 rounded-xl border border-gray-800 backdrop-blur-3xl">
|
||||
<div className="lg:text-sm text-xs text-gray-300 text-center">
|
||||
Select technologies from the left panel to customize your stack. The
|
||||
graph will automatically update connections.
|
||||
|
||||
{/* Main container with proper layout */}
|
||||
<div className="relative rounded-xl border border-gray-800 overflow-hidden">
|
||||
<div className="absolute inset-0 backdrop-blur-3xl bg-gradient-to-r from-blue-500/10 via-purple-500/10 to-pink-500/10" />
|
||||
|
||||
{/* Tech selector fixed to the left side */}
|
||||
<div className="absolute left-0 top-0 bottom-0 lg:w-52 md:w-44 w-36 z-50 bg-gray-950/30 border-r border-gray-800/50">
|
||||
<TechSelector onSelect={handleTechSelect} activeNodes={activeNodes} />
|
||||
</div>
|
||||
|
||||
{/* Help text */}
|
||||
<div className="max-sm:hidden bg-gray-950/30 lg:p-4 p-1 absolute lg:top-4 top-2 lg:right-4 right-2 z-50 w-80 rounded-xl border border-gray-800 backdrop-blur-3xl">
|
||||
<div className="lg:text-sm text-xs text-gray-300 text-center">
|
||||
Select technologies from the left panel to customize your stack. The
|
||||
graph will automatically update connections.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Flow container with proper spacing from the selector */}
|
||||
<div className="h-[600px] lg:pl-52 md:pl-44 pl-36 relative backdrop-blur-sm bg-gray-950/50">
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
nodeTypes={nodeTypes}
|
||||
fitView
|
||||
maxZoom={windowSize === "sm" ? 0.6 : windowSize === "md" ? 0.6 : 1}
|
||||
zoomOnScroll={false}
|
||||
zoomOnPinch={false}
|
||||
preventScrolling={false}
|
||||
nodesConnectable={true}
|
||||
nodesDraggable={true}
|
||||
connectOnClick={true}
|
||||
deleteKeyCode="Delete"
|
||||
selectionKeyCode="Shift"
|
||||
proOptions={{
|
||||
hideAttribution: true,
|
||||
}}
|
||||
>
|
||||
<Background
|
||||
className="bg-gray-950/5"
|
||||
color="#1e293b"
|
||||
gap={12}
|
||||
size={1}
|
||||
/>
|
||||
</ReactFlow>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute inset-0 backdrop-blur-3xl bg-gradient-to-r from-blue-500/10 via-purple-500/10 to-pink-500/10 rounded-xl" />
|
||||
<div className="h-[600px] lg:pl-28 max-sm:pt-36 relative backdrop-blur-sm bg-gray-950/50 rounded-xl overflow-hidden border border-gray-800">
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
nodeTypes={nodeTypes}
|
||||
fitView
|
||||
// minZoom={1}
|
||||
maxZoom={windowSize === "sm" ? 0.6 : windowSize === "md" ? 0.6 : 1}
|
||||
zoomOnScroll={false}
|
||||
zoomOnPinch={false}
|
||||
preventScrolling={false}
|
||||
nodesConnectable={true}
|
||||
nodesDraggable={true}
|
||||
connectOnClick={true}
|
||||
deleteKeyCode="Delete"
|
||||
selectionKeyCode="Shift"
|
||||
proOptions={{
|
||||
hideAttribution: true,
|
||||
}}
|
||||
>
|
||||
<Background
|
||||
className="bg-gray-950/5"
|
||||
color="#1e293b"
|
||||
gap={12}
|
||||
size={1}
|
||||
/>
|
||||
</ReactFlow>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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<string, TechOption[]> = {
|
||||
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<string, string>;
|
||||
activeNodes: ActiveNodes;
|
||||
}
|
||||
|
||||
export function TechSelector({ onSelect, activeNodes }: TechSelectorProps) {
|
||||
return (
|
||||
<div className="absolute max-sm:w-11/12 top-4 left-4 z-50 sm:space-y-4 space-y-2 bg-gray-950/10 sm:p-4 px-4 py-2 rounded-xl border border-gray-800 backdrop-blur-3xl">
|
||||
<div className="text-sm font-medium text-gray-200">Customize Stack</div>
|
||||
{Object.entries(techOptions).map(([category, options]) => (
|
||||
<div key={category} className="space-y-2">
|
||||
<div className="text-xs text-gray-400 capitalize">{category}</div>
|
||||
<div className="flex gap-2">
|
||||
{options.map((option) => (
|
||||
<Badge
|
||||
key={option.id}
|
||||
variant="secondary"
|
||||
className={`cursor-pointer hover:bg-gray-700 ${
|
||||
activeNodes[category] === option.id &&
|
||||
"bg-blue-600 text-white"
|
||||
}`}
|
||||
onClick={() => onSelect(category, option.id)}
|
||||
>
|
||||
{option.label}
|
||||
</Badge>
|
||||
))}
|
||||
<div className="h-full overflow-y-auto p-3 space-y-5">
|
||||
<div className="text-sm font-medium text-gray-200 border-b border-gray-700 pb-2">
|
||||
Options
|
||||
</div>
|
||||
|
||||
{/* Regular tech options */}
|
||||
{Object.entries(techOptions)
|
||||
.filter(([category]) => category !== "features")
|
||||
.map(([category, options]) => (
|
||||
<div key={category} className="space-y-2">
|
||||
<div className="text-xs text-gray-400 capitalize">{category}</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{options.map((option) => (
|
||||
<Badge
|
||||
key={option.id}
|
||||
variant="secondary"
|
||||
className={`cursor-pointer hover:bg-gray-700 ${
|
||||
activeNodes[
|
||||
category as keyof Omit<ActiveNodes, "features">
|
||||
] === option.id && "bg-blue-600 text-white"
|
||||
}`}
|
||||
onClick={() => onSelect(category, option.id)}
|
||||
>
|
||||
{option.label}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Feature toggles */}
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs text-gray-400">Features</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{techOptions.features.map((option) => (
|
||||
<Badge
|
||||
key={option.id}
|
||||
variant="secondary"
|
||||
className={`cursor-pointer hover:bg-gray-700 ${
|
||||
activeNodes.features[
|
||||
option.id as keyof typeof activeNodes.features
|
||||
] === true && "bg-blue-600 text-white"
|
||||
}`}
|
||||
onClick={() => onSelect("features", option.id)}
|
||||
>
|
||||
{option.label}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -65,7 +120,7 @@ const Badge = ({
|
||||
return (
|
||||
<span
|
||||
className={`
|
||||
px-2 rounded-full py-1 text-xs font-medium,
|
||||
px-2 rounded-full py-1 text-xs font-medium
|
||||
${className}
|
||||
`}
|
||||
{...props}
|
||||
|
||||
@@ -117,6 +117,7 @@ export const initialNodes: TechNode[] = [
|
||||
description: "Fast all-in-one JavaScript runtime",
|
||||
isDefault: true,
|
||||
isActive: true,
|
||||
isStatic: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -129,6 +130,7 @@ export const initialNodes: TechNode[] = [
|
||||
description: "Type-safe routing",
|
||||
isDefault: true,
|
||||
isActive: true,
|
||||
isStatic: true,
|
||||
group: "router",
|
||||
},
|
||||
},
|
||||
@@ -168,6 +170,7 @@ export const initialNodes: TechNode[] = [
|
||||
description: "Ultrafast web framework",
|
||||
isDefault: true,
|
||||
isActive: true,
|
||||
isStatic: true,
|
||||
group: "backend",
|
||||
},
|
||||
},
|
||||
@@ -176,7 +179,7 @@ export const initialNodes: TechNode[] = [
|
||||
type: "techNode",
|
||||
position: { x: 544, y: 532 },
|
||||
data: {
|
||||
label: "Sqlite",
|
||||
label: "SQLite",
|
||||
category: "database",
|
||||
description: "SQLite-compatible database",
|
||||
isDefault: true,
|
||||
@@ -197,6 +200,19 @@ export const initialNodes: TechNode[] = [
|
||||
group: "database",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "no-database",
|
||||
type: "techNode",
|
||||
position: { x: 420, y: 710 },
|
||||
data: {
|
||||
label: "No Database",
|
||||
category: "database",
|
||||
description: "Skip database setup",
|
||||
isDefault: false,
|
||||
isActive: false,
|
||||
group: "database",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "drizzle",
|
||||
type: "techNode",
|
||||
|
||||
Reference in New Issue
Block a user