feat: unify tenant admin ui and add photo moderation

This commit is contained in:
Codex Agent
2025-11-07 13:50:55 +01:00
parent 9cc9950b0c
commit 253239455b
14 changed files with 995 additions and 583 deletions

View File

@@ -1,15 +1,6 @@
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,
@@ -17,6 +8,15 @@ import {
CreditCard,
Settings as SettingsIcon,
} from 'lucide-react';
import toast from 'react-hot-toast';
import { cn } from '@/lib/utils';
import {
ADMIN_HOME_PATH,
ADMIN_EVENTS_PATH,
ADMIN_SETTINGS_PATH,
ADMIN_BILLING_PATH,
ADMIN_ENGAGEMENT_PATH,
} from '../constants';
import { LanguageSwitcher } from './LanguageSwitcher';
import { registerApiErrorListener } from '../lib/apiError';
import { getDashboardSummary, getEvents, getTenantPackagesOverview } from '../api';
@@ -92,27 +92,29 @@ export function AdminLayout({ title, subtitle, actions, children }: AdminLayoutP
}, [t]);
return (
<div className="relative min-h-svh overflow-hidden bg-slate-950 text-white">
<div className="relative min-h-svh overflow-hidden bg-gradient-to-br from-rose-50 via-white to-slate-50 text-slate-900 dark:bg-slate-950 dark: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%)]"
className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_10%_10%,rgba(255,137,170,0.25),transparent_55%),radial-gradient(circle_at_80%_0%,rgba(96,165,250,0.22),transparent_60%)] opacity-60 dark:opacity-30"
/>
<div aria-hidden className="absolute inset-0 bg-gradient-to-br from-slate-950 via-slate-900/60 to-[#1d1130]" />
<div aria-hidden className="absolute inset-0 bg-gradient-to-b from-white/80 via-white/60 to-transparent dark:from-slate-950 dark:via-slate-950/90 dark:to-slate-950/80" />
<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">
<header className="sticky top-0 z-40 border-b border-slate-200/70 bg-white/90 backdrop-blur-xl dark:border-white/10 dark:bg-slate-950/80">
<div className="mx-auto flex w-full max-w-6xl flex-col gap-3 px-4 py-4 sm:flex-row sm:items-center sm:justify-between sm:px-6">
<div className="space-y-1">
<p className="text-[10px] font-semibold uppercase tracking-[0.4em] text-rose-500 dark:text-rose-200">{t('app.brand')}</p>
<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}
<h1 className="text-xl font-semibold text-slate-900 dark:text-white sm:text-2xl">{title}</h1>
{subtitle ? <p className="text-xs text-slate-600 dark:text-slate-300 sm:text-sm">{subtitle}</p> : null}
</div>
</div>
<nav className="hidden items-center gap-2 border-t border-slate-200/70 px-6 py-4 md:flex">
<div className="flex flex-wrap items-center gap-2">
{actions}
<LanguageSwitcher />
</div>
</div>
<nav className="hidden border-t border-slate-200/60 dark:border-white/5 sm:block">
<div className="mx-auto flex w-full max-w-6xl items-center gap-1 px-4 py-2 sm:px-6">
{navItems.map(({ to, labelKey, icon: Icon, end }) => (
<NavLink
key={to}
@@ -123,10 +125,10 @@ export function AdminLayout({ title, subtitle, actions, children }: AdminLayoutP
onTouchStart={() => triggerPrefetch(to)}
className={({ isActive }) =>
cn(
'flex items-center gap-2 rounded-full px-4 py-2 text-sm font-semibold transition-all duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-300 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950',
'flex items-center gap-2 rounded-2xl px-3 py-2 text-xs font-semibold uppercase tracking-wide transition-all duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-400/60',
isActive
? 'bg-rose-600 text-white shadow-md shadow-rose-400/30'
: 'border border-slate-200/80 bg-white text-slate-700 hover:bg-rose-50/80 hover:text-rose-700'
? 'bg-rose-600 text-white shadow shadow-rose-300/40'
: 'text-slate-500 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white'
)
}
>
@@ -134,28 +136,34 @@ export function AdminLayout({ title, subtitle, actions, children }: AdminLayoutP
{t(labelKey)}
</NavLink>
))}
</nav>
</div>
</div>
</nav>
</header>
<main className="relative z-10 mx-auto w-full max-w-6xl flex-1 px-4 pb-[calc(env(safe-area-inset-bottom,0)+6.5rem)] pt-6 sm:px-6 md:pb-16">
<div className="grid gap-6">{children}</div>
<main className="relative z-10 mx-auto w-full max-w-5xl flex-1 px-4 pb-[calc(env(safe-area-inset-bottom,0)+5.5rem)] pt-5 sm:px-6 md:pb-16">
<div className="space-y-5">{children}</div>
</main>
<TenantMobileNav items={navItems} />
<TenantMobileNav items={navItems} onPrefetch={triggerPrefetch} />
</div>
</div>
);
}
function TenantMobileNav({ items }: { items: typeof navItems }) {
function TenantMobileNav({
items,
onPrefetch,
}: {
items: typeof navItems;
onPrefetch: (path: string) => void;
}) {
const { t } = useTranslation('common');
return (
<nav className="md:hidden" aria-label={t('navigation.mobile', { defaultValue: 'Tenant Navigation' })}>
<div
aria-hidden
className="pointer-events-none fixed inset-x-0 bottom-0 z-30 h-16 bg-gradient-to-t from-slate-950/35 via-transparent to-transparent dark:from-black/60"
className="pointer-events-none fixed inset-x-0 bottom-0 z-30 h-12 bg-gradient-to-t from-white via-white/40 to-transparent dark:from-slate-950 dark:via-slate-950/60 dark:to-transparent"
/>
<div className="fixed inset-x-0 bottom-0 z-40 border-t border-slate-200/80 bg-white/95 px-4 pb-[calc(env(safe-area-inset-bottom,0)+0.75rem)] pt-3 shadow-2xl shadow-rose-300/15 backdrop-blur supports-[backdrop-filter]:bg-white/90 dark:border-slate-800/70 dark:bg-slate-950/90">
<div className="mx-auto flex max-w-xl items-center justify-around gap-1">
@@ -164,9 +172,9 @@ function TenantMobileNav({ items }: { items: typeof navItems }) {
key={to}
to={to}
end={end}
onPointerEnter={() => triggerPrefetch(to)}
onFocus={() => triggerPrefetch(to)}
onTouchStart={() => triggerPrefetch(to)}
onPointerEnter={() => onPrefetch(to)}
onFocus={() => onPrefetch(to)}
onTouchStart={() => onPrefetch(to)}
className={({ isActive }) =>
cn(
'flex flex-col items-center gap-1 rounded-xl px-3 py-2 text-xs font-semibold text-slate-600 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-300 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-slate-300 dark:focus-visible:ring-offset-slate-950',