diff --git a/.gitignore b/.gitignore index 8b25bb5..10b99be 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ dist-ssr .vinxi __unconfig* todos.json +dudesky.db* \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..6861439 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,5 @@ +# Claude Instructions + +## Skill Mappings + +When working on TanStack-related tasks, read [AGENTS.md](AGENTS.md) and load the relevant SKILL.md files listed there before proceeding. diff --git a/HANDOFF.md b/HANDOFF.md new file mode 100644 index 0000000..11e15f9 --- /dev/null +++ b/HANDOFF.md @@ -0,0 +1,95 @@ +# Handoff: UI polish — feed, login, and shell + +## What this project is + +Dudesky is a Bluesky client built with TanStack Start (React, SSR, file-based routing). It uses ATProto OAuth to authenticate users via their Bluesky handle. Stack: TanStack Start + Vite, Tailwind CSS v4, `@atproto/oauth-client-node` for OAuth, `better-sqlite3` for persistence. + +## What was built last session + +### Database-backed OAuth stores (`src/lib/db.ts`, `src/lib/oauth-client.ts`) + +Replaced the in-memory `stateStore` / `sessionStore` with SQLite via `better-sqlite3`. DB file is `dudesky.db` in the project root (path overridable via `DB_PATH` env var). Two tables: + +- `oauth_state` — short-lived PKCE/DPoP state, expires after 10 minutes (TTL enforced on each `set`) +- `oauth_session` — long-lived token store keyed by DID + +Both stores serialize ATProto state objects as JSON text. + +### DID cookie (`src/routes/callback.tsx`) + +After `client.callback()` succeeds, sets a `did` cookie via a mutable `new Response(null, { headers: { 'Set-Cookie': ..., 'Location': ... } })`. **Do not use `Response.redirect()` here** — it creates a response with immutable headers, which crashes TanStack Start's `mergeEventResponseHeaders` when it tries to attach queued cookies. The DID is not sensitive on its own; actual tokens live in `sessionStore`. + +### Feed loader (`src/routes/feed.tsx`) + +Uses `createServerFn` (not a raw loader) for all server-only work — `getCookie`, `client.restore()`, DB access via the OAuth client, and `agent.getTimeline()`. This is required because TanStack Start loaders are isomorphic (run on both client and server); `better-sqlite3` is Node-only and will crash in the browser if imported directly in a loader. + +The server function returns a mapped subset of the feed data (not raw ATProto types) to avoid a type incompatibility with `createServerFn`'s serialization constraints (`{ [x: string]: unknown }` vs `{ [x: string]: {} }`). + +## Current state of key files + +``` +src/lib/db.ts ← SQLite setup, creates oauth_state + oauth_session tables +src/lib/oauth-client.ts ← NodeOAuthClient with DB-backed stateStore + sessionStore +src/routes/callback.tsx ← sets DID cookie, redirects to /feed +src/routes/feed.tsx ← createServerFn fetches timeline; basic card UI (needs polish) +src/routes/login.tsx ← plain unstyled form; working, needs polish +src/routes/__root.tsx ← shell with Header + Footer components, devtools, theme init script +src/components/Header.tsx ← sticky nav with "Feed" link chip; uses design tokens +src/components/Footer.tsx ← copyright + "Built with TanStack Start" line +src/styles.css ← Tailwind v4 + custom design system (see below) +``` + +## What needs to be done: UI cleanup + +The auth flow and data loading all work end-to-end. The feed renders but looks rough. The next session is purely visual polish — no logic changes needed. + +### Design system already in place + +`src/styles.css` has a full set of CSS custom properties and utility classes to use: + +**Color tokens** (light + dark + `prefers-color-scheme` variants): +- `--sea-ink` / `--sea-ink-soft` — primary text colors +- `--lagoon` / `--lagoon-deep` — teal accent +- `--palm` — green accent +- `--sand` / `--foam` / `--bg-base` — background layers +- `--surface` / `--surface-strong` — card/panel backgrounds +- `--line` — borders +- `--inset-glint` — inner highlight on cards +- `--header-bg` / `--chip-bg` / `--chip-line` — header/chip-specific + +**Utility classes**: +- `.page-wrap` — centered container, `min(1080px, calc(100% - 2rem))` +- `.island-shell` — frosted glass card style (border + gradient bg + box-shadow + backdrop-blur) +- `.island-kicker` — small-caps label style +- `.nav-link` — link with animated underline +- `.display-title` — Fraunces serif font +- `.rise-in` — entrance animation (opacity + translateY, 700ms) +- `.feature-card` — card with hover lift + +**Fonts**: Manrope (sans body), Fraunces (serif display) + +### Specific things to polish + +**`src/routes/feed.tsx` — the main job:** +- Post cards are bare `border rounded-lg` — should use `.island-shell` and the design token colors +- Avatar images have no fallback for missing avatars +- No timestamp on posts (it's available: `(item.post.record as { createdAt?: string }).createdAt`) +- The feed data shape currently mapped in `getTimeline` only extracts `uri`, `text`, `author.{handle,displayName,avatar}` — if you need `createdAt` or other fields, add them to the mapped return object in the server function +- No loading/pending state (`pendingComponent` on the route) +- No error state (`errorComponent` on the route) + +**`src/routes/login.tsx` — unstyled:** +- The form (``, `