Compare commits
No commits in common. "main" and "feature/booking-component" have entirely different histories.
main
...
feature/bo
|
|
@ -4,28 +4,13 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Ernährungs-Pakete – Tri-Hub</title>
|
<title>Ernährungs-Pakete – Tri-Hub</title>
|
||||||
<link rel="stylesheet" href="/src/styles/navigation.css" />
|
|
||||||
<link rel="stylesheet" href="/src/styles/packages.css" />
|
|
||||||
</head>
|
</head>
|
||||||
<body class="packages-page">
|
<body>
|
||||||
<div id="navbar-placeholder"></div>
|
<header class="container"><a href="/">Zur Startseite</a></header>
|
||||||
<main class="packages-section" id="main-content" tabindex="-1">
|
<main class="packages-section" id="angebotsuebersicht-section">
|
||||||
<div class="packages-header">
|
<div class="packages-header"><h1>Unsere Ernährungs-Pakete</h1></div>
|
||||||
<div>
|
|
||||||
<p class="packages-eyebrow">TRI-HUB · ERNÄHRUNG</p>
|
|
||||||
<h1>Unsere Ernährungs-Pakete</h1>
|
|
||||||
</div>
|
|
||||||
<p class="packages-intro">
|
|
||||||
Wähle das Paket, das zu deinem aktuellen Stand passt. Von
|
|
||||||
den Grundlagen bis zur Wettkampf-Vorbereitung.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="packages-container" id="packages-container"></div>
|
<div class="packages-container" id="packages-container"></div>
|
||||||
</main>
|
</main>
|
||||||
<script
|
|
||||||
type="module"
|
|
||||||
src="/src/components/navigationsbar/navigation.js"
|
|
||||||
></script>
|
|
||||||
<script
|
<script
|
||||||
type="module"
|
type="module"
|
||||||
src="/src/features/angebote/angebote-entry.js"
|
src="/src/features/angebote/angebote-entry.js"
|
||||||
|
|
|
||||||
|
|
@ -1101,7 +1101,6 @@
|
||||||
></div>
|
></div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
>>>>>>>>> Temporary merge branch 2
|
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csv-parse": "^7.0.2",
|
"csv-parse": "^7.0.2",
|
||||||
"csv-stringify": "^6.8.3",
|
"csv-stringify": "^6.8.3",
|
||||||
"libphonenumber-js": "^1.13.14",
|
|
||||||
"nodemailer": "^10.0.10"
|
"nodemailer": "^10.0.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -983,12 +982,6 @@
|
||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/libphonenumber-js": {
|
|
||||||
"version": "1.13.14",
|
|
||||||
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.13.14.tgz",
|
|
||||||
"integrity": "sha512-llihgCcx0BFLksecLP+x1J+6JDE1GsXS1RN/LoPF6qcwpeQcnjj0lcvZxY8AzbEpYwyZWPZW/nDuqkqzm3amiw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.19",
|
"version": "3.3.19",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz",
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csv-parse": "^7.0.2",
|
"csv-parse": "^7.0.2",
|
||||||
"csv-stringify": "^6.8.3",
|
"csv-stringify": "^6.8.3",
|
||||||
"libphonenumber-js": "^1.13.14",
|
|
||||||
"nodemailer": "^10.0.10"
|
"nodemailer": "^10.0.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,29 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="de">
|
<html lang="de" class="dark">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Paket-Details – Tri-Hub Ernährungsberatung</title>
|
<title>Paket-Details – Tri-Hub Ernährungsberatung</title>
|
||||||
<link rel="stylesheet" href="/src/styles/navigation.css" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Playfair+Display:wght@600;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
<link rel="stylesheet" href="/src/styles/packages.css" />
|
<link rel="stylesheet" href="/src/styles/packages.css" />
|
||||||
</head>
|
</head>
|
||||||
<body class="packages-page">
|
|
||||||
<div id="navbar-placeholder"></div>
|
<body>
|
||||||
<main class="packages-section" id="main-content" tabindex="-1">
|
<header class="container">
|
||||||
<a class="packages-back" href="/angebotsuebersicht.html"
|
<a href="angebotsuebersicht.html">← Zurück zur Angebotsübersicht</a>
|
||||||
>← Zurück zur Angebotsübersicht</a
|
</header>
|
||||||
>
|
|
||||||
<div id="package-detail-container"></div>
|
<main class="packages-section">
|
||||||
|
<div id="package-detail-container" class="detail-page-container">
|
||||||
|
<!-- Wird dynamisch durch paket-details.js gefüllt -->
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<script
|
|
||||||
type="module"
|
|
||||||
src="/src/components/navigationsbar/navigation.js"
|
|
||||||
></script>
|
|
||||||
<script
|
<script
|
||||||
type="module"
|
type="module"
|
||||||
src="/src/features/angebote/paket-details.js"
|
src="/src/features/angebote/paket-details.js"
|
||||||
|
|
|
||||||
|
|
@ -32,19 +32,31 @@
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="/index.html" class="navbar__link">Ernährung</a>
|
<a href="#ernaehrung" class="navbar__link">Ernährung</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="/index.html" class="navbar__link">Beraterprofil</a>
|
<a
|
||||||
|
href="/index.html"
|
||||||
|
class="navbar__link"
|
||||||
|
>Beraterprofil</a
|
||||||
|
>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="/index.html" class="navbar__link">FAQ</a>
|
<a
|
||||||
|
href="/index.html"
|
||||||
|
class="navbar__link"
|
||||||
|
>FAQ</a
|
||||||
|
>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="#erfahrungen" class="navbar__link">Rezensionen</a>
|
<a href="#erfahrungen" class="navbar__link">Rezensionen</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="/index.html" class="navbar__link">Kontakt</a>
|
<a
|
||||||
|
href="/index.html"
|
||||||
|
class="navbar__link"
|
||||||
|
>Kontakt</a
|
||||||
|
>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import "../../styles/styles.css";
|
||||||
|
import "../../styles/packages.css";
|
||||||
import { renderAngebotsuebersicht } from "./angebotsuebersicht.js";
|
import { renderAngebotsuebersicht } from "./angebotsuebersicht.js";
|
||||||
|
|
||||||
renderAngebotsuebersicht();
|
renderAngebotsuebersicht();
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ export function renderAngebotsuebersicht() {
|
||||||
${highlightHtml}
|
${highlightHtml}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="package-title">${pkg.name}</h2>
|
<h3 class="package-title">${pkg.name}</h3>
|
||||||
<p class="package-desc">${pkg.summary}</p>
|
<p class="package-desc">${pkg.summary}</p>
|
||||||
|
|
||||||
<div class="package-details-box">
|
<div class="package-details-box">
|
||||||
|
|
@ -41,7 +41,7 @@ export function renderAngebotsuebersicht() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a href="/paket-details.html?id=${pkg.id}" class="package-btn">${pkg.buttonText} <span aria-hidden="true">→</span></a>
|
<a href="paket-details.html?id=${pkg.id}" class="package-btn">${pkg.buttonText}</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
|
|
@ -1,158 +1,102 @@
|
||||||
import { packagesDetails } from "../../shared/packagesdetails.js";
|
import { packagesDetails } from "../../shared/packagesdetails.js";
|
||||||
import { mountBookingPage } from "../booking/booking-page.js";
|
|
||||||
import { formatMoney } from "../../shared/order-summary.js";
|
|
||||||
import { paymentNotice } from "../../shared/booking-copy.js";
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const container = document.getElementById("package-detail-container");
|
const container = document.getElementById("package-detail-container");
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const pkg = packagesDetails.find((item) => item.id === params.get("id"));
|
const packageId = params.get("id");
|
||||||
const requestedVariant = params.get("variant");
|
|
||||||
const invalidVariant =
|
|
||||||
requestedVariant &&
|
|
||||||
!pkg?.variants?.some(
|
|
||||||
(variant) => variant.variantId === requestedVariant,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!pkg || invalidVariant) {
|
const pkg = packagesDetails.find((item) => item.id === packageId);
|
||||||
|
|
||||||
|
if (!pkg) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="package-card">
|
<div class="package-card">
|
||||||
<h1>Paket nicht gefunden</h1>
|
<h2>Paket nicht gefunden</h2>
|
||||||
<p>Bitte wähle ein gültiges Paket und eine passende Variante aus.</p>
|
<p>Bitte wähle ein gültiges Angebot aus der Übersicht.</p>
|
||||||
<a href="/angebotsuebersicht.html" class="package-btn">Zur Übersicht</a>
|
<a href="angebotsuebersicht.html" class="package-btn">Zur Übersicht</a>
|
||||||
</div>`;
|
</div>
|
||||||
|
`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.title = `${pkg.type} – Tri-Hub Ernährungsberatung`;
|
const variantsHtml = pkg.variants
|
||||||
const variantsHtml = pkg.variants?.length
|
|
||||||
? `<div class="detail-box">
|
? `<div class="detail-box">
|
||||||
<label for="variant-select">Wähle deine Variante</label>
|
<h3>Wähle deine Variante</h3>
|
||||||
<select id="variant-select" class="variant-select">
|
<select id="variant-select" class="variant-select">
|
||||||
${pkg.variants
|
${pkg.variants.map((v) => `<option value="${v.variantId}" data-price="${v.price}">${v.label} –${v.price.toFixed(2)} €</option>`).join("")}
|
||||||
.map(
|
</select>
|
||||||
(variant) => `
|
</div>`
|
||||||
<option value="${variant.variantId}">
|
|
||||||
${variant.label} – ${formatMoney(variant.price, pkg.currency)}
|
|
||||||
</option>`,
|
|
||||||
)
|
|
||||||
.join("")}
|
|
||||||
</select>
|
|
||||||
</div>`
|
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<header class="detail-heading">
|
<div class="package-card detail-view">
|
||||||
<div class="package-type-container">
|
<div class="package-type-container">
|
||||||
<span class="package-type">${pkg.type} · <span id="package-duration"></span></span>
|
<span class="package-type">${pkg.type} · ${pkg.durationLabel}</span>
|
||||||
${pkg.badge ? `<span class="package-highlight">${pkg.badge}</span>` : ""}
|
<span class="package-highlight">${pkg.badge}</span>
|
||||||
</div>
|
</div>
|
||||||
<h1>${pkg.title}</h1>
|
|
||||||
<p class="package-desc">${pkg.subtitle}</p>
|
|
||||||
</header>
|
|
||||||
<div class="detail-grid">
|
|
||||||
<div class="detail-content">
|
|
||||||
<section aria-labelledby="target-heading">
|
|
||||||
<h2 id="target-heading">Für wen ist dieses Paket ideal?</h2>
|
|
||||||
<ul class="detail-list">
|
|
||||||
${pkg.targetGroup.map((item) => `<li>${item}</li>`).join("")}
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
<section aria-labelledby="features-heading">
|
|
||||||
<h2 id="features-heading">Das ist im Paket enthalten</h2>
|
|
||||||
<ul class="detail-list">
|
|
||||||
${pkg.includedFeatures.map((feature) => `<li>${feature}</li>`).join("")}
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
<section aria-labelledby="steps-heading">
|
|
||||||
<h2 id="steps-heading">So läuft deine Begleitung ab</h2>
|
|
||||||
${pkg.processSteps
|
|
||||||
.map(
|
|
||||||
(step) => `
|
|
||||||
<div class="process-step">
|
|
||||||
<strong>${step.step}</strong>
|
|
||||||
<p class="detail-text">${step.text}</p>
|
|
||||||
</div>`,
|
|
||||||
)
|
|
||||||
.join("")}
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
<section class="package-checkout" aria-labelledby="checkout-heading">
|
|
||||||
<h2 id="checkout-heading">Dein ${pkg.type.toLowerCase().replace(/^./, (letter) => letter.toUpperCase())}-Paket</h2>
|
|
||||||
<div class="price-box" aria-live="polite">
|
|
||||||
<span class="price-amount" id="displayed-price"></span>
|
|
||||||
<span class="price-note">inkl. ${pkg.taxRate} % MwSt. · ${pkg.billingInterval}</span>
|
|
||||||
<p id="selected-duration" class="price-note"></p>
|
|
||||||
</div>
|
|
||||||
${variantsHtml}
|
|
||||||
<button id="proceed-to-booking-btn" class="package-btn primary-cta" type="button" aria-haspopup="dialog" aria-expanded="false" disabled>
|
|
||||||
Buchungsformular wird geladen …
|
|
||||||
</button>
|
|
||||||
<p class="booking-notice" id="booking-notice"></p>
|
|
||||||
<div id="booking-root"></div>
|
|
||||||
</section>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
container.querySelector("#booking-notice").textContent = paymentNotice;
|
<h1 class="package-title">${pkg.title}</h1>
|
||||||
const variantSelect = container.querySelector("#variant-select");
|
<p class="package-desc">${pkg.subtitle}</p>
|
||||||
if (variantSelect && requestedVariant)
|
|
||||||
variantSelect.value = requestedVariant;
|
|
||||||
const bookingBtn = container.querySelector("#proceed-to-booking-btn");
|
|
||||||
const bookingRoot = container.querySelector("#booking-root");
|
|
||||||
let cleanupBooking;
|
|
||||||
let activeDialog;
|
|
||||||
|
|
||||||
function prepareBooking() {
|
<div class="price-box">
|
||||||
cleanupBooking?.();
|
<span class="price-amount" id="displayed-price">${pkg.price.toFixed(2)} €</span>
|
||||||
activeDialog = null;
|
<span class="price-note">inkl. ${pkg.taxRate}% MwSt. (${pkg.billingInterval})</span>
|
||||||
bookingBtn.disabled = true;
|
</div>
|
||||||
bookingBtn.textContent = "Buchungsformular wird geladen …";
|
|
||||||
bookingBtn.setAttribute("aria-expanded", "false");
|
${variantsHtml}
|
||||||
bookingBtn.removeAttribute("aria-controls");
|
|
||||||
const variant = pkg.variants?.find(
|
<div class="package-details-box">
|
||||||
(item) => item.variantId === variantSelect?.value,
|
<div class="detail-item">
|
||||||
);
|
<h3 class="detail-title">Für wen ist dieses Paket ideal?</h3>
|
||||||
const duration = variant
|
<ul class="detail-list">
|
||||||
? `${variant.durationWeeks} Wochen Begleitung`
|
${pkg.targetGroup.map((item) => `<li>${item}</li>`).join("")}
|
||||||
: pkg.durationLabel;
|
</ul>
|
||||||
container.querySelector("#displayed-price").textContent = formatMoney(
|
</div>
|
||||||
variant?.price ?? pkg.price,
|
|
||||||
pkg.currency,
|
<div class="detail-item">
|
||||||
);
|
<h3 class="detail-title">Das ist im Paket enthalten</h3>
|
||||||
container.querySelector("#package-duration").textContent = duration;
|
<ul class="detail-list">
|
||||||
container.querySelector("#selected-duration").textContent = duration;
|
${pkg.includedFeatures.map((feat) => `<li>${feat}</li>`).join("")}
|
||||||
cleanupBooking = mountBookingPage(bookingRoot, {
|
</ul>
|
||||||
packageId: pkg.id,
|
</div>
|
||||||
variantId: variantSelect?.value,
|
|
||||||
|
<div class="detail-item">
|
||||||
|
<h3 class="detail-title">So läuft deine Begleitung ab</h3>
|
||||||
|
${pkg.processSteps
|
||||||
|
.map(
|
||||||
|
(s) => `
|
||||||
|
<div class="process-step">
|
||||||
|
<strong>${s.step}:</strong> <span class="detail-text">${s.text}</span>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.join("")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="proceed-to-booking-btn" class="package-btn primary-cta">
|
||||||
|
${pkg.ctaButtonText}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const variantSelect = document.getElementById("variant-select");
|
||||||
|
const displayedPrice = document.getElementById("displayed-price");
|
||||||
|
if (variantSelect && displayedPrice) {
|
||||||
|
variantSelect.addEventListener("change", (e) => {
|
||||||
|
const selectedOption = e.target.selectedOptions[0];
|
||||||
|
const newPrice = parseFloat(
|
||||||
|
selectedOption.getAttribute("data-price"),
|
||||||
|
);
|
||||||
|
displayedPrice.textContent = `${newPrice.toFixed(2)} €`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Booking lädt die Paketvorschau asynchron, auch bei einem erneuten Versuch.
|
const bookingBtn = document.getElementById("proceed-to-booking-btn");
|
||||||
const observer = new MutationObserver(() => {
|
|
||||||
const dialog = bookingRoot.querySelector(".booking__dialog");
|
|
||||||
if (!dialog || dialog === activeDialog) return;
|
|
||||||
activeDialog = dialog;
|
|
||||||
bookingBtn.disabled = false;
|
|
||||||
bookingBtn.textContent = "Jetzt buchen";
|
|
||||||
bookingBtn.setAttribute("aria-controls", dialog.id);
|
|
||||||
dialog.addEventListener("close", () => {
|
|
||||||
bookingBtn.setAttribute("aria-expanded", "false");
|
|
||||||
bookingBtn.focus();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
observer.observe(bookingRoot, { childList: true, subtree: true });
|
|
||||||
prepareBooking();
|
|
||||||
variantSelect?.addEventListener("change", prepareBooking);
|
|
||||||
|
|
||||||
bookingBtn.addEventListener("click", () => {
|
bookingBtn.addEventListener("click", () => {
|
||||||
if (!activeDialog || activeDialog.open) return;
|
const params = new URLSearchParams({ id: pkg.id });
|
||||||
activeDialog.showModal();
|
if (variantSelect) params.set("variant", variantSelect.value);
|
||||||
bookingBtn.setAttribute("aria-expanded", "true");
|
window.location.href = `booking.html?${params}`;
|
||||||
const result = activeDialog.querySelector(
|
|
||||||
".booking__result:not([hidden])",
|
|
||||||
);
|
|
||||||
(result || activeDialog.querySelector('[name="name"]')).focus();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ if (
|
||||||
if (body.emailStatus === "pending" && body.packageId) {
|
if (body.emailStatus === "pending" && body.packageId) {
|
||||||
const params = new URLSearchParams({ id: body.packageId });
|
const params = new URLSearchParams({ id: body.packageId });
|
||||||
if (order?.variantId) params.set("variant", order.variantId);
|
if (order?.variantId) params.set("variant", order.variantId);
|
||||||
select("confirmation-retry").href = `/paket-details.html?${params}`;
|
select("confirmation-retry").href = `/booking.html?${params}`;
|
||||||
select("confirmation-retry").hidden = false;
|
select("confirmation-retry").hidden = false;
|
||||||
}
|
}
|
||||||
select("confirmation-empty").hidden = true;
|
select("confirmation-empty").hidden = true;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,3 @@
|
||||||
import {
|
|
||||||
isValidBookingPhone,
|
|
||||||
phoneError,
|
|
||||||
} from "../../shared/phone-validation.js";
|
|
||||||
import { previewBooking, submitBooking } from "./booking-api.js";
|
import { previewBooking, submitBooking } from "./booking-api.js";
|
||||||
import "./booking.css";
|
import "./booking.css";
|
||||||
import { orderSummaryRows } from "../../shared/order-summary.js";
|
import { orderSummaryRows } from "../../shared/order-summary.js";
|
||||||
|
|
@ -288,8 +284,12 @@ export function mountBookingPage(
|
||||||
fields.email =
|
fields.email =
|
||||||
"Bitte eine gültige E-Mail-Adresse eingeben.";
|
"Bitte eine gültige E-Mail-Adresse eingeben.";
|
||||||
phone.value = phone.value.trim();
|
phone.value = phone.value.trim();
|
||||||
if (!isValidBookingPhone(phone.value))
|
if (
|
||||||
fields.phone = phoneError;
|
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) {
|
if (Object.keys(fields).length) {
|
||||||
showError(
|
showError(
|
||||||
"Bitte die markierten Angaben prüfen.",
|
"Bitte die markierten Angaben prüfen.",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import "./styles/styles.css";
|
import "./styles/styles.css";
|
||||||
import "./components/navigationsbar/navigation.js";
|
import "./components/navigationsbar/navigation.js";
|
||||||
|
|
||||||
|
|
||||||
document.querySelector("#year").textContent = new Date().getFullYear();
|
document.querySelector("#year").textContent = new Date().getFullYear();
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,3 @@
|
||||||
import {
|
|
||||||
isValidBookingPhone,
|
|
||||||
phoneError,
|
|
||||||
} from "../../shared/phone-validation.js";
|
|
||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
import { packagesDetails } from "../../shared/packagesdetails.js";
|
import { packagesDetails } from "../../shared/packagesdetails.js";
|
||||||
|
|
||||||
|
|
@ -116,12 +112,16 @@ function validateInput(input, key) {
|
||||||
) {
|
) {
|
||||||
fields.email = "Bitte eine gültige E-Mail-Adresse eingeben.";
|
fields.email = "Bitte eine gültige E-Mail-Adresse eingeben.";
|
||||||
}
|
}
|
||||||
const phone = customer ? customer.phone : input.phone;
|
if (!customer && input.phone !== undefined) {
|
||||||
if (phone !== undefined) {
|
if (
|
||||||
if (!isValidBookingPhone(phone)) {
|
typeof input.phone !== "string" ||
|
||||||
fields.phone = phoneError;
|
input.phone.length > 50 ||
|
||||||
} else if (!customer && phone.trim()) {
|
/[\x00-\x1f\x7f]/.test(input.phone)
|
||||||
result.phone = phone.trim();
|
) {
|
||||||
|
fields.phone =
|
||||||
|
"Bitte höchstens 50 Zeichen ohne Zeilenumbrüche eingeben.";
|
||||||
|
} else if (input.phone.trim()) {
|
||||||
|
result.phone = input.phone.trim();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (Object.keys(fields).length) {
|
if (Object.keys(fields).length) {
|
||||||
|
|
|
||||||
|
|
@ -553,7 +553,7 @@
|
||||||
"phone": {
|
"phone": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"maxLength": 50,
|
"maxLength": 50,
|
||||||
"description": "Optionale Telefonnummer: deutsche Nummer mit Vorwahl oder internationale Nummer mit Ländervorwahl. Prüfung des Nummernformats, keine Bestätigung der Erreichbarkeit. Bei customer gilt customer.phone.",
|
"description": "Optionale Telefonnummer bei Übergabe von name und email, ohne Steuerzeichen. Bei customer gilt customer.phone.",
|
||||||
"example": "+49 170 1234567"
|
"example": "+49 170 1234567"
|
||||||
},
|
},
|
||||||
"customer": {
|
"customer": {
|
||||||
|
|
@ -575,8 +575,6 @@
|
||||||
},
|
},
|
||||||
"phone": {
|
"phone": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"maxLength": 50,
|
|
||||||
"description": "Optional. Deutsche Nummer mit Vorwahl oder internationale Nummer mit Ländervorwahl; keine Bestätigung der Erreichbarkeit.",
|
|
||||||
"example": "+49 170 1234567"
|
"example": "+49 170 1234567"
|
||||||
},
|
},
|
||||||
"ageGroup": {
|
"ageGroup": {
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import { isValidPhoneNumber } from "libphonenumber-js/max";
|
|
||||||
|
|
||||||
export const phoneError =
|
|
||||||
"Bitte eine gültige Telefonnummer mit Vorwahl eingeben, z. B. 0170 1234567 oder +49 170 1234567.";
|
|
||||||
|
|
||||||
export function isValidBookingPhone(value) {
|
|
||||||
if (typeof value !== "string" || value.length > 50) return false;
|
|
||||||
if (/[\x00-\x1f\x7f]/.test(value)) return false;
|
|
||||||
const phone = value.trim();
|
|
||||||
if (!phone) return true;
|
|
||||||
if (!/^\+?[0-9 ()/.-]+$/.test(phone)) return false;
|
|
||||||
if (!/^(?:\+|0)/.test(phone)) return false;
|
|
||||||
return isValidPhoneNumber(phone, "DE");
|
|
||||||
}
|
|
||||||
|
|
@ -1,279 +1,123 @@
|
||||||
.packages-page {
|
:root {
|
||||||
margin: 0;
|
--bg-dark: #0d0d1a;
|
||||||
min-height: 100vh;
|
--bg-card: #1a1a2e;
|
||||||
color: #f8fafc;
|
--accent-teal: #10b981;
|
||||||
background:
|
--text-white: #ffffff;
|
||||||
radial-gradient(circle at 12% 0%, #10b98117, transparent 30%),
|
--text-lightgray: #f8fafc;
|
||||||
radial-gradient(circle at 88% 26%, #10b9810e, transparent 27%),
|
--text-muted: #94a3b8;
|
||||||
linear-gradient(145deg, #0d0d1a, #1a1a2e);
|
|
||||||
font-family: Inter, system-ui, sans-serif;
|
--font-display: "Playfair Display", serif;
|
||||||
line-height: 1.6;
|
--font-body: "Inter", system-ui, sans-serif;
|
||||||
color-scheme: dark;
|
|
||||||
}
|
|
||||||
.packages-section,
|
|
||||||
.packages-section * {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.packages-section {
|
.packages-section {
|
||||||
width: min(1180px, calc(100% - 3rem));
|
background-color: var(--bg-dark);
|
||||||
margin: auto;
|
padding: 4rem 2rem;
|
||||||
padding-block: 5rem;
|
font-family: var(--font-body);
|
||||||
overflow-wrap: anywhere;
|
color: var(--text-lightgray);
|
||||||
}
|
|
||||||
.packages-header {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr);
|
|
||||||
gap: 3rem;
|
|
||||||
align-items: end;
|
|
||||||
margin-bottom: 3rem;
|
|
||||||
}
|
|
||||||
.packages-eyebrow,
|
|
||||||
.package-type {
|
|
||||||
color: #10b981;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 0.14em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
.packages-header h1,
|
|
||||||
.detail-heading h1 {
|
|
||||||
margin: 0.75rem 0 1rem;
|
|
||||||
font-size: clamp(2rem, 4.5vw, 3.75rem);
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.15;
|
|
||||||
letter-spacing: -0.035em;
|
|
||||||
}
|
|
||||||
.packages-intro,
|
|
||||||
.package-desc,
|
|
||||||
.detail-text,
|
|
||||||
.detail-list,
|
|
||||||
.price-note,
|
|
||||||
.booking-notice {
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
.packages-intro {
|
|
||||||
margin: 0;
|
|
||||||
max-width: 40rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.packages-container {
|
.packages-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
|
||||||
gap: 1.5rem;
|
gap: 2rem;
|
||||||
}
|
max-width: 1200px;
|
||||||
.package-card,
|
margin: 0 auto;
|
||||||
.package-checkout {
|
|
||||||
min-width: 0;
|
|
||||||
padding: 1.75rem;
|
|
||||||
border: 1px solid #f8fafc1f;
|
|
||||||
border-radius: 1.4rem;
|
|
||||||
background: #ffffff0b;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.package-card {
|
.package-card {
|
||||||
|
background-color: var(--bg-card);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 2rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.package-type-container {
|
.package-type-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.package-type {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
.package-highlight {
|
.package-highlight {
|
||||||
border-radius: 999px;
|
background-color: rgba(16, 185, 129, 0.1);
|
||||||
padding: 0.25rem 0.65rem;
|
color: var(--accent-teal);
|
||||||
background: #10b98114;
|
font-size: 0.7rem;
|
||||||
color: #34d399;
|
padding: 0.25rem 0.75rem;
|
||||||
border: 1px solid #10b98133;
|
border-radius: 9999px;
|
||||||
font-size: 0.65rem;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: 0.05em;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.package-title {
|
.package-title {
|
||||||
margin: 0 0 1rem;
|
font-family: var(--font-display);
|
||||||
color: #fff;
|
font-size: 1.75rem;
|
||||||
font-size: 1.55rem;
|
color: var(--text-white);
|
||||||
font-weight: 600;
|
margin-bottom: 1rem;
|
||||||
line-height: 1.25;
|
line-height: 1.2;
|
||||||
letter-spacing: -0.025em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.package-desc {
|
.package-desc {
|
||||||
margin: 0 0 1.5rem;
|
color: var(--text-lightgray);
|
||||||
}
|
font-size: 0.95rem;
|
||||||
.package-details-box {
|
line-height: 1.5;
|
||||||
padding-block: 1.5rem;
|
|
||||||
border-top: 1px solid #f8fafc1f;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
.detail-item + .detail-item {
|
|
||||||
margin-top: 1.25rem;
|
|
||||||
}
|
|
||||||
.detail-title {
|
|
||||||
margin: 0 0 0.5rem;
|
|
||||||
color: #f8fafc;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.detail-text {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
.package-btn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 3rem;
|
|
||||||
padding: 0.8rem 1.1rem;
|
|
||||||
border: 1px solid #94a3b840;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: #ffffff06;
|
|
||||||
color: #f8fafc;
|
|
||||||
font: inherit;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 600;
|
|
||||||
text-align: center;
|
|
||||||
text-decoration: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.package-btn:hover {
|
|
||||||
border-color: #10b981;
|
|
||||||
background: #10b98114;
|
|
||||||
}
|
|
||||||
.package-btn.primary-cta {
|
|
||||||
background: #10b981;
|
|
||||||
border-color: #10b981;
|
|
||||||
color: #06130f;
|
|
||||||
}
|
|
||||||
.package-btn.primary-cta:hover {
|
|
||||||
background: #34d399;
|
|
||||||
}
|
|
||||||
.package-btn:disabled {
|
|
||||||
opacity: 0.65;
|
|
||||||
cursor: wait;
|
|
||||||
}
|
|
||||||
.packages-section :focus-visible {
|
|
||||||
outline: 3px solid #10b981;
|
|
||||||
outline-offset: 4px;
|
|
||||||
}
|
|
||||||
.packages-back {
|
|
||||||
display: inline-block;
|
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
padding-block: 0.5rem;
|
|
||||||
color: #34d399;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
}
|
||||||
.detail-heading {
|
|
||||||
max-width: 52rem;
|
.package-details-box {
|
||||||
margin-bottom: 3rem;
|
background-color: rgba(255, 255, 255, 0.03);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
flex-grow: 1; /* Schiebt den Button immer ganz nach unten */
|
||||||
}
|
}
|
||||||
.detail-grid {
|
|
||||||
display: grid;
|
.detail-item {
|
||||||
grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr);
|
margin-bottom: 1.25rem;
|
||||||
gap: 2.5rem;
|
|
||||||
align-items: start;
|
|
||||||
}
|
}
|
||||||
.detail-content > section + section {
|
|
||||||
margin-top: 2.5rem;
|
.detail-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
.detail-content h2,
|
|
||||||
.package-checkout h2 {
|
.detail-title {
|
||||||
margin: 0 0 1rem;
|
color: var(--text-white);
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
.detail-list {
|
|
||||||
padding-left: 1.25rem;
|
|
||||||
}
|
|
||||||
.detail-list li {
|
|
||||||
margin-block: 0.65rem;
|
|
||||||
}
|
|
||||||
.detail-list li::marker {
|
|
||||||
color: #10b981;
|
|
||||||
}
|
|
||||||
.process-step {
|
|
||||||
margin-top: 1rem;
|
|
||||||
padding: 1.25rem;
|
|
||||||
border: 1px solid #f8fafc1f;
|
|
||||||
border-radius: 1rem;
|
|
||||||
background: #ffffff06;
|
|
||||||
}
|
|
||||||
.process-step strong {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 0.4rem;
|
|
||||||
}
|
|
||||||
.process-step p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.price-box {
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
.price-amount {
|
|
||||||
display: block;
|
|
||||||
font-size: clamp(2rem, 4vw, 3rem);
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: -0.04em;
|
font-size: 0.9rem;
|
||||||
color: #34d399;
|
|
||||||
}
|
|
||||||
.price-note,
|
|
||||||
.booking-notice {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
.detail-box {
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
.detail-box label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
.variant-select {
|
|
||||||
width: 100%;
|
.detail-text {
|
||||||
max-width: 100%;
|
color: var(--text-muted);
|
||||||
min-width: 0;
|
font-size: 0.85rem;
|
||||||
min-height: 3rem;
|
line-height: 1.5;
|
||||||
padding: 0.65rem;
|
|
||||||
border: 1px solid #94a3b866;
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
color: #f8fafc;
|
|
||||||
background: #0d0d1a;
|
|
||||||
font: inherit;
|
|
||||||
}
|
}
|
||||||
/* Die Detailseite zeigt ihre eigene Übersicht; Booking liefert den Dialog. */
|
|
||||||
.packages-section #booking-root > .booking > h2,
|
.package-btn {
|
||||||
.packages-section #booking-root .booking__package,
|
display: block;
|
||||||
.packages-section #booking-root .booking__payment,
|
text-align: center;
|
||||||
.packages-section #booking-root .booking__open {
|
background-color: transparent;
|
||||||
display: none;
|
color: var(--text-white);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
.packages-section .booking__dialog {
|
|
||||||
background: #0d0d1a;
|
.package-btn:hover {
|
||||||
color: #f8fafc;
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
border-color: #94a3b840;
|
border-color: var(--accent-teal);
|
||||||
}
|
color: var(--accent-teal);
|
||||||
.packages-section .booking__dialog .booking__button:not(.booking__cancel) {
|
|
||||||
background: #10b981;
|
|
||||||
border-color: #10b981;
|
|
||||||
color: #06130f;
|
|
||||||
}
|
|
||||||
.packages-section .booking__dialog :focus-visible {
|
|
||||||
outline-color: #10b981;
|
|
||||||
}
|
|
||||||
@media (max-width: 60rem) {
|
|
||||||
.packages-container {
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
|
|
||||||
}
|
|
||||||
.packages-header,
|
|
||||||
.detail-grid {
|
|
||||||
grid-template-columns: minmax(0, 1fr);
|
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (max-width: 30rem) {
|
|
||||||
.packages-section {
|
|
||||||
width: calc(100% - 2rem);
|
|
||||||
padding-block: 2.5rem;
|
|
||||||
}
|
|
||||||
.package-card,
|
|
||||||
.package-checkout {
|
|
||||||
padding: 1.25rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -619,44 +619,3 @@ test("Swagger-Kundendaten und Zustimmung werden validiert und dauerhaft gespeich
|
||||||
409,
|
409,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Telefonnummern werden in beiden Anfrageformaten geprüft", async (t) => {
|
|
||||||
const { post, sent } = await setup(t);
|
|
||||||
for (const phone of [
|
|
||||||
"123",
|
|
||||||
"00000000000",
|
|
||||||
"abc",
|
|
||||||
"+999 123456789",
|
|
||||||
"0170 test 1234567",
|
|
||||||
]) {
|
|
||||||
for (const data of [
|
|
||||||
{ ...input, phone },
|
|
||||||
{
|
|
||||||
packageId: input.packageId,
|
|
||||||
acceptedTerms: true,
|
|
||||||
customer: {
|
|
||||||
firstName: "Test",
|
|
||||||
lastName: "Person",
|
|
||||||
email: input.email,
|
|
||||||
phone,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]) {
|
|
||||||
const result = await post(data);
|
|
||||||
assert.equal(result.status, 400, phone);
|
|
||||||
assert.ok(result.body.error.fields.phone);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.equal(sent.length, 0);
|
|
||||||
for (const phone of [
|
|
||||||
"",
|
|
||||||
"0170 1234567",
|
|
||||||
"+49 170 1234567",
|
|
||||||
"0049 170 1234567",
|
|
||||||
"030 123456",
|
|
||||||
"+44 20 7946 0958",
|
|
||||||
]) {
|
|
||||||
const result = await post({ ...input, phone });
|
|
||||||
assert.equal(result.status, 201, phone);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue