diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..bb1d8fc --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,53 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project + +P2P Poll App — a peer-to-peer polling application for the Evocracy democratic coding research experiment (https://demcode.evocracy.org/). + +### Goal +Build a simple P2P polling app where users can add poll options and vote. Data exchange works peer-to-peer between clients (no central server for data), though a signaling/sync server for connection establishment is acceptable. + +### Research Context +- **Phase 1 (2 weeks):** Submit individual code proposal +- **Phase 2:** Groups of 3 merge solutions into a working prototype +- **Phase 3:** Representatives iteratively merge until one final solution remains +- Code must be clean, explainable, and easy to merge with others' work +- Final code published open-source under MIT license +- Language: JavaScript/TypeScript + +## Tech Stack + +- **Runtime:** Bun +- **Framework:** Waku (minimal React framework with RSC support) +- **Styling:** Tailwind CSS v4 +- **P2P/Data sync:** Automerge with automerge-repo (CRDT-based) +- **Networking:** `automerge-repo-network-websocket` (WebSocketClientAdapter) + `automerge-repo-network-broadcastchannel` (cross-tab sync) +- **Storage:** `automerge-repo-storage-indexeddb` (client-side persistence) + +## Commands + +```bash +bun install # Install dependencies +bun run dev # Start dev server +bun run build # Production build +bun test # Run tests (Bun's built-in test runner) +``` + +## Architecture + +- Waku pages router: pages live in `src/pages/`, layouts in `_layout.tsx` +- Client components use `'use client'` directive +- Automerge Repo is initialized in a client-side provider component wrapping the app +- The shared CRDT document holds the poll state (title, options, votes) +- Peers sync via a lightweight WebSocket sync server (can use `automerge-repo` sync server or the public `wss://sync.automerge.org`) +- `BroadcastChannelNetworkAdapter` enables cross-tab sync +- `useDocument` hook from `@automerge/automerge-repo-react-hooks` for reactive document access +- Every Waku page/layout must export `getConfig` specifying render mode (`'static'` or `'dynamic'`) + +## Key Design Principles + +- Keep it simple and merge-friendly for Phase 2 group work +- Minimal file count, clear separation of concerns +- No over-engineering — this is a research experiment, not production software diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..07a8d7f --- /dev/null +++ b/PLAN.md @@ -0,0 +1,267 @@ +# P2P Poll App — Implementation Plan + +A simple peer-to-peer polling app where users can add options and vote, built with no central server (beyond a public relay for WebSocket sync). + +--- + +## 1. Project Setup + +```bash +# Scaffold a Waku project with Bun +bunx create-waku@latest . --template basic --package-manager bun + +# Install dependencies +bun add @automerge/automerge @automerge/automerge-repo @automerge/automerge-repo-react-hooks @automerge/automerge-repo-network-websocket @automerge/automerge-repo-network-broadcastchannel @automerge/automerge-repo-storage-indexeddb + +# Tailwind CSS v4 +bun add tailwindcss @tailwindcss/vite +``` + +Waku uses Vite under the hood, so add `@tailwindcss/vite` to the Vite plugin list in `waku.config.ts` (see file structure below). Import Tailwind in a global CSS file with `@import "tailwindcss"`. + +--- + +## 2. Data Model + +A single Automerge document represents one poll: + +```typescript +interface Poll { + title: string; + options: PollOption[]; +} + +interface PollOption { + id: string; // nanoid or crypto.randomUUID() + text: string; + votes: string[]; // list of peer IDs who voted for this option +} +``` + +- **votes as `string[]`** — storing peer IDs instead of a count lets us prevent double-voting and support vote retracting. The count is derived via `.length`. +- Each peer generates a stable random ID on first visit (stored in `localStorage`). + +--- + +## 3. File Structure + +``` +waku.config.ts — Waku + Vite config (Tailwind plugin) +src/ + pages/ + _layout.tsx — root layout (server component), imports Providers + global CSS + index.tsx — landing page: create new poll or join via URL + poll/ + [id].tsx — main poll view (resolved from Automerge document URL) + components/ + Providers.tsx — 'use client': initializes Automerge Repo + RepoContext.Provider + PollView.tsx — displays poll title, options, votes; contains AddOption form and VoteButtons + ConnectionStatus.tsx — shows sync/connection state + lib/ + repo.ts — creates and exports the Automerge Repo singleton + types.ts — Poll and PollOption type definitions + peer.ts — get-or-create local peer ID from localStorage + poll.ts — poll mutation logic (pure functions) + __tests__/ + poll.test.ts — tests for poll logic + styles/ + global.css — Tailwind import +``` + +**Total: ~12 files** (5 components/pages + 1 config + 4 lib modules + 1 test file + 1 CSS) + +--- + +## 4. Implementation Steps + +### Step 1 — Scaffold and configure + +- Run the setup commands from Section 1. +- Create `waku.config.ts`: + ```typescript + import { defineConfig } from 'waku/config'; + import tailwindcss from '@tailwindcss/vite'; + + export default defineConfig({ + vite: { + plugins: [tailwindcss()], + }, + }); + ``` +- Create `src/styles/global.css` with `@import "tailwindcss"`. +- Verify `bun run dev` starts without errors. + +### Step 2 — Define types, peer identity, and poll logic + +- Create `src/lib/types.ts` with the `Poll` and `PollOption` interfaces. +- Create `src/lib/peer.ts`: read `localStorage.getItem("peerId")`, or generate one with `crypto.randomUUID()` and persist it. +- Create `src/lib/poll.ts`: extract poll mutation logic into pure functions (`createPoll`, `addOption`, `vote`, `unvote`, `hasVoted`) so they are testable independently of Automerge and React. + +### Step 3 — Initialize Automerge Repo + +- Create `src/lib/repo.ts`: + ```typescript + import { Repo } from "@automerge/automerge-repo"; + import { WebSocketClientAdapter } from "@automerge/automerge-repo-network-websocket"; + import { BroadcastChannelNetworkAdapter } from "@automerge/automerge-repo-network-broadcastchannel"; + import { IndexedDBStorageAdapter } from "@automerge/automerge-repo-storage-indexeddb"; + + export const repo = new Repo({ + network: [ + new WebSocketClientAdapter("wss://sync.automerge.org"), + new BroadcastChannelNetworkAdapter(), + ], + storage: new IndexedDBStorageAdapter("p2p-poll"), + }); + ``` + +> **Sync server note:** `wss://sync.automerge.org` is a free public relay provided by the Automerge team. For production use or better reliability, you can run your own sync server using `@automerge/automerge-repo-network-websocket`'s server adapter with a simple Node/Bun WebSocket server. + +### Step 4 — Providers (client component) and root layout (server component) + +Waku layouts are **server components** — they cannot use browser APIs or React context directly. Instead, create a client component wrapper: + +- Create `src/components/Providers.tsx`: + ```typescript + 'use client'; + + import { RepoContext } from "@automerge/automerge-repo-react-hooks"; + import { repo } from "../lib/repo"; + + export const Providers = ({ children }: { children: React.ReactNode }) => { + return ( + + {children} + + ); + }; + ``` + +- Create `src/pages/_layout.tsx` (server component — no `'use client'`): + ```typescript + import '../styles/global.css'; + import { Providers } from '../components/Providers'; + + export default async function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); + } + ``` + +### Step 5 — Landing page (create / join) + +- `src/pages/index.tsx` (`'use client'`): + - "Create Poll" button: calls `repo.create()` to make a new Automerge doc, initializes it with a default title and empty options array, then navigates to `/poll/`. + - "Join Poll" input: paste a document ID or full URL, navigate to `/poll/`. + - Show a list of recently visited polls from `localStorage` for convenience. + +### Step 6 — Poll view page + +- `src/pages/poll/[id].tsx` (`'use client'`): + - Read `id` from the route segment prop (Waku passes dynamic segments as props automatically). + - Reconstruct the Automerge `DocumentUrl` from `id`. + - Use `useDocument(url)` from `@automerge/automerge-repo-react-hooks` to get `[doc, changeDoc]`. + - Wrap the document-dependent UI in `Loading poll...}>` — `useDocument` may suspend while the document is being fetched/synced. + - Render `` and ``. + +### Step 7 — PollView component + +- `src/components/PollView.tsx` (`'use client'`): + - Display poll title (editable inline). + - List each option with its text, vote count (`option.votes.length`), and a Vote/Unvote button. + - Vote button logic: + ```typescript + changeDoc(d => { + const opt = d.options.find(o => o.id === optionId); + if (!opt.votes.includes(peerId)) { + opt.votes.push(peerId); + } + }); + ``` + - Unvote: filter out the peer ID. + - "Add Option" form at the bottom: text input + submit button that pushes a new `PollOption` into `doc.options`. + - Show a shareable link/document ID so users can invite peers. + +### Step 8 — ConnectionStatus component + +- `src/components/ConnectionStatus.tsx` (`'use client'`): + - Simple indicator (green dot / grey dot) based on whether the WebSocket adapter is connected. + - Can use `repo.networkSubsystem.on("peer", ...)` to track connected peers. + +### Step 9 — Polish and test + +- Style with Tailwind: center layout, card-style poll, colored vote bars. +- Test with two browser tabs (BroadcastChannel sync). +- Test with two devices on the same network pointing at the sync server. +- Handle loading state (doc is `undefined` until synced — use `` or a null check). +- Handle invalid/missing document IDs gracefully. + +--- + +## 5. UI Components Summary + +| Component | Responsibility | +|---|---| +| `_layout.tsx` | Server-component root layout, imports Providers + global styles | +| `Providers.tsx` | Client component: Repo initialization, RepoContext provider | +| `index.tsx` | Create new poll, join existing poll by ID | +| `[id].tsx` | Load Automerge doc by URL, wrap in Suspense, pass to PollView | +| `PollView.tsx` | Render poll title, options list, vote buttons, add-option form, share link | +| `ConnectionStatus.tsx` | Show online/offline and peer count | + +--- + +## 6. P2P Setup Details + +**How peers connect:** + +1. Creator makes a new poll — Automerge generates a unique document URL (e.g., `automerge:2Xp9Q3...`). +2. The app extracts the document ID and puts it in the page URL: `https://localhost:3000/poll/2Xp9Q3...`. +3. Creator shares that URL with others (copy button in UI). +4. When another peer opens the URL, the app reconstructs the Automerge document URL and calls `useDocument(url)` — Automerge syncs the doc via the WebSocket relay automatically. +5. Cross-tab sync happens instantly via BroadcastChannel (no network needed). +6. IndexedDB ensures the poll persists across page reloads. + +**No server to deploy** — the public `wss://sync.automerge.org` relay handles document sync. For production, consider running your own sync server for reliability and control. + +--- + +## 8. Tests + +Tests use **Bun's built-in test runner** (`bun test`) — no extra test dependencies needed. Bun provides `describe`, `it`, and `expect` out of the box. + +Tests target the pure functions in `src/lib/poll.ts`, not React components. Test file: `src/lib/__tests__/poll.test.ts`. + +**Pure functions under test:** + +- `createPoll(title: string): Poll` +- `addOption(poll: Poll, text: string): Poll` +- `vote(poll: Poll, optionId: string, peerId: string): Poll` +- `unvote(poll: Poll, optionId: string, peerId: string): Poll` +- `hasVoted(poll: Poll, optionId: string, peerId: string): boolean` + +**Test cases (6 total):** + +- **Peer ID management** — `getPeerId()` generates a UUID and returns the same value on subsequent calls (persisted in localStorage) +- **Poll creation** — `createPoll("Lunch?")` returns a poll with the given title and an empty options array +- **Adding an option** — `addOption(poll, "Pizza")` appends an option with the given text, a generated ID, and an empty votes array +- **Voting** — `vote(poll, optionId, peerId)` adds the peer ID to that option's votes array +- **Double-vote prevention** — calling `vote` twice with the same peer ID and option does not duplicate the entry +- **Vote retracting** — `unvote(poll, optionId, peerId)` removes the peer ID from the votes array + +> Run with: `bun test` + +--- + +## 9. Nice-to-Haves (if time permits) + +- **Poll title editing** — inline editable title field (trivial with `changeDoc`). +- **Vote retracting** — toggle vote on/off (already supported by the data model). +- **Peer count display** — show how many peers are currently connected to this document. +- **Option reordering** — drag to reorder or sort by vote count. +- **Results visualization** — simple bar chart showing vote distribution using Tailwind width utilities. +- **Multiple polls** — list/manage multiple polls from the landing page. +- **Poll closing** — creator can lock a poll to prevent further votes. diff --git a/TASKS.md b/TASKS.md new file mode 100644 index 0000000..ce702d5 --- /dev/null +++ b/TASKS.md @@ -0,0 +1,49 @@ +# TASKS.md + +Implementation progress tracker for the P2P Poll App. + +## Setup +- [ ] Scaffold Waku project with `bunx create-waku@latest` +- [ ] Install Automerge dependencies +- [ ] Install Tailwind CSS v4 and `@tailwindcss/vite` +- [ ] Create `waku.config.ts` with Tailwind Vite plugin +- [ ] Create `src/styles/global.css` with Tailwind import +- [ ] Verify `bun run dev` starts without errors + +## Core Logic +- [ ] Create `src/lib/types.ts` with Poll and PollOption interfaces +- [ ] Create `src/lib/peer.ts` with localStorage-backed peer ID generation +- [ ] Implement `createPoll` in `src/lib/poll.ts` +- [ ] Implement `addOption` in `src/lib/poll.ts` +- [ ] Implement `vote` and `unvote` in `src/lib/poll.ts` +- [ ] Implement `hasVoted` in `src/lib/poll.ts` + +## Automerge Integration +- [ ] Create `src/lib/repo.ts` with Repo singleton (WebSocket, BroadcastChannel, IndexedDB) +- [ ] Create `src/components/Providers.tsx` with RepoContext.Provider + +## Pages & Routing +- [ ] Create `src/pages/_layout.tsx` (server component with Providers and global CSS) +- [ ] Create `src/pages/index.tsx` with create-poll and join-poll UI +- [ ] Create `src/pages/poll/[id].tsx` with useDocument and Suspense + +## UI Components +- [ ] Build `src/components/PollView.tsx` (title, options list, vote/unvote buttons) +- [ ] Add "Add Option" form to PollView +- [ ] Add shareable link / copy button to PollView +- [ ] Build `src/components/ConnectionStatus.tsx` + +## Tests +- [ ] Test: getPeerId generates and persists a UUID +- [ ] Test: createPoll returns poll with title and empty options +- [ ] Test: addOption appends option with text, id, and empty votes +- [ ] Test: vote adds peer ID to option's votes array +- [ ] Test: double-vote prevention (no duplicate entries) +- [ ] Test: unvote removes peer ID from votes array + +## Polish +- [ ] Tailwind styling (centered layout, card-style poll, vote bars) +- [ ] Loading states for document sync +- [ ] Error handling for invalid/missing document IDs +- [ ] Cross-tab sync testing (BroadcastChannel) +- [ ] Multi-device sync testing