Files
fotospiel-app/resources/js/admin/context/EventContext.tsx
Codex Agent 73e550ee87 Implemented a shared mobile shell and navigation aligned to the new architecture, plus refactored the dashboard and
tab flows.

  - Added a dynamic MobileShell with sticky header (notification bell with badge, quick QR when an event is
    active, event switcher for multi-event users) and stabilized bottom tabs (home, tasks, uploads, profile)
    driven by useMobileNav (resources/js/admin/mobile/components/MobileShell.tsx, components/BottomNav.tsx, hooks/
    useMobileNav.ts).
  - Centralized event handling now supports 0/1/many-event states without auto-selecting in multi-tenant mode and
    exposes helper flags/activeSlug for consumers (resources/js/admin/context/EventContext.tsx).
  - Rebuilt the mobile dashboard into explicit states: onboarding/no-event, single-event focus, and multi-event picker
    with featured/secondary actions, KPI strip, and alerts (resources/js/admin/mobile/DashboardPage.tsx).
  - Introduced tab entry points that respect event context and prompt selection when needed (resources/js/admin/
    mobile/TasksTabPage.tsx, UploadsTabPage.tsx). Refreshed tasks/uploads detail screens to use the new shell and sync
    event selection (resources/js/admin/mobile/EventTasksPage.tsx, EventPhotosPage.tsx).
  - Updated mobile routes and existing screens to the new tab keys and header/footer behavior (resources/js/admin/
    router.tsx, mobile/* pages, i18n nav/header strings).
2025-12-10 16:13:44 +01:00

137 lines
3.7 KiB
TypeScript

import React from 'react';
import { useQuery } from '@tanstack/react-query';
import { getEvents, type TenantEvent } from '../api';
import { useAuth } from '../auth/context';
const STORAGE_KEY = 'tenant-admin.active-event';
export interface EventContextValue {
events: TenantEvent[];
isLoading: boolean;
isError: boolean;
activeEvent: TenantEvent | null;
activeSlug: string | null;
hasEvents: boolean;
hasMultipleEvents: boolean;
selectEvent: (slug: string | null) => void;
refetch: () => void;
}
const EventContext = React.createContext<EventContextValue | undefined>(undefined);
export function EventProvider({ children }: { children: React.ReactNode }) {
const { status } = useAuth();
const [storedSlug, setStoredSlug] = React.useState<string | null>(() => {
if (typeof window === 'undefined') {
return null;
}
return window.localStorage.getItem(STORAGE_KEY);
});
const authReady = status === 'authenticated';
const {
data: fetchedEvents = [],
isLoading: queryLoading,
isError,
refetch,
} = useQuery<TenantEvent[]>({
queryKey: ['tenant-events'],
queryFn: async () => {
try {
return await getEvents({ force: true });
} catch (error) {
console.warn('[EventContext] Failed to fetch events', error);
throw error;
}
},
staleTime: 60 * 1000,
enabled: authReady,
initialData: [],
});
const events = React.useMemo(() => (authReady ? fetchedEvents : []), [authReady, fetchedEvents]);
const isLoading = authReady ? queryLoading : status === 'loading';
React.useEffect(() => {
if (!events.length || typeof window === 'undefined') {
return;
}
const hasStored = Boolean(storedSlug);
const slugExists = hasStored && events.some((event) => event.slug === storedSlug);
if (!slugExists) {
const shouldAutoselect = events.length === 1;
const fallbackSlug = shouldAutoselect ? events[0]?.slug : null;
setStoredSlug(fallbackSlug);
if (fallbackSlug) {
window.localStorage.setItem(STORAGE_KEY, fallbackSlug);
} else {
window.localStorage.removeItem(STORAGE_KEY);
}
}
}, [events, storedSlug]);
const activeEvent = React.useMemo(() => {
if (!events.length) {
return null;
}
const matched = events.find((event) => event.slug && event.slug === storedSlug);
if (matched) {
return matched;
}
// Only auto-select the single available event. When multiple events exist and
// no stored slug is present we intentionally return null to let the UI prompt
// for a selection.
if (events.length === 1) {
return events[0];
}
return null;
}, [events, storedSlug]);
const hasEvents = events.length > 0;
const hasMultipleEvents = events.length > 1;
const activeSlug = activeEvent?.slug ?? null;
const selectEvent = React.useCallback((slug: string | null) => {
setStoredSlug(slug);
if (typeof window !== 'undefined') {
if (slug) {
window.localStorage.setItem(STORAGE_KEY, slug);
} else {
window.localStorage.removeItem(STORAGE_KEY);
}
}
}, []);
const value = React.useMemo<EventContextValue>(
() => ({
events,
isLoading,
isError,
activeEvent,
activeSlug,
hasEvents,
hasMultipleEvents,
selectEvent,
refetch,
}),
[events, isLoading, isError, activeEvent, activeSlug, hasEvents, hasMultipleEvents, selectEvent, refetch]
);
return <EventContext.Provider value={value}>{children}</EventContext.Provider>;
}
export function useEventContext(): EventContextValue {
const ctx = React.useContext(EventContext);
if (!ctx) {
throw new Error('useEventContext must be used within an EventProvider');
}
return ctx;
}