forked from Aaron/Kino-Website
add TypeScript interfaces for cart and cart items #5
@@ -44,6 +44,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { seatLayouts, occupiedSeatsData, prices, cart, updateCart } from "../scripts/bigConstants";
|
import { seatLayouts, occupiedSeatsData, prices, cart, updateCart } from "../scripts/bigConstants";
|
||||||
|
import type { MovieCartItem } from "../scripts/bigConstants";
|
||||||
|
|
||||||
let currentBookingContext: any = null;
|
let currentBookingContext: any = null;
|
||||||
let currentHallLayout: any = null;
|
let currentHallLayout: any = null;
|
||||||
@@ -120,15 +121,16 @@
|
|||||||
const selected = Array.from(document.querySelectorAll("#seat-grid .seat.selected")) as HTMLElement[];
|
const selected = Array.from(document.querySelectorAll("#seat-grid .seat.selected")) as HTMLElement[];
|
||||||
if (!selected.length) return alert("Bitte wähle Plätze aus.");
|
if (!selected.length) return alert("Bitte wähle Plätze aus.");
|
||||||
selected.forEach(seat => {
|
selected.forEach(seat => {
|
||||||
cart.push({
|
const ticket: MovieCartItem = {
|
||||||
id: Date.now() + Math.random(),
|
id: Date.now() + Math.random(),
|
||||||
category: "movie",
|
category: "movie",
|
||||||
title: currentBookingContext.movie,
|
title: currentBookingContext.movie,
|
||||||
hall: currentBookingContext.hall,
|
hall: currentBookingContext.hall,
|
||||||
time: currentBookingContext.time,
|
time: currentBookingContext.time,
|
||||||
seatId: seat.dataset.seatId,
|
seatId: seat.dataset.seatId ?? "",
|
||||||
price: prices[seat.dataset.type as keyof typeof prices] || prices.normal
|
price: prices[seat.dataset.type as keyof typeof prices] ?? prices.normal
|
||||||
});
|
};
|
||||||
|
cart.push(ticket);
|
||||||
});
|
});
|
||||||
updateCart(cart);
|
updateCart(cart);
|
||||||
window.dispatchEvent(new CustomEvent("cart-updated"));
|
window.dispatchEvent(new CustomEvent("cart-updated"));
|
||||||
|
|||||||
@@ -38,13 +38,14 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { cart, updateCart } from "../scripts/bigConstants";
|
import { cart, updateCart } from "../scripts/bigConstants";
|
||||||
|
import type { CartItem, MovieCartItem, SnackCartItem } from "../scripts/bigConstants";
|
||||||
|
|
||||||
function formatEuro(value: number) {
|
function formatEuro(value: number) {
|
||||||
return `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`;
|
return `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(value: any) {
|
function escapeHtml(value: string | number | undefined | null) {
|
||||||
return String(value || "")
|
return String(value ?? "")
|
||||||
.replaceAll("&", "&")
|
.replaceAll("&", "&")
|
||||||
.replaceAll("<", "<")
|
.replaceAll("<", "<")
|
||||||
.replaceAll(">", ">")
|
.replaceAll(">", ">")
|
||||||
@@ -52,52 +53,60 @@
|
|||||||
.replaceAll("'", "'");
|
.replaceAll("'", "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildCartKey(item: any) {
|
function buildCartKey(item: CartItem) {
|
||||||
const infoText = item.category === "movie"
|
const infoText = item.category === "movie"
|
||||||
? `Sitz: ${item.seatId} (${item.hall})`
|
? `Sitz: ${(item as MovieCartItem).seatId} (${item.hall})`
|
||||||
: item.time;
|
: item.time;
|
||||||
return `${item.title}-${item.hall}-${infoText}`;
|
return `${item.title}-${item.hall}-${infoText}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDrinkItem(item: any) {
|
function isDrinkItem(item: CartItem): item is SnackCartItem {
|
||||||
if (item.category !== "snack") return false;
|
if (item.category !== "snack") return false;
|
||||||
const title = String(item.title || "").toLowerCase();
|
const title = item.title.toLowerCase();
|
||||||
const size = String(item.hall || "").toLowerCase();
|
const size = item.hall.toLowerCase();
|
||||||
const drinkKeywords = ["cola", "sprite", "fanta", "mezzo", "fuze", "wasser", "getraenk", "drink"];
|
const drinkKeywords = ["cola", "sprite", "fanta", "mezzo", "fuze", "wasser", "getraenk", "drink"];
|
||||||
return drinkKeywords.some((word) => title.includes(word)) || size.includes("l");
|
return drinkKeywords.some((word) => title.includes(word)) || size.includes("l");
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildItemInfo(item: any) {
|
function buildItemInfo(item: CartItem) {
|
||||||
if (item.category === "movie") {
|
if (item.category === "movie") {
|
||||||
|
const movie = item as MovieCartItem;
|
||||||
return `
|
return `
|
||||||
<div>Sitzplatz: ${escapeHtml(item.seatId || "-")}</div>
|
<div>Sitzplatz: ${escapeHtml(movie.seatId)}</div>
|
||||||
<div>Saal: ${escapeHtml(item.hall || "-")}</div>
|
<div>Saal: ${escapeHtml(movie.hall)}</div>
|
||||||
<div>Uhrzeit: ${escapeHtml(item.time || "-")} Uhr</div>
|
<div>Uhrzeit: ${escapeHtml(movie.time)} Uhr</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
if (isDrinkItem(item)) {
|
if (isDrinkItem(item)) {
|
||||||
return `
|
return `
|
||||||
<div>Variante: ${escapeHtml(item.time || "-")}</div>
|
<div>Variante: ${escapeHtml(item.time)}</div>
|
||||||
<div>Groesse: ${escapeHtml(item.hall || "-")}</div>
|
<div>Groesse: ${escapeHtml(item.hall)}</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
return `
|
return `
|
||||||
<div>Kategorie: Snack</div>
|
<div>Kategorie: Snack</div>
|
||||||
<div>Variante: ${escapeHtml(item.time || "-")}</div>
|
<div>Variante: ${escapeHtml(item.time)}</div>
|
||||||
<div>Groesse: ${escapeHtml(item.hall || "-")}</div>
|
<div>Groesse: ${escapeHtml(item.hall)}</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupCartItems() {
|
interface CartGroup {
|
||||||
const groups = new Map();
|
key: string;
|
||||||
cart.forEach((item: any) => {
|
quantity: number;
|
||||||
|
total: number;
|
||||||
|
item: CartItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupCartItems(): CartGroup[] {
|
||||||
|
const groups = new Map<string, CartGroup>();
|
||||||
|
cart.forEach((item: CartItem) => {
|
||||||
const key = buildCartKey(item);
|
const key = buildCartKey(item);
|
||||||
if (!groups.has(key)) {
|
if (!groups.has(key)) {
|
||||||
groups.set(key, { key, quantity: 0, total: 0, item });
|
groups.set(key, { key, quantity: 0, total: 0, item });
|
||||||
}
|
}
|
||||||
const group = groups.get(key);
|
const group = groups.get(key)!;
|
||||||
group.quantity += 1;
|
group.quantity += 1;
|
||||||
group.total += Number(item.price || 0);
|
group.total += item.price;
|
||||||
});
|
});
|
||||||
return Array.from(groups.values());
|
return Array.from(groups.values());
|
||||||
}
|
}
|
||||||
@@ -128,7 +137,7 @@
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const rows = groupedItems.map((group) => {
|
const rows = groupedItems.map((group: CartGroup) => {
|
||||||
const imageHtml = group.item.img
|
const imageHtml = group.item.img
|
||||||
? `<img class="cart-img-small" src="${escapeHtml(group.item.img)}" alt="${escapeHtml(group.item.title)}">`
|
? `<img class="cart-img-small" src="${escapeHtml(group.item.img)}" alt="${escapeHtml(group.item.title)}">`
|
||||||
: `<div class="cart-img-fallback">Kein Bild</div>`;
|
: `<div class="cart-img-fallback">Kein Bild</div>`;
|
||||||
@@ -158,7 +167,7 @@
|
|||||||
|
|
||||||
cartList.innerHTML = header + rows;
|
cartList.innerHTML = header + rows;
|
||||||
|
|
||||||
const total = cart.reduce((sum, item) => sum + Number(item.price || 0), 0);
|
const total = cart.reduce((sum: number, item: CartItem) => sum + item.price, 0);
|
||||||
const vat = total - total / 1.19;
|
const vat = total - total / 1.19;
|
||||||
totalEl.innerText = formatEuro(total);
|
totalEl.innerText = formatEuro(total);
|
||||||
vatEl.innerText = `inkl. 19% MwSt: ${formatEuro(vat)}`;
|
vatEl.innerText = `inkl. 19% MwSt: ${formatEuro(vat)}`;
|
||||||
@@ -167,25 +176,26 @@
|
|||||||
window.addEventListener("cart-updated", renderCart);
|
window.addEventListener("cart-updated", renderCart);
|
||||||
renderCart();
|
renderCart();
|
||||||
|
|
||||||
document.getElementById("cart-view")?.addEventListener("click", (event: any) => {
|
document.getElementById("cart-view")?.addEventListener("click", (event: Event) => {
|
||||||
const deleteBtn = event.target.closest(".btn-delete-item");
|
const target = event.target as HTMLElement;
|
||||||
|
const deleteBtn = target.closest(".btn-delete-item") as HTMLElement | null;
|
||||||
if (deleteBtn) {
|
if (deleteBtn) {
|
||||||
const key = deleteBtn.dataset.key;
|
const key = deleteBtn.dataset.key;
|
||||||
const newCart = cart.filter(item => buildCartKey(item) !== key);
|
const newCart = cart.filter((item: CartItem) => buildCartKey(item) !== key);
|
||||||
updateCart(newCart);
|
updateCart(newCart);
|
||||||
window.dispatchEvent(new CustomEvent("cart-updated"));
|
window.dispatchEvent(new CustomEvent("cart-updated"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const qtyBtn = event.target.closest(".btn-qty");
|
const qtyBtn = target.closest(".btn-qty") as HTMLElement | null;
|
||||||
if (qtyBtn) {
|
if (qtyBtn) {
|
||||||
const action = qtyBtn.dataset.action;
|
const action = qtyBtn.dataset.action;
|
||||||
const key = qtyBtn.dataset.key;
|
const key = qtyBtn.dataset.key;
|
||||||
if (action === "plus") {
|
if (action === "plus") {
|
||||||
const item = cart.find(i => buildCartKey(i) === key);
|
const item = cart.find((i: CartItem) => buildCartKey(i) === key);
|
||||||
if (item) cart.push({...item, id: Date.now() + Math.random()});
|
if (item) cart.push({ ...item, id: Date.now() + Math.random() });
|
||||||
} else {
|
} else {
|
||||||
const idx = cart.findIndex(i => buildCartKey(i) === key);
|
const idx = cart.findIndex((i: CartItem) => buildCartKey(i) === key);
|
||||||
if (idx !== -1) cart.splice(idx, 1);
|
if (idx !== -1) cart.splice(idx, 1);
|
||||||
}
|
}
|
||||||
updateCart(cart);
|
updateCart(cart);
|
||||||
|
|||||||
@@ -53,6 +53,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { currentUser, users, cart, emptyCart, occupiedSeatsData, updateCart, updateOccupiedSeats } from "../scripts/bigConstants";
|
import { currentUser, users, cart, emptyCart, occupiedSeatsData, updateCart, updateOccupiedSeats } from "../scripts/bigConstants";
|
||||||
|
import type { CartItem, MovieCartItem } from "../scripts/bigConstants";
|
||||||
|
|
||||||
function formatCheckoutEuro(value: number) {
|
function formatCheckoutEuro(value: number) {
|
||||||
return `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`;
|
return `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`;
|
||||||
@@ -90,15 +91,15 @@
|
|||||||
if (!summaryList) return;
|
if (!summaryList) return;
|
||||||
summaryList.innerHTML = "";
|
summaryList.innerHTML = "";
|
||||||
|
|
||||||
const total = cart.reduce((sum: number, item: any) => sum + Number(item.price || 0), 0);
|
const total = cart.reduce((sum: number, item: CartItem) => sum + item.price, 0);
|
||||||
const vat = total - total / 1.19;
|
const vat = total - total / 1.19;
|
||||||
|
|
||||||
cart.forEach((item: any) => {
|
cart.forEach((item: CartItem) => {
|
||||||
const row = document.createElement("div");
|
const row = document.createElement("div");
|
||||||
row.style.cssText = "display:flex; justify-content:space-between; gap:12px; margin-bottom:10px; font-size:0.95rem;";
|
row.style.cssText = "display:flex; justify-content:space-between; gap:12px; margin-bottom:10px; font-size:0.95rem;";
|
||||||
const infoText = item.category === "movie"
|
const infoText = item.category === "movie"
|
||||||
? `Sitz ${item.seatId || "-"} | ${item.hall || "-"} | ${item.time || "-"} Uhr`
|
? `Sitz ${(item as MovieCartItem).seatId} | ${item.hall} | ${item.time} Uhr`
|
||||||
: `${item.time || "Standard"} | ${item.hall || "-"}`;
|
: `${item.time || "Standard"} | ${item.hall}`;
|
||||||
row.innerHTML = `<span>${item.title} (${infoText})</span><span>${formatCheckoutEuro(item.price)}</span>`;
|
row.innerHTML = `<span>${item.title} (${infoText})</span><span>${formatCheckoutEuro(item.price)}</span>`;
|
||||||
summaryList.appendChild(row);
|
summaryList.appendChild(row);
|
||||||
});
|
});
|
||||||
@@ -116,16 +117,16 @@
|
|||||||
const ticketContainer = document.getElementById("ticket-container");
|
const ticketContainer = document.getElementById("ticket-container");
|
||||||
if (!ticketContainer) return;
|
if (!ticketContainer) return;
|
||||||
|
|
||||||
const moviesInCart = cart.filter((item: any) => item.category === "movie");
|
const moviesInCart = cart.filter((item: CartItem): item is MovieCartItem => item.category === "movie");
|
||||||
if (!moviesInCart.length) {
|
if (!moviesInCart.length) {
|
||||||
ticketContainer.innerHTML = "<p>Danke für deinen Einkauf!</p>";
|
ticketContainer.innerHTML = "<p>Danke für deinen Einkauf!</p>";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mainMovie = moviesInCart[0];
|
const mainMovie: MovieCartItem = moviesInCart[0];
|
||||||
const matchingMovieSeats = moviesInCart
|
const matchingMovieSeats = moviesInCart
|
||||||
.filter((item: any) => item.title === mainMovie.title && item.time === mainMovie.time)
|
.filter((item: MovieCartItem) => item.title === mainMovie.title && item.time === mainMovie.time)
|
||||||
.map((item: any) => item.seatId)
|
.map((item: MovieCartItem) => item.seatId)
|
||||||
.join(", ");
|
.join(", ");
|
||||||
|
|
||||||
const qrData = encodeURIComponent(`EAGLE-IMAX|${mainMovie.title}|${mainMovie.hall}|${matchingMovieSeats}`);
|
const qrData = encodeURIComponent(`EAGLE-IMAX|${mainMovie.title}|${mainMovie.hall}|${matchingMovieSeats}`);
|
||||||
@@ -154,12 +155,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function completeCheckout() {
|
function completeCheckout() {
|
||||||
const orderItems = [...cart];
|
const orderItems: CartItem[] = [...cart];
|
||||||
const orderTotal = orderItems.reduce((sum, item) => sum + Number(item.price || 0), 0);
|
const orderTotal = orderItems.reduce((sum: number, item: CartItem) => sum + item.price, 0);
|
||||||
|
|
||||||
// Save order for current user
|
|
||||||
if (currentUser && Array.isArray(users)) {
|
if (currentUser && Array.isArray(users)) {
|
||||||
const userIndex = users.findIndex((entry: any) => entry.email === currentUser.email);
|
const userIndex = users.findIndex((entry: any) => entry.email === (currentUser as any).email);
|
||||||
if (userIndex !== -1) {
|
if (userIndex !== -1) {
|
||||||
if (!Array.isArray(users[userIndex].orders)) users[userIndex].orders = [];
|
if (!Array.isArray(users[userIndex].orders)) users[userIndex].orders = [];
|
||||||
users[userIndex].orders.push({
|
users[userIndex].orders.push({
|
||||||
@@ -172,8 +172,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reserve seats
|
orderItems
|
||||||
orderItems.filter(item => item.category === "movie").forEach(item => {
|
.filter((item: CartItem): item is MovieCartItem => item.category === "movie")
|
||||||
|
.forEach((item: MovieCartItem) => {
|
||||||
const key = `${item.hall}-${item.time}`;
|
const key = `${item.hall}-${item.time}`;
|
||||||
if (!occupiedSeatsData[key]) occupiedSeatsData[key] = [];
|
if (!occupiedSeatsData[key]) occupiedSeatsData[key] = [];
|
||||||
occupiedSeatsData[key].push(item.seatId);
|
occupiedSeatsData[key].push(item.seatId);
|
||||||
|
|||||||
@@ -450,6 +450,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { cart, updateCart } from "../scripts/bigConstants";
|
import { cart, updateCart } from "../scripts/bigConstants";
|
||||||
|
import type { SnackCartItem } from "../scripts/bigConstants";
|
||||||
|
|
||||||
const snacksView = document.getElementById("snacks-view");
|
const snacksView = document.getElementById("snacks-view");
|
||||||
|
|
||||||
@@ -471,7 +472,7 @@
|
|||||||
const activeOption = snackCard.querySelector(".opt-btn.active") as HTMLElement;
|
const activeOption = snackCard.querySelector(".opt-btn.active") as HTMLElement;
|
||||||
const variantVal = activeOption ? activeOption.innerText : "Normal";
|
const variantVal = activeOption ? activeOption.innerText : "Normal";
|
||||||
|
|
||||||
cart.push({
|
const snackItem: SnackCartItem = {
|
||||||
id: Date.now() + Math.random(),
|
id: Date.now() + Math.random(),
|
||||||
category: "snack",
|
category: "snack",
|
||||||
title: snackTitle,
|
title: snackTitle,
|
||||||
@@ -480,7 +481,8 @@
|
|||||||
type: "SNACK",
|
type: "SNACK",
|
||||||
price: priceVal,
|
price: priceVal,
|
||||||
img: snackImg
|
img: snackImg
|
||||||
});
|
};
|
||||||
|
cart.push(snackItem);
|
||||||
|
|
||||||
updateCart(cart);
|
updateCart(cart);
|
||||||
window.dispatchEvent(new CustomEvent("cart-updated"));
|
window.dispatchEvent(new CustomEvent("cart-updated"));
|
||||||
|
|||||||
@@ -27,12 +27,12 @@ export const timePatterns = [
|
|||||||
export const weekdayShort = ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"];
|
export const weekdayShort = ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"];
|
||||||
|
|
||||||
// Shared State
|
// Shared State
|
||||||
export const cart: unknown[] = JSON.parse(typeof window !== 'undefined' ? (localStorage.getItem("eagleCart") || '[]') : '[]');
|
export const cart: CartItem[] = JSON.parse(typeof window !== 'undefined' ? (localStorage.getItem("eagleCart") || '[]') : '[]');
|
||||||
export let occupiedSeatsData = JSON.parse(typeof window !== 'undefined' ? (localStorage.getItem("eagleOccupied") || '{}') : '{}');
|
export let occupiedSeatsData = JSON.parse(typeof window !== 'undefined' ? (localStorage.getItem("eagleOccupied") || '{}') : '{}');
|
||||||
export const users: unknown[] = JSON.parse(typeof window !== 'undefined' ? (localStorage.getItem("eagleUsers") || '[]') : '[]');
|
export const users: User[] = JSON.parse(typeof window !== 'undefined' ? (localStorage.getItem("eagleUsers") || '[]') : '[]');
|
||||||
export let currentUser: unknown = JSON.parse(typeof window !== 'undefined' ? (localStorage.getItem("currentUser") || 'null') : 'null');
|
export let currentUser: User | null = JSON.parse(typeof window !== 'undefined' ? (localStorage.getItem("currentUser") || 'null') : 'null');
|
||||||
|
|
||||||
export function updateCart(newCart: unknown[]) {
|
export function updateCart(newCart: CartItem[]) {
|
||||||
cart.splice(0, cart.length, ...newCart);
|
cart.splice(0, cart.length, ...newCart);
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
localStorage.setItem("eagleCart", JSON.stringify(cart));
|
localStorage.setItem("eagleCart", JSON.stringify(cart));
|
||||||
@@ -57,7 +57,7 @@ export function persistUsers() {
|
|||||||
if (typeof window !== 'undefined') localStorage.setItem("eagleUsers", JSON.stringify(users));
|
if (typeof window !== 'undefined') localStorage.setItem("eagleUsers", JSON.stringify(users));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function persistCurrentUser(user: unknown) {
|
export function persistCurrentUser(user: User | null) {
|
||||||
currentUser = user;
|
currentUser = user;
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
if (currentUser) localStorage.setItem("currentUser", JSON.stringify(currentUser));
|
if (currentUser) localStorage.setItem("currentUser", JSON.stringify(currentUser));
|
||||||
@@ -65,6 +65,30 @@ export function persistCurrentUser(user: unknown) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BaseCartItem {
|
||||||
|
id: number;
|
||||||
|
category: "movie" | "snack";
|
||||||
|
title: string;
|
||||||
|
/** Hall name for movies; size label (e.g. "0,33L") for snacks */
|
||||||
|
hall: string;
|
||||||
|
/** Showtime for movies; flavour/variant for snacks */
|
||||||
|
time: string;
|
||||||
|
price: number;
|
||||||
|
img?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MovieCartItem extends BaseCartItem {
|
||||||
|
category: "movie";
|
||||||
|
seatId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SnackCartItem extends BaseCartItem {
|
||||||
|
category: "snack";
|
||||||
|
type: "SNACK";
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CartItem = MovieCartItem | SnackCartItem;
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user