Erstellen einer ersten test-Komponente die in der Mockup-HTML-Seite und in Contao eingebunden werden kann

This commit is contained in:
Robert Wartenberg committed 2026-08-20 12:28:57 +02:00
1 parent e96c73000d
commit 76e45ac9ad
12 files changed
+217 -114

No files matched your search

@@ -0,0 +1,110 @@
/* Browsernatives ES-Modul; wird ohne TypeScript-Compiler ausgeliefert. */
const demoSlots = [
{ id: 'pw-2026-08-25-1000', project: 'perspektivwerk', date: '2026-08-25', start: '10:00', title: 'Erstgespräch', status: 'available' },
{ id: 'pw-2026-08-25-1300', project: 'perspektivwerk', date: '2026-08-25', start: '13:00', title: 'Soz. Päd. Modul', status: 'booked' },
{ id: 'pw-2026-08-25-1500', project: 'perspektivwerk', date: '2026-08-25', start: '15:00', title: 'Erstgespräch', status: 'available' },
{ id: 'pw-2026-08-27-0900', project: 'perspektivwerk', date: '2026-08-27', start: '09:00', title: 'Psy Modul', status: 'available' },
{ id: 'pw-2026-08-31-0800', project: 'perspektivwerk', date: '2026-08-31', start: '08:00', title: 'Praxis Modul', status: 'available' },
{ id: 'ps-2026-08-25-1000', project: 'produktionsschule', date: '2026-08-25', start: '10:00', title: 'Kennenlerntermin', status: 'available' },
{ id: 'ps-2026-08-25-1300', project: 'produktionsschule', date: '2026-08-25', start: '13:00', title: 'Kennenlerntermin', status: 'booked' },
{ id: 'ps-2026-08-31-0800', project: 'produktionsschule', date: '2026-08-31', start: '08:00', title: 'Probearbeiten', status: 'available' }
];
export function getSlotsForDate(project, date) {
return demoSlots.filter((slot) => slot.project === project && slot.date === date);
}
const dateFormatter = new Intl.DateTimeFormat('de-DE', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
const monthFormatter = new Intl.DateTimeFormat('de-DE', { month: 'long', year: 'numeric' });
const weekdays = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
function parseDate(value) {
return new Date(`${value}T12:00:00`);
}
function formatDateKey(date) {
return date.toISOString().slice(0, 10);
}
function createMonthDates(month) {
const first = new Date(month.getFullYear(), month.getMonth(), 1);
const padding = (first.getDay() + 6) % 7;
const days = new Array(padding).fill(null);
const lastDay = new Date(month.getFullYear(), month.getMonth() + 1, 0).getDate();
for (let day = 1; day <= lastDay; day += 1) {
days.push(new Date(month.getFullYear(), month.getMonth(), day));
}
return days;
}
function monthForProject(project) {
const firstSlot = demoSlots.find((slot) => slot.project === project);
return firstSlot ? new Date(parseDate(firstSlot.date).getFullYear(), parseDate(firstSlot.date).getMonth(), 1) : new Date();
}
function renderComponent(container) {
const configuredProject = container.dataset.project;
const project = configuredProject === 'produktionsschule' ? 'produktionsschule' : 'perspektivwerk';
let currentMonth = monthForProject(project);
let selectedDate = demoSlots.find((slot) => slot.project === project)?.date ?? '';
const render = () => {
const slots = selectedDate ? getSlotsForDate(project, selectedDate) : [];
const selectedLabel = selectedDate ? dateFormatter.format(parseDate(selectedDate)) : 'einen Tag';
container.innerHTML = `
<section class="sufw-booking" aria-labelledby="sufw-booking-title">
<header class="sufw-booking__header">
<p class="sufw-booking__eyebrow">Terminbuchung – Vorschau</p>
<h2 id="sufw-booking-title">${project === 'perspektivwerk' ? 'PerspektivWerk' : 'Produktionsschule'}</h2>
<p>Wählen Sie einen Tag. Freie Termine sind mit „frei“ gekennzeichnet.</p>
</header>
<div class="sufw-booking__legend" aria-label="Legende"><span><i class="sufw-booking__marker sufw-booking__marker--available"></i> Termin verfügbar</span><span><i class="sufw-booking__marker sufw-booking__marker--booked"></i> Termin belegt</span></div>
<div class="sufw-booking__month-nav">
<button class="sufw-booking__nav" type="button" data-action="previous" aria-label="Vorheriger Monat">‹</button>
<h3 aria-live="polite">${monthFormatter.format(currentMonth)}</h3>
<button class="sufw-booking__nav" type="button" data-action="next" aria-label="Nächster Monat">›</button>
</div>
<div class="sufw-booking__weekdays" aria-hidden="true">${weekdays.map((weekday) => `<span>${weekday}</span>`).join('')}</div>
<div class="sufw-booking__calendar" role="grid" aria-label="Kalender ${monthFormatter.format(currentMonth)}">
${createMonthDates(currentMonth).map((date) => {
if (!date)
return '<span class="sufw-booking__empty" aria-hidden="true"></span>';
const key = formatDateKey(date);
const daySlots = getSlotsForDate(project, key);
const available = daySlots.some((slot) => slot.status === 'available');
const booked = daySlots.length > 0 && !available;
const selected = key === selectedDate;
const state = available ? 'available' : booked ? 'booked' : 'none';
const label = daySlots.length ? `${dateFormatter.format(date)}: ${available ? 'freie Termine verfügbar' : 'nur belegte Termine'}` : dateFormatter.format(date);
return `<button type="button" role="gridcell" class="sufw-booking__day sufw-booking__day--${state}${selected ? ' is-selected' : ''}" data-date="${key}" aria-pressed="${selected}" aria-label="${label}"><time datetime="${key}">${date.getDate()}</time>${daySlots.length ? `<small>${available ? 'frei' : 'belegt'}</small>` : ''}</button>`;
}).join('')}
</div>
<section class="sufw-booking__slots" aria-labelledby="sufw-slot-title">
<h3 id="sufw-slot-title">Termine am ${selectedLabel}</h3>
${slots.length ? `<ul>${slots.map((slot) => `<li><button type="button" class="sufw-booking__slot" data-slot-id="${slot.id}" ${slot.status === 'booked' ? 'disabled aria-disabled="true"' : ''}><strong>${slot.start} Uhr</strong><span>${slot.title}</span><em>${slot.status === 'available' ? 'Verfügbar' : 'Belegt'}</em></button></li>`).join('')}</ul>` : '<p class="sufw-booking__empty-state">Für diesen Tag liegen in der Vorschau keine Termine vor.</p>'}
<p class="sufw-booking__status" role="status" aria-live="polite"></p>
</section>
</section>`;
container.querySelector('[data-action="previous"]')?.addEventListener('click', () => {
currentMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1);
render();
});
container.querySelector('[data-action="next"]')?.addEventListener('click', () => {
currentMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1);
render();
});
container.querySelectorAll('[data-date]').forEach((button) => button.addEventListener('click', () => {
selectedDate = button.dataset.date ?? '';
render();
}));
container.querySelectorAll('[data-slot-id]').forEach((button) => button.addEventListener('click', () => {
const status = container.querySelector('.sufw-booking__status');
if (status)
status.textContent = 'Vorschau: Die eigentliche Buchungsmaske wird im nächsten Schritt ergänzt.';
}));
};
render();
}
export function initializeBookingComponents(root = document) {
root.querySelectorAll('[data-sufw-booking]').forEach(renderComponent);
}
if (typeof document !== 'undefined') {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => initializeBookingComponents());
}
else {
initializeBookingComponents();
}
}