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:
@@ -16,11 +16,13 @@
|
||||
* POST .../slideshow/{generate,disable}.
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'react-toastify';
|
||||
import { MonitorPlay, Copy, CheckCircle, RotateCw, Trash2, Save } from 'lucide-react';
|
||||
import { Button, Card } from '../common';
|
||||
import { eventsService } from '../../services/events.service';
|
||||
import { categoriesService } from '../../services/categories.service';
|
||||
import { DEFAULT_SLIDESHOW_STYLE, type SlideshowStyle } from '../../services/slideshow.service';
|
||||
import { SlideshowStyleFields } from './SlideshowStyleFields';
|
||||
|
||||
@@ -35,6 +37,8 @@ export interface SlideshowSettingsCardProps {
|
||||
show_transition_ms?: number;
|
||||
show_watermark?: boolean | null;
|
||||
show_colorfilter?: string;
|
||||
show_order?: string;
|
||||
show_category_id?: number | null;
|
||||
};
|
||||
onChanged?: () => void;
|
||||
}
|
||||
@@ -52,6 +56,8 @@ function styleFromInitial(initial: SlideshowSettingsCardProps['initial']): Slide
|
||||
transition_ms: initial.show_transition_ms ?? DEFAULT_SLIDESHOW_STYLE.transition_ms,
|
||||
watermark: watermarkMode(initial.show_watermark),
|
||||
colorfilter: (initial.show_colorfilter as SlideshowStyle['colorfilter']) ?? DEFAULT_SLIDESHOW_STYLE.colorfilter,
|
||||
order: (initial.show_order as SlideshowStyle['order']) ?? DEFAULT_SLIDESHOW_STYLE.order,
|
||||
category_id: initial.show_category_id ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -68,6 +74,14 @@ export const SlideshowSettingsCard: React.FC<SlideshowSettingsCardProps> = ({
|
||||
|
||||
const link = token ? `${window.location.origin}/gallery/${slug}/show/${token}` : '';
|
||||
|
||||
// Event categories for the slideshow content filter (#202). Global + this
|
||||
// event's own categories; empty for events without any → picker hides.
|
||||
const { data: categories = [] } = useQuery({
|
||||
queryKey: ['event-categories', eventId],
|
||||
queryFn: () => categoriesService.getEventCategories(eventId),
|
||||
staleTime: 60_000,
|
||||
});
|
||||
|
||||
const generate = async () => {
|
||||
setBusy(true);
|
||||
try {
|
||||
@@ -129,6 +143,8 @@ export const SlideshowSettingsCard: React.FC<SlideshowSettingsCardProps> = ({
|
||||
// is global-only (Settings → Slideshow); we only send the mode here.
|
||||
show_watermark: style.watermark === 'inherit' ? null : style.watermark === 'on',
|
||||
show_colorfilter: style.colorfilter,
|
||||
show_order: style.order,
|
||||
show_category_id: style.category_id,
|
||||
});
|
||||
toast.success(t('slideshow.settingsSaved', 'Slideshow settings saved'));
|
||||
onChanged?.();
|
||||
@@ -208,7 +224,7 @@ export const SlideshowSettingsCard: React.FC<SlideshowSettingsCardProps> = ({
|
||||
|
||||
{/* Live style settings */}
|
||||
<div className="pt-2 border-t border-neutral-200 dark:border-neutral-700">
|
||||
<SlideshowStyleFields value={style} onChange={setStyle} />
|
||||
<SlideshowStyleFields value={style} onChange={setStyle} categories={categories} />
|
||||
</div>
|
||||
<p className="text-xs text-neutral-500 dark:text-neutral-400">
|
||||
{t('slideshow.liveHint', 'Changes apply to a running slideshow within a few seconds — no need to regenerate the link.')}
|
||||
|
||||
Reference in New Issue
Block a user