import React from 'react'; import { Button } from '@/components/ui/button'; import { createBrowserRouter, useParams, Link, Navigate } from 'react-router-dom'; import Header from './components/Header'; import BottomNav from './components/BottomNav'; import RouteTransition from './components/RouteTransition'; import { useEventData } from './hooks/useEventData'; import { AlertTriangle, Loader2 } from 'lucide-react'; import { EventStatsProvider } from './context/EventStatsContext'; import { GuestIdentityProvider, useOptionalGuestIdentity } from './context/GuestIdentityContext'; import { EventBrandingProvider } from './context/EventBrandingContext'; import { LocaleProvider } from './i18n/LocaleContext'; import { DEFAULT_LOCALE, isLocaleCode } from './i18n/messages'; import { useTranslation, type TranslateFn } from './i18n/useTranslation'; import type { EventBranding } from './types/event-branding'; import type { EventBrandingPayload, FetchEventErrorCode } from './services/eventApi'; import { NotificationCenterProvider } from './context/NotificationCenterContext'; import RouteErrorElement from '@/components/RouteErrorElement'; import { isTaskModeEnabled } from './lib/engagement'; const LandingPage = React.lazy(() => import('./pages/LandingPage')); const ProfileSetupPage = React.lazy(() => import('./pages/ProfileSetupPage')); const HomePage = React.lazy(() => import('./pages/HomePage')); const TaskPickerPage = React.lazy(() => import('./pages/TaskPickerPage')); const TaskDetailPage = React.lazy(() => import('./pages/TaskDetailPage')); const UploadPage = React.lazy(() => import('./pages/UploadPage')); const UploadQueuePage = React.lazy(() => import('./pages/UploadQueuePage')); const GalleryPage = React.lazy(() => import('./pages/GalleryPage')); const PhotoLightbox = React.lazy(() => import('./pages/PhotoLightbox')); const AchievementsPage = React.lazy(() => import('./pages/AchievementsPage')); const SlideshowPage = React.lazy(() => import('./pages/SlideshowPage')); const SettingsPage = React.lazy(() => import('./pages/SettingsPage')); const LegalPage = React.lazy(() => import('./pages/LegalPage')); const HelpCenterPage = React.lazy(() => import('./pages/HelpCenterPage')); const HelpArticlePage = React.lazy(() => import('./pages/HelpArticlePage')); const PublicGalleryPage = React.lazy(() => import('./pages/PublicGalleryPage')); const SharedPhotoPage = React.lazy(() => import('./pages/SharedPhotoPage')); const NotFoundPage = React.lazy(() => import('./pages/NotFoundPage')); function HomeLayout() { const { token } = useParams(); if (!token) { return (
); } return ( ); } export const router = createBrowserRouter([ { path: '/event', element: , errorElement: }, { path: '/share/:slug', element: , errorElement: }, { path: '/setup/:token', element: , errorElement: , children: [ { index: true, element: }, ], }, { path: '/g/:token', element: , errorElement: }, { path: '/e/:token', element: , errorElement: , children: [ { index: true, element: }, { path: 'tasks', element: }, { path: 'tasks/:taskId', element: }, { path: 'upload', element: }, { path: 'queue', element: }, { path: 'gallery', element: }, { path: 'photo/:photoId', element: }, { path: 'achievements', element: }, { path: 'slideshow', element: }, { path: 'help', element: }, { path: 'help/:slug', element: }, ], }, { path: '/settings', element: , errorElement: }, { path: '/legal/:page', element: , errorElement: }, { path: '/help', element: , errorElement: }, { path: '/help/:slug', element: , errorElement: }, { path: '*', element: , errorElement: }, ]); function EventBoundary({ token }: { token: string }) { const identity = useOptionalGuestIdentity(); const { event, status, error, errorCode } = useEventData(); if (status === 'loading') { return ; } if (status === 'error' || !event) { return ; } if (identity?.hydrated && !identity.name) { return ; } const eventLocale = isLocaleCode(event.default_locale) ? event.default_locale : DEFAULT_LOCALE; const localeStorageKey = `guestLocale_event_${event.id ?? token}`; const branding = mapEventBranding(event.branding ?? (event as any)?.settings?.branding ?? null); return (
); } function TaskGuard({ children }: { children: React.ReactNode }) { const { token } = useParams<{ token: string }>(); const { event, status } = useEventData(); if (status === 'loading') { return ; } if (event && !isTaskModeEnabled(event)) { return ; } return <>{children}; } function SetupLayout() { const { token } = useParams<{ token: string }>(); const { event } = useEventData(); if (!token) return null; const eventLocale = event && isLocaleCode(event.default_locale) ? event.default_locale : DEFAULT_LOCALE; const localeStorageKey = event ? `guestLocale_event_${event.id}` : `guestLocale_event_${token}`; const branding = event ? mapEventBranding(event.branding ?? (event as any)?.settings?.branding ?? null) : null; return (
); } function EventLoadingView() { const { t } = useTranslation(); return (

{t('eventAccess.loading.title')}

{t('eventAccess.loading.subtitle')}

); } function mapEventBranding(raw?: EventBrandingPayload | null): EventBranding | null { if (!raw) { return null; } const palette = raw.palette ?? {}; const typography = raw.typography ?? {}; const buttons = raw.buttons ?? {}; const logo = raw.logo ?? {}; const primary = palette.primary ?? raw.primary_color ?? ''; const secondary = palette.secondary ?? raw.secondary_color ?? ''; const background = palette.background ?? raw.background_color ?? ''; const surface = palette.surface ?? raw.surface_color ?? background; const headingFont = typography.heading ?? raw.heading_font ?? raw.font_family ?? null; const bodyFont = typography.body ?? raw.body_font ?? raw.font_family ?? null; const sizePreset = (typography.size as 's' | 'm' | 'l' | undefined) ?? (raw.font_size as 's' | 'm' | 'l' | undefined) ?? 'm'; const logoMode = logo.mode ?? raw.logo_mode ?? (logo.value || raw.logo_url ? 'upload' : 'emoticon'); const logoValue = logo.value ?? raw.logo_value ?? raw.logo_url ?? raw.icon ?? null; const logoPosition = logo.position ?? raw.logo_position ?? 'left'; const logoSize = (logo.size as 's' | 'm' | 'l' | undefined) ?? (raw.logo_size as 's' | 'm' | 'l' | undefined) ?? 'm'; const buttonStyle = (buttons.style as 'filled' | 'outline' | undefined) ?? (raw.button_style as 'filled' | 'outline' | undefined) ?? 'filled'; const buttonRadius = typeof buttons.radius === 'number' ? buttons.radius : (typeof raw.button_radius === 'number' ? raw.button_radius : 12); const buttonPrimary = buttons.primary ?? raw.button_primary_color ?? primary ?? ''; const buttonSecondary = buttons.secondary ?? raw.button_secondary_color ?? secondary ?? ''; const linkColor = buttons.link_color ?? raw.link_color ?? secondary ?? ''; return { primaryColor: primary ?? '', secondaryColor: secondary ?? '', backgroundColor: background ?? '', fontFamily: bodyFont, logoUrl: logoMode === 'upload' ? (logoValue ?? null) : null, palette: { primary: primary ?? '', secondary: secondary ?? '', background: background ?? '', surface: surface ?? background ?? '', }, typography: { heading: headingFont, body: bodyFont, sizePreset, }, logo: { mode: logoMode, value: logoValue, position: logoPosition, size: logoSize, }, buttons: { style: buttonStyle, radius: buttonRadius, primary: buttonPrimary, secondary: buttonSecondary, linkColor, }, mode: (raw.mode as 'light' | 'dark' | 'auto' | undefined) ?? 'auto', useDefaultBranding: raw.use_default_branding ?? undefined, }; } interface EventErrorViewProps { code: FetchEventErrorCode | null; message: string | null; } function EventErrorView({ code, message }: EventErrorViewProps) { const { t } = useTranslation(); const content = getErrorContent(t, code, message); return (

{content.title}

{content.description}

{content.hint && (

{content.hint}

)}
{content.ctaHref && content.ctaLabel && ( )}
); } function getErrorContent( t: TranslateFn, code: FetchEventErrorCode | null, message: string | null, ) { const build = (key: string, options?: { ctaHref?: string }) => { const ctaLabel = t(`eventAccess.error.${key}.ctaLabel`, ''); const hint = t(`eventAccess.error.${key}.hint`, ''); return { title: t(`eventAccess.error.${key}.title`), description: message ?? t(`eventAccess.error.${key}.description`), ctaLabel: ctaLabel.trim().length > 0 ? ctaLabel : undefined, ctaHref: options?.ctaHref, hint: hint.trim().length > 0 ? hint : null, }; }; switch (code) { case 'invalid_token': return build('invalid_token', { ctaHref: '/event' }); case 'token_revoked': return build('token_revoked', { ctaHref: '/event' }); case 'token_expired': return build('token_expired', { ctaHref: '/event' }); case 'token_rate_limited': return build('token_rate_limited'); case 'access_rate_limited': return build('access_rate_limited'); case 'event_not_public': return build('event_not_public'); case 'gallery_expired': return build('gallery_expired', { ctaHref: '/event' }); case 'network_error': return build('network_error'); case 'server_error': return build('server_error'); default: return build('default', { ctaHref: '/event' }); } } function SimpleLayout({ title, children }: { title: string; children: React.ReactNode }) { return (
{children}
); } function HelpStandalone() { const { t } = useTranslation(); return ( ); } function HelpArticleStandalone() { const { t } = useTranslation(); return ( ); }