Files
blackheart-website/src/app/admin/layout.tsx
diana.dolgolyova 27c1348f89 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>
2026-03-11 16:59:12 +03:00

147 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState } from "react";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import {
LayoutDashboard,
Sparkles,
Users,
BookOpen,
Calendar,
DollarSign,
HelpCircle,
Phone,
FileText,
Globe,
LogOut,
Menu,
X,
ChevronLeft,
} from "lucide-react";
const NAV_ITEMS = [
{ href: "/admin", label: "Дашборд", icon: LayoutDashboard },
{ href: "/admin/meta", label: "SEO / Мета", icon: Globe },
{ href: "/admin/hero", label: "Главный экран", icon: Sparkles },
{ href: "/admin/about", label: "О студии", icon: FileText },
{ href: "/admin/team", label: "Команда", icon: Users },
{ href: "/admin/classes", label: "Направления", icon: BookOpen },
{ href: "/admin/schedule", label: "Расписание", icon: Calendar },
{ href: "/admin/pricing", label: "Цены", icon: DollarSign },
{ href: "/admin/faq", label: "FAQ", icon: HelpCircle },
{ href: "/admin/contact", label: "Контакты", icon: Phone },
];
export default function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
const pathname = usePathname();
const router = useRouter();
const [sidebarOpen, setSidebarOpen] = useState(false);
// Don't render admin shell on login page
if (pathname === "/admin/login") {
return <>{children}</>;
}
async function handleLogout() {
await fetch("/api/logout", { method: "POST" });
router.push("/admin/login");
}
function isActive(href: string) {
if (href === "/admin") return pathname === "/admin";
return pathname.startsWith(href);
}
return (
<div className="flex min-h-screen bg-neutral-950 text-white">
{/* Mobile overlay */}
{sidebarOpen && (
<div
className="fixed inset-0 z-40 bg-black/60 lg:hidden"
onClick={() => setSidebarOpen(false)}
/>
)}
{/* Sidebar */}
<aside
className={`fixed inset-y-0 left-0 z-50 flex w-64 flex-col border-r border-white/10 bg-neutral-900 transition-transform lg:static lg:translate-x-0 ${
sidebarOpen ? "translate-x-0" : "-translate-x-full"
}`}
>
<div className="flex items-center justify-between border-b border-white/10 px-5 py-4">
<Link href="/admin" className="text-lg font-bold">
BLACK HEART
</Link>
<button
onClick={() => setSidebarOpen(false)}
className="lg:hidden text-neutral-400 hover:text-white"
>
<X size={20} />
</button>
</div>
<nav className="flex-1 overflow-y-auto p-3 space-y-1">
{NAV_ITEMS.map((item) => {
const Icon = item.icon;
const active = isActive(item.href);
return (
<Link
key={item.href}
href={item.href}
onClick={() => setSidebarOpen(false)}
className={`flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm transition-colors ${
active
? "bg-gold/10 text-gold font-medium"
: "text-neutral-400 hover:text-white hover:bg-white/5"
}`}
>
<Icon size={18} />
{item.label}
</Link>
);
})}
</nav>
<div className="border-t border-white/10 p-3 space-y-1">
<Link
href="/"
target="_blank"
className="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm text-neutral-400 hover:text-white hover:bg-white/5 transition-colors"
>
<ChevronLeft size={18} />
Открыть сайт
</Link>
<button
onClick={handleLogout}
className="flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-sm text-neutral-400 hover:text-red-400 hover:bg-white/5 transition-colors"
>
<LogOut size={18} />
Выйти
</button>
</div>
</aside>
{/* Main content */}
<div className="flex-1 flex flex-col min-w-0">
{/* Top bar (mobile) */}
<header className="flex items-center gap-3 border-b border-white/10 px-4 py-3 lg:hidden">
<button
onClick={() => setSidebarOpen(true)}
className="text-neutral-400 hover:text-white"
>
<Menu size={24} />
</button>
<span className="font-bold">BLACK HEART</span>
</header>
<main className="flex-1 p-4 sm:p-6 lg:p-8">{children}</main>
</div>
</div>
);
}