✨ Add grid/list layout toggle to admin Photos tab
The event detail Photos tab (AdminPhotoGrid) only offered a thumbnail grid. Add a Grid/List toggle in the action bar so admins can scan photos in a compact, metadata-oriented list. - New utils/photoViewPrefs.ts persists the choice per admin via localStorage (mirrors utils/calendarPrefs.ts), defaulting to grid - List view is a compact <table> following the established admin list pattern (EventsListPage), with responsive column hiding: Photo (thumbnail + filename + original + Video/Hidden badges), Category (lg+), Uploaded date (md+, via useLocalizedDate), Engagement views/downloads/likes (xl+), Feedback rating/comments (sm+), Size, and hover Actions (download, delete) - Rows reuse the existing selection, download, delete and category handlers; row click opens the photo viewer - Toggle buttons use LayoutGrid / List icons with aria-pressed state - Add en.json + de.json keys under admin.photos (viewMode, gridView, listView, columns.*) - Tests for the persistence util and the toggle's render + persistence
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Coverage for the admin Photos-tab layout toggle persistence.
|
||||
*
|
||||
* The event detail Photos tab can render as a Grid or a List; the
|
||||
* choice is stored per browser/admin via localStorage so it survives
|
||||
* reloads. These tests pin the default ('grid'), the round-trip, and
|
||||
* the defensive fallback on malformed / blocked storage so a refactor
|
||||
* can't silently break the persisted preference.
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { getPhotoViewMode, setPhotoViewMode } from '../photoViewPrefs';
|
||||
|
||||
describe('photoViewPrefs', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('defaults to grid when nothing is stored', () => {
|
||||
expect(getPhotoViewMode()).toBe('grid');
|
||||
});
|
||||
|
||||
it('round-trips a persisted view mode', () => {
|
||||
setPhotoViewMode('list');
|
||||
expect(getPhotoViewMode()).toBe('list');
|
||||
setPhotoViewMode('grid');
|
||||
expect(getPhotoViewMode()).toBe('grid');
|
||||
});
|
||||
|
||||
it('falls back to grid for an unrecognised stored value', () => {
|
||||
localStorage.setItem('picpeak.adminPhotos.view', 'mosaic');
|
||||
expect(getPhotoViewMode()).toBe('grid');
|
||||
});
|
||||
|
||||
it('ignores attempts to persist an invalid view mode', () => {
|
||||
setPhotoViewMode('list');
|
||||
// @ts-expect-error — exercising the runtime guard against bad input
|
||||
setPhotoViewMode('carousel');
|
||||
expect(getPhotoViewMode()).toBe('list');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* photoViewPrefs — localStorage-backed admin photo-grid preferences.
|
||||
*
|
||||
* Single key today: the last-used layout for the event Photos tab
|
||||
* (Grid or List). Stored per browser/admin pair via localStorage so the
|
||||
* toggle persists across page reloads. No server round-trip.
|
||||
*
|
||||
* If more admin-tunable photo preferences land, extend this module with
|
||||
* a JSON object keyed at `picpeak.adminPhotos.prefs` instead of more
|
||||
* individual keys (see utils/calendarPrefs.ts for the same convention).
|
||||
*
|
||||
* The getter swallows malformed values (e.g. someone hand-edits the
|
||||
* stored value) and falls back to the documented default — never
|
||||
* throws on read.
|
||||
*/
|
||||
|
||||
const VIEW_KEY = 'picpeak.adminPhotos.view';
|
||||
|
||||
export type PhotoViewMode = 'grid' | 'list';
|
||||
|
||||
const ALLOWED_VIEWS: ReadonlyArray<PhotoViewMode> = ['grid', 'list'];
|
||||
|
||||
/**
|
||||
* Return the persisted view or the default ('grid').
|
||||
* Safe to call before localStorage exists (SSR / test envs).
|
||||
*/
|
||||
export function getPhotoViewMode(): PhotoViewMode {
|
||||
if (typeof window === 'undefined' || !window.localStorage) return 'grid';
|
||||
try {
|
||||
const raw = window.localStorage.getItem(VIEW_KEY);
|
||||
if (raw && (ALLOWED_VIEWS as readonly string[]).includes(raw)) {
|
||||
return raw as PhotoViewMode;
|
||||
}
|
||||
} catch (_) {
|
||||
// ignore — fall through to default
|
||||
}
|
||||
return 'grid';
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the active view. Silently no-ops when localStorage is
|
||||
* unavailable.
|
||||
*/
|
||||
export function setPhotoViewMode(view: PhotoViewMode): void {
|
||||
if (typeof window === 'undefined' || !window.localStorage) return;
|
||||
if (!(ALLOWED_VIEWS as readonly string[]).includes(view)) return;
|
||||
try {
|
||||
window.localStorage.setItem(VIEW_KEY, view);
|
||||
} catch (_) {
|
||||
// ignore — quota / disabled storage
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user