adding a work functions! sort of

+3 -8
astro.config.mjs
···
import node from '@astrojs/node';
import db from "@astrojs/db";
import fujocodedAuthproto from "@fujocoded/authproto";
-
import tailwindcss from "@tailwindcss/vite";
// https://astro.build/config
export default defineConfig({
output: "server",
-
adapter: node({
mode: 'standalone',
}),
-
integrations: [
db(),
fujocodedAuthproto({
···
// },
})
],
-
+
vite: {
+
plugins: [tailwindcss()],
+
},
experimental: {
fonts: [
{
···
],
}
],
-
},
-
-
vite: {
-
plugins: [tailwindcss()],
},
});
+13
bun.lock
···
"tailwindcss": "^4.1.13",
},
"devDependencies": {
+
"@tailwindcss/typography": "^0.5.16",
"daisyui": "^5.1.7",
},
},
···
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.13", "", { "os": "win32", "cpu": "x64" }, "sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw=="],
+
"@tailwindcss/typography": ["@tailwindcss/typography@0.5.16", "", { "dependencies": { "lodash.castarray": "^4.4.0", "lodash.isplainobject": "^4.0.6", "lodash.merge": "^4.6.2", "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA=="],
+
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.13", "", { "dependencies": { "@tailwindcss/node": "4.1.13", "@tailwindcss/oxide": "4.1.13", "tailwindcss": "4.1.13" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-0PmqLQ010N58SbMTJ7BVJ4I2xopiQn/5i6nlb4JmxzQf8zcS5+m2Cv6tqh+sfDwtIdjoEnOvwsGQ1hkUi8QEHQ=="],
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
···
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="],
+
"lodash.castarray": ["lodash.castarray@4.4.0", "", {}, "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q=="],
+
+
"lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="],
+
+
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
+
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
···
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
+
"postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="],
+
"prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
"promise-limit": ["promise-limit@2.7.0", "", {}, "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw=="],
···
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw=="],
"unstorage": ["unstorage@1.17.0", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^4.0.3", "destr": "^2.0.5", "h3": "^1.15.4", "lru-cache": "^10.4.3", "node-fetch-native": "^1.6.7", "ofetch": "^1.4.1", "ufo": "^1.6.1" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6.0.3 || ^7.0.0", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12", "@vercel/kv": "^1.0.1", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-l9Z7lBiwtNp8ZmcoZ/dmPkFXFdtEdZtTZafCSnEIj3YvtkXeGAtL2rN8MQFy/0cs4eOLpuRJMp9ivdug7TCvww=="],
+
+
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
+1
package.json
···
},
"type": "module",
"devDependencies": {
+
"@tailwindcss/typography": "^0.5.16",
"daisyui": "^5.1.7"
}
}
+1 -1
src/actions/users.ts
···
addUser: defineAction({
accept: "form",
input: z.object({
-
nickname: z.string(),
+
nickname: z.string().optional(),
}),
handler: async (input, context) => {
const loggedInUser = context.locals.loggedInUser;
+5 -7
src/actions/works.ts
···
const workSchema = z.object({
title: z.string(),
content: z.string(),
-
tags: z.array(
-
z.object({
-
label: z.string(),
-
uri: z.string(),
-
})
-
),
+
tags: z.string(),
});
export const worksActions = {
···
}
const user = query[0];
+
// check nanoid for collision probability: https://zelark.github.io/nano-id-cc/
const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
const nanoid = customAlphabet(alphabet, 16);
const slug = nanoid();
+
+
// convert the tags into json thru shenaniganery
const work = await db.insert(Works).values({
slug,
···
content: input.content,
tags: input.tags,
}).returning();
-
+
// depending on whether someone toggled the privacy option, push this into firehouse
// const agent = await
+28 -1
src/assets/styles/global.css
···
@import "tailwindcss";
-
@plugin "daisyui";
+
@plugin "daisyui";
+
@plugin "@tailwindcss/typography";
+
+
@theme {
+
/* font tokens */
+
--font-sans: var(--atkinson);
+
--font-serif: var(--plex-serif);
+
--font-mono: var(--plex-mono);
+
--font-dyslexic: var(--dyslexic);
+
+
/* color tokens */
+
+
/* size tokens */
+
/* @link https://utopia.fyi/type/calculator?c=360,18,1.618,1240,20,1.25,10,2,&s=0.75|0.5|0.25,1.5|2|3|4|6,s-l&g=s,l,xl,12 */
+
--text-xs: clamp(0.4297rem, 0.2783rem + 0.6732cqi, 0.8rem);
+
--text-sm: clamp(0.6953rem, 0.5707rem + 0.554cqi, 1rem);
+
--text-base: clamp(1.125rem, 1.0739rem + 0.2273cqi, 1.25rem);
+
--text-lg: clamp(1.5625rem, 1.9257rem + -0.4686cqi, 1.8203rem);
+
--text-xl: clamp(1.9531rem, 3.351rem + -1.8037cqi, 2.9452rem);
+
--text-2xl: clamp(2.4414rem, 5.716rem + -4.2252cqi, 4.7653rem);
+
--text-3xl: clamp(3.0518rem, 9.616rem + -8.4699cqi, 7.7102rem);
+
--text-4xl: clamp(3.8147rem, 16.018rem + -15.7462cqi, 12.4751rem);
+
--text-5xl: clamp(4.7684rem, 26.4915rem + -28.0298cqi, 20.1848rem);
+
--text-6xl: clamp(5.9605rem, 43.581rem + -48.5427cqi, 32.6589rem);
+
--text-7xl: clamp(7.4506rem, 71.4115rem + -82.5302cqi, 52.8422rem);
+
--text-8xl: clamp(9.3132rem, 116.6654rem + -138.5189cqi, 85.4986rem);
+
--text-9xl: clamp(11.6415rem, 190.1667rem + -230.355cqi, 138.3368rem);
+
}
+3 -31
src/components/Dialog.astro
···
interface Props {
id: string;
title: string;
+
class?: string;
alert?: boolean;
}
-
const { id, title, alert = false } = Astro.props;
+
const { id, title, class: className, alert = false } = Astro.props;
---
-
<dialog {id} role={alert ? "alertdialog" : undefined} closedby="any">
+
<dialog {id} class:list={["card", className]} role={alert ? "alertdialog" : undefined} closedby="any">
<header>
<h1>{title}</h1>
<form method="dialog">
···
</dialog>
<style>
-
dialog {
-
margin: auto;
-
min-height: 200px;
-
padding: 0;
-
-
header {
-
display: flex;
-
align-items: center;
-
background-color: aqua;
-
padding: 0.5rem;
-
-
h1 {
-
flex: 1;
-
font-size: var(--step--1);
-
}
-
-
.close {
-
cursor: pointer;
-
min-width: 44px;
-
min-height: 44px;
-
display: grid;
-
place-content: center;
-
}
-
}
-
-
.dialog-content {
-
padding: 1rem;
-
}
-
}
</style>
+13 -15
src/components/Navbar.astro
···
---
-
const LINKS = [
-
{ label: "Home", url: "/" },
-
{ label: "Works", url: "/works" },
-
];
-
const loggedInUser = Astro.locals.loggedInUser;
---
-
<nav id="main-nav">
-
<ul>
-
{LINKS.map(({ label, url }) => (
-
<li><a href={url}>{label}</a></li>
-
))}
-
{loggedInUser
-
? <li><a href="/user">Settings</a></li>
-
: <li><a href="/login">Login</a></li>
-
}
-
</ul>
+
<nav id="main-nav" class="navbar bg-base-100">
+
<div class="flex-1">
+
<a href="/" class="btn btn-ghost text-xl">Home</a>
+
</div>
+
<div class="flex-none">
+
<ul class="menu menu-horizontal px-1">
+
<li><a href="/works">Works</a></li>
+
{loggedInUser
+
? <li><a href="/user">Settings</a></li>
+
: <li><a href="/login">Login</a></li>
+
}
+
</ul>
+
</div>
</nav>
+7 -5
src/components/Popover.astro
···
interface Props {
id?: string;
label: string;
+
direction: "top" | "bottom";
icon?: "info" | "warning" | "danger";
title?: string;
class?: string;
}
-
const { id, label, icon, title, class: className, ...rest } = Astro.props;
+
const { id, label, direction = "top", icon, title, class: className, ...rest } = Astro.props;
---
<!-- type button needs to be set here, otherwise it doesn't work inside forms -->
<button
···
(icon === "warning") ? "text-warning" :
(icon === "danger") ? "text-error" :
"text-base-content",
-
"popover-btn",
-
className
+
"popover-btn"
]}
aria-describedby={id}
popovertarget={id}
···
</div>
</div>
-
<style define:vars={{ anchor: `--${id}-anchor` }}>
+
<style define:vars={{ anchor: `--${id}-anchor`, direction }}>
.popover-btn {
anchor-name: var(--anchor);
}
.popover-content {
position-anchor: var(--anchor);
-
top: calc(anchor(top) - 5em);
+
top: anchor(var(--direction));
+
left: anchor(center);
+
transform: translateX(-50%);
position-try-fallbacks: flip-block, flip-inline;
}
</style>
+14 -10
src/layouts/Layout.astro
···
interface Props {
title?: string;
skipLink?: string;
+
class?: string;
}
-
const { title = "fanfics", skipLink } = Astro.props;
+
const { title = "fanfics", skipLink, class: className, ...rest } = Astro.props;
---
<html lang="en">
<head>
···
<Font cssVariable="--plex-serif" preload />
<slot name="head" />
</head>
-
<body>
+
<body class="flex flex-col min-h-svh">
<header>
{skipLink && (
<a class="skip-link" href={`#${skipLink}`}>Skip to content</a>
···
<Navbar />
</header>
-
<slot />
+
<div class:list={["min-w-[65ch] max-w-10/12 mx-auto", className]} {...rest}>
+
<slot />
+
</div>
-
<footer>
+
<footer class="footer sm:footer-horizontal footer-center bg-base-300 text-base-content mt-auto p-5">
copyright or something ig
</footer>
</body>
</html>
<style>
-
.skip-link {
-
position: absolute;
-
left: calc(max-content * -1);
-
}
+
@reference "../assets/styles/global.css";
-
footer {
-
margin-top: auto;
+
.skip-link {
+
@apply absolute top-3 -left-full transition-all ease-in-out btn btn-ghost;
+
+
&:focus {
+
@apply left-3;
+
}
}
</style>
-11
src/pages/index.astro
···
---
-
import Popover from "~/Popover.astro";
import Layout from "../layouts/Layout.astro";
const currentUser = Astro.locals.loggedInUser;
···
<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>
-
-
<form>
-
<Popover id="hey" icon="info" label="test">
-
<p>hello?</p>
-
</Popover>
-
</form>
-
-
<Popover id="what" label="tell me">
-
<p>fdajlkdfsjl?</p>
-
</Popover>
{currentUser
? <>
+27 -20
src/pages/login.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</label>
-
<Popover id="handle-help" 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>
-
<input name="atproto-id" id="handle" required />
-
<button type="submit">Login</button>
-
</form>
-
)
-
}
+
<main id="content">
+
<h1 class="text-xl">Log in</h1>
+
{
+
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">
+
<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" direction="bottom">
+
<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 type="text" class="input w-full" name="atproto-id" id="handle" required />
+
<button class="btn btn-primary" type="submit">Login</button>
+
</fieldset>
+
</form>
+
)
+
}
+
</main>
</Layout>
+44 -25
src/pages/user/index.astro
···
const loggedInUser = Astro.locals.loggedInUser;
-
const user = await db.select()
-
.from(Users)
-
.where(eq(Users.userDid, loggedInUser?.did ?? ""))
-
.fullJoin(Works, eq(Users.userDid, Works.author));
-
if (!loggedInUser) {
return Astro.redirect("/login");
}
+
+
const query = await db.select()
+
.from(Users)
+
.where(eq(Users.userDid, loggedInUser.did))
+
.limit(1);
+
const user = query[0];
+
+
const works = await db.select()
+
.from(Works)
+
.where(eq(Works.author, user.userDid));
---
<Layout>
<h1>User Settings</h1>
<p>{loggedInUser?.handle}</p>
<!-- registration will only happen in the below form! -->
-
{(user.length === 0) && (
+
{(query.length === 0) && (
<>
<h2>Connect account</h2>
<div class="info">
···
<Dialog id="connect-account" title="Are you sure?">
<form action={actions.usersActions.addUser} method="post">
<label for="nickname">Nickname</label>
-
<Popover id="nickname-info" label="info" icon="danger">
+
<Popover id="nickname-info" label="info" icon="warning" direction="bottom">
<p>You can optionally set your nickname for this site. This is separate from your handle and acts as your identifier.</p>
<p>Think of your handle as what you use to log in with, and your nickname as the name you want to publish your works under.</p>
<h3>Important</h3>
-
<p>If you do set a nickname, </p>
+
<p>If you do set a nickname, please make sure it's unique! Having two people with the same nickname would cause confusion, unfortunately.</p>
</Popover>
<input type="text" name="nickname" id="nickname" />
···
</>
)}
-
<!-- this is weird but this should ONLY render if the user is registered -->
-
{user.map(({ Users, Works }) => {
-
if (Users) {
-
return (
-
<>
-
<time datetime={Users.joinedAt.toISOString()}>{Users.joinedAt}</time>
-
<p>{Users.userDid}</p>
+
{user && (
+
<>
+
<h2>your nickname???</h2>
+
<time datetime={user.joinedAt.toISOString()}>{user.joinedAt}</time>
+
<p>{user.userDid}</p>
+
+
{works && (
+
<section>
+
<ul>
+
{works.map(work => (
+
<article>
+
<h3>{work.title}</h3>
+
+
<time datetime={work.createdAt.toISOString()}>
+
{work.createdAt}
+
</time>
+
{work.updatedAt && (
+
<time datetime={work.updatedAt.toISOString()}>
+
{work.updatedAt}
+
</time>
+
)}
-
{Works
-
? <section>
<ul>
-
{JSON.stringify(Works)}
+
{JSON.stringify(work.tags)}
</ul>
-
</section>
-
: <a href="/works/add">go write! if you want :)</a>
-
}
-
</>
-
)
-
}
-
})}
+
+
summary here
+
</article>
+
))}
+
</ul>
+
</section>
+
)}
+
</>
+
)}
</Layout>
<script>
+41 -17
src/pages/works/add.astro
···
---
import Layout from "@/layouts/Layout.astro";
-
import { actions } from "astro:actions";
+
import { actions, isInputError } from "astro:actions";
const loggedInUser = Astro.locals.loggedInUser;
···
}
const result = Astro.getActionResult(actions.worksActions.addWork);
+
const errors = isInputError(result?.error) ? result.error.fields : {};
---
<Layout>
-
<h1>Add a new work</h1>
+
<main>
+
<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 />
+
<form action={actions.worksActions.addWork} method="post">
+
<label for="title">title</label>
+
<input transition:persist type="text" name="title" id="title" aria-describedby="title-error" required />
+
{errors.title && (
+
<div id="title-error">
+
{errors.title}
+
</div>
+
)}
-
<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="tags">add tags</label>
+
<input transition:persist type="text" list="tags-list" name="tags" id="tags" aria-describedby="tags-error" />
+
<!-- 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>
+
{errors.tags && (
+
<div id="tags-error">
+
{errors.tags}
+
</div>
+
)}
-
<label for="content">body</label>
-
<textarea name="content" id="content"></textarea>
+
<label for="content">body</label>
+
<textarea transition:persist name="content" id="content" aria-describedby="content-error"></textarea>
+
{errors.content && (
+
<div id="content-error">
+
{errors.content}
+
</div>
+
)}
-
<button>submit</button>
-
</form>
+
<button>submit</button>
+
</form>
+
+
{result?.error && (
+
<div class="error">
+
{result.error.message}
+
</div>
+
)}
+
</main>
</Layout>
+40 -16
src/pages/works/index.astro
···
---
<Layout>
-
<h1>works</h1>
-
{loggedInUser &&
-
<a href="/works/add">wanna write kid?</a>
-
}
+
<h1 class="text-5xl">works</h1>
-
{works.map(({ Works, Users }) => (
-
<article>
-
<h2><a href={`/works/${Works.slug}`}>{Works.title}</a></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>
-
))}
+
<main id="content">
+
{loggedInUser &&
+
<a href="/works/add">wanna write kid?</a>
+
}
-
<Fragment set:html={Works.content} />
-
</article>
-
))}
-
</Layout>
+
{works.map(({ Works, Users }) => (
+
<article class="card">
+
<header>
+
<div class="flex items-center justify-between">
+
<hgroup class="flex-1">
+
<h2 class="card-title">
+
<a href={`/works/${Works.slug}`}>{Works.title}</a>
+
</h2>
+
<h3>{Users.userDid}</h3>
+
</hgroup>
+
<time datetime={Works.createdAt.toISOString()}>{Works.createdAt}</time>
+
</div>
+
{JSON.stringify(Works.tags)}
+
{/* <ul class="flex flex-wrap gap-1.5">
+
{(Works.tags as Tag[]).map((tag: Tag) => (
+
<li><a class="tag" href={tag.url}>{tag.label}</a></li>
+
))}
+
</ul> */}
+
</header>
+
+
<div class="card-body prose lg:prose-xl">
+
<Fragment set:html={Works.content} />
+
</div>
+
</article>
+
))}
+
</main>
+
</Layout>
+
+
<style>
+
@reference "../../assets/styles/global.css";
+
+
.tag {
+
@apply btn btn-accent;
+
}
+
</style>