9 Commits

130 changed files with 4573 additions and 13358 deletions

View File

@@ -1,2 +0,0 @@
TMDB_API_TOKEN=yourapitoken
TMDB_API_KEY=yourapikey

4
.gitattributes vendored
View File

@@ -1,2 +1,2 @@
public/img/* filter=lfs diff=lfs merge=lfs -text img/ filter=lfs diff=lfs merge=lfs -text
img/** filter=lfs diff=lfs merge=lfs -text

25
.gitignore vendored
View File

@@ -1,25 +0,0 @@
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
# jetbrains setting folder
.idea/
.claude/

View File

@@ -1,4 +0,0 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}

11
.vscode/launch.json vendored
View File

@@ -1,11 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}

View File

@@ -1,43 +0,0 @@
# Astro Starter Kit: Minimal
```sh
npm create astro@latest -- --template minimal
```
> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
## 🚀 Project Structure
Inside of your Astro project, you'll see the following folders and files:
```text
/
├── public/
├── src/
│ └── pages/
│ └── index.astro
└── package.json
```
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
Any static assets, like images, can be placed in the `public/` directory.
## 🧞 Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `npm install` | Installs dependencies |
| `npm run dev` | Starts local dev server at `localhost:4321` |
| `npm run build` | Build your production site to `./dist/` |
| `npm run preview` | Preview your build locally, before deploying |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI |
## 👀 Want to learn more?
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).

760
account.js Normal file
View 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("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
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>&times;</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>&times;</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>&times;</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>&times;</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();
}

View File

@@ -1,23 +0,0 @@
// @ts-check
import { defineConfig, envField } from 'astro/config';
import react from '@astrojs/react';
import tailwindcss from '@tailwindcss/vite';
// https://astro.build/config
export default defineConfig({
integrations: [react({
include: ['**/react/*']
})],
vite: {
// @ts-ignore
plugins: [tailwindcss({optimize:false})]
},
env: {
schema: {
TMDB_API_TOKEN: envField.string({ context: 'server', access: 'secret'}),
TMDB_API_KEY: envField.string({context: "server", access: "secret"})
}
}
});

View File

@@ -1,36 +1,32 @@
import { seatLayouts, occupiedSeatsData, prices, cart } from "./main.js" let currentBookingContext = null;
import { renderCart, saveCart } from "./cart.js"; let currentHallLayout = null;
import { renderCheckout } from "./checkout.js";
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 &&

View File

@@ -1,10 +1,8 @@
import { cart } from "./main.js"; 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("&", "&amp;") .replaceAll("&", "&amp;")
.replaceAll("<", "&lt;") .replaceAll("<", "&lt;")
@@ -13,14 +11,14 @@ function escapeHtml(value: any) {
.replaceAll("'", "&#39;"); .replaceAll("'", "&#39;");
} }
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();
}; };

View File

@@ -1,15 +1,11 @@
import { currentUser, users } from "./account.js"; function formatCheckoutEuro(value) {
import { renderCart, saveCart } from "./cart.js";
import { cart, emptyCart, occupiedSeatsData } from "./main.js";
function formatCheckoutEuro(value: number) {
return `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`; 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

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/youtube-logo.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> <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>&gt;</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">&times;</button> <button id="btn-close-register" class="modal-close-btn" type="button" aria-label="Registrierung schliessen">&times;</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 type="module" src="dist/main.js"></script> <script src="account.js"></script>
<script type="module" src="dist/cart.js"></script> <script src="cart.js"></script>
<script type="module" src="dist/booking.js"></script> <script src="booking.js"></script>
<script type="module" src="dist/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>

1467
main.js Normal file

File diff suppressed because it is too large Load Diff

6543
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,29 +0,0 @@
{
"name": "kino-astro",
"type": "module",
"version": "0.0.1",
"engines": {
"node": ">=22.12.0"
},
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/react": "^5.0.4",
"@tailwindcss/vite": "^4.2.4",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"astro": "^6.2.0",
"dotenv": "^17.4.2",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"tailwindcss": "^4.2.4",
"vite": "^6.4.2"
},
"devDependencies": {
"@types/node": "^25.6.0"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 655 B

View File

@@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
<style>
path { fill: #000; }
@media (prefers-color-scheme: dark) {
path { fill: #FFF; }
}
</style>
</svg>

Before

Width:  |  Height:  |  Size: 749 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 878 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 362 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 910 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 643 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 481 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 530 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

Some files were not shown because too many files have changed in this diff Show More