new notes layout
This commit is contained in:
parent
ae41a40866
commit
1bd2d9edb1
@ -1,6 +1,7 @@
|
|||||||
import { env as cfEnv } from "cloudflare:workers";
|
import { env as cfEnv } from "cloudflare:workers";
|
||||||
|
|
||||||
export interface Note {
|
export interface Note {
|
||||||
|
id: string;
|
||||||
body: string;
|
body: string;
|
||||||
date: string;
|
date: string;
|
||||||
}
|
}
|
||||||
@ -17,10 +18,13 @@ export interface Project {
|
|||||||
draft: boolean;
|
draft: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_NOTE: Note = {
|
export const DEFAULT_NOTES: Note[] = [
|
||||||
body: "rebuilt the site from scratch this weekend. corkboard of badges, a rolling note, the usual webrings. more soon.",
|
{
|
||||||
date: "2026-09-06T00:00:00.000Z",
|
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[] = [
|
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();
|
const store = kv();
|
||||||
if (!store) return DEFAULT_NOTE;
|
if (!store) return DEFAULT_NOTES;
|
||||||
let raw: string | null = null;
|
let raw: string | null = null;
|
||||||
try {
|
try {
|
||||||
raw = await store.get("note");
|
raw = await store.get("notes");
|
||||||
} catch {
|
} catch {
|
||||||
return DEFAULT_NOTE;
|
return DEFAULT_NOTES;
|
||||||
|
}
|
||||||
|
if (raw) {
|
||||||
|
try {
|
||||||
|
const list = JSON.parse(raw);
|
||||||
|
if (Array.isArray(list)) return list as Note[];
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!raw) return DEFAULT_NOTE;
|
|
||||||
try {
|
try {
|
||||||
const n = JSON.parse(raw);
|
const legacy = await store.get("note");
|
||||||
if (n && typeof n.body === "string") {
|
if (legacy) {
|
||||||
return { body: n.body, date: n.date ?? new Date().toISOString() };
|
const n = JSON.parse(legacy);
|
||||||
|
if (n && typeof n.body === "string") {
|
||||||
|
return [{ id: "legacy", body: n.body, date: n.date ?? new Date().toISOString() }];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} 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();
|
const store = kv();
|
||||||
if (!store) throw new Error("CONTENT KV namespace is not bound");
|
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[]> {
|
export async function getProjects(): Promise<Project[]> {
|
||||||
|
|||||||
@ -1,15 +1,21 @@
|
|||||||
---
|
---
|
||||||
import Base from "../../layouts/Base.astro";
|
import Base from "../../layouts/Base.astro";
|
||||||
import { isAuthed, adminPasswordSet } from "../../lib/session";
|
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 authed = await isAuthed(Astro.request);
|
||||||
const loginError = Astro.url.searchParams.has("e");
|
const loginError = Astro.url.searchParams.has("e");
|
||||||
const noPassword = !adminPasswordSet();
|
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 projects = authed ? await getProjects() : [];
|
||||||
const thisYear = String(new Date().getFullYear());
|
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.">
|
<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">
|
<section class="card">
|
||||||
<input type="hidden" name="action" value="note" />
|
<h2 class="label">notes — {notes.length}</h2>
|
||||||
<h2 class="label">note</h2>
|
|
||||||
<textarea name="body" rows="5" spellcheck="true" set:text={note.body} />
|
<details class="pj pj--add" open={notes.length === 0}>
|
||||||
<p class="hint">markdown ok. saving stamps today's date.</p>
|
<summary>+ new note</summary>
|
||||||
<div class="row">
|
<form method="POST" action="/api/admin/save" class="pj__form">
|
||||||
<button type="submit">save note</button>
|
<input type="hidden" name="action" value="note-save" />
|
||||||
</div>
|
<label class="fld fld--wide">
|
||||||
</form>
|
<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">
|
<section class="card">
|
||||||
<h2 class="label">projects — {projects.length}</h2>
|
<h2 class="label">projects — {projects.length}</h2>
|
||||||
|
|||||||
@ -3,7 +3,8 @@ import { isAuthed } from "../../../lib/session";
|
|||||||
import {
|
import {
|
||||||
getProjects,
|
getProjects,
|
||||||
setProjects,
|
setProjects,
|
||||||
setNote,
|
getNotes,
|
||||||
|
setNotes,
|
||||||
type Project,
|
type Project,
|
||||||
} from "../../../lib/content";
|
} from "../../../lib/content";
|
||||||
|
|
||||||
@ -36,8 +37,22 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
const form = await request.formData();
|
const form = await request.formData();
|
||||||
const action = String(form.get("action") ?? "");
|
const action = String(form.get("action") ?? "");
|
||||||
|
|
||||||
if (action === "note") {
|
if (action === "note-save") {
|
||||||
await setNote(String(form.get("body") ?? ""));
|
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();
|
return back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
---
|
---
|
||||||
import Base from "../layouts/Base.astro";
|
import Base from "../layouts/Base.astro";
|
||||||
import { getNote } from "../lib/content";
|
import { getNotes } from "../lib/content";
|
||||||
import { renderMarkdown } from "../lib/md";
|
import { renderMarkdown } from "../lib/md";
|
||||||
|
|
||||||
const note = await getNote();
|
const notes = (await getNotes())
|
||||||
const html = note.body.trim() ? renderMarkdown(note.body) : "";
|
.slice()
|
||||||
|
.sort((a, b) => +new Date(b.date) - +new Date(a.date));
|
||||||
|
|
||||||
const fmt = (d: string) =>
|
const fmt = (d: string) =>
|
||||||
new Date(d)
|
new Date(d)
|
||||||
@ -12,17 +13,23 @@ const fmt = (d: string) =>
|
|||||||
.toUpperCase();
|
.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">
|
<section class="intro">
|
||||||
<h1>notes</h1>
|
<h1>notes</h1>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{
|
{
|
||||||
html ? (
|
notes.length ? (
|
||||||
<article class="notepage">
|
<div class="notelog">
|
||||||
<p class="notepage__date">{fmt(note.date)}</p>
|
{notes.map((n) => (
|
||||||
<div class="notepage__body" set:html={html} />
|
<article class="notelog__entry" id={n.id}>
|
||||||
</article>
|
<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>
|
<p class="notepage__empty">nothing noted yet.</p>
|
||||||
)
|
)
|
||||||
@ -30,36 +37,60 @@ const fmt = (d: string) =>
|
|||||||
</Base>
|
</Base>
|
||||||
|
|
||||||
<style>
|
<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-family: var(--mono);
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
letter-spacing: 0.16em;
|
letter-spacing: 0.16em;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
margin-bottom: 1.3rem;
|
text-decoration: none;
|
||||||
|
transition: color 0.16s ease;
|
||||||
|
}
|
||||||
|
.notelog__date a:hover {
|
||||||
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
.notepage__empty {
|
.notepage__empty {
|
||||||
color: var(--faint);
|
color: var(--faint);
|
||||||
}
|
}
|
||||||
.notepage__body {
|
.notelog__body {
|
||||||
max-width: 62ch;
|
max-width: 62ch;
|
||||||
}
|
}
|
||||||
.notepage__body :global(p) {
|
.notelog__body :global(p) {
|
||||||
margin: 0 0 1.1rem;
|
margin: 0 0 1.1rem;
|
||||||
}
|
}
|
||||||
.notepage__body :global(p:last-child) {
|
.notelog__body :global(p:last-child) {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
.notepage__body :global(a) {
|
.notelog__body :global(a) {
|
||||||
text-decoration-color: var(--accent);
|
text-decoration-color: var(--accent);
|
||||||
}
|
}
|
||||||
.notepage__body :global(ul) {
|
.notelog__body :global(ul) {
|
||||||
margin: 0 0 1.1rem;
|
margin: 0 0 1.1rem;
|
||||||
padding-left: 1.2rem;
|
padding-left: 1.2rem;
|
||||||
}
|
}
|
||||||
.notepage__body :global(li) {
|
.notelog__body :global(li) {
|
||||||
margin: 0 0 0.35rem;
|
margin: 0 0 0.35rem;
|
||||||
}
|
}
|
||||||
.notepage__body :global(code) {
|
.notelog__body :global(code) {
|
||||||
font-family: var(--mono);
|
font-family: var(--mono);
|
||||||
font-size: 0.88em;
|
font-size: 0.88em;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
@ -67,9 +98,9 @@ const fmt = (d: string) =>
|
|||||||
padding: 0.05em 0.3em;
|
padding: 0.05em 0.3em;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
.notepage__body :global(h2),
|
.notelog__body :global(h2),
|
||||||
.notepage__body :global(h3),
|
.notelog__body :global(h3),
|
||||||
.notepage__body :global(h4) {
|
.notelog__body :global(h4) {
|
||||||
font-family: var(--mono);
|
font-family: var(--mono);
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
letter-spacing: 0.14em;
|
letter-spacing: 0.14em;
|
||||||
@ -77,14 +108,14 @@ const fmt = (d: string) =>
|
|||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
margin: 1.8rem 0 0.7rem;
|
margin: 1.8rem 0 0.7rem;
|
||||||
}
|
}
|
||||||
.notepage__body :global(pre) {
|
.notelog__body :global(pre) {
|
||||||
background: var(--paper-2);
|
background: var(--paper-2);
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
.notepage__body :global(pre code) {
|
.notelog__body :global(pre code) {
|
||||||
background: none;
|
background: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user