Merge pull request 'Buchungsbereich als Komponente umgebaut und Bestätigungsseite ergänzt' (#3) from feature/booking-component into feature/angebote-booking-integration
Reviewed-on: #3feature/angebote-booking-integration
commit
6437077342
|
|
@ -0,0 +1,169 @@
|
|||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="robots" content="noindex" />
|
||||
<title>Buchungsbestätigung – Tri-Hub</title>
|
||||
</head>
|
||||
<body class="confirmation">
|
||||
<header class="confirmation__header">
|
||||
<a
|
||||
class="confirmation__brand"
|
||||
href="/index.html"
|
||||
aria-label="Tri-Hub Startseite"
|
||||
><span class="confirmation__mark" aria-hidden="true">TH</span
|
||||
><span
|
||||
>TRI-HUB<small>Ernährung. Ausdauer. Dein Weg.</small></span
|
||||
></a
|
||||
>
|
||||
<a class="confirmation__link" href="/index.html"
|
||||
>Startseite <span aria-hidden="true">↗</span></a
|
||||
>
|
||||
</header>
|
||||
<main class="confirmation__main">
|
||||
<section
|
||||
id="confirmation-empty"
|
||||
class="confirmation__empty"
|
||||
aria-labelledby="empty-title"
|
||||
>
|
||||
<p class="confirmation__eyebrow">Deine Buchungsübersicht</p>
|
||||
<h1 id="empty-title">Keine Bestätigung verfügbar.</h1>
|
||||
<p>
|
||||
In diesem Tab liegt keine gespeicherte Buchungsbestätigung
|
||||
vor. Öffne die Bestätigung im Tab deiner Buchung. Falls du
|
||||
bereits gebucht hast, prüfe deine E-Mails, bevor du erneut
|
||||
buchst.
|
||||
</p>
|
||||
<a class="confirmation__button" href="/angebotsuebersicht.html"
|
||||
>Zur Paketübersicht →</a
|
||||
>
|
||||
</section>
|
||||
<div id="confirmation-content" hidden>
|
||||
<section
|
||||
class="confirmation__hero"
|
||||
aria-labelledby="confirmation-title"
|
||||
>
|
||||
<p class="confirmation__eyebrow">
|
||||
<span aria-hidden="true">✓</span> Deine
|
||||
Buchungsbestätigung
|
||||
</p>
|
||||
<h1 id="confirmation-title">
|
||||
Dein nächster Schritt.<br /><em>Alles im Blick.</em>
|
||||
</h1>
|
||||
<p>
|
||||
Danke für dein Vertrauen. Deine Buchungsdaten wurden
|
||||
gespeichert. Hier findest du deine persönliche
|
||||
Übersicht.
|
||||
</p>
|
||||
<p
|
||||
class="confirmation__simulation"
|
||||
id="confirmation-payment"
|
||||
></p>
|
||||
</section>
|
||||
<div class="confirmation__grid">
|
||||
<section
|
||||
class="confirmation__card"
|
||||
aria-labelledby="summary-title"
|
||||
>
|
||||
<p class="confirmation__eyebrow">
|
||||
Dein gewähltes Paket
|
||||
</p>
|
||||
<h2 id="summary-title"></h2>
|
||||
<p
|
||||
id="confirmation-summary"
|
||||
class="confirmation__muted"
|
||||
></p>
|
||||
<dl
|
||||
id="confirmation-totals"
|
||||
class="confirmation__details"
|
||||
></dl>
|
||||
<p id="confirmation-no-snapshot" hidden>
|
||||
Für diese ältere Buchung ist keine Preis- und
|
||||
Leistungsübersicht gespeichert.
|
||||
</p>
|
||||
<div id="confirmation-features-section" hidden>
|
||||
<h3>Das ist enthalten</h3>
|
||||
<ul
|
||||
id="confirmation-features"
|
||||
class="confirmation__features"
|
||||
></ul>
|
||||
</div>
|
||||
</section>
|
||||
<aside
|
||||
class="confirmation__sidebar"
|
||||
aria-label="Buchungsdaten und Bestätigungsversand"
|
||||
>
|
||||
<section
|
||||
class="confirmation__card"
|
||||
aria-labelledby="receipt-title"
|
||||
>
|
||||
<p class="confirmation__eyebrow">
|
||||
Für deine Unterlagen
|
||||
</p>
|
||||
<h2 id="receipt-title">Deine Buchung</h2>
|
||||
<dl
|
||||
id="confirmation-receipt"
|
||||
class="confirmation__details confirmation__details--stacked"
|
||||
></dl>
|
||||
</section>
|
||||
<section
|
||||
class="confirmation__card confirmation__mail"
|
||||
aria-labelledby="mail-title"
|
||||
>
|
||||
<h2 id="mail-title">Deine E-Mail-Bestätigung</h2>
|
||||
<p id="confirmation-mail-status"></p>
|
||||
<a
|
||||
id="confirmation-retry"
|
||||
class="confirmation__link"
|
||||
hidden
|
||||
>Versandstatus prüfen →</a
|
||||
>
|
||||
</section>
|
||||
</aside>
|
||||
</div>
|
||||
<section
|
||||
id="confirmation-steps-section"
|
||||
class="confirmation__card confirmation__next"
|
||||
aria-labelledby="steps-title"
|
||||
hidden
|
||||
>
|
||||
<p class="confirmation__eyebrow">Dein weiterer Weg</p>
|
||||
<h2 id="steps-title">So geht es weiter</h2>
|
||||
<p class="confirmation__muted">
|
||||
So ist der Ablauf deines gewählten Pakets vorgesehen.
|
||||
Die aktuelle Buchung ist eine Simulation.
|
||||
</p>
|
||||
<ol
|
||||
id="confirmation-steps"
|
||||
class="confirmation__steps"
|
||||
></ol>
|
||||
</section>
|
||||
<nav
|
||||
class="confirmation__actions"
|
||||
aria-label="Weiterführende Links"
|
||||
>
|
||||
<a
|
||||
class="confirmation__button"
|
||||
href="/angebotsuebersicht.html"
|
||||
>Zur Paketübersicht <span aria-hidden="true">→</span></a
|
||||
>
|
||||
<a class="confirmation__link" href="/index.html"
|
||||
>Zur Startseite</a
|
||||
>
|
||||
</nav>
|
||||
</div>
|
||||
<noscript
|
||||
>Bitte aktiviere JavaScript, um die gespeicherte
|
||||
Buchungsbestätigung anzuzeigen.</noscript
|
||||
>
|
||||
</main>
|
||||
<footer class="confirmation__footer">
|
||||
TRI-HUB <span>Dein Weg. Deine Ausdauer. Deine Ernährung.</span>
|
||||
</footer>
|
||||
<script
|
||||
type="module"
|
||||
src="/src/features/booking/booking-confirmation.js"
|
||||
></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -7,7 +7,12 @@
|
|||
<title>Paket buchen – Tri-Hub</title>
|
||||
</head>
|
||||
<body>
|
||||
<main id="booking-root" class="container"></main>
|
||||
<main class="container">
|
||||
<a href="/angebotsuebersicht.html">Zur Angebotsseite</a>
|
||||
<h1>Deine Buchung</h1>
|
||||
<!-- Nur diesen Mount-Punkt später in die Paketdetailseite übernehmen. -->
|
||||
<div id="booking-root"></div>
|
||||
</main>
|
||||
<script
|
||||
type="module"
|
||||
src="/src/features/booking/booking-entry.js"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,288 @@
|
|||
.confirmation {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
color: #f8fafc;
|
||||
background:
|
||||
radial-gradient(ellipse at 5% 20%, #10b98114, transparent 45%),
|
||||
radial-gradient(ellipse at 95% 45%, #1a1a2e, transparent 55%), #0d0d1a;
|
||||
font-family: Inter, system-ui, sans-serif;
|
||||
line-height: 1.65;
|
||||
color-scheme: dark;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.confirmation * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.confirmation [hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
.confirmation a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
.confirmation a:focus-visible {
|
||||
outline: 3px solid #10b981;
|
||||
outline-offset: 6px;
|
||||
}
|
||||
.confirmation__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1.2rem clamp(1rem, 5vw, 5rem);
|
||||
border-bottom: 1px solid #ffffff12;
|
||||
background: #09091299;
|
||||
}
|
||||
.confirmation__brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
letter-spacing: 0.22em;
|
||||
font-weight: 600;
|
||||
}
|
||||
.confirmation__brand small {
|
||||
display: block;
|
||||
color: #94a3b8;
|
||||
letter-spacing: normal;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
.confirmation__mark {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
background: #f8fafc;
|
||||
color: #087452;
|
||||
border-radius: 0.9rem;
|
||||
letter-spacing: -0.08em;
|
||||
font-family: Georgia, serif;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
.confirmation__main {
|
||||
max-width: 74rem;
|
||||
padding: 4rem 1.5rem;
|
||||
margin: auto;
|
||||
}
|
||||
.confirmation__hero {
|
||||
max-width: 49rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
.confirmation__eyebrow {
|
||||
color: #94a3b8;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.17em;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.confirmation__eyebrow span {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 1.6rem;
|
||||
height: 1.6rem;
|
||||
margin-right: 0.5rem;
|
||||
border: 1px solid #10b98170;
|
||||
border-radius: 50%;
|
||||
color: #10b981;
|
||||
}
|
||||
.confirmation h1 {
|
||||
margin: 1rem 0 1.5rem;
|
||||
font-family: "Playfair Display", Georgia, serif;
|
||||
font-weight: 500;
|
||||
font-size: clamp(2.5rem, 5vw, 4.5rem);
|
||||
line-height: 1.12;
|
||||
letter-spacing: -0.035em;
|
||||
}
|
||||
.confirmation h1 em {
|
||||
color: #10b981;
|
||||
font-weight: inherit;
|
||||
}
|
||||
.confirmation h2 {
|
||||
margin: 0.5rem 0 1rem;
|
||||
font-family: "Playfair Display", Georgia, serif;
|
||||
font-weight: 500;
|
||||
font-size: 1.75rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.confirmation h3 {
|
||||
font-size: 1rem;
|
||||
margin: 1.5rem 0 0.65rem;
|
||||
}
|
||||
.confirmation__hero > p:not(.confirmation__eyebrow) {
|
||||
max-width: 42rem;
|
||||
}
|
||||
.confirmation__simulation {
|
||||
color: #cbd5e1;
|
||||
padding-left: 1rem;
|
||||
border-left: 2px solid #10b981;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.confirmation__grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
|
||||
gap: 1.5rem;
|
||||
align-items: start;
|
||||
}
|
||||
.confirmation__card {
|
||||
border: 1px solid #ffffff14;
|
||||
border-radius: 1.4rem;
|
||||
padding: clamp(1.25rem, 3vw, 2.25rem);
|
||||
background: linear-gradient(145deg, #ffffff06, #ffffff02);
|
||||
}
|
||||
.confirmation__sidebar {
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
.confirmation__muted,
|
||||
.confirmation__steps p {
|
||||
color: #94a3b8;
|
||||
}
|
||||
.confirmation__details {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 0.75rem 1rem;
|
||||
margin: 1.75rem 0 0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.confirmation__details dt {
|
||||
color: #94a3b8;
|
||||
}
|
||||
.confirmation__details dd {
|
||||
margin: 0;
|
||||
text-align: right;
|
||||
}
|
||||
.confirmation__details:not(.confirmation__details--stacked) dt:last-of-type,
|
||||
.confirmation__details:not(.confirmation__details--stacked) dd:last-of-type {
|
||||
border-top: 1px solid #ffffff20;
|
||||
padding-top: 1rem;
|
||||
font-size: 1.1rem;
|
||||
color: #f8fafc;
|
||||
font-weight: 600;
|
||||
}
|
||||
.confirmation__details--stacked {
|
||||
display: block;
|
||||
}
|
||||
.confirmation__details--stacked dd {
|
||||
text-align: left;
|
||||
margin: 0.2rem 0 1rem;
|
||||
}
|
||||
.confirmation__mail {
|
||||
border-color: #10b98140;
|
||||
background: #10b98108;
|
||||
}
|
||||
.confirmation__mail h2 {
|
||||
font-family: inherit;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.confirmation__mail p {
|
||||
color: #cbd5e1;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.confirmation__features {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
color: #cbd5e1;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.confirmation__features li {
|
||||
position: relative;
|
||||
padding-left: 1.5rem;
|
||||
margin: 0.8rem 0;
|
||||
}
|
||||
.confirmation__features li::before {
|
||||
content: "✓";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: #10b981;
|
||||
}
|
||||
.confirmation__next {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.confirmation__steps {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
|
||||
gap: 1.5rem;
|
||||
list-style: none;
|
||||
counter-reset: steps;
|
||||
padding: 0;
|
||||
}
|
||||
.confirmation__steps li {
|
||||
counter-increment: steps;
|
||||
border-top: 1px solid #ffffff20;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
.confirmation__steps li::before {
|
||||
content: "0" counter(steps);
|
||||
color: #10b981;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.confirmation__steps h3 {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.confirmation__steps p {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.confirmation__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
.confirmation__button,
|
||||
.confirmation__link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
min-height: 2.75rem;
|
||||
padding: 0.65rem 1.3rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.confirmation__button {
|
||||
background: #10b981;
|
||||
color: #071a14 !important;
|
||||
}
|
||||
.confirmation__link {
|
||||
border: 1px solid #ffffff20;
|
||||
}
|
||||
.confirmation__button:hover {
|
||||
background: #34d399;
|
||||
}
|
||||
.confirmation__link:hover {
|
||||
background: #ffffff08;
|
||||
}
|
||||
.confirmation__empty {
|
||||
max-width: 42rem;
|
||||
min-height: 60vh;
|
||||
}
|
||||
.confirmation__footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
border-top: 1px solid #ffffff12;
|
||||
padding: 1.5rem clamp(1rem, 5vw, 5rem);
|
||||
color: #94a3b8;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.12em;
|
||||
}
|
||||
.confirmation__footer span {
|
||||
letter-spacing: normal;
|
||||
}
|
||||
@media (max-width: 45rem) {
|
||||
.confirmation__grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
.confirmation__main {
|
||||
padding: 2.5rem 1rem;
|
||||
}
|
||||
.confirmation__header {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.confirmation__brand small {
|
||||
max-width: 12rem;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
import "./booking-confirmation.css";
|
||||
import { orderSummaryRows } from "../../shared/order-summary.js";
|
||||
import { paymentNotice } from "../../shared/booking-copy.js";
|
||||
|
||||
const mailMessages = {
|
||||
accepted:
|
||||
"Die Bestätigung wurde vom Mailserver zum Versand angenommen. Die Zustellung ist noch nicht bestätigt. Bitte prüfe auch deinen Spam-Ordner.",
|
||||
pending:
|
||||
"Deine Buchung ist gespeichert. Der Bestätigungsversand steht noch aus. Du kannst den Versand mit derselben Buchung erneut versuchen.",
|
||||
failed: "Deine Buchung ist gespeichert, aber der Bestätigungsversand ist fehlgeschlagen. Bitte buche nicht erneut. Wende dich mit deiner Buchungsnummer an Tri-Hub.",
|
||||
unknown:
|
||||
"Deine Buchung ist gespeichert. Der Versandstatus ist unklar. Bitte buche nicht erneut. Wende dich mit deiner Buchungsnummer an Tri-Hub.",
|
||||
};
|
||||
const select = (id) => document.getElementById(id);
|
||||
function rows(id, values) {
|
||||
const list = select(id);
|
||||
for (const [label, value] of values) {
|
||||
const term = document.createElement("dt");
|
||||
const description = document.createElement("dd");
|
||||
term.textContent = label;
|
||||
description.textContent = value || "Nicht verfügbar";
|
||||
list.append(term, description);
|
||||
}
|
||||
}
|
||||
|
||||
// Nur die Antwort der gespeicherten Buchung anzeigen, keine aktuellen
|
||||
// Katalogpreise nachladen und keine erneute Buchungsanfrage auslösen.
|
||||
let body;
|
||||
try {
|
||||
body = JSON.parse(sessionStorage.getItem("trihub.booking.confirmation"));
|
||||
} catch {
|
||||
// Ohne lesbaren Sitzungsspeicher bleibt der Hinweis sichtbar.
|
||||
}
|
||||
if (
|
||||
body?.saved === true &&
|
||||
typeof body.bookingId === "string" &&
|
||||
body.bookingId
|
||||
) {
|
||||
const order = body.bookedPackage;
|
||||
select("confirmation-payment").textContent = paymentNotice;
|
||||
select("summary-title").textContent =
|
||||
body.packageName || order?.title || "Dein Paket";
|
||||
select("confirmation-summary").textContent = order?.summaryForBooking || "";
|
||||
const date = new Date(body.createdAt);
|
||||
rows("confirmation-receipt", [
|
||||
["Buchungsnummer", body.bookingId],
|
||||
[
|
||||
"Buchungsdatum",
|
||||
Number.isNaN(date.getTime())
|
||||
? "Nicht verfügbar"
|
||||
: new Intl.DateTimeFormat("de-DE", {
|
||||
dateStyle: "long",
|
||||
timeStyle: "short",
|
||||
}).format(date),
|
||||
],
|
||||
["E-Mail-Adresse", body.customerEmail],
|
||||
[
|
||||
"Buchungsstatus",
|
||||
body.status === "CONFIRMED"
|
||||
? "Bestätigt (Simulation)"
|
||||
: "Gespeichert (Simulation)",
|
||||
],
|
||||
]);
|
||||
select("confirmation-mail-status").textContent =
|
||||
mailMessages[body.emailStatus] || mailMessages.unknown;
|
||||
if (order) {
|
||||
rows("confirmation-totals", orderSummaryRows(order));
|
||||
for (const feature of order.includedFeatures || []) {
|
||||
const item = document.createElement("li");
|
||||
item.textContent = feature;
|
||||
select("confirmation-features").append(item);
|
||||
}
|
||||
select("confirmation-features-section").hidden =
|
||||
!order.includedFeatures?.length;
|
||||
for (const step of order.processSteps || []) {
|
||||
const item = document.createElement("li");
|
||||
const title = document.createElement("h3");
|
||||
const text = document.createElement("p");
|
||||
title.textContent = step.step;
|
||||
text.textContent = step.text;
|
||||
item.append(title, text);
|
||||
select("confirmation-steps").append(item);
|
||||
}
|
||||
select("confirmation-steps-section").hidden =
|
||||
!order.processSteps?.length;
|
||||
} else {
|
||||
select("confirmation-no-snapshot").hidden = false;
|
||||
rows("confirmation-totals", [["Paket-ID", body.packageId]]);
|
||||
}
|
||||
if (body.emailStatus === "pending" && body.packageId) {
|
||||
const params = new URLSearchParams({ id: body.packageId });
|
||||
if (order?.variantId) params.set("variant", order.variantId);
|
||||
select("confirmation-retry").href = `/booking.html?${params}`;
|
||||
select("confirmation-retry").hidden = false;
|
||||
}
|
||||
select("confirmation-empty").hidden = true;
|
||||
select("confirmation-content").hidden = false;
|
||||
}
|
||||
|
|
@ -3,16 +3,26 @@ import "./booking.css";
|
|||
import { orderSummaryRows } from "../../shared/order-summary.js";
|
||||
import { paymentNotice } from "../../shared/booking-copy.js";
|
||||
|
||||
export function mountBookingPage(root, { packageId, variantId } = {}) {
|
||||
// Die Detailseite kann dieselbe Section direkt mounten und später aufräumen.
|
||||
// Die Zieladresse kann bei der Integration bei Bedarf überschrieben werden.
|
||||
export function mountBookingPage(
|
||||
root,
|
||||
{
|
||||
packageId,
|
||||
variantId,
|
||||
confirmationUrl = root.dataset.confirmationUrl ||
|
||||
"/booking-confirmation.html",
|
||||
} = {},
|
||||
) {
|
||||
const controller = new AbortController();
|
||||
const { signal } = controller;
|
||||
const instanceId = `booking-${crypto.randomUUID()}`;
|
||||
root.innerHTML = `
|
||||
<div class="booking">
|
||||
<a href="/angebotsuebersicht.html">Zur Angebotsseite</a>
|
||||
<h1>Paket buchen</h1>
|
||||
<section class="booking" aria-labelledby="${instanceId}-heading">
|
||||
<h2 id="${instanceId}-heading">Paket buchen</h2>
|
||||
<p class="booking__intro" role="status">Paket wird geladen …</p>
|
||||
<div class="booking__content"></div>
|
||||
</div>`;
|
||||
</section>`;
|
||||
const intro = root.querySelector(".booking__intro");
|
||||
const content = root.querySelector(".booking__content");
|
||||
|
||||
|
|
@ -68,28 +78,69 @@ export function mountBookingPage(root, { packageId, variantId } = {}) {
|
|||
<ul class="booking__features"></ul>
|
||||
<div class="booking__steps"></div>
|
||||
</div>
|
||||
<aside class="booking__payment" aria-labelledby="booking-payment-heading">
|
||||
<h2 id="booking-payment-heading">Hinweis zum Buchungsablauf</h2>
|
||||
<aside class="booking__payment" aria-labelledby="${instanceId}-booking-payment-heading">
|
||||
<h2 id="${instanceId}-booking-payment-heading">Hinweis zum Buchungsablauf</h2>
|
||||
<p class="booking__payment-notice"></p>
|
||||
</aside>
|
||||
<button class="booking__button booking__open" type="button" aria-haspopup="dialog" aria-controls="${instanceId}-dialog" aria-expanded="false">Jetzt buchen</button>
|
||||
<dialog class="booking__dialog" id="${instanceId}-dialog" aria-labelledby="${instanceId}-dialog-heading" aria-describedby="${instanceId}-dialog-intro">
|
||||
<button class="booking__close" type="button" aria-label="Buchungsdialog schließen">×</button>
|
||||
<h2 id="${instanceId}-dialog-heading">Paket buchen</h2>
|
||||
<p id="${instanceId}-dialog-intro">Bitte gib deine Kontaktdaten ein. Name und E-Mail-Adresse sind Pflichtfelder.</p>
|
||||
<form novalidate>
|
||||
<p>Alle Felder sind Pflichtfelder.</p>
|
||||
<div class="booking__errors" role="alert" tabindex="-1" hidden></div>
|
||||
<div class="booking__field">
|
||||
<label for="booking-name">Name (Pflichtfeld)</label>
|
||||
<input id="booking-name" name="name" type="text" autocomplete="name" required maxlength="120" aria-describedby="booking-name-error" />
|
||||
<p class="booking__field-error" id="booking-name-error"></p>
|
||||
<label for="${instanceId}-booking-name">Name (Pflichtfeld)</label>
|
||||
<input id="${instanceId}-booking-name" name="name" type="text" autocomplete="name" required maxlength="120" aria-describedby="${instanceId}-booking-name-error" />
|
||||
<p class="booking__field-error" id="${instanceId}-booking-name-error"></p>
|
||||
</div>
|
||||
<div class="booking__field">
|
||||
<label for="booking-email">E-Mail-Adresse (Pflichtfeld)</label>
|
||||
<input id="booking-email" name="email" type="email" autocomplete="email" inputmode="email" required maxlength="254" aria-describedby="booking-email-hint booking-email-error" />
|
||||
<p id="booking-email-hint">An diese Adresse senden wir deine Bestätigung.</p>
|
||||
<p class="booking__field-error" id="booking-email-error"></p>
|
||||
<label for="${instanceId}-booking-email">E-Mail-Adresse (Pflichtfeld)</label>
|
||||
<input id="${instanceId}-booking-email" name="email" type="email" autocomplete="email" inputmode="email" required maxlength="254" aria-describedby="${instanceId}-booking-email-hint ${instanceId}-booking-email-error" />
|
||||
<p id="${instanceId}-booking-email-hint">An diese Adresse senden wir deine Bestätigung.</p>
|
||||
<p class="booking__field-error" id="${instanceId}-booking-email-error"></p>
|
||||
</div>
|
||||
<div class="booking__field">
|
||||
<label for="${instanceId}-booking-phone">Telefonnummer (optional)</label>
|
||||
<input id="${instanceId}-booking-phone" name="phone" type="tel" autocomplete="tel" maxlength="50" aria-describedby="${instanceId}-booking-phone-hint ${instanceId}-booking-phone-error" />
|
||||
<p id="${instanceId}-booking-phone-hint">Die Telefonnummer wird derzeit noch nicht an die Buchungsstelle übermittelt.</p>
|
||||
<p class="booking__field-error" id="${instanceId}-booking-phone-error"></p>
|
||||
</div>
|
||||
<div class="booking__actions">
|
||||
<button class="booking__button booking__cancel" type="button">Abbrechen</button>
|
||||
<button class="booking__button" type="submit">Paket buchen</button>
|
||||
</div>
|
||||
<button class="booking__button" type="submit">Paket buchen</button>
|
||||
<p class="booking__progress" role="status"></p>
|
||||
</form>
|
||||
<div class="booking__result" role="status" tabindex="-1" hidden></div>`;
|
||||
<div class="booking__result" role="status" tabindex="-1" hidden></div>
|
||||
</dialog>`;
|
||||
const dialog = content.querySelector("dialog");
|
||||
const opener = content.querySelector(".booking__open");
|
||||
opener.addEventListener(
|
||||
"click",
|
||||
() => {
|
||||
dialog.showModal();
|
||||
opener.setAttribute("aria-expanded", "true");
|
||||
const focusTarget =
|
||||
content.querySelector(".booking__result:not([hidden])") ||
|
||||
content.querySelector('[name="name"]');
|
||||
focusTarget.focus();
|
||||
},
|
||||
{ signal },
|
||||
);
|
||||
for (const close of content.querySelectorAll(
|
||||
".booking__close, .booking__cancel",
|
||||
)) {
|
||||
close.addEventListener("click", () => dialog.close(), { signal });
|
||||
}
|
||||
dialog.addEventListener(
|
||||
"close",
|
||||
() => {
|
||||
opener.setAttribute("aria-expanded", "false");
|
||||
opener.focus();
|
||||
},
|
||||
{ signal },
|
||||
);
|
||||
function renderOrder(order) {
|
||||
content.querySelector(".booking__package-name").textContent =
|
||||
order.title;
|
||||
|
|
@ -133,7 +184,8 @@ export function mountBookingPage(root, { packageId, variantId } = {}) {
|
|||
const form = content.querySelector("form");
|
||||
const name = form.elements.namedItem("name");
|
||||
const email = form.elements.namedItem("email");
|
||||
const button = form.querySelector("button");
|
||||
const phone = form.elements.namedItem("phone");
|
||||
const button = form.querySelector('button[type="submit"]');
|
||||
const errors = form.querySelector(".booking__errors");
|
||||
const progress = form.querySelector(".booking__progress");
|
||||
const result = content.querySelector(".booking__result");
|
||||
|
|
@ -145,14 +197,15 @@ export function mountBookingPage(root, { packageId, variantId } = {}) {
|
|||
function lockFields(locked) {
|
||||
name.readOnly = locked;
|
||||
email.readOnly = locked;
|
||||
phone.readOnly = locked;
|
||||
}
|
||||
function showError(message, fields = {}) {
|
||||
errors.textContent = message;
|
||||
errors.hidden = false;
|
||||
for (const field of [name, email]) {
|
||||
for (const field of [name, email, phone]) {
|
||||
const message = fields[field.name] || "";
|
||||
content.querySelector(
|
||||
`#booking-${field.name}-error`,
|
||||
`#${instanceId}-booking-${field.name}-error`,
|
||||
).textContent = message;
|
||||
field.setAttribute("aria-invalid", String(Boolean(message)));
|
||||
}
|
||||
|
|
@ -160,10 +213,10 @@ export function mountBookingPage(root, { packageId, variantId } = {}) {
|
|||
}
|
||||
function clearErrors() {
|
||||
errors.hidden = true;
|
||||
for (const field of [name, email]) {
|
||||
for (const field of [name, email, phone]) {
|
||||
field.removeAttribute("aria-invalid");
|
||||
content.querySelector(
|
||||
`#booking-${field.name}-error`,
|
||||
`#${instanceId}-booking-${field.name}-error`,
|
||||
).textContent = "";
|
||||
}
|
||||
}
|
||||
|
|
@ -199,6 +252,7 @@ export function mountBookingPage(root, { packageId, variantId } = {}) {
|
|||
if (stored.order) renderOrder(stored.order);
|
||||
name.value = stored.payload.name;
|
||||
email.value = stored.payload.email;
|
||||
phone.value = stored.phone || "";
|
||||
lockFields(true);
|
||||
button.textContent = "Status prüfen / erneut versuchen";
|
||||
if (stored.result) showResult(stored.result, false);
|
||||
|
|
@ -230,6 +284,13 @@ export function mountBookingPage(root, { packageId, variantId } = {}) {
|
|||
if (!email.validity.valid || email.value.length > 254)
|
||||
fields.email =
|
||||
"Bitte eine gültige E-Mail-Adresse eingeben.";
|
||||
phone.value = phone.value.trim();
|
||||
if (
|
||||
phone.value.length > 50 ||
|
||||
/[\x00-\x1f\x7f]/.test(phone.value)
|
||||
)
|
||||
fields.phone =
|
||||
"Bitte höchstens 50 Zeichen ohne Zeilenumbrüche eingeben.";
|
||||
if (Object.keys(fields).length) {
|
||||
showError(
|
||||
"Bitte die markierten Angaben prüfen.",
|
||||
|
|
@ -240,6 +301,8 @@ export function mountBookingPage(root, { packageId, variantId } = {}) {
|
|||
const nextAttempt = {
|
||||
key: crypto.randomUUID(),
|
||||
order: selected,
|
||||
// Bis zur Backend-Erweiterung nur für die Wiederherstellung.
|
||||
phone: phone.value,
|
||||
payload: {
|
||||
packageId: selected.packageId,
|
||||
...(selected.variantId
|
||||
|
|
@ -283,7 +346,26 @@ export function mountBookingPage(root, { packageId, variantId } = {}) {
|
|||
} catch {
|
||||
/* Der ursprüngliche Schlüssel bleibt erhalten. */
|
||||
}
|
||||
if (!signal.aborted) showResult(body);
|
||||
if (!signal.aborted) {
|
||||
showResult(body);
|
||||
if (
|
||||
body.saved &&
|
||||
body.emailStatus === "accepted" &&
|
||||
confirmationUrl
|
||||
) {
|
||||
// Nur weiterleiten, wenn die Bestätigungsseite die
|
||||
// Serverantwort in diesem Tab auch lesen kann.
|
||||
try {
|
||||
sessionStorage.setItem(
|
||||
"trihub.booking.confirmation",
|
||||
JSON.stringify(body),
|
||||
);
|
||||
} catch {
|
||||
return; // Die bestehende Bestätigung bleibt sichtbar.
|
||||
}
|
||||
window.location.assign(confirmationUrl);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Nur eindeutige Ablehnungen vor dem Speichern erlauben eine
|
||||
// Korrektur. Bei Verbindungsfehlern bleiben Daten und Key fest.
|
||||
|
|
@ -322,5 +404,9 @@ export function mountBookingPage(root, { packageId, variantId } = {}) {
|
|||
);
|
||||
}
|
||||
load();
|
||||
return () => controller.abort();
|
||||
return () => {
|
||||
controller.abort();
|
||||
root.querySelector(".booking__dialog")?.close();
|
||||
root.replaceChildren();
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,3 +117,76 @@
|
|||
.booking__features li {
|
||||
margin-block: 0.5rem;
|
||||
}
|
||||
|
||||
/* Sämtliche Dialogregeln bleiben innerhalb der Booking-Section. */
|
||||
.booking .booking__dialog {
|
||||
box-sizing: border-box;
|
||||
width: min(32rem, calc(100vw - 2rem));
|
||||
max-height: calc(100dvh - 2rem);
|
||||
margin: auto;
|
||||
padding: 1.75rem;
|
||||
overflow-y: auto;
|
||||
color: #edf5f5;
|
||||
background: #071316;
|
||||
border: 1px solid #324449;
|
||||
border-radius: 1.25rem;
|
||||
box-shadow: 0 1.5rem 4rem #0006;
|
||||
color-scheme: dark;
|
||||
}
|
||||
.booking .booking__dialog::backdrop {
|
||||
background: rgb(0 0 0 / 65%);
|
||||
}
|
||||
.booking__dialog h2 {
|
||||
margin: 0 2rem 0.75rem 0;
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
.booking .booking__close {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
font-size: 1.75rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.booking__dialog input {
|
||||
box-sizing: border-box;
|
||||
background: #101e22;
|
||||
border-color: #61777d;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
.booking__dialog .booking__button {
|
||||
background: #00b6c7;
|
||||
border-color: #00b6c7;
|
||||
color: #061619;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
.booking__dialog .booking__cancel {
|
||||
background: transparent;
|
||||
border-color: #61777d;
|
||||
color: inherit;
|
||||
}
|
||||
.booking__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.booking__actions > button {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.booking__dialog .booking__errors,
|
||||
.booking__dialog .booking__field-error {
|
||||
color: #ffb5b5;
|
||||
}
|
||||
.booking__dialog [aria-invalid="true"] {
|
||||
border-color: #ffb5b5;
|
||||
}
|
||||
.booking__dialog :focus-visible,
|
||||
.booking__dialog .booking__errors:focus,
|
||||
.booking__dialog .booking__result:focus {
|
||||
outline-color: #00d6e8;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ export default defineConfig({
|
|||
new URL("./paket-details.html", import.meta.url),
|
||||
),
|
||||
main: fileURLToPath(new URL("./index.html", import.meta.url)),
|
||||
bookingConfirmation: fileURLToPath(
|
||||
new URL("./booking-confirmation.html", import.meta.url),
|
||||
),
|
||||
booking: fileURLToPath(
|
||||
new URL("./booking.html", import.meta.url),
|
||||
),
|
||||
|
|
|
|||
Loading…
Reference in New Issue