Navigation now feels more “app‑like” with

stateful tabs and reliable back behavior, and a full onboarding flow is wired in with conditional package selection
  (skips when an active package exists).

  What changed

  - Added per‑tab history + back navigation fallback to make tab switching/Back feel native (resources/js/admin/mobile/
    lib/tabHistory.ts, resources/js/admin/mobile/hooks/useBackNavigation.ts, resources/js/admin/mobile/hooks/
    useMobileNav.ts, resources/js/admin/mobile/components/MobileShell.tsx + updates across mobile pages).
  - Implemented onboarding flow pages + shared shell, and wired new routes/prefetch (resources/js/admin/mobile/welcome/
    WelcomeLandingPage.tsx, resources/js/admin/mobile/welcome/WelcomePackagesPage.tsx, resources/js/admin/mobile/
    welcome/WelcomeSummaryPage.tsx, resources/js/admin/mobile/welcome/WelcomeEventPage.tsx, resources/js/admin/mobile/
    components/OnboardingShell.tsx, resources/js/admin/router.tsx, resources/js/admin/mobile/prefetch.ts).
  - Conditional package step: packages page redirects to event setup if activePackage exists; selection stored locally
    for summary (resources/js/admin/mobile/lib/onboardingSelection.ts, resources/js/admin/mobile/welcome/
    WelcomePackagesPage.tsx).
  - Added a “Start welcome journey” CTA in the empty dashboard state (resources/js/admin/mobile/DashboardPage.tsx).
  - Added translations for onboarding shell + selected package + dashboard CTA (resources/js/admin/i18n/locales/en/
    onboarding.json, resources/js/admin/i18n/locales/de/onboarding.json, resources/js/admin/i18n/locales/en/
    management.json, resources/js/admin/i18n/locales/de/management.json).
  - Tests for new helpers/hooks (resources/js/admin/mobile/lib/tabHistory.test.ts, resources/js/admin/mobile/lib/
    onboardingSelection.test.ts, resources/js/admin/mobile/hooks/useBackNavigation.test.tsx).
This commit is contained in:
Codex Agent
2025-12-28 19:51:57 +01:00
parent 718c129a8d
commit cf73f408b2
36 changed files with 1097 additions and 47 deletions

View File

@@ -1,5 +1,5 @@
import React, { Suspense } from 'react';
import { useNavigate } from 'react-router-dom';
import { useLocation, useNavigate } from 'react-router-dom';
import { ChevronDown, ChevronLeft, Bell, QrCode } from 'lucide-react';
import { YStack, XStack } from '@tamagui/stacks';
import { SizableText as Text } from '@tamagui/text';
@@ -17,6 +17,7 @@ import { useOnlineStatus } from '../hooks/useOnlineStatus';
import { formatEventDate, resolveEventDisplayName } from '../../lib/events';
import { TenantEvent, getEvents } from '../../api';
import { withAlpha } from './colors';
import { setTabHistory } from '../lib/tabHistory';
type MobileShellProps = {
title?: string;
@@ -31,6 +32,7 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
const { events, activeEvent, hasMultipleEvents, hasEvents, selectEvent } = useEventContext();
const { go } = useMobileNav(activeEvent?.slug);
const navigate = useNavigate();
const location = useLocation();
const { t, i18n } = useTranslation('mobile');
const { count: notificationCount } = useNotificationsBadge();
const online = useOnlineStatus();
@@ -81,6 +83,11 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
.finally(() => setLoadingEvents(false));
}, [pickerOpen, effectiveEvents.length]);
React.useEffect(() => {
const path = `${location.pathname}${location.search}${location.hash}`;
setTabHistory(activeTab, path);
}, [activeTab, location.hash, location.pathname, location.search]);
const eventTitle = title ?? (effectiveActive ? resolveEventDisplayName(effectiveActive) : t('header.appName', 'Event Admin'));
const subtitleText =
subtitle ??

View File

@@ -0,0 +1,113 @@
import React from 'react';
import { ChevronLeft } from 'lucide-react';
import { YStack, XStack } from '@tamagui/stacks';
import { SizableText as Text } from '@tamagui/text';
import { Pressable } from '@tamagui/react-native-web-lite';
import { useTheme } from '@tamagui/core';
import { useTranslation } from 'react-i18next';
type OnboardingShellProps = {
eyebrow?: string;
title: string;
subtitle?: string;
children: React.ReactNode;
onBack?: () => void;
onSkip?: () => void;
footer?: React.ReactNode;
backLabel?: string;
skipLabel?: string;
};
export function OnboardingShell({
eyebrow,
title,
subtitle,
children,
onBack,
onSkip,
footer,
backLabel,
skipLabel,
}: OnboardingShellProps) {
const { t } = useTranslation('onboarding');
const theme = useTheme();
const background = String(theme.background?.val ?? '#f7f8fb');
const surface = String(theme.surface?.val ?? '#ffffff');
const text = String(theme.color?.val ?? '#111827');
const muted = String(theme.gray?.val ?? '#6b7280');
const border = String(theme.borderColor?.val ?? '#e5e7eb');
const resolvedBackLabel = backLabel ?? t('layout.back', 'Back');
const resolvedSkipLabel = skipLabel ?? t('layout.skip', 'Skip');
return (
<YStack minHeight="100vh" backgroundColor={background} alignItems="center">
<YStack
width="100%"
maxWidth={860}
paddingHorizontal="$5"
paddingTop="$5"
paddingBottom="$6"
space="$4"
style={{
paddingTop: 'calc(env(safe-area-inset-top, 0px) + 20px)',
paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 24px)',
}}
>
<XStack alignItems="center" justifyContent="space-between">
{onBack ? (
<Pressable onPress={onBack} aria-label="Back">
<XStack alignItems="center" space="$1.5">
<ChevronLeft size={22} color={text} />
<Text fontSize="$sm" fontWeight="700" color={text}>
{resolvedBackLabel}
</Text>
</XStack>
</Pressable>
) : (
<XStack width={64} />
)}
{onSkip ? (
<Pressable onPress={onSkip} aria-label="Skip">
<Text fontSize="$sm" fontWeight="700" color={muted}>
{resolvedSkipLabel}
</Text>
</Pressable>
) : (
<XStack width={64} />
)}
</XStack>
<YStack
padding="$4"
borderRadius={20}
borderWidth={1}
borderColor={border}
backgroundColor={surface}
shadowColor="#0f172a"
shadowOpacity={0.06}
shadowRadius={14}
shadowOffset={{ width: 0, height: 8 }}
space="$2"
>
{eyebrow ? (
<Text fontSize="$xs" fontWeight="700" color={muted} textTransform="uppercase" letterSpacing={0.6}>
{eyebrow}
</Text>
) : null}
<Text fontSize="$xl" fontWeight="900" color={text}>
{title}
</Text>
{subtitle ? (
<Text fontSize="$sm" color={muted}>
{subtitle}
</Text>
) : null}
</YStack>
<YStack space="$4">{children}</YStack>
{footer ? <YStack marginTop="$2">{footer}</YStack> : null}
</YStack>
</YStack>
);
}