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
|
||||
TURSO_AUTH_TOKEN=your-token
|
||||
ASTRO_DB_REMOTE_URL=libsql://your-db.turso.io
|
||||
ASTRO_DB_APP_TOKEN=your-token
|
||||
GITHUB_CLIENT_ID=your-client-id
|
||||
GITHUB_CLIENT_SECRET=your-client-secret
|
||||
ADMIN_GITHUB_ID=your-github-user-id
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
import vercel from '@astrojs/vercel';
|
||||
import db from '@astrojs/db';
|
||||
import auth from 'auth-astro';
|
||||
import remarkDirective from 'remark-directive';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
|
|
@ -10,7 +11,7 @@ import remarkAside from './src/plugins/remark-aside.ts';
|
|||
export default defineConfig({
|
||||
output: 'static',
|
||||
adapter: vercel(),
|
||||
integrations: [auth()],
|
||||
integrations: [db(), auth()],
|
||||
markdown: {
|
||||
remarkPlugins: [
|
||||
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",
|
||||
"dependencies": {
|
||||
"@astrojs/db": "^0.19.0",
|
||||
"@astrojs/vercel": "^9.0.4",
|
||||
"@auth/core": "^0.37.4",
|
||||
"@libsql/client": "^0.17.0",
|
||||
|
|
@ -318,6 +319,22 @@
|
|||
"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": {
|
||||
"version": "0.7.5",
|
||||
"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": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
|
|
@ -1453,6 +1477,127 @@
|
|||
"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": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
|
|
@ -1695,6 +1840,15 @@
|
|||
"license": "MIT",
|
||||
"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": {
|
||||
"version": "0.5.22",
|
||||
"resolved": "https://registry.npmjs.org/libsql/-/libsql-0.5.22.tgz",
|
||||
|
|
@ -1784,6 +1938,24 @@
|
|||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
|
|
@ -1901,6 +2073,12 @@
|
|||
"integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
|
||||
"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": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
|
|
@ -1938,6 +2116,19 @@
|
|||
"integrity": "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==",
|
||||
"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": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
|
|
@ -2024,6 +2215,12 @@
|
|||
"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": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
|
|
@ -2338,6 +2535,24 @@
|
|||
"engines": {
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/db": "^0.19.0",
|
||||
"@astrojs/vercel": "^9.0.4",
|
||||
"@auth/core": "^0.37.4",
|
||||
"@libsql/client": "^0.17.0",
|
||||
"astro": "^5.16.13",
|
||||
"auth-astro": "^4.2.0",
|
||||
"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({
|
||||
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 type GuestbookEntry = typeof Guestbook.$inferSelect;
|
||||
|
||||
export async function getApprovedEntries(): Promise<GuestbookEntry[]> {
|
||||
const result = await db.execute(
|
||||
'SELECT * FROM guestbook WHERE approved = 1 ORDER BY created_at DESC'
|
||||
);
|
||||
return result.rows as unknown as GuestbookEntry[];
|
||||
return db.select().from(Guestbook).where(eq(Guestbook.approved, true)).orderBy(desc(Guestbook.createdAt));
|
||||
}
|
||||
|
||||
export async function getPendingEntries(): Promise<GuestbookEntry[]> {
|
||||
const result = await db.execute(
|
||||
'SELECT * FROM guestbook WHERE approved = 0 ORDER BY created_at DESC'
|
||||
);
|
||||
return result.rows as unknown as GuestbookEntry[];
|
||||
return db.select().from(Guestbook).where(eq(Guestbook.approved, false)).orderBy(desc(Guestbook.createdAt));
|
||||
}
|
||||
|
||||
export async function createEntry(name: string, message: string, url: string | null): Promise<void> {
|
||||
await db.execute({
|
||||
sql: 'INSERT INTO guestbook (name, message, url) VALUES (?, ?, ?)',
|
||||
args: [name, message, url],
|
||||
await db.insert(Guestbook).values({
|
||||
name,
|
||||
message,
|
||||
url,
|
||||
createdAt: new Date(),
|
||||
approved: false,
|
||||
});
|
||||
}
|
||||
|
||||
export async function approveEntry(id: number): Promise<void> {
|
||||
await db.execute({
|
||||
sql: 'UPDATE guestbook SET approved = 1 WHERE id = ?',
|
||||
args: [id],
|
||||
});
|
||||
await db.update(Guestbook).set({ approved: true }).where(eq(Guestbook.id, id));
|
||||
}
|
||||
|
||||
export async function deleteEntry(id: number): Promise<void> {
|
||||
await db.execute({
|
||||
sql: 'DELETE FROM guestbook WHERE id = ?',
|
||||
args: [id],
|
||||
});
|
||||
await db.delete(Guestbook).where(eq(Guestbook.id, id));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import yaml from 'js-yaml';
|
|||
import bookmarksRaw from '../data/bookmarks.yaml?raw';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { getApprovedEntries, type GuestbookEntry } from '../lib/db';
|
||||
|
||||
interface Bookmark {
|
||||
date: string;
|
||||
|
|
@ -35,6 +36,13 @@ const txtFiles: TxtFile[] = fs.existsSync(txtDir)
|
|||
.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 {
|
||||
const d = String(date.getDate()).padStart(2, '0');
|
||||
const m = String(date.getMonth() + 1).padStart(2, '0');
|
||||
|
|
@ -78,5 +86,47 @@ function extractDomain(url: string): string {
|
|||
<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')} />
|
||||
</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>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -46,3 +46,48 @@ summary::-webkit-details-marker {
|
|||
details pre {
|
||||
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