170 lines
8.4 KiB
JavaScript
170 lines
8.4 KiB
JavaScript
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 '<div class="gift' + (taken ? " taken" : "") + '">' +
|
||
'<div class="g-emoji">' + esc(w.emoji) + '</div>' +
|
||
'<div class="g-title">' + esc(w.title) + '</div>' +
|
||
'<div class="g-status' + (taken ? " taken" : "") + '">' + (taken ? "✦ Забрал(а): " + esc(w.reservedBy) : "Ещё свободен") + '</div>' +
|
||
(taken
|
||
? '<button class="g-btn done" disabled>Забронировано</button>'
|
||
: '<button class="g-btn" data-id="' + w.id + '">Забираю этот дар</button>') +
|
||
'</div>';
|
||
}).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 '<div class="a-row"><span class="g-emoji" style="font-size:20px">' + esc(w.emoji) + '</span>' +
|
||
'<span class="grow">' + esc(w.title) + (w.reservedBy ? ' <span class="gold">· ' + esc(w.reservedBy) + '</span>' : "") + '</span>' +
|
||
'<button class="a-del" data-id="' + w.id + '">Удалить</button></div>';
|
||
}).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 '<div class="a-row"><span class="grow"><span class="guest-name">' + esc(r.name) + '</span><div class="guest-info">' + esc(r.phone) + " · гостей: " + r.guests + (r.note ? " · «" + esc(r.note) + "»" : "") + " · " + r.date + '</div></span></div>';
|
||
}).join("")
|
||
: '<div class="muted">Пока никто не подтвердил участие.</div>';
|
||
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); });
|