265 lines
9.5 KiB
TypeScript
265 lines
9.5 KiB
TypeScript
import React from 'react';
|
|
import { YStack, XStack } from '@tamagui/stacks';
|
|
import { SizableText as Text } from '@tamagui/text';
|
|
import { Button } from '@tamagui/button';
|
|
import { Share2, QrCode, Link, Users, Loader2, RefreshCcw } from 'lucide-react';
|
|
import AppShell from '../components/AppShell';
|
|
import { useEventData } from '../context/EventDataContext';
|
|
import { buildEventShareLink } from '../services/eventLink';
|
|
import { usePollStats } from '../hooks/usePollStats';
|
|
import { fetchEventQrCode } from '../services/qrApi';
|
|
import { useGuestThemeVariant } from '../lib/guestTheme';
|
|
import { useTranslation } from '@/guest/i18n/useTranslation';
|
|
import { getBentoSurfaceTokens } from '../lib/bento';
|
|
import { pushGuestToast } from '../lib/toast';
|
|
|
|
export default function ShareScreen() {
|
|
const { event, token } = useEventData();
|
|
const { t } = useTranslation();
|
|
const { isDark } = useGuestThemeVariant();
|
|
const surface = getBentoSurfaceTokens(isDark);
|
|
const [copyState, setCopyState] = React.useState<'idle' | 'copied' | 'failed'>('idle');
|
|
const { stats } = usePollStats(token ?? null);
|
|
const [qrCodeDataUrl, setQrCodeDataUrl] = React.useState('');
|
|
const [qrLoading, setQrLoading] = React.useState(false);
|
|
const [qrError, setQrError] = React.useState(false);
|
|
const shareUrl = buildEventShareLink(event, token);
|
|
|
|
const loadQrCode = React.useCallback(async () => {
|
|
if (!token) return;
|
|
setQrLoading(true);
|
|
setQrError(false);
|
|
try {
|
|
const payload = await fetchEventQrCode(token, 240);
|
|
setQrCodeDataUrl(payload.qr_code_data_url ?? '');
|
|
} catch (error) {
|
|
console.error('Failed to load QR code', error);
|
|
setQrCodeDataUrl('');
|
|
setQrError(true);
|
|
} finally {
|
|
setQrLoading(false);
|
|
}
|
|
}, [token]);
|
|
|
|
const handleCopy = React.useCallback(async () => {
|
|
if (!shareUrl) {
|
|
return;
|
|
}
|
|
try {
|
|
await navigator.clipboard?.writeText(shareUrl);
|
|
setCopyState('copied');
|
|
pushGuestToast({ text: t('share.copySuccess', 'Link copied!'), type: 'success' });
|
|
} catch (error) {
|
|
console.error('Copy failed', error);
|
|
setCopyState('failed');
|
|
pushGuestToast({ text: t('share.copyError', 'Link could not be copied.'), type: 'error' });
|
|
} finally {
|
|
window.setTimeout(() => setCopyState('idle'), 2000);
|
|
}
|
|
}, [shareUrl, t]);
|
|
|
|
const handleShare = React.useCallback(async () => {
|
|
if (!shareUrl) return;
|
|
const title = event?.name ?? t('share.defaultEvent', 'Fotospiel');
|
|
const data: ShareData = { title, text: title, url: shareUrl };
|
|
if (navigator.share && (!navigator.canShare || navigator.canShare(data))) {
|
|
try {
|
|
await navigator.share(data);
|
|
} catch (error) {
|
|
// user dismissed
|
|
}
|
|
} else {
|
|
await handleCopy();
|
|
}
|
|
}, [event?.name, handleCopy, shareUrl]);
|
|
|
|
React.useEffect(() => {
|
|
if (!token) {
|
|
setQrCodeDataUrl('');
|
|
setQrLoading(false);
|
|
setQrError(false);
|
|
return;
|
|
}
|
|
loadQrCode();
|
|
}, [loadQrCode, token]);
|
|
|
|
const guestCountLabel = (stats.onlineGuests ?? 0).toString();
|
|
const inviteDisabled = !shareUrl;
|
|
|
|
return (
|
|
<AppShell>
|
|
<YStack gap="$4" paddingBottom={80}>
|
|
<YStack
|
|
padding="$4"
|
|
borderRadius="$card"
|
|
backgroundColor={surface.backgroundColor}
|
|
borderWidth={1}
|
|
borderBottomWidth={3}
|
|
borderColor={surface.borderColor}
|
|
borderBottomColor={surface.borderBottomColor}
|
|
gap="$2"
|
|
style={{
|
|
boxShadow: surface.shadow,
|
|
}}
|
|
>
|
|
<XStack alignItems="center" gap="$2">
|
|
<Share2 size={18} color={isDark ? '#F8FAFF' : '#0F172A'} />
|
|
<Text fontSize="$4" fontWeight="$7">
|
|
{t('share.invite.title', 'Invite guests')}
|
|
</Text>
|
|
</XStack>
|
|
<Text fontSize="$2" color="$color" opacity={0.7}>
|
|
{t('share.invite.description', 'Share the event link or show the QR code to join.')}
|
|
</Text>
|
|
</YStack>
|
|
|
|
<XStack gap="$3" flexWrap="wrap">
|
|
<YStack
|
|
flex={1}
|
|
height={180}
|
|
minWidth={220}
|
|
borderRadius="$card"
|
|
backgroundColor={surface.backgroundColor}
|
|
borderWidth={1}
|
|
borderBottomWidth={3}
|
|
borderColor={surface.borderColor}
|
|
borderBottomColor={surface.borderBottomColor}
|
|
alignItems="center"
|
|
justifyContent="center"
|
|
gap="$2"
|
|
style={{
|
|
boxShadow: surface.shadow,
|
|
backgroundImage: isDark
|
|
? 'radial-gradient(circle at 30% 30%, rgba(255, 79, 216, 0.2), transparent 55%)'
|
|
: 'radial-gradient(circle at 30% 30%, color-mix(in oklab, var(--guest-primary, #FF5A5F) 18%, white), transparent 60%)',
|
|
}}
|
|
>
|
|
{qrCodeDataUrl ? (
|
|
<img
|
|
src={qrCodeDataUrl}
|
|
alt={t('share.invite.qrAlt', 'Event QR code')}
|
|
style={{ width: 120, height: 120, borderRadius: 16 }}
|
|
/>
|
|
) : qrLoading ? (
|
|
<Loader2 size={22} className="animate-spin" color={isDark ? '#F8FAFF' : '#0F172A'} />
|
|
) : (
|
|
<QrCode size={28} color={isDark ? '#F8FAFF' : '#0F172A'} />
|
|
)}
|
|
<Text fontSize="$3" fontWeight="$7">
|
|
{t('share.invite.qrLabel', 'Show QR')}
|
|
</Text>
|
|
{qrLoading ? (
|
|
<Text fontSize="$1" color="$color" opacity={0.7}>
|
|
{t('share.invite.qrLoading', 'Generating QR…')}
|
|
</Text>
|
|
) : null}
|
|
{qrError ? (
|
|
<Button
|
|
size="$2"
|
|
borderRadius="$pill"
|
|
backgroundColor={isDark ? 'rgba(255, 255, 255, 0.12)' : 'rgba(15, 23, 42, 0.08)'}
|
|
borderWidth={1}
|
|
borderColor={surface.borderColor}
|
|
onPress={loadQrCode}
|
|
>
|
|
<XStack alignItems="center" gap="$2">
|
|
<RefreshCcw size={14} color={isDark ? '#F8FAFF' : '#0F172A'} />
|
|
<Text fontSize="$1" fontWeight="$6">
|
|
{t('share.invite.qrRetry', 'Retry')}
|
|
</Text>
|
|
</XStack>
|
|
</Button>
|
|
) : null}
|
|
</YStack>
|
|
<YStack
|
|
flex={1}
|
|
height={180}
|
|
minWidth={220}
|
|
borderRadius="$card"
|
|
backgroundColor={surface.backgroundColor}
|
|
borderWidth={1}
|
|
borderBottomWidth={3}
|
|
borderColor={surface.borderColor}
|
|
borderBottomColor={surface.borderBottomColor}
|
|
alignItems="center"
|
|
justifyContent="center"
|
|
gap="$2"
|
|
style={{
|
|
boxShadow: surface.shadow,
|
|
}}
|
|
>
|
|
<Link size={24} color={isDark ? '#F8FAFF' : '#0F172A'} />
|
|
<Text fontSize="$3" fontWeight="$7">
|
|
{copyState === 'copied'
|
|
? t('share.copySuccess', 'Copied')
|
|
: copyState === 'failed'
|
|
? t('share.copyError', 'Copy failed')
|
|
: t('share.invite.copyLabel', 'Copy link')}
|
|
</Text>
|
|
{shareUrl ? (
|
|
<Text
|
|
fontSize="$1"
|
|
color="$color"
|
|
opacity={0.7}
|
|
style={{ wordBreak: 'break-all', overflowWrap: 'anywhere' }}
|
|
>
|
|
{shareUrl}
|
|
</Text>
|
|
) : null}
|
|
<Button size="$2" backgroundColor="$primary" borderRadius="$pill" onPress={handleCopy} disabled={!shareUrl}>
|
|
{t('share.copyLink', 'Copy link')}
|
|
</Button>
|
|
</YStack>
|
|
</XStack>
|
|
|
|
<YStack
|
|
padding="$4"
|
|
borderRadius="$card"
|
|
backgroundColor={surface.backgroundColor}
|
|
borderWidth={1}
|
|
borderBottomWidth={3}
|
|
borderColor={surface.borderColor}
|
|
borderBottomColor={surface.borderBottomColor}
|
|
gap="$3"
|
|
style={{
|
|
backgroundImage: isDark
|
|
? 'linear-gradient(135deg, rgba(79, 209, 255, 0.12), rgba(255, 79, 216, 0.18))'
|
|
: 'linear-gradient(135deg, color-mix(in oklab, var(--guest-secondary, #F43F5E) 8%, white), color-mix(in oklab, var(--guest-primary, #FF5A5F) 12%, white))',
|
|
boxShadow: surface.shadow,
|
|
}}
|
|
>
|
|
<XStack alignItems="center" gap="$2">
|
|
<Users size={18} color={isDark ? '#F8FAFF' : '#0F172A'} />
|
|
<Text fontSize="$4" fontWeight="$7">
|
|
{t('share.invite.guestsTitle', 'Guests joined')}
|
|
</Text>
|
|
</XStack>
|
|
<YStack gap="$1">
|
|
<Text fontSize="$6" fontWeight="$8">
|
|
{guestCountLabel}
|
|
</Text>
|
|
<Text fontSize="$2" color="$color" opacity={0.7}>
|
|
{t('home.stats.online', 'Guests online')}
|
|
</Text>
|
|
</YStack>
|
|
<Text fontSize="$2" color="$color" opacity={0.7}>
|
|
{event?.name
|
|
? t('share.invite.guestsSubtitleEvent', { event: event.name }, 'Share {event} with your guests.')
|
|
: t('share.invite.guestsSubtitle', 'Share the event with your guests.')}
|
|
</Text>
|
|
<Button
|
|
size="$3"
|
|
backgroundColor="$primary"
|
|
borderRadius="$pill"
|
|
alignSelf="flex-start"
|
|
onPress={handleShare}
|
|
disabled={inviteDisabled}
|
|
>
|
|
{t('share.invite.send', 'Send invite')}
|
|
</Button>
|
|
</YStack>
|
|
</YStack>
|
|
</AppShell>
|
|
);
|
|
}
|