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();
});
}
/* ==== Обратный отсчёт до 20.07.2027 19:00 ==== */
var TARGET = new Date("2027-07-20T19:00:00").getTime();
function tick() {
var diff = Math.max(0, TARGET - Date.now());
document.getElementById("cd-d").textContent = Math.floor(diff / 86400000);
document.getElementById("cd-h").textContent = Math.floor(diff / 3600000) % 24;
document.getElementById("cd-m").textContent = Math.floor(diff / 60000) % 60;
document.getElementById("cd-s").textContent = Math.floor(diff / 1000) % 60;
}
setInterval(tick, 1000);
tick();
/* ==== Мок-данные для Предпросмотра (когда сервер не запущен) ==== */
var MOCK = [
{ id: 101, title: "Книга «Властелин колец» (подарочное издание)", emoji: "📚", reservedBy: "" },
{ id: 102, title: "Шахматы в фэнтези-стиле (драконы против эльфов)", emoji: "♟️", reservedBy: "" },
{ id: 103, title: "Настольная игра «Война кольца»", emoji: "🎲", reservedBy: "Айгуль" }
];
/* ==== Регистрация ==== */
var regForm = document.getElementById("regForm");
regForm.addEventListener("submit", function (e) {
e.preventDefault();
var msg = document.getElementById("regMsg");
var data = {
name: document.getElementById("r-name").value.trim(),
phone: document.getElementById("r-phone").value.trim(),
guests: document.getElementById("r-guests").value,
note: document.getElementById("r-note").value.trim()
};
msg.className = "msg"; msg.textContent = "Отправляем клятву в королевство...";
api("/api/regs", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data) })
.then(function (j) {
msg.className = "msg ok";
msg.textContent = "🎉 Ты в списке героев! Врата откроются 20.07.2027 в 19:00 — до встречи!";
var c = document.getElementById("confirmedCount");
c.hidden = false; c.querySelector("b").textContent = j.total;
regForm.reset();
})
.catch(function (err) {
if (err.message === "Failed to fetch" || /fetch/i.test(err.message)) {
msg.className = "msg ok";
msg.textContent = "🎉 Ты в списке героев! (демо-режим: сервер не запущен, на живой ссылке заявка сохранится)";
} else {
msg.className = "msg err";
msg.textContent = "Не получилось: " + err.message;
}
});
});
/* ==== Вишлист ==== */
function esc(s) {
return String(s == null ? "" : s).replace(/[&<>"']/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); });