deploy / deploy (push) Successful in 4s
- Главная: тихая кнопка «Помахать» (web/js/wave.js), общий счётчик для всех посетителей - server/server.mjs: мини-API (/api/waves, /api/wave), счётчик хранится вне репозитория (/var/lib/vizitka-site/waves.json), лимит раз в секунду с IP, проверка Origin - deploy: systemd-сервис vizitka-api, проксирование /api в nginx, setup-server.sh ставит nodejs - CI: после git pull перезапускает API - Домен https://me.ibrash.duckdns.org вместо заглушки, исправлен пробел в шапке - README и заметки обновлены Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
58 lines
2.9 KiB
JavaScript
58 lines
2.9 KiB
JavaScript
// Тихая кнопка «Помахать»: общий счётчик для всех посетителей хранится на сервере (/api/waves, /api/wave).
|
|
// Если API недоступен (например, сайт открыт без бэкенда), блок просто прячется, остальная страница не страдает.
|
|
(function () {
|
|
var row = document.getElementById('wave-row');
|
|
var btn = document.getElementById('wave-btn');
|
|
var num = document.getElementById('wave-n');
|
|
var line = document.getElementById('wave-count');
|
|
if (!row || !btn || !num) return;
|
|
|
|
var count = null, busy = false, msgTimer = null;
|
|
var reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
var fmt = function (n) { return n.toLocaleString('ru-RU'); };
|
|
|
|
// число плавно «докручивается» до нового значения
|
|
function setCount(n) {
|
|
var from = count === null ? n : count; count = n;
|
|
if (reduce || from === n) { num.textContent = fmt(n); return; }
|
|
var t0 = performance.now();
|
|
(function step(now) {
|
|
var k = Math.min(1, (now - t0) / 350);
|
|
num.textContent = fmt(Math.round(from + (n - from) * k));
|
|
if (k < 1) requestAnimationFrame(step);
|
|
})(t0);
|
|
}
|
|
function say(text) { // короткое сообщение вместо счётчика
|
|
clearTimeout(msgTimer);
|
|
line.textContent = text;
|
|
msgTimer = setTimeout(function () { line.textContent = 'Уже помахали: '; line.appendChild(num); }, 2200);
|
|
}
|
|
function plusOne() { // маленькое «+1» уплывает вверх
|
|
if (reduce) return;
|
|
var s = document.createElement('span');
|
|
s.className = 'wave-plus'; s.textContent = '+1'; s.setAttribute('aria-hidden', 'true');
|
|
btn.appendChild(s);
|
|
s.addEventListener('animationend', function () { s.remove(); });
|
|
}
|
|
|
|
fetch('/api/waves').then(function (r) { if (!r.ok) throw 0; return r.json(); })
|
|
.then(function (d) { setCount(d.count); })
|
|
.catch(function () { row.classList.add('off'); });
|
|
|
|
btn.addEventListener('click', function () {
|
|
if (busy || count === null) return;
|
|
busy = true; btn.disabled = true;
|
|
var prev = count;
|
|
setCount(count + 1);
|
|
btn.classList.remove('waving'); void btn.offsetWidth; btn.classList.add('waving');
|
|
plusOne();
|
|
fetch('/api/wave', { method: 'POST' }).then(function (r) {
|
|
if (r.status === 429) { say('Не так быстро, раз в секунду'); throw 0; }
|
|
if (!r.ok) throw new Error(r.status);
|
|
return r.json();
|
|
}).then(function (d) { setCount(d.count); })
|
|
.catch(function (e) { count = prev; num.textContent = fmt(prev); if (e !== 0) say('Не засчиталось, попробуй ещё раз'); })
|
|
.then(function () { setTimeout(function () { busy = false; btn.disabled = false; }, 1000); });
|
|
});
|
|
})();
|