Files
fotospiel-app/resources/js/admin/mobile/welcome/WelcomePackagesPage.tsx

184 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useQuery } from '@tanstack/react-query';
import { Check, Package as PackageIcon } from 'lucide-react';
import { YStack, XStack } from '@tamagui/stacks';
import { SizableText as Text } from '@tamagui/text';
import { Pressable } from '@tamagui/react-native-web-lite';
import { OnboardingShell } from '../components/OnboardingShell';
import { MobileCard, CTAButton, PillBadge } from '../components/Primitives';
import { getPackages, getTenantPackagesOverview, Package, trackOnboarding } from '../../api';
import { ADMIN_WELCOME_BASE_PATH, ADMIN_WELCOME_EVENT_PATH, ADMIN_WELCOME_SUMMARY_PATH, adminPath } from '../../constants';
import { getSelectedPackageId, setSelectedPackageId } from '../lib/onboardingSelection';
import { useAdminTheme } from '../theme';
export default function WelcomePackagesPage() {
const navigate = useNavigate();
const { t } = useTranslation('onboarding');
const { muted } = useAdminTheme();
const [selectedId, setSelectedId] = React.useState<number | null>(() => getSelectedPackageId());
const { data: overview } = useQuery({
queryKey: ['mobile', 'onboarding', 'packages-overview'],
queryFn: () => getTenantPackagesOverview({ force: true }),
staleTime: 60_000,
});
const { data: packages, isLoading, isError } = useQuery({
queryKey: ['mobile', 'onboarding', 'packages-list'],
queryFn: () => getPackages('endcustomer'),
staleTime: 60_000,
});
const hasActivePackage =
Boolean(overview?.activePackage) || Boolean(overview?.packages?.some((pkg) => pkg.active));
React.useEffect(() => {
if (!hasActivePackage) {
return;
}
setSelectedPackageId(null);
navigate(ADMIN_WELCOME_EVENT_PATH, { replace: true });
}, [hasActivePackage, navigate]);
const handleSelect = (pkg: Package) => {
setSelectedId(pkg.id);
setSelectedPackageId(pkg.id);
void trackOnboarding('package_selected', { package_id: pkg.id, package_name: pkg.name });
};
return (
<OnboardingShell
eyebrow={t('packages.layout.eyebrow', 'Step 2')}
title={t('packages.layout.title', 'Choose your package')}
subtitle={t(
'packages.layout.subtitle',
'Fotospiel supports flexible pricing: single-use event slots or subscriptions covering multiple events.',
)}
onBack={() => navigate(ADMIN_WELCOME_BASE_PATH)}
onSkip={() => navigate(adminPath('/mobile/billing#packages'))}
skipLabel={t('packages.cta.billing.button', 'Open billing')}
>
{isLoading ? (
<MobileCard>
<Text fontSize="$sm" color={muted}>
{t('packages.state.loading', 'Loading packages …')}
</Text>
</MobileCard>
) : isError ? (
<MobileCard>
<Text fontSize="$sm" fontWeight="700">
{t('packages.state.errorTitle', 'Failed to load')}
</Text>
<Text fontSize="$sm" color={muted}>
{t('packages.state.errorDescription', 'Please try again or contact support.')}
</Text>
</MobileCard>
) : (packages?.length ?? 0) === 0 ? (
<MobileCard>
<Text fontSize="$sm" fontWeight="700">
{t('packages.state.emptyTitle', 'Catalogue is empty')}
</Text>
<Text fontSize="$sm" color={muted}>
{t('packages.state.emptyDescription', 'No packages are currently available. Reach out to support to enable new offers.')}
</Text>
</MobileCard>
) : (
<YStack space="$3">
{packages?.map((pkg) => (
<PackageCard
key={pkg.id}
pkg={pkg}
selected={selectedId === pkg.id}
onSelect={() => handleSelect(pkg)}
/>
))}
</YStack>
)}
<MobileCard space="$2">
<Text fontSize="$sm" fontWeight="800">
{t('packages.step.title', 'Activate the right plan')}
</Text>
<Text fontSize="$sm" color={muted}>
{t('packages.step.description', 'Secure capacity for your next event. Upgrade at any time only pay for what you need.')}
</Text>
</MobileCard>
<XStack space="$2">
<CTAButton
label={t('packages.cta.summary.button', 'Continue to summary')}
onPress={() => navigate(ADMIN_WELCOME_SUMMARY_PATH)}
disabled={!selectedId}
fullWidth={false}
/>
<CTAButton
label={t('packages.cta.billing.button', 'Open billing')}
tone="ghost"
onPress={() => navigate(adminPath('/mobile/billing#packages'))}
fullWidth={false}
/>
</XStack>
</OnboardingShell>
);
}
function PackageCard({
pkg,
selected,
onSelect,
}: {
pkg: Package;
selected: boolean;
onSelect: () => void;
}) {
const { t } = useTranslation('onboarding');
const { primary, border, accentSoft, muted } = useAdminTheme();
const badges = [
t('packages.card.badges.photos', { count: pkg.max_photos ?? t('summary.details.infinity', '∞') }),
t('packages.card.badges.guests', { count: pkg.max_guests ?? t('summary.details.infinity', '∞') }),
t('packages.card.badges.days', { count: pkg.gallery_days ?? t('summary.details.infinity', '∞') }),
];
return (
<Pressable onPress={onSelect}>
<MobileCard borderColor={selected ? primary : border} space="$2">
<XStack alignItems="center" justifyContent="space-between">
<XStack alignItems="center" space="$2">
<XStack width={36} height={36} borderRadius={12} backgroundColor={accentSoft} alignItems="center" justifyContent="center">
<PackageIcon size={18} color={primary} />
</XStack>
<YStack>
<Text fontSize="$sm" fontWeight="800">
{pkg.name}
</Text>
<Text fontSize="$xs" color={muted}>
{t('packages.card.description', 'Ready for your next event right away.')}
</Text>
</YStack>
</XStack>
<PillBadge tone={selected ? 'success' : 'muted'}>
{selected ? t('packages.card.selected', 'Selected') : t('packages.card.select', 'Select package')}
</PillBadge>
</XStack>
<XStack flexWrap="wrap" space="$2">
{badges.map((badge) => (
<PillBadge key={badge} tone="muted">
{badge}
</PillBadge>
))}
</XStack>
{selected ? (
<XStack alignItems="center" space="$1">
<Check size={14} color={primary} />
<Text fontSize="$xs" color={primary} fontWeight="700">
{t('packages.card.selected', 'Selected')}
</Text>
</XStack>
) : null}
</MobileCard>
</Pressable>
);
}