import React from 'react'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Sparkles, RefreshCw, Smile, Timer as TimerIcon, CheckCircle2, AlertTriangle } from 'lucide-react'; import { useGuestTaskProgress } from '../hooks/useGuestTaskProgress'; interface Task { id: number; title: string; description: string; instructions: string; duration: number; // minutes emotion?: { slug: string; name: string; }; is_completed: boolean; } type EmotionOption = { slug: string; name: string; }; const TASK_PROGRESS_TARGET = 5; const TIMER_VIBRATION = [0, 60, 120, 60]; export default function TaskPickerPage() { const { token } = useParams<{ token: string }>(); const eventKey = token ?? ''; const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); const { completedCount, markCompleted, isCompleted } = useGuestTaskProgress(eventKey); const [tasks, setTasks] = React.useState([]); const [currentTask, setCurrentTask] = React.useState(null); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); const [selectedEmotion, setSelectedEmotion] = React.useState('all'); const [timeLeft, setTimeLeft] = React.useState(0); const [timerRunning, setTimerRunning] = React.useState(false); const [timeUp, setTimeUp] = React.useState(false); const [isFetching, setIsFetching] = React.useState(false); const recentTaskIdsRef = React.useRef([]); const initialEmotionRef = React.useRef(false); const fetchTasks = React.useCallback(async () => { if (!eventKey) return; setIsFetching(true); setLoading(true); setError(null); try { const response = await fetch(`/api/v1/events/${encodeURIComponent(eventKey)}/tasks`); if (!response.ok) throw new Error('Aufgaben konnten nicht geladen werden.'); const payload = await response.json(); if (Array.isArray(payload)) { setTasks(payload); } else { setTasks([]); } } catch (err) { console.error('Failed to load tasks', err); setError(err instanceof Error ? err.message : 'Unbekannter Fehler'); setTasks([]); } finally { setIsFetching(false); setLoading(false); } }, [eventKey]); React.useEffect(() => { fetchTasks(); }, [fetchTasks]); React.useEffect(() => { if (initialEmotionRef.current) return; const queryEmotion = searchParams.get('emotion'); if (queryEmotion) { setSelectedEmotion(queryEmotion); } initialEmotionRef.current = true; }, [searchParams]); const emotionOptions = React.useMemo(() => { const map = new Map(); tasks.forEach((task) => { if (task.emotion?.slug) { map.set(task.emotion.slug, task.emotion.name); } }); return Array.from(map.entries()).map(([slugValue, name]) => ({ slug: tokenValue, name })); }, [tasks]); const filteredTasks = React.useMemo(() => { if (selectedEmotion === 'all') return tasks; return tasks.filter((task) => task.emotion?.token === selectedEmotion); }, [tasks, selectedEmotion]); const selectRandomTask = React.useCallback( (list: Task[]) => { 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 (!filteredTasks.length) { setCurrentTask(null); return; } if (!currentTask || !filteredTasks.some((task) => task.id === currentTask.id)) { selectRandomTask(filteredTasks); return; } const matchingTask = filteredTasks.find((task) => task.id === currentTask.id); const durationMinutes = matchingTask?.duration ?? currentTask.duration; setTimeLeft(durationMinutes * 60); setTimerRunning(false); setTimeUp(false); }, [filteredTasks, currentTask, selectRandomTask]); React.useEffect(() => { if (!currentTask) { setTimeLeft(0); setTimerRunning(false); setTimeUp(false); return; } setTimeLeft(currentTask.duration * 60); setTimerRunning(false); setTimeUp(false); }, [currentTask]); React.useEffect(() => { if (!timerRunning) return; if (timeLeft <= 0) { setTimerRunning(false); triggerTimeUp(); return; } const tick = window.setInterval(() => { setTimeLeft((prev) => { if (prev <= 1) { window.clearInterval(tick); triggerTimeUp(); return 0; } return prev - 1; }); }, 1000); return () => window.clearInterval(tick); }, [timerRunning, timeLeft]); function triggerTimeUp() { const supportsVibration = typeof navigator !== 'undefined' && typeof navigator.vibrate === 'function'; setTimerRunning(false); setTimeUp(true); if (supportsVibration) { try { navigator.vibrate(TIMER_VIBRATION); } catch (error) { console.warn('Vibration not permitted', error); } } window.setTimeout(() => setTimeUp(false), 4000); return; } const formatTime = React.useCallback((seconds: number) => { const mins = Math.floor(seconds / 60); const secs = Math.max(0, seconds % 60); return `${mins}:${secs.toString().padStart(2, '0')}`; }, []); const progressRatio = currentTask ? Math.min(1, completedCount / TASK_PROGRESS_TARGET) : 0; const handleSelectEmotion = (slugValue: string) => { setSelectedEmotion(slugValue); const next = new URLSearchParams(searchParams.toString()); if (slugValue === 'all') { next.delete('emotion'); } else { next.set('emotion', slugValue); } setSearchParams(next, { replace: true }); }; const handleNewTask = () => { selectRandomTask(filteredTasks); }; const handleStartUpload = () => { if (!currentTask || !eventKey) return; if (!eventKey) return; navigate(`/e/${encodeURIComponent(eventKey)}/upload?task=${currentTask.id}&emotion=${currentTask.emotion?.slug || ''}`); }; const handleMarkCompleted = () => { if (!currentTask) return; markCompleted(currentTask.id); selectRandomTask(filteredTasks); }; const handleRetryFetch = () => { fetchTasks(); }; const handleTimerToggle = () => { if (!currentTask) return; if (timerRunning) { setTimerRunning(false); setTimeLeft(currentTask.duration * 60); setTimeUp(false); } else { if (timeLeft <= 0) { setTimeLeft(currentTask.duration * 60); } setTimerRunning(true); setTimeUp(false); } }; const emptyState = !loading && (!filteredTasks.length || !currentTask); return (

Aufgabe auswaehlen

Schon {completedCount} Aufgaben erledigt
Auf dem Weg zum naechsten Erfolg {completedCount >= TASK_PROGRESS_TARGET ? 'Stark!' : `${Math.max(0, TASK_PROGRESS_TARGET - completedCount)} bis zum Badge`}
{emotionOptions.length > 0 && (
handleSelectEmotion('all')} /> {emotionOptions.map((emotion) => ( handleSelectEmotion(emotion.slug)} /> ))}
)}
{loading && (
)} {error && !loading && ( {error} )} {emptyState && ( )} {!emptyState && currentTask && (

Deine Mission

{currentTask.title}

{timeUp && ( Zeit abgelaufen! )}
{currentTask.duration} Min {currentTask.emotion?.name && ( {currentTask.emotion.name} )} {isCompleted(currentTask.id) && ( Bereits erledigt )}

{currentTask.description}

{currentTask.instructions && (
{currentTask.instructions}
)}
{timerRunning && currentTask.duration > 0 && (
Countdown Restzeit: {formatTime(timeLeft)}
)}
)} {!loading && !tasks.length && !error && ( Fuer dieses Event sind derzeit keine Aufgaben hinterlegt. )}
); } function timerTone(timeLeft: number, durationMinutes: number) { const totalSeconds = Math.max(1, durationMinutes * 60); const ratio = timeLeft / totalSeconds; if (ratio > 0.5) return 'okay'; if (ratio > 0.25) return 'warm'; return 'hot'; } function EmotionChip({ label, active, onClick }: { label: string; active: boolean; onClick: () => void }) { return ( ); } function ChecklistItem({ text }: { text: string }) { return (
  • {text}
  • ); } function BadgeTimer({ label, value, tone }: { label: string; value: string; tone: 'okay' | 'warm' | 'hot' }) { const toneClasses = { okay: 'bg-emerald-500/15 text-emerald-500 border-emerald-500/30', warm: 'bg-amber-500/15 text-amber-500 border-amber-500/30', hot: 'bg-rose-500/15 text-rose-500 border-rose-500/30', }[tone]; return (
    {label} {value}
    ); } function SkeletonBlock() { return
    ; } function EmptyState({ hasTasks, onRetry, emotionOptions, onEmotionSelect, }: { hasTasks: boolean; onRetry: () => void; emotionOptions: EmotionOption[]; onEmotionSelect: (slug: string) => void; }) { return (

    Keine passende Aufgabe gefunden

    {hasTasks ? 'Fuer deine aktuelle Stimmung gibt es gerade keine Aufgabe. Waehle eine andere Stimmung oder lade neue Aufgaben.' : 'Hier sind noch keine Aufgaben hinterlegt. Bitte versuche es spaeter erneut.'}

    {hasTasks && emotionOptions.length > 0 && (
    {emotionOptions.map((emotion) => ( onEmotionSelect(emotion.slug)} /> ))}
    )}
    ); }