22
33import {
44 Background ,
5+ type Connection ,
56 ReactFlow ,
67 useEdgesState ,
78 useNodesState ,
@@ -16,8 +17,8 @@ import { TechNodeComponent } from "./TechNodeComponent";
1617const initialEdges = [
1718 { id : "bun-hono" , source : "bun" , target : "hono" , animated : true } ,
1819 { id : "bun-tanstack" , source : "bun" , target : "tanstack" , animated : true } ,
19- { id : "hono-libsql" , source : "hono" , target : "libsql " , animated : true } ,
20- { id : "libsql-drizzle" , source : "libsql " , target : "drizzle" , animated : true } ,
20+ { id : "hono-libsql" , source : "hono" , target : "sqlite " , animated : true } ,
21+ { id : "libsql-drizzle" , source : "sqlite " , target : "drizzle" , animated : true } ,
2122 {
2223 id : "hono-better-auth" ,
2324 source : "hono" ,
@@ -47,6 +48,29 @@ const CustomizableStack = () => {
4748 auth : "better-auth" ,
4849 } ) ;
4950
51+ // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
52+ const cleanupConnectionsByCategory = useCallback ( ( category : string ) => {
53+ setEdges ( ( eds ) =>
54+ eds . filter ( ( edge ) => {
55+ if ( category === "database" ) {
56+ return ! (
57+ [ "postgres" , "sqlite" ] . includes ( edge . target ) ||
58+ [ "postgres" , "sqlite" ] . includes ( edge . source ) ||
59+ edge . target === "drizzle" ||
60+ edge . target === "prisma"
61+ ) ;
62+ }
63+ if ( category === "orm" ) {
64+ return ! ( edge . target === "drizzle" || edge . target === "prisma" ) ;
65+ }
66+ if ( category === "auth" ) {
67+ return ! [ "better-auth" , "no-auth" ] . includes ( edge . target ) ;
68+ }
69+ return true ;
70+ } ) ,
71+ ) ;
72+ } , [ ] ) ;
73+
5074 const handleTechSelect = useCallback (
5175 ( category : string , techId : string ) => {
5276 setActiveNodes ( ( prev ) => ( { ...prev , [ category ] : techId } ) ) ;
@@ -64,16 +88,8 @@ const CustomizableStack = () => {
6488 } ,
6589 } ) ) ,
6690 ) ;
67- setEdges ( ( eds ) =>
68- eds . filter ( ( edge ) => {
69- const targetNode = nodes . find ( ( n ) => n . id === edge . target ) ;
70- const sourceNode = nodes . find ( ( n ) => n . id === edge . source ) ;
71- return ! (
72- targetNode ?. data . category === category ||
73- sourceNode ?. data . category === category
74- ) ;
75- } ) ,
76- ) ;
91+
92+ cleanupConnectionsByCategory ( category ) ;
7793
7894 if ( category === "database" ) {
7995 const honoNode = nodes . find ( ( n ) => n . id === "hono" ) ;
@@ -125,12 +141,105 @@ const CustomizableStack = () => {
125141 }
126142 }
127143 } ,
128- [ nodes , setNodes , setEdges ] ,
144+ [ nodes , setNodes , setEdges , cleanupConnectionsByCategory ] ,
129145 ) ;
130146
131- const onConnect = useCallback ( ( ) => {
132- return ;
133- } , [ ] ) ;
147+ const isValidConnection = useCallback (
148+ ( connection : Connection ) => {
149+ const sourceNode = nodes . find ( ( n ) => n . id === connection . source ) ;
150+ const targetNode = nodes . find ( ( n ) => n . id === connection . target ) ;
151+
152+ if ( ! sourceNode || ! targetNode ) return false ;
153+
154+ if ( sourceNode . id === "hono" && targetNode . data . category === "database" ) {
155+ return [ "postgres" , "sqlite" ] . includes ( targetNode . id ) ;
156+ }
157+
158+ if ( sourceNode . id === "hono" && targetNode . data . category === "auth" ) {
159+ return [ "better-auth" , "no-auth" ] . includes ( targetNode . id ) ;
160+ }
161+
162+ if (
163+ [ "postgres" , "sqlite" ] . includes ( sourceNode . id ) &&
164+ targetNode . data . category === "orm"
165+ ) {
166+ return true ;
167+ }
168+
169+ return false ;
170+ } ,
171+ [ nodes ] ,
172+ ) ;
173+
174+ const cleanupPreviousConnections = useCallback (
175+ ( connection : Connection ) => {
176+ const sourceNode = nodes . find ( ( n ) => n . id === connection . source ) ;
177+ const targetNode = nodes . find ( ( n ) => n . id === connection . target ) ;
178+ if ( ! targetNode || ! sourceNode ) return ;
179+
180+ cleanupConnectionsByCategory ( targetNode . data . category ) ;
181+ } ,
182+ [ nodes , cleanupConnectionsByCategory ] ,
183+ ) ;
184+
185+ const onConnect = useCallback (
186+ ( connection : Connection ) => {
187+ if ( ! isValidConnection ( connection ) ) return ;
188+ cleanupPreviousConnections ( connection ) ;
189+ const targetNode = nodes . find ( ( n ) => n . id === connection . target ) ;
190+ if ( ! targetNode ) return ;
191+
192+ setEdges ( ( eds ) => {
193+ const newEdges = [
194+ ...eds ,
195+ {
196+ id : `${ connection . source } -${ connection . target } ` ,
197+ source : connection . source ,
198+ target : connection . target ,
199+ animated : true ,
200+ } ,
201+ ] ;
202+
203+ if ( targetNode . data . category === "database" ) {
204+ const activeOrm = nodes . find (
205+ ( n ) => n . data . category === "orm" && n . data . isActive ,
206+ ) ;
207+ if ( activeOrm ) {
208+ newEdges . push ( {
209+ id : `${ connection . target } -${ activeOrm . id } ` ,
210+ source : connection . target ,
211+ target : activeOrm . id ,
212+ animated : true ,
213+ } ) ;
214+ }
215+ }
216+
217+ return newEdges ;
218+ } ) ;
219+
220+ if ( targetNode . data . category ) {
221+ setActiveNodes ( ( prev ) => ( {
222+ ...prev ,
223+ [ targetNode . data . category ] : connection . target ,
224+ } ) ) ;
225+
226+ setNodes ( ( nds ) =>
227+ nds . map ( ( node ) => ( {
228+ ...node ,
229+ data : {
230+ ...node . data ,
231+ isActive : node . data . isStatic
232+ ? true
233+ : node . data . category === targetNode . data . category
234+ ? node . id === connection . target
235+ : node . data . isActive ,
236+ } ,
237+ } ) ) ,
238+ ) ;
239+ }
240+ } ,
241+ [ nodes , setEdges , setNodes , cleanupPreviousConnections , isValidConnection ] ,
242+ ) ;
134243
135244 const generateCommand = useCallback ( ( ) => {
136245 const flags : string [ ] = [ "-y" ] ;
@@ -178,6 +287,10 @@ const CustomizableStack = () => {
178287 zoomOnPinch = { false }
179288 preventScrolling = { false }
180289 nodesConnectable = { true }
290+ nodesDraggable = { false }
291+ connectOnClick = { true }
292+ deleteKeyCode = "Delete"
293+ selectionKeyCode = "Shift"
181294 >
182295 < Background
183296 className = "bg-gray-950/5"
0 commit comments