/* =========================================================
MPS Engineering 2026 - minimal vanilla JS
========================================================= */
(function(){
const $ = (s, root=document) => root.querySelector(s);
const $$ = (s, root=document) => Array.from(root.querySelectorAll(s));
const STORAGE_KEY = "mps_catalog_products";
const TELEGRAM = "https://t.me/yourcompany";
const VIBER = "viber://chat?number=%2B380975058913";
const menuToggle = $("#menuToggle");
const mainNav = $("#mainNav");
menuToggle?.addEventListener("click", () => {
const open = mainNav.classList.toggle("open");
menuToggle.setAttribute("aria-expanded", String(open));
});
$$("#mainNav a").forEach(a => a.addEventListener("click", () => mainNav?.classList.remove("open")));
const baseProducts = [];
function getAdminProducts(){ return []; }
function catName(cat){
return {canopy:"Навіси", frame:"Каркаси", truss:"Ферми / балки", stairs:"Сходи", fence:"Ворота / огорожі", other:"Інше"}[cat] || "Інше";
}
function iconSvg(cat){
const stroke = "currentColor";
if(cat === "stairs") return ``;
if(cat === "fence") return ``;
if(cat === "truss") return ``;
return ``;
}
function productCard(p){
const img = p.image ? `
` : `
${iconSvg(p.cat || p.category || "frame")}
`;
const cat = p.cat || p.category || "other";
const price = p.price ? `від $${Number(p.price).toLocaleString("en-US")}` : "від за запитом";
const text = `${p.name} ${p.desc || p.description || ""} ${catName(cat)} ${(p.specs || []).join(" ")}`.toLowerCase();
return `
${img}${p.status || "Під замовлення"}
${catName(cat)}
${p.name}
${p.desc || p.description || "Виготовлення під ваші розміри та умови монтажу."}
${(p.specs || ["під розмір", "монтаж", "гарантія"]).map(s=>`${s}`).join("")}
`;
}
function allProducts(){ return []; }
function renderProducts(){
const grid = $("#productGrid"); if(!grid) return;
grid.innerHTML = allProducts().map(productCard).join("");
filterProducts();
}
function filterProducts(){
const grid = $("#productGrid"); if(!grid) return;
const q = ($("#catalogSearch")?.value || "").trim().toLowerCase();
const cat = $("#catalogFilter")?.value || "all";
let visible = 0;
$$(".product-card", grid).forEach(card => {
const ok = (!q || (card.dataset.text || "").includes(q)) && (cat === "all" || card.dataset.cat === cat);
card.style.display = ok ? "" : "none"; if(ok) visible++;
});
const empty = $("#catalogEmpty"); if(empty) empty.hidden = true;
}
$("#catalogSearch")?.addEventListener("input", filterProducts);
$("#catalogFilter")?.addEventListener("change", filterProducts);
renderProducts();
const rates = { canopy: 2100, frame: 4200, truss: 2600, stairs: 5200, fence: 1800 };
const typeNames = { canopy:"Металевий навіс", frame:"Каркас ангара / складу", truss:"Ферми / балки", stairs:"Сходи / площадка", fence:"Ворота / огорожа" };
const material = { paint:1, zinc:1.25, premium:1.4, painted:1, galvanized:1.25, heavy:1.4 };
const materialNames = {
paint:"сталь + фарбування",
zinc:"оцинкована сталь",
premium:"посилена конструкція",
painted:"сталь + фарбування",
galvanized:"оцинкована сталь",
heavy:"посилена конструкція"
};
function calc(){
const priceEl = $("#calcPrice");
const summaryEl = $("#calcSummary");
if (!priceEl || !summaryEl) {
return {summary:"", price:""};
}
const type = $("#calcType")?.value || "canopy";
const mat = $("#calcMaterial")?.value || "painted";
const width = Math.max(1, Number($("#calcWidth")?.value || 1));
const length = Math.max(1, Number($("#calcLength")?.value || 1));
const complexity = Number($("#calcComplexity")?.value || 1);
const area = width * length;
const base = area * rates[type] * material[mat] * complexity;
const low = Math.round(base * 0.9 / 1000) * 1000;
const high = Math.round(base * 1.25 / 1000) * 1000;
const fmt = n => n.toLocaleString("uk-UA");
const summary = `${typeNames[type]}, ${materialNames[mat]}, ${width} x ${length} м, площа ${area.toFixed(1)} м².`;
priceEl.textContent = `від ${fmt(low)} до ${fmt(high)} грн`;
summaryEl.textContent = summary;
const minField = $("#estimatedMin");
const maxField = $("#estimatedMax");
const areaField = $("#areaM2");
if (minField) minField.value = String(low);
if (maxField) maxField.value = String(high);
if (areaField) areaField.value = String(area.toFixed(2));
return {summary, price:`${fmt(low)}-${fmt(high)} грн`};
}
["#calcType","#calcMaterial","#calcWidth","#calcLength","#calcComplexity"].forEach(s => $(s)?.addEventListener("input", calc));
["#calcType","#calcMaterial","#calcComplexity"].forEach(s => $(s)?.addEventListener("change", calc));
calc();
$("#useCalcRequest")?.addEventListener("click", () => {
const result = calc();
const msg = $("#requestMessage");
if(msg) msg.value = `Параметри з калькулятора: ${result.summary} Орієнтовна вартість: ${result.price}`;
$("#contacts")?.scrollIntoView({behavior:"smooth", block:"start"});
setTimeout(() => $("#requestName")?.focus(), 500);
});
$$("[data-project-filter]").forEach(btn => btn.addEventListener("click", () => {
const type = btn.dataset.projectFilter;
$$("[data-project-filter]").forEach(b=>b.classList.toggle("active", b===btn));
$$(".project-card").forEach(card => { card.style.display = (type === "all" || card.dataset.type === type) ? "" : "none"; });
}));
})();
/* =========================================================
MySQL API integration for MPS Engineering
========================================================= */
(() => {
const API_PRODUCTS = "api/products.php";
const API_REQUESTS = "api/requests.php";
const secureFetch = (...args) => window.MPSSecurity ? window.MPSSecurity.fetch(...args) : fetch(...args);
const categoryLabels = {
canopy: "Навіси",
frame: "Каркаси",
beam: "Ферми / балки",
stairs: "Сходи",
fence: "Огорожі / ворота",
other: "Інше"
};
const statusLabels = {
available: "В наявності",
order: "Під замовлення"
};
function qs(selector, root = document) {
return root.querySelector(selector);
}
function qsa(selector, root = document) {
return Array.from(root.querySelectorAll(selector));
}
function money(value) {
if (!value && value !== 0) return "За запитом";
return "від " + Number(value).toLocaleString("uk-UA") + " грн";
}
function productGallery(product) {
return [
product.image_path,
...((product.gallery_images || []).filter(Boolean))
].filter(Boolean);
}
function productImage(product) {
if (product.image_path) {
return `
`;
}
return `
`;
}
function escapeHtml(text = "") {
return String(text)
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
function specsHtml(product) {
const specs = String(product.specs || "")
.split(";")
.map(s => s.trim())
.filter(Boolean)
.slice(0, 4);
if (product.material) specs.unshift(product.material);
if (product.size_info) specs.unshift(product.size_info);
return specs.slice(0, 4).map(s => `${escapeHtml(s)}`).join("");
}
function specsList(product) {
const specs = String(product.specs || "")
.split(";")
.map(s => s.trim())
.filter(Boolean);
if (product.material) specs.unshift("Матеріал: " + product.material);
if (product.size_info) specs.unshift("Розмір: " + product.size_info);
if (product.price_from) specs.push("Ціна: " + money(product.price_from));
return specs;
}
function materialKey(product) {
const text = String(product.material || product.specs || "").toLowerCase();
if (text.includes("цинк") || text.includes("оцинк")) return "zinc";
if (text.includes("посилен") || text.includes("антикор")) return "premium";
if (text.includes("фарб") || text.includes("сталь")) return "paint";
return "other";
}
function numericArea(product) {
const source = String(product.size_info || product.specs || "");
const nums = source.match(/\d+(?:[.,]\d+)?/g)?.map(n => Number(n.replace(",", "."))) || [];
if (nums.length >= 2) return nums[0] * nums[1];
if (nums.length === 1) return nums[0];
return 0;
}
function sizeBucket(product) {
const area = numericArea(product);
if (!area) return "unknown";
if (area < 20) return "small";
if (area <= 80) return "medium";
return "large";
}
function productCard(product) {
const status = product.status || "order";
const pdf = product.pdf_path
? `PDF / креслення`
: `PDF / характеристики`;
const gallery = productGallery(product);
const galleryCount = gallery.length || (product.image_path ? 1 : 0);
const specItems = specsHtml(product) || `розміри за запитомпокриття за запитомвага за запитом`;
const title = escapeHtml(product.title || "Товар");
const priceText = money(product.price_from);
return `
${statusLabels[status] || "Під замовлення"}
${galleryCount ? galleryCount + " фото" : "фото за запитом"}
${productImage(product)}
${categoryLabels[product.category] || "Інше"}
${statusLabels[status] || "Під замовлення"}
${title}
${escapeHtml(product.description || "Виготовлення під ваші розміри та умови монтажу.")}
${specItems}
Ціна${priceText}
${pdf}
`;
}
async function fetchProducts(params = {}) {
const query = new URLSearchParams(params);
const res = await fetch(API_PRODUCTS + (query.toString() ? "?" + query.toString() : ""), { cache: "no-store" });
const data = await res.json();
if (!data.ok) throw new Error(data.error || "Помилка завантаження товарів");
return data.products || [];
}
async function renderPublicProducts() {
const grids = [
qs("#productGrid"),
qs("#productsGrid"),
qs("#catalogGrid"),
qs("#homeCatalogGrid")
].filter(Boolean);
if (!grids.length) return;
const searchInput = qs("#catalogSearch") || qs("#search") || qs("#homeCatalogSearch");
const categorySelect = qs("#catalogFilter") || qs("#catalogCategory") || qs("#category") || qs("#homeCatalogCategory");
const materialSelect = qs("#materialFilter") || qs("#homeMaterialFilter");
const priceSelect = qs("#priceFilter") || qs("#homePriceFilter");
const sizeSelect = qs("#sizeFilter") || qs("#homeSizeFilter");
let currentProducts = [];
const selectedCompare = new Set((() => { try { return JSON.parse(localStorage.getItem("mps_compare_ids") || "[]"); } catch { return []; } })());
function filteredProducts(products) {
const material = materialSelect ? materialSelect.value : "all";
const price = priceSelect ? priceSelect.value : "all";
const size = sizeSelect ? sizeSelect.value : "all";
return products.filter(product => {
const okMaterial = material === "all" || materialKey(product) === material;
const okPrice = price === "all" || (Number(product.price_from || 0) > 0 && Number(product.price_from) <= Number(price));
const okSize = size === "all" || sizeBucket(product) === size;
return okMaterial && okPrice && okSize;
});
}
function syncCompareInputs() {
qsa(".compare-input").forEach(input => {
input.checked = selectedCompare.has(String(input.value));
});
}
function renderCompareBar() {
const bar = qs("#compareBar");
const count = qs("#compareCount");
if (!bar || !count) return;
count.textContent = String(selectedCompare.size);
const headerCount = qs("#headerCompareCount");
if (headerCount) headerCount.textContent = String(selectedCompare.size);
bar.hidden = selectedCompare.size === 0;
}
function productById(id) {
return currentProducts.find(product => String(product.id) === String(id));
}
function showProductDetails(product) {
if (!product) return;
const gallery = productGallery(product);
const firstImage = gallery[0];
const specs = specsList(product);
const modal = document.createElement("div");
modal.className = "product-modal";
modal.innerHTML = `
${firstImage ? `

` : productImage(product)}
${gallery.map(path => `
`).join("")}
${categoryLabels[product.category] || "Інше"}
${escapeHtml(product.title)}
${escapeHtml(product.description || "Опис товару можна додати в адмінці.")}
${money(product.price_from)}
Технічні характеристики
${(specs.length ? specs : ["Розміри уточнюються", "Товщина металу уточнюється", "Покриття уточнюється"]).map(item => `- ${escapeHtml(item)}
`).join("")}
`;
document.body.appendChild(modal);
window.MPSSecurity?.prepareForm(modal.querySelector("form")).catch(() => {});
modal.querySelector(".product-modal__close").addEventListener("click", () => modal.remove());
modal.addEventListener("click", event => {
if (event.target === modal) modal.remove();
});
modal.querySelectorAll(".product-modal__thumbs button").forEach(button => {
button.addEventListener("click", () => {
const img = button.querySelector("img");
const main = modal.querySelector(".product-modal__main");
if (img && main) main.src = img.src;
});
});
}
function showCompare() {
const products = Array.from(selectedCompare).map(productById).filter(Boolean);
if (products.length < 2) {
alert("Оберіть мінімум два товари для порівняння.");
return;
}
const rows = [
["Тип", p => categoryLabels[p.category] || "Інше"],
["Розмір", p => p.size_info || "за запитом"],
["Матеріал", p => p.material || "за запитом"],
["Ціна", p => money(p.price_from)],
["Покриття / вага / товщина", p => String(p.specs || "за запитом")]
];
const modal = document.createElement("div");
modal.className = "product-modal compare-modal";
modal.innerHTML = `
Порівняння товарів
| Параметр | ${products.map(p => `${escapeHtml(p.title)} | `).join("")}
${rows.map(([label, getter]) => `| ${label} | ${products.map(p => `${escapeHtml(getter(p))} | `).join("")}
`).join("")}
`;
document.body.appendChild(modal);
window.MPSSecurity?.prepareForm(modal.querySelector("form")).catch(() => {});
modal.querySelector(".product-modal__close").addEventListener("click", () => modal.remove());
modal.addEventListener("click", event => {
if (event.target === modal) modal.remove();
});
}
async function load() {
try {
const products = await fetchProducts({
search: searchInput ? searchInput.value.trim() : "",
category: categorySelect ? categorySelect.value : "all"
});
currentProducts = products;
const shownProducts = filteredProducts(products);
grids.forEach(grid => {
grid.innerHTML = shownProducts.length ? shownProducts.map(productCard).join("") : "";
});
syncCompareInputs();
renderCompareBar();
const counter = qs("#catalogCount") || qs("#productCount") || qs("#homeCatalogCount");
if (counter) counter.textContent = String(shownProducts.length);
const emptyBox = qs("#catalogEmptyBox");
if (emptyBox) emptyBox.hidden = shownProducts.length > 0;
} catch (err) {
grids.forEach(grid => {
grid.innerHTML = "";
});
const emptyBox = qs("#catalogEmptyBox");
if (emptyBox) {
emptyBox.hidden = false;
emptyBox.innerHTML = `Каталог поки недоступний
Перевірте запуск Apache/MySQL або налаштування бази даних.
`;
}
}
}
searchInput?.addEventListener("input", load);
categorySelect?.addEventListener("change", load);
materialSelect?.addEventListener("change", load);
priceSelect?.addEventListener("change", load);
sizeSelect?.addEventListener("change", load);
document.addEventListener("change", event => {
const input = event.target.closest(".compare-input");
if (!input) return;
if (input.checked) {
selectedCompare.add(String(input.value));
} else {
selectedCompare.delete(String(input.value));
}
localStorage.setItem("mps_compare_ids", JSON.stringify(Array.from(selectedCompare)));
renderCompareBar();
});
document.addEventListener("click", event => {
const details = event.target.closest("[data-details]");
if (details) {
showProductDetails(productById(details.dataset.details));
}
});
qs("#compareBtn")?.addEventListener("click", showCompare);
qs("#clearCompareBtn")?.addEventListener("click", () => {
selectedCompare.clear();
localStorage.setItem("mps_compare_ids", "[]");
syncCompareInputs();
renderCompareBar();
});
await load();
}
function fillAdminForm(product) {
qs("#productId").value = product.id || "";
qs("#formAction").value = "update";
qs("#formTitle").textContent = "Редагування товару";
qs("#title").value = product.title || "";
qs("#category").value = product.category || "other";
qs("#status").value = product.status || "order";
qs("#price_from").value = product.price_from || "";
qs("#size_info").value = product.size_info || "";
qs("#material").value = product.material || "";
qs("#description").value = product.description || "";
qs("#specs").value = product.specs || "";
qs("#is_active").checked = Number(product.is_active) === 1;
window.scrollTo({ top: 0, behavior: "smooth" });
}
function resetAdminForm() {
const form = qs("#dbProductForm");
if (!form) return;
form.reset();
qs("#productId").value = "";
qs("#formAction").value = "create";
qs("#formTitle").textContent = "Новий товар";
qs("#is_active").checked = true;
}
async function renderAdminProducts() {
const list = qs("#adminProducts");
if (!list) return;
try {
const products = await fetchProducts({ admin: "1" });
qs("#adminCount").textContent = "Усього товарів: " + products.length;
if (!products.length) {
list.innerHTML = `Товарів ще немає. Заповніть форму зліва.
`;
return;
}
list.innerHTML = products.map(product => `
${product.image_path ? `

` : productImage(product)}
${escapeHtml(product.title)}
${categoryLabels[product.category] || "Інше"} · ${money(product.price_from)} · ${Number(product.is_active) ? "показується" : "приховано"}
${escapeHtml(product.description || "")}
`).join("");
list._products = products;
} catch (err) {
list.innerHTML = `Помилка API: ${escapeHtml(err.message)}. Перевірте базу metalconstruct_db.
`;
qs("#adminCount").textContent = "Помилка";
}
}
function setupAdmin() {
const form = qs("#dbProductForm");
if (!form) return;
form.addEventListener("submit", async (event) => {
event.preventDefault();
const fd = new FormData(form);
fd.set("is_active", qs("#is_active").checked ? "1" : "0");
const status = qs("#adminStatus");
status.textContent = "Збереження...";
try {
const res = await fetch(API_PRODUCTS, {
method: "POST",
body: fd
});
const data = await res.json();
if (!data.ok) throw new Error(data.error || "Не вдалося зберегти");
status.textContent = "Товар збережено в базу.";
resetAdminForm();
await renderAdminProducts();
} catch (err) {
status.textContent = "Помилка: " + err.message;
}
});
qs("#resetProductForm")?.addEventListener("click", resetAdminForm);
qs("#reloadProducts")?.addEventListener("click", renderAdminProducts);
qs("#adminProducts")?.addEventListener("click", async (event) => {
const edit = event.target.closest("[data-edit]");
const del = event.target.closest("[data-delete]");
if (edit) {
const products = qs("#adminProducts")._products || [];
const product = products.find(p => String(p.id) === String(edit.dataset.edit));
if (product) fillAdminForm(product);
}
if (del) {
if (!confirm("Видалити цей товар з бази?")) return;
const fd = new FormData();
fd.set("action", "delete");
fd.set("id", del.dataset.delete);
await fetch(API_PRODUCTS, { method: "POST", body: fd });
await renderAdminProducts();
}
});
renderAdminProducts();
}
function setupRequestForms() {
qsa("form").forEach(form => {
if (form.id === "dbProductForm") return;
if (!form.matches("#requestForm, #estimateForm")) return;
window.MPSSecurity?.addHoneypot(form);
form.addEventListener("submit", async (event) => {
event.preventDefault();
const fd = new FormData(form);
const area = qs("#areaM2");
const status = qs("#requestStatus", form) || qs("#requestStatus") || form.querySelector(".form-status");
if (status) {
status.textContent = "Надсилання заявки...";
status.style.color = "";
}
if (area && !fd.get("area_m2")) fd.set("area_m2", area.value || "");
try {
const response = await secureFetch(API_REQUESTS, { method: "POST", body: fd });
const data = await response.json();
if (!response.ok || !data.ok) throw new Error(data.error || "Не вдалося надіслати заявку");
if (status) status.textContent = "Заявку надіслано. Менеджер зв’яжеться з вами.";
form.reset();
const fileName = form.querySelector(".file-name");
if (fileName) fileName.textContent = "Файл не вибрано";
} catch (err) {
if (status) {
status.textContent = err.message || "Не вдалося надіслати заявку.";
status.style.color = "#b42318";
}
}
});
});
}
function setupCallbackModal() {
function openModal() {
const modal = document.createElement("div");
modal.className = "product-modal callback-modal";
modal.innerHTML = `
`;
document.body.appendChild(modal);
window.MPSSecurity?.prepareForm(modal.querySelector("form")).catch(() => {});
modal.querySelector(".product-modal__close").addEventListener("click", () => modal.remove());
modal.addEventListener("click", event => {
if (event.target === modal) modal.remove();
});
modal.querySelector("form").addEventListener("submit", async event => {
event.preventDefault();
const form = event.currentTarget;
const status = form.querySelector(".form-status");
const payload = Object.fromEntries(new FormData(form).entries());
status.textContent = "Надсилання...";
try {
const response = await secureFetch(API_REQUESTS, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
const data = await response.json();
if (!response.ok || !data.ok) throw new Error(data.error || "Не вдалося надіслати заявку");
status.textContent = "Заявку на дзвінок надіслано.";
form.reset();
} catch (err) {
status.textContent = "Не вдалося надіслати. Спробуйте телефоном.";
status.style.color = "#b42318";
}
});
}
document.addEventListener("click", event => {
const trigger = event.target.closest("[data-callback-open]");
if (!trigger) return;
event.preventDefault();
openModal();
});
}
document.addEventListener("DOMContentLoaded", () => {
renderPublicProducts();
setupAdmin();
setupRequestForms();
setupCallbackModal();
});
})();
/* Improved online-store interface */
(function(){
const $ = (s, r=document) => r.querySelector(s);
const $$ = (s, r=document) => Array.from(r.querySelectorAll(s));
const FAVORITES_KEY = "mps_store_favorites";
const REQUEST_KEY = "mps_store_request";
const read = (key) => {
try { return JSON.parse(localStorage.getItem(key) || "[]"); }
catch { return []; }
};
const write = (key, value) => localStorage.setItem(key, JSON.stringify(value));
function cardData(card) {
return {
id: String(card?.dataset.productId || card?.dataset.id || ""),
title: card?.dataset.title || card?.querySelector("h3")?.textContent?.trim() || "Товар",
price: card?.querySelector(".store-price-block b,.price-row b")?.textContent?.trim() || "За запитом"
};
}
function updateHeaderCounts() {
const fav = read(FAVORITES_KEY).length;
const req = read(REQUEST_KEY).length;
const cmp = read("mps_compare_ids").length;
const setCount = (selector, value) => {
$$(selector).forEach(el => {
el.textContent = String(value);
el.hidden = value === 0;
});
};
setCount("#headerFavoriteCount", fav);
setCount("#headerRequestCount", req);
setCount("#headerCompareCount", cmp);
}
function syncCards() {
const favorites = new Set(read(FAVORITES_KEY).map(String));
const requests = new Set(read(REQUEST_KEY).map(item => String(item.id)));
$$(".store-product-card").forEach(card => {
const id = String(card.dataset.productId || "");
const favBtn = card.querySelector("[data-favorite]");
const reqBtn = card.querySelector("[data-request]");
const isFav = favorites.has(id);
const isReq = requests.has(id);
favBtn?.classList.toggle("active", isFav);
if (favBtn) {
favBtn.textContent = isFav ? "♥" : "♡";
favBtn.setAttribute("aria-label", isFav ? "Видалити з обраного" : "Додати в обране");
}
reqBtn?.classList.toggle("active", isReq);
if (reqBtn) reqBtn.textContent = isReq ? "У заявці" : "До заявки";
});
}
function renderDrawer() {
const body = $("#requestDrawerItems");
if (!body) return;
const items = read(REQUEST_KEY);
body.innerHTML = items.length
? items.map(item => `
${escapeStore(item.title)}${escapeStore(item.price)}
`).join("")
: `Список порожнійДодайте товари з каталогу для прорахунку.
`;
}
function escapeStore(value) {
return String(value ?? "")
.replaceAll("&","&")
.replaceAll("<","<")
.replaceAll(">",">")
.replaceAll('"',""")
.replaceAll("'","'");
}
function openDrawer() {
const drawer = $("#requestDrawer");
const overlay = $("#requestDrawerOverlay");
if (!drawer || !overlay) return;
renderDrawer();
drawer.classList.add("open");
drawer.setAttribute("aria-hidden", "false");
overlay.hidden = false;
document.body.style.overflow = "hidden";
}
function closeDrawer() {
const drawer = $("#requestDrawer");
const overlay = $("#requestDrawerOverlay");
drawer?.classList.remove("open");
drawer?.setAttribute("aria-hidden", "true");
if (overlay) overlay.hidden = true;
document.body.style.overflow = "";
}
function applyFavoritesFilter() {
const params = new URLSearchParams(location.search);
if (params.get("favorites") !== "1") return;
const favorites = new Set(read(FAVORITES_KEY).map(String));
const note = $("#favoritesNote");
if (note) note.hidden = false;
$$(".store-product-card").forEach(card => {
card.hidden = !favorites.has(String(card.dataset.productId || ""));
});
const count = $$(".store-product-card").filter(card => !card.hidden).length;
const counter = $("#catalogCount");
if (counter) counter.textContent = String(count);
const empty = $("#catalogEmptyBox");
if (empty) empty.hidden = count > 0;
}
function prefillEstimateRequest() {
if (!location.pathname.endsWith("estimate.html")) return;
const items = read(REQUEST_KEY);
if (!items.length) return;
const textarea = $('textarea[name="comment"]');
if (!textarea || textarea.value.trim()) return;
textarea.value = "Товари для прорахунку:\n" + items.map((item, i) => `${i + 1}. ${item.title} — ${item.price}`).join("\n");
}
document.addEventListener("DOMContentLoaded", () => {
updateHeaderCounts();
renderDrawer();
syncCards();
prefillEstimateRequest();
const grids = ["#catalogGrid", "#homeCatalogGrid"].map($).filter(Boolean);
grids.forEach(grid => {
new MutationObserver(() => {
syncCards();
setTimeout(applyFavoritesFilter, 0);
}).observe(grid, {childList:true});
});
setTimeout(applyFavoritesFilter, 250);
$$(".store-request-open").forEach(btn => btn.addEventListener("click", openDrawer));
$("#requestDrawerClose")?.addEventListener("click", closeDrawer);
$("#requestDrawerOverlay")?.addEventListener("click", closeDrawer);
$("#clearRequestList")?.addEventListener("click", () => {
write(REQUEST_KEY, []);
updateHeaderCounts();
syncCards();
renderDrawer();
});
$("#requestCheckout")?.addEventListener("click", () => {
sessionStorage.setItem("mps_request_checkout", "1");
});
document.addEventListener("click", event => {
const favorite = event.target.closest("[data-favorite]");
if (favorite) {
const card = favorite.closest(".store-product-card");
const id = String(card?.dataset.productId || favorite.dataset.favorite || "");
const current = read(FAVORITES_KEY).map(String);
const next = current.includes(id) ? current.filter(item => item !== id) : [id, ...current];
write(FAVORITES_KEY, next);
updateHeaderCounts();
syncCards();
applyFavoritesFilter();
return;
}
const request = event.target.closest("[data-request]");
if (request) {
const card = request.closest(".store-product-card");
const product = cardData(card);
const current = read(REQUEST_KEY);
const exists = current.some(item => String(item.id) === product.id);
const next = exists ? current.filter(item => String(item.id) !== product.id) : [product, ...current];
write(REQUEST_KEY, next);
updateHeaderCounts();
syncCards();
renderDrawer();
if (!exists) openDrawer();
return;
}
const remove = event.target.closest("[data-request-remove]");
if (remove) {
const id = String(remove.dataset.requestRemove);
write(REQUEST_KEY, read(REQUEST_KEY).filter(item => String(item.id) !== id));
updateHeaderCounts();
syncCards();
renderDrawer();
}
});
const filters = $("#catalogFilters");
$("#filterToggle")?.addEventListener("click", () => filters?.classList.add("open"));
$("#filterClose")?.addEventListener("click", () => filters?.classList.remove("open"));
$("#applyCatalogFilters")?.addEventListener("click", () => filters?.classList.remove("open"));
$("#resetCatalogFilters")?.addEventListener("click", () => {
["search","category","materialFilter","sizeFilter","priceFilter"].forEach(id => {
const el = $("#" + id);
if (!el) return;
if (el.tagName === "SELECT") el.selectedIndex = 0;
else el.value = "";
el.dispatchEvent(new Event(el.tagName === "SELECT" ? "change" : "input", {bubbles:true}));
});
filters?.classList.remove("open");
});
const params = new URLSearchParams(location.search);
const category = params.get("category");
const query = params.get("q");
if (category && $("#category")) {
$("#category").value = category;
$("#category").dispatchEvent(new Event("change", {bubbles:true}));
}
if (query && $("#search")) {
$("#search").value = query;
$("#search").dispatchEvent(new Event("input", {bubbles:true}));
}
const sort = $("#catalogSort");
const grid = $("#catalogGrid");
function sortCards() {
if (!sort || !grid) return;
const cards = $$(".store-product-card", grid);
cards.sort((a,b) => {
const pa = Number(a.dataset.price || 0);
const pb = Number(b.dataset.price || 0);
if (sort.value === "price-asc") return pa - pb;
if (sort.value === "price-desc") return pb - pa;
return 0;
}).forEach(card => grid.appendChild(card));
}
sort?.addEventListener("change", sortCards);
});
})();
/* Catalog breadcrumbs */
(function(){
const labels = {
canopy: "Навіси",
frame: "Каркаси",
beam: "Ферми та балки",
stairs: "Сходи та перила",
fence: "Ворота й огорожі",
other: "Інше"
};
document.addEventListener("DOMContentLoaded", () => {
const params = new URLSearchParams(location.search);
const category = params.get("category");
const wrap = document.querySelector("#catalogBreadcrumbCategoryWrap");
const label = document.querySelector("#catalogBreadcrumbCategory");
if (category && labels[category] && wrap && label) {
label.textContent = labels[category];
wrap.hidden = false;
}
});
})();
/* FAQ accordion */
(function(){
document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll(".store-faq article > button").forEach(button => {
button.addEventListener("click", () => {
const article = button.closest("article");
const content = article?.querySelector(":scope > div");
if (!content) return;
const open = button.getAttribute("aria-expanded") === "true";
button.setAttribute("aria-expanded", open ? "false" : "true");
content.hidden = open;
const icon = button.querySelector("b");
if (icon) icon.textContent = open ? "+" : "−";
});
});
});
})();
/* User account status in the public header */
(function(){
document.addEventListener("DOMContentLoaded", async () => {
const desktopLink = document.querySelector("#userAccountLink");
const navLink = document.querySelector("#userAccountNavLink");
if (!desktopLink && !navLink) return;
try {
const response = await fetch("api/user-status.php", {
credentials: "same-origin",
headers: {"Accept":"application/json"}
});
if (!response.ok) return;
const data = await response.json();
const loggedIn = Boolean(data.logged_in);
const url = loggedIn ? (data.dashboard_url || "dashboard.php") : (data.login_url || "login.php");
const label = loggedIn ? "Кабінет" : "Увійти";
if (desktopLink) {
desktopLink.href = url;
const text = desktopLink.querySelector(".store-account-label");
if (text) text.textContent = label;
desktopLink.title = loggedIn && data.name ? `Кабінет: ${data.name}` : label;
}
if (navLink) {
navLink.href = url;
navLink.textContent = label;
}
} catch (_) {
// The public site remains usable even if the auth status endpoint is unavailable.
}
});
})();