/** * * * Per-event "Live Slideshow" ("Diashow") admin surface (migrations 138/139). * A token-only fullscreen kiosk link for live events that auto-picks-up new * uploads while it runs. Mounted once on the EventDetailsPage; admin can: * - Generate the slideshow link on demand (mints show_share_token) * - Copy / Regenerate (rotate, kills the old link) / Disable it * - Tune the LIVE style (transition, timing, color filter, logo watermark) * via the shared ; a running projector picks the * changes up within a few seconds via the show page's settings poll. * * New events inherit their initial style from the event TYPE preset; this * card edits the per-event override. Settings save through * PATCH /api/admin/events/:id/slideshow; link actions through * POST .../slideshow/{generate,disable}. */ import React, { useState } from 'react'; 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 { DEFAULT_SLIDESHOW_STYLE, type SlideshowStyle } from '../../services/slideshow.service'; import { SlideshowStyleFields } from './SlideshowStyleFields'; export interface SlideshowSettingsCardProps { eventId: number; slug: string; isArchived?: boolean; initial: { show_share_token?: string | null; show_interval_ms?: number; show_transition?: string; show_transition_ms?: number; show_watermark?: boolean | null; show_colorfilter?: string; }; onChanged?: () => void; } // Tri-state: null/undefined → inherit the global default; true → on; false → off. function watermarkMode(v: boolean | null | undefined): SlideshowStyle['watermark'] { if (v === null || v === undefined) return 'inherit'; return v ? 'on' : 'off'; } function styleFromInitial(initial: SlideshowSettingsCardProps['initial']): SlideshowStyle { return { interval_ms: initial.show_interval_ms ?? DEFAULT_SLIDESHOW_STYLE.interval_ms, transition: (initial.show_transition as SlideshowStyle['transition']) ?? DEFAULT_SLIDESHOW_STYLE.transition, 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, }; } export const SlideshowSettingsCard: React.FC = ({ eventId, slug, isArchived, initial, onChanged, }) => { const { t } = useTranslation(); const [token, setToken] = useState(initial.show_share_token ?? null); const [style, setStyle] = useState(() => styleFromInitial(initial)); const [copied, setCopied] = useState(false); const [busy, setBusy] = useState(false); const [saving, setSaving] = useState(false); const link = token ? `${window.location.origin}/gallery/${slug}/show/${token}` : ''; const generate = async () => { setBusy(true); try { const res = await eventsService.generateSlideshowLink(eventId); setToken(res.show_share_token); toast.success(t('slideshow.linkGenerated', 'Slideshow link generated')); onChanged?.(); } catch (e: any) { // Surface the real backend reason (the toast otherwise just says "Error"). console.error('[slideshow]', e?.response?.status, e?.response?.data || e?.message || e); toast.error(e?.response?.data?.error || t('common.error', 'Something went wrong')); } finally { setBusy(false); } }; const disable = async () => { if (!confirm(t('slideshow.disableConfirm', 'Disable this slideshow link? The current link will stop working.'))) { return; } setBusy(true); try { await eventsService.disableSlideshowLink(eventId); setToken(null); toast.success(t('slideshow.linkDisabled', 'Slideshow link disabled')); onChanged?.(); } catch (e: any) { // Surface the real backend reason (the toast otherwise just says "Error"). console.error('[slideshow]', e?.response?.status, e?.response?.data || e?.message || e); toast.error(e?.response?.data?.error || t('common.error', 'Something went wrong')); } finally { setBusy(false); } }; const copy = async () => { try { await navigator.clipboard.writeText(link); } catch { const ta = document.createElement('textarea'); ta.value = link; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); } setCopied(true); setTimeout(() => setCopied(false), 2000); }; const saveSettings = async () => { setSaving(true); try { await eventsService.updateSlideshowSettings(eventId, { show_interval_ms: style.interval_ms, show_transition: style.transition, show_transition_ms: style.transition_ms, // Tri-state → null (inherit global) / true / false. The watermark LOOK // is global-only (Settings → Slideshow); we only send the mode here. show_watermark: style.watermark === 'inherit' ? null : style.watermark === 'on', show_colorfilter: style.colorfilter, }); toast.success(t('slideshow.settingsSaved', 'Slideshow settings saved')); onChanged?.(); } catch (e: any) { // Surface the real backend reason (the toast otherwise just says "Error"). console.error('[slideshow]', e?.response?.status, e?.response?.data || e?.message || e); toast.error(e?.response?.data?.error || t('common.error', 'Something went wrong')); } finally { setSaving(false); } }; const inputClass = 'w-full px-3 py-2 bg-neutral-50 dark:bg-neutral-700 border border-neutral-300 dark:border-neutral-600 text-neutral-900 dark:text-neutral-100 rounded-lg text-sm'; const labelClass = 'block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1'; return (

{t('slideshow.adminTitle', 'Live Slideshow')}

{t('slideshow.adminDescription', 'A separate fullscreen link for projectors at live events. It shows all published photos and automatically picks up new uploads while running.')}

{!token ? ( ) : ( <>
{/* Live style settings */}

{t('slideshow.liveHint', 'Changes apply to a running slideshow within a few seconds — no need to regenerate the link.')}

)}
); }; export default SlideshowSettingsCard;