Files
fotospiel-app/resources/js/guest-v2/components/ShareSheet.tsx
Codex Agent c6aaf859f5
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled
Add emotion data and lightbox share/download
2026-02-05 20:35:11 +01:00

260 lines
8.7 KiB
TypeScript

import React from 'react';
import { Sheet } from '@tamagui/sheet';
import { YStack, XStack } from '@tamagui/stacks';
import { SizableText as Text } from '@tamagui/text';
import { Button } from '@tamagui/button';
import { Share2, MessageSquare, Copy, X } from 'lucide-react';
import { useTranslation } from '@/guest/i18n/useTranslation';
import { useGuestThemeVariant } from '../lib/guestTheme';
type ShareSheetProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
photoId?: number | null;
eventName?: string | null;
url?: string | null;
loading?: boolean;
variant?: 'modal' | 'inline';
onShareNative: () => void;
onShareWhatsApp: () => void;
onShareMessages: () => void;
onCopyLink: () => void;
};
const WhatsAppIcon = (props: React.SVGProps<SVGSVGElement>) => (
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden focusable="false" {...props}>
<path
fill="currentColor"
d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413Z"
/>
</svg>
);
export default function ShareSheet({
open,
onOpenChange,
photoId,
eventName,
url,
loading = false,
variant = 'modal',
onShareNative,
onShareWhatsApp,
onShareMessages,
onCopyLink,
}: ShareSheetProps) {
const { t } = useTranslation();
const { isDark } = useGuestThemeVariant();
const mutedSurface = isDark ? 'rgba(255, 255, 255, 0.08)' : 'rgba(15, 23, 42, 0.06)';
const mutedBorder = isDark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(15, 23, 42, 0.12)';
const [inlineMounted, setInlineMounted] = React.useState(false);
const [inlineVisible, setInlineVisible] = React.useState(false);
React.useEffect(() => {
if (variant !== 'inline') return;
if (open) {
setInlineMounted(true);
const raf = window.requestAnimationFrame(() => {
setInlineVisible(true);
});
return () => window.cancelAnimationFrame(raf);
}
setInlineVisible(false);
const timeout = window.setTimeout(() => {
setInlineMounted(false);
}, 220);
return () => window.clearTimeout(timeout);
}, [open, variant]);
const content = (
<YStack gap="$3">
<XStack alignItems="center" justifyContent="space-between">
<YStack gap="$1">
<Text fontSize="$2" color="$color" opacity={0.7} textTransform="uppercase" letterSpacing={1.2}>
{t('share.title', 'Shared photo')}
</Text>
{photoId ? (
<Text fontSize="$5" fontWeight="$8">
#{photoId}
</Text>
) : null}
{eventName ? (
<Text fontSize="$2" color="$color" opacity={0.7}>
{eventName}
</Text>
) : null}
</YStack>
<Button
size="$3"
circular
backgroundColor={mutedSurface}
borderWidth={1}
borderColor={mutedBorder}
onPress={() => onOpenChange(false)}
aria-label={t('common.actions.close', 'Close')}
>
<X size={16} color={isDark ? '#F8FAFF' : '#0F172A'} />
</Button>
</XStack>
<XStack gap="$2" flexWrap="wrap">
<Button
flex={1}
minWidth="45%"
borderRadius="$card"
backgroundColor={mutedSurface}
borderWidth={1}
borderColor={mutedBorder}
onPress={onShareNative}
disabled={loading}
>
<XStack alignItems="center" gap="$2">
<Share2 size={16} color={isDark ? '#F8FAFF' : '#0F172A'} />
<YStack>
<Text fontSize="$2" fontWeight="$7">
{t('share.button', 'Share')}
</Text>
<Text fontSize="$1" color="$color" opacity={0.7}>
{t('share.title', 'Shared photo')}
</Text>
</YStack>
</XStack>
</Button>
<Button
flex={1}
minWidth="45%"
borderRadius="$card"
backgroundColor="#22C55E"
onPress={onShareWhatsApp}
disabled={loading}
>
<XStack alignItems="center" gap="$2">
<WhatsAppIcon width={18} height={18} />
<YStack>
<Text fontSize="$2" fontWeight="$7" color="#FFFFFF">
{t('share.whatsapp', 'WhatsApp')}
</Text>
<Text fontSize="$1" color="rgba(255,255,255,0.8)">
{loading ? '...' : ''}
</Text>
</YStack>
</XStack>
</Button>
<Button
flex={1}
minWidth="45%"
borderRadius="$card"
backgroundColor="#38BDF8"
onPress={onShareMessages}
disabled={loading}
>
<XStack alignItems="center" gap="$2">
<MessageSquare size={16} color="#FFFFFF" />
<YStack>
<Text fontSize="$2" fontWeight="$7" color="#FFFFFF">
{t('share.imessage', 'Messages')}
</Text>
<Text fontSize="$1" color="rgba(255,255,255,0.8)">
{loading ? '...' : ''}
</Text>
</YStack>
</XStack>
</Button>
<Button
flex={1}
minWidth="45%"
borderRadius="$card"
backgroundColor={mutedSurface}
borderWidth={1}
borderColor={mutedBorder}
onPress={onCopyLink}
disabled={loading}
>
<XStack alignItems="center" gap="$2">
<Copy size={16} color={isDark ? '#F8FAFF' : '#0F172A'} />
<YStack>
<Text fontSize="$2" fontWeight="$7">
{t('share.copyLink', 'Copy link')}
</Text>
<Text fontSize="$1" color="$color" opacity={0.7}>
{loading ? t('share.loading', 'Loading...') : ''}
</Text>
</YStack>
</XStack>
</Button>
</XStack>
{url ? (
<Text fontSize="$1" color="$color" opacity={0.7} numberOfLines={1}>
{url}
</Text>
) : null}
</YStack>
);
if (variant === 'inline') {
if (!inlineMounted) {
return null;
}
return (
<YStack
position="absolute"
inset={0}
zIndex={20}
justifyContent="flex-end"
pointerEvents={open ? 'auto' : 'none'}
>
<Button
unstyled
onPress={() => onOpenChange(false)}
style={{
position: 'absolute',
inset: 0,
backgroundColor: isDark ? 'rgba(15, 23, 42, 0.5)' : 'rgba(15, 23, 42, 0.35)',
opacity: inlineVisible ? 1 : 0,
transition: 'opacity 200ms ease',
}}
aria-label={t('common.actions.close', 'Close')}
/>
<YStack
padding="$4"
backgroundColor="$surface"
borderTopLeftRadius="$6"
borderTopRightRadius="$6"
style={{
transform: inlineVisible ? 'translateY(0)' : 'translateY(100%)',
transition: 'transform 220ms cubic-bezier(0.22, 1, 0.36, 1)',
}}
>
<YStack
height={5}
width={52}
backgroundColor="#CBD5E1"
borderRadius={999}
alignSelf="center"
marginBottom="$3"
/>
{content}
</YStack>
</YStack>
);
}
return (
<Sheet
open={open}
onOpenChange={onOpenChange}
snapPoints={[60]}
position={open ? 0 : -1}
modal
>
<Sheet.Overlay {...({ backgroundColor: isDark ? 'rgba(15, 23, 42, 0.6)' : 'rgba(15, 23, 42, 0.3)' } as any)} />
<Sheet.Frame padding="$4" backgroundColor="$surface" borderTopLeftRadius="$6" borderTopRightRadius="$6">
<Sheet.Handle height={5} width={52} backgroundColor="#CBD5E1" borderRadius={999} marginBottom="$3" />
{content}
</Sheet.Frame>
</Sheet>
);
}