feat: improve accessibility and admin safety

This commit is contained in:
ik
2026-09-07 08:58:20 +07:00
parent 2b7ad4a03a
commit fec830879b
6 changed files with 14 additions and 9 deletions
@@ -7,7 +7,7 @@ const apiUrl = import.meta.env.PUBLIC_API_URL || "http://localhost:8000";
<section class="moderation-app" data-api-url={apiUrl}>
<form class="admin-login" autocomplete="off"><label>Административный токен<input name="token" type="password" required autocomplete="off" /></label><button type="submit">Открыть очередь</button></form>
<p class="privacy">Токен остаётся только в памяти страницы. Исходная ссылка и происхождение сохраняются при публикации.</p>
<div class="notice error" data-admin-error hidden></div><div class="moderation-list" data-external-list aria-live="polite"></div>
<div class="notice error" data-admin-error role="alert" hidden></div><div class="moderation-list" data-external-list aria-live="polite"></div>
</section>
<script>
const root = document.querySelector<HTMLElement>(".moderation-app");
+5 -2
View File
@@ -7,7 +7,7 @@ const apiUrl = import.meta.env.PUBLIC_API_URL || "http://localhost:8000";
<section class="moderation-app" data-api-url={apiUrl}>
<form class="admin-login" autocomplete="off"><label>Административный токен<input name="token" type="password" required autocomplete="off" /></label><button type="submit">Открыть очередь</button></form>
<p class="privacy">Токен хранится только в памяти страницы и не записывается в URL или localStorage.</p>
<div class="notice error" data-admin-error hidden></div><div class="moderation-list" data-moderation-list aria-live="polite"></div>
<div class="notice error" data-admin-error role="alert" hidden></div><div class="moderation-list" data-moderation-list aria-live="polite"></div>
</section>
<script>
const root = document.querySelector<HTMLElement>(".moderation-app");
@@ -29,7 +29,10 @@ const apiUrl = import.meta.env.PUBLIC_API_URL || "http://localhost:8000";
login?.addEventListener("submit", async event => { event.preventDefault(); token = String(new FormData(login).get("token") || ""); try { await loadQueue(); login.hidden = true; } catch (cause) { fail(cause instanceof Error ? cause.message : "Ошибка загрузки."); } });
list?.addEventListener("click", async event => {
const button = (event.target as HTMLElement).closest<HTMLButtonElement>("button[data-decision],button[data-delete]"); const card = button?.closest<HTMLElement>("[data-report-id]"); if (!button || !card || !root) return;
button.disabled = true; const reason = card.querySelector<HTMLTextAreaElement>("textarea")?.value || null;
const reason = card.querySelector<HTMLTextAreaElement>("textarea")?.value.trim() || null;
if (button.dataset.decision === "rejected" && !reason) { fail("Укажите причину отклонения."); card.querySelector("textarea")?.focus(); return; }
if (button.hasAttribute("data-delete") && !window.confirm("Удалить и обезличить эту заявку? Действие нельзя отменить.")) return;
button.disabled = true;
try { const deleting = button.hasAttribute("data-delete"); const response = await fetch(`${root.dataset.apiUrl}/api/v1/admin/catch-reports/${card.dataset.reportId}`, {method:deleting ? "DELETE" : "PATCH",headers:{Authorization:`Bearer ${token}`,"Content-Type":"application/json"},body:deleting ? undefined : JSON.stringify({status:button.dataset.decision,reason})}); if (!response.ok) throw new Error("Не удалось сохранить решение."); card.remove(); if (list && !list.children.length) list.innerHTML = '<div class="state"><h2>Очередь пуста</h2><p>Все записи обработаны.</p></div>'; } catch (cause) { button.disabled = false; fail(cause instanceof Error ? cause.message : "Ошибка сохранения."); }
});
</script>