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