WE GOT A WORK TO THE PDS LET'S GOOO

+3
.env.example
··· 1 + ASTRO_DB_REMOTE_URL=file://local.db 2 + # below is only needed if you have a hosted db 3 + # ASTRO_DB_APP_TOKEN=""
+12 -1
astro.config.mjs
··· 22 22 }) 23 23 ], 24 24 vite: { 25 + // @ts-ignore 25 26 plugins: [tailwindcss()], 26 27 }, 27 28 experimental: { ··· 39 40 }, 40 41 { 41 42 provider: fontProviders.fontsource(), 42 - name: "Atkinson Hyperlegible", 43 + name: "Readex Pro", 44 + cssVariable: "--readex", 45 + }, 46 + { 47 + provider: fontProviders.fontsource(), 48 + name: "Sora", 49 + cssVariable: "--sora", 50 + }, 51 + { 52 + provider: fontProviders.fontsource(), 53 + name: "Atkinson Hyperlegible Next", 43 54 cssVariable: "--atkinson", 44 55 }, 45 56 {
+9 -5
bun.lock
··· 4 4 "": { 5 5 "name": "atproto-ao3", 6 6 "dependencies": { 7 - "@astrojs/db": "^0.17.1", 7 + "@astrojs/db": "^0.17.2", 8 8 "@astrojs/node": "^9.4.3", 9 9 "@atproto/api": "^0.16.7", 10 10 "@floating-ui/dom": "^1.7.4", 11 11 "@fujocoded/authproto": "^0.0.4", 12 12 "@lucide/astro": "^0.542.0", 13 13 "@tailwindcss/vite": "^4.1.13", 14 - "astro": "^5.13.5", 14 + "astro": "^5.13.6", 15 15 "nanoid": "^5.1.5", 16 16 "tailwindcss": "^4.1.13", 17 17 }, 18 18 "devDependencies": { 19 19 "@tailwindcss/typography": "^0.5.16", 20 - "daisyui": "^5.1.7", 20 + "daisyui": "^5.1.8", 21 21 }, 22 22 }, 23 23 }, ··· 27 27 "packages": { 28 28 "@astrojs/compiler": ["@astrojs/compiler@2.12.2", "", {}, "sha512-w2zfvhjNCkNMmMMOn5b0J8+OmUaBL1o40ipMvqcG6NRpdC+lKxmTi48DT8Xw0SzJ3AfmeFLB45zXZXtmbsjcgw=="], 29 29 30 - "@astrojs/db": ["@astrojs/db@0.17.1", "", { "dependencies": { "@libsql/client": "^0.15.2", "deep-diff": "^1.0.2", "drizzle-orm": "^0.42.0", "kleur": "^4.1.5", "nanoid": "^5.1.5", "prompts": "^2.4.2", "yargs-parser": "^21.1.1", "zod": "^3.24.4" } }, "sha512-QL09xZf5Om8AshIlt+YhLDYf6M1QSzv+kfuljsPrhEXJ8U/tuKnbWs2M3wimFaLG3/fU0prFix8lWt7zU8ytfA=="], 30 + "@astrojs/db": ["@astrojs/db@0.17.2", "", { "dependencies": { "@libsql/client": "^0.15.14", "deep-diff": "^1.0.2", "drizzle-orm": "^0.42.0", "kleur": "^4.1.5", "nanoid": "^5.1.5", "prompts": "^2.4.2", "yargs-parser": "^21.1.1", "zod": "^3.25.76" } }, "sha512-rFkw8Cj/kLwr63n1bS/sUw3hNywyvTkPZbKCdwAqd9FfbH3LdN+dH29XwmBC0NhXOxK3wA1jZBRsOOxpcVkV5w=="], 31 31 32 32 "@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.7.2", "", {}, "sha512-KCkCqR3Goym79soqEtbtLzJfqhTWMyVaizUi35FLzgGSzBotSw8DB1qwsu7U96ihOJgYhDk2nVPz+3LnXPeX6g=="], 33 33 ··· 375 375 376 376 "array-iterate": ["array-iterate@2.0.1", "", {}, "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg=="], 377 377 378 - "astro": ["astro@5.13.5", "", { "dependencies": { "@astrojs/compiler": "^2.12.2", "@astrojs/internal-helpers": "0.7.2", "@astrojs/markdown-remark": "6.3.6", "@astrojs/telemetry": "3.3.0", "@capsizecss/unpack": "^2.4.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.1.4", "acorn": "^8.14.1", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "boxen": "8.0.1", "ci-info": "^4.2.0", "clsx": "^2.1.1", "common-ancestor-path": "^1.0.1", "cookie": "^1.0.2", "cssesc": "^3.0.0", "debug": "^4.4.0", "deterministic-object-hash": "^2.0.2", "devalue": "^5.1.1", "diff": "^5.2.0", "dlv": "^1.1.3", "dset": "^3.1.4", "es-module-lexer": "^1.6.0", "esbuild": "^0.25.0", "estree-walker": "^3.0.3", "flattie": "^1.1.1", "fontace": "~0.3.0", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.1.1", "import-meta-resolve": "^4.1.0", "js-yaml": "^4.1.0", "kleur": "^4.1.5", "magic-string": "^0.30.17", "magicast": "^0.3.5", "mrmime": "^2.0.1", "neotraverse": "^0.6.18", "p-limit": "^6.2.0", "p-queue": "^8.1.0", "package-manager-detector": "^1.1.0", "picomatch": "^4.0.2", "prompts": "^2.4.2", "rehype": "^13.0.2", "semver": "^7.7.1", "shiki": "^3.2.1", "smol-toml": "^1.3.4", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.12", "tsconfck": "^3.1.5", "ultrahtml": "^1.6.0", "unifont": "~0.5.0", "unist-util-visit": "^5.0.0", "unstorage": "^1.15.0", "vfile": "^6.0.3", "vite": "^6.3.4", "vitefu": "^1.0.6", "xxhash-wasm": "^1.1.0", "yargs-parser": "^21.1.1", "yocto-spinner": "^0.2.1", "zod": "^3.24.4", "zod-to-json-schema": "^3.24.5", "zod-to-ts": "^1.2.0" }, "optionalDependencies": { "sharp": "^0.33.3" }, "bin": { "astro": "astro.js" } }, "sha512-XmBzkl13XU97+n/QiOM5uXQdAVe0yKt5gO+Wlgc8dHRwHR499qhMQ5sMFckLJweUINLzcNGjP3F5nG4wV8a2XA=="], 378 + "astro": ["astro@5.13.6", "", { "dependencies": { "@astrojs/compiler": "^2.12.2", "@astrojs/internal-helpers": "0.7.2", "@astrojs/markdown-remark": "6.3.6", "@astrojs/telemetry": "3.3.0", "@capsizecss/unpack": "^2.4.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.2.0", "acorn": "^8.15.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "boxen": "8.0.1", "ci-info": "^4.3.0", "clsx": "^2.1.1", "common-ancestor-path": "^1.0.1", "cookie": "^1.0.2", "cssesc": "^3.0.0", "debug": "^4.4.1", "deterministic-object-hash": "^2.0.2", "devalue": "^5.1.1", "diff": "^5.2.0", "dlv": "^1.1.3", "dset": "^3.1.4", "es-module-lexer": "^1.7.0", "esbuild": "^0.25.0", "estree-walker": "^3.0.3", "flattie": "^1.1.1", "fontace": "~0.3.0", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "import-meta-resolve": "^4.2.0", "js-yaml": "^4.1.0", "kleur": "^4.1.5", "magic-string": "^0.30.18", "magicast": "^0.3.5", "mrmime": "^2.0.1", "neotraverse": "^0.6.18", "p-limit": "^6.2.0", "p-queue": "^8.1.0", "package-manager-detector": "^1.3.0", "picomatch": "^4.0.3", "prompts": "^2.4.2", "rehype": "^13.0.2", "semver": "^7.7.2", "shiki": "^3.12.0", "simple-swizzle": "0.2.2", "smol-toml": "^1.4.2", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tsconfck": "^3.1.6", "ultrahtml": "^1.6.0", "unifont": "~0.5.2", "unist-util-visit": "^5.0.0", "unstorage": "^1.17.0", "vfile": "^6.0.3", "vite": "^6.3.6", "vitefu": "^1.1.1", "xxhash-wasm": "^1.1.0", "yargs-parser": "^21.1.1", "yocto-spinner": "^0.2.3", "zod": "^3.25.76", "zod-to-json-schema": "^3.24.6", "zod-to-ts": "^1.2.0" }, "optionalDependencies": { "sharp": "^0.33.3" }, "bin": { "astro": "astro.js" } }, "sha512-chy1J+AO3d4lui4MjUyqusiW1jilfkviCBDz+c2MoXxhIImF96GqoliX+79fGy6KMsnMh5lUn+qwy3yUBJqZqg=="], 379 379 380 380 "astro-integration-kit": ["astro-integration-kit@0.19.0", "", { "dependencies": { "pathe": "^1.1.2" }, "peerDependencies": { "astro": "^4.14.0 || ^5.0.0" } }, "sha512-ftDrem91kJZoenhpJJfRtB29D/bmNglEp2oOXqF1uL5yODZauGIy3tDgIbec0UEMp6tNuky4tfWseUXpej5Dng=="], 381 381 ··· 981 981 982 982 "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], 983 983 984 + "@fujocoded/authproto/@astrojs/db": ["@astrojs/db@0.17.1", "", { "dependencies": { "@libsql/client": "^0.15.2", "deep-diff": "^1.0.2", "drizzle-orm": "^0.42.0", "kleur": "^4.1.5", "nanoid": "^5.1.5", "prompts": "^2.4.2", "yargs-parser": "^21.1.1", "zod": "^3.24.4" } }, "sha512-QL09xZf5Om8AshIlt+YhLDYf6M1QSzv+kfuljsPrhEXJ8U/tuKnbWs2M3wimFaLG3/fU0prFix8lWt7zU8ytfA=="], 985 + 984 986 "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], 985 987 986 988 "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="], ··· 998 1000 "ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], 999 1001 1000 1002 "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], 1003 + 1004 + "astro/vite": ["vite@6.3.6", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA=="], 1001 1005 1002 1006 "cross-fetch/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], 1003 1007
+3 -3
package.json
··· 2 2 "name": "atproto-ao3", 3 3 "version": "0.0.1", 4 4 "dependencies": { 5 - "@astrojs/db": "^0.17.1", 5 + "@astrojs/db": "^0.17.2", 6 6 "@astrojs/node": "^9.4.3", 7 7 "@atproto/api": "^0.16.7", 8 8 "@floating-ui/dom": "^1.7.4", 9 9 "@fujocoded/authproto": "^0.0.4", 10 10 "@lucide/astro": "^0.542.0", 11 11 "@tailwindcss/vite": "^4.1.13", 12 - "astro": "^5.13.5", 12 + "astro": "^5.13.6", 13 13 "nanoid": "^5.1.5", 14 14 "tailwindcss": "^4.1.13" 15 15 }, ··· 22 22 "type": "module", 23 23 "devDependencies": { 24 24 "@tailwindcss/typography": "^0.5.16", 25 - "daisyui": "^5.1.7" 25 + "daisyui": "^5.1.8" 26 26 }, 27 27 "trustedDependencies": [ 28 28 "@tailwindcss/oxide"
+38 -11
src/actions/works.ts
··· 1 + import { TID } from "@atproto/common-web"; 1 2 import { getAgent } from "@/lib/atproto"; 2 3 import { ActionError, defineAction } from "astro:actions"; 3 4 import { z } from "astro:content"; ··· 26 27 }); 27 28 } 28 29 29 - // find the did of the logged in user 30 + // find the did of the logged in user from our db 30 31 const query = await db 31 32 .select({ did: Users.userDid }) 32 33 .from(Users) ··· 40 41 }); 41 42 } 42 43 43 - const user = query[0]; 44 + const [user] = query; 44 45 45 46 // check nanoid for collision probability: https://zelark.github.io/nano-id-cc/ 46 47 const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; ··· 48 49 const slug = nanoid(); 49 50 50 51 // convert the tags into json thru shenaniganery 52 + const tags = input.tags; 51 53 52 54 const work = await db.insert(Works).values({ 53 55 slug, 54 56 author: user.did, 55 57 title: input.title, 56 58 content: input.content, 57 - tags: input.tags, 59 + tags, 58 60 }).returning(); 59 61 62 + const [newWork] = work; 63 + 60 64 // depending on whether someone toggled the privacy option, push this into user pds 61 65 if (input.public) { 66 + // we don't need the id, but we'll need the author's did 67 + // we'll grab the created + updated timestamps and convert them into strings 68 + const { author, id, createdAt, updatedAt, ...data } = newWork; 69 + const createdTimestamp = createdAt.toISOString(); 70 + const record = { 71 + ...data, 72 + createdAt: createdTimestamp, 73 + }; 74 + 62 75 try { 76 + const rkey = TID.nextStr(); 63 77 const agent = await getAgent(context.locals); 64 - const result = await agent!.com.atproto.repo.createRecord({ 65 - repo: loggedInUser.did, 66 - collection: "", // need to figure out WHERE this needs to go 67 - record: work[0], 78 + 79 + if (!agent) { 80 + console.error("Agent not found!"); 81 + throw new ActionError({ 82 + code: "BAD_REQUEST", 83 + message: "Something went wrong when connecting to your PDS.", 84 + }); 85 + } 86 + 87 + // ideally, we'd like tags to be references to another record but we won't process them here 88 + // we'll just smush this in and pray 89 + const result = await agent.com.atproto.repo.putRecord({ 90 + repo: author, // since we KNOW that the author is the users' did 91 + collection: "moe.fanfics.works", 92 + rkey, 93 + record, 94 + validate: false, 68 95 }); 69 - 70 - return result; 96 + 97 + return result.data.uri; 71 98 } catch (error) { 72 99 console.error(error); 73 100 throw new ActionError({ ··· 76 103 }); 77 104 } 78 105 } 79 - // otherwise just return the work 80 106 81 - return work; 107 + // otherwise just return the work 108 + return newWork; 82 109 }, 83 110 }), 84 111 };
+5 -1
src/components/Navbar.astro
··· 7 7 </div> 8 8 <div class="flex-none"> 9 9 <ul class="menu menu-horizontal px-1"> 10 + <li><a href="/search">Search</a></li> 10 11 <li><a href="/works">Works</a></li> 11 12 {loggedInUser 12 - ? <li><a href="/user">Settings</a></li> 13 + ? <> 14 + <li><a href="/works/add">New Work</a></li> 15 + <li><a href="/user">Settings</a></li> 16 + </> 13 17 : <li><a href="/login">Login</a></li> 14 18 } 15 19 </ul>
+1 -1
src/layouts/Layout.astro
··· 32 32 <Navbar /> 33 33 </header> 34 34 35 - <div class:list={["min-w-[65ch] max-w-10/12 mx-auto", className]} {...rest}> 35 + <div class:list={["min-w-[65ch] max-w-10/12 mx-auto text-base", className]} {...rest}> 36 36 <slot /> 37 37 </div> 38 38
+1 -2
src/pages/login.astro
··· 19 19 <fieldset class="fieldset mx-auto place-content-center max-w-md"> 20 20 <label class="fieldset-label" for="handle"> 21 21 Input your handle 22 - <Popover id="handle-help" icon="info" label="help"> 23 - <h3>What's my handle?</h3> 22 + <Popover id="handle-help" icon="info" label="help" title="What's my handle?"> 24 23 <p>It'll look like a website URL without the <samp>https://</samp> or slashes, so a typical BlueSky handle will look something like: <b>alice.bsky.social</b>.</p> 25 24 <p>What yours will look like depends on whether you made a custom handle!</p> 26 25 </Popover>
+14
src/pages/search.astro
··· 1 + --- 2 + import Layout from "../layouts/Layout.astro"; 3 + // we need an appview to search all possible fanfics in the protocol 4 + --- 5 + 6 + <Layout> 7 + <main id="search"> 8 + <h1 class="text-lg">Search</h1> 9 + <form method="get"> 10 + <label class="label" for="work-search">Title</label> 11 + <input class="input" type="search" name="workSearch" id="work-search" /> 12 + </form> 13 + </main> 14 + </Layout>