upgrade to tamagui v2 and guest pwa overhaul
This commit is contained in:
@@ -93,7 +93,7 @@ export function DevTenantSwitcher({ bottomOffset = 16, variant = 'floating' }: D
|
||||
borderColor="rgba(234,179,8,0.5)"
|
||||
backgroundColor="rgba(255,255,255,0.95)"
|
||||
padding="$3"
|
||||
space="$2"
|
||||
gap="$2"
|
||||
borderRadius="$4"
|
||||
shadowColor="#f59e0b"
|
||||
shadowOpacity={0.25}
|
||||
@@ -102,7 +102,7 @@ export function DevTenantSwitcher({ bottomOffset = 16, variant = 'floating' }: D
|
||||
maxWidth={320}
|
||||
>
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Text fontSize={13} fontWeight="800" color="#92400e">
|
||||
Demo tenants
|
||||
</Text>
|
||||
@@ -119,7 +119,7 @@ export function DevTenantSwitcher({ bottomOffset = 16, variant = 'floating' }: D
|
||||
aria-label="Switcher minimieren"
|
||||
/>
|
||||
</XStack>
|
||||
<YStack space="$1">
|
||||
<YStack gap="$1">
|
||||
{DEV_TENANT_KEYS.map(({ key, label }) => (
|
||||
<Button
|
||||
key={key}
|
||||
@@ -162,7 +162,7 @@ export function DevTenantSwitcher({ bottomOffset = 16, variant = 'floating' }: D
|
||||
right="$4"
|
||||
zIndex={1000}
|
||||
maxWidth={320}
|
||||
space="$2"
|
||||
gap="$2"
|
||||
borderWidth={1}
|
||||
borderColor="rgba(234,179,8,0.5)"
|
||||
backgroundColor="rgba(255,255,255,0.95)"
|
||||
@@ -176,7 +176,7 @@ export function DevTenantSwitcher({ bottomOffset = 16, variant = 'floating' }: D
|
||||
style={{ bottom: bottomOffset + 70 }}
|
||||
>
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Text fontSize={13} fontWeight="800" color="#92400e">
|
||||
Demo tenants
|
||||
</Text>
|
||||
@@ -196,7 +196,7 @@ export function DevTenantSwitcher({ bottomOffset = 16, variant = 'floating' }: D
|
||||
<Text fontSize={11} color="#a16207">
|
||||
Select a seeded tenant to mint Sanctum PATs and jump straight into their admin space. Available only in development builds.
|
||||
</Text>
|
||||
<YStack space="$1">
|
||||
<YStack gap="$1">
|
||||
{DEV_TENANT_KEYS.map(({ key, label }) => (
|
||||
<Button
|
||||
key={key}
|
||||
|
||||
@@ -67,7 +67,7 @@ export default function AuthCallbackPage(): React.ReactElement {
|
||||
shadowRadius={16}
|
||||
shadowOffset={{ width: 0, height: 10 }}
|
||||
>
|
||||
<YStack alignItems="center" space="$2">
|
||||
<YStack alignItems="center" gap="$2">
|
||||
<Spinner size="small" color={textStrong} />
|
||||
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
||||
{t('processing.title', 'Signing you in …')}
|
||||
|
||||
@@ -274,9 +274,9 @@ export default function MobileBillingPage() {
|
||||
<ContextHelpLink slug="billing-packages-exports" />
|
||||
</XStack>
|
||||
{pendingCheckout && (checkoutStatus === 'failed' || checkoutStatus === 'cancelled') ? (
|
||||
<MobileCard borderColor={danger} backgroundColor="$red1" space="$2">
|
||||
<MobileCard borderColor={danger} backgroundColor="$red1" gap="$2">
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
<YStack space="$0.5" flex={1}>
|
||||
<YStack gap="$0.5" flex={1}>
|
||||
<Text fontSize="$sm" fontWeight="800" color={danger}>
|
||||
{t('billing.checkoutFailedTitle', 'Checkout failed')}
|
||||
</Text>
|
||||
@@ -296,7 +296,7 @@ export default function MobileBillingPage() {
|
||||
{t('billing.checkoutFailedBadge', 'Failed')}
|
||||
</PillBadge>
|
||||
</XStack>
|
||||
<XStack space="$2">
|
||||
<XStack gap="$2">
|
||||
<CTAButton
|
||||
label={t('billing.checkoutFailedRetry', 'Try again')}
|
||||
onPress={() => navigate(shopLink)}
|
||||
@@ -312,9 +312,9 @@ export default function MobileBillingPage() {
|
||||
</MobileCard>
|
||||
) : null}
|
||||
{pendingCheckout && checkoutStatus === 'requires_customer_action' ? (
|
||||
<MobileCard borderColor={accentSoft} backgroundColor={accentSoft} space="$2">
|
||||
<MobileCard borderColor={accentSoft} backgroundColor={accentSoft} gap="$2">
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
<YStack space="$0.5" flex={1}>
|
||||
<YStack gap="$0.5" flex={1}>
|
||||
<Text fontSize="$sm" fontWeight="800" color={textStrong}>
|
||||
{t('billing.checkoutActionTitle', 'Action required')}
|
||||
</Text>
|
||||
@@ -326,7 +326,7 @@ export default function MobileBillingPage() {
|
||||
{t('billing.checkoutActionBadge', 'Action needed')}
|
||||
</PillBadge>
|
||||
</XStack>
|
||||
<XStack space="$2">
|
||||
<XStack gap="$2">
|
||||
<CTAButton
|
||||
label={t('billing.checkoutActionButton', 'Continue checkout')}
|
||||
onPress={() => {
|
||||
@@ -348,9 +348,9 @@ export default function MobileBillingPage() {
|
||||
</MobileCard>
|
||||
) : null}
|
||||
{pendingCheckout && checkoutStatus !== 'failed' && checkoutStatus !== 'cancelled' && checkoutStatus !== 'requires_customer_action' ? (
|
||||
<MobileCard borderColor={accentSoft} backgroundColor={accentSoft} space="$2">
|
||||
<MobileCard borderColor={accentSoft} backgroundColor={accentSoft} gap="$2">
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
<YStack space="$0.5" flex={1}>
|
||||
<YStack gap="$0.5" flex={1}>
|
||||
<Text fontSize="$sm" fontWeight="800" color={textStrong}>
|
||||
{t('billing.checkoutPendingTitle', 'Activating your package')}
|
||||
</Text>
|
||||
@@ -365,7 +365,7 @@ export default function MobileBillingPage() {
|
||||
{t('billing.checkoutPendingBadge', 'Pending')}
|
||||
</PillBadge>
|
||||
</XStack>
|
||||
<XStack space="$2">
|
||||
<XStack gap="$2">
|
||||
<CTAButton label={t('billing.checkoutPendingRefresh', 'Refresh')} onPress={load} fullWidth={false} />
|
||||
<CTAButton
|
||||
label={t('billing.checkoutPendingDismiss', 'Dismiss')}
|
||||
@@ -377,8 +377,8 @@ export default function MobileBillingPage() {
|
||||
</MobileCard>
|
||||
) : null}
|
||||
|
||||
<MobileCard space="$2" ref={packagesRef as any}>
|
||||
<XStack alignItems="center" space="$2">
|
||||
<MobileCard gap="$2" ref={packagesRef as any}>
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Package size={18} color={textStrong} />
|
||||
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
||||
{t('billing.sections.packages.title', 'Packages')}
|
||||
@@ -397,7 +397,7 @@ export default function MobileBillingPage() {
|
||||
{t('common.loading', 'Lädt...')}
|
||||
</Text>
|
||||
) : (
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
{activePackage ? (
|
||||
<PackageCard
|
||||
pkg={activePackage}
|
||||
@@ -415,8 +415,8 @@ export default function MobileBillingPage() {
|
||||
)}
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$2" ref={invoicesRef as any}>
|
||||
<XStack alignItems="center" space="$2">
|
||||
<MobileCard gap="$2" ref={invoicesRef as any}>
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Receipt size={18} color={textStrong} />
|
||||
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
||||
{t('billing.sections.invoices.title', 'Invoices & Payments')}
|
||||
@@ -430,7 +430,7 @@ export default function MobileBillingPage() {
|
||||
{t('common.loading', 'Lädt...')}
|
||||
</Text>
|
||||
) : transactions.length === 0 ? (
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<Text fontSize="$sm" color={text}>
|
||||
{t('billing.sections.invoices.empty', 'Keine Zahlungen gefunden.')}
|
||||
</Text>
|
||||
@@ -438,7 +438,7 @@ export default function MobileBillingPage() {
|
||||
<CTAButton label={t('billing.actions.contactSupport', 'Contact support')} tone="ghost" onPress={openSupport} />
|
||||
</YStack>
|
||||
) : (
|
||||
<YStack space="$1.5">
|
||||
<YStack gap="$1.5">
|
||||
{transactions.slice(0, 8).map((trx) => (
|
||||
<XStack key={trx.id} alignItems="center" justifyContent="space-between" borderBottomWidth={1} borderColor={border} paddingVertical="$1.5">
|
||||
<YStack>
|
||||
@@ -475,8 +475,8 @@ export default function MobileBillingPage() {
|
||||
)}
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$2">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<MobileCard gap="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Sparkles size={18} color={textStrong} />
|
||||
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
||||
{t('billing.sections.addOns.title', 'Add-ons')}
|
||||
@@ -494,7 +494,7 @@ export default function MobileBillingPage() {
|
||||
{t('billing.sections.addOns.empty', 'Keine Add-ons gebucht.')}
|
||||
</Text>
|
||||
) : (
|
||||
<YStack space="$1.5">
|
||||
<YStack gap="$1.5">
|
||||
{addons.slice(0, 8).map((addon) => (
|
||||
<AddonRow key={addon.id} addon={addon} />
|
||||
))}
|
||||
@@ -550,7 +550,7 @@ function PackageCard({
|
||||
borderColor={isActive ? primary : border}
|
||||
borderWidth={isActive ? 2 : 1}
|
||||
backgroundColor={isActive ? accentSoft : undefined}
|
||||
space="$2"
|
||||
gap="$2"
|
||||
>
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
||||
@@ -563,7 +563,7 @@ function PackageCard({
|
||||
{expires}
|
||||
</Text>
|
||||
) : null}
|
||||
<XStack space="$2" marginTop="$2" flexWrap="wrap">
|
||||
<XStack gap="$2" marginTop="$2" flexWrap="wrap">
|
||||
<PillBadge tone="muted">{remainingText}</PillBadge>
|
||||
{pkg.price !== null && pkg.price !== undefined ? (
|
||||
<PillBadge tone="muted">{formatAmount(pkg.price, pkg.currency ?? 'EUR')}</PillBadge>
|
||||
@@ -578,7 +578,7 @@ function PackageCard({
|
||||
</Text>
|
||||
) : null}
|
||||
{limitEntries.length ? (
|
||||
<YStack space="$1.5" marginTop="$2">
|
||||
<YStack gap="$1.5" marginTop="$2">
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{t('mobileBilling.details.limitsTitle', 'Limits')}
|
||||
</Text>
|
||||
@@ -595,12 +595,12 @@ function PackageCard({
|
||||
</YStack>
|
||||
) : null}
|
||||
{featureKeys.length ? (
|
||||
<YStack space="$1.5" marginTop="$2">
|
||||
<YStack gap="$1.5" marginTop="$2">
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{t('mobileBilling.details.featuresTitle', 'Features')}
|
||||
</Text>
|
||||
{featureKeys.map((feature) => (
|
||||
<XStack key={feature} alignItems="center" space="$2">
|
||||
<XStack key={feature} alignItems="center" gap="$2">
|
||||
<Sparkles size={14} color={primary} />
|
||||
<Text fontSize="$xs" color={textStrong}>
|
||||
{getPackageFeatureLabel(feature, t)}
|
||||
@@ -610,7 +610,7 @@ function PackageCard({
|
||||
</YStack>
|
||||
) : null}
|
||||
{usageMetrics.length ? (
|
||||
<YStack space="$2" marginTop="$2">
|
||||
<YStack gap="$2" marginTop="$2">
|
||||
{usageMetrics.map((metric) => (
|
||||
<UsageBar key={metric.key} metric={metric} />
|
||||
))}
|
||||
@@ -684,12 +684,12 @@ function UsageBar({ metric }: { metric: PackageUsageMetric }) {
|
||||
const fillColor = status === 'danger' ? danger : status === 'warning' ? warningText : primary;
|
||||
|
||||
return (
|
||||
<YStack space="$1.5">
|
||||
<YStack gap="$1.5">
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{labelMap[metric.key]}
|
||||
</Text>
|
||||
<XStack alignItems="center" space="$1.5">
|
||||
<XStack alignItems="center" gap="$1.5">
|
||||
{statusLabel ? <PillBadge tone={status === 'danger' ? 'danger' : 'warning'}>{statusLabel}</PillBadge> : null}
|
||||
<Text fontSize="$xs" color={textStrong} fontWeight="700">
|
||||
{valueText}
|
||||
@@ -737,7 +737,7 @@ function AddonRow({ addon }: { addon: TenantAddonHistoryEntry }) {
|
||||
const eventPath = addon.event?.slug ? ADMIN_EVENT_VIEW_PATH(addon.event.slug) : null;
|
||||
const hasImpact = Boolean(addon.extra_photos || addon.extra_guests || addon.extra_gallery_days);
|
||||
const impactBadges = hasImpact ? (
|
||||
<XStack space="$2" marginTop="$1.5" flexWrap="wrap">
|
||||
<XStack gap="$2" marginTop="$1.5" flexWrap="wrap">
|
||||
{addon.extra_photos ? (
|
||||
<PillBadge tone="muted">{t('mobileBilling.extra.photos', '+{{count}} photos', { count: addon.extra_photos })}</PillBadge>
|
||||
) : null}
|
||||
@@ -751,7 +751,7 @@ function AddonRow({ addon }: { addon: TenantAddonHistoryEntry }) {
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<MobileCard borderColor={border} padding="$3" space="$1.5">
|
||||
<MobileCard borderColor={border} padding="$3" gap="$1.5">
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
|
||||
{addon.label ?? addon.addon_key}
|
||||
|
||||
@@ -389,7 +389,7 @@ export default function MobileBrandingPage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<MobileCard space="$3">
|
||||
<MobileCard gap="$3">
|
||||
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
|
||||
{t('events.watermark.previewTitle', 'Watermark Preview')}
|
||||
</Text>
|
||||
@@ -421,7 +421,7 @@ export default function MobileBrandingPage() {
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<MobileCard space="$3">
|
||||
<MobileCard gap="$3">
|
||||
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
||||
{t('events.watermark.title', 'Wasserzeichen')}
|
||||
</Text>
|
||||
@@ -449,14 +449,14 @@ export default function MobileBrandingPage() {
|
||||
</MobileField>
|
||||
|
||||
{resolvedMode === 'custom' && !controlsLocked ? (
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
|
||||
{t('events.watermark.upload', 'Wasserzeichen hochladen')}
|
||||
</Text>
|
||||
<Pressable onPress={() => document.getElementById('watermark-upload-input')?.click()}>
|
||||
<XStack
|
||||
alignItems="center"
|
||||
space="$2"
|
||||
gap="$2"
|
||||
paddingHorizontal="$3.5"
|
||||
paddingVertical="$2.5"
|
||||
borderRadius={12}
|
||||
@@ -520,7 +520,7 @@ export default function MobileBrandingPage() {
|
||||
) : null}
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$3">
|
||||
<MobileCard gap="$3">
|
||||
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
||||
{t('events.watermark.placement', 'Position & Größe')}
|
||||
</Text>
|
||||
@@ -603,8 +603,8 @@ export default function MobileBrandingPage() {
|
||||
<ContextHelpLink slug="event-branding-assets" />
|
||||
</XStack>
|
||||
|
||||
<MobileCard space="$2">
|
||||
<XStack space="$2">
|
||||
<MobileCard gap="$2">
|
||||
<XStack gap="$2">
|
||||
<TabButton label={t('events.branding.titleShort', 'Branding')} active={activeTab === 'branding'} onPress={() => setActiveTab('branding')} />
|
||||
<TabButton label={t('events.watermark.tab', 'Wasserzeichen')} active={activeTab === 'watermark'} onPress={() => setActiveTab('watermark')} />
|
||||
</XStack>
|
||||
@@ -612,20 +612,20 @@ export default function MobileBrandingPage() {
|
||||
|
||||
{activeTab === 'branding' ? (
|
||||
<>
|
||||
<MobileCard space="$3">
|
||||
<MobileCard gap="$3">
|
||||
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
|
||||
{t('events.branding.previewTitle', 'Guest App Preview')}
|
||||
</Text>
|
||||
<YStack borderRadius={16} borderWidth={1} borderColor={previewBorder} backgroundColor={previewBackground} padding="$3" space="$2" alignItems="center">
|
||||
<YStack borderRadius={16} borderWidth={1} borderColor={previewBorder} backgroundColor={previewBackground} padding="$3" gap="$2" alignItems="center">
|
||||
<YStack width="100%" borderRadius={12} backgroundColor={previewSurface} borderWidth={1} borderColor={previewBorder} overflow="hidden">
|
||||
<YStack
|
||||
height={64}
|
||||
style={{ background: `linear-gradient(135deg, ${previewForm.primary}, ${previewForm.accent})` }}
|
||||
/>
|
||||
<YStack padding="$3" space="$2">
|
||||
<YStack padding="$3" gap="$2">
|
||||
<XStack
|
||||
alignItems="center"
|
||||
space="$2"
|
||||
gap="$2"
|
||||
flexDirection={previewForm.logoPosition === 'center' ? 'column' : previewForm.logoPosition === 'right' ? 'row-reverse' : 'row'}
|
||||
justifyContent={previewForm.logoPosition === 'center' ? 'center' : 'flex-start'}
|
||||
>
|
||||
@@ -665,7 +665,7 @@ export default function MobileBrandingPage() {
|
||||
</Text>
|
||||
</YStack>
|
||||
</XStack>
|
||||
<XStack space="$2" marginTop="$1">
|
||||
<XStack gap="$2" marginTop="$1">
|
||||
<ColorSwatch color={previewForm.primary} label={t('events.branding.primary', 'Primary')} borderColor={previewBorder} />
|
||||
<ColorSwatch color={previewForm.accent} label={t('events.branding.accent', 'Accent')} borderColor={previewBorder} />
|
||||
<ColorSwatch color={previewBackground} label={t('events.branding.background', 'Background')} borderColor={previewBorder} />
|
||||
@@ -701,11 +701,11 @@ export default function MobileBrandingPage() {
|
||||
) : null}
|
||||
|
||||
<>
|
||||
<MobileCard space="$3">
|
||||
<MobileCard gap="$3">
|
||||
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
||||
{t('events.branding.mode', 'Theme')}
|
||||
</Text>
|
||||
<XStack space="$2">
|
||||
<XStack gap="$2">
|
||||
<ModeButton
|
||||
label={t('events.branding.modeLight', 'Light')}
|
||||
active={form.mode === 'light'}
|
||||
@@ -727,7 +727,7 @@ export default function MobileBrandingPage() {
|
||||
</XStack>
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$3">
|
||||
<MobileCard gap="$3">
|
||||
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
||||
{t('events.branding.colors', 'Colors')}
|
||||
</Text>
|
||||
@@ -757,7 +757,7 @@ export default function MobileBrandingPage() {
|
||||
/>
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$3">
|
||||
<MobileCard gap="$3">
|
||||
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
||||
{t('events.branding.fonts', 'Fonts')}
|
||||
</Text>
|
||||
@@ -786,7 +786,7 @@ export default function MobileBrandingPage() {
|
||||
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
|
||||
{t('events.branding.fontSize', 'Font Size')}
|
||||
</Text>
|
||||
<XStack space="$2">
|
||||
<XStack gap="$2">
|
||||
<ModeButton
|
||||
label={t('events.branding.fontSizeSmall', 'S')}
|
||||
active={form.fontSize === 's'}
|
||||
@@ -808,14 +808,14 @@ export default function MobileBrandingPage() {
|
||||
</XStack>
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$3">
|
||||
<MobileCard gap="$3">
|
||||
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
||||
{t('events.branding.logo', 'Logo')}
|
||||
</Text>
|
||||
<Text fontSize="$sm" color={muted}>
|
||||
{t('events.branding.logoHint', 'Upload a logo or use an emoji for the guest header.')}
|
||||
</Text>
|
||||
<XStack space="$2">
|
||||
<XStack gap="$2">
|
||||
<ModeButton
|
||||
label={t('events.branding.logoModeUpload', 'Upload')}
|
||||
active={form.logoMode === 'upload'}
|
||||
@@ -847,7 +847,7 @@ export default function MobileBrandingPage() {
|
||||
padding="$3"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
space="$2"
|
||||
gap="$2"
|
||||
>
|
||||
{form.logoDataUrl ? (
|
||||
<>
|
||||
@@ -856,7 +856,7 @@ export default function MobileBrandingPage() {
|
||||
alt={t('events.branding.logoAlt', 'Logo')}
|
||||
style={{ maxHeight: 80, maxWidth: '100%', objectFit: 'contain' }}
|
||||
/>
|
||||
<XStack space="$2">
|
||||
<XStack gap="$2">
|
||||
<CTAButton
|
||||
label={t('events.branding.replaceLogo', 'Replace logo')}
|
||||
onPress={() => document.getElementById('branding-logo-input')?.click()}
|
||||
@@ -868,7 +868,7 @@ export default function MobileBrandingPage() {
|
||||
>
|
||||
<XStack
|
||||
alignItems="center"
|
||||
space="$1.5"
|
||||
gap="$1.5"
|
||||
paddingHorizontal="$3"
|
||||
paddingVertical="$2"
|
||||
borderRadius={12}
|
||||
@@ -892,7 +892,7 @@ export default function MobileBrandingPage() {
|
||||
>
|
||||
<XStack
|
||||
alignItems="center"
|
||||
space="$2"
|
||||
gap="$2"
|
||||
paddingHorizontal="$3.5"
|
||||
paddingVertical="$2.5"
|
||||
borderRadius={12}
|
||||
@@ -939,7 +939,7 @@ export default function MobileBrandingPage() {
|
||||
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
|
||||
{t('events.branding.logoPosition', 'Position')}
|
||||
</Text>
|
||||
<XStack space="$2">
|
||||
<XStack gap="$2">
|
||||
<ModeButton
|
||||
label={t('events.branding.positionLeft', 'Left')}
|
||||
active={form.logoPosition === 'left'}
|
||||
@@ -962,7 +962,7 @@ export default function MobileBrandingPage() {
|
||||
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
|
||||
{t('events.branding.logoSize', 'Size')}
|
||||
</Text>
|
||||
<XStack space="$2">
|
||||
<XStack gap="$2">
|
||||
<ModeButton
|
||||
label={t('events.branding.logoSizeSmall', 'S')}
|
||||
active={form.logoSize === 's'}
|
||||
@@ -984,14 +984,14 @@ export default function MobileBrandingPage() {
|
||||
</XStack>
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$3">
|
||||
<MobileCard gap="$3">
|
||||
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
||||
{t('events.branding.buttons', 'Buttons & Links')}
|
||||
</Text>
|
||||
<Text fontSize="$sm" color={muted}>
|
||||
{t('events.branding.buttonsHint', 'Style, radius, and link color for CTA buttons.')}
|
||||
</Text>
|
||||
<XStack space="$2">
|
||||
<XStack gap="$2">
|
||||
<ModeButton
|
||||
label={t('events.branding.buttonFilled', 'Filled')}
|
||||
active={form.buttonStyle === 'filled'}
|
||||
@@ -1039,7 +1039,7 @@ export default function MobileBrandingPage() {
|
||||
renderWatermarkTab()
|
||||
)}
|
||||
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<CTAButton label={saving ? t('events.branding.saving', 'Saving...') : t('events.branding.save', 'Save Branding')} onPress={() => handleSave()} />
|
||||
<Pressable disabled={loading || saving} onPress={handleReset}>
|
||||
<XStack
|
||||
@@ -1050,7 +1050,7 @@ export default function MobileBrandingPage() {
|
||||
backgroundColor={surface}
|
||||
borderWidth={1}
|
||||
borderColor={border}
|
||||
space="$2"
|
||||
gap="$2"
|
||||
>
|
||||
<RefreshCcw size={16} color={textStrong} />
|
||||
<Text fontSize="$sm" color={textStrong} fontWeight="700">
|
||||
@@ -1067,7 +1067,7 @@ export default function MobileBrandingPage() {
|
||||
footer={null}
|
||||
bottomOffsetPx={120}
|
||||
>
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
{fontsLoading ? (
|
||||
Array.from({ length: 4 }).map((_, idx) => <SkeletonCard key={`font-sk-${idx}`} height={48} />)
|
||||
) : fonts.length === 0 ? (
|
||||
@@ -1228,11 +1228,11 @@ function ColorField({
|
||||
}) {
|
||||
const { textStrong, muted } = useAdminTheme();
|
||||
return (
|
||||
<YStack space="$2" opacity={disabled ? 0.6 : 1}>
|
||||
<YStack gap="$2" opacity={disabled ? 0.6 : 1}>
|
||||
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
|
||||
{label}
|
||||
</Text>
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<MobileColorInput
|
||||
value={value}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
@@ -1249,7 +1249,7 @@ function ColorField({
|
||||
function ColorSwatch({ color, label, borderColor }: { color: string; label: string; borderColor?: string }) {
|
||||
const { border, muted } = useAdminTheme();
|
||||
return (
|
||||
<YStack alignItems="center" space="$1">
|
||||
<YStack alignItems="center" gap="$1">
|
||||
<YStack width={40} height={40} borderRadius={12} borderWidth={1} borderColor={borderColor ?? border} backgroundColor={color} />
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{label}
|
||||
@@ -1276,7 +1276,7 @@ function InputField({
|
||||
const { primary } = useAdminTheme();
|
||||
return (
|
||||
<MobileField label={label}>
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<MobileInput
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
@@ -1316,7 +1316,7 @@ function LabeledSlider({
|
||||
}) {
|
||||
const { textStrong, muted, primary, border, surface } = useAdminTheme();
|
||||
return (
|
||||
<YStack space="$1.5">
|
||||
<YStack gap="$1.5">
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
|
||||
{label}
|
||||
@@ -1367,7 +1367,7 @@ function PositionGrid({
|
||||
];
|
||||
|
||||
return (
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
|
||||
Position
|
||||
</Text>
|
||||
@@ -1503,8 +1503,8 @@ function InfoBadge({ icon, text, tone = 'info' }: { icon?: React.ReactNode; text
|
||||
const color = tone === 'danger' ? dangerText : textStrong;
|
||||
|
||||
return (
|
||||
<MobileCard space="$2" backgroundColor={background} borderColor={border}>
|
||||
<XStack space="$2" alignItems="center">
|
||||
<MobileCard gap="$2" backgroundColor={background} borderColor={border}>
|
||||
<XStack gap="$2" alignItems="center">
|
||||
{icon}
|
||||
<Text fontSize="$sm" color={color}>
|
||||
{text}
|
||||
@@ -1529,7 +1529,7 @@ function UpgradeCard({
|
||||
|
||||
return (
|
||||
<MobileCard
|
||||
space="$4"
|
||||
gap="$4"
|
||||
padding="$6"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
@@ -1547,7 +1547,7 @@ function UpgradeCard({
|
||||
>
|
||||
<Lock size={32} color={primary} />
|
||||
</YStack>
|
||||
<YStack space="$2" alignItems="center">
|
||||
<YStack gap="$2" alignItems="center">
|
||||
<Text fontSize="$xl" fontWeight="900" color={textStrong} textAlign="center">
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
@@ -89,7 +89,7 @@ function SectionHeader({
|
||||
const subtitleSize = compact ? '$xs' : '$sm';
|
||||
const spacing = compact ? '$1' : '$1.5';
|
||||
return (
|
||||
<YStack space={spacing}>
|
||||
<YStack gap={spacing}>
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
<Text fontSize={titleSize} fontWeight="800" color={theme.textStrong}>
|
||||
{title}
|
||||
@@ -222,7 +222,7 @@ export default function MobileDashboardPage() {
|
||||
return (
|
||||
<MobileShell activeTab="home" title={t('mobileDashboard.title', 'Dashboard')}>
|
||||
<DashboardCard padding="$0">
|
||||
<YStack padding="$3" space="$2">
|
||||
<YStack padding="$3" gap="$2">
|
||||
<SectionHeader
|
||||
title={t('dashboard:overview.title', 'At a glance')}
|
||||
showSeparator={false}
|
||||
@@ -231,7 +231,7 @@ export default function MobileDashboardPage() {
|
||||
/>
|
||||
</YStack>
|
||||
<Separator backgroundColor={theme.border} opacity={0.6} />
|
||||
<YStack padding="$3" space="$2.5">
|
||||
<YStack padding="$3" gap="$2.5">
|
||||
{/* 1. LIFECYCLE HERO */}
|
||||
<LifecycleHero
|
||||
event={activeEvent}
|
||||
@@ -326,7 +326,7 @@ function LifecycleHero({
|
||||
|
||||
if (phase === 'live') {
|
||||
return (
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<Header />
|
||||
<DashboardCard
|
||||
variant={cardVariant}
|
||||
@@ -335,10 +335,10 @@ function LifecycleHero({
|
||||
borderColor="transparent"
|
||||
style={{ backgroundImage: 'linear-gradient(135deg, #4F46E5 0%, #4338CA 100%)' }}
|
||||
>
|
||||
<YStack space={isEmbedded ? '$2.5' : '$3'}>
|
||||
<YStack gap={isEmbedded ? '$2.5' : '$3'}>
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
<YStack space="$1">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<YStack gap="$1">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<YStack width={8} height={8} borderRadius={4} backgroundColor="#22C55E" />
|
||||
<Text color="white" fontWeight="700" fontSize="$xs" textTransform="uppercase" letterSpacing={1}>
|
||||
{t('dashboard:liveNow.status', 'Happening Now')}
|
||||
@@ -404,11 +404,11 @@ function LifecycleHero({
|
||||
|
||||
if (phase === 'post') {
|
||||
return (
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<Header />
|
||||
<DashboardCard variant={cardVariant} padding={cardPadding}>
|
||||
<YStack space={isEmbedded ? '$2.5' : '$3'}>
|
||||
<XStack alignItems="center" space="$2.5">
|
||||
<YStack gap={isEmbedded ? '$2.5' : '$3'}>
|
||||
<XStack alignItems="center" gap="$2.5">
|
||||
<YStack width={40} height={40} borderRadius={20} backgroundColor={theme.successText} alignItems="center" justifyContent="center">
|
||||
<CheckCircle2 size={20} color="white" />
|
||||
</YStack>
|
||||
@@ -427,7 +427,7 @@ function LifecycleHero({
|
||||
height={48}
|
||||
borderRadius={16}
|
||||
>
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Download size={16} color="white" />
|
||||
<Text fontSize="$sm" fontWeight="800" color="white">
|
||||
{t('events.recap.downloadAll', 'Download photos')}
|
||||
@@ -443,7 +443,7 @@ function LifecycleHero({
|
||||
height={48}
|
||||
borderRadius={16}
|
||||
>
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Text fontSize="$sm" fontWeight="800" color={theme.textStrong}>
|
||||
{t('events.recap.openRecap', 'Open recap')}
|
||||
</Text>
|
||||
@@ -458,7 +458,7 @@ function LifecycleHero({
|
||||
|
||||
// SETUP
|
||||
return (
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<Header />
|
||||
{showQuickControls ? (
|
||||
<XStack
|
||||
@@ -472,7 +472,7 @@ function LifecycleHero({
|
||||
paddingVertical="$2"
|
||||
>
|
||||
<Pressable onPress={() => navigate(adminPath(`/mobile/events/${event.slug}/edit`))}>
|
||||
<XStack alignItems="center" space="$1.5">
|
||||
<XStack alignItems="center" gap="$1.5">
|
||||
<Settings size={16} color={theme.primary} />
|
||||
<Text fontSize="$sm" fontWeight="700" color={theme.textStrong}>
|
||||
{t('dashboard:readiness.quickSettings', 'Event settings')}
|
||||
@@ -480,8 +480,8 @@ function LifecycleHero({
|
||||
</XStack>
|
||||
</Pressable>
|
||||
|
||||
<XStack alignItems="center" space="$3">
|
||||
<YStack alignItems="center" space="$1">
|
||||
<XStack alignItems="center" gap="$3">
|
||||
<YStack alignItems="center" gap="$1">
|
||||
<Text fontSize="$xs" color={theme.muted} textTransform="uppercase" letterSpacing={0.8}>
|
||||
{t('dashboard:readiness.publishToggle', 'Live')}
|
||||
</Text>
|
||||
@@ -499,13 +499,13 @@ function LifecycleHero({
|
||||
</XStack>
|
||||
) : null}
|
||||
<DashboardCard variant={cardVariant} padding={cardPadding}>
|
||||
<YStack space={isEmbedded ? '$2.5' : '$3'}>
|
||||
<YStack gap={isEmbedded ? '$2.5' : '$3'}>
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
<YStack>
|
||||
<Text fontSize="$xs" color={theme.muted} fontWeight="700" textTransform="uppercase">
|
||||
{t('dashboard:upcoming.status.planning', 'Countdown')}
|
||||
</Text>
|
||||
<Text fontSize="$2xl" fontWeight="900" color={theme.primary}>
|
||||
<Text fontSize="$xxl" fontWeight="900" color={theme.primary}>
|
||||
{daysToGo}{' '}
|
||||
<Text fontSize="$sm" color={theme.muted} fontWeight="500">
|
||||
{t('management:galleryStatus.daysLabel', 'days')}
|
||||
@@ -518,7 +518,7 @@ function LifecycleHero({
|
||||
</XStack>
|
||||
|
||||
{showNextStep && nextStep ? (
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<Text fontSize="$xs" fontWeight="700" color={theme.muted} textTransform="uppercase" letterSpacing={1}>
|
||||
{t('dashboard:readiness.nextStepTitle', 'Next step')}
|
||||
</Text>
|
||||
@@ -531,7 +531,7 @@ function LifecycleHero({
|
||||
paddingHorizontal="$3"
|
||||
onPress={() => navigate(adminPath(nextStep.targetPath))}
|
||||
title={
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Circle size={18} color={theme.primary} strokeWidth={2.5} />
|
||||
<Text fontSize="$sm" fontWeight="700" color={theme.textStrong}>
|
||||
{nextStep.label}
|
||||
@@ -546,7 +546,7 @@ function LifecycleHero({
|
||||
) : undefined
|
||||
}
|
||||
iconAfter={
|
||||
<XStack alignItems="center" space="$1">
|
||||
<XStack alignItems="center" gap="$1">
|
||||
<PillBadge tone="success">{nextStep.ctaLabel}</PillBadge>
|
||||
<ChevronRight size={16} color={theme.muted} />
|
||||
</XStack>
|
||||
@@ -665,7 +665,7 @@ function UnifiedToolGrid({ event, navigate, permissions, isMember, isCompleted }
|
||||
|
||||
return (
|
||||
<DashboardCard padding="$0">
|
||||
<YStack padding="$3.5" space="$2">
|
||||
<YStack padding="$3.5" gap="$2">
|
||||
<SectionHeader
|
||||
title={t('dashboard:quickActions.title', 'Quick actions')}
|
||||
subtitle={t('dashboard:quickActions.description', 'Jump straight to the most important actions.')}
|
||||
@@ -674,9 +674,9 @@ function UnifiedToolGrid({ event, navigate, permissions, isMember, isCompleted }
|
||||
/>
|
||||
</YStack>
|
||||
<Separator backgroundColor={theme.border} opacity={0.6} />
|
||||
<YStack padding="$3.5" space="$3">
|
||||
<YStack padding="$3.5" gap="$3">
|
||||
{sections.map((section) => (
|
||||
<YStack key={section.title} space="$2">
|
||||
<YStack key={section.title} gap="$2">
|
||||
<Text fontSize="$xs" fontWeight="700" color={theme.muted} textTransform="uppercase" letterSpacing={1}>
|
||||
{section.title}
|
||||
</Text>
|
||||
@@ -692,7 +692,7 @@ function UnifiedToolGrid({ event, navigate, permissions, isMember, isCompleted }
|
||||
paddingHorizontal="$3"
|
||||
onPress={() => navigate(adminPath(item.path))}
|
||||
title={
|
||||
<XStack alignItems="center" space="$2.5">
|
||||
<XStack alignItems="center" gap="$2.5">
|
||||
<XStack
|
||||
width={32}
|
||||
height={32}
|
||||
@@ -729,7 +729,7 @@ function RecentPhotosSection({ photos, navigate, slug }: { photos: TenantPhoto[]
|
||||
|
||||
return (
|
||||
<DashboardCard>
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
<Text fontSize="$xs" fontWeight="700" color={theme.muted} textTransform="uppercase" letterSpacing={1}>
|
||||
{t('photos.recentTitle', 'Latest Uploads')}
|
||||
@@ -749,7 +749,7 @@ function RecentPhotosSection({ photos, navigate, slug }: { photos: TenantPhoto[]
|
||||
|
||||
<Separator backgroundColor={theme.border} opacity={0.6} />
|
||||
|
||||
<XStack space="$2" overflow="scroll" paddingVertical="$1">
|
||||
<XStack gap="$2" overflow="scroll" paddingVertical="$1">
|
||||
{photos.map((photo) => (
|
||||
<Pressable key={photo.id} onPress={() => navigate(adminPath(`/mobile/events/${slug}/control-room`))}>
|
||||
<YStack
|
||||
@@ -762,7 +762,7 @@ function RecentPhotosSection({ photos, navigate, slug }: { photos: TenantPhoto[]
|
||||
borderColor={theme.border}
|
||||
>
|
||||
{photo.thumbnail_url ? (
|
||||
<Image source={{ uri: photo.thumbnail_url }} width={80} height={80} resizeMode="cover" />
|
||||
<Image src={photo.thumbnail_url} width={80} height={80} objectFit="cover" />
|
||||
) : (
|
||||
<YStack flex={1} alignItems="center" justifyContent="center">
|
||||
<ImageIcon size={20} color={theme.muted} />
|
||||
@@ -785,12 +785,12 @@ function AlertsSection({ event, stats, t }: any) {
|
||||
|
||||
return (
|
||||
<DashboardCard>
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<Text fontSize="$xs" fontWeight="700" color={theme.muted} textTransform="uppercase" letterSpacing={1}>
|
||||
{t('management:alertsTitle', 'Alerts')}
|
||||
</Text>
|
||||
<Separator backgroundColor={theme.border} opacity={0.6} />
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
{limitWarnings.map((w: any, idx: number) => {
|
||||
const isDanger = w.tone === 'danger';
|
||||
const bg = isDanger ? theme.dangerBg : theme.warningBg;
|
||||
@@ -807,7 +807,7 @@ function AlertsSection({ event, stats, t }: any) {
|
||||
borderWidth={1}
|
||||
borderColor={border}
|
||||
alignItems="center"
|
||||
space="$2"
|
||||
gap="$2"
|
||||
>
|
||||
<Icon size={16} color={text} />
|
||||
<Text fontSize="$sm" color={text} fontWeight="600">
|
||||
@@ -826,7 +826,7 @@ function EmptyState({ canManage, onCreate }: any) {
|
||||
const theme = useAdminTheme();
|
||||
const { t } = useTranslation(['management', 'mobile']);
|
||||
return (
|
||||
<YStack flex={1} alignItems="center" justifyContent="center" space="$4">
|
||||
<YStack flex={1} alignItems="center" justifyContent="center" gap="$4">
|
||||
<Sparkles size={48} color={theme.primary} />
|
||||
<Text fontSize="$lg" fontWeight="800" color={theme.textStrong} textAlign="center">
|
||||
{t('mobile:header.appName', 'Event Admin')}
|
||||
|
||||
@@ -192,14 +192,14 @@ export function DataExportsPanel({
|
||||
</MobileCard>
|
||||
) : null}
|
||||
|
||||
<MobileCard space="$2">
|
||||
<MobileCard gap="$2">
|
||||
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
||||
{t('dataExports.request.title', 'Export request')}
|
||||
</Text>
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{t('dataExports.request.hint', 'Export account data or a specific event archive.')}
|
||||
</Text>
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
{!isRecap ? (
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
<Text fontSize="$sm" color={text}>
|
||||
@@ -276,7 +276,7 @@ export function DataExportsPanel({
|
||||
/>
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$2">
|
||||
<MobileCard gap="$2">
|
||||
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
||||
{t('dataExports.history.title', 'Recent exports')}
|
||||
</Text>
|
||||
@@ -284,7 +284,7 @@ export function DataExportsPanel({
|
||||
{t('dataExports.history.hint', 'Latest 10 exports for your account and events.')}
|
||||
</Text>
|
||||
{loading ? (
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<SkeletonCard height={72} />
|
||||
<SkeletonCard height={72} />
|
||||
</YStack>
|
||||
@@ -293,9 +293,9 @@ export function DataExportsPanel({
|
||||
{t('dataExports.history.empty', 'No exports yet.')}
|
||||
</Text>
|
||||
) : (
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
{visibleExports.map((entry) => (
|
||||
<MobileCard key={entry.id} space="$2">
|
||||
<MobileCard key={entry.id} gap="$2">
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
<YStack>
|
||||
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
|
||||
|
||||
@@ -37,7 +37,7 @@ export default function MobileEventAnalyticsPage() {
|
||||
return (
|
||||
<MobileShell title={t('analytics.title', 'Analytics')} activeTab="home">
|
||||
<MobileCard
|
||||
space="$4"
|
||||
gap="$4"
|
||||
padding="$6"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
@@ -55,7 +55,7 @@ export default function MobileEventAnalyticsPage() {
|
||||
>
|
||||
<Lock size={32} color={primary} />
|
||||
</YStack>
|
||||
<YStack space="$2" alignItems="center">
|
||||
<YStack gap="$2" alignItems="center">
|
||||
<Text fontSize="$xl" fontWeight="900" color={textStrong} textAlign="center">
|
||||
{t('analytics.lockedTitle', 'Unlock Analytics')}
|
||||
</Text>
|
||||
@@ -75,7 +75,7 @@ export default function MobileEventAnalyticsPage() {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<MobileShell title={t('analytics.title', 'Analytics')} activeTab="home">
|
||||
<YStack space="$3">
|
||||
<YStack gap="$3">
|
||||
<SkeletonCard height={200} />
|
||||
<SkeletonCard height={150} />
|
||||
<SkeletonCard height={150} />
|
||||
@@ -116,12 +116,12 @@ export default function MobileEventAnalyticsPage() {
|
||||
activeTab="home"
|
||||
onBack={() => navigate(-1)}
|
||||
>
|
||||
<YStack space="$4">
|
||||
<YStack space="$2">
|
||||
<YStack gap="$4">
|
||||
<YStack gap="$2">
|
||||
<Text fontSize="$sm" fontWeight="800" color={textStrong}>
|
||||
{t('analytics.kpiTitle', 'Event snapshot')}
|
||||
</Text>
|
||||
<XStack space="$2" flexWrap="wrap">
|
||||
<XStack gap="$2" flexWrap="wrap">
|
||||
<KpiTile
|
||||
icon={TrendingUp}
|
||||
label={t('analytics.kpiUploads', 'Uploads')}
|
||||
@@ -140,14 +140,14 @@ export default function MobileEventAnalyticsPage() {
|
||||
</XStack>
|
||||
</YStack>
|
||||
{/* Activity Timeline */}
|
||||
<MobileCard space="$3" borderColor={border} backgroundColor={surface}>
|
||||
<XStack alignItems="center" space="$2">
|
||||
<MobileCard gap="$3" borderColor={border} backgroundColor={surface}>
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<TrendingUp size={18} color={primary} />
|
||||
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
||||
{t('analytics.activityTitle', 'Activity Timeline')}
|
||||
</Text>
|
||||
</XStack>
|
||||
<YStack space="$0.5">
|
||||
<YStack gap="$0.5">
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{t('analytics.timeframe', 'Last {{hours}} hours', { hours: timeframeHours })}
|
||||
</Text>
|
||||
@@ -159,7 +159,7 @@ export default function MobileEventAnalyticsPage() {
|
||||
</YStack>
|
||||
|
||||
{hasTimeline ? (
|
||||
<YStack height={180} justifyContent="flex-end" space="$2">
|
||||
<YStack height={180} justifyContent="flex-end" gap="$2">
|
||||
<XStack alignItems="flex-end" justifyContent="space-between" height={150} gap="$1">
|
||||
{timeline.map((point, index) => {
|
||||
const heightPercent = (point.count / maxTimelineCount) * 100;
|
||||
@@ -168,7 +168,7 @@ export default function MobileEventAnalyticsPage() {
|
||||
const showLabel = timeline.length < 8 || index % 3 === 0;
|
||||
|
||||
return (
|
||||
<YStack key={point.timestamp} flex={1} alignItems="center" space="$1">
|
||||
<YStack key={point.timestamp} flex={1} alignItems="center" gap="$1">
|
||||
<YStack
|
||||
width="100%"
|
||||
height={`${Math.max(heightPercent, 4)}%`}
|
||||
@@ -200,8 +200,8 @@ export default function MobileEventAnalyticsPage() {
|
||||
</MobileCard>
|
||||
|
||||
{/* Top Contributors */}
|
||||
<MobileCard space="$3" borderColor={border} backgroundColor={surface}>
|
||||
<XStack alignItems="center" space="$2">
|
||||
<MobileCard gap="$3" borderColor={border} backgroundColor={surface}>
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Trophy size={18} color={primary} />
|
||||
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
||||
{t('analytics.contributorsTitle', 'Top Contributors')}
|
||||
@@ -209,10 +209,10 @@ export default function MobileEventAnalyticsPage() {
|
||||
</XStack>
|
||||
|
||||
{hasContributors ? (
|
||||
<YStack space="$3">
|
||||
<YStack gap="$3">
|
||||
{contributors.map((contributor, idx) => (
|
||||
<XStack key={idx} alignItems="center" justifyContent="space-between" paddingVertical="$1">
|
||||
<XStack alignItems="center" space="$3">
|
||||
<XStack alignItems="center" gap="$3">
|
||||
<YStack
|
||||
width={28}
|
||||
height={28}
|
||||
@@ -250,8 +250,8 @@ export default function MobileEventAnalyticsPage() {
|
||||
</MobileCard>
|
||||
|
||||
{/* Task Stats */}
|
||||
<MobileCard space="$3" borderColor={border} backgroundColor={surface}>
|
||||
<XStack alignItems="center" space="$2">
|
||||
<MobileCard gap="$3" borderColor={border} backgroundColor={surface}>
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<ListTodo size={18} color={primary} />
|
||||
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
||||
{t('analytics.tasksTitle', 'Popular photo tasks')}
|
||||
@@ -259,11 +259,11 @@ export default function MobileEventAnalyticsPage() {
|
||||
</XStack>
|
||||
|
||||
{hasTasks ? (
|
||||
<YStack space="$3">
|
||||
<YStack gap="$3">
|
||||
{tasks.map((task) => {
|
||||
const percent = (task.count / maxTaskCount) * 100;
|
||||
return (
|
||||
<YStack key={task.task_id} space="$1">
|
||||
<YStack key={task.task_id} gap="$1">
|
||||
<XStack justifyContent="space-between">
|
||||
<Text fontSize="$sm" color={textStrong} numberOfLines={1} flex={1}>
|
||||
{task.task_name}
|
||||
@@ -308,7 +308,7 @@ function EmptyState({
|
||||
}) {
|
||||
const { muted } = useAdminTheme();
|
||||
return (
|
||||
<YStack padding="$4" alignItems="center" justifyContent="center" space="$2">
|
||||
<YStack padding="$4" alignItems="center" justifyContent="center" gap="$2">
|
||||
<Text fontSize="$sm" color={muted}>
|
||||
{message}
|
||||
</Text>
|
||||
|
||||
@@ -179,7 +179,7 @@ function PhotoGridTile({
|
||||
)}
|
||||
|
||||
{badges.length ? (
|
||||
<XStack position="absolute" top={6} left={6} space="$1.5">
|
||||
<XStack position="absolute" top={6} left={6} gap="$1.5">
|
||||
{badges.map((label) => (
|
||||
<PhotoStatusTag key={`${photo.id}-${label}`} label={label} />
|
||||
))}
|
||||
@@ -194,7 +194,7 @@ function PhotoGridTile({
|
||||
padding="$1"
|
||||
borderRadius={12}
|
||||
backgroundColor={overlayBg}
|
||||
space="$2"
|
||||
gap="$2"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
{actions.map((action) => (
|
||||
@@ -1034,7 +1034,7 @@ export default function MobileEventControlRoomPage() {
|
||||
}, [queuedActions, slug]);
|
||||
|
||||
const headerActions = (
|
||||
<XStack space="$2">
|
||||
<XStack gap="$2">
|
||||
<HeaderActionButton
|
||||
onPress={() => {
|
||||
if (activeTab === 'moderation') {
|
||||
@@ -1070,7 +1070,7 @@ export default function MobileEventControlRoomPage() {
|
||||
value={activeTab}
|
||||
onValueChange={(val) => setActiveTab(val as 'moderation' | 'live')}
|
||||
header={(
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<XStack justifyContent="flex-end">
|
||||
<ContextHelpLink slug="control-room-moderation" />
|
||||
</XStack>
|
||||
@@ -1094,8 +1094,8 @@ export default function MobileEventControlRoomPage() {
|
||||
</XStack>
|
||||
</Accordion.Trigger>
|
||||
<Accordion.Content {...({ paddingTop: '$2' } as any)}>
|
||||
<YStack space="$3">
|
||||
<YStack space="$2">
|
||||
<YStack gap="$3">
|
||||
<YStack gap="$2">
|
||||
<Text fontSize="$sm" fontWeight="800" color={text}>
|
||||
{t('controlRoom.automation.title', 'Automation')}
|
||||
</Text>
|
||||
@@ -1177,7 +1177,7 @@ export default function MobileEventControlRoomPage() {
|
||||
</MobileField>
|
||||
</YStack>
|
||||
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<Text fontSize="$sm" fontWeight="800" color={text}>
|
||||
{t('controlRoom.automation.uploaders.title', 'Uploader overrides')}
|
||||
</Text>
|
||||
@@ -1195,7 +1195,7 @@ export default function MobileEventControlRoomPage() {
|
||||
'Uploads from these devices skip the approval queue.',
|
||||
)}
|
||||
>
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<MobileSelect
|
||||
value={trustedUploaderSelection}
|
||||
onChange={(event) => setTrustedUploaderSelection(event.target.value)}
|
||||
@@ -1215,7 +1215,7 @@ export default function MobileEventControlRoomPage() {
|
||||
/>
|
||||
</YStack>
|
||||
{trustedUploaders.length ? (
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
{trustedUploaders.map((rule) => (
|
||||
<XStack
|
||||
key={`trusted-${rule.device_id}`}
|
||||
@@ -1227,7 +1227,7 @@ export default function MobileEventControlRoomPage() {
|
||||
borderColor={border}
|
||||
backgroundColor={surfaceMuted}
|
||||
>
|
||||
<YStack space="$0.5">
|
||||
<YStack gap="$0.5">
|
||||
<Text fontSize="$sm" fontWeight="700" color={text}>
|
||||
{rule.label ?? t('common.anonymous', 'Anonymous')}
|
||||
</Text>
|
||||
@@ -1267,7 +1267,7 @@ export default function MobileEventControlRoomPage() {
|
||||
'Uploads from these devices always need approval.',
|
||||
)}
|
||||
>
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<MobileSelect
|
||||
value={forceReviewSelection}
|
||||
onChange={(event) => setForceReviewSelection(event.target.value)}
|
||||
@@ -1287,7 +1287,7 @@ export default function MobileEventControlRoomPage() {
|
||||
/>
|
||||
</YStack>
|
||||
{forceReviewUploaders.length ? (
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
{forceReviewUploaders.map((rule) => (
|
||||
<XStack
|
||||
key={`force-${rule.device_id}`}
|
||||
@@ -1299,7 +1299,7 @@ export default function MobileEventControlRoomPage() {
|
||||
borderColor={border}
|
||||
backgroundColor={surfaceMuted}
|
||||
>
|
||||
<YStack space="$0.5">
|
||||
<YStack gap="$0.5">
|
||||
<Text fontSize="$sm" fontWeight="700" color={text}>
|
||||
{rule.label ?? t('common.anonymous', 'Anonymous')}
|
||||
</Text>
|
||||
@@ -1344,11 +1344,11 @@ export default function MobileEventControlRoomPage() {
|
||||
value: 'moderation',
|
||||
label: t('controlRoom.tabs.moderation', 'Moderation'),
|
||||
content: (
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
{queuedEventCount > 0 ? (
|
||||
<MobileCard>
|
||||
<XStack alignItems="center" justifyContent="space-between" space="$2">
|
||||
<YStack space="$1" flex={1}>
|
||||
<XStack alignItems="center" justifyContent="space-between" gap="$2">
|
||||
<YStack gap="$1" flex={1}>
|
||||
<Text fontSize="$sm" fontWeight="700" color={text}>
|
||||
{t('mobilePhotos.queueTitle', 'Changes waiting to sync')}
|
||||
</Text>
|
||||
@@ -1371,7 +1371,7 @@ export default function MobileEventControlRoomPage() {
|
||||
) : null}
|
||||
|
||||
<MobileCard>
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<Text fontSize="$xs" fontWeight="800" color={text}>
|
||||
{t('mobilePhotos.filtersTitle', 'Filter')}
|
||||
</Text>
|
||||
@@ -1390,7 +1390,7 @@ export default function MobileEventControlRoomPage() {
|
||||
value={moderationFilter}
|
||||
onValueChange={(value: string) => value && setModerationFilter(value as ModerationFilter)}
|
||||
>
|
||||
<XStack space="$1.5">
|
||||
<XStack gap="$1.5">
|
||||
{MODERATION_FILTERS.map((option) => {
|
||||
const active = option.value === moderationFilter;
|
||||
const count = moderationCounts[option.value] ?? 0;
|
||||
@@ -1407,7 +1407,7 @@ export default function MobileEventControlRoomPage() {
|
||||
paddingHorizontal="$3"
|
||||
pressStyle={{ opacity: 0.85 }}
|
||||
>
|
||||
<XStack alignItems="center" space="$1.5">
|
||||
<XStack alignItems="center" gap="$1.5">
|
||||
<Text fontSize="$xs" fontWeight="700" color={active ? '#fff' : muted}>
|
||||
{t(option.labelKey, option.fallback)}
|
||||
</Text>
|
||||
@@ -1455,13 +1455,13 @@ export default function MobileEventControlRoomPage() {
|
||||
) : null}
|
||||
|
||||
{moderationLoading && moderationPage === 1 ? (
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
{Array.from({ length: 3 }).map((_, idx) => (
|
||||
<SkeletonCard key={`moderation-skeleton-${idx}`} height={120} />
|
||||
))}
|
||||
</YStack>
|
||||
) : moderationPhotos.length === 0 ? (
|
||||
<MobileCard alignItems="center" justifyContent="center" space="$2">
|
||||
<MobileCard alignItems="center" justifyContent="center" gap="$2">
|
||||
<ImageIcon size={28} color={muted} />
|
||||
<Text fontSize="$sm" fontWeight="700" color={text}>
|
||||
{t('controlRoom.emptyModeration', 'No uploads match this filter.')}
|
||||
@@ -1543,7 +1543,7 @@ export default function MobileEventControlRoomPage() {
|
||||
value: 'live',
|
||||
label: t('controlRoom.tabs.live', 'Live Show'),
|
||||
content: (
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<MobileCard borderColor={border} backgroundColor="transparent">
|
||||
<Text fontSize="$sm" color={muted}>
|
||||
{t(
|
||||
@@ -1559,7 +1559,7 @@ export default function MobileEventControlRoomPage() {
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard>
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<Text fontSize="$xs" fontWeight="800" color={text}>
|
||||
{t('liveShowQueue.filterLabel', 'Live status')}
|
||||
</Text>
|
||||
@@ -1578,7 +1578,7 @@ export default function MobileEventControlRoomPage() {
|
||||
value={liveStatusFilter}
|
||||
onValueChange={(value: string) => value && setLiveStatusFilter(value as LiveShowQueueStatus)}
|
||||
>
|
||||
<XStack space="$1.5">
|
||||
<XStack gap="$1.5">
|
||||
{LIVE_STATUS_OPTIONS.map((option) => {
|
||||
const active = option.value === liveStatusFilter;
|
||||
const count = liveCounts[option.value] ?? 0;
|
||||
@@ -1595,7 +1595,7 @@ export default function MobileEventControlRoomPage() {
|
||||
paddingHorizontal="$3"
|
||||
pressStyle={{ opacity: 0.85 }}
|
||||
>
|
||||
<XStack alignItems="center" space="$1.5">
|
||||
<XStack alignItems="center" gap="$1.5">
|
||||
<Text fontSize="$xs" fontWeight="700" color={active ? '#fff' : muted}>
|
||||
{t(option.labelKey, option.fallback)}
|
||||
</Text>
|
||||
@@ -1631,13 +1631,13 @@ export default function MobileEventControlRoomPage() {
|
||||
) : null}
|
||||
|
||||
{liveLoading && livePage === 1 ? (
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
{Array.from({ length: 3 }).map((_, idx) => (
|
||||
<SkeletonCard key={`live-skeleton-${idx}`} height={120} />
|
||||
))}
|
||||
</YStack>
|
||||
) : livePhotos.length === 0 ? (
|
||||
<MobileCard alignItems="center" justifyContent="center" space="$2">
|
||||
<MobileCard alignItems="center" justifyContent="center" gap="$2">
|
||||
<Text fontSize="$sm" fontWeight="700" color={text}>
|
||||
{t('controlRoom.emptyLive', 'No photos waiting for Live Show.')}
|
||||
</Text>
|
||||
|
||||
@@ -377,7 +377,7 @@ export default function MobileEventFormPage() {
|
||||
|
||||
const requiredLabel = React.useCallback(
|
||||
(label: string) => (
|
||||
<XStack alignItems="center" space="$1">
|
||||
<XStack alignItems="center" gap="$1">
|
||||
<Text fontSize="$sm" fontWeight="800" color={text}>
|
||||
{label}
|
||||
</Text>
|
||||
@@ -407,7 +407,7 @@ export default function MobileEventFormPage() {
|
||||
<ContextHelpLink slug="event-settings" />
|
||||
</XStack>
|
||||
|
||||
<MobileCard space="$3">
|
||||
<MobileCard gap="$3">
|
||||
<MobileField label={requiredLabel(t('eventForm.fields.name.label', 'Event name'))}>
|
||||
<MobileInput
|
||||
type="text"
|
||||
@@ -471,7 +471,7 @@ export default function MobileEventFormPage() {
|
||||
) : null}
|
||||
|
||||
<MobileField label={requiredLabel(t('eventForm.fields.date.label', 'Date & time'))}>
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<MobileDateInput
|
||||
value={extractDateValue(form.date)}
|
||||
onChange={handleDateChange}
|
||||
@@ -511,7 +511,7 @@ export default function MobileEventFormPage() {
|
||||
</MobileField>
|
||||
|
||||
<MobileField label={t('eventForm.fields.location.label', 'Location')}>
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<MobileInput
|
||||
type="text"
|
||||
value={form.location}
|
||||
@@ -524,7 +524,7 @@ export default function MobileEventFormPage() {
|
||||
</MobileField>
|
||||
|
||||
<MobileField label={t('eventForm.fields.publish.label', 'Publish immediately')}>
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Switch
|
||||
checked={form.published}
|
||||
onCheckedChange={(checked: boolean) =>
|
||||
@@ -543,7 +543,7 @@ export default function MobileEventFormPage() {
|
||||
</MobileField>
|
||||
|
||||
<MobileField label={t('eventForm.fields.tasksMode.label', 'Photo tasks & challenges')}>
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Switch
|
||||
checked={form.tasksEnabled}
|
||||
onCheckedChange={(checked: boolean) =>
|
||||
@@ -574,7 +574,7 @@ export default function MobileEventFormPage() {
|
||||
</MobileField>
|
||||
|
||||
<MobileField label={t('eventForm.fields.uploadVisibility.label', 'Uploads visible immediately')}>
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Switch
|
||||
checked={form.autoApproveUploads}
|
||||
onCheckedChange={(checked: boolean) =>
|
||||
@@ -605,7 +605,7 @@ export default function MobileEventFormPage() {
|
||||
</MobileField>
|
||||
</MobileCard>
|
||||
|
||||
<YStack space="$2" paddingBottom="$10">
|
||||
<YStack gap="$2" paddingBottom="$10">
|
||||
{!isEdit ? (
|
||||
<CTAButton
|
||||
label={t('eventForm.actions.create', 'Create event')}
|
||||
|
||||
@@ -204,11 +204,11 @@ export default function MobileEventGuestNotificationsPage() {
|
||||
) : null}
|
||||
|
||||
<YStack ref={formRef}>
|
||||
<MobileCard space="$3">
|
||||
<MobileCard gap="$3">
|
||||
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
||||
{t('guestMessages.composeTitle', 'Send a message')}
|
||||
</Text>
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<MobileField label={t('guestMessages.form.title', 'Title')}>
|
||||
<MobileInput
|
||||
type="text"
|
||||
@@ -244,7 +244,7 @@ export default function MobileEventGuestNotificationsPage() {
|
||||
</MobileField>
|
||||
) : null}
|
||||
<MobileField label={t('guestMessages.form.cta', 'CTA (optional)')}>
|
||||
<YStack space="$1.5">
|
||||
<YStack gap="$1.5">
|
||||
<MobileInput
|
||||
type="text"
|
||||
value={form.cta_label}
|
||||
@@ -262,7 +262,7 @@ export default function MobileEventGuestNotificationsPage() {
|
||||
</Text>
|
||||
</YStack>
|
||||
</MobileField>
|
||||
<XStack space="$2">
|
||||
<XStack gap="$2">
|
||||
<MobileField label={t('guestMessages.form.expiresIn', 'Expires in (minutes)')}>
|
||||
<MobileInput
|
||||
type="number"
|
||||
@@ -301,7 +301,7 @@ export default function MobileEventGuestNotificationsPage() {
|
||||
</MobileCard>
|
||||
</YStack>
|
||||
|
||||
<MobileCard space="$2">
|
||||
<MobileCard gap="$2">
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
||||
{t('guestMessages.historyTitle', 'Recent messages')}
|
||||
@@ -312,13 +312,13 @@ export default function MobileEventGuestNotificationsPage() {
|
||||
</XStack>
|
||||
|
||||
{loading ? (
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
{Array.from({ length: 3 }).map((_, idx) => (
|
||||
<SkeletonCard key={`s-${idx}`} height={72} />
|
||||
))}
|
||||
</YStack>
|
||||
) : history.length === 0 ? (
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
|
||||
{t('guestMessages.emptyTitle', 'Send your first guest message')}
|
||||
</Text>
|
||||
@@ -332,14 +332,14 @@ export default function MobileEventGuestNotificationsPage() {
|
||||
/>
|
||||
</YStack>
|
||||
) : (
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
{history.map((item) => (
|
||||
<MobileCard key={item.id} space="$2" borderColor={border}>
|
||||
<MobileCard key={item.id} gap="$2" borderColor={border}>
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
<Text fontSize="$sm" fontWeight="800" color={textStrong}>
|
||||
{item.title || t('guestMessages.history.untitled', 'Untitled')}
|
||||
</Text>
|
||||
<XStack space="$1.5" alignItems="center">
|
||||
<XStack gap="$1.5" alignItems="center">
|
||||
<PillBadge tone={item.status === 'active' ? 'success' : 'muted'}>
|
||||
{t(`guestMessages.status.${item.status}`, item.status)}
|
||||
</PillBadge>
|
||||
@@ -354,11 +354,11 @@ export default function MobileEventGuestNotificationsPage() {
|
||||
{item.body ?? t('guestMessages.history.noBody', 'No body provided.')}
|
||||
</Text>
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
<XStack space="$1.5" alignItems="center">
|
||||
<XStack gap="$1.5" alignItems="center">
|
||||
<PillBadge tone="muted">{t(`guestMessages.type.${item.type}`, item.type)}</PillBadge>
|
||||
{item.target_identifier ? (
|
||||
<PillBadge tone="muted">
|
||||
<XStack alignItems="center" space="$1.5">
|
||||
<XStack alignItems="center" gap="$1.5">
|
||||
<User size={12} color={muted} />
|
||||
<Text fontSize="$xs" fontWeight="700" color={muted}>
|
||||
{item.target_identifier}
|
||||
@@ -367,7 +367,7 @@ export default function MobileEventGuestNotificationsPage() {
|
||||
</PillBadge>
|
||||
) : (
|
||||
<PillBadge tone="muted">
|
||||
<XStack alignItems="center" space="$1.5">
|
||||
<XStack alignItems="center" gap="$1.5">
|
||||
<Users size={12} color={muted} />
|
||||
<Text fontSize="$xs" fontWeight="700" color={muted}>
|
||||
{t('guestMessages.audience.all', 'All guests')}
|
||||
|
||||
@@ -285,15 +285,15 @@ export default function MobileEventLiveShowSettingsPage() {
|
||||
</XStack>
|
||||
|
||||
{loading ? (
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
{Array.from({ length: 3 }).map((_, idx) => (
|
||||
<SkeletonCard key={`ls-skel-${idx}`} height={110} />
|
||||
))}
|
||||
</YStack>
|
||||
) : (
|
||||
<YStack space="$2">
|
||||
<MobileCard space="$2">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<YStack gap="$2">
|
||||
<MobileCard gap="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Link2 size={18} color={text} />
|
||||
<Text fontSize="$md" fontWeight="800" color={text}>
|
||||
{t('liveShowSettings.link.title', 'Live Show link')}
|
||||
@@ -313,7 +313,7 @@ export default function MobileEventLiveShowSettingsPage() {
|
||||
{t('liveShowSettings.link.empty', 'No Live Show link available.')}
|
||||
</Text>
|
||||
)}
|
||||
<XStack space="$2" marginTop="$2" alignItems="center" flexWrap="nowrap">
|
||||
<XStack gap="$2" marginTop="$2" alignItems="center" flexWrap="nowrap">
|
||||
<IconAction
|
||||
label={t('liveShowSettings.link.copy', 'Copy')}
|
||||
disabled={!liveShowLink?.url}
|
||||
@@ -344,7 +344,7 @@ export default function MobileEventLiveShowSettingsPage() {
|
||||
</IconAction>
|
||||
</XStack>
|
||||
{liveShowLink?.qr_code_data_url ? (
|
||||
<XStack space="$2" alignItems="center" marginTop="$2" flexWrap="wrap">
|
||||
<XStack gap="$2" alignItems="center" marginTop="$2" flexWrap="wrap">
|
||||
<Pressable
|
||||
onPress={() => downloadQr(liveShowLink.qr_code_data_url!, 'live-show-qr.png')}
|
||||
title={t('liveShowSettings.link.downloadQr', 'Download QR')}
|
||||
@@ -373,8 +373,8 @@ export default function MobileEventLiveShowSettingsPage() {
|
||||
) : null}
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$2">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<MobileCard gap="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Settings size={18} color={text} />
|
||||
<Text fontSize="$md" fontWeight="800" color={text}>
|
||||
{t('liveShowSettings.title', 'Live Show settings')}
|
||||
@@ -385,7 +385,7 @@ export default function MobileEventLiveShowSettingsPage() {
|
||||
</Text>
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$3">
|
||||
<MobileCard gap="$3">
|
||||
<Text fontSize="$sm" fontWeight="800" color={text}>
|
||||
{t('liveShowSettings.sections.moderation', 'Moderation')}
|
||||
</Text>
|
||||
@@ -423,7 +423,7 @@ export default function MobileEventLiveShowSettingsPage() {
|
||||
</MobileField>
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$3">
|
||||
<MobileCard gap="$3">
|
||||
<Text fontSize="$sm" fontWeight="800" color={text}>
|
||||
{t('liveShowSettings.sections.playback', 'Playback')}
|
||||
</Text>
|
||||
@@ -478,7 +478,7 @@ export default function MobileEventLiveShowSettingsPage() {
|
||||
) : null}
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$3">
|
||||
<MobileCard gap="$3">
|
||||
<Text fontSize="$sm" fontWeight="800" color={text}>
|
||||
{t('liveShowSettings.sections.effects', 'Effects & layout')}
|
||||
</Text>
|
||||
@@ -661,7 +661,7 @@ function EffectSlider({
|
||||
const { text, muted, primary, border, surface } = useAdminTheme();
|
||||
|
||||
return (
|
||||
<YStack space="$1.5">
|
||||
<YStack gap="$1.5">
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
<Text fontSize="$sm" fontWeight="700" color={text}>
|
||||
{label}
|
||||
|
||||
@@ -163,11 +163,11 @@ export default function MobileEventMembersPage() {
|
||||
<ContextHelpLink slug="event-team-invites" />
|
||||
</XStack>
|
||||
|
||||
<MobileCard space="$3">
|
||||
<MobileCard gap="$3">
|
||||
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
||||
{t('events.members.inviteTitle', 'Invite Member')}
|
||||
</Text>
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<MobileField label={t('events.members.name', 'Name')}>
|
||||
<MobileInput
|
||||
type="text"
|
||||
@@ -223,11 +223,11 @@ export default function MobileEventMembersPage() {
|
||||
) : null}
|
||||
|
||||
{members.length > 0 ? (
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<Text fontSize="$xs" fontWeight="700" color={muted}>
|
||||
{t('events.members.filters.statusLabel', 'Status')}
|
||||
</Text>
|
||||
<XStack space="$2" flexWrap="wrap">
|
||||
<XStack gap="$2" flexWrap="wrap">
|
||||
{statusOptions.map((option) => {
|
||||
const isActive = statusFilter === option.key;
|
||||
return (
|
||||
@@ -251,7 +251,7 @@ export default function MobileEventMembersPage() {
|
||||
<Text fontSize="$xs" fontWeight="700" color={muted}>
|
||||
{t('events.members.filters.roleLabel', 'Role')}
|
||||
</Text>
|
||||
<XStack space="$2" flexWrap="wrap">
|
||||
<XStack gap="$2" flexWrap="wrap">
|
||||
{roleOptions.map((option) => {
|
||||
const isActive = roleFilter === option.key;
|
||||
return (
|
||||
@@ -275,18 +275,18 @@ export default function MobileEventMembersPage() {
|
||||
</YStack>
|
||||
) : null}
|
||||
|
||||
<MobileCard space="$3">
|
||||
<MobileCard gap="$3">
|
||||
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
||||
{t('events.members.listTitle', 'Team & Guests')}
|
||||
</Text>
|
||||
{loading ? (
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
{Array.from({ length: 4 }).map((_, idx) => (
|
||||
<SkeletonCard key={`m-${idx}`} height={70} />
|
||||
))}
|
||||
</YStack>
|
||||
) : members.length === 0 ? (
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
|
||||
{t('events.members.emptyTitle', 'Invite your team')}
|
||||
</Text>
|
||||
@@ -295,9 +295,9 @@ export default function MobileEventMembersPage() {
|
||||
</Text>
|
||||
</YStack>
|
||||
) : (
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
{filteredMembers.length === 0 ? (
|
||||
<YStack space="$1.5" padding="$2">
|
||||
<YStack gap="$1.5" padding="$2">
|
||||
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
|
||||
{t('events.members.emptyFilteredTitle', 'No matching members')}
|
||||
</Text>
|
||||
@@ -322,14 +322,14 @@ export default function MobileEventMembersPage() {
|
||||
return (
|
||||
<MobileCard key={member.id} padding="$3" borderColor={border}>
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
<YStack space="$1">
|
||||
<YStack gap="$1">
|
||||
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
|
||||
{member.name || member.email || t('events.members.fallbackName', 'Guest')}
|
||||
</Text>
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{member.email ?? ''}
|
||||
</Text>
|
||||
<XStack space="$1.5" alignItems="center">
|
||||
<XStack gap="$1.5" alignItems="center">
|
||||
<PillBadge tone={statusInfo.tone}>
|
||||
{statusInfo.label}
|
||||
</PillBadge>
|
||||
@@ -340,7 +340,7 @@ export default function MobileEventMembersPage() {
|
||||
</PillBadge>
|
||||
</XStack>
|
||||
</YStack>
|
||||
<XStack space="$2">
|
||||
<XStack gap="$2">
|
||||
<Pressable
|
||||
aria-label={t('events.members.copyEmailLabel', 'Copy email')}
|
||||
onPress={async () => {
|
||||
|
||||
@@ -191,15 +191,15 @@ export default function MobileEventPhotoboothPage() {
|
||||
) : null}
|
||||
|
||||
{loading ? (
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
{Array.from({ length: 3 }).map((_, idx) => (
|
||||
<SkeletonCard key={`ph-skel-${idx}`} height={110} />
|
||||
))}
|
||||
</YStack>
|
||||
) : (
|
||||
<YStack space="$2">
|
||||
<MobileCard space="$3">
|
||||
<YStack space="$1">
|
||||
<YStack gap="$2">
|
||||
<MobileCard gap="$3">
|
||||
<YStack gap="$1">
|
||||
<Text fontSize="$sm" fontWeight="800" color={text}>
|
||||
{t('photobooth.steps.activate.title', '1. Photobooth aktivieren')}
|
||||
</Text>
|
||||
@@ -207,7 +207,7 @@ export default function MobileEventPhotoboothPage() {
|
||||
{t('photobooth.steps.activate.description', 'Schalte den Upload-Zugang fuer dieses Event frei.')}
|
||||
</Text>
|
||||
</YStack>
|
||||
<XStack alignItems="center" justifyContent="space-between" space="$3" flexWrap="wrap">
|
||||
<XStack alignItems="center" justifyContent="space-between" gap="$3" flexWrap="wrap">
|
||||
<PillBadge tone={isActive ? 'success' : 'warning'}>
|
||||
{isActive ? t('photobooth.status.badgeActive', 'ACTIVE') : t('photobooth.status.badgeInactive', 'INACTIVE')}
|
||||
</PillBadge>
|
||||
@@ -215,7 +215,7 @@ export default function MobileEventPhotoboothPage() {
|
||||
{t('photobooth.mode.active', 'Current: {{mode}}', { mode: modeLabel })}
|
||||
</Text>
|
||||
</XStack>
|
||||
<XStack space="$2" marginTop="$2">
|
||||
<XStack gap="$2" marginTop="$2">
|
||||
<CTAButton
|
||||
label={isActive ? t('photobooth.actions.disable', 'Disable uploads') : t('photobooth.actions.enable', 'Enable uploads')}
|
||||
onPress={() => (isActive ? handleDisable() : handleEnable())}
|
||||
@@ -237,8 +237,8 @@ export default function MobileEventPhotoboothPage() {
|
||||
</XStack>
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$3">
|
||||
<YStack space="$1">
|
||||
<MobileCard gap="$3">
|
||||
<YStack gap="$1">
|
||||
<Text fontSize="$sm" fontWeight="800" color={text}>
|
||||
{t('photobooth.steps.download.title', '2. Uploader App herunterladen')}
|
||||
</Text>
|
||||
@@ -249,7 +249,7 @@ export default function MobileEventPhotoboothPage() {
|
||||
)}
|
||||
</Text>
|
||||
</YStack>
|
||||
<XStack space="$2" marginTop="$2" flexWrap="wrap">
|
||||
<XStack gap="$2" marginTop="$2" flexWrap="wrap">
|
||||
<CTAButton
|
||||
label={t('photobooth.uploaderDownload.actionWindows', 'Uploader herunterladen (Windows)')}
|
||||
onPress={() => {
|
||||
@@ -278,7 +278,7 @@ export default function MobileEventPhotoboothPage() {
|
||||
fullWidth={false}
|
||||
/>
|
||||
</XStack>
|
||||
<XStack space="$2" marginTop="$2">
|
||||
<XStack gap="$2" marginTop="$2">
|
||||
<CTAButton
|
||||
label={
|
||||
sendingEmail
|
||||
@@ -294,8 +294,8 @@ export default function MobileEventPhotoboothPage() {
|
||||
</XStack>
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$3">
|
||||
<YStack space="$1">
|
||||
<MobileCard gap="$3">
|
||||
<YStack gap="$1">
|
||||
<Text fontSize="$sm" fontWeight="800" color={text}>
|
||||
{t('photobooth.steps.access.title', '3. Verbindungscode erstellen')}
|
||||
</Text>
|
||||
@@ -303,7 +303,7 @@ export default function MobileEventPhotoboothPage() {
|
||||
{t('photobooth.steps.access.description', 'Der Code verbindet die App sicher mit deinem Event.')}
|
||||
</Text>
|
||||
</YStack>
|
||||
<XStack space="$2" marginTop="$2">
|
||||
<XStack gap="$2" marginTop="$2">
|
||||
<CTAButton
|
||||
label={
|
||||
connectLoading
|
||||
@@ -326,7 +326,7 @@ export default function MobileEventPhotoboothPage() {
|
||||
fullWidth={false}
|
||||
/>
|
||||
</XStack>
|
||||
<YStack space="$2" marginTop="$2">
|
||||
<YStack gap="$2" marginTop="$2">
|
||||
{connectCode ? (
|
||||
<CredentialRow label={t('photobooth.connectCode.label', 'Connect code')} value={connectCode} border={border} />
|
||||
) : null}
|
||||
@@ -338,7 +338,7 @@ export default function MobileEventPhotoboothPage() {
|
||||
</Text>
|
||||
) : null}
|
||||
{showCredentials ? (
|
||||
<YStack space="$1">
|
||||
<YStack gap="$1">
|
||||
<CredentialRow label={t('photobooth.credentials.postUrl', 'Upload URL')} value={uploadUrl ?? '—'} border={border} />
|
||||
<CredentialRow label={t('photobooth.credentials.username', 'Username')} value={username ?? '—'} border={border} />
|
||||
<CredentialRow label={t('photobooth.credentials.password', 'Password')} value={password ?? '—'} border={border} masked />
|
||||
@@ -354,11 +354,11 @@ export default function MobileEventPhotoboothPage() {
|
||||
</YStack>
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$2">
|
||||
<MobileCard gap="$2">
|
||||
<Text fontSize="$sm" fontWeight="700" color={text}>
|
||||
{t('photobooth.status.heading', 'Status')}
|
||||
</Text>
|
||||
<YStack space="$1">
|
||||
<YStack gap="$1">
|
||||
<StatusRow icon={<ShieldCheck size={16} color={text} />} label={t('photobooth.status.mode', 'Mode')} value={modeLabel} />
|
||||
<StatusRow
|
||||
icon={<PlugZap size={16} color={text} />}
|
||||
@@ -437,7 +437,7 @@ function StatusRow({ icon, label, value }: { icon: React.ReactNode; label: strin
|
||||
const { text } = useAdminTheme();
|
||||
return (
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
{icon}
|
||||
<Text fontSize="$sm" color={text}>
|
||||
{label}
|
||||
|
||||
@@ -170,7 +170,7 @@ export default function MobileEventRecapPage() {
|
||||
if (loading) {
|
||||
return (
|
||||
<MobileShell activeTab="home" title={t('events.recap.title', 'Event Recap')} onBack={back}>
|
||||
<YStack space="$3">
|
||||
<YStack gap="$3">
|
||||
<SkeletonCard height={120} />
|
||||
<SkeletonCard height={200} />
|
||||
<SkeletonCard height={150} />
|
||||
@@ -207,8 +207,8 @@ export default function MobileEventRecapPage() {
|
||||
title={t('events.recap.title', 'Event Recap')}
|
||||
onBack={back}
|
||||
>
|
||||
<YStack space="$4">
|
||||
<XStack space="$2">
|
||||
<YStack gap="$4">
|
||||
<XStack gap="$2">
|
||||
<TabButton
|
||||
label={t('events.recap.tabs.overview', 'Overview')}
|
||||
active={activeTab === 'overview'}
|
||||
@@ -227,10 +227,10 @@ export default function MobileEventRecapPage() {
|
||||
</XStack>
|
||||
|
||||
{activeTab === 'overview' ? (
|
||||
<YStack space="$4">
|
||||
<MobileCard space="$3">
|
||||
<YStack gap="$4">
|
||||
<MobileCard gap="$3">
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
<YStack space="$1">
|
||||
<YStack gap="$1">
|
||||
<Text fontSize="$xl" fontWeight="800" color={textStrong}>
|
||||
{t('events.recap.completedTitle', 'Event abgeschlossen')}
|
||||
</Text>
|
||||
@@ -248,8 +248,8 @@ export default function MobileEventRecapPage() {
|
||||
</XStack>
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$3">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<MobileCard gap="$3">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Share2 size={18} color={primary} />
|
||||
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
||||
{t('events.recap.shareGuests', 'Gäste-Galerie teilen')}
|
||||
@@ -260,7 +260,7 @@ export default function MobileEventRecapPage() {
|
||||
</Text>
|
||||
|
||||
{guestLink ? (
|
||||
<YStack space="$2" marginTop="$1">
|
||||
<YStack gap="$2" marginTop="$1">
|
||||
<XStack
|
||||
backgroundColor={border}
|
||||
padding="$3"
|
||||
@@ -292,7 +292,7 @@ export default function MobileEventRecapPage() {
|
||||
)}
|
||||
|
||||
{guestLink && activeInvite?.qr_code_data_url ? (
|
||||
<YStack alignItems="center" space="$2" marginTop="$2">
|
||||
<YStack alignItems="center" gap="$2" marginTop="$2">
|
||||
<YStack
|
||||
padding="$2"
|
||||
backgroundColor="white"
|
||||
@@ -315,15 +315,15 @@ export default function MobileEventRecapPage() {
|
||||
) : null}
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$3">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<MobileCard gap="$3">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Users size={18} color={primary} />
|
||||
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
||||
{t('events.recap.settings', 'Nachlauf-Optionen')}
|
||||
</Text>
|
||||
</XStack>
|
||||
|
||||
<YStack space="$1.5">
|
||||
<YStack gap="$1.5">
|
||||
<ToggleOption
|
||||
label={t('events.recap.allowDownloads', 'Gäste dürfen Fotos laden')}
|
||||
value={Boolean(event.settings?.guest_downloads_enabled)}
|
||||
@@ -337,8 +337,8 @@ export default function MobileEventRecapPage() {
|
||||
</YStack>
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$3">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<MobileCard gap="$3">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Sparkles size={18} color={primary} />
|
||||
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
||||
{t('events.recap.addons', 'Galerie verlängern')}
|
||||
@@ -348,7 +348,7 @@ export default function MobileEventRecapPage() {
|
||||
{t('events.recap.addonBody', 'Die Online-Zeit deiner Galerie neigt sich dem Ende? Hier kannst du sie verlängern.')}
|
||||
</Text>
|
||||
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
{addons
|
||||
.filter((a) => a.key === 'gallery_extension')
|
||||
.map((addon) => (
|
||||
@@ -365,9 +365,9 @@ export default function MobileEventRecapPage() {
|
||||
) : null}
|
||||
|
||||
{activeTab === 'engagement' ? (
|
||||
<YStack space="$4">
|
||||
<YStack gap="$4">
|
||||
{engagementLoading ? (
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<SkeletonCard height={140} />
|
||||
<SkeletonCard height={180} />
|
||||
<SkeletonCard height={180} />
|
||||
@@ -387,9 +387,9 @@ export default function MobileEventRecapPage() {
|
||||
</Text>
|
||||
</MobileCard>
|
||||
) : (
|
||||
<YStack space="$4">
|
||||
<MobileCard space="$3">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<YStack gap="$4">
|
||||
<MobileCard gap="$3">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<TrendingUp size={18} color={primary} />
|
||||
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
||||
{t('events.recap.engagement.title', 'Guest engagement')}
|
||||
@@ -418,8 +418,8 @@ export default function MobileEventRecapPage() {
|
||||
</XStack>
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$3">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<MobileCard gap="$3">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Trophy size={18} color={primary} />
|
||||
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
||||
{t('events.recap.engagement.leaderboards.uploadsTitle', 'Top contributors')}
|
||||
@@ -430,7 +430,7 @@ export default function MobileEventRecapPage() {
|
||||
{t('events.recap.engagement.leaderboards.uploadsEmpty', 'No uploads yet.')}
|
||||
</Text>
|
||||
) : (
|
||||
<YStack space="$1.5" marginTop="$1">
|
||||
<YStack gap="$1.5" marginTop="$1">
|
||||
{engagement.leaderboards.uploads.slice(0, 5).map((entry, index) => (
|
||||
<LeaderboardRow
|
||||
key={`${entry.guest}-${entry.photos}-${index}`}
|
||||
@@ -443,8 +443,8 @@ export default function MobileEventRecapPage() {
|
||||
)}
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$3">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<MobileCard gap="$3">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Heart size={18} color={primary} />
|
||||
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
||||
{t('events.recap.engagement.leaderboards.likesTitle', 'Most liked')}
|
||||
@@ -455,7 +455,7 @@ export default function MobileEventRecapPage() {
|
||||
{t('events.recap.engagement.leaderboards.likesEmpty', 'No likes yet.')}
|
||||
</Text>
|
||||
) : (
|
||||
<YStack space="$1.5" marginTop="$1">
|
||||
<YStack gap="$1.5" marginTop="$1">
|
||||
{engagement.leaderboards.likes.slice(0, 5).map((entry, index) => (
|
||||
<LeaderboardRow
|
||||
key={`${entry.guest}-${entry.likes}-${index}`}
|
||||
@@ -468,15 +468,15 @@ export default function MobileEventRecapPage() {
|
||||
)}
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$3">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<MobileCard gap="$3">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Sparkles size={18} color={primary} />
|
||||
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
||||
{t('events.recap.engagement.highlightsTitle', 'Highlights')}
|
||||
</Text>
|
||||
</XStack>
|
||||
<YStack space="$2" marginTop="$1">
|
||||
<XStack space="$2" alignItems="center">
|
||||
<YStack gap="$2" marginTop="$1">
|
||||
<XStack gap="$2" alignItems="center">
|
||||
<YStack
|
||||
width={72}
|
||||
height={72}
|
||||
@@ -519,7 +519,7 @@ export default function MobileEventRecapPage() {
|
||||
</Text>
|
||||
</XStack>
|
||||
|
||||
<YStack space="$1">
|
||||
<YStack gap="$1">
|
||||
<Text fontSize="$sm" fontWeight="600" color={textStrong}>
|
||||
{t('events.recap.engagement.timeline', 'Uploads over time')}
|
||||
</Text>
|
||||
@@ -528,7 +528,7 @@ export default function MobileEventRecapPage() {
|
||||
{t('events.recap.engagement.timelineEmpty', 'No timeline data yet.')}
|
||||
</Text>
|
||||
) : (
|
||||
<YStack space="$1">
|
||||
<YStack gap="$1">
|
||||
{engagement.highlights.timeline.slice(-5).map((point) => (
|
||||
<XStack key={point.date} alignItems="center" justifyContent="space-between">
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
@@ -553,7 +553,7 @@ export default function MobileEventRecapPage() {
|
||||
) : null}
|
||||
|
||||
{activeTab === 'compliance' ? (
|
||||
<YStack space="$4">
|
||||
<YStack gap="$4">
|
||||
<DataExportsPanel variant="recap" event={event} />
|
||||
</YStack>
|
||||
) : null}
|
||||
@@ -643,7 +643,7 @@ function LeaderboardRow({ rank, name, value }: { rank: number; name: string; val
|
||||
borderColor={border}
|
||||
backgroundColor={surfaceMuted}
|
||||
>
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Text fontSize="$xs" color={muted} fontWeight="700">
|
||||
#{rank}
|
||||
</Text>
|
||||
|
||||
@@ -588,8 +588,8 @@ export default function MobileEventTasksPage() {
|
||||
}
|
||||
|
||||
const taskPanel = assignedTasks.length === 0 ? (
|
||||
<YStack space="$2">
|
||||
<MobileCard space="$2">
|
||||
<YStack gap="$2">
|
||||
<MobileCard gap="$2">
|
||||
<Text fontSize={13} fontWeight="700" color={text}>
|
||||
{t('events.tasks.emptyTitle', 'No photo tasks yet')}
|
||||
</Text>
|
||||
@@ -602,7 +602,7 @@ export default function MobileEventTasksPage() {
|
||||
{limitReachedHint ? ` ${limitReachedHint}` : ''}
|
||||
</Text>
|
||||
) : null}
|
||||
<XStack space="$2">
|
||||
<XStack gap="$2">
|
||||
<CTAButton
|
||||
label={t('events.tasks.emptyActionTask', 'Add photo task')}
|
||||
onPress={() => setShowTaskSheet(true)}
|
||||
@@ -631,7 +631,7 @@ export default function MobileEventTasksPage() {
|
||||
setShowTaskSheet(true);
|
||||
}}
|
||||
title={
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<YStack
|
||||
width={28}
|
||||
height={28}
|
||||
@@ -669,7 +669,7 @@ export default function MobileEventTasksPage() {
|
||||
setActiveTab('collections');
|
||||
}}
|
||||
title={
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<YStack
|
||||
width={28}
|
||||
height={28}
|
||||
@@ -698,9 +698,9 @@ export default function MobileEventTasksPage() {
|
||||
</YGroup>
|
||||
</YStack>
|
||||
) : (
|
||||
<YStack space="$2">
|
||||
<XStack alignItems="baseline" justifyContent="space-between" flexWrap="wrap" space="$2">
|
||||
<XStack alignItems="baseline" space="$2" flexWrap="wrap">
|
||||
<YStack gap="$2">
|
||||
<XStack alignItems="baseline" justifyContent="space-between" flexWrap="wrap" gap="$2">
|
||||
<XStack alignItems="baseline" gap="$2" flexWrap="wrap">
|
||||
<Text fontSize="$sm" fontWeight="800" color={text}>
|
||||
{t('events.tasks.assignedTitle', 'Task list')}
|
||||
</Text>
|
||||
@@ -718,11 +718,11 @@ export default function MobileEventTasksPage() {
|
||||
) : null}
|
||||
</XStack>
|
||||
{selectionMode ? (
|
||||
<MobileCard padding="$2.5" space="$2">
|
||||
<MobileCard padding="$2.5" gap="$2">
|
||||
<Text fontSize={12} fontWeight="700" color={text}>
|
||||
{t('events.tasks.selectionCount', '{{count}} ausgewählt', { count: selectedTaskIds.size })}
|
||||
</Text>
|
||||
<XStack space="$2">
|
||||
<XStack gap="$2">
|
||||
<CTAButton
|
||||
label={t('events.tasks.bulkRemove', 'Auswahl löschen')}
|
||||
tone="danger"
|
||||
@@ -751,7 +751,7 @@ export default function MobileEventTasksPage() {
|
||||
onPointerLeave={cancelLongPress}
|
||||
onPointerCancel={cancelLongPress}
|
||||
title={
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
{selectionMode ? (
|
||||
<Checkbox
|
||||
size="$3"
|
||||
@@ -779,7 +779,7 @@ export default function MobileEventTasksPage() {
|
||||
}
|
||||
iconAfter={
|
||||
selectionMode ? null : (
|
||||
<XStack space="$2" alignItems="center">
|
||||
<XStack gap="$2" alignItems="center">
|
||||
{task.emotion ? (
|
||||
<Tag label={task.emotion.name ?? ''} color={task.emotion.color ?? text} />
|
||||
) : null}
|
||||
@@ -811,7 +811,7 @@ export default function MobileEventTasksPage() {
|
||||
);
|
||||
|
||||
const libraryPanel = (
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<XStack alignItems="center" justifyContent="space-between" flexWrap="wrap">
|
||||
<Text fontSize="$sm" fontWeight="700" color={text}>
|
||||
{t('events.tasks.tabs.library', 'Task Library')}
|
||||
@@ -865,7 +865,7 @@ export default function MobileEventTasksPage() {
|
||||
) : null
|
||||
}
|
||||
iconAfter={
|
||||
<XStack space="$1.5" alignItems="center">
|
||||
<XStack gap="$1.5" alignItems="center">
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
if (!canAddTasks) {
|
||||
@@ -875,7 +875,7 @@ export default function MobileEventTasksPage() {
|
||||
quickAssign(task.id);
|
||||
}}
|
||||
>
|
||||
<XStack alignItems="center" space="$1">
|
||||
<XStack alignItems="center" gap="$1">
|
||||
<Plus size={14} color={canAddTasks ? primary : muted} />
|
||||
<Text fontSize={12} fontWeight="600" color={canAddTasks ? primary : muted}>
|
||||
{assigningId === task.id ? t('common.processing', '...') : t('events.tasks.add', 'Add')}
|
||||
@@ -896,7 +896,7 @@ export default function MobileEventTasksPage() {
|
||||
);
|
||||
|
||||
const collectionsPanel = (
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{t('events.tasks.importHint', 'Use predefined packs for your event type.')}
|
||||
</Text>
|
||||
@@ -937,7 +937,7 @@ export default function MobileEventTasksPage() {
|
||||
) : null
|
||||
}
|
||||
iconAfter={
|
||||
<XStack space="$1.5" alignItems="center">
|
||||
<XStack gap="$1.5" alignItems="center">
|
||||
<Button
|
||||
size="$2"
|
||||
backgroundColor={withAlpha(primary, 0.12)}
|
||||
@@ -969,7 +969,7 @@ export default function MobileEventTasksPage() {
|
||||
);
|
||||
|
||||
const emotionsPanel = (
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<XStack alignItems="center" justifyContent="space-between" flexWrap="wrap">
|
||||
<Text fontSize="$sm" fontWeight="700" color={text}>
|
||||
{t('events.tasks.tabs.emotions', 'Emotions')}
|
||||
@@ -1002,12 +1002,12 @@ export default function MobileEventTasksPage() {
|
||||
hoverTheme
|
||||
pressTheme
|
||||
title={
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Tag label={emotion.name ?? ''} color={emotion.color ?? border} />
|
||||
</XStack>
|
||||
}
|
||||
iconAfter={
|
||||
<XStack space="$2">
|
||||
<XStack gap="$2">
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
setEditingEmotion(emotion);
|
||||
@@ -1039,7 +1039,7 @@ export default function MobileEventTasksPage() {
|
||||
title={t('events.tasks.title', 'Photo tasks for guests')}
|
||||
onBack={back}
|
||||
headerActions={
|
||||
<XStack space="$2">
|
||||
<XStack gap="$2">
|
||||
<HeaderActionButton onPress={() => load()} ariaLabel={t('common.refresh', 'Refresh')}>
|
||||
<RefreshCcw size={18} color={text} />
|
||||
</HeaderActionButton>
|
||||
@@ -1061,7 +1061,7 @@ export default function MobileEventTasksPage() {
|
||||
) : null}
|
||||
|
||||
{loading ? (
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
{Array.from({ length: 4 }).map((_, idx) => (
|
||||
<SkeletonCard key={`tsk-${idx}`} height={70} />
|
||||
))}
|
||||
@@ -1074,7 +1074,7 @@ export default function MobileEventTasksPage() {
|
||||
backgroundColor={surface}
|
||||
padding="$3"
|
||||
>
|
||||
<YStack space="$3">
|
||||
<YStack gap="$3">
|
||||
<Card
|
||||
borderRadius={18}
|
||||
borderWidth={1}
|
||||
@@ -1082,8 +1082,8 @@ export default function MobileEventTasksPage() {
|
||||
backgroundColor={surfaceMuted}
|
||||
padding="$3"
|
||||
>
|
||||
<YStack space="$2">
|
||||
<XStack alignItems="center" justifyContent="space-between" space="$2">
|
||||
<YStack gap="$2">
|
||||
<XStack alignItems="center" justifyContent="space-between" gap="$2">
|
||||
<Text fontSize="$xs" fontWeight="700" color={text}>
|
||||
{t('events.tasks.toggle.title', 'Photo tasks for guests')}
|
||||
</Text>
|
||||
@@ -1121,7 +1121,7 @@ export default function MobileEventTasksPage() {
|
||||
<Text fontSize="$xs" fontWeight="700" color={text}>
|
||||
{t('events.tasks.toggle.switchLabel', 'Photo task mode')}
|
||||
</Text>
|
||||
<XStack alignItems="center" space="$1.5">
|
||||
<XStack alignItems="center" gap="$1.5">
|
||||
<Switch
|
||||
size="$3"
|
||||
checked={tasksEnabled}
|
||||
@@ -1205,7 +1205,7 @@ export default function MobileEventTasksPage() {
|
||||
|
||||
<Tabs.Content value="assigned" paddingTop="$2">
|
||||
<Card borderRadius={18} borderWidth={1} borderColor={border} backgroundColor={surface} padding="$3">
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<Card
|
||||
borderRadius={16}
|
||||
borderWidth={1}
|
||||
@@ -1213,7 +1213,7 @@ export default function MobileEventTasksPage() {
|
||||
backgroundColor={surfaceMuted}
|
||||
padding="$2.5"
|
||||
>
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<XStack flex={1}>
|
||||
<MobileInput
|
||||
type="search"
|
||||
@@ -1226,7 +1226,7 @@ export default function MobileEventTasksPage() {
|
||||
<Pressable onPress={() => setShowEmotionFilterSheet(true)}>
|
||||
<XStack
|
||||
alignItems="center"
|
||||
space="$1.5"
|
||||
gap="$1.5"
|
||||
paddingVertical="$2"
|
||||
paddingHorizontal="$3"
|
||||
borderRadius={14}
|
||||
@@ -1286,7 +1286,7 @@ export default function MobileEventTasksPage() {
|
||||
/>
|
||||
}
|
||||
>
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
{!canAddTasks ? (
|
||||
<Text fontSize={12} fontWeight="600" color={dangerText}>
|
||||
{limitReachedMessage}
|
||||
@@ -1338,7 +1338,7 @@ export default function MobileEventTasksPage() {
|
||||
/>
|
||||
}
|
||||
>
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
{!canAddTasks ? (
|
||||
<Text fontSize={12} fontWeight="600" color={dangerText}>
|
||||
{limitReachedMessage}
|
||||
@@ -1381,7 +1381,7 @@ export default function MobileEventTasksPage() {
|
||||
/>
|
||||
}
|
||||
>
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<MobileField label={t('events.tasks.emotionName', 'Name')}>
|
||||
<MobileInput
|
||||
type="text"
|
||||
@@ -1416,8 +1416,8 @@ export default function MobileEventTasksPage() {
|
||||
setShowEmotionFilterSheet(false);
|
||||
}}
|
||||
>
|
||||
<YStack space="$2">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<YStack gap="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<RadioGroup.Item value="">
|
||||
<RadioGroup.Indicator />
|
||||
</RadioGroup.Item>
|
||||
@@ -1426,7 +1426,7 @@ export default function MobileEventTasksPage() {
|
||||
</Text>
|
||||
</XStack>
|
||||
{emotions.map((emotion) => (
|
||||
<XStack key={`emo-filter-${emotion.id}`} alignItems="center" space="$2">
|
||||
<XStack key={`emo-filter-${emotion.id}`} alignItems="center" gap="$2">
|
||||
<RadioGroup.Item value={String(emotion.id)}>
|
||||
<RadioGroup.Indicator />
|
||||
</RadioGroup.Item>
|
||||
@@ -1458,7 +1458,7 @@ export default function MobileEventTasksPage() {
|
||||
maxWidth={420}
|
||||
width="90%"
|
||||
>
|
||||
<YStack space="$3">
|
||||
<YStack gap="$3">
|
||||
<AlertDialog.Title>
|
||||
<Text fontSize="$md" fontWeight="800" color={text}>
|
||||
{t('events.tasks.removeTitle', 'Remove photo task?')}
|
||||
@@ -1471,7 +1471,7 @@ export default function MobileEventTasksPage() {
|
||||
: t('events.tasks.removeBodyFallback', 'This will remove the photo task from the event.')}
|
||||
</Text>
|
||||
</AlertDialog.Description>
|
||||
<XStack space="$2" justifyContent="flex-end">
|
||||
<XStack gap="$2" justifyContent="flex-end">
|
||||
<AlertDialog.Cancel asChild>
|
||||
<CTAButton
|
||||
label={t('common.cancel', 'Cancel')}
|
||||
@@ -1513,7 +1513,7 @@ export default function MobileEventTasksPage() {
|
||||
maxWidth={420}
|
||||
width="90%"
|
||||
>
|
||||
<YStack space="$3">
|
||||
<YStack gap="$3">
|
||||
<AlertDialog.Title>
|
||||
<Text fontSize="$md" fontWeight="800" color={text}>
|
||||
{t('events.tasks.bulkRemoveTitle', 'Auswahl löschen')}
|
||||
@@ -1524,7 +1524,7 @@ export default function MobileEventTasksPage() {
|
||||
{t('events.tasks.bulkRemoveBody', 'This will remove the selected photo tasks from the event.')}
|
||||
</Text>
|
||||
</AlertDialog.Description>
|
||||
<XStack space="$2" justifyContent="flex-end">
|
||||
<XStack gap="$2" justifyContent="flex-end">
|
||||
<AlertDialog.Cancel asChild>
|
||||
<CTAButton
|
||||
label={t('common.cancel', 'Cancel')}
|
||||
|
||||
@@ -136,7 +136,7 @@ export default function MobileEventsPage() {
|
||||
shadowRadius={16}
|
||||
shadowOffset={{ width: 0, height: 10 }}
|
||||
>
|
||||
<YStack space="$2.5">
|
||||
<YStack gap="$2.5">
|
||||
<XStack
|
||||
alignItems="center"
|
||||
paddingHorizontal="$3"
|
||||
@@ -165,7 +165,7 @@ export default function MobileEventsPage() {
|
||||
</Card>
|
||||
|
||||
{loading ? (
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
{Array.from({ length: 3 }).map((_, idx) => (
|
||||
<SkeletonCard key={`sk-${idx}`} height={90} />
|
||||
))}
|
||||
@@ -182,7 +182,7 @@ export default function MobileEventsPage() {
|
||||
shadowRadius={16}
|
||||
shadowOffset={{ width: 0, height: 10 }}
|
||||
>
|
||||
<YStack space="$2" alignItems="center">
|
||||
<YStack gap="$2" alignItems="center">
|
||||
<Text fontSize="$md" fontWeight="700">
|
||||
{t('events.list.title')}
|
||||
</Text>
|
||||
@@ -263,7 +263,7 @@ function EventsList({
|
||||
];
|
||||
|
||||
return (
|
||||
<YStack space="$3">
|
||||
<YStack gap="$3">
|
||||
{filteredEvents.length === 0 ? (
|
||||
<Card
|
||||
borderRadius={22}
|
||||
@@ -276,7 +276,7 @@ function EventsList({
|
||||
shadowRadius={14}
|
||||
shadowOffset={{ width: 0, height: 8 }}
|
||||
>
|
||||
<YStack space="$2" alignItems="center">
|
||||
<YStack gap="$2" alignItems="center">
|
||||
<Text fontSize="$sm" fontWeight="700" color={text}>
|
||||
{t('events.list.empty.filtered')}
|
||||
</Text>
|
||||
@@ -303,7 +303,7 @@ function EventsList({
|
||||
shadowRadius={14}
|
||||
shadowOffset={{ width: 0, height: 8 }}
|
||||
>
|
||||
<YStack space="$2.5">
|
||||
<YStack gap="$2.5">
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
<Text fontSize="$xs" fontWeight="800" color={text}>
|
||||
{t('events.workspace.fields.status')}
|
||||
@@ -326,7 +326,7 @@ function EventsList({
|
||||
value={statusFilter}
|
||||
onValueChange={(value: string) => value && onStatusChange(value as EventStatusKey)}
|
||||
>
|
||||
<XStack space="$1.5">
|
||||
<XStack gap="$1.5">
|
||||
{filters.map((filter) => {
|
||||
const active = filter.key === statusFilter;
|
||||
return (
|
||||
@@ -340,7 +340,7 @@ function EventsList({
|
||||
paddingVertical="$1.5"
|
||||
paddingHorizontal="$3"
|
||||
>
|
||||
<XStack alignItems="center" space="$1.5">
|
||||
<XStack alignItems="center" gap="$1.5">
|
||||
<Text fontSize="$xs" fontWeight="700" color={active ? primary : muted}>
|
||||
{filter.label}
|
||||
</Text>
|
||||
@@ -407,7 +407,7 @@ function EventsList({
|
||||
/>
|
||||
}
|
||||
iconAfter={
|
||||
<XStack alignItems="center" space="$1.5">
|
||||
<XStack alignItems="center" gap="$1.5">
|
||||
<Text fontSize="$xs" color={primary} fontWeight="700">
|
||||
{t('events.list.actions.open')}
|
||||
</Text>
|
||||
@@ -448,12 +448,12 @@ function EventListItem({
|
||||
const locale = i18n.language;
|
||||
const stats = buildEventListStats(event);
|
||||
return (
|
||||
<YStack space="$1.5">
|
||||
<XStack alignItems="center" justifyContent="space-between" space="$2">
|
||||
<YStack gap="$1.5">
|
||||
<XStack alignItems="center" justifyContent="space-between" gap="$2">
|
||||
<Text fontSize="$md" fontWeight="800" color={text}>
|
||||
{renderName(event.name, t)}
|
||||
</Text>
|
||||
<XStack alignItems="center" space="$1.5">
|
||||
<XStack alignItems="center" gap="$1.5">
|
||||
<PillBadge tone={statusTone}>{statusLabel}</PillBadge>
|
||||
{onEdit ? (
|
||||
<Pressable onPress={() => onEdit(event.slug)}>
|
||||
@@ -464,21 +464,21 @@ function EventListItem({
|
||||
) : null}
|
||||
</XStack>
|
||||
</XStack>
|
||||
<XStack alignItems="center" space="$2" flexWrap="wrap">
|
||||
<XStack alignItems="center" space="$1.5">
|
||||
<XStack alignItems="center" gap="$2" flexWrap="wrap">
|
||||
<XStack alignItems="center" gap="$1.5">
|
||||
<CalendarDays size={12} color={subtle} />
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{formatDate(event.event_date, t, locale)}
|
||||
</Text>
|
||||
</XStack>
|
||||
<XStack alignItems="center" space="$1.5">
|
||||
<XStack alignItems="center" gap="$1.5">
|
||||
<MapPin size={12} color={subtle} />
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{resolveLocation(event, t)}
|
||||
</Text>
|
||||
</XStack>
|
||||
</XStack>
|
||||
<XStack alignItems="center" space="$2" flexWrap="wrap">
|
||||
<XStack alignItems="center" gap="$2" flexWrap="wrap">
|
||||
<EventStatChip icon={Camera} label={t('events.list.stats.photos')} value={stats.photos} muted={subtle} />
|
||||
<EventStatChip icon={Users} label={t('events.list.stats.guests')} value={stats.guests} muted={subtle} />
|
||||
<EventStatChip icon={Sparkles} label={t('events.list.stats.tasks')} value={stats.tasks} muted={subtle} />
|
||||
@@ -499,7 +499,7 @@ function EventStatChip({
|
||||
muted: string;
|
||||
}) {
|
||||
return (
|
||||
<XStack alignItems="center" space="$1">
|
||||
<XStack alignItems="center" gap="$1">
|
||||
<Icon size={12} color={muted} />
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{value} {label}
|
||||
|
||||
@@ -76,10 +76,10 @@ export default function ForgotPasswordPage() {
|
||||
paddingVertical="$5"
|
||||
style={{ ...safeAreaStyle, background: ADMIN_GRADIENTS.loginBackground }}
|
||||
>
|
||||
<YStack width="100%" maxWidth={520} space="$4">
|
||||
<YStack width="100%" maxWidth={520} gap="$4">
|
||||
<MobileCard backgroundColor="rgba(15, 23, 42, 0.6)" borderColor="rgba(255,255,255,0.12)">
|
||||
<YStack space="$2">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<YStack gap="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<XStack
|
||||
width={42}
|
||||
height={42}
|
||||
@@ -103,7 +103,7 @@ export default function ForgotPasswordPage() {
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard backgroundColor={surface} borderColor={border}>
|
||||
<YStack space="$3">
|
||||
<YStack gap="$3">
|
||||
<MobileField label={t('login.email', 'Email address')}>
|
||||
<MobileInput
|
||||
type="email"
|
||||
|
||||
@@ -34,7 +34,7 @@ export default function MobileHelpArticlePage() {
|
||||
return (
|
||||
<MobileShell activeTab="profile" title={article?.title ?? t('common.help', 'Help')} onBack={back}>
|
||||
{isLoading ? (
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<SkeletonCard height={120} />
|
||||
<SkeletonCard height={160} />
|
||||
</YStack>
|
||||
@@ -42,7 +42,7 @@ export default function MobileHelpArticlePage() {
|
||||
|
||||
{isError ? (
|
||||
<MobileCard>
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<Text fontSize="$sm" fontWeight="700" color={theme.textStrong}>
|
||||
{t('dashboard:help.error', 'Help could not be loaded.')}
|
||||
</Text>
|
||||
@@ -56,9 +56,9 @@ export default function MobileHelpArticlePage() {
|
||||
) : null}
|
||||
|
||||
{!isLoading && article ? (
|
||||
<YStack space="$3">
|
||||
<YStack gap="$3">
|
||||
<MobileCard>
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<Text fontSize="$lg" fontWeight="800" color={theme.textStrong}>
|
||||
{article.title}
|
||||
</Text>
|
||||
@@ -81,7 +81,7 @@ export default function MobileHelpArticlePage() {
|
||||
|
||||
{article.related && article.related.length > 0 ? (
|
||||
<MobileCard>
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<Text fontSize="$sm" fontWeight="700" color={theme.textStrong}>
|
||||
{t('help.article.relatedTitle', 'Weitere Artikel')}
|
||||
</Text>
|
||||
|
||||
@@ -41,7 +41,7 @@ export default function MobileHelpCenterPage() {
|
||||
return (
|
||||
<MobileShell activeTab="profile" title={t('common.help', 'Help')} onBack={back}>
|
||||
{isLoading ? (
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<SkeletonCard height={120} />
|
||||
<SkeletonCard height={120} />
|
||||
</YStack>
|
||||
@@ -49,7 +49,7 @@ export default function MobileHelpCenterPage() {
|
||||
|
||||
{isError ? (
|
||||
<MobileCard>
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<Text fontSize="$sm" fontWeight="700" color={theme.textStrong}>
|
||||
{t('dashboard:help.error', 'Help could not be loaded.')}
|
||||
</Text>
|
||||
@@ -63,7 +63,7 @@ export default function MobileHelpCenterPage() {
|
||||
) : null}
|
||||
|
||||
{!isLoading && !isError ? (
|
||||
<YStack space="$3">
|
||||
<YStack gap="$3">
|
||||
<HelpSection
|
||||
title={t('dashboard:help.title', 'FAQ')}
|
||||
icon={HelpCircle}
|
||||
@@ -100,8 +100,8 @@ function HelpSection({
|
||||
|
||||
return (
|
||||
<MobileCard padding="$0">
|
||||
<YStack padding="$3" space="$2">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<YStack padding="$3" gap="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
{IconCmp ? (
|
||||
<XStack
|
||||
width={28}
|
||||
|
||||
@@ -202,9 +202,9 @@ export default function MobileLoginPage() {
|
||||
paddingVertical="$5"
|
||||
style={{ ...safeAreaStyle, background: ADMIN_GRADIENTS.loginBackground }}
|
||||
>
|
||||
<YStack width="100%" maxWidth={520} space="$4">
|
||||
<YStack width="100%" maxWidth={520} gap="$4">
|
||||
<MobileCard backgroundColor="rgba(15, 23, 42, 0.6)" borderColor="rgba(255,255,255,0.12)">
|
||||
<YStack alignItems="center" space="$3">
|
||||
<YStack alignItems="center" gap="$3">
|
||||
<XStack
|
||||
width={56}
|
||||
height={56}
|
||||
@@ -217,7 +217,7 @@ export default function MobileLoginPage() {
|
||||
>
|
||||
<img src="/logo-transparent-md.png" alt={t('auth.logoAlt', 'Fotospiel')} width={40} height={40} />
|
||||
</XStack>
|
||||
<YStack alignItems="center" space="$1">
|
||||
<YStack alignItems="center" gap="$1">
|
||||
<Text fontSize="$xl" fontWeight="800" color="white" textAlign="center">
|
||||
{t('login.panel_title', 'Fotospiel.App Event Login')}
|
||||
</Text>
|
||||
@@ -230,7 +230,7 @@ export default function MobileLoginPage() {
|
||||
|
||||
<MobileCard backgroundColor={surface} borderColor={border}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<YStack space="$3">
|
||||
<YStack gap="$3">
|
||||
{oauthMessage ? (
|
||||
<YStack
|
||||
borderRadius={12}
|
||||
@@ -313,7 +313,7 @@ export default function MobileLoginPage() {
|
||||
pressStyle={{ opacity: 0.9 }}
|
||||
style={{ boxShadow: '0 12px 24px rgba(255, 90, 95, 0.25)' }}
|
||||
>
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Loader2 size={16} className={isSubmitting ? 'animate-spin' : ''} />
|
||||
<Text fontSize="$sm" color="white" fontWeight="800">
|
||||
{isSubmitting ? t('login.loading', 'Anmeldung …') : t('login.submit', 'Anmelden')}
|
||||
@@ -332,7 +332,7 @@ export default function MobileLoginPage() {
|
||||
borderWidth={1}
|
||||
color={text}
|
||||
>
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
{isRedirectingToGoogle ? (
|
||||
<Loader2 size={16} className="animate-spin" />
|
||||
) : (
|
||||
@@ -355,7 +355,7 @@ export default function MobileLoginPage() {
|
||||
borderWidth={1}
|
||||
color={text}
|
||||
>
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
{isRedirectingToFacebook ? (
|
||||
<Loader2 size={16} className="animate-spin" />
|
||||
) : (
|
||||
@@ -380,7 +380,7 @@ export default function MobileLoginPage() {
|
||||
}}
|
||||
/>
|
||||
|
||||
<YStack alignItems="center" space="$2">
|
||||
<YStack alignItems="center" gap="$2">
|
||||
<Text fontSize="$xs" color={muted} textAlign="center">
|
||||
{t('login.support', 'Fragen? Schreib uns an support@fotospiel.de oder antworte direkt auf deine Einladung.')}
|
||||
</Text>
|
||||
|
||||
@@ -52,7 +52,7 @@ export default function LoginStartPage(): React.ReactElement {
|
||||
shadowRadius={16}
|
||||
shadowOffset={{ width: 0, height: 10 }}
|
||||
>
|
||||
<YStack alignItems="center" space="$2">
|
||||
<YStack alignItems="center" gap="$2">
|
||||
<Spinner size="small" color={textStrong} />
|
||||
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
|
||||
{t('redirecting', 'Redirecting to login …')}
|
||||
|
||||
@@ -43,7 +43,7 @@ export default function LogoutPage() {
|
||||
shadowRadius={16}
|
||||
shadowOffset={{ width: 0, height: 10 }}
|
||||
>
|
||||
<YStack alignItems="center" space="$2">
|
||||
<YStack alignItems="center" gap="$2">
|
||||
<Spinner size="small" color={textStrong} />
|
||||
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
|
||||
Abmeldung wird vorbereitet ...
|
||||
|
||||
@@ -86,13 +86,13 @@ function NotificationSwipeRow({ item, onOpen, onMarkRead, children }: Notificati
|
||||
pointerEvents="none"
|
||||
style={{ position: 'absolute', top: 0, right: 0, bottom: 0, left: 0 }}
|
||||
>
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Check size={16} color={markText} />
|
||||
<Text fontSize="$xs" fontWeight="700" color={markText}>
|
||||
{item.is_read ? t('notificationLogs.read', 'Read') : t('notificationLogs.markRead', 'Mark read')}
|
||||
</Text>
|
||||
</XStack>
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Text fontSize="$xs" fontWeight="700" color={detailText}>
|
||||
Details
|
||||
</Text>
|
||||
@@ -508,7 +508,7 @@ export default function MobileNotificationsPage() {
|
||||
) : null}
|
||||
|
||||
{showFilterNotice ? (
|
||||
<MobileCard space="$2">
|
||||
<MobileCard gap="$2">
|
||||
<Text fontSize="$sm" fontWeight="700" color={text}>
|
||||
{t('notificationLogs.filterEmpty', 'No notifications for this event.')}
|
||||
</Text>
|
||||
@@ -524,7 +524,7 @@ export default function MobileNotificationsPage() {
|
||||
</MobileCard>
|
||||
) : null}
|
||||
|
||||
<XStack space="$2" marginBottom="$2">
|
||||
<XStack gap="$2" marginBottom="$2">
|
||||
<MobileSelect
|
||||
value={statusParam}
|
||||
onChange={(e) => updateFilters({ status: e.target.value })}
|
||||
@@ -552,7 +552,7 @@ export default function MobileNotificationsPage() {
|
||||
) : null}
|
||||
</XStack>
|
||||
|
||||
<XStack space="$2" flexWrap="wrap" marginBottom="$2">
|
||||
<XStack gap="$2" flexWrap="wrap" marginBottom="$2">
|
||||
{([
|
||||
{ key: 'all', label: t('notificationLogs.scope.all', 'All scopes') },
|
||||
{ key: 'photos', label: t('notificationLogs.scope.photos', 'Photos') },
|
||||
@@ -585,13 +585,13 @@ export default function MobileNotificationsPage() {
|
||||
</XStack>
|
||||
|
||||
{loading ? (
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
{Array.from({ length: 4 }).map((_, idx) => (
|
||||
<SkeletonCard key={`al-${idx}`} height={70} />
|
||||
))}
|
||||
</YStack>
|
||||
) : statusFiltered.length === 0 ? (
|
||||
<MobileCard alignItems="center" justifyContent="center" space="$2">
|
||||
<MobileCard alignItems="center" justifyContent="center" gap="$2">
|
||||
<Bell size={24} color={subtle} />
|
||||
<Text fontSize="$sm" fontWeight="700" color={text}>
|
||||
{t('mobileNotifications.emptyTitle', 'All caught up')}
|
||||
@@ -607,7 +607,7 @@ export default function MobileNotificationsPage() {
|
||||
/>
|
||||
</MobileCard>
|
||||
) : (
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
{events.length ? (
|
||||
<Pressable onPress={() => setShowEventPicker(true)}>
|
||||
<Text fontSize="$sm" color={primary} fontWeight="700">
|
||||
@@ -616,12 +616,12 @@ export default function MobileNotificationsPage() {
|
||||
</Pressable>
|
||||
) : null}
|
||||
{grouped.map((group) => (
|
||||
<YStack key={group.scope} space="$2">
|
||||
<YStack key={group.scope} gap="$2">
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
<Text fontSize="$xs" fontWeight="700" color={muted}>
|
||||
{t(`notificationLogs.scope.${group.scope}`, group.scope)}
|
||||
</Text>
|
||||
<XStack space="$2" alignItems="center">
|
||||
<XStack gap="$2" alignItems="center">
|
||||
{group.unread > 0 ? (
|
||||
<Pressable onPress={() => markGroupRead(group)}>
|
||||
<Text fontSize="$xs" color={primary} fontWeight="700">
|
||||
@@ -646,8 +646,8 @@ export default function MobileNotificationsPage() {
|
||||
onOpen={openNotification}
|
||||
onMarkRead={markNotificationRead}
|
||||
>
|
||||
<MobileCard space="$2" borderColor={item.is_read ? border : primary}>
|
||||
<XStack alignItems="center" space="$2">
|
||||
<MobileCard gap="$2" borderColor={item.is_read ? border : primary}>
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<XStack
|
||||
width={36}
|
||||
height={36}
|
||||
@@ -658,7 +658,7 @@ export default function MobileNotificationsPage() {
|
||||
>
|
||||
<Bell size={18} color={item.tone === 'warning' ? warningIcon : infoIcon} />
|
||||
</XStack>
|
||||
<YStack space="$0.5" flex={1}>
|
||||
<YStack gap="$0.5" flex={1}>
|
||||
<Text fontSize="$sm" fontWeight="700" color={text}>
|
||||
{item.title}
|
||||
</Text>
|
||||
@@ -699,14 +699,14 @@ export default function MobileNotificationsPage() {
|
||||
}
|
||||
>
|
||||
{selectedNotification ? (
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<Text fontSize="$sm" color={text} fontWeight="700">
|
||||
{selectedNotification.title}
|
||||
</Text>
|
||||
<Text fontSize="$sm" color={muted}>
|
||||
{selectedNotification.body}
|
||||
</Text>
|
||||
<XStack space="$2" flexWrap="wrap" style={{ rowGap: 8 }}>
|
||||
<XStack gap="$2" flexWrap="wrap" style={{ rowGap: 8 }}>
|
||||
<PillBadge tone={selectedNotification.tone === 'warning' ? 'warning' : 'muted'}>
|
||||
{selectedNotification.scope}
|
||||
</PillBadge>
|
||||
@@ -725,7 +725,7 @@ export default function MobileNotificationsPage() {
|
||||
title={t('mobileNotifications.filterByEvent', 'Nach Event filtern')}
|
||||
footer={null}
|
||||
>
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
{events.length === 0 ? (
|
||||
<Text fontSize="$sm" color={muted}>
|
||||
{t('events.list.empty.description', 'Starte jetzt mit deinem ersten Event.')}
|
||||
|
||||
@@ -65,7 +65,7 @@ export default function MobilePackageShopPage() {
|
||||
onBack={() => navigate(-1)}
|
||||
activeTab="profile"
|
||||
>
|
||||
<YStack space="$3">
|
||||
<YStack gap="$3">
|
||||
<SkeletonCard height={150} />
|
||||
<SkeletonCard height={150} />
|
||||
</YStack>
|
||||
@@ -125,10 +125,10 @@ export default function MobilePackageShopPage() {
|
||||
onBack={() => navigate(-1)}
|
||||
activeTab="profile"
|
||||
>
|
||||
<YStack space="$4">
|
||||
<YStack gap="$4">
|
||||
{catalogType !== 'reseller' && recommendedFeature && (
|
||||
<MobileCard borderColor={primary} backgroundColor={accentSoft} space="$2" padding="$3">
|
||||
<XStack space="$2" alignItems="center">
|
||||
<MobileCard borderColor={primary} backgroundColor={accentSoft} gap="$2" padding="$3">
|
||||
<XStack gap="$2" alignItems="center">
|
||||
<Sparkles size={16} color={primary} />
|
||||
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
|
||||
{t('shop.recommendationTitle', 'Recommended for you')}
|
||||
@@ -149,7 +149,7 @@ export default function MobilePackageShopPage() {
|
||||
</YStack>
|
||||
|
||||
{packageEntries.length > 1 ? (
|
||||
<XStack space="$2" paddingHorizontal="$2">
|
||||
<XStack gap="$2" paddingHorizontal="$2">
|
||||
<CTAButton
|
||||
label={t('shop.compare.toggleCards', 'Cards')}
|
||||
tone={viewMode === 'cards' ? 'primary' : 'ghost'}
|
||||
@@ -167,7 +167,7 @@ export default function MobilePackageShopPage() {
|
||||
</XStack>
|
||||
) : null}
|
||||
|
||||
<YStack space="$3">
|
||||
<YStack gap="$3">
|
||||
{viewMode === 'compare' ? (
|
||||
<PackageShopCompareView
|
||||
entries={packageEntries}
|
||||
@@ -234,14 +234,14 @@ function PackageShopCard({
|
||||
onPress={handlePress}
|
||||
borderColor={isRecommended ? primary : (isActive ? '$green8' : border)}
|
||||
borderWidth={isRecommended || isActive ? 2 : 1}
|
||||
space="$3"
|
||||
gap="$3"
|
||||
pressStyle={handlePress ? { backgroundColor: accentSoft } : undefined}
|
||||
backgroundColor={isActive ? '$green1' : undefined}
|
||||
style={{ opacity: isSubdued ? 0.6 : 1 }}
|
||||
>
|
||||
<XStack justifyContent="space-between" alignItems="flex-start">
|
||||
<YStack space="$1">
|
||||
<XStack space="$2" alignItems="center">
|
||||
<YStack gap="$1">
|
||||
<XStack gap="$2" alignItems="center">
|
||||
<Text fontSize="$lg" fontWeight="800" color={textStrong}>
|
||||
{pkg.name}
|
||||
</Text>
|
||||
@@ -255,7 +255,7 @@ function PackageShopCard({
|
||||
{!isResellerCatalog && isActive ? <PillBadge tone="success">{t('shop.badges.active', 'Active')}</PillBadge> : null}
|
||||
</XStack>
|
||||
|
||||
<XStack space="$2" alignItems="center">
|
||||
<XStack gap="$2" alignItems="center">
|
||||
<Text fontSize="$md" color={primary} fontWeight="700">
|
||||
{new Intl.NumberFormat(undefined, { style: 'currency', currency: 'EUR' }).format(pkg.price)}
|
||||
</Text>
|
||||
@@ -271,7 +271,7 @@ function PackageShopCard({
|
||||
</YStack>
|
||||
</XStack>
|
||||
|
||||
<YStack space="$1.5">
|
||||
<YStack gap="$1.5">
|
||||
{isResellerCatalog ? (
|
||||
<>
|
||||
{includedTierLabel ? (
|
||||
@@ -333,7 +333,7 @@ function PackageShopCard({
|
||||
function FeatureRow({ label }: { label: string }) {
|
||||
const { textStrong, primary } = useAdminTheme();
|
||||
return (
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Check size={14} color={primary} />
|
||||
<Text fontSize="$sm" color={textStrong}>{label}</Text>
|
||||
</XStack>
|
||||
@@ -411,8 +411,8 @@ function PackageShopCompareView({
|
||||
};
|
||||
|
||||
return (
|
||||
<MobileCard space="$3" borderColor={border}>
|
||||
<YStack space="$1">
|
||||
<MobileCard gap="$3" borderColor={border}>
|
||||
<YStack gap="$1">
|
||||
<Text fontSize="$md" fontWeight="700" color={textStrong}>
|
||||
{t('shop.compare.title', 'Compare plans')}
|
||||
</Text>
|
||||
@@ -422,7 +422,7 @@ function PackageShopCompareView({
|
||||
</YStack>
|
||||
|
||||
<XStack style={{ overflowX: 'auto' }}>
|
||||
<YStack space="$1.5" minWidth={labelWidth + columnWidth * entries.length}>
|
||||
<YStack gap="$1.5" minWidth={labelWidth + columnWidth * entries.length}>
|
||||
{rows.map((row) => (
|
||||
<XStack key={row.id} borderBottomWidth={1} borderColor={border}>
|
||||
<YStack
|
||||
@@ -443,11 +443,11 @@ function PackageShopCompareView({
|
||||
if (row.id === 'meta.plan') {
|
||||
const statusLabel = getPackageStatusLabel({ t, isActive: entry.isActive, owned: entry.owned });
|
||||
content = (
|
||||
<YStack space="$1">
|
||||
<YStack gap="$1">
|
||||
<Text fontSize="$sm" fontWeight="800" color={textStrong}>
|
||||
{entry.pkg.name}
|
||||
</Text>
|
||||
<XStack space="$1.5" flexWrap="wrap">
|
||||
<XStack gap="$1.5" flexWrap="wrap">
|
||||
{entry.isRecommended ? (
|
||||
<PillBadge tone="warning">{t('shop.badges.recommended', 'Recommended')}</PillBadge>
|
||||
) : null}
|
||||
@@ -492,7 +492,7 @@ function PackageShopCompareView({
|
||||
} else if (row.type === 'feature') {
|
||||
const enabled = getEnabledPackageFeatures(entry.pkg).includes(row.featureKey);
|
||||
content = (
|
||||
<XStack alignItems="center" space="$1.5">
|
||||
<XStack alignItems="center" gap="$1.5">
|
||||
{enabled ? (
|
||||
<Check size={16} color={primary} />
|
||||
) : (
|
||||
@@ -607,8 +607,8 @@ function CheckoutConfirmation({ pkg, onCancel }: { pkg: Package; onCancel: () =>
|
||||
|
||||
return (
|
||||
<MobileShell title={t('shop.confirmTitle', 'Confirm Purchase')} onBack={onCancel} activeTab="profile">
|
||||
<YStack space="$4">
|
||||
<MobileCard space="$2" borderColor={border}>
|
||||
<YStack gap="$4">
|
||||
<MobileCard gap="$2" borderColor={border}>
|
||||
<Text fontSize="$sm" color={muted}>{subtitle}</Text>
|
||||
<Text fontSize="$xl" fontWeight="800" color={textStrong}>{pkg.name}</Text>
|
||||
<Text fontSize="$lg" color={primary} fontWeight="700">
|
||||
@@ -616,13 +616,13 @@ function CheckoutConfirmation({ pkg, onCancel }: { pkg: Package; onCancel: () =>
|
||||
</Text>
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$3" borderColor={border}>
|
||||
<XStack alignItems="center" space="$2">
|
||||
<MobileCard gap="$3" borderColor={border}>
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<ShieldCheck size={18} color={textStrong} />
|
||||
<Text fontSize="$md" fontWeight="700" color={textStrong}>{t('shop.legalTitle', 'Terms & Conditions')}</Text>
|
||||
</XStack>
|
||||
|
||||
<XStack space="$3" alignItems="flex-start">
|
||||
<XStack gap="$3" alignItems="flex-start">
|
||||
<Checkbox
|
||||
id="agb"
|
||||
size="$4"
|
||||
@@ -638,7 +638,7 @@ function CheckoutConfirmation({ pkg, onCancel }: { pkg: Package; onCancel: () =>
|
||||
</Text>
|
||||
</XStack>
|
||||
|
||||
<XStack space="$3" alignItems="flex-start">
|
||||
<XStack gap="$3" alignItems="flex-start">
|
||||
<Checkbox
|
||||
id="withdrawal"
|
||||
size="$4"
|
||||
@@ -655,7 +655,7 @@ function CheckoutConfirmation({ pkg, onCancel }: { pkg: Package; onCancel: () =>
|
||||
</XStack>
|
||||
</MobileCard>
|
||||
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<CTAButton
|
||||
label={busy ? t('shop.processing', 'Processing...') : t('shop.payNow', 'Pay Now')}
|
||||
onPress={handleCheckout}
|
||||
|
||||
@@ -306,7 +306,7 @@ export default function MobileProfileAccountPage() {
|
||||
onBack={back}
|
||||
>
|
||||
{brandingTabEnabled ? (
|
||||
<XStack space="$2">
|
||||
<XStack gap="$2">
|
||||
<TabButton
|
||||
label={t('profile.tabs.account', 'Account')}
|
||||
active={activeTab === 'account'}
|
||||
@@ -330,7 +330,7 @@ export default function MobileProfileAccountPage() {
|
||||
</MobileCard>
|
||||
) : null}
|
||||
|
||||
<MobileCard space="$3">
|
||||
<MobileCard gap="$3">
|
||||
<Text fontSize="$md" fontWeight="800" color={text}>
|
||||
{t('profile.branding.title', 'Standard-Branding')}
|
||||
</Text>
|
||||
@@ -339,11 +339,11 @@ export default function MobileProfileAccountPage() {
|
||||
</Text>
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$3">
|
||||
<MobileCard gap="$3">
|
||||
<Text fontSize="$md" fontWeight="800" color={text}>
|
||||
{t('profile.branding.theme', 'Theme')}
|
||||
</Text>
|
||||
<YStack space="$3">
|
||||
<YStack gap="$3">
|
||||
<MobileField label={t('events.branding.mode', 'Theme')}>
|
||||
<MobileSelect
|
||||
value={brandingForm.mode}
|
||||
@@ -369,11 +369,11 @@ export default function MobileProfileAccountPage() {
|
||||
</YStack>
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$3">
|
||||
<MobileCard gap="$3">
|
||||
<Text fontSize="$md" fontWeight="800" color={text}>
|
||||
{t('events.branding.colors', 'Colors')}
|
||||
</Text>
|
||||
<YStack space="$3">
|
||||
<YStack gap="$3">
|
||||
<ColorField
|
||||
label={t('events.branding.primary', 'Primary Color')}
|
||||
value={brandingForm.primary}
|
||||
@@ -401,11 +401,11 @@ export default function MobileProfileAccountPage() {
|
||||
</YStack>
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$3">
|
||||
<MobileCard gap="$3">
|
||||
<Text fontSize="$md" fontWeight="800" color={text}>
|
||||
{t('events.branding.fonts', 'Fonts')}
|
||||
</Text>
|
||||
<YStack space="$3">
|
||||
<YStack gap="$3">
|
||||
<MobileField label={t('events.branding.headingFont', 'Headline Font')}>
|
||||
<MobileInput
|
||||
value={brandingForm.headingFont}
|
||||
@@ -444,8 +444,8 @@ export default function MobileProfileAccountPage() {
|
||||
</MobileCard>
|
||||
) : null}
|
||||
|
||||
<MobileCard space="$3">
|
||||
<XStack alignItems="center" space="$3">
|
||||
<MobileCard gap="$3">
|
||||
<XStack alignItems="center" gap="$3">
|
||||
<XStack
|
||||
width={48}
|
||||
height={48}
|
||||
@@ -456,7 +456,7 @@ export default function MobileProfileAccountPage() {
|
||||
>
|
||||
<User size={20} color={primary} />
|
||||
</XStack>
|
||||
<YStack space="$1">
|
||||
<YStack gap="$1">
|
||||
<Text fontSize="$md" fontWeight="800" color={text}>
|
||||
{form.name || profile?.email || t('profile.title', 'Profil')}
|
||||
</Text>
|
||||
@@ -465,7 +465,7 @@ export default function MobileProfileAccountPage() {
|
||||
</Text>
|
||||
</YStack>
|
||||
</XStack>
|
||||
<XStack alignItems="center" space="$2" flexWrap="wrap">
|
||||
<XStack alignItems="center" gap="$2" flexWrap="wrap">
|
||||
{profile?.email_verified ? (
|
||||
<CheckCircle2 size={14} color={subtle} />
|
||||
) : (
|
||||
@@ -480,8 +480,8 @@ export default function MobileProfileAccountPage() {
|
||||
</XStack>
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$3">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<MobileCard gap="$3">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<User size={16} color={text} />
|
||||
<Text fontSize="$md" fontWeight="800" color={text}>
|
||||
{t('profile.sections.account.heading', 'Account-Informationen')}
|
||||
@@ -495,7 +495,7 @@ export default function MobileProfileAccountPage() {
|
||||
{t('profile.loading', 'Lädt ...')}
|
||||
</Text>
|
||||
) : (
|
||||
<YStack space="$3">
|
||||
<YStack gap="$3">
|
||||
<MobileField label={t('profile.fields.name', 'Anzeigename')}>
|
||||
<MobileInput
|
||||
value={form.name}
|
||||
@@ -535,8 +535,8 @@ export default function MobileProfileAccountPage() {
|
||||
)}
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$3">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<MobileCard gap="$3">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Lock size={16} color={text} />
|
||||
<Text fontSize="$md" fontWeight="800" color={text}>
|
||||
{t('profile.sections.password.heading', 'Passwort ändern')}
|
||||
@@ -545,7 +545,7 @@ export default function MobileProfileAccountPage() {
|
||||
<Text fontSize="$sm" color={muted}>
|
||||
{t('profile.sections.password.description', 'Wähle ein sicheres Passwort, um deinen Admin-Zugang zu schützen.')}
|
||||
</Text>
|
||||
<YStack space="$3">
|
||||
<YStack gap="$3">
|
||||
<MobileField label={t('profile.fields.currentPassword', 'Aktuelles Passwort')}>
|
||||
<MobileInput
|
||||
value={form.currentPassword}
|
||||
@@ -625,11 +625,11 @@ function ColorField({
|
||||
}) {
|
||||
const { text, muted } = useAdminTheme();
|
||||
return (
|
||||
<YStack space="$2" opacity={disabled ? 0.6 : 1}>
|
||||
<YStack gap="$2" opacity={disabled ? 0.6 : 1}>
|
||||
<Text fontSize="$sm" fontWeight="700" color={text}>
|
||||
{label}
|
||||
</Text>
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<MobileColorInput
|
||||
value={value}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
|
||||
@@ -68,13 +68,13 @@ export default function MobileProfilePage() {
|
||||
shadowRadius={16}
|
||||
shadowOffset={{ width: 0, height: 10 }}
|
||||
>
|
||||
<YStack space="$2.5" alignItems="center">
|
||||
<YStack gap="$2.5" alignItems="center">
|
||||
<Avatar size="$7" borderRadius={20} backgroundColor={avatarBg}>
|
||||
<Avatar.Fallback>
|
||||
<User size={28} color={primary} />
|
||||
</Avatar.Fallback>
|
||||
</Avatar>
|
||||
<YStack space="$0.5" alignItems="center">
|
||||
<YStack gap="$0.5" alignItems="center">
|
||||
<Text fontSize="$md" fontWeight="800" color={textColor}>
|
||||
{name}
|
||||
</Text>
|
||||
@@ -101,7 +101,7 @@ export default function MobileProfilePage() {
|
||||
shadowRadius={16}
|
||||
shadowOffset={{ width: 0, height: 10 }}
|
||||
>
|
||||
<YStack space="$2.5">
|
||||
<YStack gap="$2.5">
|
||||
<XStack
|
||||
alignItems="center"
|
||||
paddingHorizontal="$3"
|
||||
@@ -210,7 +210,7 @@ export default function MobileProfilePage() {
|
||||
shadowRadius={16}
|
||||
shadowOffset={{ width: 0, height: 10 }}
|
||||
>
|
||||
<YStack space="$2.5">
|
||||
<YStack gap="$2.5">
|
||||
<XStack
|
||||
alignItems="center"
|
||||
paddingHorizontal="$3"
|
||||
@@ -230,7 +230,7 @@ export default function MobileProfilePage() {
|
||||
paddingVertical="$2"
|
||||
paddingHorizontal="$3"
|
||||
title={
|
||||
<XStack space="$2" alignItems="center">
|
||||
<XStack gap="$2" alignItems="center">
|
||||
<Globe size={16} color={muted} />
|
||||
<Text fontSize="$sm" color={textColor}>
|
||||
{t('mobileProfile.language', 'Language')}
|
||||
@@ -259,7 +259,7 @@ export default function MobileProfilePage() {
|
||||
paddingVertical="$2"
|
||||
paddingHorizontal="$3"
|
||||
title={
|
||||
<XStack space="$2" alignItems="center">
|
||||
<XStack gap="$2" alignItems="center">
|
||||
<Moon size={16} color={muted} />
|
||||
<Text fontSize="$sm" color={textColor}>
|
||||
{t('mobileProfile.theme', 'Theme')}
|
||||
@@ -295,8 +295,8 @@ export default function MobileProfilePage() {
|
||||
shadowRadius={12}
|
||||
shadowOffset={{ width: 0, height: 6 }}
|
||||
>
|
||||
<YStack space="$2">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<YStack gap="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<XStack
|
||||
width={36}
|
||||
height={36}
|
||||
|
||||
@@ -54,10 +54,10 @@ export default function PublicHelpPage() {
|
||||
paddingVertical="$4"
|
||||
style={{ ...safeAreaStyle, background: ADMIN_GRADIENTS.loginBackground }}
|
||||
>
|
||||
<YStack width="100%" maxWidth={680} alignSelf="center" space="$4">
|
||||
<YStack width="100%" maxWidth={680} alignSelf="center" gap="$4">
|
||||
<MobileCard backgroundColor="rgba(15, 23, 42, 0.55)" borderColor="rgba(255,255,255,0.08)">
|
||||
<YStack space="$3">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<YStack gap="$3">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<XStack
|
||||
width={42}
|
||||
height={42}
|
||||
@@ -82,8 +82,8 @@ export default function PublicHelpPage() {
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard backgroundColor={surface} borderColor={border}>
|
||||
<YStack space="$3">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<YStack gap="$3">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<XStack
|
||||
width={36}
|
||||
height={36}
|
||||
@@ -98,7 +98,7 @@ export default function PublicHelpPage() {
|
||||
{t('login.help_faq_title', 'Haeufige Fragen vor dem Login')}
|
||||
</Text>
|
||||
</XStack>
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
{faqItems.map((item, index) => (
|
||||
<YStack
|
||||
key={`${item.question}-${index}`}
|
||||
@@ -107,7 +107,7 @@ export default function PublicHelpPage() {
|
||||
borderWidth={1}
|
||||
borderColor={border}
|
||||
backgroundColor="rgba(255,255,255,0.6)"
|
||||
space="$1"
|
||||
gap="$1"
|
||||
>
|
||||
<Text fontSize="$sm" fontWeight="700" color={text}>
|
||||
{item.question}
|
||||
@@ -122,8 +122,8 @@ export default function PublicHelpPage() {
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard backgroundColor={surface} borderColor={border}>
|
||||
<YStack space="$3">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<YStack gap="$3">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<XStack
|
||||
width={36}
|
||||
height={36}
|
||||
|
||||
@@ -262,7 +262,7 @@ export default function MobileQrLayoutCustomizePage() {
|
||||
|
||||
<Stepper current={step} onStepChange={setStep} />
|
||||
|
||||
<MobileCard space="$2" marginTop="$2">
|
||||
<MobileCard gap="$2" marginTop="$2">
|
||||
{step === 'background' && (
|
||||
<BackgroundStep
|
||||
onBack={back}
|
||||
@@ -330,8 +330,8 @@ function Stepper({ current, onStepChange }: { current: Step; onStepChange: (step
|
||||
const progress = ((currentIndex + 1) / steps.length) * 100;
|
||||
|
||||
return (
|
||||
<YStack space="$2" marginTop="$2">
|
||||
<XStack space="$2" alignItems="center">
|
||||
<YStack gap="$2" marginTop="$2">
|
||||
<XStack gap="$2" alignItems="center">
|
||||
{steps.map((step, idx) => {
|
||||
const active = step.key === current;
|
||||
const completed = idx < currentIndex;
|
||||
@@ -800,10 +800,10 @@ function BackgroundStep({
|
||||
];
|
||||
|
||||
return (
|
||||
<YStack space="$3" marginTop="$2">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<YStack gap="$3" marginTop="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Pressable onPress={onBack}>
|
||||
<XStack alignItems="center" space="$1">
|
||||
<XStack alignItems="center" gap="$1">
|
||||
<ArrowLeft size={16} color={textStrong} />
|
||||
<Text fontSize="$sm" color={textStrong}>
|
||||
{t('common.back', 'Zurück')}
|
||||
@@ -813,7 +813,7 @@ function BackgroundStep({
|
||||
<PillBadge tone="muted">{formatLabel}</PillBadge>
|
||||
</XStack>
|
||||
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
|
||||
{disablePresets
|
||||
? t('events.qr.backgroundPickerFoldable', 'Hintergrund für A5 (Gradient/Farbe)')
|
||||
@@ -859,7 +859,7 @@ function BackgroundStep({
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<YStack space="$2" marginTop="$2">
|
||||
<YStack gap="$2" marginTop="$2">
|
||||
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
|
||||
{t('events.qr.gradients', 'Gradienten')}
|
||||
</Text>
|
||||
@@ -894,7 +894,7 @@ function BackgroundStep({
|
||||
</XStack>
|
||||
</YStack>
|
||||
|
||||
<YStack space="$2" marginTop="$2">
|
||||
<YStack gap="$2" marginTop="$2">
|
||||
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
|
||||
{t('events.qr.colors', 'Vollfarbe')}
|
||||
</Text>
|
||||
@@ -966,10 +966,10 @@ function TextStep({
|
||||
};
|
||||
|
||||
return (
|
||||
<YStack space="$3" marginTop="$2">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<YStack gap="$3" marginTop="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Pressable onPress={onBack}>
|
||||
<XStack alignItems="center" space="$1">
|
||||
<XStack alignItems="center" gap="$1">
|
||||
<ArrowLeft size={16} color={textStrong} />
|
||||
<Text fontSize="$sm" color={textStrong}>
|
||||
{t('common.back', 'Zurück')}
|
||||
@@ -979,7 +979,7 @@ function TextStep({
|
||||
<PillBadge tone="muted">{t('events.qr.textStep', 'Texte & Hinweise')}</PillBadge>
|
||||
</XStack>
|
||||
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
|
||||
{t('events.qr.textFields', 'Texte')}
|
||||
</Text>
|
||||
@@ -1000,12 +1000,12 @@ function TextStep({
|
||||
/>
|
||||
</YStack>
|
||||
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
|
||||
{t('events.qr.instructions', 'Anleitung')}
|
||||
</Text>
|
||||
{textFields.instructions.map((item, idx) => (
|
||||
<XStack key={idx} alignItems="center" space="$2">
|
||||
<XStack key={idx} alignItems="center" gap="$2">
|
||||
<MobileTextArea
|
||||
value={item}
|
||||
onChange={(event) => updateInstruction(idx, event.target.value)}
|
||||
@@ -1196,10 +1196,10 @@ function PreviewStep({
|
||||
const qrImageSrc = qrPngUrl ?? qrUrl ?? '';
|
||||
|
||||
return (
|
||||
<YStack space="$3" marginTop="$2">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<YStack gap="$3" marginTop="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Pressable onPress={onBack}>
|
||||
<XStack alignItems="center" space="$1">
|
||||
<XStack alignItems="center" gap="$1">
|
||||
<ArrowLeft size={16} color={textStrong} />
|
||||
<Text fontSize="$sm" color={textStrong}>
|
||||
{t('common.back', 'Zurück')}
|
||||
@@ -1211,7 +1211,7 @@ function PreviewStep({
|
||||
</PillBadge>
|
||||
</XStack>
|
||||
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
|
||||
{t('events.qr.preview', 'Vorschau')}
|
||||
</Text>
|
||||
@@ -1243,7 +1243,7 @@ function PreviewStep({
|
||||
|
||||
<LayoutControls slots={resolvedSlots} slotOverrides={slotOverrides} onUpdateSlot={onUpdateSlot} tenantFonts={tenantFonts} qrUrl={qrImageSrc} />
|
||||
|
||||
<XStack space="$2" width="100%" flexWrap="wrap">
|
||||
<XStack gap="$2" width="100%" flexWrap="wrap">
|
||||
<CTAButton
|
||||
label={t('events.qr.exportPdf', 'Export PDF')}
|
||||
onPress={async () => {
|
||||
@@ -1318,7 +1318,7 @@ function LayoutControls({
|
||||
const decimals = step % 1 === 0 ? 0 : Math.min(4, `${step}`.split('.')[1]?.length ?? 1);
|
||||
const formatValue = (val: number) => val.toFixed(decimals);
|
||||
return (
|
||||
<XStack space="$1" alignItems="center">
|
||||
<XStack gap="$1" alignItems="center">
|
||||
<Pressable onPress={dec}>
|
||||
<XStack width={36} height={36} borderRadius={10} alignItems="center" justifyContent="center" borderWidth={1} borderColor={border} backgroundColor={surface}>
|
||||
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
||||
@@ -1398,13 +1398,13 @@ function LayoutControls({
|
||||
</XStack>
|
||||
</Accordion.Trigger>
|
||||
<Accordion.Content {...({ paddingTop: "$2" } as any)}>
|
||||
<YStack space="$2" padding="$2" borderWidth={1} borderColor={border} borderRadius={12}>
|
||||
<XStack space="$3">
|
||||
<YStack flex={1} space="$1">
|
||||
<YStack gap="$2" padding="$2" borderWidth={1} borderColor={border} borderRadius={12}>
|
||||
<XStack gap="$3">
|
||||
<YStack flex={1} gap="$1">
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{t('events.qr.positionX', 'X (%)')}
|
||||
</Text>
|
||||
<XStack space="$2" alignItems="center">
|
||||
<XStack gap="$2" alignItems="center">
|
||||
<StepperInput
|
||||
value={currentX * 100}
|
||||
min={0}
|
||||
@@ -1428,7 +1428,7 @@ function LayoutControls({
|
||||
</Pressable>
|
||||
</XStack>
|
||||
</YStack>
|
||||
<YStack flex={1} space="$1">
|
||||
<YStack flex={1} gap="$1">
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{t('events.qr.positionY', 'Y (%)')}
|
||||
</Text>
|
||||
@@ -1440,7 +1440,7 @@ function LayoutControls({
|
||||
onChange={(val) => onPercentChange('y')(val)}
|
||||
/>
|
||||
</YStack>
|
||||
<YStack flex={1} space="$1">
|
||||
<YStack flex={1} gap="$1">
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{t('events.qr.width', 'Breite (%)')}
|
||||
</Text>
|
||||
@@ -1454,14 +1454,14 @@ function LayoutControls({
|
||||
</YStack>
|
||||
</XStack>
|
||||
|
||||
<XStack space="$3">
|
||||
<YStack flex={1} space="$1">
|
||||
<XStack gap="$3">
|
||||
<YStack flex={1} gap="$1">
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{t('events.qr.fontSize', 'Font Size (px)')}
|
||||
</Text>
|
||||
<StepperInput value={override.fontSize ?? slot.fontSize ?? 16} min={8} max={200} step={1} onChange={(val) => onFontSizeChange(val)} />
|
||||
</YStack>
|
||||
<YStack flex={1} space="$1">
|
||||
<YStack flex={1} gap="$1">
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{t('events.qr.fontFamily', 'Font Family')}
|
||||
</Text>
|
||||
@@ -1477,12 +1477,12 @@ function LayoutControls({
|
||||
))}
|
||||
</MobileSelect>
|
||||
</YStack>
|
||||
<YStack flex={1} space="$1">
|
||||
<YStack flex={1} gap="$1">
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{t('events.qr.fontColor', 'Schriftfarbe')}
|
||||
</Text>
|
||||
<YStack space="$2">
|
||||
<XStack space="$2" alignItems="center">
|
||||
<YStack gap="$2">
|
||||
<XStack gap="$2" alignItems="center">
|
||||
<Pressable onPress={() => setOpenColorSlot(openColorSlot === slotKey ? null : slotKey)}>
|
||||
<XStack
|
||||
width={48}
|
||||
@@ -1536,7 +1536,7 @@ function LayoutControls({
|
||||
onChange={(val) => onUpdateSlot(slotKey, { color: val })}
|
||||
style={{ width: 240, height: 200 }}
|
||||
/>
|
||||
<XStack space="$2" justifyContent="flex-end">
|
||||
<XStack gap="$2" justifyContent="flex-end">
|
||||
<Pressable onPress={() => setOpenColorSlot(null)}>
|
||||
<XStack
|
||||
paddingHorizontal="$3"
|
||||
@@ -1560,8 +1560,8 @@ function LayoutControls({
|
||||
</YStack>
|
||||
</XStack>
|
||||
|
||||
<XStack space="$2">
|
||||
<YStack flex={1} space="$1">
|
||||
<XStack gap="$2">
|
||||
<YStack flex={1} gap="$1">
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{t('events.qr.align', 'Align')}
|
||||
</Text>
|
||||
@@ -1574,7 +1574,7 @@ function LayoutControls({
|
||||
<option value="right">{t('common.right', 'Rechts')}</option>
|
||||
</MobileSelect>
|
||||
</YStack>
|
||||
<YStack flex={1} space="$1">
|
||||
<YStack flex={1} gap="$1">
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{t('events.qr.lineHeight', 'Line Height')}
|
||||
</Text>
|
||||
@@ -1606,7 +1606,7 @@ function LayoutControls({
|
||||
const accordionDefaults = ['headline'];
|
||||
|
||||
return (
|
||||
<YStack space="$3" marginTop="$2">
|
||||
<YStack gap="$3" marginTop="$2">
|
||||
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
|
||||
{t('events.qr.layoutControls', 'Layout & Schrift')}
|
||||
</Text>
|
||||
@@ -1627,9 +1627,9 @@ function LayoutControls({
|
||||
</XStack>
|
||||
</Accordion.Trigger>
|
||||
<Accordion.Content {...({ paddingTop: "$2" } as any)}>
|
||||
<YStack space="$2" padding="$2" borderWidth={1} borderColor={border} borderRadius={12}>
|
||||
<XStack space="$2">
|
||||
<YStack flex={1} space="$1">
|
||||
<YStack gap="$2" padding="$2" borderWidth={1} borderColor={border} borderRadius={12}>
|
||||
<XStack gap="$2">
|
||||
<YStack flex={1} gap="$1">
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{t('events.qr.positionX', 'X (%)')}
|
||||
</Text>
|
||||
@@ -1641,7 +1641,7 @@ function LayoutControls({
|
||||
onChange={(val) => onQrPercentChange('x')(val)}
|
||||
/>
|
||||
</YStack>
|
||||
<YStack flex={1} space="$1">
|
||||
<YStack flex={1} gap="$1">
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{t('events.qr.positionY', 'Y (%)')}
|
||||
</Text>
|
||||
@@ -1653,7 +1653,7 @@ function LayoutControls({
|
||||
onChange={(val) => onQrPercentChange('y')(val)}
|
||||
/>
|
||||
</YStack>
|
||||
<YStack flex={1} space="$1">
|
||||
<YStack flex={1} gap="$1">
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{t('events.qr.size', 'Größe (%)')}
|
||||
</Text>
|
||||
|
||||
@@ -106,7 +106,7 @@ export default function MobileQrPrintPage() {
|
||||
<ContextHelpLink slug="guest-access-qr" />
|
||||
</XStack>
|
||||
|
||||
<MobileCard space="$3" alignItems="center">
|
||||
<MobileCard gap="$3" alignItems="center">
|
||||
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
||||
{t('events.qr.heroTitle', 'Entrance QR Code')}
|
||||
</Text>
|
||||
@@ -148,7 +148,7 @@ export default function MobileQrPrintPage() {
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{t('events.qr.description', 'Scan to access the event guest app.')}
|
||||
</Text>
|
||||
<XStack space="$2" width="100%" marginTop="$2">
|
||||
<XStack gap="$2" width="100%" marginTop="$2">
|
||||
<CTAButton
|
||||
label={t('events.qr.download', 'Download')}
|
||||
fullWidth={false}
|
||||
@@ -191,7 +191,7 @@ export default function MobileQrPrintPage() {
|
||||
</XStack>
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$2">
|
||||
<MobileCard gap="$2">
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{t('events.qr.step1', 'Schritt 1: Format wählen')}
|
||||
</Text>
|
||||
@@ -226,7 +226,7 @@ export default function MobileQrPrintPage() {
|
||||
/>
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$2">
|
||||
<MobileCard gap="$2">
|
||||
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
|
||||
{t('events.qr.createLink', 'Neuen QR-Link erstellen')}
|
||||
</Text>
|
||||
@@ -299,7 +299,7 @@ function FormatSelection({
|
||||
];
|
||||
|
||||
return (
|
||||
<YStack space="$2" marginTop="$2">
|
||||
<YStack gap="$2" marginTop="$2">
|
||||
{cards.map((card) => {
|
||||
const isSelected = selectedFormat === card.key;
|
||||
return (
|
||||
@@ -314,15 +314,15 @@ function FormatSelection({
|
||||
borderWidth={isSelected ? 2 : 1}
|
||||
backgroundColor={isSelected ? accentSoft : surface}
|
||||
>
|
||||
<XStack alignItems="center" justifyContent="space-between" space="$3">
|
||||
<YStack space="$1" flex={1}>
|
||||
<XStack alignItems="center" justifyContent="space-between" gap="$3">
|
||||
<YStack gap="$1" flex={1}>
|
||||
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
|
||||
{card.title}
|
||||
</Text>
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{card.subtitle}
|
||||
</Text>
|
||||
<XStack space="$2" alignItems="center" flexWrap="wrap">
|
||||
<XStack gap="$2" alignItems="center" flexWrap="wrap">
|
||||
{card.badges.map((badge) => (
|
||||
<PillBadge tone="muted" key={badge}>
|
||||
{badge}
|
||||
@@ -377,10 +377,10 @@ function BackgroundStep({
|
||||
const formatLabel = isFoldable ? 'A4 Landscape (Double/Mirror)' : 'A4 Portrait';
|
||||
|
||||
return (
|
||||
<YStack space="$3" marginTop="$2">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<YStack gap="$3" marginTop="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Pressable onPress={onBack}>
|
||||
<XStack alignItems="center" space="$1">
|
||||
<XStack alignItems="center" gap="$1">
|
||||
<ArrowLeft size={16} color={textStrong} />
|
||||
<Text fontSize="$sm" color={textStrong}>
|
||||
{t('common.back', 'Zurück')}
|
||||
@@ -392,7 +392,7 @@ function BackgroundStep({
|
||||
</PillBadge>
|
||||
</XStack>
|
||||
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
|
||||
{t(
|
||||
'events.qr.backgroundPicker',
|
||||
@@ -488,10 +488,10 @@ function TextStep({
|
||||
};
|
||||
|
||||
return (
|
||||
<YStack space="$3" marginTop="$2">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<YStack gap="$3" marginTop="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Pressable onPress={onBack}>
|
||||
<XStack alignItems="center" space="$1">
|
||||
<XStack alignItems="center" gap="$1">
|
||||
<ArrowLeft size={16} color={textStrong} />
|
||||
<Text fontSize="$sm" color={textStrong}>
|
||||
{t('common.back', 'Zurück')}
|
||||
@@ -501,7 +501,7 @@ function TextStep({
|
||||
<PillBadge tone="muted">{t('events.qr.textStep', 'Texte & Hinweise')}</PillBadge>
|
||||
</XStack>
|
||||
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
|
||||
{t('events.qr.textFields', 'Texte')}
|
||||
</Text>
|
||||
@@ -522,12 +522,12 @@ function TextStep({
|
||||
/>
|
||||
</YStack>
|
||||
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
|
||||
{t('events.qr.instructions', 'Anleitung')}
|
||||
</Text>
|
||||
{textFields.instructions.map((item, idx) => (
|
||||
<XStack key={idx} alignItems="center" space="$2">
|
||||
<XStack key={idx} alignItems="center" gap="$2">
|
||||
<MobileInput
|
||||
style={{ flex: 1 }}
|
||||
placeholder={t('events.qr.instructionsPlaceholder', 'Schritt hinzufügen')}
|
||||
@@ -580,10 +580,10 @@ function PreviewStep({
|
||||
const previewBody = layout?.preview?.text ?? text;
|
||||
|
||||
return (
|
||||
<YStack space="$3" marginTop="$2">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<YStack gap="$3" marginTop="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Pressable onPress={onBack}>
|
||||
<XStack alignItems="center" space="$1">
|
||||
<XStack alignItems="center" gap="$1">
|
||||
<ArrowLeft size={16} color={textStrong} />
|
||||
<Text fontSize="$sm" color={textStrong}>
|
||||
{t('common.back', 'Zurück')}
|
||||
@@ -597,7 +597,7 @@ function PreviewStep({
|
||||
) : null}
|
||||
</XStack>
|
||||
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
|
||||
{t('events.qr.preview', 'Vorschau')}
|
||||
</Text>
|
||||
@@ -632,7 +632,7 @@ function PreviewStep({
|
||||
{textFields.description}
|
||||
</Text>
|
||||
) : null}
|
||||
<YStack space="$1">
|
||||
<YStack gap="$1">
|
||||
{textFields.instructions.filter((i) => i.trim().length > 0).map((item, idx) => (
|
||||
<Text key={idx} fontSize="$xs" color={previewBody}>
|
||||
• {item}
|
||||
@@ -660,7 +660,7 @@ function PreviewStep({
|
||||
</YStack>
|
||||
</YStack>
|
||||
|
||||
<XStack space="$2">
|
||||
<XStack gap="$2">
|
||||
<CTAButton label={t('events.qr.exportPdf', 'Export PDF')} onPress={() => onExport('pdf')} />
|
||||
<CTAButton label={t('events.qr.exportPng', 'Export PNG')} onPress={() => onExport('png')} />
|
||||
</XStack>
|
||||
|
||||
@@ -110,10 +110,10 @@ export default function ResetPasswordPage() {
|
||||
paddingVertical="$5"
|
||||
style={{ ...safeAreaStyle, background: ADMIN_GRADIENTS.loginBackground }}
|
||||
>
|
||||
<YStack width="100%" maxWidth={520} space="$4">
|
||||
<YStack width="100%" maxWidth={520} gap="$4">
|
||||
<MobileCard backgroundColor="rgba(15, 23, 42, 0.6)" borderColor="rgba(255,255,255,0.12)">
|
||||
<YStack space="$2">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<YStack gap="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<XStack
|
||||
width={42}
|
||||
height={42}
|
||||
@@ -137,7 +137,7 @@ export default function ResetPasswordPage() {
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard backgroundColor={surface} borderColor={border}>
|
||||
<YStack space="$3">
|
||||
<YStack gap="$3">
|
||||
<MobileField label={t('login.email', 'Email address')} error={fieldErrors.email?.[0]}>
|
||||
<MobileInput
|
||||
type="email"
|
||||
|
||||
@@ -210,8 +210,8 @@ export default function MobileSettingsPage() {
|
||||
}}
|
||||
/>
|
||||
|
||||
<MobileCard space="$3">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<MobileCard gap="$3">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Shield size={18} color={text} />
|
||||
<Text fontSize="$md" fontWeight="800" color={text}>
|
||||
{t('mobileSettings.accountTitle', 'Account')}
|
||||
@@ -223,14 +223,14 @@ export default function MobileSettingsPage() {
|
||||
{user?.tenant_id ? (
|
||||
<PillBadge tone="muted">{t('mobileSettings.tenantBadge', 'Account #{{id}}', { id: user.tenant_id })}</PillBadge>
|
||||
) : null}
|
||||
<XStack space="$2">
|
||||
<XStack gap="$2">
|
||||
<CTAButton label={t('settings.profile.actions.openProfile', 'Profil bearbeiten')} onPress={() => navigate(ADMIN_PROFILE_ACCOUNT_PATH)} />
|
||||
<CTAButton label={t('settings.session.logout', 'Abmelden')} tone="ghost" onPress={() => logout({ redirect: adminPath('/logout') })} />
|
||||
</XStack>
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$3">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<MobileCard gap="$3">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Bell size={18} color={text} />
|
||||
<Text fontSize="$md" fontWeight="800" color={text}>
|
||||
{t('mobileSettings.notificationsTitle', 'Notifications')}
|
||||
@@ -316,14 +316,14 @@ export default function MobileSettingsPage() {
|
||||
{pushState.error}
|
||||
</Text>
|
||||
) : null}
|
||||
<XStack space="$2">
|
||||
<XStack gap="$2">
|
||||
<CTAButton label={saving ? t('common.processing', '...') : t('settings.notifications.actions.save', 'Speichern')} onPress={() => handleSave()} />
|
||||
<CTAButton label={t('common.reset', 'Reset')} tone="ghost" onPress={() => handleReset()} />
|
||||
</XStack>
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$3">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<MobileCard gap="$3">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Smartphone size={18} color={text} />
|
||||
<Text fontSize="$md" fontWeight="800" color={text}>
|
||||
{t('mobileSettings.deviceTitle', 'Device & permissions')}
|
||||
@@ -337,9 +337,9 @@ export default function MobileSettingsPage() {
|
||||
{t('mobileSettings.deviceLoading', 'Checking device status ...')}
|
||||
</Text>
|
||||
) : (
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<XStack alignItems="center" justifyContent="space-between" gap="$2">
|
||||
<YStack flex={1} space="$1">
|
||||
<YStack flex={1} gap="$1">
|
||||
<Text fontSize="$sm" fontWeight="700" color={text}>
|
||||
{t('mobileSettings.deviceStatus.notifications.label', 'Notifications')}
|
||||
</Text>
|
||||
@@ -352,7 +352,7 @@ export default function MobileSettingsPage() {
|
||||
</PillBadge>
|
||||
</XStack>
|
||||
<XStack alignItems="center" justifyContent="space-between" gap="$2">
|
||||
<YStack flex={1} space="$1">
|
||||
<YStack flex={1} gap="$1">
|
||||
<Text fontSize="$sm" fontWeight="700" color={text}>
|
||||
{t('mobileSettings.deviceStatus.camera.label', 'Camera')}
|
||||
</Text>
|
||||
@@ -365,7 +365,7 @@ export default function MobileSettingsPage() {
|
||||
</PillBadge>
|
||||
</XStack>
|
||||
<XStack alignItems="center" justifyContent="space-between" gap="$2">
|
||||
<YStack flex={1} space="$1">
|
||||
<YStack flex={1} gap="$1">
|
||||
<Text fontSize="$sm" fontWeight="700" color={text}>
|
||||
{t('mobileSettings.deviceStatus.storage.label', 'Offline storage')}
|
||||
</Text>
|
||||
@@ -378,7 +378,7 @@ export default function MobileSettingsPage() {
|
||||
</PillBadge>
|
||||
</XStack>
|
||||
<XStack alignItems="center" justifyContent="space-between" gap="$2">
|
||||
<YStack flex={1} space="$1">
|
||||
<YStack flex={1} gap="$1">
|
||||
<Text fontSize="$sm" fontWeight="700" color={text}>
|
||||
{t('mobileSettings.deviceStatus.connection.label', 'Connection')}
|
||||
</Text>
|
||||
@@ -404,8 +404,8 @@ export default function MobileSettingsPage() {
|
||||
) : null}
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$3">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<MobileCard gap="$3">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Sparkles size={18} color={text} />
|
||||
<Text fontSize="$md" fontWeight="800" color={text}>
|
||||
{t('mobileSettings.experienceTitle', 'Experience')}
|
||||
@@ -414,7 +414,7 @@ export default function MobileSettingsPage() {
|
||||
<Text fontSize="$sm" color={muted}>
|
||||
{t('mobileSettings.experienceBody', 'Replay the quick tour or re-enable the install banner.')}
|
||||
</Text>
|
||||
<XStack space="$2">
|
||||
<XStack gap="$2">
|
||||
<CTAButton
|
||||
label={t('mobileSettings.experienceReplay', 'Replay quick tour')}
|
||||
onPress={handleReplayTour}
|
||||
@@ -430,8 +430,8 @@ export default function MobileSettingsPage() {
|
||||
</XStack>
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$3">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<MobileCard gap="$3">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<User size={18} color={text} />
|
||||
<Text fontSize="$md" fontWeight="800" color={text}>
|
||||
{t('settings.appearance.title', 'Darstellung')}
|
||||
|
||||
@@ -25,7 +25,7 @@ export default function MobileTasksTabPage() {
|
||||
if (activeEvent?.slug && !tasksEnabled) {
|
||||
return (
|
||||
<MobileShell activeTab="tasks" title={t('events.tasks.title', 'Photo tasks')}>
|
||||
<MobileCard alignItems="flex-start" space="$3">
|
||||
<MobileCard alignItems="flex-start" gap="$3">
|
||||
<Text fontSize="$lg" fontWeight="800" color={text}>
|
||||
{t('events.tasks.disabledTitle', 'Photo task mode is off for this event')}
|
||||
</Text>
|
||||
@@ -44,7 +44,7 @@ export default function MobileTasksTabPage() {
|
||||
if (!hasEvents) {
|
||||
return (
|
||||
<MobileShell activeTab="tasks" title={t('events.tasks.title', 'Photo tasks')}>
|
||||
<MobileCard alignItems="flex-start" space="$3">
|
||||
<MobileCard alignItems="flex-start" gap="$3">
|
||||
<Text fontSize="$lg" fontWeight="800" color={text}>
|
||||
{t('events.tasks.emptyTitle', 'Create an event first')}
|
||||
</Text>
|
||||
@@ -64,7 +64,7 @@ export default function MobileTasksTabPage() {
|
||||
|
||||
return (
|
||||
<MobileShell activeTab="tasks" title={t('events.tasks.title', 'Photo tasks')}>
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<Text fontSize="$sm" color={text} fontWeight="700">
|
||||
{t('events.tasks.pickEvent', 'Pick an event to manage photo tasks')}
|
||||
</Text>
|
||||
@@ -78,9 +78,9 @@ export default function MobileTasksTabPage() {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MobileCard borderColor={border} space="$2">
|
||||
<MobileCard borderColor={border} gap="$2">
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
<YStack space="$1">
|
||||
<YStack gap="$1">
|
||||
<Text fontSize="$md" fontWeight="800" color={text}>
|
||||
{resolveEventDisplayName(event)}
|
||||
</Text>
|
||||
|
||||
@@ -37,7 +37,7 @@ export default function MobileUploadsTabPage() {
|
||||
shadowRadius={16}
|
||||
shadowOffset={{ width: 0, height: 10 }}
|
||||
>
|
||||
<YStack space="$2.5">
|
||||
<YStack gap="$2.5">
|
||||
<XStack
|
||||
alignItems="center"
|
||||
paddingHorizontal="$3"
|
||||
@@ -82,7 +82,7 @@ export default function MobileUploadsTabPage() {
|
||||
shadowRadius={16}
|
||||
shadowOffset={{ width: 0, height: 10 }}
|
||||
>
|
||||
<YStack space="$2.5">
|
||||
<YStack gap="$2.5">
|
||||
<XStack
|
||||
alignItems="center"
|
||||
paddingHorizontal="$3"
|
||||
@@ -105,7 +105,7 @@ export default function MobileUploadsTabPage() {
|
||||
paddingVertical="$2"
|
||||
paddingHorizontal="$3"
|
||||
title={
|
||||
<YStack space="$1">
|
||||
<YStack gap="$1">
|
||||
<Text fontSize="$md" fontWeight="800" color={text}>
|
||||
{resolveEventDisplayName(event)}
|
||||
</Text>
|
||||
|
||||
@@ -77,7 +77,7 @@ export function BottomNav({ active, onNavigate }: { active: NavKey; onNavigate:
|
||||
<YStack
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
space="$1"
|
||||
gap="$1"
|
||||
minHeight={50}
|
||||
style={{
|
||||
transform: isPressed ? 'scale(0.92)' : (activeState ? 'scale(1.05)' : 'scale(1)'),
|
||||
|
||||
@@ -27,7 +27,7 @@ export function ContextHelpLink({ slug, label }: ContextHelpLinkProps) {
|
||||
>
|
||||
<XStack
|
||||
alignItems="center"
|
||||
space="$1.5"
|
||||
gap="$1.5"
|
||||
paddingHorizontal="$2.5"
|
||||
paddingVertical="$1.5"
|
||||
borderRadius={999}
|
||||
|
||||
@@ -41,14 +41,14 @@ export function EventSwitcherSheet({
|
||||
onClose={onClose}
|
||||
snapPoints={[65]}
|
||||
>
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
{events.map((event) => {
|
||||
const isActive = event.slug === activeSlug;
|
||||
return (
|
||||
<Pressable key={event.slug} onPress={() => handleSelect(event.slug)}>
|
||||
<XStack
|
||||
alignItems="center"
|
||||
space="$3"
|
||||
gap="$3"
|
||||
padding="$3"
|
||||
borderRadius={14}
|
||||
backgroundColor={isActive ? theme.surfaceMuted : 'transparent'}
|
||||
|
||||
@@ -18,7 +18,7 @@ export function MobileField({ label, hint, error, children }: FieldProps) {
|
||||
const { text, muted, danger } = useAdminTheme();
|
||||
|
||||
return (
|
||||
<YStack space="$1.5">
|
||||
<YStack gap="$1.5">
|
||||
{typeof label === 'string' || typeof label === 'number' ? (
|
||||
<Text fontSize="$sm" fontWeight="800" color={text}>
|
||||
{label}
|
||||
|
||||
@@ -76,7 +76,7 @@ export function LegalConsentSheet({
|
||||
onClose={onClose}
|
||||
title={copy?.title ?? t('events.legalConsent.title', 'Before purchase')}
|
||||
footer={
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
{error ? (
|
||||
<Text fontSize="$sm" color={danger}>
|
||||
{error}
|
||||
@@ -97,12 +97,12 @@ export function LegalConsentSheet({
|
||||
</YStack>
|
||||
}
|
||||
>
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<Text fontSize="$sm" color={text}>
|
||||
{copy?.description ?? t('events.legalConsent.description', 'Please confirm the legal notes before buying an add-on.')}
|
||||
</Text>
|
||||
{requireTerms ? (
|
||||
<XStack space="$3" alignItems="flex-start">
|
||||
<XStack gap="$3" alignItems="flex-start">
|
||||
<Checkbox
|
||||
id="legal-terms"
|
||||
size="$4"
|
||||
@@ -130,7 +130,7 @@ export function LegalConsentSheet({
|
||||
</XStack>
|
||||
) : null}
|
||||
{requireWaiver ? (
|
||||
<XStack space="$3" alignItems="flex-start">
|
||||
<XStack gap="$3" alignItems="flex-start">
|
||||
<Checkbox
|
||||
id="legal-waiver"
|
||||
size="$4"
|
||||
|
||||
@@ -33,9 +33,9 @@ export function LimitWarnings({
|
||||
}
|
||||
|
||||
return (
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
{warnings.map((warning) => (
|
||||
<MobileCard key={warning.id} borderColor={borderColor} space="$2">
|
||||
<MobileCard key={warning.id} borderColor={borderColor} gap="$2">
|
||||
<Text fontSize="$sm" color={textColor} fontWeight="700">
|
||||
{warning.message}
|
||||
</Text>
|
||||
@@ -100,7 +100,7 @@ function MobileAddonsPicker({
|
||||
}
|
||||
|
||||
return (
|
||||
<XStack space="$2" alignItems="center">
|
||||
<XStack gap="$2" alignItems="center">
|
||||
<MobileSelect
|
||||
value={selected}
|
||||
onChange={(event) => setSelected(event.target.value)}
|
||||
|
||||
@@ -33,13 +33,13 @@ export function MobileInstallBanner({
|
||||
|
||||
return (
|
||||
<MobileCard
|
||||
space={isCompact ? '$1.5' : '$2'}
|
||||
gap={isCompact ? '$1.5' : '$2'}
|
||||
borderColor={border}
|
||||
backgroundColor={surfaceMuted}
|
||||
padding={isCompact ? '$2' : '$3'}
|
||||
>
|
||||
<XStack alignItems="center" justifyContent="space-between" gap="$2">
|
||||
<XStack alignItems="center" space="$2" flex={1}>
|
||||
<XStack alignItems="center" gap="$2" flex={1}>
|
||||
<XStack
|
||||
width={isCompact ? 32 : 36}
|
||||
height={isCompact ? 32 : 36}
|
||||
@@ -50,7 +50,7 @@ export function MobileInstallBanner({
|
||||
>
|
||||
{isPrompt ? <Download size={16} color={primary} /> : <Share2 size={16} color={primary} />}
|
||||
</XStack>
|
||||
<YStack flex={1} space="$0.5">
|
||||
<YStack flex={1} gap="$0.5">
|
||||
<Text fontSize={isCompact ? '$xs' : '$sm'} fontWeight="800" color={textStrong}>
|
||||
{t('installBanner.title', 'Install Fotospiel Admin')}
|
||||
</Text>
|
||||
@@ -61,7 +61,7 @@ export function MobileInstallBanner({
|
||||
</Text>
|
||||
</YStack>
|
||||
</XStack>
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
{isPrompt && onInstall && isCompact ? (
|
||||
<Pressable onPress={onInstall}>
|
||||
<Text fontSize={10} fontWeight="700" color={primary}>
|
||||
|
||||
@@ -177,7 +177,7 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
|
||||
paddingVertical="$1.5"
|
||||
borderRadius={999}
|
||||
alignItems="center"
|
||||
space="$1.5"
|
||||
gap="$1.5"
|
||||
maxWidth={220}
|
||||
borderWidth={1}
|
||||
borderColor="rgba(255, 255, 255, 0.08)"
|
||||
@@ -201,7 +201,7 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
|
||||
|
||||
const headerBackButton = onBack ? (
|
||||
<HeaderActionButton onPress={onBack} ariaLabel={t('actions.back', 'Back')}>
|
||||
<XStack alignItems="center" space="$1.5">
|
||||
<XStack alignItems="center" gap="$1.5">
|
||||
<ChevronLeft size={28} color="white" strokeWidth={2.5} />
|
||||
</XStack>
|
||||
</HeaderActionButton>
|
||||
@@ -214,7 +214,7 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
|
||||
);
|
||||
|
||||
const headerActionsRow = (
|
||||
<XStack alignItems="center" space="$2.5">
|
||||
<XStack alignItems="center" gap="$2.5">
|
||||
{showQr ? (
|
||||
<HeaderActionButton
|
||||
onPress={() => navigate(adminPath(`/mobile/events/${effectiveActive?.slug}/qr`))}
|
||||
@@ -282,7 +282,7 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
|
||||
borderWidth={1} borderColor={actionBorder}
|
||||
>
|
||||
{user?.avatar_url ? (
|
||||
<Image source={{ uri: user.avatar_url }} width={36} height={36} resizeMode="cover" />
|
||||
<Image src={user.avatar_url} width={36} height={36} objectFit="cover" />
|
||||
) : (
|
||||
<Text fontSize="$xs" fontWeight="700" color="white">
|
||||
{user?.name?.charAt(0).toUpperCase() ?? 'U'}
|
||||
@@ -315,7 +315,7 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
|
||||
paddingTop: 'calc(env(safe-area-inset-top, 0px) + 16px)',
|
||||
}}
|
||||
>
|
||||
<XStack alignItems="center" justifyContent="space-between" minHeight={48} space="$2">
|
||||
<XStack alignItems="center" justifyContent="space-between" minHeight={48} gap="$2">
|
||||
{headerBackButton}
|
||||
|
||||
<XStack flex={1} justifyContent="center" alignItems="center">
|
||||
@@ -332,7 +332,7 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
|
||||
flex={1}
|
||||
padding="$4"
|
||||
paddingBottom="$10"
|
||||
space="$3"
|
||||
gap="$3"
|
||||
width="100%"
|
||||
maxWidth={800}
|
||||
style={{ paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 96px)' }}
|
||||
@@ -345,7 +345,7 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
|
||||
</XStack>
|
||||
) : null}
|
||||
{queuedPhotoCount > 0 ? (
|
||||
<MobileCard space="$2">
|
||||
<MobileCard gap="$2">
|
||||
<Text fontSize="$sm" fontWeight="700" color={theme.textStrong}>
|
||||
{t('status.queueTitle', 'Photo actions pending')}
|
||||
</Text>
|
||||
@@ -365,7 +365,7 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
|
||||
</MobileCard>
|
||||
) : null}
|
||||
{subtitle ? (
|
||||
<YStack space="$1">
|
||||
<YStack gap="$1">
|
||||
{title ? (
|
||||
<Text fontSize="$lg" fontWeight="800" color={theme.textStrong}>
|
||||
{title}
|
||||
|
||||
@@ -45,7 +45,7 @@ export function OnboardingShell({
|
||||
paddingHorizontal="$5"
|
||||
paddingTop="$5"
|
||||
paddingBottom="$6"
|
||||
space="$4"
|
||||
gap="$4"
|
||||
style={{
|
||||
paddingTop: 'calc(env(safe-area-inset-top, 0px) + 20px)',
|
||||
paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 24px)',
|
||||
@@ -54,7 +54,7 @@ export function OnboardingShell({
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
{onBack ? (
|
||||
<Pressable onPress={onBack} aria-label={resolvedBackLabel}>
|
||||
<XStack alignItems="center" space="$1.5">
|
||||
<XStack alignItems="center" gap="$1.5">
|
||||
<ChevronLeft size={22} color={text} />
|
||||
<Text fontSize="$sm" fontWeight="700" color={text}>
|
||||
{resolvedBackLabel}
|
||||
@@ -86,7 +86,7 @@ export function OnboardingShell({
|
||||
shadowOpacity={0.06}
|
||||
shadowRadius={14}
|
||||
shadowOffset={{ width: 0, height: 8 }}
|
||||
space="$2"
|
||||
gap="$2"
|
||||
>
|
||||
{eyebrow ? (
|
||||
<Text fontSize="$xs" fontWeight="700" color={muted} textTransform="uppercase" letterSpacing={0.6}>
|
||||
@@ -103,7 +103,7 @@ export function OnboardingShell({
|
||||
) : null}
|
||||
</YStack>
|
||||
|
||||
<YStack space="$4">{children}</YStack>
|
||||
<YStack gap="$4">{children}</YStack>
|
||||
{footer ? <YStack marginTop="$2">{footer}</YStack> : null}
|
||||
</YStack>
|
||||
</YStack>
|
||||
|
||||
@@ -26,7 +26,7 @@ export function MobileCard({
|
||||
shadowRadius={16}
|
||||
shadowOffset={{ width: 0, height: 10 }}
|
||||
padding="$3.5"
|
||||
space="$2"
|
||||
gap="$2"
|
||||
style={{
|
||||
...style,
|
||||
}}
|
||||
@@ -138,7 +138,7 @@ export function CTAButton({
|
||||
backgroundColor={backgroundColor}
|
||||
borderWidth={isPrimary || isDanger ? 0 : 2}
|
||||
borderColor={borderColor}
|
||||
space="$2"
|
||||
gap="$2"
|
||||
style={primaryStyle}
|
||||
>
|
||||
{iconLeft}
|
||||
@@ -169,7 +169,7 @@ export function KpiTile({
|
||||
const iconColor = color || primary;
|
||||
|
||||
return (
|
||||
<MobileCard borderRadius={14} padding="$2.5" width="31%" minWidth={100} space="$1.5">
|
||||
<MobileCard borderRadius={14} padding="$2.5" width="31%" minWidth={100} gap="$1.5">
|
||||
<XStack
|
||||
width={28}
|
||||
height={28}
|
||||
@@ -181,7 +181,7 @@ export function KpiTile({
|
||||
<IconCmp size={14} color={iconColor} />
|
||||
</XStack>
|
||||
|
||||
<YStack space="$0">
|
||||
<YStack gap="$0">
|
||||
<Text fontSize="$xl" fontWeight="900" color={textStrong} letterSpacing={-0.5} lineHeight="$xl">
|
||||
{value}
|
||||
</Text>
|
||||
@@ -237,7 +237,7 @@ export function KpiStrip({
|
||||
minWidth={150}
|
||||
maxWidth={220}
|
||||
>
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Text
|
||||
fontSize={32}
|
||||
fontWeight="900"
|
||||
@@ -248,7 +248,7 @@ export function KpiStrip({
|
||||
{item.value}
|
||||
</Text>
|
||||
<Separator vertical backgroundColor={separatorColor} height={32} marginHorizontal="$1.5" />
|
||||
<YStack alignItems="center" space="$0.5" paddingLeft="$0.5">
|
||||
<YStack alignItems="center" gap="$0.5" paddingLeft="$0.5">
|
||||
<XStack
|
||||
width={28}
|
||||
height={28}
|
||||
@@ -339,7 +339,7 @@ export function ActionTile({
|
||||
style={tileStyle}
|
||||
borderRadius={isCluster ? 14 : 16}
|
||||
padding="$3"
|
||||
space="$2.5"
|
||||
gap="$2.5"
|
||||
backgroundColor={glassSurface ?? backgroundColor}
|
||||
borderWidth={2}
|
||||
borderColor={borderColor}
|
||||
@@ -405,7 +405,7 @@ export function FloatingActionButton({
|
||||
borderRadius={999}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
space="$2"
|
||||
gap="$2"
|
||||
backgroundColor={primary}
|
||||
shadowColor={shadow}
|
||||
shadowOpacity={0.2}
|
||||
|
||||
@@ -41,10 +41,10 @@ export function MobileScaffold({ title, onBack, rightSlot, children, footer }: M
|
||||
WebkitBackdropFilter: 'blur(12px)',
|
||||
}}
|
||||
>
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
{onBack ? (
|
||||
<Pressable onPress={onBack}>
|
||||
<XStack alignItems="center" space="$1.5">
|
||||
<XStack alignItems="center" gap="$1.5">
|
||||
<ChevronLeft size={18} color={primary} />
|
||||
<Text fontSize="$sm" color={primary} fontWeight="600">
|
||||
{t('actions.back', 'Back')}
|
||||
@@ -63,7 +63,7 @@ export function MobileScaffold({ title, onBack, rightSlot, children, footer }: M
|
||||
</XStack>
|
||||
</XStack>
|
||||
|
||||
<YStack flex={1} padding="$4" space="$3" paddingBottom={footer ? '$14' : '$5'}>
|
||||
<YStack flex={1} padding="$4" gap="$3" paddingBottom={footer ? '$14' : '$5'}>
|
||||
{children}
|
||||
</YStack>
|
||||
|
||||
|
||||
@@ -38,9 +38,9 @@ export function SetupChecklist({
|
||||
const content = (
|
||||
<YStack>
|
||||
<Pressable onPress={() => setCollapsed(!collapsed)}>
|
||||
<YStack padding="$3" paddingVertical="$2.5" space="$2">
|
||||
<YStack padding="$3" paddingVertical="$2.5" gap="$2">
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Text fontSize="$xs" fontWeight="700" color={theme.muted} textTransform="uppercase" letterSpacing={1}>
|
||||
{title}
|
||||
</Text>
|
||||
@@ -51,7 +51,7 @@ export function SetupChecklist({
|
||||
)}
|
||||
</XStack>
|
||||
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Text fontSize="$xs" color={theme.muted} fontWeight="600">
|
||||
{completedCount}/{steps.length}
|
||||
</Text>
|
||||
@@ -89,7 +89,7 @@ export function SetupChecklist({
|
||||
backgroundColor={isNext ? theme.surfaceMuted : 'transparent'}
|
||||
onPress={() => navigate(adminPath(step.targetPath))}
|
||||
title={
|
||||
<XStack alignItems="center" space="$2.5">
|
||||
<XStack alignItems="center" gap="$2.5">
|
||||
{step.isComplete ? (
|
||||
<CheckCircle2 size={18} color={theme.successText} />
|
||||
) : isNext ? (
|
||||
|
||||
@@ -77,7 +77,7 @@ export function MobileSheet({
|
||||
showsVerticalScrollIndicator={false}
|
||||
{...({ contentContainerStyle: { paddingBottom: 6 } } as any)}
|
||||
>
|
||||
<YStack space={contentSpacing}>
|
||||
<YStack gap={contentSpacing}>
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
||||
{title}
|
||||
|
||||
@@ -106,7 +106,7 @@ export function UserMenuSheet({ open, onClose, user, isMember, navigate }: UserM
|
||||
borderLeftWidth={1}
|
||||
borderColor={theme.border}
|
||||
padding="$4"
|
||||
space="$3"
|
||||
gap="$3"
|
||||
style={{
|
||||
transform: open ? 'translateX(0)' : 'translateX(100%)',
|
||||
transition: 'transform 220ms ease',
|
||||
@@ -133,7 +133,7 @@ export function UserMenuSheet({ open, onClose, user, isMember, navigate }: UserM
|
||||
</Pressable>
|
||||
</XStack>
|
||||
|
||||
<XStack alignItems="center" space="$3">
|
||||
<XStack alignItems="center" gap="$3">
|
||||
<XStack
|
||||
width={48}
|
||||
height={48}
|
||||
@@ -158,7 +158,7 @@ export function UserMenuSheet({ open, onClose, user, isMember, navigate }: UserM
|
||||
|
||||
<Separator backgroundColor={theme.border} opacity={0.6} />
|
||||
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<Text fontSize="$xs" fontWeight="700" color={theme.muted} textTransform="uppercase" letterSpacing={1}>
|
||||
{t('mobileProfile.settings', 'Einstellungen')}
|
||||
</Text>
|
||||
@@ -172,7 +172,7 @@ export function UserMenuSheet({ open, onClose, user, isMember, navigate }: UserM
|
||||
paddingHorizontal="$3"
|
||||
onPress={() => handleNavigate(item.path)}
|
||||
title={
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<XStack
|
||||
width={28}
|
||||
height={28}
|
||||
@@ -197,7 +197,7 @@ export function UserMenuSheet({ open, onClose, user, isMember, navigate }: UserM
|
||||
</YGroup>
|
||||
</YStack>
|
||||
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<Text fontSize="$xs" fontWeight="700" color={theme.muted} textTransform="uppercase" letterSpacing={1}>
|
||||
{t('settings.appearance.title', 'Darstellung')}
|
||||
</Text>
|
||||
@@ -207,7 +207,7 @@ export function UserMenuSheet({ open, onClose, user, isMember, navigate }: UserM
|
||||
paddingVertical="$2"
|
||||
paddingHorizontal="$3"
|
||||
title={
|
||||
<XStack space="$2" alignItems="center">
|
||||
<XStack gap="$2" alignItems="center">
|
||||
<Text fontSize="$sm" color={theme.textStrong}>
|
||||
{t('mobileProfile.language', 'Sprache')}
|
||||
</Text>
|
||||
@@ -235,7 +235,7 @@ export function UserMenuSheet({ open, onClose, user, isMember, navigate }: UserM
|
||||
paddingVertical="$2"
|
||||
paddingHorizontal="$3"
|
||||
title={
|
||||
<XStack space="$2" alignItems="center">
|
||||
<XStack gap="$2" alignItems="center">
|
||||
<Text fontSize="$sm" color={theme.textStrong}>
|
||||
{t('mobileProfile.theme', 'Dark Mode')}
|
||||
</Text>
|
||||
|
||||
@@ -50,7 +50,7 @@ export default function WelcomeEventPage() {
|
||||
onSkip={handleSkip}
|
||||
skipLabel={t('layout.jumpToDashboard', 'Jump to dashboard')}
|
||||
>
|
||||
<MobileCard space="$3">
|
||||
<MobileCard gap="$3">
|
||||
<Text fontSize="$sm" fontWeight="800">
|
||||
{t('eventSetup.step.title', 'Event setup in minutes')}
|
||||
</Text>
|
||||
@@ -60,7 +60,7 @@ export default function WelcomeEventPage() {
|
||||
'We guide you through name, date, mood, and tasks. Afterwards you can moderate photos and support guests live.',
|
||||
)}
|
||||
</Text>
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<FeatureRow
|
||||
icon={Sparkles}
|
||||
title={t('eventSetup.tiles.story.title', 'Story & mood')}
|
||||
@@ -79,7 +79,7 @@ export default function WelcomeEventPage() {
|
||||
</YStack>
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$2">
|
||||
<MobileCard gap="$2">
|
||||
<Text fontSize="$sm" fontWeight="800">
|
||||
{t('eventSetup.cta.heading', 'Ready for your first event?')}
|
||||
</Text>
|
||||
@@ -95,7 +95,7 @@ export default function WelcomeEventPage() {
|
||||
/>
|
||||
</MobileCard>
|
||||
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<CTAButton
|
||||
label={t('eventSetup.actions.dashboard.button', 'Open dashboard')}
|
||||
tone="ghost"
|
||||
@@ -121,7 +121,7 @@ function FeatureRow({
|
||||
body: string;
|
||||
}) {
|
||||
return (
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<XStack
|
||||
width={34}
|
||||
height={34}
|
||||
|
||||
@@ -48,7 +48,7 @@ export default function WelcomeLandingPage() {
|
||||
onSkip={handleSkip}
|
||||
skipLabel={t('layout.jumpToDashboard', 'Jump to dashboard')}
|
||||
>
|
||||
<MobileCard space="$3">
|
||||
<MobileCard gap="$3">
|
||||
<PillBadge tone="muted">{t('hero.eyebrow', 'Your event, your stage')}</PillBadge>
|
||||
<Text fontSize="$lg" fontWeight="900">
|
||||
{t('hero.title', 'Design the next Fotospiel experience')}
|
||||
@@ -59,7 +59,7 @@ export default function WelcomeLandingPage() {
|
||||
'In just a few steps you guide guests through a magical photo journey – complete with storytelling, tasks, and a moderated gallery.',
|
||||
)}
|
||||
</Text>
|
||||
<XStack space="$2" flexWrap="wrap">
|
||||
<XStack gap="$2" flexWrap="wrap">
|
||||
<CTAButton
|
||||
label={
|
||||
shouldGoBilling
|
||||
@@ -80,7 +80,7 @@ export default function WelcomeLandingPage() {
|
||||
</XStack>
|
||||
</MobileCard>
|
||||
|
||||
<YStack space="$3">
|
||||
<YStack gap="$3">
|
||||
<FeatureCard
|
||||
icon={ImageIcon}
|
||||
title={t('highlights.gallery.title', 'Premium guest gallery')}
|
||||
@@ -117,9 +117,9 @@ function FeatureCard({
|
||||
badge?: string;
|
||||
}) {
|
||||
return (
|
||||
<MobileCard space="$2">
|
||||
<MobileCard gap="$2">
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<XStack
|
||||
width={36}
|
||||
height={36}
|
||||
|
||||
@@ -85,7 +85,7 @@ export default function WelcomePackagesPage() {
|
||||
</Text>
|
||||
</MobileCard>
|
||||
) : (
|
||||
<YStack space="$3">
|
||||
<YStack gap="$3">
|
||||
{packages?.map((pkg) => (
|
||||
<PackageCard
|
||||
key={pkg.id}
|
||||
@@ -97,7 +97,7 @@ export default function WelcomePackagesPage() {
|
||||
</YStack>
|
||||
)}
|
||||
|
||||
<MobileCard space="$2">
|
||||
<MobileCard gap="$2">
|
||||
<Text fontSize="$sm" fontWeight="800">
|
||||
{t('packages.step.title', 'Activate the right plan')}
|
||||
</Text>
|
||||
@@ -106,7 +106,7 @@ export default function WelcomePackagesPage() {
|
||||
</Text>
|
||||
</MobileCard>
|
||||
|
||||
<XStack space="$2">
|
||||
<XStack gap="$2">
|
||||
<CTAButton
|
||||
label={t('packages.cta.summary.button', 'Continue to summary')}
|
||||
onPress={() => navigate(ADMIN_WELCOME_SUMMARY_PATH)}
|
||||
@@ -143,9 +143,9 @@ function PackageCard({
|
||||
|
||||
return (
|
||||
<Pressable onPress={onSelect}>
|
||||
<MobileCard borderColor={selected ? primary : border} space="$2">
|
||||
<MobileCard borderColor={selected ? primary : border} gap="$2">
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<XStack width={36} height={36} borderRadius={12} backgroundColor={accentSoft} alignItems="center" justifyContent="center">
|
||||
<PackageIcon size={18} color={primary} />
|
||||
</XStack>
|
||||
@@ -162,7 +162,7 @@ function PackageCard({
|
||||
{selected ? t('packages.card.selected', 'Selected') : t('packages.card.select', 'Select package')}
|
||||
</PillBadge>
|
||||
</XStack>
|
||||
<XStack flexWrap="wrap" space="$2">
|
||||
<XStack flexWrap="wrap" gap="$2">
|
||||
{badges.map((badge) => (
|
||||
<PillBadge key={badge as any} tone="muted">
|
||||
{badge as any}
|
||||
@@ -170,7 +170,7 @@ function PackageCard({
|
||||
))}
|
||||
</XStack>
|
||||
{selected ? (
|
||||
<XStack alignItems="center" space="$1">
|
||||
<XStack alignItems="center" gap="$1">
|
||||
<Check size={14} color={primary} />
|
||||
<Text fontSize="$xs" color={primary} fontWeight="700">
|
||||
{t('packages.card.selected', 'Selected')}
|
||||
|
||||
@@ -94,9 +94,9 @@ export default function WelcomeSummaryPage() {
|
||||
<CTAButton label={t('summary.footer.back', 'Back to package selection')} onPress={() => navigate(ADMIN_WELCOME_PACKAGES_PATH)} />
|
||||
</MobileCard>
|
||||
) : (
|
||||
<MobileCard space="$3">
|
||||
<MobileCard gap="$3">
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<XStack
|
||||
width={36}
|
||||
height={36}
|
||||
@@ -123,7 +123,7 @@ export default function WelcomeSummaryPage() {
|
||||
</PillBadge>
|
||||
</XStack>
|
||||
|
||||
<YStack space="$2">
|
||||
<YStack gap="$2">
|
||||
<SummaryRow
|
||||
label={t('summary.details.section.photosTitle', 'Photos & gallery')}
|
||||
value={t('summary.details.section.photosValue', {
|
||||
@@ -148,7 +148,7 @@ export default function WelcomeSummaryPage() {
|
||||
</YStack>
|
||||
|
||||
{resolvedPackage.active ? (
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<CheckCircle2 size={18} color={ADMIN_COLORS.success} />
|
||||
<Text fontSize="$sm" color={ADMIN_COLORS.success} fontWeight="700">
|
||||
{t('summary.details.section.statusActive', 'Already purchased')}
|
||||
@@ -158,11 +158,11 @@ export default function WelcomeSummaryPage() {
|
||||
</MobileCard>
|
||||
)}
|
||||
|
||||
<MobileCard space="$2">
|
||||
<MobileCard gap="$2">
|
||||
<Text fontSize="$sm" fontWeight="800">
|
||||
{t('summary.nextStepsTitle', 'Next steps')}
|
||||
</Text>
|
||||
<YStack space="$1">
|
||||
<YStack gap="$1">
|
||||
{(t('summary.nextSteps', {
|
||||
returnObjects: true,
|
||||
defaultValue: [
|
||||
@@ -171,7 +171,7 @@ export default function WelcomeSummaryPage() {
|
||||
'Check your event slots before go-live and share your guest link.',
|
||||
],
|
||||
}) as string[]).map((item) => (
|
||||
<XStack key={item} space="$2">
|
||||
<XStack key={item} gap="$2">
|
||||
<Text fontSize="$xs" color={ADMIN_COLORS.textMuted}>
|
||||
•
|
||||
</Text>
|
||||
@@ -183,7 +183,7 @@ export default function WelcomeSummaryPage() {
|
||||
</YStack>
|
||||
</MobileCard>
|
||||
|
||||
<XStack space="$2">
|
||||
<XStack gap="$2">
|
||||
<CTAButton
|
||||
label={t('summary.cta.billing.button', 'Go to billing')}
|
||||
tone="ghost"
|
||||
|
||||
Reference in New Issue
Block a user