Erstellen einer ersten test-Komponente die in der Mockup-HTML-Seite und in Contao eingebunden werden kann
This commit is contained in:
1 parent
e96c73000d
commit
76e45ac9ad
12 files changed
+217
-114
No files matched your search
@@ -0,0 +1 @@
|
||||
.sufw-booking{--sufw-booking-green:#08752a;--sufw-booking-text:#202020;--sufw-booking-muted:#5a5a5a;--sufw-booking-line:#c7c7c7;--sufw-booking-pale:#f3f6f3;box-sizing:border-box;max-width:860px;margin:2rem auto;padding:1.25rem;color:var(--sufw-booking-text);background:#fff;border:1px solid var(--sufw-booking-line);font:1rem/1.5 Arial,"Helvetica Neue",sans-serif}.sufw-booking *{box-sizing:border-box}.sufw-booking__header h2,.sufw-booking__slots h3{margin:.15rem 0 .5rem;color:var(--sufw-booking-green);font-size:1.4rem}.sufw-booking__header p{margin:.3rem 0}.sufw-booking__eyebrow{color:var(--sufw-booking-green);font-size:.875rem;font-weight:700;text-transform:uppercase;letter-spacing:.04em}.sufw-booking__legend{display:flex;gap:1rem;flex-wrap:wrap;margin:1.25rem 0;font-size:.9rem}.sufw-booking__marker{display:inline-block;width:.8rem;height:.8rem;margin-right:.35rem;border-radius:50%}.sufw-booking__marker--available{background:var(--sufw-booking-green)}.sufw-booking__marker--booked{background:#535353}.sufw-booking__month-nav{display:grid;grid-template-columns:3rem 1fr 3rem;align-items:center;margin-top:1rem;text-align:center}.sufw-booking__month-nav h3{margin:0;font-size:1.1rem;text-transform:capitalize}.sufw-booking__nav,.sufw-booking__day,.sufw-booking__slot{font:inherit}.sufw-booking__nav{min-width:2.75rem;min-height:2.75rem;border:1px solid var(--sufw-booking-green);color:var(--sufw-booking-green);background:#fff;font-size:1.7rem;cursor:pointer}.sufw-booking__nav:hover,.sufw-booking__nav:focus-visible{background:var(--sufw-booking-pale)}.sufw-booking__weekdays,.sufw-booking__calendar{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:.3rem}.sufw-booking__weekdays{margin-top:1rem;text-align:center;font-weight:700;font-size:.8rem}.sufw-booking__day{display:grid;place-items:center;gap:.1rem;min-height:3.8rem;padding:.25rem;border:1px solid transparent;background:#fff;color:var(--sufw-booking-text);cursor:pointer}.sufw-booking__day:hover{border-color:var(--sufw-booking-green);background:var(--sufw-booking-pale)}.sufw-booking__day--available{background:#dff3e4;border-color:var(--sufw-booking-green)}.sufw-booking__day--booked{background:#ececec;border-color:#777}.sufw-booking__day.is-selected{outline:3px solid #1f4fbc;outline-offset:1px}.sufw-booking__day small{font-size:.7rem}.sufw-booking__empty{min-height:3.8rem}.sufw-booking__slots{margin-top:1.5rem;padding-top:1.25rem;border-top:1px solid var(--sufw-booking-line)}.sufw-booking__slots ul{padding:0;margin:0;list-style:none}.sufw-booking__slot{display:grid;grid-template-columns:5rem 1fr auto;gap:.7rem;width:100%;margin:.45rem 0;padding:.8rem;text-align:left;color:var(--sufw-booking-text);background:#fff;border:1px solid var(--sufw-booking-green);cursor:pointer}.sufw-booking__slot:hover,.sufw-booking__slot:focus-visible{background:var(--sufw-booking-pale)}.sufw-booking__slot em{color:var(--sufw-booking-green);font-style:normal;font-weight:700}.sufw-booking__slot:disabled{border-color:#777;background:#eee;color:#555;cursor:not-allowed}.sufw-booking__slot:disabled em{color:#555}.sufw-booking__empty-state,.sufw-booking__status{margin:.7rem 0}.sufw-booking__status{font-weight:700}@media (max-width:520px){.sufw-booking{margin:1rem 0;padding:1rem}.sufw-booking__weekdays{font-size:.7rem}.sufw-booking__day{min-height:3.4rem;padding:.15rem}.sufw-booking__slot{grid-template-columns:1fr;gap:.15rem}.sufw-booking__slot strong{font-size:1.1rem}}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user