master #9
14
index.html
14
index.html
@@ -735,8 +735,8 @@
|
|||||||
<div class="header-sub-info">
|
<div class="header-sub-info">
|
||||||
<p id="modal-info-text">Saal • Zeit</p>
|
<p id="modal-info-text">Saal • Zeit</p>
|
||||||
<div id="tech-badges" class="tech-badges-container hidden">
|
<div id="tech-badges" class="tech-badges-container hidden">
|
||||||
<img src="img/dolby.png" alt="Dolby" class="tech-badge">
|
<img src="img/Dolby.png" alt="Dolby" class="tech-badge">
|
||||||
<img src="img/dbox.png" alt="D-Box" class="tech-badge">
|
<img src="img/dbox.jpg" alt="D-Box" class="tech-badge">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -948,11 +948,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="account.js"></script>
|
<script src="src/main.js"></script>
|
||||||
<script src="cart.js"></script>
|
<script src="src/cart.js"></script>
|
||||||
<script src="booking.js"></script>
|
<script src="src/booking.js"></script>
|
||||||
<script src="checkout.js"></script>
|
<script src="src/checkout.js"></script>
|
||||||
<script src="main.js"></script>
|
<script type="module" src="dist/account.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
3093
package-lock.json
generated
Normal file
3093
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
package.json
Normal file
25
package.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "p1---kino",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "main.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"watch": "tsc --watch",
|
||||||
|
"serve": "serve .",
|
||||||
|
"run": "npm-run-all --parallel watch serve"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "ssh://git@gitea.starfour.de:2222/Aaron/Kino-Website.git"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "Aaron und Jannis",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "commonjs",
|
||||||
|
"devDependencies": {
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
|
"serve": "^14.2.6",
|
||||||
|
"typescript": "^6.0.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
466
src/account.ts
Normal file
466
src/account.ts
Normal file
@@ -0,0 +1,466 @@
|
|||||||
|
import type { User } from "./interfaces";
|
||||||
|
|
||||||
|
function readStorageJson(key: string, fallbackValue: any) {
|
||||||
|
const raw = localStorage.getItem(key);
|
||||||
|
|
||||||
|
if (!raw || raw === "undefined" || raw === "null") {
|
||||||
|
return fallbackValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(raw);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Konnte LocalStorage-Wert fuer ${key} nicht lesen.`, error);
|
||||||
|
return fallbackValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeUser(user: User): User {
|
||||||
|
return {
|
||||||
|
firstName: user.firstName || "",
|
||||||
|
lastName: user.lastName || "",
|
||||||
|
email: user.email || "",
|
||||||
|
hashedPassword: user.hashedPassword || "",
|
||||||
|
orders: Array.isArray(user.orders) ? user.orders : [],
|
||||||
|
paymentMethods: Array.isArray(user.paymentMethods) ? user.paymentMethods : []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(value: string) {
|
||||||
|
return String(value || "")
|
||||||
|
.replaceAll("&", "&")
|
||||||
|
.replaceAll("<", "<")
|
||||||
|
.replaceAll(">", ">")
|
||||||
|
.replaceAll('"', """)
|
||||||
|
.replaceAll("'", "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatEuro(value: string) {
|
||||||
|
return `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistUsers() {
|
||||||
|
localStorage.setItem("eagleUsers", JSON.stringify(users));
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistCurrentUser() {
|
||||||
|
if (currentUser) {
|
||||||
|
localStorage.setItem("currentUser", JSON.stringify(currentUser));
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem("currentUser");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let users = readStorageJson("eagleUsers", []);
|
||||||
|
if (!Array.isArray(users)) {
|
||||||
|
users = [];
|
||||||
|
}
|
||||||
|
users = users.map(normalizeUser).filter(Boolean);
|
||||||
|
|
||||||
|
const rawCurrentUser = readStorageJson("currentUser", null);
|
||||||
|
var currentUser: User | null = rawCurrentUser ? normalizeUser(rawCurrentUser) : null;
|
||||||
|
if (currentUser && currentUser.email) {
|
||||||
|
const currentEmail = currentUser.email;
|
||||||
|
const storedMatch = users.find((user: { email: string; }) => {
|
||||||
|
return user.email === currentEmail;
|
||||||
|
});
|
||||||
|
if (storedMatch) {
|
||||||
|
currentUser = storedMatch;
|
||||||
|
} else {
|
||||||
|
users.push(currentUser);
|
||||||
|
persistUsers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hashMessage(message: string) {
|
||||||
|
const msgBuffer = new TextEncoder().encode(message); // Encode as UTF-8
|
||||||
|
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer); // Hash
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer)); // Convert to bytes
|
||||||
|
return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); // Hex string
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInputValue(id: string): string {
|
||||||
|
const el = document.getElementById(id) as HTMLInputElement | null;
|
||||||
|
return el?.value.trim() ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function registerUser() {
|
||||||
|
const firstName = getInputValue("reg-firstname");
|
||||||
|
const lastName = getInputValue("reg-lastname");
|
||||||
|
const email = getInputValue("reg-email").toLowerCase();
|
||||||
|
const password = document.querySelector<HTMLInputElement>("#reg-password")?.value ?? "";
|
||||||
|
|
||||||
|
if (!firstName || !lastName || !email || !password) {
|
||||||
|
alert("Bitte fuelle alle Felder aus.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!email.includes("@")) {
|
||||||
|
alert("Bitte gib eine gueltige E-Mail-Adresse ein.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingUser = users.find((user: User) => user.email.toLowerCase() === email);
|
||||||
|
if (existingUser) {
|
||||||
|
alert("E-Mail bereits registriert");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashedPassword = await hashMessage(password);
|
||||||
|
|
||||||
|
const newUser = {
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
email,
|
||||||
|
hashedPassword,
|
||||||
|
orders: [],
|
||||||
|
paymentMethods: []
|
||||||
|
};
|
||||||
|
|
||||||
|
users.push(newUser);
|
||||||
|
currentUser = newUser;
|
||||||
|
|
||||||
|
persistUsers();
|
||||||
|
persistCurrentUser();
|
||||||
|
|
||||||
|
alert("Registrierung erfolgreich");
|
||||||
|
document.getElementById("register-modal")?.classList.add("hidden");
|
||||||
|
|
||||||
|
openAccountDashboard();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loginUser() {
|
||||||
|
const email = (document.querySelector<HTMLInputElement>("#login-email")?.value.trim() || "").toLowerCase();
|
||||||
|
const password = document.querySelector<HTMLInputElement>("#login-password")?.value || "";
|
||||||
|
const hashedPassword = await hashMessage(password);
|
||||||
|
|
||||||
|
const user = users.find(
|
||||||
|
(entry: User) => entry.email.toLowerCase() === email && entry.hashedPassword === hashedPassword
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
document.getElementById("login-error")?.classList.remove("hidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentUser = user;
|
||||||
|
persistCurrentUser();
|
||||||
|
openAccountDashboard();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAccountDashboard() {
|
||||||
|
const accountView = document.getElementById("account-view");
|
||||||
|
if (!accountView) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentUser) {
|
||||||
|
accountView.innerHTML = "<div class='account-login-box'><h2>Mein Konto</h2><p>Bitte melde dich an oder registriere dich.</p></div>";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
accountView.innerHTML = /*html*/`
|
||||||
|
<div class="account-panel">
|
||||||
|
<div class="account-panel-header">
|
||||||
|
<h2>Mein Konto</h2>
|
||||||
|
<button class="account-logout-btn" onclick="logoutUser()">Abmelden</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="account-tabs">
|
||||||
|
<button class="account-tab-btn" onclick="renderPersonalInfo()">Persönliche Daten</button>
|
||||||
|
<button class="account-tab-btn" onclick="renderOrders()">Meine Bestellungen</button>
|
||||||
|
<button class="account-tab-btn" onclick="renderPayments()">Zahlungsmethoden</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="account-tab-content"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
renderPersonalInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPersonalInfo() {
|
||||||
|
const target = document.getElementById("account-tab-content");
|
||||||
|
if (!target || !currentUser) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
target.innerHTML = `
|
||||||
|
<div class="account-card">
|
||||||
|
<p><strong>Vorname:</strong> ${currentUser.firstName || "-"}</p>
|
||||||
|
<p><strong>Nachname:</strong> ${currentUser.lastName || "-"}</p>
|
||||||
|
<p><strong>E-Mail:</strong> ${currentUser.email || "-"}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderOrders() {
|
||||||
|
const target = document.getElementById("account-tab-content");
|
||||||
|
if (!target || !currentUser) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const orders = Array.isArray(currentUser.orders) ? currentUser.orders : [];
|
||||||
|
|
||||||
|
if (!orders.length) {
|
||||||
|
target.innerHTML = `
|
||||||
|
<div class="account-card">
|
||||||
|
<h3>Meine Bestellungen</h3>
|
||||||
|
<p>Noch keine Bestellungen vorhanden.</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderHtml = orders
|
||||||
|
.map((order, index) => {
|
||||||
|
const movieItems = Array.isArray(order.items)
|
||||||
|
? order.items.filter((item: any) => item.category === "movie")
|
||||||
|
: [];
|
||||||
|
const previewItem = movieItems[0] || (Array.isArray(order.items) ? order.items[0] : null);
|
||||||
|
const previewTitle = previewItem?.title || "Bestellung";
|
||||||
|
const ticketsCount = movieItems.length || (Array.isArray(order.items) ? order.items.length : 0);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<button type="button" class="order-box order-item-btn" data-order-index="${index}">
|
||||||
|
<div class="order-item-head">
|
||||||
|
<h4>${escapeHtml(previewTitle)}</h4>
|
||||||
|
<span>${formatEuro(order.total || 0)}</span>
|
||||||
|
</div>
|
||||||
|
<p><strong>Datum:</strong> ${escapeHtml(order.date || "-")}</p>
|
||||||
|
<p><strong>Anzahl:</strong> ${ticketsCount}x</p>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
target.innerHTML = `
|
||||||
|
<div class="account-orders-shell">
|
||||||
|
<h3>Meine Bestellungen</h3>
|
||||||
|
<p class="account-payments-note">Klicke auf eine Bestellung, um dein Ticket-Detail zu sehen.</p>
|
||||||
|
<div class="account-orders-grid">${orderHtml}</div>
|
||||||
|
<div id="order-ticket-details" class="order-ticket-details hidden"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const detailTarget = document.getElementById("order-ticket-details");
|
||||||
|
const orderButtons = Array.from(target.querySelectorAll<HTMLButtonElement>(".order-item-btn"));
|
||||||
|
|
||||||
|
const renderOrderTicket = (orderIndex: number) => {
|
||||||
|
const order = orders[orderIndex];
|
||||||
|
if (!order || !detailTarget) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const movieItems = Array.isArray(order.items)
|
||||||
|
? order.items.filter((item: any) => item.category === "movie")
|
||||||
|
: [];
|
||||||
|
const primaryMovie = movieItems[0] || (Array.isArray(order.items) ? order.items[0] : null);
|
||||||
|
const poster = primaryMovie?.img || "";
|
||||||
|
const seats = movieItems.map((item: any) => item.seatId).filter(Boolean).join(", ") || "-";
|
||||||
|
const ticketCount = movieItems.length || (Array.isArray(order.items) ? order.items.length : 0);
|
||||||
|
const hall = primaryMovie?.hall || "-";
|
||||||
|
const time = primaryMovie?.time ? `${primaryMovie.time} Uhr` : "-";
|
||||||
|
|
||||||
|
detailTarget.innerHTML = `
|
||||||
|
<article class="order-ticket-card">
|
||||||
|
<div class="order-ticket-poster">
|
||||||
|
${poster
|
||||||
|
? `<img src="${escapeHtml(poster)}" alt="${escapeHtml(primaryMovie?.title || "Film")}">`
|
||||||
|
: `<div class="order-ticket-poster-fallback">Kein Poster</div>`}
|
||||||
|
</div>
|
||||||
|
<div class="order-ticket-content">
|
||||||
|
<div class="order-ticket-brand">EAGLE'S IMAX | Bestell-Details</div>
|
||||||
|
<h4>${escapeHtml(primaryMovie?.title || "Bestellung")}</h4>
|
||||||
|
<div class="order-ticket-grid">
|
||||||
|
<p><span>Datum</span><strong>${escapeHtml(order.date || "-")}</strong></p>
|
||||||
|
<p><span>Saal</span><strong>${escapeHtml(hall)}</strong></p>
|
||||||
|
<p><span>Uhrzeit</span><strong>${escapeHtml(time)}</strong></p>
|
||||||
|
<p><span>Tickets</span><strong>${ticketCount}x</strong></p>
|
||||||
|
<p><span>Sitze</span><strong>${escapeHtml(seats)}</strong></p>
|
||||||
|
<p><span>Gesamt</span><strong>${formatEuro(order.total || 0)}</strong></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
`;
|
||||||
|
|
||||||
|
detailTarget.classList.remove("hidden");
|
||||||
|
orderButtons.forEach((button) => {
|
||||||
|
button.classList.toggle("active", Number(button.dataset.orderIndex) === orderIndex);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
orderButtons.forEach((button) => {
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
const orderIndex = Number(button.dataset.orderIndex || -1);
|
||||||
|
if (orderIndex >= 0) {
|
||||||
|
renderOrderTicket(orderIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPayments() {
|
||||||
|
const target = document.getElementById("account-tab-content");
|
||||||
|
if (!target || !currentUser) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
target.innerHTML = /*html*/`
|
||||||
|
<div class="account-card">
|
||||||
|
<h3>Zahlungsmethoden</h3>
|
||||||
|
<p class="account-payments-note">Platzhalter zum Hinterlegen deiner Logos oder Anbieter-Informationen.</p>
|
||||||
|
<div class="account-payment-grid">
|
||||||
|
<button type="button" class="account-payment-card account-pay-trigger" data-pay-modal="pay-modal-card">
|
||||||
|
<div class="payment-logo-slot">
|
||||||
|
<img src="img/mastercard.png" alt="Mastercard">
|
||||||
|
</div>
|
||||||
|
<h4>Visa / Mastercard</h4>
|
||||||
|
<p>Karteninformationen hinterlegen</p>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="account-payment-card account-pay-trigger" data-pay-modal="pay-modal-paypal">
|
||||||
|
<div class="payment-logo-slot">
|
||||||
|
<img src="img/paypal.png" alt="PayPal">
|
||||||
|
</div>
|
||||||
|
<h4>PayPal</h4>
|
||||||
|
<p>Konto verbinden</p>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="account-payment-card account-pay-trigger" data-pay-modal="pay-modal-apple">
|
||||||
|
<div class="payment-logo-slot">
|
||||||
|
<img src="img/applepay.png" alt="Apple Pay">
|
||||||
|
</div>
|
||||||
|
<h4>Apple Pay</h4>
|
||||||
|
<p>Geraet freischalten</p>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="account-payment-card account-pay-trigger" data-pay-modal="pay-modal-google">
|
||||||
|
<div class="payment-logo-slot">
|
||||||
|
<img src="img/googlepay.png" alt="Google Pay">
|
||||||
|
</div>
|
||||||
|
<h4>Google Pay</h4>
|
||||||
|
<p>Wallet verknuepfen</p>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="pay-modal-card" class="pay-modal-overlay hidden">
|
||||||
|
<div class="pay-modal-panel pay-modal-card-style">
|
||||||
|
<button type="button" class="pay-close-btn" data-pay-close>×</button>
|
||||||
|
<div class="pay-modal-head">
|
||||||
|
<img src="img/mastercard.png" alt="Kreditkarte">
|
||||||
|
<h4>Kreditkarte hinterlegen</h4>
|
||||||
|
</div>
|
||||||
|
<div class="pay-form-grid">
|
||||||
|
<label>Kartennummer
|
||||||
|
<input type="text" placeholder="1234 5678 9012 3456">
|
||||||
|
</label>
|
||||||
|
<div class="pay-form-row">
|
||||||
|
<label>Exp. Datum
|
||||||
|
<input type="text" placeholder="MM/JJ">
|
||||||
|
</label>
|
||||||
|
<label>CVV
|
||||||
|
<input type="text" placeholder="123">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<label>Name auf Karte
|
||||||
|
<input type="text" placeholder="Max Mustermann">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="pay-submit-btn">Karte speichern</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="pay-modal-paypal" class="pay-modal-overlay hidden">
|
||||||
|
<div class="pay-modal-panel pay-modal-paypal-style">
|
||||||
|
<button type="button" class="pay-close-btn" data-pay-close>×</button>
|
||||||
|
<div class="pay-modal-head">
|
||||||
|
<img src="img/paypal.png" alt="PayPal">
|
||||||
|
<h4>PayPal verbinden</h4>
|
||||||
|
</div>
|
||||||
|
<p>Einloggen und dein PayPal-Konto mit deinem Kino-Account verbinden.</p>
|
||||||
|
<label>E-Mail
|
||||||
|
<input type="email" placeholder="name@beispiel.de">
|
||||||
|
</label>
|
||||||
|
<label>Passwort
|
||||||
|
<input type="password" placeholder="Passwort">
|
||||||
|
</label>
|
||||||
|
<button type="button" class="pay-submit-btn paypal-btn">Mit PayPal fortfahren</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="pay-modal-apple" class="pay-modal-overlay hidden">
|
||||||
|
<div class="pay-modal-panel pay-modal-apple-style">
|
||||||
|
<button type="button" class="pay-close-btn" data-pay-close>×</button>
|
||||||
|
<div class="pay-modal-head">
|
||||||
|
<img src="img/applepay.png" alt="Apple Pay">
|
||||||
|
<h4>Apple Pay einrichten</h4>
|
||||||
|
</div>
|
||||||
|
<p>Apple Pay wirkt schlicht, klar und fokussiert. Hinterlege hier die bevorzugte Karte für schnelle Zahlungen.</p>
|
||||||
|
<label>Apple-ID E-Mail
|
||||||
|
<input type="email" placeholder="appleid@beispiel.de">
|
||||||
|
</label>
|
||||||
|
<label>Bevorzugte Karte
|
||||||
|
<input type="text" placeholder="Visa, Mastercard, ...">
|
||||||
|
</label>
|
||||||
|
<button type="button" class="pay-submit-btn apple-btn">Zu Wallet hinzufügen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="pay-modal-google" class="pay-modal-overlay hidden">
|
||||||
|
<div class="pay-modal-panel pay-modal-google-style">
|
||||||
|
<button type="button" class="pay-close-btn" data-pay-close>×</button>
|
||||||
|
<div class="pay-modal-head">
|
||||||
|
<img src="img/googlepay.png" alt="Google Pay">
|
||||||
|
<h4>Google Pay einrichten</h4>
|
||||||
|
</div>
|
||||||
|
<p>Verbinde deine Wallet, damit zukünftige Bestellungen in wenigen Klicks abgeschlossen werden.</p>
|
||||||
|
<label>Google-Konto
|
||||||
|
<input type="email" placeholder="konto@gmail.com">
|
||||||
|
</label>
|
||||||
|
<label>Standard-Zahlungsquelle
|
||||||
|
<input type="text" placeholder="z. B. Visa 1234">
|
||||||
|
</label>
|
||||||
|
<button type="button" class="pay-submit-btn google-btn">Wallet verbinden</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const modals = Array.from(target.querySelectorAll(".pay-modal-overlay"));
|
||||||
|
const triggers = Array.from(target.querySelectorAll(".account-pay-trigger"));
|
||||||
|
const closeButtons = Array.from(target.querySelectorAll("[data-pay-close]"));
|
||||||
|
|
||||||
|
const closeAllPaymentModals = () => {
|
||||||
|
modals.forEach((modal) => modal.classList.add("hidden"));
|
||||||
|
document.body.style.overflow = "auto";
|
||||||
|
};
|
||||||
|
|
||||||
|
triggers.forEach((trigger) => {
|
||||||
|
trigger.addEventListener("click", () => {
|
||||||
|
closeAllPaymentModals();
|
||||||
|
const targetId = trigger.getAttribute("data-pay-modal");
|
||||||
|
const modal = targetId ? target.querySelector(`#${targetId}`) : null;
|
||||||
|
|
||||||
|
if (modal) {
|
||||||
|
modal.classList.remove("hidden");
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
closeButtons.forEach((button) => {
|
||||||
|
button.addEventListener("click", closeAllPaymentModals);
|
||||||
|
});
|
||||||
|
|
||||||
|
modals.forEach((modal) => {
|
||||||
|
modal.addEventListener("click", (event) => {
|
||||||
|
if (event.target === modal) {
|
||||||
|
closeAllPaymentModals();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function logoutUser() {
|
||||||
|
persistCurrentUser();
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
352
src/booking.js
Normal file
352
src/booking.js
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
let currentBookingContext = null;
|
||||||
|
let currentHallLayout = null;
|
||||||
|
|
||||||
|
function openBooking(movie, hall, time) {
|
||||||
|
const titleEl = document.getElementById("modal-movie-title");
|
||||||
|
const infoEl = document.getElementById("modal-info-text");
|
||||||
|
|
||||||
|
if (titleEl) {
|
||||||
|
titleEl.innerText = movie;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (infoEl) {
|
||||||
|
infoEl.innerText = `${hall} • ${time} Uhr`;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentBookingContext = { movie, hall, time };
|
||||||
|
|
||||||
|
createSeats(hall, time);
|
||||||
|
renderBookingLegend();
|
||||||
|
updateBookingSummary();
|
||||||
|
|
||||||
|
document.getElementById("booking-modal")?.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRowLabel(rowIndex) {
|
||||||
|
return String(rowIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildHallLayout(hallName, baseConfig) {
|
||||||
|
const rows = Number(baseConfig.rows || 0);
|
||||||
|
const totalCols = Number(baseConfig.left || 0) + Number(baseConfig.right || 0);
|
||||||
|
const isDeluxe = /deluxe/i.test(hallName);
|
||||||
|
|
||||||
|
const left = isDeluxe
|
||||||
|
? Math.max(3, Number(baseConfig.left || 0) - 1)
|
||||||
|
: Number(baseConfig.left || 0);
|
||||||
|
const right = Math.max(0, totalCols - left);
|
||||||
|
|
||||||
|
const vipRows = rows > 0 ? [rows] : [];
|
||||||
|
|
||||||
|
const dboxMap = new Set();
|
||||||
|
const markDboxRange = (rowNumber, startCol, width) => {
|
||||||
|
if (!rowNumber || width <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxCol = Math.min(totalCols, startCol + width - 1);
|
||||||
|
for (let col = startCol; col <= maxCol; col++) {
|
||||||
|
dboxMap.add(`${rowNumber}-${col}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isDeluxe) {
|
||||||
|
const configuredDboxSeats = Array.isArray(baseConfig.dbox)
|
||||||
|
? baseConfig.dbox.reduce((sum, section) => sum + Number(section.w || 0), 0)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const totalDboxSeats = Math.max(4, configuredDboxSeats || 0);
|
||||||
|
|
||||||
|
const firstRow = Math.max(1, rows - 2);
|
||||||
|
const secondRow = Math.max(1, rows - 1);
|
||||||
|
const targetRows = [firstRow, secondRow]
|
||||||
|
.filter((rowNumber, index, arr) => arr.indexOf(rowNumber) === index)
|
||||||
|
.filter((rowNumber) => !vipRows.includes(rowNumber));
|
||||||
|
|
||||||
|
const rowCount = Math.max(1, targetRows.length);
|
||||||
|
const seatsPerFirstRows = Math.ceil(totalDboxSeats / rowCount);
|
||||||
|
let remaining = totalDboxSeats;
|
||||||
|
|
||||||
|
targetRows.forEach((rowNumber, index) => {
|
||||||
|
const seatsForRow = index === targetRows.length - 1
|
||||||
|
? remaining
|
||||||
|
: Math.min(seatsPerFirstRows, remaining);
|
||||||
|
remaining -= seatsForRow;
|
||||||
|
|
||||||
|
const startCol = left + Math.max(1, Math.floor((right - seatsForRow) / 2) + 1);
|
||||||
|
markDboxRange(rowNumber, startCol, seatsForRow);
|
||||||
|
});
|
||||||
|
} else if (Array.isArray(baseConfig.dbox)) {
|
||||||
|
baseConfig.dbox.forEach((section) => {
|
||||||
|
const rowNumber = Number(section.r || 0);
|
||||||
|
const width = Number(section.w || 0);
|
||||||
|
const startCol = Number(section.c || 0);
|
||||||
|
markDboxRange(rowNumber, startCol, width);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
rows,
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
totalCols,
|
||||||
|
vipRows,
|
||||||
|
dboxMap,
|
||||||
|
isImax: Boolean(baseConfig.isImax)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSeatType(layout, rowNumber, colNumber) {
|
||||||
|
if (layout.dboxMap.has(`${rowNumber}-${colNumber}`)) {
|
||||||
|
return "dbox";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layout.vipRows.includes(rowNumber)) {
|
||||||
|
return "vip";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layout.isImax) {
|
||||||
|
return "imax";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "normal";
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSeatElement({ seatId, seatType, occupiedSeats }) {
|
||||||
|
const seat = document.createElement("button");
|
||||||
|
seat.type = "button";
|
||||||
|
seat.classList.add("seat", seatType);
|
||||||
|
seat.dataset.seatId = seatId;
|
||||||
|
seat.dataset.type = seatType;
|
||||||
|
seat.title = `${seatId} (${seatType.toUpperCase()})`;
|
||||||
|
|
||||||
|
if (occupiedSeats.has(seatId)) {
|
||||||
|
seat.classList.add("occupied");
|
||||||
|
seat.disabled = true;
|
||||||
|
seat.setAttribute("aria-label", `${seatId} belegt`);
|
||||||
|
return seat;
|
||||||
|
}
|
||||||
|
|
||||||
|
seat.setAttribute("aria-label", `${seatId} frei`);
|
||||||
|
seat.addEventListener("click", () => {
|
||||||
|
seat.classList.toggle("selected");
|
||||||
|
updateBookingSummary();
|
||||||
|
});
|
||||||
|
|
||||||
|
return seat;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSeats(hallName, time) {
|
||||||
|
const seatGrid = document.getElementById("seat-grid");
|
||||||
|
if (!seatGrid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
seatGrid.innerHTML = "";
|
||||||
|
|
||||||
|
const baseConfig = seatLayouts[hallName];
|
||||||
|
if (!baseConfig) {
|
||||||
|
currentHallLayout = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentHallLayout = buildHallLayout(hallName, baseConfig);
|
||||||
|
|
||||||
|
const occupiedKey = `${hallName}-${time}`;
|
||||||
|
const occupiedSeats = new Set(Array.isArray(occupiedSeatsData?.[occupiedKey]) ? occupiedSeatsData[occupiedKey] : []);
|
||||||
|
|
||||||
|
for (let rowIndex = 0; rowIndex < currentHallLayout.rows; rowIndex++) {
|
||||||
|
const rowNumber = rowIndex + 1;
|
||||||
|
const rowLabel = getRowLabel(rowIndex);
|
||||||
|
|
||||||
|
const perspectiveFactor = (currentHallLayout.rows - rowNumber) / Math.max(currentHallLayout.rows - 1, 1);
|
||||||
|
const rowIndent = Math.round(18 * perspectiveFactor);
|
||||||
|
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.className = "seat-row cinema-row";
|
||||||
|
row.style.setProperty("--row-indent", `${rowIndent}px`);
|
||||||
|
|
||||||
|
const leftLabel = document.createElement("div");
|
||||||
|
leftLabel.className = "row-label";
|
||||||
|
leftLabel.textContent = rowLabel;
|
||||||
|
|
||||||
|
const rightLabel = document.createElement("div");
|
||||||
|
rightLabel.className = "row-label row-label-right";
|
||||||
|
rightLabel.textContent = rowLabel;
|
||||||
|
|
||||||
|
const leftBlock = document.createElement("div");
|
||||||
|
leftBlock.className = "row-seat-block left-block";
|
||||||
|
|
||||||
|
const rightBlock = document.createElement("div");
|
||||||
|
rightBlock.className = "row-seat-block right-block";
|
||||||
|
|
||||||
|
for (let col = 1; col <= currentHallLayout.totalCols; col++) {
|
||||||
|
const seatId = `R${rowNumber}-P${col}`;
|
||||||
|
const seatType = getSeatType(currentHallLayout, rowNumber, col);
|
||||||
|
const seat = createSeatElement({ seatId, seatType, occupiedSeats });
|
||||||
|
|
||||||
|
if (col <= currentHallLayout.left) {
|
||||||
|
leftBlock.appendChild(seat);
|
||||||
|
} else {
|
||||||
|
rightBlock.appendChild(seat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const aisle = document.createElement("div");
|
||||||
|
aisle.className = "aisle-gap";
|
||||||
|
|
||||||
|
row.append(leftLabel, leftBlock, aisle, rightBlock, rightLabel);
|
||||||
|
seatGrid.appendChild(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBookingLegend() {
|
||||||
|
const legend = document.getElementById("dynamic-legend");
|
||||||
|
if (!legend || !currentHallLayout) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const legendItems = [
|
||||||
|
{ type: "normal", label: "Standard" },
|
||||||
|
{ type: "selected", label: "Ausgewählt" },
|
||||||
|
{ type: "occupied", label: "Belegt" }
|
||||||
|
];
|
||||||
|
|
||||||
|
if (currentHallLayout.isImax) {
|
||||||
|
legendItems.unshift({ type: "imax", label: "IMAX" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentHallLayout.vipRows.length > 0) {
|
||||||
|
legendItems.unshift({ type: "vip", label: "VIP" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentHallLayout.dboxMap.size > 0) {
|
||||||
|
legendItems.unshift({ type: "dbox", label: "D-BOX" });
|
||||||
|
}
|
||||||
|
|
||||||
|
legend.innerHTML = legendItems
|
||||||
|
.map((item) => `
|
||||||
|
<div class="item">
|
||||||
|
<span class="seat ${item.type}"></span>
|
||||||
|
<span>${item.label}</span>
|
||||||
|
</div>
|
||||||
|
`)
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateBookingSummary() {
|
||||||
|
const selectedSeats = Array.from(document.querySelectorAll("#seat-grid .seat.selected"));
|
||||||
|
const summaryPanel = document.getElementById("booking-summary");
|
||||||
|
const summaryItems = document.getElementById("summary-items");
|
||||||
|
const totalEl = document.getElementById("total-price");
|
||||||
|
|
||||||
|
let total = 0;
|
||||||
|
|
||||||
|
if (summaryItems) {
|
||||||
|
summaryItems.innerHTML = selectedSeats
|
||||||
|
.map((seat) => {
|
||||||
|
const type = seat.dataset.type || "normal";
|
||||||
|
const seatPrice = Number(prices?.[type] ?? prices?.normal ?? 11);
|
||||||
|
total += seatPrice;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="summary-row">
|
||||||
|
<span>${seat.dataset.seatId}</span>
|
||||||
|
<span>${seatPrice.toFixed(2).replace(".", ",")} EUR</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
} else {
|
||||||
|
selectedSeats.forEach((seat) => {
|
||||||
|
const type = seat.dataset.type || "normal";
|
||||||
|
const seatPrice = Number(prices?.[type] ?? prices?.normal ?? 11);
|
||||||
|
total += seatPrice;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalEl) {
|
||||||
|
totalEl.innerText = `${total.toFixed(2).replace(".", ",")} EUR`;
|
||||||
|
}
|
||||||
|
|
||||||
|
summaryPanel?.classList.toggle("hidden", selectedSeats.length === 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findMoviePoster(movieTitle) {
|
||||||
|
const cards = Array.from(document.querySelectorAll(".movie-card, .detailed-card"));
|
||||||
|
const normalizedTarget = String(movieTitle || "").trim().toLowerCase();
|
||||||
|
|
||||||
|
for (const card of cards) {
|
||||||
|
const title = card.querySelector("h2, h3")?.innerText?.trim().toLowerCase();
|
||||||
|
if (title === normalizedTarget) {
|
||||||
|
const imageSrc = card.querySelector("img")?.src;
|
||||||
|
if (imageSrc) {
|
||||||
|
return imageSrc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmSelectedSeats() {
|
||||||
|
const selectedSeats = Array.from(document.querySelectorAll("#seat-grid .seat.selected"));
|
||||||
|
|
||||||
|
if (!currentBookingContext || selectedSeats.length === 0) {
|
||||||
|
alert("Bitte waehle mindestens einen Platz aus.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const moviePoster = findMoviePoster(currentBookingContext.movie);
|
||||||
|
const addedSeats = [];
|
||||||
|
|
||||||
|
selectedSeats.forEach((seat) => {
|
||||||
|
const seatId = seat.dataset.seatId;
|
||||||
|
const seatType = seat.dataset.type || "normal";
|
||||||
|
|
||||||
|
const alreadyInCart = cart.some((item) =>
|
||||||
|
item.category === "movie" &&
|
||||||
|
item.title === currentBookingContext.movie &&
|
||||||
|
item.hall === currentBookingContext.hall &&
|
||||||
|
item.time === currentBookingContext.time &&
|
||||||
|
item.seatId === seatId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (alreadyInCart) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cart.push({
|
||||||
|
id: Date.now() + Math.random(),
|
||||||
|
category: "movie",
|
||||||
|
title: currentBookingContext.movie,
|
||||||
|
hall: currentBookingContext.hall,
|
||||||
|
time: currentBookingContext.time,
|
||||||
|
seatId,
|
||||||
|
type: seatType.toUpperCase(),
|
||||||
|
price: Number(prices?.[seatType] ?? prices?.normal ?? 11),
|
||||||
|
img: moviePoster
|
||||||
|
});
|
||||||
|
|
||||||
|
addedSeats.push(seatId);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!addedSeats.length) {
|
||||||
|
alert("Diese Plaetze sind bereits im Warenkorb.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveCart?.();
|
||||||
|
renderCart?.();
|
||||||
|
renderCheckout?.();
|
||||||
|
|
||||||
|
document.getElementById("booking-modal")?.classList.add("hidden");
|
||||||
|
|
||||||
|
const snackOverlay = document.getElementById("snack-prompt-overlay");
|
||||||
|
snackOverlay?.classList.remove("hidden");
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
document.getElementById("btn-confirm-seats")?.addEventListener("click", confirmSelectedSeats);
|
||||||
|
});
|
||||||
199
src/cart.js
Normal file
199
src/cart.js
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
function formatEuro(value) {
|
||||||
|
return `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return String(value || "")
|
||||||
|
.replaceAll("&", "&")
|
||||||
|
.replaceAll("<", "<")
|
||||||
|
.replaceAll(">", ">")
|
||||||
|
.replaceAll('"', """)
|
||||||
|
.replaceAll("'", "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCartKey(item) {
|
||||||
|
const infoText = item.category === "movie"
|
||||||
|
? `Sitz: ${item.seatId} (${item.hall})`
|
||||||
|
: item.time;
|
||||||
|
return `${item.title}-${item.hall}-${infoText}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDrinkItem(item) {
|
||||||
|
if (item.category !== "snack") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = String(item.title || "").toLowerCase();
|
||||||
|
const size = String(item.hall || "").toLowerCase();
|
||||||
|
const drinkKeywords = [
|
||||||
|
"cola",
|
||||||
|
"sprite",
|
||||||
|
"fanta",
|
||||||
|
"mezzo",
|
||||||
|
"fuze",
|
||||||
|
"wasser",
|
||||||
|
"getraenk",
|
||||||
|
"drink"
|
||||||
|
];
|
||||||
|
|
||||||
|
return drinkKeywords.some((word) => title.includes(word)) || size.includes("l");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildItemInfo(item) {
|
||||||
|
if (item.category === "movie") {
|
||||||
|
return `
|
||||||
|
<div>Sitzplatz: ${escapeHtml(item.seatId || "-")}</div>
|
||||||
|
<div>Saal: ${escapeHtml(item.hall || "-")}</div>
|
||||||
|
<div>Uhrzeit: ${escapeHtml(item.time || "-")} Uhr</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDrinkItem(item)) {
|
||||||
|
return `
|
||||||
|
<div>Variante: ${escapeHtml(item.time || "-")}</div>
|
||||||
|
<div>Groesse: ${escapeHtml(item.hall || "-")}</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div>Kategorie: Snack</div>
|
||||||
|
<div>Variante: ${escapeHtml(item.time || "-")}</div>
|
||||||
|
<div>Groesse: ${escapeHtml(item.hall || "-")}</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupCartItems() {
|
||||||
|
const groups = new Map();
|
||||||
|
|
||||||
|
cart.forEach((item) => {
|
||||||
|
const key = buildCartKey(item);
|
||||||
|
|
||||||
|
if (!groups.has(key)) {
|
||||||
|
groups.set(key, {
|
||||||
|
key,
|
||||||
|
quantity: 0,
|
||||||
|
total: 0,
|
||||||
|
item
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const group = groups.get(key);
|
||||||
|
group.quantity += 1;
|
||||||
|
group.total += Number(item.price || 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(groups.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveCart() {
|
||||||
|
localStorage.setItem("eagleCart", JSON.stringify(cart));
|
||||||
|
updateCartBadge();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCartBadge() {
|
||||||
|
const cartBadge = document.getElementById("cart-badge");
|
||||||
|
|
||||||
|
if (!cartBadge) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cartBadge.innerText = cart.length;
|
||||||
|
cartBadge.classList.toggle("hidden", cart.length === 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCart() {
|
||||||
|
const cartList = document.getElementById("cart-items-list");
|
||||||
|
const totalEl = document.getElementById("cart-total-right");
|
||||||
|
const vatEl = document.getElementById("cart-vat-right");
|
||||||
|
|
||||||
|
if (!cartList || !totalEl || !vatEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(cart) || cart.length === 0) {
|
||||||
|
cartList.innerHTML = '<p>Dein Warenkorb ist leer.</p>';
|
||||||
|
totalEl.innerText = formatEuro(0);
|
||||||
|
vatEl.innerText = `inkl. 19% MwSt: ${formatEuro(0)}`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupedItems = groupCartItems();
|
||||||
|
|
||||||
|
const header = /*html*/`
|
||||||
|
<div class="cart-header-row">
|
||||||
|
<div class="col-amount">MENGE</div>
|
||||||
|
<div class="col-img">VORSCHAU</div>
|
||||||
|
<div class="col-product">NAME</div>
|
||||||
|
<div class="col-details">INFO</div>
|
||||||
|
<div class="col-price">PREIS</div>
|
||||||
|
<div class="col-action">AKTION</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const rows = groupedItems
|
||||||
|
.map((group) => {
|
||||||
|
const imageHtml = group.item.img
|
||||||
|
? /*html*/`<img class="cart-img-small" src="${escapeHtml(group.item.img)}" alt="${escapeHtml(group.item.title)}">`
|
||||||
|
: /*html*/`<div class="cart-img-fallback">Kein Bild</div>`;
|
||||||
|
const quantityHtml = group.item.category === "movie"
|
||||||
|
? /*html*/`<div class="qty-static" aria-label="Feste Ticketanzahl">${group.quantity}x</div>`
|
||||||
|
: /*html*/`
|
||||||
|
<div class="qty-stepper">
|
||||||
|
<button class="btn-qty" data-action="minus" data-key="${escapeHtml(group.key)}">-</button>
|
||||||
|
<span>${group.quantity}</span>
|
||||||
|
<button class="btn-qty" data-action="plus" data-key="${escapeHtml(group.key)}">+</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return /*html*/`
|
||||||
|
<div class="cart-item-row">
|
||||||
|
<div class="col-amount">
|
||||||
|
${quantityHtml}
|
||||||
|
</div>
|
||||||
|
<div class="col-img">${imageHtml}</div>
|
||||||
|
<div class="col-product">${escapeHtml(group.item.title)}</div>
|
||||||
|
<div class="col-details cart-item-info">${buildItemInfo(group.item)}</div>
|
||||||
|
<div class="col-price">${formatEuro(group.total)}</div>
|
||||||
|
<div class="col-action">
|
||||||
|
<button class="btn-delete-item" data-key="${escapeHtml(group.key)}" aria-label="Eintrag entfernen"><span aria-hidden="true">🗑️</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
cartList.innerHTML = header + rows;
|
||||||
|
|
||||||
|
const total = cart.reduce((sum, item) => sum + Number(item.price || 0), 0);
|
||||||
|
const vat = total - total / 1.19;
|
||||||
|
|
||||||
|
totalEl.innerText = formatEuro(total);
|
||||||
|
vatEl.innerText = `inkl. 19% MwSt: ${formatEuro(vat)}`;
|
||||||
|
|
||||||
|
saveCart();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.removeItem = function removeItem(id) {
|
||||||
|
cart = cart.filter((item) => item.id !== id);
|
||||||
|
saveCart();
|
||||||
|
renderCart();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.changeQty = function changeQty(title, delta) {
|
||||||
|
if (delta > 0) {
|
||||||
|
const item = cart.find((entry) => entry.title === title);
|
||||||
|
if (item) {
|
||||||
|
cart.push({ ...item, id: Date.now() + Math.random() });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const index = cart
|
||||||
|
.map((entry) => entry.title)
|
||||||
|
.lastIndexOf(title);
|
||||||
|
if (index !== -1) {
|
||||||
|
cart.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveCart();
|
||||||
|
renderCart();
|
||||||
|
};
|
||||||
232
src/checkout.js
Normal file
232
src/checkout.js
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
function formatCheckoutEuro(value) {
|
||||||
|
return `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let selectedPaymentMethod = "";
|
||||||
|
let checkoutEventsBound = false;
|
||||||
|
|
||||||
|
function setCheckoutStep(step) {
|
||||||
|
const step1 = document.getElementById("checkout-step-1");
|
||||||
|
const step2 = document.getElementById("checkout-step-2");
|
||||||
|
const step3 = document.getElementById("checkout-step-3");
|
||||||
|
|
||||||
|
step1?.classList.toggle("hidden", step !== 1);
|
||||||
|
step2?.classList.toggle("hidden", step !== 2);
|
||||||
|
step3?.classList.toggle("hidden", step !== 3);
|
||||||
|
|
||||||
|
const line1 = document.getElementById("line-1");
|
||||||
|
const line2 = document.getElementById("line-2");
|
||||||
|
const indicator1 = document.getElementById("step-1-indicator");
|
||||||
|
const indicator2 = document.getElementById("step-2-indicator");
|
||||||
|
const indicator3 = document.getElementById("step-3-indicator");
|
||||||
|
|
||||||
|
indicator1?.classList.add("active");
|
||||||
|
indicator2?.classList.toggle("active", step >= 2);
|
||||||
|
indicator3?.classList.toggle("active", step >= 3);
|
||||||
|
line1?.classList.toggle("active", step >= 2);
|
||||||
|
line2?.classList.toggle("active", step >= 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCheckout() {
|
||||||
|
const summaryList = document.getElementById("checkout-summary-list");
|
||||||
|
const totalDisplay = document.getElementById("checkout-total-display");
|
||||||
|
const vatDisplay = document.getElementById("checkout-vat-display");
|
||||||
|
const nextButton = document.getElementById("btn-next-step-2");
|
||||||
|
|
||||||
|
if (!summaryList) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
summaryList.innerHTML = "";
|
||||||
|
|
||||||
|
const safeCart = Array.isArray(cart) ? cart : [];
|
||||||
|
const total = safeCart.reduce((sum, item) => sum + Number(item.price || 0), 0);
|
||||||
|
const vat = total - total / 1.19;
|
||||||
|
|
||||||
|
safeCart.forEach((item) => {
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.style.cssText = "display:flex; justify-content:space-between; gap:12px; margin-bottom:10px; font-size:0.95rem;";
|
||||||
|
|
||||||
|
const infoText = item.category === "movie"
|
||||||
|
? `Sitz ${item.seatId || "-"} | ${item.hall || "-"} | ${item.time || "-"} Uhr`
|
||||||
|
: `${item.time || "Standard"} | ${item.hall || "-"}`;
|
||||||
|
|
||||||
|
row.innerHTML = `<span>${item.title} (${infoText})</span><span>${formatCheckoutEuro(item.price)}</span>`;
|
||||||
|
summaryList.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (totalDisplay) {
|
||||||
|
totalDisplay.innerText = `Gesamtbetrag: ${formatCheckoutEuro(total)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vatDisplay) {
|
||||||
|
vatDisplay.innerText = `inkl. 19% MwSt: ${formatCheckoutEuro(vat)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedPaymentMethod = "";
|
||||||
|
document.querySelectorAll(".payment-method").forEach((method) => {
|
||||||
|
method.classList.remove("selected");
|
||||||
|
});
|
||||||
|
|
||||||
|
nextButton?.classList.add("hidden");
|
||||||
|
setCheckoutStep(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateTicket() {
|
||||||
|
const ticketContainer = document.getElementById("ticket-container");
|
||||||
|
if (!ticketContainer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const moviesInCart = (Array.isArray(cart) ? cart : []).filter((item) => item.category === "movie");
|
||||||
|
if (!moviesInCart.length) {
|
||||||
|
ticketContainer.innerHTML = "<p>Danke fuer deinen Einkauf!</p>";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mainMovie = moviesInCart[0];
|
||||||
|
const matchingMovieSeats = moviesInCart
|
||||||
|
.filter((item) => item.title === mainMovie.title && item.time === mainMovie.time)
|
||||||
|
.map((item) => item.seatId)
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
const qrData = encodeURIComponent(`EAGLE-IMAX|${mainMovie.title}|${mainMovie.hall}|${matchingMovieSeats}`);
|
||||||
|
const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=${qrData}&bgcolor=ffffff`;
|
||||||
|
|
||||||
|
ticketContainer.innerHTML = /*html*/`
|
||||||
|
<div class="luxury-ticket">
|
||||||
|
<div class="ticket-left">
|
||||||
|
<img src="${mainMovie.img}" class="ticket-poster" alt="${mainMovie.title}">
|
||||||
|
</div>
|
||||||
|
<div class="ticket-right">
|
||||||
|
<div class="ticket-brand">EAGLE'S IMAX PREMIUM</div>
|
||||||
|
<h2 class="ticket-title">${mainMovie.title}</h2>
|
||||||
|
<div class="ticket-details">
|
||||||
|
<p><span>SAAL</span> <strong>${mainMovie.hall}</strong></p>
|
||||||
|
<p><span>ZEIT</span> <strong>${mainMovie.time} Uhr</strong></p>
|
||||||
|
<p><span>SITZE</span> <strong>${matchingMovieSeats || "-"}</strong></p>
|
||||||
|
</div>
|
||||||
|
<div class="ticket-footer">
|
||||||
|
<img src="${qrUrl}" class="ticket-qr" alt="QR Code">
|
||||||
|
<div class="ticket-code">#${Math.floor(Math.random() * 90000) + 10000}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveOrderForCurrentUser(orderItems, orderTotal) {
|
||||||
|
if (typeof currentUser === "undefined" || !currentUser) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof users === "undefined" || !Array.isArray(users)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const order = {
|
||||||
|
date: new Date().toLocaleString("de-DE"),
|
||||||
|
items: orderItems,
|
||||||
|
total: orderTotal,
|
||||||
|
paymentMethod: selectedPaymentMethod || "-"
|
||||||
|
};
|
||||||
|
|
||||||
|
const userIndex = users.findIndex((entry) => entry.email === currentUser.email);
|
||||||
|
if (userIndex === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(users[userIndex].orders)) {
|
||||||
|
users[userIndex].orders = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
users[userIndex].orders.push(order);
|
||||||
|
localStorage.setItem("eagleUsers", JSON.stringify(users));
|
||||||
|
}
|
||||||
|
|
||||||
|
function reserveSeatsAfterPayment(orderItems) {
|
||||||
|
const movieItems = orderItems.filter((item) => item.category === "movie");
|
||||||
|
|
||||||
|
movieItems.forEach((item) => {
|
||||||
|
const key = `${item.hall}-${item.time}`;
|
||||||
|
if (!occupiedSeatsData[key]) {
|
||||||
|
occupiedSeatsData[key] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
occupiedSeatsData[key].push(item.seatId);
|
||||||
|
});
|
||||||
|
|
||||||
|
localStorage.setItem("eagleOccupied", JSON.stringify(occupiedSeatsData));
|
||||||
|
}
|
||||||
|
|
||||||
|
function completeCheckout() {
|
||||||
|
const orderItems = Array.isArray(cart) ? [...cart] : [];
|
||||||
|
const orderTotal = orderItems.reduce((sum, item) => sum + Number(item.price || 0), 0);
|
||||||
|
|
||||||
|
saveOrderForCurrentUser(orderItems, orderTotal);
|
||||||
|
reserveSeatsAfterPayment(orderItems);
|
||||||
|
|
||||||
|
cart = [];
|
||||||
|
saveCart?.();
|
||||||
|
renderCart?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindCheckoutEvents() {
|
||||||
|
if (checkoutEventsBound) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkoutEventsBound = true;
|
||||||
|
|
||||||
|
const nextButton = document.getElementById("btn-next-step-2");
|
||||||
|
const backButton = document.getElementById("btn-back-to-step1");
|
||||||
|
const payNowButton = document.getElementById("btn-pay-now");
|
||||||
|
|
||||||
|
document.querySelectorAll(".payment-method").forEach((method) => {
|
||||||
|
method.addEventListener("click", () => {
|
||||||
|
document.querySelectorAll(".payment-method").forEach((entry) => {
|
||||||
|
entry.classList.remove("selected");
|
||||||
|
});
|
||||||
|
|
||||||
|
method.classList.add("selected");
|
||||||
|
selectedPaymentMethod = method.dataset.method || "";
|
||||||
|
nextButton?.classList.remove("hidden");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
nextButton?.addEventListener("click", () => {
|
||||||
|
if (!selectedPaymentMethod) {
|
||||||
|
alert("Bitte waehle zuerst eine Zahlungsmethode aus.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCheckoutStep(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
backButton?.addEventListener("click", () => {
|
||||||
|
setCheckoutStep(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
payNowButton?.addEventListener("click", () => {
|
||||||
|
if (!Array.isArray(cart) || !cart.length) {
|
||||||
|
alert("Dein Warenkorb ist leer.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
payNowButton.disabled = true;
|
||||||
|
payNowButton.innerText = "Verarbeite...";
|
||||||
|
payNowButton.style.opacity = "0.7";
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setCheckoutStep(3);
|
||||||
|
generateTicket();
|
||||||
|
completeCheckout();
|
||||||
|
|
||||||
|
payNowButton.disabled = false;
|
||||||
|
payNowButton.innerText = "Jetzt Bezahlen";
|
||||||
|
payNowButton.style.opacity = "1";
|
||||||
|
}, 1200);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", bindCheckoutEvents);
|
||||||
8
src/interfaces.ts
Normal file
8
src/interfaces.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export interface User {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
email: string;
|
||||||
|
hashedPassword: string;
|
||||||
|
orders: any[]; // TODO: figure out proper array type of orders. Probably smartest do create an Order interface which this would be an array of
|
||||||
|
paymentMethods: any[]; // TODO: figure out proper array type of paymentMethods. create paymentMethod interface and make this an array of it
|
||||||
|
}
|
||||||
1092
src/main.js
Normal file
1092
src/main.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2902,15 +2902,15 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.inline-halls .inline-media {
|
.inline-halls .inline-media {
|
||||||
background-image: linear-gradient(120deg, rgba(0, 113, 227, 0.3), rgba(7, 10, 16, 0.55)), url('img/placeholder-hall.jpg');
|
background-image: linear-gradient(120deg, rgba(0, 113, 227, 0.3), rgba(7, 10, 16, 0.55)), url('img/shelter.jpg');
|
||||||
}
|
}
|
||||||
|
|
||||||
.inline-dbox .inline-media {
|
.inline-dbox .inline-media {
|
||||||
background-image: linear-gradient(120deg, rgba(255, 176, 0, 0.2), rgba(8, 12, 18, 0.62)), url('img/placeholder-dbox.jpg');
|
background-image: linear-gradient(120deg, rgba(255, 176, 0, 0.2), rgba(8, 12, 18, 0.62)), url('img/dbox.jpg');
|
||||||
}
|
}
|
||||||
|
|
||||||
.inline-collectors .inline-media {
|
.inline-collectors .inline-media {
|
||||||
background-image: linear-gradient(120deg, rgba(185, 124, 255, 0.15), rgba(8, 12, 18, 0.62)), url('img/placeholder-collector.jpg');
|
background-image: linear-gradient(120deg, rgba(185, 124, 255, 0.15), rgba(8, 12, 18, 0.62)), url('img/popcorn.jpg');
|
||||||
}
|
}
|
||||||
|
|
||||||
.inline-content {
|
.inline-content {
|
||||||
|
|||||||
45
tsconfig.json
Normal file
45
tsconfig.json
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
// Visit https://aka.ms/tsconfig to read more about this file
|
||||||
|
"compilerOptions": {
|
||||||
|
// File Layout
|
||||||
|
"rootDir": "./src",
|
||||||
|
"outDir": "./dist",
|
||||||
|
|
||||||
|
// Environment Settings
|
||||||
|
// See also https://aka.ms/tsconfig/module
|
||||||
|
"module": "ES2020",
|
||||||
|
"target": "ES2021",
|
||||||
|
"types": [],
|
||||||
|
// For nodejs:
|
||||||
|
// "lib": ["esnext"],
|
||||||
|
// "types": ["node"],
|
||||||
|
// and npm install -D @types/node
|
||||||
|
|
||||||
|
// Other Outputs
|
||||||
|
"sourceMap": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
|
||||||
|
// Stricter Typechecking Options
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"exactOptionalPropertyTypes": true,
|
||||||
|
|
||||||
|
// Style Options
|
||||||
|
// "noImplicitReturns": true,
|
||||||
|
// "noImplicitOverride": true,
|
||||||
|
// "noUnusedLocals": true,
|
||||||
|
// "noUnusedParameters": true,
|
||||||
|
// "noFallthroughCasesInSwitch": true,
|
||||||
|
// "noPropertyAccessFromIndexSignature": true,
|
||||||
|
|
||||||
|
// Recommended Options
|
||||||
|
"strict": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noUncheckedSideEffectImports": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
},
|
||||||
|
"include": ["src/*"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user