feat: switch to Astro DB for guestbook
- Add @astrojs/db integration - Define Guestbook schema in db/config.ts - Add seed data for development - Update db.ts to use astro:db - Add guestbook section to homepage with form - Update env vars to use ASTRO_DB_REMOTE_URL
This commit is contained in:
parent
4e2c09b770
commit
79c7aff48b
9 changed files with 365 additions and 37 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
TURSO_DATABASE_URL=libsql://your-db.turso.io
|
ASTRO_DB_REMOTE_URL=libsql://your-db.turso.io
|
||||||
TURSO_AUTH_TOKEN=your-token
|
ASTRO_DB_APP_TOKEN=your-token
|
||||||
GITHUB_CLIENT_ID=your-client-id
|
GITHUB_CLIENT_ID=your-client-id
|
||||||
GITHUB_CLIENT_SECRET=your-client-secret
|
GITHUB_CLIENT_SECRET=your-client-secret
|
||||||
ADMIN_GITHUB_ID=your-github-user-id
|
ADMIN_GITHUB_ID=your-github-user-id
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { defineConfig } from 'astro/config';
|
import { defineConfig } from 'astro/config';
|
||||||
import vercel from '@astrojs/vercel';
|
import vercel from '@astrojs/vercel';
|
||||||
|
import db from '@astrojs/db';
|
||||||
import auth from 'auth-astro';
|
import auth from 'auth-astro';
|
||||||
import remarkDirective from 'remark-directive';
|
import remarkDirective from 'remark-directive';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
|
|
@ -10,7 +11,7 @@ import remarkAside from './src/plugins/remark-aside.ts';
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
output: 'static',
|
output: 'static',
|
||||||
adapter: vercel(),
|
adapter: vercel(),
|
||||||
integrations: [auth()],
|
integrations: [db(), auth()],
|
||||||
markdown: {
|
markdown: {
|
||||||
remarkPlugins: [
|
remarkPlugins: [
|
||||||
remarkGfm,
|
remarkGfm,
|
||||||
|
|
|
||||||
16
apps/blog/db/config.ts
Normal file
16
apps/blog/db/config.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { defineDb, defineTable, column } from 'astro:db';
|
||||||
|
|
||||||
|
const Guestbook = defineTable({
|
||||||
|
columns: {
|
||||||
|
id: column.number({ primaryKey: true }),
|
||||||
|
name: column.text(),
|
||||||
|
message: column.text(),
|
||||||
|
url: column.text({ optional: true }),
|
||||||
|
createdAt: column.date({ default: new Date() }),
|
||||||
|
approved: column.boolean({ default: false }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default defineDb({
|
||||||
|
tables: { Guestbook },
|
||||||
|
});
|
||||||
22
apps/blog/db/seed.ts
Normal file
22
apps/blog/db/seed.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { db, Guestbook } from 'astro:db';
|
||||||
|
|
||||||
|
export default async function seed() {
|
||||||
|
await db.insert(Guestbook).values([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'alice',
|
||||||
|
message: 'love the site!',
|
||||||
|
url: 'https://example.com',
|
||||||
|
createdAt: new Date('2026-01-20'),
|
||||||
|
approved: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'bob',
|
||||||
|
message: 'great blog posts',
|
||||||
|
url: null,
|
||||||
|
createdAt: new Date('2026-01-18'),
|
||||||
|
approved: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
215
apps/blog/package-lock.json
generated
215
apps/blog/package-lock.json
generated
|
|
@ -6,6 +6,7 @@
|
||||||
"": {
|
"": {
|
||||||
"name": "@ily/blog",
|
"name": "@ily/blog",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@astrojs/db": "^0.19.0",
|
||||||
"@astrojs/vercel": "^9.0.4",
|
"@astrojs/vercel": "^9.0.4",
|
||||||
"@auth/core": "^0.37.4",
|
"@auth/core": "^0.37.4",
|
||||||
"@libsql/client": "^0.17.0",
|
"@libsql/client": "^0.17.0",
|
||||||
|
|
@ -318,6 +319,22 @@
|
||||||
"url": "https://opencollective.com/unified"
|
"url": "https://opencollective.com/unified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@astrojs/db": {
|
||||||
|
"version": "0.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@astrojs/db/-/db-0.19.0.tgz",
|
||||||
|
"integrity": "sha512-YrVsqxwODr6Bid4nRgzGsF9K8K8xSoFd7j8bAU+4CxN3tSBx/1kmTE3BClwfVH2xO74wFVsyr7ucBzw/yEsEBw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@libsql/client": "^0.17.0",
|
||||||
|
"deep-diff": "^1.0.2",
|
||||||
|
"drizzle-orm": "^0.42.0",
|
||||||
|
"nanoid": "^5.1.6",
|
||||||
|
"piccolore": "^0.1.3",
|
||||||
|
"prompts": "^2.4.2",
|
||||||
|
"yargs-parser": "^21.1.1",
|
||||||
|
"zod": "^3.25.76"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@astrojs/internal-helpers": {
|
"node_modules/@astrojs/internal-helpers": {
|
||||||
"version": "0.7.5",
|
"version": "0.7.5",
|
||||||
"resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.7.5.tgz",
|
"resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.7.5.tgz",
|
||||||
|
|
@ -1444,6 +1461,13 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/deep-diff": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg==",
|
||||||
|
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
|
|
@ -1453,6 +1477,127 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/drizzle-orm": {
|
||||||
|
"version": "0.42.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.42.0.tgz",
|
||||||
|
"integrity": "sha512-pS8nNJm2kBNZwrOjTHJfdKkaU+KuUQmV/vk5D57NojDq4FG+0uAYGMulXtYT///HfgsMF0hnFFvu1ezI3OwOkg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@aws-sdk/client-rds-data": ">=3",
|
||||||
|
"@cloudflare/workers-types": ">=4",
|
||||||
|
"@electric-sql/pglite": ">=0.2.0",
|
||||||
|
"@libsql/client": ">=0.10.0",
|
||||||
|
"@libsql/client-wasm": ">=0.10.0",
|
||||||
|
"@neondatabase/serverless": ">=0.10.0",
|
||||||
|
"@op-engineering/op-sqlite": ">=2",
|
||||||
|
"@opentelemetry/api": "^1.4.1",
|
||||||
|
"@planetscale/database": ">=1.13",
|
||||||
|
"@prisma/client": "*",
|
||||||
|
"@tidbcloud/serverless": "*",
|
||||||
|
"@types/better-sqlite3": "*",
|
||||||
|
"@types/pg": "*",
|
||||||
|
"@types/sql.js": "*",
|
||||||
|
"@vercel/postgres": ">=0.8.0",
|
||||||
|
"@xata.io/client": "*",
|
||||||
|
"better-sqlite3": ">=7",
|
||||||
|
"bun-types": "*",
|
||||||
|
"expo-sqlite": ">=14.0.0",
|
||||||
|
"gel": ">=2",
|
||||||
|
"knex": "*",
|
||||||
|
"kysely": "*",
|
||||||
|
"mysql2": ">=2",
|
||||||
|
"pg": ">=8",
|
||||||
|
"postgres": ">=3",
|
||||||
|
"sql.js": ">=1",
|
||||||
|
"sqlite3": ">=5"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@aws-sdk/client-rds-data": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@cloudflare/workers-types": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@electric-sql/pglite": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@libsql/client": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@libsql/client-wasm": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@neondatabase/serverless": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@op-engineering/op-sqlite": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@opentelemetry/api": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@planetscale/database": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@prisma/client": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@tidbcloud/serverless": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/better-sqlite3": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/pg": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/sql.js": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vercel/postgres": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@xata.io/client": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"better-sqlite3": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"bun-types": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"expo-sqlite": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"gel": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"knex": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"kysely": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"mysql2": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"pg": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"postgres": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"prisma": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sql.js": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sqlite3": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/eastasianwidth": {
|
"node_modules/eastasianwidth": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||||
|
|
@ -1695,6 +1840,15 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/kleur": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/libsql": {
|
"node_modules/libsql": {
|
||||||
"version": "0.5.22",
|
"version": "0.5.22",
|
||||||
"resolved": "https://registry.npmjs.org/libsql/-/libsql-0.5.22.tgz",
|
"resolved": "https://registry.npmjs.org/libsql/-/libsql-0.5.22.tgz",
|
||||||
|
|
@ -1784,6 +1938,24 @@
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/nanoid": {
|
||||||
|
"version": "5.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz",
|
||||||
|
"integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"nanoid": "bin/nanoid.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18 || >=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-domexception": {
|
"node_modules/node-domexception": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||||
|
|
@ -1901,6 +2073,12 @@
|
||||||
"integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
|
"integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/piccolore": {
|
||||||
|
"version": "0.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz",
|
||||||
|
"integrity": "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
|
|
@ -1938,6 +2116,19 @@
|
||||||
"integrity": "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==",
|
"integrity": "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/prompts": {
|
||||||
|
"version": "2.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
|
||||||
|
"integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"kleur": "^3.0.3",
|
||||||
|
"sisteransi": "^1.0.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
|
|
@ -2024,6 +2215,12 @@
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sisteransi": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/string-width": {
|
"node_modules/string-width": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||||
|
|
@ -2338,6 +2535,24 @@
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs-parser": {
|
||||||
|
"version": "21.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||||
|
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zod": {
|
||||||
|
"version": "3.25.76",
|
||||||
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||||
|
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,9 @@
|
||||||
"preview": "astro preview"
|
"preview": "astro preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@astrojs/db": "^0.19.0",
|
||||||
"@astrojs/vercel": "^9.0.4",
|
"@astrojs/vercel": "^9.0.4",
|
||||||
"@auth/core": "^0.37.4",
|
"@auth/core": "^0.37.4",
|
||||||
"@libsql/client": "^0.17.0",
|
|
||||||
"astro": "^5.16.13",
|
"astro": "^5.16.13",
|
||||||
"auth-astro": "^4.2.0",
|
"auth-astro": "^4.2.0",
|
||||||
"js-yaml": "^4.1.1"
|
"js-yaml": "^4.1.1"
|
||||||
|
|
|
||||||
|
|
@ -1,50 +1,29 @@
|
||||||
import { createClient } from '@libsql/client';
|
import { db, Guestbook, eq, desc } from 'astro:db';
|
||||||
|
|
||||||
export const db = createClient({
|
export type GuestbookEntry = typeof Guestbook.$inferSelect;
|
||||||
url: import.meta.env.TURSO_DATABASE_URL,
|
|
||||||
authToken: import.meta.env.TURSO_AUTH_TOKEN,
|
|
||||||
});
|
|
||||||
|
|
||||||
export interface GuestbookEntry {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
message: string;
|
|
||||||
url: string | null;
|
|
||||||
created_at: string;
|
|
||||||
approved: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getApprovedEntries(): Promise<GuestbookEntry[]> {
|
export async function getApprovedEntries(): Promise<GuestbookEntry[]> {
|
||||||
const result = await db.execute(
|
return db.select().from(Guestbook).where(eq(Guestbook.approved, true)).orderBy(desc(Guestbook.createdAt));
|
||||||
'SELECT * FROM guestbook WHERE approved = 1 ORDER BY created_at DESC'
|
|
||||||
);
|
|
||||||
return result.rows as unknown as GuestbookEntry[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPendingEntries(): Promise<GuestbookEntry[]> {
|
export async function getPendingEntries(): Promise<GuestbookEntry[]> {
|
||||||
const result = await db.execute(
|
return db.select().from(Guestbook).where(eq(Guestbook.approved, false)).orderBy(desc(Guestbook.createdAt));
|
||||||
'SELECT * FROM guestbook WHERE approved = 0 ORDER BY created_at DESC'
|
|
||||||
);
|
|
||||||
return result.rows as unknown as GuestbookEntry[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createEntry(name: string, message: string, url: string | null): Promise<void> {
|
export async function createEntry(name: string, message: string, url: string | null): Promise<void> {
|
||||||
await db.execute({
|
await db.insert(Guestbook).values({
|
||||||
sql: 'INSERT INTO guestbook (name, message, url) VALUES (?, ?, ?)',
|
name,
|
||||||
args: [name, message, url],
|
message,
|
||||||
|
url,
|
||||||
|
createdAt: new Date(),
|
||||||
|
approved: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function approveEntry(id: number): Promise<void> {
|
export async function approveEntry(id: number): Promise<void> {
|
||||||
await db.execute({
|
await db.update(Guestbook).set({ approved: true }).where(eq(Guestbook.id, id));
|
||||||
sql: 'UPDATE guestbook SET approved = 1 WHERE id = ?',
|
|
||||||
args: [id],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteEntry(id: number): Promise<void> {
|
export async function deleteEntry(id: number): Promise<void> {
|
||||||
await db.execute({
|
await db.delete(Guestbook).where(eq(Guestbook.id, id));
|
||||||
sql: 'DELETE FROM guestbook WHERE id = ?',
|
|
||||||
args: [id],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import yaml from 'js-yaml';
|
||||||
import bookmarksRaw from '../data/bookmarks.yaml?raw';
|
import bookmarksRaw from '../data/bookmarks.yaml?raw';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
import { getApprovedEntries, type GuestbookEntry } from '../lib/db';
|
||||||
|
|
||||||
interface Bookmark {
|
interface Bookmark {
|
||||||
date: string;
|
date: string;
|
||||||
|
|
@ -35,6 +36,13 @@ const txtFiles: TxtFile[] = fs.existsSync(txtDir)
|
||||||
.sort((a, b) => b.mtime.getTime() - a.mtime.getTime())
|
.sort((a, b) => b.mtime.getTime() - a.mtime.getTime())
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
let guestbookEntries: GuestbookEntry[] = [];
|
||||||
|
try {
|
||||||
|
guestbookEntries = await getApprovedEntries();
|
||||||
|
} catch {
|
||||||
|
// DB not available during dev without env vars
|
||||||
|
}
|
||||||
|
|
||||||
function formatDate(date: Date): string {
|
function formatDate(date: Date): string {
|
||||||
const d = String(date.getDate()).padStart(2, '0');
|
const d = String(date.getDate()).padStart(2, '0');
|
||||||
const m = String(date.getMonth() + 1).padStart(2, '0');
|
const m = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
|
@ -78,5 +86,47 @@ function extractDomain(url: string): string {
|
||||||
<summary>bookmarks</summary>
|
<summary>bookmarks</summary>
|
||||||
<pre set:html={bookmarks.map(b => `<span class="muted">${formatBookmarkDate(b.date)}</span> <a href="${b.url}">${b.title}</a> <span class="muted">(${extractDomain(b.url)})</span>`).join('\n')} />
|
<pre set:html={bookmarks.map(b => `<span class="muted">${formatBookmarkDate(b.date)}</span> <a href="${b.url}">${b.title}</a> <span class="muted">(${extractDomain(b.url)})</span>`).join('\n')} />
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<details open>
|
||||||
|
<summary>guestbook</summary>
|
||||||
|
<pre set:html={guestbookEntries.length > 0
|
||||||
|
? guestbookEntries.map(e => `<span class="muted">${formatDate(e.createdAt)}</span> ${e.url ? `<a href="${e.url}">${e.name}</a>` : e.name}: ${e.message}`).join('\n')
|
||||||
|
: '<span class="muted">no entries yet</span>'} />
|
||||||
|
<form id="guestbook-form">
|
||||||
|
<input type="text" name="name" placeholder="name" required maxlength="100" />
|
||||||
|
<input type="text" name="url" placeholder="url (optional)" maxlength="200" />
|
||||||
|
<textarea name="message" placeholder="message" required maxlength="500"></textarea>
|
||||||
|
<button type="submit">sign</button>
|
||||||
|
</form>
|
||||||
|
<div id="guestbook-status"></div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const form = document.getElementById('guestbook-form') as HTMLFormElement;
|
||||||
|
const status = document.getElementById('guestbook-status')!;
|
||||||
|
|
||||||
|
form.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const data = Object.fromEntries(new FormData(form));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/guestbook', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
status.textContent = 'thanks! your message is pending approval.';
|
||||||
|
form.reset();
|
||||||
|
} else {
|
||||||
|
const err = await res.json();
|
||||||
|
status.textContent = err.error || 'something went wrong';
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
status.textContent = 'failed to submit';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -46,3 +46,48 @@ summary::-webkit-details-marker {
|
||||||
details pre {
|
details pre {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#guestbook-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding-left: 1rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#guestbook-form input,
|
||||||
|
#guestbook-form textarea {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 0.25rem;
|
||||||
|
border: 1px solid #888;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
#guestbook-form textarea {
|
||||||
|
min-height: 3rem;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
#guestbook-form button {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #888;
|
||||||
|
color: inherit;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
#guestbook-form button:hover {
|
||||||
|
background: #888;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#guestbook-status {
|
||||||
|
padding-left: 1rem;
|
||||||
|
color: #888;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue