226 lines
8.6 KiB
TypeScript
226 lines
8.6 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,
|
|
} 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: '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>
|
|
);
|
|
}
|