who needs allat. maybe i want to live simply

This commit is contained in:
haetae 2025-08-08 03:00:12 -04:00
parent db51f003bb
commit 776fc1b38e
12 changed files with 113 additions and 182 deletions

View File

@ -17,12 +17,6 @@ export default defineConfig({
adapter: node({ adapter: node({
mode: "standalone", mode: "standalone",
}), }),
session: {
driver: "localstorage",
options: {
base: "app:",
},
},
experimental: { experimental: {
fonts: [ fonts: [
{ {

View File

@ -12,19 +12,7 @@ const Guestbook = defineTable({
}, },
}); });
const Comment = defineTable({
columns: {
id: column.number({ primaryKey: true }),
postId: column.text(),
replyId: column.number(),
username: column.text(),
website: column.text({ optional: true }),
comment: column.text({ multiline: true }),
published: column.date({ default: NOW }),
},
});
// https://astro.build/db/config // https://astro.build/db/config
export default defineDb({ export default defineDb({
tables: { Guestbook, Comment }, tables: { Guestbook },
}); });

View File

@ -1,21 +0,0 @@
import { defineAction } from "astro:actions";
import { z } from "astro:content";
import sanitize from "sanitize-html";
export const comments = {
addComment: defineAction({
accept: "form",
input: z.object({
postId: z.string(),
replyId: z.number().optional(),
name: z.string(),
website: z.string().url().optional(),
comment: z.string(),
}),
handler: async (input) => {
// sanitize but allow line breaks
sanitize(input.comment);
// post to comment server
},
}),
};

View File

@ -1,7 +1,6 @@
import { ActionError, defineAction } from "astro:actions"; import { ActionError, defineAction } from "astro:actions";
import { z } from "astro:content"; import { z } from "astro:content";
import { db, eq, Guestbook } from "astro:db"; import { db, eq, Guestbook } from "astro:db";
import bcrypt from "bcryptjs";
import sanitize from "sanitize-html"; import sanitize from "sanitize-html";
export const guestbook = { export const guestbook = {
@ -14,6 +13,8 @@ export const guestbook = {
}), }),
handler: async ({ username, website, message }) => { handler: async ({ username, website, message }) => {
// figure out how to add line breaks and THEN sanitize message // figure out how to add line breaks and THEN sanitize message
const addLine = message.replaceAll("/n", "<br/>");
sanitize(addLine);
const entry = await db.insert(Guestbook).values({ const entry = await db.insert(Guestbook).values({
username, username,
@ -31,7 +32,7 @@ export const guestbook = {
reply: z.string(), reply: z.string(),
}), }),
handler: async ({ id, reply }, context) => { handler: async ({ id, reply }, context) => {
if (!context.session?.get("pwd")) { if (context.url.hostname !== "127.0.0.1" || "localhost") {
throw new ActionError({ code: "UNAUTHORIZED" }); throw new ActionError({ code: "UNAUTHORIZED" });
} }
@ -44,32 +45,9 @@ export const guestbook = {
} }
// sanitize reply here // sanitize reply here
const update = await db.update(Guestbook).set({ reply }).where(eq(Guestbook.id, id)).returning(); const update = await db.update(Guestbook).set({ reply }).where(eq(Guestbook.id, id)).returning();
return update[0]; return update[0];
}, },
}), }),
login: defineAction({
accept: "form",
input: z.object({
password: z.string(),
}),
handler: async ({ password }, context) => {
// find env var here
if (password !== "super secret password") {
throw new ActionError({ code: "UNAUTHORIZED" });
}
const hash = await bcrypt.hash(password, 10);
context.session?.set("pwd", hash);
return { code: 200, message: "set the thing" };
}
}),
logout: defineAction({
accept: "form",
handler: async (_input, context) => {
context.session?.destroy();
return { code: 200, message: "set the thing" };
}
}),
}; };

View File

@ -1,9 +1,7 @@
import { comments } from "./comment";
import { contact } from "./contact"; import { contact } from "./contact";
import { guestbook } from "./guestbook"; import { guestbook } from "./guestbook";
export const server = { export const server = {
comments,
contact, contact,
guestbook, guestbook,
}; };

View File

