Update guest v2 branding and theming
This commit is contained in:
@@ -8,15 +8,14 @@ import { useOptionalGuestIdentity } from '../context/GuestIdentityContext';
|
||||
import { fetchAchievements, type AchievementsPayload } from '../services/achievementsApi';
|
||||
import { useTranslation } from '@/guest/i18n/useTranslation';
|
||||
import { useLocale } from '@/guest/i18n/LocaleContext';
|
||||
import { useAppearance } from '@/hooks/use-appearance';
|
||||
import { useGuestThemeVariant } from '../lib/guestTheme';
|
||||
|
||||
export default function AchievementsScreen() {
|
||||
const { token } = useEventData();
|
||||
const identity = useOptionalGuestIdentity();
|
||||
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 cardShadow = isDark ? '0 18px 40px rgba(2, 6, 23, 0.4)' : '0 16px 30px rgba(15, 23, 42, 0.12)';
|
||||
const [payload, setPayload] = React.useState<AchievementsPayload | null>(null);
|
||||
|
||||
@@ -8,7 +8,7 @@ import PhotoFrameTile from '../components/PhotoFrameTile';
|
||||
import { useEventData } from '../context/EventDataContext';
|
||||
import { fetchGallery } from '../services/photosApi';
|
||||
import { usePollGalleryDelta } from '../hooks/usePollGalleryDelta';
|
||||
import { useAppearance } from '@/hooks/use-appearance';
|
||||
import { useGuestThemeVariant } from '../lib/guestTheme';
|
||||
import { useTranslation } from '@/guest/i18n/useTranslation';
|
||||
import { useLocale } from '@/guest/i18n/LocaleContext';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
@@ -47,8 +47,7 @@ export default function GalleryScreen() {
|
||||
const { t } = useTranslation();
|
||||
const { locale } = useLocale();
|
||||
const navigate = useNavigate();
|
||||
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 cardShadow = isDark ? '0 18px 40px rgba(2, 6, 23, 0.4)' : '0 16px 30px rgba(15, 23, 42, 0.12)';
|
||||
const mutedButton = isDark ? 'rgba(255, 255, 255, 0.08)' : 'rgba(15, 23, 42, 0.06)';
|
||||
|
||||
@@ -11,19 +11,21 @@ import PullToRefresh from '@/guest/components/PullToRefresh';
|
||||
import { getHelpArticle, type HelpArticleDetail } from '@/guest/services/helpApi';
|
||||
import { useLocale } from '@/guest/i18n/LocaleContext';
|
||||
import { useTranslation } from '@/guest/i18n/useTranslation';
|
||||
import { useAppearance } from '@/hooks/use-appearance';
|
||||
import EventLogo from '../components/EventLogo';
|
||||
import { useGuestThemeVariant } from '../lib/guestTheme';
|
||||
|
||||
export default function HelpArticleScreen() {
|
||||
const params = useParams<{ token?: string; slug: string }>();
|
||||
const slug = params.slug;
|
||||
const { locale } = useLocale();
|
||||
const { t } = useTranslation();
|
||||
const { resolved } = useAppearance();
|
||||
const isDark = resolved === 'dark';
|
||||
const { isDark } = useGuestThemeVariant();
|
||||
const mutedText = isDark ? 'rgba(248, 250, 252, 0.7)' : 'rgba(15, 23, 42, 0.65)';
|
||||
const [article, setArticle] = React.useState<HelpArticleDetail | null>(null);
|
||||
const [state, setState] = React.useState<'loading' | 'ready' | 'error'>('loading');
|
||||
const basePath = params.token ? `/e/${encodeURIComponent(params.token)}/help` : '/help';
|
||||
const brandName = 'Fotospiel';
|
||||
const showStandaloneHeader = !params.token;
|
||||
|
||||
const loadArticle = React.useCallback(async () => {
|
||||
if (!slug) {
|
||||
@@ -78,14 +80,19 @@ export default function HelpArticleScreen() {
|
||||
</SurfaceCard>
|
||||
|
||||
<SurfaceCard glow>
|
||||
<Text fontSize="$5" fontWeight="$8">
|
||||
{title}
|
||||
</Text>
|
||||
{article?.updated_at ? (
|
||||
<Text fontSize="$2" color={mutedText} marginTop="$2">
|
||||
{t('help.article.updated', { date: formatDate(article.updated_at, locale) })}
|
||||
</Text>
|
||||
) : null}
|
||||
<XStack alignItems="center" gap="$3">
|
||||
{showStandaloneHeader ? <EventLogo name={brandName} size="s" /> : null}
|
||||
<YStack gap="$1">
|
||||
<Text fontSize="$5" fontWeight="$8">
|
||||
{title}
|
||||
</Text>
|
||||
{article?.updated_at ? (
|
||||
<Text fontSize="$2" color={mutedText}>
|
||||
{t('help.article.updated', { date: formatDate(article.updated_at, locale) })}
|
||||
</Text>
|
||||
) : null}
|
||||
</YStack>
|
||||
</XStack>
|
||||
</SurfaceCard>
|
||||
|
||||
{state === 'loading' ? (
|
||||
|
||||
@@ -12,14 +12,14 @@ import PullToRefresh from '@/guest/components/PullToRefresh';
|
||||
import { getHelpArticles, type HelpArticleSummary } from '@/guest/services/helpApi';
|
||||
import { useLocale } from '@/guest/i18n/LocaleContext';
|
||||
import { useTranslation } from '@/guest/i18n/useTranslation';
|
||||
import { useAppearance } from '@/hooks/use-appearance';
|
||||
import EventLogo from '../components/EventLogo';
|
||||
import { useGuestThemeVariant } from '../lib/guestTheme';
|
||||
|
||||
export default function HelpCenterScreen() {
|
||||
const params = useParams<{ token?: string }>();
|
||||
const { locale } = useLocale();
|
||||
const { t } = useTranslation();
|
||||
const { resolved } = useAppearance();
|
||||
const isDark = resolved === 'dark';
|
||||
const { isDark } = useGuestThemeVariant();
|
||||
const mutedText = isDark ? 'rgba(248, 250, 252, 0.7)' : 'rgba(15, 23, 42, 0.65)';
|
||||
const [articles, setArticles] = React.useState<HelpArticleSummary[]>([]);
|
||||
const [query, setQuery] = React.useState('');
|
||||
@@ -27,6 +27,8 @@ export default function HelpCenterScreen() {
|
||||
const [servedFromCache, setServedFromCache] = React.useState(false);
|
||||
const [isOnline, setIsOnline] = React.useState(() => (typeof navigator !== 'undefined' ? navigator.onLine : true));
|
||||
const basePath = params.token ? `/e/${encodeURIComponent(params.token)}/help` : '/help';
|
||||
const brandName = 'Fotospiel';
|
||||
const showStandaloneHeader = !params.token;
|
||||
|
||||
const loadArticles = React.useCallback(async (forceRefresh = false) => {
|
||||
setState('loading');
|
||||
@@ -73,12 +75,17 @@ export default function HelpCenterScreen() {
|
||||
>
|
||||
<YStack gap="$4">
|
||||
<SurfaceCard glow>
|
||||
<Text fontSize="$5" fontWeight="$8">
|
||||
{t('help.center.title')}
|
||||
</Text>
|
||||
<Text fontSize="$2" color={mutedText} marginTop="$2">
|
||||
{t('help.center.subtitle')}
|
||||
</Text>
|
||||
<XStack alignItems="center" gap="$3">
|
||||
{showStandaloneHeader ? <EventLogo name={brandName} size="s" /> : null}
|
||||
<YStack gap="$1">
|
||||
<Text fontSize="$5" fontWeight="$8">
|
||||
{t('help.center.title')}
|
||||
</Text>
|
||||
<Text fontSize="$2" color={mutedText}>
|
||||
{t('help.center.subtitle')}
|
||||
</Text>
|
||||
</YStack>
|
||||
</XStack>
|
||||
</SurfaceCard>
|
||||
|
||||
<SurfaceCard>
|
||||
|
||||
@@ -14,7 +14,7 @@ import { usePollStats } from '../hooks/usePollStats';
|
||||
import { fetchGallery } from '../services/photosApi';
|
||||
import { useUploadQueue } from '../services/uploadApi';
|
||||
import { useTranslation } from '@/guest/i18n/useTranslation';
|
||||
import { useAppearance } from '@/hooks/use-appearance';
|
||||
import { useGuestThemeVariant } from '../lib/guestTheme';
|
||||
import { useLocale } from '@/guest/i18n/LocaleContext';
|
||||
import { fetchTasks, type TaskItem } from '../services/tasksApi';
|
||||
import { useGuestTaskProgress } from '@/guest/hooks/useGuestTaskProgress';
|
||||
@@ -182,8 +182,7 @@ export default function HomeScreen() {
|
||||
const [taskError, setTaskError] = React.useState<string | null>(null);
|
||||
const [hasSwiped, setHasSwiped] = React.useState(false);
|
||||
const [emotionMap, setEmotionMap] = React.useState<Record<string, string>>({});
|
||||
const { resolved } = useAppearance();
|
||||
const isDark = resolved === 'dark';
|
||||
const { isDark } = useGuestThemeVariant();
|
||||
const bentoSurface = getBentoSurfaceTokens(isDark);
|
||||
const mutedButton = isDark ? 'rgba(255, 255, 255, 0.08)' : 'rgba(255, 255, 255, 0.75)';
|
||||
const mutedButtonBorder = isDark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(15, 23, 42, 0.08)';
|
||||
|
||||
@@ -10,7 +10,8 @@ import { QrCode, ArrowRight } from 'lucide-react';
|
||||
import { useTranslation } from '@/guest/i18n/useTranslation';
|
||||
import { fetchEvent } from '../services/eventApi';
|
||||
import { readGuestName } from '../context/GuestIdentityContext';
|
||||
import { useAppearance } from '@/hooks/use-appearance';
|
||||
import EventLogo from '../components/EventLogo';
|
||||
import { useGuestThemeVariant } from '../lib/guestTheme';
|
||||
|
||||
const qrConfig = { fps: 10, qrbox: { width: 240, height: 240 } } as const;
|
||||
|
||||
@@ -24,14 +25,14 @@ export default function LandingScreen() {
|
||||
const [errorKey, setErrorKey] = React.useState<LandingErrorKey | null>(null);
|
||||
const [isScanning, setIsScanning] = React.useState(false);
|
||||
const scannerRef = React.useRef<Html5Qrcode | null>(null);
|
||||
const { resolved } = useAppearance();
|
||||
const isDark = resolved === 'dark';
|
||||
const { isDark } = useGuestThemeVariant();
|
||||
const cardBackground = isDark ? 'rgba(15, 23, 42, 0.7)' : 'rgba(255, 255, 255, 0.9)';
|
||||
const cardBorder = isDark ? 'rgba(148, 163, 184, 0.2)' : 'rgba(15, 23, 42, 0.12)';
|
||||
const primaryText = isDark ? '#F8FAFF' : '#0F172A';
|
||||
const mutedText = isDark ? 'rgba(226, 232, 240, 0.78)' : 'rgba(15, 23, 42, 0.6)';
|
||||
const mutedButton = isDark ? 'rgba(248, 250, 255, 0.1)' : 'rgba(15, 23, 42, 0.06)';
|
||||
const mutedButtonBorder = isDark ? 'rgba(248, 250, 255, 0.2)' : 'rgba(15, 23, 42, 0.12)';
|
||||
const brandName = 'Fotospiel';
|
||||
|
||||
const errorMessage = errorKey ? t(`landing.errors.${errorKey}`) : null;
|
||||
|
||||
@@ -159,6 +160,12 @@ export default function LandingScreen() {
|
||||
return (
|
||||
<YStack flex={1} minHeight="100vh" padding="$5" justifyContent="center" backgroundColor={isDark ? '#0B101E' : '#FFF8F5'}>
|
||||
<YStack gap="$5" maxWidth={480} width="100%" alignSelf="center">
|
||||
<XStack alignItems="center" gap="$3">
|
||||
<EventLogo name={brandName} size="s" />
|
||||
<Text fontSize="$5" fontFamily="$display" fontWeight="$8" color={primaryText}>
|
||||
{brandName}
|
||||
</Text>
|
||||
</XStack>
|
||||
<YStack gap="$2">
|
||||
<Text fontSize="$8" fontFamily="$display" fontWeight="$9" color={primaryText}>
|
||||
{t('landing.pageTitle')}
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { YStack } from '@tamagui/stacks';
|
||||
import { YStack, XStack } from '@tamagui/stacks';
|
||||
import { SizableText as Text } from '@tamagui/text';
|
||||
import { Card } from '@tamagui/card';
|
||||
import { useTranslation } from '@/guest/i18n/useTranslation';
|
||||
import { useLocale } from '@/guest/i18n/LocaleContext';
|
||||
import { LegalMarkdown } from '@/guest/components/legal-markdown';
|
||||
import { useAppearance } from '@/hooks/use-appearance';
|
||||
import EventLogo from '../components/EventLogo';
|
||||
import { useGuestThemeVariant } from '../lib/guestTheme';
|
||||
|
||||
export default function LegalScreen() {
|
||||
const { page } = useParams<{ page: string }>();
|
||||
const { t } = useTranslation();
|
||||
const { locale } = useLocale();
|
||||
const { resolved } = useAppearance();
|
||||
const isDark = resolved === 'dark';
|
||||
const { isDark } = useGuestThemeVariant();
|
||||
const primaryText = isDark ? '#F8FAFF' : '#0F172A';
|
||||
const mutedText = isDark ? 'rgba(226, 232, 240, 0.7)' : 'rgba(15, 23, 42, 0.6)';
|
||||
const cardBackground = isDark ? 'rgba(15, 23, 42, 0.75)' : 'rgba(255, 255, 255, 0.9)';
|
||||
const cardBorder = isDark ? 'rgba(148, 163, 184, 0.18)' : 'rgba(15, 23, 42, 0.12)';
|
||||
const brandName = 'Fotospiel';
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [title, setTitle] = React.useState('');
|
||||
const [body, setBody] = React.useState('');
|
||||
@@ -67,6 +68,12 @@ export default function LegalScreen() {
|
||||
return (
|
||||
<YStack flex={1} minHeight="100vh" padding="$5" backgroundColor={isDark ? '#0B101E' : '#FFF8F5'}>
|
||||
<YStack gap="$4" maxWidth={720} width="100%" alignSelf="center">
|
||||
<XStack alignItems="center" gap="$3">
|
||||
<EventLogo name={brandName} size="s" />
|
||||
<Text fontSize="$5" fontFamily="$display" fontWeight="$8" color={primaryText}>
|
||||
{brandName}
|
||||
</Text>
|
||||
</XStack>
|
||||
<Text fontSize="$7" fontFamily="$display" fontWeight="$9" color={primaryText}>
|
||||
{title || fallbackTitle}
|
||||
</Text>
|
||||
|
||||
@@ -9,6 +9,7 @@ import LiveShowBackdrop from '@/guest/components/LiveShowBackdrop';
|
||||
import { useTranslation } from '@/guest/i18n/useTranslation';
|
||||
import { prefersReducedMotion } from '@/guest/lib/motion';
|
||||
import { resolveLiveShowEffect } from '@/guest/lib/liveShowEffects';
|
||||
import EventLogo from '../components/EventLogo';
|
||||
|
||||
export default function LiveShowScreen() {
|
||||
const { token } = useParams<{ token: string }>();
|
||||
@@ -125,9 +126,12 @@ export default function LiveShowScreen() {
|
||||
>
|
||||
<LiveShowBackdrop mode={settings.background_mode} photo={frame[0]} intensity={settings.effect_intensity} />
|
||||
<div className="pointer-events-none absolute inset-x-0 top-0 z-30 flex items-center justify-between px-6 py-4 text-sm">
|
||||
<span className="font-semibold text-white" style={{ fontFamily: 'var(--guest-heading-font)' }}>
|
||||
{stageTitle}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<EventLogo name={stageTitle} icon={event?.type?.icon ?? null} size="s" />
|
||||
<span className="font-semibold text-white" style={{ fontFamily: 'var(--guest-heading-font)' }}>
|
||||
{stageTitle}
|
||||
</span>
|
||||
</div>
|
||||
<span className="rounded-full bg-white/10 px-3 py-1 text-xs font-semibold uppercase tracking-[0.24em] text-white/70">
|
||||
{connection === 'sse'
|
||||
? t('liveShowPlayer.connection.live', 'Live')
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { YStack } from '@tamagui/stacks';
|
||||
import { YStack, XStack } from '@tamagui/stacks';
|
||||
import { SizableText as Text } from '@tamagui/text';
|
||||
import { Button } from '@tamagui/button';
|
||||
import { Input } from '@tamagui/input';
|
||||
@@ -8,7 +8,8 @@ import { Card } from '@tamagui/card';
|
||||
import { useTranslation } from '@/guest/i18n/useTranslation';
|
||||
import { useEventData } from '../context/EventDataContext';
|
||||
import { useGuestIdentity } from '../context/GuestIdentityContext';
|
||||
import { useAppearance } from '@/hooks/use-appearance';
|
||||
import EventLogo from '../components/EventLogo';
|
||||
import { useGuestThemeVariant } from '../lib/guestTheme';
|
||||
|
||||
export default function ProfileSetupScreen() {
|
||||
const { token } = useParams<{ token: string }>();
|
||||
@@ -16,8 +17,7 @@ export default function ProfileSetupScreen() {
|
||||
const { t } = useTranslation();
|
||||
const { event, status } = useEventData();
|
||||
const identity = useGuestIdentity();
|
||||
const { resolved } = useAppearance();
|
||||
const isDark = resolved === 'dark';
|
||||
const { isDark } = useGuestThemeVariant();
|
||||
const primaryText = isDark ? '#F8FAFF' : '#0F172A';
|
||||
const mutedText = isDark ? 'rgba(226, 232, 240, 0.7)' : 'rgba(15, 23, 42, 0.6)';
|
||||
const cardBackground = isDark ? 'rgba(15, 23, 42, 0.75)' : 'rgba(255, 255, 255, 0.9)';
|
||||
@@ -67,9 +67,12 @@ export default function ProfileSetupScreen() {
|
||||
<YStack flex={1} minHeight="100vh" padding="$5" justifyContent="center" backgroundColor={isDark ? '#0B101E' : '#FFF8F5'}>
|
||||
<YStack gap="$5" maxWidth={420} width="100%" alignSelf="center">
|
||||
<YStack gap="$2">
|
||||
<Text fontSize="$7" fontFamily="$display" fontWeight="$9" color={primaryText}>
|
||||
{event.name}
|
||||
</Text>
|
||||
<XStack alignItems="center" gap="$3">
|
||||
<EventLogo name={event.name} icon={event.type?.icon ?? null} size="m" />
|
||||
<Text fontSize="$7" fontFamily="$display" fontWeight="$9" color={primaryText}>
|
||||
{event.name}
|
||||
</Text>
|
||||
</XStack>
|
||||
<Text color={mutedText}>
|
||||
{t('profileSetup.card.description')}
|
||||
</Text>
|
||||
|
||||
@@ -3,17 +3,18 @@ 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 { Image as ImageIcon, Download, Share2 } from 'lucide-react';
|
||||
import { Download, Share2 } from 'lucide-react';
|
||||
import { Sheet } from '@tamagui/sheet';
|
||||
import StandaloneShell from '../components/StandaloneShell';
|
||||
import SurfaceCard from '../components/SurfaceCard';
|
||||
import EventLogo from '../components/EventLogo';
|
||||
import { fetchGalleryMeta, fetchGalleryPhotos, type GalleryMetaResponse, type GalleryPhotoResource } from '@/guest/services/galleryApi';
|
||||
import { createPhotoShareLink } from '@/guest/services/photosApi';
|
||||
import { useTranslation } from '@/guest/i18n/useTranslation';
|
||||
import { EventBrandingProvider } from '@/guest/context/EventBrandingContext';
|
||||
import { mapEventBranding } from '../lib/eventBranding';
|
||||
import { BrandingTheme } from '../lib/brandingTheme';
|
||||
import { useAppearance } from '@/hooks/use-appearance';
|
||||
import { useGuestThemeVariant } from '../lib/guestTheme';
|
||||
|
||||
type GalleryState = {
|
||||
meta: GalleryMetaResponse | null;
|
||||
@@ -40,9 +41,6 @@ const PAGE_SIZE = 30;
|
||||
export default function PublicGalleryScreen() {
|
||||
const { token } = useParams<{ token: string }>();
|
||||
const { t, locale } = useTranslation();
|
||||
const { resolved } = useAppearance();
|
||||
const isDark = resolved === 'dark';
|
||||
const mutedText = isDark ? 'rgba(248, 250, 252, 0.7)' : 'rgba(15, 23, 42, 0.65)';
|
||||
const [state, setState] = React.useState<GalleryState>(INITIAL_STATE);
|
||||
const [selected, setSelected] = React.useState<GalleryPhotoResource | null>(null);
|
||||
const [shareLoading, setShareLoading] = React.useState(false);
|
||||
@@ -50,16 +48,10 @@ export default function PublicGalleryScreen() {
|
||||
|
||||
const branding = React.useMemo(() => {
|
||||
if (!state.meta) return null;
|
||||
const raw = state.meta.branding ?? null;
|
||||
return mapEventBranding({
|
||||
primary_color: raw.primary_color,
|
||||
secondary_color: raw.secondary_color,
|
||||
background_color: raw.background_color,
|
||||
surface_color: raw.surface_color ?? raw.background_color,
|
||||
palette: raw.palette ?? undefined,
|
||||
mode: raw.mode ?? 'auto',
|
||||
} as any);
|
||||
return mapEventBranding(state.meta.branding ?? null);
|
||||
}, [state.meta]);
|
||||
const { isDark } = useGuestThemeVariant(branding);
|
||||
const mutedText = isDark ? 'rgba(248, 250, 252, 0.7)' : 'rgba(15, 23, 42, 0.65)';
|
||||
|
||||
const loadInitial = React.useCallback(async () => {
|
||||
if (!token) return;
|
||||
@@ -117,15 +109,17 @@ export default function PublicGalleryScreen() {
|
||||
const content = (
|
||||
<StandaloneShell>
|
||||
<SurfaceCard glow>
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<ImageIcon size={20} color={isDark ? '#F8FAFF' : '#0F172A'} />
|
||||
<Text fontSize="$4" fontWeight="$7">
|
||||
{t('galleryPublic.title')}
|
||||
</Text>
|
||||
<XStack alignItems="center" gap="$3">
|
||||
<EventLogo name={state.meta?.event?.name ?? t('galleryPage.hero.eventFallback', 'Event')} size="s" />
|
||||
<YStack gap="$1">
|
||||
<Text fontSize="$4" fontWeight="$7">
|
||||
{t('galleryPublic.title')}
|
||||
</Text>
|
||||
<Text fontSize="$2" color={mutedText}>
|
||||
{state.meta?.event?.name ?? t('galleryPage.hero.eventFallback', 'Event')}
|
||||
</Text>
|
||||
</YStack>
|
||||
</XStack>
|
||||
<Text fontSize="$2" color={mutedText} marginTop="$2">
|
||||
{state.meta?.event?.name ?? t('galleryPage.hero.eventFallback', 'Event')}
|
||||
</Text>
|
||||
</SurfaceCard>
|
||||
|
||||
{state.loading ? (
|
||||
|
||||
@@ -6,21 +6,22 @@ import { Share2, QrCode, Link, Users } from 'lucide-react';
|
||||
import AppShell from '../components/AppShell';
|
||||
import { useEventData } from '../context/EventDataContext';
|
||||
import { buildEventShareLink } from '../services/eventLink';
|
||||
import { useAppearance } from '@/hooks/use-appearance';
|
||||
import { usePollStats } from '../hooks/usePollStats';
|
||||
import { fetchEventQrCode } from '../services/qrApi';
|
||||
import { useGuestThemeVariant } from '../lib/guestTheme';
|
||||
import { useTranslation } from '@/guest/i18n/useTranslation';
|
||||
|
||||
export default function ShareScreen() {
|
||||
const { event, token } = useEventData();
|
||||
const { t } = useTranslation();
|
||||
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 cardShadow = isDark ? '0 18px 40px rgba(2, 6, 23, 0.4)' : '0 16px 30px rgba(15, 23, 42, 0.12)';
|
||||
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 shareUrl = buildEventShareLink(event, token);
|
||||
const qrUrl = shareUrl
|
||||
? `https://api.qrserver.com/v1/create-qr-code/?size=240x240&data=${encodeURIComponent(shareUrl)}`
|
||||
: '';
|
||||
|
||||
const handleCopy = React.useCallback(async () => {
|
||||
if (!shareUrl) {
|
||||
@@ -52,6 +53,44 @@ export default function ShareScreen() {
|
||||
}
|
||||
}, [event?.name, handleCopy, shareUrl]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!token) {
|
||||
setQrCodeDataUrl('');
|
||||
setQrLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let active = true;
|
||||
setQrLoading(true);
|
||||
|
||||
fetchEventQrCode(token, 240)
|
||||
.then((payload) => {
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
setQrCodeDataUrl(payload.qr_code_data_url ?? '');
|
||||
})
|
||||
.catch((error) => {
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
console.error('Failed to load QR code', error);
|
||||
setQrCodeDataUrl('');
|
||||
})
|
||||
.finally(() => {
|
||||
if (active) {
|
||||
setQrLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [token]);
|
||||
|
||||
const guestCountLabel = stats.onlineGuests.toString();
|
||||
const inviteDisabled = !shareUrl;
|
||||
|
||||
return (
|
||||
<AppShell>
|
||||
<YStack gap="$4">
|
||||
@@ -94,14 +133,14 @@ export default function ShareScreen() {
|
||||
: 'radial-gradient(circle at 30% 30%, color-mix(in oklab, var(--guest-primary, #FF5A5F) 18%, white), transparent 60%)',
|
||||
}}
|
||||
>
|
||||
{qrUrl ? (
|
||||
{qrCodeDataUrl ? (
|
||||
<img
|
||||
src={qrUrl}
|
||||
src={qrCodeDataUrl}
|
||||
alt={t('share.invite.qrAlt', 'Event QR code')}
|
||||
style={{ width: 120, height: 120, borderRadius: 16 }}
|
||||
/>
|
||||
) : (
|
||||
<QrCode size={28} color={isDark ? '#F8FAFF' : '#0F172A'} />
|
||||
<QrCode size={qrLoading ? 22 : 28} color={isDark ? '#F8FAFF' : '#0F172A'} />
|
||||
)}
|
||||
<Text fontSize="$3" fontWeight="$7">
|
||||
{t('share.invite.qrLabel', 'Show QR')}
|
||||
@@ -155,12 +194,27 @@ export default function ShareScreen() {
|
||||
{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', 'Share {event} with your guests.', { event: event.name })
|
||||
: t('share.invite.guestsSubtitle', 'Share the event with your guests.')}
|
||||
</Text>
|
||||
<Button size="$3" backgroundColor="$primary" borderRadius="$pill" alignSelf="flex-start" onPress={handleShare}>
|
||||
<Button
|
||||
size="$3"
|
||||
backgroundColor="$primary"
|
||||
borderRadius="$pill"
|
||||
alignSelf="flex-start"
|
||||
onPress={handleShare}
|
||||
disabled={inviteDisabled}
|
||||
>
|
||||
{t('share.invite.send', 'Send invite')}
|
||||
</Button>
|
||||
</YStack>
|
||||
|
||||
@@ -6,9 +6,14 @@ import { Button } from '@tamagui/button';
|
||||
import { AlertCircle, Download } from 'lucide-react';
|
||||
import StandaloneShell from '../components/StandaloneShell';
|
||||
import SurfaceCard from '../components/SurfaceCard';
|
||||
import EventLogo from '../components/EventLogo';
|
||||
import { fetchPhotoShare } from '@/guest/services/photosApi';
|
||||
import type { EventBrandingPayload } from '@/guest/services/eventApi';
|
||||
import { useTranslation } from '@/guest/i18n/useTranslation';
|
||||
import { useAppearance } from '@/hooks/use-appearance';
|
||||
import { EventBrandingProvider } from '@/guest/context/EventBrandingContext';
|
||||
import { mapEventBranding } from '../lib/eventBranding';
|
||||
import { BrandingTheme } from '../lib/brandingTheme';
|
||||
import { useGuestThemeVariant } from '../lib/guestTheme';
|
||||
|
||||
interface ShareResponse {
|
||||
slug: string;
|
||||
@@ -22,19 +27,25 @@ interface ShareResponse {
|
||||
image_urls: { full: string; thumbnail: string };
|
||||
};
|
||||
event?: { id: number; name?: string | null } | null;
|
||||
branding?: EventBrandingPayload | null;
|
||||
}
|
||||
|
||||
export default function SharedPhotoScreen() {
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
const { t } = useTranslation();
|
||||
const { resolved } = useAppearance();
|
||||
const isDark = resolved === 'dark';
|
||||
const mutedText = isDark ? 'rgba(248, 250, 252, 0.7)' : 'rgba(15, 23, 42, 0.65)';
|
||||
const [state, setState] = React.useState<{ loading: boolean; error: string | null; data: ShareResponse | null }>({
|
||||
loading: true,
|
||||
error: null,
|
||||
data: null,
|
||||
});
|
||||
const branding = React.useMemo(() => {
|
||||
if (!state.data?.branding) {
|
||||
return null;
|
||||
}
|
||||
return mapEventBranding(state.data.branding);
|
||||
}, [state.data]);
|
||||
const { isDark } = useGuestThemeVariant(branding);
|
||||
const mutedText = isDark ? 'rgba(248, 250, 252, 0.7)' : 'rgba(15, 23, 42, 0.65)';
|
||||
|
||||
React.useEffect(() => {
|
||||
let active = true;
|
||||
@@ -93,15 +104,20 @@ export default function SharedPhotoScreen() {
|
||||
const { data } = state;
|
||||
const chips = buildChips(data, t);
|
||||
|
||||
return (
|
||||
const content = (
|
||||
<StandaloneShell>
|
||||
<SurfaceCard glow>
|
||||
<Text fontSize="$2" letterSpacing={2} textTransform="uppercase" color={mutedText}>
|
||||
{t('share.title', 'Geteiltes Foto')}
|
||||
</Text>
|
||||
<Text fontSize="$6" fontWeight="$8" marginTop="$2">
|
||||
{data.event?.name ?? t('share.defaultEvent', 'Ein besonderer Moment')}
|
||||
</Text>
|
||||
<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}
|
||||
@@ -151,6 +167,18 @@ export default function SharedPhotoScreen() {
|
||||
</Button>
|
||||
</StandaloneShell>
|
||||
);
|
||||
|
||||
if (!branding) {
|
||||
return content;
|
||||
}
|
||||
|
||||
return (
|
||||
<EventBrandingProvider branding={branding}>
|
||||
<BrandingTheme>
|
||||
{content}
|
||||
</BrandingTheme>
|
||||
</EventBrandingProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function buildChips(
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { ChevronLeft, ChevronRight, Pause, Play, Maximize2, Minimize2 } from 'lucide-react';
|
||||
import { useEventData } from '../context/EventDataContext';
|
||||
import EventLogo from '../components/EventLogo';
|
||||
import { fetchGallery, type GalleryPhoto } from '../services/photosApi';
|
||||
import { useTranslation } from '@/guest/i18n/useTranslation';
|
||||
|
||||
@@ -91,9 +92,12 @@ export default function SlideshowScreen() {
|
||||
return (
|
||||
<div className="relative flex min-h-screen flex-col items-center justify-center overflow-hidden bg-black text-white">
|
||||
<div className="pointer-events-none absolute inset-x-0 top-0 z-20 flex items-center justify-between px-6 py-4 text-sm">
|
||||
<span className="font-semibold text-white" style={{ fontFamily: 'var(--guest-heading-font)' }}>
|
||||
{event?.name ?? t('galleryPage.hero.eventFallback', 'Event')}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<EventLogo name={event?.name ?? t('galleryPage.hero.eventFallback', 'Event')} icon={event?.type?.icon ?? null} size="s" />
|
||||
<span className="font-semibold text-white" style={{ fontFamily: 'var(--guest-heading-font)' }}>
|
||||
{event?.name ?? t('galleryPage.hero.eventFallback', 'Event')}
|
||||
</span>
|
||||
</div>
|
||||
<span className="rounded-full bg-white/10 px-3 py-1 text-xs font-semibold uppercase tracking-[0.24em] text-white/70">
|
||||
{t('galleryPage.title', 'Gallery')}
|
||||
</span>
|
||||
|
||||
@@ -10,7 +10,7 @@ import { fetchTasks, type TaskItem } from '../services/tasksApi';
|
||||
import { useEventData } from '../context/EventDataContext';
|
||||
import { buildEventPath } from '../lib/routes';
|
||||
import { useTranslation } from '@/guest/i18n/useTranslation';
|
||||
import { useAppearance } from '@/hooks/use-appearance';
|
||||
import { useGuestThemeVariant } from '../lib/guestTheme';
|
||||
|
||||
function getTaskValue(task: TaskItem, key: string): string | undefined {
|
||||
const value = task?.[key as keyof TaskItem];
|
||||
@@ -39,8 +39,7 @@ export default function TaskDetailScreen() {
|
||||
const { taskId } = useParams<{ taskId: string }>();
|
||||
const { t, locale } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { resolved } = useAppearance();
|
||||
const isDark = resolved === 'dark';
|
||||
const { isDark } = useGuestThemeVariant();
|
||||
const mutedText = isDark ? 'rgba(248, 250, 252, 0.7)' : 'rgba(15, 23, 42, 0.65)';
|
||||
const [task, setTask] = React.useState<TaskItem | null>(null);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useTranslation } from '@/guest/i18n/useTranslation';
|
||||
import { useLocale } from '@/guest/i18n/LocaleContext';
|
||||
import { fetchTasks } from '../services/tasksApi';
|
||||
import { fetchEmotions } from '../services/emotionsApi';
|
||||
import { useAppearance } from '@/hooks/use-appearance';
|
||||
import { useGuestThemeVariant } from '../lib/guestTheme';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useGuestTaskProgress } from '@/guest/hooks/useGuestTaskProgress';
|
||||
|
||||
@@ -27,8 +27,7 @@ export default function TasksScreen() {
|
||||
const { locale } = useLocale();
|
||||
const navigate = useNavigate();
|
||||
const { completedCount } = useGuestTaskProgress(token ?? undefined);
|
||||
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 cardShadow = isDark ? '0 16px 32px rgba(2, 6, 23, 0.35)' : '0 14px 24px rgba(15, 23, 42, 0.12)';
|
||||
const mutedButton = isDark ? 'rgba(255, 255, 255, 0.08)' : 'rgba(15, 23, 42, 0.06)';
|
||||
|
||||
@@ -7,7 +7,7 @@ import AppShell from '../components/AppShell';
|
||||
import SurfaceCard from '../components/SurfaceCard';
|
||||
import { useUploadQueue } from '../services/uploadApi';
|
||||
import { useTranslation } from '@/guest/i18n/useTranslation';
|
||||
import { useAppearance } from '@/hooks/use-appearance';
|
||||
import { useGuestThemeVariant } from '../lib/guestTheme';
|
||||
import { useLocale } from '@/guest/i18n/LocaleContext';
|
||||
import { useEventData } from '../context/EventDataContext';
|
||||
import { fetchPendingUploadsSummary, type PendingUpload } from '@/guest/services/pendingUploadsApi';
|
||||
@@ -20,8 +20,7 @@ export default function UploadQueueScreen() {
|
||||
const { token } = useEventData();
|
||||
const { items, loading, retryAll, clearFinished, refresh } = useUploadQueue();
|
||||
const [progress, setProgress] = React.useState<ProgressMap>({});
|
||||
const { resolved } = useAppearance();
|
||||
const isDark = resolved === 'dark';
|
||||
const { isDark } = useGuestThemeVariant();
|
||||
const mutedText = isDark ? 'rgba(248, 250, 252, 0.7)' : 'rgba(15, 23, 42, 0.65)';
|
||||
const [pending, setPending] = React.useState<PendingUpload[]>([]);
|
||||
const [pendingLoading, setPendingLoading] = React.useState(false);
|
||||
|
||||
@@ -7,7 +7,7 @@ import AppShell from '../components/AppShell';
|
||||
import { useEventData } from '../context/EventDataContext';
|
||||
import { useOptionalGuestIdentity } from '../context/GuestIdentityContext';
|
||||
import { uploadPhoto, useUploadQueue } from '../services/uploadApi';
|
||||
import { useAppearance } from '@/hooks/use-appearance';
|
||||
import { useGuestThemeVariant } from '../lib/guestTheme';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useTranslation } from '@/guest/i18n/useTranslation';
|
||||
import { useGuestTaskProgress } from '@/guest/hooks/useGuestTaskProgress';
|
||||
@@ -15,6 +15,7 @@ import { fetchPendingUploadsSummary, type PendingUpload } from '@/guest/services
|
||||
import { resolveUploadErrorDialog, type UploadErrorDialog } from '@/guest/lib/uploadErrorDialog';
|
||||
import { fetchTasks, type TaskItem } from '../services/tasksApi';
|
||||
import SurfaceCard from '../components/SurfaceCard';
|
||||
import { pushGuestToast } from '../lib/toast';
|
||||
|
||||
function getTaskValue(task: TaskItem, key: string): string | undefined {
|
||||
const value = task?.[key as keyof TaskItem];
|
||||
@@ -46,8 +47,7 @@ export default function UploadScreen() {
|
||||
const [mirror, setMirror] = React.useState(true);
|
||||
const [previewFile, setPreviewFile] = React.useState<File | null>(null);
|
||||
const [previewUrl, setPreviewUrl] = React.useState<string | null>(null);
|
||||
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 cardShadow = isDark ? '0 18px 40px rgba(2, 6, 23, 0.4)' : '0 16px 30px rgba(15, 23, 42, 0.12)';
|
||||
const iconColor = isDark ? '#F8FAFF' : '#0F172A';
|
||||
@@ -186,6 +186,7 @@ export default function UploadScreen() {
|
||||
for (const file of files) {
|
||||
if (!navigator.onLine) {
|
||||
await enqueueFile(file);
|
||||
pushGuestToast({ text: t('uploadV2.toast.queued', 'Offline — added to upload queue.'), type: 'info' });
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -204,6 +205,7 @@ export default function UploadScreen() {
|
||||
if (autoApprove) {
|
||||
void triggerConfetti();
|
||||
}
|
||||
pushGuestToast({ text: t('uploadV2.toast.uploaded', 'Upload complete.'), type: 'success' });
|
||||
void loadPending();
|
||||
} catch (err) {
|
||||
const uploadErr = err as { code?: string; meta?: Record<string, unknown> };
|
||||
|
||||
Reference in New Issue
Block a user