Compare commits
1 Commits
f7fc6e4387
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| e13f35a9e0 |
@@ -1,2 +0,0 @@
|
|||||||
TMDB_API_TOKEN=yourapitoken
|
|
||||||
TMDB_API_KEY=yourapikey
|
|
||||||
4
.gitattributes
vendored
@@ -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
@@ -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/
|
|
||||||
4
.vscode/extensions.json
vendored
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"recommendations": ["astro-build.astro-vscode"],
|
|
||||||
"unwantedRecommendations": []
|
|
||||||
}
|
|
||||||
11
.vscode/launch.json
vendored
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"command": "./node_modules/.bin/astro dev",
|
|
||||||
"name": "Development server",
|
|
||||||
"request": "launch",
|
|
||||||
"type": "node-terminal"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
43
README.md
@@ -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).
|
|
||||||
450
account.js
Normal file
@@ -0,0 +1,450 @@
|
|||||||
|
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 {
|
||||||
|
firstName: user.firstName || "",
|
||||||
|
lastName: user.lastName || "",
|
||||||
|
email: user.email || "",
|
||||||
|
password: user.password || "",
|
||||||
|
orders: Array.isArray(user.orders) ? user.orders : [],
|
||||||
|
paymentMethods: Array.isArray(user.paymentMethods) ? user.paymentMethods : []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return String(value || "")
|
||||||
|
.replaceAll("&", "&")
|
||||||
|
.replaceAll("<", "<")
|
||||||
|
.replaceAll(">", ">")
|
||||||
|
.replaceAll('"', """)
|
||||||
|
.replaceAll("'", "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatEuro(value) {
|
||||||
|
return `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistUsers() {
|
||||||
|
localStorage.setItem("eagleUsers", JSON.stringify(users));
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistCurrentUser() {
|
||||||
|
if (currentUser) {
|
||||||
|
localStorage.setItem("currentUser", JSON.stringify(currentUser));
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem("currentUser");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let users = readStorageJson("eagleUsers", []);
|
||||||
|
if (!Array.isArray(users)) {
|
||||||
|
users = [];
|
||||||
|
}
|
||||||
|
users = users.map(normalizeUser).filter(Boolean);
|
||||||
|
|
||||||
|
let currentUser = normalizeUser(readStorageJson("currentUser", null));
|
||||||
|
if (currentUser && currentUser.email) {
|
||||||
|
const storedMatch = users.find((user) => user.email === currentUser.email);
|
||||||
|
if (storedMatch) {
|
||||||
|
currentUser = storedMatch;
|
||||||
|
} else {
|
||||||
|
users.push(currentUser);
|
||||||
|
persistUsers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerUser() {
|
||||||
|
const firstName = document.getElementById("reg-firstname")?.value.trim() || "";
|
||||||
|
const lastName = document.getElementById("reg-lastname")?.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");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newUser = {
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
orders: [],
|
||||||
|
paymentMethods: []
|
||||||
|
};
|
||||||
|
|
||||||
|
users.push(newUser);
|
||||||
|
currentUser = newUser;
|
||||||
|
|
||||||
|
persistUsers();
|
||||||
|
persistCurrentUser();
|
||||||
|
|
||||||
|
alert("Registrierung erfolgreich");
|
||||||
|
document.getElementById("register-modal")?.classList.add("hidden");
|
||||||
|
|
||||||
|
openAccountDashboard();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loginUser() {
|
||||||
|
const email = (document.getElementById("login-email")?.value.trim() || "").toLowerCase();
|
||||||
|
const password = document.getElementById("login-password")?.value || "";
|
||||||
|
|
||||||
|
const user = users.find(
|
||||||
|
(entry) => entry.email.toLowerCase() === email && entry.password === password
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
document.getElementById("login-error")?.classList.remove("hidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentUser = user;
|
||||||
|
persistCurrentUser();
|
||||||
|
openAccountDashboard();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAccountDashboard() {
|
||||||
|
const accountView = document.getElementById("account-view");
|
||||||
|
if (!accountView) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentUser) {
|
||||||
|
accountView.innerHTML = "<div class='account-login-box'><h2>Mein Konto</h2><p>Bitte melde dich an oder registriere dich.</p></div>";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
accountView.innerHTML = `
|
||||||
|
<div class="account-panel">
|
||||||
|
<div class="account-panel-header">
|
||||||
|
<h2>Mein Konto</h2>
|
||||||
|
<button class="account-logout-btn" onclick="logoutUser()">Abmelden</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="account-tabs">
|
||||||
|
<button class="account-tab-btn" onclick="renderPersonalInfo()">Persönliche Daten</button>
|
||||||
|
<button class="account-tab-btn" onclick="renderOrders()">Meine Bestellungen</button>
|
||||||
|
<button class="account-tab-btn" onclick="renderPayments()">Zahlungsmethoden</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="account-tab-content"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
renderPersonalInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPersonalInfo() {
|
||||||
|
const target = document.getElementById("account-tab-content");
|
||||||
|
if (!target || !currentUser) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
target.innerHTML = `
|
||||||
|
<div class="account-card">
|
||||||
|
<p><strong>Vorname:</strong> ${currentUser.firstName || "-"}</p>
|
||||||
|
<p><strong>Nachname:</strong> ${currentUser.lastName || "-"}</p>
|
||||||
|
<p><strong>E-Mail:</strong> ${currentUser.email || "-"}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderOrders() {
|
||||||
|
const target = document.getElementById("account-tab-content");
|
||||||
|
if (!target || !currentUser) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const orders = Array.isArray(currentUser.orders) ? currentUser.orders : [];
|
||||||
|
|
||||||
|
if (!orders.length) {
|
||||||
|
target.innerHTML = `
|
||||||
|
<div class="account-card">
|
||||||
|
<h3>Meine Bestellungen</h3>
|
||||||
|
<p>Noch keine Bestellungen vorhanden.</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderHtml = orders
|
||||||
|
.map((order, index) => {
|
||||||
|
const movieItems = Array.isArray(order.items)
|
||||||
|
? order.items.filter((item) => item.category === "movie")
|
||||||
|
: [];
|
||||||
|
const previewItem = movieItems[0] || (Array.isArray(order.items) ? order.items[0] : null);
|
||||||
|
const previewTitle = previewItem?.title || "Bestellung";
|
||||||
|
const ticketsCount = movieItems.length || (Array.isArray(order.items) ? order.items.length : 0);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<button type="button" class="order-box order-item-btn" data-order-index="${index}">
|
||||||
|
<div class="order-item-head">
|
||||||
|
<h4>${escapeHtml(previewTitle)}</h4>
|
||||||
|
<span>${formatEuro(order.total || 0)}</span>
|
||||||
|
</div>
|
||||||
|
<p><strong>Datum:</strong> ${escapeHtml(order.date || "-")}</p>
|
||||||
|
<p><strong>Anzahl:</strong> ${ticketsCount}x</p>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
target.innerHTML = `
|
||||||
|
<div class="account-orders-shell">
|
||||||
|
<h3>Meine Bestellungen</h3>
|
||||||
|
<p class="account-payments-note">Klicke auf eine Bestellung, um dein Ticket-Detail zu sehen.</p>
|
||||||
|
<div class="account-orders-grid">${orderHtml}</div>
|
||||||
|
<div id="order-ticket-details" class="order-ticket-details hidden"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const detailTarget = document.getElementById("order-ticket-details");
|
||||||
|
const orderButtons = Array.from(target.querySelectorAll(".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">Platzhalter zum Hinterlegen deiner Logos oder Anbieter-Informationen.</p>
|
||||||
|
<div class="account-payment-grid">
|
||||||
|
<button type="button" class="account-payment-card account-pay-trigger" data-pay-modal="pay-modal-card">
|
||||||
|
<div class="payment-logo-slot">
|
||||||
|
<img src="img/mastercard.png" alt="Mastercard">
|
||||||
|
</div>
|
||||||
|
<h4>Visa / Mastercard</h4>
|
||||||
|
<p>Karteninformationen hinterlegen</p>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="account-payment-card account-pay-trigger" data-pay-modal="pay-modal-paypal">
|
||||||
|
<div class="payment-logo-slot">
|
||||||
|
<img src="img/paypal.png" alt="PayPal">
|
||||||
|
</div>
|
||||||
|
<h4>PayPal</h4>
|
||||||
|
<p>Konto verbinden</p>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="account-payment-card account-pay-trigger" data-pay-modal="pay-modal-apple">
|
||||||
|
<div class="payment-logo-slot">
|
||||||
|
<img src="img/applepay.png" alt="Apple Pay">
|
||||||
|
</div>
|
||||||
|
<h4>Apple Pay</h4>
|
||||||
|
<p>Geraet freischalten</p>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="account-payment-card account-pay-trigger" data-pay-modal="pay-modal-google">
|
||||||
|
<div class="payment-logo-slot">
|
||||||
|
<img src="img/googlepay.png" alt="Google Pay">
|
||||||
|
</div>
|
||||||
|
<h4>Google Pay</h4>
|
||||||
|
<p>Wallet verknuepfen</p>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="pay-modal-card" class="pay-modal-overlay hidden">
|
||||||
|
<div class="pay-modal-panel pay-modal-card-style">
|
||||||
|
<button type="button" class="pay-close-btn" data-pay-close>×</button>
|
||||||
|
<div class="pay-modal-head">
|
||||||
|
<img src="img/mastercard.png" alt="Kreditkarte">
|
||||||
|
<h4>Kreditkarte hinterlegen</h4>
|
||||||
|
</div>
|
||||||
|
<div class="pay-form-grid">
|
||||||
|
<label>Kartennummer
|
||||||
|
<input type="text" placeholder="1234 5678 9012 3456">
|
||||||
|
</label>
|
||||||
|
<div class="pay-form-row">
|
||||||
|
<label>Exp. Datum
|
||||||
|
<input type="text" placeholder="MM/JJ">
|
||||||
|
</label>
|
||||||
|
<label>CVV
|
||||||
|
<input type="text" placeholder="123">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<label>Name auf Karte
|
||||||
|
<input type="text" placeholder="Max Mustermann">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="pay-submit-btn">Karte speichern</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="pay-modal-paypal" class="pay-modal-overlay hidden">
|
||||||
|
<div class="pay-modal-panel pay-modal-paypal-style">
|
||||||
|
<button type="button" class="pay-close-btn" data-pay-close>×</button>
|
||||||
|
<div class="pay-modal-head">
|
||||||
|
<img src="img/paypal.png" alt="PayPal">
|
||||||
|
<h4>PayPal verbinden</h4>
|
||||||
|
</div>
|
||||||
|
<p>Einloggen und dein PayPal-Konto mit deinem Kino-Account verbinden.</p>
|
||||||
|
<label>E-Mail
|
||||||
|
<input type="email" placeholder="name@beispiel.de">
|
||||||
|
</label>
|
||||||
|
<label>Passwort
|
||||||
|
<input type="password" placeholder="Passwort">
|
||||||
|
</label>
|
||||||
|
<button type="button" class="pay-submit-btn paypal-btn">Mit PayPal fortfahren</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="pay-modal-apple" class="pay-modal-overlay hidden">
|
||||||
|
<div class="pay-modal-panel pay-modal-apple-style">
|
||||||
|
<button type="button" class="pay-close-btn" data-pay-close>×</button>
|
||||||
|
<div class="pay-modal-head">
|
||||||
|
<img src="img/applepay.png" alt="Apple Pay">
|
||||||
|
<h4>Apple Pay einrichten</h4>
|
||||||
|
</div>
|
||||||
|
<p>Apple Pay wirkt schlicht, klar und fokussiert. Hinterlege hier die bevorzugte Karte für schnelle Zahlungen.</p>
|
||||||
|
<label>Apple-ID E-Mail
|
||||||
|
<input type="email" placeholder="appleid@beispiel.de">
|
||||||
|
</label>
|
||||||
|
<label>Bevorzugte Karte
|
||||||
|
<input type="text" placeholder="Visa, Mastercard, ...">
|
||||||
|
</label>
|
||||||
|
<button type="button" class="pay-submit-btn apple-btn">Zu Wallet hinzufügen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="pay-modal-google" class="pay-modal-overlay hidden">
|
||||||
|
<div class="pay-modal-panel pay-modal-google-style">
|
||||||
|
<button type="button" class="pay-close-btn" data-pay-close>×</button>
|
||||||
|
<div class="pay-modal-head">
|
||||||
|
<img src="img/googlepay.png" alt="Google Pay">
|
||||||
|
<h4>Google Pay einrichten</h4>
|
||||||
|
</div>
|
||||||
|
<p>Verbinde deine Wallet, damit zukünftige Bestellungen in wenigen Klicks abgeschlossen werden.</p>
|
||||||
|
<label>Google-Konto
|
||||||
|
<input type="email" placeholder="konto@gmail.com">
|
||||||
|
</label>
|
||||||
|
<label>Standard-Zahlungsquelle
|
||||||
|
<input type="text" placeholder="z. B. Visa 1234">
|
||||||
|
</label>
|
||||||
|
<button type="button" class="pay-submit-btn google-btn">Wallet verbinden</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const modals = Array.from(target.querySelectorAll(".pay-modal-overlay"));
|
||||||
|
const triggers = Array.from(target.querySelectorAll(".account-pay-trigger"));
|
||||||
|
const closeButtons = Array.from(target.querySelectorAll("[data-pay-close]"));
|
||||||
|
|
||||||
|
const closeAllPaymentModals = () => {
|
||||||
|
modals.forEach((modal) => modal.classList.add("hidden"));
|
||||||
|
document.body.style.overflow = "auto";
|
||||||
|
};
|
||||||
|
|
||||||
|
triggers.forEach((trigger) => {
|
||||||
|
trigger.addEventListener("click", () => {
|
||||||
|
closeAllPaymentModals();
|
||||||
|
const targetId = trigger.getAttribute("data-pay-modal");
|
||||||
|
const modal = targetId ? target.querySelector(`#${targetId}`) : null;
|
||||||
|
|
||||||
|
if (modal) {
|
||||||
|
modal.classList.remove("hidden");
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
closeButtons.forEach((button) => {
|
||||||
|
button.addEventListener("click", closeAllPaymentModals);
|
||||||
|
});
|
||||||
|
|
||||||
|
modals.forEach((modal) => {
|
||||||
|
modal.addEventListener("click", (event) => {
|
||||||
|
if (event.target === modal) {
|
||||||
|
closeAllPaymentModals();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function logoutUser() {
|
||||||
|
currentUser = null;
|
||||||
|
persistCurrentUser();
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
@@ -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"})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
352
booking.js
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
let currentBookingContext = null;
|
||||||
|
let currentHallLayout = null;
|
||||||
|
|
||||||
|
function openBooking(movie, hall, time) {
|
||||||
|
const titleEl = document.getElementById("modal-movie-title");
|
||||||
|
const infoEl = document.getElementById("modal-info-text");
|
||||||
|
|
||||||
|
if (titleEl) {
|
||||||
|
titleEl.innerText = movie;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (infoEl) {
|
||||||
|
infoEl.innerText = `${hall} • ${time} Uhr`;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentBookingContext = { movie, hall, time };
|
||||||
|
|
||||||
|
createSeats(hall, time);
|
||||||
|
renderBookingLegend();
|
||||||
|
updateBookingSummary();
|
||||||
|
|
||||||
|
document.getElementById("booking-modal")?.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRowLabel(rowIndex) {
|
||||||
|
return String(rowIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildHallLayout(hallName, baseConfig) {
|
||||||
|
const rows = Number(baseConfig.rows || 0);
|
||||||
|
const totalCols = Number(baseConfig.left || 0) + Number(baseConfig.right || 0);
|
||||||
|
const isDeluxe = /deluxe/i.test(hallName);
|
||||||
|
|
||||||
|
const left = isDeluxe
|
||||||
|
? Math.max(3, Number(baseConfig.left || 0) - 1)
|
||||||
|
: Number(baseConfig.left || 0);
|
||||||
|
const right = Math.max(0, totalCols - left);
|
||||||
|
|
||||||
|
const vipRows = rows > 0 ? [rows] : [];
|
||||||
|
|
||||||
|
const dboxMap = new Set();
|
||||||
|
const markDboxRange = (rowNumber, startCol, width) => {
|
||||||
|
if (!rowNumber || width <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxCol = Math.min(totalCols, startCol + width - 1);
|
||||||
|
for (let col = startCol; col <= maxCol; col++) {
|
||||||
|
dboxMap.add(`${rowNumber}-${col}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isDeluxe) {
|
||||||
|
const configuredDboxSeats = Array.isArray(baseConfig.dbox)
|
||||||
|
? baseConfig.dbox.reduce((sum, section) => sum + Number(section.w || 0), 0)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const totalDboxSeats = Math.max(4, configuredDboxSeats || 0);
|
||||||
|
|
||||||
|
const firstRow = Math.max(1, rows - 2);
|
||||||
|
const secondRow = Math.max(1, rows - 1);
|
||||||
|
const targetRows = [firstRow, secondRow]
|
||||||
|
.filter((rowNumber, index, arr) => arr.indexOf(rowNumber) === index)
|
||||||
|
.filter((rowNumber) => !vipRows.includes(rowNumber));
|
||||||
|
|
||||||
|
const rowCount = Math.max(1, targetRows.length);
|
||||||
|
const seatsPerFirstRows = Math.ceil(totalDboxSeats / rowCount);
|
||||||
|
let remaining = totalDboxSeats;
|
||||||
|
|
||||||
|
targetRows.forEach((rowNumber, index) => {
|
||||||
|
const seatsForRow = index === targetRows.length - 1
|
||||||
|
? remaining
|
||||||
|
: Math.min(seatsPerFirstRows, remaining);
|
||||||
|
remaining -= seatsForRow;
|
||||||
|
|
||||||
|
const startCol = left + Math.max(1, Math.floor((right - seatsForRow) / 2) + 1);
|
||||||
|
markDboxRange(rowNumber, startCol, seatsForRow);
|
||||||
|
});
|
||||||
|
} else if (Array.isArray(baseConfig.dbox)) {
|
||||||
|
baseConfig.dbox.forEach((section) => {
|
||||||
|
const rowNumber = Number(section.r || 0);
|
||||||
|
const width = Number(section.w || 0);
|
||||||
|
const startCol = Number(section.c || 0);
|
||||||
|
markDboxRange(rowNumber, startCol, width);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
rows,
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
totalCols,
|
||||||
|
vipRows,
|
||||||
|
dboxMap,
|
||||||
|
isImax: Boolean(baseConfig.isImax)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSeatType(layout, rowNumber, colNumber) {
|
||||||
|
if (layout.dboxMap.has(`${rowNumber}-${colNumber}`)) {
|
||||||
|
return "dbox";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layout.vipRows.includes(rowNumber)) {
|
||||||
|
return "vip";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layout.isImax) {
|
||||||
|
return "imax";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "normal";
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSeatElement({ seatId, seatType, occupiedSeats }) {
|
||||||
|
const seat = document.createElement("button");
|
||||||
|
seat.type = "button";
|
||||||
|
seat.classList.add("seat", seatType);
|
||||||
|
seat.dataset.seatId = seatId;
|
||||||
|
seat.dataset.type = seatType;
|
||||||
|
seat.title = `${seatId} (${seatType.toUpperCase()})`;
|
||||||
|
|
||||||
|
if (occupiedSeats.has(seatId)) {
|
||||||
|
seat.classList.add("occupied");
|
||||||
|
seat.disabled = true;
|
||||||
|
seat.setAttribute("aria-label", `${seatId} belegt`);
|
||||||
|
return seat;
|
||||||
|
}
|
||||||
|
|
||||||
|
seat.setAttribute("aria-label", `${seatId} frei`);
|
||||||
|
seat.addEventListener("click", () => {
|
||||||
|
seat.classList.toggle("selected");
|
||||||
|
updateBookingSummary();
|
||||||
|
});
|
||||||
|
|
||||||
|
return seat;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSeats(hallName, time) {
|
||||||
|
const seatGrid = document.getElementById("seat-grid");
|
||||||
|
if (!seatGrid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
seatGrid.innerHTML = "";
|
||||||
|
|
||||||
|
const baseConfig = seatLayouts[hallName];
|
||||||
|
if (!baseConfig) {
|
||||||
|
currentHallLayout = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentHallLayout = buildHallLayout(hallName, baseConfig);
|
||||||
|
|
||||||
|
const occupiedKey = `${hallName}-${time}`;
|
||||||
|
const occupiedSeats = new Set(Array.isArray(occupiedSeatsData?.[occupiedKey]) ? occupiedSeatsData[occupiedKey] : []);
|
||||||
|
|
||||||
|
for (let rowIndex = 0; rowIndex < currentHallLayout.rows; rowIndex++) {
|
||||||
|
const rowNumber = rowIndex + 1;
|
||||||
|
const rowLabel = getRowLabel(rowIndex);
|
||||||
|
|
||||||
|
const perspectiveFactor = (currentHallLayout.rows - rowNumber) / Math.max(currentHallLayout.rows - 1, 1);
|
||||||
|
const rowIndent = Math.round(18 * perspectiveFactor);
|
||||||
|
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.className = "seat-row cinema-row";
|
||||||
|
row.style.setProperty("--row-indent", `${rowIndent}px`);
|
||||||
|
|
||||||
|
const leftLabel = document.createElement("div");
|
||||||
|
leftLabel.className = "row-label";
|
||||||
|
leftLabel.textContent = rowLabel;
|
||||||
|
|
||||||
|
const rightLabel = document.createElement("div");
|
||||||
|
rightLabel.className = "row-label row-label-right";
|
||||||
|
rightLabel.textContent = rowLabel;
|
||||||
|
|
||||||
|
const leftBlock = document.createElement("div");
|
||||||
|
leftBlock.className = "row-seat-block left-block";
|
||||||
|
|
||||||
|
const rightBlock = document.createElement("div");
|
||||||
|
rightBlock.className = "row-seat-block right-block";
|
||||||
|
|
||||||
|
for (let col = 1; col <= currentHallLayout.totalCols; col++) {
|
||||||
|
const seatId = `R${rowNumber}-P${col}`;
|
||||||
|
const seatType = getSeatType(currentHallLayout, rowNumber, col);
|
||||||
|
const seat = createSeatElement({ seatId, seatType, occupiedSeats });
|
||||||
|
|
||||||
|
if (col <= currentHallLayout.left) {
|
||||||
|
leftBlock.appendChild(seat);
|
||||||
|
} else {
|
||||||
|
rightBlock.appendChild(seat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const aisle = document.createElement("div");
|
||||||
|
aisle.className = "aisle-gap";
|
||||||
|
|
||||||
|
row.append(leftLabel, leftBlock, aisle, rightBlock, rightLabel);
|
||||||
|
seatGrid.appendChild(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBookingLegend() {
|
||||||
|
const legend = document.getElementById("dynamic-legend");
|
||||||
|
if (!legend || !currentHallLayout) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const legendItems = [
|
||||||
|
{ type: "normal", label: "Standard" },
|
||||||
|
{ type: "selected", label: "Ausgewählt" },
|
||||||
|
{ type: "occupied", label: "Belegt" }
|
||||||
|
];
|
||||||
|
|
||||||
|
if (currentHallLayout.isImax) {
|
||||||
|
legendItems.unshift({ type: "imax", label: "IMAX" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentHallLayout.vipRows.length > 0) {
|
||||||
|
legendItems.unshift({ type: "vip", label: "VIP" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentHallLayout.dboxMap.size > 0) {
|
||||||
|
legendItems.unshift({ type: "dbox", label: "D-BOX" });
|
||||||
|
}
|
||||||
|
|
||||||
|
legend.innerHTML = legendItems
|
||||||
|
.map((item) => `
|
||||||
|
<div class="item">
|
||||||
|
<span class="seat ${item.type}"></span>
|
||||||
|
<span>${item.label}</span>
|
||||||
|
</div>
|
||||||
|
`)
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateBookingSummary() {
|
||||||
|
const selectedSeats = Array.from(document.querySelectorAll("#seat-grid .seat.selected"));
|
||||||
|
const summaryPanel = document.getElementById("booking-summary");
|
||||||
|
const summaryItems = document.getElementById("summary-items");
|
||||||
|
const totalEl = document.getElementById("total-price");
|
||||||
|
|
||||||
|
let total = 0;
|
||||||
|
|
||||||
|
if (summaryItems) {
|
||||||
|
summaryItems.innerHTML = selectedSeats
|
||||||
|
.map((seat) => {
|
||||||
|
const type = seat.dataset.type || "normal";
|
||||||
|
const seatPrice = Number(prices?.[type] ?? prices?.normal ?? 11);
|
||||||
|
total += seatPrice;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="summary-row">
|
||||||
|
<span>${seat.dataset.seatId}</span>
|
||||||
|
<span>${seatPrice.toFixed(2).replace(".", ",")} EUR</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
} else {
|
||||||
|
selectedSeats.forEach((seat) => {
|
||||||
|
const type = seat.dataset.type || "normal";
|
||||||
|
const seatPrice = Number(prices?.[type] ?? prices?.normal ?? 11);
|
||||||
|
total += seatPrice;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalEl) {
|
||||||
|
totalEl.innerText = `${total.toFixed(2).replace(".", ",")} EUR`;
|
||||||
|
}
|
||||||
|
|
||||||
|
summaryPanel?.classList.toggle("hidden", selectedSeats.length === 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findMoviePoster(movieTitle) {
|
||||||
|
const cards = Array.from(document.querySelectorAll(".movie-card, .detailed-card"));
|
||||||
|
const normalizedTarget = String(movieTitle || "").trim().toLowerCase();
|
||||||
|
|
||||||
|
for (const card of cards) {
|
||||||
|
const title = card.querySelector("h2, h3")?.innerText?.trim().toLowerCase();
|
||||||
|
if (title === normalizedTarget) {
|
||||||
|
const imageSrc = card.querySelector("img")?.src;
|
||||||
|
if (imageSrc) {
|
||||||
|
return imageSrc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmSelectedSeats() {
|
||||||
|
const selectedSeats = Array.from(document.querySelectorAll("#seat-grid .seat.selected"));
|
||||||
|
|
||||||
|
if (!currentBookingContext || selectedSeats.length === 0) {
|
||||||
|
alert("Bitte waehle mindestens einen Platz aus.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const moviePoster = findMoviePoster(currentBookingContext.movie);
|
||||||
|
const addedSeats = [];
|
||||||
|
|
||||||
|
selectedSeats.forEach((seat) => {
|
||||||
|
const seatId = seat.dataset.seatId;
|
||||||
|
const seatType = seat.dataset.type || "normal";
|
||||||
|
|
||||||
|
const alreadyInCart = cart.some((item) =>
|
||||||
|
item.category === "movie" &&
|
||||||
|
item.title === currentBookingContext.movie &&
|
||||||
|
item.hall === currentBookingContext.hall &&
|
||||||
|
item.time === currentBookingContext.time &&
|
||||||
|
item.seatId === seatId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (alreadyInCart) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cart.push({
|
||||||
|
id: Date.now() + Math.random(),
|
||||||
|
category: "movie",
|
||||||
|
title: currentBookingContext.movie,
|
||||||
|
hall: currentBookingContext.hall,
|
||||||
|
time: currentBookingContext.time,
|
||||||
|
seatId,
|
||||||
|
type: seatType.toUpperCase(),
|
||||||
|
price: Number(prices?.[seatType] ?? prices?.normal ?? 11),
|
||||||
|
img: moviePoster
|
||||||
|
});
|
||||||
|
|
||||||
|
addedSeats.push(seatId);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!addedSeats.length) {
|
||||||
|
alert("Diese Plaetze sind bereits im Warenkorb.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveCart?.();
|
||||||
|
renderCart?.();
|
||||||
|
renderCheckout?.();
|
||||||
|
|
||||||
|
document.getElementById("booking-modal")?.classList.add("hidden");
|
||||||
|
|
||||||
|
const snackOverlay = document.getElementById("snack-prompt-overlay");
|
||||||
|
snackOverlay?.classList.remove("hidden");
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
document.getElementById("btn-confirm-seats")?.addEventListener("click", confirmSelectedSeats);
|
||||||
|
});
|
||||||
199
cart.js
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
function formatEuro(value) {
|
||||||
|
return `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return String(value || "")
|
||||||
|
.replaceAll("&", "&")
|
||||||
|
.replaceAll("<", "<")
|
||||||
|
.replaceAll(">", ">")
|
||||||
|
.replaceAll('"', """)
|
||||||
|
.replaceAll("'", "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCartKey(item) {
|
||||||
|
const infoText = item.category === "movie"
|
||||||
|
? `Sitz: ${item.seatId} (${item.hall})`
|
||||||
|
: item.time;
|
||||||
|
return `${item.title}-${item.hall}-${infoText}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDrinkItem(item) {
|
||||||
|
if (item.category !== "snack") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = String(item.title || "").toLowerCase();
|
||||||
|
const size = String(item.hall || "").toLowerCase();
|
||||||
|
const drinkKeywords = [
|
||||||
|
"cola",
|
||||||
|
"sprite",
|
||||||
|
"fanta",
|
||||||
|
"mezzo",
|
||||||
|
"fuze",
|
||||||
|
"wasser",
|
||||||
|
"getraenk",
|
||||||
|
"drink"
|
||||||
|
];
|
||||||
|
|
||||||
|
return drinkKeywords.some((word) => title.includes(word)) || size.includes("l");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildItemInfo(item) {
|
||||||
|
if (item.category === "movie") {
|
||||||
|
return `
|
||||||
|
<div>Sitzplatz: ${escapeHtml(item.seatId || "-")}</div>
|
||||||
|
<div>Saal: ${escapeHtml(item.hall || "-")}</div>
|
||||||
|
<div>Uhrzeit: ${escapeHtml(item.time || "-")} Uhr</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDrinkItem(item)) {
|
||||||
|
return `
|
||||||
|
<div>Variante: ${escapeHtml(item.time || "-")}</div>
|
||||||
|
<div>Groesse: ${escapeHtml(item.hall || "-")}</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div>Kategorie: Snack</div>
|
||||||
|
<div>Variante: ${escapeHtml(item.time || "-")}</div>
|
||||||
|
<div>Groesse: ${escapeHtml(item.hall || "-")}</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupCartItems() {
|
||||||
|
const groups = new Map();
|
||||||
|
|
||||||
|
cart.forEach((item) => {
|
||||||
|
const key = buildCartKey(item);
|
||||||
|
|
||||||
|
if (!groups.has(key)) {
|
||||||
|
groups.set(key, {
|
||||||
|
key,
|
||||||
|
quantity: 0,
|
||||||
|
total: 0,
|
||||||
|
item
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const group = groups.get(key);
|
||||||
|
group.quantity += 1;
|
||||||
|
group.total += Number(item.price || 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(groups.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveCart() {
|
||||||
|
localStorage.setItem("eagleCart", JSON.stringify(cart));
|
||||||
|
updateCartBadge();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCartBadge() {
|
||||||
|
const cartBadge = document.getElementById("cart-badge");
|
||||||
|
|
||||||
|
if (!cartBadge) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cartBadge.innerText = cart.length;
|
||||||
|
cartBadge.classList.toggle("hidden", cart.length === 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCart() {
|
||||||
|
const cartList = document.getElementById("cart-items-list");
|
||||||
|
const totalEl = document.getElementById("cart-total-right");
|
||||||
|
const vatEl = document.getElementById("cart-vat-right");
|
||||||
|
|
||||||
|
if (!cartList || !totalEl || !vatEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(cart) || cart.length === 0) {
|
||||||
|
cartList.innerHTML = '<p>Dein Warenkorb ist leer.</p>';
|
||||||
|
totalEl.innerText = formatEuro(0);
|
||||||
|
vatEl.innerText = `inkl. 19% MwSt: ${formatEuro(0)}`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupedItems = groupCartItems();
|
||||||
|
|
||||||
|
const header = `
|
||||||
|
<div class="cart-header-row">
|
||||||
|
<div class="col-amount">MENGE</div>
|
||||||
|
<div class="col-img">VORSCHAU</div>
|
||||||
|
<div class="col-product">NAME</div>
|
||||||
|
<div class="col-details">INFO</div>
|
||||||
|
<div class="col-price">PREIS</div>
|
||||||
|
<div class="col-action">AKTION</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const rows = groupedItems
|
||||||
|
.map((group) => {
|
||||||
|
const imageHtml = group.item.img
|
||||||
|
? `<img class="cart-img-small" src="${escapeHtml(group.item.img)}" alt="${escapeHtml(group.item.title)}">`
|
||||||
|
: `<div class="cart-img-fallback">Kein Bild</div>`;
|
||||||
|
const quantityHtml = group.item.category === "movie"
|
||||||
|
? `<div class="qty-static" aria-label="Feste Ticketanzahl">${group.quantity}x</div>`
|
||||||
|
: `
|
||||||
|
<div class="qty-stepper">
|
||||||
|
<button class="btn-qty" data-action="minus" data-key="${escapeHtml(group.key)}">-</button>
|
||||||
|
<span>${group.quantity}</span>
|
||||||
|
<button class="btn-qty" data-action="plus" data-key="${escapeHtml(group.key)}">+</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="cart-item-row">
|
||||||
|
<div class="col-amount">
|
||||||
|
${quantityHtml}
|
||||||
|
</div>
|
||||||
|
<div class="col-img">${imageHtml}</div>
|
||||||
|
<div class="col-product">${escapeHtml(group.item.title)}</div>
|
||||||
|
<div class="col-details cart-item-info">${buildItemInfo(group.item)}</div>
|
||||||
|
<div class="col-price">${formatEuro(group.total)}</div>
|
||||||
|
<div class="col-action">
|
||||||
|
<button class="btn-delete-item" data-key="${escapeHtml(group.key)}" aria-label="Eintrag entfernen"><span aria-hidden="true">🗑️</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
cartList.innerHTML = header + rows;
|
||||||
|
|
||||||
|
const total = cart.reduce((sum, item) => sum + Number(item.price || 0), 0);
|
||||||
|
const vat = total - total / 1.19;
|
||||||
|
|
||||||
|
totalEl.innerText = formatEuro(total);
|
||||||
|
vatEl.innerText = `inkl. 19% MwSt: ${formatEuro(vat)}`;
|
||||||
|
|
||||||
|
saveCart();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.removeItem = function removeItem(id) {
|
||||||
|
cart = cart.filter((item) => item.id !== id);
|
||||||
|
saveCart();
|
||||||
|
renderCart();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.changeQty = function changeQty(title, delta) {
|
||||||
|
if (delta > 0) {
|
||||||
|
const item = cart.find((entry) => entry.title === title);
|
||||||
|
if (item) {
|
||||||
|
cart.push({ ...item, id: Date.now() + Math.random() });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const index = cart
|
||||||
|
.map((entry) => entry.title)
|
||||||
|
.lastIndexOf(title);
|
||||||
|
if (index !== -1) {
|
||||||
|
cart.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveCart();
|
||||||
|
renderCart();
|
||||||
|
};
|
||||||
232
checkout.js
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
function formatCheckoutEuro(value) {
|
||||||
|
return `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let selectedPaymentMethod = "";
|
||||||
|
let checkoutEventsBound = false;
|
||||||
|
|
||||||
|
function setCheckoutStep(step) {
|
||||||
|
const step1 = document.getElementById("checkout-step-1");
|
||||||
|
const step2 = document.getElementById("checkout-step-2");
|
||||||
|
const step3 = document.getElementById("checkout-step-3");
|
||||||
|
|
||||||
|
step1?.classList.toggle("hidden", step !== 1);
|
||||||
|
step2?.classList.toggle("hidden", step !== 2);
|
||||||
|
step3?.classList.toggle("hidden", step !== 3);
|
||||||
|
|
||||||
|
const line1 = document.getElementById("line-1");
|
||||||
|
const line2 = document.getElementById("line-2");
|
||||||
|
const indicator1 = document.getElementById("step-1-indicator");
|
||||||
|
const indicator2 = document.getElementById("step-2-indicator");
|
||||||
|
const indicator3 = document.getElementById("step-3-indicator");
|
||||||
|
|
||||||
|
indicator1?.classList.add("active");
|
||||||
|
indicator2?.classList.toggle("active", step >= 2);
|
||||||
|
indicator3?.classList.toggle("active", step >= 3);
|
||||||
|
line1?.classList.toggle("active", step >= 2);
|
||||||
|
line2?.classList.toggle("active", step >= 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCheckout() {
|
||||||
|
const summaryList = document.getElementById("checkout-summary-list");
|
||||||
|
const totalDisplay = document.getElementById("checkout-total-display");
|
||||||
|
const vatDisplay = document.getElementById("checkout-vat-display");
|
||||||
|
const nextButton = document.getElementById("btn-next-step-2");
|
||||||
|
|
||||||
|
if (!summaryList) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
summaryList.innerHTML = "";
|
||||||
|
|
||||||
|
const safeCart = Array.isArray(cart) ? cart : [];
|
||||||
|
const total = safeCart.reduce((sum, item) => sum + Number(item.price || 0), 0);
|
||||||
|
const vat = total - total / 1.19;
|
||||||
|
|
||||||
|
safeCart.forEach((item) => {
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.style.cssText = "display:flex; justify-content:space-between; gap:12px; margin-bottom:10px; font-size:0.95rem;";
|
||||||
|
|
||||||
|
const infoText = item.category === "movie"
|
||||||
|
? `Sitz ${item.seatId || "-"} | ${item.hall || "-"} | ${item.time || "-"} Uhr`
|
||||||
|
: `${item.time || "Standard"} | ${item.hall || "-"}`;
|
||||||
|
|
||||||
|
row.innerHTML = `<span>${item.title} (${infoText})</span><span>${formatCheckoutEuro(item.price)}</span>`;
|
||||||
|
summaryList.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (totalDisplay) {
|
||||||
|
totalDisplay.innerText = `Gesamtbetrag: ${formatCheckoutEuro(total)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vatDisplay) {
|
||||||
|
vatDisplay.innerText = `inkl. 19% MwSt: ${formatCheckoutEuro(vat)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedPaymentMethod = "";
|
||||||
|
document.querySelectorAll(".payment-method").forEach((method) => {
|
||||||
|
method.classList.remove("selected");
|
||||||
|
});
|
||||||
|
|
||||||
|
nextButton?.classList.add("hidden");
|
||||||
|
setCheckoutStep(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateTicket() {
|
||||||
|
const ticketContainer = document.getElementById("ticket-container");
|
||||||
|
if (!ticketContainer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const moviesInCart = (Array.isArray(cart) ? cart : []).filter((item) => item.category === "movie");
|
||||||
|
if (!moviesInCart.length) {
|
||||||
|
ticketContainer.innerHTML = "<p>Danke fuer deinen Einkauf!</p>";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mainMovie = moviesInCart[0];
|
||||||
|
const matchingMovieSeats = moviesInCart
|
||||||
|
.filter((item) => item.title === mainMovie.title && item.time === mainMovie.time)
|
||||||
|
.map((item) => item.seatId)
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
const qrData = encodeURIComponent(`EAGLE-IMAX|${mainMovie.title}|${mainMovie.hall}|${matchingMovieSeats}`);
|
||||||
|
const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=${qrData}&bgcolor=ffffff`;
|
||||||
|
|
||||||
|
ticketContainer.innerHTML = `
|
||||||
|
<div class="luxury-ticket">
|
||||||
|
<div class="ticket-left">
|
||||||
|
<img src="${mainMovie.img}" class="ticket-poster" alt="${mainMovie.title}">
|
||||||
|
</div>
|
||||||
|
<div class="ticket-right">
|
||||||
|
<div class="ticket-brand">EAGLE'S IMAX PREMIUM</div>
|
||||||
|
<h2 class="ticket-title">${mainMovie.title}</h2>
|
||||||
|
<div class="ticket-details">
|
||||||
|
<p><span>SAAL</span> <strong>${mainMovie.hall}</strong></p>
|
||||||
|
<p><span>ZEIT</span> <strong>${mainMovie.time} Uhr</strong></p>
|
||||||
|
<p><span>SITZE</span> <strong>${matchingMovieSeats || "-"}</strong></p>
|
||||||
|
</div>
|
||||||
|
<div class="ticket-footer">
|
||||||
|
<img src="${qrUrl}" class="ticket-qr" alt="QR Code">
|
||||||
|
<div class="ticket-code">#${Math.floor(Math.random() * 90000) + 10000}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveOrderForCurrentUser(orderItems, orderTotal) {
|
||||||
|
if (typeof currentUser === "undefined" || !currentUser) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof users === "undefined" || !Array.isArray(users)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const order = {
|
||||||
|
date: new Date().toLocaleString("de-DE"),
|
||||||
|
items: orderItems,
|
||||||
|
total: orderTotal,
|
||||||
|
paymentMethod: selectedPaymentMethod || "-"
|
||||||
|
};
|
||||||
|
|
||||||
|
const userIndex = users.findIndex((entry) => entry.email === currentUser.email);
|
||||||
|
if (userIndex === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(users[userIndex].orders)) {
|
||||||
|
users[userIndex].orders = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
users[userIndex].orders.push(order);
|
||||||
|
localStorage.setItem("eagleUsers", JSON.stringify(users));
|
||||||
|
}
|
||||||
|
|
||||||
|
function reserveSeatsAfterPayment(orderItems) {
|
||||||
|
const movieItems = orderItems.filter((item) => item.category === "movie");
|
||||||
|
|
||||||
|
movieItems.forEach((item) => {
|
||||||
|
const key = `${item.hall}-${item.time}`;
|
||||||
|
if (!occupiedSeatsData[key]) {
|
||||||
|
occupiedSeatsData[key] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
occupiedSeatsData[key].push(item.seatId);
|
||||||
|
});
|
||||||
|
|
||||||
|
localStorage.setItem("eagleOccupied", JSON.stringify(occupiedSeatsData));
|
||||||
|
}
|
||||||
|
|
||||||
|
function completeCheckout() {
|
||||||
|
const orderItems = Array.isArray(cart) ? [...cart] : [];
|
||||||
|
const orderTotal = orderItems.reduce((sum, item) => sum + Number(item.price || 0), 0);
|
||||||
|
|
||||||
|
saveOrderForCurrentUser(orderItems, orderTotal);
|
||||||
|
reserveSeatsAfterPayment(orderItems);
|
||||||
|
|
||||||
|
cart = [];
|
||||||
|
saveCart?.();
|
||||||
|
renderCart?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindCheckoutEvents() {
|
||||||
|
if (checkoutEventsBound) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkoutEventsBound = true;
|
||||||
|
|
||||||
|
const nextButton = document.getElementById("btn-next-step-2");
|
||||||
|
const backButton = document.getElementById("btn-back-to-step1");
|
||||||
|
const payNowButton = document.getElementById("btn-pay-now");
|
||||||
|
|
||||||
|
document.querySelectorAll(".payment-method").forEach((method) => {
|
||||||
|
method.addEventListener("click", () => {
|
||||||
|
document.querySelectorAll(".payment-method").forEach((entry) => {
|
||||||
|
entry.classList.remove("selected");
|
||||||
|
});
|
||||||
|
|
||||||
|
method.classList.add("selected");
|
||||||
|
selectedPaymentMethod = method.dataset.method || "";
|
||||||
|
nextButton?.classList.remove("hidden");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
nextButton?.addEventListener("click", () => {
|
||||||
|
if (!selectedPaymentMethod) {
|
||||||
|
alert("Bitte waehle zuerst eine Zahlungsmethode aus.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCheckoutStep(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
backButton?.addEventListener("click", () => {
|
||||||
|
setCheckoutStep(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
payNowButton?.addEventListener("click", () => {
|
||||||
|
if (!Array.isArray(cart) || !cart.length) {
|
||||||
|
alert("Dein Warenkorb ist leer.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
payNowButton.disabled = true;
|
||||||
|
payNowButton.innerText = "Verarbeite...";
|
||||||
|
payNowButton.style.opacity = "0.7";
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setCheckoutStep(3);
|
||||||
|
generateTicket();
|
||||||
|
completeCheckout();
|
||||||
|
|
||||||
|
payNowButton.disabled = false;
|
||||||
|
payNowButton.innerText = "Jetzt Bezahlen";
|
||||||
|
payNowButton.style.opacity = "1";
|
||||||
|
}, 1200);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", bindCheckoutEvents);
|
||||||
430
index.html
@@ -267,15 +267,208 @@
|
|||||||
<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>City Edition</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">Sammlerbecher mit Popcornfüllung, wahlweise süß oder salzig.</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 Collectors</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="limited-special-grid compact-specials">
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
</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 compact-specials">
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="limited-special-block">
|
||||||
|
<div class="special-film-heading">
|
||||||
|
<img src="img/hoppers.jpg" alt="Hoppers Logo">
|
||||||
|
<div>
|
||||||
|
<span>Hoppers</span>
|
||||||
|
<h2>Family Specials</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">Stabiler Becher mit Popcornfüllung.</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">Sammlerbecher mit warmem Popcornmoment.</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 Specials</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">Verspielter Sammlerbecher für Mario-Fans.</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,6 +585,11 @@
|
|||||||
</div> </div>
|
</div> </div>
|
||||||
|
|
||||||
<div id="cat-popcorn" class="snack-category hidden">
|
<div id="cat-popcorn" class="snack-category hidden">
|
||||||
|
<div class="snack-subsection">
|
||||||
|
<div class="snack-section-heading">
|
||||||
|
<span>Frisch gepoppt</span>
|
||||||
|
<h2>Einzelprodukte</h2>
|
||||||
|
</div>
|
||||||
<div class="snack-grid">
|
<div class="snack-grid">
|
||||||
<div class="snack-card">
|
<div class="snack-card">
|
||||||
<div class="snack-img"><img src="img/popcorn-klein.png" alt="Popcorn klein"></div>
|
<div class="snack-img"><img src="img/popcorn-klein.png" alt="Popcorn klein"></div>
|
||||||
@@ -402,7 +600,7 @@
|
|||||||
<button class="opt-btn">Salzig</button>
|
<button class="opt-btn">Salzig</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="size-selector">
|
<div class="size-selector">
|
||||||
<button class="size-chip">3,50€</button>
|
<button class="size-chip">Klein <span>3,50€</span></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -415,7 +613,7 @@
|
|||||||
<button class="opt-btn">Salzig</button>
|
<button class="opt-btn">Salzig</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="size-selector">
|
<div class="size-selector">
|
||||||
<button class="size-chip">4,50€</button>
|
<button class="size-chip">Mittel <span>4,50€</span></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -428,91 +626,56 @@
|
|||||||
<button class="opt-btn">Salzig</button>
|
<button class="opt-btn">Salzig</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="size-selector">
|
<div class="size-selector">
|
||||||
<button class="size-chip">6,00€</button>
|
<button class="size-chip">Groß <span>6,00€</span></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="snack-card">
|
</div>
|
||||||
<div class="snack-img"><img src="img/zoomania-popcorn.jpg" alt="Popcorn limited - zoomania 2"></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-info">
|
<div class="snack-info">
|
||||||
<h3>Limitierter Metallbecher - Zoomania 2</h3>
|
<h3>Kleines Menü</h3>
|
||||||
<div class="option-group">
|
<p class="snack-card-note">0,33L Getränk + Popcorn klein</p>
|
||||||
<button class="opt-btn active">Süß</button>
|
|
||||||
<button class="opt-btn">Salzig</button>
|
|
||||||
</div>
|
|
||||||
<div class="size-selector">
|
<div class="size-selector">
|
||||||
<button class="size-chip">12,00€</button>
|
<button class="size-chip">Menü-Preis <span>5,00€</span></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="snack-card">
|
<div class="snack-card highlight">
|
||||||
<div class="snack-img"><img src="img/screamdoorpopcorn.jpg" alt="Popcorn limited - Scream VII"></div>
|
|
||||||
<div class="snack-info">
|
<div class="snack-info">
|
||||||
<h3>Limitierter Sammelbecher - Scream VII</h3>
|
<h3>Mittleres Menü</h3>
|
||||||
<div class="option-group">
|
<p class="snack-card-note">0,5L Getränk + Popcorn mittel</p>
|
||||||
<button class="opt-btn active">Süß</button>
|
|
||||||
<button class="opt-btn">Salzig</button>
|
|
||||||
</div>
|
|
||||||
<div class="size-selector">
|
<div class="size-selector">
|
||||||
<button class="size-chip">29,00€</button>
|
<button class="size-chip">Menü-Preis <span>6,50€</span></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="snack-card">
|
<div class="snack-card highlight">
|
||||||
<div class="snack-img"><img src="img/astronautpopcorn.jpg" alt="Popcorn limited - Der Austronaut"></div>
|
|
||||||
<div class="snack-info">
|
<div class="snack-info">
|
||||||
<h3>Limitierter Sammelbecher - Der Austronaut</h3>
|
<span class="badge">Bestseller</span>
|
||||||
<div class="option-group">
|
<h3>Großes Menü</h3>
|
||||||
<button class="opt-btn active">Süß</button>
|
<p class="snack-card-note">1L Getränk + Popcorn groß</p>
|
||||||
<button class="opt-btn">Salzig</button>
|
|
||||||
</div>
|
|
||||||
<div class="size-selector">
|
<div class="size-selector">
|
||||||
<button class="size-chip">34,00€</button>
|
<button class="size-chip">Menü-Preis <span>8,00€</span></button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div id="cat-nachos" class="snack-category hidden">
|
<div id="cat-nachos" class="snack-category hidden">
|
||||||
|
<div class="snack-subsection">
|
||||||
|
<div class="snack-section-heading">
|
||||||
|
<span>Nachos</span>
|
||||||
|
<h2>Einzelprodukte</h2>
|
||||||
|
</div>
|
||||||
<div class="snack-grid">
|
<div class="snack-grid">
|
||||||
<div class="snack-card">
|
<div class="snack-card">
|
||||||
<div class="snack-img"><img src="img/nachosnormal.png" alt="Nachos"></div>
|
<div class="snack-img"><img src="img/nachosnormal.png" alt="Nachos"></div>
|
||||||
@@ -523,7 +686,7 @@
|
|||||||
<button class="opt-btn">Salsa-Dip</button>
|
<button class="opt-btn">Salsa-Dip</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="size-selector">
|
<div class="size-selector">
|
||||||
<button class="size-chip">Klein<span>5,00€</span></button>
|
<button class="size-chip">Klein <span>5,00€</span></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -536,7 +699,7 @@
|
|||||||
<button class="opt-btn">Salsa-Dip</button>
|
<button class="opt-btn">Salsa-Dip</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="size-selector">
|
<div class="size-selector">
|
||||||
<button class="size-chip">Mittel<span>6,50€</span></button>
|
<button class="size-chip">Mittel <span>6,50€</span></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -554,12 +717,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="list-title"> </h2>
|
</div>
|
||||||
<br>
|
</div>
|
||||||
<h2 class="list-title">Dips</h2>
|
|
||||||
<br>
|
<div class="snack-subsection">
|
||||||
<br>
|
<div class="snack-section-heading">
|
||||||
<br>
|
<span>Extra dazu</span>
|
||||||
|
<h2>Dips</h2>
|
||||||
|
</div>
|
||||||
|
<div class="snack-grid">
|
||||||
<div class="snack-card">
|
<div class="snack-card">
|
||||||
<div class="snack-img"><img src="img/käsedip.png" alt="Käse-Dip"></div>
|
<div class="snack-img"><img src="img/käsedip.png" alt="Käse-Dip"></div>
|
||||||
<div class="snack-info">
|
<div class="snack-info">
|
||||||
@@ -569,7 +735,7 @@
|
|||||||
<button class="opt-btn">Scharf</button>
|
<button class="opt-btn">Scharf</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="size-selector">
|
<div class="size-selector">
|
||||||
<button class="size-chip">Schale<span>2,00€</span></button>
|
<button class="size-chip">Schale <span>2,00€</span></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -581,7 +747,7 @@
|
|||||||
<button class="opt-btn active">Normal</button>
|
<button class="opt-btn active">Normal</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="size-selector">
|
<div class="size-selector">
|
||||||
<button class="size-chip">Schale<span>2,00€</span></button>
|
<button class="size-chip">Schale <span>2,00€</span></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -593,25 +759,32 @@
|
|||||||
<button class="opt-btn active">Normal</button>
|
<button class="opt-btn active">Normal</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="size-selector">
|
<div class="size-selector">
|
||||||
<button class="size-chip">Schale<span>2,00€</span></button>
|
<button class="size-chip">Schale <span>2,00€</span></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="list-title">Nacho Kombi-Menüs</h2>
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="snack-subsection">
|
||||||
|
<div class="snack-section-heading">
|
||||||
|
<span>Alles drin</span>
|
||||||
|
<h2>Kombi Menü</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="snack-card">
|
<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-img"><img src="img/nachokombiklein.png" alt="Nacho Kombi Klein"></div>
|
||||||
<div class="snack-info">
|
<div class="snack-info">
|
||||||
<h3>Nacho Menü Klein - Nachos klein + 1 Dip + 1 0,33L Getränk</h3>
|
<h3>Nacho Menü Klein</h3>
|
||||||
|
<p class="snack-card-note">Nachos klein + 1 Dip + 1x 0,33L Getränk</p>
|
||||||
<div class="option-group">
|
<div class="option-group">
|
||||||
<button class="opt-btn active">Klein</button>
|
<button class="opt-btn active">Klein</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="size-selector">
|
<div class="size-selector">
|
||||||
<button class="size-chip">Kombi<span>6,90€</span></button>
|
<button class="size-chip">Kombi <span>6,90€</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -647,75 +820,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 +839,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>
|
||||||
@@ -948,11 +1052,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>
|
||||||
|
|
||||||
|
|||||||
6543
package-lock.json
generated
29
package.json
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 655 B |
@@ -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 |
|
Before Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 193 KiB |
|
Before Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 211 KiB |
|
Before Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 878 KiB |
|
Before Width: | Height: | Size: 203 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 362 KiB |
|
Before Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 233 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 202 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 201 KiB |
|
Before Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 2.2 MiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 307 KiB |
|
Before Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 185 KiB |
|
Before Width: | Height: | Size: 910 KiB |
|
Before Width: | Height: | Size: 586 KiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 3.0 MiB |
|
Before Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 182 KiB |
|
Before Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 186 KiB |
|
Before Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 264 KiB |
|
Before Width: | Height: | Size: 263 KiB |
|
Before Width: | Height: | Size: 211 KiB |
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 264 KiB |
|
Before Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 185 KiB |
|
Before Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 643 KiB |
|
Before Width: | Height: | Size: 481 KiB |
|
Before Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 530 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 544 KiB |
|
Before Width: | Height: | Size: 524 KiB |
@@ -1,58 +0,0 @@
|
|||||||
<section id="about-view" class="info-view">
|
|
||||||
<div class="container info-view-shell">
|
|
||||||
<div class="about-hero-block">
|
|
||||||
<div class="about-hero-content">
|
|
||||||
<h1>Über uns</h1>
|
|
||||||
<p class="about-intro">
|
|
||||||
EAGLE's IMAX vereint modernes Design, starke Technik und echtes Kinofeeling.
|
|
||||||
Unser Anspruch: Jeder Besuch soll wie ein kleines Event wirken. Von entspannten Abenden
|
|
||||||
bis zu großen Blockbuster-Premieren liefern wir Bild, Sound und Atmosphäre auf Top-Niveau.
|
|
||||||
</p>
|
|
||||||
<div class="about-pill-row">
|
|
||||||
<span>4 Säle</span>
|
|
||||||
<span>IMAX Experience</span>
|
|
||||||
<span>D-BOX Motion Seats</span>
|
|
||||||
<span>Premium Snacks</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="about-hero-media" aria-hidden="true"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="about-stats-grid">
|
|
||||||
<article>
|
|
||||||
<h3>600+</h3>
|
|
||||||
<p>Sitzplätze insgesamt</p>
|
|
||||||
</article>
|
|
||||||
<article>
|
|
||||||
<h3>4K / Laser</h3>
|
|
||||||
<p>Hochauflösende Projektion</p>
|
|
||||||
</article>
|
|
||||||
<article>
|
|
||||||
<h3>Dolby Atmos</h3>
|
|
||||||
<p>Raumklang in ausgewählten Sälen</p>
|
|
||||||
</article>
|
|
||||||
<article>
|
|
||||||
<h3>D-BOX</h3>
|
|
||||||
<p>Bewegung synchron zum Film</p>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="about-cards">
|
|
||||||
<article class="about-card about-card-halls">
|
|
||||||
<h3>Säle</h3>
|
|
||||||
<p>Vom klassischen Kinoraum bis zum IMAX-Erlebnis: Jeder Saal ist individuell abgestimmt auf Genre, Publikum und Stimmung.</p>
|
|
||||||
<a href="/halls" class="story-more-btn">Mehr erfahren</a>
|
|
||||||
</article>
|
|
||||||
<article class="about-card about-card-dbox">
|
|
||||||
<h3>D-BOX Plätze</h3>
|
|
||||||
<p>Synchronisierte Sitzbewegungen machen Action und Effekte physisch spürbar und verstärken die Immersion im Film.</p>
|
|
||||||
<a href="/dbox" class="story-more-btn">Mehr erfahren</a>
|
|
||||||
</article>
|
|
||||||
<article class="about-card about-card-tech">
|
|
||||||
<h3>Technik</h3>
|
|
||||||
<p>Leinwandgrößen, Projektoren, Soundsysteme und Kapazitäten in einem separaten Technikfenster zusammengefasst.</p>
|
|
||||||
<button id="btn-open-tech-modal" type="button" class="story-more-btn" data-about-modal-open="about-tech-modal">Mehr erfahren</button>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
<div id="account-view">
|
|
||||||
<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">
|
|
||||||
<button id="btn-forgot-password" type="button">Passwort vergessen?</button>
|
|
||||||
|
|
||||||
<div id="login-error" class="hidden">
|
|
||||||
Falsche E-Mail oder Passwort
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button id="btn-login-account" type="button">Anmelden</button>
|
|
||||||
<p id="account-register-hint">Noch kein Konto?</p>
|
|
||||||
<button id="btn-open-register" type="button">Registrieren</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="register-modal" class="modal hidden">
|
|
||||||
<div class="modal-content account-auth-modal">
|
|
||||||
<button id="btn-close-register" class="modal-close-btn" type="button" aria-label="Registrierung schliessen">×</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">
|
|
||||||
<button id="btn-register-save" class="auth-submit-btn" type="button">Konto erstellen</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="forgot-modal" class="modal hidden">
|
|
||||||
<div class="modal-content account-auth-modal">
|
|
||||||
<button id="btn-close-forgot" class="modal-close-btn" type="button" aria-label="Passwort-Dialog schliessen">×</button>
|
|
||||||
<h2>Passwort vergessen</h2>
|
|
||||||
<p class="auth-modal-subtitle">Gib deine E-Mail ein und wir senden dir einen Reset-Code.</p>
|
|
||||||
|
|
||||||
<input type="email" id="forgot-email" placeholder="E-Mail">
|
|
||||||
<button id="btn-send-reset" class="auth-submit-btn" type="button">Code senden</button>
|
|
||||||
|
|
||||||
<div id="reset-message" class="hidden">
|
|
||||||
Ein Code wurde simuliert versendet.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { users, currentUser, persistUsers, persistCurrentUser } from "../scripts/bigConstants";
|
|
||||||
|
|
||||||
async function hashMessage(message: string) {
|
|
||||||
const msgBuffer = new TextEncoder().encode(message);
|
|
||||||
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
|
|
||||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
||||||
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeHtml(value: string) {
|
|
||||||
return String(value || "").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatEuro(value: any) {
|
|
||||||
return `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const openModal = (modal: HTMLElement | null) => modal?.classList.remove("hidden");
|
|
||||||
const closeModal = (modal: HTMLElement | null) => modal?.classList.add("hidden");
|
|
||||||
|
|
||||||
function renderPersonalInfo() {
|
|
||||||
const target = document.getElementById("account-tab-content");
|
|
||||||
if (!target || !currentUser) return;
|
|
||||||
target.innerHTML = `
|
|
||||||
<div class="account-card">
|
|
||||||
<p><strong>Vorname:</strong> ${currentUser.firstName || "-"}</p>
|
|
||||||
<p><strong>Nachname:</strong> ${currentUser.lastName || "-"}</p>
|
|
||||||
<p><strong>E-Mail:</strong> ${currentUser.email || "-"}</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderOrders() {
|
|
||||||
const target = document.getElementById("account-tab-content");
|
|
||||||
if (!target || !currentUser) return;
|
|
||||||
const orders = Array.isArray(currentUser.orders) ? currentUser.orders : [];
|
|
||||||
if (!orders.length) {
|
|
||||||
target.innerHTML = `<div class="account-card"><h3>Meine Bestellungen</h3><p>Noch keine Bestellungen vorhanden.</p></div>`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const orderHtml = orders.map((order, index) => {
|
|
||||||
const movieItems = Array.isArray(order.items) ? order.items.filter((item: any) => item.category === "movie") : [];
|
|
||||||
const previewTitle = movieItems[0]?.title || (Array.isArray(order.items) ? order.items[0]?.title : "Bestellung");
|
|
||||||
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> ${movieItems.length}x</p>
|
|
||||||
</button>`;
|
|
||||||
}).join("");
|
|
||||||
|
|
||||||
target.innerHTML = `
|
|
||||||
<div class="account-orders-shell">
|
|
||||||
<h3>Meine Bestellungen</h3>
|
|
||||||
<p class="account-payments-note">Klicke auf eine Bestellung, um dein Ticket-Detail zu sehen.</p>
|
|
||||||
<div class="account-orders-grid">${orderHtml}</div>
|
|
||||||
<div id="order-ticket-details" class="order-ticket-details hidden"></div>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
target.querySelectorAll(".order-item-btn").forEach(btn => btn.addEventListener("click", () => {
|
|
||||||
const orderIndex = Number((btn as HTMLElement).dataset.orderIndex);
|
|
||||||
const order = orders[orderIndex];
|
|
||||||
const detailTarget = document.getElementById("order-ticket-details");
|
|
||||||
if (!order || !detailTarget) return;
|
|
||||||
const movieItems = Array.isArray(order.items) ? order.items.filter((item: any) => item.category === "movie") : [];
|
|
||||||
const primaryMovie = movieItems[0];
|
|
||||||
detailTarget.innerHTML = `
|
|
||||||
<article class="order-ticket-card">
|
|
||||||
<div class="order-ticket-poster">${primaryMovie?.img ? `<img src="${escapeHtml(primaryMovie.img)}" />` : `<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(primaryMovie?.hall || "-")}</strong></p>
|
|
||||||
<p><span>Uhrzeit</span><strong>${escapeHtml(primaryMovie?.time || "-")} Uhr</strong></p>
|
|
||||||
<p><span>Tickets</span><strong>${movieItems.length}x</strong></p>
|
|
||||||
<p><span>Sitze</span><strong>${movieItems.map(i => i.seatId).join(", ")}</strong></p>
|
|
||||||
<p><span>Gesamt</span><strong>${formatEuro(order.total || 0)}</strong></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>`;
|
|
||||||
detailTarget.classList.remove("hidden");
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderPayments() {
|
|
||||||
const target = document.getElementById("account-tab-content");
|
|
||||||
if (!target) return;
|
|
||||||
target.innerHTML = `
|
|
||||||
<div class="account-card">
|
|
||||||
<h3>Zahlungsmethoden</h3>
|
|
||||||
<div class="account-payment-grid">
|
|
||||||
<button class="account-payment-card account-pay-trigger" data-pay-modal="pay-modal-card">Visa / Mastercard</button>
|
|
||||||
<button class="account-payment-card account-pay-trigger" data-pay-modal="pay-modal-paypal">PayPal</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="pay-modal-card" class="pay-modal-overlay hidden">Card Modal Content</div>
|
|
||||||
`;
|
|
||||||
// ... (simplified for now to keep it concise, but can add full logic if needed)
|
|
||||||
}
|
|
||||||
|
|
||||||
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><button id="logout-btn" class="account-logout-btn">Abmelden</button></div>
|
|
||||||
<div class="account-tabs">
|
|
||||||
<button id="tab-info" class="account-tab-btn">Persönliche Daten</button>
|
|
||||||
<button id="tab-orders" class="account-tab-btn">Meine Bestellungen</button>
|
|
||||||
</div>
|
|
||||||
<div id="account-tab-content"></div>
|
|
||||||
</div>`;
|
|
||||||
document.getElementById("logout-btn")?.addEventListener("click", () => {
|
|
||||||
persistCurrentUser(null);
|
|
||||||
window.location.reload();
|
|
||||||
});
|
|
||||||
document.getElementById("tab-info")?.addEventListener("click", renderPersonalInfo);
|
|
||||||
document.getElementById("tab-orders")?.addEventListener("click", renderOrders);
|
|
||||||
renderPersonalInfo();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login/Register bindings
|
|
||||||
document.getElementById("btn-login-account")?.addEventListener("click", async () => {
|
|
||||||
const email = (document.getElementById("login-email") as HTMLInputElement)?.value.toLowerCase();
|
|
||||||
const password = (document.getElementById("login-password") as HTMLInputElement)?.value;
|
|
||||||
const hashedPassword = await hashMessage(password);
|
|
||||||
const user = users.find(u => u.email.toLowerCase() === email && u.hashedPassword === hashedPassword);
|
|
||||||
if (user) {
|
|
||||||
persistCurrentUser(user);
|
|
||||||
openAccountDashboard();
|
|
||||||
} else {
|
|
||||||
document.getElementById("login-error")?.classList.remove("hidden");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById("btn-register-save")?.addEventListener("click", async () => {
|
|
||||||
const firstName = (document.getElementById("reg-firstname") as HTMLInputElement).value;
|
|
||||||
const email = (document.getElementById("reg-email") as HTMLInputElement).value.toLowerCase();
|
|
||||||
const password = (document.getElementById("reg-password") as HTMLInputElement).value;
|
|
||||||
const hashedPassword = await hashMessage(password);
|
|
||||||
const newUser = { firstName, email, hashedPassword, orders: [], paymentMethods: [] };
|
|
||||||
users.push(newUser);
|
|
||||||
persistUsers();
|
|
||||||
persistCurrentUser(newUser);
|
|
||||||
openAccountDashboard();
|
|
||||||
closeModal(document.getElementById("register-modal"));
|
|
||||||
});
|
|
||||||
|
|
||||||
if (currentUser) openAccountDashboard();
|
|
||||||
</script>
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
<div id="booking-modal" class="modal hidden">
|
|
||||||
<div class="modal-content modal-large">
|
|
||||||
<div class="modal-header">
|
|
||||||
<div class="header-text-container">
|
|
||||||
<h2 id="modal-movie-title">Film Titel</h2>
|
|
||||||
<div class="header-sub-info">
|
|
||||||
<p id="modal-info-text">Saal • Zeit</p>
|
|
||||||
<div id="tech-badges" class="tech-badges-container hidden">
|
|
||||||
<img src="/img/Dolby.png" alt="Dolby" class="tech-badge">
|
|
||||||
<img src="/img/dbox.jpg" alt="D-Box" class="tech-badge">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span class="close-btn">×</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="screen-container">
|
|
||||||
<div class="screen"></div>
|
|
||||||
<p class="screen-text">LEINWAND</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="booking-layout">
|
|
||||||
<div class="seat-map-container">
|
|
||||||
<div id="seat-grid" class="seat-grid-custom"></div>
|
|
||||||
</div>
|
|
||||||
<div id="booking-summary" class="summary-panel hidden">
|
|
||||||
<h3>Deine Auswahl</h3>
|
|
||||||
<div id="summary-items"></div>
|
|
||||||
<div class="summary-total">
|
|
||||||
<div class="divider"></div>
|
|
||||||
<div class="total-row">
|
|
||||||
<span>Gesamtbetrag:</span>
|
|
||||||
<span id="total-price">0,00€</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="legend" id="dynamic-legend"></div>
|
|
||||||
|
|
||||||
<button id="btn-confirm-seats" class="btn-primary" style="margin-top:20px">Plätze bestätigen</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { seatLayouts, occupiedSeatsData, prices, cart, updateCart } from "../scripts/bigConstants";
|
|
||||||
|
|
||||||
let currentBookingContext: any = null;
|
|
||||||
let currentHallLayout: any = null;
|
|
||||||
|
|
||||||
function getRowLabel(rowIndex: number) { return String(rowIndex + 1); }
|
|
||||||
|
|
||||||
function buildHallLayout(hallName: string, baseConfig: any) {
|
|
||||||
const rows = Number(baseConfig.rows || 0);
|
|
||||||
const totalCols = Number(baseConfig.left || 0) + Number(baseConfig.right || 0);
|
|
||||||
const isDeluxe = /deluxe/i.test(hallName);
|
|
||||||
const left = isDeluxe ? Math.max(3, Number(baseConfig.left || 0) - 1) : Number(baseConfig.left || 0);
|
|
||||||
const right = Math.max(0, totalCols - left);
|
|
||||||
const vipRows = rows > 0 ? [rows] : [];
|
|
||||||
const dboxMap = new Set();
|
|
||||||
const markDboxRange = (row: number, start: number, width: number) => {
|
|
||||||
for (let c = start; c < Math.min(totalCols, start + width); c++) dboxMap.add(`${row}-${c}`);
|
|
||||||
};
|
|
||||||
// ... (simplified logic) ...
|
|
||||||
return { rows, left, right, totalCols, vipRows, dboxMap, isImax: Boolean(baseConfig.isImax) };
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateBookingSummary() {
|
|
||||||
const selectedSeats = Array.from(document.querySelectorAll("#seat-grid .seat.selected")) as HTMLElement[];
|
|
||||||
const totalEl = document.getElementById("total-price");
|
|
||||||
const summaryItems = document.getElementById("summary-items");
|
|
||||||
let total = 0;
|
|
||||||
if (summaryItems) {
|
|
||||||
summaryItems.innerHTML = selectedSeats.map(seat => {
|
|
||||||
const type = (seat.dataset.type || "normal") as keyof typeof prices;
|
|
||||||
const p = prices[type] || prices.normal;
|
|
||||||
total += p;
|
|
||||||
return `<div class="summary-row"><span>${seat.dataset.seatId}</span><span>${p.toFixed(2).replace(".", ",")} EUR</span></div>`;
|
|
||||||
}).join("");
|
|
||||||
}
|
|
||||||
if (totalEl) totalEl.innerText = `${total.toFixed(2).replace(".", ",")} EUR`;
|
|
||||||
document.getElementById("booking-summary")?.classList.toggle("hidden", selectedSeats.length === 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createSeats(hallName: string, time: any) {
|
|
||||||
const seatGrid = document.getElementById("seat-grid");
|
|
||||||
if (!seatGrid) return;
|
|
||||||
seatGrid.innerHTML = "";
|
|
||||||
const baseConfig = seatLayouts[hallName as keyof typeof seatLayouts];
|
|
||||||
if (!baseConfig) return;
|
|
||||||
currentHallLayout = buildHallLayout(hallName, baseConfig);
|
|
||||||
const occupiedKey = `${hallName}-${time}`;
|
|
||||||
const occupied = new Set(occupiedSeatsData[occupiedKey] || []);
|
|
||||||
|
|
||||||
for (let r = 1; r <= currentHallLayout.rows; r++) {
|
|
||||||
const row = document.createElement("div");
|
|
||||||
row.className = "seat-row cinema-row";
|
|
||||||
for (let c = 1; c <= currentHallLayout.totalCols; c++) {
|
|
||||||
const seatId = `R${r}-P${c}`;
|
|
||||||
const seat = document.createElement("button");
|
|
||||||
seat.className = "seat " + (currentHallLayout.isImax ? "imax" : "normal");
|
|
||||||
seat.dataset.seatId = seatId;
|
|
||||||
if (occupied.has(seatId)) { seat.classList.add("occupied"); (seat as HTMLButtonElement).disabled = true; }
|
|
||||||
else seat.addEventListener("click", () => { seat.classList.toggle("selected"); updateBookingSummary(); });
|
|
||||||
row.appendChild(seat);
|
|
||||||
}
|
|
||||||
seatGrid.appendChild(row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(window as any).openBooking = (movie: string, hall: string, time: any) => {
|
|
||||||
document.getElementById("modal-movie-title")!.innerText = movie;
|
|
||||||
document.getElementById("modal-info-text")!.innerText = `${hall} • ${time} Uhr`;
|
|
||||||
currentBookingContext = { movie, hall, time };
|
|
||||||
createSeats(hall, time);
|
|
||||||
document.getElementById("booking-modal")?.classList.remove("hidden");
|
|
||||||
};
|
|
||||||
|
|
||||||
document.getElementById("btn-confirm-seats")?.addEventListener("click", () => {
|
|
||||||
const selected = Array.from(document.querySelectorAll("#seat-grid .seat.selected")) as HTMLElement[];
|
|
||||||
if (!selected.length) return alert("Bitte wähle Plätze aus.");
|
|
||||||
selected.forEach(seat => {
|
|
||||||
cart.push({
|
|
||||||
id: Date.now() + Math.random(),
|
|
||||||
category: "movie",
|
|
||||||
title: currentBookingContext.movie,
|
|
||||||
hall: currentBookingContext.hall,
|
|
||||||
time: currentBookingContext.time,
|
|
||||||
seatId: seat.dataset.seatId,
|
|
||||||
price: prices[seat.dataset.type as keyof typeof prices] || prices.normal
|
|
||||||
});
|
|
||||||
});
|
|
||||||
updateCart(cart);
|
|
||||||
window.dispatchEvent(new CustomEvent("cart-updated"));
|
|
||||||
document.getElementById("booking-modal")?.classList.add("hidden");
|
|
||||||
document.getElementById("snack-prompt-overlay")?.classList.remove("hidden");
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const bookingModal = document.getElementById("booking-modal");
|
|
||||||
const closeBtn = bookingModal?.querySelector(".close-btn");
|
|
||||||
|
|
||||||
const closeBookingModal = () => {
|
|
||||||
bookingModal?.classList.add("hidden");
|
|
||||||
};
|
|
||||||
|
|
||||||
closeBtn?.addEventListener("click", closeBookingModal);
|
|
||||||
|
|
||||||
bookingModal?.addEventListener("click", (event) => {
|
|
||||||
if (event.target === bookingModal) closeBookingModal();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener("keydown", (event) => {
|
|
||||||
if (event.key === "Escape") closeBookingModal();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
<section id="cart-view" class="cart-section">
|
|
||||||
<div class="container" style="padding: 120px 8% 50px 8%;">
|
|
||||||
<h1 class="list-title">Dein Warenkorb</h1>
|
|
||||||
|
|
||||||
<div class="cart-container">
|
|
||||||
<div class="cart-left">
|
|
||||||
<div id="cart-items-list"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="cart-right">
|
|
||||||
<div class="summary-box-black">
|
|
||||||
<h2>Zusammenfassung</h2>
|
|
||||||
<div class="summary-row-large">
|
|
||||||
<span>Gesamtsumme:</span>
|
|
||||||
<span id="cart-total-right">0,00€</span>
|
|
||||||
</div>
|
|
||||||
<p id="cart-vat-right" style="text-align:right; color:#86868b; font-size:0.8rem; margin-top:5px;">
|
|
||||||
inkl. 19% MwSt: 0,00€
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<button id="btn-checkout-final" class="blue-button">Jetzt kostenpflichtig bestellen</button>
|
|
||||||
|
|
||||||
<div class="payment-methods" style="margin-top: 25px; text-align: center;">
|
|
||||||
<p style="font-size: 0.75rem; color: #86868b; margin-bottom: 12px;">Sicher bezahlen mit</p>
|
|
||||||
<div style="display: flex; justify-content: center; gap: 15px; opacity: 0.6;">
|
|
||||||
<img src="/img/paypal.png" alt="PayPal" style="height: 20px;">
|
|
||||||
<img src="/img/visa.png" alt="Visa" style="height: 20px;">
|
|
||||||
<img src="/img/mastercard.png" alt="Mastercard" style="height: 20px;">
|
|
||||||
<img src="/img/applepay.png" alt="Apple Pay" style="height: 20px;">
|
|
||||||
<img src="/img/googlepay.png" alt="Google Pay" style="height: 20px;">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { cart, updateCart } from "../scripts/bigConstants";
|
|
||||||
|
|
||||||
function formatEuro(value: number) {
|
|
||||||
return `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeHtml(value: any) {
|
|
||||||
return String(value || "")
|
|
||||||
.replaceAll("&", "&")
|
|
||||||
.replaceAll("<", "<")
|
|
||||||
.replaceAll(">", ">")
|
|
||||||
.replaceAll('"', """)
|
|
||||||
.replaceAll("'", "'");
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildCartKey(item: any) {
|
|
||||||
const infoText = item.category === "movie"
|
|
||||||
? `Sitz: ${item.seatId} (${item.hall})`
|
|
||||||
: item.time;
|
|
||||||
return `${item.title}-${item.hall}-${infoText}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isDrinkItem(item: any) {
|
|
||||||
if (item.category !== "snack") return false;
|
|
||||||
const title = String(item.title || "").toLowerCase();
|
|
||||||
const size = String(item.hall || "").toLowerCase();
|
|
||||||
const drinkKeywords = ["cola", "sprite", "fanta", "mezzo", "fuze", "wasser", "getraenk", "drink"];
|
|
||||||
return drinkKeywords.some((word) => title.includes(word)) || size.includes("l");
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildItemInfo(item: any) {
|
|
||||||
if (item.category === "movie") {
|
|
||||||
return `
|
|
||||||
<div>Sitzplatz: ${escapeHtml(item.seatId || "-")}</div>
|
|
||||||
<div>Saal: ${escapeHtml(item.hall || "-")}</div>
|
|
||||||
<div>Uhrzeit: ${escapeHtml(item.time || "-")} Uhr</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
if (isDrinkItem(item)) {
|
|
||||||
return `
|
|
||||||
<div>Variante: ${escapeHtml(item.time || "-")}</div>
|
|
||||||
<div>Groesse: ${escapeHtml(item.hall || "-")}</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
return `
|
|
||||||
<div>Kategorie: Snack</div>
|
|
||||||
<div>Variante: ${escapeHtml(item.time || "-")}</div>
|
|
||||||
<div>Groesse: ${escapeHtml(item.hall || "-")}</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function groupCartItems() {
|
|
||||||
const groups = new Map();
|
|
||||||
cart.forEach((item: any) => {
|
|
||||||
const key = buildCartKey(item);
|
|
||||||
if (!groups.has(key)) {
|
|
||||||
groups.set(key, { key, quantity: 0, total: 0, item });
|
|
||||||
}
|
|
||||||
const group = groups.get(key);
|
|
||||||
group.quantity += 1;
|
|
||||||
group.total += Number(item.price || 0);
|
|
||||||
});
|
|
||||||
return Array.from(groups.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
export function renderCart() {
|
|
||||||
const cartList = document.getElementById("cart-items-list");
|
|
||||||
const totalEl = document.getElementById("cart-total-right");
|
|
||||||
const vatEl = document.getElementById("cart-vat-right");
|
|
||||||
|
|
||||||
if (!cartList || !totalEl || !vatEl) return;
|
|
||||||
|
|
||||||
if (!Array.isArray(cart) || cart.length === 0) {
|
|
||||||
cartList.innerHTML = '<p>Dein Warenkorb ist leer.</p>';
|
|
||||||
totalEl.innerText = formatEuro(0);
|
|
||||||
vatEl.innerText = `inkl. 19% MwSt: ${formatEuro(0)}`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const groupedItems = groupCartItems();
|
|
||||||
const header = `
|
|
||||||
<div class="cart-header-row">
|
|
||||||
<div class="col-amount">MENGE</div>
|
|
||||||
<div class="col-img">VORSCHAU</div>
|
|
||||||
<div class="col-product">NAME</div>
|
|
||||||
<div class="col-details">INFO</div>
|
|
||||||
<div class="col-price">PREIS</div>
|
|
||||||
<div class="col-action">AKTION</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const rows = groupedItems.map((group) => {
|
|
||||||
const imageHtml = group.item.img
|
|
||||||
? `<img class="cart-img-small" src="${escapeHtml(group.item.img)}" alt="${escapeHtml(group.item.title)}">`
|
|
||||||
: `<div class="cart-img-fallback">Kein Bild</div>`;
|
|
||||||
const quantityHtml = group.item.category === "movie"
|
|
||||||
? `<div class="qty-static" aria-label="Feste Ticketanzahl">${group.quantity}x</div>`
|
|
||||||
: `
|
|
||||||
<div class="qty-stepper">
|
|
||||||
<button class="btn-qty" data-action="minus" data-key="${escapeHtml(group.key)}">-</button>
|
|
||||||
<span>${group.quantity}</span>
|
|
||||||
<button class="btn-qty" data-action="plus" data-key="${escapeHtml(group.key)}">+</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="cart-item-row">
|
|
||||||
<div class="col-amount">${quantityHtml}</div>
|
|
||||||
<div class="col-img">${imageHtml}</div>
|
|
||||||
<div class="col-product">${escapeHtml(group.item.title)}</div>
|
|
||||||
<div class="col-details cart-item-info">${buildItemInfo(group.item)}</div>
|
|
||||||
<div class="col-price">${formatEuro(group.total)}</div>
|
|
||||||
<div class="col-action">
|
|
||||||
<button class="btn-delete-item" data-key="${escapeHtml(group.key)}" aria-label="Eintrag entfernen"><span aria-hidden="true">🗑️</span></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}).join("");
|
|
||||||
|
|
||||||
cartList.innerHTML = header + rows;
|
|
||||||
|
|
||||||
const total = cart.reduce((sum, item) => sum + Number(item.price || 0), 0);
|
|
||||||
const vat = total - total / 1.19;
|
|
||||||
totalEl.innerText = formatEuro(total);
|
|
||||||
vatEl.innerText = `inkl. 19% MwSt: ${formatEuro(vat)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("cart-updated", renderCart);
|
|
||||||
renderCart();
|
|
||||||
|
|
||||||
document.getElementById("cart-view")?.addEventListener("click", (event: any) => {
|
|
||||||
const deleteBtn = event.target.closest(".btn-delete-item");
|
|
||||||
if (deleteBtn) {
|
|
||||||
const key = deleteBtn.dataset.key;
|
|
||||||
const newCart = cart.filter(item => buildCartKey(item) !== key);
|
|
||||||
updateCart(newCart);
|
|
||||||
window.dispatchEvent(new CustomEvent("cart-updated"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const qtyBtn = event.target.closest(".btn-qty");
|
|
||||||
if (qtyBtn) {
|
|
||||||
const action = qtyBtn.dataset.action;
|
|
||||||
const key = qtyBtn.dataset.key;
|
|
||||||
if (action === "plus") {
|
|
||||||
const item = cart.find(i => buildCartKey(i) === key);
|
|
||||||
if (item) cart.push({...item, id: Date.now() + Math.random()});
|
|
||||||
} else {
|
|
||||||
const idx = cart.findIndex(i => buildCartKey(i) === key);
|
|
||||||
if (idx !== -1) cart.splice(idx, 1);
|
|
||||||
}
|
|
||||||
updateCart(cart);
|
|
||||||
window.dispatchEvent(new CustomEvent("cart-updated"));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById("btn-checkout-final")?.addEventListener("click", () => {
|
|
||||||
if (!cart.length) {
|
|
||||||
alert("Dein Warenkorb ist leer.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
window.location.href = "/checkout";
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
@@ -1,238 +0,0 @@
|
|||||||
<section id="checkout-view" style="padding: 40px 20px;">
|
|
||||||
<div class="checkout-container">
|
|
||||||
<div class="progress-bar">
|
|
||||||
<div class="step active" id="step-1-indicator">1</div>
|
|
||||||
<div class="line" id="line-1"></div>
|
|
||||||
<div class="step" id="step-2-indicator">2</div>
|
|
||||||
<div class="line" id="line-2"></div>
|
|
||||||
<div class="step" id="step-3-indicator">3</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="checkout-step-1" class="checkout-step">
|
|
||||||
<h2 style="text-align: center; margin-bottom: 20px;">Zahlungsmethode wählen</h2>
|
|
||||||
<div class="payment-grid">
|
|
||||||
<div class="payment-method" data-method="Apple Pay">
|
|
||||||
<img src="/img/applepay.png" alt="Apple Pay" style="height: 20px;">
|
|
||||||
<span>Apple Pay</span>
|
|
||||||
</div>
|
|
||||||
<div class="payment-method" data-method="PayPal">
|
|
||||||
<img src="/img/paypal.png" alt="PayPal" style="height: 20px;">
|
|
||||||
<span>PayPal</span>
|
|
||||||
</div>
|
|
||||||
<div class="payment-method" data-method="Google Pay">
|
|
||||||
<img src="/img/googlepay.png" alt="Google Pay" style="height: 20px;">
|
|
||||||
<span>Google Pay</span>
|
|
||||||
</div>
|
|
||||||
<div class="payment-method" data-method="Visa">
|
|
||||||
<img src="/img/visa.png" alt="Visa" style="height: 20px;">
|
|
||||||
<span>Visa</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button id="btn-next-step-2" class="hidden" style="margin-top: 25px; width: 100%; padding: 15px; background: #0071e3; color: white; border: none; border-radius: 8px; font-weight: bold; cursor: pointer;">Weiter zur Übersicht</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="checkout-step-2" class="checkout-step hidden">
|
|
||||||
<div style="position: relative;">
|
|
||||||
<button id="btn-back-to-step1" style="position: absolute; top: -15px; left: 0; background: none; border: none; color: white; font-size: 1.5rem; cursor: pointer; opacity: 0.6; transition: 0.3s;">←</button>
|
|
||||||
<h2 style="text-align: center; margin-bottom: 25px;">Persönliche Daten</h2>
|
|
||||||
</div>
|
|
||||||
<h2 style="text-align: center; margin-bottom: 20px;">Bestellübersicht</h2>
|
|
||||||
<div id="checkout-summary-list" style="background: #222; padding: 15px; border-radius: 8px;"></div>
|
|
||||||
<h3 id="checkout-total-display" style="text-align: right; margin-top: 15px;"></h3>
|
|
||||||
<div id="checkout-vat-display" style="text-align: right; color: #86868b; font-size: 0.8rem;"></div>
|
|
||||||
<button id="btn-pay-now" style="margin-top: 25px; width: 100%; padding: 15px; background: #4caf50; color: white; border: none; border-radius: 8px; font-weight: bold; cursor: pointer;">Jetzt Bezahlen</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="checkout-step-3" class="checkout-step hidden">
|
|
||||||
<h2 style="color: #4caf50; text-align: center;">Kauf erfolgreich!</h2>
|
|
||||||
<div id="ticket-container" style="margin-top: 30px;"></div>
|
|
||||||
<button id="btn-back-home" style="margin-top: 30px; width: 100%; padding: 15px; background: #333; color: white; border: none; border-radius: 8px; cursor: pointer;">Zurück zur Startseite</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { currentUser, users, cart, emptyCart, occupiedSeatsData, updateCart, updateOccupiedSeats } from "../scripts/bigConstants";
|
|
||||||
|
|
||||||
function formatCheckoutEuro(value: number) {
|
|
||||||
return `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let selectedPaymentMethod = "";
|
|
||||||
|
|
||||||
function setCheckoutStep(step: number) {
|
|
||||||
const step1 = document.getElementById("checkout-step-1");
|
|
||||||
const step2 = document.getElementById("checkout-step-2");
|
|
||||||
const step3 = document.getElementById("checkout-step-3");
|
|
||||||
step1?.classList.toggle("hidden", step !== 1);
|
|
||||||
step2?.classList.toggle("hidden", step !== 2);
|
|
||||||
step3?.classList.toggle("hidden", step !== 3);
|
|
||||||
|
|
||||||
const line1 = document.getElementById("line-1");
|
|
||||||
const line2 = document.getElementById("line-2");
|
|
||||||
const indicator1 = document.getElementById("step-1-indicator");
|
|
||||||
const indicator2 = document.getElementById("step-2-indicator");
|
|
||||||
const indicator3 = document.getElementById("step-3-indicator");
|
|
||||||
|
|
||||||
indicator1?.classList.add("active");
|
|
||||||
indicator2?.classList.toggle("active", step >= 2);
|
|
||||||
indicator3?.classList.toggle("active", step >= 3);
|
|
||||||
line1?.classList.toggle("active", step >= 2);
|
|
||||||
line2?.classList.toggle("active", step >= 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderCheckout() {
|
|
||||||
const summaryList = document.getElementById("checkout-summary-list");
|
|
||||||
const totalDisplay = document.getElementById("checkout-total-display");
|
|
||||||
const vatDisplay = document.getElementById("checkout-vat-display");
|
|
||||||
const nextButton = document.getElementById("btn-next-step-2");
|
|
||||||
|
|
||||||
if (!summaryList) return;
|
|
||||||
summaryList.innerHTML = "";
|
|
||||||
|
|
||||||
const total = cart.reduce((sum: number, item: any) => sum + Number(item.price || 0), 0);
|
|
||||||
const vat = total - total / 1.19;
|
|
||||||
|
|
||||||
cart.forEach((item: any) => {
|
|
||||||
const row = document.createElement("div");
|
|
||||||
row.style.cssText = "display:flex; justify-content:space-between; gap:12px; margin-bottom:10px; font-size:0.95rem;";
|
|
||||||
const infoText = item.category === "movie"
|
|
||||||
? `Sitz ${item.seatId || "-"} | ${item.hall || "-"} | ${item.time || "-"} Uhr`
|
|
||||||
: `${item.time || "Standard"} | ${item.hall || "-"}`;
|
|
||||||
row.innerHTML = `<span>${item.title} (${infoText})</span><span>${formatCheckoutEuro(item.price)}</span>`;
|
|
||||||
summaryList.appendChild(row);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (totalDisplay) totalDisplay.innerText = `Gesamtbetrag: ${formatCheckoutEuro(total)}`;
|
|
||||||
if (vatDisplay) vatDisplay.innerText = `inkl. 19% MwSt: ${formatCheckoutEuro(vat)}`;
|
|
||||||
|
|
||||||
selectedPaymentMethod = "";
|
|
||||||
document.querySelectorAll(".payment-method").forEach((method) => method.classList.remove("selected"));
|
|
||||||
nextButton?.classList.add("hidden");
|
|
||||||
setCheckoutStep(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateTicket() {
|
|
||||||
const ticketContainer = document.getElementById("ticket-container");
|
|
||||||
if (!ticketContainer) return;
|
|
||||||
|
|
||||||
const moviesInCart = cart.filter((item: any) => item.category === "movie");
|
|
||||||
if (!moviesInCart.length) {
|
|
||||||
ticketContainer.innerHTML = "<p>Danke für deinen Einkauf!</p>";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mainMovie = moviesInCart[0];
|
|
||||||
const matchingMovieSeats = moviesInCart
|
|
||||||
.filter((item: any) => item.title === mainMovie.title && item.time === mainMovie.time)
|
|
||||||
.map((item: any) => item.seatId)
|
|
||||||
.join(", ");
|
|
||||||
|
|
||||||
const qrData = encodeURIComponent(`EAGLE-IMAX|${mainMovie.title}|${mainMovie.hall}|${matchingMovieSeats}`);
|
|
||||||
const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=${qrData}&bgcolor=ffffff`;
|
|
||||||
|
|
||||||
ticketContainer.innerHTML = `
|
|
||||||
<div class="luxury-ticket">
|
|
||||||
<div class="ticket-left">
|
|
||||||
<img src="${mainMovie.img || mainMovie.poster}" class="ticket-poster" alt="${mainMovie.title}">
|
|
||||||
</div>
|
|
||||||
<div class="ticket-right">
|
|
||||||
<div class="ticket-brand">EAGLE'S IMAX PREMIUM</div>
|
|
||||||
<h2 class="ticket-title">${mainMovie.title}</h2>
|
|
||||||
<div class="ticket-details">
|
|
||||||
<p><span>SAAL</span> <strong>${mainMovie.hall}</strong></p>
|
|
||||||
<p><span>ZEIT</span> <strong>${mainMovie.time} Uhr</strong></p>
|
|
||||||
<p><span>SITZE</span> <strong>${matchingMovieSeats || "-"}</strong></p>
|
|
||||||
</div>
|
|
||||||
<div class="ticket-footer">
|
|
||||||
<img src="${qrUrl}" class="ticket-qr" alt="QR Code">
|
|
||||||
<div class="ticket-code">#${Math.floor(Math.random() * 90000) + 10000}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function completeCheckout() {
|
|
||||||
const orderItems = [...cart];
|
|
||||||
const orderTotal = orderItems.reduce((sum, item) => sum + Number(item.price || 0), 0);
|
|
||||||
|
|
||||||
// Save order for current user
|
|
||||||
if (currentUser && Array.isArray(users)) {
|
|
||||||
const userIndex = users.findIndex((entry: any) => entry.email === currentUser.email);
|
|
||||||
if (userIndex !== -1) {
|
|
||||||
if (!Array.isArray(users[userIndex].orders)) users[userIndex].orders = [];
|
|
||||||
users[userIndex].orders.push({
|
|
||||||
date: new Date().toLocaleString("de-DE"),
|
|
||||||
items: orderItems,
|
|
||||||
total: orderTotal,
|
|
||||||
paymentMethod: selectedPaymentMethod || "-"
|
|
||||||
});
|
|
||||||
localStorage.setItem("eagleUsers", JSON.stringify(users));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reserve seats
|
|
||||||
orderItems.filter(item => item.category === "movie").forEach(item => {
|
|
||||||
const key = `${item.hall}-${item.time}`;
|
|
||||||
if (!occupiedSeatsData[key]) occupiedSeatsData[key] = [];
|
|
||||||
occupiedSeatsData[key].push(item.seatId);
|
|
||||||
});
|
|
||||||
updateOccupiedSeats(occupiedSeatsData);
|
|
||||||
|
|
||||||
emptyCart();
|
|
||||||
window.dispatchEvent(new CustomEvent("cart-updated"));
|
|
||||||
}
|
|
||||||
|
|
||||||
function bindCheckoutEvents() {
|
|
||||||
const nextButton = document.getElementById("btn-next-step-2");
|
|
||||||
const backButton = document.getElementById("btn-back-to-step1");
|
|
||||||
const payNowButton = document.getElementById("btn-pay-now") as HTMLButtonElement;
|
|
||||||
|
|
||||||
document.querySelectorAll(".payment-method").forEach((method) => {
|
|
||||||
method.addEventListener("click", () => {
|
|
||||||
document.querySelectorAll(".payment-method").forEach((entry) => entry.classList.remove("selected"));
|
|
||||||
method.classList.add("selected");
|
|
||||||
selectedPaymentMethod = (method as HTMLElement).dataset.method || "";
|
|
||||||
nextButton?.classList.remove("hidden");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
nextButton?.addEventListener("click", () => {
|
|
||||||
if (!selectedPaymentMethod) {
|
|
||||||
alert("Bitte wähle zuerst eine Zahlungsmethode aus.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setCheckoutStep(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
backButton?.addEventListener("click", () => setCheckoutStep(1));
|
|
||||||
|
|
||||||
payNowButton?.addEventListener("click", () => {
|
|
||||||
if (!cart.length) {
|
|
||||||
alert("Dein Warenkorb ist leer.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
payNowButton.disabled = true;
|
|
||||||
payNowButton.innerText = "Verarbeite...";
|
|
||||||
payNowButton.style.opacity = "0.7";
|
|
||||||
setTimeout(() => {
|
|
||||||
setCheckoutStep(3);
|
|
||||||
generateTicket();
|
|
||||||
completeCheckout();
|
|
||||||
payNowButton.disabled = false;
|
|
||||||
payNowButton.innerText = "Jetzt Bezahlen";
|
|
||||||
payNowButton.style.opacity = "1";
|
|
||||||
}, 1200);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById("btn-back-home")?.addEventListener("click", () => {
|
|
||||||
window.location.href = "/";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.getElementById("checkout-view")) {
|
|
||||||
renderCheckout();
|
|
||||||
bindCheckoutEvents();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<section id="collectors-view" class="info-view">
|
|
||||||
<div class="container info-view-shell">
|
|
||||||
<button class="subpage-back-btn" data-go-home type="button">← Zur Startseite</button>
|
|
||||||
<h1>Collectors Popcorn Specials</h1>
|
|
||||||
<p class="info-view-subtitle">Filmbezogene Specials in einer links/rechts versetzten Darstellung.</p>
|
|
||||||
<div class="collector-zigzag">
|
|
||||||
<article class="collector-entry left">
|
|
||||||
<div class="collector-film-logo">Filmlogo</div>
|
|
||||||
<div class="collector-entry-content">
|
|
||||||
<h4>Special Becher 01</h4>
|
|
||||||
<p>Kurzbeschreibung des Artikels und Bezug zum Film.</p>
|
|
||||||
<input type="text" placeholder="Bildpfad (z.B. img/becher1.jpg)">
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<article class="collector-entry right">
|
|
||||||
<div class="collector-film-logo">Filmlogo</div>
|
|
||||||
<div class="collector-entry-content">
|
|
||||||
<h4>Special Eimer 02</h4>
|
|
||||||
<p>Weitere Edition mit eigenem Motiv und Text.</p>
|
|
||||||
<input type="text" placeholder="Bildpfad (z.B. img/eimer2.jpg)">
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<article class="collector-entry left">
|
|
||||||
<div class="collector-film-logo">Filmlogo</div>
|
|
||||||
<div class="collector-entry-content">
|
|
||||||
<h4>Special Box 03</h4>
|
|
||||||
<p>Noch ein Eintrag fuer limitierte Collectors.</p>
|
|
||||||
<input type="text" placeholder="Bildpfad (z.B. img/collector3.jpg)">
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
<section id="dbox-view" class="info-view">
|
|
||||||
<div class="container info-view-shell">
|
|
||||||
<button class="subpage-back-btn" data-go-home type="button">← Zur Startseite</button>
|
|
||||||
<h1>D-BOX & Technik</h1>
|
|
||||||
<p class="info-view-subtitle">Inspiriert von Technikseiten moderner Kinos, aber in deinem Design.</p>
|
|
||||||
<section class="dbox-modal-section">
|
|
||||||
<h3>Wie D-BOX funktioniert</h3>
|
|
||||||
<p>D-BOX Sitze reagieren synchron zum Film. Die Bewegungsintensität ist individuell steuerbar und kann bei Bedarf reduziert oder deaktiviert werden.</p>
|
|
||||||
</section>
|
|
||||||
<section class="dbox-modal-section">
|
|
||||||
<h3>Technik im Saal</h3>
|
|
||||||
<p>Ergänze hier deine Informationen zu Projektion, Soundsystem, Sitzkomfort und den Sälen mit D-BOX-Unterstützung.</p>
|
|
||||||
<div class="dbox-image-grid">
|
|
||||||
<div class="dbox-image-slot">Bildplatz Technik 1</div>
|
|
||||||
<div class="dbox-image-slot">Bildplatz Technik 2</div>
|
|
||||||
<div class="dbox-image-slot">Bildplatz Technik 3</div>
|
|
||||||
<div class="dbox-image-slot">Bildplatz Technik 4</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section class="dbox-now-highlight modal-highlight">
|
|
||||||
<h4>Filme in D-BOX</h4>
|
|
||||||
<div class="dbox-mini-cards">
|
|
||||||
<div>Zoomania 2</div>
|
|
||||||
<div>Der Austronaut</div>
|
|
||||||
<div>Spider Man</div>
|
|
||||||
<div>Scream VII</div>
|
|
||||||
<div>Gangster Gang 2</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<section id="halls-view" class="info-view">
|
|
||||||
<div class="container info-view-shell">
|
|
||||||
<button class="subpage-back-btn" data-go-home type="button">← Zur Startseite</button>
|
|
||||||
<h1>Unsere Säle</h1>
|
|
||||||
<p class="info-view-subtitle">Pflegbare Infoseite zu allen Sälen inkl. Bild- und Textbereichen.</p>
|
|
||||||
<div class="hall-modal-grid full-page-grid">
|
|
||||||
<article class="hall-modal-item">
|
|
||||||
<h4>Kino 1</h4>
|
|
||||||
<textarea placeholder="Infos zu Kino 1 eintragen..."></textarea>
|
|
||||||
<input type="text" placeholder="Bildpfad fuer den Hintergrund (z.B. img/saal1.jpg)">
|
|
||||||
</article>
|
|
||||||
<article class="hall-modal-item">
|
|
||||||
<h4>Kino 2</h4>
|
|
||||||
<textarea placeholder="Infos zu Kino 2 eintragen..."></textarea>
|
|
||||||
<input type="text" placeholder="Bildpfad fuer den Hintergrund (z.B. img/saal2.jpg)">
|
|
||||||
</article>
|
|
||||||
<article class="hall-modal-item">
|
|
||||||
<h4>Deluxe 1</h4>
|
|
||||||
<textarea placeholder="Infos zu Deluxe 1 eintragen..."></textarea>
|
|
||||||
<input type="text" placeholder="Bildpfad fuer den Hintergrund (z.B. img/deluxe.jpg)">
|
|
||||||
</article>
|
|
||||||
<article class="hall-modal-item">
|
|
||||||
<h4>IMAX</h4>
|
|
||||||
<textarea placeholder="Infos zu IMAX eintragen..."></textarea>
|
|
||||||
<input type="text" placeholder="Bildpfad fuer den Hintergrund (z.B. img/imax.jpg)">
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
<header class="hero">
|
|
||||||
<div id="hero-slider" class="hero-slider"></div>
|
|
||||||
<div class="hero-overlay">
|
|
||||||
<span class="badge">Neu im Kino</span>
|
|
||||||
<h1 id="hero-title">Jetzt im Kino</h1>
|
|
||||||
<p id="hero-text">Erlebe die neuesten Highlights auf der großen Leinwand.</p>
|
|
||||||
<button class="btn-primary" id="hero-booking-btn">Jetzt Tickets kaufen</button>
|
|
||||||
<div id="hero-dots" class="hero-dots"></div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { movieCatalog } from "../scripts/bigConstants";
|
|
||||||
|
|
||||||
const ui = {
|
|
||||||
heroSlider: document.getElementById("hero-slider"),
|
|
||||||
heroDots: document.getElementById("hero-dots"),
|
|
||||||
heroTitle: document.getElementById("hero-title"),
|
|
||||||
heroText: document.getElementById("hero-text"),
|
|
||||||
heroBookingBtn: document.getElementById("hero-booking-btn"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let heroItems = movieCatalog.slice(0, 5);
|
|
||||||
let heroIndex = 0;
|
|
||||||
let heroTimer: any = null;
|
|
||||||
|
|
||||||
const escapeHtml = (value: string) => String(value || "")
|
|
||||||
.replaceAll("&", "&")
|
|
||||||
.replaceAll("<", "<")
|
|
||||||
.replaceAll(">", ">")
|
|
||||||
.replaceAll('"', """)
|
|
||||||
.replaceAll("'", "'");
|
|
||||||
|
|
||||||
const setHeroSlide = (index: number) => {
|
|
||||||
if (!heroItems.length || !ui.heroSlider) return;
|
|
||||||
heroIndex = (index + heroItems.length) % heroItems.length;
|
|
||||||
|
|
||||||
ui.heroSlider.querySelectorAll(".hero-slide").forEach((slide, slideIndex) => {
|
|
||||||
slide.classList.toggle("active", slideIndex === heroIndex);
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.heroDots?.querySelectorAll(".hero-dot").forEach((dot, dotIndex) => {
|
|
||||||
dot.classList.toggle("active", dotIndex === heroIndex);
|
|
||||||
});
|
|
||||||
|
|
||||||
const activeMovie = heroItems[heroIndex];
|
|
||||||
if (ui.heroTitle) ui.heroTitle.textContent = activeMovie.title;
|
|
||||||
if (ui.heroText) ui.heroText.textContent = `${activeMovie.genre} • ${activeMovie.duration} Min. • Heute erste Vorstellung um 13:00 Uhr.`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderHero = () => {
|
|
||||||
if (!ui.heroSlider || !heroItems.length) return;
|
|
||||||
|
|
||||||
ui.heroSlider.innerHTML = heroItems.map((movie: any, index: number) => `
|
|
||||||
<div class="hero-slide ${index === 0 ? "active" : ""}" style="background-image: linear-gradient(118deg, rgba(0,0,0,0.34), rgba(0,0,0,0.04)), url('${escapeHtml(movie.backdrop || movie.poster)}');"></div>
|
|
||||||
`).join("");
|
|
||||||
|
|
||||||
if (ui.heroDots) {
|
|
||||||
ui.heroDots.innerHTML = heroItems.map((_: any, index: number) => `
|
|
||||||
<button type="button" class="hero-dot ${index === 0 ? "active" : ""}" data-hero-index="${index}"></button>
|
|
||||||
`).join("");
|
|
||||||
|
|
||||||
ui.heroDots.addEventListener("click", (event: any) => {
|
|
||||||
const dot = (event.target as HTMLElement).closest(".hero-dot") as HTMLElement;
|
|
||||||
if (!dot) return;
|
|
||||||
setHeroSlide(Number(dot.dataset.heroIndex || 0));
|
|
||||||
if (heroTimer) {
|
|
||||||
clearInterval(heroTimer);
|
|
||||||
heroTimer = setInterval(() => setHeroSlide(heroIndex + 1), 6500);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setHeroSlide(0);
|
|
||||||
if (heroTimer) clearInterval(heroTimer);
|
|
||||||
heroTimer = setInterval(() => setHeroSlide(heroIndex + 1), 6500);
|
|
||||||
};
|
|
||||||
|
|
||||||
ui.heroBookingBtn?.addEventListener("click", () => {
|
|
||||||
window.location.href = "/movies";
|
|
||||||
});
|
|
||||||
|
|
||||||
renderHero();
|
|
||||||
</script>
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
<section id="movies-grid-section" class="movie-section cinema-home">
|
|
||||||
<div class="section-header reveal-on-scroll">
|
|
||||||
<h2>Dein Kinoabend beginnt hier</h2>
|
|
||||||
<p>Premieren, Lieblingsfilme und flexible Spielzeiten in allen Sälen.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="home-poster-band reveal-on-scroll">
|
|
||||||
<div class="home-band-header">
|
|
||||||
<h3>Jetzt läuft</h3>
|
|
||||||
<span>Heute im Fokus</span>
|
|
||||||
</div>
|
|
||||||
<div id="now-running-row" class="now-running-row"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="home-inline-showcase reveal-on-scroll">
|
|
||||||
<article class="inline-section inline-halls">
|
|
||||||
<div class="inline-media"></div>
|
|
||||||
<div class="inline-content">
|
|
||||||
<h3>Unsere Säle im Überblick</h3>
|
|
||||||
<p>Jeder Saal hat ein eigenes Profil: von klassischem Kinofeeling bis High-End-Erlebnis mit Premiumtechnik und mehr Komfort.</p>
|
|
||||||
<div class="inline-tags">
|
|
||||||
<span>IMAX</span>
|
|
||||||
<span>Deluxe 1</span>
|
|
||||||
<span>Kino 1</span>
|
|
||||||
<span>Kino 2</span>
|
|
||||||
</div>
|
|
||||||
<a href="/halls" class="story-more-btn">Mehr erfahren</a>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article class="inline-section inline-dbox">
|
|
||||||
<div class="inline-media"></div>
|
|
||||||
<div class="inline-content">
|
|
||||||
<h3>D-BOX & Technik</h3>
|
|
||||||
<p>Spüre Bewegungen synchron zum Film und kombiniere das Erlebnis mit modernem Raumklang und Premium-Bestuhlung.</p>
|
|
||||||
<div class="dbox-now-highlight">
|
|
||||||
<h4>Aktuell in D-BOX</h4>
|
|
||||||
<div class="dbox-mini-cards">
|
|
||||||
<div>Zoomania 2</div>
|
|
||||||
<div>Shelter</div>
|
|
||||||
<div>Hoppers</div>
|
|
||||||
<div>Spider Man</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a href="/dbox" class="story-more-btn">Mehr erfahren</a>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article class="inline-section inline-collectors">
|
|
||||||
<div class="inline-media"></div>
|
|
||||||
<div class="inline-content">
|
|
||||||
<h3>Collectors Popcorn Specials</h3>
|
|
||||||
<p>Präsentiere Sonderbecher und Eimer filmbezogen mit Bild, Logo und kurzem Text in einer lebendigen Timeline.</p>
|
|
||||||
<a href="/collectors" class="story-more-btn">Mehr erfahren</a>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { movieCatalog } from "../scripts/bigConstants";
|
|
||||||
|
|
||||||
const nowRunningRow = document.getElementById("now-running-row");
|
|
||||||
|
|
||||||
const escapeHtml = (value: string) => String(value || "")
|
|
||||||
.replaceAll("&", "&")
|
|
||||||
.replaceAll("<", "<")
|
|
||||||
.replaceAll(">", ">")
|
|
||||||
.replaceAll('"', """)
|
|
||||||
.replaceAll("'", "'");
|
|
||||||
|
|
||||||
const renderNowRunningRow = () => {
|
|
||||||
if (!nowRunningRow) return;
|
|
||||||
|
|
||||||
nowRunningRow.innerHTML = movieCatalog.map((movie: any, index: number) => `
|
|
||||||
<article class="running-poster">
|
|
||||||
<img src="${escapeHtml(movie.poster)}" alt="${escapeHtml(movie.title)}">
|
|
||||||
<div class="running-meta">
|
|
||||||
<h4>${escapeHtml(movie.title)}</h4>
|
|
||||||
<p>${escapeHtml(movie.genre)}</p>
|
|
||||||
<button type="button" class="open-program-btn" data-program-index="${index}">Spielzeiten ansehen</button>
|
|
||||||
<div>AHHH ICH HAB SCHMERZEN BITTE BRINGT MICH ENDLICH UM</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
`).join("");
|
|
||||||
|
|
||||||
nowRunningRow.addEventListener("click", (event: any) => {
|
|
||||||
const trigger = event.target.closest(".open-program-btn");
|
|
||||||
if (!trigger) return;
|
|
||||||
const programIndex = trigger.dataset.programIndex;
|
|
||||||
window.location.href = `/movies?focus=${programIndex}`;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const initRevealAnimations = () => {
|
|
||||||
const revealElements = Array.from(document.querySelectorAll(".reveal-on-scroll"));
|
|
||||||
if (!revealElements.length) return;
|
|
||||||
|
|
||||||
if (!("IntersectionObserver" in window)) {
|
|
||||||
revealElements.forEach((element) => element.classList.add("is-visible"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const observer = new IntersectionObserver((entries, obs) => {
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
entry.target.classList.add("is-visible");
|
|
||||||
obs.unobserve(entry.target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, { threshold: 0.2 });
|
|
||||||
|
|
||||||
revealElements.forEach((element) => observer.observe(element));
|
|
||||||
};
|
|
||||||
|
|
||||||
renderNowRunningRow();
|
|
||||||
initRevealAnimations();
|
|
||||||
</script>
|
|
||||||
@@ -1,241 +0,0 @@
|
|||||||
---
|
|
||||||
import {
|
|
||||||
timePatterns,
|
|
||||||
hallRotation,
|
|
||||||
weekdayShort,
|
|
||||||
type MovieInterface,
|
|
||||||
type ITMDBResponse,
|
|
||||||
} from "../scripts/bigConstants";
|
|
||||||
|
|
||||||
async function getTopMovies(): Promise<MovieInterface[]> {
|
|
||||||
const API_KEY = import.meta.env.TMDB_API_KEY;
|
|
||||||
console.log("Fetching with Key:", API_KEY ? "Key found" : "KEY MISSING!");
|
|
||||||
|
|
||||||
const IMAGE_BASE_URL = "https://image.tmdb.org/t/p/w500";
|
|
||||||
|
|
||||||
// 1. Corrected "discover" spelling
|
|
||||||
const response = await fetch(
|
|
||||||
`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&language=de-DE&sort_by=popularity.desc`,
|
|
||||||
);
|
|
||||||
console.log("Response Status:", response.status);
|
|
||||||
|
|
||||||
const data: ITMDBResponse = await response.json();
|
|
||||||
console.log("Results found:", data.results?.length);
|
|
||||||
|
|
||||||
if (!data.results) return [];
|
|
||||||
|
|
||||||
return data.results?.map((movie) => ({
|
|
||||||
id: movie.id,
|
|
||||||
title: movie.title || "Unknown Title",
|
|
||||||
poster: movie.poster_path
|
|
||||||
? `${IMAGE_BASE_URL}${movie.poster_path}`
|
|
||||||
: "/placeholder.jpg",
|
|
||||||
rating: movie.vote_average || 0,
|
|
||||||
// Add optional chaining (?.) and a fallback
|
|
||||||
year: movie.release_date?.split("-")[0] || "N/A",
|
|
||||||
genre: "Movie", // Discover doesn't provide the name, only an ID
|
|
||||||
duration: 120, // Discover doesn't provide duration
|
|
||||||
fsk: "12",
|
|
||||||
description: movie.overview || "No description available.",
|
|
||||||
backdrop: movie.backdrop_path,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatDateShort = (dateObj: Date) => {
|
|
||||||
const day = String(dateObj.getDate()).padStart(2, "0");
|
|
||||||
const month = String(dateObj.getMonth() + 1).padStart(2, "0");
|
|
||||||
return `${day}.${month}.`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildDayMeta = (offset: number) => {
|
|
||||||
const date = new Date();
|
|
||||||
date.setHours(0, 0, 0, 0);
|
|
||||||
date.setDate(date.getDate() + offset);
|
|
||||||
|
|
||||||
const weekday = weekdayShort[date.getDay()];
|
|
||||||
const formattedDate = formatDateShort(date);
|
|
||||||
|
|
||||||
if (offset === 0)
|
|
||||||
return {
|
|
||||||
offset,
|
|
||||||
date,
|
|
||||||
short: "Heute",
|
|
||||||
long: `Heute, ${formattedDate}`,
|
|
||||||
};
|
|
||||||
if (offset === 1)
|
|
||||||
return {
|
|
||||||
offset,
|
|
||||||
date,
|
|
||||||
short: "Morgen",
|
|
||||||
long: `Morgen, ${formattedDate}`,
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
offset,
|
|
||||||
date,
|
|
||||||
short: weekday,
|
|
||||||
long: `${weekday}, ${formattedDate}`,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildScheduleForMovie = (movieIndex: number) => {
|
|
||||||
return Array.from({ length: 7 }, (_, dayOffset) => {
|
|
||||||
const dayMeta = buildDayMeta(dayOffset);
|
|
||||||
const pattern =
|
|
||||||
timePatterns[(movieIndex + dayOffset) % timePatterns.length];
|
|
||||||
const desiredCount = 4 + ((movieIndex + dayOffset) % 2);
|
|
||||||
const showCount = Math.min(pattern.length, desiredCount);
|
|
||||||
|
|
||||||
const showings = pattern
|
|
||||||
.slice(0, showCount)
|
|
||||||
.map((time: string, slotIndex: number) => {
|
|
||||||
const hall =
|
|
||||||
hallRotation[
|
|
||||||
(movieIndex + dayOffset + slotIndex) %
|
|
||||||
hallRotation.length
|
|
||||||
];
|
|
||||||
return { time, hall };
|
|
||||||
});
|
|
||||||
|
|
||||||
return { ...dayMeta, showings };
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
let movieCatalog = await getTopMovies();
|
|
||||||
const movieProgram = movieCatalog?.map((movie, movieIndex) => ({
|
|
||||||
...movie,
|
|
||||||
schedule: buildScheduleForMovie(movieIndex),
|
|
||||||
}));
|
|
||||||
---
|
|
||||||
|
|
||||||
<section id="movie-list-view">
|
|
||||||
<div class="container movie-list-shell">
|
|
||||||
<h1 class="list-title">Aktuelle Filme & Spielzeiten</h1>
|
|
||||||
<p class="list-subtitle">
|
|
||||||
Alle Filme mit 7 Tagen Spielplan. Erste Vorstellung täglich ab 13:00
|
|
||||||
Uhr.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div id="movie-program-list" class="movie-program-list">
|
|
||||||
{
|
|
||||||
movieProgram?.map((movie, programIndex) => (
|
|
||||||
<article
|
|
||||||
class="detailed-card program-card"
|
|
||||||
data-program-index={programIndex}
|
|
||||||
data-schedule={JSON.stringify(movie.schedule)}
|
|
||||||
>
|
|
||||||
<div class="card-left">
|
|
||||||
<img src={movie.poster} alt={movie.title} />
|
|
||||||
<span class={`fsk fsk-${movie.fsk}`}>
|
|
||||||
{movie.fsk}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="card-right">
|
|
||||||
<div class="card-header">
|
|
||||||
<h2>{movie.title}</h2>
|
|
||||||
<span class="duration">
|
|
||||||
{movie.duration} Min. | {movie.genre} | FSK:{" "}
|
|
||||||
{movie.fsk}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p class="description">{movie.description}</p>
|
|
||||||
|
|
||||||
<div class="program-day-tabs">
|
|
||||||
{movie.schedule.map((day, dayIndex) => (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={`program-day-tab ${dayIndex === 0 ? "active" : ""}`}
|
|
||||||
data-program-index={programIndex}
|
|
||||||
data-day-index={dayIndex}
|
|
||||||
>
|
|
||||||
<span>{day.short}</span>
|
|
||||||
<small>
|
|
||||||
{formatDateShort(day.date)}
|
|
||||||
</small>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="schedule-container program-schedule-shell">
|
|
||||||
<div class="schedule-header">
|
|
||||||
<span>Tag</span>
|
|
||||||
<span>Kinosaal</span>
|
|
||||||
<span>Uhrzeit</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
id={`schedule-body-${programIndex}`}
|
|
||||||
class="program-schedule-body"
|
|
||||||
>
|
|
||||||
{movie.schedule[0].showings.map(
|
|
||||||
(showing) => (
|
|
||||||
<button
|
|
||||||
class="schedule-row time-chip program-time-row"
|
|
||||||
data-movie={movie.title}
|
|
||||||
data-hall={showing.hall}
|
|
||||||
data-time={showing.time}
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
{movie.schedule[0].long}
|
|
||||||
</span>
|
|
||||||
<span class="hall-pill">
|
|
||||||
{showing.hall}
|
|
||||||
</span>
|
|
||||||
<span class="time-btn">
|
|
||||||
{showing.time}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const movieProgramList = document.getElementById("movie-program-list");
|
|
||||||
|
|
||||||
movieProgramList?.addEventListener("click", (event: any) => {
|
|
||||||
const dayButton = event.target.closest(".program-day-tab");
|
|
||||||
if (dayButton) {
|
|
||||||
const programIndex = dayButton.getAttribute("data-program-index");
|
|
||||||
const dayIndex = parseInt(dayButton.getAttribute("data-day-index"));
|
|
||||||
const card = dayButton.closest(".detailed-card");
|
|
||||||
const scheduleData = JSON.parse(card.getAttribute("data-schedule"));
|
|
||||||
const selectedDay = scheduleData[dayIndex];
|
|
||||||
|
|
||||||
// Update the schedule body HTML
|
|
||||||
const body:any = document.getElementById(
|
|
||||||
`schedule-body-${programIndex}`,
|
|
||||||
);
|
|
||||||
body.innerHTML = selectedDay.showings
|
|
||||||
.map(
|
|
||||||
(showing:any) => `
|
|
||||||
<button class="schedule-row time-chip" data-movie="${card.querySelector("h2").innerText}" data-hall="${showing.hall}" data-time="${showing.time}">
|
|
||||||
<span>${selectedDay.long}</span>
|
|
||||||
<span class="hall-pill">${showing.hall}</span>
|
|
||||||
<span class="time-btn">${showing.time}</span>
|
|
||||||
</button>
|
|
||||||
`,
|
|
||||||
)
|
|
||||||
.join("");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle deep links
|
|
||||||
const params = new URLSearchParams(window.location.search);
|
|
||||||
const focusIndex = params.get("focus");
|
|
||||||
if (focusIndex !== null) {
|
|
||||||
const target = document.querySelector(
|
|
||||||
`[data-program-index="${focusIndex}"]`,
|
|
||||||
);
|
|
||||||
if (target) {
|
|
||||||
target.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
||||||
target.classList.add("flash-focus");
|
|
||||||
setTimeout(() => target.classList.remove("flash-focus"), 1200);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
<nav class="navbar">
|
|
||||||
<a href="/" class="logo" id="logo-home" style="text-decoration: none; color: inherit;">EAGLE's IMAX</a>
|
|
||||||
<ul class="nav-links">
|
|
||||||
<li><a href="/movies" id="link-filme">Aktuelle Filme</a></li>
|
|
||||||
<li><a href="/snacks" id="link-snacks">Snacks & Getränke</a></li>
|
|
||||||
<li><a href="/about" id="link-about">Über uns</a></li>
|
|
||||||
<li><a href="/account" id="link-account">Mein Konto</a></li>
|
|
||||||
<li>
|
|
||||||
<a href="/cart" id="link-cart" style="position: relative; display: flex; align-items: center; gap: 5px;">
|
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="21" r="1"></circle><circle cx="20" cy="21" r="1"></circle><path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path></svg>
|
|
||||||
Warenkorb
|
|
||||||
<span id="cart-badge" class="badge">0</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="theme-toggle-item">
|
|
||||||
<button id="theme-toggle" class="theme-toggle-btn" type="button" aria-label="Theme wechseln">
|
|
||||||
<span class="theme-icon theme-icon-sun">☀</span>
|
|
||||||
<span class="theme-icon theme-icon-moon">☾</span>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { cart } from "../scripts/bigConstants";
|
|
||||||
|
|
||||||
const themeToggle = document.getElementById("theme-toggle");
|
|
||||||
const cartBadge = document.getElementById("cart-badge");
|
|
||||||
|
|
||||||
const updateCartBadge = () => {
|
|
||||||
if (!cartBadge) return;
|
|
||||||
const totalItems = cart.length;
|
|
||||||
cartBadge.textContent = totalItems.toString();
|
|
||||||
cartBadge.style.display = totalItems > 0 ? "flex" : "none";
|
|
||||||
};
|
|
||||||
|
|
||||||
const initThemeToggle = () => {
|
|
||||||
if (!themeToggle) return;
|
|
||||||
const THEME_KEY = "eagleTheme";
|
|
||||||
|
|
||||||
const applyTheme = (theme: string) => {
|
|
||||||
const isLight = theme === "light";
|
|
||||||
document.body.classList.toggle("theme-light", isLight);
|
|
||||||
document.body.classList.toggle("theme-dark", !isLight);
|
|
||||||
themeToggle.classList.toggle("is-light", isLight);
|
|
||||||
localStorage.setItem(THEME_KEY, isLight ? "light" : "dark");
|
|
||||||
};
|
|
||||||
|
|
||||||
const storedTheme = localStorage.getItem(THEME_KEY);
|
|
||||||
applyTheme(storedTheme === "light" ? "light" : "dark");
|
|
||||||
|
|
||||||
themeToggle.addEventListener("click", () => {
|
|
||||||
const nextTheme = document.body.classList.contains("theme-light") ? "dark" : "light";
|
|
||||||
applyTheme(nextTheme);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
initThemeToggle();
|
|
||||||
updateCartBadge();
|
|
||||||
|
|
||||||
window.addEventListener("cart-updated", updateCartBadge);
|
|
||||||
</script>
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<div id="snack-prompt-overlay" class="snack-overlay hidden">
|
|
||||||
<div class="snack-prompt-box">
|
|
||||||
<h2>Popcorn gefällig? 🍿</h2>
|
|
||||||
<p>Möchtest du noch Snacks oder Getränke hinzufügen?</p>
|
|
||||||
<div class="prompt-buttons">
|
|
||||||
<button id="btn-yes-snacks" class="btn-primary">Ja, Snacks wählen</button>
|
|
||||||
<button id="btn-no-cart" class="btn-secondary">Weiter zum Warenkorb</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const overlay = document.getElementById("snack-prompt-overlay");
|
|
||||||
const btnYes = document.getElementById("btn-yes-snacks");
|
|
||||||
const btnNo = document.getElementById("btn-no-cart");
|
|
||||||
|
|
||||||
btnYes?.addEventListener("click", () => {
|
|
||||||
overlay?.classList.add("hidden");
|
|
||||||
window.location.href = "/snacks";
|
|
||||||
});
|
|
||||||
|
|
||||||
btnNo?.addEventListener("click", () => {
|
|
||||||
overlay?.classList.add("hidden");
|
|
||||||
window.location.href = "/cart";
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||