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,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