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

228 lines
8.8 KiB
TypeScript

import React from 'react';
import { useNavigate, NavLink, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { CalendarDays, ChevronDown, PlusCircle } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@/components/ui/sheet';
import { useEventContext } from '../context/EventContext';
import {
ADMIN_EVENT_CREATE_PATH,
ADMIN_EVENT_INVITES_PATH,
ADMIN_EVENT_MEMBERS_PATH,
ADMIN_EVENT_PHOTOS_PATH,
ADMIN_EVENT_TASKS_PATH,
ADMIN_EVENT_TOOLKIT_PATH,
ADMIN_EVENT_VIEW_PATH,
ADMIN_EVENT_PHOTOBOOTH_PATH,
} from '../constants';
import type { TenantEvent } from '../api';
import { cn } from '@/lib/utils';
function resolveEventName(event: TenantEvent): string {
const name = event.name;
if (typeof name === 'string' && name.trim().length > 0) {
return name;
}
if (name && typeof name === 'object') {
const first = Object.values(name).find((value) => typeof value === 'string' && value.trim().length > 0);
if (first) {
return first;
}
}
return event.slug ?? 'Event';
}
function formatEventDate(value?: string | null, locale = 'de-DE'): string | null {
if (!value) {
return null;
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return null;
}
try {
return new Intl.DateTimeFormat(locale, { day: '2-digit', month: 'short', year: 'numeric' }).format(date);
} catch {
return date.toISOString().slice(0, 10);
}
}
function buildEventLinks(slug: string, t: ReturnType<typeof useTranslation>['t']) {
return [
{ key: 'summary', label: t('eventMenu.summary', 'Übersicht'), href: ADMIN_EVENT_VIEW_PATH(slug) },
{ key: 'photos', label: t('eventMenu.photos', 'Uploads'), href: ADMIN_EVENT_PHOTOS_PATH(slug) },
{ key: 'photobooth', label: t('eventMenu.photobooth', 'Photobooth'), href: ADMIN_EVENT_PHOTOBOOTH_PATH(slug) },
{ key: 'guests', label: t('eventMenu.guests', 'Team & Gäste'), href: ADMIN_EVENT_MEMBERS_PATH(slug) },
{ key: 'tasks', label: t('eventMenu.tasks', 'Aufgaben'), href: ADMIN_EVENT_TASKS_PATH(slug) },
{ key: 'invites', label: t('eventMenu.invites', 'Einladungen'), href: ADMIN_EVENT_INVITES_PATH(slug) },
{ key: 'toolkit', label: t('eventMenu.toolkit', 'Toolkit'), href: ADMIN_EVENT_TOOLKIT_PATH(slug) },
];
}
export function EventSwitcher() {
const { events, activeEvent, selectEvent } = useEventContext();
const { t, i18n } = useTranslation('common');
const navigate = useNavigate();
const [open, setOpen] = React.useState(false);
const locale = i18n.language?.startsWith('en') ? 'en-GB' : 'de-DE';
const buttonLabel = activeEvent ? resolveEventName(activeEvent) : t('eventSwitcher.placeholder', 'Event auswählen');
const buttonHint = activeEvent?.event_date
? formatEventDate(activeEvent.event_date, locale)
: events.length > 1
? t('eventSwitcher.multiple', 'Mehrere Events')
: t('eventSwitcher.empty', 'Noch kein Event');
const handleSelect = (event: TenantEvent) => {
selectEvent(event.slug ?? null);
setOpen(false);
if (event.slug) {
navigate(ADMIN_EVENT_VIEW_PATH(event.slug));
}
};
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button variant="outline" size="sm" className="rounded-full border-rose-100 bg-white/80 px-4 text-sm font-semibold text-slate-700 shadow-sm hover:bg-rose-50 dark:border-white/20 dark:bg-white/10 dark:text-white">
<CalendarDays className="mr-2 h-4 w-4" />
<span className="hidden sm:inline">{buttonLabel}</span>
<span className="text-xs text-slate-500 dark:text-slate-300 sm:ml-2">
{buttonHint}
</span>
<ChevronDown className="ml-2 h-4 w-4" />
</Button>
</SheetTrigger>
<SheetContent side="bottom" className="rounded-t-3xl p-0">
<SheetHeader className="border-b border-slate-200 p-4 dark:border-white/10">
<SheetTitle>{t('eventSwitcher.title', 'Event auswählen')}</SheetTitle>
<SheetDescription>
{events.length === 0
? t('eventSwitcher.emptyDescription', 'Erstelle dein erstes Event, um loszulegen.')
: t('eventSwitcher.description', 'Wähle ein Event für die Bearbeitung oder lege ein neues an.')}
</SheetDescription>
</SheetHeader>
<div className="max-h-[60vh] overflow-y-auto p-4">
{events.length === 0 ? (
<div className="rounded-2xl border border-dashed border-slate-200 p-6 text-center text-sm text-slate-600 dark:border-white/15 dark:text-slate-300">
{t('eventSwitcher.noEvents', 'Noch keine Events vorhanden.')}
</div>
) : (
<div className="space-y-2">
{events.map((event) => {
const isActive = activeEvent?.id === event.id;
const date = formatEventDate(event.event_date, locale);
return (
<button
key={event.id}
type="button"
onClick={() => handleSelect(event)}
className={cn(
'w-full rounded-2xl border px-4 py-3 text-left transition hover:border-rose-200 dark:border-white/10 dark:bg-white/5',
isActive
? 'border-rose-500 bg-rose-50/70 text-rose-900 dark:border-rose-300 dark:bg-rose-200/10 dark:text-rose-100'
: 'bg-white text-slate-900'
)}
>
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-sm font-semibold">{resolveEventName(event)}</p>
<p className="text-xs text-slate-500 dark:text-slate-300">{date ?? t('eventSwitcher.noDate', 'Kein Datum')}</p>
</div>
{isActive ? (
<Badge className="bg-rose-600 text-white">{t('eventSwitcher.active', 'Aktiv')}</Badge>
) : null}
</div>
</button>
);
})}
</div>
)}
<Button
size="sm"
variant="secondary"
className="mt-4 w-full rounded-full"
onClick={() => {
setOpen(false);
navigate(ADMIN_EVENT_CREATE_PATH);
}}
>
<PlusCircle className="mr-2 h-4 w-4" />
{t('eventSwitcher.create', 'Neues Event anlegen')}
</Button>
{activeEvent?.slug ? (
<div className="mt-6 space-y-3">
<p className="text-xs uppercase tracking-[0.3em] text-slate-500 dark:text-slate-300">
{t('eventSwitcher.actions', 'Event-Funktionen')}
</p>
<div className="grid gap-2">
{buildEventLinks(activeEvent.slug, t).map((action) => (
<Button
key={action.key}
variant="ghost"
className="justify-between rounded-2xl border border-slate-200 bg-white text-left text-sm font-semibold text-slate-700 hover:bg-rose-50 dark:border-white/10 dark:bg-white/5 dark:text-white"
onClick={() => {
setOpen(false);
navigate(action.href);
}}
>
{action.label}
<ChevronDown className="rotate-[-90deg]" />
</Button>
))}
</div>
</div>
) : null}
</div>
</SheetContent>
</Sheet>
);
}
export function EventMenuBar() {
const { activeEvent } = useEventContext();
const { t } = useTranslation('common');
const location = useLocation();
if (!activeEvent?.slug) {
return null;
}
const links = buildEventLinks(activeEvent.slug, t);
return (
<div className="border-t border-slate-200 bg-white/80 px-4 py-2 dark:border-white/10 dark:bg-slate-950/80">
<div className="flex items-center gap-2 overflow-x-auto text-sm">
{links.map((link) => (
<NavLink
key={link.key}
to={link.href}
className={({ isActive }) =>
cn(
'whitespace-nowrap rounded-full px-3 py-1 text-xs font-semibold transition',
isActive || location.pathname.startsWith(link.href)
? 'bg-rose-600 text-white shadow shadow-rose-400/40'
: 'bg-white text-slate-600 ring-1 ring-slate-200 hover:text-rose-600 dark:bg-white/10 dark:text-white dark:ring-white/10'
)
}
>
{link.label}
</NavLink>
))}
</div>
</div>
);
}