WE GOT A WORK TO THE PDS LET'S GOOO

+3
.env.example
···
+
ASTRO_DB_REMOTE_URL=file://local.db
+
# below is only needed if you have a hosted db
+
# ASTRO_DB_APP_TOKEN=""
+12 -1
astro.config.mjs
···
})
],
vite: {
+
// @ts-ignore
plugins: [tailwindcss()],
},
experimental: {
···
},
{
provider: fontProviders.fontsource(),
-
name: "Atkinson Hyperlegible",
+
name: "Readex Pro",
+
cssVariable: "--readex",
+
},
+
{
+
provider: fontProviders.fontsource(),
+
name: "Sora",
+
cssVariable: "--sora",
+
},
+
{
+
provider: fontProviders.fontsource(),
+
name: "Atkinson Hyperlegible Next",
cssVariable: "--atkinson",
},
{
+9 -5
bun.lock
···
"": {
"name": "atproto-ao3",
"dependencies": {
-
"@astrojs/db": "^0.17.1",
+
"@astrojs/db": "^0.17.2",
"@astrojs/node": "^9.4.3",
"@atproto/api": "^0.16.7",
"@floating-ui/dom": "^1.7.4",
"@fujocoded/authproto": "^0.0.4",
"@lucide/astro": "^0.542.0",
"@tailwindcss/vite": "^4.1.13",
-
"astro": "^5.13.5",
+
"astro": "^5.13.6",
"nanoid": "^5.1.5",
"tailwindcss": "^4.1.13",
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.16",
-
"daisyui": "^5.1.7",
+
"daisyui": "^5.1.8",
},
},
},
···
"packages": {
"@astrojs/compiler": ["@astrojs/compiler@2.12.2", "", {}, "sha512-w2zfvhjNCkNMmMMOn5b0J8+OmUaBL1o40ipMvqcG6NRpdC+lKxmTi48DT8Xw0SzJ3AfmeFLB45zXZXtmbsjcgw=="],
-
"@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=="],
+
"@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=="],
"@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.7.2", "", {}, "sha512-KCkCqR3Goym79soqEtbtLzJfqhTWMyVaizUi35FLzgGSzBotSw8DB1qwsu7U96ihOJgYhDk2nVPz+3LnXPeX6g=="],
···
"array-iterate": ["array-iterate@2.0.1", "", {}, "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg=="],
-
"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=="],
+
"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=="],
"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=="],
···
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
+
"@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=="],
+
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"@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=="],
···
"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=="],
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
+
+
"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=="],
"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=="],
+3 -3
package.json
···
"name": "atproto-ao3",
"version": "0.0.1",
"dependencies": {
-
"@astrojs/db": "^0.17.1",
+
"@astrojs/db": "^0.17.2",
"@astrojs/node": "^9.4.3",
"@atproto/api": "^0.16.7",
"@floating-ui/dom": "^1.7.4",
"@fujocoded/authproto": "^0.0.4",
"@lucide/astro": "^0.542.0",
"@tailwindcss/vite": "^4.1.13",
-
"astro": "^5.13.5",
+
"astro": "^5.13.6",
"nanoid": "^5.1.5",
"tailwindcss": "^4.1.13"
},
···
"type": "module",
"devDependencies": {
"@tailwindcss/typography": "^0.5.16",
-
"daisyui": "^5.1.7"
+
"daisyui": "^5.1.8"
},
"trustedDependencies": [
"@tailwindcss/oxide"
+38 -11
src/actions/works.ts
···
+
import { TID } from "@atproto/common-web";
import { getAgent } from "@/lib/atproto";
import { ActionError, defineAction } from "astro:actions";
import { z } from "astro:content";
···
});
}
-
// find the did of the logged in user
+
// find the did of the logged in user from our db
const query = await db
.select({ did: Users.userDid })
.from(Users)
···
});
}
-
const user = query[0];
+
const [user] = query;
// check nanoid for collision probability: https://zelark.github.io/nano-id-cc/
const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
···
const slug = nanoid();
// convert the tags into json thru shenaniganery
+
const tags = input.tags;
const work = await db.insert(Works).values({
slug,
author: user.did,
title: input.title,
content: input.content,
-
tags: input.tags,
+
tags,
}).returning();
+
const [newWork] = work;
+
// depending on whether someone toggled the privacy option, push this into user pds
if (input.public) {
+
// we don't need the id, but we'll need the author's did
+
// we'll grab the created + updated timestamps and convert them into strings
+
const { author, id, createdAt, updatedAt, ...data } = newWork;
+
const createdTimestamp = createdAt.toISOString();
+
const record = {
+
...data,
+
createdAt: createdTimestamp,
+
};
+
try {
+
const rkey = TID.nextStr();
const agent = await getAgent(context.locals);
-
const result = await agent!.com.atproto.repo.createRecord({
-
repo: loggedInUser.did,
-
collection: "", // need to figure out WHERE this needs to go
-
record: work[0],
+
+
if (!agent) {
+
console.error("Agent not found!");
+
throw new ActionError({
+
code: "BAD_REQUEST",
+
message: "Something went wrong when connecting to your PDS.",
+
});
+
}
+
+
// ideally, we'd like tags to be references to another record but we won't process them here
+
// we'll just smush this in and pray
+
const result = await agent.com.atproto.repo.putRecord({
+
repo: author, // since we KNOW that the author is the users' did
+
collection: "moe.fanfics.works",
+
rkey,
+
record,
+
validate: false,
});
-
-
return result;
+
+
return result.data.uri;
} catch (error) {
console.error(error);
throw new ActionError({
···
});
}
}
-
// otherwise just return the work
-
return work;
+
// otherwise just return the work
+
return newWork;
},
}),
};
+5 -1
src/components/Navbar.astro
···
</div>
<div class="flex-none">
<ul class="menu menu-horizontal px-1">
+
<li><a href="/search">Search</a></li>
<li><a href="/works">Works</a></li>
{loggedInUser
-
? <li><a href="/user">Settings</a></li>
+
? <>
+
<li><a href="/works/add">New Work</a></li>
+
<li><a href="/user">Settings</a></li>
+
</>
: <li><a href="/login">Login</a></li>
}
</ul>
+1 -1
src/layouts/Layout.astro
···
<Navbar />
</header>
-
<div class:list={["min-w-[65ch] max-w-10/12 mx-auto", className]} {...rest}>
+
<div class:list={["min-w-[65ch] max-w-10/12 mx-auto text-base", className]} {...rest}>
<slot />
</div>
+1 -2
src/pages/login.astro
···
<fieldset class="fieldset mx-auto place-content-center max-w-md">
<label class="fieldset-label" for="handle">
Input your handle
-
<Popover id="handle-help" icon="info" label="help">
-
<h3>What's my handle?</h3>
+
<Popover id="handle-help" icon="info" label="help" title="What's my handle?">
<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>
<p>What yours will look like depends on whether you made a custom handle!</p>
</Popover>
+14
src/pages/search.astro
···
+
---
+
import Layout from "../layouts/Layout.astro";
+
// we need an appview to search all possible fanfics in the protocol
+
---
+
+
<Layout>
+
<main id="search">
+
<h1 class="text-lg">Search</h1>
+
<form method="get">
+
<label class="label" for="work-search">Title</label>
+
<input class="input" type="search" name="workSearch" id="work-search" />
+
</form>
+
</main>
+
</Layout>