import React from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { AlertTriangle, ArrowRight, CalendarDays, Camera, Heart, Plus } from 'lucide-react'; import { YStack, XStack } from '@tamagui/stacks'; import { SizableText as Text } from '@tamagui/text'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { AppCard, PrimaryCTA, Segmented, StatusPill, MetaRow, BottomNav } from '../tamagui/primitives'; import { AdminLayout } from '../components/AdminLayout'; import { getEvents, TenantEvent } from '../api'; import { isAuthError } from '../auth/tokens'; import { getApiErrorMessage } from '../lib/apiError'; import { adminPath, 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_PHOTOBOOTH_PATH, ADMIN_EVENT_BRANDING_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 pageTitle = translateManagement('events.list.title', 'Deine Events'); 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 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} ) : null} {t('events.list.dashboardTitle', 'All Events Dashboard')} {t('events.list.dashboardSubtitle', 'Schneller Überblick über deine Events')} ({ key: opt.key, label: `${opt.label} (${opt.count})` }))} value={statusFilter} onChange={(key) => setStatusFilter(key as typeof statusFilter)} /> navigate(adminPath('/events/new'))} /> {loading ? ( ) : filteredRows.length === 0 ? ( navigate(adminPath('/events/new'))} /> ) : ( {filteredRows.map((event) => ( ))} )} { if (key === 'analytics') { navigate(adminPath('/dashboard')); } else if (key === 'settings') { navigate(adminPath('/settings')); } else { navigate(adminPath('/events')); } }} /> ); } 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 statusLabel = translateCommon( event.status === 'published' ? 'events.status.published' : event.status === 'archived' ? 'events.status.archived' : 'events.status.draft', event.status === 'published' ? 'Live' : event.status === 'archived' ? 'Archiviert' : 'Entwurf', ); 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: 'branding', label: translate('events.list.actions.branding', 'Branding'), to: ADMIN_EVENT_BRANDING_PATH(slug) }, { key: 'photobooth', label: translate('events.list.actions.photobooth', 'Photobooth'), to: ADMIN_EVENT_PHOTOBOOTH_PATH(slug) }, { key: 'toolkit', label: translate('events.list.actions.toolkit', 'Toolkit'), to: ADMIN_EVENT_VIEW_PATH(slug) }, ]; return ( {translate('events.list.item.label', 'Event')} {renderName(event.name)} {statusLabel} {metaItems.map((item) => ( ))} {limitWarnings.length > 0 ? ( {limitWarnings.map((warning) => ( {warning.message} ))} ) : null} {translateCommon('actions.open', 'Öffnen')} {translate('events.list.actions.photos', 'Fotos moderieren')} {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'; } function resolveLocation( event: TenantEvent, translate: (key: string, fallback?: string, options?: Record) => string, ): string { const settings = (event.settings ?? {}) as Record; const candidate = (settings.location as string | undefined) ?? (settings.address as string | undefined) ?? (settings.city as string | undefined); if (typeof candidate === 'string' && candidate.trim().length > 0) { return candidate; } return translate('events.list.meta.locationFallback', 'Ort folgt'); }