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,27 +12,34 @@ 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); |  | ||||||
| 
 | 
 | ||||||
|  |       try { | ||||||
|         const entry = await db.insert(Guestbook).values({ |         const entry = await db.insert(Guestbook).values({ | ||||||
|           username, |           username, | ||||||
|           website, |           website, | ||||||
|         message, |           message: sanitized, | ||||||
|         }).returning(); |         }).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 }); | ||||||
|  |       } | ||||||
|     }, |     }, | ||||||
|   }), |   }), | ||||||
|  |   ...import.meta.env.DEV && { | ||||||
|     reply: defineAction({ |     reply: defineAction({ | ||||||
|       accept: "form", |       accept: "form", | ||||||
|       input: z.object({ |       input: z.object({ | ||||||
|       id: z.number(), |         id: z.coerce.number(), | ||||||
|         reply: z.string(), |         reply: z.string(), | ||||||
|       }), |       }), | ||||||
|     handler: async ({ id, reply }, context) => { |       handler: async ({ id, reply }) => { | ||||||
|       if (context.url.hostname !== "127.0.0.1" || "localhost") { |         if (!import.meta.env.DEV) { | ||||||
|           throw new ActionError({ code: "UNAUTHORIZED" }); |           throw new ActionError({ code: "UNAUTHORIZED" }); | ||||||
|         } |         } | ||||||
|          |          | ||||||
| @ -44,10 +51,53 @@ export const guestbook = { | |||||||
|           }); |           }); | ||||||
|         } |         } | ||||||
|          |          | ||||||
|       // sanitize reply here
 |         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(); | ||||||
| 
 | 
 | ||||||
|       const update = await db.update(Guestbook).set({ reply }).where(eq(Guestbook.id, id)).returning(); |  | ||||||
|           return update[0]; |           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 }); | ||||||
|  |         } | ||||||
|       }, |       }, | ||||||
|     }), |     }), | ||||||
|  |     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!" | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         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 }); | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |     }), | ||||||
|  |   }, | ||||||
| }; | }; | ||||||
							
								
								
									
										
											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; } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -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"> | ||||||
|  |   <Fragment slot="head"> | ||||||
|  |     <Font cssVariable="--mono" preload /> | ||||||
|  |   </Fragment> | ||||||
|  |    | ||||||
|  |   <header> | ||||||
|     <h1>entries</h1> |     <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> | ||||||
| 
 | 
 | ||||||
|  |         <div class="actions" style="justify-content: space-between;"> | ||||||
|  |           <button type="button" id="try-delete" class="cancel">Delete</button> | ||||||
|           <button type="submit">Reply</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"> |  | ||||||
|         <menu> |  | ||||||
|           <button type="submit" aria-label="Close"> |  | ||||||
|             <span>x</span> |  | ||||||
|           </button> |  | ||||||
|         </menu> |  | ||||||
|       Successfully posted! Refreshing in <span id="seconds">5</span> seconds. |       Successfully posted! Refreshing in <span id="seconds">5</span> seconds. | ||||||
|       </form> |     </Dialog> | ||||||
|     </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,22 +1,28 @@ | |||||||
| --- | --- | ||||||
| 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"> | ||||||
|  |       <div class="card"> | ||||||
|         <h1>welcome!</h1> |         <h1>welcome!</h1> | ||||||
|         <article> |         <article> | ||||||
|           this is  |           this is  | ||||||
|         </article> |         </article> | ||||||
|  |       </div> | ||||||
|     </section> |     </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> | ||||||
|  |         <div class="content"> | ||||||
|           <p>some stuff happened</p> |           <p>some stuff happened</p> | ||||||
|  |         </div> | ||||||
|       </article> |       </article> | ||||||
|     </section> |     </section> | ||||||
| 
 | 
 | ||||||
| @ -30,24 +36,76 @@ import Layout from '@/layouts/Layout.astro'; | |||||||
|   </main> |   </main> | ||||||
| </Layout> | </Layout> | ||||||
| 
 | 
 | ||||||
| <style> | <style define:vars={{ bulletin: `url(${bulletin.src})` }}> | ||||||
|  |   body { | ||||||
|  |     --acnl-border: #ac6835; | ||||||
|  |     --acnl-bg: #8c5321; | ||||||
|  |     --card-border-1: #ffd53c; | ||||||
|  |     --card-border-2: #5acbbd; | ||||||
|  |     --card-bg: white; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   main { |   main { | ||||||
|  |     display: grid; | ||||||
|  |     grid-template-columns: repeat(2, 1fr); | ||||||
|  |     justify-content: center; | ||||||
|  |     gap: 1rem; | ||||||
|     max-width: 50em; |     max-width: 50em; | ||||||
|     margin: 0 auto; |     margin: 0 auto; | ||||||
| 		display: grid; |  | ||||||
| 		grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); |  | ||||||
| 		grid-template-rows: 1.5fr 0.5fr; |  | ||||||
| 		gap: 1rem; |  | ||||||
| 		grid-template-areas: |  | ||||||
| 			"welcome updates" |  | ||||||
| 			"wall wall"; |  | ||||||
| 
 | 
 | ||||||
| 		section { |     @media screen and (max-width: 1360px) { | ||||||
| 			border: 2px solid #ccc; |       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 { |   #wall { | ||||||
| 		grid-area: wall; |     grid-column: 1 / -1; | ||||||
|   } |   } | ||||||
| </style> | </style> | ||||||