import React, { useState, useRef, useEffect, useCallback } 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 { Camera, RotateCcw, Zap, ZapOff } from 'lucide-react'; import BottomNav from '../components/BottomNav'; import { uploadPhoto } from '../services/photosApi'; interface Task { id: number; title: string; description: string; instructions?: string; duration: number; emotion?: { slug: string; name: string }; difficulty?: 'easy' | 'medium' | 'hard'; } export default function UploadPage() { const { slug } = useParams<{ slug: string }>(); const [searchParams] = useSearchParams(); const navigate = useNavigate(); const { appearance } = useAppearance(); const isDark = appearance === 'dark'; // Task data from URL params const taskId = searchParams.get('task'); const emotionSlug = searchParams.get('emotion') || ''; const [task, setTask] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [uploading, setUploading] = useState(false); // Camera state const videoRef = useRef(null); const canvasRef = useRef(null); const [stream, setStream] = useState(null); const [facingMode, setFacingMode] = useState<'user' | 'environment'>('user'); // front = user, back = environment const [flashOn, setFlashOn] = useState(false); const [isPulsing, setIsPulsing] = useState(false); const [countdown, setCountdown] = useState(3); const [capturing, setCapturing] = useState(false); // Load task data from API useEffect(() => { if (!slug || !taskId) { setError('Keine Aufgabendaten gefunden'); setLoading(false); return; } const taskIdNum = parseInt(taskId); if (isNaN(taskIdNum)) { setError('Ungültige Aufgaben-ID'); setLoading(false); return; } async function fetchTask() { try { setLoading(true); setError(null); const response = await fetch(`/api/v1/events/${slug}/tasks`); if (!response.ok) throw new Error('Tasks konnten nicht geladen werden'); const tasks = await response.json(); const foundTask = tasks.find((t: any) => t.id === taskIdNum); if (foundTask) { setTask({ id: foundTask.id, title: foundTask.title || `Aufgabe ${taskIdNum}`, description: foundTask.description || 'Stelle dich für das Foto auf und lächle in die Kamera.', instructions: foundTask.instructions, duration: foundTask.duration || 2, emotion: foundTask.emotion, difficulty: 'medium' as const }); } else { // Fallback for unknown task ID setTask({ id: taskIdNum, title: `Unbekannte Aufgabe ${taskIdNum}`, description: 'Stelle dich für das Foto auf und lächle in die Kamera.', instructions: 'Positioniere dich gut und warte auf den Countdown.', duration: 2, emotion: emotionSlug ? { slug: emotionSlug, name: emotionSlug.replace('-', ' ').replace(/\b\w/g, l => l.toUpperCase()) } : undefined, difficulty: 'medium' as const }); } } catch (err) { console.error('Failed to fetch task:', err); setError('Aufgabe konnte nicht geladen werden'); // Set fallback task setTask({ id: taskIdNum, title: `Unbekannte Aufgabe ${taskIdNum}`, description: 'Stelle dich für das Foto auf und lächle in die Kamera.', instructions: 'Positioniere dich gut und warte auf den Countdown.', duration: 2, emotion: emotionSlug ? { slug: emotionSlug, name: emotionSlug.replace('-', ' ').replace(/\b\w/g, l => l.toUpperCase()) } : undefined, difficulty: 'medium' as const }); } finally { setLoading(false); } } fetchTask(); }, [slug, taskId, emotionSlug]); // Camera setup useEffect(() => { if (!slug || loading || !task) return; const setupCamera = async () => { try { const constraints: MediaStreamConstraints = { video: { width: { ideal: 1280 }, height: { ideal: 720 }, facingMode: facingMode ? { ideal: facingMode } : undefined } }; console.log('Requesting camera with constraints:', constraints); const newStream = await navigator.mediaDevices.getUserMedia(constraints); if (videoRef.current) { videoRef.current.srcObject = newStream; videoRef.current.play().catch(e => console.error('Video play error:', e)); // Set video dimensions after metadata is loaded videoRef.current.onloadedmetadata = () => { if (videoRef.current) { videoRef.current.style.display = 'block'; } }; } setStream(newStream); setError(null); // Clear any previous errors } catch (err: any) { console.error('Camera access error:', err.name, err.message); let errorMessage = 'Kamera konnte nicht gestartet werden.'; switch (err.name) { case 'NotAllowedError': errorMessage = 'Kamera-Zugriff verweigert.\n\n' + '• Chrome: Adressleiste klicken → Kamera-Symbol → "Zulassen"\n' + '• Safari: Einstellungen → Website-Einstellungen → Kamera → "Erlauben"\n' + '• Firefox: Adressleiste → Berechtigungen → Kamera → "Erlauben"\n\n' + 'Danach Seite neu laden.'; break; case 'NotFoundError': errorMessage = 'Keine Kamera gefunden. Bitte überprüfen Sie:\n' + '• Ob eine Kamera am Gerät verfügbar ist\n' + '• Ob andere Apps die Kamera verwenden\n' + '• Gerätekonfiguration in den Browser-Einstellungen'; break; case 'NotSupportedError': errorMessage = 'Kamera nicht unterstützt. Bitte verwenden Sie:\n' + '• Chrome, Firefox oder Safari (neueste Version)\n' + '• HTTPS-Verbindung (nicht HTTP)'; break; case 'OverconstrainedError': errorMessage = 'Kamera-Einstellungen nicht verfügbar. Versuche mit Standard-Einstellungen...'; // Fallback to basic constraints try { const fallbackConstraints = { video: true }; const fallbackStream = await navigator.mediaDevices.getUserMedia(fallbackConstraints); if (videoRef.current) { videoRef.current.srcObject = fallbackStream; videoRef.current.play(); } setStream(fallbackStream); setError(null); return; } catch (fallbackErr) { console.error('Fallback camera failed:', fallbackErr); } break; default: errorMessage = `Kamera-Fehler (${err.name}): ${err.message}\n\nBitte versuchen Sie:\n• Seite neu laden\n• Browser neu starten\n• Anderen Browser verwenden`; } setError(errorMessage); } }; setupCamera(); return () => { if (stream) { stream.getTracks().forEach(track => track.stop()); setStream(null); } }; }, [slug, loading, task, facingMode]); // Handle capture const handleCapture = useCallback(async () => { if (!videoRef.current || !canvasRef.current || !task) return; setCapturing(true); setIsPulsing(false); // Start countdown let count = 3; setCountdown(count); const countdownInterval = setInterval(() => { count--; setCountdown(count); if (count <= 0) { clearInterval(countdownInterval); // Capture photo const video = videoRef.current; const canvas = canvasRef.current; if (!canvas) return; const context = canvas.getContext('2d'); if (!context || !video) return; canvas.width = video.videoWidth; canvas.height = video.videoHeight; context.drawImage(video, 0, 0); // Convert to blob canvas.toBlob(async (blob) => { if (blob && task && slug) { try { // Show uploading state setUploading(true); setCapturing(false); setCountdown(3); setError(null); // Use emotionSlug directly (backend expects string slug) // Convert Blob to File with proper filename const timestamp = Date.now(); const fileName = `photo-${timestamp}-${task.id}.jpg`; const file = new File([blob], fileName, { type: 'image/jpeg', lastModified: timestamp }); console.log('Uploading photo:', { taskId: task.id, emotionSlug, fileName, fileSize: file.size }); // Upload the photo const photoId = await uploadPhoto(slug, file, task.id, emotionSlug); console.log('Upload successful, photo ID:', photoId); // Navigate to gallery with success navigate(`/e/${slug}/gallery?task=${task.id}&emotion=${emotionSlug}&uploaded=true`); } catch (error: any) { console.error('Upload failed:', error); setError(`Upload fehlgeschlagen: ${error.message}\n\nFoto wurde erstellt, aber nicht hochgeladen.\nVersuchen Sie es erneut oder wählen Sie ein anderes Foto aus.`); } finally { setUploading(false); } } }, 'image/jpeg', 0.8); setCapturing(false); setCountdown(3); } }, 1000); // Start pulsing animation setIsPulsing(true); }, [task, emotionSlug, slug, navigate]); // Switch camera const switchCamera = () => { setFacingMode(prev => prev === 'user' ? 'environment' : 'user'); }; // Toggle flash (for back camera only) const toggleFlash = () => { if (facingMode !== 'environment') return; setFlashOn(prev => !prev); // TODO: Implement actual flash control if possible }; if (loading) { return (

Kamera wird gestartet...

); } if (error || !task) { return (

Kamera nicht verfügbar

{error}

); } if (uploading) { return (

Foto wird hochgeladen

Bitte warten... Dies kann einen Moment dauern.

); } const difficultyColor = task.difficulty === 'easy' ? 'text-green-400' : task.difficulty === 'medium' ? 'text-yellow-400' : 'text-red-400'; return (
{/* Camera Preview Container */}
{/* Video Background */}
); }