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:
43
resources/js/admin/mobile/lib/lightboxImage.test.ts
Normal file
43
resources/js/admin/mobile/lib/lightboxImage.test.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { TenantPhoto } from '../../api';
|
||||
import { resolveLightboxSources } from './lightboxImage';
|
||||
|
||||
const basePhoto = (overrides: Partial<TenantPhoto>): TenantPhoto => ({
|
||||
id: overrides.id ?? 1,
|
||||
filename: overrides.filename ?? null,
|
||||
original_name: overrides.original_name ?? null,
|
||||
mime_type: overrides.mime_type ?? null,
|
||||
size: overrides.size ?? 1024,
|
||||
url: overrides.url ?? null,
|
||||
thumbnail_url: overrides.thumbnail_url ?? null,
|
||||
status: overrides.status ?? 'approved',
|
||||
is_featured: overrides.is_featured ?? false,
|
||||
likes_count: overrides.likes_count ?? 0,
|
||||
uploaded_at: overrides.uploaded_at ?? new Date().toISOString(),
|
||||
uploader_name: overrides.uploader_name ?? null,
|
||||
ingest_source: overrides.ingest_source ?? null,
|
||||
caption: overrides.caption ?? null,
|
||||
});
|
||||
|
||||
describe('resolveLightboxSources', () => {
|
||||
it('prefers thumbnail for initial source and keeps full when different', () => {
|
||||
const photo = basePhoto({ thumbnail_url: '/thumb.jpg', url: '/full.jpg' });
|
||||
const sources = resolveLightboxSources(photo);
|
||||
expect(sources.initial).toBe('/thumb.jpg');
|
||||
expect(sources.full).toBe('/full.jpg');
|
||||
});
|
||||
|
||||
it('falls back to full when thumbnail is missing', () => {
|
||||
const photo = basePhoto({ thumbnail_url: null, url: '/full.jpg' });
|
||||
const sources = resolveLightboxSources(photo);
|
||||
expect(sources.initial).toBe('/full.jpg');
|
||||
expect(sources.full).toBe('/full.jpg');
|
||||
});
|
||||
|
||||
it('returns nulls when no sources exist', () => {
|
||||
const photo = basePhoto({ thumbnail_url: null, url: null });
|
||||
const sources = resolveLightboxSources(photo);
|
||||
expect(sources.initial).toBeNull();
|
||||
expect(sources.full).toBeNull();
|
||||
});
|
||||
});
|
||||
16
resources/js/admin/mobile/lib/lightboxImage.ts
Normal file
16
resources/js/admin/mobile/lib/lightboxImage.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { TenantPhoto } from '../../api';
|
||||
|
||||
export type LightboxImageSources = {
|
||||
initial: string | null;
|
||||
full: string | null;
|
||||
};
|
||||
|
||||
export function resolveLightboxSources(photo: TenantPhoto): LightboxImageSources {
|
||||
const thumbnail = photo.thumbnail_url ?? null;
|
||||
const full = photo.url ?? null;
|
||||
|
||||
return {
|
||||
initial: thumbnail ?? full,
|
||||
full: full && full !== thumbnail ? full : null,
|
||||
};
|
||||
}
|
||||
23
resources/js/admin/mobile/lib/queueStatus.test.ts
Normal file
23
resources/js/admin/mobile/lib/queueStatus.test.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { PhotoModerationAction } from './photoModerationQueue';
|
||||
import { countQueuedPhotoActions } from './queueStatus';
|
||||
|
||||
const baseAction = (overrides: Partial<PhotoModerationAction>): PhotoModerationAction => ({
|
||||
id: overrides.id ?? '1',
|
||||
eventSlug: overrides.eventSlug ?? 'event-a',
|
||||
photoId: overrides.photoId ?? 1,
|
||||
action: overrides.action ?? 'approve',
|
||||
createdAt: overrides.createdAt ?? new Date().toISOString(),
|
||||
});
|
||||
|
||||
describe('countQueuedPhotoActions', () => {
|
||||
it('returns total count when slug is not provided', () => {
|
||||
const queue = [baseAction({ id: '1' }), baseAction({ id: '2', eventSlug: 'event-b' })];
|
||||
expect(countQueuedPhotoActions(queue)).toBe(2);
|
||||
});
|
||||
|
||||
it('filters by slug when provided', () => {
|
||||
const queue = [baseAction({ id: '1' }), baseAction({ id: '2', eventSlug: 'event-b' })];
|
||||
expect(countQueuedPhotoActions(queue, 'event-a')).toBe(1);
|
||||
});
|
||||
});
|
||||
8
resources/js/admin/mobile/lib/queueStatus.ts
Normal file
8
resources/js/admin/mobile/lib/queueStatus.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { PhotoModerationAction } from './photoModerationQueue';
|
||||
|
||||
export function countQueuedPhotoActions(queue: PhotoModerationAction[], slug?: string | null): number {
|
||||
if (!slug) {
|
||||
return queue.length;
|
||||
}
|
||||
return queue.filter((item) => item.eventSlug === slug).length;
|
||||
}
|
||||
Reference in New Issue
Block a user