import React from 'react'; import { YStack, XStack } from '@tamagui/stacks'; import { SizableText as Text } from '@tamagui/text'; import { Button } from '@tamagui/button'; import { Trophy, Play } from 'lucide-react'; import AppShell from '../components/AppShell'; import TaskHeroCard, { type TaskHero } from '../components/TaskHeroCard'; import { useEventData } from '../context/EventDataContext'; import { useTranslation } from '@/guest/i18n/useTranslation'; import { useLocale } from '@/guest/i18n/LocaleContext'; import { fetchTasks } from '../services/tasksApi'; import { fetchEmotions } from '../services/emotionsApi'; import { useGuestThemeVariant } from '../lib/guestTheme'; import { useNavigate } from 'react-router-dom'; import { useGuestTaskProgress } from '@/guest/hooks/useGuestTaskProgress'; import { getBentoSurfaceTokens } from '../lib/bento'; import { buildEventPath } from '../lib/routes'; type TaskItem = TaskHero & { points?: number; }; export default function TasksScreen() { const { tasksEnabled, token } = useEventData(); const { t } = useTranslation(); const { locale } = useLocale(); const navigate = useNavigate(); const { completedCount, isCompleted } = useGuestTaskProgress(token ?? undefined); const { isDark } = useGuestThemeVariant(); const bentoSurface = getBentoSurfaceTokens(isDark); const cardBorder = bentoSurface.borderColor; const cardShadow = bentoSurface.shadow; const mutedButton = isDark ? 'rgba(255, 255, 255, 0.08)' : 'rgba(15, 23, 42, 0.06)'; const mutedButtonBorder = isDark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(15, 23, 42, 0.12)'; const [tasks, setTasks] = React.useState([]); const [highlight, setHighlight] = React.useState(null); const [loading, setLoading] = React.useState(false); const [error, setError] = React.useState(null); const [hasSwiped, setHasSwiped] = React.useState(false); const progressTotal = tasks.length || 1; const progressPercent = Math.min(100, Math.round((completedCount / progressTotal) * 100)); const mapTaskItem = React.useCallback((task: unknown, emotionMap: Record) => { 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 emotionSlug = typeof record.emotion_slug === 'string' ? record.emotion_slug : null; const emotionMeta = emotionSlug ? emotionMap[emotionSlug] : undefined; return { id, title, description, instructions, duration: Number.isFinite(duration) ? duration : null, emotion: emotionSlug ? { slug: emotionSlug, name: emotionMeta?.name ?? emotionSlug, emoji: emotionMeta?.emoji, } : undefined, points: typeof record.points === 'number' ? record.points : undefined, } satisfies TaskItem; }, []); const loadTasks = React.useCallback(async () => { if (!token) { setTasks([]); setHighlight(null); setError(null); setLoading(false); return; } setLoading(true); setError(null); try { const [taskList, emotionList] = await Promise.all([ fetchTasks(token, { locale, perPage: 12 }), fetchEmotions(token, locale), ]); const emotionMap: 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 : ''; const emoji = typeof record.emoji === 'string' ? record.emoji : undefined; if (slug) { emotionMap[slug] = { name: title || slug, emoji }; } } const mapped = taskList.map((task) => mapTaskItem(task, emotionMap)).filter(Boolean) as TaskItem[]; setTasks(mapped); setHighlight((prev) => { if (!mapped.length) return null; if (prev && mapped.some((task) => task.id === prev.id)) { return prev; } return mapped[0] ?? null; }); } catch (err) { console.error('Failed to load tasks', err); setError(t('pendingUploads.error', 'Loading failed. Please try again.')); } finally { setLoading(false); } }, [locale, mapTaskItem, t, token]); React.useEffect(() => { if (!token) { setTasks([]); setHighlight(null); setError(null); setLoading(false); return; } loadTasks(); }, [loadTasks, token]); React.useEffect(() => { if (!tasks.length) { setHighlight(null); return; } if (highlight && tasks.some((task) => task.id === highlight.id)) return; setHighlight(tasks[0] ?? null); }, [highlight, tasks]); const handleStartTask = React.useCallback(() => { if (!highlight) return; navigate(buildEventPath(token, `/upload?taskId=${highlight.id}`)); }, [highlight, navigate, token]); const handleShuffle = React.useCallback(() => { if (!tasks.length) return; const candidates = tasks.filter((task) => task.id !== highlight?.id); const nextList = candidates.length ? candidates : tasks; const next = nextList[Math.floor(Math.random() * nextList.length)]; setHighlight(next); setHasSwiped(true); }, [highlight?.id, tasks]); const handleViewSimilar = React.useCallback(() => { if (!highlight) return; navigate(buildEventPath(token, `/gallery?task=${highlight.id}`)); }, [highlight, navigate, token]); const handleOpenPhoto = React.useCallback( (photoId: number) => { if (!highlight) return; navigate(buildEventPath(token, `/gallery?photo=${photoId}&task=${highlight.id}`)); }, [highlight, navigate, token] ); if (!tasksEnabled) { return ( {t('tasks.disabled.title', 'Tasks are disabled')} {t('tasks.disabled.subtitle', 'This event is set to photo-only mode.')} ); } return ( {t('tasks.page.eyebrow', 'Mission hub')} {t('tasks.page.title', 'Your next task')} {t('tasks.page.subtitle', 'Pick a mood or stay spontaneous.')} setHasSwiped(true)} onStart={handleStartTask} onShuffle={handleShuffle} onViewSimilar={handleViewSimilar} onRetry={loadTasks} onOpenPhoto={handleOpenPhoto} isCompleted={Boolean(highlight && isCompleted(highlight.id))} photos={[]} photosLoading={false} /> {t('tasks.page.progressLabel', 'Quest progress')} {highlight ? `${progressPercent}%` : '--'} {t('tasks.page.progressValue', { count: completedCount, total: tasks.length }, '{count}/{total} tasks completed')} {error ? ( {error} ) : null} {tasks.length === 0 && loading ? ( {t('common.actions.loading', 'Loading...')} ) : null} {tasks.length === 0 && !loading && !error ? ( {t('tasks.page.noTasksAlert', 'No tasks available for this event yet.')} ) : null} {tasks.map((task) => ( {task.title} {task.emotion?.name ? ( {task.emotion.name} ) : null} {typeof task.points === 'number' ? ( +{task.points} ) : null} ))} ); }