5 Commits

136 changed files with 3860 additions and 18090 deletions

View File

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

4
.gitattributes vendored
View File

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

View File

@@ -1,52 +0,0 @@
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

25
.gitignore vendored
View File

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

View File

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

11
.vscode/launch.json vendored
View File

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

View File

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

450
account.js Normal file
View File

@@ -0,0 +1,450 @@
function readStorageJson(key, fallbackValue) {
const raw = localStorage.getItem(key);
if (!raw || raw === "undefined" || raw === "null") {
return fallbackValue;
}
try {
return JSON.parse(raw);
} catch (error) {
console.warn(`Konnte LocalStorage-Wert fuer ${key} nicht lesen.`, error);
return fallbackValue;
}
}
function normalizeUser(user) {
if (!user || typeof user !== "object") {
return null;
}
return {
firstName: user.firstName || "",
lastName: user.lastName || "",
email: user.email || "",
password: user.password || "",
orders: Array.isArray(user.orders) ? user.orders : [],
paymentMethods: Array.isArray(user.paymentMethods) ? user.paymentMethods : []
};
}
function escapeHtml(value) {
return String(value || "")
.replaceAll("&", "&")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
function formatEuro(value) {
return `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`;
}
function persistUsers() {
localStorage.setItem("eagleUsers", JSON.stringify(users));
}
function persistCurrentUser() {
if (currentUser) {
localStorage.setItem("currentUser", JSON.stringify(currentUser));
} else {
localStorage.removeItem("currentUser");
}
}
let users = readStorageJson("eagleUsers", []);
if (!Array.isArray(users)) {
users = [];
}
users = users.map(normalizeUser).filter(Boolean);
let currentUser = normalizeUser(readStorageJson("currentUser", null));
if (currentUser && currentUser.email) {
const storedMatch = users.find((user) => user.email === currentUser.email);
if (storedMatch) {
currentUser = storedMatch;
} else {
users.push(currentUser);
persistUsers();
}
}
function registerUser() {
const firstName = document.getElementById("reg-firstname")?.value.trim() || "";
const lastName = document.getElementById("reg-lastname")?.value.trim() || "";
const email = (document.getElementById("reg-email")?.value.trim() || "").toLowerCase();
const password = document.getElementById("reg-password")?.value || "";
if (!firstName || !lastName || !email || !password) {
alert("Bitte fuelle alle Felder aus.");
return;
}
if (!email.includes("@")) {
alert("Bitte gib eine gueltige E-Mail-Adresse ein.");
return;
}
const existingUser = users.find((user) => user.email.toLowerCase() === email);
if (existingUser) {
alert("E-Mail bereits registriert");
return;
}
const newUser = {
firstName,
lastName,
email,
password,
orders: [],
paymentMethods: []
};
users.push(newUser);
currentUser = newUser;
persistUsers();
persistCurrentUser();
alert("Registrierung erfolgreich");
document.getElementById("register-modal")?.classList.add("hidden");
openAccountDashboard();
}
function loginUser() {
const email = (document.getElementById("login-email")?.value.trim() || "").toLowerCase();
const password = document.getElementById("login-password")?.value || "";
const user = users.find(
(entry) => entry.email.toLowerCase() === email && entry.password === password
);
if (!user) {
document.getElementById("login-error")?.classList.remove("hidden");
return;
}
currentUser = user;
persistCurrentUser();
openAccountDashboard();
}
function openAccountDashboard() {
const accountView = document.getElementById("account-view");
if (!accountView) {
return;
}
if (!currentUser) {
accountView.innerHTML = "<div class='account-login-box'><h2>Mein Konto</h2><p>Bitte melde dich an oder registriere dich.</p></div>";
return;
}
accountView.innerHTML = `
<div class="account-panel">
<div class="account-panel-header">
<h2>Mein Konto</h2>
<button class="account-logout-btn" onclick="logoutUser()">Abmelden</button>
</div>
<div class="account-tabs">
<button class="account-tab-btn" onclick="renderPersonalInfo()">Persönliche Daten</button>
<button class="account-tab-btn" onclick="renderOrders()">Meine Bestellungen</button>
<button class="account-tab-btn" onclick="renderPayments()">Zahlungsmethoden</button>
</div>
<div id="account-tab-content"></div>
</div>
`;
renderPersonalInfo();
}
function renderPersonalInfo() {
const target = document.getElementById("account-tab-content");
if (!target || !currentUser) {
return;
}
target.innerHTML = `
<div class="account-card">
<p><strong>Vorname:</strong> ${currentUser.firstName || "-"}</p>
<p><strong>Nachname:</strong> ${currentUser.lastName || "-"}</p>
<p><strong>E-Mail:</strong> ${currentUser.email || "-"}</p>
</div>
`;
}
function renderOrders() {
const target = document.getElementById("account-tab-content");
if (!target || !currentUser) {
return;
}
const orders = Array.isArray(currentUser.orders) ? currentUser.orders : [];
if (!orders.length) {
target.innerHTML = `
<div class="account-card">
<h3>Meine Bestellungen</h3>
<p>Noch keine Bestellungen vorhanden.</p>
</div>
`;
return;
}
const orderHtml = orders
.map((order, index) => {
const movieItems = Array.isArray(order.items)
? order.items.filter((item) => item.category === "movie")
: [];
const previewItem = movieItems[0] || (Array.isArray(order.items) ? order.items[0] : null);
const previewTitle = previewItem?.title || "Bestellung";
const ticketsCount = movieItems.length || (Array.isArray(order.items) ? order.items.length : 0);
return `
<button type="button" class="order-box order-item-btn" data-order-index="${index}">
<div class="order-item-head">
<h4>${escapeHtml(previewTitle)}</h4>
<span>${formatEuro(order.total || 0)}</span>
</div>
<p><strong>Datum:</strong> ${escapeHtml(order.date || "-")}</p>
<p><strong>Anzahl:</strong> ${ticketsCount}x</p>
</button>
`;
})
.join("");
target.innerHTML = `
<div class="account-orders-shell">
<h3>Meine Bestellungen</h3>
<p class="account-payments-note">Klicke auf eine Bestellung, um dein Ticket-Detail zu sehen.</p>
<div class="account-orders-grid">${orderHtml}</div>
<div id="order-ticket-details" class="order-ticket-details hidden"></div>
</div>
`;
const detailTarget = document.getElementById("order-ticket-details");
const orderButtons = Array.from(target.querySelectorAll(".order-item-btn"));
const renderOrderTicket = (orderIndex) => {
const order = orders[orderIndex];
if (!order || !detailTarget) {
return;
}
const movieItems = Array.isArray(order.items)
? order.items.filter((item) => item.category === "movie")
: [];
const primaryMovie = movieItems[0] || (Array.isArray(order.items) ? order.items[0] : null);
const poster = primaryMovie?.img || "";
const seats = movieItems.map((item) => item.seatId).filter(Boolean).join(", ") || "-";
const ticketCount = movieItems.length || (Array.isArray(order.items) ? order.items.length : 0);
const hall = primaryMovie?.hall || "-";
const time = primaryMovie?.time ? `${primaryMovie.time} Uhr` : "-";
detailTarget.innerHTML = `
<article class="order-ticket-card">
<div class="order-ticket-poster">
${poster
? `<img src="${escapeHtml(poster)}" alt="${escapeHtml(primaryMovie?.title || "Film")}">`
: `<div class="order-ticket-poster-fallback">Kein Poster</div>`}
</div>
<div class="order-ticket-content">
<div class="order-ticket-brand">EAGLE'S IMAX | Bestell-Details</div>
<h4>${escapeHtml(primaryMovie?.title || "Bestellung")}</h4>
<div class="order-ticket-grid">
<p><span>Datum</span><strong>${escapeHtml(order.date || "-")}</strong></p>
<p><span>Saal</span><strong>${escapeHtml(hall)}</strong></p>
<p><span>Uhrzeit</span><strong>${escapeHtml(time)}</strong></p>
<p><span>Tickets</span><strong>${ticketCount}x</strong></p>
<p><span>Sitze</span><strong>${escapeHtml(seats)}</strong></p>
<p><span>Gesamt</span><strong>${formatEuro(order.total || 0)}</strong></p>
</div>
</div>
</article>
`;
detailTarget.classList.remove("hidden");
orderButtons.forEach((button) => {
button.classList.toggle("active", Number(button.dataset.orderIndex) === orderIndex);
});
};
orderButtons.forEach((button) => {
button.addEventListener("click", () => {
const orderIndex = Number(button.dataset.orderIndex || -1);
if (orderIndex >= 0) {
renderOrderTicket(orderIndex);
}
});
});
}
function renderPayments() {
const target = document.getElementById("account-tab-content");
if (!target || !currentUser) {
return;
}
target.innerHTML = `
<div class="account-card">
<h3>Zahlungsmethoden</h3>
<p class="account-payments-note">Platzhalter zum Hinterlegen deiner Logos oder Anbieter-Informationen.</p>
<div class="account-payment-grid">
<button type="button" class="account-payment-card account-pay-trigger" data-pay-modal="pay-modal-card">
<div class="payment-logo-slot">
<img src="img/mastercard.png" alt="Mastercard">
</div>
<h4>Visa / Mastercard</h4>
<p>Karteninformationen hinterlegen</p>
</button>
<button type="button" class="account-payment-card account-pay-trigger" data-pay-modal="pay-modal-paypal">
<div class="payment-logo-slot">
<img src="img/paypal.png" alt="PayPal">
</div>
<h4>PayPal</h4>
<p>Konto verbinden</p>
</button>
<button type="button" class="account-payment-card account-pay-trigger" data-pay-modal="pay-modal-apple">
<div class="payment-logo-slot">
<img src="img/applepay.png" alt="Apple Pay">
</div>
<h4>Apple Pay</h4>
<p>Geraet freischalten</p>
</button>
<button type="button" class="account-payment-card account-pay-trigger" data-pay-modal="pay-modal-google">
<div class="payment-logo-slot">
<img src="img/googlepay.png" alt="Google Pay">
</div>
<h4>Google Pay</h4>
<p>Wallet verknuepfen</p>
</button>
</div>
</div>
<div id="pay-modal-card" class="pay-modal-overlay hidden">
<div class="pay-modal-panel pay-modal-card-style">
<button type="button" class="pay-close-btn" data-pay-close>&times;</button>
<div class="pay-modal-head">
<img src="img/mastercard.png" alt="Kreditkarte">
<h4>Kreditkarte hinterlegen</h4>
</div>
<div class="pay-form-grid">
<label>Kartennummer
<input type="text" placeholder="1234 5678 9012 3456">
</label>
<div class="pay-form-row">
<label>Exp. Datum
<input type="text" placeholder="MM/JJ">
</label>
<label>CVV
<input type="text" placeholder="123">
</label>
</div>
<label>Name auf Karte
<input type="text" placeholder="Max Mustermann">
</label>
</div>
<button type="button" class="pay-submit-btn">Karte speichern</button>
</div>
</div>
<div id="pay-modal-paypal" class="pay-modal-overlay hidden">
<div class="pay-modal-panel pay-modal-paypal-style">
<button type="button" class="pay-close-btn" data-pay-close>&times;</button>
<div class="pay-modal-head">
<img src="img/paypal.png" alt="PayPal">
<h4>PayPal verbinden</h4>
</div>
<p>Einloggen und dein PayPal-Konto mit deinem Kino-Account verbinden.</p>
<label>E-Mail
<input type="email" placeholder="name@beispiel.de">
</label>
<label>Passwort
<input type="password" placeholder="Passwort">
</label>
<button type="button" class="pay-submit-btn paypal-btn">Mit PayPal fortfahren</button>
</div>
</div>
<div id="pay-modal-apple" class="pay-modal-overlay hidden">
<div class="pay-modal-panel pay-modal-apple-style">
<button type="button" class="pay-close-btn" data-pay-close>&times;</button>
<div class="pay-modal-head">
<img src="img/applepay.png" alt="Apple Pay">
<h4>Apple Pay einrichten</h4>
</div>
<p>Apple Pay wirkt schlicht, klar und fokussiert. Hinterlege hier die bevorzugte Karte für schnelle Zahlungen.</p>
<label>Apple-ID E-Mail
<input type="email" placeholder="appleid@beispiel.de">
</label>
<label>Bevorzugte Karte
<input type="text" placeholder="Visa, Mastercard, ...">
</label>
<button type="button" class="pay-submit-btn apple-btn">Zu Wallet hinzufügen</button>
</div>
</div>
<div id="pay-modal-google" class="pay-modal-overlay hidden">
<div class="pay-modal-panel pay-modal-google-style">
<button type="button" class="pay-close-btn" data-pay-close>&times;</button>
<div class="pay-modal-head">
<img src="img/googlepay.png" alt="Google Pay">
<h4>Google Pay einrichten</h4>
</div>
<p>Verbinde deine Wallet, damit zukünftige Bestellungen in wenigen Klicks abgeschlossen werden.</p>
<label>Google-Konto
<input type="email" placeholder="konto@gmail.com">
</label>
<label>Standard-Zahlungsquelle
<input type="text" placeholder="z. B. Visa 1234">
</label>
<button type="button" class="pay-submit-btn google-btn">Wallet verbinden</button>
</div>
</div>
`;
const modals = Array.from(target.querySelectorAll(".pay-modal-overlay"));
const triggers = Array.from(target.querySelectorAll(".account-pay-trigger"));
const closeButtons = Array.from(target.querySelectorAll("[data-pay-close]"));
const closeAllPaymentModals = () => {
modals.forEach((modal) => modal.classList.add("hidden"));
document.body.style.overflow = "auto";
};
triggers.forEach((trigger) => {
trigger.addEventListener("click", () => {
closeAllPaymentModals();
const targetId = trigger.getAttribute("data-pay-modal");
const modal = targetId ? target.querySelector(`#${targetId}`) : null;
if (modal) {
modal.classList.remove("hidden");
document.body.style.overflow = "hidden";
}
});
});
closeButtons.forEach((button) => {
button.addEventListener("click", closeAllPaymentModals);
});
modals.forEach((modal) => {
modal.addEventListener("click", (event) => {
if (event.target === modal) {
closeAllPaymentModals();
}
});
});
}
function logoutUser() {
currentUser = null;
persistCurrentUser();
window.location.reload();
}

View File

@@ -1,24 +0,0 @@
/* 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"})
}
}
});

352
booking.js Normal file
View File

@@ -0,0 +1,352 @@
let currentBookingContext = null;
let currentHallLayout = null;
function openBooking(movie, hall, time) {
const titleEl = document.getElementById("modal-movie-title");
const infoEl = document.getElementById("modal-info-text");
if (titleEl) {
titleEl.innerText = movie;
}
if (infoEl) {
infoEl.innerText = `${hall}${time} Uhr`;
}
currentBookingContext = { movie, hall, time };
createSeats(hall, time);
renderBookingLegend();
updateBookingSummary();
document.getElementById("booking-modal")?.classList.remove("hidden");
}
function getRowLabel(rowIndex) {
return String(rowIndex + 1);
}
function buildHallLayout(hallName, baseConfig) {
const rows = Number(baseConfig.rows || 0);
const totalCols = Number(baseConfig.left || 0) + Number(baseConfig.right || 0);
const isDeluxe = /deluxe/i.test(hallName);
const left = isDeluxe
? Math.max(3, Number(baseConfig.left || 0) - 1)
: Number(baseConfig.left || 0);
const right = Math.max(0, totalCols - left);
const vipRows = rows > 0 ? [rows] : [];
const dboxMap = new Set();
const markDboxRange = (rowNumber, startCol, width) => {
if (!rowNumber || width <= 0) {
return;
}
const maxCol = Math.min(totalCols, startCol + width - 1);
for (let col = startCol; col <= maxCol; col++) {
dboxMap.add(`${rowNumber}-${col}`);
}
};
if (isDeluxe) {
const configuredDboxSeats = Array.isArray(baseConfig.dbox)
? baseConfig.dbox.reduce((sum, section) => sum + Number(section.w || 0), 0)
: 0;
const totalDboxSeats = Math.max(4, configuredDboxSeats || 0);
const firstRow = Math.max(1, rows - 2);
const secondRow = Math.max(1, rows - 1);
const targetRows = [firstRow, secondRow]
.filter((rowNumber, index, arr) => arr.indexOf(rowNumber) === index)
.filter((rowNumber) => !vipRows.includes(rowNumber));
const rowCount = Math.max(1, targetRows.length);
const seatsPerFirstRows = Math.ceil(totalDboxSeats / rowCount);
let remaining = totalDboxSeats;
targetRows.forEach((rowNumber, index) => {
const seatsForRow = index === targetRows.length - 1
? remaining
: Math.min(seatsPerFirstRows, remaining);
remaining -= seatsForRow;
const startCol = left + Math.max(1, Math.floor((right - seatsForRow) / 2) + 1);
markDboxRange(rowNumber, startCol, seatsForRow);
});
} else if (Array.isArray(baseConfig.dbox)) {
baseConfig.dbox.forEach((section) => {
const rowNumber = Number(section.r || 0);
const width = Number(section.w || 0);
const startCol = Number(section.c || 0);
markDboxRange(rowNumber, startCol, width);
});
}
return {
rows,
left,
right,
totalCols,
vipRows,
dboxMap,
isImax: Boolean(baseConfig.isImax)
};
}
function getSeatType(layout, rowNumber, colNumber) {
if (layout.dboxMap.has(`${rowNumber}-${colNumber}`)) {
return "dbox";
}
if (layout.vipRows.includes(rowNumber)) {
return "vip";
}
if (layout.isImax) {
return "imax";
}
return "normal";
}
function createSeatElement({ seatId, seatType, occupiedSeats }) {
const seat = document.createElement("button");
seat.type = "button";
seat.classList.add("seat", seatType);
seat.dataset.seatId = seatId;
seat.dataset.type = seatType;
seat.title = `${seatId} (${seatType.toUpperCase()})`;
if (occupiedSeats.has(seatId)) {
seat.classList.add("occupied");
seat.disabled = true;
seat.setAttribute("aria-label", `${seatId} belegt`);
return seat;
}
seat.setAttribute("aria-label", `${seatId} frei`);
seat.addEventListener("click", () => {
seat.classList.toggle("selected");
updateBookingSummary();
});
return seat;
}
function createSeats(hallName, time) {
const seatGrid = document.getElementById("seat-grid");
if (!seatGrid) {
return;
}
seatGrid.innerHTML = "";
const baseConfig = seatLayouts[hallName];
if (!baseConfig) {
currentHallLayout = null;
return;
}
currentHallLayout = buildHallLayout(hallName, baseConfig);
const occupiedKey = `${hallName}-${time}`;
const occupiedSeats = new Set(Array.isArray(occupiedSeatsData?.[occupiedKey]) ? occupiedSeatsData[occupiedKey] : []);
for (let rowIndex = 0; rowIndex < currentHallLayout.rows; rowIndex++) {
const rowNumber = rowIndex + 1;
const rowLabel = getRowLabel(rowIndex);
const perspectiveFactor = (currentHallLayout.rows - rowNumber) / Math.max(currentHallLayout.rows - 1, 1);
const rowIndent = Math.round(18 * perspectiveFactor);
const row = document.createElement("div");
row.className = "seat-row cinema-row";
row.style.setProperty("--row-indent", `${rowIndent}px`);
const leftLabel = document.createElement("div");
leftLabel.className = "row-label";
leftLabel.textContent = rowLabel;
const rightLabel = document.createElement("div");
rightLabel.className = "row-label row-label-right";
rightLabel.textContent = rowLabel;
const leftBlock = document.createElement("div");
leftBlock.className = "row-seat-block left-block";
const rightBlock = document.createElement("div");
rightBlock.className = "row-seat-block right-block";
for (let col = 1; col <= currentHallLayout.totalCols; col++) {
const seatId = `R${rowNumber}-P${col}`;
const seatType = getSeatType(currentHallLayout, rowNumber, col);
const seat = createSeatElement({ seatId, seatType, occupiedSeats });
if (col <= currentHallLayout.left) {
leftBlock.appendChild(seat);
} else {
rightBlock.appendChild(seat);
}
}
const aisle = document.createElement("div");
aisle.className = "aisle-gap";
row.append(leftLabel, leftBlock, aisle, rightBlock, rightLabel);
seatGrid.appendChild(row);
}
}
function renderBookingLegend() {
const legend = document.getElementById("dynamic-legend");
if (!legend || !currentHallLayout) {
return;
}
const legendItems = [
{ type: "normal", label: "Standard" },
{ type: "selected", label: "Ausgewählt" },
{ type: "occupied", label: "Belegt" }
];
if (currentHallLayout.isImax) {
legendItems.unshift({ type: "imax", label: "IMAX" });
}
if (currentHallLayout.vipRows.length > 0) {
legendItems.unshift({ type: "vip", label: "VIP" });
}
if (currentHallLayout.dboxMap.size > 0) {
legendItems.unshift({ type: "dbox", label: "D-BOX" });
}
legend.innerHTML = legendItems
.map((item) => `
<div class="item">
<span class="seat ${item.type}"></span>
<span>${item.label}</span>
</div>
`)
.join("");
}
function updateBookingSummary() {
const selectedSeats = Array.from(document.querySelectorAll("#seat-grid .seat.selected"));
const summaryPanel = document.getElementById("booking-summary");
const summaryItems = document.getElementById("summary-items");
const totalEl = document.getElementById("total-price");
let total = 0;
if (summaryItems) {
summaryItems.innerHTML = selectedSeats
.map((seat) => {
const type = seat.dataset.type || "normal";
const seatPrice = Number(prices?.[type] ?? prices?.normal ?? 11);
total += seatPrice;
return `
<div class="summary-row">
<span>${seat.dataset.seatId}</span>
<span>${seatPrice.toFixed(2).replace(".", ",")} EUR</span>
</div>
`;
})
.join("");
} else {
selectedSeats.forEach((seat) => {
const type = seat.dataset.type || "normal";
const seatPrice = Number(prices?.[type] ?? prices?.normal ?? 11);
total += seatPrice;
});
}
if (totalEl) {
totalEl.innerText = `${total.toFixed(2).replace(".", ",")} EUR`;
}
summaryPanel?.classList.toggle("hidden", selectedSeats.length === 0);
}
function findMoviePoster(movieTitle) {
const cards = Array.from(document.querySelectorAll(".movie-card, .detailed-card"));
const normalizedTarget = String(movieTitle || "").trim().toLowerCase();
for (const card of cards) {
const title = card.querySelector("h2, h3")?.innerText?.trim().toLowerCase();
if (title === normalizedTarget) {
const imageSrc = card.querySelector("img")?.src;
if (imageSrc) {
return imageSrc;
}
}
}
return "";
}
function confirmSelectedSeats() {
const selectedSeats = Array.from(document.querySelectorAll("#seat-grid .seat.selected"));
if (!currentBookingContext || selectedSeats.length === 0) {
alert("Bitte waehle mindestens einen Platz aus.");
return;
}
const moviePoster = findMoviePoster(currentBookingContext.movie);
const addedSeats = [];
selectedSeats.forEach((seat) => {
const seatId = seat.dataset.seatId;
const seatType = seat.dataset.type || "normal";
const alreadyInCart = cart.some((item) =>
item.category === "movie" &&
item.title === currentBookingContext.movie &&
item.hall === currentBookingContext.hall &&
item.time === currentBookingContext.time &&
item.seatId === seatId
);
if (alreadyInCart) {
return;
}
cart.push({
id: Date.now() + Math.random(),
category: "movie",
title: currentBookingContext.movie,
hall: currentBookingContext.hall,
time: currentBookingContext.time,
seatId,
type: seatType.toUpperCase(),
price: Number(prices?.[seatType] ?? prices?.normal ?? 11),
img: moviePoster
});
addedSeats.push(seatId);
});
if (!addedSeats.length) {
alert("Diese Plaetze sind bereits im Warenkorb.");
return;
}
saveCart?.();
renderCart?.();
renderCheckout?.();
document.getElementById("booking-modal")?.classList.add("hidden");
const snackOverlay = document.getElementById("snack-prompt-overlay");
snackOverlay?.classList.remove("hidden");
document.body.style.overflow = "hidden";
}
document.addEventListener("DOMContentLoaded", () => {
document.getElementById("btn-confirm-seats")?.addEventListener("click", confirmSelectedSeats);
});

199
cart.js Normal file
View File

@@ -0,0 +1,199 @@
function formatEuro(value) {
return `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`;
}
function escapeHtml(value) {
return String(value || "")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
function buildCartKey(item) {
const infoText = item.category === "movie"
? `Sitz: ${item.seatId} (${item.hall})`
: item.time;
return `${item.title}-${item.hall}-${infoText}`;
}
function isDrinkItem(item) {
if (item.category !== "snack") {
return false;
}
const title = String(item.title || "").toLowerCase();
const size = String(item.hall || "").toLowerCase();
const drinkKeywords = [
"cola",
"sprite",
"fanta",
"mezzo",
"fuze",
"wasser",
"getraenk",
"drink"
];
return drinkKeywords.some((word) => title.includes(word)) || size.includes("l");
}
function buildItemInfo(item) {
if (item.category === "movie") {
return `
<div>Sitzplatz: ${escapeHtml(item.seatId || "-")}</div>
<div>Saal: ${escapeHtml(item.hall || "-")}</div>
<div>Uhrzeit: ${escapeHtml(item.time || "-")} Uhr</div>
`;
}
if (isDrinkItem(item)) {
return `
<div>Variante: ${escapeHtml(item.time || "-")}</div>
<div>Groesse: ${escapeHtml(item.hall || "-")}</div>
`;
}
return `
<div>Kategorie: Snack</div>
<div>Variante: ${escapeHtml(item.time || "-")}</div>
<div>Groesse: ${escapeHtml(item.hall || "-")}</div>
`;
}
function groupCartItems() {
const groups = new Map();
cart.forEach((item) => {
const key = buildCartKey(item);
if (!groups.has(key)) {
groups.set(key, {
key,
quantity: 0,
total: 0,
item
});
}
const group = groups.get(key);
group.quantity += 1;
group.total += Number(item.price || 0);
});
return Array.from(groups.values());
}
function saveCart() {
localStorage.setItem("eagleCart", JSON.stringify(cart));
updateCartBadge();
}
function updateCartBadge() {
const cartBadge = document.getElementById("cart-badge");
if (!cartBadge) {
return;
}
cartBadge.innerText = cart.length;
cartBadge.classList.toggle("hidden", cart.length === 0);
}
function renderCart() {
const cartList = document.getElementById("cart-items-list");
const totalEl = document.getElementById("cart-total-right");
const vatEl = document.getElementById("cart-vat-right");
if (!cartList || !totalEl || !vatEl) {
return;
}
if (!Array.isArray(cart) || cart.length === 0) {
cartList.innerHTML = '<p>Dein Warenkorb ist leer.</p>';
totalEl.innerText = formatEuro(0);
vatEl.innerText = `inkl. 19% MwSt: ${formatEuro(0)}`;
return;
}
const groupedItems = groupCartItems();
const header = `
<div class="cart-header-row">
<div class="col-amount">MENGE</div>
<div class="col-img">VORSCHAU</div>
<div class="col-product">NAME</div>
<div class="col-details">INFO</div>
<div class="col-price">PREIS</div>
<div class="col-action">AKTION</div>
</div>
`;
const rows = groupedItems
.map((group) => {
const imageHtml = group.item.img
? `<img class="cart-img-small" src="${escapeHtml(group.item.img)}" alt="${escapeHtml(group.item.title)}">`
: `<div class="cart-img-fallback">Kein Bild</div>`;
const quantityHtml = group.item.category === "movie"
? `<div class="qty-static" aria-label="Feste Ticketanzahl">${group.quantity}x</div>`
: `
<div class="qty-stepper">
<button class="btn-qty" data-action="minus" data-key="${escapeHtml(group.key)}">-</button>
<span>${group.quantity}</span>
<button class="btn-qty" data-action="plus" data-key="${escapeHtml(group.key)}">+</button>
</div>
`;
return `
<div class="cart-item-row">
<div class="col-amount">
${quantityHtml}
</div>
<div class="col-img">${imageHtml}</div>
<div class="col-product">${escapeHtml(group.item.title)}</div>
<div class="col-details cart-item-info">${buildItemInfo(group.item)}</div>
<div class="col-price">${formatEuro(group.total)}</div>
<div class="col-action">
<button class="btn-delete-item" data-key="${escapeHtml(group.key)}" aria-label="Eintrag entfernen"><span aria-hidden="true">🗑️</span></button>
</div>
</div>
`;
})
.join("");
cartList.innerHTML = header + rows;
const total = cart.reduce((sum, item) => sum + Number(item.price || 0), 0);
const vat = total - total / 1.19;
totalEl.innerText = formatEuro(total);
vatEl.innerText = `inkl. 19% MwSt: ${formatEuro(vat)}`;
saveCart();
}
window.removeItem = function removeItem(id) {
cart = cart.filter((item) => item.id !== id);
saveCart();
renderCart();
};
window.changeQty = function changeQty(title, delta) {
if (delta > 0) {
const item = cart.find((entry) => entry.title === title);
if (item) {
cart.push({ ...item, id: Date.now() + Math.random() });
}
} else {
const index = cart
.map((entry) => entry.title)
.lastIndexOf(title);
if (index !== -1) {
cart.splice(index, 1);
}
}
saveCart();
renderCart();
};

232
checkout.js Normal file
View File

@@ -0,0 +1,232 @@
function formatCheckoutEuro(value) {
return `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`;
}
let selectedPaymentMethod = "";
let checkoutEventsBound = false;
function setCheckoutStep(step) {
const step1 = document.getElementById("checkout-step-1");
const step2 = document.getElementById("checkout-step-2");
const step3 = document.getElementById("checkout-step-3");
step1?.classList.toggle("hidden", step !== 1);
step2?.classList.toggle("hidden", step !== 2);
step3?.classList.toggle("hidden", step !== 3);
const line1 = document.getElementById("line-1");
const line2 = document.getElementById("line-2");
const indicator1 = document.getElementById("step-1-indicator");
const indicator2 = document.getElementById("step-2-indicator");
const indicator3 = document.getElementById("step-3-indicator");
indicator1?.classList.add("active");
indicator2?.classList.toggle("active", step >= 2);
indicator3?.classList.toggle("active", step >= 3);
line1?.classList.toggle("active", step >= 2);
line2?.classList.toggle("active", step >= 3);
}
function renderCheckout() {
const summaryList = document.getElementById("checkout-summary-list");
const totalDisplay = document.getElementById("checkout-total-display");
const vatDisplay = document.getElementById("checkout-vat-display");
const nextButton = document.getElementById("btn-next-step-2");
if (!summaryList) {
return;
}
summaryList.innerHTML = "";
const safeCart = Array.isArray(cart) ? cart : [];
const total = safeCart.reduce((sum, item) => sum + Number(item.price || 0), 0);
const vat = total - total / 1.19;
safeCart.forEach((item) => {
const row = document.createElement("div");
row.style.cssText = "display:flex; justify-content:space-between; gap:12px; margin-bottom:10px; font-size:0.95rem;";
const infoText = item.category === "movie"
? `Sitz ${item.seatId || "-"} | ${item.hall || "-"} | ${item.time || "-"} Uhr`
: `${item.time || "Standard"} | ${item.hall || "-"}`;
row.innerHTML = `<span>${item.title} (${infoText})</span><span>${formatCheckoutEuro(item.price)}</span>`;
summaryList.appendChild(row);
});
if (totalDisplay) {
totalDisplay.innerText = `Gesamtbetrag: ${formatCheckoutEuro(total)}`;
}
if (vatDisplay) {
vatDisplay.innerText = `inkl. 19% MwSt: ${formatCheckoutEuro(vat)}`;
}
selectedPaymentMethod = "";
document.querySelectorAll(".payment-method").forEach((method) => {
method.classList.remove("selected");
});
nextButton?.classList.add("hidden");
setCheckoutStep(1);
}
function generateTicket() {
const ticketContainer = document.getElementById("ticket-container");
if (!ticketContainer) {
return;
}
const moviesInCart = (Array.isArray(cart) ? cart : []).filter((item) => item.category === "movie");
if (!moviesInCart.length) {
ticketContainer.innerHTML = "<p>Danke fuer deinen Einkauf!</p>";
return;
}
const mainMovie = moviesInCart[0];
const matchingMovieSeats = moviesInCart
.filter((item) => item.title === mainMovie.title && item.time === mainMovie.time)
.map((item) => item.seatId)
.join(", ");
const qrData = encodeURIComponent(`EAGLE-IMAX|${mainMovie.title}|${mainMovie.hall}|${matchingMovieSeats}`);
const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=${qrData}&bgcolor=ffffff`;
ticketContainer.innerHTML = `
<div class="luxury-ticket">
<div class="ticket-left">
<img src="${mainMovie.img}" class="ticket-poster" alt="${mainMovie.title}">
</div>
<div class="ticket-right">
<div class="ticket-brand">EAGLE'S IMAX PREMIUM</div>
<h2 class="ticket-title">${mainMovie.title}</h2>
<div class="ticket-details">
<p><span>SAAL</span> <strong>${mainMovie.hall}</strong></p>
<p><span>ZEIT</span> <strong>${mainMovie.time} Uhr</strong></p>
<p><span>SITZE</span> <strong>${matchingMovieSeats || "-"}</strong></p>
</div>
<div class="ticket-footer">
<img src="${qrUrl}" class="ticket-qr" alt="QR Code">
<div class="ticket-code">#${Math.floor(Math.random() * 90000) + 10000}</div>
</div>
</div>
</div>
`;
}
function saveOrderForCurrentUser(orderItems, orderTotal) {
if (typeof currentUser === "undefined" || !currentUser) {
return;
}
if (typeof users === "undefined" || !Array.isArray(users)) {
return;
}
const order = {
date: new Date().toLocaleString("de-DE"),
items: orderItems,
total: orderTotal,
paymentMethod: selectedPaymentMethod || "-"
};
const userIndex = users.findIndex((entry) => entry.email === currentUser.email);
if (userIndex === -1) {
return;
}
if (!Array.isArray(users[userIndex].orders)) {
users[userIndex].orders = [];
}
users[userIndex].orders.push(order);
localStorage.setItem("eagleUsers", JSON.stringify(users));
}
function reserveSeatsAfterPayment(orderItems) {
const movieItems = orderItems.filter((item) => item.category === "movie");
movieItems.forEach((item) => {
const key = `${item.hall}-${item.time}`;
if (!occupiedSeatsData[key]) {
occupiedSeatsData[key] = [];
}
occupiedSeatsData[key].push(item.seatId);
});
localStorage.setItem("eagleOccupied", JSON.stringify(occupiedSeatsData));
}
function completeCheckout() {
const orderItems = Array.isArray(cart) ? [...cart] : [];
const orderTotal = orderItems.reduce((sum, item) => sum + Number(item.price || 0), 0);
saveOrderForCurrentUser(orderItems, orderTotal);
reserveSeatsAfterPayment(orderItems);
cart = [];
saveCart?.();
renderCart?.();
}
function bindCheckoutEvents() {
if (checkoutEventsBound) {
return;
}
checkoutEventsBound = true;
const nextButton = document.getElementById("btn-next-step-2");
const backButton = document.getElementById("btn-back-to-step1");
const payNowButton = document.getElementById("btn-pay-now");
document.querySelectorAll(".payment-method").forEach((method) => {
method.addEventListener("click", () => {
document.querySelectorAll(".payment-method").forEach((entry) => {
entry.classList.remove("selected");
});
method.classList.add("selected");
selectedPaymentMethod = method.dataset.method || "";
nextButton?.classList.remove("hidden");
});
});
nextButton?.addEventListener("click", () => {
if (!selectedPaymentMethod) {
alert("Bitte waehle zuerst eine Zahlungsmethode aus.");
return;
}
setCheckoutStep(2);
});
backButton?.addEventListener("click", () => {
setCheckoutStep(1);
});
payNowButton?.addEventListener("click", () => {
if (!Array.isArray(cart) || !cart.length) {
alert("Dein Warenkorb ist leer.");
return;
}
payNowButton.disabled = true;
payNowButton.innerText = "Verarbeite...";
payNowButton.style.opacity = "0.7";
setTimeout(() => {
setCheckoutStep(3);
generateTicket();
completeCheckout();
payNowButton.disabled = false;
payNowButton.innerText = "Jetzt Bezahlen";
payNowButton.style.opacity = "1";
}, 1200);
});
}
document.addEventListener("DOMContentLoaded", bindCheckoutEvents);

View File

@@ -1,9 +0,0 @@
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/astronaut-rockypopcorn.jpg LFS Normal file

Binary file not shown.

BIN
img/cashtruck.jpg LFS Normal file

Binary file not shown.

BIN
img/fsk-0.png LFS Normal file

Binary file not shown.

BIN
img/fsk-12.png LFS Normal file

Binary file not shown.

BIN
img/fsk-16.png LFS Normal file

Binary file not shown.

BIN
img/fsk-18.png LFS Normal file

Binary file not shown.

BIN
img/fsk-6.png LFS Normal file

Binary file not shown.

BIN
img/nachokombigross.png LFS Normal file

Binary file not shown.

BIN
img/nachokombimittel.png LFS Normal file

Binary file not shown.

BIN
img/popcornkombigross.png LFS Normal file

Binary file not shown.

BIN
img/popcornkombiklein.png LFS Normal file

Binary file not shown.

BIN
img/popcornkombimittel.png LFS Normal file

Binary file not shown.

BIN
img/screammetalpopcorn.png LFS Normal file

Binary file not shown.

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

Binary file not shown.

View File

@@ -51,7 +51,14 @@
<h3>Jetzt läuft</h3> <h3>Jetzt läuft</h3>
<span>Heute im Fokus</span> <span>Heute im Fokus</span>
</div> </div>
<div id="now-running-row" class="now-running-row"></div> <div id="now-running-shell" class="now-running-shell is-collapsed">
<div id="now-running-row" class="now-running-row"></div>
<div class="now-running-fade">
<button id="now-running-toggle" class="now-running-toggle" type="button" aria-expanded="false" aria-label="Weitere Filme anzeigen">
<span>&gt;</span>
</button>
</div>
</div>
</div> </div>
<div class="home-inline-showcase reveal-on-scroll"> <div class="home-inline-showcase reveal-on-scroll">
@@ -267,15 +274,238 @@
<h1 class="list-title">Snacks & Getränke</h1> <h1 class="list-title">Snacks & Getränke</h1>
<div class="category-tabs"> <div class="category-tabs">
<button class="tab-btn active" data-target="cat-getraenke">Getränke</button> <button class="tab-btn active" data-target="cat-limited">Limitierte Specials</button>
<button class="tab-btn" data-target="cat-getraenke">Getränke</button>
<button class="tab-btn" data-target="cat-popcorn">Popcorn</button> <button class="tab-btn" data-target="cat-popcorn">Popcorn</button>
<button class="tab-btn" data-target="cat-nachos">Nachos</button> <button class="tab-btn" data-target="cat-nachos">Nachos</button>
<button class="tab-btn" data-target="cat-snacks">Snacks</button> <button class="tab-btn" data-target="cat-snacks">Snacks</button>
<button class="tab-btn" data-target="cat-kombi">Kombi</button>
<button class="tab-btn" data-target="cat-eis">Eis</button> <button class="tab-btn" data-target="cat-eis">Eis</button>
</div> </div>
<div id="cat-getraenke" class="snack-category active"> <div id="cat-limited" class="snack-category active limited-specials-category">
<div class="limited-specials-hero">
<div>
<span class="limited-kicker">Nur für kurze Zeit</span>
<h2>Limitierte Specials</h2>
<p>Filmbecher, Sammler-Eimer und Kids-Menüs als kleine Vitrine für besondere Aktionen.</p>
</div>
</div>
<div class="limited-special-block">
<div class="special-film-heading">
<img src="img/Zoomania-2.jpg" alt="Zoomania 2 Logo">
<div>
<span>Zoomania 2</span>
<h2>Tiereische Collection</h2>
</div>
</div>
<div class="limited-special-grid">
<div class="snack-card limited-special-card">
<div class="snack-img"><img src="img/zoomania-popcorn.jpg" alt="Zoomania 2 Metallbecher"></div>
<div class="snack-info">
<span class="badge">Metallbecher</span>
<h3>Limitierter Metallbecher - Zoomania 2</h3>
<p class="snack-card-note">Sammlerbecher mit Popcornfüllung, wahlweise süß oder salzig.</p>
<div class="option-group">
<button class="opt-btn active">Süß</button>
<button class="opt-btn">Salzig</button>
</div>
<div class="size-selector">
<button class="size-chip">Special <span>12,00€</span></button>
</div>
</div>
</div>
<div class="snack-card limited-special-card wide-special">
<div class="snack-img"><img src="img/zoomaniakidsmenu.jpg" alt="Zoomania Kids Menu"></div>
<div class="snack-info">
<span class="badge">Kids Special</span>
<h3>Zoomania Kids Menü</h3>
<p class="snack-card-note">0,5L Getränk im Zoomania Becher + Zoomania Popcorn Schale + Figur zum Aussuchen.</p>
<div class="size-selector">
<button class="size-chip">Menü-Preis <span>10,00€</span></button>
</div>
</div>
</div>
</div>
</div>
<div class="limited-special-block">
<div class="special-film-heading">
<img src="img/screamvii.jpg" alt="Scream VII Logo">
<div>
<span>Scream VII</span>
<h2>Horror Collection</h2>
</div>
</div>
<div class="limited-special-grid">
<div class="snack-card limited-special-card">
<div class="snack-img"><img src="img/screamdoorpopcorn.jpg" alt="Scream VII Sammelbecher"></div>
<div class="snack-info">
<span class="badge">Collector</span>
<h3>Limitierter Sammelbecher - Scream VII</h3>
<p class="snack-card-note">Hallo Sydney! Ghostface in der Tür.</p>
<div class="option-group">
<button class="opt-btn active">Süß</button>
<button class="opt-btn">Salzig</button>
</div>
<div class="size-selector">
<button class="size-chip">Special <span>29,00€</span></button>
</div>
</div>
</div>
<div class="snack-card limited-special-card">
<div class="snack-img"><img src="img/screammetalpopcorn.png" alt="Scream VII Sammelbecher"></div>
<div class="snack-info">
<span class="badge">Collector</span>
<h3>Limitierter Metallbecher - Scream VII</h3>
<p class="snack-card-note">Metall Sammelbecher im SCREAM VII Design</p>
<div class="option-group">
<button class="opt-btn active">Süß</button>
<button class="opt-btn">Salzig</button>
</div>
<div class="size-selector">
<button class="size-chip">Special <span>12,00€</span></button>
</div>
</div>
</div>
</div>
</div>
<div class="limited-special-block">
<div class="special-film-heading">
<img src="img/derAustronaut.jpg" alt="Der Austronaut Logo">
<div>
<span>Project Hail Mary</span>
<h2>Space Collection</h2>
</div>
</div>
<div class="limited-special-grid">
<div class="snack-card limited-special-card">
<div class="snack-img"><img src="img/astronautpopcorn.jpg" alt="Der Austronaut Sammelbecher"></div>
<div class="snack-info">
<span class="badge">Space Cup</span>
<h3>Limitierter Sammelbecher - Der Austronaut</h3>
<p class="snack-card-note">Der Helm von Ryland Grace aus "Der Austronaut"</p>
<div class="option-group">
<button class="opt-btn active">Süß</button>
<button class="opt-btn">Salzig</button>
</div>
<div class="size-selector">
<button class="size-chip">Special <span>34,00€</span></button>
</div>
</div>
</div>
<div class="snack-card limited-special-card">
<div class="snack-img"><img src="img/astronaut-rockypopcorn.jpg" alt="Der Austronaut - Rocky"></div>
<div class="snack-info">
<span class="badge">Collector</span>
<h3>Limitierter Sammelbecher - Der Austronaut</h3>
<p class="snack-card-note">Die Kapsel von Rocky - Mit abnehmbarer Rocky Figur</p>
<div class="option-group">
<button class="opt-btn active">Süß</button>
<button class="opt-btn">Salzig</button>
</div>
<div class="size-selector">
<button class="size-chip">Special <span>22,00€</span></button>
</div>
</div>
</div>
</div>
</div>
<div class="limited-special-block">
<div class="special-film-heading">
<img src="img/hoppers.jpg" alt="Hoppers Logo">
<div>
<span>Hoppers</span>
<h2>Biber Specials (Ist das eine Eidechse?)</h2>
</div>
</div>
<div class="limited-special-grid">
<div class="snack-card limited-special-card">
<div class="snack-img"><img src="img/hopperspopcornmetall.jpg" alt="Hoppers Metallbecher"></div>
<div class="snack-info">
<span class="badge">Metallbecher</span>
<h3>Limitierter Metallbecher - Hoppers</h3>
<p class="snack-card-note">Stabiler Becher mit Popcornfüllung.</p>
<div class="option-group">
<button class="opt-btn active">Süß</button>
<button class="opt-btn">Salzig</button>
</div>
<div class="size-selector">
<button class="size-chip">Special <span>12,00€</span></button>
</div>
</div>
</div>
<div class="snack-card limited-special-card">
<div class="snack-img"><img src="img/hopperspopcornwood.png" alt="Hoppers Sammelbecher"></div>
<div class="snack-info">
<span class="badge">Collector</span>
<h3>Limitierter Sammelbecher - Hoppers</h3>
<p class="snack-card-note">Sammlerbecher mit warmem Popcornmoment.</p>
<div class="option-group">
<button class="opt-btn active">Süß</button>
<button class="opt-btn">Salzig</button>
</div>
<div class="size-selector">
<button class="size-chip">Special <span>21,00€</span></button>
</div>
</div>
</div>
<div class="snack-card limited-special-card wide-special">
<div class="snack-img"><img src="img/hopperskidsmenu.jpg" alt="Hoppers Kids Menu"></div>
<div class="snack-info">
<span class="badge">Kids Special</span>
<h3>Hoppers Kids Menü</h3>
<p class="snack-card-note">0,5L Getränk im Hoppers Becher + Hoppers Popcorn Schale + Hoppers Figur.</p>
<div class="size-selector">
<button class="size-chip">Menü-Preis <span>10,00€</span></button>
</div>
</div>
</div>
</div>
</div>
<div class="limited-special-block">
<div class="special-film-heading">
<img src="img/mariogalaxy.jpg" alt="Mario Galaxy Logo">
<div>
<span>Super Mario Galaxy</span>
<h2>Galaxy Collection</h2>
</div>
</div>
<div class="limited-special-grid">
<div class="snack-card limited-special-card">
<div class="snack-img"><img src="img/marioyoshipopcorn.png" alt="Yoshi Sammelbecher"></div>
<div class="snack-info">
<span class="badge">Yoshi Cup</span>
<h3>Limitierter Sammelbecher - Yoshi Becher</h3>
<p class="snack-card-note">Verspielter Sammlerbecher für Mario-Fans.</p>
<div class="option-group">
<button class="opt-btn active">Süß</button>
<button class="opt-btn">Salzig</button>
</div>
<div class="size-selector">
<button class="size-chip">Special <span>35,90€</span></button>
</div>
</div>
</div>
<div class="snack-card limited-special-card wide-special">
<div class="snack-img"><img src="img/mariokidsmenu.png" alt="Mario Kids Menu"></div>
<div class="snack-info">
<span class="badge">Kids Special</span>
<h3>Mario Galaxy Kids Menü</h3>
<p class="snack-card-note">0,5L Getränk im Mario Galaxy Becher + Mario Galaxy Popcorn Schale.</p>
<div class="size-selector">
<button class="size-chip">Menü-Preis <span>10,00€</span></button>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="cat-getraenke" class="snack-category hidden">
<div class="snack-grid"> <div class="snack-grid">
<div class="snack-card"> <div class="snack-card">
@@ -392,229 +622,238 @@
</div> </div> </div> </div>
<div id="cat-popcorn" class="snack-category hidden"> <div id="cat-popcorn" class="snack-category hidden">
<div class="snack-grid"> <div class="snack-subsection">
<div class="snack-card"> <div class="snack-section-heading">
<div class="snack-img"><img src="img/popcorn-klein.png" alt="Popcorn klein"></div> <span>Frisch gepoppt</span>
<div class="snack-info"> <h2>Einzelprodukte</h2>
<h3>Popcorn klein</h3> </div>
<div class="option-group"> <div class="snack-grid">
<button class="opt-btn active">Süß</button> <div class="snack-card">
<button class="opt-btn">Salzig</button> <div class="snack-img"><img src="img/popcorn-klein.png" alt="Popcorn klein"></div>
<div class="snack-info">
<h3>Popcorn klein</h3>
<div class="option-group">
<button class="opt-btn active">Süß</button>
<button class="opt-btn">Salzig</button>
</div>
<div class="size-selector">
<button class="size-chip">Klein <span>3,50€</span></button>
</div>
</div> </div>
<div class="size-selector"> </div>
<button class="size-chip">3,50€</button> <div class="snack-card">
<div class="snack-img"><img src="img/popcorn-mittel.png" alt="Popcorn mittel"></div>
<div class="snack-info">
<h3>Popcorn Mittel</h3>
<div class="option-group">
<button class="opt-btn active">Süß</button>
<button class="opt-btn">Salzig</button>
</div>
<div class="size-selector">
<button class="size-chip">Mittel <span>4,50€</span></button>
</div>
</div>
</div>
<div class="snack-card">
<div class="snack-img"><img src="img/popcorn-big.png" alt="Popcorn groß"></div>
<div class="snack-info">
<h3>Popcorn Groß</h3>
<div class="option-group">
<button class="opt-btn active">Süß</button>
<button class="opt-btn">Salzig</button>
</div>
<div class="size-selector">
<button class="size-chip">Groß <span>6,00€</span></button>
</div>
</div>
</div>
</div>
</div>
<div class="snack-subsection">
<div class="snack-section-heading popcorn-combo-heading">
<span>Für Filmabende</span>
<h2>Kombi Menü</h2>
</div>
<div class="snack-grid">
<div class="snack-card highlight">
<div class="snack-img"><img src="img/popcornkombiklein.png" alt="Popcorn klein - Kombi-Menü"></div>
<div class="snack-info">
<h3>Kleines Menü</h3>
<p class="snack-card-note">0,33L Getränk + Popcorn klein</p>
<div class="size-selector">
<button class="size-chip">Menü-Preis <span>5,00€</span></button>
</div>
</div>
</div>
<div class="snack-card highlight">
<div class="snack-img"><img src="img/popcornkombimittel.png" alt="Popcorn mittel - Kombi-Menü"></div>
<div class="snack-info">
<h3>Mittleres Menü</h3>
<p class="snack-card-note">0,5L Getränk + Popcorn mittel</p>
<div class="size-selector">
<button class="size-chip">Menü-Preis <span>6,50€</span></button>
</div>
</div>
</div>
<div class="snack-card highlight">
<div class="snack-img"><img src="img/popcornkombigross.png" alt="Popcorn groß - Kombi-Menü"></div>
<div class="snack-info">
<span class="badge">Bestseller</span>
<h3>Großes Menü</h3>
<p class="snack-card-note">1L Getränk + Popcorn groß</p>
<div class="size-selector">
<button class="size-chip">Menü-Preis <span>8,00€</span></button>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div>
<div class="snack-card">
<div class="snack-img"><img src="img/popcorn-mittel.png" alt="Popcorn mittel"></div>
<div class="snack-info">
<h3>Popcorn Mittel</h3>
<div class="option-group">
<button class="opt-btn active">Süß</button>
<button class="opt-btn">Salzig</button>
</div>
<div class="size-selector">
<button class="size-chip">4,50€</button>
</div>
</div>
</div>
<div class="snack-card">
<div class="snack-img"><img src="img/popcorn-big.png" alt="Popcorn groß"></div>
<div class="snack-info">
<h3>Popcorn Groß</h3>
<div class="option-group">
<button class="opt-btn active">Süß</button>
<button class="opt-btn">Salzig</button>
</div>
<div class="size-selector">
<button class="size-chip">6,00€</button>
</div>
</div>
</div>
<div class="snack-card">
<div class="snack-img"><img src="img/zoomania-popcorn.jpg" alt="Popcorn limited - zoomania 2"></div>
<div class="snack-info">
<h3>Limitierter Metallbecher - Zoomania 2</h3>
<div class="option-group">
<button class="opt-btn active">Süß</button>
<button class="opt-btn">Salzig</button>
</div>
<div class="size-selector">
<button class="size-chip">12,00€</button>
</div>
</div>
</div>
<div class="snack-card">
<div class="snack-img"><img src="img/screamdoorpopcorn.jpg" alt="Popcorn limited - Scream VII"></div>
<div class="snack-info">
<h3>Limitierter Sammelbecher - Scream VII</h3>
<div class="option-group">
<button class="opt-btn active">Süß</button>
<button class="opt-btn">Salzig</button>
</div>
<div class="size-selector">
<button class="size-chip">29,00€</button>
</div>
</div>
</div>
<div class="snack-card">
<div class="snack-img"><img src="img/astronautpopcorn.jpg" alt="Popcorn limited - Der Austronaut"></div>
<div class="snack-info">
<h3>Limitierter Sammelbecher - Der Austronaut</h3>
<div class="option-group">
<button class="opt-btn active">Süß</button>
<button class="opt-btn">Salzig</button>
</div>
<div class="size-selector">
<button class="size-chip">34,00€</button>
</div>
</div>
</div>
<div class="snack-card">
<div class="snack-img"><img src="img/hopperspopcornmetall.jpg" alt="Popcorn limited - Hoppers"></div>
<div class="snack-info">
<h3>Limitierter Metallbecher - Hoppers</h3>
<div class="option-group">
<button class="opt-btn active">Süß</button>
<button class="opt-btn">Salzig</button>
</div>
<div class="size-selector">
<button class="size-chip">12,00€</button>
</div>
</div>
</div>
<div class="snack-card">
<div class="snack-img"><img src="img/hopperspopcornwood.png" alt="Popcorn limited - Hoppers"></div>
<div class="snack-info">
<h3>Limitierter Sammelbecher - Hoppers</h3>
<div class="option-group">
<button class="opt-btn active">Süß</button>
<button class="opt-btn">Salzig</button>
</div>
<div class="size-selector">
<button class="size-chip">21,00€</button>
</div>
</div>
</div>
<div class="snack-card">
<div class="snack-img"><img src="img/marioyoshipopcorn.png" alt="Popcorn limited - Yoshi"></div>
<div class="snack-info">
<h3>Limitierter Sammelbecher - Yoshi Becher</h3>
<div class="option-group">
<button class="opt-btn active">Süß</button>
<button class="opt-btn">Salzig</button>
</div>
<div class="size-selector">
<button class="size-chip">35,90€</button>
</div>
</div>
</div>
</div>
</div>
<div id="cat-nachos" class="snack-category hidden"> <div id="cat-nachos" class="snack-category hidden">
<div class="snack-grid"> <div class="snack-subsection">
<div class="snack-card"> <div class="snack-section-heading">
<div class="snack-img"><img src="img/nachosnormal.png" alt="Nachos"></div> <span>Nachos</span>
<div class="snack-info"> <h2>Einzelprodukte</h2>
<h3>Nachos Klein</h3> </div>
<div class="option-group"> <div class="snack-grid">
<button class="opt-btn active">Käse-Dip</button> <div class="snack-card">
<button class="opt-btn">Salsa-Dip</button> <div class="snack-img"><img src="img/nachosnormal.png" alt="Nachos"></div>
<div class="snack-info">
<h3>Nachos Klein</h3>
<div class="option-group">
<button class="opt-btn active">Käse-Dip</button>
<button class="opt-btn">Salsa-Dip</button>
</div>
<div class="size-selector">
<button class="size-chip">Klein <span>5,00€</span></button>
</div>
</div> </div>
<div class="size-selector"> </div>
<button class="size-chip">Klein<span>5,00€</span></button> <div class="snack-card">
<div class="snack-img"><img src="img/nachosnormal.png" alt="Nachos"></div>
<div class="snack-info">
<h3>Nachos Normal</h3>
<div class="option-group">
<button class="opt-btn active">Käse-Dip</button>
<button class="opt-btn">Salsa-Dip</button>
</div>
<div class="size-selector">
<button class="size-chip">Mittel <span>6,50€</span></button>
</div>
</div>
</div>
<div class="snack-card">
<div class="snack-img"><img src="img/nachos.jpg" alt="Nachos"></div>
<div class="snack-info">
<h3>Nachos Groß</h3>
<div class="option-group">
<button class="opt-btn active">Käse-Dip</button>
<button class="opt-btn">Sourcreme-Dip</button>
<button class="opt-btn">Salsa-Dip</button>
</div>
<div class="size-selector">
<button class="size-chip">Groß <span>8,00€</span></button>
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="snack-card"> </div>
<div class="snack-img"><img src="img/nachosnormal.png" alt="Nachos"></div>
<div class="snack-info"> <div class="snack-subsection">
<h3>Nachos Normal</h3> <div class="snack-section-heading">
<div class="option-group"> <span>Extra dazu</span>
<button class="opt-btn active">Käse-Dip</button> <h2>Dips</h2>
<button class="opt-btn">Salsa-Dip</button> </div>
<div class="snack-grid">
<div class="snack-card">
<div class="snack-img"><img src="img/käsedip.png" alt="Käse-Dip"></div>
<div class="snack-info">
<h3>Käse-Dip<br>(warm)</h3>
<div class="option-group">
<button class="opt-btn active">Normal</button>
<button class="opt-btn">Scharf</button>
</div>
<div class="size-selector">
<button class="size-chip">Schale <span>2,00€</span></button>
</div>
</div> </div>
<div class="size-selector"> </div>
<button class="size-chip">Mittel<span>6,50€</span></button> <div class="snack-card">
<div class="snack-img"><img src="img/sourdip.png" alt="Sourcreme-Dip"></div>
<div class="snack-info">
<h3>Sourcreme-Dip</h3>
<div class="option-group">
<button class="opt-btn active">Normal</button>
</div>
<div class="size-selector">
<button class="size-chip">Schale <span>2,00€</span></button>
</div>
</div>
</div>
<div class="snack-card">
<div class="snack-img"><img src="img/salsadip.png" alt="Salsa-Dip"></div>
<div class="snack-info">
<h3>Salsa-Dip</h3>
<div class="option-group">
<button class="opt-btn active">Normal</button>
</div>
<div class="size-selector">
<button class="size-chip">Schale <span>2,00€</span></button>
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="snack-card"> </div>
<div class="snack-img"><img src="img/nachos.jpg" alt="Nachos"></div>
<div class="snack-info"> <div class="snack-subsection">
<h3>Nachos Groß</h3> <div class="snack-section-heading">
<div class="option-group"> <span>Alles drin</span>
<button class="opt-btn active">Käse-Dip</button> <h2>Kombi Menü</h2>
<button class="opt-btn">Sourcreme-Dip</button> </div>
<button class="opt-btn">Salsa-Dip</button> <div class="snack-grid">
<div class="snack-card highlight">
<div class="snack-img"><img src="img/nachokombiklein.png" alt="Nacho Kombi Klein"></div>
<div class="snack-info">
<h3>Nacho Menü Klein</h3>
<p class="snack-card-note">Nachos klein + 1 Dip + 1x 0,33L Getränk</p>
<div class="option-group">
<button class="opt-btn active">Klein</button>
</div>
<div class="size-selector">
<button class="size-chip">Kombi <span>6,90€</span></button>
</div>
</div> </div>
<div class="size-selector"> </div>
<button class="size-chip">Groß <span>8,00€</span></button> <div class="snack-card highlight">
<div class="snack-img"><img src="img/nachokombimittel.png" alt="Nacho Kombi Mittel"></div>
<div class="snack-info">
<h3>Nacho Menü Mittel</h3>
<p class="snack-card-note">Nachos mittel + 1 Dip + 1x 0,33L Getränk</p>
<div class="option-group">
<button class="opt-btn active">Mittel</button>
</div>
<div class="size-selector">
<button class="size-chip">Kombi <span>6,90€</span></button>
</div>
</div>
</div>
<div class="snack-card highlight">
<div class="snack-img"><img src="img/nachokombigross.png" alt="Nacho Kombi Groß"></div>
<div class="snack-info">
<h3>Nacho Menü Groß</h3>
<p class="snack-card-note">Nachos groß + 1 Dip + 1x 0,33L Getränk</p>
<div class="option-group">
<button class="opt-btn active">Groß</button>
</div>
<div class="size-selector">
<button class="size-chip">Kombi <span>6,90€</span></button>
</div>
</div> </div>
</div> </div>
</div> </div>
<h2 class="list-title"> </h2>
<br>
<h2 class="list-title">Dips</h2>
<br>
<br>
<br>
<div class="snack-card">
<div class="snack-img"><img src="img/käsedip.png" alt="Käse-Dip"></div>
<div class="snack-info">
<h3>Käse-Dip<br>(warm)</h3>
<div class="option-group">
<button class="opt-btn active">Normal</button>
<button class="opt-btn">Scharf</button>
</div>
<div class="size-selector">
<button class="size-chip">Schale<span>2,00€</span></button>
</div>
</div>
</div>
<div class="snack-card">
<div class="snack-img"><img src="img/sourdip.png" alt="Sourcreme-Dip"></div>
<div class="snack-info">
<h3>Sourcreme-Dip</h3>
<div class="option-group">
<button class="opt-btn active">Normal</button>
</div>
<div class="size-selector">
<button class="size-chip">Schale<span>2,00€</span></button>
</div>
</div>
</div>
<div class="snack-card">
<div class="snack-img"><img src="img/salsadip.png" alt="Salsa-Dip"></div>
<div class="snack-info">
<h3>Salsa-Dip</h3>
<div class="option-group">
<button class="opt-btn active">Normal</button>
</div>
<div class="size-selector">
<button class="size-chip">Schale<span>2,00€</span></button>
</div>
</div>
</div>
<h2 class="list-title">Nacho Kombi-Menüs</h2>
<br>
<br>
</div> </div>
</div> </div>
</div>
<div class="snack-card">
<div class="snack-img"><img src="img/nachokombiklein.png" alt="Nacho Kombi Klein"></div>
<div class="snack-info">
<h3>Nacho Menü Klein - Nachos klein + 1 Dip + 1 0,33L Getränk</h3>
<div class="option-group">
<button class="opt-btn active">Klein</button>
</div>
<div class="size-selector">
<button class="size-chip">Kombi<span>6,90€</span></button>
</div>
</div>
</div>
<div id="cat-snacks" class="snack-category hidden"> <div id="cat-snacks" class="snack-category hidden">
<div class="snack-grid"> <div class="snack-grid">
<div class="snack-card"> <div class="snack-card">
@@ -647,75 +886,6 @@
</div> </div>
</div> </div>
<div id="cat-kombi" class="snack-category hidden">
<div class="snack-grid">
<div class="snack-card highlight">
<div class="snack-info">
<h3>Kleines Menü</h3>
<p style="font-size: 0.8rem; color: #86868b; margin-bottom: 10px;">0,33L Getränk + Popcorn Klein</p>
<div class="size-selector">
<button class="size-chip">Menü-Preis <span>5,00€</span></button>
</div>
</div>
</div>
<div class="snack-card highlight">
<div class="snack-info">
<h3>Mittleres Menü</h3>
<p style="font-size: 0.8rem; color: #86868b; margin-bottom: 10px;">0,5L Getränk + Popcorn Mittel</p>
<div class="size-selector">
<button class="size-chip">Menü-Preis <span>6,50€</span></button>
</div>
</div>
</div>
<div class="snack-card highlight">
<div class="snack-info">
<span class="badge">Bestseller</span>
<h3>Großes Menü</h3>
<p style="font-size: 0.8rem; color: #86868b; margin-bottom: 10px;">1L Getränk + Popcorn Groß</p>
<div class="size-selector">
<button class="size-chip">Menü-Preis <span>8,00€</span></button>
</div>
</div>
</div>
<br>
<div class="snack-card highlight">
<div class="snack-info">
<div class="snack-img"><img src="img/hopperskidsmenu.jpg" alt="Hoppers Kids Menu"></div>
<span class="badge">SPECIAL</span>
<h3>Limitiertes Menü</h3>
<p style="font-size: 0.8rem; color: #86868b; margin-bottom: 10px;">0,5L Getränk im HOPPERS Becher + HOPPERS Popcorn Schale<br>+HOPPERS Figur</p>
<div class="size-selector">
<button class="size-chip">Menü-Preis <span>10,00€</span></button>
</div>
</div>
</div>
<div class="snack-card highlight">
<div class="snack-info">
<div class="snack-img"><img src="img/mariokidsmenu.png" alt="Mario Kids Menu"></div>
<br>
<span class="badge">SPECIAL</span>
<h3>Limitiertes Menü</h3>
<p style="font-size: 0.8rem; color: #86868b; margin-bottom: 10px;">0,5L Getränk im MARIO GALXY Becher + MARIO GALAXY Popcorn Schale</p>
<div class="size-selector">
<button class="size-chip">Menü-Preis <span>10,00€</span></button>
</div>
</div>
</div>
<div class="snack-card highlight">
<div class="snack-info">
<div class="snack-img"><img src="img/zoomaniakidsmenu.jpg" alt="Zoomania Kids Menu"></div>
<br>
<span class="badge">SPECIAL</span>
<h3>Limitiertes Menü</h3>
<p style="font-size: 0.8rem; color: #86868b; margin-bottom: 10px;">0,5L Getränk im ZOOMANIA Becher + ZOOMANIA Popcorn Schale<br>+ Figur zum aussuchen</p>
<div class="size-selector">
<button class="size-chip">Menü-Preis <span>10,00€</span></button>
</div>
</div>
</div>
</div>
</div>
<div id="cat-eis" class="snack-category hidden"> <div id="cat-eis" class="snack-category hidden">
<div class="coming-soon-banner"> <div class="coming-soon-banner">
<h2>Eiscreme & Shakes</h2> <h2>Eiscreme & Shakes</h2>
@@ -735,8 +905,8 @@
<div class="header-sub-info"> <div class="header-sub-info">
<p id="modal-info-text">Saal • Zeit</p> <p id="modal-info-text">Saal • Zeit</p>
<div id="tech-badges" class="tech-badges-container hidden"> <div id="tech-badges" class="tech-badges-container hidden">
<img src="img/Dolby.png" alt="Dolby" class="tech-badge"> <img src="img/dolby.png" alt="Dolby" class="tech-badge">
<img src="img/dbox.jpg" alt="D-Box" class="tech-badge"> <img src="img/dbox.png" alt="D-Box" class="tech-badge">
</div> </div>
</div> </div>
</div> </div>
@@ -948,11 +1118,11 @@
</div> </div>
</div> </div>
<script type="module" src="dist/main.js"></script> <script src="account.js"></script>
<script type="module" src="dist/cart.js"></script> <script src="cart.js"></script>
<script type="module" src="dist/booking.js"></script> <script src="booking.js"></script>
<script type="module" src="dist/checkout.js"></script> <script src="checkout.js"></script>
<script type="module" src="dist/account.js"></script> <script src="main.js"></script>
</body> </body>
</html> </html>

1146
main.js Normal file

File diff suppressed because it is too large Load Diff

11345
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,38 +0,0 @@
{
"name": "kino-astro",
"type": "module",
"version": "0.0.1",
"engines": {
"node": ">=22.12.0"
},
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro",
"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.2.0",
"dotenv": "^17.4.2",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"tailwindcss": "^4.2.4",
"vite": "^6.4.2"
},
"devDependencies": {
"@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"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 655 B

View File

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

Before

Width:  |  Height:  |  Size: 749 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 878 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 362 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 910 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 643 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 481 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 530 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

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