experimental bluesky client
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Get feeds loading

+672 -119
+1
.gitignore
··· 11 11 .vinxi 12 12 __unconfig* 13 13 todos.json 14 + dudesky.db*
+5
CLAUDE.md
··· 1 + # Claude Instructions 2 + 3 + ## Skill Mappings 4 + 5 + When working on TanStack-related tasks, read [AGENTS.md](AGENTS.md) and load the relevant SKILL.md files listed there before proceeding.
+95
HANDOFF.md
··· 1 + # Handoff: UI polish — feed, login, and shell 2 + 3 + ## What this project is 4 + 5 + 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. 6 + 7 + ## What was built last session 8 + 9 + ### Database-backed OAuth stores (`src/lib/db.ts`, `src/lib/oauth-client.ts`) 10 + 11 + 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: 12 + 13 + - `oauth_state` — short-lived PKCE/DPoP state, expires after 10 minutes (TTL enforced on each `set`) 14 + - `oauth_session` — long-lived token store keyed by DID 15 + 16 + Both stores serialize ATProto state objects as JSON text. 17 + 18 + ### DID cookie (`src/routes/callback.tsx`) 19 + 20 + 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`. 21 + 22 + ### Feed loader (`src/routes/feed.tsx`) 23 + 24 + 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. 25 + 26 + 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]: {} }`). 27 + 28 + ## Current state of key files 29 + 30 + ``` 31 + src/lib/db.ts ← SQLite setup, creates oauth_state + oauth_session tables 32 + src/lib/oauth-client.ts ← NodeOAuthClient with DB-backed stateStore + sessionStore 33 + src/routes/callback.tsx ← sets DID cookie, redirects to /feed 34 + src/routes/feed.tsx ← createServerFn fetches timeline; basic card UI (needs polish) 35 + src/routes/login.tsx ← plain unstyled form; working, needs polish 36 + src/routes/__root.tsx ← shell with Header + Footer components, devtools, theme init script 37 + src/components/Header.tsx ← sticky nav with "Feed" link chip; uses design tokens 38 + src/components/Footer.tsx ← copyright + "Built with TanStack Start" line 39 + src/styles.css ← Tailwind v4 + custom design system (see below) 40 + ``` 41 + 42 + ## What needs to be done: UI cleanup 43 + 44 + 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. 45 + 46 + ### Design system already in place 47 + 48 + `src/styles.css` has a full set of CSS custom properties and utility classes to use: 49 + 50 + **Color tokens** (light + dark + `prefers-color-scheme` variants): 51 + - `--sea-ink` / `--sea-ink-soft` — primary text colors 52 + - `--lagoon` / `--lagoon-deep` — teal accent 53 + - `--palm` — green accent 54 + - `--sand` / `--foam` / `--bg-base` — background layers 55 + - `--surface` / `--surface-strong` — card/panel backgrounds 56 + - `--line` — borders 57 + - `--inset-glint` — inner highlight on cards 58 + - `--header-bg` / `--chip-bg` / `--chip-line` — header/chip-specific 59 + 60 + **Utility classes**: 61 + - `.page-wrap` — centered container, `min(1080px, calc(100% - 2rem))` 62 + - `.island-shell` — frosted glass card style (border + gradient bg + box-shadow + backdrop-blur) 63 + - `.island-kicker` — small-caps label style 64 + - `.nav-link` — link with animated underline 65 + - `.display-title` — Fraunces serif font 66 + - `.rise-in` — entrance animation (opacity + translateY, 700ms) 67 + - `.feature-card` — card with hover lift 68 + 69 + **Fonts**: Manrope (sans body), Fraunces (serif display) 70 + 71 + ### Specific things to polish 72 + 73 + **`src/routes/feed.tsx` — the main job:** 74 + - Post cards are bare `border rounded-lg` — should use `.island-shell` and the design token colors 75 + - Avatar images have no fallback for missing avatars 76 + - No timestamp on posts (it's available: `(item.post.record as { createdAt?: string }).createdAt`) 77 + - 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 78 + - No loading/pending state (`pendingComponent` on the route) 79 + - No error state (`errorComponent` on the route) 80 + 81 + **`src/routes/login.tsx` — unstyled:** 82 + - The form (`<input>`, `<button>`) has zero styling 83 + - Should match the app's aesthetic — centered card layout, `.island-shell`, lagoon accent on the button 84 + 85 + **`src/routes/__root.tsx` — minor:** 86 + - Page title is still "TanStack Start Starter" — should be "Dudesky" or similar 87 + 88 + ## Conventions 89 + 90 + - Path alias `#/` maps to `src/` 91 + - No semicolons in `.ts`/`.tsx` files 92 + - Server route handlers return raw `Response` objects — use `new Response(null, { status: 302, headers: {...} })` for redirects, NOT `Response.redirect()` (immutable headers) and NOT TanStack Router's `redirect()` (doesn't work in server handlers) 93 + - `createServerFn` for any server-only code called from loaders (DB, cookies, secrets) 94 + - Env vars: `VITE_APP_URL`, `PRIVATE_KEY_0/1/2`, optionally `DB_PATH` 95 + - Read AGENTS.md and load the relevant SKILL.md files before working on TanStack-related tasks
+449 -1
package-lock.json
··· 15 15 "@tanstack/react-router-ssr-query": "latest", 16 16 "@tanstack/react-start": "latest", 17 17 "@tanstack/router-plugin": "^1.132.0", 18 + "better-sqlite3": "^12.8.0", 18 19 "lucide-react": "^0.545.0", 19 20 "react": "^19.2.0", 20 21 "react-dom": "^19.2.0", ··· 25 26 "@tanstack/devtools-vite": "latest", 26 27 "@testing-library/dom": "^10.4.1", 27 28 "@testing-library/react": "^16.3.0", 29 + "@types/better-sqlite3": "^7.6.13", 28 30 "@types/node": "^22.10.2", 29 31 "@types/react": "^19.2.0", 30 32 "@types/react-dom": "^19.2.0", ··· 2870 2872 "@babel/types": "^7.28.2" 2871 2873 } 2872 2874 }, 2875 + "node_modules/@types/better-sqlite3": { 2876 + "version": "7.6.13", 2877 + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", 2878 + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", 2879 + "dev": true, 2880 + "license": "MIT", 2881 + "dependencies": { 2882 + "@types/node": "*" 2883 + } 2884 + }, 2873 2885 "node_modules/@types/chai": { 2874 2886 "version": "5.2.3", 2875 2887 "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", ··· 3200 3212 "@babel/types": "^7.23.6" 3201 3213 } 3202 3214 }, 3215 + "node_modules/base64-js": { 3216 + "version": "1.5.1", 3217 + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 3218 + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", 3219 + "funding": [ 3220 + { 3221 + "type": "github", 3222 + "url": "https://github.com/sponsors/feross" 3223 + }, 3224 + { 3225 + "type": "patreon", 3226 + "url": "https://www.patreon.com/feross" 3227 + }, 3228 + { 3229 + "type": "consulting", 3230 + "url": "https://feross.org/support" 3231 + } 3232 + ], 3233 + "license": "MIT" 3234 + }, 3203 3235 "node_modules/baseline-browser-mapping": { 3204 3236 "version": "2.10.16", 3205 3237 "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.16.tgz", ··· 3212 3244 "node": ">=6.0.0" 3213 3245 } 3214 3246 }, 3247 + "node_modules/better-sqlite3": { 3248 + "version": "12.8.0", 3249 + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.8.0.tgz", 3250 + "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", 3251 + "hasInstallScript": true, 3252 + "license": "MIT", 3253 + "dependencies": { 3254 + "bindings": "^1.5.0", 3255 + "prebuild-install": "^7.1.1" 3256 + }, 3257 + "engines": { 3258 + "node": "20.x || 22.x || 23.x || 24.x || 25.x" 3259 + } 3260 + }, 3215 3261 "node_modules/bidi-js": { 3216 3262 "version": "1.0.3", 3217 3263 "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", ··· 3234 3280 "url": "https://github.com/sponsors/sindresorhus" 3235 3281 } 3236 3282 }, 3283 + "node_modules/bindings": { 3284 + "version": "1.5.0", 3285 + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", 3286 + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", 3287 + "license": "MIT", 3288 + "dependencies": { 3289 + "file-uri-to-path": "1.0.0" 3290 + } 3291 + }, 3292 + "node_modules/bl": { 3293 + "version": "4.1.0", 3294 + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", 3295 + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", 3296 + "license": "MIT", 3297 + "dependencies": { 3298 + "buffer": "^5.5.0", 3299 + "inherits": "^2.0.4", 3300 + "readable-stream": "^3.4.0" 3301 + } 3302 + }, 3237 3303 "node_modules/boolbase": { 3238 3304 "version": "1.0.0", 3239 3305 "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", ··· 3285 3351 "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" 3286 3352 } 3287 3353 }, 3354 + "node_modules/buffer": { 3355 + "version": "5.7.1", 3356 + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", 3357 + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", 3358 + "funding": [ 3359 + { 3360 + "type": "github", 3361 + "url": "https://github.com/sponsors/feross" 3362 + }, 3363 + { 3364 + "type": "patreon", 3365 + "url": "https://www.patreon.com/feross" 3366 + }, 3367 + { 3368 + "type": "consulting", 3369 + "url": "https://feross.org/support" 3370 + } 3371 + ], 3372 + "license": "MIT", 3373 + "dependencies": { 3374 + "base64-js": "^1.3.1", 3375 + "ieee754": "^1.1.13" 3376 + } 3377 + }, 3288 3378 "node_modules/cac": { 3289 3379 "version": "6.7.14", 3290 3380 "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", ··· 3420 3510 "optionalDependencies": { 3421 3511 "fsevents": "~2.3.2" 3422 3512 } 3513 + }, 3514 + "node_modules/chownr": { 3515 + "version": "1.1.4", 3516 + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", 3517 + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", 3518 + "license": "ISC" 3423 3519 }, 3424 3520 "node_modules/clsx": { 3425 3521 "version": "2.1.1", ··· 3594 3690 "dev": true, 3595 3691 "license": "MIT" 3596 3692 }, 3693 + "node_modules/decompress-response": { 3694 + "version": "6.0.0", 3695 + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", 3696 + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", 3697 + "license": "MIT", 3698 + "dependencies": { 3699 + "mimic-response": "^3.1.0" 3700 + }, 3701 + "engines": { 3702 + "node": ">=10" 3703 + }, 3704 + "funding": { 3705 + "url": "https://github.com/sponsors/sindresorhus" 3706 + } 3707 + }, 3597 3708 "node_modules/deep-eql": { 3598 3709 "version": "5.0.2", 3599 3710 "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", ··· 3604 3715 "node": ">=6" 3605 3716 } 3606 3717 }, 3718 + "node_modules/deep-extend": { 3719 + "version": "0.6.0", 3720 + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", 3721 + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", 3722 + "license": "MIT", 3723 + "engines": { 3724 + "node": ">=4.0.0" 3725 + } 3726 + }, 3607 3727 "node_modules/dequal": { 3608 3728 "version": "2.0.3", 3609 3729 "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", ··· 3713 3833 "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" 3714 3834 } 3715 3835 }, 3836 + "node_modules/end-of-stream": { 3837 + "version": "1.4.5", 3838 + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", 3839 + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", 3840 + "license": "MIT", 3841 + "dependencies": { 3842 + "once": "^1.4.0" 3843 + } 3844 + }, 3716 3845 "node_modules/enhanced-resolve": { 3717 3846 "version": "5.20.1", 3718 3847 "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", ··· 3818 3947 "@types/estree": "^1.0.0" 3819 3948 } 3820 3949 }, 3950 + "node_modules/expand-template": { 3951 + "version": "2.0.3", 3952 + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", 3953 + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", 3954 + "license": "(MIT OR WTFPL)", 3955 + "engines": { 3956 + "node": ">=6" 3957 + } 3958 + }, 3821 3959 "node_modules/expect-type": { 3822 3960 "version": "1.3.0", 3823 3961 "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", ··· 3851 3989 } 3852 3990 } 3853 3991 }, 3992 + "node_modules/file-uri-to-path": { 3993 + "version": "1.0.0", 3994 + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", 3995 + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", 3996 + "license": "MIT" 3997 + }, 3854 3998 "node_modules/fill-range": { 3855 3999 "version": "7.1.1", 3856 4000 "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", ··· 3862 4006 "engines": { 3863 4007 "node": ">=8" 3864 4008 } 4009 + }, 4010 + "node_modules/fs-constants": { 4011 + "version": "1.0.0", 4012 + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", 4013 + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", 4014 + "license": "MIT" 3865 4015 }, 3866 4016 "node_modules/fsevents": { 3867 4017 "version": "2.3.3", ··· 3897 4047 "funding": { 3898 4048 "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" 3899 4049 } 4050 + }, 4051 + "node_modules/github-from-package": { 4052 + "version": "0.0.0", 4053 + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", 4054 + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", 4055 + "license": "MIT" 3900 4056 }, 3901 4057 "node_modules/glob-parent": { 3902 4058 "version": "5.1.2", ··· 4041 4197 "node": ">=0.10.0" 4042 4198 } 4043 4199 }, 4200 + "node_modules/ieee754": { 4201 + "version": "1.2.1", 4202 + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 4203 + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", 4204 + "funding": [ 4205 + { 4206 + "type": "github", 4207 + "url": "https://github.com/sponsors/feross" 4208 + }, 4209 + { 4210 + "type": "patreon", 4211 + "url": "https://www.patreon.com/feross" 4212 + }, 4213 + { 4214 + "type": "consulting", 4215 + "url": "https://feross.org/support" 4216 + } 4217 + ], 4218 + "license": "BSD-3-Clause" 4219 + }, 4220 + "node_modules/inherits": { 4221 + "version": "2.0.4", 4222 + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 4223 + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 4224 + "license": "ISC" 4225 + }, 4226 + "node_modules/ini": { 4227 + "version": "1.3.8", 4228 + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", 4229 + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", 4230 + "license": "ISC" 4231 + }, 4044 4232 "node_modules/ipaddr.js": { 4045 4233 "version": "2.3.0", 4046 4234 "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", ··· 4574 4762 "dev": true, 4575 4763 "license": "CC0-1.0" 4576 4764 }, 4765 + "node_modules/mimic-response": { 4766 + "version": "3.1.0", 4767 + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", 4768 + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", 4769 + "license": "MIT", 4770 + "engines": { 4771 + "node": ">=10" 4772 + }, 4773 + "funding": { 4774 + "url": "https://github.com/sponsors/sindresorhus" 4775 + } 4776 + }, 4777 + "node_modules/minimist": { 4778 + "version": "1.2.8", 4779 + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", 4780 + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", 4781 + "license": "MIT", 4782 + "funding": { 4783 + "url": "https://github.com/sponsors/ljharb" 4784 + } 4785 + }, 4786 + "node_modules/mkdirp-classic": { 4787 + "version": "0.5.3", 4788 + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", 4789 + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", 4790 + "license": "MIT" 4791 + }, 4577 4792 "node_modules/ms": { 4578 4793 "version": "2.1.3", 4579 4794 "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", ··· 4604 4819 "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 4605 4820 } 4606 4821 }, 4822 + "node_modules/napi-build-utils": { 4823 + "version": "2.0.0", 4824 + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", 4825 + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", 4826 + "license": "MIT" 4827 + }, 4828 + "node_modules/node-abi": { 4829 + "version": "3.89.0", 4830 + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", 4831 + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", 4832 + "license": "MIT", 4833 + "dependencies": { 4834 + "semver": "^7.3.5" 4835 + }, 4836 + "engines": { 4837 + "node": ">=10" 4838 + } 4839 + }, 4840 + "node_modules/node-abi/node_modules/semver": { 4841 + "version": "7.7.4", 4842 + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", 4843 + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", 4844 + "license": "ISC", 4845 + "bin": { 4846 + "semver": "bin/semver.js" 4847 + }, 4848 + "engines": { 4849 + "node": ">=10" 4850 + } 4851 + }, 4607 4852 "node_modules/node-releases": { 4608 4853 "version": "2.0.37", 4609 4854 "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", ··· 4629 4874 }, 4630 4875 "funding": { 4631 4876 "url": "https://github.com/fb55/nth-check?sponsor=1" 4877 + } 4878 + }, 4879 + "node_modules/once": { 4880 + "version": "1.4.0", 4881 + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 4882 + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 4883 + "license": "ISC", 4884 + "dependencies": { 4885 + "wrappy": "1" 4632 4886 } 4633 4887 }, 4634 4888 "node_modules/parse5": { ··· 4756 5010 "node": ">=4" 4757 5011 } 4758 5012 }, 5013 + "node_modules/prebuild-install": { 5014 + "version": "7.1.3", 5015 + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", 5016 + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", 5017 + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", 5018 + "license": "MIT", 5019 + "dependencies": { 5020 + "detect-libc": "^2.0.0", 5021 + "expand-template": "^2.0.3", 5022 + "github-from-package": "0.0.0", 5023 + "minimist": "^1.2.3", 5024 + "mkdirp-classic": "^0.5.3", 5025 + "napi-build-utils": "^2.0.0", 5026 + "node-abi": "^3.3.0", 5027 + "pump": "^3.0.0", 5028 + "rc": "^1.2.7", 5029 + "simple-get": "^4.0.0", 5030 + "tar-fs": "^2.0.0", 5031 + "tunnel-agent": "^0.6.0" 5032 + }, 5033 + "bin": { 5034 + "prebuild-install": "bin.js" 5035 + }, 5036 + "engines": { 5037 + "node": ">=10" 5038 + } 5039 + }, 4759 5040 "node_modules/prettier": { 4760 5041 "version": "3.8.1", 4761 5042 "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", ··· 4786 5067 "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" 4787 5068 } 4788 5069 }, 5070 + "node_modules/pump": { 5071 + "version": "3.0.4", 5072 + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", 5073 + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", 5074 + "license": "MIT", 5075 + "dependencies": { 5076 + "end-of-stream": "^1.1.0", 5077 + "once": "^1.3.1" 5078 + } 5079 + }, 4789 5080 "node_modules/punycode": { 4790 5081 "version": "2.3.1", 4791 5082 "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", ··· 4796 5087 "node": ">=6" 4797 5088 } 4798 5089 }, 5090 + "node_modules/rc": { 5091 + "version": "1.2.8", 5092 + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", 5093 + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", 5094 + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", 5095 + "dependencies": { 5096 + "deep-extend": "^0.6.0", 5097 + "ini": "~1.3.0", 5098 + "minimist": "^1.2.0", 5099 + "strip-json-comments": "~2.0.1" 5100 + }, 5101 + "bin": { 5102 + "rc": "cli.js" 5103 + } 5104 + }, 4799 5105 "node_modules/react": { 4800 5106 "version": "19.2.5", 4801 5107 "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", ··· 4832 5138 "license": "MIT", 4833 5139 "engines": { 4834 5140 "node": ">=0.10.0" 5141 + } 5142 + }, 5143 + "node_modules/readable-stream": { 5144 + "version": "3.6.2", 5145 + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", 5146 + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", 5147 + "license": "MIT", 5148 + "dependencies": { 5149 + "inherits": "^2.0.3", 5150 + "string_decoder": "^1.1.1", 5151 + "util-deprecate": "^1.0.1" 5152 + }, 5153 + "engines": { 5154 + "node": ">= 6" 4835 5155 } 4836 5156 }, 4837 5157 "node_modules/readdirp": { ··· 4952 5272 "integrity": "sha512-ePa+XGk00/3HuCqrEnK3LxJW7I0SdNg6EFzKUJG73hMAdDcOUC/i/aSz7LSDwLrGr33kal/rqOGydzwl6U7zBA==", 4953 5273 "license": "MIT" 4954 5274 }, 5275 + "node_modules/safe-buffer": { 5276 + "version": "5.2.1", 5277 + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 5278 + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 5279 + "funding": [ 5280 + { 5281 + "type": "github", 5282 + "url": "https://github.com/sponsors/feross" 5283 + }, 5284 + { 5285 + "type": "patreon", 5286 + "url": "https://www.patreon.com/feross" 5287 + }, 5288 + { 5289 + "type": "consulting", 5290 + "url": "https://feross.org/support" 5291 + } 5292 + ], 5293 + "license": "MIT" 5294 + }, 4955 5295 "node_modules/safer-buffer": { 4956 5296 "version": "2.1.2", 4957 5297 "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", ··· 5027 5367 "dev": true, 5028 5368 "license": "ISC" 5029 5369 }, 5370 + "node_modules/simple-concat": { 5371 + "version": "1.0.1", 5372 + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", 5373 + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", 5374 + "funding": [ 5375 + { 5376 + "type": "github", 5377 + "url": "https://github.com/sponsors/feross" 5378 + }, 5379 + { 5380 + "type": "patreon", 5381 + "url": "https://www.patreon.com/feross" 5382 + }, 5383 + { 5384 + "type": "consulting", 5385 + "url": "https://feross.org/support" 5386 + } 5387 + ], 5388 + "license": "MIT" 5389 + }, 5390 + "node_modules/simple-get": { 5391 + "version": "4.0.1", 5392 + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", 5393 + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", 5394 + "funding": [ 5395 + { 5396 + "type": "github", 5397 + "url": "https://github.com/sponsors/feross" 5398 + }, 5399 + { 5400 + "type": "patreon", 5401 + "url": "https://www.patreon.com/feross" 5402 + }, 5403 + { 5404 + "type": "consulting", 5405 + "url": "https://feross.org/support" 5406 + } 5407 + ], 5408 + "license": "MIT", 5409 + "dependencies": { 5410 + "decompress-response": "^6.0.0", 5411 + "once": "^1.3.1", 5412 + "simple-concat": "^1.0.0" 5413 + } 5414 + }, 5030 5415 "node_modules/solid-js": { 5031 5416 "version": "1.9.12", 5032 5417 "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.12.tgz", ··· 5082 5467 "dev": true, 5083 5468 "license": "MIT" 5084 5469 }, 5470 + "node_modules/string_decoder": { 5471 + "version": "1.3.0", 5472 + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 5473 + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 5474 + "license": "MIT", 5475 + "dependencies": { 5476 + "safe-buffer": "~5.2.0" 5477 + } 5478 + }, 5479 + "node_modules/strip-json-comments": { 5480 + "version": "2.0.1", 5481 + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", 5482 + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", 5483 + "license": "MIT", 5484 + "engines": { 5485 + "node": ">=0.10.0" 5486 + } 5487 + }, 5085 5488 "node_modules/strip-literal": { 5086 5489 "version": "3.1.0", 5087 5490 "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", ··· 5128 5531 "url": "https://opencollective.com/webpack" 5129 5532 } 5130 5533 }, 5534 + "node_modules/tar-fs": { 5535 + "version": "2.1.4", 5536 + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", 5537 + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", 5538 + "license": "MIT", 5539 + "dependencies": { 5540 + "chownr": "^1.1.1", 5541 + "mkdirp-classic": "^0.5.2", 5542 + "pump": "^3.0.0", 5543 + "tar-stream": "^2.1.4" 5544 + } 5545 + }, 5546 + "node_modules/tar-stream": { 5547 + "version": "2.2.0", 5548 + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", 5549 + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", 5550 + "license": "MIT", 5551 + "dependencies": { 5552 + "bl": "^4.0.3", 5553 + "end-of-stream": "^1.4.1", 5554 + "fs-constants": "^1.0.0", 5555 + "inherits": "^2.0.3", 5556 + "readable-stream": "^3.1.1" 5557 + }, 5558 + "engines": { 5559 + "node": ">=6" 5560 + } 5561 + }, 5131 5562 "node_modules/tiny-invariant": { 5132 5563 "version": "1.3.3", 5133 5564 "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", ··· 5307 5738 "fsevents": "~2.3.3" 5308 5739 } 5309 5740 }, 5741 + "node_modules/tunnel-agent": { 5742 + "version": "0.6.0", 5743 + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 5744 + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", 5745 + "license": "Apache-2.0", 5746 + "dependencies": { 5747 + "safe-buffer": "^5.0.1" 5748 + }, 5749 + "engines": { 5750 + "node": "*" 5751 + } 5752 + }, 5310 5753 "node_modules/typescript": { 5311 5754 "version": "5.9.3", 5312 5755 "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", ··· 5416 5859 "version": "1.0.2", 5417 5860 "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 5418 5861 "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", 5419 - "dev": true, 5420 5862 "license": "MIT" 5421 5863 }, 5422 5864 "node_modules/vite": { ··· 5710 6152 "engines": { 5711 6153 "node": ">=8" 5712 6154 } 6155 + }, 6156 + "node_modules/wrappy": { 6157 + "version": "1.0.2", 6158 + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 6159 + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", 6160 + "license": "ISC" 5713 6161 }, 5714 6162 "node_modules/ws": { 5715 6163 "version": "8.20.0",
+3
package.json
··· 21 21 "@tanstack/react-router-ssr-query": "latest", 22 22 "@tanstack/react-start": "latest", 23 23 "@tanstack/router-plugin": "^1.132.0", 24 + "better-sqlite3": "^12.8.0", 24 25 "lucide-react": "^0.545.0", 25 26 "react": "^19.2.0", 26 27 "react-dom": "^19.2.0", ··· 31 32 "@tanstack/devtools-vite": "latest", 32 33 "@testing-library/dom": "^10.4.1", 33 34 "@testing-library/react": "^16.3.0", 35 + "@types/better-sqlite3": "^7.6.13", 34 36 "@types/node": "^22.10.2", 35 37 "@types/react": "^19.2.0", 36 38 "@types/react-dom": "^19.2.0", ··· 43 45 }, 44 46 "pnpm": { 45 47 "onlyBuiltDependencies": [ 48 + "better-sqlite3", 46 49 "esbuild", 47 50 "lightningcss" 48 51 ]
+1 -31
src/components/Footer.tsx
··· 5 5 <footer className="mt-20 border-t border-[var(--line)] px-4 pb-14 pt-10 text-[var(--sea-ink-soft)]"> 6 6 <div className="page-wrap flex flex-col items-center justify-between gap-4 text-center sm:flex-row sm:text-left"> 7 7 <p className="m-0 text-sm"> 8 - &copy; {year} Your name here. All rights reserved. 8 + &copy; {year} dude.computer. All rights reserved. 9 9 </p> 10 10 <p className="island-kicker m-0">Built with TanStack Start</p> 11 - </div> 12 - <div className="mt-4 flex justify-center gap-4"> 13 - <a 14 - href="https://x.com/tan_stack" 15 - target="_blank" 16 - rel="noreferrer" 17 - className="rounded-xl p-2 text-[var(--sea-ink-soft)] transition hover:bg-[var(--link-bg-hover)] hover:text-[var(--sea-ink)]" 18 - > 19 - <span className="sr-only">Follow TanStack on X</span> 20 - <svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32"> 21 - <path 22 - fill="currentColor" 23 - d="M12.6 1h2.2L10 6.48 15.64 15h-4.41L7.78 9.82 3.23 15H1l5.14-5.84L.72 1h4.52l3.12 4.73L12.6 1zm-.77 12.67h1.22L4.57 2.26H3.26l8.57 11.41z" 24 - /> 25 - </svg> 26 - </a> 27 - <a 28 - href="https://github.com/TanStack" 29 - target="_blank" 30 - rel="noreferrer" 31 - className="rounded-xl p-2 text-[var(--sea-ink-soft)] transition hover:bg-[var(--link-bg-hover)] hover:text-[var(--sea-ink)]" 32 - > 33 - <span className="sr-only">Go to TanStack GitHub</span> 34 - <svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32"> 35 - <path 36 - fill="currentColor" 37 - d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z" 38 - /> 39 - </svg> 40 - </a> 41 11 </div> 42 12 </footer> 43 13 )
+2 -61
src/components/Header.tsx
··· 1 1 import { Link } from '@tanstack/react-router' 2 - import ThemeToggle from './ThemeToggle' 3 2 4 3 export default function Header() { 5 4 return ( ··· 7 6 <nav className="page-wrap flex flex-wrap items-center gap-x-3 gap-y-2 py-3 sm:py-4"> 8 7 <h2 className="m-0 flex-shrink-0 text-base font-semibold tracking-tight"> 9 8 <Link 10 - to="/" 9 + to="/feed" 11 10 className="inline-flex items-center gap-2 rounded-full border border-[var(--chip-line)] bg-[var(--chip-bg)] px-3 py-1.5 text-sm text-[var(--sea-ink)] no-underline shadow-[0_8px_24px_rgba(30,90,72,0.08)] sm:px-4 sm:py-2" 12 11 > 13 12 <span className="h-2 w-2 rounded-full bg-[linear-gradient(90deg,#56c6be,#7ed3bf)]" /> 14 - TanStack Start 13 + Feed 15 14 </Link> 16 15 </h2> 17 - 18 - <div className="ml-auto flex items-center gap-1.5 sm:ml-0 sm:gap-2"> 19 - <a 20 - href="https://x.com/tan_stack" 21 - target="_blank" 22 - rel="noreferrer" 23 - className="hidden rounded-xl p-2 text-[var(--sea-ink-soft)] transition hover:bg-[var(--link-bg-hover)] hover:text-[var(--sea-ink)] sm:block" 24 - > 25 - <span className="sr-only">Follow TanStack on X</span> 26 - <svg viewBox="0 0 16 16" aria-hidden="true" width="24" height="24"> 27 - <path 28 - fill="currentColor" 29 - d="M12.6 1h2.2L10 6.48 15.64 15h-4.41L7.78 9.82 3.23 15H1l5.14-5.84L.72 1h4.52l3.12 4.73L12.6 1zm-.77 12.67h1.22L4.57 2.26H3.26l8.57 11.41z" 30 - /> 31 - </svg> 32 - </a> 33 - <a 34 - href="https://github.com/TanStack" 35 - target="_blank" 36 - rel="noreferrer" 37 - className="hidden rounded-xl p-2 text-[var(--sea-ink-soft)] transition hover:bg-[var(--link-bg-hover)] hover:text-[var(--sea-ink)] sm:block" 38 - > 39 - <span className="sr-only">Go to TanStack GitHub</span> 40 - <svg viewBox="0 0 16 16" aria-hidden="true" width="24" height="24"> 41 - <path 42 - fill="currentColor" 43 - d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z" 44 - /> 45 - </svg> 46 - </a> 47 - 48 - <ThemeToggle /> 49 - </div> 50 - 51 - <div className="order-3 flex w-full flex-wrap items-center gap-x-4 gap-y-1 pb-1 text-sm font-semibold sm:order-2 sm:w-auto sm:flex-nowrap sm:pb-0"> 52 - <Link 53 - to="/" 54 - className="nav-link" 55 - activeProps={{ className: 'nav-link is-active' }} 56 - > 57 - Home 58 - </Link> 59 - <Link 60 - to="/about" 61 - className="nav-link" 62 - activeProps={{ className: 'nav-link is-active' }} 63 - > 64 - About 65 - </Link> 66 - <a 67 - href="https://tanstack.com/start/latest/docs/framework/react/overview" 68 - className="nav-link" 69 - target="_blank" 70 - rel="noreferrer" 71 - > 72 - Docs 73 - </a> 74 - </div> 75 16 </nav> 76 17 </header> 77 18 )
+25
src/lib/db.ts
··· 1 + import Database from 'better-sqlite3' 2 + import path from 'node:path' 3 + 4 + const DB_PATH = process.env.DB_PATH ?? path.join(process.cwd(), 'dudesky.db') 5 + 6 + const db = new Database(DB_PATH) 7 + 8 + db.pragma('journal_mode = WAL') 9 + db.pragma('foreign_keys = ON') 10 + 11 + db.exec(` 12 + CREATE TABLE IF NOT EXISTS oauth_state ( 13 + key TEXT PRIMARY KEY, 14 + value TEXT NOT NULL, 15 + created_at INTEGER NOT NULL DEFAULT (unixepoch()) 16 + ); 17 + 18 + CREATE TABLE IF NOT EXISTS oauth_session ( 19 + did TEXT PRIMARY KEY, 20 + value TEXT NOT NULL, 21 + updated_at INTEGER NOT NULL DEFAULT (unixepoch()) 22 + ); 23 + `) 24 + 25 + export { db }
+32 -20
src/lib/oauth-client.ts
··· 1 1 import { JoseKey, NodeOAuthClient, type NodeSavedState, type NodeSavedSession } from '@atproto/oauth-client-node' 2 - 3 - interface SessionStore { 4 - [index: string]: NodeSavedSession 5 - } 6 - 7 - interface StateStore { 8 - [index: string]: NodeSavedState 9 - } 2 + import { db } from '#/lib/db' 10 3 11 - const sessionStore: SessionStore = {} 12 - const stateStore: StateStore = {} 4 + // OAuth PKCE/DPoP state is only needed for the ~10 minutes between /login and 5 + // /callback, so we prune anything older than 10 minutes on each set. 6 + const STATE_TTL_SECONDS = 600 13 7 14 8 const rootUrl = process.env.VITE_APP_URL 15 9 ··· 35 29 ]), 36 30 37 31 stateStore: { 38 - async set(key: string, internalState: NodeSavedState): Promise<void> { 39 - stateStore[key] = internalState 32 + async set(key: string, value: NodeSavedState): Promise<void> { 33 + db.prepare(` 34 + INSERT INTO oauth_state (key, value, created_at) 35 + VALUES (?, ?, unixepoch()) 36 + ON CONFLICT(key) DO UPDATE SET value = excluded.value, created_at = excluded.created_at 37 + `).run(key, JSON.stringify(value)) 38 + 39 + // Prune expired state entries 40 + db.prepare(`DELETE FROM oauth_state WHERE created_at < unixepoch() - ?`) 41 + .run(STATE_TTL_SECONDS) 40 42 }, 41 43 async get(key: string): Promise<NodeSavedState | undefined> { 42 - return stateStore[key] 44 + const row = db.prepare(` 45 + SELECT value FROM oauth_state 46 + WHERE key = ? AND created_at >= unixepoch() - ? 47 + `).get(key, STATE_TTL_SECONDS) as { value: string } | undefined 48 + return row ? JSON.parse(row.value) : undefined 43 49 }, 44 50 async del(key: string): Promise<void> { 45 - delete stateStore[key] 51 + db.prepare(`DELETE FROM oauth_state WHERE key = ?`).run(key) 46 52 }, 47 53 }, 48 54 49 55 sessionStore: { 50 - async set(sub: string, session: NodeSavedSession): Promise<void> { 51 - sessionStore[sub] = session 56 + async set(did: string, value: NodeSavedSession): Promise<void> { 57 + db.prepare(` 58 + INSERT INTO oauth_session (did, value, updated_at) 59 + VALUES (?, ?, unixepoch()) 60 + ON CONFLICT(did) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at 61 + `).run(did, JSON.stringify(value)) 52 62 }, 53 - async get(sub: string): Promise<NodeSavedSession | undefined> { 54 - return sessionStore[sub] 63 + async get(did: string): Promise<NodeSavedSession | undefined> { 64 + const row = db.prepare(`SELECT value FROM oauth_session WHERE did = ?`) 65 + .get(did) as { value: string } | undefined 66 + return row ? JSON.parse(row.value) : undefined 55 67 }, 56 - async del(sub: string): Promise<void> { 57 - delete sessionStore[sub] 68 + async del(did: string): Promise<void> { 69 + db.prepare(`DELETE FROM oauth_session WHERE did = ?`).run(did) 58 70 }, 59 71 }, 60 72 })
+8 -2
src/routes/callback.tsx
··· 8 8 try { 9 9 const params = new URL(request.url).searchParams 10 10 const { session } = await client.callback(params) 11 - // session.sub is the user's DID 12 11 console.log('[/callback] authenticated:', session.sub) 13 - return Response.redirect(new URL('/feed', request.url).toString(), 302) 12 + 13 + return new Response(null, { 14 + status: 302, 15 + headers: { 16 + 'Location': new URL('/feed', request.url).toString(), 17 + 'Set-Cookie': `did=${encodeURIComponent(session.sub)}; Path=/; HttpOnly; SameSite=Lax`, 18 + }, 19 + }) 14 20 } catch (e) { 15 21 console.error('[/callback]', e) 16 22 return new Response(String(e), { status: 500 })
+51 -4
src/routes/feed.tsx
··· 1 - import { createFileRoute } from '@tanstack/react-router' 1 + import { createFileRoute, redirect } from '@tanstack/react-router' 2 + import { createServerFn } from '@tanstack/react-start' 3 + import { getCookie } from '@tanstack/react-start/server' 4 + import { Agent } from '@atproto/api' 5 + import { client } from '#/lib/oauth-client' 6 + 7 + const getTimeline = createServerFn({ method: 'GET' }).handler(async () => { 8 + const did = getCookie('did') 9 + 10 + if (!did) { 11 + throw redirect({ to: '/login' }) 12 + } 13 + 14 + const session = await client.restore(did) 15 + const agent = new Agent(session) 16 + const { data } = await agent.getTimeline({ limit: 50 }) 17 + return { 18 + feed: data.feed.map((item) => ({ 19 + uri: item.post.uri, 20 + text: (item.post.record as { text?: string }).text ?? '', 21 + author: { 22 + handle: item.post.author.handle, 23 + displayName: item.post.author.displayName ?? null, 24 + avatar: item.post.author.avatar ?? null, 25 + }, 26 + })), 27 + } 28 + }) 2 29 3 30 export const Route = createFileRoute('/feed')({ 4 - component: RouteComponent, 31 + loader: () => getTimeline(), 32 + component: FeedPage, 5 33 }) 6 34 7 - function RouteComponent() { 8 - return <div>Hello "/feed"!</div> 35 + function FeedPage() { 36 + const { feed } = Route.useLoaderData() 37 + 38 + return ( 39 + <div className="max-w-2xl mx-auto py-8 px-4 space-y-4"> 40 + {feed.map((item) => ( 41 + <article key={item.uri} className="border rounded-lg p-4 space-y-2"> 42 + <div className="flex items-center gap-2"> 43 + {item.author.avatar && ( 44 + <img src={item.author.avatar} alt="" className="w-8 h-8 rounded-full" /> 45 + )} 46 + <div> 47 + <span className="font-semibold">{item.author.displayName ?? item.author.handle}</span> 48 + <span className="text-sm text-gray-500 ml-1">@{item.author.handle}</span> 49 + </div> 50 + </div> 51 + <p className="whitespace-pre-wrap">{item.text}</p> 52 + </article> 53 + ))} 54 + </div> 55 + ) 9 56 }