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:
2026-03-11 16:59:12 +03:00
parent d5afaf92ba
commit 27c1348f89
44 changed files with 3709 additions and 69 deletions
+50
View File
@@ -0,0 +1,50 @@
import { NextRequest, NextResponse } from "next/server";
import { writeFile, mkdir } from "fs/promises";
import path from "path";
const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp", "image/avif"];
const MAX_SIZE = 5 * 1024 * 1024; // 5MB
export async function POST(request: NextRequest) {
const formData = await request.formData();
const file = formData.get("file") as File | null;
const folder = (formData.get("folder") as string) || "team";
if (!file) {
return NextResponse.json({ error: "No file provided" }, { status: 400 });
}
if (!ALLOWED_TYPES.includes(file.type)) {
return NextResponse.json(
{ error: "Only JPEG, PNG, WebP, and AVIF are allowed" },
{ status: 400 }
);
}
if (file.size > MAX_SIZE) {
return NextResponse.json(
{ error: "File too large (max 5MB)" },
{ status: 400 }
);
}
// Sanitize filename
const ext = path.extname(file.name) || ".webp";
const baseName = file.name
.replace(ext, "")
.toLowerCase()
.replace(/[^a-z0-9а-яё-]/gi, "-")
.replace(/-+/g, "-")
.slice(0, 50);
const fileName = `${baseName}-${Date.now()}${ext}`;
const dir = path.join(process.cwd(), "public", "images", folder);
await mkdir(dir, { recursive: true });
const buffer = Buffer.from(await file.arrayBuffer());
const filePath = path.join(dir, fileName);
await writeFile(filePath, buffer);
const publicPath = `/images/${folder}/${fileName}`;
return NextResponse.json({ path: publicPath });
}