feat(slideshow): gate behind a feature flag + move globals to a Settings tab
- New `slideshow` feature flag (backend KNOWN_FLAGS/DEFAULT_FLAGS, frontend FeatureKey + context default, a toggle card under Settings -> Features -> Core). Default off; strictly opt-in. - Move the global watermark defaults off the Event Types page into a dedicated Settings -> Slideshow tab (new SlideshowSettingsPage), shown only when the flag is on. - Gate the per-event Live Slideshow card and the per-event-type preset section behind the flag too (and stop writing a type preset when it's off). - en/de strings for the feature card + settings tab.
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.",
|
||||
)}
|
||||
/>
|
||||
|
||||
<FeatureCard
|
||||
icon={MonitorPlay}
|
||||
title={t('settings.features.slideshow.title', 'Live Slideshow')}
|
||||
description={t(
|
||||
'settings.features.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.',
|
||||
)}
|
||||
status="new"
|
||||
statusLabel={statusLabel('new')}
|
||||
sidebarHidden
|
||||
sidebarHiddenLabel={sidebarHiddenLabel}
|
||||
enabled={staged.slideshow}
|
||||
onToggle={(next) => setFlag('slideshow', next)}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
{/* Clients (#354 follow-up). Visual grouping for the CRM-area
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -2159,7 +2159,9 @@ export const EventDetailsPage: React.FC = () => {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 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 && (
|
||||
<SlideshowSettingsCard
|
||||
eventId={event.id}
|
||||
slug={event.slug}
|
||||
@@ -2179,6 +2181,7 @@ export const EventDetailsPage: React.FC = () => {
|
||||
}}
|
||||
onChanged={() => refetchEvent()}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Pre-event reminder override (migration 143). Hidden when
|
||||
the reminderEmails master flag is off — the override here
|
||||
|
||||
@@ -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 = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Global Live Slideshow watermark default (per-event/type can override) */}
|
||||
<SlideshowGlobalDefaultsCard />
|
||||
|
||||
{/* Filters */}
|
||||
<Card className="mb-6">
|
||||
<div className="p-4 flex flex-col sm:flex-row gap-4">
|
||||
@@ -339,6 +336,7 @@ const EventTypeModal: React.FC<EventTypeModalProps> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const isEditing = !!eventType;
|
||||
const slideshowEnabled = useFeatureEnabled('slideshow');
|
||||
|
||||
const [form, setForm] = useState<CreateEventTypeData>({
|
||||
name: eventType?.name || '',
|
||||
@@ -382,10 +380,10 @@ const EventTypeModal: React.FC<EventTypeModalProps> = ({
|
||||
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<EventTypeModalProps> = ({
|
||||
|
||||
{/* 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 && (
|
||||
<div className="pt-2 border-t border-neutral-200 dark:border-neutral-700">
|
||||
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
|
||||
{t('eventTypes.form.slideshowPreset', 'Slideshow preset')}
|
||||
@@ -498,6 +498,7 @@ const EventTypeModal: React.FC<EventTypeModalProps> = ({
|
||||
</p>
|
||||
<SlideshowStyleFields value={slideshowStyle} onChange={setSlideshowStyle} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Active toggle for editing */}
|
||||
{isEditing && (
|
||||
|
||||
@@ -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' && <EventTypesPage />}
|
||||
{activeTab === 'slideshow' && <SlideshowSettingsPage />}
|
||||
{activeTab === 'branding' && <BrandingPage />}
|
||||
{activeTab === 'cms' && <CMSPage />}
|
||||
{activeTab === 'email' && <EmailConfigPage />}
|
||||
|
||||
@@ -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 (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
{t('settings.slideshow.title', 'Slideshow')}
|
||||
</h2>
|
||||
<p className="text-sm text-neutral-600 dark:text-neutral-400 mt-1">
|
||||
{t('settings.slideshow.subtitle', 'Global defaults for the Live Slideshow. Events and event types can override these.')}
|
||||
</p>
|
||||
</div>
|
||||
<SlideshowGlobalDefaultsCard />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SlideshowSettingsPage;
|
||||
@@ -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<FeatureKey, boolean>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user