Files
infoscreen/dashboard/src/components/CustomEventModal.tsx
Olaf 2580aa5e0d docs: extract frontend design rules and add presentation persistence fix
Create FRONTEND_DESIGN_RULES.md as the single source of truth for all dashboard
UI conventions, including component library (Syncfusion first), component defaults
table, layout structure, buttons, dialogs, badges, toasts, form fields, tabs,
statistics cards, warnings, color palette, CSS files, loading states, locale
rules, and icon conventions (TentTree for skip-holidays events).

Move embedded design rules from ACADEMIC_PERIODS_CRUD_BUILD_PLAN.md to the new
file and replace with a reference link for maintainability.

Update copilot-instructions.md to point to FRONTEND_DESIGN_RULES.md and remove
redundant Syncfusion/Tailwind prose from the Theming section.

Add reference blockquote to README.md under Frontend Features directing readers
to FRONTEND_DESIGN_RULES.md.

Bug fix: Presentation events now reliably persist page_progress and auto_progress
flags across create, update, and detached occurrence flows so display settings
survive round-trips to the API.

Files changed:
- Created: FRONTEND_DESIGN_RULES.md (15 sections, 340+ lines)
- Modified: ACADEMIC_PERIODS_CRUD_BUILD_PLAN.md (extract rules, consolidate)
- Modified: .github/copilot-instructions.md (link to new rules file)
- Modified: README.md (reference blockquote)
2026-03-31 07:29:42 +00:00

