fix single work displaying

+28 -12
README.md
···
- [Infrastructure](#infrastructure)
- [Notes](#notes)
- [Structure](#structure)
## Background
···
1. The client (the actual site that displays data)
2. The PDS (the server that stores user's data)
-
3. The AppView (the thing that grabs only relevant data)
4. The Lexicon (the blueprint for data to be used in AppViews and clients)
... And probably more, and there's a lot of discussion around how user data ownership / censorship / etc gets handled on those layers.
···
Currently, I'm using these technologies:
- Astro
-
- Drizzle ORM
-
- Turso
-
- Unstorage
Mainly because: 1, Astro is a really well-documented web framework that's pretty approachable as someone who used to handwrite HTML pages; 2, SQLite / LibSQL are (to my knowledge) fairly cheap databases to run; and 3, Unstorage is pretty dead simple for setting up auth sessions from scratch.
### Structure
-
#### `src/actions`
-
These hold actions that run every time a user wants to publish a new work or signs up for the archive.
-
#### `src/assets`
This has images / libre font / `.css` files to be used stylistically throughout the site.
-
#### `src/components`
These hold components that are reused throughout `src/pages`. Like PHP includes but in HTML and JavaScript (well, technically it's JSX).
-
#### `src/lib/db`
-
This holds all the relevant database connecting code. This also contains the types for database tables.
-
#### `src/pages`
-
These are the actual pages where data, user interactions, etc happen. So this would be more HTML/CSS/JavaScript-oriented.
···
- [Infrastructure](#infrastructure)
- [Notes](#notes)
- [Structure](#structure)
+
- [Contributing](#contributing)
+
+
> [!WARNING]
+
> This is alpha software, there's no guarantee that this will work on your system. But if you'd like to try hacking at this, take a look at the [Contributing](#contributing) section once instructions are written up.
## Background
···
1. The client (the actual site that displays data)
2. The PDS (the server that stores user's data)
+
3. The AppView (the thing that grabs only relevant data and serves as an API)
4. The Lexicon (the blueprint for data to be used in AppViews and clients)
... And probably more, and there's a lot of discussion around how user data ownership / censorship / etc gets handled on those layers.
···
Currently, I'm using these technologies:
- Astro
+
- Drizzle ORM (via Astro DB)
+
- Turso (LibSQL under the hood)
+
- Unstorage (via [@fujocoded/authproto](https://github.com/FujoWebDev/fujocoded-plugins/tree/main/astro-authproto))
Mainly because: 1, Astro is a really well-documented web framework that's pretty approachable as someone who used to handwrite HTML pages; 2, SQLite / LibSQL are (to my knowledge) fairly cheap databases to run; and 3, Unstorage is pretty dead simple for setting up auth sessions from scratch.
### Structure
+
#### Database: `db`
+
This holds all the relevant database code. This also contains the structure and types for database tables.
+
+
#### Actions: `src/actions`
+
These hold actions that run every time a user wants to publish a new work or signs up for the archive. Basically the backend functionality for this project.
+
+
#### Assets: `src/assets`
This has images / libre font / `.css` files to be used stylistically throughout the site.
+
#### Components: `src/components`
These hold components that are reused throughout `src/pages`. Like PHP includes but in HTML and JavaScript (well, technically it's JSX).
+
#### Pages: `src/pages`
+
These are the actual routes that are available to end-users. Under the pages are nested pages grouped under folders, namely:
+
##### Users: `src/pages/users`
+
These are pages where users can view all user profiles, find a specific user profile, or update their account settings.
+
+
##### Works: `src/pages/works`
+
+
These only hold pages that are relevant to adding, editing, deleting, or viewing works.
+
+
## Contributing
+
+
To be added, but feel free to open an issue in the meantime!
+49 -1
astro.config.mjs
···
{
provider: fontProviders.fontsource(),
name: "IBM Plex Serif",
-
cssVariable: "--junicode",
fallbacks: [ 'Charter', 'Bitstream Charter', 'Sitka Text', 'Cambria', 'Georgia', "serif"],
},
{
provider: fontProviders.fontsource(),
name: "Atkinson Hyperlegible",
cssVariable: "--atkinson",
}
],
},
···
{
provider: fontProviders.fontsource(),
name: "IBM Plex Serif",
+
cssVariable: "--plex-serif",
fallbacks: [ 'Charter', 'Bitstream Charter', 'Sitka Text', 'Cambria', 'Georgia', "serif"],
+
},
+
{
+
provider: fontProviders.fontsource(),
+
name: "IBM Plex Mono",
+
cssVariable: "--plex-mono",
},
{
provider: fontProviders.fontsource(),
name: "Atkinson Hyperlegible",
cssVariable: "--atkinson",
+
},
+
{
+
provider: "local",
+
name: "OpenDyslexic",
+
cssVariable: "--dyslexic",
+
variants: [
+
{
+
src: [
+
"./src/assets/fonts/opendyslexic/OpenDyslexic-Regular.otf",
+
"./src/assets/fonts/opendyslexic/OpenDyslexic-Regular.woff",
+
"./src/assets/fonts/opendyslexic/OpenDyslexic-Regular.woff2",
+
],
+
weight: 400,
+
style: "normal"
+
},
+
{
+
src: [
+
"./src/assets/fonts/opendyslexic/OpenDyslexic-Italic.otf",
+
"./src/assets/fonts/opendyslexic/OpenDyslexic-Italic.woff",
+
"./src/assets/fonts/opendyslexic/OpenDyslexic-Italic.woff2",
+
],
+
weight: 400,
+
style: "italic",
+
},
+
{
+
src: [
+
"./src/assets/fonts/opendyslexic/OpenDyslexic-Bold.otf",
+
"./src/assets/fonts/opendyslexic/OpenDyslexic-Bold.woff",
+
"./src/assets/fonts/opendyslexic/OpenDyslexic-Bold.woff2",
+
],
+
weight: 700,
+
style: "normal",
+
},
+
{
+
src: [
+
"./src/assets/fonts/opendyslexic/OpenDyslexic-Bold-Italic.otf",
+
"./src/assets/fonts/opendyslexic/OpenDyslexic-Bold-Italic.woff",
+
"./src/assets/fonts/opendyslexic/OpenDyslexic-Bold-Italic.woff2",
+
],
+
weight: 700,
+
style: "italic",
+
},
+
],
}
],
},
+1
bun.lock
···
"@astrojs/node": "^9.4.3",
"@fujocoded/authproto": "^0.0.4",
"astro": "^5.13.5",
},
},
},
···
"@astrojs/node": "^9.4.3",
"@fujocoded/authproto": "^0.0.4",
"astro": "^5.13.5",
+
"nanoid": "^5.1.5",
},
},
},
+4
db/config.ts
···
const Works = defineTable({
columns: {
id: column.number({ primaryKey: true }),
author: column.text({ references: () => Users.columns.userDid }),
// recordkey
title: column.text(),
···
createdAt: column.date({ name: "created_at", default: NOW }),
updatedAt: column.date({ name: "updated_at", optional: true }),
},
});
export default defineDb({
···
const Works = defineTable({
columns: {
id: column.number({ primaryKey: true }),
+
slug: column.text({ unique: true }),
author: column.text({ references: () => Users.columns.userDid }),
// recordkey
title: column.text(),
···
createdAt: column.date({ name: "created_at", default: NOW }),
updatedAt: column.date({ name: "updated_at", optional: true }),
},
+
indexes: [
+
{ on: ["slug", "createdAt"], unique: true },
+
],
});
export default defineDb({
+2 -1
package.json
···
"@astrojs/db": "^0.17.1",
"@astrojs/node": "^9.4.3",
"@fujocoded/authproto": "^0.0.4",
-
"astro": "^5.13.5"
},
"scripts": {
"dev": "astro dev",
···
"@astrojs/db": "^0.17.1",
"@astrojs/node": "^9.4.3",
"@fujocoded/authproto": "^0.0.4",
+
"astro": "^5.13.5",
+
"nanoid": "^5.1.5"
},
"scripts": {
"dev": "astro dev",
+29
src/actions/users.ts
···
···
+
import { ActionError, defineAction } from "astro:actions";
+
import { z } from "astro:content";
+
import { db, Users } from "astro:db";
+
+
export const usersActions = {
+
addUser: defineAction({
+
accept: "form",
+
input: z.object({
+
did: z.string(),
+
}),
+
handler: async ({ did }, context) => {
+
const loggedInUser = context.locals.loggedInUser;
+
+
if (!loggedInUser) {
+
throw new ActionError({
+
code: "UNAUTHORIZED",
+
message: "To connect your PDS, you need to be logged in!",
+
});
+
}
+
+
const user = await db
+
.insert(Users)
+
.values({ userDid: did })
+
.returning();
+
+
return user;
+
},
+
})
+
}
src/assets/fonts/opendyslexic/OpenDyslexic-Bold-Italic.otf

This is a binary file and will not be displayed.

src/assets/fonts/opendyslexic/OpenDyslexic-Bold-Italic.woff

This is a binary file and will not be displayed.

src/assets/fonts/opendyslexic/OpenDyslexic-Bold-Italic.woff2

This is a binary file and will not be displayed.

src/assets/fonts/opendyslexic/OpenDyslexic-Bold.otf

This is a binary file and will not be displayed.

src/assets/fonts/opendyslexic/OpenDyslexic-Bold.woff

This is a binary file and will not be displayed.

src/assets/fonts/opendyslexic/OpenDyslexic-Bold.woff2

This is a binary file and will not be displayed.

src/assets/fonts/opendyslexic/OpenDyslexic-Italic.otf

This is a binary file and will not be displayed.

src/assets/fonts/opendyslexic/OpenDyslexic-Italic.woff

This is a binary file and will not be displayed.

src/assets/fonts/opendyslexic/OpenDyslexic-Italic.woff2

This is a binary file and will not be displayed.

src/assets/fonts/opendyslexic/OpenDyslexic-Regular.otf

This is a binary file and will not be displayed.

src/assets/fonts/opendyslexic/OpenDyslexic-Regular.woff

This is a binary file and will not be displayed.

src/assets/fonts/opendyslexic/OpenDyslexic-Regular.woff2

This is a binary file and will not be displayed.

+5 -1
src/assets/styles/base.css
···
--space-2xl-3xl: clamp(4.5rem, 3.2727rem + 5.4545cqi, 7.5rem);
/* font tokens */
-
--body: var(--junicode);
}
body {
···
--space-2xl-3xl: clamp(4.5rem, 3.2727rem + 5.4545cqi, 7.5rem);
/* font tokens */
+
--serif: var(--plex-serif);
+
--sans: var(--atkinson);
+
--mono: var(--plex-mono);
+
+
--body: var(--serif);
}
body {
+7 -4
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>
···
<button id="confirm-settings">save</button>
</form>
-
</div>
<style>
#test-area {
···
+
---
+
import Dialog from "./Dialog.astro";
+
---
+
<Dialog id="settings" title="User preferences">
<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">sans serif</option>
+
<option value="--dyslexic">dyslexic</option>
</select>
<label for="font-size">text size</label>
···
<button id="confirm-settings">save</button>
</form>
+
</Dialog>
<style>
#test-area {
+4 -1
src/layouts/Layout.astro
···
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width" />
<title>{title}</title>
-
<Font cssVariable="--junicode" preload />
<slot name="head" />
</head>
<body>
···
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width" />
<title>{title}</title>
+
<Font cssVariable="--atkinson" preload />
+
<Font cssVariable="--dyslexic" preload />
+
<Font cssVariable="--plex-mono" preload />
+
<Font cssVariable="--plex-serif" preload />
<slot name="head" />
</head>
<body>
+18 -19
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>
···
---
import Layout from "@/layouts/Layout.astro";
import type { Tag } from "@/lib/types";
import { db, eq, Users, Works } from "astro:db";
+
const { id } = Astro.params;
+
+
const works = await db.select()
+
.from(Works)
+
.where(eq(Works.id, Number(id)))
+
.innerJoin(Users, eq(Works.author, Users.userDid))
+
.limit(1);
---
<Layout>
+
{works.map(({ Works, Users }) => (
+
<h1>{Works.title}</h1>
+
<h2>{Users.userDid}</h2>
+
<time datetime={Works.createdAt.toISOString()}>{Works.createdAt}</time>
+
<ul>
+
{(Works.tags as unknown as Tag[]).map(tag => (
+
<li><a href={tag.url}>{tag.label}</a></li>
+
))}
+
</ul>
+
<Fragment set:html={Works.content} />
+
))}
</Layout>