Files
fotospiel-app/resources/js/admin/components/AdminLayout.tsx

143 lines
5.3 KiB
TypeScript

import React from 'react';
import { NavLink } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { cn } from '@/lib/utils';
import toast from 'react-hot-toast';
import {
ADMIN_HOME_PATH,
ADMIN_EVENTS_PATH,
ADMIN_SETTINGS_PATH,
ADMIN_BILLING_PATH,
ADMIN_ENGAGEMENT_PATH,
} from '../constants';
import {
LayoutDashboard,
CalendarDays,
Sparkles,
CreditCard,
Settings as SettingsIcon,
} from 'lucide-react';
import { LanguageSwitcher } from './LanguageSwitcher';
import { registerApiErrorListener } from '../lib/apiError';
const navItems = [
{ to: ADMIN_HOME_PATH, labelKey: 'navigation.dashboard', icon: LayoutDashboard, end: true },
{ to: ADMIN_EVENTS_PATH, labelKey: 'navigation.events', icon: CalendarDays },
{ to: ADMIN_ENGAGEMENT_PATH, labelKey: 'navigation.engagement', icon: Sparkles },
{ to: ADMIN_BILLING_PATH, labelKey: 'navigation.billing', icon: CreditCard },
{ to: ADMIN_SETTINGS_PATH, labelKey: 'navigation.settings', icon: SettingsIcon },
];
interface AdminLayoutProps {
title: string;
subtitle?: string;
actions?: React.ReactNode;
children: React.ReactNode;
}
export function AdminLayout({ title, subtitle, actions, children }: AdminLayoutProps) {
const { t } = useTranslation('common');
React.useEffect(() => {
document.body.classList.add('tenant-admin-theme');
return () => {
document.body.classList.remove('tenant-admin-theme');
};
}, []);
React.useEffect(() => {
const unsubscribe = registerApiErrorListener((detail) => {
const fallback = t('errors.generic');
const message = detail?.message?.trim() ? detail.message : fallback;
toast.error(message, {
id: detail?.code ? `api-error-${detail.code}` : undefined,
});
});
return unsubscribe;
}, [t]);
return (
<div className="relative min-h-svh overflow-hidden bg-slate-950 text-white">
<div
aria-hidden
className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_at_top,_rgba(255,137,170,0.28),_transparent_60%),radial-gradient(ellipse_at_bottom,_rgba(99,102,241,0.25),_transparent_65%)]"
/>
<div aria-hidden className="absolute inset-0 bg-gradient-to-br from-slate-950 via-slate-900/60 to-[#1d1130]" />
<div className="relative z-10 flex min-h-svh flex-col">
<header className="sticky top-0 z-30 px-4 pt-6 sm:px-6">
<div className="mx-auto w-full max-w-6xl rounded-3xl border border-white/15 bg-white/85 text-slate-900 shadow-2xl shadow-rose-400/10 backdrop-blur-xl">
<div className="flex flex-col gap-6 px-6 py-6 md:flex-row md:items-center md:justify-between">
<div>
<p className="text-xs uppercase tracking-[0.35em] text-rose-400">{t('app.brand')}</p>
<h1 className="font-display text-3xl font-semibold text-slate-900">{title}</h1>
{subtitle ? <p className="mt-1 text-sm text-slate-600">{subtitle}</p> : null}
</div>
<div className="flex flex-wrap items-center gap-2">
<LanguageSwitcher />
{actions}
</div>
</div>
<nav className="hidden items-center gap-2 border-t border-white/20 px-6 py-4 md:flex">
{navItems.map(({ to, labelKey, icon: Icon, end }) => (
<NavLink
key={to}
to={to}
end={end}
className={({ isActive }) =>
cn(
'flex items-center gap-2 rounded-full px-4 py-2 text-sm font-medium transition-all duration-200',
isActive
? 'bg-gradient-to-r from-[#ff5f87] via-[#ec4899] to-[#6366f1] text-white shadow-lg shadow-rose-300/30'
: 'bg-white/70 text-slate-600 hover:bg-white hover:text-slate-900'
)
}
>
<Icon className="h-4 w-4" />
{t(labelKey)}
</NavLink>
))}
</nav>
</div>
</header>
<main className="relative z-10 mx-auto w-full max-w-6xl flex-1 px-4 pb-28 pt-6 sm:px-6">
<div className="grid gap-6">{children}</div>
</main>
<TenantMobileNav items={navItems} />
</div>
</div>
);
}
function TenantMobileNav({ items }: { items: typeof navItems }) {
const { t } = useTranslation('common');
return (
<nav className="sticky bottom-4 z-40 px-4 pb-2 md:hidden">
<div className="mx-auto flex w-full max-w-md items-center justify-around rounded-full border border-white/20 bg-white/90 p-2 text-slate-600 shadow-2xl shadow-rose-300/20 backdrop-blur-xl">
{items.map(({ to, labelKey, icon: Icon, end }) => (
<NavLink
key={to}
to={to}
end={end}
className={({ isActive }) =>
cn(
'flex flex-col items-center gap-1 rounded-full px-3 py-2 text-xs font-medium transition',
isActive
? 'bg-gradient-to-r from-[#ff5f87] via-[#ec4899] to-[#6366f1] text-white shadow-lg shadow-rose-300/30'
: 'text-slate-600 hover:text-slate-900'
)
}
>
<Icon className="h-5 w-5" />
<span>{t(labelKey)}</span>
</NavLink>
))}
</div>
</nav>
);
}