Skip to content

Commit 19a31d7

Browse files
committed
refactor: Restyle recursion tree nodes and fix pathfinder visited cells
Recursion tree: replace double-border nodes with single border, drop shadow, and three-state coloring (amber=active, teal=partial, slate=completed). Soften edge/arrow colors to slate-500. Pathfinder: fix bug where visited cell colors disappeared when shortest path was animated (stale grid closure). Update REFACTOR.md to reflect completed tasks.
1 parent 18941d0 commit 19a31d7

6 files changed

Lines changed: 48 additions & 223 deletions

File tree

REFACTOR.md

Lines changed: 18 additions & 202 deletions
Original file line numberDiff line numberDiff line change
@@ -4,141 +4,21 @@ A prioritized list of refactoring tasks for the Algorithm Visualizer project. Ea
44

55
---
66

7-
## 1. Convert Class Components to Functional Components with Hooks
7+
## 1. ~~Convert Class Components to Functional Components with Hooks~~
88

9-
**Priority:** High
10-
**Effort:** Large (9 page components + ~30 child components)
11-
**Impact:** Eliminates stale state bugs, simplifies code, enables better React patterns
12-
13-
### Problem
14-
15-
All visualizer pages except Game of Life use class components. This causes subtle bugs — for example, calling `this.setState({ number })` and immediately reading `this.state.number` gives the old value because `setState` is async in class components.
16-
17-
### Files to convert (pages first, then children)
18-
19-
**Page components (10):**
20-
- `src/app/pathfinder/page.jsx``Pathfinder`
21-
- `src/app/sorting/page.jsx``Sort`
22-
- `src/app/recursive-sorting/page.jsx``RecursiveSort`
23-
- `src/app/n-queen/page.jsx``Queen`
24-
- `src/app/convex-hull/page.jsx``ConvexHull`
25-
- `src/app/prime-numbers/page.jsx``Seive`
26-
- `src/app/recursion-tree/page.jsx``Graph`
27-
- `src/app/turing-machine/page.jsx``TuringMachine`
28-
- `src/app/15-puzzle/page.jsx``Puzzle`
29-
- `src/app/binary-search/page.jsx``BinarySearch`
30-
31-
**Menu components (8):**
32-
- `src/app/convex-hull/menu.jsx`
33-
- `src/app/n-queen/menu.jsx`
34-
- `src/app/prime-numbers/menu.jsx`
35-
- `src/app/recursion-tree/menu.jsx`
36-
- `src/app/recursive-sorting/menu.jsx`
37-
- `src/app/sorting/menu.jsx`
38-
- `src/app/turing-machine/menu.jsx`
39-
- `src/app/pathfinder/menu.jsx`
40-
41-
**Child components (~20):**
42-
- `src/app/pathfinder/node.jsx`, `grid.jsx`
43-
- `src/app/n-queen/cell.jsx`, `cells.jsx`
44-
- `src/app/prime-numbers/cell.jsx`, `cells.jsx`, `spiral.jsx`
45-
- `src/app/convex-hull/canvas.jsx`, `timer.jsx`
46-
- `src/app/turing-machine/table.jsx`
47-
- `src/app/recursion-tree/canvasSVG.jsx`, `vertex.jsx`, `vertexOriginal.jsx`, `edge.jsx`, `details.jsx`
48-
- `src/app/recursive-sorting/rect.jsx`
49-
- `src/app/binary-search/entryPoint.jsx`, `guess.jsx`, `result.jsx`, `search.jsx`
50-
- `src/app/sorting/rects.jsx`
51-
52-
### How to convert
53-
54-
```jsx
55-
// BEFORE — class component
56-
class Queen extends Component {
57-
state = { board: [], number: 4, speed: 490, isRunning: false }
58-
59-
handleQueenChange = (number) => {
60-
this.setState({ number });
61-
const board = getBoard(this.state.number); // BUG: reads stale state
62-
this.setState({ board });
63-
}
64-
65-
startAlgo = async () => {
66-
this.setState({ isRunning: true });
67-
// animation loop reads this.state.speed
68-
await sleep(this.state.speed);
69-
}
70-
}
71-
72-
// AFTER — functional component
73-
function Queen() {
74-
const [board, setBoard] = useState([]);
75-
const [number, setNumber] = useState(4);
76-
const [speed, setSpeed] = useState(490);
77-
const [isRunning, setIsRunning] = useState(false);
78-
const speedRef = useRef(speed); // for reading inside async loops
79-
80-
useEffect(() => { speedRef.current = speed; }, [speed]);
81-
82-
const handleQueenChange = (num) => {
83-
setNumber(num);
84-
setBoard(getBoard(num)); // no stale state — uses the param directly
85-
}
86-
87-
const startAlgo = async () => {
88-
setIsRunning(true);
89-
// use ref inside async loops so speed changes take effect mid-animation
90-
await sleep(speedRef.current);
91-
}
92-
}
93-
```
94-
95-
### Key patterns for async animations
9+
**Status:** Done
9610

