I finished the remaining reliability, sharing, performance, and polish items across the admin

app.
  What’s done
    locales/en/mobile.json and resources/js/admin/i18n/locales/de/mobile.json.
  - Error recovery CTAs on Photos, Notifications, Tasks, and QR screens so users can retry without a full reload in    resources/js/admin/mobile/EventPhotosPage.tsx, resources/js/admin/mobile/NotificationsPage.tsx, resources/js/admin/
    mobile/EventTasksPage.tsx, resources/js/admin/mobile/QrPrintPage.tsx.
  - QR share uses native share sheet when available, with clipboard fallback in resources/js/admin/mobile/
    QrPrintPage.tsx.
  - Lazy‑loaded photo grid thumbnails for better performance in resources/js/admin/mobile/EventPhotosPage.tsx.
  - New helper + tests for queue count logic in resources/js/admin/mobile/lib/queueStatus.ts and resources/js/admin/
    mobile/lib/queueStatus.test.ts.
This commit is contained in:
Codex Agent
2025-12-28 21:29:30 +01:00
parent 1e0c38fce4
commit 9d367512c5
12 changed files with 337 additions and 57 deletions

View File

@@ -11,13 +11,15 @@ import { BottomNav, NavKey } from './BottomNav';
import { useMobileNav } from '../hooks/useMobileNav';
import { adminPath } from '../../constants';
import { MobileSheet } from './Sheet';
import { MobileCard, PillBadge } from './Primitives';
import { MobileCard, PillBadge, CTAButton } from './Primitives';
import { useNotificationsBadge } from '../hooks/useNotificationsBadge';
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';
import { loadPhotoQueue } from '../lib/photoModerationQueue';
import { countQueuedPhotoActions } from '../lib/queueStatus';
type MobileShellProps = {
title?: string;
@@ -49,6 +51,7 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
const [fallbackEvents, setFallbackEvents] = React.useState<TenantEvent[]>([]);
const [loadingEvents, setLoadingEvents] = React.useState(false);
const [attemptedFetch, setAttemptedFetch] = React.useState(false);
const [queuedPhotoCount, setQueuedPhotoCount] = React.useState(0);
const locale = i18n.language?.startsWith('en') ? 'en-GB' : 'de-DE';
const effectiveEvents = events.length ? events : fallbackEvents;
@@ -88,6 +91,23 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
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 eventTitle = title ?? (effectiveActive ? resolveEventDisplayName(effectiveActive) : t('header.appName', 'Event Admin'));
const subtitleText =
subtitle ??
@@ -235,10 +255,30 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
paddingHorizontal="$3"
>
<Text fontSize="$xs" fontWeight="700" color={warningText}>
{t('mobile.offline', 'Offline mode: changes will sync when you are back online.')}
{t('status.offline', 'Offline mode: changes will sync when you are back online.')}
</Text>
</XStack>
) : null}
{queuedPhotoCount > 0 ? (
<MobileCard space="$2">
<Text fontSize="$sm" fontWeight="700" color={textColor}>
{t('status.queueTitle', 'Photo actions pending')}
</Text>
<Text fontSize="$xs" color={mutedText}>
{online
? t('status.queueBodyOnline', '{{count}} actions ready to sync.', { count: queuedPhotoCount })
: t('status.queueBodyOffline', '{{count}} actions saved offline.', { count: queuedPhotoCount })}
</Text>
{effectiveActive?.slug ? (
<CTAButton
label={t('status.queueAction', 'Open Photos')}
tone="ghost"
fullWidth={false}
onPress={() => navigate(adminPath(`/mobile/events/${effectiveActive.slug}/photos`))}
/>
) : null}
</MobileCard>
) : null}
{children}
</YStack>