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) => (
))}
);
}