diff --git a/.gitignore b/.gitignore index 016b59e..b67e3c5 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,6 @@ pnpm-debug.log* # jetbrains setting folder .idea/ + +# dont grab database files you dont need that +local.db \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 3662b37..1170042 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,12 @@ { - "typescript.tsdk": "node_modules/typescript/lib" + "typescript.tsdk": "node_modules/typescript/lib", + "sqltools.useNodeRuntime": true, + "sqltools.connections": [ + { + "previewLimit": 50, + "driver": "SQLite", + "database": "./guestbook.db", + "name": "test" + } + ] } \ No newline at end of file diff --git a/astro.config.mjs b/astro.config.mjs index 34eb68b..cdc084c 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -3,8 +3,8 @@ import { defineConfig } from 'astro/config'; import { modifiedTime } from './src/utils/lastModified.mjs'; import mdx from "@astrojs/mdx"; import db from "@astrojs/db"; - import node from "@astrojs/node"; +import devOnlyRoutes from '@fujocoded/astro-dev-only'; // https://astro.build/config export default defineConfig({ @@ -13,7 +13,14 @@ export default defineConfig({ remarkPlugins: [modifiedTime], smartypants: false, }, - integrations: [mdx(), db()], + integrations: [ + mdx(), + db(), + devOnlyRoutes({ + // dryRun: true, + routePatterns: ["/guestbook/admin"] + }), + ], adapter: node({ mode: "standalone", }), diff --git a/bun.lock b/bun.lock index 8966634..04b3c2a 100644 --- a/bun.lock +++ b/bun.lock @@ -6,11 +6,11 @@ "dependencies": { "@astrojs/db": "^0.16.1", "@astrojs/mdx": "^4.3.3", - "@astrojs/node": "^9.3.3", + "@astrojs/node": "^9.4.0", "@astrojs/rss": "4.0.12", + "@fujocoded/astro-dev-only": "0.0.3", "astro": "5.12.3", "astro-breadcrumbs": "^3.3.1", - "bcryptjs": "^3.0.2", "dayjs": "^1.11.13", "markdown-it": "^14.1.0", "node-html-parser": "^7.0.1", @@ -18,7 +18,7 @@ }, "devDependencies": { "@types/markdown-it": "^14.1.2", - "@types/node": "^22.17.0", + "@types/node": "^22.17.1", "@types/sanitize-html": "^2.16.0", }, }, @@ -34,7 +34,7 @@ "@astrojs/mdx": ["@astrojs/mdx@4.3.3", "", { "dependencies": { "@astrojs/markdown-remark": "6.3.5", "@mdx-js/mdx": "^3.1.0", "acorn": "^8.14.1", "es-module-lexer": "^1.6.0", "estree-util-visit": "^2.0.0", "hast-util-to-html": "^9.0.5", "kleur": "^4.1.5", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-smartypants": "^3.0.2", "source-map": "^0.7.4", "unist-util-visit": "^5.0.0", "vfile": "^6.0.3" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-+9+xGP2TBXxcm84cpiq4S9JbuHOHM1fcvREfqW7VHxlUyfUQPByoJ9YYliqHkLS6BMzG+O/+o7n8nguVhuEv4w=="], - "@astrojs/node": ["@astrojs/node@9.3.3", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.1", "send": "^1.2.0", "server-destroy": "^1.0.1" }, "peerDependencies": { "astro": "^5.3.0" } }, "sha512-5jVuDbSxrY7rH7H+6QoRiN78AITLobYXWu+t1A2wRaFPKywaXNr8YHSXfOE4i2YN4c+VqMCv83SjZLWjTK6f9w=="], + "@astrojs/node": ["@astrojs/node@9.4.0", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.1", "send": "^1.2.0", "server-destroy": "^1.0.1" }, "peerDependencies": { "astro": "^5.3.0" } }, "sha512-Gxs0iVUvOmQmK+H1DBoabcgvdSDg277SwbujRv2cUBlnpcOTJQDFRhRvyJ7G+Zkd06/jhRphsTTmmrBY0PqI4g=="], "@astrojs/prism": ["@astrojs/prism@3.3.0", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ=="], @@ -106,6 +106,8 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.8", "", { "os": "win32", "cpu": "x64" }, "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw=="], + "@fujocoded/astro-dev-only": ["@fujocoded/astro-dev-only@0.0.3", "", {}, "sha512-BOLYZcivrJVUA60d4R2+yEGwDJZ+3Z/zndLACxk6YpClu5ETl0adghwCCt9yIHsq79KDx/Or8LH2aqS6fVlQbg=="], + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="], "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="], @@ -262,7 +264,7 @@ "@types/nlcst": ["@types/nlcst@2.0.3", "", { "dependencies": { "@types/unist": "*" } }, "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA=="], - "@types/node": ["@types/node@22.17.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-bbAKTCqX5aNVryi7qXVMi+OkB3w/OyblodicMbvE38blyAz7GxXf6XYhklokijuPwwVg9sDLKRxt0ZHXQwZVfQ=="], + "@types/node": ["@types/node@22.17.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-y3tBaz+rjspDTylNjAX37jEC3TETEFGNJL6uQDxwF9/8GLLIjW1rvVHlynyuUKMnMr1Roq8jOv3vkopBjC4/VA=="], "@types/sanitize-html": ["@types/sanitize-html@2.16.0", "", { "dependencies": { "htmlparser2": "^8.0.0" } }, "sha512-l6rX1MUXje5ztPT0cAFtUayXF06DqPhRyfVXareEN5gGCFaP/iwsxIyKODr9XDhfxPpN6vXUFNfo5kZMXCxBtw=="], @@ -304,8 +306,6 @@ "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "bcryptjs": ["bcryptjs@3.0.2", "", { "bin": { "bcrypt": "bin/bcrypt" } }, "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog=="], - "blob-to-buffer": ["blob-to-buffer@1.2.9", "", {}, "sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA=="], "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], @@ -952,6 +952,10 @@ "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "@types/fontkit/@types/node": ["@types/node@22.17.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-bbAKTCqX5aNVryi7qXVMi+OkB3w/OyblodicMbvE38blyAz7GxXf6XYhklokijuPwwVg9sDLKRxt0ZHXQwZVfQ=="], + + "@types/ws/@types/node": ["@types/node@22.17.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-bbAKTCqX5aNVryi7qXVMi+OkB3w/OyblodicMbvE38blyAz7GxXf6XYhklokijuPwwVg9sDLKRxt0ZHXQwZVfQ=="], + "ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], diff --git a/package.json b/package.json index 8864d8a..68fe9ad 100644 --- a/package.json +++ b/package.json @@ -4,18 +4,18 @@ "version": "0.0.1", "scripts": { "dev": "astro dev", - "build": "astro build", + "build": "astro build --remote", "preview": "astro preview", "astro": "astro" }, "dependencies": { "@astrojs/db": "^0.16.1", "@astrojs/mdx": "^4.3.3", - "@astrojs/node": "^9.3.3", + "@astrojs/node": "^9.4.0", "@astrojs/rss": "4.0.12", + "@fujocoded/astro-dev-only": "0.0.3", "astro": "5.12.3", "astro-breadcrumbs": "^3.3.1", - "bcryptjs": "^3.0.2", "dayjs": "^1.11.13", "markdown-it": "^14.1.0", "node-html-parser": "^7.0.1", @@ -23,7 +23,7 @@ }, "devDependencies": { "@types/markdown-it": "^14.1.2", - "@types/node": "^22.17.0", + "@types/node": "^22.17.1", "@types/sanitize-html": "^2.16.0" } } diff --git a/src/actions/guestbook.ts b/src/actions/guestbook.ts index 97eaf5a..8ab6e4a 100644 --- a/src/actions/guestbook.ts +++ b/src/actions/guestbook.ts @@ -1,6 +1,6 @@ import { ActionError, defineAction } from "astro:actions"; import { z } from "astro:content"; -import { db, eq, Guestbook } from "astro:db"; +import { db, eq, Guestbook, isDbError } from "astro:db"; import sanitize from "sanitize-html"; export const guestbook = { @@ -12,42 +12,92 @@ export const guestbook = { message: z.string().min(1, "Can't be that short..."), }), handler: async ({ username, website, message }) => { - // figure out how to add line breaks and THEN sanitize message - const addLine = message.replaceAll("/n", "
"); - sanitize(addLine); + const addLine = message.replaceAll(/\r?\n/g, "
"); + const sanitized = sanitize(addLine, { allowedTags: ["br"] }); - const entry = await db.insert(Guestbook).values({ - username, - website, - message, - }).returning(); - - return entry[0]; + try { + const entry = await db.insert(Guestbook).values({ + username, + website, + message: sanitized, + }).returning(); + + return entry[0]; + } catch (e) { + if (isDbError(e)) { + return new Response(`Cannot insert entry\n\n${e.message}`, { status: 400 }); + } + return new Response('An unexpected error occurred', { status: 500 }); + } }, }), - reply: defineAction({ - accept: "form", - input: z.object({ - id: z.number(), - reply: z.string(), + ...import.meta.env.DEV && { + reply: defineAction({ + accept: "form", + input: z.object({ + id: z.coerce.number(), + reply: z.string(), + }), + handler: async ({ id, reply }) => { + if (!import.meta.env.DEV) { + throw new ActionError({ code: "UNAUTHORIZED" }); + } + + const entry = await db.select().from(Guestbook).where(eq(Guestbook.id, id)); + if (!entry) { + throw new ActionError({ + code: "NOT_FOUND", + message: "That entry doesn't exist!" + }); + } + + const addLine = reply.replaceAll(/\r?\n/g, "
"); + const sanitized = sanitize(addLine, { allowedTags: ["br"] }); + + try { + const update = await db.update(Guestbook).set({ + reply: sanitized, + updated: new Date(), + }).where(eq(Guestbook.id, id)).returning(); + + return update[0]; + } catch (e) { + if (isDbError(e)) { + return new Response(`Cannot update entry\n\n${e.message}`, { status: 400 }); + } + return new Response('An unexpected error occurred', { status: 500 }); + } + }, }), - handler: async ({ id, reply }, context) => { - if (context.url.hostname !== "127.0.0.1" || "localhost") { - throw new ActionError({ code: "UNAUTHORIZED" }); - } + deleteEntry: defineAction({ + accept: "form", + input: z.object({ + id: z.coerce.number() + }), + handler: async ({ id }) => { + if (!import.meta.env.DEV) { + throw new ActionError({ code: "UNAUTHORIZED" }); + } + + const entry = await db.select().from(Guestbook).where(eq(Guestbook.id, id)); + if (!entry) { + throw new ActionError({ + code: "NOT_FOUND", + message: "That entry doesn't exist!" + }); + } - const entry = await db.select().from(Guestbook).where(eq(Guestbook.id, id)); - if (!entry) { - throw new ActionError({ - code: "NOT_FOUND", - message: "That entry doesn't exist!" - }); - } - - // sanitize reply here - - const update = await db.update(Guestbook).set({ reply }).where(eq(Guestbook.id, id)).returning(); - return update[0]; - }, - }), + try { + const entry = await db.delete(Guestbook).where(eq(Guestbook.id, id)).returning(); + + return entry[0]; + } catch (e) { + if (isDbError(e)) { + return new Response(`Cannot update entry\n\n${e.message}`, { status: 400 }); + } + return new Response('An unexpected error occurred', { status: 500 }); + } + }, + }), + }, }; \ No newline at end of file diff --git a/src/assets/acnl-bulletin.png b/src/assets/acnl-bulletin.png new file mode 100644 index 0000000..0ef22db Binary files /dev/null and b/src/assets/acnl-bulletin.png differ diff --git a/src/assets/buttons.png b/src/assets/guild-bbs-buttons.png similarity index 100% rename from src/assets/buttons.png rename to src/assets/guild-bbs-buttons.png diff --git a/src/assets/moon-bullet.gif b/src/assets/moon-bullet.gif new file mode 100644 index 0000000..29a3cac Binary files /dev/null and b/src/assets/moon-bullet.gif differ diff --git a/src/assets/border.png b/src/assets/pmd-border.png similarity index 100% rename from src/assets/border.png rename to src/assets/pmd-border.png diff --git a/src/assets/frame.png b/src/assets/pmd-frame.png similarity index 100% rename from src/assets/frame.png rename to src/assets/pmd-frame.png diff --git a/src/assets/star-bullet.gif b/src/assets/star-bullet.gif new file mode 100644 index 0000000..2947b89 Binary files /dev/null and b/src/assets/star-bullet.gif differ diff --git a/src/components/Dialog.astro b/src/components/Dialog.astro new file mode 100644 index 0000000..fd1010b --- /dev/null +++ b/src/components/Dialog.astro @@ -0,0 +1,90 @@ +--- +interface Props { + id?: string; + title?: string; + class?: string; +} + +const { id, title, class: className, ...rest } = Astro.props; +--- + + +
+ + {title &&
{title}
} + +
+
+ +
+ +
+
+ + \ No newline at end of file diff --git a/src/components/Entries.astro b/src/components/Entries.astro index a9f25da..25312ac 100644 --- a/src/components/Entries.astro +++ b/src/components/Entries.astro @@ -1,6 +1,7 @@ --- import { db, desc, Guestbook } from "astro:db"; import formatDate from "@/utils/formatDate"; +import pikachu from "$/images/portrait-0025.png"; const entries = await db.select().from(Guestbook).orderBy(desc(Guestbook.published)); --- @@ -20,13 +21,14 @@ const entries = await db.select().from(Guestbook).orderBy(desc(Guestbook.publish - {entry.message} +
+ +
- {entry.reply && help -
- + {entry.reply &&
+ a portrait of pikachu

Reply to {entry.username}

@@ -35,16 +37,24 @@ const entries = await db.select().from(Guestbook).orderBy(desc(Guestbook.publish
- {entry.reply} +
+ +
} ))} + {entries.length === 0 && ( +
+

Huh...

+

There's nothing here! Want to be the first to comment?

+
+ )} \ No newline at end of file diff --git a/src/layouts/About.astro b/src/layouts/About.astro index 1ec42e2..e515f89 100644 --- a/src/layouts/About.astro +++ b/src/layouts/About.astro @@ -5,8 +5,8 @@ import Layout from "./Layout.astro"; import Navbar from "~/Navbar.astro"; import Figure from "~/Figure.astro"; -import border from "$/border.png"; -import frame from "$/frame.png"; +import border from "$/pmd-border.png"; +import frame from "$/pmd-frame.png"; type Props = MarkdownLayoutProps<{ avatar?: string; @@ -30,6 +30,10 @@ const { frontmatter } = Astro.props;
+ + \ No newline at end of file diff --git a/src/pages/guestbook/index.astro b/src/pages/guestbook/index.astro index b8f1a6d..4c4bd0b 100644 --- a/src/pages/guestbook/index.astro +++ b/src/pages/guestbook/index.astro @@ -5,6 +5,7 @@ import { Font } from "astro:assets"; import Layout from "@/layouts/Layout.astro"; import speech from "$/speech.png"; import Entries from "~/Entries.astro"; +import Dialog from "~/Dialog.astro"; const result = Astro.getActionResult(actions.guestbook.addEntry); const inputErrors = isInputError(result?.error) ? result.error.fields : {}; @@ -19,6 +20,7 @@ const inputErrors = isInputError(result?.error) ? result.error.fields : {};

Guestbook

+ {import.meta.env.DEV &&

for your eyes only...

}
@@ -43,20 +45,9 @@ const inputErrors = isInputError(result?.error) ? result.error.fields : {};

Loading...

- - - - - - Successfully posted! Refreshing in 5 seconds. - -
- - + + Successfully posted! Refreshing in 5 seconds. + @@ -91,70 +82,6 @@ const inputErrors = isInputError(result?.error) ? result.error.fields : {}; color: color-mix(in oklab, var(--fg-color) 80%, var(--bg-color)); } } - - #notification { - margin: auto; - max-width: 35ch; - color: var(--fg-color); - background: var(--bg-color); - transition: - display 1s allow-discrete, - overlay 1s allow-discrete; - animation: fadeOut 1s forwards; - - menu { - position: absolute; - top: 0; - right: 0; - width: calc(100% - 4px); - display: flex; - justify-content: end; - padding: 2px; - margin: 2px 2px 0; - background-color: color-mix(in oklab, var(--bg-color) 95%, var(--fg-color)); - border: 2px solid var(--fg-color); - line-height: 1; - - button { - padding: 0; - box-shadow: none; - line-height: 1; - transform: none; - display: grid; - place-content: center; - width: 44px; - height: 44px; - - span { transform: translateY(-2px); } - - &:focus { - border: 4px inset var(--secondary-color); - outline: 2px solid var(--fg-color); - box-shadow: none; - - span { transform: translateY(0); } - } - } - } - - form { - margin-top: calc(44px + 2px); - } - - &[open] { - animation: fadeIn 1.0s forwards; - } - } - - @keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } - } - - @keyframes fadeOut { - from { opacity: 1; } - to { opacity: 0; } - }