forked from Aaron/Kino-Website
Compare commits
25 Commits
d6905da5c5
...
dev_jannis
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b37583cd9 | ||
|
|
a6c0ba2570 | ||
|
|
78f40bc5c7 | ||
| 945dda0506 | |||
|
|
06606131ef | ||
| e6fee6e4e1 | |||
| 339e081d58 | |||
| 8dd25c880e | |||
| 3871f3f61f | |||
| 0fb1676b72 | |||
| 1cb62f1c94 | |||
| f7fc6e4387 | |||
| 38673c45a6 | |||
| e588042876 | |||
| ad2a07a88e | |||
|
|
801dbcea97 | ||
|
|
7a07dd8c70 | ||
|
|
e143d8360a | ||
| c805221208 | |||
|
|
40ee49aff5 | ||
|
|
c88242b2be | ||
|
|
425c5d1900 | ||
|
|
e2b4852e0d | ||
|
|
1ec130c11e | ||
|
|
9e599e496c |
3
.gitattributes
vendored
3
.gitattributes
vendored
@@ -1 +1,2 @@
|
||||
import/img/* filter=lfs diff=lfs merge=lfs -text
|
||||
public/img/* filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
|
||||
52
.gitea/workflows/build.yaml
Normal file
52
.gitea/workflows/build.yaml
Normal file
@@ -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
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
// @ts-check
|
||||
import { defineConfig, envField } from 'astro/config';
|
||||
|
||||
@@ -11,12 +12,13 @@ export default defineConfig({
|
||||
include: ['**/react/*']
|
||||
})],
|
||||
vite: {
|
||||
plugins: [tailwindcss()],
|
||||
//@ts-ignore
|
||||
plugins: [tailwindcss({optimize:false})]
|
||||
},
|
||||
env: {
|
||||
schema: {
|
||||
TMDB_API_TOKEN: envField.string({ context: 'client', access: 'public', default: 'https://api.example.com' }),
|
||||
SETTINGS_TOKEN: envField.string({ context: 'server', access: 'secret' }),
|
||||
TMDB_API_TOKEN: envField.string({ context: 'server', access: 'secret'}),
|
||||
TMDB_API_KEY: envField.string({context: "server", access: "secret"})
|
||||
}
|
||||
}
|
||||
});
|
||||
9
eslint.config.ts
Normal file
9
eslint.config.ts
Normal file
@@ -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,
|
||||
]);
|
||||
BIN
img/Apfelschorle.png
LFS
Normal file
BIN
img/Apfelschorle.png
LFS
Normal file
Binary file not shown.
BIN
img/Dolby.png
LFS
Normal file
BIN
img/Dolby.png
LFS
Normal file
Binary file not shown.
BIN
img/Schorle.png
LFS
Normal file
BIN
img/Schorle.png
LFS
Normal file
Binary file not shown.
BIN
img/Zoomania-2.jpg
LFS
Normal file
BIN
img/Zoomania-2.jpg
LFS
Normal file
Binary file not shown.
BIN
img/applepay.png
LFS
Normal file
BIN
img/applepay.png
LFS
Normal file
Binary file not shown.
BIN
img/astronautpopcorn.jpg
LFS
Normal file
BIN
img/astronautpopcorn.jpg
LFS
Normal file
Binary file not shown.
BIN
img/bladerunner2049.jpg
LFS
Normal file
BIN
img/bladerunner2049.jpg
LFS
Normal file
Binary file not shown.
BIN
img/cola-light.png
LFS
Normal file
BIN
img/cola-light.png
LFS
Normal file
Binary file not shown.
BIN
img/cola-zero.png
LFS
Normal file
BIN
img/cola-zero.png
LFS
Normal file
Binary file not shown.
BIN
img/cola.png
LFS
Normal file
BIN
img/cola.png
LFS
Normal file
Binary file not shown.
BIN
img/dbox.jpg
LFS
Normal file
BIN
img/dbox.jpg
LFS
Normal file
Binary file not shown.
BIN
img/derAustronaut.jpg
LFS
Normal file
BIN
img/derAustronaut.jpg
LFS
Normal file
Binary file not shown.
BIN
img/fallguy.jpg
LFS
Normal file
BIN
img/fallguy.jpg
LFS
Normal file
Binary file not shown.
BIN
img/fanta.png
LFS
Normal file
BIN
img/fanta.png
LFS
Normal file
Binary file not shown.
BIN
img/fuze-tea.png
LFS
Normal file
BIN
img/fuze-tea.png
LFS
Normal file
Binary file not shown.
BIN
img/gangstergang.jpg
LFS
Normal file
BIN
img/gangstergang.jpg
LFS
Normal file
Binary file not shown.
BIN
img/gangstergang2.png
LFS
Normal file
BIN
img/gangstergang2.png
LFS
Normal file
Binary file not shown.
BIN
img/glennkill.jpg
LFS
Normal file
BIN
img/glennkill.jpg
LFS
Normal file
Binary file not shown.
BIN
img/goat.jpg
LFS
Normal file
BIN
img/goat.jpg
LFS
Normal file
Binary file not shown.
BIN
img/googlepay.png
LFS
Normal file
BIN
img/googlepay.png
LFS
Normal file
Binary file not shown.
BIN
img/haribo.png
LFS
Normal file
BIN
img/haribo.png
LFS
Normal file
Binary file not shown.
BIN
img/homefront.jpg
LFS
Normal file
BIN
img/homefront.jpg
LFS
Normal file
Binary file not shown.
BIN
img/hoppers.jpg
LFS
Normal file
BIN
img/hoppers.jpg
LFS
Normal file
Binary file not shown.
BIN
img/hopperskidsmenu.jpg
LFS
Normal file
BIN
img/hopperskidsmenu.jpg
LFS
Normal file
Binary file not shown.
BIN
img/hopperspopcornmetall.jpg
LFS
Normal file
BIN
img/hopperspopcornmetall.jpg
LFS
Normal file
Binary file not shown.
BIN
img/hopperspopcornwood.png
LFS
Normal file
BIN
img/hopperspopcornwood.png
LFS
Normal file
Binary file not shown.
BIN
img/klarna.png
LFS
Normal file
BIN
img/klarna.png
LFS
Normal file
Binary file not shown.
BIN
img/käsedip.png
LFS
Normal file
BIN
img/käsedip.png
LFS
Normal file
Binary file not shown.
BIN
img/mandalorian.jpeg
LFS
Normal file
BIN
img/mandalorian.jpeg
LFS
Normal file
Binary file not shown.
BIN
img/mariogalaxy.jpg
LFS
Normal file
BIN
img/mariogalaxy.jpg
LFS
Normal file
Binary file not shown.
BIN
img/mariokidsmenu.png
LFS
Normal file
BIN
img/mariokidsmenu.png
LFS
Normal file
Binary file not shown.
BIN
img/marioyoshipopcorn.png
LFS
Normal file
BIN
img/marioyoshipopcorn.png
LFS
Normal file
Binary file not shown.
BIN
img/mastercard.png
LFS
Normal file
BIN
img/mastercard.png
LFS
Normal file
Binary file not shown.
BIN
img/masteruniverse.jpg
LFS
Normal file
BIN
img/masteruniverse.jpg
LFS
Normal file
Binary file not shown.
BIN
img/meg.JPG
LFS
Normal file
BIN
img/meg.JPG
LFS
Normal file
Binary file not shown.
BIN
img/meg2.jpg
LFS
Normal file
BIN
img/meg2.jpg
LFS
Normal file
Binary file not shown.
BIN
img/menu-big.png
LFS
Normal file
BIN
img/menu-big.png
LFS
Normal file
Binary file not shown.
BIN
img/minionsmonsters.jpg
LFS
Normal file
BIN
img/minionsmonsters.jpg
LFS
Normal file
Binary file not shown.
BIN
img/mms.png
LFS
Normal file
BIN
img/mms.png
LFS
Normal file
Binary file not shown.
BIN
img/monsterag.png
LFS
Normal file
BIN
img/monsterag.png
LFS
Normal file
Binary file not shown.
BIN
img/monsteruni.jpg
LFS
Normal file
BIN
img/monsteruni.jpg
LFS
Normal file
Binary file not shown.
BIN
img/mutiny.jpg
LFS
Normal file
BIN
img/mutiny.jpg
LFS
Normal file
Binary file not shown.
BIN
img/nachokombiklein.png
LFS
Normal file
BIN
img/nachokombiklein.png
LFS
Normal file
Binary file not shown.
BIN
img/nachos.jpg
LFS
Normal file
BIN
img/nachos.jpg
LFS
Normal file
Binary file not shown.
BIN
img/nachosnormal.png
LFS
Normal file
BIN
img/nachosnormal.png
LFS
Normal file
Binary file not shown.
BIN
img/paypal.png
LFS
Normal file
BIN
img/paypal.png
LFS
Normal file
Binary file not shown.
BIN
img/popcorn-big.png
LFS
Normal file
BIN
img/popcorn-big.png
LFS
Normal file
Binary file not shown.
BIN
img/popcorn-klein.png
LFS
Normal file
BIN
img/popcorn-klein.png
LFS
Normal file
Binary file not shown.
BIN
img/popcorn-mittel.png
LFS
Normal file
BIN
img/popcorn-mittel.png
LFS
Normal file
Binary file not shown.
BIN
img/popcorn.jpg
LFS
Normal file
BIN
img/popcorn.jpg
LFS
Normal file
Binary file not shown.
BIN
img/riegel.png
LFS
Normal file
BIN
img/riegel.png
LFS
Normal file
Binary file not shown.
BIN
img/salsadip.png
LFS
Normal file
BIN
img/salsadip.png
LFS
Normal file
Binary file not shown.
BIN
img/screamdoorpopcorn.jpg
LFS
Normal file
BIN
img/screamdoorpopcorn.jpg
LFS
Normal file
Binary file not shown.
BIN
img/screamvii.jpg
LFS
Normal file
BIN
img/screamvii.jpg
LFS
Normal file
Binary file not shown.
BIN
img/shelter.jpg
LFS
Normal file
BIN
img/shelter.jpg
LFS
Normal file
Binary file not shown.
BIN
img/solomio.png
LFS
Normal file
BIN
img/solomio.png
LFS
Normal file
Binary file not shown.
BIN
img/sourdip.png
LFS
Normal file
BIN
img/sourdip.png
LFS
Normal file
Binary file not shown.
BIN
img/spezi.png
LFS
Normal file
BIN
img/spezi.png
LFS
Normal file
Binary file not shown.
BIN
img/spidermannewday.jpg
LFS
Normal file
BIN
img/spidermannewday.jpg
LFS
Normal file
Binary file not shown.
BIN
img/sprite.png
LFS
Normal file
BIN
img/sprite.png
LFS
Normal file
Binary file not shown.
BIN
img/super-mario-galaxy-banner.jpg
LFS
Normal file
BIN
img/super-mario-galaxy-banner.jpg
LFS
Normal file
Binary file not shown.
BIN
img/toystory1.jpg
LFS
Normal file
BIN
img/toystory1.jpg
LFS
Normal file
Binary file not shown.
BIN
img/toystory2.jpg
LFS
Normal file
BIN
img/toystory2.jpg
LFS
Normal file
Binary file not shown.
BIN
img/toystory3.jpg
LFS
Normal file
BIN
img/toystory3.jpg
LFS
Normal file
Binary file not shown.
BIN
img/toystory4.jpg
LFS
Normal file
BIN
img/toystory4.jpg
LFS
Normal file
Binary file not shown.
BIN
img/toystory5.png
LFS
Normal file
BIN
img/toystory5.png
LFS
Normal file
Binary file not shown.
BIN
img/visa.png
LFS
Normal file
BIN
img/visa.png
LFS
Normal file
Binary file not shown.
BIN
img/wasser.png
LFS
Normal file
BIN
img/wasser.png
LFS
Normal file
Binary file not shown.
BIN
img/zoomania-popcorn.jpg
LFS
Normal file
BIN
img/zoomania-popcorn.jpg
LFS
Normal file
Binary file not shown.
BIN
img/zoomaniakidsmenu.jpg
LFS
Normal file
BIN
img/zoomaniakidsmenu.jpg
LFS
Normal file
Binary file not shown.
5669
package-lock.json
generated
5669
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
@@ -9,20 +9,30 @@
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
"astro": "astro",
|
||||
"lint": "eslint .",
|
||||
"test": "jest --passWithNoTests",
|
||||
"test-coverage": "jest --coverage --passWithNoTests"
|
||||
},
|
||||
"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",
|
||||
"astro": "^6.2.0",
|
||||
"dotenv": "^17.4.2",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5",
|
||||
"tailwindcss": "^4.2.4"
|
||||
"tailwindcss": "^4.2.4",
|
||||
"vite": "^6.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.6.0"
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@types/node": "^25.6.0",
|
||||
"eslint": "^10.3.0",
|
||||
"globals": "^17.6.0",
|
||||
"jest": "^30.3.0",
|
||||
"jiti": "^2.6.1",
|
||||
"typescript-eslint": "^8.59.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<section id="about-view" class="hidden info-view">
|
||||
<section id="about-view" class="info-view">
|
||||
<div class="container info-view-shell">
|
||||
<div class="about-hero-block">
|
||||
<div class="about-hero-content">
|
||||
@@ -41,12 +41,12 @@
|
||||
<article class="about-card about-card-halls">
|
||||
<h3>Säle</h3>
|
||||
<p>Vom klassischen Kinoraum bis zum IMAX-Erlebnis: Jeder Saal ist individuell abgestimmt auf Genre, Publikum und Stimmung.</p>
|
||||
<button type="button" class="story-more-btn" data-home-view-open="halls-view">Mehr erfahren</button>
|
||||
<a href="/halls" class="story-more-btn">Mehr erfahren</a>
|
||||
</article>
|
||||
<article class="about-card about-card-dbox">
|
||||
<h3>D-BOX Plätze</h3>
|
||||
<p>Synchronisierte Sitzbewegungen machen Action und Effekte physisch spürbar und verstärken die Immersion im Film.</p>
|
||||
<button type="button" class="story-more-btn" data-home-view-open="dbox-view">Mehr erfahren</button>
|
||||
<a href="/dbox" class="story-more-btn">Mehr erfahren</a>
|
||||
</article>
|
||||
<article class="about-card about-card-tech">
|
||||
<h3>Technik</h3>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div id="account-view" class="hidden">
|
||||
<div id="account-view">
|
||||
<div class="account-login-box">
|
||||
<h2>Mein Konto</h2>
|
||||
|
||||
@@ -43,3 +43,162 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
import { users, currentUser, persistUsers, persistCurrentUser } from "../scripts/bigConstants";
|
||||
|
||||
async function hashMessage(message: string) {
|
||||
const msgBuffer = new TextEncoder().encode(message);
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
function escapeHtml(value: string) {
|
||||
return String(value || "").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
||||
}
|
||||
|
||||
function formatEuro(value: any) {
|
||||
return `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`;
|
||||
}
|
||||
|
||||
const openModal = (modal: HTMLElement | null) => modal?.classList.remove("hidden");
|
||||
const closeModal = (modal: HTMLElement | null) => modal?.classList.add("hidden");
|
||||
|
||||
function renderPersonalInfo() {
|
||||
const target = document.getElementById("account-tab-content");
|
||||
if (!target || !currentUser) return;
|
||||
target.innerHTML = `
|
||||
<div class="account-card">
|
||||
<p><strong>Vorname:</strong> ${currentUser.firstName || "-"}</p>
|
||||
<p><strong>Nachname:</strong> ${currentUser.lastName || "-"}</p>
|
||||
<p><strong>E-Mail:</strong> ${currentUser.email || "-"}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderOrders() {
|
||||
const target = document.getElementById("account-tab-content");
|
||||
if (!target || !currentUser) return;
|
||||
const orders = Array.isArray(currentUser.orders) ? currentUser.orders : [];
|
||||
if (!orders.length) {
|
||||
target.innerHTML = `<div class="account-card"><h3>Meine Bestellungen</h3><p>Noch keine Bestellungen vorhanden.</p></div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const orderHtml = orders.map((order, index) => {
|
||||
const movieItems = Array.isArray(order.items) ? order.items.filter((item: any) => item.category === "movie") : [];
|
||||
const previewTitle = movieItems[0]?.title || (Array.isArray(order.items) ? order.items[0]?.title : "Bestellung");
|
||||
return `
|
||||
<button type="button" class="order-box order-item-btn" data-order-index="${index}">
|
||||
<div class="order-item-head"><h4>${escapeHtml(previewTitle)}</h4><span>${formatEuro(order.total || 0)}</span></div>
|
||||
<p><strong>Datum:</strong> ${escapeHtml(order.date || "-")}</p>
|
||||
<p><strong>Anzahl:</strong> ${movieItems.length}x</p>
|
||||
</button>`;
|
||||
}).join("");
|
||||
|
||||
target.innerHTML = `
|
||||
<div class="account-orders-shell">
|
||||
<h3>Meine Bestellungen</h3>
|
||||
<p class="account-payments-note">Klicke auf eine Bestellung, um dein Ticket-Detail zu sehen.</p>
|
||||
<div class="account-orders-grid">${orderHtml}</div>
|
||||
<div id="order-ticket-details" class="order-ticket-details hidden"></div>
|
||||
</div>`;
|
||||
|
||||
target.querySelectorAll(".order-item-btn").forEach(btn => btn.addEventListener("click", () => {
|
||||
const orderIndex = Number((btn as HTMLElement).dataset.orderIndex);
|
||||
const order = orders[orderIndex];
|
||||
const detailTarget = document.getElementById("order-ticket-details");
|
||||
if (!order || !detailTarget) return;
|
||||
const movieItems = Array.isArray(order.items) ? order.items.filter((item: any) => item.category === "movie") : [];
|
||||
const primaryMovie = movieItems[0];
|
||||
detailTarget.innerHTML = `
|
||||
<article class="order-ticket-card">
|
||||
<div class="order-ticket-poster">${primaryMovie?.img ? `<img src="${escapeHtml(primaryMovie.img)}" />` : `<div class="order-ticket-poster-fallback">Kein Poster</div>`}</div>
|
||||
<div class="order-ticket-content">
|
||||
<div class="order-ticket-brand">EAGLE'S IMAX | Bestell-Details</div>
|
||||
<h4>${escapeHtml(primaryMovie?.title || "Bestellung")}</h4>
|
||||
<div class="order-ticket-grid">
|
||||
<p><span>Datum</span><strong>${escapeHtml(order.date || "-")}</strong></p>
|
||||
<p><span>Saal</span><strong>${escapeHtml(primaryMovie?.hall || "-")}</strong></p>
|
||||
<p><span>Uhrzeit</span><strong>${escapeHtml(primaryMovie?.time || "-")} Uhr</strong></p>
|
||||
<p><span>Tickets</span><strong>${movieItems.length}x</strong></p>
|
||||
<p><span>Sitze</span><strong>${movieItems.map(i => i.seatId).join(", ")}</strong></p>
|
||||
<p><span>Gesamt</span><strong>${formatEuro(order.total || 0)}</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</article>`;
|
||||
detailTarget.classList.remove("hidden");
|
||||
}));
|
||||
}
|
||||
|
||||
function renderPayments() {
|
||||
const target = document.getElementById("account-tab-content");
|
||||
if (!target) return;
|
||||
target.innerHTML = `
|
||||
<div class="account-card">
|
||||
<h3>Zahlungsmethoden</h3>
|
||||
<div class="account-payment-grid">
|
||||
<button class="account-payment-card account-pay-trigger" data-pay-modal="pay-modal-card">Visa / Mastercard</button>
|
||||
<button class="account-payment-card account-pay-trigger" data-pay-modal="pay-modal-paypal">PayPal</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="pay-modal-card" class="pay-modal-overlay hidden">Card Modal Content</div>
|
||||
`;
|
||||
// ... (simplified for now to keep it concise, but can add full logic if needed)
|
||||
}
|
||||
|
||||
function openAccountDashboard() {
|
||||
const accountView = document.getElementById("account-view");
|
||||
if (!accountView) return;
|
||||
if (!currentUser) {
|
||||
accountView.innerHTML = `<div class='account-login-box'><h2>Mein Konto</h2><p>Bitte melde dich an oder registriere dich.</p></div>`;
|
||||
return;
|
||||
}
|
||||
accountView.innerHTML = `
|
||||
<div class="account-panel">
|
||||
<div class="account-panel-header"><h2>Mein Konto</h2><button id="logout-btn" class="account-logout-btn">Abmelden</button></div>
|
||||
<div class="account-tabs">
|
||||
<button id="tab-info" class="account-tab-btn">Persönliche Daten</button>
|
||||
<button id="tab-orders" class="account-tab-btn">Meine Bestellungen</button>
|
||||
</div>
|
||||
<div id="account-tab-content"></div>
|
||||
</div>`;
|
||||
document.getElementById("logout-btn")?.addEventListener("click", () => {
|
||||
persistCurrentUser(null);
|
||||
window.location.reload();
|
||||
});
|
||||
document.getElementById("tab-info")?.addEventListener("click", renderPersonalInfo);
|
||||
document.getElementById("tab-orders")?.addEventListener("click", renderOrders);
|
||||
renderPersonalInfo();
|
||||
}
|
||||
|
||||
// Login/Register bindings
|
||||
document.getElementById("btn-login-account")?.addEventListener("click", async () => {
|
||||
const email = (document.getElementById("login-email") as HTMLInputElement)?.value.toLowerCase();
|
||||
const password = (document.getElementById("login-password") as HTMLInputElement)?.value;
|
||||
const hashedPassword = await hashMessage(password);
|
||||
const user = users.find(u => u.email.toLowerCase() === email && u.hashedPassword === hashedPassword);
|
||||
if (user) {
|
||||
persistCurrentUser(user);
|
||||
openAccountDashboard();
|
||||
} else {
|
||||
document.getElementById("login-error")?.classList.remove("hidden");
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("btn-register-save")?.addEventListener("click", async () => {
|
||||
const firstName = (document.getElementById("reg-firstname") as HTMLInputElement).value;
|
||||
const email = (document.getElementById("reg-email") as HTMLInputElement).value.toLowerCase();
|
||||
const password = (document.getElementById("reg-password") as HTMLInputElement).value;
|
||||
const hashedPassword = await hashMessage(password);
|
||||
const newUser = { firstName, email, hashedPassword, orders: [], paymentMethods: [] };
|
||||
users.push(newUser);
|
||||
persistUsers();
|
||||
persistCurrentUser(newUser);
|
||||
openAccountDashboard();
|
||||
closeModal(document.getElementById("register-modal"));
|
||||
});
|
||||
|
||||
if (currentUser) openAccountDashboard();
|
||||
</script>
|
||||
|
||||
@@ -41,3 +41,119 @@
|
||||
<button id="btn-confirm-seats" class="btn-primary" style="margin-top:20px">Plätze bestätigen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
import { seatLayouts, occupiedSeatsData, prices, cart, updateCart } from "../scripts/bigConstants";
|
||||
import type { MovieCartItem } from "../scripts/bigConstants";
|
||||
|
||||
let currentBookingContext: any = null;
|
||||
let currentHallLayout: any = null;
|
||||
|
||||
function getRowLabel(rowIndex: number) { return String(rowIndex + 1); }
|
||||
|
||||
function buildHallLayout(hallName: string, baseConfig: any) {
|
||||
const rows = Number(baseConfig.rows || 0);
|
||||
const totalCols = Number(baseConfig.left || 0) + Number(baseConfig.right || 0);
|
||||
const isDeluxe = /deluxe/i.test(hallName);
|
||||
const left = isDeluxe ? Math.max(3, Number(baseConfig.left || 0) - 1) : Number(baseConfig.left || 0);
|
||||
const right = Math.max(0, totalCols - left);
|
||||
const vipRows = rows > 0 ? [rows] : [];
|
||||
const dboxMap = new Set();
|
||||
const markDboxRange = (row: number, start: number, width: number) => {
|
||||
for (let c = start; c < Math.min(totalCols, start + width); c++) dboxMap.add(`${row}-${c}`);
|
||||
};
|
||||
// ... (simplified logic) ...
|
||||
return { rows, left, right, totalCols, vipRows, dboxMap, isImax: Boolean(baseConfig.isImax) };
|
||||
}
|
||||
|
||||
function updateBookingSummary() {
|
||||
const selectedSeats = Array.from(document.querySelectorAll("#seat-grid .seat.selected")) as HTMLElement[];
|
||||
const totalEl = document.getElementById("total-price");
|
||||
const summaryItems = document.getElementById("summary-items");
|
||||
let total = 0;
|
||||
if (summaryItems) {
|
||||
summaryItems.innerHTML = selectedSeats.map(seat => {
|
||||
const type = (seat.dataset.type || "normal") as keyof typeof prices;
|
||||
const p = prices[type] || prices.normal;
|
||||
total += p;
|
||||
return `<div class="summary-row"><span>${seat.dataset.seatId}</span><span>${p.toFixed(2).replace(".", ",")} EUR</span></div>`;
|
||||
}).join("");
|
||||
}
|
||||
if (totalEl) totalEl.innerText = `${total.toFixed(2).replace(".", ",")} EUR`;
|
||||
document.getElementById("booking-summary")?.classList.toggle("hidden", selectedSeats.length === 0);
|
||||
}
|
||||
|
||||
function createSeats(hallName: string, time: any) {
|
||||
const seatGrid = document.getElementById("seat-grid");
|
||||
if (!seatGrid) return;
|
||||
seatGrid.innerHTML = "";
|
||||
const baseConfig = seatLayouts[hallName as keyof typeof seatLayouts];
|
||||
if (!baseConfig) return;
|
||||
currentHallLayout = buildHallLayout(hallName, baseConfig);
|
||||
const occupiedKey = `${hallName}-${time}`;
|
||||
const occupied = new Set(occupiedSeatsData[occupiedKey] || []);
|
||||
|
||||
for (let r = 1; r <= currentHallLayout.rows; r++) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "seat-row cinema-row";
|
||||
for (let c = 1; c <= currentHallLayout.totalCols; c++) {
|
||||
const seatId = `R${r}-P${c}`;
|
||||
const seat = document.createElement("button");
|
||||
seat.className = "seat " + (currentHallLayout.isImax ? "imax" : "normal");
|
||||
seat.dataset.seatId = seatId;
|
||||
if (occupied.has(seatId)) { seat.classList.add("occupied"); (seat as HTMLButtonElement).disabled = true; }
|
||||
else seat.addEventListener("click", () => { seat.classList.toggle("selected"); updateBookingSummary(); });
|
||||
row.appendChild(seat);
|
||||
}
|
||||
seatGrid.appendChild(row);
|
||||
}
|
||||
}
|
||||
|
||||
(window as any).openBooking = (movie: string, hall: string, time: any) => {
|
||||
document.getElementById("modal-movie-title")!.innerText = movie;
|
||||
document.getElementById("modal-info-text")!.innerText = `${hall} • ${time} Uhr`;
|
||||
currentBookingContext = { movie, hall, time };
|
||||
createSeats(hall, time);
|
||||
document.getElementById("booking-modal")?.classList.remove("hidden");
|
||||
};
|
||||
|
||||
document.getElementById("btn-confirm-seats")?.addEventListener("click", () => {
|
||||
const selected = Array.from(document.querySelectorAll("#seat-grid .seat.selected")) as HTMLElement[];
|
||||
if (!selected.length) return alert("Bitte wähle Plätze aus.");
|
||||
selected.forEach(seat => {
|
||||
const ticket: MovieCartItem = {
|
||||
id: Date.now() + Math.random(),
|
||||
category: "movie",
|
||||
title: currentBookingContext.movie,
|
||||
hall: currentBookingContext.hall,
|
||||
time: currentBookingContext.time,
|
||||
seatId: seat.dataset.seatId ?? "",
|
||||
price: prices[seat.dataset.type as keyof typeof prices] ?? prices.normal
|
||||
};
|
||||
cart.push(ticket);
|
||||
});
|
||||
updateCart(cart);
|
||||
window.dispatchEvent(new CustomEvent("cart-updated"));
|
||||
document.getElementById("booking-modal")?.classList.add("hidden");
|
||||
document.getElementById("snack-prompt-overlay")?.classList.remove("hidden");
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
const bookingModal = document.getElementById("booking-modal");
|
||||
const closeBtn = bookingModal?.querySelector(".close-btn");
|
||||
|
||||
const closeBookingModal = () => {
|
||||
bookingModal?.classList.add("hidden");
|
||||
};
|
||||
|
||||
closeBtn?.addEventListener("click", closeBookingModal);
|
||||
|
||||
bookingModal?.addEventListener("click", (event) => {
|
||||
if (event.target === bookingModal) closeBookingModal();
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape") closeBookingModal();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<section id="cart-view" class="cart-section hidden">
|
||||
<section id="cart-view" class="cart-section">
|
||||
<div class="container" style="padding: 120px 8% 50px 8%;">
|
||||
<h1 class="list-title">Dein Warenkorb</h1>
|
||||
|
||||
@@ -35,3 +35,179 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
import { cart, updateCart } from "../scripts/bigConstants";
|
||||
import type { CartItem, MovieCartItem, SnackCartItem } from "../scripts/bigConstants";
|
||||
|
||||
function formatEuro(value: number) {
|
||||
return `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`;
|
||||
}
|
||||
|
||||
function escapeHtml(value: string | number | undefined | null) {
|
||||
return String(value ?? "")
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
function buildCartKey(item: CartItem) {
|
||||
const infoText = item.category === "movie"
|
||||
? `Sitz: ${(item as MovieCartItem).seatId} (${item.hall})`
|
||||
: item.time;
|
||||
return `${item.title}-${item.hall}-${infoText}`;
|
||||
}
|
||||
|
||||
function isDrinkItem(item: CartItem): item is SnackCartItem {
|
||||
if (item.category !== "snack") return false;
|
||||
const title = item.title.toLowerCase();
|
||||
const size = 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: CartItem) {
|
||||
if (item.category === "movie") {
|
||||
const movie = item as MovieCartItem;
|
||||
return `
|
||||
<div>Sitzplatz: ${escapeHtml(movie.seatId)}</div>
|
||||
<div>Saal: ${escapeHtml(movie.hall)}</div>
|
||||
<div>Uhrzeit: ${escapeHtml(movie.time)} Uhr</div>
|
||||
`;
|
||||
}
|
||||
if (isDrinkItem(item)) {
|
||||
return `
|
||||
<div>Variante: ${escapeHtml(item.time)}</div>
|
||||
<div>Groesse: ${escapeHtml(item.hall)}</div>
|
||||
`;
|
||||
}
|
||||
return `
|
||||
<div>Kategorie: Snack</div>
|
||||
<div>Variante: ${escapeHtml(item.time)}</div>
|
||||
<div>Groesse: ${escapeHtml(item.hall)}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
interface CartGroup {
|
||||
key: string;
|
||||
quantity: number;
|
||||
total: number;
|
||||
item: CartItem;
|
||||
}
|
||||
|
||||
function groupCartItems(): CartGroup[] {
|
||||
const groups = new Map<string, CartGroup>();
|
||||
cart.forEach((item: CartItem) => {
|
||||
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 += item.price;
|
||||
});
|
||||
return Array.from(groups.values());
|
||||
}
|
||||
|
||||
export function renderCart() {
|
||||
const cartList = document.getElementById("cart-items-list");
|
||||
const totalEl = document.getElementById("cart-total-right");
|
||||
const vatEl = document.getElementById("cart-vat-right");
|
||||
|
||||
if (!cartList || !totalEl || !vatEl) return;
|
||||
|
||||
if (!Array.isArray(cart) || cart.length === 0) {
|
||||
cartList.innerHTML = '<p>Dein Warenkorb ist leer.</p>';
|
||||
totalEl.innerText = formatEuro(0);
|
||||
vatEl.innerText = `inkl. 19% MwSt: ${formatEuro(0)}`;
|
||||
return;
|
||||
}
|
||||
|
||||
const groupedItems = groupCartItems();
|
||||
const header = `
|
||||
<div class="cart-header-row">
|
||||
<div class="col-amount">MENGE</div>
|
||||
<div class="col-img">VORSCHAU</div>
|
||||
<div class="col-product">NAME</div>
|
||||
<div class="col-details">INFO</div>
|
||||
<div class="col-price">PREIS</div>
|
||||
<div class="col-action">AKTION</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const rows = groupedItems.map((group: CartGroup) => {
|
||||
const imageHtml = group.item.img
|
||||
? `<img class="cart-img-small" src="${escapeHtml(group.item.img)}" alt="${escapeHtml(group.item.title)}">`
|
||||
: `<div class="cart-img-fallback">Kein Bild</div>`;
|
||||
const quantityHtml = group.item.category === "movie"
|
||||
? `<div class="qty-static" aria-label="Feste Ticketanzahl">${group.quantity}x</div>`
|
||||
: `
|
||||
<div class="qty-stepper">
|
||||
<button class="btn-qty" data-action="minus" data-key="${escapeHtml(group.key)}">-</button>
|
||||
<span>${group.quantity}</span>
|
||||
<button class="btn-qty" data-action="plus" data-key="${escapeHtml(group.key)}">+</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return `
|
||||
<div class="cart-item-row">
|
||||
<div class="col-amount">${quantityHtml}</div>
|
||||
<div class="col-img">${imageHtml}</div>
|
||||
<div class="col-product">${escapeHtml(group.item.title)}</div>
|
||||
<div class="col-details cart-item-info">${buildItemInfo(group.item)}</div>
|
||||
<div class="col-price">${formatEuro(group.total)}</div>
|
||||
<div class="col-action">
|
||||
<button class="btn-delete-item" data-key="${escapeHtml(group.key)}" aria-label="Eintrag entfernen"><span aria-hidden="true">🗑️</span></button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join("");
|
||||
|
||||
cartList.innerHTML = header + rows;
|
||||
|
||||
const total = cart.reduce((sum: number, item: CartItem) => sum + item.price, 0);
|
||||
const vat = total - total / 1.19;
|
||||
totalEl.innerText = formatEuro(total);
|
||||
vatEl.innerText = `inkl. 19% MwSt: ${formatEuro(vat)}`;
|
||||
}
|
||||
|
||||
window.addEventListener("cart-updated", renderCart);
|
||||
renderCart();
|
||||
|
||||
document.getElementById("cart-view")?.addEventListener("click", (event: Event) => {
|
||||
const target = event.target as HTMLElement;
|
||||
const deleteBtn = target.closest(".btn-delete-item") as HTMLElement | null;
|
||||
if (deleteBtn) {
|
||||
const key = deleteBtn.dataset.key;
|
||||
const newCart = cart.filter((item: CartItem) => buildCartKey(item) !== key);
|
||||
updateCart(newCart);
|
||||
window.dispatchEvent(new CustomEvent("cart-updated"));
|
||||
return;
|
||||
}
|
||||
|
||||
const qtyBtn = target.closest(".btn-qty") as HTMLElement | null;
|
||||
if (qtyBtn) {
|
||||
const action = qtyBtn.dataset.action;
|
||||
const key = qtyBtn.dataset.key;
|
||||
if (action === "plus") {
|
||||
const item = cart.find((i: CartItem) => buildCartKey(i) === key);
|
||||
if (item) cart.push({ ...item, id: Date.now() + Math.random() });
|
||||
} else {
|
||||
const idx = cart.findIndex((i: CartItem) => buildCartKey(i) === key);
|
||||
if (idx !== -1) cart.splice(idx, 1);
|
||||
}
|
||||
updateCart(cart);
|
||||
window.dispatchEvent(new CustomEvent("cart-updated"));
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("btn-checkout-final")?.addEventListener("click", () => {
|
||||
if (!cart.length) {
|
||||
alert("Dein Warenkorb ist leer.");
|
||||
return;
|
||||
}
|
||||
window.location.href = "/checkout";
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<section id="checkout-view" class="hidden" style="padding: 40px 20px;">
|
||||
<section id="checkout-view" style="padding: 40px 20px;">
|
||||
<div class="checkout-container">
|
||||
<div class="progress-bar">
|
||||
<div class="step active" id="step-1-indicator">1</div>
|
||||
@@ -50,3 +50,190 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
import { currentUser, users, cart, emptyCart, occupiedSeatsData, updateCart, updateOccupiedSeats } from "../scripts/bigConstants";
|
||||
import type { CartItem, MovieCartItem } from "../scripts/bigConstants";
|
||||
|
||||
function formatCheckoutEuro(value: number) {
|
||||
return `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`;
|
||||
}
|
||||
|
||||
let selectedPaymentMethod = "";
|
||||
|
||||
function setCheckoutStep(step: number) {
|
||||
const step1 = document.getElementById("checkout-step-1");
|
||||
const step2 = document.getElementById("checkout-step-2");
|
||||
const step3 = document.getElementById("checkout-step-3");
|
||||
step1?.classList.toggle("hidden", step !== 1);
|
||||
step2?.classList.toggle("hidden", step !== 2);
|
||||
step3?.classList.toggle("hidden", step !== 3);
|
||||
|
||||
const line1 = document.getElementById("line-1");
|
||||
const line2 = document.getElementById("line-2");
|
||||
const indicator1 = document.getElementById("step-1-indicator");
|
||||
const indicator2 = document.getElementById("step-2-indicator");
|
||||
const indicator3 = document.getElementById("step-3-indicator");
|
||||
|
||||
indicator1?.classList.add("active");
|
||||
indicator2?.classList.toggle("active", step >= 2);
|
||||
indicator3?.classList.toggle("active", step >= 3);
|
||||
line1?.classList.toggle("active", step >= 2);
|
||||
line2?.classList.toggle("active", step >= 3);
|
||||
}
|
||||
|
||||
function renderCheckout() {
|
||||
const summaryList = document.getElementById("checkout-summary-list");
|
||||
const totalDisplay = document.getElementById("checkout-total-display");
|
||||
const vatDisplay = document.getElementById("checkout-vat-display");
|
||||
const nextButton = document.getElementById("btn-next-step-2");
|
||||
|
||||
if (!summaryList) return;
|
||||
summaryList.innerHTML = "";
|
||||
|
||||
const total = cart.reduce((sum: number, item: CartItem) => sum + item.price, 0);
|
||||
const vat = total - total / 1.19;
|
||||
|
||||
cart.forEach((item: CartItem) => {
|
||||
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 as MovieCartItem).seatId} | ${item.hall} | ${item.time} Uhr`
|
||||
: `${item.time || "Standard"} | ${item.hall}`;
|
||||
row.innerHTML = `<span>${item.title} (${infoText})</span><span>${formatCheckoutEuro(item.price)}</span>`;
|
||||
summaryList.appendChild(row);
|
||||
});
|
||||
|
||||
if (totalDisplay) totalDisplay.innerText = `Gesamtbetrag: ${formatCheckoutEuro(total)}`;
|
||||
if (vatDisplay) vatDisplay.innerText = `inkl. 19% MwSt: ${formatCheckoutEuro(vat)}`;
|
||||
|
||||
selectedPaymentMethod = "";
|
||||
document.querySelectorAll(".payment-method").forEach((method) => method.classList.remove("selected"));
|
||||
nextButton?.classList.add("hidden");
|
||||
setCheckoutStep(1);
|
||||
}
|
||||
|
||||
function generateTicket() {
|
||||
const ticketContainer = document.getElementById("ticket-container");
|
||||
if (!ticketContainer) return;
|
||||
|
||||
const moviesInCart = cart.filter((item: CartItem): item is MovieCartItem => item.category === "movie");
|
||||
if (!moviesInCart.length) {
|
||||
ticketContainer.innerHTML = "<p>Danke für deinen Einkauf!</p>";
|
||||
return;
|
||||
}
|
||||
|
||||
const mainMovie: MovieCartItem = moviesInCart[0];
|
||||
const matchingMovieSeats = moviesInCart
|
||||
.filter((item: MovieCartItem) => item.title === mainMovie.title && item.time === mainMovie.time)
|
||||
.map((item: MovieCartItem) => item.seatId)
|
||||
.join(", ");
|
||||
|
||||
const qrData = encodeURIComponent(`EAGLE-IMAX|${mainMovie.title}|${mainMovie.hall}|${matchingMovieSeats}`);
|
||||
const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=${qrData}&bgcolor=ffffff`;
|
||||
|
||||
ticketContainer.innerHTML = `
|
||||
<div class="luxury-ticket">
|
||||
<div class="ticket-left">
|
||||
<img src="${mainMovie.img || mainMovie.poster}" class="ticket-poster" alt="${mainMovie.title}">
|
||||
</div>
|
||||
<div class="ticket-right">
|
||||
<div class="ticket-brand">EAGLE'S IMAX PREMIUM</div>
|
||||
<h2 class="ticket-title">${mainMovie.title}</h2>
|
||||
<div class="ticket-details">
|
||||
<p><span>SAAL</span> <strong>${mainMovie.hall}</strong></p>
|
||||
<p><span>ZEIT</span> <strong>${mainMovie.time} Uhr</strong></p>
|
||||
<p><span>SITZE</span> <strong>${matchingMovieSeats || "-"}</strong></p>
|
||||
</div>
|
||||
<div class="ticket-footer">
|
||||
<img src="${qrUrl}" class="ticket-qr" alt="QR Code">
|
||||
<div class="ticket-code">#${Math.floor(Math.random() * 90000) + 10000}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function completeCheckout() {
|
||||
const orderItems: CartItem[] = [...cart];
|
||||
const orderTotal = orderItems.reduce((sum: number, item: CartItem) => sum + item.price, 0);
|
||||
|
||||
if (currentUser && Array.isArray(users)) {
|
||||
const userIndex = users.findIndex((entry: any) => entry.email === (currentUser as any).email);
|
||||
if (userIndex !== -1) {
|
||||
if (!Array.isArray(users[userIndex].orders)) users[userIndex].orders = [];
|
||||
users[userIndex].orders.push({
|
||||
date: new Date().toLocaleString("de-DE"),
|
||||
items: orderItems,
|
||||
total: orderTotal,
|
||||
paymentMethod: selectedPaymentMethod || "-"
|
||||
});
|
||||
localStorage.setItem("eagleUsers", JSON.stringify(users));
|
||||
}
|
||||
}
|
||||
|
||||
orderItems
|
||||
.filter((item: CartItem): item is MovieCartItem => item.category === "movie")
|
||||
.forEach((item: MovieCartItem) => {
|
||||
const key = `${item.hall}-${item.time}`;
|
||||
if (!occupiedSeatsData[key]) occupiedSeatsData[key] = [];
|
||||
occupiedSeatsData[key].push(item.seatId);
|
||||
});
|
||||
updateOccupiedSeats(occupiedSeatsData);
|
||||
|
||||
emptyCart();
|
||||
window.dispatchEvent(new CustomEvent("cart-updated"));
|
||||
}
|
||||
|
||||
function bindCheckoutEvents() {
|
||||
const nextButton = document.getElementById("btn-next-step-2");
|
||||
const backButton = document.getElementById("btn-back-to-step1");
|
||||
const payNowButton = document.getElementById("btn-pay-now") as HTMLButtonElement;
|
||||
|
||||
document.querySelectorAll(".payment-method").forEach((method) => {
|
||||
method.addEventListener("click", () => {
|
||||
document.querySelectorAll(".payment-method").forEach((entry) => entry.classList.remove("selected"));
|
||||
method.classList.add("selected");
|
||||
selectedPaymentMethod = (method as HTMLElement).dataset.method || "";
|
||||
nextButton?.classList.remove("hidden");
|
||||
});
|
||||
});
|
||||
|
||||
nextButton?.addEventListener("click", () => {
|
||||
if (!selectedPaymentMethod) {
|
||||
alert("Bitte wähle zuerst eine Zahlungsmethode aus.");
|
||||
return;
|
||||
}
|
||||
setCheckoutStep(2);
|
||||
});
|
||||
|
||||
backButton?.addEventListener("click", () => setCheckoutStep(1));
|
||||
|
||||
payNowButton?.addEventListener("click", () => {
|
||||
if (!cart.length) {
|
||||
alert("Dein Warenkorb ist leer.");
|
||||
return;
|
||||
}
|
||||
payNowButton.disabled = true;
|
||||
payNowButton.innerText = "Verarbeite...";
|
||||
payNowButton.style.opacity = "0.7";
|
||||
setTimeout(() => {
|
||||
setCheckoutStep(3);
|
||||
generateTicket();
|
||||
completeCheckout();
|
||||
payNowButton.disabled = false;
|
||||
payNowButton.innerText = "Jetzt Bezahlen";
|
||||
payNowButton.style.opacity = "1";
|
||||
}, 1200);
|
||||
});
|
||||
|
||||
document.getElementById("btn-back-home")?.addEventListener("click", () => {
|
||||
window.location.href = "/";
|
||||
});
|
||||
}
|
||||
|
||||
if (document.getElementById("checkout-view")) {
|
||||
renderCheckout();
|
||||
bindCheckoutEvents();
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<section id="collectors-view" class="hidden info-view">
|
||||
<section id="collectors-view" class="info-view">
|
||||
<div class="container info-view-shell">
|
||||
<button class="subpage-back-btn" data-go-home type="button">← Zur Startseite</button>
|
||||
<h1>Collectors Popcorn Specials</h1>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<section id="dbox-view" class="hidden info-view">
|
||||
<section id="dbox-view" class="info-view">
|
||||
<div class="container info-view-shell">
|
||||
<button class="subpage-back-btn" data-go-home type="button">← Zur Startseite</button>
|
||||
<h1>D-BOX & Technik</h1>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<section id="halls-view" class="hidden info-view">
|
||||
<section id="halls-view" class="info-view">
|
||||
<div class="container info-view-shell">
|
||||
<button class="subpage-back-btn" data-go-home type="button">← Zur Startseite</button>
|
||||
<h1>Unsere Säle</h1>
|
||||
|
||||
@@ -8,3 +8,77 @@
|
||||
<div id="hero-dots" class="hero-dots"></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<script>
|
||||
import { movieCatalog } from "../scripts/bigConstants";
|
||||
|
||||
const ui = {
|
||||
heroSlider: document.getElementById("hero-slider"),
|
||||
heroDots: document.getElementById("hero-dots"),
|
||||
heroTitle: document.getElementById("hero-title"),
|
||||
heroText: document.getElementById("hero-text"),
|
||||
heroBookingBtn: document.getElementById("hero-booking-btn"),
|
||||
};
|
||||
|
||||
let heroItems = movieCatalog.slice(0, 5);
|
||||
let heroIndex = 0;
|
||||
let heroTimer: any = null;
|
||||
|
||||
const escapeHtml = (value: string) => String(value || "")
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
|
||||
const setHeroSlide = (index: number) => {
|
||||
if (!heroItems.length || !ui.heroSlider) return;
|
||||
heroIndex = (index + heroItems.length) % heroItems.length;
|
||||
|
||||
ui.heroSlider.querySelectorAll(".hero-slide").forEach((slide, slideIndex) => {
|
||||
slide.classList.toggle("active", slideIndex === heroIndex);
|
||||
});
|
||||
|
||||
ui.heroDots?.querySelectorAll(".hero-dot").forEach((dot, dotIndex) => {
|
||||
dot.classList.toggle("active", dotIndex === heroIndex);
|
||||
});
|
||||
|
||||
const activeMovie = heroItems[heroIndex];
|
||||
if (ui.heroTitle) ui.heroTitle.textContent = activeMovie.title;
|
||||
if (ui.heroText) ui.heroText.textContent = `${activeMovie.genre} • ${activeMovie.duration} Min. • Heute erste Vorstellung um 13:00 Uhr.`;
|
||||
};
|
||||
|
||||
const renderHero = () => {
|
||||
if (!ui.heroSlider || !heroItems.length) return;
|
||||
|
||||
ui.heroSlider.innerHTML = heroItems.map((movie: any, index: number) => `
|
||||
<div class="hero-slide ${index === 0 ? "active" : ""}" style="background-image: linear-gradient(118deg, rgba(0,0,0,0.34), rgba(0,0,0,0.04)), url('${escapeHtml(movie.backdrop || movie.poster)}');"></div>
|
||||
`).join("");
|
||||
|
||||
if (ui.heroDots) {
|
||||
ui.heroDots.innerHTML = heroItems.map((_: any, index: number) => `
|
||||
<button type="button" class="hero-dot ${index === 0 ? "active" : ""}" data-hero-index="${index}"></button>
|
||||
`).join("");
|
||||
|
||||
ui.heroDots.addEventListener("click", (event: any) => {
|
||||
const dot = (event.target as HTMLElement).closest(".hero-dot") as HTMLElement;
|
||||
if (!dot) return;
|
||||
setHeroSlide(Number(dot.dataset.heroIndex || 0));
|
||||
if (heroTimer) {
|
||||
clearInterval(heroTimer);
|
||||
heroTimer = setInterval(() => setHeroSlide(heroIndex + 1), 6500);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setHeroSlide(0);
|
||||
if (heroTimer) clearInterval(heroTimer);
|
||||
heroTimer = setInterval(() => setHeroSlide(heroIndex + 1), 6500);
|
||||
};
|
||||
|
||||
ui.heroBookingBtn?.addEventListener("click", () => {
|
||||
window.location.href = "/movies";
|
||||
});
|
||||
|
||||
renderHero();
|
||||
</script>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<span>Kino 1</span>
|
||||
<span>Kino 2</span>
|
||||
</div>
|
||||
<button type="button" class="story-more-btn" data-home-view-open="halls-view">Mehr erfahren</button>
|
||||
<a href="/halls" class="story-more-btn">Mehr erfahren</a>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<div>Spider Man</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="story-more-btn" data-home-view-open="dbox-view">Mehr erfahren</button>
|
||||
<a href="/dbox" class="story-more-btn">Mehr erfahren</a>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
@@ -51,8 +51,68 @@
|
||||
<div class="inline-content">
|
||||
<h3>Collectors Popcorn Specials</h3>
|
||||
<p>Präsentiere Sonderbecher und Eimer filmbezogen mit Bild, Logo und kurzem Text in einer lebendigen Timeline.</p>
|
||||
<button type="button" class="story-more-btn" data-home-view-open="collectors-view">Mehr erfahren</button>
|
||||
<a href="/collectors" class="story-more-btn">Mehr erfahren</a>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
import { movieCatalog } from "../scripts/bigConstants";
|
||||
|
||||
const nowRunningRow = document.getElementById("now-running-row");
|
||||
|
||||
const escapeHtml = (value: string) => String(value || "")
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
|
||||
const renderNowRunningRow = () => {
|
||||
if (!nowRunningRow) return;
|
||||
|
||||
nowRunningRow.innerHTML = movieCatalog.map((movie: any, index: number) => `
|
||||
<article class="running-poster">
|
||||
<img src="${escapeHtml(movie.poster)}" alt="${escapeHtml(movie.title)}">
|
||||
<div class="running-meta">
|
||||
<h4>${escapeHtml(movie.title)}</h4>
|
||||
<p>${escapeHtml(movie.genre)}</p>
|
||||
<button type="button" class="open-program-btn" data-program-index="${index}">Spielzeiten ansehen</button>
|
||||
<div>AHHH ICH HAB SCHMERZEN BITTE BRINGT MICH ENDLICH UM</div>
|
||||
</div>
|
||||
</article>
|
||||
`).join("");
|
||||
|
||||
nowRunningRow.addEventListener("click", (event: any) => {
|
||||
const trigger = event.target.closest(".open-program-btn");
|
||||
if (!trigger) return;
|
||||
const programIndex = trigger.dataset.programIndex;
|
||||
window.location.href = `/movies?focus=${programIndex}`;
|
||||
});
|
||||
};
|
||||
|
||||
const initRevealAnimations = () => {
|
||||
const revealElements = Array.from(document.querySelectorAll(".reveal-on-scroll"));
|
||||
if (!revealElements.length) return;
|
||||
|
||||
if (!("IntersectionObserver" in window)) {
|
||||
revealElements.forEach((element) => element.classList.add("is-visible"));
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver((entries, obs) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add("is-visible");
|
||||
obs.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, { threshold: 0.2 });
|
||||
|
||||
revealElements.forEach((element) => observer.observe(element));
|
||||
};
|
||||
|
||||
renderNowRunningRow();
|
||||
initRevealAnimations();
|
||||
</script>
|
||||
|
||||
@@ -1,19 +1,264 @@
|
||||
---
|
||||
import {
|
||||
timePatterns,
|
||||
hallRotation,
|
||||
weekdayShort,
|
||||
type MovieInterface,
|
||||
type ITMDBResponse,
|
||||
type ITMDBMovie,
|
||||
} from "../scripts/bigConstants";
|
||||
|
||||
async function getTopMovies(): Promise<MovieInterface[]> {
|
||||
const API_KEY = import.meta.env.TMDB_API_KEY;
|
||||
console.log("Fetching with Key:", API_KEY ? "Key found" : "KEY MISSING!");
|
||||
|
||||
const IMAGE_BASE_URL = "https://image.tmdb.org/t/p/w500";
|
||||
|
||||
// 1. Corrected "discover" spelling
|
||||
const response = await fetch(
|
||||
`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&language=de-DE&sort_by=popularity.desc`,
|
||||
);
|
||||
console.log("Response Status:", response.status);
|
||||
|
||||
const data: ITMDBResponse = await response.json();
|
||||
console.log("Results found:", data.results?.length);
|
||||
|
||||
if (!data.results) return [];
|
||||
|
||||
return (data.results as ITMDBMovie[]).map((movie: ITMDBMovie) => ({
|
||||
id: movie.id,
|
||||
title: movie.title || "Unknown Title",
|
||||
poster: movie.poster_path
|
||||
? `${IMAGE_BASE_URL}${movie.poster_path}`
|
||||
: "/placeholder.jpg",
|
||||
rating: movie.vote_average || 0,
|
||||
// Add optional chaining (?.) and a fallback
|
||||
year: movie.release_date?.split("-")[0] || "N/A",
|
||||
genre: "Movie", // Discover doesn't provide the name, only an ID
|
||||
duration: 120, // Discover doesn't provide duration
|
||||
fsk: "12",
|
||||
description: movie.overview || "No description available.",
|
||||
backdrop: movie.backdrop_path
|
||||
? `${IMAGE_BASE_URL}${movie.backdrop_path}`
|
||||
: "/placeholder.jpg",
|
||||
}));
|
||||
}
|
||||
|
||||
const formatDateShort = (dateObj: Date) => {
|
||||
const day = String(dateObj.getDate()).padStart(2, "0");
|
||||
const month = String(dateObj.getMonth() + 1).padStart(2, "0");
|
||||
return `${day}.${month}.`;
|
||||
};
|
||||
|
||||
const buildDayMeta = (offset: number) => {
|
||||
const date = new Date();
|
||||
date.setHours(0, 0, 0, 0);
|
||||
date.setDate(date.getDate() + offset);
|
||||
|
||||
const weekday = weekdayShort[date.getDay()];
|
||||
const formattedDate = formatDateShort(date);
|
||||
|
||||
if (offset === 0)
|
||||
return {
|
||||
offset,
|
||||
date,
|
||||
short: "Heute",
|
||||
long: `Heute, ${formattedDate}`,
|
||||
};
|
||||
if (offset === 1)
|
||||
return {
|
||||
offset,
|
||||
date,
|
||||
short: "Morgen",
|
||||
long: `Morgen, ${formattedDate}`,
|
||||
};
|
||||
return {
|
||||
offset,
|
||||
date,
|
||||
short: weekday,
|
||||
long: `${weekday}, ${formattedDate}`,
|
||||
};
|
||||
};
|
||||
|
||||
const buildScheduleForMovie = (movieIndex: number) => {
|
||||
return Array.from({ length: 7 }, (_, dayOffset) => {
|
||||
const dayMeta = buildDayMeta(dayOffset);
|
||||
const pattern =
|
||||
timePatterns[(movieIndex + dayOffset) % timePatterns.length];
|
||||
const desiredCount = 4 + ((movieIndex + dayOffset) % 2);
|
||||
const showCount = Math.min(pattern.length, desiredCount);
|
||||
|
||||
const showings = pattern
|
||||
.slice(0, showCount)
|
||||
.map((time: string, slotIndex: number) => {
|
||||
const hall =
|
||||
hallRotation[
|
||||
(movieIndex + dayOffset + slotIndex) %
|
||||
hallRotation.length
|
||||
];
|
||||
return { time, hall };
|
||||
});
|
||||
|
||||
return { ...dayMeta, showings };
|
||||
});
|
||||
};
|
||||
|
||||
let movieCatalog = await getTopMovies();
|
||||
const movieProgram = movieCatalog?.map((movie, movieIndex) => ({
|
||||
...movie,
|
||||
schedule: buildScheduleForMovie(movieIndex),
|
||||
}));
|
||||
---
|
||||
|
||||
const api_token = import.meta.env.TMDB_API_TOKEN;
|
||||
|
||||
var api = await fetch("https://api.themoviedb.org/3/movie/11")
|
||||
var request = await api.json
|
||||
|
||||
---
|
||||
|
||||
<section id="movie-list-view" class="hidden">
|
||||
<section id="movie-list-view">
|
||||
<div class="container movie-list-shell">
|
||||
<h1 class="list-title">Aktuelle Filme & Spielzeiten</h1>
|
||||
<p class="list-subtitle">Alle Filme mit 7 Tagen Spielplan. Erste Vorstellung täglich ab 13:00 Uhr.</p>
|
||||
<div>{ api_token }</div>
|
||||
<p class="list-subtitle">
|
||||
Alle Filme mit 7 Tagen Spielplan. Erste Vorstellung täglich ab 13:00
|
||||
Uhr.
|
||||
</p>
|
||||
|
||||
<!-- Movie List. -->
|
||||
<div id="movie-program-list" class="movie-program-list"></div>
|
||||
<div id="movie-program-list" class="movie-program-list">
|
||||
{
|
||||
movieProgram?.map((movie, programIndex) => (
|
||||
<article
|
||||
class="detailed-card program-card"
|
||||
data-program-index={programIndex}
|
||||
data-schedule={JSON.stringify(movie.schedule)}
|
||||
>
|
||||
<div class="card-left">
|
||||
<img src={movie.poster} alt={movie.title} />
|
||||
<span class={`fsk fsk-${movie.fsk}`}>
|
||||
{movie.fsk}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-right">
|
||||
<div class="card-header">
|
||||
<h2>{movie.title}</h2>
|
||||
<span class="duration">
|
||||
{movie.duration} Min. | {movie.genre} | FSK:{" "}
|
||||
{movie.fsk}
|
||||
</span>
|
||||
</div>
|
||||
<p class="description">{movie.description}</p>
|
||||
|
||||
<div class="program-day-tabs">
|
||||
{movie.schedule.map((day, dayIndex) => (
|
||||
<button
|
||||
type="button"
|
||||
class={`program-day-tab ${dayIndex === 0 ? "active" : ""}`}
|
||||
data-program-index={programIndex}
|
||||
data-day-index={dayIndex}
|
||||
>
|
||||
<span>{day.short}</span>
|
||||
<small>
|
||||
{formatDateShort(day.date)}
|
||||
</small>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div class="schedule-container program-schedule-shell">
|
||||
<div class="schedule-header">
|
||||
<span>Tag</span>
|
||||
<span>Kinosaal</span>
|
||||
<span>Uhrzeit</span>
|
||||
</div>
|
||||
<div
|
||||
id={`schedule-body-${programIndex}`}
|
||||
class="program-schedule-body"
|
||||
>
|
||||
{movie.schedule[0].showings.map(
|
||||
(showing) => (
|
||||
<button
|
||||
class="schedule-row time-chip program-time-row"
|
||||
data-movie={movie.title}
|
||||
data-hall={showing.hall}
|
||||
data-time={showing.time}
|
||||
>
|
||||
<span>
|
||||
{movie.schedule[0].long}
|
||||
</span>
|
||||
<span class="hall-pill">
|
||||
{showing.hall}
|
||||
</span>
|
||||
<span class="time-btn">
|
||||
{showing.time}
|
||||
</span>
|
||||
</button>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
import { length } from "astro:schema";
|
||||
|
||||
const movieProgramList = document.getElementById("movie-program-list");
|
||||
|
||||
movieProgramList?.addEventListener("click", (event: any) => {
|
||||
const dayButton = event.target.closest(".program-day-tab");
|
||||
let dayButtonsAll = document.getElementsByClassName("program-day-tab");
|
||||
let dayButtons: any[] = [];
|
||||
//only select the daybuttons for this movie
|
||||
|
||||
for (let i = 0; i < dayButtonsAll.length; i++) {
|
||||
if (dayButtonsAll[i].getAttribute("data-program-index") == dayButton.getAttribute("data-program-index")) {
|
||||
dayButtons.push(dayButtonsAll[i])
|
||||
}
|
||||
}
|
||||
|
||||
if (dayButton) {
|
||||
const programIndex = dayButton.getAttribute("data-program-index");
|
||||
const dayIndex = parseInt(dayButton.getAttribute("data-day-index"));
|
||||
const card = dayButton.closest(".detailed-card");
|
||||
const scheduleData = JSON.parse(card.getAttribute("data-schedule"));
|
||||
const selectedDay = scheduleData[dayIndex];
|
||||
|
||||
// Update the schedule body HTML
|
||||
const body:any = document.getElementById(
|
||||
`schedule-body-${programIndex}`,
|
||||
);
|
||||
body.innerHTML = selectedDay.showings
|
||||
.map(
|
||||
(showing:any) => `
|
||||
<button class="schedule-row time-chip" data-movie="${card.querySelector("h2").innerText}" data-hall="${showing.hall}" data-time="${showing.time}">
|
||||
<span>${selectedDay.long}</span>
|
||||
<span class="hall-pill">${showing.hall}</span>
|
||||
<span class="time-btn">${showing.time}</span>
|
||||
</button>
|
||||
`,
|
||||
)
|
||||
.join("");
|
||||
dayButton.setAttribute("class", dayButton.getAttribute("class") + " active")
|
||||
for (let i = 0; i < dayButtons.length; i++) {
|
||||
// check if current button is the one needed
|
||||
if (dayButtons[i].getAttribute("data-day-index") != dayIndex.toString()) {
|
||||
dayButtons[i].setAttribute("class", "program-day-tab");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
// Handle deep links
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const focusIndex = params.get("focus");
|
||||
if (focusIndex !== null) {
|
||||
const target = document.querySelector(
|
||||
`[data-program-index="${focusIndex}"]`,
|
||||
);
|
||||
if (target) {
|
||||
target.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
target.classList.add("flash-focus");
|
||||
setTimeout(() => target.classList.remove("flash-focus"), 1200);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<nav class="navbar">
|
||||
<div class="logo" id="logo-home" style="cursor:pointer">EAGLE's IMAX</div>
|
||||
<a href="/" class="logo" id="logo-home" style="text-decoration: none; color: inherit;">EAGLE's IMAX</a>
|
||||
<ul class="nav-links">
|
||||
<li><a href="#" id="link-filme">Aktuelle Filme</a></li>
|
||||
<li><a href="#" id="link-snacks">Snacks & Getränke</a></li>
|
||||
<li><a href="#" id="link-about">Über uns</a></li>
|
||||
<li><a href="#" id="link-account">Mein Konto</a></li>
|
||||
<li><a href="/movies" id="link-filme">Aktuelle Filme</a></li>
|
||||
<li><a href="/snacks" id="link-snacks">Snacks & Getränke</a></li>
|
||||
<li><a href="/about" id="link-about">Über uns</a></li>
|
||||
<li><a href="/account" id="link-account">Mein Konto</a></li>
|
||||
<li>
|
||||
<a href="#" id="link-cart" style="position: relative; display: flex; align-items: center; gap: 5px;">
|
||||
<a href="/cart" id="link-cart" style="position: relative; display: flex; align-items: center; gap: 5px;">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="21" r="1"></circle><circle cx="20" cy="21" r="1"></circle><path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path></svg>
|
||||
Warenkorb
|
||||
<span id="cart-badge" class="badge">0</span>
|
||||
@@ -20,3 +20,43 @@
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<script>
|
||||
import { cart } from "../scripts/bigConstants";
|
||||
|
||||
const themeToggle = document.getElementById("theme-toggle");
|
||||
const cartBadge = document.getElementById("cart-badge");
|
||||
|
||||
const updateCartBadge = () => {
|
||||
if (!cartBadge) return;
|
||||
const totalItems = cart.length;
|
||||
cartBadge.textContent = totalItems.toString();
|
||||
cartBadge.style.display = totalItems > 0 ? "flex" : "none";
|
||||
};
|
||||
|
||||
const initThemeToggle = () => {
|
||||
if (!themeToggle) return;
|
||||
const THEME_KEY = "eagleTheme";
|
||||
|
||||
const applyTheme = (theme: string) => {
|
||||
const isLight = theme === "light";
|
||||
document.body.classList.toggle("theme-light", isLight);
|
||||
document.body.classList.toggle("theme-dark", !isLight);
|
||||
themeToggle.classList.toggle("is-light", isLight);
|
||||
localStorage.setItem(THEME_KEY, isLight ? "light" : "dark");
|
||||
};
|
||||
|
||||
const storedTheme = localStorage.getItem(THEME_KEY);
|
||||
applyTheme(storedTheme === "light" ? "light" : "dark");
|
||||
|
||||
themeToggle.addEventListener("click", () => {
|
||||
const nextTheme = document.body.classList.contains("theme-light") ? "dark" : "light";
|
||||
applyTheme(nextTheme);
|
||||
});
|
||||
};
|
||||
|
||||
initThemeToggle();
|
||||
updateCartBadge();
|
||||
|
||||
window.addEventListener("cart-updated", updateCartBadge);
|
||||
</script>
|
||||
|
||||
@@ -8,3 +8,19 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const overlay = document.getElementById("snack-prompt-overlay");
|
||||
const btnYes = document.getElementById("btn-yes-snacks");
|
||||
const btnNo = document.getElementById("btn-no-cart");
|
||||
|
||||
btnYes?.addEventListener("click", () => {
|
||||
overlay?.classList.add("hidden");
|
||||
window.location.href = "/snacks";
|
||||
});
|
||||
|
||||
btnNo?.addEventListener("click", () => {
|
||||
overlay?.classList.add("hidden");
|
||||
window.location.href = "/cart";
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<section id="snacks-view" class="hidden">
|
||||
<section id="snacks-view">
|
||||
<div class="container" style="padding: 120px 8% 50px 8%;">
|
||||
<h1 class="list-title">Snacks & Getränke</h1>
|
||||
|
||||
@@ -447,3 +447,73 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
import { cart, updateCart } from "../scripts/bigConstants";
|
||||
import type { SnackCartItem } from "../scripts/bigConstants";
|
||||
|
||||
const snacksView = document.getElementById("snacks-view");
|
||||
|
||||
snacksView?.addEventListener("click", (event: any) => {
|
||||
const target = event.target as HTMLElement;
|
||||
const sizeChip = target.closest(".size-chip") as HTMLElement;
|
||||
if (!sizeChip) return;
|
||||
|
||||
const snackCard = sizeChip.closest(".snack-card");
|
||||
if (!snackCard) return;
|
||||
|
||||
const snackTitle = (snackCard.querySelector("h3, h2") as HTMLElement)?.innerText || "Snack";
|
||||
const snackImg = (snackCard.querySelector("img") as HTMLImageElement)?.src || "";
|
||||
const priceSpan = sizeChip.querySelector("span") as HTMLElement;
|
||||
const rawPriceText = (priceSpan ? priceSpan.innerText : sizeChip.innerText)
|
||||
.replace("EUR", "").replace("€", "").replace(",", ".").trim();
|
||||
const priceVal = parseFloat(rawPriceText) || 0;
|
||||
const sizeVal = sizeChip.innerText.replace(priceSpan?.innerText || "", "").trim() || "Standard";
|
||||
const activeOption = snackCard.querySelector(".opt-btn.active") as HTMLElement;
|
||||
const variantVal = activeOption ? activeOption.innerText : "Normal";
|
||||
|
||||
const snackItem: SnackCartItem = {
|
||||
id: Date.now() + Math.random(),
|
||||
category: "snack",
|
||||
title: snackTitle,
|
||||
hall: sizeVal,
|
||||
time: variantVal,
|
||||
type: "SNACK",
|
||||
price: priceVal,
|
||||
img: snackImg
|
||||
};
|
||||
cart.push(snackItem);
|
||||
|
||||
updateCart(cart);
|
||||
window.dispatchEvent(new CustomEvent("cart-updated"));
|
||||
|
||||
const originalHtml = sizeChip.innerHTML;
|
||||
sizeChip.innerHTML = "Hinzugefügt!";
|
||||
setTimeout(() => {
|
||||
sizeChip.innerHTML = originalHtml;
|
||||
}, 800);
|
||||
});
|
||||
|
||||
document.querySelectorAll(".tab-btn").forEach((button: any) => {
|
||||
button.addEventListener("click", () => {
|
||||
document.querySelectorAll(".tab-btn").forEach((tab) => tab.classList.remove("active"));
|
||||
button.classList.add("active");
|
||||
|
||||
document.querySelectorAll(".snack-category").forEach((category) => category.classList.add("hidden"));
|
||||
const targetId = button.dataset.target;
|
||||
if (targetId) {
|
||||
document.getElementById(targetId)?.classList.remove("hidden");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Option button handling
|
||||
snacksView?.addEventListener("click", (event: any) => {
|
||||
const target = event.target as HTMLElement;
|
||||
if (target.classList.contains("opt-btn")) {
|
||||
const optionGroup = target.parentElement;
|
||||
optionGroup?.querySelectorAll(".opt-btn").forEach((button: any) => button.classList.remove("active"));
|
||||
target.classList.add("active");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -23,3 +23,33 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const aboutModal = document.getElementById("about-tech-modal");
|
||||
const openButtons = document.querySelectorAll("[data-about-modal-open='about-tech-modal']");
|
||||
const closeButtons = document.querySelectorAll("[data-about-modal-close='about-tech-modal']");
|
||||
|
||||
openButtons.forEach(btn => btn.addEventListener("click", () => {
|
||||
aboutModal?.classList.remove("hidden");
|
||||
document.body.style.overflow = "hidden";
|
||||
}));
|
||||
|
||||
closeButtons.forEach(btn => btn.addEventListener("click", () => {
|
||||
aboutModal?.classList.add("hidden");
|
||||
document.body.style.overflow = "auto";
|
||||
}));
|
||||
|
||||
aboutModal?.addEventListener("click", (event) => {
|
||||
if (event.target === aboutModal) {
|
||||
aboutModal.classList.add("hidden");
|
||||
document.body.style.overflow = "auto";
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape" && aboutModal && !aboutModal.classList.contains("hidden")) {
|
||||
aboutModal.classList.add("hidden");
|
||||
document.body.style.overflow = "auto";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
export default function topbar() {
|
||||
return (
|
||||
<div className="navbar bg-[rgba(29, 29, 31, 0.75)]">
|
||||
<div className="leftButtonArray">Hallo</div>
|
||||
<div className="rightButtonArray"></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
24
src/layouts/BaseLayout.astro
Normal file
24
src/layouts/BaseLayout.astro
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
import Navbar from "../components/Navbar.astro";
|
||||
import "../styles/global.css";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
}
|
||||
|
||||
const { title } = Astro.props;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>{title} | EAGLE's IMAX</title>
|
||||
</head>
|
||||
<body>
|
||||
<Navbar />
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
||||
12
src/pages/about.astro
Normal file
12
src/pages/about.astro
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
import AboutView from "../components/AboutView.astro";
|
||||
import TechModal from "../components/TechModal.astro";
|
||||
---
|
||||
|
||||
<BaseLayout title="Über uns">
|
||||
<main>
|
||||
<AboutView />
|
||||
<TechModal />
|
||||
</main>
|
||||
</BaseLayout>
|
||||
10
src/pages/account.astro
Normal file
10
src/pages/account.astro
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
import AccountView from "../components/AccountView.astro";
|
||||
---
|
||||
|
||||
<BaseLayout title="Mein Konto">
|
||||
<main>
|
||||
<AccountView />
|
||||
</main>
|
||||
</BaseLayout>
|
||||
10
src/pages/cart.astro
Normal file
10
src/pages/cart.astro
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
import CartView from "../components/CartView.astro";
|
||||
---
|
||||
|
||||
<BaseLayout title="Warenkorb">
|
||||
<main>
|
||||
<CartView />
|
||||
</main>
|
||||
</BaseLayout>
|
||||
10
src/pages/checkout.astro
Normal file
10
src/pages/checkout.astro
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
import CheckoutView from "../components/CheckoutView.astro";
|
||||
---
|
||||
|
||||
<BaseLayout title="Checkout">
|
||||
<main>
|
||||
<CheckoutView />
|
||||
</main>
|
||||
</BaseLayout>
|
||||
10
src/pages/collectors.astro
Normal file
10
src/pages/collectors.astro
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
import CollectorsView from "../components/CollectorsView.astro";
|
||||
---
|
||||
|
||||
<BaseLayout title="Sammler-Editionen">
|
||||
<main>
|
||||
<CollectorsView />
|
||||
</main>
|
||||
</BaseLayout>
|
||||
10
src/pages/dbox.astro
Normal file
10
src/pages/dbox.astro
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
import DboxView from "../components/DboxView.astro";
|
||||
---
|
||||
|
||||
<BaseLayout title="D-BOX Experience">
|
||||
<main>
|
||||
<DboxView />
|
||||
</main>
|
||||
</BaseLayout>
|
||||
10
src/pages/halls.astro
Normal file
10
src/pages/halls.astro
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
import HallsView from "../components/HallsView.astro";
|
||||
---
|
||||
|
||||
<BaseLayout title="Unsere Kinosäle">
|
||||
<main>
|
||||
<HallsView />
|
||||
</main>
|
||||
</BaseLayout>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user