@ -12,34 +12,6 @@
--ko-font: GulimChe, DotumChe, Gulim, Dotum, system-ui-ko, system-ui-ja, system-ui-zh-cn, system-ui-zh-tw, system-ui-zh-hk, monospace, sans-serif; --ko-font: GulimChe, DotumChe, Gulim, Dotum, system-ui-ko, system-ui-ja, system-ui-zh-cn, system-ui-zh-tw, system-ui-zh-hk, monospace, sans-serif;
--dotum-11-font: var(--dotumche-11), var(--dotum-11), var(--ko-font); --dotum-11-font: var(--dotumche-11), var(--dotum-11), var(--ko-font);
--dotum-12-font: var(--dotumche-12), var(--dotum-12), var(--ko-font); --dotum-12-font: var(--dotumche-12), var(--dotum-12), var(--ko-font);
@media screen and (prefers-color-scheme: light) {
--bg-color: #e6f2ef;
--fg-color: #151640;
--accent-color: #f783b0;
--secondary-color: #3f6d9e;
}
@media screen and (prefers-color-scheme: dark) {
--bg-color: #555568;
--fg-color: #f3eded;
--accent-color: #eeb9c7;
--secondary-color: #b9eedc;
}
}
.light {
--bg-color: #e6f2ef;
--fg-color: #151640;
--accent-color: #f783b0;
--secondary-color: #3f6d9e;
}
.dark {
--bg-color: #555568;
--fg-color: #f3eded;
--accent-color: #eeb9c7;
--secondary-color: #b9eedc;
} }
body { body {
@ -124,22 +96,4 @@ button, .button {
border-color: var(--bg-color); border-color: var(--bg-color);
box-shadow: 0 0 0 var(--fg-color); box-shadow: 0 0 0 var(--fg-color);
} }
}
.dark {
button, .button {
background-color: var(--accent-color);
color: var(--bg-color);
&:hover {
background-color: hsl(from var(--accent-color) h s calc(l - 10));
color: var(--bg-color);
}
&:active, &:focus {
color: var(--bg-color);
background-color: var(--secondary-color);
border-color: var(--bg-color);
}
}
} }

View File

@ -0,0 +1,50 @@
:root {
@media screen and (prefers-color-scheme: light) {
--bg-color: #e6f2ef;
--fg-color: #151640;
--accent-color: #f783b0;
--secondary-color: #3f6d9e;
}
@media screen and (prefers-color-scheme: dark) {
--bg-color: #555568;
--fg-color: #f3eded;
--accent-color: #eeb9c7;
--secondary-color: #b9eedc;
}
}
.light {
--bg-color: #e6f2ef;
--fg-color: #151640;
--accent-color: #f783b0;
--secondary-color: #3f6d9e;
}
.dark {
--bg-color: #555568;
--fg-color: #f3eded;
--accent-color: #eeb9c7;
--secondary-color: #b9eedc;
}
/* custom theme styling */
/* dark-specific styles */
.dark {
button, .button {
background-color: var(--accent-color);
color: var(--bg-color);
&:hover {
background-color: hsl(from var(--accent-color) h s calc(l - 10));
color: var(--bg-color);
}
&:active, &:focus {
color: var(--bg-color);
background-color: var(--secondary-color);
border-color: var(--bg-color);
}
}
}

3
src/env.d.ts vendored
View File

@ -1,3 +0,0 @@
interface Window {
Alpine: import("alpinejs").Alpine;
}

View File

@ -1,5 +1,6 @@
--- ---
import "$/styles/base.css"; import "$/styles/base.css";
import "$/styles/themes.css";
import { Font } from "astro:assets"; import { Font } from "astro:assets";
interface Props { title?: string; } interface Props { title?: string; }
@ -11,7 +12,6 @@ const { title = "haetae" }: Props = Astro.props;
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<meta name="pinterest" content="nopin nohover" /> <meta name="pinterest" content="nopin nohover" />
<meta name="robots" content="noai, noimageai" /> <meta name="robots" content="noai, noimageai" />
<Font cssVariable="--sq" preload /> <Font cssVariable="--sq" preload />

View File

