add components and test fetching works from db

+10 -21
astro.config.mjs
···
import { defineConfig, fontProviders } from 'astro/config';
import node from '@astrojs/node';
import db from "@astrojs/db";
-
import authproto from "@fujocoded/authproto";
+
import fujocodedAuthproto from "@fujocoded/authproto";
// https://astro.build/config
export default defineConfig({
+
output: "server",
adapter: node({
mode: 'standalone',
}),
integrations: [
db(),
-
authproto({
+
fujocodedAuthproto({
applicationName: "fan archive",
-
applicationDomain: "",
-
driver: { name: "astro:db" },
-
}),
+
applicationDomain: "localhost:4321",
+
// driver: {
+
// name: "astro:db",
+
// },
+
})
],
experimental: {
fonts: [
{
-
provider: "local",
-
name: "Junicode",
+
provider: fontProviders.fontsource(),
+
name: "IBM Plex Serif",
cssVariable: "--junicode",
-
variants: [
-
{
-
weight: "300 700",
-
style: "normal",
-
variationSettings: "'wdth' 100 125, 'ENLA' 0 1",
-
src: ["./src/assets/fonts/JunicodeVF-Roman.woff2"],
-
},
-
{
-
weight: "300 700",
-
style: "italic",
-
variationSettings: "'wdth' 100 125, 'ENLA' 0 1",
-
src: ["./src/assets/fonts/JunicodeVF-Italic.woff2"],
-
},
-
],
fallbacks: [ 'Charter', 'Bitstream Charter', 'Sitka Text', 'Cambria', 'Georgia', "serif"],
},
{
+6 -2
db/config.ts
···
const Users = defineTable({
columns: {
id: column.number({ primaryKey: true }),
-
userDid: column.text({ name: "user_did" }),
-
joinedAt: column.date({ default: NOW }),
+
userDid: column.text({ name: "user_did", unique: true }),
+
joinedAt: column.date({ name: "joined_at", default: NOW }),
},
+
indexes: [
+
{ on: ["userDid"], unique: true },
+
],
});
const Works = defineTable({
columns: {
id: column.number({ primaryKey: true }),
author: column.text({ references: () => Users.columns.userDid }),
+
// recordkey
title: column.text(),
content: column.text({ multiline: true }),
tags: column.json(),
+2 -2
db/seed.ts
···
author: "test",
title: "Hey there title",
content: "<p>i have evil html</p>",
-
tags: { label: "test", url: "#" },
+
tags: [{ label: "test", url: "#" }],
},
{
author: "another",
title: "Hello world",
content: "<p>whoag i have <b>BOLD</b></p>",
-
tags: { label: "label", url: "#" },
+
tags: [{ label: "label", url: "#" }],
},
]);
}
+35 -15
src/actions/works.ts
···
-
import { driver, worksTable } from "@/lib/db";
-
import { defineAction } from "astro:actions";
+
import { ActionError, defineAction } from "astro:actions";
import { z } from "astro:content";
+
import { db, eq, Users, Works } from "astro:db";
+
+
const workSchema = z.object({
+
title: z.string(),
+
content: z.string(),
+
tags: z.array(
+
z.object({
+
label: z.string(),
+
uri: z.string(),
+
})
+
),
+
});
export const worksActions = {
addWork: defineAction({
accept: "form",
-
input: z.object({
-
author: z.union([z.string(), z.array(z.string())]),
-
title: z.string(),
-
content: z.string(),
-
}),
-
handler: async ({ author, title, content }) => {
+
input: workSchema,
+
handler: async (input, context) => {
// check against auth
-
+
if (!context.locals.loggedInUser) {
+
throw new ActionError({
+
code: "UNAUTHORIZED",
+
message: "You're not logged in!",
+
});
+
}
-
// handle multiple authors
-
const convertedAuthors = author.toString();
+
// const agent = await
-
const work = await driver.insert(worksTable).values({
-
author: convertedAuthors,
-
title,
-
body: content
+
// find the id of the logged in user
+
const userId = await db
+
.select({ did: Users.userDid })
+
.from(Users)
+
.where(
+
eq(Users.userDid, context.locals.loggedInUser.did)
+
);
+
+
const work = await db.insert(Works).values({
+
author: userId[0].did,
+
title: input.title,
+
content: input.content,
+
tags: input.tags,
}).returning();
return work;
+3
src/assets/styles/base.css
···
}
body {
+
height: 100vh;
font-family: var(--body);
+
font-size: var(--step-0);
+
line-height: 1.5;
}
+40
src/components/Popover.astro
···
+
---
+
interface Props {
+
label: string;
+
icon?: string;
+
title?: string;
+
class?: string;
+
}
+
+
const { label, icon, title, class: className, ...rest } = Astro.props;
+
---
+
<details class:list={["popup", className]} {...rest}>
+
<summary>
+
{icon
+
? <div class="icon" aria-label={label}>x</div>
+
: <span>{label}</span>
+
}
+
</summary>
+
+
{title &&
+
<h3>{title}</h3>
+
}
+
+
<slot />
+
</details>
+
+
<style>
+
.popup {
+
display: inline-block;
+
+
summary {
+
font-size: var(--step--2);
+
cursor: pointer;
+
}
+
+
&::details-content {
+
position: absolute;
+
z-index: 1;
+
}
+
}
+
</style>
+90
src/components/Settings.astro
···
+
<div id="settings">
+
<form id="user-settings">
+
<label for="font-family">font family</label>
+
<select name="fontFamily" id="font-family">
+
<option value="default">choose...</option>
+
<option value="--serif">serif</option>
+
<option value="--mono">monospaced</option>
+
<option value="--sans-serif">sans serif</option>
+
<option value="--dyslexia">dyslexic</option>
+
</select>
+
+
<label for="font-size">text size</label>
+
<input type="range" name="fontSize" id="font-size" min="-1" max="2" step="1" />
+
+
<label for="line-height">line height</label>
+
<input type="range" name="lineHeight" id="line-height" min="1" max="2" step="0.05" />
+
+
<label for="letter-spacing">letter spacing</label>
+
<input type="range" name="letterSpacing" id="letter-spacing" min="0" max="0.1" step="0.01" />
+
+
<label for="word-spacing">word spacing</label>
+
<input type="range" name="wordSpacing" id="word-spacing" min="0" max="0.5" step="0.01" />
+
+
<div id="test-area">
+
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Asperiores quae dolorum debitis vero nostrum nobis aspernatur ipsam sunt dolorem, eum ut corrupti unde commodi soluta natus repellendus totam animi adipisci.</p>
+
</div>
+
+
<button id="confirm-settings">save</button>
+
</form>
+
</div>
+
+
<style>
+
#test-area {
+
--font: var(--body);
+
--size: var(--step-0);
+
--letter-spacing: 0em;
+
--word-spacing: 0em;
+
--line-height: 1.5;
+
+
font-family: var(--font);
+
font-size: var(--size);
+
letter-spacing: var(--letter-spacing);
+
word-spacing: var(--word-spacing);
+
line-height: var(--line-height);
+
}
+
</style>
+
+
<script>
+
const form = document.forms.namedItem("user-settings");
+
const inputs = document.querySelectorAll("#user-settings :not(button, label, option)");
+
const submitter = document.getElementById("confirm-settings");
+
const test = document.getElementById("test-area");
+
+
form?.addEventListener("submit", (e) => {
+
e.preventDefault();
+
});
+
+
inputs.forEach((input) => {
+
(input as HTMLElement).addEventListener("change", (e) => {
+
const target = e.target as HTMLInputElement || HTMLSelectElement;
+
switch (target.name) {
+
case "fontFamily":
+
(target.value !== "default")
+
? test?.style.setProperty("--font", `var(${target.value})`)
+
: test?.style.setProperty("--font", `var(--body)`);
+
break;
+
case "fontSize":
+
test?.style.setProperty("--size", `var(--step-${target.value})`);
+
break;
+
case "lineHeight":
+
test?.style.setProperty("--line-height", target.value);
+
break;
+
case "letterSpacing":
+
test?.style.setProperty("--letter-spacing", `${target.value}em`);
+
break;
+
case "wordSpacing":
+
test?.style.setProperty("--word-spacing", `${target.value}em`);
+
break;
+
default:
+
break;
+
}
+
});
+
});
+
+
submitter?.addEventListener("click", (e) => {
+
e.preventDefault();
+
const data = new FormData(form!);
+
console.table(data);
+
});
+
</script>
+5
src/lib/atproto.ts
···
+
import type { APIContext } from "astro";
+
+
async function getAgent(locals: APIContext["locals"]) {
+
const agent = new Object()
+
}
+14
src/lib/types.ts
···
+
export interface Work {
+
id: number;
+
title: string;
+
author: number | string;
+
tags: Array<Tag>;
+
content: string;
+
createdAt: Date | string;
+
updatedAt: Date | string | undefined;
+
}
+
+
export interface Tag {
+
label: string;
+
url: string;
+
}
+14
src/pages/index.astro
···
---
import Layout from "../layouts/Layout.astro";
+
import Settings from "~/Settings.astro";
+
+
const currentUser = Astro.locals.loggedInUser;
---
<Layout>
<h1>hi</h1>
···
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Praesentium eum est quisquam distinctio magni recusandae quia vero tempore consectetur! Dolore repellat, voluptatem dignissimos sit eaque iste atque facilis in saepe?</p>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorem, maxime libero eveniet repellat corporis, architecto voluptate maiores ullam accusamus quasi nostrum nihil placeat cum earum ex voluptatum, harum sunt quam!</p>
</main>
+
+
<Settings />
+
+
{currentUser
+
? <>
+
<p>you're logged in!</p>
+
<form action="/oauth/logout" method="post">
+
<button>logout</button>
+
</form>
+
</>
+
: <a href="/login">login</a>}
</Layout>
+29
src/pages/login.astro
···
+
---
+
import Layout from "../layouts/Layout.astro";
+
import Popover from "~/Popover.astro";
+
+
const currentUser = Astro.locals.loggedInUser;
+
---
+
<Layout>
+
{
+
currentUser ? (
+
// If there's a current user, show the log out button
+
<form action="/oauth/logout" method="post">
+
<button type="submit">Logout</button>
+
</form>
+
) : (
+
// If there's no current user, show the log in button
+
<form action="/oauth/login" method="post">
+
<label for="handle">Input your handle
+
<Popover label="help">
+
<h3>What's my handle?</h3>
+
<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>
+
</label>
+
<input name="atproto-id" id="handle" required />
+
<button type="submit">Login</button>
+
</form>
+
)
+
}
+
</Layout>
+29
src/pages/works/[id].astro
···
+
---
+
import Layout from "@/layouts/Layout.astro";
+
import type { Tag } from "@/lib/types";
+
import type { GetStaticPaths } from "astro";
+
import { db, eq, Users, Works } from "astro:db";
+
+
export const getStaticPaths = (async () => {
+
const works = await db.select().from(Works).all();
+
return works.map(work => {
+
return {
+
params: { id: work.id, },
+
props: { work },
+
};
+
});
+
}) satisfies GetStaticPaths;
+
+
const { work } = Astro.props;
+
// const author = await db.select().from(Users).where(eq(Users.id, Works.author));
+
---
+
<Layout>
+
<h1>{work.title}</h1>
+
<!-- <h2>{author[0].userDid}</h2> -->
+
<time datetime={work.createdAt.toISOString()}>{work.createdAt}</time>
+
{(work.tags as Tag[]).map(tag => (
+
<a href={tag.url}>{tag.label}</a>
+
))}
+
+
<Fragment set:html={work.content} />
+
</Layout>
+34
src/pages/works/add.astro
···
+
---
+
import Layout from "@/layouts/Layout.astro";
+
import { actions } from "astro:actions";
+
+
const loggedInUser = Astro.locals.loggedInUser;
+
+
if (!loggedInUser) {
+
Astro.redirect("/works");
+
}
+
+
const result = Astro.getActionResult(actions.worksActions.addWork);
+
---
+
<Layout>
+
<h1>Add a new work</h1>
+
+
<form action={actions.worksActions.addWork} method="post">
+
<label for="title">title</label>
+
<input type="text" name="title" id="title" required />
+
+
<label for="tags">add tags</label>
+
<input list="tags-list" name="tags" id="tags" />
+
<!-- could be cool to fetch tags from a tags server or smth? idk -->
+
<datalist id="tags-list">
+
<option value="test">here</option>
+
<option value="tag2">another</option>
+
<option value="tag3">try them all!</option>
+
</datalist>
+
+
<label for="content">body</label>
+
<textarea name="content" id="content"></textarea>
+
+
<button>submit</button>
+
</form>
+
</Layout>
+31
src/pages/works/index.astro
···
+
---
+
import Layout from "@/layouts/Layout.astro";
+
import type { Tag } from "@/lib/types";
+
import { db, eq, Users, Works } from "astro:db";
+
+
const works = await db.select()
+
.from(Works)
+
.innerJoin(Users, eq(Works.author, Users.userDid));
+
+
const loggedInUser = Astro.locals.loggedInUser;
+
---
+
+
<Layout>
+
<h1>works</h1>
+
{loggedInUser &&
+
<a href="/works/add">wanna write kid?</a>
+
}
+
+
{works.map(({ Works, Users }) => (
+
<article>
+
<h2>{Works.title}</h2>
+
<h3>{Users.userDid}</h3>
+
<time datetime={Works.createdAt.toISOString()}>{Works.createdAt}</time>
+
{(Works.tags as Tag[]).map((tag: Tag) => (
+
<a href={tag.url}>{tag.label}</a>
+
))}
+
+
<Fragment set:html={Works.content} />
+
</article>
+
))}
+
</Layout>