import React from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Check, Copy, Download, Share2, Sparkles, Trophy, Users } from 'lucide-react'; import { YStack, XStack } from '@tamagui/stacks'; import { SizableText as Text } from '@tamagui/text'; import { Pressable } from '@tamagui/react-native-web-lite'; import { MobileShell } from './components/MobileShell'; import { MobileCard, CTAButton, PillBadge, SkeletonCard } from './components/Primitives'; import { LegalConsentSheet } from './components/LegalConsentSheet'; import { getEvent, getEventStats, getEventQrInvites, updateEvent, TenantEvent, EventStats, EventQrInvite, EventAddonCatalogItem, getAddonCatalog, createEventAddonCheckout, } from '../api'; import { isAuthError } from '../auth/tokens'; import { getApiErrorMessage } from '../lib/apiError'; import { adminPath } from '../constants'; import toast from 'react-hot-toast'; import { useBackNavigation } from './hooks/useBackNavigation'; import { useAdminTheme } from './theme'; type GalleryCounts = { photos: number; likes: number; pending: number; }; export default function MobileEventRecapPage() { const { slug } = useParams<{ slug: string }>(); const navigate = useNavigate(); const { t } = useTranslation('management'); const { textStrong, text, muted, border, primary, successText, danger } = useAdminTheme(); const [event, setEvent] = React.useState(null); const [stats, setEventStats] = React.useState(null); const [invites, setInvites] = React.useState([]); const [addons, setAddons] = React.useState([]); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); const [consentOpen, setConsentOpen] = React.useState(false); const [busyScope, setBusyScope] = React.useState(null); const back = useBackNavigation(slug ? adminPath(`/mobile/events/${slug}`) : adminPath('/mobile/events')); const load = React.useCallback(async () => { if (!slug) return; setLoading(true); try { const [eventData, statsData, invitesData, addonsData] = await Promise.all([ getEvent(slug), getEventStats(slug), getEventQrInvites(slug), getAddonCatalog(), ]); setEvent(eventData); setEventStats(statsData); setInvites(invitesData); setAddons(addonsData); setError(null); } catch (err) { if (!isAuthError(err)) { setError(getApiErrorMessage(err, t('events.errors.loadFailed', 'Recap konnte nicht geladen werden.'))); } } finally { setLoading(false); } }, [slug, t]); React.useEffect(() => { void load(); }, [load]); const handleCheckout = async (addonKey: string) => { if (!slug || busyScope) return; setBusyScope(addonKey); try { const { checkout_url } = await createEventAddonCheckout(slug, { addon_key: addonKey, success_url: window.location.href, cancel_url: window.location.href, }); if (checkout_url) { window.location.href = checkout_url; } } catch (err) { toast.error(getApiErrorMessage(err, t('events.errors.checkoutFailed', 'Bezahlvorgang konnte nicht gestartet werden.'))); setBusyScope(null); } }; const handleConsentConfirm = async (consents: { acceptedTerms: boolean }) => { if (!slug || !busyScope) return; try { const { checkout_url } = await createEventAddonCheckout(slug, { addon_key: busyScope, success_url: window.location.href, cancel_url: window.location.href, accepted_terms: consents.acceptedTerms, } as any); if (checkout_url) { window.location.href = checkout_url; } } catch (err) { toast.error(getApiErrorMessage(err, t('events.errors.checkoutFailed', 'Bezahlvorgang konnte nicht gestartet werden.'))); setBusyScope(null); setConsentOpen(false); } }; if (loading) { return ( ); } if (error || !event) { return ( {error || t('common.error', 'Ein Fehler ist aufgetreten.')} ); } const galleryCounts: GalleryCounts = { photos: stats?.total ?? 0, likes: stats?.likes ?? 0, pending: stats?.pending_photos ?? 0, }; const activeInvite = invites.find((i) => i.is_active) ?? invites[0] ?? null; const guestLink = activeInvite?.url ?? ''; return ( {/* Status & Summary */} {t('events.recap.done', 'Event beendet')} {formatDate(event.event_date)} {t('events.recap.statusClosed', 'Archiviert')} {/* Share Section */} {t('events.recap.shareGallery', 'Galerie teilen')} {t('events.recap.shareBody', 'Deine Gäste können die Galerie auch nach dem Event weiterhin ansehen.')} {guestLink} guestLink && copyToClipboard(guestLink, t)} /> {typeof navigator !== 'undefined' && !!navigator.share && ( shareLink(guestLink, event, t)} /> )} {activeInvite?.qr_code_data_url ? ( QR downloadQr(activeInvite.qr_code_data_url!)} /> ) : null} {/* Settings */} {t('events.recap.settings', 'Nachlauf-Optionen')} updateSetting(event, setEvent, slug, 'guest_downloads_enabled', value, setError, t as any)} /> updateSetting(event, setEvent, slug, 'guest_sharing_enabled', value, setError, t as any)} /> {/* Extensions */} {t('events.recap.addons', 'Galerie verlängern')} {t('events.recap.addonBody', 'Die Online-Zeit deiner Galerie neigt sich dem Ende? Hier kannst du sie verlängern.')} {addons .filter((a) => a.key === 'gallery_extension') .map((addon) => ( handleCheckout(addon.key)} loading={busyScope === addon.key} /> ))} { setConsentOpen(false); setBusyScope(null); }} onConfirm={handleConsentConfirm} busy={Boolean(busyScope)} t={t as any} /> ); } function Stat({ label, value, pill }: { label: string; value: string; pill?: boolean }) { const { textStrong, muted, accentSoft, border } = useAdminTheme(); return ( {label} {value} ); } function ToggleOption({ label, value, onToggle }: { label: string; value: boolean; onToggle: (val: boolean) => void }) { const { textStrong } = useAdminTheme(); return ( {label} onToggle(e.target.checked)} style={{ width: 20, height: 20 }} /> ); } async function updateSetting( event: TenantEvent, setEvent: (event: TenantEvent) => void, slug: string | undefined, key: 'guest_downloads_enabled' | 'guest_sharing_enabled', value: boolean, setError: (message: string | null) => void, t: (key: string, fallback?: string) => string, ): Promise { if (!slug) return; try { const updated = await updateEvent(slug, { settings: { ...(event.settings ?? {}), [key]: value, }, }); setEvent(updated); } catch (err) { if (!isAuthError(err)) { setError(getApiErrorMessage(err, t('events.errors.toggleFailed', 'Einstellung konnte nicht gespeichert werden.'))); } } } function copyToClipboard(value: string, t: any) { if (typeof window !== 'undefined') { void window.navigator.clipboard.writeText(value); toast.success(t('events.recap.copySuccess', 'Link kopiert')); } } async function shareLink(value: string, event: TenantEvent | null, t: any) { if (typeof window !== 'undefined' && navigator.share) { try { await navigator.share({ title: resolveName(event?.name ?? ''), text: t('events.recap.shareText', 'Schau dir die Fotos von unserem Event an!'), url: value, }); } catch { // silently ignore or fallback to copy } } } function downloadQr(dataUrl: string) { const link = document.createElement('a'); link.href = dataUrl; link.download = 'guest-gallery-qr.png'; link.click(); } function resolveName(name: TenantEvent['name']): string { if (typeof name === 'string') return name; if (name && typeof name === 'object') { return name.de ?? name.en ?? Object.values(name)[0] ?? 'Event'; } return 'Event'; } function formatDate(iso?: string | null): string { if (!iso) return ''; const date = new Date(iso); if (Number.isNaN(date.getTime())) return ''; return date.toLocaleDateString(undefined, { day: '2-digit', month: 'short', year: 'numeric' }); }