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:
@@ -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 ??
|
||||
|
||||
113
resources/js/admin/mobile/components/OnboardingShell.tsx
Normal file
113
resources/js/admin/mobile/components/OnboardingShell.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user