diff --git a/backend/src/routes/adminFeatureFlags.js b/backend/src/routes/adminFeatureFlags.js index 6464aa24..1b3badd0 100644 --- a/backend/src/routes/adminFeatureFlags.js +++ b/backend/src/routes/adminFeatureFlags.js @@ -84,6 +84,10 @@ const KNOWN_FLAGS = [ // opt-in — operators must register a Meta-approved template before turning // it on. Independent of email; both can fire on the same event. 'whatsapp', + // Live Slideshow ("Diashow") — the per-event fullscreen kiosk link + its + // per-event-type presets and global watermark defaults tab. Strictly opt-in; + // gates all slideshow admin UI (per-event card, type preset, settings tab). + 'slideshow', ]; // Spec defaults for any flag missing from the DB (e.g. a row added by a @@ -112,6 +116,7 @@ const DEFAULT_FLAGS = { expenses: false, projects: false, whatsapp: false, + slideshow: false, }; async function readAllFlags() { diff --git a/frontend/src/contexts/FeatureFlagsContext.tsx b/frontend/src/contexts/FeatureFlagsContext.tsx index b5549205..c97d4c1b 100644 --- a/frontend/src/contexts/FeatureFlagsContext.tsx +++ b/frontend/src/contexts/FeatureFlagsContext.tsx @@ -62,6 +62,8 @@ export const DEFAULT_FLAGS: FeatureFlags = { projects: false, // WhatsApp Business API delivery channel (migration 136, #640D). whatsapp: false, + // Live Slideshow ("Diashow") — opt-in; gates all slideshow admin UI. + slideshow: false, }; export const FEATURE_FLAGS_QUERY_KEY = ['feature-flags'] as const; diff --git a/frontend/src/features/settings/tabs/FeaturesTab.tsx b/frontend/src/features/settings/tabs/FeaturesTab.tsx index 0f392494..5816bb3f 100644 --- a/frontend/src/features/settings/tabs/FeaturesTab.tsx +++ b/frontend/src/features/settings/tabs/FeaturesTab.tsx @@ -22,6 +22,7 @@ import { ScanLine, Wallet, FolderKanban, + MonitorPlay, } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { Button, Card } from '../../../components/common'; @@ -116,6 +117,21 @@ export const FeaturesTab: React.FC = () => { "Galleries are the foundation of PicPeak and can't be turned off.", )} /> + + setFlag('slideshow', next)} + /> {/* Clients (#354 follow-up). Visual grouping for the CRM-area diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index 27aa9379..48d49bfd 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -1768,6 +1768,10 @@ "whatsapp": { "title": "WhatsApp", "description": "Liefert die Gallerie-Bereit-Benachrichtigung zusätzlich zur E-Mail über die WhatsApp Business API. Voraussetzung: Meta-Business-Konto, genehmigte Nachrichtenvorlage und eine Kunden-Telefonnummer am Event. Zugangsdaten unter Einstellungen → WhatsApp konfigurieren." + }, + "slideshow": { + "title": "Live-Diashow", + "description": "Ein separater Vollbild-„Diashow“-Link pro Event für Beamer bei Live-Events – übernimmt neue Uploads automatisch, mit Voreinstellungen je Event-Typ und globalen Wasserzeichen-Vorgaben unter Einstellungen → Diashow." } }, "customerSurface": { @@ -1815,6 +1819,10 @@ "hourlyRateHint": "Verrechnungs-Fallback, wenn ein Kunde keinen eigenen Satz hat (Stundenerfassung). In {{currency}}, in Hauptwährungseinheiten. Leer lassen, um einen Satz pro Kunde oder pro Eintrag zu verlangen." } }, + "slideshow": { + "title": "Diashow", + "subtitle": "Globale Vorgaben für die Live-Diashow. Events und Event-Typen können diese überschreiben." + }, "whatsapp": { "title": "WhatsApp", "subtitle": "Meta-Business-Zugangsdaten konfigurieren, um die Gallerie-Bereit-Benachrichtigung zusätzlich zur E-Mail per WhatsApp zu senden.", diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 6b753c97..74f84f66 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -1324,6 +1324,10 @@ "whatsapp": { "title": "WhatsApp", "description": "Deliver the gallery-ready notification via WhatsApp Business API in addition to email. Requires a Meta Business Account, an approved message template, and a customer phone number on the event. Configure credentials under Settings → WhatsApp." + }, + "slideshow": { + "title": "Live Slideshow", + "description": "A separate fullscreen \"Diashow\" link per event for projectors at live events — auto-picks-up new uploads, with per-event-type presets and global watermark defaults under Settings → Slideshow." } }, "customerSurface": { @@ -1371,6 +1375,10 @@ "hourlyRateHint": "Billing fallback used when a customer has no own rate (hours logging). In {{currency}}, major units. Leave blank to require a per-customer or per-entry rate." } }, + "slideshow": { + "title": "Slideshow", + "subtitle": "Global defaults for the Live Slideshow. Events and event types can override these." + }, "whatsapp": { "title": "WhatsApp", "subtitle": "Configure Meta Business credentials to deliver the gallery-ready notification via WhatsApp alongside email.", diff --git a/frontend/src/pages/admin/EventDetailsPage.tsx b/frontend/src/pages/admin/EventDetailsPage.tsx index e5492baf..49cc26bf 100644 --- a/frontend/src/pages/admin/EventDetailsPage.tsx +++ b/frontend/src/pages/admin/EventDetailsPage.tsx @@ -2159,7 +2159,9 @@ export const EventDetailsPage: React.FC = () => { - {/* Live Slideshow ("Diashow") link + live display settings (migration 137) */} + {/* Live Slideshow ("Diashow") link + live display settings (migrations 137/138). + Gated behind the `slideshow` feature flag. */} + {flags.slideshow && ( { }} onChanged={() => refetchEvent()} /> + )} {/* Pre-event reminder override (migration 143). Hidden when the reminderEmails master flag is off — the override here diff --git a/frontend/src/pages/admin/EventTypesPage.tsx b/frontend/src/pages/admin/EventTypesPage.tsx index 8ee65b5d..0667516c 100644 --- a/frontend/src/pages/admin/EventTypesPage.tsx +++ b/frontend/src/pages/admin/EventTypesPage.tsx @@ -19,8 +19,8 @@ import { Button, Input, Card, Loading } from '../../components/common'; import { eventTypesService, EventType, CreateEventTypeData, UpdateEventTypeData } from '../../services/eventTypes.service'; import { GALLERY_THEME_PRESETS } from '../../types/theme.types'; import { SlideshowStyleFields } from '../../components/admin/SlideshowStyleFields'; -import { SlideshowGlobalDefaultsCard } from '../../components/admin/SlideshowGlobalDefaultsCard'; import { DEFAULT_SLIDESHOW_STYLE, type SlideshowStyle } from '../../services/slideshow.service'; +import { useFeatureEnabled } from '../../contexts/FeatureFlagsContext'; // Parse a type's stored slideshow_preset (JSON string | object | null) into a // full SlideshowStyle, falling back to defaults for any missing keys. @@ -152,9 +152,6 @@ export const EventTypesPage: React.FC = () => { - {/* Global Live Slideshow watermark default (per-event/type can override) */} - - {/* Filters */}
@@ -339,6 +336,7 @@ const EventTypeModal: React.FC = ({ }) => { const { t } = useTranslation(); const isEditing = !!eventType; + const slideshowEnabled = useFeatureEnabled('slideshow'); const [form, setForm] = useState({ name: eventType?.name || '', @@ -382,10 +380,10 @@ const EventTypeModal: React.FC = ({ if (form.emoji !== eventType?.emoji) updates.emoji = form.emoji; if (form.theme_preset !== eventType?.theme_preset) updates.theme_preset = form.theme_preset; const originalStyle = JSON.stringify(parseSlideshowPreset(eventType?.slideshow_preset)); - if (JSON.stringify(slideshowStyle) !== originalStyle) updates.slideshow_preset = slideshowStyle; + if (slideshowEnabled && JSON.stringify(slideshowStyle) !== originalStyle) updates.slideshow_preset = slideshowStyle; onSubmit(updates); } else { - onSubmit({ ...form, slideshow_preset: slideshowStyle }); + onSubmit({ ...form, ...(slideshowEnabled ? { slideshow_preset: slideshowStyle } : {}) }); } }; @@ -488,7 +486,9 @@ const EventTypeModal: React.FC = ({ {/* Live Slideshow preset (migration 138). New events of this type inherit these slideshow defaults; admins can still override - per event on the event detail page. */} + per event on the event detail page. Gated behind the + `slideshow` feature flag. */} + {slideshowEnabled && (
+ )} {/* Active toggle for editing */} {isEditing && ( diff --git a/frontend/src/pages/admin/SettingsPage.tsx b/frontend/src/pages/admin/SettingsPage.tsx index 66bf688a..ce8786a0 100644 --- a/frontend/src/pages/admin/SettingsPage.tsx +++ b/frontend/src/pages/admin/SettingsPage.tsx @@ -46,6 +46,7 @@ import { import { EmailConfigPage } from './EmailConfigPage'; import { BrandingPage } from './BrandingPage'; import { EventTypesPage } from './EventTypesPage'; +import { SlideshowSettingsPage } from './SlideshowSettingsPage'; import { BackupManagement } from './BackupManagement'; import { CMSPage } from './CMSPage'; // CRM (#TBD) @@ -54,7 +55,7 @@ import { CrmSettingsPage } from './settings/CrmSettingsPage'; import { ReminderTemplatesPage } from './settings/ReminderTemplatesPage'; import { BlockLibraryPage } from './contracts/BlockLibraryPage'; import { useFeatureFlags } from '../../contexts/FeatureFlagsContext'; -import { Briefcase, Receipt, ScrollText, Mail, Landmark, Smartphone } from 'lucide-react'; +import { Briefcase, Receipt, ScrollText, Mail, Landmark, Smartphone, MonitorPlay } from 'lucide-react'; // Tab keys driving the inner-nav. Must include every key used in // `navGroups` below and in the switch at the bottom of the component. @@ -85,7 +86,8 @@ type TabType = | 'contracts' | 'reminderTemplates' | 'accounting' - | 'whatsapp'; + | 'whatsapp' + | 'slideshow'; interface NavItem { key: TabType; @@ -106,6 +108,7 @@ const ALL_TAB_KEYS: TabType[] = [ 'apiTokens', 'webhooks', 'status', 'analytics', 'backup', 'businessProfile', 'crm', 'contracts', 'reminderTemplates', 'accounting', 'whatsapp', + 'slideshow', ]; function isValidTab(value: string | null): value is TabType { @@ -203,12 +206,13 @@ export const SettingsPage: React.FC = () => { reminderTemplates: !flags.reminderEmails, accounting: !flags.accounting, whatsapp: !flags.whatsapp, + slideshow: !flags.slideshow, }; if (gatedOff[activeTab]) { setActiveTab('features'); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [flagsLoading, flags.quotes, flags.bills, flags.contracts, flags.reminderEmails, flags.accounting, flags.whatsapp, activeTab]); + }, [flagsLoading, flags.quotes, flags.bills, flags.contracts, flags.reminderEmails, flags.accounting, flags.whatsapp, flags.slideshow, activeTab]); if (isLoading) { return ( @@ -240,6 +244,9 @@ export const SettingsPage: React.FC = () => { { key: 'thumbnails', label: t('settings.thumbnails.title', 'Thumbnails'), icon: ImageIcon }, { key: 'styling', label: t('settings.styling.title', 'Custom CSS'), icon: Code }, { key: 'cms', label: t('settings.cms.title', 'CMS Pages'), icon: FileText }, + ...(flags.slideshow + ? [{ key: 'slideshow' as const, label: t('settings.slideshow.title', 'Slideshow'), icon: MonitorPlay }] + : []), ], }, { @@ -433,6 +440,7 @@ export const SettingsPage: React.FC = () => { )} {activeTab === 'eventTypes' && } + {activeTab === 'slideshow' && } {activeTab === 'branding' && } {activeTab === 'cms' && } {activeTab === 'email' && } diff --git a/frontend/src/pages/admin/SlideshowSettingsPage.tsx b/frontend/src/pages/admin/SlideshowSettingsPage.tsx new file mode 100644 index 00000000..50862a40 --- /dev/null +++ b/frontend/src/pages/admin/SlideshowSettingsPage.tsx @@ -0,0 +1,28 @@ +/** + * Settings → Slideshow tab. Top-level (global) Live Slideshow settings. + * Currently the global watermark default (source/position/opacity/style) that + * every event inherits unless it overrides. Gated behind the `slideshow` + * feature flag (see SettingsPage nav). + */ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { SlideshowGlobalDefaultsCard } from '../../components/admin/SlideshowGlobalDefaultsCard'; + +export const SlideshowSettingsPage: React.FC = () => { + const { t } = useTranslation(); + return ( +
+
+

+ {t('settings.slideshow.title', 'Slideshow')} +

+

+ {t('settings.slideshow.subtitle', 'Global defaults for the Live Slideshow. Events and event types can override these.')} +

+
+ +
+ ); +}; + +export default SlideshowSettingsPage; diff --git a/frontend/src/services/featureFlags.service.ts b/frontend/src/services/featureFlags.service.ts index 18113843..176c83d8 100644 --- a/frontend/src/services/featureFlags.service.ts +++ b/frontend/src/services/featureFlags.service.ts @@ -63,7 +63,10 @@ export type FeatureKey = // Strictly opt-in — requires a Meta Business Account, an approved // message template, and a Meta access token. Independent of email; both // can fire on the same event. - | 'whatsapp'; + | 'whatsapp' + // Live Slideshow ("Diashow") — per-event fullscreen kiosk link + presets + + // global watermark settings tab. Strictly opt-in; gates all slideshow UI. + | 'slideshow'; export type FeatureFlags = Record;