import React from 'react'; import { Link, useParams } from 'react-router-dom'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Separator } from '@/components/ui/separator'; import EmotionPicker from '../components/EmotionPicker'; import GalleryPreview from '../components/GalleryPreview'; import { useGuestIdentity } from '../context/GuestIdentityContext'; import { useEventData } from '../hooks/useEventData'; import { useGuestTaskProgress } from '../hooks/useGuestTaskProgress'; import { Sparkles, UploadCloud, X, RefreshCw } from 'lucide-react'; import { useTranslation, type TranslateFn } from '../i18n/useTranslation'; import { useEventBranding } from '../context/EventBrandingContext'; import type { EventBranding } from '../types/event-branding'; import { animated, useSpring } from '@react-spring/web'; import { useGesture } from '@use-gesture/react'; export default function HomePage() { const { token } = useParams<{ token: string }>(); const { name, hydrated } = useGuestIdentity(); const { event } = useEventData(); const { completedCount } = useGuestTaskProgress(token ?? ''); const { t, locale } = useTranslation(); const { branding } = useEventBranding(); const headingFont = branding.typography?.heading ?? branding.fontFamily ?? undefined; const bodyFont = branding.typography?.body ?? branding.fontFamily ?? undefined; const radius = branding.buttons?.radius ?? 12; const heroStorageKey = token ? `guestHeroDismissed_${token}` : 'guestHeroDismissed'; const [heroVisible, setHeroVisible] = React.useState(false); React.useEffect(() => { if (typeof window === 'undefined') { return; } try { const stored = window.sessionStorage.getItem(heroStorageKey); // standardmäßig versteckt, nur sichtbar falls explizit gesetzt (kann später wieder aktiviert werden) setHeroVisible(stored === 'show'); } catch { setHeroVisible(false); } }, [heroStorageKey]); const dismissHero = React.useCallback(() => { setHeroVisible(false); if (typeof window === 'undefined') { return; } try { window.sessionStorage.setItem(heroStorageKey, '1'); } catch { // ignore storage exceptions (e.g. private mode) } }, [heroStorageKey]); const displayName = hydrated && name ? name : t('home.fallbackGuestName'); const eventNameDisplay = event?.name ?? t('home.hero.defaultEventName'); const accentColor = branding.primaryColor; const secondaryAccent = branding.secondaryColor; const [missionDeck, setMissionDeck] = React.useState([]); const [missionLoading, setMissionLoading] = React.useState(false); const missionPoolRef = React.useRef([]); const drawRandom = React.useCallback((excludeIds: Set) => { const pool = missionPoolRef.current.filter((item) => !excludeIds.has(item.id)); if (!pool.length) return null; return pool[Math.floor(Math.random() * pool.length)]; }, []); const resetDeck = React.useCallback(() => { const pool = missionPoolRef.current; if (!pool.length) { setMissionDeck([]); return; } const shuffled = [...pool].sort(() => Math.random() - 0.5); setMissionDeck(shuffled.slice(0, 4)); }, []); const advanceDeck = React.useCallback(() => { setMissionDeck((prev) => { if (!prev.length) return prev; const [, ...rest] = prev; const exclude = new Set(rest.map((r) => r.id)); const nextCandidate = drawRandom(exclude); const replenished = nextCandidate ? [...rest, nextCandidate] : rest; return replenished; }); }, [drawRandom]); React.useEffect(() => { if (!token) return; let cancelled = false; async function loadMissions() { setMissionLoading(true); try { const safeToken = token ?? ''; const response = await fetch( `/api/v1/events/${encodeURIComponent(safeToken)}/tasks?locale=${encodeURIComponent(locale)}`, { headers: { Accept: 'application/json', 'X-Locale': locale, }, } ); if (!response.ok) throw new Error('Aufgaben konnten nicht geladen werden.'); const payload = await response.json(); if (cancelled) return; if (Array.isArray(payload) && payload.length) { missionPoolRef.current = payload.map((task: Record) => ({ id: Number(task.id), title: typeof task.title === 'string' ? task.title : 'Mission', description: typeof task.description === 'string' ? task.description : '', duration: typeof task.duration === 'number' ? task.duration : 3, emotion: task.emotion ?? null, })); resetDeck(); } else { missionPoolRef.current = []; setMissionDeck([]); } } catch (err) { if (!cancelled) { console.warn('Mission preview failed', err); missionPoolRef.current = []; setMissionDeck([]); } } finally { if (!cancelled) { setMissionLoading(false); } } } loadMissions(); return () => { cancelled = true; }; }, [resetDeck, token, locale]); if (!token) { return null; } const introArray: string[] = []; for (let i = 0; i < 12; i += 1) { const candidate = t(`home.introRotating.${i}`, ''); if (candidate) { introArray.push(candidate); } } const introMessage = introArray.length > 0 ? introArray[Math.floor(Math.random() * introArray.length)] : ''; return (

{t('home.welcomeLine').replace('{name}', displayName)}

{introMessage && (

{introMessage}

)}
{heroVisible && ( )}
); } function HeroCard({ name, eventName, tasksCompleted, t, branding, onDismiss, ctaLabel, ctaHref, }: { name: string; eventName: string; tasksCompleted: number; t: TranslateFn; branding: EventBranding; onDismiss: () => void; ctaLabel?: string; ctaHref?: string; }) { const heroTitle = t('home.hero.title').replace('{name}', name); const heroDescription = t('home.hero.description').replace('{eventName}', eventName); const progressMessage = tasksCompleted > 0 ? t('home.hero.progress.some').replace('{count}', `${tasksCompleted}`) : t('home.hero.progress.none'); const style = React.useMemo(() => ({ background: `linear-gradient(135deg, ${branding.primaryColor}, ${branding.secondaryColor})`, color: '#ffffff', fontFamily: branding.fontFamily ?? undefined, }), [branding.fontFamily, branding.primaryColor, branding.secondaryColor]); return ( {t('home.hero.subtitle')} {heroTitle}

{heroDescription}

{progressMessage}

{ctaHref && ctaLabel && ( )}
); } type MissionPreview = { id: number; title: string; description?: string; duration?: number; emotion?: { name?: string; slug?: string } | null; }; function MissionActionCard({ token, mission, loading, onAdvance, stack, }: { token: string; mission: MissionPreview | null; loading: boolean; onAdvance: () => void; stack: MissionPreview[]; }) { const { branding } = useEventBranding(); const radius = branding.buttons?.radius ?? 12; const primary = branding.buttons?.primary ?? branding.primaryColor; const secondary = branding.buttons?.secondary ?? branding.secondaryColor; const buttonStyle = branding.buttons?.style ?? 'filled'; const headingFont = branding.typography?.heading ?? branding.fontFamily ?? undefined; const bodyFont = branding.typography?.body ?? branding.fontFamily ?? undefined; const textColor = '#1f2937'; const subTextColor = '#334155'; const swipeThreshold = 120; const stackLayers = stack.slice(1, 4); const cardStyle: React.CSSProperties = { borderRadius: `${radius + 8}px`, backgroundColor: '#fcf7ef', backgroundImage: `linear-gradient(0deg, ${primary}33, ${primary}22), url(/patterns/rays-sunburst.svg)`, backgroundBlendMode: 'multiply, normal', backgroundSize: '330% 330%', backgroundPosition: 'center', backgroundRepeat: 'no-repeat', filter: 'contrast(1.12) saturate(1.1)', border: `1px solid ${primary}26`, boxShadow: `0 12px 28px ${primary}22, 0 2px 6px ${primary}1f`, fontFamily: bodyFont, position: 'relative', overflow: 'hidden', }; const [{ x, y, rotateZ, rotateY, rotateX, scale, opacity }, api] = useSpring(() => ({ x: 0, y: 0, rotateZ: 0, rotateY: 0, rotateX: 0, scale: 1, opacity: 1, config: { tension: 320, friction: 26 }, })); React.useEffect(() => { api.start({ x: 0, y: 0, rotateZ: 0, rotateY: 0, rotateX: 0, scale: 1, opacity: 1, immediate: false }); }, [mission?.id, api]); const bind = useGesture( { onDrag: ({ active, movement: [mx, my], velocity: [vx], direction: [dx], cancel }) => { if (active && Math.abs(mx) > swipeThreshold) { cancel?.(); api.start({ x: dx > 0 ? 520 : -520, y: my, rotateZ: dx > 0 ? 12 : -12, rotateY: dx > 0 ? 18 : -18, rotateX: -my / 10, opacity: 0, scale: 1, immediate: false, onRest: () => { onAdvance(); api.start({ x: 0, y: 0, rotateZ: 0, rotateY: 0, rotateX: 0, opacity: 1, scale: 1, immediate: false }); }, }); return; } api.start({ x: mx, y: my, rotateZ: mx / 18, rotateY: mx / 28, rotateX: -my / 36, scale: active ? 1.02 : 1, opacity: 1, immediate: false, }); }, onDragEnd: () => { api.start({ x: 0, y: 0, rotateZ: 0, rotateY: 0, rotateX: 0, scale: 1, opacity: 1, immediate: false }); }, }, { drag: { filterTaps: true, bounds: { left: -200, right: 200, top: -120, bottom: 120 }, rubberband: true, }, } ); return (
{stackLayers.map((item, index) => { const depth = index + 1; const scaleDown = 1 - depth * 0.03; const translateY = depth * 12; const fade = Math.max(0.25, 0.55 - depth * 0.08); return (
); })}

Fotoaufgabe

Wir haben schon etwas für dich vorbereitet.

{mission ? (

{mission.title}

{mission.description && (

{mission.description}

)}
) : (

Ziehe deine erste Mission im Aufgaben-Tab oder wähle eine Stimmung.

)}
); } function EmotionActionCard() { return ( Wähle eine Stimmung und erhalte eine passende Aufgabe Tippe deinen Mood, wir picken die nächste Mission für dich. ); } function UploadActionCard({ token, accentColor, secondaryAccent, radius, bodyFont, }: { token: string; accentColor: string; secondaryAccent: string; radius: number; bodyFont?: string; }) { return (

Direkt hochladen

Kamera öffnen oder ein Foto aus deiner Galerie wählen.

Offline möglich – wir laden später hoch.

); }