5 Commits

135 changed files with 4538 additions and 13963 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.

1129
index.html Normal file

File diff suppressed because it is too large Load Diff

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