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); });