Compare commits
10 Commits
dev_aaron
...
7a07dd8c70
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a07dd8c70 | ||
|
|
d6905da5c5 | ||
|
|
e72f8a4f1f | ||
|
|
23d5a75891 | ||
|
|
e143d8360a | ||
| c805221208 | |||
|
|
40ee49aff5 | ||
|
|
c88242b2be | ||
|
|
425c5d1900 | ||
|
|
e2b4852e0d |
2
.env.example
Normal file
@@ -0,0 +1,2 @@
|
||||
TMDB_API_TOKEN=yourapitoken
|
||||
TMDB_API_KEY=yourapikey
|
||||
4
.gitattributes
vendored
@@ -1,2 +1,2 @@
|
||||
img/ filter=lfs diff=lfs merge=lfs -text
|
||||
img/** filter=lfs diff=lfs merge=lfs -text
|
||||
public/img/* filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
|
||||
25
.gitignore
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# build output
|
||||
dist/
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
|
||||
# jetbrains setting folder
|
||||
.idea/
|
||||
.claude/
|
||||
4
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"recommendations": ["astro-build.astro-vscode"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
||||
11
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "./node_modules/.bin/astro dev",
|
||||
"name": "Development server",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
43
README.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Astro Starter Kit: Minimal
|
||||
|
||||
```sh
|
||||
npm create astro@latest -- --template minimal
|
||||
```
|
||||
|
||||
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||
|
||||
## 🚀 Project Structure
|
||||
|
||||
Inside of your Astro project, you'll see the following folders and files:
|
||||
|
||||
```text
|
||||
/
|
||||
├── public/
|
||||
├── src/
|
||||
│ └── pages/
|
||||
│ └── index.astro
|
||||
└── package.json
|
||||
```
|
||||
|
||||
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
|
||||
|
||||
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
|
||||
|
||||
Any static assets, like images, can be placed in the `public/` directory.
|
||||
|
||||
## 🧞 Commands
|
||||
|
||||
All commands are run from the root of the project, from a terminal:
|
||||
|
||||
| Command | Action |
|
||||
| :------------------------ | :----------------------------------------------- |
|
||||
| `npm install` | Installs dependencies |
|
||||
| `npm run dev` | Starts local dev server at `localhost:4321` |
|
||||
| `npm run build` | Build your production site to `./dist/` |
|
||||
| `npm run preview` | Preview your build locally, before deploying |
|
||||
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||
| `npm run astro -- --help` | Get help using the Astro CLI |
|
||||
|
||||
## 👀 Want to learn more?
|
||||
|
||||
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
|
||||
760
account.js
@@ -1,760 +0,0 @@
|
||||
function readStorageJson(key, fallbackValue) {
|
||||
const raw = localStorage.getItem(key);
|
||||
|
||||
if (!raw || raw === "undefined" || raw === "null") {
|
||||
return fallbackValue;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch (error) {
|
||||
console.warn(`Konnte LocalStorage-Wert fuer ${key} nicht lesen.`, error);
|
||||
return fallbackValue;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeUser(user) {
|
||||
if (!user || typeof user !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
salutation: user.salutation || "",
|
||||
firstName: user.firstName || "",
|
||||
lastName: user.lastName || "",
|
||||
birthday: user.birthday || "",
|
||||
street: user.street || "",
|
||||
houseNumber: user.houseNumber || "",
|
||||
zip: user.zip || "",
|
||||
city: user.city || "",
|
||||
country: user.country || "Deutschland",
|
||||
email: user.email || "",
|
||||
passwordHash: user.passwordHash || (user.password ? hashPassword(user.password) : ""),
|
||||
orders: Array.isArray(user.orders) ? user.orders : [],
|
||||
paymentMethods: Array.isArray(user.paymentMethods) ? user.paymentMethods : []
|
||||
};
|
||||
}
|
||||
|
||||
function hashPassword(password) {
|
||||
let hashA = 0xdeadbeef;
|
||||
let hashB = 0x41c6ce57;
|
||||
const text = String(password || "");
|
||||
|
||||
for (let index = 0; index < text.length; index += 1) {
|
||||
const charCode = text.charCodeAt(index);
|
||||
hashA = Math.imul(hashA ^ charCode, 2654435761);
|
||||
hashB = Math.imul(hashB ^ charCode, 1597334677);
|
||||
}
|
||||
|
||||
hashA = Math.imul(hashA ^ (hashA >>> 16), 2246822507) ^ Math.imul(hashB ^ (hashB >>> 13), 3266489909);
|
||||
hashB = Math.imul(hashB ^ (hashB >>> 16), 2246822507) ^ Math.imul(hashA ^ (hashA >>> 13), 3266489909);
|
||||
|
||||
return `hash$${(hashB >>> 0).toString(16).padStart(8, "0")}${(hashA >>> 0).toString(16).padStart(8, "0")}`;
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value || "")
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
function formatEuro(value) {
|
||||
return `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`;
|
||||
}
|
||||
|
||||
function persistUsers() {
|
||||
localStorage.setItem("eagleUsers", JSON.stringify(users));
|
||||
}
|
||||
|
||||
function persistCurrentUser() {
|
||||
if (currentUser) {
|
||||
localStorage.setItem("currentUser", JSON.stringify(currentUser));
|
||||
} else {
|
||||
localStorage.removeItem("currentUser");
|
||||
}
|
||||
}
|
||||
|
||||
let users = readStorageJson("eagleUsers", []);
|
||||
if (!Array.isArray(users)) {
|
||||
users = [];
|
||||
}
|
||||
users = users.map(normalizeUser).filter(Boolean);
|
||||
persistUsers();
|
||||
|
||||
let currentUser = normalizeUser(readStorageJson("currentUser", null));
|
||||
if (currentUser && currentUser.email) {
|
||||
const storedMatch = users.find((user) => user.email === currentUser.email);
|
||||
if (storedMatch) {
|
||||
currentUser = storedMatch;
|
||||
persistCurrentUser();
|
||||
} else {
|
||||
users.push(currentUser);
|
||||
persistUsers();
|
||||
persistCurrentUser();
|
||||
}
|
||||
}
|
||||
|
||||
function registerUser() {
|
||||
if (!validateRegisterForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const salutation = document.getElementById("reg-salutation")?.value.trim() || "";
|
||||
const firstName = document.getElementById("reg-firstname")?.value.trim() || "";
|
||||
const lastName = document.getElementById("reg-lastname")?.value.trim() || "";
|
||||
const birthday = document.getElementById("reg-birthday")?.value.trim() || "";
|
||||
const street = document.getElementById("reg-street")?.value.trim() || "";
|
||||
const houseNumber = document.getElementById("reg-house-number")?.value.trim() || "";
|
||||
const zip = document.getElementById("reg-zip")?.value.trim() || "";
|
||||
const city = document.getElementById("reg-city")?.value.trim() || "";
|
||||
const country = document.getElementById("reg-country")?.value.trim() || "";
|
||||
const email = (document.getElementById("reg-email")?.value.trim() || "").toLowerCase();
|
||||
const password = document.getElementById("reg-password")?.value || "";
|
||||
|
||||
const existingUser = users.find((user) => user.email.toLowerCase() === email);
|
||||
if (existingUser) {
|
||||
setFieldError(document.getElementById("reg-email"), "Diese E-Mail ist bereits registriert.");
|
||||
return;
|
||||
}
|
||||
|
||||
const newUser = {
|
||||
salutation,
|
||||
firstName,
|
||||
lastName,
|
||||
birthday,
|
||||
street,
|
||||
houseNumber,
|
||||
zip,
|
||||
city,
|
||||
country,
|
||||
email,
|
||||
passwordHash: hashPassword(password),
|
||||
orders: [],
|
||||
paymentMethods: []
|
||||
};
|
||||
|
||||
users.push(newUser);
|
||||
currentUser = newUser;
|
||||
|
||||
persistUsers();
|
||||
persistCurrentUser();
|
||||
|
||||
document.getElementById("register-modal")?.classList.add("hidden");
|
||||
|
||||
openAccountDashboard();
|
||||
}
|
||||
|
||||
function loginUser() {
|
||||
if (!validateLoginForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const email = (document.getElementById("login-email")?.value.trim() || "").toLowerCase();
|
||||
const password = document.getElementById("login-password")?.value || "";
|
||||
const passwordHash = hashPassword(password);
|
||||
|
||||
const user = users.find(
|
||||
(entry) => entry.email.toLowerCase() === email && entry.passwordHash === passwordHash
|
||||
);
|
||||
|
||||
if (!user) {
|
||||
document.getElementById("login-error")?.classList.remove("hidden");
|
||||
setFieldError(document.getElementById("login-email"), "E-Mail oder Passwort stimmen nicht.");
|
||||
setFieldError(document.getElementById("login-password"), "E-Mail oder Passwort stimmen nicht.");
|
||||
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>
|
||||
<div class="account-header-actions">
|
||||
<button class="account-edit-btn" type="button" onclick="renderPersonalInfo(true)" aria-label="Persönliche Daten bearbeiten" title="Daten bearbeiten">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M12 20h9"></path>
|
||||
<path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="account-logout-btn" onclick="logoutUser()">Abmelden</button>
|
||||
</div>
|
||||
</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(isEditing = false) {
|
||||
const target = document.getElementById("account-tab-content");
|
||||
if (!target || !currentUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isEditing) {
|
||||
target.innerHTML = `
|
||||
<div class="account-card account-personal-card">
|
||||
<div class="account-card-title-row">
|
||||
<h3>Persönliche Daten bearbeiten</h3>
|
||||
<span>E-Mail bleibt unverändert</span>
|
||||
</div>
|
||||
<div class="account-edit-form">
|
||||
<label class="auth-field">
|
||||
<span>Anrede</span>
|
||||
<select id="account-edit-salutation">
|
||||
<option value="">Bitte wählen</option>
|
||||
<option value="Frau" ${currentUser.salutation === "Frau" ? "selected" : ""}>Frau</option>
|
||||
<option value="Herr" ${currentUser.salutation === "Herr" ? "selected" : ""}>Herr</option>
|
||||
<option value="Divers" ${currentUser.salutation === "Divers" ? "selected" : ""}>Divers</option>
|
||||
</select>
|
||||
<span class="field-error"></span>
|
||||
</label>
|
||||
<label class="auth-field">
|
||||
<span>Vorname</span>
|
||||
<input type="text" id="account-edit-firstname" value="${escapeHtml(currentUser.firstName)}" required data-required-message="Vorname ist ein Pflichtfeld.">
|
||||
<span class="field-error"></span>
|
||||
</label>
|
||||
<label class="auth-field">
|
||||
<span>Nachname</span>
|
||||
<input type="text" id="account-edit-lastname" value="${escapeHtml(currentUser.lastName)}" required data-required-message="Nachname ist ein Pflichtfeld.">
|
||||
<span class="field-error"></span>
|
||||
</label>
|
||||
<label class="auth-field">
|
||||
<span>Geburtstag</span>
|
||||
<input type="date" id="account-edit-birthday" value="${escapeHtml(currentUser.birthday)}">
|
||||
<span class="field-error"></span>
|
||||
</label>
|
||||
<label class="auth-field">
|
||||
<span>Straße</span>
|
||||
<input type="text" id="account-edit-street" value="${escapeHtml(currentUser.street)}">
|
||||
<span class="field-error"></span>
|
||||
</label>
|
||||
<label class="auth-field">
|
||||
<span>Nr.</span>
|
||||
<input type="text" id="account-edit-house-number" value="${escapeHtml(currentUser.houseNumber)}">
|
||||
<span class="field-error"></span>
|
||||
</label>
|
||||
<label class="auth-field">
|
||||
<span>PLZ</span>
|
||||
<input type="text" id="account-edit-zip" value="${escapeHtml(currentUser.zip)}">
|
||||
<span class="field-error"></span>
|
||||
</label>
|
||||
<label class="auth-field">
|
||||
<span>Ort</span>
|
||||
<input type="text" id="account-edit-city" value="${escapeHtml(currentUser.city)}">
|
||||
<span class="field-error"></span>
|
||||
</label>
|
||||
<label class="auth-field">
|
||||
<span>Land</span>
|
||||
<select id="account-edit-country">
|
||||
<option value="Deutschland" ${currentUser.country === "Deutschland" ? "selected" : ""}>Deutschland</option>
|
||||
<option value="Österreich" ${currentUser.country === "Österreich" ? "selected" : ""}>Österreich</option>
|
||||
<option value="Schweiz" ${currentUser.country === "Schweiz" ? "selected" : ""}>Schweiz</option>
|
||||
<option value="Luxemburg" ${currentUser.country === "Luxemburg" ? "selected" : ""}>Luxemburg</option>
|
||||
</select>
|
||||
<span class="field-error"></span>
|
||||
</label>
|
||||
<label class="auth-field account-email-locked">
|
||||
<span>E-Mail</span>
|
||||
<input type="email" value="${escapeHtml(currentUser.email)}" disabled>
|
||||
<small>Die E-Mail-Adresse ist fest mit deinem Konto verbunden.</small>
|
||||
</label>
|
||||
</div>
|
||||
<div class="account-edit-actions">
|
||||
<button type="button" class="account-secondary-btn" onclick="renderPersonalInfo()">Abbrechen</button>
|
||||
<button type="button" class="account-save-btn" onclick="savePersonalInfo()">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
target.innerHTML = `
|
||||
<div class="account-card account-personal-card">
|
||||
<h3>Persönliche Daten</h3>
|
||||
<div class="account-personal-grid">
|
||||
<p><span>Anrede</span><strong>${escapeHtml(currentUser.salutation || "-")}</strong></p>
|
||||
<p><span>Vorname</span><strong>${escapeHtml(currentUser.firstName || "-")}</strong></p>
|
||||
<p><span>Nachname</span><strong>${escapeHtml(currentUser.lastName || "-")}</strong></p>
|
||||
<p><span>Geburtstag</span><strong>${escapeHtml(currentUser.birthday || "-")}</strong></p>
|
||||
<p><span>Straße</span><strong>${escapeHtml(currentUser.street || "-")}</strong></p>
|
||||
<p><span>Nr.</span><strong>${escapeHtml(currentUser.houseNumber || "-")}</strong></p>
|
||||
<p><span>PLZ</span><strong>${escapeHtml(currentUser.zip || "-")}</strong></p>
|
||||
<p><span>Ort</span><strong>${escapeHtml(currentUser.city || "-")}</strong></p>
|
||||
<p><span>Land</span><strong>${escapeHtml(currentUser.country || "-")}</strong></p>
|
||||
<p><span>E-Mail</span><strong>${escapeHtml(currentUser.email || "-")}</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function savePersonalInfo() {
|
||||
if (!currentUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
const requiredFields = ["account-edit-firstname", "account-edit-lastname"];
|
||||
const isValid = requiredFields
|
||||
.map((id) => validateAccountField(document.getElementById(id)))
|
||||
.every(Boolean);
|
||||
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentUser.salutation = document.getElementById("account-edit-salutation")?.value || "";
|
||||
currentUser.firstName = document.getElementById("account-edit-firstname")?.value.trim() || "";
|
||||
currentUser.lastName = document.getElementById("account-edit-lastname")?.value.trim() || "";
|
||||
currentUser.birthday = document.getElementById("account-edit-birthday")?.value || "";
|
||||
currentUser.street = document.getElementById("account-edit-street")?.value.trim() || "";
|
||||
currentUser.houseNumber = document.getElementById("account-edit-house-number")?.value.trim() || "";
|
||||
currentUser.zip = document.getElementById("account-edit-zip")?.value.trim() || "";
|
||||
currentUser.city = document.getElementById("account-edit-city")?.value.trim() || "";
|
||||
currentUser.country = document.getElementById("account-edit-country")?.value || "Deutschland";
|
||||
|
||||
const userIndex = users.findIndex((user) => user.email === currentUser.email);
|
||||
if (userIndex >= 0) {
|
||||
users[userIndex] = currentUser;
|
||||
}
|
||||
|
||||
persistUsers();
|
||||
persistCurrentUser();
|
||||
renderPersonalInfo();
|
||||
}
|
||||
|
||||
function getFieldWrapper(input) {
|
||||
if (!input) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return input.closest(".auth-field") || document.querySelector(`[data-field-for="${input.id}"]`);
|
||||
}
|
||||
|
||||
function setFieldError(input, message) {
|
||||
const wrapper = getFieldWrapper(input);
|
||||
if (!wrapper) {
|
||||
return;
|
||||
}
|
||||
|
||||
input.dataset.touched = "true";
|
||||
wrapper.classList.add("is-invalid");
|
||||
const error = wrapper.querySelector(".field-error");
|
||||
if (error) {
|
||||
error.textContent = message;
|
||||
}
|
||||
}
|
||||
|
||||
function clearFieldError(input) {
|
||||
const wrapper = getFieldWrapper(input);
|
||||
if (!wrapper) {
|
||||
return;
|
||||
}
|
||||
|
||||
wrapper.classList.remove("is-invalid");
|
||||
const error = wrapper.querySelector(".field-error");
|
||||
if (error) {
|
||||
error.textContent = "";
|
||||
}
|
||||
}
|
||||
|
||||
function validateAccountField(input) {
|
||||
if (!input) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const value = (input.value || "").trim();
|
||||
if (input.required && !value) {
|
||||
setFieldError(input, input.dataset.requiredMessage || "Dieses Feld ist ein Pflichtfeld.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (input.type === "email" && value && !value.includes("@")) {
|
||||
setFieldError(input, input.dataset.emailMessage || "Bitte gib eine gültige E-Mail-Adresse ein.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (input.type === "password" && value && (value.length < 8 || value.length > 32)) {
|
||||
setFieldError(input, input.dataset.passwordMessage || "Passwort muss 8 bis 32 Zeichen haben.");
|
||||
return false;
|
||||
}
|
||||
|
||||
clearFieldError(input);
|
||||
return true;
|
||||
}
|
||||
|
||||
function validateRegisterForm() {
|
||||
const fieldIds = [
|
||||
"reg-salutation",
|
||||
"reg-firstname",
|
||||
"reg-lastname",
|
||||
"reg-birthday",
|
||||
"reg-street",
|
||||
"reg-house-number",
|
||||
"reg-zip",
|
||||
"reg-city",
|
||||
"reg-country",
|
||||
"reg-email",
|
||||
"reg-password"
|
||||
];
|
||||
|
||||
return fieldIds
|
||||
.map((id) => validateAccountField(document.getElementById(id)))
|
||||
.every(Boolean);
|
||||
}
|
||||
|
||||
function validateLoginForm() {
|
||||
return ["login-email", "login-password"]
|
||||
.map((id) => validateAccountField(document.getElementById(id)))
|
||||
.every(Boolean);
|
||||
}
|
||||
|
||||
function initAccountFieldValidation() {
|
||||
const fieldSelector = [
|
||||
"#login-email",
|
||||
"#login-password",
|
||||
"#reg-salutation",
|
||||
"#reg-firstname",
|
||||
"#reg-lastname",
|
||||
"#reg-birthday",
|
||||
"#reg-street",
|
||||
"#reg-house-number",
|
||||
"#reg-zip",
|
||||
"#reg-city",
|
||||
"#reg-country",
|
||||
"#reg-email",
|
||||
"#reg-password"
|
||||
].join(",");
|
||||
|
||||
document.querySelectorAll(fieldSelector).forEach((input) => {
|
||||
input.addEventListener("blur", () => {
|
||||
input.dataset.touched = "true";
|
||||
validateAccountField(input);
|
||||
});
|
||||
|
||||
input.addEventListener("input", () => {
|
||||
input.dataset.touched = "true";
|
||||
if (input.type !== "hidden") {
|
||||
validateAccountField(input);
|
||||
}
|
||||
});
|
||||
|
||||
input.addEventListener("change", () => {
|
||||
input.dataset.touched = "true";
|
||||
validateAccountField(input);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll(".gender-selector button").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const salutationInput = document.getElementById("reg-salutation");
|
||||
if (!salutationInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
salutationInput.value = button.dataset.gender || "";
|
||||
salutationInput.dataset.touched = "true";
|
||||
button.closest(".gender-selector")?.querySelectorAll("button").forEach((item) => {
|
||||
item.classList.toggle("active", item === button);
|
||||
});
|
||||
validateAccountField(salutationInput);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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 genauere Informationen 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">Hier kannst du deine bevorzugten Zahlungsmethoden verwalten.</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>Gerät 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 verknüpfen</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>×</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>×</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>×</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>×</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();
|
||||
}
|
||||
22
astro.config.mjs
Normal file
@@ -0,0 +1,22 @@
|
||||
// @ts-check
|
||||
import { defineConfig, envField } from 'astro/config';
|
||||
|
||||
import react from '@astrojs/react';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
integrations: [react({
|
||||
include: ['**/react/*']
|
||||
})],
|
||||
vite: {
|
||||
plugins: [tailwindcss()],
|
||||
},
|
||||
env: {
|
||||
schema: {
|
||||
TMDB_API_TOKEN: envField.string({ context: 'client', access: 'public', default: 'https://api.example.com' }),
|
||||
SETTINGS_TOKEN: envField.string({ context: 'server', access: 'secret' }),
|
||||
}
|
||||
}
|
||||
});
|
||||
BIN
img/astronaut-rockypopcorn.jpg
LFS
BIN
img/cashtruck.jpg
LFS
BIN
img/fsk-0.png
LFS
BIN
img/fsk-12.png
LFS
BIN
img/fsk-16.png
LFS
BIN
img/fsk-18.png
LFS
BIN
img/fsk-6.png
LFS
BIN
img/nachokombigross.png
LFS
BIN
img/nachokombimittel.png
LFS
BIN
img/popcornkombigross.png
LFS
BIN
img/popcornkombiklein.png
LFS
BIN
img/popcornkombimittel.png
LFS
BIN
img/screammetalpopcorn.png
LFS
BIN
img/youtube-logo.png
LFS
BIN
img/zoomania-2-logo.png
LFS
827
index.html
@@ -51,14 +51,7 @@
|
||||
<h3>Jetzt läuft</h3>
|
||||
<span>Heute im Fokus</span>
|
||||
</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>></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="now-running-row" class="now-running-row"></div>
|
||||
</div>
|
||||
|
||||
<div class="home-inline-showcase reveal-on-scroll">
|
||||
@@ -274,238 +267,15 @@
|
||||
<h1 class="list-title">Snacks & Getränke</h1>
|
||||
|
||||
<div class="category-tabs">
|
||||
<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 active" data-target="cat-getraenke">Getränke</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-snacks">Snacks</button>
|
||||
<button class="tab-btn" data-target="cat-kombi">Kombi</button>
|
||||
<button class="tab-btn" data-target="cat-eis">Eis</button>
|
||||
</div>
|
||||
|
||||
<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">Metall Sammelbecher im Zoomania 2 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 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 ist an 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">Metall Sammelbecher im HOPPERS 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 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">Limitierter Sammelbecher als Biberdamm - Mit verteckter Eidechse.</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">Limitierter Sammelbecher in der Form von Yoshi.</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 id="cat-getraenke" class="snack-category active">
|
||||
<div class="snack-grid">
|
||||
|
||||
<div class="snack-card">
|
||||
@@ -622,239 +392,229 @@
|
||||
</div> </div>
|
||||
|
||||
<div id="cat-popcorn" class="snack-category hidden">
|
||||
<div class="snack-subsection">
|
||||
<div class="snack-section-heading">
|
||||
<span>Frisch gepoppt</span>
|
||||
<h2>Einzelprodukte</h2>
|
||||
</div>
|
||||
<div class="snack-grid">
|
||||
<div class="snack-card">
|
||||
<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 class="snack-grid">
|
||||
<div class="snack-card">
|
||||
<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>
|
||||
<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 class="size-selector">
|
||||
<button class="size-chip">3,50€</button>
|
||||
</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 class="snack-subsection">
|
||||
<div class="snack-section-heading">
|
||||
<span>Nachos</span>
|
||||
<h2>Einzelprodukte</h2>
|
||||
</div>
|
||||
<div class="snack-grid">
|
||||
<div class="snack-card">
|
||||
<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 class="snack-grid">
|
||||
<div class="snack-card">
|
||||
<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>
|
||||
<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 class="size-selector">
|
||||
<button class="size-chip">Klein<span>5,00€</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="snack-subsection">
|
||||
<div class="snack-section-heading">
|
||||
<span>Extra dazu</span>
|
||||
<h2>Dips</h2>
|
||||
</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 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>
|
||||
<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 class="size-selector">
|
||||
<button class="size-chip">Mittel<span>6,50€</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="snack-subsection">
|
||||
<div class="snack-section-heading">
|
||||
<span>Alles drin</span>
|
||||
<h2>Kombi Menü</h2>
|
||||
</div>
|
||||
<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 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>
|
||||
<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">
|
||||
<span class="badge">Bestseller</span>
|
||||
<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 class="size-selector">
|
||||
<button class="size-chip">Groß <span>8,00€</span></button>
|
||||
</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 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 class="snack-grid">
|
||||
<div class="snack-card">
|
||||
@@ -887,6 +647,75 @@
|
||||
</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 class="coming-soon-banner">
|
||||
<h2>Eiscreme & Shakes</h2>
|
||||
@@ -906,8 +735,8 @@
|
||||
<div class="header-sub-info">
|
||||
<p id="modal-info-text">Saal • Zeit</p>
|
||||
<div id="tech-badges" class="tech-badges-container hidden">
|
||||
<img src="img/dolby.png" alt="Dolby" class="tech-badge">
|
||||
<img src="img/dbox.png" alt="D-Box" class="tech-badge">
|
||||
<img src="img/Dolby.png" alt="Dolby" class="tech-badge">
|
||||
<img src="img/dbox.jpg" alt="D-Box" class="tech-badge">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -986,14 +815,8 @@
|
||||
<div class="account-login-box">
|
||||
<h2>Mein Konto</h2>
|
||||
|
||||
<label class="auth-field">
|
||||
<input type="email" id="login-email" placeholder="E-Mail" required data-required-message="E-Mail ist ein Pflichtfeld." data-email-message="Bitte gib eine gültige E-Mail-Adresse ein.">
|
||||
<span class="field-error"></span>
|
||||
</label>
|
||||
<label class="auth-field">
|
||||
<input type="password" id="login-password" placeholder="Passwort" required data-required-message="Passwort ist ein Pflichtfeld.">
|
||||
<span class="field-error"></span>
|
||||
</label>
|
||||
<input type="email" id="login-email" placeholder="E-Mail">
|
||||
<input type="password" id="login-password" placeholder="Passwort">
|
||||
<button id="btn-forgot-password" type="button">Passwort vergessen?</button>
|
||||
|
||||
<div id="login-error" class="hidden">
|
||||
@@ -1008,80 +831,14 @@
|
||||
|
||||
|
||||
<div id="register-modal" class="modal hidden">
|
||||
<div class="modal-content account-auth-modal register-auth-modal">
|
||||
<div class="modal-content account-auth-modal">
|
||||
<button id="btn-close-register" class="modal-close-btn" type="button" aria-label="Registrierung schliessen">×</button>
|
||||
<h2>Registrieren</h2>
|
||||
<p class="auth-modal-subtitle">Erstelle dein Konto für schnellere Buchungen.</p>
|
||||
|
||||
<div class="register-form-grid">
|
||||
<section class="register-form-section">
|
||||
<h3>Meine Daten</h3>
|
||||
<div class="gender-field auth-field" data-field-for="reg-salutation">
|
||||
<input type="hidden" id="reg-salutation" required data-required-message="Anrede ist ein Pflichtfeld.">
|
||||
<div class="gender-selector" role="group" aria-label="Anrede">
|
||||
<button type="button" data-gender="Frau">Frau</button>
|
||||
<button type="button" data-gender="Herr">Herr</button>
|
||||
<button type="button" data-gender="Divers">Divers</button>
|
||||
</div>
|
||||
<span class="field-error"></span>
|
||||
</div>
|
||||
<label class="auth-field">
|
||||
<input type="text" id="reg-firstname" placeholder="Vorname" required data-required-message="Vorname ist ein Pflichtfeld.">
|
||||
<span class="field-error"></span>
|
||||
</label>
|
||||
<label class="auth-field">
|
||||
<input type="text" id="reg-lastname" placeholder="Nachname" required data-required-message="Nachname ist ein Pflichtfeld.">
|
||||
<span class="field-error"></span>
|
||||
</label>
|
||||
<label class="auth-field">
|
||||
<input type="date" id="reg-birthday" placeholder="Geburtstag" required data-required-message="Geburtstag ist ein Pflichtfeld.">
|
||||
<span class="field-error"></span>
|
||||
</label>
|
||||
<div class="register-row street-row">
|
||||
<label class="auth-field">
|
||||
<input type="text" id="reg-street" placeholder="Straße" required data-required-message="Straße ist ein Pflichtfeld.">
|
||||
<span class="field-error"></span>
|
||||
</label>
|
||||
<label class="auth-field">
|
||||
<input type="text" id="reg-house-number" placeholder="Nr." required data-required-message="Hausnummer ist ein Pflichtfeld.">
|
||||
<span class="field-error"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="register-row">
|
||||
<label class="auth-field">
|
||||
<input type="text" id="reg-zip" placeholder="PLZ" required data-required-message="PLZ ist ein Pflichtfeld.">
|
||||
<span class="field-error"></span>
|
||||
</label>
|
||||
<label class="auth-field">
|
||||
<input type="text" id="reg-city" placeholder="Ort" required data-required-message="Ort ist ein Pflichtfeld.">
|
||||
<span class="field-error"></span>
|
||||
</label>
|
||||
</div>
|
||||
<label class="auth-field">
|
||||
<select id="reg-country" required data-required-message="Land ist ein Pflichtfeld.">
|
||||
<option value="Deutschland">Deutschland</option>
|
||||
<option value="Österreich">Österreich</option>
|
||||
<option value="Schweiz">Schweiz</option>
|
||||
<option value="Luxemburg">Luxemburg</option>
|
||||
</select>
|
||||
<span class="field-error"></span>
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section class="register-form-section">
|
||||
<h3>Anmeldeinformationen</h3>
|
||||
<label class="auth-field">
|
||||
<input type="email" id="reg-email" placeholder="E-Mail" required data-required-message="E-Mail ist ein Pflichtfeld." data-email-message="Bitte gib eine gültige E-Mail-Adresse ein.">
|
||||
<span class="field-error"></span>
|
||||
</label>
|
||||
<label class="auth-field">
|
||||
<input type="password" id="reg-password" placeholder="Passwort" required minlength="8" maxlength="32" data-required-message="Passwort ist ein Pflichtfeld." data-password-message="Passwort muss 8 bis 32 Zeichen haben.">
|
||||
<span class="field-error"></span>
|
||||
</label>
|
||||
<p class="register-warning">Achte auf eine korrekte E-Mail-Adresse. An diese Adresse wird deine Buchungsbestätigung gesendet.</p>
|
||||
<p class="register-warning">Passwort mit 8 bis 32 Zeichen.</p>
|
||||
</section>
|
||||
</div>
|
||||
<input type="text" id="reg-firstname" placeholder="Vorname">
|
||||
<input type="text" id="reg-lastname" placeholder="Nachname">
|
||||
<input type="email" id="reg-email" placeholder="E-Mail">
|
||||
<input type="password" id="reg-password" placeholder="Passwort">
|
||||
<button id="btn-register-save" class="auth-submit-btn" type="button">Konto erstellen</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1191,11 +948,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="account.js"></script>
|
||||
<script src="cart.js"></script>
|
||||
<script src="booking.js"></script>
|
||||
<script src="checkout.js"></script>
|
||||
<script src="main.js"></script>
|
||||
<script type="module" src="dist/main.js"></script>
|
||||
<script type="module" src="dist/cart.js"></script>
|
||||
<script type="module" src="dist/booking.js"></script>
|
||||
<script type="module" src="dist/checkout.js"></script>
|
||||
<script type="module" src="dist/account.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
6442
package-lock.json
generated
Normal file
28
package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "kino-astro",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/react": "^5.0.4",
|
||||
"@tailwindcss/vite": "^4.2.4",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"astro": "^6.1.10",
|
||||
"dotenv": "^17.4.2",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5",
|
||||
"tailwindcss": "^4.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.6.0"
|
||||
}
|
||||
}
|
||||
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 655 B |
9
public/favicon.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 749 B |
BIN
public/img/Apfelschorle.png
Normal file
|
After Width: | Height: | Size: 144 KiB |
BIN
public/img/Dolby.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
public/img/Schorle.png
Normal file
|
After Width: | Height: | Size: 193 KiB |
BIN
public/img/Zoomania-2.jpg
Normal file
|
After Width: | Height: | Size: 145 KiB |
BIN
public/img/applepay.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
public/img/astronautpopcorn.jpg
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
public/img/bladerunner2049.jpg
Normal file
|
After Width: | Height: | Size: 211 KiB |
BIN
public/img/cola-light.png
Normal file
|
After Width: | Height: | Size: 138 KiB |
BIN
public/img/cola-zero.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
public/img/cola.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
public/img/dbox.jpg
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
public/img/derAustronaut.jpg
Normal file
|
After Width: | Height: | Size: 878 KiB |
BIN
public/img/fallguy.jpg
Normal file
|
After Width: | Height: | Size: 203 KiB |
BIN
public/img/fanta.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
public/img/fuze-tea.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
public/img/gangstergang.jpg
Normal file
|
After Width: | Height: | Size: 362 KiB |
BIN
public/img/gangstergang2.png
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
public/img/glennkill.jpg
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
public/img/goat.jpg
Normal file
|
After Width: | Height: | Size: 233 KiB |
BIN
public/img/googlepay.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
public/img/haribo.png
Normal file
|
After Width: | Height: | Size: 202 KiB |
BIN
public/img/homefront.jpg
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
public/img/hoppers.jpg
Normal file
|
After Width: | Height: | Size: 201 KiB |
BIN
public/img/hopperskidsmenu.jpg
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
public/img/hopperspopcornmetall.jpg
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
public/img/hopperspopcornwood.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
public/img/klarna.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
public/img/käsedip.png
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
public/img/mandalorian.jpeg
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
public/img/mariogalaxy.jpg
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
public/img/mariokidsmenu.png
Normal file
|
After Width: | Height: | Size: 307 KiB |
BIN
public/img/marioyoshipopcorn.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
public/img/mastercard.png
Normal file
|
After Width: | Height: | Size: 185 KiB |
BIN
public/img/masteruniverse.jpg
Normal file
|
After Width: | Height: | Size: 910 KiB |
BIN
public/img/meg.JPG
Normal file
|
After Width: | Height: | Size: 586 KiB |
BIN
public/img/meg2.jpg
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
public/img/menu-big.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
public/img/minionsmonsters.jpg
Normal file
|
After Width: | Height: | Size: 154 KiB |
BIN
public/img/mms.png
Normal file
|
After Width: | Height: | Size: 154 KiB |
BIN
public/img/monsterag.png
Normal file
|
After Width: | Height: | Size: 3.0 MiB |
BIN
public/img/monsteruni.jpg
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
public/img/mutiny.jpg
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
public/img/nachokombiklein.png
Normal file
|
After Width: | Height: | Size: 182 KiB |
BIN
public/img/nachos.jpg
Normal file
|
After Width: | Height: | Size: 135 KiB |
BIN
public/img/nachosnormal.png
Normal file
|
After Width: | Height: | Size: 136 KiB |
BIN
public/img/paypal.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
public/img/popcorn-big.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
public/img/popcorn-klein.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
public/img/popcorn-mittel.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
public/img/popcorn.jpg
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
public/img/riegel.png
Normal file
|
After Width: | Height: | Size: 186 KiB |
BIN
public/img/salsadip.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
public/img/screamdoorpopcorn.jpg
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
public/img/screamvii.jpg
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
public/img/shelter.jpg
Normal file
|
After Width: | Height: | Size: 263 KiB |
BIN
public/img/solomio.png
Normal file
|
After Width: | Height: | Size: 211 KiB |
BIN
public/img/sourdip.png
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
public/img/spezi.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
public/img/spidermannewday.jpg
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
public/img/sprite.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
public/img/super-mario-galaxy-banner.jpg
Normal file
|
After Width: | Height: | Size: 185 KiB |
BIN
public/img/toystory1.jpg
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
public/img/toystory2.jpg
Normal file
|
After Width: | Height: | Size: 643 KiB |
BIN
public/img/toystory3.jpg
Normal file
|
After Width: | Height: | Size: 481 KiB |
BIN
public/img/toystory4.jpg
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
public/img/toystory5.png
Normal file
|
After Width: | Height: | Size: 530 KiB |
BIN
public/img/visa.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
public/img/wasser.png
Normal file
|
After Width: | Height: | Size: 146 KiB |
BIN
public/img/zoomania-popcorn.jpg
Normal file
|
After Width: | Height: | Size: 544 KiB |
BIN
public/img/zoomaniakidsmenu.jpg
Normal file
|
After Width: | Height: | Size: 524 KiB |
469
src/account.ts
Normal file
@@ -0,0 +1,469 @@
|
||||
import type { User } from "./interfaces.js";
|
||||
|
||||
function readStorageJson(key: string, fallbackValue: any) {
|
||||
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: User): User {
|
||||
return {
|
||||
firstName: user.firstName || "",
|
||||
lastName: user.lastName || "",
|
||||
email: user.email || "",
|
||||
hashedPassword: user.hashedPassword || "",
|
||||
orders: Array.isArray(user.orders) ? user.orders : [],
|
||||
paymentMethods: Array.isArray(user.paymentMethods) ? user.paymentMethods : []
|
||||
};
|
||||
}
|
||||
|
||||
function escapeHtml(value: string) {
|
||||
return String(value || "")
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
function formatEuro(value: string) {
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
export let users = readStorageJson("eagleUsers", []);
|
||||
if (!Array.isArray(users)) {
|
||||
users = [];
|
||||
}
|
||||
users = users.map(normalizeUser).filter(Boolean);
|
||||
|
||||
const rawCurrentUser = readStorageJson("currentUser", null);
|
||||
|
||||
export var currentUser: User | null = rawCurrentUser ? normalizeUser(rawCurrentUser) : null;
|
||||
|
||||
if (currentUser && currentUser.email) {
|
||||
const currentEmail = currentUser.email;
|
||||
const storedMatch = users.find((user: { email: string; }) => {
|
||||
return user.email === currentEmail;
|
||||
});
|
||||
if (storedMatch) {
|
||||
currentUser = storedMatch;
|
||||
} else {
|
||||
users.push(currentUser);
|
||||
persistUsers();
|
||||
}
|
||||
}
|
||||
|
||||
async function hashMessage(message: string) {
|
||||
const msgBuffer = new TextEncoder().encode(message); // Encode as UTF-8
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer); // Hash
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer)); // Convert to bytes
|
||||
return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); // Hex string
|
||||
}
|
||||
|
||||
function getInputValue(id: string): string {
|
||||
const el = document.getElementById(id) as HTMLInputElement | null;
|
||||
return el?.value.trim() ?? "";
|
||||
}
|
||||
|
||||
export async function registerUser() {
|
||||
const firstName = getInputValue("reg-firstname");
|
||||
const lastName = getInputValue("reg-lastname");
|
||||
const email = getInputValue("reg-email").toLowerCase();
|
||||
const password = document.querySelector<HTMLInputElement>("#reg-password")?.value ?? "";
|
||||
|
||||
if (!firstName || !lastName || !email || !password) {
|
||||
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) => user.email.toLowerCase() === email);
|
||||
if (existingUser) {
|
||||
alert("E-Mail bereits registriert");
|
||||
return;
|
||||
}
|
||||
|
||||
const hashedPassword = await hashMessage(password);
|
||||
|
||||
const newUser = {
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
hashedPassword,
|
||||
orders: [],
|
||||
paymentMethods: []
|
||||
};
|
||||
|
||||
users.push(newUser);
|
||||
currentUser = newUser;
|
||||
|
||||
persistUsers();
|
||||
persistCurrentUser();
|
||||
|
||||
alert("Registrierung erfolgreich");
|
||||
document.getElementById("register-modal")?.classList.add("hidden");
|
||||
|
||||
openAccountDashboard();
|
||||
}
|
||||
|
||||
export async function loginUser() {
|
||||
const email = (document.querySelector<HTMLInputElement>("#login-email")?.value.trim() || "").toLowerCase();
|
||||
const password = document.querySelector<HTMLInputElement>("#login-password")?.value || "";
|
||||
const hashedPassword = await hashMessage(password);
|
||||
|
||||
const user = users.find(
|
||||
(entry: User) => entry.email.toLowerCase() === email && entry.hashedPassword === hashedPassword
|
||||
);
|
||||
|
||||
if (!user) {
|
||||
document.getElementById("login-error")?.classList.remove("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
currentUser = user;
|
||||
persistCurrentUser();
|
||||
openAccountDashboard();
|
||||
}
|
||||
|
||||
export 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 = /*html*/`
|
||||
<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: any) => 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<HTMLButtonElement>(".order-item-btn"));
|
||||
|
||||
const renderOrderTicket = (orderIndex: number) => {
|
||||
const order = orders[orderIndex];
|
||||
if (!order || !detailTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
const movieItems = Array.isArray(order.items)
|
||||
? order.items.filter((item: any) => item.category === "movie")
|
||||
: [];
|
||||
const primaryMovie = movieItems[0] || (Array.isArray(order.items) ? order.items[0] : null);
|
||||
const poster = primaryMovie?.img || "";
|
||||
const seats = movieItems.map((item: any) => 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 = /*html*/`
|
||||
<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>×</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>×</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>×</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>×</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() {
|
||||
persistCurrentUser();
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
@@ -1,32 +1,36 @@
|
||||
let currentBookingContext = null;
|
||||
let currentHallLayout = null;
|
||||
import { seatLayouts, occupiedSeatsData, prices, cart } from "./main.js"
|
||||
import { renderCart, saveCart } from "./cart.js";
|
||||
import { renderCheckout } from "./checkout.js";
|
||||
|
||||
function openBooking(movie, hall, time) {
|
||||
const titleEl = document.getElementById("modal-movie-title");
|
||||
const infoEl = document.getElementById("modal-info-text");
|
||||
let currentBookingContext: any = null;
|
||||
let currentHallLayout: any = null;
|
||||
|
||||
if (titleEl) {
|
||||
titleEl.innerText = movie;
|
||||
}
|
||||
export function openBooking(movie: string, hall: string, time: any) {
|
||||
const titleEl = document.getElementById("modal-movie-title");
|
||||
const infoEl = document.getElementById("modal-info-text");
|
||||
|
||||
if (infoEl) {
|
||||
infoEl.innerText = `${hall} • ${time} Uhr`;
|
||||
}
|
||||
if (titleEl) {
|
||||
titleEl.innerText = movie;
|
||||
}
|
||||
|
||||
currentBookingContext = { movie, hall, time };
|
||||
if (infoEl) {
|
||||
infoEl.innerText = `${hall} • ${time} Uhr`;
|
||||
}
|
||||
|
||||
createSeats(hall, time);
|
||||
renderBookingLegend();
|
||||
updateBookingSummary();
|
||||
currentBookingContext = { movie, hall, time };
|
||||
|
||||
document.getElementById("booking-modal")?.classList.remove("hidden");
|
||||
}
|
||||
createSeats(hall, time);
|
||||
renderBookingLegend();
|
||||
updateBookingSummary();
|
||||
|
||||
function getRowLabel(rowIndex) {
|
||||
document.getElementById("booking-modal")?.classList.remove("hidden");
|
||||
}
|
||||
|
||||
function getRowLabel(rowIndex: number) {
|
||||
return String(rowIndex + 1);
|
||||
}
|
||||
|
||||
function buildHallLayout(hallName, baseConfig) {
|
||||
function buildHallLayout(hallName: string, baseConfig:any) {
|
||||
const rows = Number(baseConfig.rows || 0);
|
||||
const totalCols = Number(baseConfig.left || 0) + Number(baseConfig.right || 0);
|
||||
const isDeluxe = /deluxe/i.test(hallName);
|
||||
@@ -39,7 +43,7 @@ function buildHallLayout(hallName, baseConfig) {
|
||||
const vipRows = rows > 0 ? [rows] : [];
|
||||
|
||||
const dboxMap = new Set();
|
||||
const markDboxRange = (rowNumber, startCol, width) => {
|
||||
const markDboxRange = (rowNumber: number, startCol: number, width: number) => {
|
||||
if (!rowNumber || width <= 0) {
|
||||
return;
|
||||
}
|
||||
@@ -52,7 +56,7 @@ function buildHallLayout(hallName, baseConfig) {
|
||||
|
||||
if (isDeluxe) {
|
||||
const configuredDboxSeats = Array.isArray(baseConfig.dbox)
|
||||
? baseConfig.dbox.reduce((sum, section) => sum + Number(section.w || 0), 0)
|
||||
? baseConfig.dbox.reduce((sum: number, section: any) => sum + Number(section.w || 0), 0)
|
||||
: 0;
|
||||
|
||||
const totalDboxSeats = Math.max(4, configuredDboxSeats || 0);
|
||||
@@ -77,7 +81,7 @@ function buildHallLayout(hallName, baseConfig) {
|
||||
markDboxRange(rowNumber, startCol, seatsForRow);
|
||||
});
|
||||
} else if (Array.isArray(baseConfig.dbox)) {
|
||||
baseConfig.dbox.forEach((section) => {
|
||||
baseConfig.dbox.forEach((section: any) => {
|
||||
const rowNumber = Number(section.r || 0);
|
||||
const width = Number(section.w || 0);
|
||||
const startCol = Number(section.c || 0);
|
||||
@@ -96,7 +100,7 @@ function buildHallLayout(hallName, baseConfig) {
|
||||
};
|
||||
}
|
||||
|
||||
function getSeatType(layout, rowNumber, colNumber) {
|
||||
function getSeatType(layout: any, rowNumber: number, colNumber: number) {
|
||||
if (layout.dboxMap.has(`${rowNumber}-${colNumber}`)) {
|
||||
return "dbox";
|
||||
}
|
||||
@@ -112,7 +116,7 @@ function getSeatType(layout, rowNumber, colNumber) {
|
||||
return "normal";
|
||||
}
|
||||
|
||||
function createSeatElement({ seatId, seatType, occupiedSeats }) {
|
||||
function createSeatElement({seatId, seatType, occupiedSeats }:any) {
|
||||
const seat = document.createElement("button");
|
||||
seat.type = "button";
|
||||
seat.classList.add("seat", seatType);
|
||||
@@ -136,7 +140,7 @@ function createSeatElement({ seatId, seatType, occupiedSeats }) {
|
||||
return seat;
|
||||
}
|
||||
|
||||
function createSeats(hallName, time) {
|
||||
function createSeats(hallName: string, time: any) {
|
||||
const seatGrid = document.getElementById("seat-grid");
|
||||
if (!seatGrid) {
|
||||
return;
|
||||
@@ -144,7 +148,8 @@ function createSeats(hallName, time) {
|
||||
|
||||
seatGrid.innerHTML = "";
|
||||
|
||||
const baseConfig = seatLayouts[hallName];
|
||||
const arrIndex = hallName as keyof typeof seatLayouts;
|
||||
const baseConfig: any = seatLayouts[arrIndex];
|
||||
if (!baseConfig) {
|
||||
currentHallLayout = null;
|
||||
return;
|
||||
@@ -235,7 +240,7 @@ function renderBookingLegend() {
|
||||
}
|
||||
|
||||
function updateBookingSummary() {
|
||||
const selectedSeats = Array.from(document.querySelectorAll("#seat-grid .seat.selected"));
|
||||
const selectedSeats = Array.from(document.querySelectorAll("#seat-grid .seat.selected")) as HTMLElement[];;
|
||||
const summaryPanel = document.getElementById("booking-summary");
|
||||
const summaryItems = document.getElementById("summary-items");
|
||||
const totalEl = document.getElementById("total-price");
|
||||
@@ -245,7 +250,7 @@ function updateBookingSummary() {
|
||||
if (summaryItems) {
|
||||
summaryItems.innerHTML = selectedSeats
|
||||
.map((seat) => {
|
||||
const type = seat.dataset.type || "normal";
|
||||
const type = (seat.dataset.type || "normal") as keyof typeof prices;
|
||||
const seatPrice = Number(prices?.[type] ?? prices?.normal ?? 11);
|
||||
total += seatPrice;
|
||||
|
||||
@@ -272,12 +277,13 @@ function updateBookingSummary() {
|
||||
summaryPanel?.classList.toggle("hidden", selectedSeats.length === 0);
|
||||
}
|
||||
|
||||
function findMoviePoster(movieTitle) {
|
||||
function findMoviePoster(movieTitle: string) {
|
||||
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();
|
||||
const currentCard = card.querySelector("h2, h3") as HTMLElement;
|
||||
const title = currentCard.innerText?.trim().toLowerCase();
|
||||
if (title === normalizedTarget) {
|
||||
const imageSrc = card.querySelector("img")?.src;
|
||||
if (imageSrc) {
|
||||
@@ -290,7 +296,7 @@ function findMoviePoster(movieTitle) {
|
||||
}
|
||||
|
||||
function confirmSelectedSeats() {
|
||||
const selectedSeats = Array.from(document.querySelectorAll("#seat-grid .seat.selected"));
|
||||
const selectedSeats = Array.from(document.querySelectorAll("#seat-grid .seat.selected")) as HTMLElement[];
|
||||
|
||||
if (!currentBookingContext || selectedSeats.length === 0) {
|
||||
alert("Bitte waehle mindestens einen Platz aus.");
|
||||
@@ -304,7 +310,7 @@ function confirmSelectedSeats() {
|
||||
const seatId = seat.dataset.seatId;
|
||||
const seatType = seat.dataset.type || "normal";
|
||||
|
||||
const alreadyInCart = cart.some((item) =>
|
||||
const alreadyInCart = cart.some((item: any) =>
|
||||
item.category === "movie" &&
|
||||
item.title === currentBookingContext.movie &&
|
||||
item.hall === currentBookingContext.hall &&
|
||||