upgrade to tamagui v2 and guest pwa overhaul
This commit is contained in:
183
resources/js/guest-v2/screens/SharedPhotoScreen.tsx
Normal file
183
resources/js/guest-v2/screens/SharedPhotoScreen.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
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 { fetchPhotoShare } from '@/guest/services/photosApi';
|
||||
import { useTranslation } from '@/guest/i18n/useTranslation';
|
||||
import { useAppearance } from '@/hooks/use-appearance';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export default function SharedPhotoScreen() {
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
const { t } = useTranslation();
|
||||
const { resolved } = useAppearance();
|
||||
const isDark = resolved === 'dark';
|
||||
const mutedText = isDark ? 'rgba(248, 250, 252, 0.7)' : 'rgba(15, 23, 42, 0.65)';
|
||||
const [state, setState] = React.useState<{ loading: boolean; error: string | null; data: ShareResponse | null }>({
|
||||
loading: true,
|
||||
error: null,
|
||||
data: null,
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
return (
|
||||
<StandaloneShell>
|
||||
<SurfaceCard glow>
|
||||
<Text fontSize="$2" letterSpacing={2} textTransform="uppercase" color={mutedText}>
|
||||
{t('share.title', 'Geteiltes Foto')}
|
||||
</Text>
|
||||
<Text fontSize="$6" fontWeight="$8" marginTop="$2">
|
||||
{data.event?.name ?? t('share.defaultEvent', 'Ein besonderer Moment')}
|
||||
</Text>
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
||||
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' });
|
||||
}
|
||||
Reference in New Issue
Block a user