Files
hado/core/public/inbox.js

108 lines
6.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(() => {
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(); } });
})();