Ersters Mockup das in der statischen Seite und in Contao angeschaut werden kann
This commit is contained in:
1 parent
6dcc315d25
commit
6a9d31ca5e
7 files changed
+165
-8
No files matched your search
@@ -58,13 +58,13 @@ http://localhost:8080/perspektivwerk/
|
||||
|
||||
Die genaue Contao-, PHP- und Template-Version der öffentlichen Seite ist nicht verlässlich aus dem ausgelieferten HTML ableitbar. Das Setup bildet daher die wahrscheinlichste Komponentenfamilie nach; es ist kein Dump der Produktivinstallation. IONOS/Hosting, Matomo-Konfiguration, Inhalte, Templates und Erweiterungen müssen bei Bedarf separat ergänzt werden.
|
||||
|
||||
Die Terminlogik liegt zunächst in `public/booking/booking.js` als browsernatives ES-Modul.
|
||||
Die Terminlogik liegt in `public/booking/booking.js` als browsernatives ES-Modul. Für die statische Perspektivwerk-Vorschau liegen identische Kopien unter `public/perspektivwerk/js/`.
|
||||
|
||||
## Zwei Einbindungswege
|
||||
|
||||
Die Terminbuchungskomponente hat eine Vorschau auf einem Webseiten-Nachbau der Orginalseite:
|
||||
|
||||
**Apache-Vorschau:** auf der statischen Perspektivwerk-Seite unter <http://localhost:8080/perspektivwerk/>. Die Seite liegt in `public/perspektivwerk/`; ihre Referenzen auf die Komponente verwenden die öffentlichen Pfade `/booking/booking.css` und `/booking/booking.js`.
|
||||
**Apache-Vorschau:** auf der statischen Perspektivwerk-Seite unter <http://localhost:8080/perspektivwerk/>. Die Seite liegt in `public/perspektivwerk/`; ihre Referenzen auf die Komponente verwenden die relativen Pfade `js/booking.css` und `js/booking.js`.
|
||||
2. **Contao:** als eigenständiges Inhaltselement vom Typ **HTML** auf einer Contao-Seite. Es enthält das kombinierte Snippet mit CSS, Komponentenplatzhalter und JavaScript.
|
||||
|
||||
`public/` ist der Apache-Webroot der lokalen Entwicklungsumgebung. Daher ist kein Kopieren der Komponentenassets nach `files/terminbuchung/` erforderlich.
|
||||
@@ -93,7 +93,8 @@ Contao maskiert nicht freigegebene HTML-Tags auch im Inhaltselement **HTML**. Vo
|
||||
|
||||
Danach den Contao-Cache über **Systemwartung** leeren und das Inhaltselement erneut speichern. Keine globale Freigabe mit `*` verwenden. Wer ein HTML-Inhaltselement bearbeiten darf, kann mit dem freigegebenen `script`-Tag JavaScript auf der Website ausführen; diese Bearbeitungsrechte müssen daher auf vertrauenswürdige Backend-Nutzer beschränkt bleiben.
|
||||
|
||||
Die Beispieltermine stehen ausschließlich in `public/booking/booking.js`. Der spätere serverseitige Endpunkt muss sie ersetzen; Datenbankzugänge oder SQL gehören niemals in das HTML oder das Browser-JavaScript.
|
||||
Die Beispieltermine stehen in `public/booking/booking.js`; `public/perspektivwerk/js/booking.js` ist die für den statischen Upload bestimmte Kopie. Ein Test stellt sicher, dass beide Dateien gleich bleiben. Der spätere serverseitige Endpunkt muss sie ersetzen; Datenbankzugänge oder SQL gehören niemals in das HTML oder das Browser-JavaScript.
|
||||
|
||||
## Statische Perspektivwerk-Seite übertragen
|
||||
|
||||
Das Skript [scripts/Deploy-Perspektivwerk.ps1](scripts/Deploy-Perspektivwerk.ps1) überträgt ausschließlich den Inhalt von `public/perspektivwerk/` per SCP und korrigiert anschließend die Lesbarkeit auf dem Server per SSH. Dieser Ordner enthält auch `js/booking.js` und `js/booking.css`. Es lädt weder Contao noch die Datenbank oder andere Projektdateien hoch.
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<meta name="description" content="PerspektivWerk – Brücke zwischen Diagnostik, Begleitung und betrieblicher Praxis.">
|
||||
<title>PerspektivWerk | SUFW Dresden</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="stylesheet" href="/booking/booking.css">
|
||||
<link rel="stylesheet" href="js/booking.css">
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip-link" href="#main">Navigation überspringen</a>
|
||||
@@ -26,6 +26,6 @@
|
||||
<section class="contact page-width" id="kontakt"><h2>Sie haben Fragen zu dieser Maßnahme?</h2><p>Haben Sie Interesse an unserem Angebot, melden Sie sich bei uns. Wir beraten Sie gern vorab zu den weiteren Schritten.</p><p><strong>Kontakt:</strong> <a href="tel:+4917660328929">(0176) 60328929</a> · <a href="mailto:perspektivwerk@sufw.de">perspektivwerk@sufw.de</a> · Dresdner Straße 16, 01326 Dresden</p></section>
|
||||
</main>
|
||||
<footer><div class="page-width">Sächsisches Umschulungs- und Fortbildungswerk Dresden e. V.</div></footer>
|
||||
<script type="module" src="/booking/booking.js"></script>
|
||||
<script type="module" src="js/booking.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
$projectRoot = Split-Path -Parent $PSScriptRoot
|
||||
$deployScriptPath = Join-Path $projectRoot 'scripts\Deploy-Perspektivwerk.ps1'
|
||||
|
||||
Describe 'Perspektivwerk deployment' {
|
||||
It 'provides a deployment script for the static page' {
|
||||
(Test-Path -LiteralPath $deployScriptPath) | Should Be $true
|
||||
}
|
||||
|
||||
It 'uploads the static page and its nested booking assets with one scp invocation' {
|
||||
$script = Get-Content -LiteralPath $deployScriptPath -Raw
|
||||
|
||||
$script | Should Match 'public[\\/]perspektivwerk'
|
||||
$script | Should Not Match 'public[\\/]booking'
|
||||
$script | Should Match 'boogie:/home/boogie/html/perspektivwerk/'
|
||||
([regex]::Matches($script, '& scp')).Count | Should Be 1
|
||||
$script | Should Not Match '& rsync'
|
||||
}
|
||||
|
||||
It 'repairs read and traversal permissions with a separate SSH invocation' {
|
||||
$script = Get-Content -LiteralPath $deployScriptPath -Raw
|
||||
|
||||
([regex]::Matches($script, '& ssh')).Count | Should Be 1
|
||||
$script | Should Match 'chmod -R a\+rX /home/boogie/html/perspektivwerk'
|
||||
}
|
||||
|
||||
It 'prints the complete static-page target directory' {
|
||||
$script = Get-Content -LiteralPath $deployScriptPath -Raw
|
||||
|
||||
$script | Should Match 'Zielverzeichnisse auf dem Server'
|
||||
$script | Should Match 'boogie:/home/boogie/html/perspektivwerk/'
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
$projectRoot = Split-Path -Parent $PSScriptRoot
|
||||
$mockupPath = Join-Path $projectRoot 'public\perspektivwerk\index.html'
|
||||
$bookingScriptPath = Join-Path $projectRoot 'public\perspektivwerk\js\booking.js'
|
||||
$bookingStylesPath = Join-Path $projectRoot 'public\perspektivwerk\js\booking.css'
|
||||
$contaoBookingScriptPath = Join-Path $projectRoot 'public\booking\booking.js'
|
||||
$contaoBookingStylesPath = Join-Path $projectRoot 'public\booking\booking.css'
|
||||
$htaccessPath = Join-Path $projectRoot 'public\.htaccess'
|
||||
|
||||
Describe 'Perspektivwerk static route' {
|
||||
@@ -10,9 +14,18 @@ Describe 'Perspektivwerk static route' {
|
||||
It 'embeds the appointment booking component with its public assets' {
|
||||
$mockup = Get-Content -LiteralPath $mockupPath -Raw
|
||||
|
||||
$mockup | Should Match 'href="/booking/booking\.css"'
|
||||
(Test-Path -LiteralPath $bookingScriptPath) | Should Be $true
|
||||
(Test-Path -LiteralPath $bookingStylesPath) | Should Be $true
|
||||
$mockup | Should Match 'href="js/booking\.css"'
|
||||
$mockup | Should Match 'data-sufw-booking'
|
||||
$mockup | Should Match 'src="/booking/booking\.js"'
|
||||
$mockup | Should Match 'src="js/booking\.js"'
|
||||
}
|
||||
|
||||
It 'keeps the Contao and static-preview asset copies identical' {
|
||||
(Test-Path -LiteralPath $contaoBookingScriptPath) | Should Be $true
|
||||
(Test-Path -LiteralPath $contaoBookingStylesPath) | Should Be $true
|
||||
(Get-FileHash -LiteralPath $bookingScriptPath).Hash | Should Be (Get-FileHash -LiteralPath $contaoBookingScriptPath).Hash
|
||||
(Get-FileHash -LiteralPath $bookingStylesPath).Hash | Should Be (Get-FileHash -LiteralPath $contaoBookingStylesPath).Hash
|
||||
}
|
||||
|
||||
It 'routes /perspektivwerk to the static entry point before the Contao fallback' {
|
||||
|
||||
Reference in new issue
Block a user