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

View File

@@ -0,0 +1,94 @@
"use client";
import { Plus, Trash2, ChevronUp, ChevronDown } from "lucide-react";
interface ArrayEditorProps<T> {
items: T[];
onChange: (items: T[]) => void;
renderItem: (item: T, index: number, update: (item: T) => void) => React.ReactNode;
createItem: () => T;
label?: string;
addLabel?: string;
}
export function ArrayEditor<T>({
items,
onChange,
renderItem,
createItem,
label,
addLabel = "Добавить",
}: ArrayEditorProps<T>) {
function updateItem(index: number, item: T) {
const updated = [...items];
updated[index] = item;
onChange(updated);
}
function removeItem(index: number) {
onChange(items.filter((_, i) => i !== index));
}
function moveItem(index: number, direction: -1 | 1) {
const newIndex = index + direction;
if (newIndex < 0 || newIndex >= items.length) return;
const updated = [...items];
[updated[index], updated[newIndex]] = [updated[newIndex], updated[index]];
onChange(updated);
}
return (
<div>
{label && (
<h3 className="text-sm font-medium text-neutral-300 mb-3">{label}</h3>
)}
<div className="space-y-3">
{items.map((item, i) => (
<div
key={i}
className="rounded-lg border border-white/10 bg-neutral-900/50 p-4"
>
<div className="flex items-start justify-between gap-2 mb-3">
<div className="flex gap-1">
<button
type="button"
onClick={() => moveItem(i, -1)}
disabled={i === 0}
className="rounded p-1 text-neutral-500 hover:text-white disabled:opacity-30 transition-colors"
>
<ChevronUp size={16} />
</button>
<button
type="button"
onClick={() => moveItem(i, 1)}
disabled={i === items.length - 1}
className="rounded p-1 text-neutral-500 hover:text-white disabled:opacity-30 transition-colors"
>
<ChevronDown size={16} />
</button>
</div>
<button
type="button"
onClick={() => removeItem(i)}
className="rounded p-1 text-neutral-500 hover:text-red-400 transition-colors"
>
<Trash2 size={16} />
</button>
</div>
{renderItem(item, i, (updated) => updateItem(i, updated))}
</div>
))}
</div>
<button
type="button"
onClick={() => onChange([...items, createItem()])}
className="mt-3 flex items-center gap-2 rounded-lg border border-dashed border-white/20 px-4 py-2.5 text-sm text-neutral-400 hover:text-white hover:border-white/40 transition-colors"
>
<Plus size={16} />
{addLabel}
</button>
</div>
);
}