feat(slideshow): per-event play order + category filter (#202)
The Live Slideshow already covers the core of #202 (fullscreen kiosk, live-appending new uploads, timing/transitions/watermark, per-event opt-in via the share link). This adds the two customization dimensions the reporter also asked for: - **Play order** (show_order): 'chronological' (upload order, default) or 'random' — the client shuffles the initial set (Fisher-Yates) so live-appended uploads keep working. - **Category filter** (show_category_id): restrict the slideshow to a single photo category (NULL = all photos, default). Enforced server-side on the slideshow /photos access and mirrored in the /session + /state photo_count, so the kiosk viewer can't widen the set. Per-event enable/disable (default off) is unchanged — it's the existing 'Generate/Disable slideshow link' flow (no token = no slideshow). - Migration 158: show_order (default 'chronological') + show_category_id. - Admin: Play-order dropdown + category picker in the Live Slideshow card (picker hidden for events without categories); EN + DE i18n. - Verified: migration (SQLite + PG); live API (category filter → 3/2/5 photos + matching count; order propagates) and the running kiosk requests exactly the filtered set; tsc clean, 136 backend tests pass.
This commit is contained in:
@@ -15,12 +15,17 @@ import {
|
||||
SLIDESHOW_TRANSITIONS,
|
||||
SLIDESHOW_COLORFILTERS,
|
||||
SLIDESHOW_WATERMARK_MODES,
|
||||
SLIDESHOW_ORDERS,
|
||||
type SlideshowStyle,
|
||||
} from '../../services/slideshow.service';
|
||||
import type { PhotoCategory } from '../../services/categories.service';
|
||||
|
||||
export interface SlideshowStyleFieldsProps {
|
||||
value: SlideshowStyle;
|
||||
onChange: (next: SlideshowStyle) => void;
|
||||
/** Event categories for the content filter (#202). Omitted/empty → the
|
||||
* category picker is hidden (e.g. events without any categories). */
|
||||
categories?: PhotoCategory[];
|
||||
}
|
||||
|
||||
const inputClass =
|
||||
@@ -29,7 +34,7 @@ const labelClass = 'block text-sm font-medium text-neutral-700 dark:text-neutral
|
||||
|
||||
const titleCase = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
|
||||
|
||||
export const SlideshowStyleFields: React.FC<SlideshowStyleFieldsProps> = ({ value, onChange }) => {
|
||||
export const SlideshowStyleFields: React.FC<SlideshowStyleFieldsProps> = ({ value, onChange, categories = [] }) => {
|
||||
const { t } = useTranslation();
|
||||
const set = (patch: Partial<SlideshowStyle>) => onChange({ ...value, ...patch });
|
||||
|
||||
@@ -92,6 +97,39 @@ export const SlideshowStyleFields: React.FC<SlideshowStyleFieldsProps> = ({ valu
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Play order + content filter (#202) */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className={labelClass}>{t('slideshow.orderLabel', 'Play order')}</label>
|
||||
<select
|
||||
value={value.order}
|
||||
onChange={(e) => set({ order: e.target.value as SlideshowStyle['order'] })}
|
||||
className={inputClass}
|
||||
>
|
||||
{SLIDESHOW_ORDERS.map((o) => (
|
||||
<option key={o} value={o}>
|
||||
{t(`slideshow.order.${o}`, o === 'random' ? 'Random (shuffle)' : 'Chronological')}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{categories.length > 0 && (
|
||||
<div>
|
||||
<label className={labelClass}>{t('slideshow.categoryLabel', 'Show only category')}</label>
|
||||
<select
|
||||
value={value.category_id ?? ''}
|
||||
onChange={(e) => set({ category_id: e.target.value === '' ? null : parseInt(e.target.value, 10) })}
|
||||
className={inputClass}
|
||||
>
|
||||
<option value="">{t('slideshow.categoryAll', 'All photos')}</option>
|
||||
{categories.map((c) => (
|
||||
<option key={c.id} value={c.id}>{c.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Watermark — MODE only. The look (logo/position/opacity/style/size)
|
||||
lives in Settings → Slideshow, so it isn't duplicated here. */}
|
||||
<div className="pt-2 border-t border-neutral-200 dark:border-neutral-700">
|
||||
|
||||
Reference in New Issue
Block a user