Update guest v2 branding and theming
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user