forked from Aaron/Kino-Website
dev_jannis #3
@@ -1,14 +0,0 @@
|
|||||||
---
|
|
||||||
import { getTopMovies } from "../scripts/fetchMovies";
|
|
||||||
import type { MovieCatalog } from "../scripts/interfaces";
|
|
||||||
|
|
||||||
const movieProgram = await getTopMovies();
|
|
||||||
---
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
movieProgram.map((movie: MovieCatalog, programIndex: any) => {
|
|
||||||
{<div>{movie.genre}</div>}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
@@ -1,474 +0,0 @@
|
|||||||
import type { User } from "./interfaces.js";
|
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export let users = readStorageJson("eagleUsers", []);
|
|
||||||
if (!Array.isArray(users)) {
|
|
||||||
users = [];
|
|
||||||
}
|
|
||||||
users = users.map(normalizeUser).filter(Boolean);
|
|
||||||
|
|
||||||
const rawCurrentUser = readStorageJson("currentUser", null);
|
|
||||||
|
|
||||||
export 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() ?? "";
|
|
||||||
}
|
|
||||||
|
|
||||||
export 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
export 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
export 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
(window as any).logoutUser = logoutUser;
|
|
||||||
(window as any).renderPersonalInfo = renderPersonalInfo;
|
|
||||||
(window as any).renderOrders = renderOrders;
|
|
||||||
(window as any).renderPayments = renderPayments;
|
|
||||||
|
|
||||||
@@ -1,358 +0,0 @@
|
|||||||
import { seatLayouts, occupiedSeatsData, prices, cart } from "./main.js"
|
|
||||||
import { renderCart, saveCart } from "./cart.js";
|
|
||||||
import { renderCheckout } from "./checkout.js";
|
|
||||||
|
|
||||||
let currentBookingContext: any = null;
|
|
||||||
let currentHallLayout: any = null;
|
|
||||||
|
|
||||||
export function openBooking(movie: string, hall: string, time: any) {
|
|
||||||
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: number) {
|
|
||||||
return String(rowIndex + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildHallLayout(hallName: string, baseConfig:any) {
|
|
||||||
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: number, startCol: number, width: number) => {
|
|
||||||
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: number, section: any) => 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: any) => {
|
|
||||||
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: any, rowNumber: number, colNumber: number) {
|
|
||||||
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 }:any) {
|
|
||||||
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: string, time: any) {
|
|
||||||
const seatGrid = document.getElementById("seat-grid");
|
|
||||||
if (!seatGrid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
seatGrid.innerHTML = "";
|
|
||||||
|
|
||||||
const arrIndex = hallName as keyof typeof seatLayouts;
|
|
||||||
const baseConfig: any = seatLayouts[arrIndex];
|
|
||||||
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")) as HTMLElement[];;
|
|
||||||
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") as keyof typeof prices;
|
|
||||||
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: string) {
|
|
||||||
const cards = Array.from(document.querySelectorAll(".movie-card, .detailed-card"));
|
|
||||||
const normalizedTarget = String(movieTitle || "").trim().toLowerCase();
|
|
||||||
|
|
||||||
for (const card of cards) {
|
|
||||||
const currentCard = card.querySelector("h2, h3") as HTMLElement;
|
|
||||||
const title = currentCard.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")) as HTMLElement[];
|
|
||||||
|
|
||||||
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: any) =>
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
import { cart } from "./main.js";
|
|
||||||
|
|
||||||
function formatEuro(value: number) {
|
|
||||||
return `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeHtml(value: any) {
|
|
||||||
return String(value || "")
|
|
||||||
.replaceAll("&", "&")
|
|
||||||
.replaceAll("<", "<")
|
|
||||||
.replaceAll(">", ">")
|
|
||||||
.replaceAll('"', """)
|
|
||||||
.replaceAll("'", "'");
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildCartKey(item: { category: string; seatId: any; hall: any; time: any; title: any; }) {
|
|
||||||
const infoText = item.category === "movie"
|
|
||||||
? `Sitz: ${item.seatId} (${item.hall})`
|
|
||||||
: item.time;
|
|
||||||
return `${item.title}-${item.hall}-${infoText}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isDrinkItem(item: { category: string; title: any; hall: any; }) {
|
|
||||||
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: { category: any; seatId?: any; hall: any; time?: any; title: any; }) {
|
|
||||||
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: { price?: any; category: string; seatId: any; hall: any; time: any; title: any; }) => {
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
export function saveCart() {
|
|
||||||
localStorage.setItem("eagleCart", JSON.stringify(cart));
|
|
||||||
updateCartBadge();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateCartBadge() {
|
|
||||||
const cartBadge = document.getElementById("cart-badge");
|
|
||||||
|
|
||||||
if (!cartBadge) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cartBadge.innerText = cart.length;
|
|
||||||
cartBadge.classList.toggle("hidden", cart.length === 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
export 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();
|
|
||||||
}
|
|
||||||
//@ts-ignore
|
|
||||||
window.removeItem = function removeItem(id: any) {
|
|
||||||
var localCart = cart.filter((item: { id: any; }) => item.id !== id);
|
|
||||||
saveCart();
|
|
||||||
renderCart();
|
|
||||||
};
|
|
||||||
|
|
||||||
//@ts-ignore
|
|
||||||
window.changeQty = function changeQty(title, delta): void {
|
|
||||||
if (delta > 0) {
|
|
||||||
const item = cart.find((entry: { title: any; }) => entry.title === title);
|
|
||||||
if (item) {
|
|
||||||
cart.push({ ...item, id: Date.now() + Math.random() });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const index = cart
|
|
||||||
.map((entry: { title: any; }) => entry.title)
|
|
||||||
.lastIndexOf(title);
|
|
||||||
if (index !== -1) {
|
|
||||||
cart.splice(index, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
saveCart();
|
|
||||||
renderCart();
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -1,238 +0,0 @@
|
|||||||
import { currentUser, users } from "./account.js";
|
|
||||||
import { renderCart, saveCart } from "./cart.js";
|
|
||||||
import { cart, emptyCart, occupiedSeatsData } from "./main.js";
|
|
||||||
|
|
||||||
function formatCheckoutEuro(value: number) {
|
|
||||||
return `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let selectedPaymentMethod = "";
|
|
||||||
let checkoutEventsBound = false;
|
|
||||||
|
|
||||||
function setCheckoutStep(step: number) {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
export 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: any[], orderTotal: any) {
|
|
||||||
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 || "-"
|
|
||||||
};
|
|
||||||
|
|
||||||
//@ts-ignore
|
|
||||||
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: any[]) {
|
|
||||||
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);
|
|
||||||
|
|
||||||
emptyCart?.()
|
|
||||||
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") as HTMLButtonElement;
|
|
||||||
|
|
||||||
document.querySelectorAll(".payment-method").forEach((method) => {
|
|
||||||
method.addEventListener("click", () => {
|
|
||||||
document.querySelectorAll(".payment-method").forEach((entry) => {
|
|
||||||
entry.classList.remove("selected");
|
|
||||||
});
|
|
||||||
|
|
||||||
method.classList.add("selected");
|
|
||||||
//@ts-ignore
|
|
||||||
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);
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import type { MovieCatalog, ITMDBResponse } from "./interfaces";
|
|
||||||
|
|
||||||
export async function getTopMovies(): Promise<MovieCatalog[]> {
|
|
||||||
const API_KEY = import.meta.env.TMDB_API_KEY;
|
|
||||||
const IMAGE_BASE_URL = 'https://image.tmdb.org/t/p/w500'
|
|
||||||
|
|
||||||
const response = await fetch(`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}`);
|
|
||||||
const data: ITMDBResponse = await response.json();
|
|
||||||
|
|
||||||
return data.results.slice(0, 5).map((movie) => ({
|
|
||||||
id: movie.id,
|
|
||||||
title: movie.title,
|
|
||||||
poster: `${IMAGE_BASE_URL}${movie.poster_path}`,
|
|
||||||
rating: movie.vote_average,
|
|
||||||
year: movie.release_date.split('-')[0], // Extracts just the year
|
|
||||||
genre: movie.genre,
|
|
||||||
duration: movie.duration,
|
|
||||||
fsk: movie.age_reccomendation,
|
|
||||||
description: movie.overview,
|
|
||||||
backdrop: movie.backdrop_path,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MovieCatalog {
|
|
||||||
id: number;
|
|
||||||
title: string;
|
|
||||||
genre: string;
|
|
||||||
duration: number;
|
|
||||||
fsk: string;
|
|
||||||
description: string;
|
|
||||||
poster: string;
|
|
||||||
backdrop: string;
|
|
||||||
rating: number;
|
|
||||||
year: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The shape of a single movie object from TMDb
|
|
||||||
interface ITMDBMovie {
|
|
||||||
id: number;
|
|
||||||
title: string;
|
|
||||||
poster_path: string;
|
|
||||||
release_date: string;
|
|
||||||
vote_average: number;
|
|
||||||
overview: string;
|
|
||||||
genre: string;
|
|
||||||
duration: number;
|
|
||||||
age_reccomendation: string;
|
|
||||||
backdrop_path: string;
|
|
||||||
// ... add other fields as needed
|
|
||||||
}
|
|
||||||
|
|
||||||
// The shape of the API response
|
|
||||||
export interface ITMDBResponse {
|
|
||||||
page: number;
|
|
||||||
results: ITMDBMovie[];
|
|
||||||
total_pages: number;
|
|
||||||
total_results: number;
|
|
||||||
}
|
|
||||||
@@ -1,865 +0,0 @@
|
|||||||
import { currentUser, loginUser, openAccountDashboard, registerUser } from "./account.js";
|
|
||||||
import { openBooking } from "./booking.js";
|
|
||||||
import { renderCart, saveCart, updateCartBadge } from "./cart.js";
|
|
||||||
import { renderCheckout } from "./checkout.js";
|
|
||||||
import type { MovieCatalog as MovieCatalogInterface } from "./interfaces.js";
|
|
||||||
|
|
||||||
export const movieCatalog: MovieCatalogInterface[] = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: "oh hell nah",
|
|
||||||
genre: "n",
|
|
||||||
duration: 3,
|
|
||||||
fsk: "jfd",
|
|
||||||
description: "jsss",
|
|
||||||
poster: "g",
|
|
||||||
backdrop: "f",
|
|
||||||
rating: 1,
|
|
||||||
year: "d",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
// Shared app state for legacy script files (account.js, booking.js, cart.js, checkout.js)
|
|
||||||
export const prices: Record<string, number> = { normal: 11.0, imax: 15.0, vip: 12.0, dbox: 16.0 };
|
|
||||||
export var seatLayouts = {
|
|
||||||
"Kino 1": { rows: 6, left: 3, right: 7, vipRows: [5], dbox: [] },
|
|
||||||
"Kino 2": { rows: 7, left: 5, right: 5, vipRows: [6], dbox: [] },
|
|
||||||
"Deluxe 1": { rows: 10, left: 7, right: 8, vipRows: [9], dbox: [{ r: 4, c: 5, w: 4 }] },
|
|
||||||
IMAX: { rows: 15, left: 10, right: 10, vipRows: [], dbox: [], isImax: true }
|
|
||||||
};
|
|
||||||
export var cart: any[] = JSON.parse(localStorage.getItem("eagleCart") || '[]');
|
|
||||||
export var occupiedSeatsData = JSON.parse(localStorage.getItem("eagleOccupied") || '{}');
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
const views = {
|
|
||||||
hero: document.querySelector(".hero"),
|
|
||||||
moviesGrid: document.getElementById("movies-grid-section"),
|
|
||||||
list: document.getElementById("movie-list-view"),
|
|
||||||
halls: document.getElementById("halls-view"),
|
|
||||||
dbox: document.getElementById("dbox-view"),
|
|
||||||
collectors: document.getElementById("collectors-view"),
|
|
||||||
about: document.getElementById("about-view"),
|
|
||||||
snacks: document.getElementById("snacks-view"),
|
|
||||||
cart: document.getElementById("cart-view"),
|
|
||||||
checkout: document.getElementById("checkout-view"),
|
|
||||||
account: document.getElementById("account-view")
|
|
||||||
};
|
|
||||||
|
|
||||||
const ui = {
|
|
||||||
logo: document.getElementById("logo-home"),
|
|
||||||
linkFilme: document.getElementById("link-filme"),
|
|
||||||
linkSnacks: document.getElementById("link-snacks"),
|
|
||||||
linkAbout: document.getElementById("link-about"),
|
|
||||||
linkCart: document.getElementById("link-cart"),
|
|
||||||
linkAccount: document.getElementById("link-account"),
|
|
||||||
themeToggle: document.getElementById("theme-toggle"),
|
|
||||||
heroBookingBtn: document.getElementById("hero-booking-btn"),
|
|
||||||
heroSlider: document.getElementById("hero-slider"),
|
|
||||||
heroDots: document.getElementById("hero-dots"),
|
|
||||||
heroTitle: document.getElementById("hero-title"),
|
|
||||||
heroText: document.getElementById("hero-text"),
|
|
||||||
nowRunningRow: document.getElementById("now-running-row"),
|
|
||||||
movieProgramList: document.getElementById("movie-program-list"),
|
|
||||||
checkoutBtn: document.getElementById("btn-checkout-final"),
|
|
||||||
backHomeBtn: document.getElementById("btn-back-home"),
|
|
||||||
snacksView: document.getElementById("snacks-view"),
|
|
||||||
snackOverlay: document.getElementById("snack-prompt-overlay"),
|
|
||||||
btnYesSnacks: document.getElementById("btn-yes-snacks"),
|
|
||||||
btnNoCart: document.getElementById("btn-no-cart"),
|
|
||||||
bookingModal: document.getElementById("booking-modal"),
|
|
||||||
closeBookingModalBtn: document.querySelector(".close-btn")
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkoutSteps = {
|
|
||||||
one: document.getElementById("checkout-step-1"),
|
|
||||||
two: document.getElementById("checkout-step-2"),
|
|
||||||
three: document.getElementById("checkout-step-3")
|
|
||||||
};
|
|
||||||
|
|
||||||
const hallRotation = ["IMAX", "Deluxe 1", "Kino 1", "Kino 2"];
|
|
||||||
const timePatterns = [
|
|
||||||
["13:00", "15:20", "17:40", "20:00", "22:20"],
|
|
||||||
["13:00", "14:50", "17:10", "19:30", "21:50"],
|
|
||||||
["13:00", "15:10", "17:30", "19:50", "22:10"],
|
|
||||||
["13:00", "16:00", "18:20", "20:40"]
|
|
||||||
];
|
|
||||||
|
|
||||||
let movieProgram: any = []; // TODO: Find type
|
|
||||||
let heroItems: any = []; // TODO: find Type
|
|
||||||
let heroIndex = 0;
|
|
||||||
let heroTimer: any = null; // TODO: find type
|
|
||||||
|
|
||||||
const weekdayShort = ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"];
|
|
||||||
|
|
||||||
const hideAllViews = () => {
|
|
||||||
Object.values(views).forEach((view) => view?.classList.add("hidden"));
|
|
||||||
document.getElementById("about-tech-modal")?.classList.add("hidden");
|
|
||||||
document.body.style.overflow = "auto";
|
|
||||||
};
|
|
||||||
|
|
||||||
const showHome = () => {
|
|
||||||
hideAllViews();
|
|
||||||
views.hero?.classList.remove("hidden");
|
|
||||||
views.moviesGrid?.classList.remove("hidden");
|
|
||||||
document.getElementById("about-tech-modal")?.classList.add("hidden");
|
|
||||||
document.body.style.overflow = "auto";
|
|
||||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
||||||
};
|
|
||||||
|
|
||||||
const showMovieList = (programIndexToFocus: number = NaN) => {
|
|
||||||
hideAllViews();
|
|
||||||
views.list?.classList.remove("hidden");
|
|
||||||
|
|
||||||
if (programIndexToFocus === null) {
|
|
||||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const target = views.list?.querySelector(`[data-program-index="${programIndexToFocus}"]`);
|
|
||||||
if (target) {
|
|
||||||
target.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
||||||
target.classList.add("flash-focus");
|
|
||||||
setTimeout(() => target.classList.remove("flash-focus"), 1200);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const showStaticView = (viewElement: HTMLElement) => {
|
|
||||||
if (!viewElement) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
hideAllViews();
|
|
||||||
viewElement.classList.remove("hidden");
|
|
||||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
||||||
};
|
|
||||||
|
|
||||||
const showCheckoutStart = () => {
|
|
||||||
if (!cart.length) {
|
|
||||||
alert("Dein Warenkorb ist leer.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
hideAllViews();
|
|
||||||
views.checkout?.classList.remove("hidden");
|
|
||||||
checkoutSteps.one?.classList.remove("hidden");
|
|
||||||
checkoutSteps.two?.classList.add("hidden");
|
|
||||||
checkoutSteps.three?.classList.add("hidden");
|
|
||||||
renderCheckout?.();
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeBookingModal = () => {
|
|
||||||
ui.bookingModal?.classList.add("hidden");
|
|
||||||
};
|
|
||||||
|
|
||||||
const escapeHtml = (value: string) => String(value || "")
|
|
||||||
.replaceAll("&", "&")
|
|
||||||
.replaceAll("<", "<")
|
|
||||||
.replaceAll(">", ">")
|
|
||||||
.replaceAll('"', """)
|
|
||||||
.replaceAll("'", "'");
|
|
||||||
|
|
||||||
const formatDateShort = (dateObj: any) => {
|
|
||||||
const day = String(dateObj.getDate()).padStart(2, "0");
|
|
||||||
const month = String(dateObj.getMonth() + 1).padStart(2, "0");
|
|
||||||
return `${day}.${month}.`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildDayMeta = (offset: number) => {
|
|
||||||
const date = new Date();
|
|
||||||
date.setHours(0, 0, 0, 0);
|
|
||||||
date.setDate(date.getDate() + offset);
|
|
||||||
|
|
||||||
const weekday = weekdayShort[date.getDay()];
|
|
||||||
const formattedDate = formatDateShort(date);
|
|
||||||
|
|
||||||
if (offset === 0) {
|
|
||||||
return {
|
|
||||||
offset,
|
|
||||||
date,
|
|
||||||
short: "Heute",
|
|
||||||
long: `Heute, ${formattedDate}`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (offset === 1) {
|
|
||||||
return {
|
|
||||||
offset,
|
|
||||||
date,
|
|
||||||
short: "Morgen",
|
|
||||||
long: `Morgen, ${formattedDate}`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
offset,
|
|
||||||
date,
|
|
||||||
short: weekday,
|
|
||||||
long: `${weekday}, ${formattedDate}`
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildScheduleForMovie = (movieIndex: number) => {
|
|
||||||
return Array.from({ length: 7 }, (_, dayOffset) => {
|
|
||||||
const dayMeta = buildDayMeta(dayOffset);
|
|
||||||
const pattern = timePatterns[(movieIndex + dayOffset) % timePatterns.length] || "Error reading";
|
|
||||||
const desiredCount = 4 + ((movieIndex + dayOffset) % 2);
|
|
||||||
const showCount = Math.min(pattern.length, desiredCount);
|
|
||||||
|
|
||||||
//@ts-ignore
|
|
||||||
const showings = pattern.slice(0, showCount).map((time: any, slotIndex: number) => { // TODO: fix map issue
|
|
||||||
const hall = hallRotation[(movieIndex + dayOffset + slotIndex) % hallRotation.length];
|
|
||||||
return { time, hall };
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...dayMeta,
|
|
||||||
showings
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildMovieProgram = () => {
|
|
||||||
movieProgram = movieCatalog.map((movie: any, movieIndex: number) => ({
|
|
||||||
...movie,
|
|
||||||
schedule: buildScheduleForMovie(movieIndex)
|
|
||||||
}));
|
|
||||||
heroItems = movieProgram.slice(0, 5);
|
|
||||||
};
|
|
||||||
|
|
||||||
const setHeroSlide = (index: number) => {
|
|
||||||
if (!heroItems.length || !ui.heroSlider) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
heroIndex = (index + heroItems.length) % heroItems.length;
|
|
||||||
|
|
||||||
ui.heroSlider.querySelectorAll(".hero-slide").forEach((slide, slideIndex) => {
|
|
||||||
slide.classList.toggle("active", slideIndex === heroIndex);
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.heroDots?.querySelectorAll(".hero-dot").forEach((dot, dotIndex) => {
|
|
||||||
dot.classList.toggle("active", dotIndex === heroIndex);
|
|
||||||
});
|
|
||||||
|
|
||||||
const activeMovie = heroItems[heroIndex];
|
|
||||||
if (ui.heroTitle) {
|
|
||||||
ui.heroTitle.textContent = activeMovie.title;
|
|
||||||
}
|
|
||||||
if (ui.heroText) {
|
|
||||||
ui.heroText.textContent = `${activeMovie.genre} • ${activeMovie.duration} Min. • Heute erste Vorstellung um 13:00 Uhr.`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderHero = () => {
|
|
||||||
if (!ui.heroSlider || !heroItems.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.heroSlider.innerHTML = heroItems.map((movie: any, index: number) => `
|
|
||||||
<div class="hero-slide ${index === 0 ? "active" : ""}" style="background-image: linear-gradient(118deg, rgba(0,0,0,0.34), rgba(0,0,0,0.04)), url('${escapeHtml(movie.backdrop || movie.poster)}');"></div>
|
|
||||||
`).join("");
|
|
||||||
|
|
||||||
if (ui.heroDots) {
|
|
||||||
ui.heroDots.innerHTML = heroItems.map((_: any, index: number) => `
|
|
||||||
<button type="button" class="hero-dot ${index === 0 ? "active" : ""}" data-hero-index="${index}"></button>
|
|
||||||
`).join("");
|
|
||||||
|
|
||||||
ui.heroDots.addEventListener("click", (event: any) => {
|
|
||||||
const dotTarget = event.target || 0;
|
|
||||||
const dot = dotTarget.closest(".hero-dot");
|
|
||||||
if (!dot) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextIndex = Number(dot.dataset.heroIndex || 0);
|
|
||||||
setHeroSlide(nextIndex);
|
|
||||||
|
|
||||||
if (heroTimer) {
|
|
||||||
clearInterval(heroTimer);
|
|
||||||
heroTimer = setInterval(() => setHeroSlide(heroIndex + 1), 6500);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setHeroSlide(0);
|
|
||||||
|
|
||||||
if (heroTimer) {
|
|
||||||
clearInterval(heroTimer);
|
|
||||||
}
|
|
||||||
|
|
||||||
heroTimer = setInterval(() => {
|
|
||||||
setHeroSlide(heroIndex + 1);
|
|
||||||
}, 6500);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderNowRunningRow = () => {
|
|
||||||
if (!ui.nowRunningRow) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: implement movie interface
|
|
||||||
ui.nowRunningRow.innerHTML = movieProgram.map((movie: any, index: number) => /*html*/`
|
|
||||||
<article class="running-poster">
|
|
||||||
<img src="${escapeHtml(movie.poster)}" alt="${escapeHtml(movie.title)}">
|
|
||||||
<div class="running-meta">
|
|
||||||
<h4>${escapeHtml(movie.title)}</h4>
|
|
||||||
<p>${escapeHtml(movie.genre)}</p>
|
|
||||||
<button type="button" class="open-program-btn" data-program-index="${index}">Spielzeiten ansehen</button>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
`).join("");
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderScheduleRows = (programIndex: number, dayIndex: number) => {
|
|
||||||
const movie = movieProgram[programIndex];
|
|
||||||
if (!movie) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const day = movie.schedule[dayIndex];
|
|
||||||
const body = document.getElementById(`schedule-body-${programIndex}`);
|
|
||||||
if (!body || !day) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.innerHTML = day.showings.map((showing: { hall: string; time: string; }) => /*html*/`
|
|
||||||
<button class="schedule-row time-chip program-time-row" data-movie="${escapeHtml(movie.title)}" data-hall="${escapeHtml(showing.hall)}" data-time="${escapeHtml(showing.time)}">
|
|
||||||
<span>${escapeHtml(day.long)}</span>
|
|
||||||
<span class="hall-pill">${escapeHtml(showing.hall)}</span>
|
|
||||||
<span class="time-btn">${escapeHtml(showing.time)}</span>
|
|
||||||
</button>
|
|
||||||
`).join("");
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderMovieProgramList = () => {
|
|
||||||
if (!ui.movieProgramList) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.movieProgramList.innerHTML = movieProgram.map((movie: { schedule: any[]; poster: string; title: string; fsk: string; duration: any; genre: string; description: string; }, programIndex: any) => {
|
|
||||||
const dayTabs = movie.schedule.map((day, dayIndex) => /*html*/`
|
|
||||||
<button type="button" class="program-day-tab ${dayIndex === 0 ? "active" : ""}" data-program-index="${programIndex}" data-day-index="${dayIndex}">
|
|
||||||
<span>${escapeHtml(day.short)}</span>
|
|
||||||
<small>${escapeHtml(formatDateShort(day.date))}</small>
|
|
||||||
</button>
|
|
||||||
`).join("");
|
|
||||||
|
|
||||||
return /*html*/`
|
|
||||||
<article class="detailed-card program-card reveal-on-scroll" data-program-index="${programIndex}">
|
|
||||||
<div class="card-left">
|
|
||||||
<img src="${escapeHtml(movie.poster)}" alt="${escapeHtml(movie.title)}">
|
|
||||||
<span class="fsk fsk-${escapeHtml(movie.fsk)}">${escapeHtml(movie.fsk)}</span>
|
|
||||||
</div>
|
|
||||||
<div class="card-right">
|
|
||||||
<div class="card-header">
|
|
||||||
<h2>${escapeHtml(movie.title)}</h2>
|
|
||||||
<span class="duration">${movie.duration} Min. | ${escapeHtml(movie.genre)} | FSK: ${escapeHtml(movie.fsk)}</span>
|
|
||||||
</div>
|
|
||||||
<p class="description">${escapeHtml(movie.description)}</p>
|
|
||||||
|
|
||||||
<div class="program-day-tabs">${dayTabs}</div>
|
|
||||||
|
|
||||||
<div class="schedule-container program-schedule-shell">
|
|
||||||
<div class="schedule-header">
|
|
||||||
<span>Tag</span><span>Kinosaal</span><span>Uhrzeit</span>
|
|
||||||
</div>
|
|
||||||
<div id="schedule-body-${programIndex}" class="program-schedule-body"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
`;
|
|
||||||
}).join("");
|
|
||||||
|
|
||||||
movieProgram.forEach((_: any, programIndex: number) => {
|
|
||||||
renderScheduleRows(programIndex, 0);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const initRevealAnimations = () => {
|
|
||||||
const revealElements = Array.from(document.querySelectorAll(".reveal-on-scroll"));
|
|
||||||
if (!revealElements.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!("IntersectionObserver" in window)) {
|
|
||||||
revealElements.forEach((element) => element.classList.add("is-visible"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const observer = new IntersectionObserver((entries, obs) => {
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
if (!entry.isIntersecting) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.target.classList.add("is-visible");
|
|
||||||
obs.unobserve(entry.target);
|
|
||||||
});
|
|
||||||
}, { threshold: 0.2 });
|
|
||||||
|
|
||||||
revealElements.forEach((element) => observer.observe(element));
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderMovieExperience = () => {
|
|
||||||
buildMovieProgram();
|
|
||||||
renderHero();
|
|
||||||
renderNowRunningRow();
|
|
||||||
renderMovieProgramList();
|
|
||||||
initRevealAnimations();
|
|
||||||
};
|
|
||||||
|
|
||||||
const bindNavigation = () => {
|
|
||||||
ui.logo?.addEventListener("click", showHome);
|
|
||||||
|
|
||||||
ui.linkFilme?.addEventListener("click", (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
showMovieList();
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.linkSnacks?.addEventListener("click", (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
if (views.snacks) {
|
|
||||||
showStaticView(views.snacks);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.linkAbout?.addEventListener("click", (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
if (views.about) {
|
|
||||||
showStaticView(views.about);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.linkCart?.addEventListener("click", (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
hideAllViews();
|
|
||||||
views.cart?.classList.remove("hidden");
|
|
||||||
renderCart?.();
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.linkAccount?.addEventListener("click", (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
hideAllViews();
|
|
||||||
views.account?.classList.remove("hidden");
|
|
||||||
|
|
||||||
const isUserLoggedIn = typeof currentUser !== "undefined" && currentUser;
|
|
||||||
if (isUserLoggedIn && typeof openAccountDashboard === "function") {
|
|
||||||
openAccountDashboard();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.heroBookingBtn?.addEventListener("click", () => {
|
|
||||||
showMovieList();
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.checkoutBtn?.addEventListener("click", showCheckoutStart);
|
|
||||||
ui.backHomeBtn?.addEventListener("click", showHome);
|
|
||||||
};
|
|
||||||
|
|
||||||
const bindProgramActions = () => {
|
|
||||||
views.moviesGrid?.addEventListener("click", (event: any) => {
|
|
||||||
const trigger = event.target.closest(".open-program-btn");
|
|
||||||
if (!trigger) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const programIndex = Number(trigger.dataset.programIndex) || 0;
|
|
||||||
showMovieList(programIndex);
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.movieProgramList?.addEventListener("click", (event: any) => {
|
|
||||||
const dayButton = event.target.closest(".program-day-tab");
|
|
||||||
if (!dayButton) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const programIndex = Number(dayButton.dataset.programIndex || 0);
|
|
||||||
const dayIndex = Number(dayButton.dataset.dayIndex || 0);
|
|
||||||
|
|
||||||
const tabRow = dayButton.closest(".program-day-tabs");
|
|
||||||
tabRow?.querySelectorAll(".program-day-tab").forEach((tab: { classList: { remove: (arg0: string) => any; }; }) => tab.classList.remove("active"));
|
|
||||||
dayButton.classList.add("active");
|
|
||||||
|
|
||||||
renderScheduleRows(programIndex, dayIndex);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const bindHomeInfoNavigation = () => {
|
|
||||||
const openButtons = Array.from(document.querySelectorAll("[data-home-view-open]"));
|
|
||||||
const backButtons = Array.from(document.querySelectorAll("[data-go-home]"));
|
|
||||||
const aboutOpenButtons = Array.from(document.querySelectorAll("[data-about-modal-open]"));
|
|
||||||
const aboutCloseButtons = Array.from(document.querySelectorAll("[data-about-modal-close]"));
|
|
||||||
const aboutModal = document.getElementById("about-tech-modal");
|
|
||||||
|
|
||||||
if (!openButtons.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetMap = {
|
|
||||||
"halls-view": views.halls,
|
|
||||||
"dbox-view": views.dbox,
|
|
||||||
"collectors-view": views.collectors
|
|
||||||
};
|
|
||||||
|
|
||||||
openButtons.forEach((button) => {
|
|
||||||
button.addEventListener("click", () => {
|
|
||||||
const targetId = button.getAttribute("data-home-view-open") as keyof typeof targetMap;
|
|
||||||
const target = targetId ? targetMap[targetId] : null;
|
|
||||||
if (target) {
|
|
||||||
showStaticView(target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
backButtons.forEach((button) => {
|
|
||||||
button.addEventListener("click", () => {
|
|
||||||
showHome();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
aboutOpenButtons.forEach((button) => {
|
|
||||||
button.addEventListener("click", () => {
|
|
||||||
const targetId = button.getAttribute("data-about-modal-open");
|
|
||||||
if (targetId === "about-tech-modal" && aboutModal) {
|
|
||||||
aboutModal.classList.remove("hidden");
|
|
||||||
document.body.style.overflow = "hidden";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
aboutCloseButtons.forEach((button) => {
|
|
||||||
button.addEventListener("click", () => {
|
|
||||||
aboutModal?.classList.add("hidden");
|
|
||||||
document.body.style.overflow = "auto";
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
aboutModal?.addEventListener("click", (event) => {
|
|
||||||
if (event.target === aboutModal) {
|
|
||||||
aboutModal.classList.add("hidden");
|
|
||||||
document.body.style.overflow = "auto";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener("keydown", (event) => {
|
|
||||||
if (event.key === "Escape" && aboutModal && !aboutModal.classList.contains("hidden")) {
|
|
||||||
aboutModal.classList.add("hidden");
|
|
||||||
document.body.style.overflow = "auto";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const initThemeToggle = () => {
|
|
||||||
if (!ui.themeToggle) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const THEME_KEY = "eagleTheme";
|
|
||||||
|
|
||||||
const applyTheme = (theme: any) => {
|
|
||||||
const isLight = theme === "light";
|
|
||||||
document.body.classList.toggle("theme-light", isLight);
|
|
||||||
document.body.classList.toggle("theme-dark", !isLight);
|
|
||||||
//@ts-ignore
|
|
||||||
ui.themeToggle.classList.toggle("is-light", isLight);
|
|
||||||
localStorage.setItem(THEME_KEY, isLight ? "light" : "dark");
|
|
||||||
};
|
|
||||||
|
|
||||||
const storedTheme = localStorage.getItem(THEME_KEY);
|
|
||||||
applyTheme(storedTheme === "light" ? "light" : "dark");
|
|
||||||
|
|
||||||
ui.themeToggle.addEventListener("click", () => {
|
|
||||||
const nextTheme = document.body.classList.contains("theme-light") ? "dark" : "light";
|
|
||||||
applyTheme(nextTheme);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const bindAccountActions = () => {
|
|
||||||
const registerModal = document.getElementById("register-modal");
|
|
||||||
const forgotModal = document.getElementById("forgot-modal");
|
|
||||||
const forgotEmailInput = document.getElementById("forgot-email") as HTMLInputElement;
|
|
||||||
const resetMessage = document.getElementById("reset-message");
|
|
||||||
const loginError = document.getElementById("login-error");
|
|
||||||
const loginEmailInput = document.getElementById("login-email");
|
|
||||||
const loginPasswordInput = document.getElementById("login-password");
|
|
||||||
|
|
||||||
const openModal = (modal: HTMLElement | null) => modal?.classList.remove("hidden");
|
|
||||||
const closeModal = (modal: HTMLElement | null) => modal?.classList.add("hidden");
|
|
||||||
const triggerLogin = () => {
|
|
||||||
loginError?.classList.add("hidden");
|
|
||||||
if (typeof loginUser === "function") {
|
|
||||||
loginUser();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.getElementById("btn-open-register")?.addEventListener("click", () => {
|
|
||||||
openModal(registerModal);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById("btn-close-register")?.addEventListener("click", () => {
|
|
||||||
closeModal(registerModal);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById("btn-register-save")?.addEventListener("click", () => {
|
|
||||||
if (typeof registerUser === "function") {
|
|
||||||
registerUser();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById("btn-login-account")?.addEventListener("click", triggerLogin);
|
|
||||||
|
|
||||||
[loginEmailInput, loginPasswordInput].forEach((input) => {
|
|
||||||
input?.addEventListener("keydown", (event) => {
|
|
||||||
if (event.key !== "Enter") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
triggerLogin();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById("btn-forgot-password")?.addEventListener("click", () => {
|
|
||||||
if (forgotEmailInput != null) {
|
|
||||||
forgotEmailInput.value = "";
|
|
||||||
}
|
|
||||||
resetMessage?.classList.add("hidden");
|
|
||||||
openModal(forgotModal);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById("btn-close-forgot")?.addEventListener("click", () => {
|
|
||||||
closeModal(forgotModal);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById("btn-send-reset")?.addEventListener("click", () => {
|
|
||||||
const email = forgotEmailInput?.value.trim() || "";
|
|
||||||
if (!email || !email.includes("@")) {
|
|
||||||
alert("Bitte gib eine gueltige E-Mail-Adresse ein.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resetMessage) {
|
|
||||||
resetMessage.textContent = "Wenn ein Konto existiert, wurde ein Reset-Code simuliert versendet.";
|
|
||||||
resetMessage.classList.remove("hidden");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
registerModal?.addEventListener("click", (event) => {
|
|
||||||
if (event.target === registerModal) {
|
|
||||||
closeModal(registerModal);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
forgotModal?.addEventListener("click", (event) => {
|
|
||||||
if (event.target === forgotModal) {
|
|
||||||
closeModal(forgotModal);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener("keydown", (event) => {
|
|
||||||
if (event.key === "Escape") {
|
|
||||||
closeModal(registerModal);
|
|
||||||
closeModal(forgotModal);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const bindGlobalDocumentClicks = () => {
|
|
||||||
document.addEventListener("click", (event: any) => {
|
|
||||||
if (event.target.classList.contains("opt-btn")) {
|
|
||||||
const optionGroup = event.target.parentElement;
|
|
||||||
optionGroup?.querySelectorAll(".opt-btn").forEach((button: { classList: { remove: (arg0: string) => any; }; }) => button.classList.remove("active"));
|
|
||||||
event.target.classList.add("active");
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteBtn = event.target.closest(".btn-delete-item");
|
|
||||||
if (deleteBtn?.dataset.key) {
|
|
||||||
const row = deleteBtn.closest(".cart-item-row");
|
|
||||||
if (row) {
|
|
||||||
row.classList.add("slide-out-left");
|
|
||||||
row.querySelectorAll("button").forEach((button: { disabled: boolean; }) => {
|
|
||||||
button.disabled = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
//@ts-ignore
|
|
||||||
removeFromCartByKey(deleteBtn.dataset.key); //TODO: removeFromCartByKey doesnt exist
|
|
||||||
}, 380);
|
|
||||||
} else {
|
|
||||||
//@ts-ignore
|
|
||||||
removeFromCartByKey(deleteBtn.dataset.key); //TODO: removeFromCartByKey doesnt exist
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chip = event.target.closest(".time-chip");
|
|
||||||
if (chip) {
|
|
||||||
const movieFromData = chip.getAttribute("data-movie");
|
|
||||||
const movieCard = chip.closest(".movie-card, .detailed-card, .program-card");
|
|
||||||
const movie = movieFromData || movieCard?.querySelector("h2, h3")?.innerText || "Film";
|
|
||||||
const hall = chip.getAttribute("data-hall");
|
|
||||||
const time = chip.getAttribute("data-time");
|
|
||||||
|
|
||||||
if (hall && time && typeof openBooking === "function") {
|
|
||||||
openBooking(movie, hall, time);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const qtyBtn = event.target.closest(".btn-qty");
|
|
||||||
if (!qtyBtn) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const action = qtyBtn.dataset.action;
|
|
||||||
const key = qtyBtn.dataset.key;
|
|
||||||
if (!action || !key) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const relatedItem = cart.find((item: { category: string; seatId: any; hall: any; time: any; title: any; }) => {
|
|
||||||
const infoText = item.category === "movie"
|
|
||||||
? `Sitz: ${item.seatId} (${item.hall})`
|
|
||||||
: item.time;
|
|
||||||
return `${item.title}-${item.hall}-${infoText}` === key;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!relatedItem || relatedItem.category === "movie") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action === "plus") {
|
|
||||||
cart.push({ ...relatedItem, id: Date.now() + Math.random() });
|
|
||||||
} else {
|
|
||||||
const keyList = cart.map((item: { category: string; seatId: any; hall: any; time: any; title: any; }) => {
|
|
||||||
const infoText = item.category === "movie"
|
|
||||||
? `Sitz: ${item.seatId} (${item.hall})`
|
|
||||||
: item.time;
|
|
||||||
return `${item.title}-${item.hall}-${infoText}`;
|
|
||||||
});
|
|
||||||
const lastMatch = keyList.lastIndexOf(key);
|
|
||||||
|
|
||||||
if (lastMatch !== -1) {
|
|
||||||
cart.splice(lastMatch, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
saveCart?.();
|
|
||||||
renderCart?.();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const bindSnacksActions = () => {
|
|
||||||
ui.snacksView?.addEventListener("click", (event: any) => {
|
|
||||||
const sizeChip = event.target.closest(".size-chip");
|
|
||||||
if (!sizeChip) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const snackCard = sizeChip.closest(".snack-card");
|
|
||||||
if (!snackCard) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const snackTitle = snackCard.querySelector("h3, h2")?.innerText || "Snack";
|
|
||||||
const snackImg = snackCard.querySelector("img")?.src || "";
|
|
||||||
const priceSpan = sizeChip.querySelector("span");
|
|
||||||
const rawPriceText = (priceSpan ? priceSpan.innerText : sizeChip.innerText)
|
|
||||||
.replace("EUR", "")
|
|
||||||
.replace("€", "")
|
|
||||||
.replace(",", ".")
|
|
||||||
.trim();
|
|
||||||
const priceVal = parseFloat(rawPriceText) || 0;
|
|
||||||
const sizeVal = sizeChip.innerText.replace(priceSpan?.innerText || "", "").trim() || "Standard";
|
|
||||||
const activeOption = snackCard.querySelector(".opt-btn.active");
|
|
||||||
const variantVal = activeOption ? activeOption.innerText : "Normal";
|
|
||||||
|
|
||||||
cart.push({
|
|
||||||
id: Date.now() + Math.random(),
|
|
||||||
category: "snack",
|
|
||||||
title: snackTitle,
|
|
||||||
hall: sizeVal,
|
|
||||||
time: variantVal,
|
|
||||||
type: "SNACK",
|
|
||||||
price: priceVal,
|
|
||||||
img: snackImg
|
|
||||||
});
|
|
||||||
|
|
||||||
saveCart?.();
|
|
||||||
|
|
||||||
const originalHtml = sizeChip.innerHTML;
|
|
||||||
sizeChip.innerHTML = "Hinzugefügt!";
|
|
||||||
setTimeout(() => {
|
|
||||||
sizeChip.innerHTML = originalHtml;
|
|
||||||
}, 800);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelectorAll(".tab-btn").forEach((button: any) => {
|
|
||||||
button.addEventListener("click", () => {
|
|
||||||
document.querySelectorAll(".tab-btn").forEach((tab) => tab.classList.remove("active"));
|
|
||||||
button.classList.add("active");
|
|
||||||
|
|
||||||
document.querySelectorAll(".snack-category").forEach((category) => category.classList.add("hidden"));
|
|
||||||
document.getElementById(button.dataset.target)?.classList.remove("hidden");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const bindOverlayActions = () => {
|
|
||||||
ui.btnYesSnacks?.addEventListener("click", () => {
|
|
||||||
ui.snackOverlay?.classList.add("hidden");
|
|
||||||
hideAllViews();
|
|
||||||
views.snacks?.classList.remove("hidden");
|
|
||||||
document.body.style.overflow = "auto";
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.btnNoCart?.addEventListener("click", () => {
|
|
||||||
ui.snackOverlay?.classList.add("hidden");
|
|
||||||
hideAllViews();
|
|
||||||
views.cart?.classList.remove("hidden");
|
|
||||||
renderCart?.();
|
|
||||||
document.body.style.overflow = "auto";
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const bindBookingModalClose = () => {
|
|
||||||
ui.closeBookingModalBtn?.addEventListener("click", closeBookingModal);
|
|
||||||
|
|
||||||
ui.bookingModal?.addEventListener("click", (event) => {
|
|
||||||
if (event.target === ui.bookingModal) {
|
|
||||||
closeBookingModal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
// @ts-ignore
|
|
||||||
window.removeFromCartByKey = function removeFromCartByKey(key: string) {
|
|
||||||
cart = cart.filter((item: { category: string; seatId: any; hall: any; time: any; title: any; }) => {
|
|
||||||
const infoText = item.category === "movie"
|
|
||||||
? `Sitz: ${item.seatId} (${item.hall})`
|
|
||||||
: item.time;
|
|
||||||
return `${item.title}-${item.hall}-${infoText}` !== key;
|
|
||||||
});
|
|
||||||
|
|
||||||
saveCart?.();
|
|
||||||
renderCart?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
renderMovieExperience();
|
|
||||||
initThemeToggle();
|
|
||||||
bindNavigation();
|
|
||||||
bindProgramActions();
|
|
||||||
bindHomeInfoNavigation();
|
|
||||||
bindAccountActions();
|
|
||||||
bindGlobalDocumentClicks();
|
|
||||||
bindSnacksActions();
|
|
||||||
bindOverlayActions();
|
|
||||||
bindBookingModalClose();
|
|
||||||
|
|
||||||
updateCartBadge?.();
|
|
||||||
renderCheckout?.();
|
|
||||||
});
|
|
||||||
|
|
||||||
export function emptyCart() {
|
|
||||||
cart = []
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user