import React from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { AlertTriangle, ArrowRight, CalendarDays, Camera, Heart, Plus } from 'lucide-react'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { TenantHeroCard, FrostedSurface, tenantHeroPrimaryButtonClass, tenantHeroSecondaryButtonClass, SectionCard, SectionHeader, StatCarousel, ActionGrid, } from '../components/tenant'; import { cn } from '@/lib/utils'; import { AdminLayout } from '../components/AdminLayout'; import { getEvents, TenantEvent } from '../api'; import { isAuthError } from '../auth/tokens'; import { getApiErrorMessage } from '../lib/apiError'; import { adminPath, ADMIN_SETTINGS_PATH, ADMIN_WELCOME_BASE_PATH, ADMIN_EVENT_VIEW_PATH, ADMIN_EVENT_EDIT_PATH, ADMIN_EVENT_PHOTOS_PATH, ADMIN_EVENT_MEMBERS_PATH, ADMIN_EVENT_TASKS_PATH, ADMIN_EVENT_INVITES_PATH, ADMIN_EVENT_TOOLKIT_PATH, } from '../constants'; import { buildLimitWarnings } from '../lib/limitWarnings'; import { useTranslation } from 'react-i18next'; export default function EventsPage() { const { t } = useTranslation('management'); const { t: tCommon } = useTranslation('common'); const [rows, setRows] = React.useState([]); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); const navigate = useNavigate(); React.useEffect(() => { (async () => { try { setRows(await getEvents()); } catch (err) { if (!isAuthError(err)) { setError(getApiErrorMessage(err, t('events.errors.loadFailed', 'Event konnte nicht geladen werden.'))); } } finally { setLoading(false); } })(); }, [t]); const translateManagement = React.useCallback( (key: string, fallback?: string, options?: Record) => t(key, { defaultValue: fallback, ...(options ?? {}) }), [t], ); const translateCommon = React.useCallback( (key: string, fallback?: string, options?: Record) => tCommon(key, { defaultValue: fallback, ...(options ?? {}) }), [tCommon], ); const totalEvents = rows.length; const publishedEvents = React.useMemo( () => rows.filter((event) => event.status === 'published').length, [rows], ); const nextEvent = React.useMemo(() => { return ( rows .filter((event) => event.event_date) .slice() .sort((a, b) => { const dateA = a.event_date ? new Date(a.event_date).getTime() : Infinity; const dateB = b.event_date ? new Date(b.event_date).getTime() : Infinity; return dateA - dateB; })[0] ?? null ); }, [rows]); const statItems = React.useMemo( () => [ { key: 'total', label: t('events.list.stats.total', 'Events gesamt'), value: totalEvents, }, { key: 'published', label: t('events.list.stats.published', 'Veröffentlicht'), value: publishedEvents, }, { key: 'drafts', label: t('events.list.stats.drafts', 'Entwürfe'), value: Math.max(0, totalEvents - publishedEvents), }, nextEvent ? { key: 'next', label: t('events.list.stats.nextEvent', 'Nächstes Event'), value: formatDate(nextEvent.event_date), } : null, ].filter(Boolean) as { key: string; label: string; value: string | number }[], [t, totalEvents, publishedEvents, nextEvent], ); const actionItems = React.useMemo( () => [ { key: 'new', label: t('events.list.actions.create', 'Neues Event'), description: t('events.list.actions.createDescription', 'Starte mit einem frischen Setup.'), onClick: () => navigate(adminPath('/events/new')), }, { key: 'welcome', label: t('events.list.actions.guidedSetup', 'Geführte Einrichtung'), description: t('events.list.actions.guidedSetupDescription', 'Springe zurück in den Onboarding-Flow.'), onClick: () => navigate(ADMIN_WELCOME_BASE_PATH), }, { key: 'settings', label: t('events.list.actions.settings', 'Einstellungen öffnen'), description: t('events.list.actions.settingsDescription', 'Passe Farben, Branding und Aufgabenpakete an.'), onClick: () => navigate(ADMIN_SETTINGS_PATH), }, ], [t, navigate], ); const pageTitle = translateManagement('events.list.title', 'Deine Events'); const pageSubtitle = translateManagement( 'events.list.subtitle', 'Plane Momente, die in Erinnerung bleiben. Hier verwaltest du alles rund um deine Veranstaltungen.' ); const heroDescription = t( 'events.list.hero.description', 'Aktiviere Storytelling, Moderation und Galerie-Workflows für jeden Anlass in wenigen Minuten.' ); const heroSummaryCopy = totalEvents > 0 ? t('events.list.hero.summary', ':count Events aktiv verwaltet – halte Aufgaben und Uploads im Blick.', { count: totalEvents }) : t('events.list.hero.summary_empty', 'Noch keine Events – starte jetzt mit deinem ersten Konzept.'); const heroSecondaryCopy = t( 'events.list.hero.secondary', 'Erstelle Events im Admin, begleite Gäste live vor Ort und prüfe Kennzahlen im Marketing-Dashboard.' ); const heroBadge = t('events.list.badge.dashboard', 'Tenant Dashboard'); const heroPrimaryAction = ( ); const heroSecondaryAction = ( ); const heroAside = (

{t('events.list.hero.published_label', 'Veröffentlichte Events')}

{publishedEvents}

{t('events.list.hero.total_label', ':count insgesamt', { count: totalEvents })}

{nextEvent ? (

{t('events.list.hero.next_label', 'Nächstes Event')}

{renderName(nextEvent.name)}

{formatDate(nextEvent.event_date)}

{nextEvent.slug ? ( ) : null}
) : (

{t('events.list.hero.no_upcoming', 'Plane ein Datum, um hier die nächste Station zu sehen.')}

)}
); const draftEvents = totalEvents - publishedEvents; const [statusFilter, setStatusFilter] = React.useState<'all' | 'published' | 'draft'>('all'); const filteredRows = React.useMemo(() => { if (statusFilter === 'published') { return rows.filter((event) => event.status === 'published'); } if (statusFilter === 'draft') { return rows.filter((event) => event.status !== 'published'); } return rows; }, [rows, statusFilter]); const overviewDescription = React.useMemo(() => { if (loading) { return t('events.list.overview.loading', 'Wir sammeln gerade deine Event-Details …'); } if (filteredRows.length === 0) { if (statusFilter === 'published') { return t('events.list.overview.empty_published', 'Noch keine veröffentlichten Events.'); } if (statusFilter === 'draft') { return t('events.list.overview.empty_drafts', 'Keine Entwürfe – nutze die Zeit für dein nächstes Event.'); } return t('events.list.overview.empty', 'Noch keine Events - starte jetzt und lege dein erstes Event an.'); } return t('events.list.overview.count', '{{count}} Events aktiv verwaltet.', { count: filteredRows.length }); }, [filteredRows.length, loading, statusFilter, t]); const filterOptions: Array<{ key: 'all' | 'published' | 'draft'; label: string; count: number }> = [ { key: 'all', label: t('events.list.filters.all', 'Alle'), count: totalEvents }, { key: 'published', label: t('events.list.filters.published', 'Live'), count: publishedEvents }, { key: 'draft', label: t('events.list.filters.drafts', 'Entwürfe'), count: Math.max(0, draftEvents) }, ]; return ( {error && ( Fehler beim Laden {error} )} {t('events.list.badge.dashboard', 'Tenant Dashboard')} )} />
{filterOptions.map((option) => ( ))}
{loading ? ( ) : filteredRows.length === 0 ? ( navigate(adminPath('/events/new'))} /> ) : (
{filteredRows.map((event) => ( ))}
)}
); } function EventCard({ event, translate, translateCommon, }: { event: TenantEvent; translate: (key: string, fallback?: string, options?: Record) => string; translateCommon: (key: string, fallback?: string, options?: Record) => string; }) { const slug = event.slug; const isPublished = event.status === 'published'; const photoCount = event.photo_count ?? 0; const likeCount = event.like_count ?? 0; const limitWarnings = React.useMemo( () => buildLimitWarnings(event.limits ?? null, (key, opts) => translateCommon(`limits.${key}`, undefined, opts)), [event.limits, translateCommon], ); const metaItems = [ { key: 'date', label: translate('events.list.meta.date', 'Eventdatum'), value: formatDate(event.event_date), icon: , }, { key: 'photos', label: translate('events.list.meta.photos', 'Uploads'), value: photoCount, icon: , }, { key: 'likes', label: translate('events.list.meta.likes', 'Likes'), value: likeCount, icon: , }, ]; const secondaryLinks = [ { key: 'edit', label: translateCommon('actions.edit', 'Bearbeiten'), to: ADMIN_EVENT_EDIT_PATH(slug) }, { key: 'members', label: translate('events.list.actions.members', 'Mitglieder'), to: ADMIN_EVENT_MEMBERS_PATH(slug) }, { key: 'tasks', label: translate('events.list.actions.tasks', 'Tasks'), to: ADMIN_EVENT_TASKS_PATH(slug) }, { key: 'invites', label: translate('events.list.actions.invites', 'QR-Einladungen'), to: ADMIN_EVENT_INVITES_PATH(slug) }, { key: 'toolkit', label: translate('events.list.actions.toolkit', 'Toolkit'), to: ADMIN_EVENT_TOOLKIT_PATH(slug) }, ]; return (

{translate('events.list.item.label', 'Event')}

{renderName(event.name)}

{isPublished ? translateCommon('events.status.published', 'Veröffentlicht') : translateCommon('events.status.draft', 'Entwurf')}
{metaItems.map((item) => ( ))}
{limitWarnings.length > 0 && (
{limitWarnings.map((warning) => (
{warning.message}
))}
)}
{secondaryLinks.map((action) => ( {action.label} ))}
); } function MetaChip({ icon, label, value, }: { icon: React.ReactNode; label: string; value: string | number; }) { return (
{icon} {label}

{value}

); } function ActionChip({ to, children }: { to: string; children: React.ReactNode }) { return ( {children} ); } function LoadingState() { return (
{Array.from({ length: 3 }).map((_, index) => ( ))}
); } function EmptyState({ title, description, onCreate, }: { title: string; description: string; onCreate: () => void; }) { return (

{title}

{description}

); } function formatDate(iso: string | null): string { if (!iso) return 'Noch kein Datum'; const date = new Date(iso); if (Number.isNaN(date.getTime())) { return 'Unbekanntes Datum'; } return date.toLocaleDateString('de-DE', { day: '2-digit', month: 'short', year: 'numeric', }); } function renderName(name: TenantEvent['name']): string { if (typeof name === 'string') return name; if (name && typeof name === 'object') { return name.de ?? name.en ?? Object.values(name)[0] ?? 'Unbenanntes Event'; } return 'Unbenanntes Event'; }