Fix share assets, shared photo UI, and live show expiry
This commit is contained in:
@@ -43,7 +43,6 @@ vi.mock('lucide-react', () => ({
|
||||
ListVideo: () => <span>list</span>,
|
||||
RefreshCcw: () => <span>refresh</span>,
|
||||
FlipHorizontal: () => <span>flip</span>,
|
||||
X: () => <span>close</span>,
|
||||
Sparkles: () => <span>sparkles</span>,
|
||||
Trophy: () => <span>trophy</span>,
|
||||
Play: () => <span>play</span>,
|
||||
@@ -53,6 +52,8 @@ vi.mock('lucide-react', () => ({
|
||||
ChevronLeft: () => <span>chevron-left</span>,
|
||||
ChevronRight: () => <span>chevron-right</span>,
|
||||
QrCode: () => <span>qr</span>,
|
||||
Loader2: () => <span>loader</span>,
|
||||
Maximize2: () => <span>maximize</span>,
|
||||
Link: () => <span>link</span>,
|
||||
Users: () => <span>users</span>,
|
||||
Heart: () => <span>heart</span>,
|
||||
|
||||
@@ -47,24 +47,27 @@ export default function ShareSheet({
|
||||
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 inlineActive = variant === 'inline';
|
||||
const [inlineMounted, setInlineMounted] = React.useState(false);
|
||||
const [inlineVisible, setInlineVisible] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (variant !== 'inline') return;
|
||||
if (!inlineActive) {
|
||||
setInlineMounted(false);
|
||||
setInlineVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (open) {
|
||||
setInlineMounted(true);
|
||||
const raf = window.requestAnimationFrame(() => {
|
||||
setInlineVisible(true);
|
||||
});
|
||||
return () => window.cancelAnimationFrame(raf);
|
||||
const frame = window.requestAnimationFrame(() => setInlineVisible(true));
|
||||
return () => window.cancelAnimationFrame(frame);
|
||||
}
|
||||
|
||||
setInlineVisible(false);
|
||||
const timeout = window.setTimeout(() => {
|
||||
setInlineMounted(false);
|
||||
}, 220);
|
||||
return () => window.clearTimeout(timeout);
|
||||
}, [open, variant]);
|
||||
const timer = window.setTimeout(() => setInlineMounted(false), 260);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [inlineActive, open]);
|
||||
|
||||
const content = (
|
||||
<YStack gap="$3">
|
||||
@@ -184,16 +187,14 @@ export default function ShareSheet({
|
||||
</Button>
|
||||
</XStack>
|
||||
|
||||
{url ? (
|
||||
<Text fontSize="$1" color="$color" opacity={0.7} numberOfLines={1}>
|
||||
{url}
|
||||
</Text>
|
||||
) : null}
|
||||
<Text fontSize="$1" color="$color" opacity={url ? 0.7 : 0} numberOfLines={1} style={{ minHeight: 16 }}>
|
||||
{url ?? ' '}
|
||||
</Text>
|
||||
</YStack>
|
||||
);
|
||||
|
||||
if (variant === 'inline') {
|
||||
if (!inlineMounted) {
|
||||
if (inlineActive) {
|
||||
if (!inlineMounted && !open) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -203,16 +204,16 @@ export default function ShareSheet({
|
||||
inset={0}
|
||||
zIndex={20}
|
||||
justifyContent="flex-end"
|
||||
pointerEvents={open ? 'auto' : 'none'}
|
||||
pointerEvents={inlineVisible ? 'auto' : 'none'}
|
||||
>
|
||||
<Button
|
||||
unstyled
|
||||
onPress={() => onOpenChange(false)}
|
||||
opacity={inlineVisible ? 1 : 0}
|
||||
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')}
|
||||
@@ -222,9 +223,12 @@ export default function ShareSheet({
|
||||
backgroundColor="$surface"
|
||||
borderTopLeftRadius="$6"
|
||||
borderTopRightRadius="$6"
|
||||
opacity={inlineVisible ? 1 : 0}
|
||||
style={{
|
||||
transform: inlineVisible ? 'translateY(0)' : 'translateY(100%)',
|
||||
transition: 'transform 220ms cubic-bezier(0.22, 1, 0.36, 1)',
|
||||
backfaceVisibility: 'hidden',
|
||||
transform: inlineVisible ? 'translate3d(0, 0, 0)' : 'translate3d(0, 56px, 0)',
|
||||
transition: 'transform 260ms cubic-bezier(0.22, 1, 0.36, 1), opacity 220ms ease',
|
||||
willChange: 'transform, opacity',
|
||||
}}
|
||||
>
|
||||
<YStack
|
||||
|
||||
@@ -113,6 +113,8 @@ export default function GalleryScreen() {
|
||||
});
|
||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = React.useState(false);
|
||||
const [deleteBusy, setDeleteBusy] = React.useState(false);
|
||||
const [deleteConfirmMounted, setDeleteConfirmMounted] = React.useState(false);
|
||||
const [deleteConfirmVisible, setDeleteConfirmVisible] = React.useState(false);
|
||||
const [likedIds, setLikedIds] = React.useState<Set<number>>(new Set());
|
||||
const touchStartX = React.useRef<number | null>(null);
|
||||
const fallbackAttemptedRef = React.useRef(false);
|
||||
@@ -130,6 +132,26 @@ export default function GalleryScreen() {
|
||||
const [lightboxMounted, setLightboxMounted] = React.useState(false);
|
||||
const [lightboxVisible, setLightboxVisible] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (deleteConfirmOpen) {
|
||||
setDeleteConfirmVisible(false);
|
||||
setDeleteConfirmMounted(true);
|
||||
let frame1 = 0;
|
||||
let frame2 = 0;
|
||||
frame1 = window.requestAnimationFrame(() => {
|
||||
frame2 = window.requestAnimationFrame(() => setDeleteConfirmVisible(true));
|
||||
});
|
||||
return () => {
|
||||
window.cancelAnimationFrame(frame1);
|
||||
window.cancelAnimationFrame(frame2);
|
||||
};
|
||||
}
|
||||
|
||||
setDeleteConfirmVisible(false);
|
||||
const timer = window.setTimeout(() => setDeleteConfirmMounted(false), 220);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [deleteConfirmOpen]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!token) {
|
||||
setPhotos([]);
|
||||
@@ -1334,7 +1356,7 @@ export default function GalleryScreen() {
|
||||
</Button>
|
||||
</XStack>
|
||||
</XStack>
|
||||
{deleteConfirmOpen ? (
|
||||
{deleteConfirmMounted ? (
|
||||
<YStack
|
||||
position="absolute"
|
||||
top={0}
|
||||
@@ -1345,7 +1367,13 @@ export default function GalleryScreen() {
|
||||
justifyContent="center"
|
||||
padding="$4"
|
||||
backgroundColor={isDark ? 'rgba(2, 6, 23, 0.7)' : 'rgba(15, 23, 42, 0.4)'}
|
||||
style={{ backdropFilter: 'blur(6px)', zIndex: 6 }}
|
||||
opacity={deleteConfirmVisible ? 1 : 0}
|
||||
pointerEvents={deleteConfirmVisible ? 'auto' : 'none'}
|
||||
style={{
|
||||
backdropFilter: 'blur(6px)',
|
||||
zIndex: 6,
|
||||
transition: 'opacity 200ms ease',
|
||||
}}
|
||||
>
|
||||
<YStack
|
||||
width="100%"
|
||||
@@ -1356,7 +1384,15 @@ export default function GalleryScreen() {
|
||||
backgroundColor="$surface"
|
||||
borderWidth={1}
|
||||
borderColor={mutedButtonBorder}
|
||||
style={{ boxShadow: cardShadow }}
|
||||
opacity={deleteConfirmVisible ? 1 : 0}
|
||||
style={{
|
||||
boxShadow: cardShadow,
|
||||
transform: deleteConfirmVisible
|
||||
? 'translate3d(0, 0, 0) scale(1)'
|
||||
: 'translate3d(0, 12px, 0) scale(0.98)',
|
||||
transition: 'transform 220ms cubic-bezier(0.22, 1, 0.36, 1), opacity 200ms ease',
|
||||
willChange: 'transform, opacity',
|
||||
}}
|
||||
>
|
||||
<YStack gap="$1">
|
||||
<Text fontSize="$4" fontWeight="$7">
|
||||
|
||||
@@ -3,7 +3,7 @@ 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 { AlertCircle, Download, Maximize2, X } from 'lucide-react';
|
||||
import StandaloneShell from '../components/StandaloneShell';
|
||||
import SurfaceCard from '../components/SurfaceCard';
|
||||
import EventLogo from '../components/EventLogo';
|
||||
@@ -14,6 +14,7 @@ import { EventBrandingProvider } from '@/guest/context/EventBrandingContext';
|
||||
import { mapEventBranding } from '../lib/eventBranding';
|
||||
import { BrandingTheme } from '../lib/brandingTheme';
|
||||
import { useGuestThemeVariant } from '../lib/guestTheme';
|
||||
import { getBentoSurfaceTokens } from '../lib/bento';
|
||||
|
||||
interface ShareResponse {
|
||||
slug: string;
|
||||
@@ -38,6 +39,7 @@ export default function SharedPhotoScreen() {
|
||||
error: null,
|
||||
data: null,
|
||||
});
|
||||
const [fullScreenOpen, setFullScreenOpen] = React.useState(false);
|
||||
const branding = React.useMemo(() => {
|
||||
if (!state.data?.branding) {
|
||||
return null;
|
||||
@@ -46,6 +48,7 @@ export default function SharedPhotoScreen() {
|
||||
}, [state.data]);
|
||||
const { isDark } = useGuestThemeVariant(branding);
|
||||
const mutedText = isDark ? 'rgba(248, 250, 252, 0.7)' : 'rgba(15, 23, 42, 0.65)';
|
||||
const bento = getBentoSurfaceTokens(isDark);
|
||||
|
||||
React.useEffect(() => {
|
||||
let active = true;
|
||||
@@ -105,66 +108,182 @@ export default function SharedPhotoScreen() {
|
||||
const chips = buildChips(data, t);
|
||||
|
||||
const content = (
|
||||
<StandaloneShell>
|
||||
<SurfaceCard glow>
|
||||
<XStack alignItems="center" gap="$3">
|
||||
<EventLogo name={data.event?.name ?? t('share.defaultEvent', 'Ein besonderer Moment')} size="s" />
|
||||
<YStack gap="$1">
|
||||
<Text fontSize="$2" letterSpacing={2} textTransform="uppercase" color={mutedText}>
|
||||
{t('share.title', 'Geteiltes Foto')}
|
||||
</Text>
|
||||
<Text fontSize="$6" fontWeight="$8">
|
||||
{data.event?.name ?? t('share.defaultEvent', 'Ein besonderer Moment')}
|
||||
</Text>
|
||||
</YStack>
|
||||
</XStack>
|
||||
{data.photo.title ? (
|
||||
<Text fontSize="$3" color={mutedText} marginTop="$1">
|
||||
{data.photo.title}
|
||||
</Text>
|
||||
) : null}
|
||||
</SurfaceCard>
|
||||
|
||||
<SurfaceCard padding={0} overflow="hidden">
|
||||
<StandaloneShell compact>
|
||||
<YStack gap="$3" style={{ paddingBottom: 'calc(var(--space-6) + 50px)' }}>
|
||||
<YStack
|
||||
height={360}
|
||||
padding="$4"
|
||||
borderRadius="$card"
|
||||
borderWidth={1}
|
||||
borderBottomWidth={2}
|
||||
borderColor={bento.borderColor}
|
||||
borderBottomColor={bento.borderBottomColor}
|
||||
backgroundColor={bento.backgroundColor}
|
||||
style={{ boxShadow: bento.shadow }}
|
||||
>
|
||||
<XStack alignItems="center" gap="$3" flexWrap="wrap">
|
||||
<EventLogo name={data.event?.name ?? t('share.defaultEvent', 'Ein besonderer Moment')} size="s" />
|
||||
<YStack gap="$1" flex={1} minWidth={180}>
|
||||
<Text fontSize="$2" letterSpacing={2} textTransform="uppercase" color={mutedText}>
|
||||
{t('share.title', 'Geteiltes Foto')}
|
||||
</Text>
|
||||
<Text fontSize="$6" fontWeight="$8">
|
||||
{data.event?.name ?? t('share.defaultEvent', 'Ein besonderer Moment')}
|
||||
</Text>
|
||||
</YStack>
|
||||
</XStack>
|
||||
{data.photo.title ? (
|
||||
<Text fontSize="$3" color={mutedText} marginTop="$2" numberOfLines={2}>
|
||||
{data.photo.title}
|
||||
</Text>
|
||||
) : null}
|
||||
</YStack>
|
||||
|
||||
<YStack
|
||||
padding="$2"
|
||||
borderRadius="$card"
|
||||
borderWidth={1}
|
||||
borderBottomWidth={2}
|
||||
borderColor={bento.borderColor}
|
||||
borderBottomColor={bento.borderBottomColor}
|
||||
backgroundColor={bento.backgroundColor}
|
||||
style={{ boxShadow: bento.shadow }}
|
||||
>
|
||||
<YStack
|
||||
borderRadius="$card"
|
||||
overflow="hidden"
|
||||
position="relative"
|
||||
style={{
|
||||
height: 'min(45vh, 320px)',
|
||||
backgroundColor: isDark ? 'rgba(15, 23, 42, 0.4)' : 'rgba(15, 23, 42, 0.06)',
|
||||
backgroundImage: `url(${data.photo.image_urls.full})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
size="$3"
|
||||
circular
|
||||
position="absolute"
|
||||
top="$2"
|
||||
right="$2"
|
||||
backgroundColor={isDark ? 'rgba(15, 23, 42, 0.6)' : 'rgba(255, 255, 255, 0.85)'}
|
||||
borderWidth={1}
|
||||
borderColor={bento.borderColor}
|
||||
onPress={() => setFullScreenOpen(true)}
|
||||
aria-label={t('share.fullscreen', 'Fullscreen')}
|
||||
>
|
||||
<Maximize2 size={16} color={isDark ? '#F8FAFF' : '#0F172A'} />
|
||||
</Button>
|
||||
</YStack>
|
||||
</YStack>
|
||||
|
||||
{chips.length > 0 ? (
|
||||
<XStack gap="$2" flexWrap="wrap" justifyContent="center">
|
||||
{chips.map((chip) => (
|
||||
<SurfaceCard key={chip.id} padding="$2" borderRadius="$pill" minWidth={120}>
|
||||
<XStack alignItems="center" gap="$2" flexWrap="wrap">
|
||||
{chip.icon ? <Text fontSize="$3">{chip.icon}</Text> : null}
|
||||
<Text fontSize="$2" color={mutedText}>
|
||||
{chip.label}
|
||||
</Text>
|
||||
<Text fontSize="$2" fontWeight="$7" numberOfLines={2} style={{ maxWidth: 220 }}>
|
||||
{chip.value}
|
||||
</Text>
|
||||
</XStack>
|
||||
</SurfaceCard>
|
||||
))}
|
||||
</XStack>
|
||||
) : null}
|
||||
|
||||
<YStack style={{ position: 'sticky', bottom: 12 }}>
|
||||
<Button
|
||||
size="$4"
|
||||
borderRadius="$pill"
|
||||
backgroundColor="$primary"
|
||||
onPress={() => window.open(data.photo.image_urls.full, '_blank')}
|
||||
justifyContent="center"
|
||||
>
|
||||
<XStack alignItems="center" justifyContent="center" gap="$2" width="100%">
|
||||
<Download size={18} color="white" />
|
||||
<Text fontSize="$3" fontWeight="$7" color="white">
|
||||
{t('galleryPublic.download', 'Download')}
|
||||
</Text>
|
||||
</XStack>
|
||||
</Button>
|
||||
</YStack>
|
||||
</YStack>
|
||||
{fullScreenOpen ? (
|
||||
<YStack
|
||||
position="fixed"
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
zIndex={3000}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
style={{
|
||||
backgroundImage: `url(${data.photo.image_urls.full})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
backgroundColor: isDark ? 'rgba(2, 6, 23, 0.8)' : 'rgba(15, 23, 42, 0.45)',
|
||||
backdropFilter: 'blur(14px)',
|
||||
}}
|
||||
/>
|
||||
</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>
|
||||
>
|
||||
<Button
|
||||
unstyled
|
||||
onPress={() => setFullScreenOpen(false)}
|
||||
style={{ position: 'absolute', inset: 0 }}
|
||||
aria-label={t('common.actions.close', 'Close')}
|
||||
/>
|
||||
<YStack
|
||||
width="92vw"
|
||||
height="82vh"
|
||||
maxWidth={920}
|
||||
maxHeight={720}
|
||||
borderRadius="$bentoLg"
|
||||
overflow="hidden"
|
||||
borderWidth={1}
|
||||
borderColor={bento.borderColor}
|
||||
style={{ position: 'relative', boxShadow: bento.shadow }}
|
||||
>
|
||||
<YStack
|
||||
position="absolute"
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
style={{
|
||||
backgroundImage: `url(${data.photo.image_urls.full})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
filter: 'blur(26px)',
|
||||
transform: 'scale(1.08)',
|
||||
opacity: 0.6,
|
||||
}}
|
||||
/>
|
||||
<YStack flex={1} alignItems="center" justifyContent="center" padding="$4" style={{ zIndex: 1 }}>
|
||||
<img
|
||||
src={data.photo.image_urls.full}
|
||||
alt={t('galleryPage.photo.alt', { id: data.photo.id, suffix: '' }, `Foto ${data.photo.id}`)}
|
||||
style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' }}
|
||||
/>
|
||||
</YStack>
|
||||
<Button
|
||||
size="$3"
|
||||
circular
|
||||
position="absolute"
|
||||
top="$3"
|
||||
right="$3"
|
||||
backgroundColor={isDark ? 'rgba(15, 23, 42, 0.6)' : 'rgba(255, 255, 255, 0.85)'}
|
||||
borderWidth={1}
|
||||
borderColor={bento.borderColor}
|
||||
onPress={() => setFullScreenOpen(false)}
|
||||
aria-label={t('common.actions.close', 'Close')}
|
||||
style={{ zIndex: 2 }}
|
||||
>
|
||||
<X size={16} color={isDark ? '#F8FAFF' : '#0F172A'} />
|
||||
</Button>
|
||||
</YStack>
|
||||
</YStack>
|
||||
) : 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>
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user