|
1 | | -import { useState, useMemo, useRef, useEffect, useCallback } from 'react'; |
| 1 | +import { useState, useMemo } from 'react'; |
2 | 2 | import { useNavigate } from 'react-router-dom'; |
3 | 3 | import { useQuery } from '@tanstack/react-query'; |
4 | 4 | import { Box, Flex, Text, Input, Heading, SimpleGrid, Button } from '@chakra-ui/react'; |
@@ -32,14 +32,6 @@ interface ConceptsData { |
32 | 32 | concepts: ConceptNode[]; |
33 | 33 | } |
34 | 34 |
|
35 | | -interface Edge { |
36 | | - fromX: number; |
37 | | - fromY: number; |
38 | | - toX: number; |
39 | | - toY: number; |
40 | | - color: string; |
41 | | -} |
42 | | - |
43 | 35 | // ── Helpers ── |
44 | 36 |
|
45 | 37 | function isLessonCompleted(lessonId: string, worlds: World[]): boolean { |
@@ -73,8 +65,6 @@ function isLessonLocked(lessonId: string, worlds: World[]): boolean { |
73 | 65 | export default function ConceptMapPage() { |
74 | 66 | const navigate = useNavigate(); |
75 | 67 | const [search, setSearch] = useState(''); |
76 | | - const containerRef = useRef<HTMLDivElement>(null); |
77 | | - const [edges, setEdges] = useState<Edge[]>([]); |
78 | 68 |
|
79 | 69 | const { data: worlds } = useQuery<World[]>({ |
80 | 70 | queryKey: ['worlds'], |
@@ -110,66 +100,6 @@ export default function ConceptMapPage() { |
110 | 100 | return map; |
111 | 101 | }, [searchLower, layerOrder, concepts]); |
112 | 102 |
|
113 | | - // Visible concept IDs (for edge filtering) |
114 | | - const visibleConceptIds = useMemo(() => { |
115 | | - const ids = new Set<string>(); |
116 | | - for (const arr of conceptsByLayer.values()) { |
117 | | - for (const c of arr) ids.add(c.id); |
118 | | - } |
119 | | - return ids; |
120 | | - }, [conceptsByLayer]); |
121 | | - |
122 | | - // Compute edges between prerequisite concepts |
123 | | - const computeEdges = useCallback(() => { |
124 | | - const container = containerRef.current; |
125 | | - if (!container || concepts.length === 0) { setEdges([]); return; } |
126 | | - |
127 | | - const containerRect = container.getBoundingClientRect(); |
128 | | - const newEdges: Edge[] = []; |
129 | | - |
130 | | - for (const concept of concepts) { |
131 | | - if (!concept.prerequisites?.length) continue; |
132 | | - if (!visibleConceptIds.has(concept.id)) continue; |
133 | | - |
134 | | - const toEl = container.querySelector(`[data-concept-id="${concept.id}"]`); |
135 | | - if (!toEl) continue; |
136 | | - const toRect = toEl.getBoundingClientRect(); |
137 | | - const layerDef = layers[concept.layer]; |
138 | | - const color = layerDef?.color ?? '#888'; |
139 | | - |
140 | | - for (const preReqId of concept.prerequisites) { |
141 | | - if (!visibleConceptIds.has(preReqId)) continue; |
142 | | - const fromEl = container.querySelector(`[data-concept-id="${preReqId}"]`); |
143 | | - if (!fromEl) continue; |
144 | | - const fromRect = fromEl.getBoundingClientRect(); |
145 | | - |
146 | | - newEdges.push({ |
147 | | - fromX: fromRect.left + fromRect.width / 2 - containerRect.left, |
148 | | - fromY: fromRect.top + fromRect.height / 2 - containerRect.top, |
149 | | - toX: toRect.left + toRect.width / 2 - containerRect.left, |
150 | | - toY: toRect.top + toRect.height / 2 - containerRect.top, |
151 | | - color, |
152 | | - }); |
153 | | - } |
154 | | - } |
155 | | - |
156 | | - setEdges(newEdges); |
157 | | - }, [concepts, visibleConceptIds, layers]); |
158 | | - |
159 | | - useEffect(() => { |
160 | | - const timer = setTimeout(computeEdges, 100); |
161 | | - const container = containerRef.current; |
162 | | - let observer: ResizeObserver | undefined; |
163 | | - if (container) { |
164 | | - observer = new ResizeObserver(() => computeEdges()); |
165 | | - observer.observe(container); |
166 | | - } |
167 | | - return () => { |
168 | | - clearTimeout(timer); |
169 | | - observer?.disconnect(); |
170 | | - }; |
171 | | - }, [computeEdges]); |
172 | | - |
173 | 103 | // Suggested next concepts |
174 | 104 | const suggestedNext = useMemo(() => { |
175 | 105 | if (!worlds || worlds.length === 0) return []; |
@@ -201,14 +131,8 @@ export default function ConceptMapPage() { |
201 | 131 | }); |
202 | 132 | }, [worlds, concepts]); |
203 | 133 |
|
204 | | - // Unique edge colors for SVG markers |
205 | | - const edgeColors = useMemo(() => { |
206 | | - const colorSet = new Set(edges.map(e => e.color)); |
207 | | - return Array.from(colorSet); |
208 | | - }, [edges]); |
209 | | - |
210 | 134 | return ( |
211 | | - <Box minH="100vh" bg="dark.bg" p={{ base: 3, md: 5 }} ref={containerRef} position="relative"> |
| 135 | + <Box minH="100vh" bg="dark.bg" p={{ base: 3, md: 5 }} position="relative"> |
212 | 136 | {/* Header */} |
213 | 137 | <Flex align="center" justify="space-between" wrap="wrap" gap={3} mb={5}> |
214 | 138 | <Heading |
@@ -417,52 +341,6 @@ export default function ConceptMapPage() { |
417 | 341 | </Flex> |
418 | 342 | )} |
419 | 343 |
|
420 | | - {/* SVG overlay for prerequisite edges */} |
421 | | - {edges.length > 0 && ( |
422 | | - <svg |
423 | | - style={{ |
424 | | - position: 'absolute', |
425 | | - top: 0, |
426 | | - left: 0, |
427 | | - width: '100%', |
428 | | - height: '100%', |
429 | | - pointerEvents: 'none', |
430 | | - overflow: 'visible', |
431 | | - }} |
432 | | - data-testid="concept-edges-svg" |
433 | | - > |
434 | | - <defs> |
435 | | - {edgeColors.map((color) => ( |
436 | | - <marker |
437 | | - key={color} |
438 | | - id={`arrow-${color.replace('#', '')}`} |
439 | | - markerWidth={8} |
440 | | - markerHeight={6} |
441 | | - refX={8} |
442 | | - refY={3} |
443 | | - orient="auto" |
444 | | - > |
445 | | - <path d="M0,0 L8,3 L0,6 Z" fill={color} opacity={0.6} /> |
446 | | - </marker> |
447 | | - ))} |
448 | | - </defs> |
449 | | - {edges.map((edge, i) => { |
450 | | - const midY = (edge.fromY + edge.toY) / 2; |
451 | | - const d = `M ${edge.fromX},${edge.fromY} C ${edge.fromX},${midY} ${edge.toX},${midY} ${edge.toX},${edge.toY}`; |
452 | | - return ( |
453 | | - <path |
454 | | - key={i} |
455 | | - d={d} |
456 | | - fill="none" |
457 | | - stroke={edge.color} |
458 | | - strokeWidth={1.5} |
459 | | - opacity={0.35} |
460 | | - markerEnd={`url(#arrow-${edge.color.replace('#', '')})`} |
461 | | - /> |
462 | | - ); |
463 | | - })} |
464 | | - </svg> |
465 | | - )} |
466 | 344 | </Box> |
467 | 345 | ); |
468 | 346 | } |
0 commit comments