Compare commits

6 Commits

18 changed files with 1202 additions and 51 deletions

View File

@@ -19,15 +19,39 @@ function normalizeUser(user) {
}
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 || "",
password: user.password || "",
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("&", "&amp;")
@@ -58,45 +82,56 @@ 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 || "";
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.email.toLowerCase() === email);
if (existingUser) {
alert("E-Mail bereits registriert");
setFieldError(document.getElementById("reg-email"), "Diese E-Mail ist bereits registriert.");
return;
}
const newUser = {
salutation,
firstName,
lastName,
birthday,
street,
houseNumber,
zip,
city,
country,
email,
password,
passwordHash: hashPassword(password),
orders: [],
paymentMethods: []
};
@@ -107,22 +142,28 @@ function registerUser() {
persistUsers();
persistCurrentUser();
alert("Registrierung erfolgreich");
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.password === password
(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;
}
@@ -146,8 +187,16 @@ function openAccountDashboard() {
<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>
@@ -162,19 +211,280 @@ function openAccountDashboard() {
renderPersonalInfo();
}
function renderPersonalInfo() {
function renderPersonalInfo(isEditing = false) {
const target = document.getElementById("account-tab-content");
if (!target || !currentUser) {
return;
}
if (isEditing) {
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 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() {
@@ -220,7 +530,7 @@ function renderOrders() {
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>
<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>
@@ -293,7 +603,7 @@ function renderPayments() {
target.innerHTML = `
<div class="account-card">
<h3>Zahlungsmethoden</h3>
<p class="account-payments-note">Platzhalter zum Hinterlegen deiner Logos oder Anbieter-Informationen.</p>
<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">
@@ -314,14 +624,14 @@ function renderPayments() {
<img src="img/applepay.png" alt="Apple Pay">
</div>
<h4>Apple Pay</h4>
<p>Geraet freischalten</p>
<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 verknuepfen</p>
<p>Wallet verknüpfen</p>
</button>
</div>
</div>

BIN
img/astronaut-rockypopcorn.jpg LFS Normal file

Binary file not shown.

BIN
img/cashtruck.jpg LFS Normal file

Binary file not shown.

BIN
img/fsk-0.png LFS Normal file

Binary file not shown.

BIN
img/fsk-12.png LFS Normal file

Binary file not shown.

BIN
img/fsk-16.png LFS Normal file

Binary file not shown.

BIN
img/fsk-18.png LFS Normal file

Binary file not shown.

BIN
img/fsk-6.png LFS Normal file

Binary file not shown.

BIN
img/nachokombigross.png LFS Normal file

Binary file not shown.

BIN
img/nachokombimittel.png LFS Normal file

Binary file not shown.

BIN
img/popcornkombigross.png LFS Normal file

Binary file not shown.

BIN
img/popcornkombiklein.png LFS Normal file

Binary file not shown.

BIN
img/popcornkombimittel.png LFS Normal file

Binary file not shown.

BIN
img/screammetalpopcorn.png LFS Normal file

Binary file not shown.

BIN
img/zoomania-2-logo.png LFS Normal file

Binary file not shown.

View File

@@ -51,7 +51,14 @@
<h3>Jetzt läuft</h3>
<span>Heute im Fokus</span>
</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>&gt;</span>
</button>
</div>
</div>
</div>
<div class="home-inline-showcase reveal-on-scroll">
@@ -289,7 +296,7 @@
<img src="img/Zoomania-2.jpg" alt="Zoomania 2 Logo">
<div>
<span>Zoomania 2</span>
<h2>City Edition</h2>
<h2>Tiereische Collection</h2>
</div>
</div>
<div class="limited-special-grid">
@@ -298,7 +305,7 @@
<div class="snack-info">
<span class="badge">Metallbecher</span>
<h3>Limitierter Metallbecher - Zoomania 2</h3>
<p class="snack-card-note">Sammlerbecher mit Popcornfüllung, wahlweise süß oder salzig.</p>
<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>
@@ -327,16 +334,16 @@
<img src="img/screamvii.jpg" alt="Scream VII Logo">
<div>
<span>Scream VII</span>
<h2>Horror Collectors</h2>
<h2>Horror Collection</h2>
</div>
</div>
<div class="limited-special-grid compact-specials">
<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">Dunkler Sammlerbecher passend zum Horrorabend.</p>
<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>
@@ -346,6 +353,21 @@
</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>
@@ -357,13 +379,13 @@
<h2>Space Collection</h2>
</div>
</div>
<div class="limited-special-grid compact-specials">
<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">Großer Sammlerbecher im Sci-Fi-Look für deine Mission ins Kino.</p>
<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>
@@ -373,6 +395,21 @@
</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>
@@ -381,7 +418,7 @@
<img src="img/hoppers.jpg" alt="Hoppers Logo">
<div>
<span>Hoppers</span>
<h2>Family Specials</h2>
<h2>Biber Specials (Ist das eine Eidechse?)</h2>
</div>
</div>
<div class="limited-special-grid">
@@ -390,7 +427,7 @@
<div class="snack-info">
<span class="badge">Metallbecher</span>
<h3>Limitierter Metallbecher - Hoppers</h3>
<p class="snack-card-note">Stabiler Becher mit Popcornfüllung.</p>
<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>
@@ -405,7 +442,7 @@
<div class="snack-info">
<span class="badge">Collector</span>
<h3>Limitierter Sammelbecher - Hoppers</h3>
<p class="snack-card-note">Sammlerbecher mit warmem Popcornmoment.</p>
<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>
@@ -434,7 +471,7 @@
<img src="img/mariogalaxy.jpg" alt="Mario Galaxy Logo">
<div>
<span>Super Mario Galaxy</span>
<h2>Galaxy Specials</h2>
<h2>Galaxy Collection</h2>
</div>
</div>
<div class="limited-special-grid">
@@ -443,7 +480,7 @@
<div class="snack-info">
<span class="badge">Yoshi Cup</span>
<h3>Limitierter Sammelbecher - Yoshi Becher</h3>
<p class="snack-card-note">Verspielter Sammlerbecher für Mario-Fans.</p>
<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>
@@ -640,6 +677,7 @@
</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>
@@ -649,6 +687,7 @@
</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>
@@ -658,6 +697,7 @@
</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>
@@ -785,6 +825,33 @@
</div>
</div>
</div>
<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>
@@ -919,8 +986,14 @@
<div class="account-login-box">
<h2>Mein Konto</h2>
<input type="email" id="login-email" placeholder="E-Mail">
<input type="password" id="login-password" placeholder="Passwort">
<label class="auth-field">
<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>
<div id="login-error" class="hidden">
@@ -935,14 +1008,80 @@
<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">&times;</button>
<h2>Registrieren</h2>
<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">
<input type="email" id="reg-email" placeholder="E-Mail">
<input type="password" id="reg-password" placeholder="Passwort">
<div class="register-form-grid">
<section class="register-form-section">
<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>
</div>
</div>

66
main.js
View File

@@ -37,7 +37,9 @@ document.addEventListener("DOMContentLoaded", () => {
heroDots: document.getElementById("hero-dots"),
heroTitle: document.getElementById("hero-title"),
heroText: document.getElementById("hero-text"),
nowRunningShell: document.getElementById("now-running-shell"),
nowRunningRow: document.getElementById("now-running-row"),
nowRunningToggle: document.getElementById("now-running-toggle"),
movieProgramList: document.getElementById("movie-program-list"),
checkoutBtn: document.getElementById("btn-checkout-final"),
backHomeBtn: document.getElementById("btn-back-home"),
@@ -62,8 +64,8 @@ document.addEventListener("DOMContentLoaded", () => {
duration: 148,
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 DeSnake! ",
poster: "img/zoomania-2.jpg",
backdrop: "img/zoomania-2.jpg"
poster: "img/Zoomania-2.jpg",
backdrop: "img/Zoomania-2.jpg"
},
{
title: "Shelter",
@@ -221,9 +223,11 @@ document.addEventListener("DOMContentLoaded", () => {
{
title: "Cash Truck",
genre: "Action",
duration: 118,
duration: 98,
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 Hs 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",
@@ -401,6 +405,38 @@ document.addEventListener("DOMContentLoaded", () => {
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
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 formatDateShort = (dateObj) => {
const day = String(dateObj.getDate()).padStart(2, "0");
const month = String(dateObj.getMonth() + 1).padStart(2, "0");
@@ -548,6 +584,12 @@ document.addEventListener("DOMContentLoaded", () => {
</div>
</article>
`).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, dayIndex) => {
@@ -583,17 +625,23 @@ document.addEventListener("DOMContentLoaded", () => {
<small>${escapeHtml(formatDateShort(day.date))}</small>
</button>
`).join("");
const hallPills = getMovieHalls(movie).map((hall) => `
<span>${escapeHtml(hall)}</span>
`).join("");
return `
<article class="detailed-card program-card reveal-on-scroll" data-program-index="${programIndex}">
<div class="card-left">
<img src="${escapeHtml(movie.poster)}" alt="${escapeHtml(movie.title)}">
<span class="fsk fsk-${escapeHtml(movie.fsk)}">${escapeHtml(movie.fsk)}</span>
</div>
<div class="card-right">
<div class="card-header">
<h2>${escapeHtml(movie.title)}</h2>
<span class="duration">${movie.duration} Min. | ${escapeHtml(movie.genre)} | FSK: ${escapeHtml(movie.fsk)}</span>
<span class="duration movie-meta-line">${movie.duration} Min. | ${escapeHtml(movie.genre)} | FSK: ${renderFskMetaLogo(movie)}</span>
</div>
<div class="program-halls-summary">
<strong>Kinosaal</strong>
<div>${hallPills}</div>
</div>
<p class="description">${escapeHtml(movie.description)}</p>
@@ -688,6 +736,12 @@ document.addEventListener("DOMContentLoaded", () => {
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.backHomeBtn?.addEventListener("click", showHome);
};

606
style.css
View File

@@ -4238,3 +4238,609 @@ body.theme-light .limited-special-card {
align-items: flex-start;
}
}
/* --- Account payments readability + limited specials polish --- */
.account-payment-grid {
gap: 14px;
}
.account-payment-card {
min-height: 168px;
display: flex;
flex-direction: column;
gap: 8px;
background:
linear-gradient(145deg, rgba(0, 113, 227, 0.14), rgba(255, 255, 255, 0.025) 44%),
#171b24;
border: 1px solid rgba(154, 187, 227, 0.2);
border-radius: 12px;
color: #f5f8ff;
box-shadow: 0 14px 30px rgba(0, 0, 0, 0.22);
transition: transform 0.2s ease, border-color 0.2s ease, background 0.2s ease;
}
.account-payment-card h4 {
color: #f8fbff;
font-size: 0.98rem;
}
.account-payment-card p {
color: #b7c4d8;
line-height: 1.45;
}
.account-payments-note {
color: #b4bfd0;
line-height: 1.5;
}
.account-payment-card .payment-logo-slot {
height: 66px;
background: #f7f9fd;
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 10px;
}
.account-payment-card .payment-logo-slot img {
max-width: 78%;
max-height: 38px;
filter: drop-shadow(0 5px 10px rgba(20, 28, 40, 0.2));
}
.account-payment-card:hover {
background:
linear-gradient(145deg, rgba(0, 113, 227, 0.22), rgba(255, 255, 255, 0.04) 48%),
#1b2230;
border-color: rgba(91, 169, 255, 0.62);
}
body.theme-light .account-payment-card {
background: #f7faff !important;
border-color: rgba(35, 66, 108, 0.16) !important;
color: #14243d;
box-shadow: 0 12px 26px rgba(31, 62, 104, 0.1);
}
body.theme-light .account-payment-card:hover {
background: #ffffff !important;
border-color: rgba(0, 113, 227, 0.32) !important;
}
body.theme-light .account-payment-card h4 {
color: #10213a;
}
body.theme-light .account-payment-card p {
color: #52647f;
}
body.theme-light .account-payment-card .payment-logo-slot {
background: #ffffff;
border-color: rgba(28, 56, 94, 0.12);
}
.limited-specials-hero {
border-radius: 16px;
}
.limited-specials-hero h2 {
font-size: 3.35rem;
letter-spacing: 0;
}
.limited-specials-hero p {
font-size: 1rem;
}
.special-film-heading {
border-radius: 14px;
}
.special-film-heading img {
width: 86px;
height: 86px;
border-radius: 10px;
}
.special-film-heading h2 {
font-size: 1.65rem;
letter-spacing: 0;
}
.limited-special-grid {
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
align-items: stretch;
}
.limited-special-card {
border-radius: 12px;
box-shadow: 0 18px 36px rgba(0, 0, 0, 0.2);
}
.limited-special-card .snack-img {
height: 210px;
padding: 12px;
}
.limited-special-card .snack-img img {
padding: 0;
border-radius: 10px;
object-fit: contain;
}
.limited-special-card .snack-info {
align-items: flex-start;
text-align: left;
gap: 10px;
}
.limited-special-card .snack-info h3 {
margin-bottom: 0;
font-size: 1.1rem;
line-height: 1.3;
}
.limited-special-card .snack-card-note {
min-height: 42px;
line-height: 1.5;
}
.limited-special-card .option-group,
.limited-special-card .size-selector {
justify-content: flex-start;
}
.limited-special-card .size-chip {
min-width: 132px;
padding: 8px 12px;
}
.wide-special {
grid-column: span 2;
grid-template-columns: minmax(210px, 38%) 1fr;
}
.wide-special .snack-img {
min-height: 250px;
}
body.theme-light .limited-special-card {
box-shadow: 0 16px 34px rgba(31, 62, 104, 0.12);
}
body.theme-light .limited-special-card .snack-img {
background: #f3f7ff;
}
@media (max-width: 760px) {
.limited-specials-hero h2 {
font-size: 2.25rem;
}
.limited-special-card .snack-img {
height: 190px;
}
.wide-special {
grid-column: span 1;
}
}
/* --- 2026-05 refinements: limited specials, program rows, home collapse --- */
.limited-special-card .snack-info {
align-items: center;
text-align: center;
}
.limited-special-card .badge {
align-self: center;
}
.limited-special-card .option-group,
.limited-special-card .size-selector {
width: 100%;
justify-content: center;
}
.limited-special-card.wide-special {
grid-column: span 1;
grid-template-columns: minmax(118px, 42%) minmax(0, 1fr);
min-height: 220px;
}
.limited-special-card.wide-special .snack-img {
height: 100%;
min-height: 220px;
padding: 0;
background: #0b0f17;
}
.limited-special-card.wide-special .snack-img img {
width: 100%;
height: 100%;
border-radius: 0;
object-fit: cover;
object-position: center;
}
.limited-special-card.wide-special .snack-info {
padding: 16px 14px;
gap: 7px;
}
.limited-special-card.wide-special .badge {
margin-bottom: 2px;
font-size: 0.66rem;
}
.limited-special-card.wide-special .snack-info h3 {
font-size: 0.98rem;
}
.limited-special-card.wide-special .snack-card-note {
min-height: 0;
font-size: 0.82rem;
line-height: 1.38;
}
.limited-special-card.wide-special .size-chip {
min-width: 116px;
padding: 7px 10px;
}
.now-running-shell {
position: relative;
}
.now-running-row {
--running-card-height: 404px;
}
.now-running-shell.is-collapsed .now-running-row {
max-height: calc((var(--running-card-height) * 3) + 96px);
overflow: hidden;
}
.now-running-fade {
display: none;
}
.now-running-shell.is-collapsed .now-running-fade {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 140px;
display: flex;
align-items: flex-end;
justify-content: center;
padding-bottom: 12px;
background: linear-gradient(180deg, rgba(7, 8, 11, 0.08), rgba(7, 8, 11, 0.92) 72%);
backdrop-filter: blur(3px);
pointer-events: none;
}
.now-running-toggle {
width: 44px;
height: 44px;
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 50%;
background: rgba(16, 19, 27, 0.92);
color: #f5f8ff;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.34);
pointer-events: auto;
}
.now-running-toggle span {
display: block;
transform: rotate(90deg);
font-size: 1.35rem;
line-height: 1;
}
.now-running-shell:not(.is-collapsed) .now-running-fade {
position: static;
display: flex;
justify-content: center;
height: auto;
padding-top: 16px;
background: transparent;
backdrop-filter: none;
}
.now-running-shell:not(.is-collapsed) .now-running-toggle span {
transform: rotate(-90deg);
}
.program-halls-summary {
display: grid;
grid-template-columns: 88px minmax(0, 1fr);
gap: 10px;
align-items: center;
margin: -2px 0 14px;
}
.program-halls-summary strong {
color: #9aa8ba;
font-size: 0.76rem;
text-transform: uppercase;
letter-spacing: 0.9px;
}
.program-halls-summary div {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.program-halls-summary span {
border: 1px solid rgba(0, 113, 227, 0.36);
border-radius: 999px;
background: rgba(0, 113, 227, 0.12);
color: #c7ddf8;
padding: 4px 9px;
font-size: 0.76rem;
font-weight: 700;
}
.fsk-logo-field {
position: absolute;
left: 12px;
bottom: 12px;
min-width: 78px;
min-height: 42px;
max-width: 112px;
border: 1px dashed rgba(255, 255, 255, 0.42);
border-radius: 6px;
background: rgba(9, 12, 17, 0.82);
color: #dbe7f7;
display: flex;
align-items: center;
justify-content: center;
padding: 5px;
text-align: center;
font-size: 0.66rem;
font-weight: 800;
backdrop-filter: blur(4px);
}
.fsk-logo-field img {
max-width: 100%;
max-height: 56px;
display: block;
object-fit: contain;
}
.fsk-logo-field.has-logo:not(.missing-logo) {
border-style: solid;
background: rgba(255, 255, 255, 0.94);
}
.fsk-logo-field.has-logo:not(.missing-logo) span {
display: none;
}
.program-time-row:hover {
background: rgba(255, 255, 255, 0.055);
border-color: rgba(255, 255, 255, 0.16);
transform: translateX(0);
}
body.theme-dark .program-time-row:hover {
background: rgba(255, 255, 255, 0.055);
border-color: rgba(255, 255, 255, 0.16);
}
body.theme-light .program-halls-summary strong {
color: #52647f;
}
body.theme-light .program-halls-summary span {
color: #144274;
border-color: rgba(0, 113, 227, 0.28);
background: rgba(0, 113, 227, 0.1);
}
body.theme-light .fsk-logo-field {
background: rgba(255, 255, 255, 0.94);
border-color: rgba(28, 56, 94, 0.32);
color: #273952;
}
body.theme-light .now-running-shell.is-collapsed .now-running-fade {
background: linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(247, 249, 253, 0.94) 72%);
}
body.theme-light .now-running-toggle {
background: #ffffff;
color: #173050;
border-color: rgba(28, 56, 94, 0.18);
box-shadow: 0 12px 24px rgba(31, 62, 104, 0.16);
}
body.theme-light .program-time-row:hover {
border-color: rgba(24, 52, 87, 0.24);
background: #ebf3ff;
transform: translateX(0);
}
@media (max-width: 760px) {
.limited-special-card.wide-special {
grid-template-columns: 1fr;
}
.limited-special-card.wide-special .snack-img {
height: 190px;
min-height: 190px;
}
.now-running-row {
--running-card-height: 402px;
}
.program-halls-summary {
grid-template-columns: 1fr;
gap: 6px;
}
}
/* --- 2026-05 follow-up adjustments --- */
.movie-meta-line {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
}
.fsk-meta-logo {
min-width: 42px;
min-height: 22px;
display: inline-flex;
align-items: center;
justify-content: center;
vertical-align: middle;
}
.fsk-meta-logo img {
max-width: 58px;
max-height: 26px;
object-fit: contain;
display: block;
}
.fsk-meta-logo.has-logo:not(.missing-logo) span {
display: none;
}
.fsk-meta-logo.missing-logo {
padding: 2px 7px;
border-radius: 4px;
border: 1px solid rgba(255, 255, 255, 0.22);
background: rgba(255, 255, 255, 0.08);
color: #dbe7f7;
font-size: 0.68rem;
font-weight: 800;
}
.program-time-row {
grid-template-columns: 1fr 1fr 1fr;
}
.schedule-header span:nth-child(2),
.schedule-header span:nth-child(3),
.program-time-row .hall-pill,
.program-time-row .time-btn {
justify-self: center;
text-align: center;
}
.program-time-row .time-btn {
min-width: 64px;
}
.limited-special-grid {
grid-template-columns: repeat(auto-fit, minmax(238px, 1fr));
gap: 16px;
}
.limited-special-card {
border-radius: 10px;
}
.limited-special-card .snack-img {
height: 185px;
padding: 10px;
}
.limited-special-card .snack-info {
padding: 16px 14px;
gap: 8px;
}
.limited-special-card .snack-info h3 {
font-size: 1rem;
line-height: 1.25;
}
.limited-special-card .snack-card-note {
min-height: 34px;
font-size: 0.86rem;
line-height: 1.38;
}
.limited-special-card .option-group {
width: auto;
padding: 3px;
gap: 4px;
}
.limited-special-card .option-group .opt-btn {
flex: 0 0 auto;
min-width: 66px;
padding: 5px 12px;
}
.limited-special-card .size-selector {
width: auto;
}
.limited-special-card .size-chip {
min-width: 116px;
padding: 7px 11px;
}
.limited-special-card.wide-special {
min-height: 198px;
grid-template-columns: minmax(122px, 40%) minmax(0, 1fr);
}
.limited-special-card.wide-special .snack-img {
min-height: 198px;
padding: 8px;
}
.limited-special-card.wide-special .snack-img img {
border-radius: 8px;
object-fit: contain;
background: #080b10;
}
.limited-special-card.wide-special .snack-info {
padding: 14px 12px;
}
body.theme-light .fsk-meta-logo.missing-logo {
background: #f3f7ff;
border-color: rgba(28, 56, 94, 0.24);
color: #223854;
}
body.theme-light .limited-special-card.wide-special .snack-img img {
background: #f3f7ff;
}
@media (max-width: 760px) {
.program-time-row {
grid-template-columns: 1fr;
}
.schedule-header span:nth-child(2),
.schedule-header span:nth-child(3),
.program-time-row .hall-pill,
.program-time-row .time-btn {
justify-self: flex-start;
text-align: left;
}
.limited-special-card .snack-img {
height: 176px;
}
.limited-special-card.wide-special .snack-img {
min-height: 176px;
}
}