3.9 KiB
3.9 KiB
Taskpile
A task manager with force-directed graph visualization.
Architecture
- Frontend: Next.js 14 (App Router) + React 18 + Tailwind CSS 3 + TypeScript
- Backend: Rust (Axum 0.7) + SQLite (via SQLx)
- Graph:
react-force-graph-2dfor force-directed visualization
Project Structure
frontend/
src/app/page.tsx — Main page: tabs, panels, task state management
src/app/layout.tsx — Root layout
src/components/
GraphView.tsx — Force graph with node selection, drag-to-center
TaskList.tsx — Pending/completed task list with selection
TaskItem.tsx — Individual task card
AddTaskForm.tsx — New task form
TaskDetailPanel.tsx — Right panel: selected task details
UserPanel.tsx — Left panel: user profile (example data)
ForceGraphClient.tsx — ForceGraph2D ref wrapper for dynamic import
src/lib/
api.ts — API client (fetch wrappers)
types.ts — TypeScript interfaces
src/__tests__/
unit/ — Jest unit tests (API, TaskItem)
e2e/ — Jest integration tests (full user flows)
backend/
src/main.rs — Axum server on port 3001
src/models.rs — Task, GraphNode, GraphEdge structs
src/db.rs — SQLite pool + migrations
src/graph.rs — Deterministic edge generation (~30% pairs)
src/routes/tasks.rs — CRUD: GET/POST /api/tasks, PATCH/DELETE /api/tasks/:id
src/routes/graph.rs — GET /api/graph
tests/integration_test.rs — Axum integration tests
Running
# Backend (port 3001)
cd backend && cargo run
# Frontend (port 3003, proxies /api to backend)
cd frontend && npm run dev -- -p 3003
Port 3000 is used by Gitea on this machine — use 3003 for the frontend.
Testing
# Frontend tests
cd frontend && npx jest
# Backend tests
cd backend && cargo test
Key Design Decisions
- Task IDs are UUIDs (TEXT in SQLite, string from backend). Frontend
Task.idis typed asnumberbut actually receives strings — selection uses string comparison throughout. - Graph tab and task list are switched via tabs in the center area. Left panel (user info) and right panel (task details) are independently foldable.
- Selecting a task (from list or graph node click) triggers a 3-phase animation: (1) charge force jumps to -600 so other nodes repel outward immediately, (2) after 80ms the selected node slides to canvas center over 500ms with cubic ease-out, (3) charge restores to -120 and the graph stabilizes. The node stays pinned (
fx/fy) until a different task is selected. - Both views (task list and graph) are always mounted using
absolute inset-0with opacity/pointer-events toggle — neverhidden. This ensuresGraphViewalways has real canvas dimensions from page load, so the force simulation runs correctly in the background. ForceGraph2Dcanvas dimensions are driven by aResizeObserveron the container div. Canvas is only mounted after the first measurement to avoid the 300×300 default size.- Graph re-fits on tab switch (
isVisibleeffect) and on panel resize (dimensionseffect). When a node is selected,zoomToFitis suppressed to avoid fighting the pin animation. - Panel transitions are 150ms CSS opacity. When switching to graph tab with a pending node selection, animation is delayed 400ms to let the re-fit settle first.
API
All endpoints under /api:
| Method | Path | Description |
|---|---|---|
| GET | /tasks | List all tasks |
| POST | /tasks | Create task {title, description?} |
| PATCH | /tasks/:id | Update task {title?, description?, completed?} |
| DELETE | /tasks/:id | Delete task |
| GET | /graph | Get nodes + weighted edges |
Proxy
Do not use system proxy env vars when testing the app locally — curl --noproxy '*' or equivalent.