// @ts-nocheck import React from 'react'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { AlertTriangle, ArrowLeft, Bell, Camera, CheckCircle2, Circle, Clock3, Loader2, MessageSquare, Printer, QrCode, PlugZap, RefreshCw, Smile, Sparkles, ShoppingCart, Users, } from 'lucide-react'; import toast from 'react-hot-toast'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { AdminLayout } from '../components/AdminLayout'; import { EventToolkit, EventToolkitTask, TenantEmotion, TenantEvent, TenantPhoto, EventStats, getEvent, getEventStats, getEventToolkit, toggleEvent, submitTenantFeedback, updatePhotoVisibility, createEventAddonCheckout, featurePhoto, unfeaturePhoto, getEmotions, } from '../api'; import { buildLimitWarnings } from '../lib/limitWarnings'; import { getApiErrorMessage } from '../lib/apiError'; import { isAuthError } from '../auth/tokens'; import { ADMIN_EVENTS_PATH, ADMIN_EVENT_EDIT_PATH, ADMIN_EVENT_INVITES_PATH, ADMIN_EVENT_MEMBERS_PATH, ADMIN_EVENT_PHOTOBOOTH_PATH, ADMIN_EVENT_PHOTOS_PATH, ADMIN_EVENT_TASKS_PATH, buildEngagementTabPath, } from '../constants'; import { SectionCard, SectionHeader, ActionGrid, TenantHeroCard, } from '../components/tenant'; import { AddonsPicker } from '../components/Addons/AddonsPicker'; import { AddonSummaryList } from '../components/Addons/AddonSummaryList'; import { EventAddonCatalogItem, getAddonCatalog } from '../api'; import { GuestBroadcastCard } from '../components/GuestBroadcastCard'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { filterEmotionsByEventType } from '../lib/emotions'; import { buildEventTabs } from '../lib/eventTabs'; type EventDetailPageProps = { mode?: 'detail' | 'toolkit'; }; type ToolkitState = { data: EventToolkit | null; loading: boolean; error: string | null; }; type WorkspaceState = { event: TenantEvent | null; stats: EventStats | null; loading: boolean; busy: boolean; error: string | null; }; export default function EventDetailPage({ mode = 'detail' }: EventDetailPageProps) { const { slug: slugParam } = useParams<{ slug?: string }>(); const [searchParams] = useSearchParams(); const navigate = useNavigate(); const { t } = useTranslation('management'); const { t: tCommon } = useTranslation('common'); const slug = slugParam ?? null; const [state, setState] = React.useState({ event: null, stats: null, loading: true, busy: false, error: null, }); const [toolkit, setToolkit] = React.useState({ data: null, loading: true, error: null }); const [addonBusyId, setAddonBusyId] = React.useState(null); const [addonRefreshCount, setAddonRefreshCount] = React.useState(0); const [addonsCatalog, setAddonsCatalog] = React.useState([]); const [emotions, setEmotions] = React.useState([]); const load = React.useCallback(async () => { if (!slug) { setState({ event: null, stats: null, loading: false, busy: false, error: t('events.errors.missingSlug', 'Kein Event ausgewählt.') }); setToolkit({ data: null, loading: false, error: null }); return; } setState((prev) => ({ ...prev, loading: true, error: null })); setToolkit((prev) => ({ ...prev, loading: true, error: null })); try { const [eventData, statsData, addonOptions] = await Promise.all([getEvent(slug), getEventStats(slug), getAddonCatalog()]); setState((prev) => ({ ...prev, event: eventData, stats: statsData, loading: false })); setAddonsCatalog(addonOptions); } catch (error) { if (!isAuthError(error)) { setState((prev) => ({ ...prev, error: getApiErrorMessage(error, t('events.errors.loadFailed', 'Event konnte nicht geladen werden.')), loading: false, })); } } try { const toolkitData = await getEventToolkit(slug); setToolkit({ data: toolkitData, loading: false, error: null }); } catch (error) { if (!isAuthError(error)) { setToolkit({ data: null, loading: false, error: getApiErrorMessage(error, t('toolkit.errors.loadFailed', 'Toolkit-Daten konnten nicht geladen werden.')), }); } } }, [slug, t]); React.useEffect(() => { void load(); }, [load]); React.useEffect(() => { let cancelled = false; (async () => { try { const list = await getEmotions(); if (!cancelled) { setEmotions(list); } } catch (error) { if (!isAuthError(error)) { console.warn('Failed to load emotions for event detail', error); } } })(); return () => { cancelled = true; }; }, []); async function handleToggle(): Promise { if (!slug) { return; } setState((prev) => ({ ...prev, busy: true, error: null })); try { const updated = await toggleEvent(slug); setState((prev) => ({ ...prev, busy: false, event: updated, stats: prev.stats ? { ...prev.stats, status: updated.status, is_active: Boolean(updated.is_active), } : prev.stats, })); } catch (error) { if (!isAuthError(error)) { setState((prev) => ({ ...prev, busy: false, error: getApiErrorMessage(error, t('events.errors.toggleFailed', 'Status konnte nicht angepasst werden.')), })); } else { setState((prev) => ({ ...prev, busy: false })); } } } const { event, stats, loading, busy, error } = state; const toolkitData = toolkit.data; const eventName = event ? resolveName(event.name) : t('events.placeholders.untitled', 'Unbenanntes Event'); const subtitle = mode === 'toolkit' ? t('events.workspace.toolkitSubtitle', 'Moderation, Aufgaben und Einladungen für deinen Eventtag bündeln.') : t('events.workspace.detailSubtitle', 'Behalte Status, Aufgaben und Einladungen deines Events im Blick.'); const tabLabels = React.useMemo( () => ({ overview: t('events.workspace.tabs.overview', 'Überblick'), live: t('events.workspace.tabs.live', 'Live'), setup: t('events.workspace.tabs.setup', 'Vorbereitung'), recap: t('events.workspace.tabs.recap', 'Nachbereitung'), }), [t], ); const limitWarnings = React.useMemo( () => (event?.limits ? buildLimitWarnings(event.limits, (key, options) => tCommon(`limits.${key}`, options)) : []), [event?.limits, tCommon], ); const eventTabs = React.useMemo(() => { if (!event) { return []; } const translateMenu = (key: string, fallback: string) => t(key, { defaultValue: fallback }); return buildEventTabs(event, translateMenu, { photos: toolkitData?.photos?.pending?.length ?? event.photo_count ?? 0, tasks: toolkitData?.tasks?.summary.total ?? event.tasks_count ?? 0, invites: toolkitData?.invites?.summary.active ?? event.active_invites_count ?? event.total_invites_count ?? 0, }); }, [event, toolkitData?.photos?.pending?.length, toolkitData?.tasks?.summary.total, toolkitData?.invites?.summary.active, t]); const shownWarningToasts = React.useRef>(new Set()); //const [addonBusyId, setAddonBusyId] = React.useState(null); const handleAddonPurchase = React.useCallback( async (scope: 'photos' | 'guests' | 'gallery', addonKeyOverride?: string) => { if (!slug) return; const defaultAddons: Record = { photos: 'extra_photos_500', guests: 'extra_guests_100', gallery: 'extend_gallery_30d', }; const addonKey = addonKeyOverride ?? defaultAddons[scope]; setAddonBusyId(scope); try { const currentUrl = window.location.origin + window.location.pathname; const successUrl = `${currentUrl}?addon_success=1`; const checkout = await createEventAddonCheckout(slug, { addon_key: addonKey, quantity: 1, success_url: successUrl, cancel_url: currentUrl, }); if (checkout.checkout_url) { window.location.href = checkout.checkout_url; } else { toast(t('events.errors.checkoutMissing', 'Checkout konnte nicht gestartet werden.')); } } catch (err) { toast(getApiErrorMessage(err, t('events.errors.checkoutFailed', 'Add-on Checkout fehlgeschlagen.'))); } finally { setAddonBusyId(null); } }, [slug, t], ); React.useEffect(() => { limitWarnings.forEach((warning) => { const id = `${warning.id}-${warning.message}`; if (shownWarningToasts.current.has(id)) { return; } shownWarningToasts.current.add(id); toast(warning.message, { icon: warning.tone === 'danger' ? '🚨' : '⚠️', id, }); }); }, [limitWarnings]); React.useEffect(() => { const success = searchParams.get('addon_success'); if (success && slug) { toast(t('events.success.addonApplied', 'Add-on angewendet. Limits aktualisieren sich in Kürze.')); void load(); setAddonRefreshCount(3); const params = new URLSearchParams(window.location.search); params.delete('addon_success'); const search = params.toString(); navigate(search ? `${window.location.pathname}?${search}` : window.location.pathname, { replace: true }); } }, [searchParams, slug, load, navigate, t]); React.useEffect(() => { if (addonRefreshCount <= 0) { return; } const timer = setTimeout(() => { void load(); setAddonRefreshCount((count) => count - 1); }, 8000); return () => clearTimeout(timer); }, [addonRefreshCount, load]); if (!slug) { return (

