Skip to content

Commit 77e532e

Browse files
authored
Merge pull request #6 from kayYZ1/dev
Merge changes from dev
2 parents 2b79490 + ed27ab9 commit 77e532e

File tree

6 files changed

+849
-79
lines changed

6 files changed

+849
-79
lines changed

apps/web/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"class-variance-authority": "^0.7.1",
3030
"clsx": "^2.1.1",
3131
"cmdk": "1.0.0",
32+
"d3": "^7.9.0",
3233
"dexie": "^4.0.11",
3334
"dexie-react-hooks": "^1.1.7",
3435
"framer-motion": "^12.4.2",
@@ -51,6 +52,7 @@
5152
},
5253
"devDependencies": {
5354
"@eslint/js": "^9.19.0",
55+
"@types/d3": "^7.4.3",
5456
"@types/node": "^22.13.1",
5557
"@types/react": "^19.0.8",
5658
"@types/react-dom": "^19.0.3",

apps/web/src/components/command-search.tsx

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Cloud, Keyboard, Settings, StickyNote } from "lucide-react";
1+
import { Cloud, GitBranch, Keyboard, Settings, StickyNote } from "lucide-react";
22
import { useNavigate } from "react-router";
33
import { useLiveQuery } from "dexie-react-hooks";
44

@@ -44,7 +44,11 @@ export default function CommandSearch({
4444
))}
4545
</CommandGroup>
4646
<CommandSeparator />
47-
<CommandGroup heading="Settings">
47+
<CommandGroup heading="Other">
48+
<CommandItem onSelect={() => navigate("/graph")}>
49+
<GitBranch className="mr-2 h-4 w-4" />
50+
<span>Graph view</span>
51+
</CommandItem>
4852
<CommandItem onSelect={() => navigate("/settings")}>
4953
<Settings className="mr-2 h-4 w-4" />
5054
<span>Settings</span>
+237-9
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,242 @@
1-
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
1+
import { useEffect, useRef } from "react";
2+
import * as d3 from "d3";
3+
import { useLiveQuery } from "dexie-react-hooks";
4+
5+
import { db } from "@/lib/db";
6+
import { Folder, Note } from "@/lib/interfaces";
7+
import { useThemeToggle } from "@/shared/hooks/use-theme";
8+
9+
interface Node extends d3.SimulationNodeDatum {
10+
id: string;
11+
title: string;
12+
type: "folder" | "note";
13+
}
14+
15+
interface Link extends d3.SimulationLinkDatum<Node> {
16+
source: string;
17+
target: string;
18+
}
219