97-
- Use `useRef` for values read inside `async` loops (`speed`, `isRunning`) — state closures will be stale
98-
- Game of Life (`src/app/game-of-life/page.jsx`) is already converted and serves as a reference
99-
- Convert `componentDidMount` to `useEffect(() => { ... }, [])`
100-
- Convert `componentDidUpdate` to `useEffect` with dependency arrays
101-
102-
### Recommended order
11+
All visualizer pages and child components converted to functional components with hooks. Uses `useRef` for values read inside async animation loops (speed, isRunning, etc.) to avoid stale closures. The only remaining class component is `src/app/15-puzzle/page.jsx` (skipped intentionally).
10312

104-
Convert menu components first (they're simpler — mostly render-only), then child components, then page components last since they have the most logic.
13+
Unused files removed during conversion: `convex-hull/cnvas2.jsx`, `convex-hull/timer.jsx`, `recursion-tree/details.jsx`, `recursion-tree/vertexOriginal.jsx`.
10514

10615
---
10716

108-
## 2. Standardize Disabled Prop Naming
109-
110-
**Priority:** High
111-
**Effort:** Small
112-
**Impact:** Removes confusion, prevents bugs from mismatched prop names
113-
114-
### Problem
115-
116-
Three different prop names are used for the same concept:
117-
- `disable` — used in pathfinder, sorting, recursive-sorting, n-queen, recursion-tree, turing-machine
118-
- `isDisabled` — used in convex-hull, prime-numbers
119-
- `disabled` — standard HTML attribute name
120-
121-
The `CustomSlider` component even accepts **both** `disable` and `isDisabled` and reconciles them.
122-
123-
### Fix
124-
125-
Standardize everything to `disabled` (the HTML standard). Update:
17+
## 2. ~~Standardize Disabled Prop Naming~~
12618

127-
**Shared components:**
128-
- `src/components/custom-slider.jsx` — change `{ ..., disable, isDisabled }` to `{ ..., disabled }`
19+
**Status:** Done
12920

130-
**Page → Menu prop passing (change `disable=` and `isDisabled=` to `disabled=`):**
131-
- `src/app/pathfinder/page.jsx`
132-
- `src/app/sorting/page.jsx`
133-
- `src/app/recursive-sorting/page.jsx`
134-
- `src/app/n-queen/page.jsx`
135-
- `src/app/convex-hull/page.jsx`
136-
- `src/app/prime-numbers/page.jsx`
137-
- `src/app/recursion-tree/page.jsx`
138-
- `src/app/turing-machine/page.jsx`
139-
140-
**Menu components (change `this.props.disable` / `this.props.isDisabled` to `this.props.disabled`):**
141-
- All 8 menu files listed above
21+
All components now use `disabled` (the HTML standard). Removed `disable` and `isDisabled` variants from all pages, menus, and shared components. Added visual disabled state (opacity + pointer-events) to the Slider UI component.
14222

14323
---
14424

@@ -148,19 +28,7 @@ Standardize everything to `disabled` (the HTML standard). Update:
14828
**Effort:** Small
14929
**Impact:** Clean production output
15030

151-
### 18 active `console.log` statements to remove
152-
153-
- `src/app/convex-hull/page.jsx:27`
154-
- `src/app/convex-hull/cnvas2.jsx:16, 58, 60, 75`
155-
- `src/app/prime-numbers/page.jsx:106`
156-
- `src/app/sorting/page.jsx:112`
157-
- `src/app/recursion-tree/canvasSVG.jsx:24`
158-
- `src/app/game-of-life/page.jsx:55`
159-
- `src/app/recursive-sorting/page.jsx:27`
160-
- `src/app/recursion-tree/Tree.js:226, 241`
161-
- `src/app/binary-search/entryPoint.jsx:42`
162-
- `src/app/binary-search/custom-dual-slider.jsx:13`
163-
- `src/lib/algorithms/15puzzle.js:92`
31+
ESLint `no-console` rule is now configured as a warning. Most console.log statements were removed during the hooks migration. Remaining ones are flagged by the linter.
16432

16533
---
16634

@@ -330,65 +198,11 @@ Per-page metadata can also be added using Next.js `generateMetadata` or `metadat
330198

331199
---
332200

333-
## 10. Add ESLint
334-
335-
**Priority:** High
336-
**Effort:** Small
337-
**Impact:** Catches bugs automatically, enforces consistency, runs during `next build`
338-
339-
### Current state
340-
341-
The `package.json` has a stale `eslintConfig` block from Create React App, but ESLint is not installed. The `next build` output even warns: *"ESLint must be installed in order to run during builds"*.
342-
343-
### Setup
344-
345-
```bash
346-
npm install -D eslint eslint-config-next
347-
```
348-
349-
Then replace the CRA `eslintConfig` block in `package.json` with an `.eslintrc.json`:
350-
351-
```json
352-
{
353-
"extends": "next/core-web-vitals",
354-
"rules": {
355-
"no-console": "warn",
356-
"no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
357-
"react/no-unescaped-entities": "off"
358-
}
359-
}
360-
```
361-
362-
Also add a lint script to `package.json`:
363-
364-
```json
365-
"scripts": {
366-
"lint": "next lint"
367-
}
368-
```
369-
370-
### What this catches
371-
372-
- `no-console` — flags the 18 console.log statements as warnings
373-
- `no-unused-vars` — catches dead variables (several exist, e.g. destructured `row, col` in pathfinder node.jsx)
374-
- `next/core-web-vitals` includes:
375-
- `react-hooks/rules-of-hooks` — prevents hook misuse
376-
- `react-hooks/exhaustive-deps` — catches missing useEffect dependencies
377-
- `@next/next/no-img-element` — flags `<img>` tags that should use `next/image`
378-
- `@next/next/no-html-link-for-pages` — flags `<a>` tags that should use `next/link`
201+
## 10. ~~Add ESLint~~
379202

380-
### Clean up the stale CRA config
203+
**Status:** Done
381204

382-
Remove this block from `package.json`:
383-
384-
```json
385-
"eslintConfig": {
386-
"extends": [
387-
"react-app",
388-
"react-app/jest"
389-
]
390-
}
391-
```
205+
ESLint configured in `eslint.config.mjs` using eslint-config-next v16 native flat config. Rules: `no-console` (warn), `no-unused-vars` (warn, `_` prefix ignored), `react/no-unescaped-entities` (off). All lint errors resolved. Stale CRA `eslintConfig` removed from `package.json`. Run with `npm run lint`.
392206

393207
---
394208

@@ -420,12 +234,14 @@ Rename and type one visualizer at a time, starting with the simplest (Game of Li
420234

421235
## Quick Wins Checklist
422236

423-
- [ ] Install ESLint with `next/core-web-vitals` config
424-
- [ ] Remove stale CRA `eslintConfig` from `package.json`
425-
- [ ] Remove 18 `console.log` statements
426-
- [ ] Standardize `disabled` prop naming
237+
- [x] Install ESLint with `next/core-web-vitals` config
238+
- [x] Remove stale CRA `eslintConfig` from `package.json`
239+
- [x] Remove unused variables and dead code
240+
- [x] Standardize `disabled` prop naming
241+
- [x] Add `alt="Queen"` to queen cell image
242+
- [x] Convert class components to functional components with hooks
243+
- [ ] Remove remaining `console.log` statements (flagged by linter)
427244
- [ ] Remove commented-out code blocks
428245
- [ ] Fix hardcoded `/AlgorithmVisualizer/` image paths
429246
- [ ] Remove unused dependencies (`react-router`, `react-router-dom`, `fontsource-roboto`)
430-
- [ ] Add `alt="Queen"` to queen cell image
431247
- [ ] Add Open Graph metadata to `layout.js`

src/app/pathfinder/page.jsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,11 +134,10 @@ export default function Pathfinder() {
134134
await sleep(10);
135135
}
136136
await sleep(100);
137-
await animateShortestPath(nodesInShortestPathOrder);
137+
await animateShortestPath(nodesInShortestPathOrder, currentGrid);
138138
};
139139

140-
const animateShortestPath = async (nodesInShortestPathOrder) => {
141-
let currentGrid = grid.map(row => [...row]);
140+
const animateShortestPath = async (nodesInShortestPathOrder, currentGrid) => {
142141
for (let i = 0; i < nodesInShortestPathOrder.length; i++) {
143142
const node = nodesInShortestPathOrder[i];
144143
currentGrid[node.row][node.col] = { ...currentGrid[node.row][node.col], ispathNode: true };

src/app/recursion-tree/canvasSVG.jsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ export default function CanvasSvg({ vertices, edges, current, offset }) {
66
return (
77
<div>
88
<svg viewBox="0 0 240 150" xmlns="http://www.w3.org/2000/svg">
9+
<defs>
10+
<filter id="nodeShadow" x="-20%" y="-20%" width="140%" height="140%">
11+
<feDropShadow dx="0.3" dy="0.3" stdDeviation="0.5" floodOpacity="0.25" />
12+
</filter>
13+
<marker id="arrow" viewBox="0 0 10 10" refX="5" refY="5"
14+
markerWidth="4" markerHeight="4" orient="auto-start-reverse">
15+
<path d="M 0 0 L 10 5 L 0 10 z" fill="#64748b" />
16+
</marker>
17+
</defs>
918
{edges.map((edge, cellidx) => (
1019
<Edge
1120
key={cellidx}
@@ -21,6 +30,7 @@ export default function CanvasSvg({ vertices, edges, current, offset }) {
2130
key={cellidx}
2231
id={cellidx}
2332
current={current === cellidx}
33+
completed={vertex.completed}
2434
label={vertex.label}
2535
ret={vertex.val}
2636
pos={{

src/app/recursion-tree/edge.jsx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,9 @@ export default function Edge({ id, pos }) {
3232

3333
return (
3434
<g>
35-
<defs>
36-
<marker id="arrow" viewBox="0 0 10 10" refX="5" refY="5"
37-
markerWidth="4" markerHeight="4" orient="auto-start-reverse">
38-
<path d="M 0 0 L 10 5 L 0 10 z" />
39-
</marker>
40-
</defs>
4135
<line
4236
x2={getEndX()} y2={getEndY()} x1={pos.x1} y1={pos.y1}
43-
style={{ stroke: 'black', strokeWidth: '0.5' }}
37+
style={{ stroke: '#64748b', strokeWidth: '0.4' }}
4438
markerEnd="url(#arrow)"
4539
>
4640
<animate

src/app/recursion-tree/page.jsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ export default function Graph() {
4343
let newVertex;
4444
if (parent !== undefined) {
4545
newVertex = node.children.length
46-
? { label: node.tree.label, val: 0, x: node.x, y: node.y, px: parent.x, py: parent.y }
47-
: { label: node.tree.label, val: node.tree.node, x: node.x, y: node.y, px: parent.x, py: parent.y };
46+
? { label: node.tree.label, val: 0, x: node.x, y: node.y, px: parent.x, py: parent.y, completed: false }
47+
: { label: node.tree.label, val: node.tree.node, x: node.x, y: node.y, px: parent.x, py: parent.y, completed: false };
4848

4949
verts = [...verts, newVertex];
5050
verticesRef.current = verts;
@@ -56,8 +56,8 @@ export default function Graph() {
5656
setEdges([...edgesRef.current]);
5757
} else {
5858
newVertex = node.children.length
59-
? { label: node.tree.label, val: 0, x: node.x, y: node.y, px: node.x, py: node.y }
60-
: { label: node.tree.label, val: node.tree.node, x: node.x, y: node.y, px: node.x, py: node.y };
59+
? { label: node.tree.label, val: 0, x: node.x, y: node.y, px: node.x, py: node.y, completed: false }
60+
: { label: node.tree.label, val: node.tree.node, x: node.x, y: node.y, px: node.x, py: node.y, completed: false };
6161

6262
verts = [...verts, newVertex];
6363
verticesRef.current = verts;
@@ -73,7 +73,7 @@ export default function Graph() {
7373
}
7474

7575
let updatedVerts = [...verticesRef.current];
76-
updatedVerts[currentIdx] = { ...updatedVerts[currentIdx], val: node.tree.node };
76+
updatedVerts[currentIdx] = { ...updatedVerts[currentIdx], val: node.tree.node, completed: true };
7777
verticesRef.current = updatedVerts;
7878
setVertices([...updatedVerts]);
7979
};

src/app/recursion-tree/vertex.jsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,27 @@
11
import { useEffect } from 'react';
22

3-
export default function Vertex({ id, pos, current, label, ret }) {
3+
export default function Vertex({ id, pos, current, completed, label, ret }) {
44
useEffect(() => {
55
if (id === 0) return;
66
document.getElementById('cxanim' + id)?.beginElement();
77
document.getElementById('cyanim' + id)?.beginElement();
88
document.getElementById('tanim' + id)?.beginElement();
99
}, []);
1010

11+
const fillColor = current ? '#f59e0b' : completed ? '#334155' : '#0d9488';
12+
const strokeColor = current ? '#d97706' : completed ? '#475569' : '#0f766e';
13+
const textColor = '#f8fafc';
14+
1115
return (
1216
<g>
1317
<circle
1418
cx={pos.x}
1519
cy={pos.y}
1620
r={6}
17-
stroke="black"
18-
strokeWidth="0.5"
19-
fill={current ? 'cyan' : 'white'}
21+
stroke={strokeColor}
22+
strokeWidth="0.4"
23+
fill={fillColor}
24+
filter="url(#nodeShadow)"
2025
>
2126
<animate
2227
id={'cxanim' + id}
@@ -34,10 +39,11 @@ export default function Vertex({ id, pos, current, label, ret }) {
3439
/>
3540
</circle>
3641
<text
37-
style={{ font: '3px sans-serif' }}
42+
style={{ font: '2.8px sans-serif', fontWeight: 500 }}
3843
x={pos.x}
39-
y={pos.y - 4}
44+
y={pos.y - 3.5}
4045
textAnchor="middle"
46+
fill={textColor}
4147
>
4248
<animate
4349
id={'tanim' + id}

0 commit comments

Comments
 (0)