import React from 'react'; import { useNavigate } from 'react-router-dom'; import { XStack, YStack } from '@tamagui/stacks'; import { SizableText as Text } from '@tamagui/text'; import { Button } from '@tamagui/button'; import { Camera, Image as ImageIcon, Trophy, Star, X } from 'lucide-react'; import AppShell from '../components/AppShell'; import PhotoFrameTile from '../components/PhotoFrameTile'; import TaskHeroCard, { type TaskHero, type TaskHeroPhoto } from '../components/TaskHeroCard'; import { useEventData } from '../context/EventDataContext'; import { buildEventPath } from '../lib/routes'; import { useStaggeredReveal } from '../lib/useStaggeredReveal'; import { usePollStats } from '../hooks/usePollStats'; import { fetchGallery } from '../services/photosApi'; import { useTranslation } from '@/shared/guest/i18n/useTranslation'; import { useGuestThemeVariant } from '../lib/guestTheme'; import { useLocale } from '@/shared/guest/i18n/LocaleContext'; import { fetchTasks, type TaskItem } from '../services/tasksApi'; import { useGuestTaskProgress } from '@/shared/guest/hooks/useGuestTaskProgress'; import { fetchEmotions } from '../services/emotionsApi'; import { getBentoSurfaceTokens } from '../lib/bento'; import { useEventBranding } from '@/shared/guest/context/EventBrandingContext'; import { useOptionalGuestIdentity } from '../context/GuestIdentityContext'; type ActionTileProps = { label: string; icon: React.ReactNode; onPress: () => void; }; type GalleryPreview = { id: number; imageUrl: string; }; type TaskPhoto = TaskHeroPhoto & { taskId?: number | null; }; function ActionTile({ label, icon, onPress, isDark, }: ActionTileProps & { isDark: boolean }) { const surface = getBentoSurfaceTokens(isDark); return ( ); } function QuickStats({ reveal, stats, isDark, }: { reveal: number; stats: { guestCount: number; likesCount: number }; isDark: boolean; }) { const { t } = useTranslation(); const surface = getBentoSurfaceTokens(isDark); return ( = 3 ? 1 : 0} y={reveal >= 3 ? 0 : 12} > {stats.guestCount} {t('homeV2.stats.guestCount', 'Guests')} {stats.likesCount} {t('homeV2.stats.likesCount', 'Likes')} ); } function normalizeImageUrl(src?: string | null) { if (!src) { return ''; } if (/^https?:/i.test(src)) { return src; } let cleanPath = src.replace(/^\/+/g, '').replace(/\/+/g, '/'); if (!cleanPath.startsWith('storage/')) { cleanPath = `storage/${cleanPath}`; } return `/${cleanPath}`.replace(/\/+/g, '/'); } export default function HomeScreen() { const { tasksEnabled, hasActiveTasks, token, event } = useEventData(); const navigate = useNavigate(); const revealStage = useStaggeredReveal({ steps: 5, intervalMs: 140, delayMs: 120 }); const { stats } = usePollStats(token ?? null); const { t } = useTranslation(); const { locale } = useLocale(); const { isCompleted } = useGuestTaskProgress(token ?? undefined); const { branding } = useEventBranding(); const identity = useOptionalGuestIdentity(); const [galleryPhotos, setGalleryPhotos] = React.useState([]); const [galleryLoading, setGalleryLoading] = React.useState(false); const [galleryError, setGalleryError] = React.useState(null); const [tasks, setTasks] = React.useState([]); const [currentTask, setCurrentTask] = React.useState(null); const [taskLoading, setTaskLoading] = React.useState(false); const [taskError, setTaskError] = React.useState(null); const [hasSwiped, setHasSwiped] = React.useState(false); const { isDark } = useGuestThemeVariant(); const bentoSurface = getBentoSurfaceTokens(isDark); const mutedButton = isDark ? 'rgba(255, 255, 255, 0.08)' : 'rgba(255, 255, 255, 0.75)'; const mutedButtonBorder = isDark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(15, 23, 42, 0.08)'; const cardShadow = bentoSurface.shadow; const welcomeStorageKey = token ? `guestWelcomeDismissed_${token}` : 'guestWelcomeDismissed'; const [welcomeVisible, setWelcomeVisible] = React.useState(true); const eventName = event?.name ?? t('galleryPage.hero.eventFallback', 'Event'); const guestName = identity?.name?.trim() ? identity.name.trim() : t('home.fallbackGuestName', 'Gast'); const welcomeTemplate = branding?.welcomeMessage?.trim() ? branding.welcomeMessage.trim() : t('homeV2.welcome.message', 'Welcome to {eventName}.'); const welcomeMessage = welcomeTemplate .replace('{eventName}', eventName) .replace('{name}', guestName); const dismissWelcome = React.useCallback(() => { setWelcomeVisible(false); if (typeof window === 'undefined') return; try { window.sessionStorage.setItem(welcomeStorageKey, '1'); } catch { // ignore storage errors } }, [welcomeStorageKey]); const goTo = (path: string) => () => navigate(buildEventPath(token, path)); const recentTaskIdsRef = React.useRef([]); React.useEffect(() => { if (typeof window === 'undefined') return; try { const stored = window.sessionStorage.getItem(welcomeStorageKey); setWelcomeVisible(stored !== '1'); } catch { setWelcomeVisible(true); } }, [welcomeStorageKey]); const rings = [ { label: t('home.actions.items.upload.label', 'Upload photo'), icon: , path: '/upload', }, { label: t('homeV2.rings.newUploads', 'New uploads'), icon: , path: '/gallery', }, { label: t('homeV2.rings.topMoments', 'Top moments'), icon: , path: '/gallery', }, { label: t('navigation.achievements', 'Achievements'), icon: , path: '/achievements', }, ]; const mapTaskItem = React.useCallback((task: TaskItem, emotionMap: Record): TaskHero | null => { const record = task as Record; const id = Number(record.id ?? record.task_id ?? 0); const title = typeof record.title === 'string' ? record.title : typeof record.name === 'string' ? record.name : ''; if (!id || !title) return null; const description = typeof record.description === 'string' ? record.description : typeof record.prompt === 'string' ? record.prompt : null; const instructions = typeof record.instructions === 'string' ? record.instructions : typeof record.instruction === 'string' ? record.instruction : null; const durationValue = record.duration ?? record.time_limit ?? record.minutes ?? null; const duration = typeof durationValue === 'number' ? durationValue : Number(durationValue); const emotionValue = record.emotion; const slugValue = typeof record.emotion_slug === 'string' ? (record.emotion_slug as string) : undefined; const nameValue = typeof record.emotion_name === 'string' ? (record.emotion_name as string) : typeof record.emotion_title === 'string' ? (record.emotion_title as string) : slugValue ? emotionMap[slugValue] : undefined; const emotion = typeof emotionValue === 'object' && emotionValue ? { slug: typeof (emotionValue as Record).slug === 'string' ? (emotionValue as Record).slug as string : undefined, name: typeof (emotionValue as Record).name === 'string' ? (emotionValue as Record).name as string : undefined, emoji: typeof (emotionValue as Record).emoji === 'string' ? (emotionValue as Record).emoji as string : undefined, } : { slug: slugValue, name: nameValue, }; return { id, title, description, instructions, duration: Number.isFinite(duration) ? duration : null, emotion, }; }, []); const selectRandomTask = React.useCallback( (list: TaskHero[]) => { if (!list.length) { setCurrentTask(null); return; } const avoidIds = recentTaskIdsRef.current; const available = list.filter((task) => !isCompleted(task.id)); const base = available.length ? available : list; let candidates = base.filter((task) => !avoidIds.includes(task.id)); if (!candidates.length) { candidates = base; } const chosen = candidates[Math.floor(Math.random() * candidates.length)]; setCurrentTask(chosen); recentTaskIdsRef.current = [...avoidIds.filter((id) => id !== chosen.id), chosen.id].slice(-3); }, [isCompleted] ); React.useEffect(() => { if (!token) { setGalleryPhotos([]); setGalleryError(null); return; } let active = true; setGalleryLoading(true); setGalleryError(null); fetchGallery(token, { limit: 72, locale }) .then((response) => { if (!active) return; const photos = Array.isArray(response.data) ? response.data : []; const mapped = photos.map((photo) => { const record = photo as Record; const id = Number(record.id ?? 0); const imageUrl = normalizeImageUrl( (record.thumbnail_url as string | null | undefined) ?? (record.thumbnail_path as string | null | undefined) ?? (record.file_path as string | null | undefined) ?? (record.full_url as string | null | undefined) ?? (record.url as string | null | undefined) ?? (record.image_url as string | null | undefined) ); const taskId = Number(record.task_id ?? record.taskId ?? 0); const likesCount = typeof record.likes_count === 'number' ? record.likes_count : typeof record.likesCount === 'number' ? record.likesCount : undefined; return { id, imageUrl, taskId: Number.isFinite(taskId) && taskId > 0 ? taskId : null, likesCount, }; }); setGalleryPhotos(mapped.filter((item) => item.id && item.imageUrl)); }) .catch((error) => { console.error('Failed to load gallery preview', error); if (active) { setGalleryPhotos([]); setGalleryError(t('gallery.error', 'Gallery could not be loaded.')); } }) .finally(() => { if (active) { setGalleryLoading(false); } }); return () => { active = false; }; }, [locale, t, token]); const reloadTasks = React.useCallback(() => { if (!token || !tasksEnabled) { setTasks([]); setCurrentTask(null); setTaskError(null); setTaskLoading(false); return Promise.resolve(); } setTaskLoading(true); setTaskError(null); return Promise.all([ fetchTasks(token, { locale }), fetchEmotions(token, locale), ]) .then(([taskList, emotionList]) => { const nextMap: Record = {}; for (const emotion of emotionList) { const record = emotion as Record; const slug = typeof record.slug === 'string' ? record.slug : ''; const title = typeof record.title === 'string' ? record.title : typeof record.name === 'string' ? record.name : ''; if (slug) { nextMap[slug] = title || slug; } } const mapped = taskList.map((task) => mapTaskItem(task, nextMap)).filter(Boolean) as TaskHero[]; setTasks(mapped); }) .catch((error) => { console.error('Failed to load tasks', error); setTasks([]); setCurrentTask(null); setTaskError(t('tasks.error', 'Tasks could not be loaded.')); }) .finally(() => { setTaskLoading(false); }); }, [locale, mapTaskItem, t, tasksEnabled, token]); React.useEffect(() => { let active = true; if (!token || !tasksEnabled) { setTasks([]); setCurrentTask(null); setTaskError(null); setTaskLoading(false); return; } setTaskLoading(true); setTaskError(null); Promise.all([ fetchTasks(token, { locale }), fetchEmotions(token, locale), ]) .then(([taskList, emotionList]) => { if (!active) return; const nextMap: Record = {}; for (const emotion of emotionList) { const record = emotion as Record; const slug = typeof record.slug === 'string' ? record.slug : ''; const title = typeof record.title === 'string' ? record.title : typeof record.name === 'string' ? record.name : ''; if (slug) { nextMap[slug] = title || slug; } } const mapped = taskList.map((task) => mapTaskItem(task, nextMap)).filter(Boolean) as TaskHero[]; setTasks(mapped); }) .catch((error) => { console.error('Failed to load tasks', error); if (active) { setTasks([]); setCurrentTask(null); setTaskError(t('tasks.error', 'Tasks could not be loaded.')); } }) .finally(() => { if (active) { setTaskLoading(false); } }); return () => { active = false; }; }, [locale, mapTaskItem, t, tasksEnabled, token]); React.useEffect(() => { if (!tasksEnabled || tasks.length === 0) { setCurrentTask(null); return; } if (currentTask && tasks.some((task) => task.id === currentTask.id) && !isCompleted(currentTask.id)) { return; } selectRandomTask(tasks); }, [currentTask, isCompleted, selectRandomTask, tasks, tasksEnabled]); const preview = React.useMemo( () => galleryPhotos.slice(0, 4).map((photo) => ({ id: photo.id, imageUrl: photo.imageUrl, })), [galleryPhotos] ); const taskPhotos = React.useMemo(() => { if (!currentTask) return []; const matches = galleryPhotos.filter((photo) => photo.taskId === currentTask.id); if (matches.length >= 6) { return matches.slice(0, 6).map((photo) => ({ id: photo.id, imageUrl: photo.imageUrl, likesCount: photo.likesCount, })); } const fallback = galleryPhotos.filter((photo) => photo.taskId !== currentTask.id); const combined = [...matches, ...fallback].slice(0, 6); return combined.map((photo) => ({ id: photo.id, imageUrl: photo.imageUrl, likesCount: photo.likesCount, })); }, [currentTask, galleryPhotos]); const openPreviewPhoto = React.useCallback( (photoId: number) => { navigate(buildEventPath(token, `/gallery?photo=${photoId}`)); }, [navigate, token] ); const openTaskPhoto = React.useCallback( (photoId: number) => { if (!currentTask) return; navigate(buildEventPath(token, `/gallery?photo=${photoId}&task=${currentTask.id}`)); }, [currentTask, navigate, token] ); const handleStartTask = React.useCallback(() => { if (!currentTask) return; navigate(buildEventPath(token, `/upload?taskId=${currentTask.id}`)); }, [currentTask, navigate, token]); const handleViewSimilar = React.useCallback(() => { if (!currentTask) return; navigate(buildEventPath(token, `/gallery?task=${currentTask.id}`)); }, [currentTask, navigate, token]); const handleShuffle = React.useCallback(() => { selectRandomTask(tasks); setHasSwiped(true); }, [selectRandomTask, tasks]); const showTaskHero = tasksEnabled && hasActiveTasks; return ( {welcomeVisible && welcomeMessage.trim() ? ( = 1 ? 1 : 0} y={revealStage >= 1 ? 0 : 12} > {t('homeV2.welcome.label', 'Welcome')} {welcomeMessage} ) : null} {showTaskHero ? ( = 2 ? 1 : 0} y={revealStage >= 2 ? 0 : 16} > setHasSwiped(true)} onStart={handleStartTask} onShuffle={handleShuffle} onViewSimilar={handleViewSimilar} onRetry={reloadTasks} onOpenPhoto={openTaskPhoto} isCompleted={isCompleted(currentTask?.id)} photos={taskPhotos} photosLoading={galleryLoading} photosError={galleryError} /> ) : ( = 2 ? 1 : 0} y={revealStage >= 2 ? 0 : 16} style={{ backgroundImage: isDark ? 'linear-gradient(135deg, rgba(79, 209, 255, 0.18), rgba(255, 79, 216, 0.12))' : 'linear-gradient(135deg, color-mix(in oklab, var(--guest-secondary, #F43F5E) 10%, white), color-mix(in oklab, var(--guest-primary, #FF5A5F) 10%, white))', boxShadow: cardShadow, }} > {t('homeV2.captureReady.label', 'Capture ready')} {t('homeV2.captureReady.title', 'Add a photo to the shared gallery')} {t('homeV2.captureReady.subtitle', 'Quick add from your camera or device.')} )} = 4 ? 1 : 0} y={revealStage >= 4 ? 0 : 16} style={{ boxShadow: cardShadow, }} > {t('homeV2.galleryPreview.title', 'Gallery preview')} {(galleryLoading ? [1, 2, 3, 4] : preview).map((tile) => { if (typeof tile === 'number') { return ( ); } return ( ); })} {!galleryLoading && preview.length === 0 ? ( {t('homeV2.galleryPreview.emptyTitle', 'No photos yet')} {t('homeV2.galleryPreview.emptyDescription', 'You should start taking some and fill this gallery with moments.')} ) : null} = 5 ? 1 : 0} y={revealStage >= 5 ? 0 : 12} > {rings.map((ring) => ( ))} ); }