5 Commits

131 changed files with 1683 additions and 14141 deletions

View File

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

4
.gitattributes vendored
View File

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

25
.gitignore vendored
View File

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

View File

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

11
.vscode/launch.json vendored
View File

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

View File

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

View File

@@ -1,6 +1,4 @@
import type { User } from "./interfaces.js"; function readStorageJson(key, fallbackValue) {
function readStorageJson(key: string, fallbackValue: any) {
const raw = localStorage.getItem(key); const raw = localStorage.getItem(key);
if (!raw || raw === "undefined" || raw === "null") { if (!raw || raw === "undefined" || raw === "null") {
@@ -15,18 +13,22 @@ function readStorageJson(key: string, fallbackValue: any) {
} }
} }
function normalizeUser(user: User): User { function normalizeUser(user) {
if (!user || typeof user !== "object") {
return null;
}
return { return {
firstName: user.firstName || "", firstName: user.firstName || "",
lastName: user.lastName || "", lastName: user.lastName || "",
email: user.email || "", email: user.email || "",
hashedPassword: user.hashedPassword || "", password: user.password || "",
orders: Array.isArray(user.orders) ? user.orders : [], orders: Array.isArray(user.orders) ? user.orders : [],
paymentMethods: Array.isArray(user.paymentMethods) ? user.paymentMethods : [] paymentMethods: Array.isArray(user.paymentMethods) ? user.paymentMethods : []
}; };
} }
function escapeHtml(value: string) { function escapeHtml(value) {
return String(value || "") return String(value || "")
.replaceAll("&", "&") .replaceAll("&", "&")
.replaceAll("<", "&lt;") .replaceAll("<", "&lt;")
@@ -35,7 +37,7 @@ function escapeHtml(value: string) {
.replaceAll("'", "&#39;"); .replaceAll("'", "&#39;");
} }
function formatEuro(value: string) { function formatEuro(value) {
return `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`; return `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`;
} }
@@ -51,21 +53,15 @@ function persistCurrentUser() {
} }
} }
export let users = readStorageJson("eagleUsers", []); let users = readStorageJson("eagleUsers", []);
if (!Array.isArray(users)) { if (!Array.isArray(users)) {
users = []; users = [];
} }
users = users.map(normalizeUser).filter(Boolean); users = users.map(normalizeUser).filter(Boolean);
const rawCurrentUser = readStorageJson("currentUser", null); let currentUser = normalizeUser(readStorageJson("currentUser", null));
export var currentUser: User | null = rawCurrentUser ? normalizeUser(rawCurrentUser) : null;
if (currentUser && currentUser.email) { if (currentUser && currentUser.email) {
const currentEmail = currentUser.email; const storedMatch = users.find((user) => user.email === currentUser.email);
const storedMatch = users.find((user: { email: string; }) => {
return user.email === currentEmail;
});
if (storedMatch) { if (storedMatch) {
currentUser = storedMatch; currentUser = storedMatch;
} else { } else {
@@ -74,23 +70,11 @@ if (currentUser && currentUser.email) {
} }
} }
async function hashMessage(message: string) { function registerUser() {
const msgBuffer = new TextEncoder().encode(message); // Encode as UTF-8 const firstName = document.getElementById("reg-firstname")?.value.trim() || "";
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer); // Hash const lastName = document.getElementById("reg-lastname")?.value.trim() || "";
const hashArray = Array.from(new Uint8Array(hashBuffer)); // Convert to bytes const email = (document.getElementById("reg-email")?.value.trim() || "").toLowerCase();
return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); // Hex string const password = document.getElementById("reg-password")?.value || "";
}
function getInputValue(id: string): string {
const el = document.getElementById(id) as HTMLInputElement | null;
return el?.value.trim() ?? "";
}
export async function registerUser() {
const firstName = getInputValue("reg-firstname");
const lastName = getInputValue("reg-lastname");
const email = getInputValue("reg-email").toLowerCase();
const password = document.querySelector<HTMLInputElement>("#reg-password")?.value ?? "";
if (!firstName || !lastName || !email || !password) { if (!firstName || !lastName || !email || !password) {
alert("Bitte fuelle alle Felder aus."); alert("Bitte fuelle alle Felder aus.");
@@ -102,19 +86,17 @@ const password = document.querySelector<HTMLInputElement>("#reg-password")?.valu
return; return;
} }
const existingUser = users.find((user: User) => user.email.toLowerCase() === email); const existingUser = users.find((user) => user.email.toLowerCase() === email);
if (existingUser) { if (existingUser) {
alert("E-Mail bereits registriert"); alert("E-Mail bereits registriert");
return; return;
} }
const hashedPassword = await hashMessage(password);
const newUser = { const newUser = {
firstName, firstName,
lastName, lastName,
email, email,
hashedPassword, password,
orders: [], orders: [],
paymentMethods: [] paymentMethods: []
}; };
@@ -131,13 +113,12 @@ const password = document.querySelector<HTMLInputElement>("#reg-password")?.valu
openAccountDashboard(); openAccountDashboard();
} }
export async function loginUser() { function loginUser() {
const email = (document.querySelector<HTMLInputElement>("#login-email")?.value.trim() || "").toLowerCase(); const email = (document.getElementById("login-email")?.value.trim() || "").toLowerCase();
const password = document.querySelector<HTMLInputElement>("#login-password")?.value || ""; const password = document.getElementById("login-password")?.value || "";
const hashedPassword = await hashMessage(password);
const user = users.find( const user = users.find(
(entry: User) => entry.email.toLowerCase() === email && entry.hashedPassword === hashedPassword (entry) => entry.email.toLowerCase() === email && entry.password === password
); );
if (!user) { if (!user) {
@@ -150,7 +131,7 @@ export async function loginUser() {
openAccountDashboard(); openAccountDashboard();
} }
export function openAccountDashboard() { function openAccountDashboard() {
const accountView = document.getElementById("account-view"); const accountView = document.getElementById("account-view");
if (!accountView) { if (!accountView) {
return; return;
@@ -161,7 +142,7 @@ export function openAccountDashboard() {
return; return;
} }
accountView.innerHTML = /*html*/` accountView.innerHTML = `
<div class="account-panel"> <div class="account-panel">
<div class="account-panel-header"> <div class="account-panel-header">
<h2>Mein Konto</h2> <h2>Mein Konto</h2>
@@ -217,7 +198,7 @@ function renderOrders() {
const orderHtml = orders const orderHtml = orders
.map((order, index) => { .map((order, index) => {
const movieItems = Array.isArray(order.items) const movieItems = Array.isArray(order.items)
? order.items.filter((item: any) => item.category === "movie") ? order.items.filter((item) => item.category === "movie")
: []; : [];
const previewItem = movieItems[0] || (Array.isArray(order.items) ? order.items[0] : null); const previewItem = movieItems[0] || (Array.isArray(order.items) ? order.items[0] : null);
const previewTitle = previewItem?.title || "Bestellung"; const previewTitle = previewItem?.title || "Bestellung";
@@ -246,20 +227,20 @@ function renderOrders() {
`; `;
const detailTarget = document.getElementById("order-ticket-details"); const detailTarget = document.getElementById("order-ticket-details");
const orderButtons = Array.from(target.querySelectorAll<HTMLButtonElement>(".order-item-btn")); const orderButtons = Array.from(target.querySelectorAll(".order-item-btn"));
const renderOrderTicket = (orderIndex: number) => { const renderOrderTicket = (orderIndex) => {
const order = orders[orderIndex]; const order = orders[orderIndex];
if (!order || !detailTarget) { if (!order || !detailTarget) {
return; return;
} }
const movieItems = Array.isArray(order.items) const movieItems = Array.isArray(order.items)
? order.items.filter((item: any) => item.category === "movie") ? order.items.filter((item) => item.category === "movie")
: []; : [];
const primaryMovie = movieItems[0] || (Array.isArray(order.items) ? order.items[0] : null); const primaryMovie = movieItems[0] || (Array.isArray(order.items) ? order.items[0] : null);
const poster = primaryMovie?.img || ""; const poster = primaryMovie?.img || "";
const seats = movieItems.map((item: any) => item.seatId).filter(Boolean).join(", ") || "-"; const seats = movieItems.map((item) => item.seatId).filter(Boolean).join(", ") || "-";
const ticketCount = movieItems.length || (Array.isArray(order.items) ? order.items.length : 0); const ticketCount = movieItems.length || (Array.isArray(order.items) ? order.items.length : 0);
const hall = primaryMovie?.hall || "-"; const hall = primaryMovie?.hall || "-";
const time = primaryMovie?.time ? `${primaryMovie.time} Uhr` : "-"; const time = primaryMovie?.time ? `${primaryMovie.time} Uhr` : "-";
@@ -309,7 +290,7 @@ function renderPayments() {
return; return;
} }
target.innerHTML = /*html*/` target.innerHTML = `
<div class="account-card"> <div class="account-card">
<h3>Zahlungsmethoden</h3> <h3>Zahlungsmethoden</h3>
<p class="account-payments-note">Platzhalter zum Hinterlegen deiner Logos oder Anbieter-Informationen.</p> <p class="account-payments-note">Platzhalter zum Hinterlegen deiner Logos oder Anbieter-Informationen.</p>
@@ -463,7 +444,7 @@ function renderPayments() {
} }
function logoutUser() { function logoutUser() {
currentUser = null;
persistCurrentUser(); persistCurrentUser();
window.location.reload(); window.location.reload();
} }

View File

@@ -1,22 +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: {
plugins: [tailwindcss()],
},
env: {
schema: {
TMDB_API_TOKEN: envField.string({ context: 'client', access: 'public', default: 'https://api.example.com' }),
SETTINGS_TOKEN: envField.string({ context: 'server', access: 'secret' }),
}
}
});

View File

@@ -1,11 +1,7 @@
import { seatLayouts, occupiedSeatsData, prices, cart } from "./main.js" let currentBookingContext = null;
import { renderCart, saveCart } from "./cart.js"; let currentHallLayout = null;
import { renderCheckout } from "./checkout.js";
let currentBookingContext: any = null; function openBooking(movie, hall, time) {
let currentHallLayout: any = null;
export function openBooking(movie: string, hall: string, time: any) {
const titleEl = document.getElementById("modal-movie-title"); const titleEl = document.getElementById("modal-movie-title");
const infoEl = document.getElementById("modal-info-text"); const infoEl = document.getElementById("modal-info-text");
@@ -26,11 +22,11 @@ export function openBooking(movie: string, hall: string, time: any) {
document.getElementById("booking-modal")?.classList.remove("hidden"); document.getElementById("booking-modal")?.classList.remove("hidden");
} }
function getRowLabel(rowIndex: number) { function getRowLabel(rowIndex) {
return String(rowIndex + 1); return String(rowIndex + 1);
} }
function buildHallLayout(hallName: string, baseConfig:any) { function buildHallLayout(hallName, baseConfig) {
const rows = Number(baseConfig.rows || 0); const rows = Number(baseConfig.rows || 0);
const totalCols = Number(baseConfig.left || 0) + Number(baseConfig.right || 0); const totalCols = Number(baseConfig.left || 0) + Number(baseConfig.right || 0);
const isDeluxe = /deluxe/i.test(hallName); const isDeluxe = /deluxe/i.test(hallName);
@@ -43,7 +39,7 @@ function buildHallLayout(hallName: string, baseConfig:any) {
const vipRows = rows > 0 ? [rows] : []; const vipRows = rows > 0 ? [rows] : [];
const dboxMap = new Set(); const dboxMap = new Set();
const markDboxRange = (rowNumber: number, startCol: number, width: number) => { const markDboxRange = (rowNumber, startCol, width) => {
if (!rowNumber || width <= 0) { if (!rowNumber || width <= 0) {
return; return;
} }
@@ -56,7 +52,7 @@ function buildHallLayout(hallName: string, baseConfig:any) {
if (isDeluxe) { if (isDeluxe) {
const configuredDboxSeats = Array.isArray(baseConfig.dbox) const configuredDboxSeats = Array.isArray(baseConfig.dbox)
? baseConfig.dbox.reduce((sum: number, section: any) => sum + Number(section.w || 0), 0) ? baseConfig.dbox.reduce((sum, section) => sum + Number(section.w || 0), 0)
: 0; : 0;
const totalDboxSeats = Math.max(4, configuredDboxSeats || 0); const totalDboxSeats = Math.max(4, configuredDboxSeats || 0);
@@ -81,7 +77,7 @@ function buildHallLayout(hallName: string, baseConfig:any) {
markDboxRange(rowNumber, startCol, seatsForRow); markDboxRange(rowNumber, startCol, seatsForRow);
}); });
} else if (Array.isArray(baseConfig.dbox)) { } else if (Array.isArray(baseConfig.dbox)) {
baseConfig.dbox.forEach((section: any) => { baseConfig.dbox.forEach((section) => {
const rowNumber = Number(section.r || 0); const rowNumber = Number(section.r || 0);
const width = Number(section.w || 0); const width = Number(section.w || 0);
const startCol = Number(section.c || 0); const startCol = Number(section.c || 0);
@@ -100,7 +96,7 @@ function buildHallLayout(hallName: string, baseConfig:any) {
}; };
} }
function getSeatType(layout: any, rowNumber: number, colNumber: number) { function getSeatType(layout, rowNumber, colNumber) {
if (layout.dboxMap.has(`${rowNumber}-${colNumber}`)) { if (layout.dboxMap.has(`${rowNumber}-${colNumber}`)) {
return "dbox"; return "dbox";
} }
@@ -116,7 +112,7 @@ function getSeatType(layout: any, rowNumber: number, colNumber: number) {
return "normal"; return "normal";
} }
function createSeatElement({seatId, seatType, occupiedSeats }:any) { function createSeatElement({ seatId, seatType, occupiedSeats }) {
const seat = document.createElement("button"); const seat = document.createElement("button");
seat.type = "button"; seat.type = "button";
seat.classList.add("seat", seatType); seat.classList.add("seat", seatType);
@@ -140,7 +136,7 @@ function createSeatElement({seatId, seatType, occupiedSeats }:any) {
return seat; return seat;
} }
function createSeats(hallName: string, time: any) { function createSeats(hallName, time) {
const seatGrid = document.getElementById("seat-grid"); const seatGrid = document.getElementById("seat-grid");
if (!seatGrid) { if (!seatGrid) {
return; return;
@@ -148,8 +144,7 @@ function createSeats(hallName: string, time: any) {
seatGrid.innerHTML = ""; seatGrid.innerHTML = "";
const arrIndex = hallName as keyof typeof seatLayouts; const baseConfig = seatLayouts[hallName];
const baseConfig: any = seatLayouts[arrIndex];
if (!baseConfig) { if (!baseConfig) {
currentHallLayout = null; currentHallLayout = null;
return; return;
@@ -240,7 +235,7 @@ function renderBookingLegend() {
} }
function updateBookingSummary() { function updateBookingSummary() {
const selectedSeats = Array.from(document.querySelectorAll("#seat-grid .seat.selected")) as HTMLElement[];; const selectedSeats = Array.from(document.querySelectorAll("#seat-grid .seat.selected"));
const summaryPanel = document.getElementById("booking-summary"); const summaryPanel = document.getElementById("booking-summary");
const summaryItems = document.getElementById("summary-items"); const summaryItems = document.getElementById("summary-items");
const totalEl = document.getElementById("total-price"); const totalEl = document.getElementById("total-price");
@@ -250,7 +245,7 @@ function updateBookingSummary() {
if (summaryItems) { if (summaryItems) {
summaryItems.innerHTML = selectedSeats summaryItems.innerHTML = selectedSeats
.map((seat) => { .map((seat) => {
const type = (seat.dataset.type || "normal") as keyof typeof prices; const type = seat.dataset.type || "normal";
const seatPrice = Number(prices?.[type] ?? prices?.normal ?? 11); const seatPrice = Number(prices?.[type] ?? prices?.normal ?? 11);
total += seatPrice; total += seatPrice;
@@ -277,13 +272,12 @@ function updateBookingSummary() {
summaryPanel?.classList.toggle("hidden", selectedSeats.length === 0); summaryPanel?.classList.toggle("hidden", selectedSeats.length === 0);
} }
function findMoviePoster(movieTitle: string) { function findMoviePoster(movieTitle) {
const cards = Array.from(document.querySelectorAll(".movie-card, .detailed-card")); const cards = Array.from(document.querySelectorAll(".movie-card, .detailed-card"));
const normalizedTarget = String(movieTitle || "").trim().toLowerCase(); const normalizedTarget = String(movieTitle || "").trim().toLowerCase();
for (const card of cards) { for (const card of cards) {
const currentCard = card.querySelector("h2, h3") as HTMLElement; const title = card.querySelector("h2, h3")?.innerText?.trim().toLowerCase();
const title = currentCard.innerText?.trim().toLowerCase();
if (title === normalizedTarget) { if (title === normalizedTarget) {
const imageSrc = card.querySelector("img")?.src; const imageSrc = card.querySelector("img")?.src;
if (imageSrc) { if (imageSrc) {
@@ -296,7 +290,7 @@ function findMoviePoster(movieTitle: string) {
} }
function confirmSelectedSeats() { function confirmSelectedSeats() {
const selectedSeats = Array.from(document.querySelectorAll("#seat-grid .seat.selected")) as HTMLElement[]; const selectedSeats = Array.from(document.querySelectorAll("#seat-grid .seat.selected"));
if (!currentBookingContext || selectedSeats.length === 0) { if (!currentBookingContext || selectedSeats.length === 0) {
alert("Bitte waehle mindestens einen Platz aus."); alert("Bitte waehle mindestens einen Platz aus.");
@@ -310,7 +304,7 @@ function confirmSelectedSeats() {
const seatId = seat.dataset.seatId; const seatId = seat.dataset.seatId;
const seatType = seat.dataset.type || "normal"; const seatType = seat.dataset.type || "normal";
const alreadyInCart = cart.some((item: any) => const alreadyInCart = cart.some((item) =>
item.category === "movie" && item.category === "movie" &&
item.title === currentBookingContext.movie && item.title === currentBookingContext.movie &&
item.hall === currentBookingContext.hall && item.hall === currentBookingContext.hall &&

View File

@@ -1,10 +1,8 @@
import { cart } from "./main.js"; function formatEuro(value) {
function formatEuro(value: number) {
return `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`; return `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`;
} }
function escapeHtml(value: any) { function escapeHtml(value) {
return String(value || "") return String(value || "")
.replaceAll("&", "&amp;") .replaceAll("&", "&amp;")
.replaceAll("<", "&lt;") .replaceAll("<", "&lt;")
@@ -13,14 +11,14 @@ function escapeHtml(value: any) {
.replaceAll("'", "&#39;"); .replaceAll("'", "&#39;");
} }
function buildCartKey(item: { category: string; seatId: any; hall: any; time: any; title: any; }) { function buildCartKey(item) {
const infoText = item.category === "movie" const infoText = item.category === "movie"
? `Sitz: ${item.seatId} (${item.hall})` ? `Sitz: ${item.seatId} (${item.hall})`
: item.time; : item.time;
return `${item.title}-${item.hall}-${infoText}`; return `${item.title}-${item.hall}-${infoText}`;
} }
function isDrinkItem(item: { category: string; title: any; hall: any; }) { function isDrinkItem(item) {
if (item.category !== "snack") { if (item.category !== "snack") {
return false; return false;
} }
@@ -41,7 +39,7 @@ function isDrinkItem(item: { category: string; title: any; hall: any; }) {
return drinkKeywords.some((word) => title.includes(word)) || size.includes("l"); return drinkKeywords.some((word) => title.includes(word)) || size.includes("l");
} }
function buildItemInfo(item: { category: any; seatId?: any; hall: any; time?: any; title: any; }) { function buildItemInfo(item) {
if (item.category === "movie") { if (item.category === "movie") {
return ` return `
<div>Sitzplatz: ${escapeHtml(item.seatId || "-")}</div> <div>Sitzplatz: ${escapeHtml(item.seatId || "-")}</div>
@@ -67,7 +65,7 @@ function buildItemInfo(item: { category: any; seatId?: any; hall: any; time?: an
function groupCartItems() { function groupCartItems() {
const groups = new Map(); const groups = new Map();
cart.forEach((item: { price?: any; category: string; seatId: any; hall: any; time: any; title: any; }) => { cart.forEach((item) => {
const key = buildCartKey(item); const key = buildCartKey(item);
if (!groups.has(key)) { if (!groups.has(key)) {
@@ -87,12 +85,12 @@ function groupCartItems() {
return Array.from(groups.values()); return Array.from(groups.values());
} }
export function saveCart() { function saveCart() {
localStorage.setItem("eagleCart", JSON.stringify(cart)); localStorage.setItem("eagleCart", JSON.stringify(cart));
updateCartBadge(); updateCartBadge();
} }
export function updateCartBadge() { function updateCartBadge() {
const cartBadge = document.getElementById("cart-badge"); const cartBadge = document.getElementById("cart-badge");
if (!cartBadge) { if (!cartBadge) {
@@ -103,7 +101,7 @@ export function updateCartBadge() {
cartBadge.classList.toggle("hidden", cart.length === 0); cartBadge.classList.toggle("hidden", cart.length === 0);
} }
export function renderCart() { function renderCart() {
const cartList = document.getElementById("cart-items-list"); const cartList = document.getElementById("cart-items-list");
const totalEl = document.getElementById("cart-total-right"); const totalEl = document.getElementById("cart-total-right");
const vatEl = document.getElementById("cart-vat-right"); const vatEl = document.getElementById("cart-vat-right");
@@ -121,7 +119,7 @@ export function renderCart() {
const groupedItems = groupCartItems(); const groupedItems = groupCartItems();
const header = /*html*/` const header = `
<div class="cart-header-row"> <div class="cart-header-row">
<div class="col-amount">MENGE</div> <div class="col-amount">MENGE</div>
<div class="col-img">VORSCHAU</div> <div class="col-img">VORSCHAU</div>
@@ -135,11 +133,11 @@ export function renderCart() {
const rows = groupedItems const rows = groupedItems
.map((group) => { .map((group) => {
const imageHtml = group.item.img const imageHtml = group.item.img
? /*html*/`<img class="cart-img-small" src="${escapeHtml(group.item.img)}" alt="${escapeHtml(group.item.title)}">` ? `<img class="cart-img-small" src="${escapeHtml(group.item.img)}" alt="${escapeHtml(group.item.title)}">`
: /*html*/`<div class="cart-img-fallback">Kein Bild</div>`; : `<div class="cart-img-fallback">Kein Bild</div>`;
const quantityHtml = group.item.category === "movie" const quantityHtml = group.item.category === "movie"
? /*html*/`<div class="qty-static" aria-label="Feste Ticketanzahl">${group.quantity}x</div>` ? `<div class="qty-static" aria-label="Feste Ticketanzahl">${group.quantity}x</div>`
: /*html*/` : `
<div class="qty-stepper"> <div class="qty-stepper">
<button class="btn-qty" data-action="minus" data-key="${escapeHtml(group.key)}">-</button> <button class="btn-qty" data-action="minus" data-key="${escapeHtml(group.key)}">-</button>
<span>${group.quantity}</span> <span>${group.quantity}</span>
@@ -147,7 +145,7 @@ export function renderCart() {
</div> </div>
`; `;
return /*html*/` return `
<div class="cart-item-row"> <div class="cart-item-row">
<div class="col-amount"> <div class="col-amount">
${quantityHtml} ${quantityHtml}
@@ -174,23 +172,22 @@ export function renderCart() {
saveCart(); saveCart();
} }
//@ts-ignore
window.removeItem = function removeItem(id: any) { window.removeItem = function removeItem(id) {
var localCart = cart.filter((item: { id: any; }) => item.id !== id); cart = cart.filter((item) => item.id !== id);
saveCart(); saveCart();
renderCart(); renderCart();
}; };
//@ts-ignore window.changeQty = function changeQty(title, delta) {
window.changeQty = function changeQty(title, delta): void {
if (delta > 0) { if (delta > 0) {
const item = cart.find((entry: { title: any; }) => entry.title === title); const item = cart.find((entry) => entry.title === title);
if (item) { if (item) {
cart.push({ ...item, id: Date.now() + Math.random() }); cart.push({ ...item, id: Date.now() + Math.random() });
} }
} else { } else {
const index = cart const index = cart
.map((entry: { title: any; }) => entry.title) .map((entry) => entry.title)
.lastIndexOf(title); .lastIndexOf(title);
if (index !== -1) { if (index !== -1) {
cart.splice(index, 1); cart.splice(index, 1);
@@ -200,4 +197,3 @@ window.changeQty = function changeQty(title, delta): void {
saveCart(); saveCart();
renderCart(); renderCart();
}; };

View File

@@ -1,15 +1,11 @@
import { currentUser, users } from "./account.js"; function formatCheckoutEuro(value) {
import { renderCart, saveCart } from "./cart.js";
import { cart, emptyCart, occupiedSeatsData } from "./main.js";
function formatCheckoutEuro(value: number) {
return `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`; return `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`;
} }
let selectedPaymentMethod = ""; let selectedPaymentMethod = "";
let checkoutEventsBound = false; let checkoutEventsBound = false;
function setCheckoutStep(step: number) { function setCheckoutStep(step) {
const step1 = document.getElementById("checkout-step-1"); const step1 = document.getElementById("checkout-step-1");
const step2 = document.getElementById("checkout-step-2"); const step2 = document.getElementById("checkout-step-2");
const step3 = document.getElementById("checkout-step-3"); const step3 = document.getElementById("checkout-step-3");
@@ -31,7 +27,7 @@ function setCheckoutStep(step: number) {
line2?.classList.toggle("active", step >= 3); line2?.classList.toggle("active", step >= 3);
} }
export function renderCheckout() { function renderCheckout() {
const summaryList = document.getElementById("checkout-summary-list"); const summaryList = document.getElementById("checkout-summary-list");
const totalDisplay = document.getElementById("checkout-total-display"); const totalDisplay = document.getElementById("checkout-total-display");
const vatDisplay = document.getElementById("checkout-vat-display"); const vatDisplay = document.getElementById("checkout-vat-display");
@@ -97,7 +93,7 @@ function generateTicket() {
const qrData = encodeURIComponent(`EAGLE-IMAX|${mainMovie.title}|${mainMovie.hall}|${matchingMovieSeats}`); const qrData = encodeURIComponent(`EAGLE-IMAX|${mainMovie.title}|${mainMovie.hall}|${matchingMovieSeats}`);
const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=${qrData}&bgcolor=ffffff`; const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=${qrData}&bgcolor=ffffff`;
ticketContainer.innerHTML = /*html*/` ticketContainer.innerHTML = `
<div class="luxury-ticket"> <div class="luxury-ticket">
<div class="ticket-left"> <div class="ticket-left">
<img src="${mainMovie.img}" class="ticket-poster" alt="${mainMovie.title}"> <img src="${mainMovie.img}" class="ticket-poster" alt="${mainMovie.title}">
@@ -119,7 +115,7 @@ function generateTicket() {
`; `;
} }
function saveOrderForCurrentUser(orderItems: any[], orderTotal: any) { function saveOrderForCurrentUser(orderItems, orderTotal) {
if (typeof currentUser === "undefined" || !currentUser) { if (typeof currentUser === "undefined" || !currentUser) {
return; return;
} }
@@ -135,7 +131,6 @@ function saveOrderForCurrentUser(orderItems: any[], orderTotal: any) {
paymentMethod: selectedPaymentMethod || "-" paymentMethod: selectedPaymentMethod || "-"
}; };
//@ts-ignore
const userIndex = users.findIndex((entry) => entry.email === currentUser.email); const userIndex = users.findIndex((entry) => entry.email === currentUser.email);
if (userIndex === -1) { if (userIndex === -1) {
return; return;
@@ -149,7 +144,7 @@ function saveOrderForCurrentUser(orderItems: any[], orderTotal: any) {
localStorage.setItem("eagleUsers", JSON.stringify(users)); localStorage.setItem("eagleUsers", JSON.stringify(users));
} }
function reserveSeatsAfterPayment(orderItems: any[]) { function reserveSeatsAfterPayment(orderItems) {
const movieItems = orderItems.filter((item) => item.category === "movie"); const movieItems = orderItems.filter((item) => item.category === "movie");
movieItems.forEach((item) => { movieItems.forEach((item) => {
@@ -171,7 +166,7 @@ function completeCheckout() {
saveOrderForCurrentUser(orderItems, orderTotal); saveOrderForCurrentUser(orderItems, orderTotal);
reserveSeatsAfterPayment(orderItems); reserveSeatsAfterPayment(orderItems);
emptyCart?.() cart = [];
saveCart?.(); saveCart?.();
renderCart?.(); renderCart?.();
} }
@@ -185,7 +180,7 @@ function bindCheckoutEvents() {
const nextButton = document.getElementById("btn-next-step-2"); const nextButton = document.getElementById("btn-next-step-2");
const backButton = document.getElementById("btn-back-to-step1"); const backButton = document.getElementById("btn-back-to-step1");
const payNowButton = document.getElementById("btn-pay-now") as HTMLButtonElement; const payNowButton = document.getElementById("btn-pay-now");
document.querySelectorAll(".payment-method").forEach((method) => { document.querySelectorAll(".payment-method").forEach((method) => {
method.addEventListener("click", () => { method.addEventListener("click", () => {
@@ -194,7 +189,6 @@ function bindCheckoutEvents() {
}); });
method.classList.add("selected"); method.classList.add("selected");
//@ts-ignore
selectedPaymentMethod = method.dataset.method || ""; selectedPaymentMethod = method.dataset.method || "";
nextButton?.classList.remove("hidden"); nextButton?.classList.remove("hidden");
}); });

BIN
img/astronaut-rockypopcorn.jpg LFS Normal file

Binary file not shown.

BIN
img/cashtruck.jpg LFS Normal file

Binary file not shown.

BIN
img/fsk-0.png LFS Normal file

Binary file not shown.

BIN
img/fsk-12.png LFS Normal file

Binary file not shown.

BIN
img/fsk-16.png LFS Normal file

Binary file not shown.

BIN
img/fsk-18.png LFS Normal file

Binary file not shown.

BIN
img/fsk-6.png LFS Normal file

Binary file not shown.

BIN
img/nachokombigross.png LFS Normal file

Binary file not shown.

BIN
img/nachokombimittel.png LFS Normal file

Binary file not shown.

BIN
img/popcornkombigross.png LFS Normal file

Binary file not shown.

BIN
img/popcornkombiklein.png LFS Normal file

Binary file not shown.

BIN
img/popcornkombimittel.png LFS Normal file

Binary file not shown.

BIN
img/screammetalpopcorn.png LFS Normal file

Binary file not shown.

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

Binary file not shown.

View File

@@ -51,7 +51,14 @@
<h3>Jetzt läuft</h3> <h3>Jetzt läuft</h3>
<span>Heute im Fokus</span> <span>Heute im Fokus</span>
</div> </div>
<div id="now-running-shell" class="now-running-shell is-collapsed">
<div id="now-running-row" class="now-running-row"></div> <div id="now-running-row" class="now-running-row"></div>
<div class="now-running-fade">
<button id="now-running-toggle" class="now-running-toggle" type="button" aria-expanded="false" aria-label="Weitere Filme anzeigen">
<span>&gt;</span>
</button>
</div>
</div>
</div> </div>
<div class="home-inline-showcase reveal-on-scroll"> <div class="home-inline-showcase reveal-on-scroll">
@@ -267,15 +274,238 @@
<h1 class="list-title">Snacks & Getränke</h1> <h1 class="list-title">Snacks & Getränke</h1>
<div class="category-tabs"> <div class="category-tabs">
<button class="tab-btn active" data-target="cat-getraenke">Getränke</button> <button class="tab-btn active" data-target="cat-limited">Limitierte Specials</button>
<button class="tab-btn" data-target="cat-getraenke">Getränke</button>
<button class="tab-btn" data-target="cat-popcorn">Popcorn</button> <button class="tab-btn" data-target="cat-popcorn">Popcorn</button>
<button class="tab-btn" data-target="cat-nachos">Nachos</button> <button class="tab-btn" data-target="cat-nachos">Nachos</button>
<button class="tab-btn" data-target="cat-snacks">Snacks</button> <button class="tab-btn" data-target="cat-snacks">Snacks</button>
<button class="tab-btn" data-target="cat-kombi">Kombi</button>
<button class="tab-btn" data-target="cat-eis">Eis</button> <button class="tab-btn" data-target="cat-eis">Eis</button>
</div> </div>
<div id="cat-getraenke" class="snack-category active"> <div id="cat-limited" class="snack-category active limited-specials-category">
<div class="limited-specials-hero">
<div>
<span class="limited-kicker">Nur für kurze Zeit</span>
<h2>Limitierte Specials</h2>
<p>Filmbecher, Sammler-Eimer und Kids-Menüs als kleine Vitrine für besondere Aktionen.</p>
</div>
</div>
<div class="limited-special-block">
<div class="special-film-heading">
<img src="img/Zoomania-2.jpg" alt="Zoomania 2 Logo">
<div>
<span>Zoomania 2</span>
<h2>Tiereische Collection</h2>
</div>
</div>
<div class="limited-special-grid">
<div class="snack-card limited-special-card">
<div class="snack-img"><img src="img/zoomania-popcorn.jpg" alt="Zoomania 2 Metallbecher"></div>
<div class="snack-info">
<span class="badge">Metallbecher</span>
<h3>Limitierter Metallbecher - Zoomania 2</h3>
<p class="snack-card-note">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 Collection</h2>
</div>
</div>
<div class="limited-special-grid">
<div class="snack-card limited-special-card">
<div class="snack-img"><img src="img/screamdoorpopcorn.jpg" alt="Scream VII Sammelbecher"></div>
<div class="snack-info">
<span class="badge">Collector</span>
<h3>Limitierter Sammelbecher - Scream VII</h3>
<p class="snack-card-note">Hallo Sydney! Ghostface in der Tür.</p>
<div class="option-group">
<button class="opt-btn active">Süß</button>
<button class="opt-btn">Salzig</button>
</div>
<div class="size-selector">
<button class="size-chip">Special <span>29,00€</span></button>
</div>
</div>
</div>
<div class="snack-card limited-special-card">
<div class="snack-img"><img src="img/screammetalpopcorn.png" alt="Scream VII Sammelbecher"></div>
<div class="snack-info">
<span class="badge">Collector</span>
<h3>Limitierter Metallbecher - Scream VII</h3>
<p class="snack-card-note">Metall Sammelbecher im SCREAM VII Design</p>
<div class="option-group">
<button class="opt-btn active">Süß</button>
<button class="opt-btn">Salzig</button>
</div>
<div class="size-selector">
<button class="size-chip">Special <span>12,00€</span></button>
</div>
</div>
</div>
</div>
</div>
<div class="limited-special-block">
<div class="special-film-heading">
<img src="img/derAustronaut.jpg" alt="Der Austronaut Logo">
<div>
<span>Project Hail Mary</span>
<h2>Space Collection</h2>
</div>
</div>
<div class="limited-special-grid">
<div class="snack-card limited-special-card">
<div class="snack-img"><img src="img/astronautpopcorn.jpg" alt="Der Austronaut Sammelbecher"></div>
<div class="snack-info">
<span class="badge">Space Cup</span>
<h3>Limitierter Sammelbecher - Der Austronaut</h3>
<p class="snack-card-note">Der Helm von Ryland Grace aus "Der Austronaut"</p>
<div class="option-group">
<button class="opt-btn active">Süß</button>
<button class="opt-btn">Salzig</button>
</div>
<div class="size-selector">
<button class="size-chip">Special <span>34,00€</span></button>
</div>
</div>
</div>
<div class="snack-card limited-special-card">
<div class="snack-img"><img src="img/astronaut-rockypopcorn.jpg" alt="Der Austronaut - Rocky"></div>
<div class="snack-info">
<span class="badge">Collector</span>
<h3>Limitierter Sammelbecher - Der Austronaut</h3>
<p class="snack-card-note">Die Kapsel von Rocky - Mit abnehmbarer Rocky Figur</p>
<div class="option-group">
<button class="opt-btn active">Süß</button>
<button class="opt-btn">Salzig</button>
</div>
<div class="size-selector">
<button class="size-chip">Special <span>22,00€</span></button>
</div>
</div>
</div>
</div>
</div>
<div class="limited-special-block">
<div class="special-film-heading">
<img src="img/hoppers.jpg" alt="Hoppers Logo">
<div>
<span>Hoppers</span>
<h2>Biber Specials (Ist das eine Eidechse?)</h2>
</div>
</div>
<div class="limited-special-grid">
<div class="snack-card limited-special-card">
<div class="snack-img"><img src="img/hopperspopcornmetall.jpg" alt="Hoppers Metallbecher"></div>
<div class="snack-info">
<span class="badge">Metallbecher</span>
<h3>Limitierter Metallbecher - Hoppers</h3>
<p class="snack-card-note">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 Collection</h2>
</div>
</div>
<div class="limited-special-grid">
<div class="snack-card limited-special-card">
<div class="snack-img"><img src="img/marioyoshipopcorn.png" alt="Yoshi Sammelbecher"></div>
<div class="snack-info">
<span class="badge">Yoshi Cup</span>
<h3>Limitierter Sammelbecher - Yoshi Becher</h3>
<p class="snack-card-note">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 +622,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 +637,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 +650,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 +663,59 @@
<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-img"><img src="img/popcornkombiklein.png" alt="Popcorn klein - Kombi-Menü"></div>
<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-img"><img src="img/popcornkombimittel.png" alt="Popcorn mittel - Kombi-Menü"></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-img"><img src="img/popcornkombigross.png" alt="Popcorn groß - Kombi-Menü"></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">ß</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>
<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 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>
@@ -554,12 +757,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">
@@ -597,16 +803,20 @@
</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>
@@ -615,6 +825,35 @@
</div> </div>
</div> </div>
</div> </div>
<div class="snack-card highlight">
<div class="snack-img"><img src="img/nachokombimittel.png" alt="Nacho Kombi Mittel"></div>
<div class="snack-info">
<h3>Nacho Menü Mittel</h3>
<p class="snack-card-note">Nachos mittel + 1 Dip + 1x 0,33L Getränk</p>
<div class="option-group">
<button class="opt-btn active">Mittel</button>
</div>
<div class="size-selector">
<button class="size-chip">Kombi <span>6,90€</span></button>
</div>
</div>
</div>
<div class="snack-card highlight">
<div class="snack-img"><img src="img/nachokombigross.png" alt="Nacho Kombi Groß"></div>
<div class="snack-info">
<h3>Nacho Menü Groß</h3>
<p class="snack-card-note">Nachos groß + 1 Dip + 1x 0,33L Getränk</p>
<div class="option-group">
<button class="opt-btn active">Groß</button>
</div>
<div class="size-selector">
<button class="size-chip">Kombi <span>6,90€</span></button>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="cat-snacks" class="snack-category hidden"> <div id="cat-snacks" class="snack-category hidden">
<div class="snack-grid"> <div class="snack-grid">
<div class="snack-card"> <div class="snack-card">
@@ -647,75 +886,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 +905,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 +1118,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>

View File

@@ -1,18 +1,13 @@
import { currentUser, loginUser, openAccountDashboard, registerUser } from "./account.js"; // Shared app state for legacy script files (account.js, booking.js, cart.js, checkout.js)
import { openBooking } from "./booking.js"; var prices = { normal: 11.0, imax: 15.0, vip: 12.0, dbox: 16.0 };
import { renderCart, saveCart, updateCartBadge } from "./cart.js"; var seatLayouts = {
import { renderCheckout } from "./checkout.js";
// Shared app state for legacy script files (account.js, booking.js, cart.js, checkout.js)
export const prices: Record<string, number> = { normal: 11.0, imax: 15.0, vip: 12.0, dbox: 16.0 };
export var seatLayouts = {
"Kino 1": { rows: 6, left: 3, right: 7, vipRows: [5], dbox: [] }, "Kino 1": { rows: 6, left: 3, right: 7, vipRows: [5], dbox: [] },
"Kino 2": { rows: 7, left: 5, right: 5, vipRows: [6], dbox: [] }, "Kino 2": { rows: 7, left: 5, right: 5, vipRows: [6], dbox: [] },
"Deluxe 1": { rows: 10, left: 7, right: 8, vipRows: [9], dbox: [{ r: 4, c: 5, w: 4 }] }, "Deluxe 1": { rows: 10, left: 7, right: 8, vipRows: [9], dbox: [{ r: 4, c: 5, w: 4 }] },
IMAX: { rows: 15, left: 10, right: 10, vipRows: [], dbox: [], isImax: true } IMAX: { rows: 15, left: 10, right: 10, vipRows: [], dbox: [], isImax: true }
}; };
export var cart = JSON.parse(localStorage.getItem("eagleCart") || '{}'); var cart = JSON.parse(localStorage.getItem("eagleCart")) || [];
export var occupiedSeatsData = JSON.parse(localStorage.getItem("eagleOccupied") || '{}'); var occupiedSeatsData = JSON.parse(localStorage.getItem("eagleOccupied")) || {};
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
const views = { const views = {
@@ -42,7 +37,9 @@ document.addEventListener("DOMContentLoaded", () => {
heroDots: document.getElementById("hero-dots"), heroDots: document.getElementById("hero-dots"),
heroTitle: document.getElementById("hero-title"), heroTitle: document.getElementById("hero-title"),
heroText: document.getElementById("hero-text"), heroText: document.getElementById("hero-text"),
nowRunningShell: document.getElementById("now-running-shell"),
nowRunningRow: document.getElementById("now-running-row"), nowRunningRow: document.getElementById("now-running-row"),
nowRunningToggle: document.getElementById("now-running-toggle"),
movieProgramList: document.getElementById("movie-program-list"), movieProgramList: document.getElementById("movie-program-list"),
checkoutBtn: document.getElementById("btn-checkout-final"), checkoutBtn: document.getElementById("btn-checkout-final"),
backHomeBtn: document.getElementById("btn-back-home"), backHomeBtn: document.getElementById("btn-back-home"),
@@ -226,9 +223,11 @@ document.addEventListener("DOMContentLoaded", () => {
{ {
title: "Cash Truck", title: "Cash Truck",
genre: "Action", genre: "Action",
duration: 118, duration: 98,
fsk: "16", fsk: "16",
description: "" description: "Der verschlossene Einzelgänger „H” nimmt einen Job bei einer Geldtransporter-Firma an, die jede Woche hunderte von Millionen Dollar durch Los Angeles fährt. Gleich bei seinem ersten Einsatz wird der Geldtransport überfallen und zur Überraschung seiner Kollegen setzt H die Gangster im Alleingang außer Gefecht und wartet mit ungeahnten Präzisionsfähigkeiten auf. Doch Hs Absichten sind nicht zu durchschauen und kommen nur schrittweise ans Licht. Wer ist der geheimnisvolle Neuzugang und auf wen hat er es wirklich abgesehen?",
poster: "img/cashtruck.jpg",
backdrop: "img/cashtruck.jpg"
}, },
{ {
title: "Die Gangster Gang", title: "Die Gangster Gang",
@@ -331,10 +330,10 @@ document.addEventListener("DOMContentLoaded", () => {
["13:00", "16:00", "18:20", "20:40"] ["13:00", "16:00", "18:20", "20:40"]
]; ];
let movieProgram: any = []; // TODO: Find type let movieProgram = [];
let heroItems: any = []; // TODO: find Type let heroItems = [];
let heroIndex = 0; let heroIndex = 0;
let heroTimer:any = null; // TODO: find type let heroTimer = null;
const weekdayShort = ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"]; const weekdayShort = ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"];
@@ -353,7 +352,7 @@ document.addEventListener("DOMContentLoaded", () => {
window.scrollTo({ top: 0, behavior: "smooth" }); window.scrollTo({ top: 0, behavior: "smooth" });
}; };
const showMovieList = (programIndexToFocus:number = NaN) => { const showMovieList = (programIndexToFocus = null) => {
hideAllViews(); hideAllViews();
views.list?.classList.remove("hidden"); views.list?.classList.remove("hidden");
@@ -370,7 +369,7 @@ document.addEventListener("DOMContentLoaded", () => {
} }
}; };
const showStaticView = (viewElement: HTMLElement) => { const showStaticView = (viewElement) => {
if (!viewElement) { if (!viewElement) {
return; return;
} }
@@ -399,20 +398,52 @@ document.addEventListener("DOMContentLoaded", () => {
ui.bookingModal?.classList.add("hidden"); ui.bookingModal?.classList.add("hidden");
}; };
const escapeHtml = (value: string) => String(value || "") const escapeHtml = (value) => String(value || "")
.replaceAll("&", "&amp;") .replaceAll("&", "&amp;")
.replaceAll("<", "&lt;") .replaceAll("<", "&lt;")
.replaceAll(">", "&gt;") .replaceAll(">", "&gt;")
.replaceAll('"', "&quot;") .replaceAll('"', "&quot;")
.replaceAll("'", "&#39;"); .replaceAll("'", "&#39;");
const formatDateShort = (dateObj: any) => { const getMovieHalls = (movie) => {
const halls = new Set();
movie?.schedule?.forEach((day) => {
day.showings?.forEach((showing) => halls.add(showing.hall));
});
return Array.from(halls);
};
const getFskLogoPath = (movie) => {
if (movie?.fskLogo) {
return movie.fskLogo;
}
const fskValue = String(movie?.fsk || "").trim();
return fskValue && fskValue !== "?" ? `img/fsk-${fskValue}.png` : "";
};
const renderFskMetaLogo = (movie) => {
const fskValue = escapeHtml(movie.fsk || "?");
const logoPath = getFskLogoPath(movie);
const imageMarkup = logoPath
? `<img src="${escapeHtml(logoPath)}" alt="FSK ${fskValue}" onerror="this.parentElement.classList.add('missing-logo'); this.remove();">`
: "";
return `
<span class="fsk-meta-logo ${logoPath ? "has-logo" : "missing-logo"}" data-fsk="${fskValue}">
${imageMarkup}
<span>FSK ${fskValue}</span>
</span>
`;
};
const formatDateShort = (dateObj) => {
const day = String(dateObj.getDate()).padStart(2, "0"); const day = String(dateObj.getDate()).padStart(2, "0");
const month = String(dateObj.getMonth() + 1).padStart(2, "0"); const month = String(dateObj.getMonth() + 1).padStart(2, "0");
return `${day}.${month}.`; return `${day}.${month}.`;
}; };
const buildDayMeta = (offset: number) => { const buildDayMeta = (offset) => {
const date = new Date(); const date = new Date();
date.setHours(0, 0, 0, 0); date.setHours(0, 0, 0, 0);
date.setDate(date.getDate() + offset); date.setDate(date.getDate() + offset);
@@ -446,15 +477,14 @@ document.addEventListener("DOMContentLoaded", () => {
}; };
}; };
const buildScheduleForMovie = (movieIndex: number) => { const buildScheduleForMovie = (movieIndex) => {
return Array.from({ length: 7 }, (_, dayOffset) => { return Array.from({ length: 7 }, (_, dayOffset) => {
const dayMeta = buildDayMeta(dayOffset); const dayMeta = buildDayMeta(dayOffset);
const pattern = timePatterns[(movieIndex + dayOffset) % timePatterns.length] || "Error reading"; const pattern = timePatterns[(movieIndex + dayOffset) % timePatterns.length];
const desiredCount = 4 + ((movieIndex + dayOffset) % 2); const desiredCount = 4 + ((movieIndex + dayOffset) % 2);
const showCount = Math.min(pattern.length, desiredCount); const showCount = Math.min(pattern.length, desiredCount);
//@ts-ignore const showings = pattern.slice(0, showCount).map((time, slotIndex) => {
const showings = pattern.slice(0, showCount).map((time: any, slotIndex: number) => { // TODO: fix map issue
const hall = hallRotation[(movieIndex + dayOffset + slotIndex) % hallRotation.length]; const hall = hallRotation[(movieIndex + dayOffset + slotIndex) % hallRotation.length];
return { time, hall }; return { time, hall };
}); });
@@ -474,7 +504,7 @@ document.addEventListener("DOMContentLoaded", () => {
heroItems = movieProgram.slice(0, 5); heroItems = movieProgram.slice(0, 5);
}; };
const setHeroSlide = (index: number) => { const setHeroSlide = (index) => {
if (!heroItems.length || !ui.heroSlider) { if (!heroItems.length || !ui.heroSlider) {
return; return;
} }
@@ -503,18 +533,17 @@ document.addEventListener("DOMContentLoaded", () => {
return; return;
} }
ui.heroSlider.innerHTML = heroItems.map((movie: any, index: number) => ` ui.heroSlider.innerHTML = heroItems.map((movie, index) => `
<div class="hero-slide ${index === 0 ? "active" : ""}" style="background-image: linear-gradient(118deg, rgba(0,0,0,0.34), rgba(0,0,0,0.04)), url('${escapeHtml(movie.backdrop || movie.poster)}');"></div> <div class="hero-slide ${index === 0 ? "active" : ""}" style="background-image: linear-gradient(118deg, rgba(0,0,0,0.34), rgba(0,0,0,0.04)), url('${escapeHtml(movie.backdrop || movie.poster)}');"></div>
`).join(""); `).join("");
if (ui.heroDots) { if (ui.heroDots) {
ui.heroDots.innerHTML = heroItems.map((_:any, index: number) => ` ui.heroDots.innerHTML = heroItems.map((_, index) => `
<button type="button" class="hero-dot ${index === 0 ? "active" : ""}" data-hero-index="${index}"></button> <button type="button" class="hero-dot ${index === 0 ? "active" : ""}" data-hero-index="${index}"></button>
`).join(""); `).join("");
ui.heroDots.addEventListener("click", (event: any) => { ui.heroDots.addEventListener("click", (event) => {
const dotTarget = event.target || 0; const dot = event.target.closest(".hero-dot");
const dot = dotTarget.closest(".hero-dot");
if (!dot) { if (!dot) {
return; return;
} }
@@ -545,8 +574,7 @@ document.addEventListener("DOMContentLoaded", () => {
return; return;
} }
// TODO: implement movie interface ui.nowRunningRow.innerHTML = movieProgram.map((movie, index) => `
ui.nowRunningRow.innerHTML = movieProgram.map((movie: any, index: number) => /*html*/`
<article class="running-poster"> <article class="running-poster">
<img src="${escapeHtml(movie.poster)}" alt="${escapeHtml(movie.title)}"> <img src="${escapeHtml(movie.poster)}" alt="${escapeHtml(movie.title)}">
<div class="running-meta"> <div class="running-meta">
@@ -556,9 +584,15 @@ document.addEventListener("DOMContentLoaded", () => {
</div> </div>
</article> </article>
`).join(""); `).join("");
if (ui.nowRunningShell && ui.nowRunningToggle) {
ui.nowRunningShell.classList.add("is-collapsed");
ui.nowRunningToggle.setAttribute("aria-expanded", "false");
ui.nowRunningToggle.setAttribute("aria-label", "Weitere Filme anzeigen");
}
}; };
const renderScheduleRows = (programIndex: number, dayIndex: number) => { const renderScheduleRows = (programIndex, dayIndex) => {
const movie = movieProgram[programIndex]; const movie = movieProgram[programIndex];
if (!movie) { if (!movie) {
return; return;
@@ -570,7 +604,7 @@ document.addEventListener("DOMContentLoaded", () => {
return; return;
} }
body.innerHTML = day.showings.map((showing: { hall: string; time: string; }) => /*html*/` body.innerHTML = day.showings.map((showing) => `
<button class="schedule-row time-chip program-time-row" data-movie="${escapeHtml(movie.title)}" data-hall="${escapeHtml(showing.hall)}" data-time="${escapeHtml(showing.time)}"> <button class="schedule-row time-chip program-time-row" data-movie="${escapeHtml(movie.title)}" data-hall="${escapeHtml(showing.hall)}" data-time="${escapeHtml(showing.time)}">
<span>${escapeHtml(day.long)}</span> <span>${escapeHtml(day.long)}</span>
<span class="hall-pill">${escapeHtml(showing.hall)}</span> <span class="hall-pill">${escapeHtml(showing.hall)}</span>
@@ -584,24 +618,30 @@ document.addEventListener("DOMContentLoaded", () => {
return; return;
} }
ui.movieProgramList.innerHTML = movieProgram.map((movie: { schedule: any[]; poster: string; title: string; fsk: string; duration: any; genre: string; description: string; }, programIndex: any) => { ui.movieProgramList.innerHTML = movieProgram.map((movie, programIndex) => {
const dayTabs = movie.schedule.map((day, dayIndex) => /*html*/` const dayTabs = movie.schedule.map((day, dayIndex) => `
<button type="button" class="program-day-tab ${dayIndex === 0 ? "active" : ""}" data-program-index="${programIndex}" data-day-index="${dayIndex}"> <button type="button" class="program-day-tab ${dayIndex === 0 ? "active" : ""}" data-program-index="${programIndex}" data-day-index="${dayIndex}">
<span>${escapeHtml(day.short)}</span> <span>${escapeHtml(day.short)}</span>
<small>${escapeHtml(formatDateShort(day.date))}</small> <small>${escapeHtml(formatDateShort(day.date))}</small>
</button> </button>
`).join(""); `).join("");
const hallPills = getMovieHalls(movie).map((hall) => `
<span>${escapeHtml(hall)}</span>
`).join("");
return /*html*/` return `
<article class="detailed-card program-card reveal-on-scroll" data-program-index="${programIndex}"> <article class="detailed-card program-card reveal-on-scroll" data-program-index="${programIndex}">
<div class="card-left"> <div class="card-left">
<img src="${escapeHtml(movie.poster)}" alt="${escapeHtml(movie.title)}"> <img src="${escapeHtml(movie.poster)}" alt="${escapeHtml(movie.title)}">
<span class="fsk fsk-${escapeHtml(movie.fsk)}">${escapeHtml(movie.fsk)}</span>
</div> </div>
<div class="card-right"> <div class="card-right">
<div class="card-header"> <div class="card-header">
<h2>${escapeHtml(movie.title)}</h2> <h2>${escapeHtml(movie.title)}</h2>
<span class="duration">${movie.duration} Min. | ${escapeHtml(movie.genre)} | FSK: ${escapeHtml(movie.fsk)}</span> <span class="duration movie-meta-line">${movie.duration} Min. | ${escapeHtml(movie.genre)} | FSK: ${renderFskMetaLogo(movie)}</span>
</div>
<div class="program-halls-summary">
<strong>Kinosaal</strong>
<div>${hallPills}</div>
</div> </div>
<p class="description">${escapeHtml(movie.description)}</p> <p class="description">${escapeHtml(movie.description)}</p>
@@ -618,7 +658,7 @@ document.addEventListener("DOMContentLoaded", () => {
`; `;
}).join(""); }).join("");
movieProgram.forEach((_: any, programIndex: number) => { movieProgram.forEach((_, programIndex) => {
renderScheduleRows(programIndex, 0); renderScheduleRows(programIndex, 0);
}); });
}; };
@@ -666,16 +706,12 @@ document.addEventListener("DOMContentLoaded", () => {
ui.linkSnacks?.addEventListener("click", (event) => { ui.linkSnacks?.addEventListener("click", (event) => {
event.preventDefault(); event.preventDefault();
if (views.snacks) {
showStaticView(views.snacks); showStaticView(views.snacks);
}
}); });
ui.linkAbout?.addEventListener("click", (event) => { ui.linkAbout?.addEventListener("click", (event) => {
event.preventDefault(); event.preventDefault();
if (views.about) {
showStaticView(views.about); showStaticView(views.about);
}
}); });
ui.linkCart?.addEventListener("click", (event) => { ui.linkCart?.addEventListener("click", (event) => {
@@ -700,22 +736,28 @@ document.addEventListener("DOMContentLoaded", () => {
showMovieList(); showMovieList();
}); });
ui.nowRunningToggle?.addEventListener("click", () => {
const isCollapsed = ui.nowRunningShell?.classList.toggle("is-collapsed");
ui.nowRunningToggle.setAttribute("aria-expanded", String(!isCollapsed));
ui.nowRunningToggle.setAttribute("aria-label", isCollapsed ? "Weitere Filme anzeigen" : "Filme einklappen");
});
ui.checkoutBtn?.addEventListener("click", showCheckoutStart); ui.checkoutBtn?.addEventListener("click", showCheckoutStart);
ui.backHomeBtn?.addEventListener("click", showHome); ui.backHomeBtn?.addEventListener("click", showHome);
}; };
const bindProgramActions = () => { const bindProgramActions = () => {
views.moviesGrid?.addEventListener("click", (event:any) => { views.moviesGrid?.addEventListener("click", (event) => {
const trigger = event.target.closest(".open-program-btn"); const trigger = event.target.closest(".open-program-btn");
if (!trigger) { if (!trigger) {
return; return;
} }
const programIndex = Number(trigger.dataset.programIndex) || 0; const programIndex = Number(trigger.dataset.programIndex || 0);
showMovieList(programIndex); showMovieList(programIndex);
}); });
ui.movieProgramList?.addEventListener("click", (event:any) => { ui.movieProgramList?.addEventListener("click", (event) => {
const dayButton = event.target.closest(".program-day-tab"); const dayButton = event.target.closest(".program-day-tab");
if (!dayButton) { if (!dayButton) {
return; return;
@@ -725,7 +767,7 @@ document.addEventListener("DOMContentLoaded", () => {
const dayIndex = Number(dayButton.dataset.dayIndex || 0); const dayIndex = Number(dayButton.dataset.dayIndex || 0);
const tabRow = dayButton.closest(".program-day-tabs"); const tabRow = dayButton.closest(".program-day-tabs");
tabRow?.querySelectorAll(".program-day-tab").forEach((tab: { classList: { remove: (arg0: string) => any; }; }) => tab.classList.remove("active")); tabRow?.querySelectorAll(".program-day-tab").forEach((tab) => tab.classList.remove("active"));
dayButton.classList.add("active"); dayButton.classList.add("active");
renderScheduleRows(programIndex, dayIndex); renderScheduleRows(programIndex, dayIndex);
@@ -751,7 +793,7 @@ document.addEventListener("DOMContentLoaded", () => {
openButtons.forEach((button) => { openButtons.forEach((button) => {
button.addEventListener("click", () => { button.addEventListener("click", () => {
const targetId = button.getAttribute("data-home-view-open") as keyof typeof targetMap; const targetId = button.getAttribute("data-home-view-open");
const target = targetId ? targetMap[targetId] : null; const target = targetId ? targetMap[targetId] : null;
if (target) { if (target) {
showStaticView(target); showStaticView(target);
@@ -804,11 +846,10 @@ document.addEventListener("DOMContentLoaded", () => {
const THEME_KEY = "eagleTheme"; const THEME_KEY = "eagleTheme";
const applyTheme = (theme: any) => { const applyTheme = (theme) => {
const isLight = theme === "light"; const isLight = theme === "light";
document.body.classList.toggle("theme-light", isLight); document.body.classList.toggle("theme-light", isLight);
document.body.classList.toggle("theme-dark", !isLight); document.body.classList.toggle("theme-dark", !isLight);
//@ts-ignore
ui.themeToggle.classList.toggle("is-light", isLight); ui.themeToggle.classList.toggle("is-light", isLight);
localStorage.setItem(THEME_KEY, isLight ? "light" : "dark"); localStorage.setItem(THEME_KEY, isLight ? "light" : "dark");
}; };
@@ -825,14 +866,14 @@ document.addEventListener("DOMContentLoaded", () => {
const bindAccountActions = () => { const bindAccountActions = () => {
const registerModal = document.getElementById("register-modal"); const registerModal = document.getElementById("register-modal");
const forgotModal = document.getElementById("forgot-modal"); const forgotModal = document.getElementById("forgot-modal");
const forgotEmailInput = document.getElementById("forgot-email") as HTMLInputElement; const forgotEmailInput = document.getElementById("forgot-email");
const resetMessage = document.getElementById("reset-message"); const resetMessage = document.getElementById("reset-message");
const loginError = document.getElementById("login-error"); const loginError = document.getElementById("login-error");
const loginEmailInput = document.getElementById("login-email"); const loginEmailInput = document.getElementById("login-email");
const loginPasswordInput = document.getElementById("login-password"); const loginPasswordInput = document.getElementById("login-password");
const openModal = (modal: HTMLElement | null) => modal?.classList.remove("hidden"); const openModal = (modal) => modal?.classList.remove("hidden");
const closeModal = (modal: HTMLElement | null) => modal?.classList.add("hidden"); const closeModal = (modal) => modal?.classList.add("hidden");
const triggerLogin = () => { const triggerLogin = () => {
loginError?.classList.add("hidden"); loginError?.classList.add("hidden");
if (typeof loginUser === "function") { if (typeof loginUser === "function") {
@@ -868,7 +909,7 @@ document.addEventListener("DOMContentLoaded", () => {
}); });
document.getElementById("btn-forgot-password")?.addEventListener("click", () => { document.getElementById("btn-forgot-password")?.addEventListener("click", () => {
if (forgotEmailInput != null) { if (forgotEmailInput) {
forgotEmailInput.value = ""; forgotEmailInput.value = "";
} }
resetMessage?.classList.add("hidden"); resetMessage?.classList.add("hidden");
@@ -913,10 +954,10 @@ document.addEventListener("DOMContentLoaded", () => {
}; };
const bindGlobalDocumentClicks = () => { const bindGlobalDocumentClicks = () => {
document.addEventListener("click", (event: any) => { document.addEventListener("click", (event) => {
if (event.target.classList.contains("opt-btn")) { if (event.target.classList.contains("opt-btn")) {
const optionGroup = event.target.parentElement; const optionGroup = event.target.parentElement;
optionGroup?.querySelectorAll(".opt-btn").forEach((button: { classList: { remove: (arg0: string) => any; }; }) => button.classList.remove("active")); optionGroup?.querySelectorAll(".opt-btn").forEach((button) => button.classList.remove("active"));
event.target.classList.add("active"); event.target.classList.add("active");
} }
@@ -925,17 +966,15 @@ document.addEventListener("DOMContentLoaded", () => {
const row = deleteBtn.closest(".cart-item-row"); const row = deleteBtn.closest(".cart-item-row");
if (row) { if (row) {
row.classList.add("slide-out-left"); row.classList.add("slide-out-left");
row.querySelectorAll("button").forEach((button: { disabled: boolean; }) => { row.querySelectorAll("button").forEach((button) => {
button.disabled = true; button.disabled = true;
}); });
setTimeout(() => { setTimeout(() => {
//@ts-ignore removeFromCartByKey(deleteBtn.dataset.key);
removeFromCartByKey(deleteBtn.dataset.key); //TODO: removeFromCartByKey doesnt exist
}, 380); }, 380);
} else { } else {
//@ts-ignore removeFromCartByKey(deleteBtn.dataset.key);
removeFromCartByKey(deleteBtn.dataset.key); //TODO: removeFromCartByKey doesnt exist
} }
return; return;
} }
@@ -964,7 +1003,7 @@ document.addEventListener("DOMContentLoaded", () => {
return; return;
} }
const relatedItem = cart.find((item: { category: string; seatId: any; hall: any; time: any; title: any; }) => { const relatedItem = cart.find((item) => {
const infoText = item.category === "movie" const infoText = item.category === "movie"
? `Sitz: ${item.seatId} (${item.hall})` ? `Sitz: ${item.seatId} (${item.hall})`
: item.time; : item.time;
@@ -978,7 +1017,7 @@ document.addEventListener("DOMContentLoaded", () => {
if (action === "plus") { if (action === "plus") {
cart.push({ ...relatedItem, id: Date.now() + Math.random() }); cart.push({ ...relatedItem, id: Date.now() + Math.random() });
} else { } else {
const keyList = cart.map((item: { category: string; seatId: any; hall: any; time: any; title: any; }) => { const keyList = cart.map((item) => {
const infoText = item.category === "movie" const infoText = item.category === "movie"
? `Sitz: ${item.seatId} (${item.hall})` ? `Sitz: ${item.seatId} (${item.hall})`
: item.time; : item.time;
@@ -997,7 +1036,7 @@ document.addEventListener("DOMContentLoaded", () => {
}; };
const bindSnacksActions = () => { const bindSnacksActions = () => {
ui.snacksView?.addEventListener("click", (event:any) => { ui.snacksView?.addEventListener("click", (event) => {
const sizeChip = event.target.closest(".size-chip"); const sizeChip = event.target.closest(".size-chip");
if (!sizeChip) { if (!sizeChip) {
return; return;
@@ -1041,7 +1080,7 @@ document.addEventListener("DOMContentLoaded", () => {
}, 800); }, 800);
}); });
document.querySelectorAll(".tab-btn").forEach((button: any) => { document.querySelectorAll(".tab-btn").forEach((button) => {
button.addEventListener("click", () => { button.addEventListener("click", () => {
document.querySelectorAll(".tab-btn").forEach((tab) => tab.classList.remove("active")); document.querySelectorAll(".tab-btn").forEach((tab) => tab.classList.remove("active"));
button.classList.add("active"); button.classList.add("active");
@@ -1078,9 +1117,9 @@ document.addEventListener("DOMContentLoaded", () => {
} }
}); });
}; };
// @ts-ignore
window.removeFromCartByKey = function removeFromCartByKey(key: string) { window.removeFromCartByKey = function removeFromCartByKey(key) {
cart = cart.filter((item: { category: string; seatId: any; hall: any; time: any; title: any; }) => { cart = cart.filter((item) => {
const infoText = item.category === "movie" const infoText = item.category === "movie"
? `Sitz: ${item.seatId} (${item.hall})` ? `Sitz: ${item.seatId} (${item.hall})`
: item.time; : item.time;
@@ -1105,8 +1144,3 @@ document.addEventListener("DOMContentLoaded", () => {
updateCartBadge?.(); updateCartBadge?.();
renderCheckout?.(); renderCheckout?.();
}); });
export function emptyCart() {
cart = []
return
}

6442
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +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.1.10",
"dotenv": "^17.4.2",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"tailwindcss": "^4.2.4"
},
"devDependencies": {
"@types/node": "^25.6.0"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 655 B

View File

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

Before

Width:  |  Height:  |  Size: 749 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 878 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 362 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 910 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 643 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 481 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 530 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 KiB

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