320
export default function GraphView() {
21+
const svgRef = useRef<SVGSVGElement>(null);
22+
const containerRef = useRef<HTMLDivElement>(null);
23+
const { theme } = useThemeToggle();
24+
25+
const folders = useLiveQuery(() => db.folders.toArray());
26+
const notes = useLiveQuery(() => db.notes.toArray());
27+
28+
useEffect(() => {
29+
if (folders && notes) {
30+
createGraph(notes, folders);
31+
}
32+
}, [folders, notes, theme]);
33+
34+
if (!notes) {
35+
return;
36+
}
37+
38+
const nodes: Node[] = [];
39+
const links: Link[] = [];
40+
41+
const createGraph = (notes: Note[], folders?: Folder[]) => {
42+
if (!svgRef.current || !containerRef.current) {
43+
return;
44+
}
45+
46+
folders?.forEach((folder) => {
47+
nodes.push({
48+
id: folder.id,
49+
title: folder.name,
50+
type: "folder",
51+
});
52+
});
53+
54+
notes.forEach((note) => {
55+
nodes.push({
56+
id: note.id,
57+
title: note.title,
58+
type: "note",
59+
});
60+
61+
folders?.forEach((folder) => {
62+
const containsNote = folder.notes.some(
63+
(folderNote) => folderNote.id === note.id,
64+
);
65+
if (containsNote) {
66+
links.push({
67+
source: folder.id,
68+
target: note.id,
69+
});
70+
}
71+
});
72+
});
73+
74+
// Theme-based colors
75+
const isDarkTheme = theme === "dark";
76+
77+
const colors = {
78+
textColor: isDarkTheme ? "#e5e7eb" : "#1f2937",
79+
linkColor: isDarkTheme ? "#9ca3af" : "#6b7280",
80+
folderColor: isDarkTheme ? "#6b7280" : "#4b5563",
81+
noteColor: isDarkTheme ? "#d1d5db" : "#9ca3af",
82+
};
83+
84+
// Clear any existing elements
85+
d3.select(svgRef.current).selectAll("*").remove();
86+
87+
// Get the fixed dimensions
88+
const width = containerRef.current.clientWidth;
89+
const height = containerRef.current.clientHeight;
90+
91+
// Set SVG dimensions
92+
d3.select(svgRef.current).attr("width", width).attr("height", height);
93+
94+
// Create the SVG container with zoom capability
95+
const svg = d3
96+
.select(svgRef.current)
97+
.attr("viewBox", [0, 0, width, height])
98+
.call(
99+
d3
100+
.zoom<SVGSVGElement, unknown>()
101+
.extent([
102+
[0, 0],
103+
[width, height],
104+
])
105+
.scaleExtent([0.5, 3])
106+
.on("zoom", (event) => {
107+
container.attr("transform", event.transform);
108+
}),
109+
);
110+
111+
// Create a container for the graph
112+
const container = svg.append("g");
113+
114+
// Node sizes (increased)
115+
const folderSize = 16;
116+
const noteRadius = 12;
117+
118+
// Create the force simulation with modified parameters
119+
const simulation = d3
120+
.forceSimulation<Node>()
121+
// Reduced link distance to bring nodes closer
122+
.force(
123+
"link",
124+
d3
125+
.forceLink<Node, Link>()
126+
.id((d) => d.id)
127+
.distance(50),
128+
)
129+
.force("charge", d3.forceManyBody().strength(-100))
130+
.force("center", d3.forceCenter(width / 2, height / 2))
131+
// Adjusted collision radius to match node sizes
132+
.force("collision", d3.forceCollide().radius(noteRadius * 1.5));
133+
134+
// Add the links
135+
const link = container
136+
.append("g")
137+
.selectAll("line")
138+
.data(links)
139+
.join("line")
140+
.attr("stroke", colors.linkColor)
141+
.attr("stroke-width", 1.5) // Slightly thicker lines
142+
.attr("stroke-opacity", 0.6);
143+
144+
// Add the nodes
145+
const node = container
146+
.append("g")
147+
.selectAll("g")
148+
.data(nodes)
149+
.join("g")
150+
.call((g) => {
151+
g.filter((d) => d.type === "folder")
152+
.append("rect")
153+
.attr("x", -folderSize)
154+
.attr("y", -folderSize)
155+
.attr("width", folderSize * 2)
156+
.attr("height", folderSize * 2)
157+
.attr("rx", 4)
158+
.attr("fill", colors.folderColor);
159+
160+
g.filter((d) => d.type === "note")
161+
.append("circle")
162+
.attr("r", noteRadius)
163+
.attr("fill", colors.noteColor)
164+
.attr("stroke", colors.folderColor)
165+
.attr("stroke-width", 1.5);
166+
167+
g.append("text")
168+
.text((d) => d.title)
169+
.attr("x", (d) =>
170+
d.type === "folder" ? folderSize + 4 : noteRadius + 4,
171+
)
172+
.attr("y", 4)
173+
.attr("font-size", "12px") // Slightly larger text
174+
.attr("fill", colors.textColor); // Use theme-aware text color
175+
});
176+
177+
// Update simulation
178+
simulation.nodes(nodes);
179+
(simulation.force("link") as d3.ForceLink<Node, Link>).links(links);
180+
181+
// Update positions on tick
182+
simulation.on("tick", () => {
183+
link
184+
.attr("x1", (d) => {
185+
const source =
186+
typeof d.source === "string"
187+
? nodes.find((n) => n.id === d.source)
188+
: d.source;
189+
return source?.x || 0;
190+
})
191+
.attr("y1", (d) => {
192+
const source =
193+
typeof d.source === "string"
194+
? nodes.find((n) => n.id === d.source)
195+
: d.source;
196+
return source?.y || 0;
197+
})
198+
.attr("x2", (d) => {
199+
const target =
200+
typeof d.target === "string"
201+
? nodes.find((n) => n.id === d.target)
202+
: d.target;
203+
return target?.x || 0;
204+
})
205+
.attr("y2", (d) => {
206+
const target =
207+
typeof d.target === "string"
208+
? nodes.find((n) => n.id === d.target)
209+
: d.target;
210+
return target?.y || 0;
211+
});
212+
213+
node.attr("transform", (d) => `translate(${d.x || 0},${d.y || 0})`);
214+
});
215+
};
216+
217+
const isDarkTheme = theme === "dark";
218+
const dotsOnBackground = {
219+
backgroundImage: `radial-gradient(circle, ${
220+
isDarkTheme ? "rgba(255, 255, 255, 0.05)" : "rgba(0, 0, 0, 0.1)"
221+
} 1px, transparent 1px)`,
222+
backgroundSize: "16px 16px",
223+
};
224+
4225
return (
5-
<Card>
6-
<CardHeader>
7-
<CardTitle>Graph View</CardTitle>
8-
</CardHeader>
9-
<CardContent>
10-
<p>Graph view coming soon!</p>
11-
</CardContent>
12-
</Card>
226+
<div ref={containerRef} className="w-full max-w-none overflow-hidden">
227+
<div className="flex justify-between items-center mb-4">
228+
<h1 className="text-2xl font-bold">Graph View</h1>
229+
</div>
230+
<div
231+
className="w-full h-[700px] rounded-lg shadow-md border-1"
232+
style={dotsOnBackground}
233+
>
234+
<svg
235+
ref={svgRef}
236+
className="w-full h-full"
237+
style={{ cursor: "grab" }}
238+
/>
239+
</div>
240+
</div>
13241
);
14242
}

apps/web/src/index.css

-1
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,6 @@ body {
114114
height: var(--radix-accordion-content-height);
115115
}
116116
}
117-
118117
@keyframes accordion-up {
119118
from {
120119
height: var(--radix-accordion-content-height);

0 commit comments

Comments
 (0)