799 lines
30 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from 'react';
import { DialogComponent } from '@syncfusion/ej2-react-popups';
import { TextBoxComponent } from '@syncfusion/ej2-react-inputs';
import { DatePickerComponent, TimePickerComponent } from '@syncfusion/ej2-react-calendars';
import { DropDownListComponent, MultiSelectComponent } from '@syncfusion/ej2-react-dropdowns';
import { CheckBoxComponent } from '@syncfusion/ej2-react-buttons';
import CustomSelectUploadEventModal from './CustomSelectUploadEventModal';
import { updateEvent, detachEventOccurrence } from '../apiEvents';
// Holiday exceptions are now created in the backend
type CustomEventData = {
title: string;
startDate: Date | null;
startTime: Date | null;
endTime: Date | null;
type: string;
description: string;
repeat: boolean;
weekdays: number[];
repeatUntil: Date | null;
skipHolidays: boolean;
media?: { id: string; path: string; name: string } | null;
slideshowInterval?: number;
pageProgress?: boolean;
autoProgress?: boolean;
websiteUrl?: string;
// Video-specific fields
autoplay?: boolean;
loop?: boolean;
volume?: number;
muted?: boolean;
};
// Typ für initialData erweitern, damit Id unterstützt wird
type CustomEventModalProps = {
open: boolean;
onClose: () => void;
onSave: (eventData: CustomEventData) => void;
initialData?: Partial<CustomEventData> & {
Id?: string;
OccurrenceOfId?: string;
isSingleOccurrence?: boolean;
occurrenceDate?: Date;
};
groupName: string | { id: string | null; name: string };
groupColor?: string;
editMode?: boolean;
// Removed unused blockHolidays and isHolidayRange
};
const weekdayOptions = [
{ value: 0, label: 'Montag' },
{ value: 1, label: 'Dienstag' },
{ value: 2, label: 'Mittwoch' },
{ value: 3, label: 'Donnerstag' },
{ value: 4, label: 'Freitag' },
{ value: 5, label: 'Samstag' },
{ value: 6, label: 'Sonntag' },
];
const typeOptions = [
{ value: 'presentation', label: 'Präsentation' },
{ value: 'website', label: 'Website' },
{ value: 'video', label: 'Video' },
{ value: 'message', label: 'Nachricht' },
{ value: 'webuntis', label: 'WebUntis' },
];
const CustomEventModal: React.FC<CustomEventModalProps> = ({
open,
onClose,
onSave,
initialData = {},
groupName,
groupColor,
editMode,
}) => {
const [title, setTitle] = React.useState(initialData.title || '');
const [startDate, setStartDate] = React.useState(initialData.startDate || null);
const [startTime, setStartTime] = React.useState(
initialData.startTime || new Date(0, 0, 0, 9, 0)
);
const [endTime, setEndTime] = React.useState(initialData.endTime || new Date(0, 0, 0, 9, 30));
const [type, setType] = React.useState(initialData.type ?? 'presentation');
const [description, setDescription] = React.useState(initialData.description || '');
// Initialize recurrence state - force to false/empty for single occurrence editing
const isSingleOccurrence = initialData.isSingleOccurrence || false;
const [repeat, setRepeat] = React.useState(isSingleOccurrence ? false : (initialData.repeat || false));
const [weekdays, setWeekdays] = React.useState<number[]>(isSingleOccurrence ? [] : (initialData.weekdays || []));
const [repeatUntil, setRepeatUntil] = React.useState(isSingleOccurrence ? null : (initialData.repeatUntil || null));
// Default to true so recurrences skip holidays by default, but false for single occurrences
const [skipHolidays, setSkipHolidays] = React.useState(
isSingleOccurrence ? false : (initialData.skipHolidays !== undefined ? initialData.skipHolidays : true)
);
const [errors, setErrors] = React.useState<{ [key: string]: string }>({});
// --- KORREKTUR: Media, SlideshowInterval, WebsiteUrl aus initialData übernehmen ---
const [media, setMedia] = React.useState<{ id: string; path: string; name: string } | null>(
initialData.media ?? null
);
// General settings state for presentation
// Removed unused generalLoaded and setGeneralLoaded
// Removed unused generalLoaded/generalSlideshowInterval/generalPageProgress/generalAutoProgress
// Per-event state
const [slideshowInterval, setSlideshowInterval] = React.useState<number>(
initialData.slideshowInterval ?? 10
);
const [pageProgress, setPageProgress] = React.useState<boolean>(
initialData.pageProgress ?? true
);
const [autoProgress, setAutoProgress] = React.useState<boolean>(
initialData.autoProgress ?? true
);
const [websiteUrl, setWebsiteUrl] = React.useState<string>(initialData.websiteUrl ?? '');
// Video-specific state with system defaults loading
const [autoplay, setAutoplay] = React.useState<boolean>(initialData.autoplay ?? true);
const [loop, setLoop] = React.useState<boolean>(initialData.loop ?? true);
const [volume, setVolume] = React.useState<number>(initialData.volume ?? 0.8);
const [muted, setMuted] = React.useState<boolean>(initialData.muted ?? false);
const [videoDefaultsLoaded, setVideoDefaultsLoaded] = React.useState<boolean>(false);
const [isSaving, setIsSaving] = React.useState(false);
const [mediaModalOpen, setMediaModalOpen] = React.useState(false);
// Load system video defaults once when opening for a new video event
React.useEffect(() => {
if (open && !editMode && !videoDefaultsLoaded) {
(async () => {
try {
const api = await import('../apiSystemSettings');
const keys = ['video_autoplay', 'video_loop', 'video_volume', 'video_muted'] as const;
const [autoplayRes, loopRes, volumeRes, mutedRes] = await Promise.all(
keys.map(k => api.getSetting(k).catch(() => ({ value: null } as { value: string | null })))
);
// Only apply defaults if not already set from initialData
if (initialData.autoplay === undefined) {
setAutoplay(autoplayRes.value == null ? true : autoplayRes.value === 'true');
}
if (initialData.loop === undefined) {
setLoop(loopRes.value == null ? true : loopRes.value === 'true');
}
if (initialData.volume === undefined) {
const volParsed = volumeRes.value == null ? 0.8 : parseFloat(String(volumeRes.value));
setVolume(Number.isFinite(volParsed) ? volParsed : 0.8);
}
if (initialData.muted === undefined) {
setMuted(mutedRes.value == null ? false : mutedRes.value === 'true');
}
setVideoDefaultsLoaded(true);
} catch {
// Silently fall back to hard-coded defaults
setVideoDefaultsLoaded(true);
}
})();
}
}, [open, editMode, videoDefaultsLoaded, initialData]);
React.useEffect(() => {
if (open) {
const isSingleOccurrence = initialData.isSingleOccurrence || false;
setTitle(initialData.title || '');
setStartDate(initialData.startDate || null);
setStartTime(initialData.startTime || new Date(0, 0, 0, 9, 0));
setEndTime(initialData.endTime || new Date(0, 0, 0, 9, 30));
setType(initialData.type ?? 'presentation');
setDescription(initialData.description || '');
// For single occurrence editing, force recurrence settings to be disabled
if (isSingleOccurrence) {
setRepeat(false);
setWeekdays([]);
setRepeatUntil(null);
setSkipHolidays(false);
} else {
setRepeat(initialData.repeat || false);
setWeekdays(initialData.weekdays || []);
setRepeatUntil(initialData.repeatUntil || null);
setSkipHolidays(initialData.skipHolidays !== undefined ? initialData.skipHolidays : true);
}
// --- KORREKTUR: Media, SlideshowInterval, WebsiteUrl aus initialData übernehmen ---
setMedia(initialData.media ?? null);
setSlideshowInterval(initialData.slideshowInterval ?? 10);
setPageProgress(initialData.pageProgress ?? true);
setAutoProgress(initialData.autoProgress ?? true);
setWebsiteUrl(initialData.websiteUrl ?? '');
// Video fields - use initialData values when editing
if (editMode) {
setAutoplay(initialData.autoplay ?? true);
setLoop(initialData.loop ?? true);
setVolume(initialData.volume ?? 0.8);
setMuted(initialData.muted ?? false);
}
}
}, [open, initialData, editMode]);
const handleSave = async () => {
if (isSaving) {
return;
}
const newErrors: { [key: string]: string } = {};
if (!title.trim()) newErrors.title = 'Titel ist erforderlich';
if (!startDate) newErrors.startDate = 'Startdatum ist erforderlich';
if (!startTime) newErrors.startTime = 'Startzeit ist erforderlich';
if (!endTime) newErrors.endTime = 'Endzeit ist erforderlich';
if (!type) newErrors.type = 'Termintyp ist erforderlich';
// Vergangenheitsprüfung - für Einzeltermine strikt, für Serien flexibler
const startDateTime =
startDate && startTime
? new Date(
startDate.getFullYear(),
startDate.getMonth(),
startDate.getDate(),
startTime.getHours(),
startTime.getMinutes()
)
: null;
const isPast = startDateTime && startDateTime < new Date();
if (isPast) {
if (isSingleOccurrence || !repeat) {
// Einzeltermine oder nicht-wiederkehrende Events dürfen nicht in der Vergangenheit liegen
newErrors.startDate = 'Termine in der Vergangenheit sind nicht erlaubt!';
} else if (repeat && repeatUntil && repeatUntil < new Date()) {
// Wiederkehrende Events sind erlaubt, wenn das End-Datum in der Zukunft liegt
newErrors.repeatUntil = 'Terminserien mit End-Datum in der Vergangenheit sind nicht erlaubt!';
}
// Andernfalls: Wiederkehrende Serie ohne End-Datum oder mit End-Datum in der Zukunft ist erlaubt
}
if (type === 'presentation') {
if (!media) newErrors.media = 'Bitte eine Präsentation auswählen';
if (!slideshowInterval || slideshowInterval < 1)
newErrors.slideshowInterval = 'Intervall angeben';
}
if (type === 'website') {
if (!websiteUrl.trim()) newErrors.websiteUrl = 'Webseiten-URL ist erforderlich';
}
if (type === 'video') {
if (!media) newErrors.media = 'Bitte ein Video auswählen';
}
const parsedMediaId = media?.id ? Number(media.id) : null;
if (
(type === 'presentation' || type === 'video') &&
(!Number.isFinite(parsedMediaId) || (parsedMediaId as number) <= 0)
) {
newErrors.media = 'Ausgewähltes Medium ist ungültig. Bitte Datei erneut auswählen.';
}
// Holiday blocking logic removed (blockHolidays, isHolidayRange no longer used)
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}
setIsSaving(true);
const group_id = typeof groupName === 'object' && groupName !== null ? groupName.id : groupName;
// Build recurrence rule if repeat is enabled
let recurrenceRule = null;
let recurrenceEnd = null;
if (repeat && weekdays.length > 0) {
// Convert weekdays to RRULE format (0=Monday -> MO)
const rruleDays = weekdays.map(day => {
const dayNames = ['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'];
return dayNames[day];
}).join(',');
recurrenceRule = `FREQ=WEEKLY;BYDAY=${rruleDays}`;
if (repeatUntil) {
const untilDate = new Date(repeatUntil);
untilDate.setHours(23, 59, 59);
recurrenceEnd = untilDate.toISOString();
// Note: RRULE UNTIL should be in UTC format for consistency
const untilUTC = untilDate.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z';
recurrenceRule += `;UNTIL=${untilUTC}`;
}
}
const payload: CustomEventData & { [key: string]: unknown } = {
group_id,
title,
description,
start:
startDate && startTime
? new Date(
startDate.getFullYear(),
startDate.getMonth(),
startDate.getDate(),
startTime.getHours(),
startTime.getMinutes()
).toISOString()
: null,
end:
startDate && endTime
? new Date(
startDate.getFullYear(),
startDate.getMonth(),
startDate.getDate(),
endTime.getHours(),
endTime.getMinutes()
).toISOString()
: null,
type,
startDate,
startTime,
endTime,
repeat: isSingleOccurrence ? false : repeat,
weekdays: isSingleOccurrence ? [] : weekdays,
repeatUntil: isSingleOccurrence ? null : repeatUntil,
skipHolidays: isSingleOccurrence ? false : skipHolidays,
event_type: type,
is_active: 1,
created_by: 1,
recurrence_rule: isSingleOccurrence ? null : recurrenceRule,
recurrence_end: isSingleOccurrence ? null : recurrenceEnd,
};
if (type === 'presentation') {
payload.event_media_id = parsedMediaId as number;
payload.slideshow_interval = slideshowInterval;
payload.page_progress = pageProgress;
payload.auto_progress = autoProgress;
}
if (type === 'website') {
payload.website_url = websiteUrl;
}
if (type === 'video') {
payload.event_media_id = parsedMediaId as number;
payload.autoplay = autoplay;
payload.loop = loop;
payload.volume = volume;
payload.muted = muted;
}
try {
let res;
if (editMode && initialData && typeof initialData.Id === 'string') {
// Check if this is a recurring event occurrence that should be detached
const shouldDetach = isSingleOccurrence &&
initialData.OccurrenceOfId &&
!isNaN(Number(initialData.OccurrenceOfId));
if (shouldDetach) {
// DETACH single occurrence from recurring series
// Use occurrenceDate from initialData, or fall back to startDate
const sourceDate = initialData.occurrenceDate || startDate;
if (!sourceDate) {
setErrors({ api: 'Fehler: Kein Datum für Einzeltermin verfügbar' });
return;
}
const occurrenceDate = sourceDate instanceof Date
? sourceDate.toISOString().split('T')[0]
: new Date(sourceDate).toISOString().split('T')[0];
try {
// Use the master event ID (OccurrenceOfId) for detaching
const masterId = Number(initialData.OccurrenceOfId);
res = await detachEventOccurrence(masterId, occurrenceDate, payload);
} catch (error) {
console.error('Detach operation failed:', error);
setErrors({ api: `API Fehler: ${error instanceof Error ? error.message : String(error)}` });
return;
}
} else {
// UPDATE entire event/series OR standalone event
res = await updateEvent(initialData.Id, payload);
}
} else {
// CREATE
const createResponse = await fetch('/api/events', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
let createData: { success?: boolean; error?: string } = {};
try {
createData = await createResponse.json();
} catch {
createData = { error: `HTTP ${createResponse.status}` };
}
if (!createResponse.ok) {
setErrors({
api:
createData.error ||
`Fehler beim Speichern (HTTP ${createResponse.status})`,
});
return;
}
res = createData;
}
if (res.success) {
onSave(payload);
onClose(); // <--- Box nach erfolgreichem Speichern schließen
} else {
setErrors({ api: res.error || 'Fehler beim Speichern' });
}
} catch {
setErrors({ api: 'Netzwerkfehler beim Speichern' });
} finally {
setIsSaving(false);
}
};
// Vergangenheitsprüfung (außerhalb von handleSave, damit im Render verfügbar)
const startDateTime =
startDate && startTime
? new Date(
startDate.getFullYear(),
startDate.getMonth(),
startDate.getDate(),
startTime.getHours(),
startTime.getMinutes()
)
: null;
const isPast = !!(startDateTime && startDateTime < new Date());
// Button sollte nur für Einzeltermine in der Vergangenheit deaktiviert werden
// Wiederkehrende Serien können bearbeitet werden, auch wenn der Starttermin vergangen ist
const shouldDisableButton = isPast && (isSingleOccurrence || !repeat);
return (
<DialogComponent
target="#root"
visible={open}
width="800px"
header={() => (
<div
style={{
background: groupColor || '#f5f5f5',
padding: '8px 16px',
borderRadius: '6px 6px 0 0',
color: '#fff',
fontWeight: 600,
fontSize: 20,
display: 'flex',
alignItems: 'center',
}}
>
{editMode
? (isSingleOccurrence ? 'Einzeltermin bearbeiten' : 'Terminserie bearbeiten')
: 'Neuen Termin anlegen'}
{groupName && (
<span style={{ fontWeight: 400, fontSize: 16, marginLeft: 16, color: '#fff' }}>
für Raumgruppe: <b>{typeof groupName === 'object' ? groupName.name : groupName}</b>
</span>
)}
</div>
)}
showCloseIcon={true}
close={onClose}
isModal={true}
footerTemplate={() => (
<div className="flex gap-2 justify-end">
<button className="e-btn e-danger" onClick={onClose}>
Schließen
</button>
<button
className="e-btn e-success"
onClick={handleSave}
disabled={shouldDisableButton || isSaving} // <--- Button nur für Einzeltermine in Vergangenheit deaktivieren
>
{isSaving ? 'Speichert...' : 'Termin(e) speichern'}
</button>
</div>
)}
>
<div style={{ padding: '24px' }}>
{errors.api && (
<div
style={{
marginBottom: 12,
color: '#721c24',
background: '#f8d7da',
border: '1px solid #f5c6cb',
borderRadius: 4,
padding: '8px 12px',
fontSize: 13,
}}
>
{errors.api}
</div>
)}
<div style={{ display: 'flex', gap: 24, flexWrap: 'wrap' }}>
<div style={{ flex: 1, minWidth: 260 }}>
{/* ...Titel, Beschreibung, Datum, Zeit... */}
<div style={{ marginBottom: 12 }}>
<TextBoxComponent
placeholder="Titel"
floatLabelType="Auto"
value={title}
change={e => setTitle(e.value)}
/>
{errors.title && <div style={{ color: 'red', fontSize: 12 }}>{errors.title}</div>}
</div>
<div style={{ marginBottom: 12 }}>
<TextBoxComponent
placeholder="Beschreibung"
floatLabelType="Auto"
multiline={true}
value={description}
change={e => setDescription(e.value)}
/>
</div>
<div style={{ marginBottom: 12 }}>
<DatePickerComponent
placeholder="Startdatum"
floatLabelType="Auto"
value={startDate ?? undefined}
change={e => setStartDate(e.value)}
/>
{errors.startDate && (
<div style={{ color: 'red', fontSize: 12 }}>{errors.startDate}</div>
)}
{isPast && (isSingleOccurrence || !repeat) && (
<span
style={{
color: 'orange',
fontWeight: 600,
marginLeft: 8,
display: 'inline-block',
background: '#fff3cd',
borderRadius: 4,
padding: '2px 8px',
border: '1px solid #ffeeba',
}}
>
Termin liegt in der Vergangenheit!
</span>
)}
{isPast && repeat && !isSingleOccurrence && (
<span
style={{
color: 'blue',
fontWeight: 600,
marginLeft: 8,
display: 'inline-block',
background: '#e3f2fd',
borderRadius: 4,
padding: '2px 8px',
border: '1px solid #90caf9',
}}
>
Bearbeitung einer Terminserie (Startdatum kann in Vergangenheit liegen)
</span>
)}
</div>
<div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
<div style={{ flex: 1 }}>
<TimePickerComponent
placeholder="Startzeit"
floatLabelType="Auto"
value={startTime}
step={30}
change={e => setStartTime(e.value)}
/>
{errors.startTime && (
<div style={{ color: 'red', fontSize: 12 }}>{errors.startTime}</div>
)}
</div>
<div style={{ flex: 1 }}>
<TimePickerComponent
placeholder="Endzeit"
floatLabelType="Auto"
value={endTime}
step={30}
change={e => setEndTime(e.value)}
/>
{errors.endTime && (
<div style={{ color: 'red', fontSize: 12 }}>{errors.endTime}</div>
)}
</div>
</div>
</div>
<div style={{ flex: 1, minWidth: 260 }}>
{/* ...Wiederholung, MultiSelect, Wiederholung bis, Ferien... */}
{isSingleOccurrence && (
<div style={{ marginBottom: 12, padding: '8px 12px', backgroundColor: '#e8f4fd', borderRadius: 4, border: '1px solid #bee5eb' }}>
<span style={{ fontSize: '14px', color: '#0c5460', fontWeight: 500 }}>
Bearbeitung eines Einzeltermins - Wiederholungsoptionen nicht verfügbar
</span>
</div>
)}
<div style={{ marginBottom: 12 }}>
<CheckBoxComponent
label="Wiederholender Termin"
checked={repeat}
change={e => setRepeat(e.checked)}
disabled={isSingleOccurrence}
/>
</div>
<div style={{ marginBottom: 12 }}>
<MultiSelectComponent
key={repeat ? 'enabled' : 'disabled'}
dataSource={weekdayOptions}
fields={{ text: 'label', value: 'value' }}
placeholder="Wochentage"
value={weekdays}
change={e => setWeekdays(e.value as number[])}
disabled={!repeat || isSingleOccurrence}
showDropDownIcon={true}
closePopupOnSelect={false}
/>
</div>
<div style={{ marginBottom: 12 }}>
<DatePickerComponent
key={repeat ? 'enabled' : 'disabled'}
placeholder="Wiederholung bis"
floatLabelType="Auto"
value={repeatUntil ?? undefined}
change={e => setRepeatUntil(e.value)}
disabled={!repeat || isSingleOccurrence}
/>
</div>
<div style={{ marginBottom: 12 }}>
<CheckBoxComponent
label="Ferientage berücksichtigen"
checked={skipHolidays}
change={e => setSkipHolidays(e.checked)}
disabled={!repeat || isSingleOccurrence}
/>
</div>
</div>
</div>
{/* NEUER ZWEISPALTIGER BEREICH für Termintyp und Zusatzfelder */}
<div style={{ display: 'flex', gap: 24, flexWrap: 'wrap', marginTop: 8 }}>
<div style={{ flex: 1, minWidth: 260 }}>
<div style={{ marginBottom: 12, marginTop: 16 }}>
<DropDownListComponent
dataSource={typeOptions}
fields={{ text: 'label', value: 'value' }}
placeholder="Termintyp"
value={type}
change={e => setType(e.value as string)}
style={{ width: '100%' }}
/>
{errors.type && <div style={{ color: 'red', fontSize: 12 }}>{errors.type}</div>}
</div>
</div>
<div style={{ flex: 1, minWidth: 260 }}>
<div style={{ marginBottom: 12, minHeight: 60 }}>
{type === 'presentation' && (
<div>
<div style={{ marginBottom: 8, marginTop: 16 }}>
<button
className="e-btn"
onClick={() => setMediaModalOpen(true)}
style={{ width: '100%' }}
>
Medium auswählen/hochladen
</button>
</div>
<div style={{ marginBottom: 8 }}>
<b>Ausgewähltes Medium:</b>{' '}
{media ? (
media.path
) : (
<span style={{ color: '#888' }}>Kein Medium ausgewählt</span>
)}
</div>
{errors.media && <div style={{ color: 'red', fontSize: 12 }}>{errors.media}</div>}
{errors.slideshowInterval && (
<div style={{ color: 'red', fontSize: 12 }}>{errors.slideshowInterval}</div>
)}
<TextBoxComponent
placeholder="Slideshow-Intervall (Sekunden)"
floatLabelType="Auto"
type="number"
value={String(slideshowInterval)}
change={e => setSlideshowInterval(Number(e.value))}
/>
<div style={{ marginTop: 8 }}>
<CheckBoxComponent
label="Seitenfortschritt anzeigen"
checked={pageProgress}
change={e => setPageProgress(e.checked || false)}
/>
</div>
<div style={{ marginTop: 8 }}>
<CheckBoxComponent
label="Automatischer Fortschritt"
checked={autoProgress}
change={e => setAutoProgress(e.checked || false)}
/>
</div>
</div>
)}
{type === 'website' && (
<div>
<TextBoxComponent
placeholder="Webseiten-URL"
floatLabelType="Always"
value={websiteUrl}
change={e => setWebsiteUrl(e.value)}
/>
</div>
)}
{type === 'video' && (
<div>
<div style={{ marginBottom: 8, marginTop: 16 }}>
<button
className="e-btn"
onClick={() => setMediaModalOpen(true)}
style={{ width: '100%' }}
>
Video auswählen/hochladen
</button>
</div>
<div style={{ marginBottom: 8 }}>
<b>Ausgewähltes Video:</b>{' '}
{media ? (
media.path
) : (
<span style={{ color: '#888' }}>Kein Video ausgewählt</span>
)}
</div>
{errors.media && <div style={{ color: 'red', fontSize: 12 }}>{errors.media}</div>}
<div style={{ marginTop: 8 }}>
<CheckBoxComponent
label="Automatisch abspielen"
checked={autoplay}
change={e => setAutoplay(e.checked || false)}
/>
</div>
<div style={{ marginTop: 8 }}>
<CheckBoxComponent
label="In Schleife abspielen"
checked={loop}
change={e => setLoop(e.checked || false)}
/>
</div>
<div style={{ marginTop: 8 }}>
<label style={{ display: 'block', marginBottom: 4, fontWeight: 500, fontSize: '14px' }}>
Lautstärke
</label>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<TextBoxComponent
placeholder="0.0 - 1.0"
floatLabelType="Never"
type="number"
value={String(volume)}
change={e => setVolume(Math.max(0, Math.min(1, Number(e.value))))}
style={{ flex: 1 }}
/>
<CheckBoxComponent
label="Ton aus"
checked={muted}
change={e => setMuted(e.checked || false)}
/>
</div>
</div>
</div>
)}
</div>
</div>
</div>
</div>
{mediaModalOpen && (
<CustomSelectUploadEventModal
open={mediaModalOpen}
onClose={() => setMediaModalOpen(false)}
onSelect={({ id, path, name }) => {
setMedia({ id, path, name });
setErrors(prev => {
if (!prev.media) return prev;
const next = { ...prev };
delete next.media;
return next;
});
setMediaModalOpen(false);
}}
selectedFileId={null}
/>
)}
</DialogComponent>
);
};
export default CustomEventModal;