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

@@ -38,46 +38,48 @@ export default function MobileQrPrintPage() {
const [qrImage, setQrImage] = React.useState<string>('');
const back = useBackNavigation(slug ? adminPath(`/mobile/events/${slug}`) : adminPath('/mobile/events'));
React.useEffect(() => {
const load = React.useCallback(async () => {
if (!slug) return;
(async () => {
setLoading(true);
try {
const data = await getEvent(slug);
const invites = await getEventQrInvites(slug);
setEvent(data);
const primaryInvite = invites.find((item) => item.is_active) ?? invites[0] ?? null;
setSelectedInvite(primaryInvite);
const initialLayout = primaryInvite?.layouts?.[0];
const initialFormat =
initialLayout && ((initialLayout.panel_mode ?? '').toLowerCase() === 'double-mirror' || (initialLayout.orientation ?? '').toLowerCase() === 'landscape')
? 'a5-foldable'
: 'a4-poster';
setSelectedFormat(initialFormat);
const resolvedLayoutId = primaryInvite?.layouts
? resolveLayoutForFormat(initialFormat, primaryInvite.layouts) ?? initialLayout?.id ?? null
: initialLayout?.id ?? null;
setSelectedLayoutId(resolvedLayoutId);
setQrUrl(primaryInvite?.url ?? data.public_url ?? '');
setQrImage(primaryInvite?.qr_code_data_url ?? '');
setError(null);
} catch (err) {
if (!isAuthError(err)) {
setError(getApiErrorMessage(err, t('events.errors.loadFailed', 'QR-Daten konnten nicht geladen werden.')));
}
} finally {
setLoading(false);
setLoading(true);
try {
const data = await getEvent(slug);
const invites = await getEventQrInvites(slug);
setEvent(data);
const primaryInvite = invites.find((item) => item.is_active) ?? invites[0] ?? null;
setSelectedInvite(primaryInvite);
const initialLayout = primaryInvite?.layouts?.[0];
const initialFormat =
initialLayout && ((initialLayout.panel_mode ?? '').toLowerCase() === 'double-mirror' || (initialLayout.orientation ?? '').toLowerCase() === 'landscape')
? 'a5-foldable'
: 'a4-poster';
setSelectedFormat(initialFormat);
const resolvedLayoutId = primaryInvite?.layouts
? resolveLayoutForFormat(initialFormat, primaryInvite.layouts) ?? initialLayout?.id ?? null
: initialLayout?.id ?? null;
setSelectedLayoutId(resolvedLayoutId);
setQrUrl(primaryInvite?.url ?? data.public_url ?? '');
setQrImage(primaryInvite?.qr_code_data_url ?? '');
setError(null);
} catch (err) {
if (!isAuthError(err)) {
setError(getApiErrorMessage(err, t('events.errors.loadFailed', 'QR-Daten konnten nicht geladen werden.')));
}
})();
} finally {
setLoading(false);
}
}, [slug, t]);
React.useEffect(() => {
void load();
}, [load]);
return (
<MobileShell
activeTab="home"
title={t('events.qr.title', 'QR Code & Print Layouts')}
onBack={back}
headerActions={
<HeaderActionButton onPress={() => window.location.reload()} ariaLabel={t('common.refresh', 'Refresh')}>
<HeaderActionButton onPress={() => load()} ariaLabel={t('common.refresh', 'Refresh')}>
<RefreshCcw size={18} color="#0f172a" />
</HeaderActionButton>
}
@@ -87,6 +89,12 @@ export default function MobileQrPrintPage() {
<Text fontWeight="700" color="#b91c1c">
{error}
</Text>
<CTAButton
label={t('common.retry', 'Retry')}
tone="ghost"
fullWidth={false}
onPress={() => load()}
/>
</MobileCard>
) : null}
@@ -149,6 +157,15 @@ export default function MobileQrPrintPage() {
onPress={async () => {
try {
const shareUrl = String(qrUrl || (event as any)?.public_url || '');
if (navigator.share && shareUrl) {
await navigator.share({
title: t('events.qr.title', 'QR Code & Print Layouts'),
text: t('events.qr.description', 'Scan to access the event guest app.'),
url: shareUrl,
});
toast.success(t('events.qr.shareSuccess', 'Link geteilt'));
return;
}
await navigator.clipboard.writeText(shareUrl);
toast.success(t('events.qr.shareSuccess', 'Link kopiert'));
} catch {