447 lines
14 KiB
JavaScript
447 lines
14 KiB
JavaScript
import { endpoints } from "./config.js";
|
|
import { getLang, onLangChange, t } from "./i18n.js";
|
|
import { trackEvent } from "./analytics.js";
|
|
|
|
let itemModalKeys = [];
|
|
let itemModalIndex = -1;
|
|
let itemModalTouchStart = null;
|
|
let itemModalDragging = false;
|
|
let itemModalAnimating = false;
|
|
|
|
function escapeHtml(value) {
|
|
return String(value ?? "")
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
function escapeAttr(value) {
|
|
return escapeHtml(value).replace(/`/g, "`");
|
|
}
|
|
|
|
function resetItemModalPane() {
|
|
const pane = document.getElementById("itemModalPane");
|
|
if (!pane) return;
|
|
pane.classList.remove(
|
|
"is-dragging",
|
|
"is-exiting-left",
|
|
"is-exiting-right",
|
|
"is-entering-from-left",
|
|
"is-entering-from-right"
|
|
);
|
|
pane.style.transform = "";
|
|
pane.style.opacity = "";
|
|
}
|
|
|
|
function waitForPaneTransition(pane, timeoutMs = 400) {
|
|
return new Promise((resolve) => {
|
|
let settled = false;
|
|
const finish = () => {
|
|
if (settled) return;
|
|
settled = true;
|
|
pane.removeEventListener("transitionend", onEnd);
|
|
resolve();
|
|
};
|
|
const onEnd = (e) => {
|
|
if (e.target === pane) finish();
|
|
};
|
|
pane.addEventListener("transitionend", onEnd);
|
|
setTimeout(finish, timeoutMs);
|
|
});
|
|
}
|
|
|
|
function isValidMenuImageUrl(url) {
|
|
return typeof url === "string" && url.trim().length > 0;
|
|
}
|
|
|
|
export function handleMenuImageError(imgEl) {
|
|
if (!imgEl) return;
|
|
imgEl.onerror = null;
|
|
imgEl.classList.add("hidden");
|
|
const wrap = imgEl.closest(".rmc-image-wrap, .item-modal-image-wrap");
|
|
const placeholder = wrap?.querySelector(".menu-image-placeholder");
|
|
if (placeholder) placeholder.classList.remove("hidden");
|
|
}
|
|
|
|
function applyMenuItemImage(imgEl, placeholderEl, url) {
|
|
if (!imgEl || !placeholderEl) return;
|
|
|
|
if (!isValidMenuImageUrl(url)) {
|
|
imgEl.src = "";
|
|
imgEl.classList.add("hidden");
|
|
placeholderEl.classList.remove("hidden");
|
|
return;
|
|
}
|
|
|
|
imgEl.onload = () => {
|
|
imgEl.classList.remove("hidden");
|
|
placeholderEl.classList.add("hidden");
|
|
};
|
|
imgEl.onerror = () => handleMenuImageError(imgEl);
|
|
imgEl.classList.remove("hidden");
|
|
placeholderEl.classList.add("hidden");
|
|
imgEl.src = url;
|
|
}
|
|
|
|
function renderMenuListImage(url) {
|
|
const hasUrl = isValidMenuImageUrl(url);
|
|
const imgClass = hasUrl ? "rmc-image" : "rmc-image hidden";
|
|
const placeholderClass = hasUrl ? "menu-image-placeholder hidden" : "menu-image-placeholder";
|
|
const srcAttr = hasUrl ? ` src="${escapeAttr(url)}"` : "";
|
|
const onerror = hasUrl ? ' onerror="handleMenuImageError(this)"' : "";
|
|
|
|
return `<div class="rmc-image-wrap"><img class="${imgClass}"${srcAttr} alt="" loading="lazy"${onerror}><div class="${placeholderClass}" aria-hidden="true"><svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span>${escapeHtml(t("menu.no_image"))}</span></div></div>`;
|
|
}
|
|
|
|
function findMenuItem(categoryId, position) {
|
|
if (!window.menuDataRaw) return null;
|
|
for (const cat of window.menuDataRaw) {
|
|
for (const item of cat.items || []) {
|
|
if (item.categoryId == categoryId && item.position == position) {
|
|
return item;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function buildVisibleMenuItemKeys() {
|
|
const keys = [];
|
|
document.querySelectorAll(".rmc-position").forEach((el) => {
|
|
if (el.style.display === "none") return;
|
|
const category = el.closest(".rm-category");
|
|
if (category && category.style.display === "none") return;
|
|
keys.push({
|
|
categoryId: el.getAttribute("data-category-id"),
|
|
position: el.getAttribute("data-position"),
|
|
});
|
|
});
|
|
return keys;
|
|
}
|
|
|
|
function updateItemModalNavigation() {
|
|
const total = itemModalKeys.length;
|
|
const counter = document.getElementById("itemModalCounter");
|
|
if (counter) counter.textContent = total > 1 ? `${itemModalIndex + 1} / ${total}` : "";
|
|
}
|
|
|
|
function populateItemModal(item) {
|
|
const imgEl = document.getElementById("itemModalImage");
|
|
const placeholderEl = document.getElementById("itemModalImagePlaceholder");
|
|
const titleEl = document.getElementById("itemModalTitle");
|
|
const descEl = document.getElementById("itemModalDesc");
|
|
const priceEl = document.getElementById("itemModalPrice");
|
|
|
|
applyMenuItemImage(imgEl, placeholderEl, item.image);
|
|
if (titleEl) titleEl.textContent = item.title;
|
|
if (descEl) descEl.textContent = item.description || "";
|
|
if (priceEl) priceEl.textContent = item.price;
|
|
updateItemModalNavigation();
|
|
}
|
|
|
|
export async function navigateItemModal(delta) {
|
|
if (!itemModalKeys.length || !delta || itemModalAnimating) return;
|
|
const nextIndex = itemModalIndex + delta;
|
|
if (nextIndex < 0 || nextIndex >= itemModalKeys.length) return;
|
|
|
|
const key = itemModalKeys[nextIndex];
|
|
const item = findMenuItem(key.categoryId, key.position);
|
|
const pane = document.getElementById("itemModalPane");
|
|
if (!item || !pane) return;
|
|
|
|
itemModalAnimating = true;
|
|
resetItemModalPane();
|
|
|
|
const exitClass = delta > 0 ? "is-exiting-left" : "is-exiting-right";
|
|
const enterClass = delta > 0 ? "is-entering-from-right" : "is-entering-from-left";
|
|
|
|
pane.classList.add(exitClass);
|
|
await waitForPaneTransition(pane);
|
|
|
|
itemModalIndex = nextIndex;
|
|
populateItemModal(item);
|
|
|
|
pane.classList.remove(exitClass);
|
|
pane.classList.add(enterClass);
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(() => {
|
|
pane.classList.remove(enterClass);
|
|
});
|
|
});
|
|
|
|
await waitForPaneTransition(pane);
|
|
itemModalAnimating = false;
|
|
}
|
|
|
|
export function bindItemModalSwipe() {
|
|
const content = document.getElementById("itemModalContent");
|
|
const pane = document.getElementById("itemModalPane");
|
|
const modal = document.getElementById("itemModal");
|
|
if (!content || !pane || !modal || content.dataset.swipeBound) return;
|
|
content.dataset.swipeBound = "1";
|
|
|
|
content.addEventListener(
|
|
"touchstart",
|
|
(e) => {
|
|
if (itemModalAnimating || itemModalKeys.length <= 1 || e.touches.length !== 1) return;
|
|
if (e.target.closest("button")) return;
|
|
const touch = e.touches[0];
|
|
itemModalTouchStart = { x: touch.clientX, y: touch.clientY };
|
|
itemModalDragging = false;
|
|
},
|
|
{ passive: true }
|
|
);
|
|
|
|
content.addEventListener(
|
|
"touchmove",
|
|
(e) => {
|
|
if (!itemModalTouchStart || itemModalAnimating || itemModalKeys.length <= 1) return;
|
|
const touch = e.touches[0];
|
|
const dx = touch.clientX - itemModalTouchStart.x;
|
|
const dy = touch.clientY - itemModalTouchStart.y;
|
|
|
|
if (!itemModalDragging) {
|
|
if (Math.abs(dx) > 12 && Math.abs(dx) > Math.abs(dy)) {
|
|
itemModalDragging = true;
|
|
pane.classList.add("is-dragging");
|
|
} else if (Math.abs(dy) > 12) {
|
|
itemModalTouchStart = null;
|
|
return;
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
|
|
let translateX = dx;
|
|
if (itemModalIndex <= 0 && dx > 0) translateX = dx * 0.3;
|
|
if (itemModalIndex >= itemModalKeys.length - 1 && dx < 0) translateX = dx * 0.3;
|
|
|
|
pane.style.transform = `translateX(${translateX}px)`;
|
|
pane.style.opacity = String(1 - Math.min(Math.abs(translateX) / 320, 0.18));
|
|
},
|
|
{ passive: true }
|
|
);
|
|
|
|
content.addEventListener(
|
|
"touchend",
|
|
(e) => {
|
|
if (!itemModalTouchStart) return;
|
|
const touch = e.changedTouches[0];
|
|
const dx = touch.clientX - itemModalTouchStart.x;
|
|
const dy = touch.clientY - itemModalTouchStart.y;
|
|
const wasDragging = itemModalDragging;
|
|
|
|
itemModalTouchStart = null;
|
|
itemModalDragging = false;
|
|
|
|
if (!wasDragging) return;
|
|
|
|
pane.classList.remove("is-dragging");
|
|
pane.style.transform = "";
|
|
pane.style.opacity = "";
|
|
|
|
if (Math.abs(dx) >= 50 && Math.abs(dx) > Math.abs(dy)) {
|
|
navigateItemModal(dx < 0 ? 1 : -1);
|
|
}
|
|
},
|
|
{ passive: true }
|
|
);
|
|
|
|
document.addEventListener("keydown", (e) => {
|
|
if (!modal.classList.contains("active")) return;
|
|
if (e.key === "ArrowRight") navigateItemModal(1);
|
|
if (e.key === "ArrowLeft") navigateItemModal(-1);
|
|
if (e.key === "Escape") closeItemModal();
|
|
});
|
|
}
|
|
|
|
function renderCategoryNav(categories) {
|
|
const ul = document.querySelector(".menu-categories-nav ul");
|
|
if (!ul) return;
|
|
|
|
const list = Array.isArray(categories) ? categories : [];
|
|
let html = `<li><a href="#" class="active" data-category-badge="0" onclick="showCategory(0); return false;">${escapeHtml(t("menu.all"))}</a></li>`;
|
|
|
|
list.forEach((cat) => {
|
|
const id = String(cat?.id ?? "").trim();
|
|
const name = String(cat?.name ?? "").trim();
|
|
if (!id || !name) return;
|
|
html += `<li><a href="#" data-category-badge="${escapeAttr(id)}" onclick="showCategory('${escapeAttr(id)}'); return false;">${escapeHtml(name)}</a></li>`;
|
|
});
|
|
|
|
ul.innerHTML = html;
|
|
}
|
|
|
|
function showMenuLoadError(container) {
|
|
if (!container) return;
|
|
container.innerHTML = `<p style="text-align:center; padding: 20px; color: var(--text-muted);">${escapeHtml(t("menu.load_error"))}</p>`;
|
|
}
|
|
|
|
function getActiveCategoryId() {
|
|
return (
|
|
document.querySelector(".menu-categories-nav a.active")?.getAttribute("data-category-badge") ||
|
|
"0"
|
|
);
|
|
}
|
|
|
|
export async function loadMenu(lang = getLang()) {
|
|
const container = document.getElementById("menuContainer");
|
|
const previousCategoryId = getActiveCategoryId();
|
|
try {
|
|
const response = await fetch(`${endpoints.menu}?lang=${encodeURIComponent(lang)}`);
|
|
if (!response.ok) throw new Error(t("menu.load_error"));
|
|
const payload = await response.json();
|
|
|
|
if (payload.status !== "success" || !Array.isArray(payload.sections)) {
|
|
throw new Error(t("menu.load_error"));
|
|
}
|
|
|
|
const sections = payload.sections;
|
|
window.menuDataRaw = sections;
|
|
if (!container) return;
|
|
|
|
container.innerHTML = "";
|
|
|
|
sections.forEach((section) => {
|
|
const items = Array.isArray(section.items) ? section.items : [];
|
|
const catId = items.length > 0 ? items[0].categoryId : "";
|
|
|
|
const catDiv = document.createElement("div");
|
|
catDiv.className = "rm-category";
|
|
catDiv.setAttribute("data-cat-id", catId);
|
|
|
|
let html = `<div class="restaurant-menu-category">${escapeHtml(section.categoryName || "")}</div>
|
|
<div class="rmc-positions">`;
|
|
|
|
items.forEach((item) => {
|
|
const categoryId = escapeAttr(item.categoryId);
|
|
const position = escapeAttr(item.position);
|
|
html += `
|
|
<div class="rmc-position" data-position="${position}" data-category-id="${categoryId}" onclick="openItemModal('${categoryId}', '${position}')" style="cursor: pointer;">
|
|
${renderMenuListImage(item.image)}
|
|
<div class="rmc-title">
|
|
<h4>${escapeHtml(item.title)}<span>${escapeHtml(item.description || "")}</span></h4>
|
|
</div>
|
|
<div class="rmc-other"><span>${escapeHtml(item.price)}</span></div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
html += `</div>`;
|
|
catDiv.innerHTML = html;
|
|
container.appendChild(catDiv);
|
|
});
|
|
|
|
renderCategoryNav(payload.categories);
|
|
|
|
const safePrev = String(previousCategoryId).replace(/"/g, "");
|
|
const categoryStillExists =
|
|
safePrev === "0" ||
|
|
!!document.querySelector(`.menu-categories-nav a[data-category-badge="${safePrev}"]`);
|
|
showCategory(categoryStillExists ? previousCategoryId : 0);
|
|
|
|
const searchInput = document.getElementById("menuSearchInput");
|
|
if (searchInput?.value) {
|
|
filterMenu();
|
|
}
|
|
} catch (err) {
|
|
console.error("Błąd ładowania menu:", err);
|
|
showMenuLoadError(container);
|
|
}
|
|
}
|
|
|
|
onLangChange((lang) => loadMenu(lang));
|
|
|
|
export function openItemModal(categoryId, position) {
|
|
itemModalKeys = buildVisibleMenuItemKeys();
|
|
itemModalIndex = itemModalKeys.findIndex(
|
|
(key) => key.categoryId == categoryId && key.position == position
|
|
);
|
|
|
|
if (itemModalIndex < 0) {
|
|
itemModalKeys = [{ categoryId, position }];
|
|
itemModalIndex = 0;
|
|
}
|
|
|
|
const foundItem = findMenuItem(categoryId, position);
|
|
if (!foundItem) return;
|
|
|
|
populateItemModal(foundItem);
|
|
resetItemModalPane();
|
|
|
|
const modal = document.getElementById("itemModal");
|
|
if (modal) {
|
|
modal.classList.add("active");
|
|
document.body.style.overflow = "hidden";
|
|
}
|
|
}
|
|
|
|
export function closeItemModal() {
|
|
const modal = document.getElementById("itemModal");
|
|
if (modal) {
|
|
modal.classList.remove("active");
|
|
document.body.style.overflow = "";
|
|
}
|
|
itemModalTouchStart = null;
|
|
itemModalDragging = false;
|
|
itemModalAnimating = false;
|
|
resetItemModalPane();
|
|
}
|
|
|
|
export function filterMenu() {
|
|
const searchInput = document.getElementById("menuSearchInput");
|
|
const query = (searchInput?.value || "").toLowerCase();
|
|
const now = Date.now();
|
|
if (query.length >= 2 && (!window.lastMenuSearchEventAt || now - window.lastMenuSearchEventAt > 8000)) {
|
|
window.lastMenuSearchEventAt = now;
|
|
trackEvent("menu_search", { queryLength: query.length });
|
|
}
|
|
const categories = document.querySelectorAll(".rm-category");
|
|
|
|
categories.forEach((category) => {
|
|
let hasVisibleItems = false;
|
|
const items = category.querySelectorAll(".rmc-position");
|
|
|
|
items.forEach((item) => {
|
|
const title = item.querySelector(".rmc-title h4")?.textContent?.toLowerCase() || "";
|
|
if (title.includes(query)) {
|
|
item.style.display = "";
|
|
hasVisibleItems = true;
|
|
} else {
|
|
item.style.display = "none";
|
|
}
|
|
});
|
|
|
|
if (hasVisibleItems) {
|
|
category.style.display = "";
|
|
} else {
|
|
category.style.display = "none";
|
|
}
|
|
});
|
|
}
|
|
|
|
export function showCategory(categoryId) {
|
|
document.querySelectorAll(".menu-categories-nav a").forEach((a) => a.classList.remove("active"));
|
|
|
|
const clickedLink = document.querySelector(`.menu-categories-nav a[data-category-badge="${categoryId}"]`);
|
|
if (clickedLink) {
|
|
clickedLink.classList.add("active");
|
|
clickedLink.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" });
|
|
}
|
|
|
|
const showAll = categoryId === 0 || categoryId === "0";
|
|
const targetId = String(categoryId);
|
|
|
|
document.querySelectorAll(".rm-category").forEach((section) => {
|
|
let hasVisible = false;
|
|
section.querySelectorAll(".rmc-position").forEach((item) => {
|
|
const match = showAll || String(item.getAttribute("data-category-id")) === targetId;
|
|
item.style.display = match ? "" : "none";
|
|
if (match) hasVisible = true;
|
|
});
|
|
section.style.display = hasVisible ? "" : "none";
|
|
});
|
|
}
|