new notes layout

This commit is contained in:
zoop 2026-09-11 16:08:57 -04:00
parent ae41a40866
commit 1bd2d9edb1
No known key found for this signature in database
GPG Key ID: 15C0D336C9E08F3F
4 changed files with 161 additions and 54 deletions

View File

@ -1,6 +1,7 @@
import { env as cfEnv } from "cloudflare:workers";
export interface Note {
id: string;
body: string;
date: string;
}
@ -17,10 +18,13 @@ export interface Project {
draft: boolean;
}
export const DEFAULT_NOTE: Note = {
body: "rebuilt the site from scratch this weekend. corkboard of badges, a rolling note, the usual webrings. more soon.",
export const DEFAULT_NOTES: Note[] = [
{
id: "1",
body: "rebuilt the site from scratch this weekend. corkboard of badges, a running log, the usual webrings. more soon.",
date: "2026-09-06T00:00:00.000Z",
};
},
];
export const DEFAULT_PROJECTS: Project[] = [
{
@ -154,31 +158,39 @@ function kv(): any {
}
}
export async function getNote(): Promise<Note> {
export async function getNotes(): Promise<Note[]> {
const store = kv();
if (!store) return DEFAULT_NOTE;
if (!store) return DEFAULT_NOTES;
let raw: string | null = null;
try {
raw = await store.get("note");
raw = await store.get("notes");
} catch {
return DEFAULT_NOTE;
return DEFAULT_NOTES;
}
if (!raw) return DEFAULT_NOTE;
if (raw) {
try {
const n = JSON.parse(raw);
const list = JSON.parse(raw);
if (Array.isArray(list)) return list as Note[];
} catch {
}
}
try {
const legacy = await store.get("note");
if (legacy) {
const n = JSON.parse(legacy);
if (n && typeof n.body === "string") {
return { body: n.body, date: n.date ?? new Date().toISOString() };
return [{ id: "legacy", body: n.body, date: n.date ?? new Date().toISOString() }];
}
}
} catch {
return { body: raw, date: new Date().toISOString() };
}
return DEFAULT_NOTE;
return DEFAULT_NOTES;
}
export async function setNote(body: string): Promise<void> {
export async function setNotes(list: Note[]): Promise<void> {
const store = kv();
if (!store) throw new Error("CONTENT KV namespace is not bound");
await store.put("note", JSON.stringify({ body, date: new Date().toISOString() }));
await store.put("notes", JSON.stringify(list));
}
export async function getProjects(): Promise<Project[]> {

View File

@ -1,15 +1,21 @@
---
import Base from "../../layouts/Base.astro";
import { isAuthed, adminPasswordSet } from "../../lib/session";
import { getNote, getProjects } from "../../lib/content";
import { getNotes, getProjects } from "../../lib/content";
const authed = await isAuthed(Astro.request);
const loginError = Astro.url.searchParams.has("e");
const noPassword = !adminPasswordSet();
const note = authed ? await getNote() : null;
const notes = authed
? (await getNotes()).slice().sort((a, b) => +new Date(b.date) - +new Date(a.date))
: [];
const projects = authed ? await getProjects() : [];
const thisYear = String(new Date().getFullYear());
const fmtShort = (d: string) =>
new Date(d).toLocaleDateString("en-GB", { day: "numeric", month: "short" });
const preview = (s: string, n = 56) => (s.length > n ? `${s.slice(0, n).trim()}…` : s);
---
<Base title="admin" description="edit site content.">
@ -43,17 +49,60 @@ const thisYear = String(new Date().getFullYear());
}
{
authed && note && (
authed && (
<>
<form class="card" method="POST" action="/api/admin/save">
<input type="hidden" name="action" value="note" />
<h2 class="label">note</h2>
<textarea name="body" rows="5" spellcheck="true" set:text={note.body} />
<p class="hint">markdown ok. saving stamps today's date.</p>
<div class="row">
<button type="submit">save note</button>
<section class="card">
<h2 class="label">notes &mdash; {notes.length}</h2>
<details class="pj pj--add" open={notes.length === 0}>
<summary>+ new note</summary>
<form method="POST" action="/api/admin/save" class="pj__form">
<input type="hidden" name="action" value="note-save" />
<label class="fld fld--wide">
<span>body (markdown ok)</span>
<textarea name="body" rows="4" spellcheck="true" required />
</label>
<div class="row fld--wide">
<button type="submit">post</button>
</div>
</form>
</details>
{notes.map((n) => (
<details class="pj">
<summary>
<span class="pj__name">{preview(n.body)}</span>
<span class="pj__year">{fmtShort(n.date)}</span>
</summary>
<form method="POST" action="/api/admin/save" class="pj__form">
<input type="hidden" name="action" value="note-save" />
<input type="hidden" name="id" value={n.id} />
<label class="fld fld--wide">
<span>body</span>
<textarea name="body" rows="4" spellcheck="true" set:text={n.body} />
</label>
<div class="row fld--wide">
<button type="submit">save</button>
</div>
</form>
<div class="row pj__ops">
<form
method="POST"
action="/api/admin/save"
onsubmit="return confirm('delete this note?')"
>
<input type="hidden" name="action" value="note-delete" />
<input type="hidden" name="id" value={n.id} />
<button type="submit" class="danger">
delete
</button>
</form>
</div>
</details>
))}
</section>
<section class="card">
<h2 class="label">projects &mdash; {projects.length}</h2>

View File

@ -3,7 +3,8 @@ import { isAuthed } from "../../../lib/session";
import {
getProjects,
setProjects,
setNote,
getNotes,
setNotes,
type Project,
} from "../../../lib/content";
@ -36,8 +37,22 @@ export const POST: APIRoute = async ({ request }) => {
const form = await request.formData();
const action = String(form.get("action") ?? "");
if (action === "note") {
await setNote(String(form.get("body") ?? ""));
if (action === "note-save") {
const body = String(form.get("body") ?? "").trim();
if (!body) return back();
const id = String(form.get("id") ?? "").trim();
const notes = await getNotes();
const i = notes.findIndex((x) => x.id === id);
if (i >= 0) {
notes[i] = { ...notes[i], body };
} else {
notes.unshift({ id: crypto.randomUUID().slice(0, 8), body, date: new Date().toISOString() });
}
await setNotes(notes);
return back();
} else if (action === "note-delete") {
const id = String(form.get("id") ?? "");
await setNotes((await getNotes()).filter((x) => x.id !== id));
return back();
}

View File

@ -1,10 +1,11 @@
---
import Base from "../layouts/Base.astro";
import { getNote } from "../lib/content";
import { getNotes } from "../lib/content";
import { renderMarkdown } from "../lib/md";
const note = await getNote();
const html = note.body.trim() ? renderMarkdown(note.body) : "";
const notes = (await getNotes())
.slice()
.sort((a, b) => +new Date(b.date) - +new Date(a.date));
const fmt = (d: string) =>
new Date(d)
@ -12,17 +13,23 @@ const fmt = (d: string) =>
.toUpperCase();
---
<Base title="notes — zach" description="a note from zach.">
<Base title="notes — zach" description="a running log of thoughts from zach.">
<section class="intro">
<h1>notes</h1>
</section>
{
html ? (
<article class="notepage">
<p class="notepage__date">{fmt(note.date)}</p>
<div class="notepage__body" set:html={html} />
notes.length ? (
<div class="notelog">
{notes.map((n) => (
<article class="notelog__entry" id={n.id}>
<p class="notelog__date">
<a href={`#${n.id}`}>{fmt(n.date)}</a>
</p>
<div class="notelog__body" set:html={renderMarkdown(n.body)} />
</article>
))}
</div>
) : (
<p class="notepage__empty">nothing noted yet.</p>
)
@ -30,36 +37,60 @@ const fmt = (d: string) =>
</Base>
<style>
.notepage__date {
.notelog {
display: flex;
flex-direction: column;
}
.notelog__entry {
padding: 1.6rem 0;
border-bottom: 1px solid var(--line);
}
.notelog__entry:first-child {
padding-top: 0;
}
.notelog__entry:last-child {
border-bottom: 0;
}
.notelog__entry:target .notelog__date a {
color: var(--accent);
}
.notelog__date {
margin-bottom: 0.9rem;
}
.notelog__date a {
font-family: var(--mono);
font-size: 0.78rem;
letter-spacing: 0.16em;
color: var(--muted);
margin-bottom: 1.3rem;
text-decoration: none;
transition: color 0.16s ease;
}
.notelog__date a:hover {
color: var(--accent);
}
.notepage__empty {
color: var(--faint);
}
.notepage__body {
.notelog__body {
max-width: 62ch;
}
.notepage__body :global(p) {
.notelog__body :global(p) {
margin: 0 0 1.1rem;
}
.notepage__body :global(p:last-child) {
.notelog__body :global(p:last-child) {
margin-bottom: 0;
}
.notepage__body :global(a) {
.notelog__body :global(a) {
text-decoration-color: var(--accent);
}
.notepage__body :global(ul) {
.notelog__body :global(ul) {
margin: 0 0 1.1rem;
padding-left: 1.2rem;
}
.notepage__body :global(li) {
.notelog__body :global(li) {
margin: 0 0 0.35rem;
}
.notepage__body :global(code) {
.notelog__body :global(code) {
font-family: var(--mono);
font-size: 0.88em;
color: var(--muted);
@ -67,9 +98,9 @@ const fmt = (d: string) =>
padding: 0.05em 0.3em;
border-radius: 3px;
}
.notepage__body :global(h2),
.notepage__body :global(h3),
.notepage__body :global(h4) {
.notelog__body :global(h2),
.notelog__body :global(h3),
.notelog__body :global(h4) {
font-family: var(--mono);
font-size: 0.8rem;
letter-spacing: 0.14em;
@ -77,14 +108,14 @@ const fmt = (d: string) =>
color: var(--muted);
margin: 1.8rem 0 0.7rem;
}
.notepage__body :global(pre) {
.notelog__body :global(pre) {
background: var(--paper-2);
padding: 1rem;
border-radius: 6px;
overflow-x: auto;
font-size: 0.85rem;
}
.notepage__body :global(pre code) {
.notelog__body :global(pre code) {
background: none;
padding: 0;
color: var(--ink);