Skip to content

Commit ce3ee6b

Browse files
committed
refactor: Slim /graph page onto the shared graph workspace
Rewrite the page as thin config (useGraphEditor + GraphCanvas + GraphMenu) and remove its per-page menu; BFS/DFS dispatch is the only page-specific logic left.
1 parent 10a21b4 commit ce3ee6b

2 files changed

Lines changed: 24 additions & 346 deletions

File tree

src/app/graph/menu.jsx

Lines changed: 0 additions & 95 deletions
This file was deleted.

src/app/graph/page.jsx

Lines changed: 24 additions & 251 deletions
Original file line numberDiff line numberDiff line change
@@ -1,263 +1,40 @@
11
"use client";
22

3-
import { useEffect, useRef, useState } from 'react';
4-
import {
5-
ReactFlow,
6-
ReactFlowProvider,
7-
Background,
8-
Controls,
9-
MarkerType,
10-
useNodesState,
11-
useEdgesState,
12-
useReactFlow,
13-
} from '@xyflow/react';
14-
import '@xyflow/react/dist/style.css';
3+
import { useState } from 'react';
4+
import { ReactFlowProvider } from '@xyflow/react';
155
import Navbar from '@/components/navbar';
16-
import {
17-
PRESETS,
18-
toFlow,
19-
adjacency,
20-
bfsActions,
21-
dfsActions,
22-
newNodeId,
23-
edgeId,
24-
} from '@/lib/algorithms/graph';
25-
import GraphNode from './graph-node';
26-
import FloatingEdge from './floating-edge';
27-
import Menu from './menu';
28-
29-
const nodeTypes = { graphNode: GraphNode };
30-
const edgeTypes = { floating: FloatingEdge };
31-
const ARROW = { type: MarkerType.ArrowClosed, color: '#64748b', width: 16, height: 16 };
32-
33-
const toDelay = (s) => 1100 - s * 10;
34-
35-
function initialGraph(presetIndex = 0) {
36-
const { nodes, edges } = toFlow(PRESETS[presetIndex]);
37-
if (nodes[0]) nodes[0] = { ...nodes[0], data: { ...nodes[0].data, role: 'start' } };
38-
return { nodes, edges, startId: nodes[0]?.id ?? null };
39-
}
40-
41-
const MODE_HINT = {
42-
idle: 'Click a node to select · N add node · E add edge',
43-
'add-node': 'Add node — click anywhere on the canvas',
44-
'add-edge': 'Add edge — click two nodes',
45-
};
6+
import { PRESETS, adjacency, bfsActions, dfsActions } from '@/lib/algorithms/graph';
7+
import { useGraphEditor } from '@/components/graph/use-graph-editor';
8+
import GraphCanvas from '@/components/graph/graph-canvas';
9+
import GraphMenu from '@/components/graph/graph-menu';
4610

