89 lines
3.9 KiB
Markdown
89 lines
3.9 KiB
Markdown
# 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-2d` for 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
|
||
|
||
```bash
|
||
# 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
|
||
|
||
```bash
|
||
# 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.id` is typed as `number` but 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-0` with opacity/pointer-events toggle — never `hidden`. This ensures `GraphView` always has real canvas dimensions from page load, so the force simulation runs correctly in the background.
|
||
- `ForceGraph2D` canvas dimensions are driven by a `ResizeObserver` on the container div. Canvas is only mounted after the first measurement to avoid the 300×300 default size.
|
||
- Graph re-fits on tab switch (`isVisible` effect) and on panel resize (`dimensions` effect). When a node is selected, `zoomToFit` is 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.
|