190 lines
8.0 KiB
TypeScript
190 lines
8.0 KiB
TypeScript
// @ts-nocheck
|
|
import React from 'react';
|
|
import { Link } from 'react-router-dom';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import { getDeviceId } from '../lib/device';
|
|
import { usePollGalleryDelta } from '../polling/usePollGalleryDelta';
|
|
import { Heart } from 'lucide-react';
|
|
import { useTranslation } from '../i18n/useTranslation';
|
|
import { useEventBranding } from '../context/EventBrandingContext';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
type Props = { token: string };
|
|
|
|
type PreviewFilter = 'latest' | 'popular' | 'mine' | 'photobooth';
|
|
type PreviewPhoto = {
|
|
id: number;
|
|
session_id?: string | null;
|
|
ingest_source?: string | null;
|
|
likes_count?: number | null;
|
|
created_at?: string | null;
|
|
task_id?: number | null;
|
|
task_title?: string | null;
|
|
emotion_id?: number | null;
|
|
emotion_name?: string | null;
|
|
thumbnail_path?: string | null;
|
|
file_path?: string | null;
|
|
title?: string | null;
|
|
};
|
|
|
|
export default function GalleryPreview({ token }: Props) {
|
|
const { locale } = useTranslation();
|
|
const { branding } = useEventBranding();
|
|
const { photos, loading } = usePollGalleryDelta(token, locale);
|
|
const [mode, setMode] = React.useState<PreviewFilter>('latest');
|
|
const typedPhotos = React.useMemo(() => photos as PreviewPhoto[], [photos]);
|
|
const hasPhotobooth = React.useMemo(() => typedPhotos.some((p) => p.ingest_source === 'photobooth'), [typedPhotos]);
|
|
const radius = branding.buttons?.radius ?? 12;
|
|
const linkColor = branding.buttons?.linkColor ?? branding.secondaryColor;
|
|
const headingFont = branding.typography?.heading ?? branding.fontFamily ?? undefined;
|
|
const bodyFont = branding.typography?.body ?? branding.fontFamily ?? undefined;
|
|
|
|
const items = React.useMemo(() => {
|
|
let arr = typedPhotos.slice();
|
|
|
|
// MyPhotos filter (requires session_id matching)
|
|
if (mode === 'mine') {
|
|
const deviceId = getDeviceId();
|
|
arr = arr.filter((photo) => photo.session_id === deviceId);
|
|
} else if (mode === 'photobooth') {
|
|
arr = arr.filter((photo) => photo.ingest_source === 'photobooth');
|
|
}
|
|
|
|
// Sorting
|
|
if (mode === 'popular') {
|
|
arr.sort((a, b) => (b.likes_count ?? 0) - (a.likes_count ?? 0));
|
|
} else {
|
|
arr.sort((a, b) => new Date(b.created_at ?? 0).getTime() - new Date(a.created_at ?? 0).getTime());
|
|
}
|
|
|
|
return arr.slice(0, 9); // up to 3x3 preview
|
|
}, [typedPhotos, mode]);
|
|
|
|
React.useEffect(() => {
|
|
if (mode === 'photobooth' && !hasPhotobooth) {
|
|
setMode('latest');
|
|
}
|
|
}, [mode, hasPhotobooth]);
|
|
|
|
// Helper function to generate photo title (must be before return)
|
|
function getPhotoTitle(photo: PreviewPhoto): string {
|
|
if (photo.task_id) {
|
|
return `Task: ${photo.task_title || 'Unbekannte Aufgabe'}`;
|
|
}
|
|
if (photo.emotion_id) {
|
|
return `Emotion: ${photo.emotion_name || 'Gefühl'}`;
|
|
}
|
|
// Fallback based on creation time or placeholder
|
|
const now = new Date();
|
|
const created = new Date(photo.created_at || now);
|
|
const hours = created.getHours();
|
|
if (hours < 12) return 'Morgenmoment';
|
|
if (hours < 18) return 'Nachmittagslicht';
|
|
return 'Abendstimmung';
|
|
}
|
|
|
|
const filters: { value: PreviewFilter; label: string }[] = [
|
|
{ value: 'latest', label: 'Newest' },
|
|
{ value: 'popular', label: 'Popular' },
|
|
{ value: 'mine', label: 'My Photos' },
|
|
...(hasPhotobooth ? [{ value: 'photobooth', label: 'Fotobox' } as const] : []),
|
|
];
|
|
|
|
return (
|
|
<Card
|
|
className="border border-muted/30 bg-[var(--guest-surface)] shadow-sm dark:border-slate-800/70 dark:bg-slate-950/70"
|
|
data-testid="gallery-preview"
|
|
style={{ borderRadius: radius, fontFamily: bodyFont }}
|
|
>
|
|
<CardContent className="space-y-3 p-3">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-xs uppercase tracking-wide text-muted-foreground" style={headingFont ? { fontFamily: headingFont } : undefined}>Live-Galerie</p>
|
|
<h3 className="text-lg font-semibold text-foreground" style={headingFont ? { fontFamily: headingFont } : undefined}>Alle Uploads auf einen Blick</h3>
|
|
</div>
|
|
<Link
|
|
to={`/e/${encodeURIComponent(token)}/gallery?mode=${mode}`}
|
|
className="text-sm font-semibold transition"
|
|
style={{ color: linkColor }}
|
|
>
|
|
Alle ansehen →
|
|
</Link>
|
|
</div>
|
|
|
|
<div className="flex overflow-x-auto pb-1 text-xs font-semibold [-ms-overflow-style:none] [scrollbar-width:none]">
|
|
<div className="inline-flex items-center rounded-full border border-border/70 bg-white/80 p-1 shadow-sm backdrop-blur dark:border-white/10 dark:bg-slate-950/70">
|
|
{filters.map((filter, index) => {
|
|
const isActive = mode === filter.value;
|
|
|
|
return (
|
|
<div key={filter.value} className="flex items-center">
|
|
<button
|
|
type="button"
|
|
onClick={() => setMode(filter.value)}
|
|
className={cn(
|
|
'inline-flex items-center rounded-full px-3 py-1.5 transition',
|
|
isActive
|
|
? 'bg-pink-500 text-white shadow'
|
|
: 'text-muted-foreground hover:bg-pink-50 hover:text-pink-600 dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white',
|
|
)}
|
|
>
|
|
<span className="whitespace-nowrap">{filter.label}</span>
|
|
</button>
|
|
{index < filters.length - 1 && (
|
|
<span className="mx-1 h-4 w-px bg-border/60 dark:bg-white/10" aria-hidden />
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{loading && <p className="text-sm text-muted-foreground">Lädt…</p>}
|
|
{!loading && items.length === 0 && (
|
|
<div className="flex items-center gap-3 rounded-xl border border-muted/30 bg-[var(--guest-surface)] p-3 text-sm text-muted-foreground dark:border-slate-800/60 dark:bg-slate-950/60">
|
|
<Heart className="h-4 w-4" style={{ color: branding.secondaryColor }} aria-hidden />
|
|
Noch keine Fotos. Starte mit deinem ersten Upload!
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid gap-3 grid-cols-2 md:grid-cols-3">
|
|
{items.map((p: PreviewPhoto) => (
|
|
<Link
|
|
key={p.id}
|
|
to={`/e/${encodeURIComponent(token)}/gallery?photoId=${p.id}`}
|
|
className="group flex flex-col overflow-hidden border border-border/60 bg-white shadow-sm ring-1 ring-black/5 transition duration-300 hover:-translate-y-0.5 hover:shadow-lg dark:border-white/10 dark:bg-slate-950 dark:ring-white/10"
|
|
style={{ borderRadius: radius }}
|
|
>
|
|
<div className="relative">
|
|
<img
|
|
src={p.thumbnail_path || p.file_path}
|
|
alt={p.title || 'Foto'}
|
|
className="aspect-[3/4] w-full object-cover transition duration-300 group-hover:scale-105"
|
|
loading="lazy"
|
|
/>
|
|
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-16 bg-gradient-to-t from-black/50 via-black/0 to-transparent" aria-hidden />
|
|
</div>
|
|
<div className="space-y-2 px-3 pb-3 pt-3">
|
|
<p className="text-sm font-semibold leading-tight line-clamp-2 text-foreground" style={headingFont ? { fontFamily: headingFont } : undefined}>
|
|
{p.title || getPhotoTitle(p)}
|
|
</p>
|
|
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
<Heart className="h-3.5 w-3.5 text-pink-500" aria-hidden />
|
|
{p.likes_count ?? 0}
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
|
|
<p className="text-center text-sm text-muted-foreground">
|
|
Lust auf mehr?{' '}
|
|
<Link to={`/e/${encodeURIComponent(token)}/gallery`} className="font-semibold transition" style={{ color: linkColor }}>
|
|
Zur Galerie →
|
|
</Link>
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|