qkeeper/static/about.html
Aleksandr Berkuta 5806fe84c4 init commit
2026-03-27 20:27:56 +03:00

148 lines
6.3 KiB
HTML
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.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1">
<title>Электронная очередь</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:#f0f0f0;color:#111;min-height:100vh}
header{background:#fff;border-bottom:1px solid #e0e0e0;padding:14px 16px}
header h1{font-size:1.2rem;font-weight:700}
header p{font-size:.85rem;color:#888;margin-top:2px}
main{max-width:520px;margin:0 auto;padding:16px 12px 48px}
section{background:#fff;border-radius:10px;padding:18px 16px;margin-bottom:14px;box-shadow:0 1px 3px rgba(0,0,0,.07)}
h2{font-size:1rem;font-weight:700;margin-bottom:12px}
.qlist{list-style:none}
.qlist li{border-bottom:1px solid #f0f0f0;padding:10px 0}
.qlist li:last-child{border-bottom:none}
.qlist a{text-decoration:none;color:#111;font-weight:600;font-size:.95rem}
.qlist a:hover{text-decoration:underline}
.qmeta{font-size:.78rem;color:#999;margin-top:2px}
.empty{color:#aaa;font-size:.9rem;padding:4px 0}
form{display:flex;gap:8px;flex-wrap:wrap}
#qname{flex:1;min-width:0;border:1.5px solid #ccc;border-radius:8px;padding:11px 14px;font-size:1rem;outline:none;-webkit-appearance:none}
#qname:focus{border-color:#111}
form button{background:#111;color:#fff;border:none;border-radius:8px;padding:11px 20px;font-size:.9rem;font-weight:700;cursor:pointer;white-space:nowrap;-webkit-tap-highlight-color:transparent}
form button:active{background:#333}
ol{padding-left:20px}
ol li{margin-bottom:8px;font-size:.9rem;line-height:1.5;color:#444}
.note{font-size:.82rem;color:#666;line-height:1.5;background:#fef9c3;border-radius:8px;padding:14px 16px;border:1px solid #fde047}
.note p+p{margin-top:8px}
footer{text-align:center;color:#bbb;font-size:.78rem;padding:8px 16px 24px}
footer a{color:#999}
#err{color:#dc2626;font-size:.85rem;margin-top:6px;display:none}
</style>
</head>
<body>
<header>
<h1>Электронная очередь</h1>
<p>Самоорганизуйтесь без бумажных талончиков</p>
</header>
<main>
<section>
<h2>Активные очереди</h2>
<ul class="qlist" id="ql"><li class="empty">Загрузка&hellip;</li></ul>
</section>
<section>
<h2>Создать очередь</h2>
<form id="nf" onsubmit="createQueue(event)">
<input id="qname" type="text" placeholder="Название, напр. «Кабинет 428»"
maxlength="100" autocomplete="off" autocorrect="off">
<button type="submit">Создать</button>
</form>
<div id="err">Не удалось создать очередь, попробуйте ещё раз</div>
</section>
<section>
<h2>Как пользоваться</h2>
<ol>
<li>Найдите нужную очередь в списке выше или создайте новую.</li>
<li>Откройте ссылку или отсканируйте QR-код на странице очереди.</li>
<li>Нажмите «Взять номер» — приложение покажет вашу позицию и ориентировочное время ожидания.</li>
<li>Когда подойдёт очередь, отметьте себя как ушедшего кнопкой «Ухожу».</li>
</ol>
</section>
<div class="note">
<p><strong>Это неофициальный инструмент самоорганизации</strong>, сделанный энтузиастом, которому надоело стоять в живых очередях. Никаких гарантий бесперебойной работы не даётся.</p>
<p>Вопросы и пожелания: <a href="mailto:line.keeper@berkuta.xyz">line.keeper@berkuta.xyz</a></p>
</div>
</main>
<footer>Очереди без записи, без регистрации</footer>
<script>
'use strict';
// XSS SAFETY NOTE
// All user-supplied strings (q.name only) are passed through esc() before
// any innerHTML insertion. q.id is server-generated hex (safe in href).
// q.count is an integer. q.createdAt is an integer (Unix ms). No raw
// user content is inserted unsanitised.
function fmtDate(ms) {
const d = new Date(ms);
return d.toLocaleDateString('ru-RU', {day:'numeric',month:'long',year:'numeric'});
}
function ruPeople(n) {
const m = n % 100, k = n % 10;
if (m >= 11 && m <= 19) return 'человек';
if (k === 1) return 'человек';
if (k >= 2 && k <= 4) return 'человека';
return 'человек';
}
// esc() HTML-encodes a string before inserting into innerHTML.
function esc(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
async function loadQueues() {
const ul = document.getElementById('ql');
try {
const res = await fetch('/queues');
const list = await res.json();
if (!list || list.length === 0) {
ul.innerHTML = '<li class="empty">Пока нет ни одной очереди — создайте первую!</li>';
return;
}
// q.id is hex (safe in href); q.name is user-supplied — esc() applied.
// q.count is integer; fmtDate receives integer ms — no user data.
ul.innerHTML = list.map(q => {
const cnt = q.count > 0 ? q.count + '\u00a0' + ruPeople(q.count) : 'пусто';
return '<li>' +
'<a href="/q/' + esc(q.id) + '">' + esc(q.name) + '</a>' +
'<div class="qmeta">Создана ' + fmtDate(q.createdAt) + ' &nbsp;&middot;&nbsp; ' + cnt + '</div>' +
'</li>';
}).join('');
} catch(e) {
ul.innerHTML = '<li class="empty">Не удалось загрузить список</li>';
}
}
function createQueue(e) {
e.preventDefault();
const name = document.getElementById('qname').value.trim();
const err = document.getElementById('err');
err.style.display = 'none';
fetch('/new', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'name=' + encodeURIComponent(name),
redirect: 'follow'
}).then(res => {
if (res.redirected) {
location.href = res.url;
} else {
err.style.display = 'block';
}
}).catch(() => { err.style.display = 'block'; });
}
loadQueues();
</script>
</body>
</html>