4711
function GraphInner() {
48-
const [initial] = useState(() => initialGraph(0));
49-
const [nodes, setNodes, onNodesChange] = useNodesState(initial.nodes);
50-
const [edges, setEdges, onEdgesChange] = useEdgesState(initial.edges);
51-
const [directed, setDirected] = useState(false);
52-
const [mode, setMode] = useState('idle');
53-
const [isRunning, setIsRunning] = useState(false);
54-
55-
const { screenToFlowPosition } = useReactFlow();
56-
57-
// refs read inside async loop / global key handler
58-
const nodesRef = useRef(initial.nodes);
59-
const edgesRef = useRef(initial.edges);
60-
const isRunningRef = useRef(false);
61-
const speedRef = useRef(toDelay(50));
62-
const modeRef = useRef('idle');
63-
const pendingEdgeRef = useRef(null);
64-
const directedRef = useRef(false);
65-
const algorithmRef = useRef(0);
66-
const startIdRef = useRef(initial.startId);
67-
const finishIdRef = useRef(null);
68-
const labelRef = useRef(0); // monotonic label counter for user-added nodes
69-
70-
useEffect(() => { nodesRef.current = nodes; }, [nodes]);
71-
useEffect(() => { edgesRef.current = edges; }, [edges]);
72-
const setModeBoth = (m) => { modeRef.current = m; setMode(m); };
73-
74-
// --- visual action appliers ---
75-
const markNode = (id, state) =>
76-
setNodes((ns) => ns.map((n) => (n.id === id ? { ...n, data: { ...n.data, state } } : n)));
77-
const markEdge = (id, state, to) =>
78-
setEdges((es) => es.map((e) => (e.id === id ? { ...e, data: { ...e.data, state, travelTo: to ?? null } } : e)));
79-
const clearMarks = () => {
80-
setNodes((ns) => ns.map((n) => ({ ...n, data: { ...n.data, state: 'normal' } })));
81-
setEdges((es) => es.map((e) => ({ ...e, data: { ...e.data, state: 'normal', travelTo: null } })));
82-
};
83-
84-
const applyAction = (action) => {
85-
if (action.type === 'markNode') markNode(action.id, action.state);
86-
else if (action.type === 'markEdge') markEdge(action.id, action.state, action.to);
87-
else if (action.type === 'clear') clearMarks();
88-
};
89-
90-
const runActions = async (actions) => {
91-
if (!actions.length || isRunningRef.current) return;
92-
isRunningRef.current = true;
93-
setIsRunning(true);
94-
clearMarks();
95-
for (const action of actions) {
96-
applyAction(action);
97-
await sleep(speedRef.current);
98-
}
99-
isRunningRef.current = false;
100-
setIsRunning(false);
101-
};
102-
103-
const handleVisualize = () => {
104-
const adj = adjacency(edgesRef.current, directedRef.current);
105-
const planner = algorithmRef.current === 1 ? dfsActions : bfsActions;
106-
runActions(planner(adj, startIdRef.current, finishIdRef.current));
107-
};
108-
109-
// --- editing ---
110-
const addNodeAt = (position) => {
111-
// Compute id/label outside the updater: both have side effects and the
112-
// updater can run twice under React strict mode.
113-
const id = newNodeId();
114-
labelRef.current += 1;
115-
const label = String(labelRef.current);
116-
setNodes((ns) => [
117-
...ns,
118-
{ id, type: 'graphNode', position, data: { label, state: 'normal', role: null } },
119-
]);
120-
};
121-
122-
const addEdge = (a, b) => {
123-
if (a === b) return;
124-
const exists = edgesRef.current.some(
125-
(e) =>
126-
(e.source === a && e.target === b) ||
127-
(!directedRef.current && e.source === b && e.target === a),
128-
);
129-
if (exists) return;
130-
setEdges((es) => [
131-
...es,
132-
{
133-
id: edgeId(a, b),
134-
source: a,
135-
target: b,
136-
type: 'floating',
137-
data: { state: 'normal' },
138-
markerEnd: directedRef.current ? ARROW : undefined,
139-
},
140-
]);
141-
};
142-
143-
const setRole = (id, role) => {
144-
if (role === 'start') { startIdRef.current = id; if (finishIdRef.current === id) finishIdRef.current = null; }
145-
if (role === 'finish') { finishIdRef.current = id; if (startIdRef.current === id) startIdRef.current = null; }
146-
setNodes((ns) =>
147-
ns.map((n) => {
148-
if (n.id === id) return { ...n, data: { ...n.data, role } };
149-
if (n.data.role === role) return { ...n, data: { ...n.data, role: null } };
150-
return n;
151-
}),
152-
);
153-
};
154-
155-
const onPaneClick = (event) => {
156-
if (isRunningRef.current) return;
157-
if (modeRef.current === 'add-node') {
158-
addNodeAt(screenToFlowPosition({ x: event.clientX, y: event.clientY }));
159-
setModeBoth('idle');
160-
}
161-
pendingEdgeRef.current = null;
162-
};
163-
164-
const onNodeClick = (_event, node) => {
165-
if (isRunningRef.current) return;
166-
if (modeRef.current === 'add-edge') {
167-
if (pendingEdgeRef.current == null) {
168-
pendingEdgeRef.current = node.id;
169-
} else {
170-
addEdge(pendingEdgeRef.current, node.id);
171-
pendingEdgeRef.current = null;
172-
setModeBoth('idle');
173-
}
174-
}
175-
};
12+
const g = useGraphEditor({ initialPreset: PRESETS[0] });
13+
const [algo, setAlgo] = useState(0);
17614

177-
const onNodesDelete = (deleted) => {
178-
for (const n of deleted) {
179-
if (n.id === startIdRef.current) startIdRef.current = null;
180-
if (n.id === finishIdRef.current) finishIdRef.current = null;
181-
}
182-
};
183-
184-
// global keyboard shortcuts
185-
useEffect(() => {
186-
const onKey = (e) => {
187-
if (isRunningRef.current) return;
188-
const el = e.target;
189-
if (el && el.closest && el.closest('input,select,textarea,[role="combobox"],button')) return;
190-
191-
const selected = nodesRef.current.find((n) => n.selected);
192-
const k = e.key.toLowerCase();
193-
if (k === 'n') { setModeBoth('add-node'); pendingEdgeRef.current = null; }
194-
else if (k === 'e') { setModeBoth('add-edge'); pendingEdgeRef.current = null; }
195-
else if (k === 's' && selected) setRole(selected.id, 'start');
196-
else if (k === 'f' && selected) setRole(selected.id, 'finish');
197-
else if (e.key === 'Escape') { setModeBoth('idle'); pendingEdgeRef.current = null; }
198-
};
199-
window.addEventListener('keydown', onKey);
200-
return () => window.removeEventListener('keydown', onKey);
201-
// eslint-disable-next-line react-hooks/exhaustive-deps
202-
}, []);
203-
204-
// --- menu callbacks ---
205-
const onDirectedChange = (val) => {
206-
directedRef.current = val;
207-
setDirected(val);
208-
setEdges((es) => es.map((e) => ({ ...e, markerEnd: val ? ARROW : undefined })));
209-
};
210-
const onPresetChange = (idx) => {
211-
const g = initialGraph(idx);
212-
nodesRef.current = g.nodes; edgesRef.current = g.edges;
213-
startIdRef.current = g.startId; finishIdRef.current = null;
214-
labelRef.current = 0;
215-
setNodes(g.nodes); setEdges(g.edges);
216-
setModeBoth('idle'); pendingEdgeRef.current = null;
217-
};
218-
const onClear = () => {
219-
if (isRunningRef.current) return;
220-
nodesRef.current = []; edgesRef.current = [];
221-
startIdRef.current = null; finishIdRef.current = null;
222-
labelRef.current = 0;
223-
setNodes([]); setEdges([]);
224-
setModeBoth('idle'); pendingEdgeRef.current = null;
15+
const onVisualize = () => {
16+
const { edges, directed, startId, finishId } = g.getContext();
17+
const adj = adjacency(edges, directed);
18+
g.run(algo === 1 ? dfsActions(adj, startId, finishId) : bfsActions(adj, startId, finishId));
22519
};
22620

22721
return (
22822
<div className="flex flex-col h-screen">
22923
<Navbar />
23024
<div className="flex flex-1 overflow-hidden">
231-
<Menu
232-
disabled={isRunning}
233-
onDirectedChange={onDirectedChange}
234-
onAlgorithmChange={(a) => { algorithmRef.current = a; }}
235-
onPresetChange={onPresetChange}
236-
onSpeedChange={(s) => { speedRef.current = toDelay(s); }}
237-
onVisualize={handleVisualize}
238-
onClear={onClear}
25+
<GraphMenu
26+
title="Graph Traversal"
27+
algorithms={['BFS', 'DFS']}
28+
presets={PRESETS}
29+
disabled={g.isRunning}
30+
onAlgorithmChange={setAlgo}
31+
onDirectedChange={g.setDirected}
32+
onPresetChange={(i) => g.loadPreset(PRESETS[i])}
33+
onSpeedChange={g.setSpeed}
34+
onVisualize={onVisualize}
35+
onClear={g.clear}
23936
/>
240-
<div className="relative flex-1">
241-
<div className="absolute top-3 left-3 z-10 rounded-md bg-white/90 px-3 py-1.5 text-xs font-medium text-gray-700 shadow">
242-
{MODE_HINT[mode]}
243-
</div>
244-
<ReactFlow
245-
nodes={nodes}
246-
edges={edges}
247-
onNodesChange={onNodesChange}
248-
onEdgesChange={onEdgesChange}
249-
onNodesDelete={onNodesDelete}
250-
onPaneClick={onPaneClick}
251-
onNodeClick={onNodeClick}
252-
nodeTypes={nodeTypes}
253-
edgeTypes={edgeTypes}
254-
nodesConnectable={false}
255-
fitView
256-
>
257-
<Background />
258-
<Controls />
259-
</ReactFlow>
260-
</div>
37+
<GraphCanvas editor={g} />
26138
</div>
26239
</div>
26340
);
@@ -270,7 +47,3 @@ export default function Graph() {
27047
</ReactFlowProvider>
27148
);
27249
}
273-
274-
function sleep(ms) {
275-
return new Promise((resolve) => setTimeout(resolve, ms));
276-
}

0 commit comments

Comments
 (0)