196 lines
5.9 KiB
TypeScript
196 lines
5.9 KiB
TypeScript
import React from 'react';
|
|
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
|
import { Dialog, DialogContent } from '@/components/ui/dialog';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Heart } from 'lucide-react';
|
|
import { likePhoto } from '../services/photosApi';
|
|
|
|
type Photo = {
|
|
id: number;
|
|
file_path?: string;
|
|
thumbnail_path?: string;
|
|
likes_count?: number;
|
|
created_at?: string;
|
|
task_id?: number
|
|
};
|
|
|
|
type Task = { id: number; title: string };
|
|
|
|
export default function PhotoLightbox() {
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const { photoId } = useParams();
|
|
|
|
const [photo, setPhoto] = React.useState<Photo | null>(null);
|
|
const [task, setTask] = React.useState<Task | null>(null);
|
|
const [taskLoading, setTaskLoading] = React.useState(false);
|
|
const [likes, setLikes] = React.useState<number | null>(null);
|
|
const [liked, setLiked] = React.useState(false);
|
|
|
|
// Extract event slug from URL path
|
|
const getEventSlug = () => {
|
|
const path = window.location.pathname;
|
|
const match = path.match(/^\/e\/([^\/]+)\/photo\/[^\/]+$/);
|
|
return match ? match[1] : null;
|
|
};
|
|
|
|
const slug = getEventSlug();
|
|
|
|
// Load photo if not passed via state
|
|
React.useEffect(() => {
|
|
const statePhoto = (location.state as any)?.photo;
|
|
if (statePhoto) {
|
|
setPhoto(statePhoto);
|
|
setLikes(statePhoto.likes_count ?? 0);
|
|
return;
|
|
}
|
|
|
|
if (!photoId) return;
|
|
|
|
(async () => {
|
|
try {
|
|
const res = await fetch(`/api/v1/photos/${photoId}`);
|
|
if (res.ok) {
|
|
const photoData = await res.json();
|
|
setPhoto(photoData);
|
|
setLikes(photoData.likes_count ?? 0);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load photo:', error);
|
|
}
|
|
})();
|
|
}, [photoId, location.state]);
|
|
|
|
// Load task info if photo has task_id and slug is available
|
|
React.useEffect(() => {
|
|
if (!photo?.task_id || !slug) {
|
|
setTask(null);
|
|
setTaskLoading(false);
|
|
return;
|
|
}
|
|
|
|
const taskId = photo.task_id;
|
|
|
|
(async () => {
|
|
setTaskLoading(true);
|
|
try {
|
|
const res = await fetch(`/api/v1/events/${slug}/tasks`);
|
|
if (res.ok) {
|
|
const tasks = await res.json();
|
|
const foundTask = tasks.find((t: any) => t.id === taskId);
|
|
if (foundTask) {
|
|
setTask({
|
|
id: foundTask.id,
|
|
title: foundTask.title || `Aufgabe ${taskId}`
|
|
});
|
|
} else {
|
|
setTask({
|
|
id: taskId,
|
|
title: `Unbekannte Aufgabe ${taskId}`
|
|
});
|
|
}
|
|
} else {
|
|
setTask({
|
|
id: taskId,
|
|
title: `Unbekannte Aufgabe ${taskId}`
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load task:', error);
|
|
setTask({
|
|
id: taskId,
|
|
title: `Unbekannte Aufgabe ${taskId}`
|
|
});
|
|
} finally {
|
|
setTaskLoading(false);
|
|
}
|
|
})();
|
|
}, [photo?.task_id, slug]);
|
|
|
|
async function onLike() {
|
|
if (liked || !photo) return;
|
|
setLiked(true);
|
|
try {
|
|
const count = await likePhoto(photo.id);
|
|
setLikes(count);
|
|
} catch (error) {
|
|
console.error('Like failed:', error);
|
|
setLiked(false);
|
|
}
|
|
}
|
|
|
|
function onOpenChange(open: boolean) {
|
|
if (!open) navigate(-1);
|
|
}
|
|
|
|
if (!photo && !photoId) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<Dialog open={true} onOpenChange={onOpenChange}>
|
|
<DialogContent className="max-w-5xl border-0 bg-black p-0 text-white">
|
|
{/* Header with controls */}
|
|
<div className="flex items-center justify-between p-2">
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
variant="secondary"
|
|
size="sm"
|
|
onClick={onLike}
|
|
disabled={liked || !photo}
|
|
>
|
|
<Heart className={`mr-1 h-4 w-4 ${liked ? 'fill-red-400 text-red-400' : ''}`} />
|
|
{likes ?? 0}
|
|
</Button>
|
|
</div>
|
|
<Button variant="secondary" size="sm" onClick={() => navigate(-1)}>
|
|
Schließen
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Task Info Overlay */}
|
|
{task && (
|
|
<div className="absolute top-4 left-4 right-4 z-20 bg-black/60 backdrop-blur-sm rounded-xl p-3 border border-white/20 max-w-md">
|
|
<div className="text-sm">
|
|
<div className="font-semibold mb-1 text-white">Task: {task.title}</div>
|
|
{taskLoading && (
|
|
<div className="text-xs opacity-70 text-gray-300">Lade Aufgabe...</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Photo Display */}
|
|
<div className="flex items-center justify-center min-h-[60vh] p-4">
|
|
{photo ? (
|
|
<img
|
|
src={photo.file_path || photo.thumbnail_path}
|
|
alt="Foto"
|
|
className="max-h-[80vh] max-w-full object-contain"
|
|
onError={(e) => {
|
|
console.error('Image load error:', e);
|
|
(e.target as HTMLImageElement).style.display = 'none';
|
|
}}
|
|
/>
|
|
) : (
|
|
<div className="p-6 text-center">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-white mx-auto mb-2"></div>
|
|
<div>Lade Foto...</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Loading state for task */}
|
|
{taskLoading && !task && (
|
|
<div className="absolute top-4 left-4 right-4 z-20 bg-black/60 backdrop-blur-sm rounded-xl p-3 border border-white/20 max-w-md">
|
|
<div className="text-sm text-center">
|
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mx-auto mb-1"></div>
|
|
<div className="text-xs opacity-70">Lade Aufgabe...</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|