Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BCrwHHnGCB5XH968Nxokqw
108 lines
6.0 KiB
JavaScript
108 lines
6.0 KiB
JavaScript
(() => {
|
||
const csrf = document.querySelector('meta[name=csrf-token]').content;
|
||
const call = async (method, url, body) => {
|
||
const r = await fetch(url, {
|
||
method, headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-CSRF-TOKEN': csrf },
|
||
body: body ? JSON.stringify(body) : undefined,
|
||
});
|
||
if (r.status === 419) { location.reload(); return r; }
|
||
return r;
|
||
};
|
||
const showErr = async (el, r) => {
|
||
const d = await r.json().catch(() => ({}));
|
||
el.textContent = d.message || Object.values(d.errors || {}).flat().join(' ') || 'Ошибка';
|
||
el.hidden = false;
|
||
};
|
||
|
||
// Звёзды: 90 по небу, 170 в полосе (дуга −14°). Генератор с фиксированным зерном,
|
||
// чтобы небо было одинаковым при каждой перезагрузке и не «прыгало» после действий.
|
||
let seed = 0x48414430; // "HAD0"
|
||
const rand = () => {
|
||
seed = (seed + 0x6D2B79F5) | 0;
|
||
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
|
||
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||
};
|
||
const star = (color, size, bright, x, y, dur, delay) => {
|
||
const s = document.createElement('div');
|
||
s.style.cssText = `position:absolute;border-radius:50%;left:${x}%;top:${y}%;width:${size}px;height:${size}px;background:${color};${bright ? `box-shadow:0 0 6px ${color};` : ''}animation:skTwinkle ${dur}s ease-in-out ${delay}s infinite`;
|
||
return s;
|
||
};
|
||
const sky = document.getElementById('stars'), band = document.getElementById('band');
|
||
for (let i = 0; i < 90; i++) sky.appendChild(star(rand() < .2 ? '#8FD6DC' : '#F2ECE0', rand() < .85 ? 1.5 : 2.5, false, (rand() * 100).toFixed(2), (rand() * 100).toFixed(2), (3 + rand() * 5).toFixed(1), (-rand() * 8).toFixed(1)));
|
||
for (let j = 0; j < 170; j++) {
|
||
const bright = rand() < .08, r = rand();
|
||
const color = r < .68 ? '#F2ECE0' : (r < .88 ? '#F7CEDC' : '#8FD6DC');
|
||
const g = (rand() + rand() + rand()) / 3, x = rand() * 100;
|
||
const arc = 28 * Math.pow((x - 50) / 50, 2) + Math.max(0, 35 - x) * .35;
|
||
band.appendChild(star(color, bright ? 2.2 : (0.8 + rand() * 1.1).toFixed(1), bright, x.toFixed(2), (6 + g * 55 + arc).toFixed(2), (2.5 + rand() * 5).toFixed(1), (-rand() * 8).toFixed(1)));
|
||
}
|
||
|
||
// Панели архив/настройки
|
||
document.querySelectorAll('.panel .head').forEach(h => h.addEventListener('click', () => {
|
||
const body = h.nextElementSibling; body.hidden = !body.hidden;
|
||
h.querySelector('.chev').textContent = body.hidden ? '▼' : '▲';
|
||
}));
|
||
|
||
// Форма добавления
|
||
const addform = document.getElementById('addform');
|
||
const toggleAdd = () => { addform.hidden = !addform.hidden; document.getElementById('add-err').hidden = true; };
|
||
document.getElementById('toggle-add').addEventListener('click', toggleAdd);
|
||
document.getElementById('cancel-add').addEventListener('click', toggleAdd);
|
||
document.getElementById('add').addEventListener('submit', async e => {
|
||
e.preventDefault();
|
||
const f = new FormData(e.target);
|
||
const r = await call('POST', '/me/events', { title: f.get('title'), subtitle: f.get('subtitle') || null, due_date: f.get('due_date'), due_time: f.get('due_time') || null });
|
||
if (r.ok) location.reload(); else showErr(document.getElementById('add-err'), r);
|
||
});
|
||
|
||
// «Помню» / «Сделано»
|
||
document.querySelectorAll('.card button[data-action]').forEach(btn => btn.addEventListener('click', async () => {
|
||
const card = btn.closest('.card');
|
||
card.querySelectorAll('button').forEach(b => b.disabled = true);
|
||
const r = await call('POST', `/me/events/${card.dataset.id}/${btn.dataset.action}`);
|
||
if (r.ok) location.reload(); else if (r.status !== 419) {
|
||
card.querySelectorAll('button').forEach(b => b.disabled = false);
|
||
showErr(document.getElementById('card-err'), r);
|
||
}
|
||
}));
|
||
|
||
// Настройки
|
||
document.getElementById('profile').addEventListener('submit', async e => {
|
||
e.preventDefault();
|
||
const f = new FormData(e.target);
|
||
const r = await call('PATCH', '/me', { tz: f.get('tz'), quiet_start: f.get('quiet_start'), quiet_end: f.get('quiet_end') });
|
||
if (r.ok) location.reload(); else showErr(document.getElementById('profile-err'), r);
|
||
});
|
||
document.getElementById('webhook').addEventListener('submit', async e => {
|
||
e.preventDefault();
|
||
const f = new FormData(e.target);
|
||
const r = await call('POST', '/me/channels', { type: 'webhook', config: { deliver_url: f.get('deliver_url'), presence_url: f.get('presence_url') } });
|
||
if (r.ok) location.reload(); else showErr(document.getElementById('webhook-err'), r);
|
||
});
|
||
document.querySelectorAll('[data-delete-channel]').forEach(btn => btn.addEventListener('click', async () => {
|
||
const r = await call('DELETE', `/me/channels/${btn.dataset.deleteChannel}`);
|
||
if (r.ok) location.reload();
|
||
}));
|
||
document.getElementById('tg-connect').addEventListener('click', async e => {
|
||
const r = await call('POST', '/me/channels/telegram/link');
|
||
if (!r.ok) return;
|
||
const d = await r.json();
|
||
document.getElementById('tg-code').textContent = `/start ${d.code}`;
|
||
document.getElementById('tg-link').href = d.bot_url;
|
||
e.target.hidden = true; document.getElementById('tg-box').hidden = false;
|
||
});
|
||
|
||
// Присутствие: heartbeat только при видимой вкладке и активности за 3 минуты.
|
||
let lastActivity = Date.now();
|
||
['pointerdown', 'pointermove', 'keydown', 'scroll'].forEach(ev => addEventListener(ev, () => { lastActivity = Date.now(); }, { passive: true }));
|
||
const heartbeat = () => {
|
||
if (document.visibilityState !== 'visible') return;
|
||
if (Date.now() - lastActivity > 3 * 60 * 1000) return;
|
||
call('POST', '/me/heartbeat').catch(() => {});
|
||
};
|
||
heartbeat();
|
||
setInterval(heartbeat, 30 * 1000);
|
||
document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { lastActivity = Date.now(); heartbeat(); } });
|
||
})();
|