import React from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { ChevronLeft, Bell, QrCode, ChevronsUpDown, Search } 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 { Image } from '@tamagui/image'; import { useTranslation } from 'react-i18next'; import { useEventContext } from '../../context/EventContext'; import { BottomNav, NavKey } from './BottomNav'; import { useMobileNav } from '../hooks/useMobileNav'; import { ADMIN_EVENTS_PATH, adminPath } from '../../constants'; import { MobileCard, CTAButton } from './Primitives'; import { useNotificationsBadge } from '../hooks/useNotificationsBadge'; import { useOnlineStatus } from '../hooks/useOnlineStatus'; import { resolveEventDisplayName } from '../../lib/events'; import { TenantEvent, getEvents } from '../../api'; import { setTabHistory } from '../lib/tabHistory'; import { loadPhotoQueue } from '../lib/photoModerationQueue'; import { countQueuedPhotoActions } from '../lib/queueStatus'; import { useAdminTheme } from '../theme'; import { useAuth } from '../../auth/context'; import { EventSwitcherSheet } from './EventSwitcherSheet'; type MobileShellProps = { title?: string; children: React.ReactNode; activeTab: NavKey; onBack?: () => void; headerActions?: React.ReactNode; }; export function MobileShell({ title, children, activeTab, onBack, headerActions }: MobileShellProps) { const { events, activeEvent, selectEvent } = useEventContext(); const { user } = useAuth(); const { go } = useMobileNav(activeEvent?.slug, activeTab); const navigate = useNavigate(); const location = useLocation(); const { t } = useTranslation('mobile'); const { count: notificationCount } = useNotificationsBadge(); const online = useOnlineStatus(); const theme = useAdminTheme(); const backgroundColor = theme.background; const [isCompactHeader, setIsCompactHeader] = React.useState(false); // --- DARK HEADER --- const headerSurface = '#0F172A'; // Slate 900 const actionSurface = theme.primary; const actionBorder = 'rgba(255, 255, 255, 0.1)'; const [fallbackEvents, setFallbackEvents] = React.useState([]); const [loadingEvents, setLoadingEvents] = React.useState(false); const [attemptedFetch, setAttemptedFetch] = React.useState(false); const [queuedPhotoCount, setQueuedPhotoCount] = React.useState(0); const [switcherOpen, setSwitcherOpen] = React.useState(false); const effectiveEvents = events.length ? events : fallbackEvents; const effectiveActive = activeEvent ?? (effectiveEvents.length === 1 ? effectiveEvents[0] : null); React.useEffect(() => { if (events.length || loadingEvents || attemptedFetch) { return; } setAttemptedFetch(true); setLoadingEvents(true); getEvents({ force: true }) .then((list) => { setFallbackEvents(list ?? []); if (!activeEvent && list?.length === 1) { selectEvent(list[0]?.slug ?? null); } }) .catch(() => setFallbackEvents([])) .finally(() => setLoadingEvents(false)); }, [events.length, loadingEvents, attemptedFetch, activeEvent, selectEvent]); React.useEffect(() => { if (typeof window === 'undefined' || !window.matchMedia) { return; } const mediaQuery = window.matchMedia('(max-width: 320px)'); const handleChange = (event: MediaQueryListEvent | MediaQueryList) => { setIsCompactHeader(event.matches); }; handleChange(mediaQuery); if (typeof mediaQuery.addEventListener === 'function') { mediaQuery.addEventListener('change', handleChange); return () => mediaQuery.removeEventListener('change', handleChange); } mediaQuery.addListener?.(handleChange); return () => mediaQuery.removeListener?.(handleChange); }, []); React.useEffect(() => { const path = `${location.pathname}${location.search}${location.hash}`; if (!location.pathname.includes('/billing/shop') && !location.pathname.includes('/welcome')) { setTabHistory(activeTab, path); } }, [activeTab, location.hash, location.pathname, location.search]); const refreshQueuedActions = React.useCallback(() => { const queue = loadPhotoQueue(); setQueuedPhotoCount(countQueuedPhotoActions(queue, effectiveActive?.slug ?? null)); }, [effectiveActive?.slug]); React.useEffect(() => { refreshQueuedActions(); }, [refreshQueuedActions, location.pathname]); React.useEffect(() => { const handleFocus = () => refreshQueuedActions(); window.addEventListener('focus', handleFocus); return () => window.removeEventListener('focus', handleFocus); }, [refreshQueuedActions]); const pageTitle = title ?? t('header.appName', 'Event Admin'); const isEventsIndex = location.pathname === ADMIN_EVENTS_PATH; const canSwitchEvents = effectiveEvents.length > 1 && !isEventsIndex; const isMember = user?.role === 'member'; const memberPermissions = Array.isArray(effectiveActive?.member_permissions) ? effectiveActive?.member_permissions ?? [] : []; const allowPermission = (permission: string) => { if (!isMember) return true; if (memberPermissions.includes('*') || memberPermissions.includes(permission)) return true; if (permission.includes(':')) { const [prefix] = permission.split(':'); return memberPermissions.includes(`${prefix}:*`); } return false; }; const showQr = Boolean(effectiveActive?.slug) && allowPermission('join-tokens:manage'); // --- CONTEXT PILL --- const EventContextPill = () => { if (!effectiveActive || isEventsIndex || isCompactHeader) { return ( {pageTitle} ); } const displayName = resolveEventDisplayName(effectiveActive); if (!canSwitchEvents) { return ( {displayName} ); } return ( setSwitcherOpen(true)} aria-label={t('header.eventSwitcher', 'Switch event')}> {displayName} ); }; const headerBackButton = onBack ? ( ) : ( {}} ariaLabel="Search"> ); const headerActionsRow = ( {showQr ? ( navigate(adminPath(`/mobile/events/${effectiveActive?.slug}/qr`))} ariaLabel={t('header.quickQr', 'Quick QR')} > ) : null} navigate(adminPath('/mobile/notifications'))} ariaLabel={t('mobile.notifications', 'Notifications')} > {notificationCount > 0 ? ( {notificationCount > 9 ? '9+' : notificationCount} ) : null} {/* User Avatar */} navigate(adminPath('/mobile/profile'))}> {user?.avatar_url ? ( ) : ( {user?.name?.charAt(0).toUpperCase() ?? 'U'} )} {headerActions ?? null} ); return ( {headerBackButton} {headerActionsRow} {!online ? ( {t('status.offline', 'Offline mode: changes will sync when you are back online.')} ) : null} {queuedPhotoCount > 0 ? ( {t('status.queueTitle', 'Photo actions pending')} {online ? t('status.queueBodyOnline', '{{count}} actions ready to sync.', { count: queuedPhotoCount }) : t('status.queueBodyOffline', '{{count}} actions saved offline.', { count: queuedPhotoCount })} {effectiveActive?.slug ? ( navigate(adminPath(`/mobile/events/${effectiveActive.slug}/control-room`))} /> ) : null} ) : null} {children} setSwitcherOpen(false)} events={effectiveEvents} activeSlug={effectiveActive?.slug ?? null} /> ); } export function HeaderActionButton({ onPress, children, ariaLabel, }: { onPress: () => void; children: React.ReactNode; ariaLabel?: string; }) { const [pressed, setPressed] = React.useState(false); return ( setPressed(true)} onPressOut={() => setPressed(false)} onPointerLeave={() => setPressed(false)} aria-label={ariaLabel} style={{ transform: pressed ? 'scale(0.96)' : 'scale(1)', opacity: pressed ? 0.86 : 1, transition: 'transform 120ms ease, opacity 120ms ease', }} > {children} ); }