Compare commits
9 Commits
40ee49aff5
...
dev_aaron
| Author | SHA1 | Date | |
|---|---|---|---|
| f44be56a8e | |||
| f8fc262290 | |||
| d913d0c145 | |||
| 8327dfb2a2 | |||
| 3653bb8078 | |||
| 10883aa381 | |||
| 33066b0017 | |||
| 252b892cff | |||
| e13f35a9e0 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +0,0 @@
|
|||||||
node_modules
|
|
||||||
dist/
|
|
||||||
760
account.js
Normal file
760
account.js
Normal file
@@ -0,0 +1,760 @@
|
|||||||
|
function readStorageJson(key, fallbackValue) {
|
||||||
|
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) {
|
||||||
|
if (!user || typeof user !== "object") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
salutation: user.salutation || "",
|
||||||
|
firstName: user.firstName || "",
|
||||||
|
lastName: user.lastName || "",
|
||||||
|
birthday: user.birthday || "",
|
||||||
|
street: user.street || "",
|
||||||
|
houseNumber: user.houseNumber || "",
|
||||||
|
zip: user.zip || "",
|
||||||
|
city: user.city || "",
|
||||||
|
country: user.country || "Deutschland",
|
||||||
|
email: user.email || "",
|
||||||
|
passwordHash: user.passwordHash || (user.password ? hashPassword(user.password) : ""),
|
||||||
|
orders: Array.isArray(user.orders) ? user.orders : [],
|
||||||
|
paymentMethods: Array.isArray(user.paymentMethods) ? user.paymentMethods : []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function hashPassword(password) {
|
||||||
|
let hashA = 0xdeadbeef;
|
||||||
|
let hashB = 0x41c6ce57;
|
||||||
|
const text = String(password || "");
|
||||||
|
|
||||||
|
for (let index = 0; index < text.length; index += 1) {
|
||||||
|
const charCode = text.charCodeAt(index);
|
||||||
|
hashA = Math.imul(hashA ^ charCode, 2654435761);
|
||||||
|
hashB = Math.imul(hashB ^ charCode, 1597334677);
|
||||||
|
}
|
||||||
|
|
||||||
|
hashA = Math.imul(hashA ^ (hashA >>> 16), 2246822507) ^ Math.imul(hashB ^ (hashB >>> 13), 3266489909);
|
||||||
|
hashB = Math.imul(hashB ^ (hashB >>> 16), 2246822507) ^ Math.imul(hashA ^ (hashA >>> 13), 3266489909);
|
||||||
|
|
||||||
|
return `hash$${(hashB >>> 0).toString(16).padStart(8, "0")}${(hashA >>> 0).toString(16).padStart(8, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return String(value || "")
|
||||||
|
.replaceAll("&", "&")
|
||||||
|
.replaceAll("<", "<")
|
||||||
|
.replaceAll(">", ">")
|
||||||
|
.replaceAll('"', """)
|
||||||
|
.replaceAll("'", "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatEuro(value) {
|
||||||
|
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);
|
||||||
|
persistUsers();
|
||||||
|
|
||||||
|
let currentUser = normalizeUser(readStorageJson("currentUser", null));
|
||||||
|
if (currentUser && currentUser.email) {
|
||||||
|
const storedMatch = users.find((user) => user.email === currentUser.email);
|
||||||
|
if (storedMatch) {
|
||||||
|
currentUser = storedMatch;
|
||||||
|
persistCurrentUser();
|
||||||
|
} else {
|
||||||
|
users.push(currentUser);
|
||||||
|
persistUsers();
|
||||||
|
persistCurrentUser();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerUser() {
|
||||||
|
if (!validateRegisterForm()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const salutation = document.getElementById("reg-salutation")?.value.trim() || "";
|
||||||
|
const firstName = document.getElementById("reg-firstname")?.value.trim() || "";
|
||||||
|
const lastName = document.getElementById("reg-lastname")?.value.trim() || "";
|
||||||
|
const birthday = document.getElementById("reg-birthday")?.value.trim() || "";
|
||||||
|
const street = document.getElementById("reg-street")?.value.trim() || "";
|
||||||
|
const houseNumber = document.getElementById("reg-house-number")?.value.trim() || "";
|
||||||
|
const zip = document.getElementById("reg-zip")?.value.trim() || "";
|
||||||
|
const city = document.getElementById("reg-city")?.value.trim() || "";
|
||||||
|
const country = document.getElementById("reg-country")?.value.trim() || "";
|
||||||
|
const email = (document.getElementById("reg-email")?.value.trim() || "").toLowerCase();
|
||||||
|
const password = document.getElementById("reg-password")?.value || "";
|
||||||
|
|
||||||
|
const existingUser = users.find((user) => user.email.toLowerCase() === email);
|
||||||
|
if (existingUser) {
|
||||||
|
setFieldError(document.getElementById("reg-email"), "Diese E-Mail ist bereits registriert.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newUser = {
|
||||||
|
salutation,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
birthday,
|
||||||
|
street,
|
||||||
|
houseNumber,
|
||||||
|
zip,
|
||||||
|
city,
|
||||||
|
country,
|
||||||
|
email,
|
||||||
|
passwordHash: hashPassword(password),
|
||||||
|
orders: [],
|
||||||
|
paymentMethods: []
|
||||||
|
};
|
||||||
|
|
||||||
|
users.push(newUser);
|
||||||
|
currentUser = newUser;
|
||||||
|
|
||||||
|
persistUsers();
|
||||||
|
persistCurrentUser();
|
||||||
|
|
||||||
|
document.getElementById("register-modal")?.classList.add("hidden");
|
||||||
|
|
||||||
|
openAccountDashboard();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loginUser() {
|
||||||
|
if (!validateLoginForm()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const email = (document.getElementById("login-email")?.value.trim() || "").toLowerCase();
|
||||||
|
const password = document.getElementById("login-password")?.value || "";
|
||||||
|
const passwordHash = hashPassword(password);
|
||||||
|
|
||||||
|
const user = users.find(
|
||||||
|
(entry) => entry.email.toLowerCase() === email && entry.passwordHash === passwordHash
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
document.getElementById("login-error")?.classList.remove("hidden");
|
||||||
|
setFieldError(document.getElementById("login-email"), "E-Mail oder Passwort stimmen nicht.");
|
||||||
|
setFieldError(document.getElementById("login-password"), "E-Mail oder Passwort stimmen nicht.");
|
||||||
|
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 = `
|
||||||
|
<div class="account-panel">
|
||||||
|
<div class="account-panel-header">
|
||||||
|
<h2>Mein Konto</h2>
|
||||||
|
<div class="account-header-actions">
|
||||||
|
<button class="account-edit-btn" type="button" onclick="renderPersonalInfo(true)" aria-label="Persönliche Daten bearbeiten" title="Daten bearbeiten">
|
||||||
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path d="M12 20h9"></path>
|
||||||
|
<path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="account-logout-btn" onclick="logoutUser()">Abmelden</button>
|
||||||
|
</div>
|
||||||
|
</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(isEditing = false) {
|
||||||
|
const target = document.getElementById("account-tab-content");
|
||||||
|
if (!target || !currentUser) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEditing) {
|
||||||
|
target.innerHTML = `
|
||||||
|
<div class="account-card account-personal-card">
|
||||||
|
<div class="account-card-title-row">
|
||||||
|
<h3>Persönliche Daten bearbeiten</h3>
|
||||||
|
<span>E-Mail bleibt unverändert</span>
|
||||||
|
</div>
|
||||||
|
<div class="account-edit-form">
|
||||||
|
<label class="auth-field">
|
||||||
|
<span>Anrede</span>
|
||||||
|
<select id="account-edit-salutation">
|
||||||
|
<option value="">Bitte wählen</option>
|
||||||
|
<option value="Frau" ${currentUser.salutation === "Frau" ? "selected" : ""}>Frau</option>
|
||||||
|
<option value="Herr" ${currentUser.salutation === "Herr" ? "selected" : ""}>Herr</option>
|
||||||
|
<option value="Divers" ${currentUser.salutation === "Divers" ? "selected" : ""}>Divers</option>
|
||||||
|
</select>
|
||||||
|
<span class="field-error"></span>
|
||||||
|
</label>
|
||||||
|
<label class="auth-field">
|
||||||
|
<span>Vorname</span>
|
||||||
|
<input type="text" id="account-edit-firstname" value="${escapeHtml(currentUser.firstName)}" required data-required-message="Vorname ist ein Pflichtfeld.">
|
||||||
|
<span class="field-error"></span>
|
||||||
|
</label>
|
||||||
|
<label class="auth-field">
|
||||||
|
<span>Nachname</span>
|
||||||
|
<input type="text" id="account-edit-lastname" value="${escapeHtml(currentUser.lastName)}" required data-required-message="Nachname ist ein Pflichtfeld.">
|
||||||
|
<span class="field-error"></span>
|
||||||
|
</label>
|
||||||
|
<label class="auth-field">
|
||||||
|
<span>Geburtstag</span>
|
||||||
|
<input type="date" id="account-edit-birthday" value="${escapeHtml(currentUser.birthday)}">
|
||||||
|
<span class="field-error"></span>
|
||||||
|
</label>
|
||||||
|
<label class="auth-field">
|
||||||
|
<span>Straße</span>
|
||||||
|
<input type="text" id="account-edit-street" value="${escapeHtml(currentUser.street)}">
|
||||||
|
<span class="field-error"></span>
|
||||||
|
</label>
|
||||||
|
<label class="auth-field">
|
||||||
|
<span>Nr.</span>
|
||||||
|
<input type="text" id="account-edit-house-number" value="${escapeHtml(currentUser.houseNumber)}">
|
||||||
|
<span class="field-error"></span>
|
||||||
|
</label>
|
||||||
|
<label class="auth-field">
|
||||||
|
<span>PLZ</span>
|
||||||
|
<input type="text" id="account-edit-zip" value="${escapeHtml(currentUser.zip)}">
|
||||||
|
<span class="field-error"></span>
|
||||||
|
</label>
|
||||||
|
<label class="auth-field">
|
||||||
|
<span>Ort</span>
|
||||||
|
<input type="text" id="account-edit-city" value="${escapeHtml(currentUser.city)}">
|
||||||
|
<span class="field-error"></span>
|
||||||
|
</label>
|
||||||
|
<label class="auth-field">
|
||||||
|
<span>Land</span>
|
||||||
|
<select id="account-edit-country">
|
||||||
|
<option value="Deutschland" ${currentUser.country === "Deutschland" ? "selected" : ""}>Deutschland</option>
|
||||||
|
<option value="Österreich" ${currentUser.country === "Österreich" ? "selected" : ""}>Österreich</option>
|
||||||
|
<option value="Schweiz" ${currentUser.country === "Schweiz" ? "selected" : ""}>Schweiz</option>
|
||||||
|
<option value="Luxemburg" ${currentUser.country === "Luxemburg" ? "selected" : ""}>Luxemburg</option>
|
||||||
|
</select>
|
||||||
|
<span class="field-error"></span>
|
||||||
|
</label>
|
||||||
|
<label class="auth-field account-email-locked">
|
||||||
|
<span>E-Mail</span>
|
||||||
|
<input type="email" value="${escapeHtml(currentUser.email)}" disabled>
|
||||||
|
<small>Die E-Mail-Adresse ist fest mit deinem Konto verbunden.</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="account-edit-actions">
|
||||||
|
<button type="button" class="account-secondary-btn" onclick="renderPersonalInfo()">Abbrechen</button>
|
||||||
|
<button type="button" class="account-save-btn" onclick="savePersonalInfo()">Speichern</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
target.innerHTML = `
|
||||||
|
<div class="account-card account-personal-card">
|
||||||
|
<h3>Persönliche Daten</h3>
|
||||||
|
<div class="account-personal-grid">
|
||||||
|
<p><span>Anrede</span><strong>${escapeHtml(currentUser.salutation || "-")}</strong></p>
|
||||||
|
<p><span>Vorname</span><strong>${escapeHtml(currentUser.firstName || "-")}</strong></p>
|
||||||
|
<p><span>Nachname</span><strong>${escapeHtml(currentUser.lastName || "-")}</strong></p>
|
||||||
|
<p><span>Geburtstag</span><strong>${escapeHtml(currentUser.birthday || "-")}</strong></p>
|
||||||
|
<p><span>Straße</span><strong>${escapeHtml(currentUser.street || "-")}</strong></p>
|
||||||
|
<p><span>Nr.</span><strong>${escapeHtml(currentUser.houseNumber || "-")}</strong></p>
|
||||||
|
<p><span>PLZ</span><strong>${escapeHtml(currentUser.zip || "-")}</strong></p>
|
||||||
|
<p><span>Ort</span><strong>${escapeHtml(currentUser.city || "-")}</strong></p>
|
||||||
|
<p><span>Land</span><strong>${escapeHtml(currentUser.country || "-")}</strong></p>
|
||||||
|
<p><span>E-Mail</span><strong>${escapeHtml(currentUser.email || "-")}</strong></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function savePersonalInfo() {
|
||||||
|
if (!currentUser) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requiredFields = ["account-edit-firstname", "account-edit-lastname"];
|
||||||
|
const isValid = requiredFields
|
||||||
|
.map((id) => validateAccountField(document.getElementById(id)))
|
||||||
|
.every(Boolean);
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentUser.salutation = document.getElementById("account-edit-salutation")?.value || "";
|
||||||
|
currentUser.firstName = document.getElementById("account-edit-firstname")?.value.trim() || "";
|
||||||
|
currentUser.lastName = document.getElementById("account-edit-lastname")?.value.trim() || "";
|
||||||
|
currentUser.birthday = document.getElementById("account-edit-birthday")?.value || "";
|
||||||
|
currentUser.street = document.getElementById("account-edit-street")?.value.trim() || "";
|
||||||
|
currentUser.houseNumber = document.getElementById("account-edit-house-number")?.value.trim() || "";
|
||||||
|
currentUser.zip = document.getElementById("account-edit-zip")?.value.trim() || "";
|
||||||
|
currentUser.city = document.getElementById("account-edit-city")?.value.trim() || "";
|
||||||
|
currentUser.country = document.getElementById("account-edit-country")?.value || "Deutschland";
|
||||||
|
|
||||||
|
const userIndex = users.findIndex((user) => user.email === currentUser.email);
|
||||||
|
if (userIndex >= 0) {
|
||||||
|
users[userIndex] = currentUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
persistUsers();
|
||||||
|
persistCurrentUser();
|
||||||
|
renderPersonalInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFieldWrapper(input) {
|
||||||
|
if (!input) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return input.closest(".auth-field") || document.querySelector(`[data-field-for="${input.id}"]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFieldError(input, message) {
|
||||||
|
const wrapper = getFieldWrapper(input);
|
||||||
|
if (!wrapper) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.dataset.touched = "true";
|
||||||
|
wrapper.classList.add("is-invalid");
|
||||||
|
const error = wrapper.querySelector(".field-error");
|
||||||
|
if (error) {
|
||||||
|
error.textContent = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFieldError(input) {
|
||||||
|
const wrapper = getFieldWrapper(input);
|
||||||
|
if (!wrapper) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper.classList.remove("is-invalid");
|
||||||
|
const error = wrapper.querySelector(".field-error");
|
||||||
|
if (error) {
|
||||||
|
error.textContent = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateAccountField(input) {
|
||||||
|
if (!input) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = (input.value || "").trim();
|
||||||
|
if (input.required && !value) {
|
||||||
|
setFieldError(input, input.dataset.requiredMessage || "Dieses Feld ist ein Pflichtfeld.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.type === "email" && value && !value.includes("@")) {
|
||||||
|
setFieldError(input, input.dataset.emailMessage || "Bitte gib eine gültige E-Mail-Adresse ein.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.type === "password" && value && (value.length < 8 || value.length > 32)) {
|
||||||
|
setFieldError(input, input.dataset.passwordMessage || "Passwort muss 8 bis 32 Zeichen haben.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearFieldError(input);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateRegisterForm() {
|
||||||
|
const fieldIds = [
|
||||||
|
"reg-salutation",
|
||||||
|
"reg-firstname",
|
||||||
|
"reg-lastname",
|
||||||
|
"reg-birthday",
|
||||||
|
"reg-street",
|
||||||
|
"reg-house-number",
|
||||||
|
"reg-zip",
|
||||||
|
"reg-city",
|
||||||
|
"reg-country",
|
||||||
|
"reg-email",
|
||||||
|
"reg-password"
|
||||||
|
];
|
||||||
|
|
||||||
|
return fieldIds
|
||||||
|
.map((id) => validateAccountField(document.getElementById(id)))
|
||||||
|
.every(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateLoginForm() {
|
||||||
|
return ["login-email", "login-password"]
|
||||||
|
.map((id) => validateAccountField(document.getElementById(id)))
|
||||||
|
.every(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initAccountFieldValidation() {
|
||||||
|
const fieldSelector = [
|
||||||
|
"#login-email",
|
||||||
|
"#login-password",
|
||||||
|
"#reg-salutation",
|
||||||
|
"#reg-firstname",
|
||||||
|
"#reg-lastname",
|
||||||
|
"#reg-birthday",
|
||||||
|
"#reg-street",
|
||||||
|
"#reg-house-number",
|
||||||
|
"#reg-zip",
|
||||||
|
"#reg-city",
|
||||||
|
"#reg-country",
|
||||||
|
"#reg-email",
|
||||||
|
"#reg-password"
|
||||||
|
].join(",");
|
||||||
|
|
||||||
|
document.querySelectorAll(fieldSelector).forEach((input) => {
|
||||||
|
input.addEventListener("blur", () => {
|
||||||
|
input.dataset.touched = "true";
|
||||||
|
validateAccountField(input);
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener("input", () => {
|
||||||
|
input.dataset.touched = "true";
|
||||||
|
if (input.type !== "hidden") {
|
||||||
|
validateAccountField(input);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener("change", () => {
|
||||||
|
input.dataset.touched = "true";
|
||||||
|
validateAccountField(input);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll(".gender-selector button").forEach((button) => {
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
const salutationInput = document.getElementById("reg-salutation");
|
||||||
|
if (!salutationInput) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
salutationInput.value = button.dataset.gender || "";
|
||||||
|
salutationInput.dataset.touched = "true";
|
||||||
|
button.closest(".gender-selector")?.querySelectorAll("button").forEach((item) => {
|
||||||
|
item.classList.toggle("active", item === button);
|
||||||
|
});
|
||||||
|
validateAccountField(salutationInput);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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) => 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 genauere Informationen 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(".order-item-btn"));
|
||||||
|
|
||||||
|
const renderOrderTicket = (orderIndex) => {
|
||||||
|
const order = orders[orderIndex];
|
||||||
|
if (!order || !detailTarget) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const movieItems = Array.isArray(order.items)
|
||||||
|
? order.items.filter((item) => item.category === "movie")
|
||||||
|
: [];
|
||||||
|
const primaryMovie = movieItems[0] || (Array.isArray(order.items) ? order.items[0] : null);
|
||||||
|
const poster = primaryMovie?.img || "";
|
||||||
|
const seats = movieItems.map((item) => 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 = `
|
||||||
|
<div class="account-card">
|
||||||
|
<h3>Zahlungsmethoden</h3>
|
||||||
|
<p class="account-payments-note">Hier kannst du deine bevorzugten Zahlungsmethoden verwalten.</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>Gerät 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 verknüpfen</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() {
|
||||||
|
currentUser = null;
|
||||||
|
persistCurrentUser();
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
@@ -1,36 +1,32 @@
|
|||||||
import { seatLayouts, occupiedSeatsData, prices, cart } from "./main"
|
let currentBookingContext = null;
|
||||||
import { renderCart, saveCart } from "./cart";
|
let currentHallLayout = null;
|
||||||
import { renderCheckout } from "./checkout";
|
|
||||||
|
|
||||||
let currentBookingContext: any = null;
|
function openBooking(movie, hall, time) {
|
||||||
let currentHallLayout: any = null;
|
const titleEl = document.getElementById("modal-movie-title");
|
||||||
|
const infoEl = document.getElementById("modal-info-text");
|
||||||
|
|
||||||
export function openBooking(movie: string, hall: string, time: any) {
|
if (titleEl) {
|
||||||
const titleEl = document.getElementById("modal-movie-title");
|
titleEl.innerText = movie;
|
||||||
const infoEl = document.getElementById("modal-info-text");
|
}
|
||||||
|
|
||||||
if (titleEl) {
|
if (infoEl) {
|
||||||
titleEl.innerText = movie;
|
infoEl.innerText = `${hall} • ${time} Uhr`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (infoEl) {
|
currentBookingContext = { movie, hall, time };
|
||||||
infoEl.innerText = `${hall} • ${time} Uhr`;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentBookingContext = { movie, hall, time };
|
createSeats(hall, time);
|
||||||
|
renderBookingLegend();
|
||||||
|
updateBookingSummary();
|
||||||
|
|
||||||
createSeats(hall, time);
|
document.getElementById("booking-modal")?.classList.remove("hidden");
|
||||||
renderBookingLegend();
|
}
|
||||||
updateBookingSummary();
|
|
||||||
|
|
||||||
document.getElementById("booking-modal")?.classList.remove("hidden");
|
function getRowLabel(rowIndex) {
|
||||||
}
|
|
||||||
|
|
||||||
function getRowLabel(rowIndex: number) {
|
|
||||||
return String(rowIndex + 1);
|
return String(rowIndex + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildHallLayout(hallName: string, baseConfig:any) {
|
function buildHallLayout(hallName, baseConfig) {
|
||||||
const rows = Number(baseConfig.rows || 0);
|
const rows = Number(baseConfig.rows || 0);
|
||||||
const totalCols = Number(baseConfig.left || 0) + Number(baseConfig.right || 0);
|
const totalCols = Number(baseConfig.left || 0) + Number(baseConfig.right || 0);
|
||||||
const isDeluxe = /deluxe/i.test(hallName);
|
const isDeluxe = /deluxe/i.test(hallName);
|
||||||
@@ -43,7 +39,7 @@ function buildHallLayout(hallName: string, baseConfig:any) {
|
|||||||
const vipRows = rows > 0 ? [rows] : [];
|
const vipRows = rows > 0 ? [rows] : [];
|
||||||
|
|
||||||
const dboxMap = new Set();
|
const dboxMap = new Set();
|
||||||
const markDboxRange = (rowNumber: number, startCol: number, width: number) => {
|
const markDboxRange = (rowNumber, startCol, width) => {
|
||||||
if (!rowNumber || width <= 0) {
|
if (!rowNumber || width <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -56,7 +52,7 @@ function buildHallLayout(hallName: string, baseConfig:any) {
|
|||||||
|
|
||||||
if (isDeluxe) {
|
if (isDeluxe) {
|
||||||
const configuredDboxSeats = Array.isArray(baseConfig.dbox)
|
const configuredDboxSeats = Array.isArray(baseConfig.dbox)
|
||||||
? baseConfig.dbox.reduce((sum: number, section: any) => sum + Number(section.w || 0), 0)
|
? baseConfig.dbox.reduce((sum, section) => sum + Number(section.w || 0), 0)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
const totalDboxSeats = Math.max(4, configuredDboxSeats || 0);
|
const totalDboxSeats = Math.max(4, configuredDboxSeats || 0);
|
||||||
@@ -81,7 +77,7 @@ function buildHallLayout(hallName: string, baseConfig:any) {
|
|||||||
markDboxRange(rowNumber, startCol, seatsForRow);
|
markDboxRange(rowNumber, startCol, seatsForRow);
|
||||||
});
|
});
|
||||||
} else if (Array.isArray(baseConfig.dbox)) {
|
} else if (Array.isArray(baseConfig.dbox)) {
|
||||||
baseConfig.dbox.forEach((section: any) => {
|
baseConfig.dbox.forEach((section) => {
|
||||||
const rowNumber = Number(section.r || 0);
|
const rowNumber = Number(section.r || 0);
|
||||||
const width = Number(section.w || 0);
|
const width = Number(section.w || 0);
|
||||||
const startCol = Number(section.c || 0);
|
const startCol = Number(section.c || 0);
|
||||||
@@ -100,7 +96,7 @@ function buildHallLayout(hallName: string, baseConfig:any) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSeatType(layout: any, rowNumber: number, colNumber: number) {
|
function getSeatType(layout, rowNumber, colNumber) {
|
||||||
if (layout.dboxMap.has(`${rowNumber}-${colNumber}`)) {
|
if (layout.dboxMap.has(`${rowNumber}-${colNumber}`)) {
|
||||||
return "dbox";
|
return "dbox";
|
||||||
}
|
}
|
||||||
@@ -116,7 +112,7 @@ function getSeatType(layout: any, rowNumber: number, colNumber: number) {
|
|||||||
return "normal";
|
return "normal";
|
||||||
}
|
}
|
||||||
|
|
||||||
function createSeatElement({seatId, seatType, occupiedSeats }:any) {
|
function createSeatElement({ seatId, seatType, occupiedSeats }) {
|
||||||
const seat = document.createElement("button");
|
const seat = document.createElement("button");
|
||||||
seat.type = "button";
|
seat.type = "button";
|
||||||
seat.classList.add("seat", seatType);
|
seat.classList.add("seat", seatType);
|
||||||
@@ -140,7 +136,7 @@ function createSeatElement({seatId, seatType, occupiedSeats }:any) {
|
|||||||
return seat;
|
return seat;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createSeats(hallName: string, time: any) {
|
function createSeats(hallName, time) {
|
||||||
const seatGrid = document.getElementById("seat-grid");
|
const seatGrid = document.getElementById("seat-grid");
|
||||||
if (!seatGrid) {
|
if (!seatGrid) {
|
||||||
return;
|
return;
|
||||||
@@ -148,8 +144,7 @@ function createSeats(hallName: string, time: any) {
|
|||||||
|
|
||||||
seatGrid.innerHTML = "";
|
seatGrid.innerHTML = "";
|
||||||
|
|
||||||
const arrIndex = hallName as keyof typeof seatLayouts;
|
const baseConfig = seatLayouts[hallName];
|
||||||
const baseConfig: any = seatLayouts[arrIndex];
|
|
||||||
if (!baseConfig) {
|
if (!baseConfig) {
|
||||||
currentHallLayout = null;
|
currentHallLayout = null;
|
||||||
return;
|
return;
|
||||||
@@ -240,7 +235,7 @@ function renderBookingLegend() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateBookingSummary() {
|
function updateBookingSummary() {
|
||||||
const selectedSeats = Array.from(document.querySelectorAll("#seat-grid .seat.selected")) as HTMLElement[];;
|
const selectedSeats = Array.from(document.querySelectorAll("#seat-grid .seat.selected"));
|
||||||
const summaryPanel = document.getElementById("booking-summary");
|
const summaryPanel = document.getElementById("booking-summary");
|
||||||
const summaryItems = document.getElementById("summary-items");
|
const summaryItems = document.getElementById("summary-items");
|
||||||
const totalEl = document.getElementById("total-price");
|
const totalEl = document.getElementById("total-price");
|
||||||
@@ -250,7 +245,7 @@ function updateBookingSummary() {
|
|||||||
if (summaryItems) {
|
if (summaryItems) {
|
||||||
summaryItems.innerHTML = selectedSeats
|
summaryItems.innerHTML = selectedSeats
|
||||||
.map((seat) => {
|
.map((seat) => {
|
||||||
const type = (seat.dataset.type || "normal") as keyof typeof prices;
|
const type = seat.dataset.type || "normal";
|
||||||
const seatPrice = Number(prices?.[type] ?? prices?.normal ?? 11);
|
const seatPrice = Number(prices?.[type] ?? prices?.normal ?? 11);
|
||||||
total += seatPrice;
|
total += seatPrice;
|
||||||
|
|
||||||
@@ -277,13 +272,12 @@ function updateBookingSummary() {
|
|||||||
summaryPanel?.classList.toggle("hidden", selectedSeats.length === 0);
|
summaryPanel?.classList.toggle("hidden", selectedSeats.length === 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function findMoviePoster(movieTitle: string) {
|
function findMoviePoster(movieTitle) {
|
||||||
const cards = Array.from(document.querySelectorAll(".movie-card, .detailed-card"));
|
const cards = Array.from(document.querySelectorAll(".movie-card, .detailed-card"));
|
||||||
const normalizedTarget = String(movieTitle || "").trim().toLowerCase();
|
const normalizedTarget = String(movieTitle || "").trim().toLowerCase();
|
||||||
|
|
||||||
for (const card of cards) {
|
for (const card of cards) {
|
||||||
const currentCard = card.querySelector("h2, h3") as HTMLElement;
|
const title = card.querySelector("h2, h3")?.innerText?.trim().toLowerCase();
|
||||||
const title = currentCard.innerText?.trim().toLowerCase();
|
|
||||||
if (title === normalizedTarget) {
|
if (title === normalizedTarget) {
|
||||||
const imageSrc = card.querySelector("img")?.src;
|
const imageSrc = card.querySelector("img")?.src;
|
||||||
if (imageSrc) {
|
if (imageSrc) {
|
||||||
@@ -296,7 +290,7 @@ function findMoviePoster(movieTitle: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function confirmSelectedSeats() {
|
function confirmSelectedSeats() {
|
||||||
const selectedSeats = Array.from(document.querySelectorAll("#seat-grid .seat.selected")) as HTMLElement[];
|
const selectedSeats = Array.from(document.querySelectorAll("#seat-grid .seat.selected"));
|
||||||
|
|
||||||
if (!currentBookingContext || selectedSeats.length === 0) {
|
if (!currentBookingContext || selectedSeats.length === 0) {
|
||||||
alert("Bitte waehle mindestens einen Platz aus.");
|
alert("Bitte waehle mindestens einen Platz aus.");
|
||||||
@@ -310,7 +304,7 @@ function confirmSelectedSeats() {
|
|||||||
const seatId = seat.dataset.seatId;
|
const seatId = seat.dataset.seatId;
|
||||||
const seatType = seat.dataset.type || "normal";
|
const seatType = seat.dataset.type || "normal";
|
||||||
|
|
||||||
const alreadyInCart = cart.some((item: any) =>
|
const alreadyInCart = cart.some((item) =>
|
||||||
item.category === "movie" &&
|
item.category === "movie" &&
|
||||||
item.title === currentBookingContext.movie &&
|
item.title === currentBookingContext.movie &&
|
||||||
item.hall === currentBookingContext.hall &&
|
item.hall === currentBookingContext.hall &&
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
import { cart } from "./main";
|
function formatEuro(value) {
|
||||||
|
|
||||||
function formatEuro(value: number) {
|
|
||||||
return `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`;
|
return `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(value: any) {
|
function escapeHtml(value) {
|
||||||
return String(value || "")
|
return String(value || "")
|
||||||
.replaceAll("&", "&")
|
.replaceAll("&", "&")
|
||||||
.replaceAll("<", "<")
|
.replaceAll("<", "<")
|
||||||
@@ -13,14 +11,14 @@ function escapeHtml(value: any) {
|
|||||||
.replaceAll("'", "'");
|
.replaceAll("'", "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildCartKey(item: { category: string; seatId: any; hall: any; time: any; title: any; }) {
|
function buildCartKey(item) {
|
||||||
const infoText = item.category === "movie"
|
const infoText = item.category === "movie"
|
||||||
? `Sitz: ${item.seatId} (${item.hall})`
|
? `Sitz: ${item.seatId} (${item.hall})`
|
||||||
: item.time;
|
: item.time;
|
||||||
return `${item.title}-${item.hall}-${infoText}`;
|
return `${item.title}-${item.hall}-${infoText}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDrinkItem(item: { category: string; title: any; hall: any; }) {
|
function isDrinkItem(item) {
|
||||||
if (item.category !== "snack") {
|
if (item.category !== "snack") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -41,7 +39,7 @@ function isDrinkItem(item: { category: string; title: any; hall: any; }) {
|
|||||||
return drinkKeywords.some((word) => title.includes(word)) || size.includes("l");
|
return drinkKeywords.some((word) => title.includes(word)) || size.includes("l");
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildItemInfo(item: { category: any; seatId?: any; hall: any; time?: any; title: any; }) {
|
function buildItemInfo(item) {
|
||||||
if (item.category === "movie") {
|
if (item.category === "movie") {
|
||||||
return `
|
return `
|
||||||
<div>Sitzplatz: ${escapeHtml(item.seatId || "-")}</div>
|
<div>Sitzplatz: ${escapeHtml(item.seatId || "-")}</div>
|
||||||
@@ -67,7 +65,7 @@ function buildItemInfo(item: { category: any; seatId?: any; hall: any; time?: an
|
|||||||
function groupCartItems() {
|
function groupCartItems() {
|
||||||
const groups = new Map();
|
const groups = new Map();
|
||||||
|
|
||||||
cart.forEach((item: { price?: any; category: string; seatId: any; hall: any; time: any; title: any; }) => {
|
cart.forEach((item) => {
|
||||||
const key = buildCartKey(item);
|
const key = buildCartKey(item);
|
||||||
|
|
||||||
if (!groups.has(key)) {
|
if (!groups.has(key)) {
|
||||||
@@ -87,12 +85,12 @@ function groupCartItems() {
|
|||||||
return Array.from(groups.values());
|
return Array.from(groups.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveCart() {
|
function saveCart() {
|
||||||
localStorage.setItem("eagleCart", JSON.stringify(cart));
|
localStorage.setItem("eagleCart", JSON.stringify(cart));
|
||||||
updateCartBadge();
|
updateCartBadge();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateCartBadge() {
|
function updateCartBadge() {
|
||||||
const cartBadge = document.getElementById("cart-badge");
|
const cartBadge = document.getElementById("cart-badge");
|
||||||
|
|
||||||
if (!cartBadge) {
|
if (!cartBadge) {
|
||||||
@@ -103,7 +101,7 @@ export function updateCartBadge() {
|
|||||||
cartBadge.classList.toggle("hidden", cart.length === 0);
|
cartBadge.classList.toggle("hidden", cart.length === 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderCart() {
|
function renderCart() {
|
||||||
const cartList = document.getElementById("cart-items-list");
|
const cartList = document.getElementById("cart-items-list");
|
||||||
const totalEl = document.getElementById("cart-total-right");
|
const totalEl = document.getElementById("cart-total-right");
|
||||||
const vatEl = document.getElementById("cart-vat-right");
|
const vatEl = document.getElementById("cart-vat-right");
|
||||||
@@ -121,7 +119,7 @@ export function renderCart() {
|
|||||||
|
|
||||||
const groupedItems = groupCartItems();
|
const groupedItems = groupCartItems();
|
||||||
|
|
||||||
const header = /*html*/`
|
const header = `
|
||||||
<div class="cart-header-row">
|
<div class="cart-header-row">
|
||||||
<div class="col-amount">MENGE</div>
|
<div class="col-amount">MENGE</div>
|
||||||
<div class="col-img">VORSCHAU</div>
|
<div class="col-img">VORSCHAU</div>
|
||||||
@@ -135,11 +133,11 @@ export function renderCart() {
|
|||||||
const rows = groupedItems
|
const rows = groupedItems
|
||||||
.map((group) => {
|
.map((group) => {
|
||||||
const imageHtml = group.item.img
|
const imageHtml = group.item.img
|
||||||
? /*html*/`<img class="cart-img-small" src="${escapeHtml(group.item.img)}" alt="${escapeHtml(group.item.title)}">`
|
? `<img class="cart-img-small" src="${escapeHtml(group.item.img)}" alt="${escapeHtml(group.item.title)}">`
|
||||||
: /*html*/`<div class="cart-img-fallback">Kein Bild</div>`;
|
: `<div class="cart-img-fallback">Kein Bild</div>`;
|
||||||
const quantityHtml = group.item.category === "movie"
|
const quantityHtml = group.item.category === "movie"
|
||||||
? /*html*/`<div class="qty-static" aria-label="Feste Ticketanzahl">${group.quantity}x</div>`
|
? `<div class="qty-static" aria-label="Feste Ticketanzahl">${group.quantity}x</div>`
|
||||||
: /*html*/`
|
: `
|
||||||
<div class="qty-stepper">
|
<div class="qty-stepper">
|
||||||
<button class="btn-qty" data-action="minus" data-key="${escapeHtml(group.key)}">-</button>
|
<button class="btn-qty" data-action="minus" data-key="${escapeHtml(group.key)}">-</button>
|
||||||
<span>${group.quantity}</span>
|
<span>${group.quantity}</span>
|
||||||
@@ -147,7 +145,7 @@ export function renderCart() {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return /*html*/`
|
return `
|
||||||
<div class="cart-item-row">
|
<div class="cart-item-row">
|
||||||
<div class="col-amount">
|
<div class="col-amount">
|
||||||
${quantityHtml}
|
${quantityHtml}
|
||||||
@@ -174,23 +172,22 @@ export function renderCart() {
|
|||||||
|
|
||||||
saveCart();
|
saveCart();
|
||||||
}
|
}
|
||||||
//@ts-ignore
|
|
||||||
window.removeItem = function removeItem(id: any) {
|
window.removeItem = function removeItem(id) {
|
||||||
var localCart = cart.filter((item: { id: any; }) => item.id !== id);
|
cart = cart.filter((item) => item.id !== id);
|
||||||
saveCart();
|
saveCart();
|
||||||
renderCart();
|
renderCart();
|
||||||
};
|
};
|
||||||
|
|
||||||
//@ts-ignore
|
window.changeQty = function changeQty(title, delta) {
|
||||||
window.changeQty = function changeQty(title, delta): void {
|
|
||||||
if (delta > 0) {
|
if (delta > 0) {
|
||||||
const item = cart.find((entry: { title: any; }) => entry.title === title);
|
const item = cart.find((entry) => entry.title === title);
|
||||||
if (item) {
|
if (item) {
|
||||||
cart.push({ ...item, id: Date.now() + Math.random() });
|
cart.push({ ...item, id: Date.now() + Math.random() });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const index = cart
|
const index = cart
|
||||||
.map((entry: { title: any; }) => entry.title)
|
.map((entry) => entry.title)
|
||||||
.lastIndexOf(title);
|
.lastIndexOf(title);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
cart.splice(index, 1);
|
cart.splice(index, 1);
|
||||||
@@ -200,4 +197,3 @@ window.changeQty = function changeQty(title, delta): void {
|
|||||||
saveCart();
|
saveCart();
|
||||||
renderCart();
|
renderCart();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1,15 +1,11 @@
|
|||||||
import { currentUser, users } from "./account";
|
function formatCheckoutEuro(value) {
|
||||||
import { renderCart, saveCart } from "./cart";
|
|
||||||
import { cart, emptyCart, occupiedSeatsData } from "./main";
|
|
||||||
|
|
||||||
function formatCheckoutEuro(value: number) {
|
|
||||||
return `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`;
|
return `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`;
|
||||||
}
|
}
|
||||||
|
|
||||||
let selectedPaymentMethod = "";
|
let selectedPaymentMethod = "";
|
||||||
let checkoutEventsBound = false;
|
let checkoutEventsBound = false;
|
||||||
|
|
||||||
function setCheckoutStep(step: number) {
|
function setCheckoutStep(step) {
|
||||||
const step1 = document.getElementById("checkout-step-1");
|
const step1 = document.getElementById("checkout-step-1");
|
||||||
const step2 = document.getElementById("checkout-step-2");
|
const step2 = document.getElementById("checkout-step-2");
|
||||||
const step3 = document.getElementById("checkout-step-3");
|
const step3 = document.getElementById("checkout-step-3");
|
||||||
@@ -31,7 +27,7 @@ function setCheckoutStep(step: number) {
|
|||||||
line2?.classList.toggle("active", step >= 3);
|
line2?.classList.toggle("active", step >= 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderCheckout() {
|
function renderCheckout() {
|
||||||
const summaryList = document.getElementById("checkout-summary-list");
|
const summaryList = document.getElementById("checkout-summary-list");
|
||||||
const totalDisplay = document.getElementById("checkout-total-display");
|
const totalDisplay = document.getElementById("checkout-total-display");
|
||||||
const vatDisplay = document.getElementById("checkout-vat-display");
|
const vatDisplay = document.getElementById("checkout-vat-display");
|
||||||
@@ -97,7 +93,7 @@ function generateTicket() {
|
|||||||
const qrData = encodeURIComponent(`EAGLE-IMAX|${mainMovie.title}|${mainMovie.hall}|${matchingMovieSeats}`);
|
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`;
|
const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=${qrData}&bgcolor=ffffff`;
|
||||||
|
|
||||||
ticketContainer.innerHTML = /*html*/`
|
ticketContainer.innerHTML = `
|
||||||
<div class="luxury-ticket">
|
<div class="luxury-ticket">
|
||||||
<div class="ticket-left">
|
<div class="ticket-left">
|
||||||
<img src="${mainMovie.img}" class="ticket-poster" alt="${mainMovie.title}">
|
<img src="${mainMovie.img}" class="ticket-poster" alt="${mainMovie.title}">
|
||||||
@@ -119,7 +115,7 @@ function generateTicket() {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveOrderForCurrentUser(orderItems: any[], orderTotal: any) {
|
function saveOrderForCurrentUser(orderItems, orderTotal) {
|
||||||
if (typeof currentUser === "undefined" || !currentUser) {
|
if (typeof currentUser === "undefined" || !currentUser) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -135,7 +131,6 @@ function saveOrderForCurrentUser(orderItems: any[], orderTotal: any) {
|
|||||||
paymentMethod: selectedPaymentMethod || "-"
|
paymentMethod: selectedPaymentMethod || "-"
|
||||||
};
|
};
|
||||||
|
|
||||||
//@ts-ignore
|
|
||||||
const userIndex = users.findIndex((entry) => entry.email === currentUser.email);
|
const userIndex = users.findIndex((entry) => entry.email === currentUser.email);
|
||||||
if (userIndex === -1) {
|
if (userIndex === -1) {
|
||||||
return;
|
return;
|
||||||
@@ -149,7 +144,7 @@ function saveOrderForCurrentUser(orderItems: any[], orderTotal: any) {
|
|||||||
localStorage.setItem("eagleUsers", JSON.stringify(users));
|
localStorage.setItem("eagleUsers", JSON.stringify(users));
|
||||||
}
|
}
|
||||||
|
|
||||||
function reserveSeatsAfterPayment(orderItems: any[]) {
|
function reserveSeatsAfterPayment(orderItems) {
|
||||||
const movieItems = orderItems.filter((item) => item.category === "movie");
|
const movieItems = orderItems.filter((item) => item.category === "movie");
|
||||||
|
|
||||||
movieItems.forEach((item) => {
|
movieItems.forEach((item) => {
|
||||||
@@ -171,7 +166,7 @@ function completeCheckout() {
|
|||||||
saveOrderForCurrentUser(orderItems, orderTotal);
|
saveOrderForCurrentUser(orderItems, orderTotal);
|
||||||
reserveSeatsAfterPayment(orderItems);
|
reserveSeatsAfterPayment(orderItems);
|
||||||
|
|
||||||
emptyCart?.()
|
cart = [];
|
||||||
saveCart?.();
|
saveCart?.();
|
||||||
renderCart?.();
|
renderCart?.();
|
||||||
}
|
}
|
||||||
@@ -185,7 +180,7 @@ function bindCheckoutEvents() {
|
|||||||
|
|
||||||
const nextButton = document.getElementById("btn-next-step-2");
|
const nextButton = document.getElementById("btn-next-step-2");
|
||||||
const backButton = document.getElementById("btn-back-to-step1");
|
const backButton = document.getElementById("btn-back-to-step1");
|
||||||
const payNowButton = document.getElementById("btn-pay-now") as HTMLButtonElement;
|
const payNowButton = document.getElementById("btn-pay-now");
|
||||||
|
|
||||||
document.querySelectorAll(".payment-method").forEach((method) => {
|
document.querySelectorAll(".payment-method").forEach((method) => {
|
||||||
method.addEventListener("click", () => {
|
method.addEventListener("click", () => {
|
||||||
@@ -194,7 +189,6 @@ function bindCheckoutEvents() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
method.classList.add("selected");
|
method.classList.add("selected");
|
||||||
//@ts-ignore
|
|
||||||
selectedPaymentMethod = method.dataset.method || "";
|
selectedPaymentMethod = method.dataset.method || "";
|
||||||
nextButton?.classList.remove("hidden");
|
nextButton?.classList.remove("hidden");
|
||||||
});
|
});
|
||||||
BIN
img/astronaut-rockypopcorn.jpg
LFS
Normal file
BIN
img/astronaut-rockypopcorn.jpg
LFS
Normal file
Binary file not shown.
BIN
img/cashtruck.jpg
LFS
Normal file
BIN
img/cashtruck.jpg
LFS
Normal file
Binary file not shown.
BIN
img/fsk-0.png
LFS
Normal file
BIN
img/fsk-0.png
LFS
Normal file
Binary file not shown.
BIN
img/fsk-12.png
LFS
Normal file
BIN
img/fsk-12.png
LFS
Normal file
Binary file not shown.
BIN
img/fsk-16.png
LFS
Normal file
BIN
img/fsk-16.png
LFS
Normal file
Binary file not shown.
BIN
img/fsk-18.png
LFS
Normal file
BIN
img/fsk-18.png
LFS
Normal file
Binary file not shown.
BIN
img/fsk-6.png
LFS
Normal file
BIN
img/fsk-6.png
LFS
Normal file
Binary file not shown.
BIN
img/nachokombigross.png
LFS
Normal file
BIN
img/nachokombigross.png
LFS
Normal file
Binary file not shown.
BIN
img/nachokombimittel.png
LFS
Normal file
BIN
img/nachokombimittel.png
LFS
Normal file
Binary file not shown.
BIN
img/popcornkombigross.png
LFS
Normal file
BIN
img/popcornkombigross.png
LFS
Normal file
Binary file not shown.
BIN
img/popcornkombiklein.png
LFS
Normal file
BIN
img/popcornkombiklein.png
LFS
Normal file
Binary file not shown.
BIN
img/popcornkombimittel.png
LFS
Normal file
BIN
img/popcornkombimittel.png
LFS
Normal file
Binary file not shown.
BIN
img/screammetalpopcorn.png
LFS
Normal file
BIN
img/screammetalpopcorn.png
LFS
Normal file
Binary file not shown.
BIN
img/youtube-logo.png
LFS
Normal file
BIN
img/youtube-logo.png
LFS
Normal file
Binary file not shown.
BIN
img/zoomania-2-logo.png
LFS
Normal file
BIN
img/zoomania-2-logo.png
LFS
Normal file
Binary file not shown.
827
index.html
827
index.html
@@ -51,7 +51,14 @@
|
|||||||
<h3>Jetzt läuft</h3>
|
<h3>Jetzt läuft</h3>
|
||||||
<span>Heute im Fokus</span>
|
<span>Heute im Fokus</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="now-running-row" class="now-running-row"></div>
|
<div id="now-running-shell" class="now-running-shell is-collapsed">
|
||||||
|
<div id="now-running-row" class="now-running-row"></div>
|
||||||
|
<div class="now-running-fade">
|
||||||
|
<button id="now-running-toggle" class="now-running-toggle" type="button" aria-expanded="false" aria-label="Weitere Filme anzeigen">
|
||||||
|
<span>></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="home-inline-showcase reveal-on-scroll">
|
<div class="home-inline-showcase reveal-on-scroll">
|
||||||
@@ -267,15 +274,238 @@
|
|||||||
<h1 class="list-title">Snacks & Getränke</h1>
|
<h1 class="list-title">Snacks & Getränke</h1>
|
||||||
|
|
||||||
<div class="category-tabs">
|
<div class="category-tabs">
|
||||||
<button class="tab-btn active" data-target="cat-getraenke">Getränke</button>
|
<button class="tab-btn active" data-target="cat-limited">Limitierte Specials</button>
|
||||||
|
<button class="tab-btn" data-target="cat-getraenke">Getränke</button>
|
||||||
<button class="tab-btn" data-target="cat-popcorn">Popcorn</button>
|
<button class="tab-btn" data-target="cat-popcorn">Popcorn</button>
|
||||||
<button class="tab-btn" data-target="cat-nachos">Nachos</button>
|
<button class="tab-btn" data-target="cat-nachos">Nachos</button>
|
||||||
<button class="tab-btn" data-target="cat-snacks">Snacks</button>
|
<button class="tab-btn" data-target="cat-snacks">Snacks</button>
|
||||||
<button class="tab-btn" data-target="cat-kombi">Kombi</button>
|
|
||||||
<button class="tab-btn" data-target="cat-eis">Eis</button>
|
<button class="tab-btn" data-target="cat-eis">Eis</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="cat-getraenke" class="snack-category active">
|
<div id="cat-limited" class="snack-category active limited-specials-category">
|
||||||
|
<div class="limited-specials-hero">
|
||||||
|
<div>
|
||||||
|
<span class="limited-kicker">Nur für kurze Zeit</span>
|
||||||
|
<h2>Limitierte Specials</h2>
|
||||||
|
<p>Filmbecher, Sammler-Eimer und Kids-Menüs als kleine Vitrine für besondere Aktionen.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="limited-special-block">
|
||||||
|
<div class="special-film-heading">
|
||||||
|
<img src="img/Zoomania-2.jpg" alt="Zoomania 2 Logo">
|
||||||
|
<div>
|
||||||
|
<span>Zoomania 2</span>
|
||||||
|
<h2>Tiereische Collection</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="limited-special-grid">
|
||||||
|
<div class="snack-card limited-special-card">
|
||||||
|
<div class="snack-img"><img src="img/zoomania-popcorn.jpg" alt="Zoomania 2 Metallbecher"></div>
|
||||||
|
<div class="snack-info">
|
||||||
|
<span class="badge">Metallbecher</span>
|
||||||
|
<h3>Limitierter Metallbecher - Zoomania 2</h3>
|
||||||
|
<p class="snack-card-note">Metall Sammelbecher im Zoomania 2 Design.</p>
|
||||||
|
<div class="option-group">
|
||||||
|
<button class="opt-btn active">Süß</button>
|
||||||
|
<button class="opt-btn">Salzig</button>
|
||||||
|
</div>
|
||||||
|
<div class="size-selector">
|
||||||
|
<button class="size-chip">Special <span>12,00€</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="snack-card limited-special-card wide-special">
|
||||||
|
<div class="snack-img"><img src="img/zoomaniakidsmenu.jpg" alt="Zoomania Kids Menu"></div>
|
||||||
|
<div class="snack-info">
|
||||||
|
<span class="badge">Kids Special</span>
|
||||||
|
<h3>Zoomania Kids Menü</h3>
|
||||||
|
<p class="snack-card-note">0,5L Getränk im Zoomania Becher + Zoomania Popcorn Schale + Figur zum Aussuchen.</p>
|
||||||
|
<div class="size-selector">
|
||||||
|
<button class="size-chip">Menü-Preis <span>10,00€</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="limited-special-block">
|
||||||
|
<div class="special-film-heading">
|
||||||
|
<img src="img/screamvii.jpg" alt="Scream VII Logo">
|
||||||
|
<div>
|
||||||
|
<span>Scream VII</span>
|
||||||
|
<h2>Horror Collection</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="limited-special-grid">
|
||||||
|
<div class="snack-card limited-special-card">
|
||||||
|
<div class="snack-img"><img src="img/screamdoorpopcorn.jpg" alt="Scream VII Sammelbecher"></div>
|
||||||
|
<div class="snack-info">
|
||||||
|
<span class="badge">Collector</span>
|
||||||
|
<h3>Limitierter Sammelbecher - Scream VII</h3>
|
||||||
|
<p class="snack-card-note">Hallo Sydney! Ghostface ist an der Tür.</p>
|
||||||
|
<div class="option-group">
|
||||||
|
<button class="opt-btn active">Süß</button>
|
||||||
|
<button class="opt-btn">Salzig</button>
|
||||||
|
</div>
|
||||||
|
<div class="size-selector">
|
||||||
|
<button class="size-chip">Special <span>29,00€</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="snack-card limited-special-card">
|
||||||
|
<div class="snack-img"><img src="img/screammetalpopcorn.png" alt="Scream VII Sammelbecher"></div>
|
||||||
|
<div class="snack-info">
|
||||||
|
<span class="badge">Collector</span>
|
||||||
|
<h3>Limitierter Metallbecher - Scream VII</h3>
|
||||||
|
<p class="snack-card-note">Metall Sammelbecher im SCREAM VII Design</p>
|
||||||
|
<div class="option-group">
|
||||||
|
<button class="opt-btn active">Süß</button>
|
||||||
|
<button class="opt-btn">Salzig</button>
|
||||||
|
</div>
|
||||||
|
<div class="size-selector">
|
||||||
|
<button class="size-chip">Special <span>12,00€</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="limited-special-block">
|
||||||
|
<div class="special-film-heading">
|
||||||
|
<img src="img/derAustronaut.jpg" alt="Der Austronaut Logo">
|
||||||
|
<div>
|
||||||
|
<span>Project Hail Mary</span>
|
||||||
|
<h2>Space Collection</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="limited-special-grid">
|
||||||
|
<div class="snack-card limited-special-card">
|
||||||
|
<div class="snack-img"><img src="img/astronautpopcorn.jpg" alt="Der Austronaut Sammelbecher"></div>
|
||||||
|
<div class="snack-info">
|
||||||
|
<span class="badge">Space Cup</span>
|
||||||
|
<h3>Limitierter Sammelbecher - Der Austronaut</h3>
|
||||||
|
<p class="snack-card-note">Der Helm von Ryland Grace aus "Der Austronaut"</p>
|
||||||
|
<div class="option-group">
|
||||||
|
<button class="opt-btn active">Süß</button>
|
||||||
|
<button class="opt-btn">Salzig</button>
|
||||||
|
</div>
|
||||||
|
<div class="size-selector">
|
||||||
|
<button class="size-chip">Special <span>34,00€</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="snack-card limited-special-card">
|
||||||
|
<div class="snack-img"><img src="img/astronaut-rockypopcorn.jpg" alt="Der Austronaut - Rocky"></div>
|
||||||
|
<div class="snack-info">
|
||||||
|
<span class="badge">Collector</span>
|
||||||
|
<h3>Limitierter Sammelbecher - Der Austronaut</h3>
|
||||||
|
<p class="snack-card-note">Die Kapsel von Rocky - Mit abnehmbarer Rocky Figur</p>
|
||||||
|
<div class="option-group">
|
||||||
|
<button class="opt-btn active">Süß</button>
|
||||||
|
<button class="opt-btn">Salzig</button>
|
||||||
|
</div>
|
||||||
|
<div class="size-selector">
|
||||||
|
<button class="size-chip">Special <span>22,00€</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="limited-special-block">
|
||||||
|
<div class="special-film-heading">
|
||||||
|
<img src="img/hoppers.jpg" alt="Hoppers Logo">
|
||||||
|
<div>
|
||||||
|
<span>Hoppers</span>
|
||||||
|
<h2>Biber Specials (Ist das eine Eidechse?)</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="limited-special-grid">
|
||||||
|
<div class="snack-card limited-special-card">
|
||||||
|
<div class="snack-img"><img src="img/hopperspopcornmetall.jpg" alt="Hoppers Metallbecher"></div>
|
||||||
|
<div class="snack-info">
|
||||||
|
<span class="badge">Metallbecher</span>
|
||||||
|
<h3>Limitierter Metallbecher - Hoppers</h3>
|
||||||
|
<p class="snack-card-note">Metall Sammelbecher im HOPPERS Design.</p>
|
||||||
|
<div class="option-group">
|
||||||
|
<button class="opt-btn active">Süß</button>
|
||||||
|
<button class="opt-btn">Salzig</button>
|
||||||
|
</div>
|
||||||
|
<div class="size-selector">
|
||||||
|
<button class="size-chip">Special <span>12,00€</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="snack-card limited-special-card">
|
||||||
|
<div class="snack-img"><img src="img/hopperspopcornwood.png" alt="Hoppers Sammelbecher"></div>
|
||||||
|
<div class="snack-info">
|
||||||
|
<span class="badge">Collector</span>
|
||||||
|
<h3>Limitierter Sammelbecher - Hoppers</h3>
|
||||||
|
<p class="snack-card-note">Limitierter Sammelbecher als Biberdamm - Mit verteckter Eidechse.</p>
|
||||||
|
<div class="option-group">
|
||||||
|
<button class="opt-btn active">Süß</button>
|
||||||
|
<button class="opt-btn">Salzig</button>
|
||||||
|
</div>
|
||||||
|
<div class="size-selector">
|
||||||
|
<button class="size-chip">Special <span>21,00€</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="snack-card limited-special-card wide-special">
|
||||||
|
<div class="snack-img"><img src="img/hopperskidsmenu.jpg" alt="Hoppers Kids Menu"></div>
|
||||||
|
<div class="snack-info">
|
||||||
|
<span class="badge">Kids Special</span>
|
||||||
|
<h3>Hoppers Kids Menü</h3>
|
||||||
|
<p class="snack-card-note">0,5L Getränk im Hoppers Becher + Hoppers Popcorn Schale + Hoppers Figur.</p>
|
||||||
|
<div class="size-selector">
|
||||||
|
<button class="size-chip">Menü-Preis <span>10,00€</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="limited-special-block">
|
||||||
|
<div class="special-film-heading">
|
||||||
|
<img src="img/mariogalaxy.jpg" alt="Mario Galaxy Logo">
|
||||||
|
<div>
|
||||||
|
<span>Super Mario Galaxy</span>
|
||||||
|
<h2>Galaxy Collection</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="limited-special-grid">
|
||||||
|
<div class="snack-card limited-special-card">
|
||||||
|
<div class="snack-img"><img src="img/marioyoshipopcorn.png" alt="Yoshi Sammelbecher"></div>
|
||||||
|
<div class="snack-info">
|
||||||
|
<span class="badge">Yoshi Cup</span>
|
||||||
|
<h3>Limitierter Sammelbecher - Yoshi Becher</h3>
|
||||||
|
<p class="snack-card-note">Limitierter Sammelbecher in der Form von Yoshi.</p>
|
||||||
|
<div class="option-group">
|
||||||
|
<button class="opt-btn active">Süß</button>
|
||||||
|
<button class="opt-btn">Salzig</button>
|
||||||
|
</div>
|
||||||
|
<div class="size-selector">
|
||||||
|
<button class="size-chip">Special <span>35,90€</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="snack-card limited-special-card wide-special">
|
||||||
|
<div class="snack-img"><img src="img/mariokidsmenu.png" alt="Mario Kids Menu"></div>
|
||||||
|
<div class="snack-info">
|
||||||
|
<span class="badge">Kids Special</span>
|
||||||
|
<h3>Mario Galaxy Kids Menü</h3>
|
||||||
|
<p class="snack-card-note">0,5L Getränk im Mario Galaxy Becher + Mario Galaxy Popcorn Schale.</p>
|
||||||
|
<div class="size-selector">
|
||||||
|
<button class="size-chip">Menü-Preis <span>10,00€</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="cat-getraenke" class="snack-category hidden">
|
||||||
<div class="snack-grid">
|
<div class="snack-grid">
|
||||||
|
|
||||||
<div class="snack-card">
|
<div class="snack-card">
|
||||||
@@ -392,229 +622,239 @@
|
|||||||
</div> </div>
|
</div> </div>
|
||||||
|
|
||||||
<div id="cat-popcorn" class="snack-category hidden">
|
<div id="cat-popcorn" class="snack-category hidden">
|
||||||
<div class="snack-grid">
|
<div class="snack-subsection">
|
||||||
<div class="snack-card">
|
<div class="snack-section-heading">
|
||||||
<div class="snack-img"><img src="img/popcorn-klein.png" alt="Popcorn klein"></div>
|
<span>Frisch gepoppt</span>
|
||||||
<div class="snack-info">
|
<h2>Einzelprodukte</h2>
|
||||||
<h3>Popcorn klein</h3>
|
</div>
|
||||||
<div class="option-group">
|
<div class="snack-grid">
|
||||||
<button class="opt-btn active">Süß</button>
|
<div class="snack-card">
|
||||||
<button class="opt-btn">Salzig</button>
|
<div class="snack-img"><img src="img/popcorn-klein.png" alt="Popcorn klein"></div>
|
||||||
|
<div class="snack-info">
|
||||||
|
<h3>Popcorn klein</h3>
|
||||||
|
<div class="option-group">
|
||||||
|
<button class="opt-btn active">Süß</button>
|
||||||
|
<button class="opt-btn">Salzig</button>
|
||||||
|
</div>
|
||||||
|
<div class="size-selector">
|
||||||
|
<button class="size-chip">Klein <span>3,50€</span></button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="size-selector">
|
</div>
|
||||||
<button class="size-chip">3,50€</button>
|
<div class="snack-card">
|
||||||
|
<div class="snack-img"><img src="img/popcorn-mittel.png" alt="Popcorn mittel"></div>
|
||||||
|
<div class="snack-info">
|
||||||
|
<h3>Popcorn Mittel</h3>
|
||||||
|
<div class="option-group">
|
||||||
|
<button class="opt-btn active">Süß</button>
|
||||||
|
<button class="opt-btn">Salzig</button>
|
||||||
|
</div>
|
||||||
|
<div class="size-selector">
|
||||||
|
<button class="size-chip">Mittel <span>4,50€</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="snack-card">
|
||||||
|
<div class="snack-img"><img src="img/popcorn-big.png" alt="Popcorn groß"></div>
|
||||||
|
<div class="snack-info">
|
||||||
|
<h3>Popcorn Groß</h3>
|
||||||
|
<div class="option-group">
|
||||||
|
<button class="opt-btn active">Süß</button>
|
||||||
|
<button class="opt-btn">Salzig</button>
|
||||||
|
</div>
|
||||||
|
<div class="size-selector">
|
||||||
|
<button class="size-chip">Groß <span>6,00€</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="snack-subsection">
|
||||||
|
<div class="snack-section-heading popcorn-combo-heading">
|
||||||
|
<span>Für Filmabende</span>
|
||||||
|
<h2>Kombi Menü</h2>
|
||||||
|
</div>
|
||||||
|
<div class="snack-grid">
|
||||||
|
<div class="snack-card highlight">
|
||||||
|
<div class="snack-img"><img src="img/popcornkombiklein.png" alt="Popcorn klein - Kombi-Menü"></div>
|
||||||
|
<div class="snack-info">
|
||||||
|
<h3>Kleines Menü</h3>
|
||||||
|
<p class="snack-card-note">0,33L Getränk + Popcorn klein</p>
|
||||||
|
<div class="size-selector">
|
||||||
|
<button class="size-chip">Menü-Preis <span>5,00€</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="snack-card highlight">
|
||||||
|
<div class="snack-img"><img src="img/popcornkombimittel.png" alt="Popcorn mittel - Kombi-Menü"></div>
|
||||||
|
<div class="snack-info">
|
||||||
|
<h3>Mittleres Menü</h3>
|
||||||
|
<p class="snack-card-note">0,5L Getränk + Popcorn mittel</p>
|
||||||
|
<div class="size-selector">
|
||||||
|
<button class="size-chip">Menü-Preis <span>6,50€</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="snack-card highlight">
|
||||||
|
<div class="snack-img"><img src="img/popcornkombigross.png" alt="Popcorn groß - Kombi-Menü"></div>
|
||||||
|
<div class="snack-info">
|
||||||
|
<span class="badge">Bestseller</span>
|
||||||
|
<h3>Großes Menü</h3>
|
||||||
|
<p class="snack-card-note">1L Getränk + Popcorn groß</p>
|
||||||
|
<div class="size-selector">
|
||||||
|
<button class="size-chip">Menü-Preis <span>8,00€</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="snack-card">
|
|
||||||
<div class="snack-img"><img src="img/popcorn-mittel.png" alt="Popcorn mittel"></div>
|
|
||||||
<div class="snack-info">
|
|
||||||
<h3>Popcorn Mittel</h3>
|
|
||||||
<div class="option-group">
|
|
||||||
<button class="opt-btn active">Süß</button>
|
|
||||||
<button class="opt-btn">Salzig</button>
|
|
||||||
</div>
|
|
||||||
<div class="size-selector">
|
|
||||||
<button class="size-chip">4,50€</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="snack-card">
|
|
||||||
<div class="snack-img"><img src="img/popcorn-big.png" alt="Popcorn groß"></div>
|
|
||||||
<div class="snack-info">
|
|
||||||
<h3>Popcorn Groß</h3>
|
|
||||||
<div class="option-group">
|
|
||||||
<button class="opt-btn active">Süß</button>
|
|
||||||
<button class="opt-btn">Salzig</button>
|
|
||||||
</div>
|
|
||||||
<div class="size-selector">
|
|
||||||
<button class="size-chip">6,00€</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="snack-card">
|
|
||||||
<div class="snack-img"><img src="img/zoomania-popcorn.jpg" alt="Popcorn limited - zoomania 2"></div>
|
|
||||||
<div class="snack-info">
|
|
||||||
<h3>Limitierter Metallbecher - Zoomania 2</h3>
|
|
||||||
<div class="option-group">
|
|
||||||
<button class="opt-btn active">Süß</button>
|
|
||||||
<button class="opt-btn">Salzig</button>
|
|
||||||
</div>
|
|
||||||
<div class="size-selector">
|
|
||||||
<button class="size-chip">12,00€</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="snack-card">
|
|
||||||
<div class="snack-img"><img src="img/screamdoorpopcorn.jpg" alt="Popcorn limited - Scream VII"></div>
|
|
||||||
<div class="snack-info">
|
|
||||||
<h3>Limitierter Sammelbecher - Scream VII</h3>
|
|
||||||
<div class="option-group">
|
|
||||||
<button class="opt-btn active">Süß</button>
|
|
||||||
<button class="opt-btn">Salzig</button>
|
|
||||||
</div>
|
|
||||||
<div class="size-selector">
|
|
||||||
<button class="size-chip">29,00€</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="snack-card">
|
|
||||||
<div class="snack-img"><img src="img/astronautpopcorn.jpg" alt="Popcorn limited - Der Austronaut"></div>
|
|
||||||
<div class="snack-info">
|
|
||||||
<h3>Limitierter Sammelbecher - Der Austronaut</h3>
|
|
||||||
<div class="option-group">
|
|
||||||
<button class="opt-btn active">Süß</button>
|
|
||||||
<button class="opt-btn">Salzig</button>
|
|
||||||
</div>
|
|
||||||
<div class="size-selector">
|
|
||||||
<button class="size-chip">34,00€</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="snack-card">
|
|
||||||
<div class="snack-img"><img src="img/hopperspopcornmetall.jpg" alt="Popcorn limited - Hoppers"></div>
|
|
||||||
<div class="snack-info">
|
|
||||||
<h3>Limitierter Metallbecher - Hoppers</h3>
|
|
||||||
<div class="option-group">
|
|
||||||
<button class="opt-btn active">Süß</button>
|
|
||||||
<button class="opt-btn">Salzig</button>
|
|
||||||
</div>
|
|
||||||
<div class="size-selector">
|
|
||||||
<button class="size-chip">12,00€</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="snack-card">
|
|
||||||
<div class="snack-img"><img src="img/hopperspopcornwood.png" alt="Popcorn limited - Hoppers"></div>
|
|
||||||
<div class="snack-info">
|
|
||||||
<h3>Limitierter Sammelbecher - Hoppers</h3>
|
|
||||||
<div class="option-group">
|
|
||||||
<button class="opt-btn active">Süß</button>
|
|
||||||
<button class="opt-btn">Salzig</button>
|
|
||||||
</div>
|
|
||||||
<div class="size-selector">
|
|
||||||
<button class="size-chip">21,00€</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="snack-card">
|
|
||||||
<div class="snack-img"><img src="img/marioyoshipopcorn.png" alt="Popcorn limited - Yoshi"></div>
|
|
||||||
<div class="snack-info">
|
|
||||||
<h3>Limitierter Sammelbecher - Yoshi Becher</h3>
|
|
||||||
<div class="option-group">
|
|
||||||
<button class="opt-btn active">Süß</button>
|
|
||||||
<button class="opt-btn">Salzig</button>
|
|
||||||
</div>
|
|
||||||
<div class="size-selector">
|
|
||||||
<button class="size-chip">35,90€</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="cat-nachos" class="snack-category hidden">
|
<div id="cat-nachos" class="snack-category hidden">
|
||||||
<div class="snack-grid">
|
<div class="snack-subsection">
|
||||||
<div class="snack-card">
|
<div class="snack-section-heading">
|
||||||
<div class="snack-img"><img src="img/nachosnormal.png" alt="Nachos"></div>
|
<span>Nachos</span>
|
||||||
<div class="snack-info">
|
<h2>Einzelprodukte</h2>
|
||||||
<h3>Nachos Klein</h3>
|
</div>
|
||||||
<div class="option-group">
|
<div class="snack-grid">
|
||||||
<button class="opt-btn active">Käse-Dip</button>
|
<div class="snack-card">
|
||||||
<button class="opt-btn">Salsa-Dip</button>
|
<div class="snack-img"><img src="img/nachosnormal.png" alt="Nachos"></div>
|
||||||
|
<div class="snack-info">
|
||||||
|
<h3>Nachos Klein</h3>
|
||||||
|
<div class="option-group">
|
||||||
|
<button class="opt-btn active">Käse-Dip</button>
|
||||||
|
<button class="opt-btn">Salsa-Dip</button>
|
||||||
|
</div>
|
||||||
|
<div class="size-selector">
|
||||||
|
<button class="size-chip">Klein <span>5,00€</span></button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="size-selector">
|
</div>
|
||||||
<button class="size-chip">Klein<span>5,00€</span></button>
|
<div class="snack-card">
|
||||||
|
<div class="snack-img"><img src="img/nachosnormal.png" alt="Nachos"></div>
|
||||||
|
<div class="snack-info">
|
||||||
|
<h3>Nachos Normal</h3>
|
||||||
|
<div class="option-group">
|
||||||
|
<button class="opt-btn active">Käse-Dip</button>
|
||||||
|
<button class="opt-btn">Salsa-Dip</button>
|
||||||
|
</div>
|
||||||
|
<div class="size-selector">
|
||||||
|
<button class="size-chip">Mittel <span>6,50€</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="snack-card">
|
||||||
|
<div class="snack-img"><img src="img/nachos.jpg" alt="Nachos"></div>
|
||||||
|
<div class="snack-info">
|
||||||
|
<h3>Nachos Groß</h3>
|
||||||
|
<div class="option-group">
|
||||||
|
<button class="opt-btn active">Käse-Dip</button>
|
||||||
|
<button class="opt-btn">Sourcreme-Dip</button>
|
||||||
|
<button class="opt-btn">Salsa-Dip</button>
|
||||||
|
</div>
|
||||||
|
<div class="size-selector">
|
||||||
|
<button class="size-chip">Groß <span>8,00€</span></button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="snack-card">
|
</div>
|
||||||
<div class="snack-img"><img src="img/nachosnormal.png" alt="Nachos"></div>
|
|
||||||
<div class="snack-info">
|
<div class="snack-subsection">
|
||||||
<h3>Nachos Normal</h3>
|
<div class="snack-section-heading">
|
||||||
<div class="option-group">
|
<span>Extra dazu</span>
|
||||||
<button class="opt-btn active">Käse-Dip</button>
|
<h2>Dips</h2>
|
||||||
<button class="opt-btn">Salsa-Dip</button>
|
</div>
|
||||||
|
<div class="snack-grid">
|
||||||
|
<div class="snack-card">
|
||||||
|
<div class="snack-img"><img src="img/käsedip.png" alt="Käse-Dip"></div>
|
||||||
|
<div class="snack-info">
|
||||||
|
<h3>Käse-Dip<br>(warm)</h3>
|
||||||
|
<div class="option-group">
|
||||||
|
<button class="opt-btn active">Normal</button>
|
||||||
|
<button class="opt-btn">Scharf</button>
|
||||||
|
</div>
|
||||||
|
<div class="size-selector">
|
||||||
|
<button class="size-chip">Schale <span>2,00€</span></button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="size-selector">
|
</div>
|
||||||
<button class="size-chip">Mittel<span>6,50€</span></button>
|
<div class="snack-card">
|
||||||
|
<div class="snack-img"><img src="img/sourdip.png" alt="Sourcreme-Dip"></div>
|
||||||
|
<div class="snack-info">
|
||||||
|
<h3>Sourcreme-Dip</h3>
|
||||||
|
<div class="option-group">
|
||||||
|
<button class="opt-btn active">Normal</button>
|
||||||
|
</div>
|
||||||
|
<div class="size-selector">
|
||||||
|
<button class="size-chip">Schale <span>2,00€</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="snack-card">
|
||||||
|
<div class="snack-img"><img src="img/salsadip.png" alt="Salsa-Dip"></div>
|
||||||
|
<div class="snack-info">
|
||||||
|
<h3>Salsa-Dip</h3>
|
||||||
|
<div class="option-group">
|
||||||
|
<button class="opt-btn active">Normal</button>
|
||||||
|
</div>
|
||||||
|
<div class="size-selector">
|
||||||
|
<button class="size-chip">Schale <span>2,00€</span></button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="snack-card">
|
</div>
|
||||||
<div class="snack-img"><img src="img/nachos.jpg" alt="Nachos"></div>
|
|
||||||
<div class="snack-info">
|
<div class="snack-subsection">
|
||||||
<h3>Nachos Groß</h3>
|
<div class="snack-section-heading">
|
||||||
<div class="option-group">
|
<span>Alles drin</span>
|
||||||
<button class="opt-btn active">Käse-Dip</button>
|
<h2>Kombi Menü</h2>
|
||||||
<button class="opt-btn">Sourcreme-Dip</button>
|
</div>
|
||||||
<button class="opt-btn">Salsa-Dip</button>
|
<div class="snack-grid">
|
||||||
|
<div class="snack-card highlight">
|
||||||
|
<div class="snack-img"><img src="img/nachokombiklein.png" alt="Nacho Kombi Klein"></div>
|
||||||
|
<div class="snack-info">
|
||||||
|
<h3>Nacho Menü Klein</h3>
|
||||||
|
<p class="snack-card-note">Nachos klein + 1 Dip + 1x 0,33L Getränk</p>
|
||||||
|
<div class="option-group">
|
||||||
|
<button class="opt-btn active">Klein</button>
|
||||||
|
</div>
|
||||||
|
<div class="size-selector">
|
||||||
|
<button class="size-chip">Kombi <span>6,90€</span></button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="size-selector">
|
</div>
|
||||||
<button class="size-chip">Groß <span>8,00€</span></button>
|
<div class="snack-card highlight">
|
||||||
|
<div class="snack-img"><img src="img/nachokombimittel.png" alt="Nacho Kombi Mittel"></div>
|
||||||
|
<div class="snack-info">
|
||||||
|
<h3>Nacho Menü Mittel</h3>
|
||||||
|
<p class="snack-card-note">Nachos mittel + 1 Dip + 1x 0,33L Getränk</p>
|
||||||
|
<div class="option-group">
|
||||||
|
<button class="opt-btn active">Mittel</button>
|
||||||
|
</div>
|
||||||
|
<div class="size-selector">
|
||||||
|
<button class="size-chip">Kombi <span>6,90€</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="snack-card highlight">
|
||||||
|
<div class="snack-img"><img src="img/nachokombigross.png" alt="Nacho Kombi Groß"></div>
|
||||||
|
<div class="snack-info">
|
||||||
|
<span class="badge">Bestseller</span>
|
||||||
|
<h3>Nacho Menü Groß</h3>
|
||||||
|
<p class="snack-card-note">Nachos groß + 1 Dip + 1x 0,33L Getränk</p>
|
||||||
|
<div class="option-group">
|
||||||
|
<button class="opt-btn active">Groß</button>
|
||||||
|
</div>
|
||||||
|
<div class="size-selector">
|
||||||
|
<button class="size-chip">Kombi <span>6,90€</span></button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="list-title"> </h2>
|
|
||||||
<br>
|
|
||||||
<h2 class="list-title">Dips</h2>
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
<div class="snack-card">
|
|
||||||
<div class="snack-img"><img src="img/käsedip.png" alt="Käse-Dip"></div>
|
|
||||||
<div class="snack-info">
|
|
||||||
<h3>Käse-Dip<br>(warm)</h3>
|
|
||||||
<div class="option-group">
|
|
||||||
<button class="opt-btn active">Normal</button>
|
|
||||||
<button class="opt-btn">Scharf</button>
|
|
||||||
</div>
|
|
||||||
<div class="size-selector">
|
|
||||||
<button class="size-chip">Schale<span>2,00€</span></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="snack-card">
|
|
||||||
<div class="snack-img"><img src="img/sourdip.png" alt="Sourcreme-Dip"></div>
|
|
||||||
<div class="snack-info">
|
|
||||||
<h3>Sourcreme-Dip</h3>
|
|
||||||
<div class="option-group">
|
|
||||||
<button class="opt-btn active">Normal</button>
|
|
||||||
</div>
|
|
||||||
<div class="size-selector">
|
|
||||||
<button class="size-chip">Schale<span>2,00€</span></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="snack-card">
|
|
||||||
<div class="snack-img"><img src="img/salsadip.png" alt="Salsa-Dip"></div>
|
|
||||||
<div class="snack-info">
|
|
||||||
<h3>Salsa-Dip</h3>
|
|
||||||
<div class="option-group">
|
|
||||||
<button class="opt-btn active">Normal</button>
|
|
||||||
</div>
|
|
||||||
<div class="size-selector">
|
|
||||||
<button class="size-chip">Schale<span>2,00€</span></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h2 class="list-title">Nacho Kombi-Menüs</h2>
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="snack-card">
|
|
||||||
<div class="snack-img"><img src="img/nachokombiklein.png" alt="Nacho Kombi Klein"></div>
|
|
||||||
<div class="snack-info">
|
|
||||||
<h3>Nacho Menü Klein - Nachos klein + 1 Dip + 1 0,33L Getränk</h3>
|
|
||||||
<div class="option-group">
|
|
||||||
<button class="opt-btn active">Klein</button>
|
|
||||||
</div>
|
|
||||||
<div class="size-selector">
|
|
||||||
<button class="size-chip">Kombi<span>6,90€</span></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="cat-snacks" class="snack-category hidden">
|
<div id="cat-snacks" class="snack-category hidden">
|
||||||
<div class="snack-grid">
|
<div class="snack-grid">
|
||||||
<div class="snack-card">
|
<div class="snack-card">
|
||||||
@@ -647,75 +887,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="cat-kombi" class="snack-category hidden">
|
|
||||||
<div class="snack-grid">
|
|
||||||
<div class="snack-card highlight">
|
|
||||||
<div class="snack-info">
|
|
||||||
<h3>Kleines Menü</h3>
|
|
||||||
<p style="font-size: 0.8rem; color: #86868b; margin-bottom: 10px;">0,33L Getränk + Popcorn Klein</p>
|
|
||||||
<div class="size-selector">
|
|
||||||
<button class="size-chip">Menü-Preis <span>5,00€</span></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="snack-card highlight">
|
|
||||||
<div class="snack-info">
|
|
||||||
<h3>Mittleres Menü</h3>
|
|
||||||
<p style="font-size: 0.8rem; color: #86868b; margin-bottom: 10px;">0,5L Getränk + Popcorn Mittel</p>
|
|
||||||
<div class="size-selector">
|
|
||||||
<button class="size-chip">Menü-Preis <span>6,50€</span></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="snack-card highlight">
|
|
||||||
<div class="snack-info">
|
|
||||||
<span class="badge">Bestseller</span>
|
|
||||||
<h3>Großes Menü</h3>
|
|
||||||
<p style="font-size: 0.8rem; color: #86868b; margin-bottom: 10px;">1L Getränk + Popcorn Groß</p>
|
|
||||||
<div class="size-selector">
|
|
||||||
<button class="size-chip">Menü-Preis <span>8,00€</span></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<br>
|
|
||||||
<div class="snack-card highlight">
|
|
||||||
<div class="snack-info">
|
|
||||||
<div class="snack-img"><img src="img/hopperskidsmenu.jpg" alt="Hoppers Kids Menu"></div>
|
|
||||||
<span class="badge">SPECIAL</span>
|
|
||||||
<h3>Limitiertes Menü</h3>
|
|
||||||
<p style="font-size: 0.8rem; color: #86868b; margin-bottom: 10px;">0,5L Getränk im HOPPERS Becher + HOPPERS Popcorn Schale<br>+HOPPERS Figur</p>
|
|
||||||
<div class="size-selector">
|
|
||||||
<button class="size-chip">Menü-Preis <span>10,00€</span></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="snack-card highlight">
|
|
||||||
<div class="snack-info">
|
|
||||||
<div class="snack-img"><img src="img/mariokidsmenu.png" alt="Mario Kids Menu"></div>
|
|
||||||
<br>
|
|
||||||
<span class="badge">SPECIAL</span>
|
|
||||||
<h3>Limitiertes Menü</h3>
|
|
||||||
<p style="font-size: 0.8rem; color: #86868b; margin-bottom: 10px;">0,5L Getränk im MARIO GALXY Becher + MARIO GALAXY Popcorn Schale</p>
|
|
||||||
<div class="size-selector">
|
|
||||||
<button class="size-chip">Menü-Preis <span>10,00€</span></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="snack-card highlight">
|
|
||||||
<div class="snack-info">
|
|
||||||
<div class="snack-img"><img src="img/zoomaniakidsmenu.jpg" alt="Zoomania Kids Menu"></div>
|
|
||||||
<br>
|
|
||||||
<span class="badge">SPECIAL</span>
|
|
||||||
<h3>Limitiertes Menü</h3>
|
|
||||||
<p style="font-size: 0.8rem; color: #86868b; margin-bottom: 10px;">0,5L Getränk im ZOOMANIA Becher + ZOOMANIA Popcorn Schale<br>+ Figur zum aussuchen</p>
|
|
||||||
<div class="size-selector">
|
|
||||||
<button class="size-chip">Menü-Preis <span>10,00€</span></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="cat-eis" class="snack-category hidden">
|
<div id="cat-eis" class="snack-category hidden">
|
||||||
<div class="coming-soon-banner">
|
<div class="coming-soon-banner">
|
||||||
<h2>Eiscreme & Shakes</h2>
|
<h2>Eiscreme & Shakes</h2>
|
||||||
@@ -735,8 +906,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.jpg" alt="D-Box" class="tech-badge">
|
<img src="img/dbox.png" alt="D-Box" class="tech-badge">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -815,8 +986,14 @@
|
|||||||
<div class="account-login-box">
|
<div class="account-login-box">
|
||||||
<h2>Mein Konto</h2>
|
<h2>Mein Konto</h2>
|
||||||
|
|
||||||
<input type="email" id="login-email" placeholder="E-Mail">
|
<label class="auth-field">
|
||||||
<input type="password" id="login-password" placeholder="Passwort">
|
<input type="email" id="login-email" placeholder="E-Mail" required data-required-message="E-Mail ist ein Pflichtfeld." data-email-message="Bitte gib eine gültige E-Mail-Adresse ein.">
|
||||||
|
<span class="field-error"></span>
|
||||||
|
</label>
|
||||||
|
<label class="auth-field">
|
||||||
|
<input type="password" id="login-password" placeholder="Passwort" required data-required-message="Passwort ist ein Pflichtfeld.">
|
||||||
|
<span class="field-error"></span>
|
||||||
|
</label>
|
||||||
<button id="btn-forgot-password" type="button">Passwort vergessen?</button>
|
<button id="btn-forgot-password" type="button">Passwort vergessen?</button>
|
||||||
|
|
||||||
<div id="login-error" class="hidden">
|
<div id="login-error" class="hidden">
|
||||||
@@ -831,14 +1008,80 @@
|
|||||||
|
|
||||||
|
|
||||||
<div id="register-modal" class="modal hidden">
|
<div id="register-modal" class="modal hidden">
|
||||||
<div class="modal-content account-auth-modal">
|
<div class="modal-content account-auth-modal register-auth-modal">
|
||||||
<button id="btn-close-register" class="modal-close-btn" type="button" aria-label="Registrierung schliessen">×</button>
|
<button id="btn-close-register" class="modal-close-btn" type="button" aria-label="Registrierung schliessen">×</button>
|
||||||
<h2>Registrieren</h2>
|
<h2>Registrieren</h2>
|
||||||
<p class="auth-modal-subtitle">Erstelle dein Konto für schnellere Buchungen.</p>
|
<p class="auth-modal-subtitle">Erstelle dein Konto für schnellere Buchungen.</p>
|
||||||
<input type="text" id="reg-firstname" placeholder="Vorname">
|
|
||||||
<input type="text" id="reg-lastname" placeholder="Nachname">
|
<div class="register-form-grid">
|
||||||
<input type="email" id="reg-email" placeholder="E-Mail">
|
<section class="register-form-section">
|
||||||
<input type="password" id="reg-password" placeholder="Passwort">
|
<h3>Meine Daten</h3>
|
||||||
|
<div class="gender-field auth-field" data-field-for="reg-salutation">
|
||||||
|
<input type="hidden" id="reg-salutation" required data-required-message="Anrede ist ein Pflichtfeld.">
|
||||||
|
<div class="gender-selector" role="group" aria-label="Anrede">
|
||||||
|
<button type="button" data-gender="Frau">Frau</button>
|
||||||
|
<button type="button" data-gender="Herr">Herr</button>
|
||||||
|
<button type="button" data-gender="Divers">Divers</button>
|
||||||
|
</div>
|
||||||
|
<span class="field-error"></span>
|
||||||
|
</div>
|
||||||
|
<label class="auth-field">
|
||||||
|
<input type="text" id="reg-firstname" placeholder="Vorname" required data-required-message="Vorname ist ein Pflichtfeld.">
|
||||||
|
<span class="field-error"></span>
|
||||||
|
</label>
|
||||||
|
<label class="auth-field">
|
||||||
|
<input type="text" id="reg-lastname" placeholder="Nachname" required data-required-message="Nachname ist ein Pflichtfeld.">
|
||||||
|
<span class="field-error"></span>
|
||||||
|
</label>
|
||||||
|
<label class="auth-field">
|
||||||
|
<input type="date" id="reg-birthday" placeholder="Geburtstag" required data-required-message="Geburtstag ist ein Pflichtfeld.">
|
||||||
|
<span class="field-error"></span>
|
||||||
|
</label>
|
||||||
|
<div class="register-row street-row">
|
||||||
|
<label class="auth-field">
|
||||||
|
<input type="text" id="reg-street" placeholder="Straße" required data-required-message="Straße ist ein Pflichtfeld.">
|
||||||
|
<span class="field-error"></span>
|
||||||
|
</label>
|
||||||
|
<label class="auth-field">
|
||||||
|
<input type="text" id="reg-house-number" placeholder="Nr." required data-required-message="Hausnummer ist ein Pflichtfeld.">
|
||||||
|
<span class="field-error"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="register-row">
|
||||||
|
<label class="auth-field">
|
||||||
|
<input type="text" id="reg-zip" placeholder="PLZ" required data-required-message="PLZ ist ein Pflichtfeld.">
|
||||||
|
<span class="field-error"></span>
|
||||||
|
</label>
|
||||||
|
<label class="auth-field">
|
||||||
|
<input type="text" id="reg-city" placeholder="Ort" required data-required-message="Ort ist ein Pflichtfeld.">
|
||||||
|
<span class="field-error"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<label class="auth-field">
|
||||||
|
<select id="reg-country" required data-required-message="Land ist ein Pflichtfeld.">
|
||||||
|
<option value="Deutschland">Deutschland</option>
|
||||||
|
<option value="Österreich">Österreich</option>
|
||||||
|
<option value="Schweiz">Schweiz</option>
|
||||||
|
<option value="Luxemburg">Luxemburg</option>
|
||||||
|
</select>
|
||||||
|
<span class="field-error"></span>
|
||||||
|
</label>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="register-form-section">
|
||||||
|
<h3>Anmeldeinformationen</h3>
|
||||||
|
<label class="auth-field">
|
||||||
|
<input type="email" id="reg-email" placeholder="E-Mail" required data-required-message="E-Mail ist ein Pflichtfeld." data-email-message="Bitte gib eine gültige E-Mail-Adresse ein.">
|
||||||
|
<span class="field-error"></span>
|
||||||
|
</label>
|
||||||
|
<label class="auth-field">
|
||||||
|
<input type="password" id="reg-password" placeholder="Passwort" required minlength="8" maxlength="32" data-required-message="Passwort ist ein Pflichtfeld." data-password-message="Passwort muss 8 bis 32 Zeichen haben.">
|
||||||
|
<span class="field-error"></span>
|
||||||
|
</label>
|
||||||
|
<p class="register-warning">Achte auf eine korrekte E-Mail-Adresse. An diese Adresse wird deine Buchungsbestätigung gesendet.</p>
|
||||||
|
<p class="register-warning">Passwort mit 8 bis 32 Zeichen.</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
<button id="btn-register-save" class="auth-submit-btn" type="button">Konto erstellen</button>
|
<button id="btn-register-save" class="auth-submit-btn" type="button">Konto erstellen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -948,11 +1191,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="src/main.js"></script>
|
<script src="account.js"></script>
|
||||||
<script src="src/cart.js"></script>
|
<script src="cart.js"></script>
|
||||||
<script src="src/booking.js"></script>
|
<script src="booking.js"></script>
|
||||||
<script src="src/checkout.js"></script>
|
<script src="checkout.js"></script>
|
||||||
<script type="module" src="dist/account.js"></script>
|
<script src="main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
import { currentUser, loginUser, openAccountDashboard, registerUser } from "./account";
|
// Shared app state for legacy script files (account.js, booking.js, cart.js, checkout.js)
|
||||||
import { openBooking } from "./booking";
|
var prices = { normal: 11.0, imax: 15.0, vip: 12.0, dbox: 16.0 };
|
||||||
import { renderCart, saveCart, updateCartBadge } from "./cart";
|
var seatLayouts = {
|
||||||
import { renderCheckout } from "./checkout";
|
|
||||||
|
|
||||||
// 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 1": { rows: 6, left: 3, right: 7, vipRows: [5], dbox: [] },
|
||||||
"Kino 2": { rows: 7, left: 5, right: 5, vipRows: [6], 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 }] },
|
"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 }
|
IMAX: { rows: 15, left: 10, right: 10, vipRows: [], dbox: [], isImax: true }
|
||||||
};
|
};
|
||||||
export var cart = JSON.parse(localStorage.getItem("eagleCart") || '{}');
|
var cart = JSON.parse(localStorage.getItem("eagleCart")) || [];
|
||||||
export var occupiedSeatsData = JSON.parse(localStorage.getItem("eagleOccupied") || '{}');
|
var occupiedSeatsData = JSON.parse(localStorage.getItem("eagleOccupied")) || {};
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const views = {
|
const views = {
|
||||||
@@ -42,7 +37,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
heroDots: document.getElementById("hero-dots"),
|
heroDots: document.getElementById("hero-dots"),
|
||||||
heroTitle: document.getElementById("hero-title"),
|
heroTitle: document.getElementById("hero-title"),
|
||||||
heroText: document.getElementById("hero-text"),
|
heroText: document.getElementById("hero-text"),
|
||||||
|
nowRunningShell: document.getElementById("now-running-shell"),
|
||||||
nowRunningRow: document.getElementById("now-running-row"),
|
nowRunningRow: document.getElementById("now-running-row"),
|
||||||
|
nowRunningToggle: document.getElementById("now-running-toggle"),
|
||||||
movieProgramList: document.getElementById("movie-program-list"),
|
movieProgramList: document.getElementById("movie-program-list"),
|
||||||
checkoutBtn: document.getElementById("btn-checkout-final"),
|
checkoutBtn: document.getElementById("btn-checkout-final"),
|
||||||
backHomeBtn: document.getElementById("btn-back-home"),
|
backHomeBtn: document.getElementById("btn-back-home"),
|
||||||
@@ -67,8 +64,10 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
duration: 148,
|
duration: 148,
|
||||||
fsk: "6",
|
fsk: "6",
|
||||||
description: "In Walt Disney Animation Studios’ \"Zoomania 2\" geraten die tierischen Detektive Judy Hopps und Nick Wilde auf die rätselhafte Spur eines geheimnisvollen Reptils, das in Zoomania auftaucht und die Metropole völlig auf den Kopf stellt: Gary De’Snake! ",
|
description: "In Walt Disney Animation Studios’ \"Zoomania 2\" geraten die tierischen Detektive Judy Hopps und Nick Wilde auf die rätselhafte Spur eines geheimnisvollen Reptils, das in Zoomania auftaucht und die Metropole völlig auf den Kopf stellt: Gary De’Snake! ",
|
||||||
poster: "img/zoomania-2.jpg",
|
poster: "img/Zoomania-2.jpg",
|
||||||
backdrop: "img/zoomania-2.jpg"
|
backdrop: "img/Zoomania-2.jpg",
|
||||||
|
trailerYoutubeId: "https://www.youtube.com/watch?v=RPE_6KbMgYU&pp=ygUZem9vbWFuaWEgMiB0cmFpbGVyIGdlcm1hbg%3D%3D",
|
||||||
|
actors: ["Name 1", "Name 2", "Name 3"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Shelter",
|
title: "Shelter",
|
||||||
@@ -77,7 +76,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
fsk: "16",
|
fsk: "16",
|
||||||
description: "Michael Mason, ein untergetauchter Elite-Agent, lebt auf einer abgelegenen Insel in Schottland. Als er in einem schweren Sturm ein Mädchen vor dem Ertrinken rettet, setzt er damit eine Kette von Ereignissen in Gang, die sein Versteck enttarnen und ihn zurück in die Welt zwingen. Dort muss er nicht nur um das Überleben der Teenagerin kämpfen, sondern sich auch den Dämonen seiner Vergangenheit stellen…",
|
description: "Michael Mason, ein untergetauchter Elite-Agent, lebt auf einer abgelegenen Insel in Schottland. Als er in einem schweren Sturm ein Mädchen vor dem Ertrinken rettet, setzt er damit eine Kette von Ereignissen in Gang, die sein Versteck enttarnen und ihn zurück in die Welt zwingen. Dort muss er nicht nur um das Überleben der Teenagerin kämpfen, sondern sich auch den Dämonen seiner Vergangenheit stellen…",
|
||||||
poster: "img/shelter.jpg",
|
poster: "img/shelter.jpg",
|
||||||
backdrop: "img/shelter.jpg"
|
backdrop: "img/shelter.jpg",
|
||||||
|
trailerYoutubeId: "https://www.youtube.com/watch?v=_DgEucbPMZI&pp=ygUWc2hlbHRlciB0cmFpbGVyIGdlcm1hbg%3D%3D",
|
||||||
|
actors: ["Name 1", "Name 2", "Name 3"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Mutiny",
|
title: "Mutiny",
|
||||||
@@ -86,7 +87,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
fsk: "?",
|
fsk: "?",
|
||||||
description: "Als sein milliardenschwerer Boss vor seinen Augen einem Mordkomplott zum Opfer fällt, wird Cole Reed (Jason Statham) für das Verbrechen verantwortlich gemacht. Im Bestreben dessen Tod zu rächen und die Täter zu überführen, gelangt Reed an Bord eines Frachters und stößt dabei auf eine internationale Verschwörung. Im Alleingang startet Cole eine gnadenlose Racheaktion auf hoher See…",
|
description: "Als sein milliardenschwerer Boss vor seinen Augen einem Mordkomplott zum Opfer fällt, wird Cole Reed (Jason Statham) für das Verbrechen verantwortlich gemacht. Im Bestreben dessen Tod zu rächen und die Täter zu überführen, gelangt Reed an Bord eines Frachters und stößt dabei auf eine internationale Verschwörung. Im Alleingang startet Cole eine gnadenlose Racheaktion auf hoher See…",
|
||||||
poster: "img/mutiny.jpg",
|
poster: "img/mutiny.jpg",
|
||||||
backdrop: "img/mutiny.jpg"
|
backdrop: "img/mutiny.jpg",
|
||||||
|
trailerYoutubeId: "https://www.youtube.com/watch?v=DU2cBNrvHA4&pp=ygUObXV0aW55IHRyYWlsZXI%3D",
|
||||||
|
actors: ["Name 1", "Name 2", "Name 3"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Der Austronaut - Project Hail Mary",
|
title: "Der Austronaut - Project Hail Mary",
|
||||||
@@ -95,7 +98,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
fsk: "12",
|
fsk: "12",
|
||||||
description: "Der Naturwissenschaftslehrer Ryland Grace wacht eines Tages auf einem Raumschiff auf - Lichtjahre von zu Hause entfernt und ohne Erinnerung daran, wer er ist oder wie er dorthin gekommen ist.",
|
description: "Der Naturwissenschaftslehrer Ryland Grace wacht eines Tages auf einem Raumschiff auf - Lichtjahre von zu Hause entfernt und ohne Erinnerung daran, wer er ist oder wie er dorthin gekommen ist.",
|
||||||
poster: "img/derAustronaut.jpg",
|
poster: "img/derAustronaut.jpg",
|
||||||
backdrop: "img/derAustronaut.jpg"
|
backdrop: "img/derAustronaut.jpg",
|
||||||
|
trailerYoutubeId: "https://www.youtube.com/watch?v=-KXQttVDPyk&pp=ygUdZGVyIGFzdHJvbmF1dCB0cmFpbGVyIGRldXRzY2g%3D",
|
||||||
|
actors: ["Name 1", "Name 2", "Name 3"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Der Super Mario Galaxy Film",
|
title: "Der Super Mario Galaxy Film",
|
||||||
@@ -104,7 +109,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
fsk: "6",
|
fsk: "6",
|
||||||
description: "''Der Super Mario Galaxy Film'' ist der neue Animationsfilm, basierend auf der Welt von „Super Mario Bros.“!",
|
description: "''Der Super Mario Galaxy Film'' ist der neue Animationsfilm, basierend auf der Welt von „Super Mario Bros.“!",
|
||||||
poster: "img/mariogalaxy.jpg",
|
poster: "img/mariogalaxy.jpg",
|
||||||
backdrop: "img/mariogalaxy.jpg"
|
backdrop: "img/mariogalaxy.jpg",
|
||||||
|
trailerYoutubeId:"https://www.youtube.com/watch?v=RtoRPSh0jvM&pp=ygUrZGVyIHN1cGVyIG1hcmlvIGdhbGF4eSBmaWxtIHRyYWlsZXIgZGV1dHNjaA%3D%3D",
|
||||||
|
actors: ["Name 1", "Name 2", "Name 3"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Hoppers",
|
title: "Hoppers",
|
||||||
@@ -113,7 +120,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
fsk: "6",
|
fsk: "6",
|
||||||
description: "In Disney Pixars ''Hoppers'' hoppt Tierfreundin Mabel mithilfe neuester Technologie in einen Roboterbiber und macht die Tierwelt (un)sicher! ",
|
description: "In Disney Pixars ''Hoppers'' hoppt Tierfreundin Mabel mithilfe neuester Technologie in einen Roboterbiber und macht die Tierwelt (un)sicher! ",
|
||||||
poster: "img/hoppers.jpg",
|
poster: "img/hoppers.jpg",
|
||||||
backdrop: "img/hoppers.jpg"
|
backdrop: "img/hoppers.jpg",
|
||||||
|
trailerYoutubeId: "https://www.youtube.com/watch?v=WqTZZcPe5R0&pp=ygUPaG9wcGVycyB0cmFpbGVy",
|
||||||
|
actors: ["Name 1", "Name 2", "Name 3"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Solo Mio",
|
title: "Solo Mio",
|
||||||
@@ -122,7 +131,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
fsk: "12",
|
fsk: "12",
|
||||||
description: "Nachdem er in Rom am Altar stehen gelassen wurde, beschließt ein Mann, allein in die Flitterwochen zu fahren.",
|
description: "Nachdem er in Rom am Altar stehen gelassen wurde, beschließt ein Mann, allein in die Flitterwochen zu fahren.",
|
||||||
poster: "img/solomio.png",
|
poster: "img/solomio.png",
|
||||||
backdrop: "img/solomio.png"
|
backdrop: "img/solomio.png",
|
||||||
|
trailerYoutubeId: "https://www.youtube.com/watch?v=tYtiinTvE3s&pp=ygUYc29sZSBtaW8gdHJhaWxlciBkZXV0c2No",
|
||||||
|
actors: ["Name 1", "Name 2", "Name 3"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "The Mandalorian and Grogu",
|
title: "The Mandalorian and Grogu",
|
||||||
@@ -131,7 +142,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
fsk: "?",
|
fsk: "?",
|
||||||
description: "Während die Neue Republik daran arbeitet, alles zu schützen, wofür die Rebellion gekämpft hat, sichern sie sich die Unterstützung des legendären mandalorianischen Kopfgeldjägers Din Djarin und seines jungen Lehrlings Grogu. ",
|
description: "Während die Neue Republik daran arbeitet, alles zu schützen, wofür die Rebellion gekämpft hat, sichern sie sich die Unterstützung des legendären mandalorianischen Kopfgeldjägers Din Djarin und seines jungen Lehrlings Grogu. ",
|
||||||
poster: "img/mandalorian.jpeg",
|
poster: "img/mandalorian.jpeg",
|
||||||
backdrop: "img/mandalorian.jpeg"
|
backdrop: "img/mandalorian.jpeg",
|
||||||
|
trailerYoutubeId: "https://www.youtube.com/watch?v=cMJMtGccVfs&pp=ygUlbWFuZGFsb3JpYW4gYW5kIGdyb2d1IHRyYWlsZXIgZGV1dHNjaA%3D%3D",
|
||||||
|
actors: ["Name 1", "Name 2", "Name 3"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Glennkill - Ein Schafskrimi",
|
title: "Glennkill - Ein Schafskrimi",
|
||||||
@@ -140,7 +153,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
fsk: "?",
|
fsk: "?",
|
||||||
description: "Schäfer George liest seinen geliebten Schafen jeden Abend Krimis vor und nimmt an, dass sie kein Wort davon verstehen. Doch als ein mysteriöser Vorfall das friedliche Leben auf der Farm durchbricht, wird den Schafen bewusst, dass sie selbst nun die Ermittlungen übernehmen müssen. Also folgen sie der Spur der Beweise und machen menschliche Verdächtige ausfindig.",
|
description: "Schäfer George liest seinen geliebten Schafen jeden Abend Krimis vor und nimmt an, dass sie kein Wort davon verstehen. Doch als ein mysteriöser Vorfall das friedliche Leben auf der Farm durchbricht, wird den Schafen bewusst, dass sie selbst nun die Ermittlungen übernehmen müssen. Also folgen sie der Spur der Beweise und machen menschliche Verdächtige ausfindig.",
|
||||||
poster: "img/glennkill.jpg",
|
poster: "img/glennkill.jpg",
|
||||||
backdrop: "img/glennkill.jpg"
|
backdrop: "img/glennkill.jpg",
|
||||||
|
trailerYoutubeId: "https://www.youtube.com/watch?v=Ylu6xVTHAlk&pp=ygUZZ2xlbm5raWxsIGVpbiBzY2hhZnNrcmltaQ%3D%3D",
|
||||||
|
actors: ["Name 1", "Name 2", "Name 3"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Masters of the Universe",
|
title: "Masters of the Universe",
|
||||||
@@ -149,7 +164,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
fsk: "?",
|
fsk: "?",
|
||||||
description: "Nach 15 Jahren der Trennung führt das Schwert der Macht Prinz Adam zurück nach Eternia und er entdeckt, dass seine Heimat unter der Herrschaft von Skeletor in Trümmern liegt.",
|
description: "Nach 15 Jahren der Trennung führt das Schwert der Macht Prinz Adam zurück nach Eternia und er entdeckt, dass seine Heimat unter der Herrschaft von Skeletor in Trümmern liegt.",
|
||||||
poster: "img/masteruniverse.jpg",
|
poster: "img/masteruniverse.jpg",
|
||||||
backdrop: "img/masteruniverse.jpg"
|
backdrop: "img/masteruniverse.jpg",
|
||||||
|
trailerYoutubeId: "https://www.youtube.com/watch?v=COJU2b4XZ8U&pp=ygUfbWFzdGVycyBvZiB0aGUgdW5pdmVyc2UgdHJhaWxlcg%3D%3D",
|
||||||
|
actors: ["Name 1", "Name 2", "Name 3"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Minions and Monsters",
|
title: "Minions and Monsters",
|
||||||
@@ -158,7 +175,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
fsk: "?",
|
fsk: "?",
|
||||||
description: "MINIONS & MONSTER erzählt die abgefahrene, aberwitzige und natürlich absolut wahre Geschichte von den Minions und wie sie Hollywood erobern, Filmstars werden, alles verlieren, Monster auf die Welt loslassen und sich dann heldenhaft daranmachen, den Planeten vor genau dem Chaos zu retten, das sie selbst angerichtet haben. ",
|
description: "MINIONS & MONSTER erzählt die abgefahrene, aberwitzige und natürlich absolut wahre Geschichte von den Minions und wie sie Hollywood erobern, Filmstars werden, alles verlieren, Monster auf die Welt loslassen und sich dann heldenhaft daranmachen, den Planeten vor genau dem Chaos zu retten, das sie selbst angerichtet haben. ",
|
||||||
poster: "img/minionsmonsters.jpg",
|
poster: "img/minionsmonsters.jpg",
|
||||||
backdrop: "img/minionsmonsters.jpg"
|
backdrop: "img/minionsmonsters.jpg",
|
||||||
|
trailerYoutubeId: "https://www.youtube.com/watch?v=a31E7qNxwtI&pp=ygUcbWluaW9ucyBhbmQgbW9uc3RlcnMgdHJhaWxlctIHCQkECwGHKiGM7w%3D%3D",
|
||||||
|
actors: ["Name 1", "Name 2", "Name 3"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Spider Man - Brand New Day",
|
title: "Spider Man - Brand New Day",
|
||||||
@@ -226,9 +245,11 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
{
|
{
|
||||||
title: "Cash Truck",
|
title: "Cash Truck",
|
||||||
genre: "Action",
|
genre: "Action",
|
||||||
duration: 118,
|
duration: 98,
|
||||||
fsk: "16",
|
fsk: "16",
|
||||||
description: ""
|
description: "Der verschlossene Einzelgänger „H” nimmt einen Job bei einer Geldtransporter-Firma an, die jede Woche hunderte von Millionen Dollar durch Los Angeles fährt. Gleich bei seinem ersten Einsatz wird der Geldtransport überfallen und zur Überraschung seiner Kollegen setzt H die Gangster im Alleingang außer Gefecht und wartet mit ungeahnten Präzisionsfähigkeiten auf. Doch H’s Absichten sind nicht zu durchschauen und kommen nur schrittweise ans Licht. Wer ist der geheimnisvolle Neuzugang und auf wen hat er es wirklich abgesehen?",
|
||||||
|
poster: "img/cashtruck.jpg",
|
||||||
|
backdrop: "img/cashtruck.jpg"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Die Gangster Gang",
|
title: "Die Gangster Gang",
|
||||||
@@ -331,10 +352,14 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
["13:00", "16:00", "18:20", "20:40"]
|
["13:00", "16:00", "18:20", "20:40"]
|
||||||
];
|
];
|
||||||
|
|
||||||
let movieProgram: any = []; // TODO: Find type
|
let movieProgram = [];
|
||||||
let heroItems: any = []; // TODO: find Type
|
let heroItems = [];
|
||||||
let heroIndex = 0;
|
let heroIndex = 0;
|
||||||
let heroTimer:any = null; // TODO: find type
|
let heroTimer = null;
|
||||||
|
let activeMovieDetail = {
|
||||||
|
index: null,
|
||||||
|
sourceCard: null
|
||||||
|
};
|
||||||
|
|
||||||
const weekdayShort = ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"];
|
const weekdayShort = ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"];
|
||||||
|
|
||||||
@@ -353,7 +378,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
};
|
};
|
||||||
|
|
||||||
const showMovieList = (programIndexToFocus:number = NaN) => {
|
const showMovieList = (programIndexToFocus = null) => {
|
||||||
hideAllViews();
|
hideAllViews();
|
||||||
views.list?.classList.remove("hidden");
|
views.list?.classList.remove("hidden");
|
||||||
|
|
||||||
@@ -370,7 +395,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const showStaticView = (viewElement: HTMLElement) => {
|
const showStaticView = (viewElement) => {
|
||||||
if (!viewElement) {
|
if (!viewElement) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -399,20 +424,75 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
ui.bookingModal?.classList.add("hidden");
|
ui.bookingModal?.classList.add("hidden");
|
||||||
};
|
};
|
||||||
|
|
||||||
const escapeHtml = (value: string) => String(value || "")
|
const escapeHtml = (value) => String(value || "")
|
||||||
.replaceAll("&", "&")
|
.replaceAll("&", "&")
|
||||||
.replaceAll("<", "<")
|
.replaceAll("<", "<")
|
||||||
.replaceAll(">", ">")
|
.replaceAll(">", ">")
|
||||||
.replaceAll('"', """)
|
.replaceAll('"', """)
|
||||||
.replaceAll("'", "'");
|
.replaceAll("'", "'");
|
||||||
|
|
||||||
const formatDateShort = (dateObj: any) => {
|
const getMovieHalls = (movie) => {
|
||||||
|
const halls = new Set();
|
||||||
|
movie?.schedule?.forEach((day) => {
|
||||||
|
day.showings?.forEach((showing) => halls.add(showing.hall));
|
||||||
|
});
|
||||||
|
return Array.from(halls);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFskLogoPath = (movie) => {
|
||||||
|
if (movie?.fskLogo) {
|
||||||
|
return movie.fskLogo;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fskValue = String(movie?.fsk || "").trim();
|
||||||
|
return fskValue && fskValue !== "?" ? `img/fsk-${fskValue}.png` : "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderFskMetaLogo = (movie) => {
|
||||||
|
const fskValue = escapeHtml(movie.fsk || "?");
|
||||||
|
const logoPath = getFskLogoPath(movie);
|
||||||
|
const imageMarkup = logoPath
|
||||||
|
? `<img src="${escapeHtml(logoPath)}" alt="FSK ${fskValue}" onerror="this.parentElement.classList.add('missing-logo'); this.remove();">`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return `
|
||||||
|
<span class="fsk-meta-logo ${logoPath ? "has-logo" : "missing-logo"}" data-fsk="${fskValue}">
|
||||||
|
${imageMarkup}
|
||||||
|
<span>FSK ${fskValue}</span>
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMovieDurationLabel = (movie) => {
|
||||||
|
const duration = movie?.duration ?? "";
|
||||||
|
return Number.isFinite(Number(duration)) ? `${duration} Min.` : escapeHtml(duration || "tba");
|
||||||
|
};
|
||||||
|
|
||||||
|
const getYoutubeVideoId = (input) => {
|
||||||
|
const value = String(input || "").trim();
|
||||||
|
if (!value) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const watchMatch = value.match(/[?&]v=([^&]+)/);
|
||||||
|
const shortMatch = value.match(/youtu\.be\/([^?&/]+)/);
|
||||||
|
const embedMatch = value.match(/embed\/([^?&/]+)/);
|
||||||
|
|
||||||
|
return decodeURIComponent(watchMatch?.[1] || shortMatch?.[1] || embedMatch?.[1] || value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getYoutubeThumbnailUrl = (videoId) => {
|
||||||
|
const id = getYoutubeVideoId(videoId);
|
||||||
|
return id ? `https://i.ytimg.com/vi/${encodeURIComponent(id)}/hqdefault.jpg` : "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDateShort = (dateObj) => {
|
||||||
const day = String(dateObj.getDate()).padStart(2, "0");
|
const day = String(dateObj.getDate()).padStart(2, "0");
|
||||||
const month = String(dateObj.getMonth() + 1).padStart(2, "0");
|
const month = String(dateObj.getMonth() + 1).padStart(2, "0");
|
||||||
return `${day}.${month}.`;
|
return `${day}.${month}.`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildDayMeta = (offset: number) => {
|
const buildDayMeta = (offset) => {
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
date.setHours(0, 0, 0, 0);
|
date.setHours(0, 0, 0, 0);
|
||||||
date.setDate(date.getDate() + offset);
|
date.setDate(date.getDate() + offset);
|
||||||
@@ -446,15 +526,14 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildScheduleForMovie = (movieIndex: number) => {
|
const buildScheduleForMovie = (movieIndex) => {
|
||||||
return Array.from({ length: 7 }, (_, dayOffset) => {
|
return Array.from({ length: 7 }, (_, dayOffset) => {
|
||||||
const dayMeta = buildDayMeta(dayOffset);
|
const dayMeta = buildDayMeta(dayOffset);
|
||||||
const pattern = timePatterns[(movieIndex + dayOffset) % timePatterns.length] || "Error reading";
|
const pattern = timePatterns[(movieIndex + dayOffset) % timePatterns.length];
|
||||||
const desiredCount = 4 + ((movieIndex + dayOffset) % 2);
|
const desiredCount = 4 + ((movieIndex + dayOffset) % 2);
|
||||||
const showCount = Math.min(pattern.length, desiredCount);
|
const showCount = Math.min(pattern.length, desiredCount);
|
||||||
|
|
||||||
//@ts-ignore
|
const showings = pattern.slice(0, showCount).map((time, slotIndex) => {
|
||||||
const showings = pattern.slice(0, showCount).map((time: any, slotIndex: number) => { // TODO: fix map issue
|
|
||||||
const hall = hallRotation[(movieIndex + dayOffset + slotIndex) % hallRotation.length];
|
const hall = hallRotation[(movieIndex + dayOffset + slotIndex) % hallRotation.length];
|
||||||
return { time, hall };
|
return { time, hall };
|
||||||
});
|
});
|
||||||
@@ -469,12 +548,15 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
const buildMovieProgram = () => {
|
const buildMovieProgram = () => {
|
||||||
movieProgram = movieCatalog.map((movie, movieIndex) => ({
|
movieProgram = movieCatalog.map((movie, movieIndex) => ({
|
||||||
...movie,
|
...movie,
|
||||||
|
trailerYoutubeId: getYoutubeVideoId(movie.trailerYoutubeId || movie.trailerUrl || ""),
|
||||||
|
trailerThumbnail: getYoutubeThumbnailUrl(movie.trailerYoutubeId || movie.trailerUrl || "") || movie.backdrop || movie.poster,
|
||||||
|
actors: Array.isArray(movie.actors) ? movie.actors : [],
|
||||||
schedule: buildScheduleForMovie(movieIndex)
|
schedule: buildScheduleForMovie(movieIndex)
|
||||||
}));
|
}));
|
||||||
heroItems = movieProgram.slice(0, 5);
|
heroItems = movieProgram.slice(0, 5);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setHeroSlide = (index: number) => {
|
const setHeroSlide = (index) => {
|
||||||
if (!heroItems.length || !ui.heroSlider) {
|
if (!heroItems.length || !ui.heroSlider) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -503,18 +585,17 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.heroSlider.innerHTML = heroItems.map((movie: any, index: number) => `
|
ui.heroSlider.innerHTML = heroItems.map((movie, index) => `
|
||||||
<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>
|
<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("");
|
`).join("");
|
||||||
|
|
||||||
if (ui.heroDots) {
|
if (ui.heroDots) {
|
||||||
ui.heroDots.innerHTML = heroItems.map((_:any, index: number) => `
|
ui.heroDots.innerHTML = heroItems.map((_, index) => `
|
||||||
<button type="button" class="hero-dot ${index === 0 ? "active" : ""}" data-hero-index="${index}"></button>
|
<button type="button" class="hero-dot ${index === 0 ? "active" : ""}" data-hero-index="${index}"></button>
|
||||||
`).join("");
|
`).join("");
|
||||||
|
|
||||||
ui.heroDots.addEventListener("click", (event: any) => {
|
ui.heroDots.addEventListener("click", (event) => {
|
||||||
const dotTarget = event.target || 0;
|
const dot = event.target.closest(".hero-dot");
|
||||||
const dot = dotTarget.closest(".hero-dot");
|
|
||||||
if (!dot) {
|
if (!dot) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -545,9 +626,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: implement movie interface
|
ui.nowRunningRow.innerHTML = movieProgram.map((movie, index) => `
|
||||||
ui.nowRunningRow.innerHTML = movieProgram.map((movie: any, index: number) => /*html*/`
|
<article class="running-poster" data-program-index="${index}" tabindex="0" role="button" aria-label="Details zu ${escapeHtml(movie.title)} öffnen">
|
||||||
<article class="running-poster">
|
|
||||||
<img src="${escapeHtml(movie.poster)}" alt="${escapeHtml(movie.title)}">
|
<img src="${escapeHtml(movie.poster)}" alt="${escapeHtml(movie.title)}">
|
||||||
<div class="running-meta">
|
<div class="running-meta">
|
||||||
<h4>${escapeHtml(movie.title)}</h4>
|
<h4>${escapeHtml(movie.title)}</h4>
|
||||||
@@ -556,9 +636,15 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
`).join("");
|
`).join("");
|
||||||
|
|
||||||
|
if (ui.nowRunningShell && ui.nowRunningToggle) {
|
||||||
|
ui.nowRunningShell.classList.add("is-collapsed");
|
||||||
|
ui.nowRunningToggle.setAttribute("aria-expanded", "false");
|
||||||
|
ui.nowRunningToggle.setAttribute("aria-label", "Weitere Filme anzeigen");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderScheduleRows = (programIndex: number, dayIndex: number) => {
|
const renderScheduleRows = (programIndex, dayIndex) => {
|
||||||
const movie = movieProgram[programIndex];
|
const movie = movieProgram[programIndex];
|
||||||
if (!movie) {
|
if (!movie) {
|
||||||
return;
|
return;
|
||||||
@@ -570,7 +656,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.innerHTML = day.showings.map((showing: { hall: string; time: string; }) => /*html*/`
|
body.innerHTML = day.showings.map((showing) => `
|
||||||
<button class="schedule-row time-chip program-time-row" data-movie="${escapeHtml(movie.title)}" data-hall="${escapeHtml(showing.hall)}" data-time="${escapeHtml(showing.time)}">
|
<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>${escapeHtml(day.long)}</span>
|
||||||
<span class="hall-pill">${escapeHtml(showing.hall)}</span>
|
<span class="hall-pill">${escapeHtml(showing.hall)}</span>
|
||||||
@@ -584,24 +670,30 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.movieProgramList.innerHTML = movieProgram.map((movie: { schedule: any[]; poster: string; title: string; fsk: string; duration: any; genre: string; description: string; }, programIndex: any) => {
|
ui.movieProgramList.innerHTML = movieProgram.map((movie, programIndex) => {
|
||||||
const dayTabs = movie.schedule.map((day, dayIndex) => /*html*/`
|
const dayTabs = movie.schedule.map((day, dayIndex) => `
|
||||||
<button type="button" class="program-day-tab ${dayIndex === 0 ? "active" : ""}" data-program-index="${programIndex}" data-day-index="${dayIndex}">
|
<button type="button" class="program-day-tab ${dayIndex === 0 ? "active" : ""}" data-program-index="${programIndex}" data-day-index="${dayIndex}">
|
||||||
<span>${escapeHtml(day.short)}</span>
|
<span>${escapeHtml(day.short)}</span>
|
||||||
<small>${escapeHtml(formatDateShort(day.date))}</small>
|
<small>${escapeHtml(formatDateShort(day.date))}</small>
|
||||||
</button>
|
</button>
|
||||||
`).join("");
|
`).join("");
|
||||||
|
const hallPills = getMovieHalls(movie).map((hall) => `
|
||||||
|
<span>${escapeHtml(hall)}</span>
|
||||||
|
`).join("");
|
||||||
|
|
||||||
return /*html*/`
|
return `
|
||||||
<article class="detailed-card program-card reveal-on-scroll" data-program-index="${programIndex}">
|
<article class="detailed-card program-card reveal-on-scroll" data-program-index="${programIndex}">
|
||||||
<div class="card-left">
|
<div class="card-left">
|
||||||
<img src="${escapeHtml(movie.poster)}" alt="${escapeHtml(movie.title)}">
|
<img src="${escapeHtml(movie.poster)}" alt="${escapeHtml(movie.title)}">
|
||||||
<span class="fsk fsk-${escapeHtml(movie.fsk)}">${escapeHtml(movie.fsk)}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-right">
|
<div class="card-right">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h2>${escapeHtml(movie.title)}</h2>
|
<h2>${escapeHtml(movie.title)}</h2>
|
||||||
<span class="duration">${movie.duration} Min. | ${escapeHtml(movie.genre)} | FSK: ${escapeHtml(movie.fsk)}</span>
|
<span class="duration movie-meta-line">${getMovieDurationLabel(movie)} | ${escapeHtml(movie.genre)} | FSK: ${renderFskMetaLogo(movie)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="program-halls-summary">
|
||||||
|
<strong>Kinosaal</strong>
|
||||||
|
<div>${hallPills}</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="description">${escapeHtml(movie.description)}</p>
|
<p class="description">${escapeHtml(movie.description)}</p>
|
||||||
|
|
||||||
@@ -618,11 +710,174 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
`;
|
`;
|
||||||
}).join("");
|
}).join("");
|
||||||
|
|
||||||
movieProgram.forEach((_: any, programIndex: number) => {
|
movieProgram.forEach((_, programIndex) => {
|
||||||
renderScheduleRows(programIndex, 0);
|
renderScheduleRows(programIndex, 0);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ensureMovieDetailOverlay = () => {
|
||||||
|
let overlay = document.getElementById("movie-detail-overlay");
|
||||||
|
if (overlay) {
|
||||||
|
return overlay;
|
||||||
|
}
|
||||||
|
|
||||||
|
overlay = document.createElement("div");
|
||||||
|
overlay.id = "movie-detail-overlay";
|
||||||
|
overlay.className = "movie-detail-overlay hidden";
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
return overlay;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderMovieDetailScheduleRows = (programIndex, dayIndex) => {
|
||||||
|
const movie = movieProgram[programIndex];
|
||||||
|
const day = movie?.schedule?.[dayIndex];
|
||||||
|
const body = document.getElementById("movie-detail-schedule-body");
|
||||||
|
if (!movie || !day || !body) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.innerHTML = day.showings.map((showing) => `
|
||||||
|
<button class="schedule-row time-chip program-time-row movie-detail-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 buildMovieDetailContent = (movie, programIndex) => {
|
||||||
|
const trailerId = getYoutubeVideoId(movie.trailerYoutubeId);
|
||||||
|
const trailerThumb = getYoutubeThumbnailUrl(trailerId) || movie.backdrop || movie.poster;
|
||||||
|
const actorMarkup = movie.actors.length
|
||||||
|
? movie.actors.map((actor) => `<span>${escapeHtml(actor)}</span>`).join("")
|
||||||
|
: `<p>Noch keine Schauspieler hinterlegt.</p>`;
|
||||||
|
const dayTabs = movie.schedule.map((day, dayIndex) => `
|
||||||
|
<button type="button" class="program-day-tab movie-detail-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 `
|
||||||
|
<div class="movie-detail-stage">
|
||||||
|
<button type="button" class="movie-detail-close" aria-label="Filmdetails schließen">×</button>
|
||||||
|
<div class="movie-detail-backdrop" style="background-image: linear-gradient(180deg, rgba(4, 6, 10, 0.28), #05070a 78%), url('${escapeHtml(movie.backdrop || movie.poster)}');"></div>
|
||||||
|
<div class="movie-detail-content">
|
||||||
|
<section class="movie-detail-hero-block">
|
||||||
|
<div class="movie-detail-poster">
|
||||||
|
<img src="${escapeHtml(movie.poster)}" alt="${escapeHtml(movie.title)}">
|
||||||
|
</div>
|
||||||
|
<div class="movie-detail-copy">
|
||||||
|
<p class="movie-detail-kicker">Aktueller Film</p>
|
||||||
|
<h2>${escapeHtml(movie.title)}</h2>
|
||||||
|
<div class="movie-detail-meta movie-meta-line">
|
||||||
|
<span>${getMovieDurationLabel(movie)}</span>
|
||||||
|
<span>${escapeHtml(movie.genre)}</span>
|
||||||
|
${renderFskMetaLogo(movie)}
|
||||||
|
</div>
|
||||||
|
<p>${escapeHtml(movie.description)}</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="movie-detail-section">
|
||||||
|
<div class="movie-detail-section-head movie-detail-trailer-head">
|
||||||
|
<h3>Trailer</h3>
|
||||||
|
<span class="movie-detail-youtube-logo"><img src="img/youtube-logo.png" alt="YouTube Logo"></span>
|
||||||
|
</div>
|
||||||
|
<div class="movie-trailer-frame">
|
||||||
|
<button type="button" class="movie-trailer-preview" data-trailer-id="${escapeHtml(trailerId)}" style="background-image: linear-gradient(180deg, rgba(0,0,0,0.05), rgba(0,0,0,0.42)), url('${escapeHtml(trailerThumb)}');">
|
||||||
|
<span class="movie-trailer-play" aria-hidden="true"></span>
|
||||||
|
<strong>Trailer ansehen</strong>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="movie-detail-section">
|
||||||
|
<div class="movie-detail-section-head">
|
||||||
|
<h3>Schauspieler</h3>
|
||||||
|
<span>Cast</span>
|
||||||
|
</div>
|
||||||
|
<div class="movie-actor-list">${actorMarkup}</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="movie-detail-section movie-detail-schedule">
|
||||||
|
<div class="movie-detail-section-head">
|
||||||
|
<h3>Spielzeiten</h3>
|
||||||
|
<span>Nur für diesen Film</span>
|
||||||
|
</div>
|
||||||
|
<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="movie-detail-schedule-body" class="program-schedule-body"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeMovieDetail = () => {
|
||||||
|
const overlay = document.getElementById("movie-detail-overlay");
|
||||||
|
if (!overlay || overlay.classList.contains("hidden")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetCard = activeMovieDetail.sourceCard?.isConnected ? activeMovieDetail.sourceCard : null;
|
||||||
|
const rect = targetCard?.getBoundingClientRect();
|
||||||
|
|
||||||
|
overlay.classList.remove("is-open");
|
||||||
|
overlay.classList.add("is-closing");
|
||||||
|
|
||||||
|
if (rect) {
|
||||||
|
overlay.style.left = `${rect.left}px`;
|
||||||
|
overlay.style.top = `${rect.top}px`;
|
||||||
|
overlay.style.width = `${rect.width}px`;
|
||||||
|
overlay.style.height = `${rect.height}px`;
|
||||||
|
overlay.style.borderRadius = getComputedStyle(targetCard).borderRadius || "20px";
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
overlay.className = "movie-detail-overlay hidden";
|
||||||
|
overlay.removeAttribute("style");
|
||||||
|
overlay.innerHTML = "";
|
||||||
|
document.body.style.overflow = "auto";
|
||||||
|
activeMovieDetail = { index: null, sourceCard: null };
|
||||||
|
}, 430);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openMovieDetail = (programIndex, sourceCard) => {
|
||||||
|
const movie = movieProgram[programIndex];
|
||||||
|
if (!movie || !sourceCard) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const overlay = ensureMovieDetailOverlay();
|
||||||
|
const rect = sourceCard.getBoundingClientRect();
|
||||||
|
activeMovieDetail = { index: programIndex, sourceCard };
|
||||||
|
|
||||||
|
overlay.innerHTML = buildMovieDetailContent(movie, programIndex);
|
||||||
|
overlay.className = "movie-detail-overlay";
|
||||||
|
overlay.style.left = `${rect.left}px`;
|
||||||
|
overlay.style.top = `${rect.top}px`;
|
||||||
|
overlay.style.width = `${rect.width}px`;
|
||||||
|
overlay.style.height = `${rect.height}px`;
|
||||||
|
overlay.style.borderRadius = getComputedStyle(sourceCard).borderRadius || "20px";
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
|
||||||
|
renderMovieDetailScheduleRows(programIndex, 0);
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
overlay.classList.add("is-open");
|
||||||
|
overlay.style.left = "0px";
|
||||||
|
overlay.style.top = "0px";
|
||||||
|
overlay.style.width = "100vw";
|
||||||
|
overlay.style.height = "100vh";
|
||||||
|
overlay.style.borderRadius = "0px";
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const initRevealAnimations = () => {
|
const initRevealAnimations = () => {
|
||||||
const revealElements = Array.from(document.querySelectorAll(".reveal-on-scroll"));
|
const revealElements = Array.from(document.querySelectorAll(".reveal-on-scroll"));
|
||||||
if (!revealElements.length) {
|
if (!revealElements.length) {
|
||||||
@@ -666,16 +921,12 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
|
|
||||||
ui.linkSnacks?.addEventListener("click", (event) => {
|
ui.linkSnacks?.addEventListener("click", (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (views.snacks) {
|
|
||||||
showStaticView(views.snacks);
|
showStaticView(views.snacks);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.linkAbout?.addEventListener("click", (event) => {
|
ui.linkAbout?.addEventListener("click", (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (views.about) {
|
|
||||||
showStaticView(views.about);
|
showStaticView(views.about);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.linkCart?.addEventListener("click", (event) => {
|
ui.linkCart?.addEventListener("click", (event) => {
|
||||||
@@ -700,35 +951,120 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
showMovieList();
|
showMovieList();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ui.nowRunningToggle?.addEventListener("click", () => {
|
||||||
|
const isCollapsed = ui.nowRunningShell?.classList.toggle("is-collapsed");
|
||||||
|
ui.nowRunningToggle.setAttribute("aria-expanded", String(!isCollapsed));
|
||||||
|
ui.nowRunningToggle.setAttribute("aria-label", isCollapsed ? "Weitere Filme anzeigen" : "Filme einklappen");
|
||||||
|
});
|
||||||
|
|
||||||
ui.checkoutBtn?.addEventListener("click", showCheckoutStart);
|
ui.checkoutBtn?.addEventListener("click", showCheckoutStart);
|
||||||
ui.backHomeBtn?.addEventListener("click", showHome);
|
ui.backHomeBtn?.addEventListener("click", showHome);
|
||||||
};
|
};
|
||||||
|
|
||||||
const bindProgramActions = () => {
|
const bindProgramActions = () => {
|
||||||
views.moviesGrid?.addEventListener("click", (event:any) => {
|
views.moviesGrid?.addEventListener("click", (event) => {
|
||||||
const trigger = event.target.closest(".open-program-btn");
|
const trigger = event.target.closest(".open-program-btn");
|
||||||
if (!trigger) {
|
if (trigger) {
|
||||||
|
const programIndex = Number(trigger.dataset.programIndex || 0);
|
||||||
|
showMovieList(programIndex);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const programIndex = Number(trigger.dataset.programIndex) || 0;
|
const runningCard = event.target.closest(".running-poster");
|
||||||
showMovieList(programIndex);
|
if (runningCard) {
|
||||||
|
const programIndex = Number(runningCard.dataset.programIndex || 0);
|
||||||
|
openMovieDetail(programIndex, runningCard);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.movieProgramList?.addEventListener("click", (event:any) => {
|
views.moviesGrid?.addEventListener("keydown", (event) => {
|
||||||
const dayButton = event.target.closest(".program-day-tab");
|
if (event.key !== "Enter" && event.key !== " ") {
|
||||||
if (!dayButton) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const programIndex = Number(dayButton.dataset.programIndex || 0);
|
if (event.target.closest(".open-program-btn")) {
|
||||||
const dayIndex = Number(dayButton.dataset.dayIndex || 0);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const tabRow = dayButton.closest(".program-day-tabs");
|
const runningCard = event.target.closest(".running-poster");
|
||||||
tabRow?.querySelectorAll(".program-day-tab").forEach((tab: { classList: { remove: (arg0: string) => any; }; }) => tab.classList.remove("active"));
|
if (!runningCard) {
|
||||||
dayButton.classList.add("active");
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
renderScheduleRows(programIndex, dayIndex);
|
event.preventDefault();
|
||||||
|
const programIndex = Number(runningCard.dataset.programIndex || 0);
|
||||||
|
openMovieDetail(programIndex, runningCard);
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.movieProgramList?.addEventListener("click", (event) => {
|
||||||
|
const dayButton = event.target.closest(".program-day-tab");
|
||||||
|
if (dayButton) {
|
||||||
|
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) => tab.classList.remove("active"));
|
||||||
|
dayButton.classList.add("active");
|
||||||
|
|
||||||
|
renderScheduleRows(programIndex, dayIndex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.target.closest("button, a, input, select, textarea")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const movieCard = event.target.closest(".program-card");
|
||||||
|
if (!movieCard) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const programIndex = Number(movieCard.dataset.programIndex || 0);
|
||||||
|
openMovieDetail(programIndex, movieCard);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const bindMovieDetailOverlay = () => {
|
||||||
|
document.addEventListener("click", (event) => {
|
||||||
|
const overlay = event.target.closest("#movie-detail-overlay");
|
||||||
|
if (!overlay) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.target.closest(".movie-detail-close")) {
|
||||||
|
closeMovieDetail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dayButton = event.target.closest(".movie-detail-day-tab");
|
||||||
|
if (dayButton) {
|
||||||
|
const dayIndex = Number(dayButton.dataset.dayIndex || 0);
|
||||||
|
overlay.querySelectorAll(".movie-detail-day-tab").forEach((tab) => tab.classList.remove("active"));
|
||||||
|
dayButton.classList.add("active");
|
||||||
|
renderMovieDetailScheduleRows(activeMovieDetail.index, dayIndex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trailerPreview = event.target.closest(".movie-trailer-preview");
|
||||||
|
if (trailerPreview) {
|
||||||
|
const trailerId = getYoutubeVideoId(trailerPreview.dataset.trailerId);
|
||||||
|
const frame = trailerPreview.closest(".movie-trailer-frame");
|
||||||
|
if (!trailerId || !frame) {
|
||||||
|
trailerPreview.classList.add("is-missing");
|
||||||
|
trailerPreview.querySelector("strong").textContent = "Trailer-ID in main.js hinterlegen";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.innerHTML = `
|
||||||
|
<iframe src="https://www.youtube-nocookie.com/embed/${encodeURIComponent(trailerId)}?autoplay=1&rel=0" title="YouTube Trailer" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
closeMovieDetail();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -751,7 +1087,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
|
|
||||||
openButtons.forEach((button) => {
|
openButtons.forEach((button) => {
|
||||||
button.addEventListener("click", () => {
|
button.addEventListener("click", () => {
|
||||||
const targetId = button.getAttribute("data-home-view-open") as keyof typeof targetMap;
|
const targetId = button.getAttribute("data-home-view-open");
|
||||||
const target = targetId ? targetMap[targetId] : null;
|
const target = targetId ? targetMap[targetId] : null;
|
||||||
if (target) {
|
if (target) {
|
||||||
showStaticView(target);
|
showStaticView(target);
|
||||||
@@ -804,12 +1140,11 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
|
|
||||||
const THEME_KEY = "eagleTheme";
|
const THEME_KEY = "eagleTheme";
|
||||||
|
|
||||||
const applyTheme = (theme: any) => {
|
const applyTheme = (theme) => {
|
||||||
const isLight = theme === "light";
|
const isLight = theme === "light";
|
||||||
document.body.classList.toggle("theme-light", isLight);
|
document.body.classList.toggle("theme-light", isLight);
|
||||||
document.body.classList.toggle("theme-dark", !isLight);
|
document.body.classList.toggle("theme-dark", !isLight);
|
||||||
//@ts-ignore
|
ui.themeToggle.classList.toggle("is-light", isLight);
|
||||||
ui.themeToggle.classList.toggle("is-light", isLight);
|
|
||||||
localStorage.setItem(THEME_KEY, isLight ? "light" : "dark");
|
localStorage.setItem(THEME_KEY, isLight ? "light" : "dark");
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -825,14 +1160,18 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
const bindAccountActions = () => {
|
const bindAccountActions = () => {
|
||||||
const registerModal = document.getElementById("register-modal");
|
const registerModal = document.getElementById("register-modal");
|
||||||
const forgotModal = document.getElementById("forgot-modal");
|
const forgotModal = document.getElementById("forgot-modal");
|
||||||
const forgotEmailInput = document.getElementById("forgot-email") as HTMLInputElement;
|
const forgotEmailInput = document.getElementById("forgot-email");
|
||||||
const resetMessage = document.getElementById("reset-message");
|
const resetMessage = document.getElementById("reset-message");
|
||||||
const loginError = document.getElementById("login-error");
|
const loginError = document.getElementById("login-error");
|
||||||
const loginEmailInput = document.getElementById("login-email");
|
const loginEmailInput = document.getElementById("login-email");
|
||||||
const loginPasswordInput = document.getElementById("login-password");
|
const loginPasswordInput = document.getElementById("login-password");
|
||||||
|
|
||||||
const openModal = (modal: HTMLElement | null) => modal?.classList.remove("hidden");
|
if (typeof initAccountFieldValidation === "function") {
|
||||||
const closeModal = (modal: HTMLElement | null) => modal?.classList.add("hidden");
|
initAccountFieldValidation();
|
||||||
|
}
|
||||||
|
|
||||||
|
const openModal = (modal) => modal?.classList.remove("hidden");
|
||||||
|
const closeModal = (modal) => modal?.classList.add("hidden");
|
||||||
const triggerLogin = () => {
|
const triggerLogin = () => {
|
||||||
loginError?.classList.add("hidden");
|
loginError?.classList.add("hidden");
|
||||||
if (typeof loginUser === "function") {
|
if (typeof loginUser === "function") {
|
||||||
@@ -841,6 +1180,24 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
document.getElementById("btn-open-register")?.addEventListener("click", () => {
|
document.getElementById("btn-open-register")?.addEventListener("click", () => {
|
||||||
|
registerModal?.querySelectorAll("input, select").forEach((input) => {
|
||||||
|
if (input.type !== "hidden" && input.tagName !== "SELECT") {
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
if (input.tagName === "SELECT") {
|
||||||
|
input.value = "Deutschland";
|
||||||
|
}
|
||||||
|
if (input.id === "reg-salutation") {
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
input.dataset.touched = "";
|
||||||
|
if (typeof clearFieldError === "function") {
|
||||||
|
clearFieldError(input);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
registerModal?.querySelectorAll(".gender-selector button").forEach((button) => {
|
||||||
|
button.classList.remove("active");
|
||||||
|
});
|
||||||
openModal(registerModal);
|
openModal(registerModal);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -857,6 +1214,10 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
document.getElementById("btn-login-account")?.addEventListener("click", triggerLogin);
|
document.getElementById("btn-login-account")?.addEventListener("click", triggerLogin);
|
||||||
|
|
||||||
[loginEmailInput, loginPasswordInput].forEach((input) => {
|
[loginEmailInput, loginPasswordInput].forEach((input) => {
|
||||||
|
input?.addEventListener("input", () => {
|
||||||
|
loginError?.classList.add("hidden");
|
||||||
|
});
|
||||||
|
|
||||||
input?.addEventListener("keydown", (event) => {
|
input?.addEventListener("keydown", (event) => {
|
||||||
if (event.key !== "Enter") {
|
if (event.key !== "Enter") {
|
||||||
return;
|
return;
|
||||||
@@ -868,7 +1229,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("btn-forgot-password")?.addEventListener("click", () => {
|
document.getElementById("btn-forgot-password")?.addEventListener("click", () => {
|
||||||
if (forgotEmailInput != null) {
|
if (forgotEmailInput) {
|
||||||
forgotEmailInput.value = "";
|
forgotEmailInput.value = "";
|
||||||
}
|
}
|
||||||
resetMessage?.classList.add("hidden");
|
resetMessage?.classList.add("hidden");
|
||||||
@@ -913,10 +1274,10 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const bindGlobalDocumentClicks = () => {
|
const bindGlobalDocumentClicks = () => {
|
||||||
document.addEventListener("click", (event: any) => {
|
document.addEventListener("click", (event) => {
|
||||||
if (event.target.classList.contains("opt-btn")) {
|
if (event.target.classList.contains("opt-btn")) {
|
||||||
const optionGroup = event.target.parentElement;
|
const optionGroup = event.target.parentElement;
|
||||||
optionGroup?.querySelectorAll(".opt-btn").forEach((button: { classList: { remove: (arg0: string) => any; }; }) => button.classList.remove("active"));
|
optionGroup?.querySelectorAll(".opt-btn").forEach((button) => button.classList.remove("active"));
|
||||||
event.target.classList.add("active");
|
event.target.classList.add("active");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -925,17 +1286,15 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
const row = deleteBtn.closest(".cart-item-row");
|
const row = deleteBtn.closest(".cart-item-row");
|
||||||
if (row) {
|
if (row) {
|
||||||
row.classList.add("slide-out-left");
|
row.classList.add("slide-out-left");
|
||||||
row.querySelectorAll("button").forEach((button: { disabled: boolean; }) => {
|
row.querySelectorAll("button").forEach((button) => {
|
||||||
button.disabled = true;
|
button.disabled = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
//@ts-ignore
|
removeFromCartByKey(deleteBtn.dataset.key);
|
||||||
removeFromCartByKey(deleteBtn.dataset.key); //TODO: removeFromCartByKey doesnt exist
|
|
||||||
}, 380);
|
}, 380);
|
||||||
} else {
|
} else {
|
||||||
//@ts-ignore
|
removeFromCartByKey(deleteBtn.dataset.key);
|
||||||
removeFromCartByKey(deleteBtn.dataset.key); //TODO: removeFromCartByKey doesnt exist
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -964,7 +1323,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedItem = cart.find((item: { category: string; seatId: any; hall: any; time: any; title: any; }) => {
|
const relatedItem = cart.find((item) => {
|
||||||
const infoText = item.category === "movie"
|
const infoText = item.category === "movie"
|
||||||
? `Sitz: ${item.seatId} (${item.hall})`
|
? `Sitz: ${item.seatId} (${item.hall})`
|
||||||
: item.time;
|
: item.time;
|
||||||
@@ -978,7 +1337,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
if (action === "plus") {
|
if (action === "plus") {
|
||||||
cart.push({ ...relatedItem, id: Date.now() + Math.random() });
|
cart.push({ ...relatedItem, id: Date.now() + Math.random() });
|
||||||
} else {
|
} else {
|
||||||
const keyList = cart.map((item: { category: string; seatId: any; hall: any; time: any; title: any; }) => {
|
const keyList = cart.map((item) => {
|
||||||
const infoText = item.category === "movie"
|
const infoText = item.category === "movie"
|
||||||
? `Sitz: ${item.seatId} (${item.hall})`
|
? `Sitz: ${item.seatId} (${item.hall})`
|
||||||
: item.time;
|
: item.time;
|
||||||
@@ -997,7 +1356,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const bindSnacksActions = () => {
|
const bindSnacksActions = () => {
|
||||||
ui.snacksView?.addEventListener("click", (event:any) => {
|
ui.snacksView?.addEventListener("click", (event) => {
|
||||||
const sizeChip = event.target.closest(".size-chip");
|
const sizeChip = event.target.closest(".size-chip");
|
||||||
if (!sizeChip) {
|
if (!sizeChip) {
|
||||||
return;
|
return;
|
||||||
@@ -1041,7 +1400,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
}, 800);
|
}, 800);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelectorAll(".tab-btn").forEach((button: any) => {
|
document.querySelectorAll(".tab-btn").forEach((button) => {
|
||||||
button.addEventListener("click", () => {
|
button.addEventListener("click", () => {
|
||||||
document.querySelectorAll(".tab-btn").forEach((tab) => tab.classList.remove("active"));
|
document.querySelectorAll(".tab-btn").forEach((tab) => tab.classList.remove("active"));
|
||||||
button.classList.add("active");
|
button.classList.add("active");
|
||||||
@@ -1078,9 +1437,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
// @ts-ignore
|
|
||||||
window.removeFromCartByKey = function removeFromCartByKey(key: string) {
|
window.removeFromCartByKey = function removeFromCartByKey(key) {
|
||||||
cart = cart.filter((item: { category: string; seatId: any; hall: any; time: any; title: any; }) => {
|
cart = cart.filter((item) => {
|
||||||
const infoText = item.category === "movie"
|
const infoText = item.category === "movie"
|
||||||
? `Sitz: ${item.seatId} (${item.hall})`
|
? `Sitz: ${item.seatId} (${item.hall})`
|
||||||
: item.time;
|
: item.time;
|
||||||
@@ -1095,6 +1454,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
initThemeToggle();
|
initThemeToggle();
|
||||||
bindNavigation();
|
bindNavigation();
|
||||||
bindProgramActions();
|
bindProgramActions();
|
||||||
|
bindMovieDetailOverlay();
|
||||||
bindHomeInfoNavigation();
|
bindHomeInfoNavigation();
|
||||||
bindAccountActions();
|
bindAccountActions();
|
||||||
bindGlobalDocumentClicks();
|
bindGlobalDocumentClicks();
|
||||||
@@ -1105,8 +1465,3 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
updateCartBadge?.();
|
updateCartBadge?.();
|
||||||
renderCheckout?.();
|
renderCheckout?.();
|
||||||
});
|
});
|
||||||
|
|
||||||
export function emptyCart() {
|
|
||||||
cart = []
|
|
||||||
return
|
|
||||||
}
|
|
||||||
3093
package-lock.json
generated
3093
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
25
package.json
25
package.json
@@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
469
src/account.ts
469
src/account.ts
@@ -1,469 +0,0 @@
|
|||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,8 +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
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
{
|
|
||||||
// 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