feat(mobile): implement event switcher sheet in header
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled

- Replaced direct navigation with a bottom sheet for event switching
- Created reusable EventSwitcherSheet component
- Preserves context when switching events
This commit is contained in:
Codex Agent
2026-01-17 19:17:19 +01:00
parent 9d7990fe71
commit 45f0cea264
2 changed files with 97 additions and 2 deletions

View File

@@ -0,0 +1,86 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { YStack, XStack } from '@tamagui/stacks';
import { SizableText as Text } from '@tamagui/text';
import { Pressable } from '@tamagui/react-native-web-lite';
import { CheckCircle2, Circle } from 'lucide-react';
import { MobileSheet } from './Sheet';
import { useAdminTheme } from '../theme';
import { TenantEvent } from '../../api';
import { resolveEventDisplayName, formatEventDate } from '../../lib/events';
import { adminPath } from '../../constants';
export function EventSwitcherSheet({
open,
onClose,
events,
activeSlug,
}: {
open: boolean;
onClose: () => void;
events: TenantEvent[];
activeSlug: string | null;
}) {
const { t, i18n } = useTranslation(['management', 'mobile']);
const navigate = useNavigate();
const theme = useAdminTheme();
const locale = i18n.language;
const handleSelect = (slug: string) => {
onClose();
// Navigate to the dashboard of the selected event
navigate(adminPath(`/mobile/events/${slug}`));
};
return (
<MobileSheet
open={open}
title={t('mobile:header.eventSwitcher', 'Switch Event')}
onClose={onClose}
snapPoints={[65]}
>
<YStack space="$2">
{events.map((event) => {
const isActive = event.slug === activeSlug;
return (
<Pressable key={event.slug} onPress={() => handleSelect(event.slug)}>
<XStack
alignItems="center"
space="$3"
padding="$3"
borderRadius={14}
backgroundColor={isActive ? theme.surfaceMuted : 'transparent'}
borderWidth={1}
borderColor={isActive ? theme.border : 'transparent'}
>
<YStack flex={1}>
<Text fontSize="$sm" fontWeight="700" color={theme.textStrong}>
{resolveEventDisplayName(event)}
</Text>
<Text fontSize="$xs" color={theme.muted}>
{formatEventDate(event.event_date, locale)}
</Text>
</YStack>
{isActive ? (
<CheckCircle2 size={20} color={theme.primary} />
) : (
<Circle size={20} color={theme.border} />
)}
</XStack>
</Pressable>
);
})}
<Pressable onPress={() => { onClose(); navigate(adminPath('/mobile/events/new')); }}>
<XStack padding="$3" justifyContent="center">
<Text fontSize="$sm" fontWeight="700" color={theme.primary}>
{t('mobile:header.createEvent', 'Create Event')}
</Text>
</XStack>
</Pressable>
</YStack>
</MobileSheet>
);
}

View File

@@ -21,6 +21,7 @@ import { loadPhotoQueue } from '../lib/photoModerationQueue';
import { countQueuedPhotoActions } from '../lib/queueStatus'; import { countQueuedPhotoActions } from '../lib/queueStatus';
import { useAdminTheme } from '../theme'; import { useAdminTheme } from '../theme';
import { useAuth } from '../../auth/context'; import { useAuth } from '../../auth/context';
import { EventSwitcherSheet } from './EventSwitcherSheet';
type MobileShellProps = { type MobileShellProps = {
title?: string; title?: string;
@@ -55,6 +56,7 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
const [loadingEvents, setLoadingEvents] = React.useState(false); const [loadingEvents, setLoadingEvents] = React.useState(false);
const [attemptedFetch, setAttemptedFetch] = React.useState(false); const [attemptedFetch, setAttemptedFetch] = React.useState(false);
const [queuedPhotoCount, setQueuedPhotoCount] = React.useState(0); const [queuedPhotoCount, setQueuedPhotoCount] = React.useState(0);
const [switcherOpen, setSwitcherOpen] = React.useState(false);
const effectiveEvents = events.length ? events : fallbackEvents; const effectiveEvents = events.length ? events : fallbackEvents;
const effectiveActive = activeEvent ?? (effectiveEvents.length === 1 ? effectiveEvents[0] : null); const effectiveActive = activeEvent ?? (effectiveEvents.length === 1 ? effectiveEvents[0] : null);
@@ -136,7 +138,7 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
} }
return ( return (
<Pressable onPress={() => navigate(ADMIN_EVENTS_PATH)}> <Pressable onPress={() => setSwitcherOpen(true)}>
<XStack <XStack
backgroundColor="rgba(255, 255, 255, 0.12)" backgroundColor="rgba(255, 255, 255, 0.12)"
paddingHorizontal="$3" paddingHorizontal="$3"
@@ -334,6 +336,13 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
</YStack> </YStack>
<BottomNav active={activeTab} onNavigate={go} /> <BottomNav active={activeTab} onNavigate={go} />
<EventSwitcherSheet
open={switcherOpen}
onClose={() => setSwitcherOpen(false)}
events={effectiveEvents}
activeSlug={effectiveActive?.slug ?? null}
/>
</YStack> </YStack>
); );
} }