Update guest v2 branding and theming

This commit is contained in:
Codex Agent
2026-02-03 15:18:44 +01:00
parent a0ef90e13a
commit a820ef2e8b
57 changed files with 1416 additions and 277 deletions

View File

@@ -8,12 +8,14 @@ import { useGesture } from '@use-gesture/react';
import { animated, to, useSpring } from '@react-spring/web';
import AppShell from '../components/AppShell';
import SurfaceCard from '../components/SurfaceCard';
import ShareSheet from '../components/ShareSheet';
import { useEventData } from '../context/EventDataContext';
import { fetchGallery, fetchPhoto, likePhoto, createPhotoShareLink } from '../services/photosApi';
import { useTranslation } from '@/guest/i18n/useTranslation';
import { useLocale } from '@/guest/i18n/LocaleContext';
import { useAppearance } from '@/hooks/use-appearance';
import { useGuestThemeVariant } from '../lib/guestTheme';
import { buildEventPath } from '../lib/routes';
import { pushGuestToast } from '../lib/toast';
type LightboxPhoto = {
id: number;
@@ -62,13 +64,12 @@ function mapPhoto(photo: Record<string, unknown>): LightboxPhoto | null {
}
export default function PhotoLightboxScreen() {
const { token } = useEventData();
const { token, event } = useEventData();
const { photoId } = useParams<{ photoId: string }>();
const navigate = useNavigate();
const { t } = useTranslation();
const { locale } = useLocale();
const { resolved } = useAppearance();
const isDark = resolved === 'dark';
const { isDark } = useGuestThemeVariant();
const cardBorder = isDark ? 'rgba(255, 255, 255, 0.12)' : 'rgba(15, 23, 42, 0.12)';
const mutedButton = isDark ? 'rgba(255, 255, 255, 0.08)' : 'rgba(15, 23, 42, 0.06)';
const mutedButtonBorder = isDark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(15, 23, 42, 0.12)';
@@ -80,7 +81,10 @@ export default function PhotoLightboxScreen() {
const [loadingMore, setLoadingMore] = React.useState(false);
const [error, setError] = React.useState<string | null>(null);
const [likes, setLikes] = React.useState<Record<number, number>>({});
const [shareStatus, setShareStatus] = React.useState<'idle' | 'loading' | 'copied' | 'failed'>('idle');
const [shareSheet, setShareSheet] = React.useState<{ url: string | null; loading: boolean }>({
url: null,
loading: false,
});
const zoomContainerRef = React.useRef<HTMLDivElement | null>(null);
const zoomImageRef = React.useRef<HTMLImageElement | null>(null);
const baseSizeRef = React.useRef({ width: 0, height: 0 });
@@ -289,36 +293,82 @@ export default function PhotoLightboxScreen() {
} catch (error) {
console.error('Like failed', error);
}
}, [selected, token]);
}, [selected, t, token]);
const handleShare = React.useCallback(async () => {
const shareTitle = event?.name ?? t('share.title', 'Shared photo');
const shareText = t('share.shareText', 'Check out this moment on Fotospiel.');
const openShareSheet = React.useCallback(async () => {
if (!selected || !token) return;
setShareStatus('loading');
setShareSheet({ url: null, loading: true });
try {
const payload = await createPhotoShareLink(token, selected.id);
const url = payload?.url ?? '';
if (!url) {
throw new Error('missing share url');
const url = payload?.url ?? null;
setShareSheet({ url, loading: false });
} catch (error) {
console.error('Share failed', error);
pushGuestToast({ text: t('share.error', 'Share failed'), type: 'error' });
setShareSheet({ url: null, loading: false });
}
}, [selected, token]);
const closeShareSheet = React.useCallback(() => {
setShareSheet({ url: null, loading: false });
}, []);
const shareWhatsApp = React.useCallback(
(url?: string | null) => {
if (!url) return;
const waUrl = `https://wa.me/?text=${encodeURIComponent(`${shareText} ${url}`)}`;
window.open(waUrl, '_blank', 'noopener');
closeShareSheet();
},
[closeShareSheet, shareText]
);
const shareMessages = React.useCallback(
(url?: string | null) => {
if (!url) return;
const smsUrl = `sms:?&body=${encodeURIComponent(`${shareText} ${url}`)}`;
window.open(smsUrl, '_blank', 'noopener');
closeShareSheet();
},
[closeShareSheet, shareText]
);
const copyLink = React.useCallback(
async (url?: string | null) => {
if (!url) return;
try {
await navigator.clipboard?.writeText(url);
pushGuestToast({ text: t('share.copySuccess', 'Link copied!') });
} catch (error) {
console.error('Copy failed', error);
pushGuestToast({ text: t('share.copyError', 'Link could not be copied.'), type: 'error' });
} finally {
closeShareSheet();
}
},
[closeShareSheet, t]
);
const shareNative = React.useCallback(
(url?: string | null) => {
if (!url) return;
const data: ShareData = {
title: t('share.defaultEvent', 'A special moment'),
text: t('share.shareText', 'Check out this moment on Fotospiel.'),
title: shareTitle,
text: shareText,
url,
};
if (navigator.share && (!navigator.canShare || navigator.canShare(data))) {
await navigator.share(data);
setShareStatus('idle');
navigator.share(data).catch(() => undefined);
closeShareSheet();
return;
}
await navigator.clipboard?.writeText(url);
setShareStatus('copied');
} catch (error) {
console.error('Share failed', error);
setShareStatus('failed');
} finally {
window.setTimeout(() => setShareStatus('idle'), 2000);
}
}, [selected, t, token]);
void copyLink(url);
},
[closeShareSheet, copyLink, shareText, shareTitle]
);
const bind = useGesture(
{
@@ -557,20 +607,14 @@ export default function PhotoLightboxScreen() {
</Button>
<Button
unstyled
onPress={handleShare}
onPress={openShareSheet}
paddingHorizontal="$3"
paddingVertical="$2"
>
<XStack alignItems="center" gap="$2">
<Share2 size={16} color={isDark ? '#F8FAFF' : '#0F172A'} />
<Text fontSize="$2" fontWeight="$6">
{shareStatus === 'loading'
? t('share.loading', 'Sharing...')
: shareStatus === 'copied'
? t('share.copySuccess', 'Copied')
: shareStatus === 'failed'
? t('share.copyError', 'Copy failed')
: t('share.button', 'Share')}
{shareSheet.loading ? t('share.loading', 'Sharing...') : t('share.button', 'Share')}
</Text>
</XStack>
</Button>
@@ -584,6 +628,22 @@ export default function PhotoLightboxScreen() {
)}
</SurfaceCard>
</YStack>
<ShareSheet
open={shareSheet.loading || Boolean(shareSheet.url)}
onOpenChange={(open) => {
if (!open) {
closeShareSheet();
}
}}
photoId={selected?.id}
eventName={event?.name ?? null}
url={shareSheet.url}
loading={shareSheet.loading}
onShareNative={() => shareNative(shareSheet.url)}
onShareWhatsApp={() => shareWhatsApp(shareSheet.url)}
onShareMessages={() => shareMessages(shareSheet.url)}
onCopyLink={() => copyLink(shareSheet.url)}
/>
</AppShell>
);
}