Files
fotospiel-app/resources/js/guest-v2/screens/SharedPhotoScreen.tsx
2026-02-03 15:18:44 +01:00

212 lines
6.6 KiB
TypeScript

import React from 'react';
import { useParams } from 'react-router-dom';
import { YStack, XStack } from '@tamagui/stacks';
import { SizableText as Text } from '@tamagui/text';
import { Button } from '@tamagui/button';
import { AlertCircle, Download } from 'lucide-react';
import StandaloneShell from '../components/StandaloneShell';
import SurfaceCard from '../components/SurfaceCard';
import EventLogo from '../components/EventLogo';
import { fetchPhotoShare } from '@/guest/services/photosApi';
import type { EventBrandingPayload } from '@/guest/services/eventApi';
import { useTranslation } from '@/guest/i18n/useTranslation';
import { EventBrandingProvider } from '@/guest/context/EventBrandingContext';
import { mapEventBranding } from '../lib/eventBranding';
import { BrandingTheme } from '../lib/brandingTheme';
import { useGuestThemeVariant } from '../lib/guestTheme';
interface ShareResponse {
slug: string;
expires_at?: string;
photo: {
id: number;
title?: string;
likes_count?: number;
emotion?: { name?: string; emoji?: string | null } | null;
created_at?: string | null;
image_urls: { full: string; thumbnail: string };
};
event?: { id: number; name?: string | null } | null;
branding?: EventBrandingPayload | null;
}
export default function SharedPhotoScreen() {
const { slug } = useParams<{ slug: string }>();
const { t } = useTranslation();
const [state, setState] = React.useState<{ loading: boolean; error: string | null; data: ShareResponse | null }>({
loading: true,
error: null,
data: null,
});
const branding = React.useMemo(() => {
if (!state.data?.branding) {
return null;
}
return mapEventBranding(state.data.branding);
}, [state.data]);
const { isDark } = useGuestThemeVariant(branding);
const mutedText = isDark ? 'rgba(248, 250, 252, 0.7)' : 'rgba(15, 23, 42, 0.65)';
React.useEffect(() => {
let active = true;
if (!slug) return;
setState({ loading: true, error: null, data: null });
fetchPhotoShare(slug)
.then((data) => {
if (active) setState({ loading: false, error: null, data });
})
.catch(() => {
if (active) {
setState({
loading: false,
error: t('share.expiredDescription', 'Dieser Link ist nicht mehr verfügbar.'),
data: null,
});
}
});
return () => {
active = false;
};
}, [slug]);
if (state.loading) {
return (
<StandaloneShell>
<SurfaceCard>
<Text fontSize="$3" color={mutedText}>
{t('share.loading', 'Moment wird geladen …')}
</Text>
</SurfaceCard>
</StandaloneShell>
);
}
if (state.error || !state.data) {
return (
<StandaloneShell>
<SurfaceCard>
<XStack alignItems="center" gap="$2">
<AlertCircle size={18} color={isDark ? '#FCA5A5' : '#B91C1C'} />
<Text fontSize="$4" fontWeight="$7">
{t('share.expiredTitle', 'Link abgelaufen')}
</Text>
</XStack>
<Text fontSize="$2" color={mutedText} marginTop="$2">
{state.error ?? t('share.expiredDescription', 'Dieses Foto ist nicht mehr verfügbar.')}
</Text>
</SurfaceCard>
</StandaloneShell>
);
}
const { data } = state;
const chips = buildChips(data, t);
const content = (
<StandaloneShell>
<SurfaceCard glow>
<XStack alignItems="center" gap="$3">
<EventLogo name={data.event?.name ?? t('share.defaultEvent', 'Ein besonderer Moment')} size="s" />
<YStack gap="$1">
<Text fontSize="$2" letterSpacing={2} textTransform="uppercase" color={mutedText}>
{t('share.title', 'Geteiltes Foto')}
</Text>
<Text fontSize="$6" fontWeight="$8">
{data.event?.name ?? t('share.defaultEvent', 'Ein besonderer Moment')}
</Text>
</YStack>
</XStack>
{data.photo.title ? (
<Text fontSize="$3" color={mutedText} marginTop="$1">
{data.photo.title}
</Text>
) : null}
</SurfaceCard>
<SurfaceCard padding={0} overflow="hidden">
<YStack
height={360}
style={{
backgroundImage: `url(${data.photo.image_urls.full})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
/>
</SurfaceCard>
{chips.length > 0 ? (
<XStack gap="$2" flexWrap="wrap" justifyContent="center">
{chips.map((chip) => (
<SurfaceCard key={chip.id} padding="$2" borderRadius="$pill">
<XStack alignItems="center" gap="$2">
{chip.icon ? <Text fontSize="$3">{chip.icon}</Text> : null}
<Text fontSize="$2" color={mutedText}>
{chip.label}
</Text>
<Text fontSize="$2" fontWeight="$7">
{chip.value}
</Text>
</XStack>
</SurfaceCard>
))}
</XStack>
) : null}
<Button
size="$4"
borderRadius="$pill"
backgroundColor="$primary"
onPress={() => window.open(data.photo.image_urls.full, '_blank')}
>
<Download size={18} color="white" />
<Text fontSize="$3" fontWeight="$7" color="white">
{t('galleryPublic.download', 'Download')}
</Text>
</Button>
</StandaloneShell>
);
if (!branding) {
return content;
}
return (
<EventBrandingProvider branding={branding}>
<BrandingTheme>
{content}
</BrandingTheme>
</EventBrandingProvider>
);
}
function buildChips(
data: ShareResponse,
t: (key: string, fallback?: string) => string
): { id: string; label: string; value: string; icon?: string }[] {
const list: { id: string; label: string; value: string; icon?: string }[] = [];
if (data.photo.emotion?.name) {
list.push({
id: 'emotion',
label: t('share.chips.emotion', 'Emotion'),
value: data.photo.emotion.name,
icon: data.photo.emotion.emoji ?? '★',
});
}
if (data.photo.title) {
list.push({ id: 'task', label: t('share.chips.task', 'Aufgabe'), value: data.photo.title });
}
if (data.photo.created_at) {
const date = formatDate(data.photo.created_at);
list.push({ id: 'date', label: t('share.chips.date', 'Aufgenommen'), value: date });
}
return list;
}
function formatDate(value: string): string {
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) return '';
return parsed.toLocaleDateString(undefined, { day: '2-digit', month: 'short', year: 'numeric' });
}