1- import { ReactFlow , type Node , Controls } from '@xyflow/react' ;
21import '@xyflow/react/dist/style.css' ;
32import '@xyflow/react/dist/base.css' ;
3+ import { ReactFlow , type Node , Controls , Background , BackgroundVariant , type Edge , useNodesState , useEdgesState , ConnectionLineType , Handle , Position , useReactFlow } from '@xyflow/react' ;
44import type { TrackDetails } from '../../../src/types' ;
5- import { useMemo } from 'react' ;
5+ import { useEffect , useState } from 'react' ;
66import { TrackItem } from './track' ;
7+ import * as Api from '../lib/api' ;
8+ import { cn } from '../lib/utils' ;
9+ import { useAutoLayout } from '../hooks/use-auto-layout' ;
10+ import { Loader2 } from 'lucide-react' ;
711
812const TrackNode = ( { data } : { data : { track : TrackDetails } } ) => {
913 return (
10- < div className = 'border border flex p-2 rounded-md gap-2' >
14+ // add click to view track details
15+ < div className = 'border border flex p-2 rounded-md gap-2 w-[200px] bg-background' >
16+ < Handle type = "target" position = { Position . Top } className = "!opacity-0" />
1117 < TrackItem track = { data . track } selected = { true } />
18+ { /* add tags for related tracks */ }
19+ < Handle type = "source" position = { Position . Bottom } className = "!opacity-0" />
1220 </ div >
1321 ) ;
1422} ;
@@ -17,37 +25,100 @@ const nodeTypes = {
1725 track : TrackNode ,
1826} ;
1927
28+ export function RelatedGraph ( { track } : { track : TrackDetails } ) {
29+ const { layout } = useAutoLayout ( ) ;
30+ const { fitView } = useReactFlow ( ) ;
31+ const [ isLoading , setIsLoading ] = useState ( true ) ;
32+ const [ relatedTracks , setRelatedTracks ] = useState < TrackDetails [ ] > ( [ ] ) ;
2033
34+ useEffect ( ( ) => {
35+ const fetchRelatedTracks = async ( ) => {
36+ try {
37+ setIsLoading ( true ) ;
38+ const relatedTracks = await Api . getRelatedTracks ( track . trackId ) ;
39+ setRelatedTracks ( relatedTracks ) ;
40+ setIsLoading ( false ) ;
41+ } catch ( error ) {
42+ console . error ( 'Error fetching related tracks' , error ) ;
43+ } finally {
44+ setIsLoading ( false ) ;
45+ }
46+ } ;
47+ fetchRelatedTracks ( ) ;
48+ } , [ track ] ) ;
2149
22- export function RelatedGraph ( { track } : { track : TrackDetails } ) {
23- // based on trackId get items with at least one tag in common
24- // then rank by number of tags in common
25- // return top 10
50+ const [ nodes , setNodes , onNodesChange ] = useNodesState < Node > ( [ ] ) ;
51+ const [ edges , setEdges , onEdgesChange ] = useEdgesState < Edge > ( [ ] ) ;
2652
27- // const [relatedTracks, setRelatedTracks] = useState<TrackDetails[]>([]);
53+ useEffect ( ( ) => {
54+ let initialNodes : Node [ ] = [ ] ;
55+ let initialEdges : Edge [ ] = [ ] ;
2856
29- const nodes : Node [ ] = useMemo ( ( ) => [
30- {
31- id : 'current-track' ,
57+ const selectedNode : Node = {
58+ id : track . trackId ,
3259 type : 'track' ,
3360 position : { x : 0 , y : 0 } ,
3461 data : { track : track } ,
35- } ,
36- // TODO: Fetch related tracks
37- ] , [ track ] ) ;
62+ } ;
63+
64+ if ( relatedTracks . length > 0 ) {
65+ const relatedTrackNodes = relatedTracks . map ( ( related ) => ( {
66+ id : related . trackId ,
67+ type : 'track' ,
68+ position : { x : 0 , y : 0 } ,
69+ data : { track : related } ,
70+ } ) ) ;
71+ initialNodes = [ selectedNode , ...relatedTrackNodes ] ;
72+ initialEdges = relatedTracks . map ( ( related ) => ( {
73+ id : `e-${ selectedNode . id } -${ related . trackId } ` ,
74+ source : selectedNode . id ,
75+ target : related . trackId ,
76+ type : ConnectionLineType . SmoothStep ,
77+ style : { stroke : '#b1b1b7' , strokeWidth : 2 } ,
78+ } ) ) ;
79+ } else {
80+ initialNodes = [ selectedNode ] ;
81+ }
82+
83+ const { nodes : layoutedNodes , edges : layoutedEdges } = layout ( {
84+ direction : 'LR' ,
85+ nodes : initialNodes ,
86+ edges : initialEdges ,
87+ } ) ;
88+
89+ setNodes ( layoutedNodes ) ;
90+ setEdges ( layoutedEdges ) ;
91+
92+ } , [ track , relatedTracks , layout , setNodes , setEdges ] ) ;
93+
94+ useEffect ( ( ) => {
95+ if ( nodes . length === 0 ) return
96+ window . requestAnimationFrame ( ( ) => {
97+ fitView ( { duration : 800 , padding : 0.2 } ) ;
98+ } ) ;
99+ } , [ nodes , fitView ] ) ;
38100
39101 return (
40102 < ReactFlow
41103 nodes = { nodes }
42- edges = { [ ] }
104+ edges = { edges }
105+ onNodesChange = { onNodesChange }
106+ onEdgesChange = { onEdgesChange }
43107 nodeTypes = { nodeTypes }
44- defaultViewport = { { x : 0 , y : 0 , zoom : 0.75 } }
45108 proOptions = { { hideAttribution : true } }
46109 minZoom = { 0.1 }
47- maxZoom = { 1.5 }
110+ maxZoom = { 1.25 }
111+ connectionLineType = { ConnectionLineType . SmoothStep }
112+ className = { cn ( isLoading && 'opacity-50 pointer-events-none' ) }
48113 fitView
49114 >
50- < Controls className = "text-black dark:text-black hover:bg-white" />
115+ < Controls
116+ className = "text-black dark:text-black hover:bg-white"
117+ position = "bottom-right"
118+ showInteractive = { false }
119+ />
120+ < Background variant = { BackgroundVariant . Dots } gap = { 24 } size = { 1 } />
121+ { isLoading && < Loader2 className = "animate-spin absolute top-1/2 left-1/2" size = { 48 } /> }
51122 </ ReactFlow >
52123 )
53124}
0 commit comments