@ -3,62 +3,71 @@ export const prerender = false;
import { actions } from "astro:actions"; import { actions } from "astro:actions";
import { db, desc, Guestbook } from "astro:db"; import { db, desc, Guestbook } from "astro:db";
import Layout from "@/layouts/Layout.astro";
import formatDate from "@/utils/formatDate";
const pwd = await Astro.session?.get("pwd"); if (!import.meta.env.DEV) {
console.error("you shouldn't be here...");
if (!pwd) { return Astro.redirect("/guestbook");
const fromUrl = Astro.url.pathname + Astro.url.search;
return Astro.redirect(`/guestbook/login?redirect=${fromUrl}`);
} }
const entries = await db.select().from(Guestbook).orderBy(desc(Guestbook.published)); const entries = await db.select().from(Guestbook).orderBy(desc(Guestbook.published));
--- ---
<h1>entries</h1> <Layout title="guestbook admin">
<h1>entries</h1>
<section> <section>
<table> <table>
<thead> <thead>
<tr> <tr>
<th>username</th> <th>username</th>
<th>website</th> <th>website</th>
<th>message</th> <th>message</th>
<th>published</th> <th>published</th>
<th>reply</th> <th>reply</th>
<th>edit</th> <th>edit</th>
</tr>
</thead>
<tbody>
{entries.map(entry => (
<tr id={`${entry.id}`}>
<td>{entry.username}</td>
<td>{entry.website}</td>
<td>{entry.message}</td>
<td>{entry.published}</td>
<td>{entry.reply}</td>
<td><button class="edit">edit</button></td>
</tr> </tr>
))} </thead>
</tbody> <tbody>
</table> {entries.map(entry => (
<tr id={`${entry.id}`}>
<td>{entry.username}</td>
<td>{entry.website}</td>
<td>{entry.message}</td>
<td>{formatDate(entry.published, false, 'MMMM D, YYYY')}</td>
<td>{entry.reply}</td>
<td><button class="edit">edit</button></td>
</tr>
))}
</tbody>
</table>
<dialog id="edit-entry"> <dialog id="edit-entry">
<form id="edit-entry-form" action={actions.guestbook.reply} method="post"> <form id="edit-entry-form" action={actions.guestbook.reply} method="post">
<input type="hidden" name="id" value="" /> <input type="hidden" name="id" id="entryId" value="" />
<p id="entry-username"></p> <p id="entry-username"></p>
<p id="entry-website"></p> <p id="entry-website"></p>
<div id="entry-message"></div> <div id="entry-message"></div>
<p id="entry-published"></p> <p id="entry-published"></p>
<label for="reply">Reply</label> <label for="reply">Reply</label>
<textarea name="reply" id="reply"></textarea> <textarea name="reply" id="reply"></textarea>
<button type="submit">Reply</button> <button type="submit">Reply</button>
</form> </form>
</dialog> </dialog>
</section> </section>
</Layout>
<form action={actions.guestbook.logout} method="post"> <style>
<button type="submit">logout</button> table {
</form> border-collapse: collapse;
td {
max-width: 30ch;
text-overflow: ellipsis;
}
}
</style>
<script> <script>
const modal = document.getElementById("edit-entry") as HTMLDialogElement; const modal = document.getElementById("edit-entry") as HTMLDialogElement;
@ -70,7 +79,7 @@ const entries = await db.select().from(Guestbook).orderBy(desc(Guestbook.publish
const data = row.cells; const data = row.cells;
const fields = document.querySelectorAll("[id^='entry-']"); const fields = document.querySelectorAll("[id^='entry-']");
(document.getElementById("id") as HTMLInputElement).value = id; (document.getElementById("entryId") as HTMLInputElement).value = id;
fields.forEach((field, i) => { fields.forEach((field, i) => {
field.innerHTML = data.item(i)!.innerText; field.innerHTML = data.item(i)!.innerText;
}); });

View File

@ -1,16 +0,0 @@
---
export const prerender = false;
import bcrypt from "bcryptjs";
if (Astro.request.method === "POST") {
const form = await Astro.request.formData();
const pwd = form.get("password");
if (pwd) {
const hashed = bcrypt.hashSync(pwd.toString(), 10);
Astro.session?.set("pwd", hashed);
return Astro.redirect("/guestbook/admin");
}
}
---

View File

@ -30,7 +30,7 @@ export function ficsLoader(loader: Loader) {
...valueWithoutDigest.data, ...valueWithoutDigest.data,
...chapters.length > 1 && { chapters: chapters }, ...chapters.length > 1 && { chapters: chapters },
}, },
}); });
if (chapters.length === 1) { if (chapters.length === 1) {
// i've committed unspeakable atrocities here // i've committed unspeakable atrocities here
const search = import.meta.glob<MarkdownInstance<any>>(`../content/fics/**/*.md`, { eager: true }); const search = import.meta.glob<MarkdownInstance<any>>(`../content/fics/**/*.md`, { eager: true });