{t('events.errors.notFoundBody', 'Ohne gültige Kennung können wir keine Daten laden. Kehre zur Eventliste zurück und wähle dort ein Event aus.')}

); } return ( {error && ( {t('events.alerts.failedTitle', 'Aktion fehlgeschlagen')} {error} )} {limitWarnings.length > 0 && (
{limitWarnings.map((warning) => (
{warning.message} {(['photos', 'guests', 'gallery'] as const).includes(warning.scope) ? (
{addonsCatalog.length > 0 ? ( { void handleAddonPurchase(warning.scope as 'photos' | 'guests' | 'gallery', key); }} busy={addonBusyId === warning.scope} t={(key, fallback) => t(key, fallback)} /> ) : null}
) : null}
))}
)} {toolkit.error && ( {toolkit.error} )} {loading ? ( ) : event ? (
{ void load(); }} loading={state.busy} navigate={navigate} /> {tabLabels.overview} {tabLabels.live} {tabLabels.setup} {tabLabels.recap}
{(toolkitData?.alerts?.length ?? 0) > 0 && }
navigate(ADMIN_EVENT_PHOTOS_PATH(event.slug))} />
navigate(ADMIN_EVENT_TASKS_PATH(event.slug))} /> navigate(`${ADMIN_EVENT_INVITES_PATH(event.slug)}?tab=layout`)} />
navigate(`${ADMIN_EVENT_INVITES_PATH(event.slug)}?tab=layout`)} onOpenCollections={() => navigate(buildEngagementTabPath('collections'))} onOpenTasks={() => navigate(ADMIN_EVENT_TASKS_PATH(event.slug))} onOpenEmotions={() => navigate(buildEngagementTabPath('emotions'))} /> {event.addons?.length ? ( t(key, fallback)} /> ) : null}
navigate(`${ADMIN_EVENT_INVITES_PATH(event.slug)}?tab=layout`)} />
) : (

{t('events.errors.notFoundBody', 'Ohne gültige Kennung können wir keine Daten laden. Kehre zur Eventliste zurück und wähle dort ein Event aus.')}

)}
); } function resolveName(name: TenantEvent['name']): string { if (typeof name === 'string' && name.trim().length > 0) { return name.trim(); } if (name && typeof name === 'object') { return name.de ?? name.en ?? Object.values(name)[0] ?? 'Event'; } return 'Event'; } function EventHeroCardSection({ event, stats, onRefresh, loading, navigate }: { event: TenantEvent; stats: EventStats | null; onRefresh: () => void; loading: boolean; navigate: ReturnType; }) { const { t } = useTranslation('management'); const statusLabel = getStatusLabel(event, t); const supporting = [ t('events.workspace.hero.status', { defaultValue: 'Status: {{status}}', status: statusLabel }), t('events.workspace.hero.date', { defaultValue: 'Eventdatum: {{date}}', date: formatDate(event.event_date) }), t('events.workspace.hero.metrics', { defaultValue: 'Uploads gesamt: {{count}} · Likes: {{likes}}', count: stats?.uploads_total ?? stats?.total ?? 0, likes: stats?.likes_total ?? stats?.likes ?? 0, }), ]; const aside = (
} label={t('events.workspace.fields.status', 'Status')} value={statusLabel} /> } label={t('events.workspace.fields.date', 'Eventdatum')} value={formatDate(event.event_date)} /> } label={t('events.workspace.fields.active', 'Aktiv für Gäste')} value={event.is_active ? t('events.workspace.activeYes', 'Ja') : t('events.workspace.activeNo', 'Nein')} />
); return ( navigate(ADMIN_EVENTS_PATH)} className="rounded-full border-pink-200 text-pink-600 hover:bg-pink-50"> {t('events.actions.backToList', 'Zurück zur Liste')} )} secondaryAction={( )} aside={aside} >
); } function StatusCard({ event, stats, busy, onToggle }: { event: TenantEvent; stats: EventStats | null; busy: boolean; onToggle: () => void }) { const { t } = useTranslation('management'); const statusLabel = getStatusLabel(event, t); return (
} label={t('events.workspace.fields.status', 'Status')} value={statusLabel} /> } label={t('events.workspace.fields.active', 'Aktiv für Gäste')} value={event.is_active ? t('events.workspace.activeYes', 'Ja') : t('events.workspace.activeNo', 'Nein')} /> } label={t('events.workspace.fields.date', 'Eventdatum')} value={formatDate(event.event_date)} /> } label={t('events.workspace.fields.eventType', 'Event-Typ')} value={resolveEventType(event)} /> {stats && (

{t('events.workspace.fields.insights', 'Letzte Aktivität')}

{t('events.workspace.fields.uploadsTotal', { defaultValue: '{{count}} Uploads gesamt', count: stats.uploads_total ?? stats.total ?? 0, })} {' · '} {t('events.workspace.fields.uploadsToday', { defaultValue: '{{count}} Uploads (24h)', count: stats.uploads_24h ?? stats.recent_uploads ?? 0, })}

{t('events.workspace.fields.likesTotal', { defaultValue: '{{count}} Likes vergeben', count: stats.likes_total ?? stats.likes ?? 0, })}

)}
); } function QuickActionsCard({ slug, busy, onToggle, navigate }: { slug: string; busy: boolean; onToggle: () => void | Promise; navigate: ReturnType }) { const { t } = useTranslation('management'); const gridItems = [ { key: 'photos', icon: , label: t('events.quickActions.moderate', 'Fotos moderieren'), description: t('events.quickActions.moderateDesc', 'Prüfe Uploads und veröffentliche Highlights.'), onClick: () => navigate(ADMIN_EVENT_PHOTOS_PATH(slug)), }, { key: 'tasks', icon: , label: t('events.quickActions.tasks', 'Aufgaben bearbeiten'), description: t('events.quickActions.tasksDesc', 'Passe Story-Prompts und Moderation an.'), onClick: () => navigate(ADMIN_EVENT_TASKS_PATH(slug)), }, { key: 'invites', icon: , label: t('events.quickActions.invites', 'Layouts & QR verwalten'), description: t('events.quickActions.invitesDesc', 'Aktualisiere QR-Kits und Einladungslayouts.'), onClick: () => navigate(`${ADMIN_EVENT_INVITES_PATH(slug)}?tab=layout`), }, { key: 'roles', icon: , label: t('events.quickActions.roles', 'Team & Rollen anpassen'), description: t('events.quickActions.rolesDesc', 'Verwalte Moderatoren und Co-Leads.'), onClick: () => navigate(ADMIN_EVENT_MEMBERS_PATH(slug)), }, { key: 'photobooth', icon: , label: t('events.quickActions.photobooth', 'Photobooth anbinden'), description: t('events.quickActions.photoboothDesc', 'FTP-Link aktivieren und Zugangsdaten kopieren.'), onClick: () => navigate(ADMIN_EVENT_PHOTOBOOTH_PATH(slug)), }, { key: 'print', icon: , label: t('events.quickActions.print', 'Layouts als PDF drucken'), description: t('events.quickActions.printDesc', 'Exportiere QR-Sets für den Druck.'), onClick: () => navigate(`${ADMIN_EVENT_INVITES_PATH(slug)}?tab=export`), }, ]; return (
); } function MetricsGrid({ metrics, stats }: { metrics: EventToolkit['metrics'] | null | undefined; stats: EventStats | null }) { const { t } = useTranslation('management'); const cards = [ { icon: , label: t('events.metrics.uploadsTotal', 'Uploads gesamt'), value: metrics?.uploads_total ?? stats?.uploads_total ?? 0, }, { icon: , label: t('events.metrics.uploads24h', 'Uploads (24h)'), value: metrics?.uploads_24h ?? stats?.uploads_24h ?? 0, }, { icon: , label: t('events.metrics.pending', 'Fotos in Moderation'), value: metrics?.pending_photos ?? stats?.pending_photos ?? 0, }, { icon: , label: t('events.metrics.activeInvites', 'Aktive Einladungen'), value: metrics?.active_invites ?? 0, }, ]; return (
{cards.map((card) => (
{card.icon}

{card.label}

{card.value}

))}
); } function InviteSummary({ invites, navigateToInvites }: { invites: EventToolkit['invites'] | undefined; navigateToInvites: () => void }) { const { t } = useTranslation('management'); return (
{t('events.invites.activeCount', { defaultValue: '{{count}} aktiv', count: invites?.summary.active ?? 0 })} {t('events.invites.totalCount', { defaultValue: '{{count}} gesamt', count: invites?.summary.total ?? 0 })}
{invites?.items?.length ? (
    {invites.items.slice(0, 3).map((invite) => (
  • {invite.label ?? invite.url}

    {invite.url}

  • ))}
) : (

{t('events.invites.empty', 'Noch keine Einladungen erstellt.')}

)}
); } function TaskOverviewCard({ tasks, navigateToTasks }: { tasks: EventToolkit['tasks'] | undefined; navigateToTasks: () => void }) { const { t } = useTranslation('management'); return ( {t('events.tasks.summary', { defaultValue: '{{completed}} von {{total}} erledigt', completed: tasks?.summary.completed ?? 0, total: tasks?.summary.total ?? 0, })} )} />
{tasks?.items?.length ? (
{tasks.items.slice(0, 4).map((task) => ( ))}
) : (

{t('events.tasks.empty', 'Noch keine Aufgaben zugewiesen.')}

)}
); } function TaskRow({ task }: { task: EventToolkitTask }) { return (

{task.title}

{task.description ?

{task.description}

: null}
{task.is_completed ? 'Erledigt' : 'Offen'}
); } function BrandingMissionCard({ event, invites, emotions, onOpenBranding, onOpenCollections, onOpenTasks, onOpenEmotions, }: { event: TenantEvent; invites?: EventToolkit['invites']; emotions?: TenantEmotion[]; onOpenBranding: () => void; onOpenCollections: () => void; onOpenTasks: () => void; onOpenEmotions: () => void; }) { const { t } = useTranslation('management'); const palette = extractBrandingPalette(event.settings); const activeInvites = invites?.summary.active ?? 0; const eventTypeId = event.event_type_id ?? event.event_type?.id ?? null; const spotlightEmotions = React.useMemo( () => filterEmotionsByEventType(emotions ?? [], eventTypeId).slice(0, 4), [emotions, eventTypeId], ); return (

{t('events.branding.brandingTitle', 'Branding')}

{palette.font ?? t('events.branding.brandingFallback', 'Aktuelle Auswahl')}

{t('events.branding.brandingCopy', 'Passe Farben & Schriftarten im Layout-Editor an.')}

{(palette.colors.length ? palette.colors : ['#f472b6', '#fef3c7', '#312e81']).map((color) => ( ))}

{t('events.branding.collectionsTitle', 'Mission Packs')}

{event.event_type?.name ?? t('events.branding.collectionsFallback', 'Empfohlene Story')}

{t('events.branding.collectionsCopy', 'Importiere passende Kollektionen oder aktiviere Emotionen im Aufgabenbereich.')}

{t('events.branding.collectionsActive', { defaultValue: '{{count}} aktive Links', count: activeInvites })} {t('events.branding.tasksCount', { defaultValue: '{{count}} Aufgaben', count: Number(event.tasks_count ?? 0), })}

{t('events.branding.emotionsTitle', 'Emotionen')}

{spotlightEmotions.length ? (
{spotlightEmotions.map((emotion) => ( {emotion.icon ? {emotion.icon} : null} {emotion.name} ))}
) : (

{t('events.branding.emotionsEmpty', 'Aktiviere Emotionen, um Aufgaben zu kategorisieren.')}

)}
); } function GalleryShareCard({ invites, onManageInvites, }: { invites?: EventToolkit['invites']; onManageInvites: () => void; }) { const { t } = useTranslation('management'); const primaryInvite = React.useMemo( () => invites?.items?.find((invite) => invite.is_active) ?? invites?.items?.[0] ?? null, [invites?.items], ); const handleCopy = React.useCallback(async () => { if (!primaryInvite?.url) { return; } try { await navigator.clipboard.writeText(primaryInvite.url); toast.success(t('events.galleryShare.copied', 'Link kopiert')); } catch (err) { console.error(err); toast.error(t('events.galleryShare.copyFailed', 'Konnte Link nicht kopieren')); } }, [primaryInvite, t]); if (!primaryInvite) { return ( ); } return (

{primaryInvite.label ?? t('events.galleryShare.linkLabel', 'Standard-Link')}

{primaryInvite.url}

{t('events.galleryShare.scans', { defaultValue: '{{count}} Aufrufe', count: primaryInvite.usage_count })} {typeof primaryInvite.usage_limit === 'number' && ( {t('events.galleryShare.limit', { defaultValue: 'Limit {{count}}', count: primaryInvite.usage_limit })} )}
); } function extractBrandingPalette( settings: TenantEvent['settings'], ): { colors: string[]; font?: string } { const colors: string[] = []; let font: string | undefined; if (settings && typeof settings === 'object') { const brandingSource = (settings as Record).branding && typeof (settings as Record).branding === 'object' ? (settings as Record).branding : settings; const candidateKeys = ['primary_color', 'secondary_color', 'accent_color', 'background_color', 'color']; candidateKeys.forEach((key) => { const value = (brandingSource as Record)[key]; if (typeof value === 'string' && value.trim()) { colors.push(value); } }); const fontKeys = ['font_family', 'font', 'heading_font']; fontKeys.some((key) => { const value = (brandingSource as Record)[key]; if (typeof value === 'string' && value.trim()) { font = value; return true; } return false; }); } return { colors, font }; } function PendingPhotosCard({ slug, photos, navigateToModeration, }: { slug: string; photos: TenantPhoto[]; navigateToModeration: () => void; }) { const { t } = useTranslation('management'); const [entries, setEntries] = React.useState(photos); const [updatingId, setUpdatingId] = React.useState(null); React.useEffect(() => { setEntries(photos); }, [photos]); const handleVisibility = async (photo: TenantPhoto, visible: boolean) => { setUpdatingId(photo.id); try { const updated = await updatePhotoVisibility(slug, photo.id, visible); setEntries((prev) => prev.map((item) => (item.id === photo.id ? updated : item))); toast.success( visible ? t('events.photos.toastVisible', 'Foto wieder sichtbar gemacht.') : t('events.photos.toastHidden', 'Foto ausgeblendet.'), ); } catch (err) { toast.error( isAuthError(err) ? t('events.photos.errorAuth', 'Session abgelaufen. Bitte erneut anmelden.') : t('events.photos.errorVisibility', 'Sichtbarkeit konnte nicht geändert werden.'), ); } finally { setUpdatingId(null); } }; const handleFeature = async (photo: TenantPhoto, feature: boolean) => { setUpdatingId(photo.id); try { const updated = feature ? await featurePhoto(slug, photo.id) : await unfeaturePhoto(slug, photo.id); setEntries((prev) => prev.map((item) => (item.id === photo.id ? updated : item))); toast.success( feature ? t('events.photos.toastFeatured', 'Foto als Highlight markiert.') : t('events.photos.toastUnfeatured', 'Highlight entfernt.'), ); } catch (err) { toast.error(getApiErrorMessage(err, t('events.photos.errorFeature', 'Aktion fehlgeschlagen.'))); } finally { setUpdatingId(null); } }; return ( {t('events.photos.pendingCount', { defaultValue: '{{count}} Fotos offen', count: entries.length })} )} />
{entries.length ? (
{entries.slice(0, 4).map((photo) => { const hidden = photo.status === 'hidden'; return (
{photo.caption {photo.is_featured ? ( Highlight ) : null}
{photo.uploader_name ?? 'Gast'} ♥ {photo.likes_count}
); })}
) : (

{t('events.photos.pendingEmpty', 'Aktuell warten keine Fotos auf Freigabe.')}

)}
); } function RecentUploadsCard({ slug, photos }: { slug: string; photos: TenantPhoto[] }) { const { t } = useTranslation('management'); const [entries, setEntries] = React.useState(photos); const [updatingId, setUpdatingId] = React.useState(null); React.useEffect(() => { setEntries(photos); }, [photos]); const handleVisibility = async (photo: TenantPhoto, visible: boolean) => { setUpdatingId(photo.id); try { const updated = await updatePhotoVisibility(slug, photo.id, visible); setEntries((prev) => prev.map((item) => (item.id === photo.id ? updated : item))); } catch (err) { toast.error( isAuthError(err) ? t('events.photos.errorAuth', 'Session abgelaufen. Bitte erneut anmelden.') : t('events.photos.errorVisibility', 'Sichtbarkeit konnte nicht geändert werden.'), ); } finally { setUpdatingId(null); } }; return (
{entries.length ? (
{entries.slice(0, 6).map((photo) => { const hidden = photo.status === 'hidden'; return (
{photo.caption {photo.is_featured ? ( Highlight ) : null}
♥ {photo.likes_count} {photo.uploader_name ?? 'Gast'}
); })}
) : (

{t('events.photos.recentEmpty', 'Noch keine neuen Uploads.')}

)}
); } function FeedbackCard({ slug }: { slug: string }) { const { t } = useTranslation('management'); const [sentiment, setSentiment] = React.useState<'positive' | 'neutral' | 'negative' | null>(null); const [message, setMessage] = React.useState(''); const [busy, setBusy] = React.useState(false); const [submitted, setSubmitted] = React.useState(false); const [error, setError] = React.useState(undefined); const copy = { positive: t('events.feedback.positive', 'Super Lauf!'), neutral: t('events.feedback.neutral', 'Läuft'), negative: t('events.feedback.negative', 'Braucht Support'), }; return (
{error && ( {t('events.feedback.errorTitle', 'Feedback konnte nicht gesendet werden.')} {error} )}
{(Object.keys(copy) as Array<'positive' | 'neutral' | 'negative'>).map((key) => ( ))}