diff --git a/_admin/guestbook.html b/_admin/guestbook.html new file mode 100644 index 0000000..7ac0c5a --- /dev/null +++ b/_admin/guestbook.html @@ -0,0 +1,92 @@ + + + + + +guestbook — admin + + + + + + +
+
+ ← admin +
guestbook
+
+ +
+
pending
+
+
loading…
+
+
+ +
+
approved
+
+
loading…
+
+
+
+ + + + diff --git a/_admin/index.html b/_admin/index.html index 621128f..327d45a 100644 --- a/_admin/index.html +++ b/_admin/index.html @@ -32,6 +32,7 @@ now status bookmarks photos + guestbook diff --git a/_api/src/index.js b/_api/src/index.js index 81432fc..70c7232 100644 --- a/_api/src/index.js +++ b/_api/src/index.js @@ -4,6 +4,7 @@ import { handleStatus } from './routes/status.js'; import { handleBookmarks } from './routes/bookmarks.js'; import { handlePhotos } from './routes/photos.js'; import { handleDiscord } from './routes/discord.js'; +import { handleGuestbook } from './routes/guestbook.js'; import { cors, corsHeaders } from './lib/cors.js'; export default { @@ -27,6 +28,8 @@ export default { response = await handlePhotos(request, env, path); else if (path === '/discord' || path.startsWith('/discord/')) response = await handleDiscord(request, env); + else if (path === '/guestbook' || path.startsWith('/guestbook/')) + response = await handleGuestbook(request, env, path); else response = new Response('not found', { status: 404 }); diff --git a/_api/src/routes/guestbook.js b/_api/src/routes/guestbook.js new file mode 100644 index 0000000..a68811b --- /dev/null +++ b/_api/src/routes/guestbook.js @@ -0,0 +1,53 @@ +import { requireAuth } from '../lib/auth.js'; +import { json } from '../lib/json.js'; + +export async function handleGuestbook(request, env, path) { + const method = request.method; + const id = path.split('/')[2] || null; + + if (method === 'GET') { + const wantsAll = new URL(request.url).searchParams.get('all') === '1'; + if (wantsAll) { + const denied = await requireAuth(request, env); + if (denied) return denied; + } + const list = await env.GUESTBOOK_KV.list({ prefix: 'gb:' }); + const items = await Promise.all( + list.keys + .sort((a, b) => b.name.localeCompare(a.name)) + .map(k => env.GUESTBOOK_KV.get(k.name, 'json')) + ); + const entries = items.filter(Boolean).filter(e => wantsAll || e.approved); + return json({ entries }); + } + + if (method === 'POST') { + const body = await request.json().catch(() => ({})); + const name = String(body.name || '').trim().slice(0, 60); + const message = String(body.message || '').trim().slice(0, 500); + if (!name || !message) return json({ error: 'name and message required' }, 400); + const ts = Date.now(); + const entry = { id: `${ts}`, name, message, approved: false, date: new Date(ts).toISOString() }; + await env.GUESTBOOK_KV.put(`gb:${ts}`, JSON.stringify(entry)); + return json({ ok: true }, 201); + } + + const denied = await requireAuth(request, env); + if (denied) return denied; + + if (method === 'PUT' && id) { + const existing = await env.GUESTBOOK_KV.get(`gb:${id}`, 'json'); + if (!existing) return json({ error: 'not found' }, 404); + const body = await request.json().catch(() => ({})); + const entry = { ...existing, ...body, id: existing.id, date: existing.date }; + await env.GUESTBOOK_KV.put(`gb:${id}`, JSON.stringify(entry)); + return json(entry); + } + + if (method === 'DELETE' && id) { + await env.GUESTBOOK_KV.delete(`gb:${id}`); + return json({ ok: true }); + } + + return json({ error: 'not found' }, 404); +} diff --git a/_api/wrangler.toml b/_api/wrangler.toml index 9e7b6b9..8eb9db3 100644 --- a/_api/wrangler.toml +++ b/_api/wrangler.toml @@ -18,6 +18,10 @@ id = "REPLACE_WITH_BOOKMARKS_KV_ID" binding = "PHOTOS_KV" id = "REPLACE_WITH_PHOTOS_KV_ID" +[[kv_namespaces]] +binding = "GUESTBOOK_KV" +id = "REPLACE_WITH_GUESTBOOK_KV_ID" + [[r2_buckets]] binding = "PHOTOS_R2" bucket_name = "jxherc-photos" diff --git a/guestbook.html b/guestbook.html new file mode 100644 index 0000000..921c0c7 --- /dev/null +++ b/guestbook.html @@ -0,0 +1,122 @@ + + + + + +guestbook — jxherc + + + + + + + + + + + + + +
+
+ spongebob my eyes meme +

are you absolutely motherfucking sure you wanna switch to light mode

+ + +
+
+ +
+ jxherc + +
+
leave a note
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
entries
+
loading…
+
+ +
+
+ + + + + diff --git a/index.html b/index.html index 68ad435..b689bd2 100644 --- a/index.html +++ b/index.html @@ -80,6 +80,10 @@ bookmarks + + guestbook + +
diff --git a/style.css b/style.css index 0929f94..bcabe74 100644 --- a/style.css +++ b/style.css @@ -383,6 +383,42 @@ a.kv-row:hover .arrow { transform: translate(3px, -3px); color: var(--muted); } line-height: 1.55; } +/* =================== GUESTBOOK =================== */ +.gb-form { display: flex; flex-direction: column; gap: 1rem; } +.gb-field { display: flex; flex-direction: column; gap: 0.35rem; } +.gb-label { font-size: 0.7rem; color: var(--muted); letter-spacing: 0.04em; font-weight: 500; } +.gb-input { + background: none; + border: 1px solid var(--faint); + border-radius: 2px; + color: var(--text); + font-family: inherit; + font-size: 0.875rem; + padding: 0.6rem 0.75rem; + transition: border-color 0.2s ease; + outline: none; + width: 100%; +} +.gb-input::placeholder { color: var(--faint); } +.gb-input:focus { border-color: var(--muted); } +.gb-textarea { resize: vertical; min-height: 88px; line-height: 1.55; } +.gb-actions { display: flex; align-items: baseline; gap: 1rem; flex-wrap: wrap; margin-top: 0.25rem; } +.gb-submit { + background: none; + border: 1px solid var(--faint); + border-radius: 2px; + color: var(--muted); + font-family: inherit; + font-size: 0.72rem; + letter-spacing: 0.04em; + padding: 0.5rem 1rem; + cursor: pointer; + transition: border-color 0.2s ease, color 0.2s ease; +} +.gb-submit:hover:not(:disabled) { border-color: var(--muted); color: var(--text); } +.gb-submit:disabled { opacity: 0.5; cursor: default; } +.gb-status { font-size: 0.72rem; color: var(--faint); } + /* Page edit stamp */ .page-edit-stamp { font-size: 0.62rem;