diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..f818879
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,2 @@
+TMDB_API_TOKEN=yourapitoken
+TMDB_API_KEY=yourapikey
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
index ba7261d..43f9d6b 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,2 +1,2 @@
-img/ filter=lfs diff=lfs merge=lfs -text
-img/** filter=lfs diff=lfs merge=lfs -text
+public/img/* filter=lfs diff=lfs merge=lfs -text
+
diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml
new file mode 100644
index 0000000..6226e22
--- /dev/null
+++ b/.gitea/workflows/build.yaml
@@ -0,0 +1,52 @@
+name: Gitea CI-CD Pipeline
+
+on:
+ push:
+ branches: [main, master, dev, dev_*]
+ pull_request:
+ branches: [main, master]
+
+jobs:
+ # --- STAGE 1: TEST ---
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+
+ - name: Install Dependencies
+ run: npm ci
+
+ - name: Run Linter
+ run: npm run lint
+
+ - name: Run Tests
+ run: npm test
+
+ # --- STAGE 2: BUILD & PUBLISH (Only on Main) ---
+ build-and-deploy:
+ needs: test
+ if: gitea.ref == 'refs/heads/main' && gitea.event_name == 'push'
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@v4
+
+ # Example: Building a Docker Image and pushing to Gitea's internal registry
+ - name: Login to Gitea Registry
+ uses: docker/login-action@v3
+ with:
+ registry: gitea.starfour.de
+ username: ${{ gitea.actor }}
+ password: ${{ secrets.GITEA_TOKEN }}
+
+ - name: Build and Push
+ uses: docker/build-push-action@v5
+ with:
+ push: true
+ tags: gitea.starfour.de/${{ gitea.repository }}:latest
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..02b38a6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,25 @@
+# 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/
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000..22a1505
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,4 @@
+{
+ "recommendations": ["astro-build.astro-vscode"],
+ "unwantedRecommendations": []
+}
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..d642209
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,11 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "command": "./node_modules/.bin/astro dev",
+ "name": "Development server",
+ "request": "launch",
+ "type": "node-terminal"
+ }
+ ]
+}
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..87b813a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,43 @@
+# 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).
diff --git a/account.js b/account.js
deleted file mode 100644
index 2b7781a..0000000
--- a/account.js
+++ /dev/null
@@ -1,450 +0,0 @@
-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 = "
Mein Konto
Bitte melde dich an oder registriere dich.
";
- return;
- }
-
- accountView.innerHTML = `
-
-
-
-
-
-
-
-
-
-
-
- `;
-
- renderPersonalInfo();
-}
-
-function renderPersonalInfo() {
- const target = document.getElementById("account-tab-content");
- if (!target || !currentUser) {
- return;
- }
-
- target.innerHTML = `
-
-
Vorname: ${currentUser.firstName || "-"}
-
Nachname: ${currentUser.lastName || "-"}
-
E-Mail: ${currentUser.email || "-"}
-
- `;
-}
-
-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 = `
-
-
Meine Bestellungen
-
Noch keine Bestellungen vorhanden.
-
- `;
- 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 `
-
- `;
- })
- .join("");
-
- target.innerHTML = `
-
-
Meine Bestellungen
-
Klicke auf eine Bestellung, um dein Ticket-Detail zu sehen.
-
${orderHtml}
-
-
- `;
-
- 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 = `
-
-
- ${poster
- ? `
})
`
- : `
Kein Poster
`}
-
-
-
EAGLE'S IMAX | Bestell-Details
-
${escapeHtml(primaryMovie?.title || "Bestellung")}
-
-
Datum${escapeHtml(order.date || "-")}
-
Saal${escapeHtml(hall)}
-
Uhrzeit${escapeHtml(time)}
-
Tickets${ticketCount}x
-
Sitze${escapeHtml(seats)}
-
Gesamt${formatEuro(order.total || 0)}
-
-
-
- `;
-
- 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 = `
-
-
Zahlungsmethoden
-
Platzhalter zum Hinterlegen deiner Logos oder Anbieter-Informationen.
-
-
-
-
-
-
-
-
-
-
-
-
-

-
Kreditkarte hinterlegen
-
-
-
-
-
-
-
-
-
-
-

-
PayPal verbinden
-
-
Einloggen und dein PayPal-Konto mit deinem Kino-Account verbinden.
-
-
-
-
-
-
-
-
-
-
-

-
Apple Pay einrichten
-
-
Apple Pay wirkt schlicht, klar und fokussiert. Hinterlege hier die bevorzugte Karte für schnelle Zahlungen.
-
-
-
-
-
-
-
-
-
-
-

-
Google Pay einrichten
-
-
Verbinde deine Wallet, damit zukünftige Bestellungen in wenigen Klicks abgeschlossen werden.
-
-
-
-
-
- `;
-
- 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();
-}
diff --git a/astro.config.mjs b/astro.config.mjs
new file mode 100644
index 0000000..f22628a
--- /dev/null
+++ b/astro.config.mjs
@@ -0,0 +1,24 @@
+/* eslint-disable @typescript-eslint/ban-ts-comment */
+// @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"})
+ }
+ }
+});
\ No newline at end of file
diff --git a/booking.js b/booking.js
deleted file mode 100644
index 52aa109..0000000
--- a/booking.js
+++ /dev/null
@@ -1,352 +0,0 @@
-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) => `
-
-
- ${item.label}
-
- `)
- .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 `
-
- ${seat.dataset.seatId}
- ${seatPrice.toFixed(2).replace(".", ",")} EUR
-
- `;
- })
- .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);
-});
diff --git a/cart.js b/cart.js
deleted file mode 100644
index befa6a1..0000000
--- a/cart.js
+++ /dev/null
@@ -1,199 +0,0 @@
-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 `
- Sitzplatz: ${escapeHtml(item.seatId || "-")}
- Saal: ${escapeHtml(item.hall || "-")}
- Uhrzeit: ${escapeHtml(item.time || "-")} Uhr
- `;
- }
-
- if (isDrinkItem(item)) {
- return `
- Variante: ${escapeHtml(item.time || "-")}
- Groesse: ${escapeHtml(item.hall || "-")}
- `;
- }
-
- return `
- Kategorie: Snack
- Variante: ${escapeHtml(item.time || "-")}
- Groesse: ${escapeHtml(item.hall || "-")}
- `;
-}
-
-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 = 'Dein Warenkorb ist leer.
';
- totalEl.innerText = formatEuro(0);
- vatEl.innerText = `inkl. 19% MwSt: ${formatEuro(0)}`;
- return;
- }
-
- const groupedItems = groupCartItems();
-
- const header = `
-
- `;
-
- const rows = groupedItems
- .map((group) => {
- const imageHtml = group.item.img
- ? `
`
- : `Kein Bild
`;
- const quantityHtml = group.item.category === "movie"
- ? `${group.quantity}x
`
- : `
-
-
- ${group.quantity}
-
-
- `;
-
- return `
-
-
- ${quantityHtml}
-
-
${imageHtml}
-
${escapeHtml(group.item.title)}
-
${buildItemInfo(group.item)}
-
${formatEuro(group.total)}
-
-
-
-
- `;
- })
- .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();
-};
diff --git a/checkout.js b/checkout.js
deleted file mode 100644
index b351e6e..0000000
--- a/checkout.js
+++ /dev/null
@@ -1,232 +0,0 @@
-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 = `${item.title} (${infoText})${formatCheckoutEuro(item.price)}`;
- 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 = "Danke fuer deinen Einkauf!
";
- 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 = `
-
-
-

-
-
-
EAGLE'S IMAX PREMIUM
-
${mainMovie.title}
-
-
SAAL ${mainMovie.hall}
-
ZEIT ${mainMovie.time} Uhr
-
SITZE ${matchingMovieSeats || "-"}
-
-
-
-
- `;
-}
-
-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);
diff --git a/eslint.config.ts b/eslint.config.ts
new file mode 100644
index 0000000..cc5f6b0
--- /dev/null
+++ b/eslint.config.ts
@@ -0,0 +1,9 @@
+import js from "@eslint/js";
+import globals from "globals";
+import tseslint from "typescript-eslint";
+import { defineConfig } from "eslint/config";
+
+export default defineConfig([
+ { files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.browser } },
+ tseslint.configs.recommended,
+]);
diff --git a/index.html b/index.html
index e7df18a..537bc3c 100644
--- a/index.html
+++ b/index.html
@@ -735,8 +735,8 @@
@@ -948,11 +948,11 @@
-
-
-
-
-
+
+
+
+
+