+3
.env.example
+3
.env.example
+12
-1
astro.config.mjs
+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
+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
+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
+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
};
+1
-1
src/layouts/Layout.astro
+1
-1
src/layouts/Layout.astro
+1
-2
src/pages/login.astro
+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
+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>