feat: admin panel with SQLite, auth, and calendar-style schedule editor
Complete admin panel for content management: - SQLite database with better-sqlite3, seed script from content.ts - Simple password auth with HMAC-signed cookies (Edge + Node compatible) - 9 section editors: meta, hero, about, team, classes, schedule, pricing, FAQ, contact - Team CRUD with image upload and drag reorder - Schedule editor with Google Calendar-style visual timeline (colored blocks, overlap detection, click-to-add) - All public components refactored to accept data props from DB (with fallback to static content) - Middleware protecting /admin/* and /api/admin/* routes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
96
src/data/seed.ts
Normal file
96
src/data/seed.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Seed script — populates the SQLite database from content.ts
|
||||
* Run: npx tsx src/data/seed.ts
|
||||
*/
|
||||
|
||||
import Database from "better-sqlite3";
|
||||
import path from "path";
|
||||
import { siteContent } from "./content";
|
||||
|
||||
const DB_PATH =
|
||||
process.env.DATABASE_PATH ||
|
||||
path.join(process.cwd(), "db", "blackheart.db");
|
||||
|
||||
const db = new Database(DB_PATH);
|
||||
db.pragma("journal_mode = WAL");
|
||||
|
||||
// Create tables
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS sections (
|
||||
key TEXT PRIMARY KEY,
|
||||
data TEXT NOT NULL,
|
||||
updated_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS team_members (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
image TEXT NOT NULL,
|
||||
instagram TEXT,
|
||||
description TEXT,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
`);
|
||||
|
||||
// Seed sections (team members go in their own table)
|
||||
const sectionData: Record<string, unknown> = {
|
||||
meta: siteContent.meta,
|
||||
hero: siteContent.hero,
|
||||
about: siteContent.about,
|
||||
classes: siteContent.classes,
|
||||
faq: siteContent.faq,
|
||||
pricing: siteContent.pricing,
|
||||
schedule: siteContent.schedule,
|
||||
contact: siteContent.contact,
|
||||
};
|
||||
|
||||
// Team section stores only the title
|
||||
sectionData.team = { title: siteContent.team.title };
|
||||
|
||||
const upsertSection = db.prepare(
|
||||
`INSERT INTO sections (key, data, updated_at) VALUES (?, ?, datetime('now'))
|
||||
ON CONFLICT(key) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at`
|
||||
);
|
||||
|
||||
const insertMember = db.prepare(
|
||||
`INSERT INTO team_members (name, role, image, instagram, description, sort_order)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`
|
||||
);
|
||||
|
||||
const tx = db.transaction(() => {
|
||||
// Upsert all sections
|
||||
for (const [key, data] of Object.entries(sectionData)) {
|
||||
upsertSection.run(key, JSON.stringify(data));
|
||||
}
|
||||
|
||||
// Clear existing team members and re-insert
|
||||
db.prepare("DELETE FROM team_members").run();
|
||||
|
||||
siteContent.team.members.forEach((m, i) => {
|
||||
insertMember.run(
|
||||
m.name,
|
||||
m.role,
|
||||
m.image,
|
||||
m.instagram ?? null,
|
||||
m.description ?? null,
|
||||
i
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
tx();
|
||||
|
||||
const sectionCount = (
|
||||
db.prepare("SELECT COUNT(*) as c FROM sections").get() as { c: number }
|
||||
).c;
|
||||
const memberCount = (
|
||||
db.prepare("SELECT COUNT(*) as c FROM team_members").get() as { c: number }
|
||||
).c;
|
||||
|
||||
console.log(`Seeded ${sectionCount} sections and ${memberCount} team members.`);
|
||||
console.log(`Database: ${DB_PATH}`);
|
||||
|
||||
db.close();
|
||||
Reference in New Issue
Block a user