adding a lot to admin, mess around with dev-only routes
3
.gitignore
vendored
@ -22,3 +22,6 @@ pnpm-debug.log*
|
|||||||
|
|
||||||
# jetbrains setting folder
|
# jetbrains setting folder
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
|
# dont grab database files you dont need that
|
||||||
|
local.db
|
11
.vscode/settings.json
vendored
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
@ -3,8 +3,8 @@ import { defineConfig } from 'astro/config';
|
|||||||
import { modifiedTime } from './src/utils/lastModified.mjs';
|
import { modifiedTime } from './src/utils/lastModified.mjs';
|
||||||
import mdx from "@astrojs/mdx";
|
import mdx from "@astrojs/mdx";
|
||||||
import db from "@astrojs/db";
|
import db from "@astrojs/db";
|
||||||
|
|
||||||
import node from "@astrojs/node";
|
import node from "@astrojs/node";
|
||||||
|
import devOnlyRoutes from '@fujocoded/astro-dev-only';
|
||||||
|
|
||||||
// https://astro.build/config
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
@ -13,7 +13,14 @@ export default defineConfig({
|
|||||||
remarkPlugins: [modifiedTime],
|
remarkPlugins: [modifiedTime],
|
||||||
smartypants: false,
|
smartypants: false,
|
||||||
},
|
},
|
||||||
integrations: [mdx(), db()],
|
integrations: [
|
||||||
|
mdx(),
|
||||||
|
db(),
|
||||||
|
devOnlyRoutes({
|
||||||
|
// dryRun: true,
|
||||||
|
routePatterns: ["/guestbook/admin"]
|
||||||
|
}),
|
||||||
|
],
|
||||||
adapter: node({
|
adapter: node({
|
||||||
mode: "standalone",
|
mode: "standalone",
|
||||||
}),
|
}),
|
||||||
|
18
bun.lock
@ -6,11 +6,11 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/db": "^0.16.1",
|
"@astrojs/db": "^0.16.1",
|
||||||
"@astrojs/mdx": "^4.3.3",
|
"@astrojs/mdx": "^4.3.3",
|
||||||
"@astrojs/node": "^9.3.3",
|
"@astrojs/node": "^9.4.0",
|
||||||
"@astrojs/rss": "4.0.12",
|
"@astrojs/rss": "4.0.12",
|
||||||
|
"@fujocoded/astro-dev-only": "0.0.3",
|
||||||
"astro": "5.12.3",
|
"astro": "5.12.3",
|
||||||
"astro-breadcrumbs": "^3.3.1",
|
"astro-breadcrumbs": "^3.3.1",
|
||||||
"bcryptjs": "^3.0.2",
|
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"node-html-parser": "^7.0.1",
|
"node-html-parser": "^7.0.1",
|
||||||
@ -18,7 +18,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/markdown-it": "^14.1.2",
|
"@types/markdown-it": "^14.1.2",
|
||||||
"@types/node": "^22.17.0",
|
"@types/node": "^22.17.1",
|
||||||
"@types/sanitize-html": "^2.16.0",
|
"@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/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=="],
|
"@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=="],
|
"@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-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=="],
|
"@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/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=="],
|
"@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=="],
|
"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=="],
|
"blob-to-buffer": ["blob-to-buffer@1.2.9", "", {}, "sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA=="],
|
||||||
|
|
||||||
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
|
"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=="],
|
"@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=="],
|
"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=="],
|
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
|
@ -4,18 +4,18 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"build": "astro build",
|
"build": "astro build --remote",
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
"astro": "astro"
|
"astro": "astro"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/db": "^0.16.1",
|
"@astrojs/db": "^0.16.1",
|
||||||
"@astrojs/mdx": "^4.3.3",
|
"@astrojs/mdx": "^4.3.3",
|
||||||
"@astrojs/node": "^9.3.3",
|
"@astrojs/node": "^9.4.0",
|
||||||
"@astrojs/rss": "4.0.12",
|
"@astrojs/rss": "4.0.12",
|
||||||
|
"@fujocoded/astro-dev-only": "0.0.3",
|
||||||
"astro": "5.12.3",
|
"astro": "5.12.3",
|
||||||
"astro-breadcrumbs": "^3.3.1",
|
"astro-breadcrumbs": "^3.3.1",
|
||||||
"bcryptjs": "^3.0.2",
|
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"node-html-parser": "^7.0.1",
|
"node-html-parser": "^7.0.1",
|
||||||
@ -23,7 +23,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/markdown-it": "^14.1.2",
|
"@types/markdown-it": "^14.1.2",
|
||||||
"@types/node": "^22.17.0",
|
"@types/node": "^22.17.1",
|
||||||
"@types/sanitize-html": "^2.16.0"
|
"@types/sanitize-html": "^2.16.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +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, isDbError } from "astro:db";
|
||||||
import sanitize from "sanitize-html";
|
import sanitize from "sanitize-html";
|
||||||
|
|
||||||
export const guestbook = {
|
export const guestbook = {
|
||||||
@ -12,42 +12,92 @@ export const guestbook = {
|
|||||||
message: z.string().min(1, "Can't be that short..."),
|
message: z.string().min(1, "Can't be that short..."),
|
||||||
}),
|
}),
|
||||||
handler: async ({ username, website, message }) => {
|
handler: async ({ username, website, message }) => {
|
||||||
// figure out how to add line breaks and THEN sanitize message
|
const addLine = message.replaceAll(/\r?\n/g, "<br />");
|
||||||
const addLine = message.replaceAll("/n", "<br/>");
|
const sanitized = sanitize(addLine, { allowedTags: ["br"] });
|
||||||
sanitize(addLine);
|
|
||||||
|
|
||||||
const entry = await db.insert(Guestbook).values({
|
try {
|
||||||
username,
|
const entry = await db.insert(Guestbook).values({
|
||||||
website,
|
username,
|
||||||
message,
|
website,
|
||||||
}).returning();
|
message: sanitized,
|
||||||
|
}).returning();
|
||||||
return entry[0];
|
|
||||||
|
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({
|
...import.meta.env.DEV && {
|
||||||
accept: "form",
|
reply: defineAction({
|
||||||
input: z.object({
|
accept: "form",
|
||||||
id: z.number(),
|
input: z.object({
|
||||||
reply: z.string(),
|
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, "<br />");
|
||||||
|
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) => {
|
deleteEntry: defineAction({
|
||||||
if (context.url.hostname !== "127.0.0.1" || "localhost") {
|
accept: "form",
|
||||||
throw new ActionError({ code: "UNAUTHORIZED" });
|
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));
|
try {
|
||||||
if (!entry) {
|
const entry = await db.delete(Guestbook).where(eq(Guestbook.id, id)).returning();
|
||||||
throw new ActionError({
|
|
||||||
code: "NOT_FOUND",
|
return entry[0];
|
||||||
message: "That entry doesn't exist!"
|
} catch (e) {
|
||||||
});
|
if (isDbError(e)) {
|
||||||
}
|
return new Response(`Cannot update entry\n\n${e.message}`, { status: 400 });
|
||||||
|
}
|
||||||
// sanitize reply here
|
return new Response('An unexpected error occurred', { status: 500 });
|
||||||
|
}
|
||||||
const update = await db.update(Guestbook).set({ reply }).where(eq(Guestbook.id, id)).returning();
|
},
|
||||||
return update[0];
|
}),
|
||||||
},
|
},
|
||||||
}),
|
|
||||||
};
|
};
|
BIN
src/assets/acnl-bulletin.png
Normal file
After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
BIN
src/assets/moon-bullet.gif
Normal file
After Width: | Height: | Size: 61 B |
Before Width: | Height: | Size: 411 B After Width: | Height: | Size: 411 B |
Before Width: | Height: | Size: 689 B After Width: | Height: | Size: 689 B |
BIN
src/assets/star-bullet.gif
Normal file
After Width: | Height: | Size: 62 B |
90
src/components/Dialog.astro
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
---
|
||||||
|
interface Props {
|
||||||
|
id?: string;
|
||||||
|
title?: string;
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id, title, class: className, ...rest } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<dialog {id} class:list={className} {...rest}>
|
||||||
|
<form method="dialog">
|
||||||
|
<menu>
|
||||||
|
{title && <div class="title">{title}</div>}
|
||||||
|
<button type="submit" aria-label="Close">
|
||||||
|
<span>x</span>
|
||||||
|
</button>
|
||||||
|
</menu>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
dialog {
|
||||||
|
margin: auto;
|
||||||
|
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;
|
||||||
|
align-items: center;
|
||||||
|
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;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
flex: 1;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
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); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content { 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; }
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
import { db, desc, Guestbook } from "astro:db";
|
import { db, desc, Guestbook } from "astro:db";
|
||||||
import formatDate from "@/utils/formatDate";
|
import formatDate from "@/utils/formatDate";
|
||||||
|
import pikachu from "$/images/portrait-0025.png";
|
||||||
|
|
||||||
const entries = await db.select().from(Guestbook).orderBy(desc(Guestbook.published));
|
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
|
|||||||
</time>
|
</time>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{entry.message}
|
<div class="content">
|
||||||
|
<Fragment set:html={entry.message} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
{entry.reply && <img src="" alt="help" />
|
{entry.reply && <article class="reply" id={`reply-${entry.username}-${entry.id}`}>
|
||||||
<article class="reply" id={`reply-${entry.username}-${entry.id}`}>
|
<img src={pikachu.src} width="80" height="80" alt="a portrait of pikachu" />
|
||||||
<img src="" alt="" />
|
|
||||||
<div class="entry">
|
<div class="entry">
|
||||||
<header>
|
<header>
|
||||||
<h1>Reply to {entry.username}</h1>
|
<h1>Reply to {entry.username}</h1>
|
||||||
@ -35,16 +37,24 @@ const entries = await db.select().from(Guestbook).orderBy(desc(Guestbook.publish
|
|||||||
</time>
|
</time>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{entry.reply}
|
<div class="content">
|
||||||
|
<Fragment set:html={entry.reply} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>}
|
</article>}
|
||||||
</>
|
</>
|
||||||
))}
|
))}
|
||||||
|
{entries.length === 0 && (
|
||||||
|
<article>
|
||||||
|
<h1>Huh...</h1>
|
||||||
|
<p>There's nothing here! Want to be the first to comment?</p>
|
||||||
|
</article>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#entries {
|
#entries {
|
||||||
margin: 2rem 0 4rem;
|
margin: 2rem 0 3rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
@ -73,7 +83,7 @@ const entries = await db.select().from(Guestbook).orderBy(desc(Guestbook.publish
|
|||||||
|
|
||||||
header {
|
header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: start;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
@ -85,6 +95,8 @@ const entries = await db.select().from(Guestbook).orderBy(desc(Guestbook.publish
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content { line-height: 1; }
|
||||||
|
|
||||||
@media screen and (width < 1000px) {
|
@media screen and (width < 1000px) {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
padding: 1rem 1.5rem;
|
padding: 1rem 1.5rem;
|
||||||
@ -92,6 +104,8 @@ const entries = await db.select().from(Guestbook).orderBy(desc(Guestbook.publish
|
|||||||
}
|
}
|
||||||
|
|
||||||
.reply {
|
.reply {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
align-self: end;
|
||||||
|
min-width: min(100%, 45ch);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
@ -5,8 +5,8 @@ import Layout from "./Layout.astro";
|
|||||||
import Navbar from "~/Navbar.astro";
|
import Navbar from "~/Navbar.astro";
|
||||||
import Figure from "~/Figure.astro";
|
import Figure from "~/Figure.astro";
|
||||||
|
|
||||||
import border from "$/border.png";
|
import border from "$/pmd-border.png";
|
||||||
import frame from "$/frame.png";
|
import frame from "$/pmd-frame.png";
|
||||||
|
|
||||||
type Props = MarkdownLayoutProps<{
|
type Props = MarkdownLayoutProps<{
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
@ -30,6 +30,10 @@ const { frontmatter } = Astro.props;
|
|||||||
<slot />
|
<slot />
|
||||||
</article>
|
</article>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
hi
|
||||||
|
</footer>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<style define:vars={{ borderImage: `url(${border.src})`, frameImage: `url(${frame.src})` }}>
|
<style define:vars={{ borderImage: `url(${border.src})`, frameImage: `url(${frame.src})` }}>
|
||||||
@ -67,6 +71,8 @@ const { frontmatter } = Astro.props;
|
|||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
li::marker { content: " "; }
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.avatar) {
|
:global(.avatar) {
|
||||||
|
@ -3,12 +3,12 @@ import { Font, Image } from "astro:assets";
|
|||||||
import { getCollection } from "astro:content";
|
import { getCollection } from "astro:content";
|
||||||
import Layout from "./Layout.astro";
|
import Layout from "./Layout.astro";
|
||||||
import Navbar from "~/Navbar.astro";
|
import Navbar from "~/Navbar.astro";
|
||||||
|
import formatDate from "@/utils/formatDate";
|
||||||
import moods from "@/utils/moods";
|
import moods from "@/utils/moods";
|
||||||
|
|
||||||
import outerBBS from "$/guild-bbs.png";
|
import outerBBS from "$/guild-bbs.png";
|
||||||
import innerBBS from "$/guild-bbs-content.png";
|
import innerBBS from "$/guild-bbs-content.png";
|
||||||
import sideBBS from "$/guild-bbs-list.png";
|
import sideBBS from "$/guild-bbs-list.png";
|
||||||
import formatDate from "@/utils/formatDate";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id?: string;
|
id?: string;
|
||||||
@ -159,6 +159,7 @@ blog.sort((a, b) => b.data.pubDate!.valueOf() - a.data.pubDate!.valueOf());
|
|||||||
--active-border: #442266;
|
--active-border: #442266;
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 { font: bold 1rem var(--arial-font); }
|
h1 { font: bold 1rem var(--arial-font); }
|
||||||
h2 {
|
h2 {
|
||||||
font: normal 1.5rem var(--dotum-12-font);
|
font: normal 1.5rem var(--dotum-12-font);
|
||||||
@ -193,8 +194,8 @@ blog.sort((a, b) => b.data.pubDate!.valueOf() - a.data.pubDate!.valueOf());
|
|||||||
height: calc(100% - 34px);
|
height: calc(100% - 34px);
|
||||||
flex-flow: column wrap;
|
flex-flow: column wrap;
|
||||||
background-color: var(--bg-3);
|
background-color: var(--bg-3);
|
||||||
padding: 2px;
|
padding: 16px 6px;
|
||||||
margin: 18px 6px;
|
margin: 18px 20px 16px;
|
||||||
border: 2px solid var(--border-6);
|
border: 2px solid var(--border-6);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -203,14 +204,13 @@ blog.sort((a, b) => b.data.pubDate!.valueOf() - a.data.pubDate!.valueOf());
|
|||||||
border-block: 2px solid var(--border-3);
|
border-block: 2px solid var(--border-3);
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
display: inline-block;
|
display: block;
|
||||||
background-color: var(--bg-8);
|
background-color: var(--bg-8);
|
||||||
color: var(--bg-0);
|
color: var(--bg-0);
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
padding: 5px 2px;
|
padding: 5px 2px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,6 +223,7 @@ blog.sort((a, b) => b.data.pubDate!.valueOf() - a.data.pubDate!.valueOf());
|
|||||||
li {
|
li {
|
||||||
padding: 4px 2px;
|
padding: 4px 2px;
|
||||||
border-bottom: 2px solid var(--bg-4);
|
border-bottom: 2px solid var(--bg-4);
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -244,7 +245,7 @@ blog.sort((a, b) => b.data.pubDate!.valueOf() - a.data.pubDate!.valueOf());
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
/* max-width: 19ch; */
|
max-width: 19ch;
|
||||||
|
|
||||||
@media screen and (max-width: 76em) {
|
@media screen and (max-width: 76em) {
|
||||||
max-width: unset;
|
max-width: unset;
|
||||||
@ -276,18 +277,15 @@ blog.sort((a, b) => b.data.pubDate!.valueOf() - a.data.pubDate!.valueOf());
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nav-section {
|
.nav-section {
|
||||||
margin: 0 4px;
|
|
||||||
background-color: var(--bg-0);
|
background-color: var(--bg-0);
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
&:first-child { margin-top: 4px; }
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
border-bottom: 2px solid var(--border-2);
|
border-bottom: 2px solid var(--border-2);
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 76em) {
|
@media screen and (max-width: 76em) {
|
||||||
margin-right: 0;
|
|
||||||
margin-bottom: -2px;
|
margin-bottom: -2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
|
import { Font } from "astro:assets";
|
||||||
import "$/styles/base.css";
|
import "$/styles/base.css";
|
||||||
import "$/styles/themes.css";
|
import "$/styles/themes.css";
|
||||||
import { Font } from "astro:assets";
|
|
||||||
|
|
||||||
interface Props { title?: string; }
|
interface Props { title?: string; }
|
||||||
|
|
||||||
@ -9,22 +9,22 @@ const { title = "haetae" }: Props = Astro.props;
|
|||||||
---
|
---
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<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="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 />
|
||||||
<Font cssVariable="--kiwi" preload />
|
<Font cssVariable="--kiwi" preload />
|
||||||
<slot name="head" />
|
<slot name="head" />
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
<script is:inline>
|
<script is:inline>
|
||||||
localStorage.getItem("theme")
|
localStorage.getItem("theme")
|
||||||
? document.documentElement.className = localStorage.getItem("theme")
|
? document.documentElement.className = localStorage.getItem("theme")
|
||||||
: document.documentElement.removeAttribute("class");
|
: document.documentElement.removeAttribute("class");
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<slot />
|
<slot />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@ -17,6 +17,7 @@ i used a bunch of assets that require attribution but i also wanted to link peop
|
|||||||
- [this about page's frame images](https://www.spriters-resource.com/ds_dsi/pokemonmysterydungeonexplorersoftimedarkness/sheet/5986/) by redblueyellow
|
- [this about page's frame images](https://www.spriters-resource.com/ds_dsi/pokemonmysterydungeonexplorersoftimedarkness/sheet/5986/) by redblueyellow
|
||||||
- the blog's frame images and additional assets (such as the mood emoticons) were graciously provided by AymShade on the MapleLegends forums! i can't directly link because i'm scared of a Certain Company™ being evil
|
- the blog's frame images and additional assets (such as the mood emoticons) were graciously provided by AymShade on the MapleLegends forums! i can't directly link because i'm scared of a Certain Company™ being evil
|
||||||
- [the guestbook's speech bubble images](https://www.spriters-resource.com/game_boy_advance/mlss/sheet/7573/) by MajinPiccolo
|
- [the guestbook's speech bubble images](https://www.spriters-resource.com/game_boy_advance/mlss/sheet/7573/) by MajinPiccolo
|
||||||
|
- [bullet point images](https://foollovers.com) by foollovers
|
||||||
|
|
||||||
## fonts
|
## fonts
|
||||||
- [Departure Mono](https://departuremono.com/) by [Helena Zhang](https://www.helenazhang.com/) is licensed under [OFL 1.1](https://www.tldrlegal.com/license/open-font-license-ofl-explained)
|
- [Departure Mono](https://departuremono.com/) by [Helena Zhang](https://www.helenazhang.com/) is licensed under [OFL 1.1](https://www.tldrlegal.com/license/open-font-license-ofl-explained)
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import Blog from '@/layouts/Blog.astro';
|
import Blog from '@/layouts/Blog.astro';
|
||||||
import type { GetStaticPaths } from 'astro';
|
import type { GetStaticPaths } from 'astro';
|
||||||
import { getCollection, render } from 'astro:content';
|
import { getCollection, render } from 'astro:content';
|
||||||
import buttons from "$/buttons.png";
|
import buttons from "$/guild-bbs-buttons.png";
|
||||||
|
|
||||||
export const getStaticPaths = (async () => {
|
export const getStaticPaths = (async () => {
|
||||||
const blog = await getCollection("blog");
|
const blog = await getCollection("blog");
|
||||||
|
@ -1,19 +1,24 @@
|
|||||||
import type { APIRoute } from "astro";
|
import type { APIRoute } from "astro";
|
||||||
import { getCollection } from "astro:content";
|
import { experimental_AstroContainer as AstroContainer } from "astro/container";
|
||||||
|
import { getContainerRenderer as getMDXRenderer } from "@astrojs/mdx";
|
||||||
|
import { loadRenderers } from "astro:container";
|
||||||
|
import { getCollection, render } from "astro:content";
|
||||||
import rss, { type RSSFeedItem } from "@astrojs/rss";
|
import rss, { type RSSFeedItem } from "@astrojs/rss";
|
||||||
import MarkdownIt from "markdown-it";
|
|
||||||
import { parse as htmlParser } from "node-html-parser";
|
import { parse as htmlParser } from "node-html-parser";
|
||||||
import sanitize from "sanitize-html";
|
import sanitize from "sanitize-html";
|
||||||
import fixRssImages from "@/utils/fixRssImages";
|
import fixRssImages from "@/utils/fixRssImages";
|
||||||
|
|
||||||
const parser = new MarkdownIt();
|
|
||||||
|
|
||||||
export const GET: APIRoute = async (context) => {
|
export const GET: APIRoute = async (context) => {
|
||||||
|
const renderers = await loadRenderers([getMDXRenderer()]);
|
||||||
|
const container = await AstroContainer.create({ renderers });
|
||||||
|
|
||||||
const blog = await getCollection("blog");
|
const blog = await getCollection("blog");
|
||||||
const feed: RSSFeedItem[] = [];
|
const feed: RSSFeedItem[] = [];
|
||||||
|
|
||||||
for (const entry of blog) {
|
for (const entry of blog) {
|
||||||
const content = parser.render(entry.body!);
|
const { Content } = await render(entry);
|
||||||
|
const content = await container.renderToString(Content);
|
||||||
const html = htmlParser.parse(content);
|
const html = htmlParser.parse(content);
|
||||||
const images = html.querySelectorAll("img");
|
const images = html.querySelectorAll("img");
|
||||||
|
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
---
|
---
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
|
|
||||||
|
import { Font } from "astro:assets";
|
||||||
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 Layout from "@/layouts/Layout.astro";
|
||||||
import formatDate from "@/utils/formatDate";
|
import formatDate from "@/utils/formatDate";
|
||||||
|
import Dialog from "~/Dialog.astro";
|
||||||
|
|
||||||
if (!import.meta.env.DEV) {
|
if (!import.meta.env.DEV) {
|
||||||
console.error("you shouldn't be here...");
|
console.error("you shouldn't be here...");
|
||||||
@ -14,7 +17,14 @@ if (!import.meta.env.DEV) {
|
|||||||
const entries = await db.select().from(Guestbook).orderBy(desc(Guestbook.published));
|
const entries = await db.select().from(Guestbook).orderBy(desc(Guestbook.published));
|
||||||
---
|
---
|
||||||
<Layout title="guestbook admin">
|
<Layout title="guestbook admin">
|
||||||
<h1>entries</h1>
|
<Fragment slot="head">
|
||||||
|
<Font cssVariable="--mono" preload />
|
||||||
|
</Fragment>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<h1>entries</h1>
|
||||||
|
<a href="/guestbook">*spooky voice* tuuuurn baaack</a>
|
||||||
|
</header>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<table>
|
<table>
|
||||||
@ -36,42 +46,107 @@ const entries = await db.select().from(Guestbook).orderBy(desc(Guestbook.publish
|
|||||||
<td>{entry.message}</td>
|
<td>{entry.message}</td>
|
||||||
<td>{formatDate(entry.published, false, 'MMMM D, YYYY')}</td>
|
<td>{formatDate(entry.published, false, 'MMMM D, YYYY')}</td>
|
||||||
<td>{entry.reply}</td>
|
<td>{entry.reply}</td>
|
||||||
<td><button class="edit">edit</button></td>
|
<td><button class="edit">edit entry #{entry.id}</button></td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<dialog id="edit-entry">
|
<Dialog id="edit-entry" title="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" id="entryId" value="" />
|
<input type="hidden" name="id" id="entryId" value="" />
|
||||||
<p id="entry-username"></p>
|
<p><strong>Name:</strong> <span id="entry-username"></span></p>
|
||||||
<p id="entry-website"></p>
|
<p><strong>Website:</strong> <span id="entry-website"></span></p>
|
||||||
|
<p><strong>Message:</strong></p>
|
||||||
<div id="entry-message"></div>
|
<div id="entry-message"></div>
|
||||||
<p id="entry-published"></p>
|
<p><strong>Published:</strong> <span id="entry-published"></span></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>
|
<div class="actions" style="justify-content: space-between;">
|
||||||
|
<button type="button" id="try-delete" class="cancel">Delete</button>
|
||||||
|
<button type="submit">Reply</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog id="confirm-delete" title="Are you sure?">
|
||||||
|
Do you want to delete this entry?
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<form id="delete-entry" action={actions.guestbook.deleteEntry} method="post">
|
||||||
|
<input type="hidden" name="id" id="delete-id" value="" />
|
||||||
|
<button type="submit">Delete</button>
|
||||||
|
</form>
|
||||||
|
<form method="dialog">
|
||||||
|
<button class="cancel" type="submit">Cancel</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
</section>
|
</section>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
header { text-align: center; }
|
||||||
|
|
||||||
table {
|
table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
margin: 1rem auto;
|
||||||
|
|
||||||
td {
|
td {
|
||||||
max-width: 30ch;
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
max-width: 20ch;
|
||||||
|
|
||||||
|
&:not(:has(button)) {
|
||||||
|
border: 2px solid var(--fg-color);
|
||||||
|
padding-inline: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-of-type(6n + 1) { font-weight: bold; }
|
||||||
|
|
||||||
|
&:has(button) {
|
||||||
|
padding-left: 0.15em;
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 0.15em 0.25em;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#edit-entry { width: clamp(35ch, 60ch, 100%); }
|
||||||
|
|
||||||
|
#edit-entry-form {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
|
||||||
|
.cancel {
|
||||||
|
background-color: color-mix(in oklab, var(--fg-color) 70%, var(--bg-color));
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: color-mix(in oklab, var(--fg-color) 60%, var(--bg-color));
|
||||||
|
color: var(--bg-color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const modal = document.getElementById("edit-entry") as HTMLDialogElement;
|
const modal = document.getElementById("edit-entry") as HTMLDialogElement;
|
||||||
|
const confirm = document.getElementById("confirm-delete") as HTMLDialogElement;
|
||||||
const buttons = document.querySelectorAll("button.edit");
|
const buttons = document.querySelectorAll("button.edit");
|
||||||
|
const trigger = document.getElementById("try-delete") as HTMLButtonElement;
|
||||||
|
|
||||||
buttons.forEach(button => {
|
buttons.forEach(button => {
|
||||||
button.addEventListener("click", e => {
|
button.addEventListener("click", e => {
|
||||||
const row = (e.target as HTMLElement).parentElement?.parentElement as HTMLTableRowElement;
|
const row = (e.target as HTMLElement).parentElement?.parentElement as HTMLTableRowElement;
|
||||||
@ -87,4 +162,12 @@ const entries = await db.select().from(Guestbook).orderBy(desc(Guestbook.publish
|
|||||||
modal.showModal();
|
modal.showModal();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
trigger.addEventListener("click", e => {
|
||||||
|
const form = (e.target as HTMLElement).parentElement?.parentElement as HTMLFormElement;
|
||||||
|
const id = (form.elements.namedItem("entryId") as HTMLInputElement).value;
|
||||||
|
(document.getElementById("delete-id") as HTMLInputElement).value = id;
|
||||||
|
|
||||||
|
confirm.showModal();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
@ -5,6 +5,7 @@ import { Font } from "astro:assets";
|
|||||||
import Layout from "@/layouts/Layout.astro";
|
import Layout from "@/layouts/Layout.astro";
|
||||||
import speech from "$/speech.png";
|
import speech from "$/speech.png";
|
||||||
import Entries from "~/Entries.astro";
|
import Entries from "~/Entries.astro";
|
||||||
|
import Dialog from "~/Dialog.astro";
|
||||||
|
|
||||||
const result = Astro.getActionResult(actions.guestbook.addEntry);
|
const result = Astro.getActionResult(actions.guestbook.addEntry);
|
||||||
const inputErrors = isInputError(result?.error) ? result.error.fields : {};
|
const inputErrors = isInputError(result?.error) ? result.error.fields : {};
|
||||||
@ -19,6 +20,7 @@ const inputErrors = isInputError(result?.error) ? result.error.fields : {};
|
|||||||
<h1>Guestbook</h1>
|
<h1>Guestbook</h1>
|
||||||
|
|
||||||
<section id="add-comment">
|
<section id="add-comment">
|
||||||
|
{import.meta.env.DEV && <p><a href="/guestbook/admin">for your eyes only...</a></p>}
|
||||||
<form action={actions.guestbook.addEntry} method="post">
|
<form action={actions.guestbook.addEntry} method="post">
|
||||||
<label for="username">Nickname</label>
|
<label for="username">Nickname</label>
|
||||||
<input type="text" id="username" name="username" required aria-describedby="username-error" />
|
<input type="text" id="username" name="username" required aria-describedby="username-error" />
|
||||||
@ -43,20 +45,9 @@ const inputErrors = isInputError(result?.error) ? result.error.fields : {};
|
|||||||
<p slot="fallback">Loading...</p>
|
<p slot="fallback">Loading...</p>
|
||||||
</Entries>
|
</Entries>
|
||||||
|
|
||||||
<dialog id="notification">
|
<Dialog id="notification">
|
||||||
<form method="dialog">
|
Successfully posted! Refreshing in <span id="seconds">5</span> seconds.
|
||||||
<menu>
|
</Dialog>
|
||||||
<button type="submit" aria-label="Close">
|
|
||||||
<span>x</span>
|
|
||||||
</button>
|
|
||||||
</menu>
|
|
||||||
Successfully posted! Refreshing in <span id="seconds">5</span> seconds.
|
|
||||||
</form>
|
|
||||||
</dialog>
|
|
||||||
|
|
||||||
<footer>
|
|
||||||
hi
|
|
||||||
</footer>
|
|
||||||
</main>
|
</main>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
@ -91,70 +82,6 @@ const inputErrors = isInputError(result?.error) ? result.error.fields : {};
|
|||||||
color: color-mix(in oklab, var(--fg-color) 80%, var(--bg-color));
|
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; }
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -177,6 +104,7 @@ const inputErrors = isInputError(result?.error) ? result.error.fields : {};
|
|||||||
if (time <= 0) {
|
if (time <= 0) {
|
||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
notification.close();
|
notification.close();
|
||||||
|
location.reload();
|
||||||
navigate("#entries");
|
navigate("#entries");
|
||||||
} else {
|
} else {
|
||||||
seconds.innerText = time.toString();
|
seconds.innerText = time.toString();
|
||||||
|
@ -1,53 +1,111 @@
|
|||||||
---
|
---
|
||||||
import Navbar from '~/Navbar.astro';
|
|
||||||
import Layout from '@/layouts/Layout.astro';
|
import Layout from '@/layouts/Layout.astro';
|
||||||
|
import Navbar from '~/Navbar.astro';
|
||||||
|
|
||||||
|
import bulletin from "$/acnl-bulletin.png";
|
||||||
---
|
---
|
||||||
<Layout>
|
<Layout>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<main>
|
<main>
|
||||||
<section id="welcome">
|
<section id="welcome" class="board">
|
||||||
<h1>welcome!</h1>
|
<div class="card">
|
||||||
<article>
|
<h1>welcome!</h1>
|
||||||
this is
|
<article>
|
||||||
</article>
|
this is
|
||||||
</section>
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section id="updates">
|
<section id="updates" class="board">
|
||||||
<article class="update-card">
|
<article class="update card">
|
||||||
<h1>update title</h1>
|
<h1>update title</h1>
|
||||||
<time datetime="">timestamp</time>
|
<time datetime="">05/01/25</time>
|
||||||
<p>some stuff happened</p>
|
<div class="content">
|
||||||
</article>
|
<p>some stuff happened</p>
|
||||||
</section>
|
</div>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section id="wall">
|
<section id="wall">
|
||||||
<h1>stuff!</h1>
|
<h1>stuff!</h1>
|
||||||
<p>i keep a bunch of links here, including the webrings i've joined and some badges i like</p>
|
<p>i keep a bunch of links here, including the webrings i've joined and some badges i like</p>
|
||||||
<div class="web">
|
<div class="web">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<style>
|
<style define:vars={{ bulletin: `url(${bulletin.src})` }}>
|
||||||
main {
|
body {
|
||||||
max-width: 50em;
|
--acnl-border: #ac6835;
|
||||||
margin: 0 auto;
|
--acnl-bg: #8c5321;
|
||||||
display: grid;
|
--card-border-1: #ffd53c;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
--card-border-2: #5acbbd;
|
||||||
grid-template-rows: 1.5fr 0.5fr;
|
--card-bg: white;
|
||||||
gap: 1rem;
|
}
|
||||||
grid-template-areas:
|
|
||||||
"welcome updates"
|
|
||||||
"wall wall";
|
|
||||||
|
|
||||||
section {
|
main {
|
||||||
border: 2px solid #ccc;
|
display: grid;
|
||||||
}
|
grid-template-columns: repeat(2, 1fr);
|
||||||
}
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
max-width: 50em;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
#wall {
|
@media screen and (max-width: 1360px) {
|
||||||
grid-area: wall;
|
margin: 0 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 76em) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 { font-size: 2rem; }
|
||||||
|
|
||||||
|
.board {
|
||||||
|
image-rendering: pixelated;
|
||||||
|
border: 2px solid var(--acnl-border);
|
||||||
|
background-color: transparent;
|
||||||
|
border-image: var(--bulletin) 42 30 30 fill / 42px 30px 30px;
|
||||||
|
padding: 42px 30px 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border: 6px solid black;
|
||||||
|
background-color: var(--card-bg);
|
||||||
|
box-shadow: inset 0 0 0 4px var(--card-bg), inset 0 0 0 6px black;
|
||||||
|
padding: calc(1rem + 4px);
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
padding-bottom: 2px;
|
||||||
|
border-bottom: 2px solid black;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#welcome {
|
||||||
|
.card {
|
||||||
|
border-color: var(--card-border-1);
|
||||||
|
box-shadow: inset 0 0 0 4px var(--card-bg), inset 0 0 0 6px var(--card-border-1);
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
h1 { border-bottom-color: var(--card-border-1); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#updates {
|
||||||
|
.card {
|
||||||
|
border-color: var(--card-border-2);
|
||||||
|
box-shadow: inset 0 0 0 4px var(--card-bg), inset 0 0 0 6px var(--card-border-2);
|
||||||
|
|
||||||
|
h1 { border-bottom-color: var(--card-border-2); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#wall {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
</style>
|
</style>
|