Add dedicated mobile event add-ons page
This commit is contained in:
@@ -2,13 +2,14 @@ import React from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Share2, Sparkles, Trophy, Users, TrendingUp, Heart, Image as ImageIcon } from 'lucide-react';
|
||||
import { isPast, isSameDay, parseISO } from 'date-fns';
|
||||
import { YStack, XStack } from '@tamagui/stacks';
|
||||
import { SizableText as Text } from '@tamagui/text';
|
||||
import { Switch } from '@tamagui/switch';
|
||||
import { Tabs } from 'tamagui';
|
||||
import toast from 'react-hot-toast';
|
||||
import { MobileShell } from './components/MobileShell';
|
||||
import { MobileCard, CTAButton, PillBadge, SkeletonCard } from './components/Primitives';
|
||||
import { LegalConsentSheet } from './components/LegalConsentSheet';
|
||||
import { DataExportsPanel } from './DataExportsPage';
|
||||
import {
|
||||
getEvent,
|
||||
@@ -20,14 +21,10 @@ import {
|
||||
EventStats,
|
||||
EventQrInvite,
|
||||
EventEngagement,
|
||||
EventAddonCatalogItem,
|
||||
getAddonCatalog,
|
||||
createEventAddonCheckout,
|
||||
} from '../api';
|
||||
import { isAuthError } from '../auth/tokens';
|
||||
import { getApiErrorMessage, isApiError } from '../lib/apiError';
|
||||
import { adminPath } from '../constants';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useBackNavigation } from './hooks/useBackNavigation';
|
||||
import { useAdminTheme } from './theme';
|
||||
|
||||
@@ -37,6 +34,25 @@ type GalleryCounts = {
|
||||
pending: number;
|
||||
};
|
||||
|
||||
function canAccessRecap(event: TenantEvent): boolean {
|
||||
if (event.status === 'archived') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!event.event_date) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const eventDate = parseISO(event.event_date);
|
||||
const today = new Date();
|
||||
|
||||
if (isSameDay(today, eventDate)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isPast(eventDate);
|
||||
}
|
||||
|
||||
export default function MobileEventRecapPage() {
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
const navigate = useNavigate();
|
||||
@@ -48,30 +64,25 @@ export default function MobileEventRecapPage() {
|
||||
const [event, setEvent] = React.useState<TenantEvent | null>(null);
|
||||
const [stats, setEventStats] = React.useState<EventStats | null>(null);
|
||||
const [invites, setInvites] = React.useState<EventQrInvite[]>([]);
|
||||
const [addons, setAddons] = React.useState<EventAddonCatalogItem[]>([]);
|
||||
const [engagement, setEngagement] = React.useState<EventEngagement | null>(null);
|
||||
const [engagementLoading, setEngagementLoading] = React.useState(false);
|
||||
const [engagementError, setEngagementError] = React.useState<string | null>(null);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const [consentOpen, setConsentOpen] = React.useState(false);
|
||||
const [busyScope, setBusyScope] = React.useState<string | null>(null);
|
||||
const back = useBackNavigation(slug ? adminPath(`/mobile/events/${slug}`) : adminPath('/mobile/events'));
|
||||
|
||||
const load = React.useCallback(async () => {
|
||||
if (!slug) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const [eventData, statsData, invitesData, addonsData] = await Promise.all([
|
||||
const [eventData, statsData, invitesData] = await Promise.all([
|
||||
getEvent(slug),
|
||||
getEventStats(slug),
|
||||
getEventQrInvites(slug),
|
||||
getAddonCatalog(),
|
||||
]);
|
||||
setEvent(eventData);
|
||||
setEventStats(statsData);
|
||||
setInvites(invitesData);
|
||||
setAddons(addonsData);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
if (!isAuthError(err)) {
|
||||
@@ -130,39 +141,13 @@ export default function MobileEventRecapPage() {
|
||||
};
|
||||
}, [event?.status, i18n.language, invites, t]);
|
||||
|
||||
const handleCheckout = (addonKey: string) => {
|
||||
if (!slug || busyScope) {
|
||||
return;
|
||||
const recapAvailable = event ? canAccessRecap(event) : true;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (event && !recapAvailable) {
|
||||
navigate(adminPath(`/mobile/events/${event.slug}`), { replace: true });
|
||||
}
|
||||
|
||||
setBusyScope(addonKey);
|
||||
setConsentOpen(true);
|
||||
};
|
||||
|
||||
const handleConsentConfirm = async (consents: { acceptedTerms: boolean; acceptedWaiver: boolean }) => {
|
||||
if (!slug || !busyScope) return;
|
||||
try {
|
||||
const { checkout_url } = await createEventAddonCheckout(slug, {
|
||||
addon_key: busyScope,
|
||||
success_url: window.location.href,
|
||||
cancel_url: window.location.href,
|
||||
accepted_terms: consents.acceptedTerms,
|
||||
accepted_waiver: consents.acceptedWaiver,
|
||||
});
|
||||
if (checkout_url) {
|
||||
window.location.href = checkout_url;
|
||||
return;
|
||||
}
|
||||
|
||||
toast.error(t('events.errors.checkoutMissing', 'Checkout konnte nicht gestartet werden.'));
|
||||
setBusyScope(null);
|
||||
setConsentOpen(false);
|
||||
} catch (err) {
|
||||
toast.error(getApiErrorMessage(err, t('events.errors.checkoutFailed', 'Bezahlvorgang konnte nicht gestartet werden.')));
|
||||
setBusyScope(null);
|
||||
setConsentOpen(false);
|
||||
}
|
||||
};
|
||||
}, [event, navigate, recapAvailable]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -197,9 +182,10 @@ export default function MobileEventRecapPage() {
|
||||
|
||||
const activeInvite = invites.find((i) => i.is_active) ?? invites[0] ?? null;
|
||||
const guestLink = activeInvite?.url ?? '';
|
||||
const galleryExtensionAddons = addons
|
||||
.filter((addon) => addon.key.startsWith('extend_gallery_') || Number(addon.increments?.extra_gallery_days ?? 0) > 0)
|
||||
.sort((left, right) => Number(left.increments?.extra_gallery_days ?? 0) - Number(right.increments?.extra_gallery_days ?? 0));
|
||||
|
||||
if (!recapAvailable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MobileShell
|
||||
@@ -389,28 +375,17 @@ export default function MobileEventRecapPage() {
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Sparkles size={18} color={primary} />
|
||||
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
||||
{t('events.recap.addons', 'Galerie verlängern')}
|
||||
{t('events.recap.addons', 'Manage add-ons')}
|
||||
</Text>
|
||||
</XStack>
|
||||
<Text fontSize="$sm" color={text}>
|
||||
{t('events.recap.addonBody', 'Die Online-Zeit deiner Galerie neigt sich dem Ende? Hier kannst du sie verlängern.')}
|
||||
{t('events.recap.addonBody', 'Need more runtime or capacity? Manage all event add-ons in one place.')}
|
||||
</Text>
|
||||
|
||||
<YStack gap="$2">
|
||||
{galleryExtensionAddons.map((addon) => (
|
||||
<CTAButton
|
||||
key={addon.key}
|
||||
label={addon.label || t('events.recap.buyExtension', 'Galerie um 30 Tage verlängern')}
|
||||
onPress={() => handleCheckout(addon.key)}
|
||||
loading={busyScope === addon.key}
|
||||
/>
|
||||
))}
|
||||
{galleryExtensionAddons.length === 0 ? (
|
||||
<Text fontSize="$sm" color={muted}>
|
||||
{t('events.recap.noAddonAvailable', 'Aktuell sind keine Galerie-Add-ons verfügbar.')}
|
||||
</Text>
|
||||
) : null}
|
||||
</YStack>
|
||||
<CTAButton
|
||||
label={t('events.recap.openAddons', 'Open add-on manager')}
|
||||
onPress={() => navigate(adminPath(`/mobile/events/${event.slug}/addons?scope=gallery`))}
|
||||
/>
|
||||
</MobileCard>
|
||||
</YStack>
|
||||
</Tabs.Content>
|
||||
@@ -611,16 +586,6 @@ export default function MobileEventRecapPage() {
|
||||
</Tabs>
|
||||
</YStack>
|
||||
|
||||
<LegalConsentSheet
|
||||
open={consentOpen}
|
||||
onClose={() => {
|
||||
setConsentOpen(false);
|
||||
setBusyScope(null);
|
||||
}}
|
||||
onConfirm={handleConsentConfirm}
|
||||
busy={Boolean(busyScope)}
|
||||
t={t as any}
|
||||
/>
|
||||
</MobileShell>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user