function api(url, opts) { return fetch(url, opts).then(function (r) { if (!r.ok) return r.json().then(function (j) { throw new Error(j.error || "Ошибка сервера"); }); return r.json(); }); } function noServer(err) { return /Failed to fetch|Unexpected token|"']/g, function (c) { return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]; }); } function renderWishes(wishes) { var grid = document.getElementById("wishGrid"); var note = document.getElementById("wishNote"); if (!wishes.length) { grid.innerHTML = ""; note.textContent = "Сокровищница пока пуста — скоро наполним ✨"; return; } grid.innerHTML = wishes.map(function (w) { var taken = !!w.reservedBy; return '
' + '
' + esc(w.emoji) + '
' + '
' + esc(w.title) + '
' + '
' + (taken ? "✦ Забрал(а): " + esc(w.reservedBy) : "Ещё свободен") + '
' + (taken ? '' : '') + '
'; }).join(""); note.textContent = wishes.filter(function (w) { return w.reservedBy; }).length + " из " + wishes.length + " даров уже обрели хозяина"; grid.querySelectorAll(".g-btn:not(.done)").forEach(function (btn) { btn.addEventListener("click", function () { var guest = prompt("Напиши своё имя, чтобы мы знали — кто несёт этот дар:"); if (!guest) return; api("api/wish/reserve", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: Number(btn.dataset.id), guest: guest.trim() }) }) .then(function (j) { renderWishes(j.data); }) .catch(function (err) { alert(err.message); }); }); }); } function loadWishes() { api("api/wish").then(function (j) { renderWishes(j.data); }) .catch(function () { renderWishes(MOCK); }); } loadWishes(); /* ==== Панель хозяина ==== */ var pin = sessionStorage.getItem("bday_pin") || ""; var aLogin = document.getElementById("a-login"); var aPanel = document.getElementById("a-panel"); var aMsg = document.getElementById("a-msg"); function renderAdminWishes(wishes) { document.getElementById("a-wishes").innerHTML = wishes.map(function (w) { return '
' + esc(w.emoji) + '' + '' + esc(w.title) + (w.reservedBy ? ' · ' + esc(w.reservedBy) + '' : "") + '' + '
'; }).join(""); document.querySelectorAll("#a-wishes .a-del").forEach(function (b) { b.addEventListener("click", function () { api("api/admin/wish/del", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ pin: pin, id: Number(b.dataset.id) }) }) .then(function (j) { renderAdminWishes(j.data); renderWishes(j.data); }) .catch(function (err) { aMsg.className = "msg err"; aMsg.textContent = err.message; }); }); }); } function loadAdmin() { api("api/admin/regs", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ pin: pin }) }) .then(function (j) { aMsg.className = "msg ok"; aMsg.textContent = "Добро пожаловать, хозяин замка 🏰"; aPanel.hidden = false; var regs = j.data; document.getElementById("a-regs").innerHTML = regs.length ? regs.map(function (r) { return '
' + esc(r.name) + '
' + esc(r.phone) + " · гостей: " + r.guests + (r.note ? " · «" + esc(r.note) + "»" : "") + " · " + r.date + '
'; }).join("") : '
Пока никто не подтвердил участие.
'; document.getElementById("confirmedCount").hidden = false; document.getElementById("confirmedCount").querySelector("b").textContent = j.total; return api("api/wish"); }) .then(function (j) { renderAdminWishes(j.data); }) .catch(function (err) { aMsg.className = "msg err"; aMsg.textContent = "Неверный код или ошибка: " + err.message; }); } aLogin.addEventListener("click", function () { pin = document.getElementById("a-pin").value.trim(); if (!pin) return; sessionStorage.setItem("bday_pin", pin); loadAdmin(); }); document.getElementById("a-pin").addEventListener("keydown", function (e) { if (e.key === "Enter") aLogin.click(); }); document.getElementById("a-wadd").addEventListener("click", function () { var title = document.getElementById("a-wtitle").value.trim(); var emoji = document.getElementById("a-wemoji").value.trim() || "🎁"; if (!title) return; api("api/admin/wish", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ pin: pin, title: title, emoji: emoji }) }) .then(function (j) { renderAdminWishes(j.data); renderWishes(j.data); document.getElementById("a-wtitle").value = ""; document.getElementById("a-wemoji").value = ""; }) .catch(function (err) { aMsg.className = "msg err"; aMsg.textContent = err.message; }); }); if (pin) { document.getElementById("a-pin").value = pin; loadAdmin(); } /* ==== Плавные появления ==== */ var io = new IntersectionObserver(function (entries) { entries.forEach(function (e) { if (e.isIntersecting) e.target.classList.add("in"); }); }, { threshold: .15 }); document.querySelectorAll(".reveal").forEach(function (el) { io.observe(el); });