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