265 lines
8.9 KiB
TypeScript
265 lines
8.9 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
|
|
import { Page } from './_util';
|
|
import { Button } from '@/components/ui/button';
|
|
import { useAppearance } from '../../hooks/use-appearance';
|
|
import { Clock, RefreshCw, Smile } from 'lucide-react';
|
|
import BottomNav from '../components/BottomNav';
|
|
import { useEventData } from '../hooks/useEventData';
|
|
import { EventData } from '../services/eventApi';
|
|
|
|
interface Task {
|
|
id: number;
|
|
title: string;
|
|
description: string;
|
|
instructions: string;
|
|
duration: number; // in minutes
|
|
emotion?: {
|
|
slug: string;
|
|
name: string;
|
|
};
|
|
is_completed: boolean;
|
|
}
|
|
|
|
export default function TaskPickerPage() {
|
|
const { slug } = useParams<{ slug: string }>();
|
|
const [searchParams] = useSearchParams();
|
|
// emotionSlug = searchParams.get('emotion'); // Temporär deaktiviert, da API-Filter nicht verfügbar
|
|
const navigate = useNavigate();
|
|
const { appearance } = useAppearance();
|
|
const isDark = appearance === 'dark';
|
|
|
|
const [tasks, setTasks] = useState<Task[]>([]);
|
|
const [currentTask, setCurrentTask] = useState<Task | null>(null);
|
|
const [timeLeft, setTimeLeft] = useState(0);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
// Timer state
|
|
useEffect(() => {
|
|
if (!currentTask) return;
|
|
|
|
const durationMs = currentTask.duration * 60 * 1000;
|
|
setTimeLeft(durationMs / 1000);
|
|
|
|
const interval = setInterval(() => {
|
|
setTimeLeft(prev => {
|
|
if (prev <= 1) {
|
|
clearInterval(interval);
|
|
return 0;
|
|
}
|
|
return prev - 1;
|
|
});
|
|
}, 1000);
|
|
|
|
return () => clearInterval(interval);
|
|
}, [currentTask]);
|
|
|
|
// Load tasks
|
|
useEffect(() => {
|
|
if (!slug) return;
|
|
|
|
async function fetchTasks() {
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
const url = `/api/v1/events/${slug}/tasks`;
|
|
console.log('Fetching tasks from:', url); // Debug
|
|
|
|
const response = await fetch(url);
|
|
if (!response.ok) throw new Error('Tasks konnten nicht geladen werden');
|
|
|
|
const data = await response.json();
|
|
setTasks(Array.isArray(data) ? data : []);
|
|
|
|
console.log('Loaded tasks:', data); // Debug
|
|
|
|
// Select random task
|
|
if (data.length > 0) {
|
|
const randomIndex = Math.floor(Math.random() * data.length);
|
|
setCurrentTask(data[randomIndex]);
|
|
console.log('Selected random task:', data[randomIndex]); // Debug
|
|
}
|
|
} catch (err) {
|
|
console.error('Fetch tasks error:', err);
|
|
setError(err instanceof Error ? err.message : 'Unbekannter Fehler');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
fetchTasks();
|
|
}, [slug]);
|
|
|
|
const formatTime = (seconds: number) => {
|
|
const mins = Math.floor(seconds / 60);
|
|
const secs = seconds % 60;
|
|
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
|
};
|
|
|
|
const handleNewTask = () => {
|
|
if (tasks.length === 0) return;
|
|
const randomIndex = Math.floor(Math.random() * tasks.length);
|
|
setCurrentTask(tasks[randomIndex]);
|
|
setTimeLeft(tasks[randomIndex].duration * 60);
|
|
};
|
|
|
|
const handleStartTask = () => {
|
|
if (!currentTask) return;
|
|
// Navigate to upload with task context
|
|
navigate(`/e/${slug}/upload?task=${currentTask.id}&emotion=${currentTask.emotion?.slug || ''}`);
|
|
};
|
|
|
|
const handleChangeMood = () => {
|
|
navigate(`/e/${slug}`);
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<Page title="Aufgabe laden...">
|
|
<div className={`flex flex-col items-center justify-center min-h-[60vh] p-4 ${
|
|
isDark ? 'text-white' : 'text-gray-900'
|
|
}`}>
|
|
<RefreshCw className="h-8 w-8 animate-spin mb-4" />
|
|
<p className="text-sm">Lade Aufgabe...</p>
|
|
</div>
|
|
<BottomNav />
|
|
</Page>
|
|
);
|
|
}
|
|
|
|
if (error || !currentTask) {
|
|
return (
|
|
<Page title="Keine Aufgaben verfügbar">
|
|
<div className={`flex flex-col items-center justify-center min-h-[60vh] p-4 space-y-4 ${
|
|
isDark ? 'text-white' : 'text-gray-900'
|
|
}`}>
|
|
<Smile className="h-12 w-12 text-pink-500" />
|
|
<div className="text-center">
|
|
<h2 className="text-xl font-semibold mb-2">Keine passende Aufgabe gefunden</h2>
|
|
<p className="text-sm text-muted-foreground max-w-md">
|
|
{error || 'Für deine Stimmung gibt es derzeit keine Aufgaben. Versuche eine andere Stimmung oder warte auf neue Inhalte.'}
|
|
</p>
|
|
</div>
|
|
<Button
|
|
variant="outline"
|
|
onClick={handleChangeMood}
|
|
className="w-full max-w-sm"
|
|
>
|
|
Andere Stimmung wählen
|
|
</Button>
|
|
</div>
|
|
<BottomNav />
|
|
</Page>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Page title={currentTask.title}>
|
|
<div className={`min-h-screen flex flex-col ${
|
|
isDark ? 'bg-gray-900 text-white' : 'bg-white text-gray-900'
|
|
}`}>
|
|
{/* Task Header with Selfie Overlay */}
|
|
<div className="p-4 space-y-4">
|
|
<div className="relative">
|
|
{/* Selfie Placeholder */}
|
|
<div className={`w-full aspect-square rounded-2xl bg-gradient-to-br ${
|
|
isDark
|
|
? 'from-gray-800 to-gray-700 shadow-2xl'
|
|
: 'from-pink-50 to-pink-100 shadow-lg'
|
|
} flex items-center justify-center`}>
|
|
<div className="text-center space-y-2">
|
|
<div className={`w-20 h-20 rounded-full bg-white/20 flex items-center justify-center mx-auto mb-2 ${
|
|
isDark ? 'text-white' : 'text-gray-600'
|
|
}`}>
|
|
📸
|
|
</div>
|
|
<p className={`text-sm font-medium ${
|
|
isDark ? 'text-gray-300' : 'text-gray-600'
|
|
}`}>
|
|
Selfie-Vorschau
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Timer */}
|
|
<div className="absolute top-2 right-2">
|
|
<div className={`flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium ${
|
|
timeLeft > 60
|
|
? 'bg-green-500/20 text-green-400 border-green-500/30'
|
|
: timeLeft > 30
|
|
? 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30'
|
|
: 'bg-red-500/20 text-red-400 border-red-500/30'
|
|
} border`}>
|
|
<Clock className="h-3 w-3" />
|
|
<span>{formatTime(timeLeft)}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Task Description Overlay */}
|
|
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/90 via-transparent to-transparent p-4 rounded-b-2xl">
|
|
<div className="space-y-2">
|
|
<h1 className={`text-xl font-bold ${
|
|
isDark ? 'text-gray-100' : 'text-white'
|
|
}`}>
|
|
{currentTask.title}
|
|
</h1>
|
|
<p className={`text-sm leading-relaxed ${
|
|
isDark ? 'text-gray-200' : 'text-gray-100'
|
|
}`}>
|
|
{currentTask.description}
|
|
</p>
|
|
{currentTask.instructions && (
|
|
<div className={`p-2 rounded-lg ${
|
|
isDark
|
|
? 'bg-gray-700/80 text-gray-100'
|
|
: 'bg-gray-800/80 text-white border border-gray-600/50'
|
|
}`}>
|
|
<p className="text-xs italic">💡 {currentTask.instructions}</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Action Buttons */}
|
|
<div className="px-4 pb-4 space-y-3">
|
|
<Button
|
|
onClick={handleStartTask}
|
|
className="w-full h-14 bg-gradient-to-r from-pink-500 to-pink-600 hover:from-pink-600 hover:to-pink-700 text-white rounded-xl text-base font-semibold shadow-lg hover:shadow-xl transition-all duration-200"
|
|
>
|
|
<span className="flex items-center justify-center gap-2">
|
|
📸 Los geht's
|
|
</span>
|
|
</Button>
|
|
|
|
<div className="flex gap-3">
|
|
<Button
|
|
variant="outline"
|
|
onClick={handleNewTask}
|
|
className="flex-1 h-12 border-gray-300 dark:border-gray-600 text-sm rounded-xl transition-all duration-200 hover:bg-gray-50 dark:hover:bg-gray-800"
|
|
>
|
|
<RefreshCw className="h-4 w-4 mr-2" />
|
|
Neue Aufgabe
|
|
</Button>
|
|
|
|
<Button
|
|
variant="ghost"
|
|
onClick={handleChangeMood}
|
|
className="flex-1 h-12 text-sm rounded-xl transition-all duration-200 hover:bg-gray-50 dark:hover:bg-gray-800"
|
|
>
|
|
<Smile className="h-4 w-4 mr-2" />
|
|
Andere Stimmung
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Bottom Navigation */}
|
|
<BottomNav />
|
|
</div>
|
|
</Page>
|
|
);
|
|
}
|