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:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user