feat: add admin workspace navigation

This commit is contained in:
ik
2026-09-16 19:59:15 +07:00
parent d9f58fb90e
commit 4c7051bc97
6 changed files with 23 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
---
const path = Astro.url.pathname;
const links = [
["/admin", "Обзор"],
["/admin/moderation", "Уловы"],
["/admin/external-sources", "Источники"],
["/admin/media", "Медиа"],
] as const;
---
<nav class="admin-nav" aria-label="Разделы админ-панели">
{links.map(([href, label]) => <a class={path === href ? "active" : undefined} aria-current={path === href ? "page" : undefined} href={href}>{label}</a>)}
</nav>
@@ -1,10 +1,12 @@
--- ---
import Layout from "../../layouts/Layout.astro"; import Layout from "../../layouts/Layout.astro";
import AdminNav from "../../components/AdminNav.astro";
const apiUrl = import.meta.env.PUBLIC_API_URL || "http://localhost:8000"; const apiUrl = import.meta.env.PUBLIC_API_URL || "http://localhost:8000";
--- ---
<Layout title="Внешние источники — RF4 Spotter"> <Layout title="Внешние источники — RF4 Spotter">
<section class="form-hero"><div><span class="eyebrow"><b>ADMIN</b> Staging</span><h1>Внешние<br/><em>наблюдения</em></h1></div><p>Сопоставьте названия с каноническим каталогом. Опубликовать можно только запись с координатами и весом.</p></section> <section class="form-hero"><div><span class="eyebrow"><b>ADMIN</b> Staging</span><h1>Внешние<br/><em>наблюдения</em></h1></div><p>Сопоставьте названия с каноническим каталогом. Опубликовать можно только запись с координатами и весом.</p></section>
<section class="moderation-app" data-api-url={apiUrl}> <section class="moderation-app" data-api-url={apiUrl}>
<AdminNav />
<form class="admin-login" autocomplete="off"><label>Административный токен<input name="token" type="password" required autocomplete="off" /></label><button data-action="primary" type="submit">Открыть очередь</button></form> <form class="admin-login" autocomplete="off"><label>Административный токен<input name="token" type="password" required autocomplete="off" /></label><button data-action="primary" type="submit">Открыть очередь</button></form>
<p class="privacy">Токен остаётся только в памяти страницы. Исходная ссылка и происхождение сохраняются при публикации.</p> <p class="privacy">Токен остаётся только в памяти страницы. Исходная ссылка и происхождение сохраняются при публикации.</p>
<div class="admin-session-bar" hidden><span>Административная сессия активна</span><button data-action="secondary" type="button" data-admin-logout>Выйти</button></div> <div class="admin-session-bar" hidden><span>Административная сессия активна</span><button data-action="secondary" type="button" data-admin-logout>Выйти</button></div>
+2
View File
@@ -1,10 +1,12 @@
--- ---
import Layout from "../../layouts/Layout.astro"; import Layout from "../../layouts/Layout.astro";
import AdminNav from "../../components/AdminNav.astro";
const apiUrl = import.meta.env.PUBLIC_API_URL || "http://localhost:8000"; const apiUrl = import.meta.env.PUBLIC_API_URL || "http://localhost:8000";
--- ---
<Layout title="Административная панель — RF4 Spotter"> <Layout title="Административная панель — RF4 Spotter">
<section class="form-hero"><div><span class="eyebrow"><b>ADMIN</b> Центр управления</span><h1>Панель<br/><em>модератора</em></h1></div><p>Очереди, состояние источников и последние импорты в одном безопасном обзоре.</p></section> <section class="form-hero"><div><span class="eyebrow"><b>ADMIN</b> Центр управления</span><h1>Панель<br/><em>модератора</em></h1></div><p>Очереди, состояние источников и последние импорты в одном безопасном обзоре.</p></section>
<main class="admin-dashboard" data-api-url={apiUrl}> <main class="admin-dashboard" data-api-url={apiUrl}>
<AdminNav />
<form class="admin-login" autocomplete="off"><label>Административный токен<input name="token" type="password" required autocomplete="off" /></label><button data-action="primary" type="submit">Открыть панель</button></form> <form class="admin-login" autocomplete="off"><label>Административный токен<input name="token" type="password" required autocomplete="off" /></label><button data-action="primary" type="submit">Открыть панель</button></form>
<p class="privacy">Токен существует только в памяти вкладки. Сессия завершится после 15 минут бездействия.</p> <p class="privacy">Токен существует только в памяти вкладки. Сессия завершится после 15 минут бездействия.</p>
<div class="admin-session-bar" hidden><span>Административная сессия активна</span><button data-action="secondary" type="button" data-admin-logout>Выйти</button></div> <div class="admin-session-bar" hidden><span>Административная сессия активна</span><button data-action="secondary" type="button" data-admin-logout>Выйти</button></div>
+2
View File
@@ -1,10 +1,12 @@
--- ---
import Layout from "../../layouts/Layout.astro"; import Layout from "../../layouts/Layout.astro";
import AdminNav from "../../components/AdminNav.astro";
const apiUrl = import.meta.env.PUBLIC_API_URL || "http://localhost:8000"; const apiUrl = import.meta.env.PUBLIC_API_URL || "http://localhost:8000";
--- ---
<Layout title="Проверка медиа — RF4 Spotter" noindex> <Layout title="Проверка медиа — RF4 Spotter" noindex>
<section class="form-hero"><div><span class="eyebrow"><b>ADMIN</b> Media review</span><h1>Проверка<br/><em>медиа</em></h1></div><p>Сравнение approved и upgrade_queued файлов перед отдельным редакционным решением.</p></section> <section class="form-hero"><div><span class="eyebrow"><b>ADMIN</b> Media review</span><h1>Проверка<br/><em>медиа</em></h1></div><p>Сравнение approved и upgrade_queued файлов перед отдельным редакционным решением.</p></section>
<main class="moderation-app" data-api-url={apiUrl}> <main class="moderation-app" data-api-url={apiUrl}>
<AdminNav />
<form class="admin-login" autocomplete="off"><label>Административный токен<input name="token" type="password" required autocomplete="off" /></label><button data-action="primary" type="submit">Открыть медиатеку</button></form> <form class="admin-login" autocomplete="off"><label>Административный токен<input name="token" type="password" required autocomplete="off" /></label><button data-action="primary" type="submit">Открыть медиатеку</button></form>
<p class="privacy">Публичные файлы не переключаются из этого экрана. Токен хранится только в памяти страницы.</p> <p class="privacy">Публичные файлы не переключаются из этого экрана. Токен хранится только в памяти страницы.</p>
<div class="admin-session-bar" hidden><span>Административная сессия активна</span><button data-action="secondary" type="button" data-admin-logout>Выйти</button></div> <div class="admin-session-bar" hidden><span>Административная сессия активна</span><button data-action="secondary" type="button" data-admin-logout>Выйти</button></div>
@@ -1,10 +1,12 @@
--- ---
import Layout from "../../layouts/Layout.astro"; import Layout from "../../layouts/Layout.astro";
import AdminNav from "../../components/AdminNav.astro";
const apiUrl = import.meta.env.PUBLIC_API_URL || "http://localhost:8000"; const apiUrl = import.meta.env.PUBLIC_API_URL || "http://localhost:8000";
--- ---
<Layout title="Модерация уловов — RF4 Spotter"> <Layout title="Модерация уловов — RF4 Spotter">
<section class="form-hero"><div><span class="eyebrow"><b>ADMIN</b> Очередь проверки</span><h1>Модерация<br/><em>уловов</em></h1></div><p>Проверьте данные и скриншот до того, как запись повлияет на публичную статистику.</p></section> <section class="form-hero"><div><span class="eyebrow"><b>ADMIN</b> Очередь проверки</span><h1>Модерация<br/><em>уловов</em></h1></div><p>Проверьте данные и скриншот до того, как запись повлияет на публичную статистику.</p></section>
<section class="moderation-app" data-api-url={apiUrl}> <section class="moderation-app" data-api-url={apiUrl}>
<AdminNav />
<form class="admin-login" autocomplete="off"><label>Административный токен<input name="token" type="password" required autocomplete="off" /></label><button data-action="primary" type="submit">Открыть очередь</button></form> <form class="admin-login" autocomplete="off"><label>Административный токен<input name="token" type="password" required autocomplete="off" /></label><button data-action="primary" type="submit">Открыть очередь</button></form>
<p class="privacy">Токен хранится только в памяти страницы и не записывается в URL или localStorage.</p> <p class="privacy">Токен хранится только в памяти страницы и не записывается в URL или localStorage.</p>
<div class="admin-session-bar" hidden><span>Административная сессия активна</span><button data-action="secondary" type="button" data-admin-logout>Выйти</button></div> <div class="admin-session-bar" hidden><span>Административная сессия активна</span><button data-action="secondary" type="button" data-admin-logout>Выйти</button></div>
+3
View File
@@ -59,6 +59,9 @@ footer{min-height:118px;background:var(--deep);color:#dbe4df;padding:28px max(32
/* One semantic treatment for empty, unavailable and invalid public states. */ /* One semantic treatment for empty, unavailable and invalid public states. */
.state.unavailable-state{border-style:solid;border-color:color-mix(in srgb,var(--warning) 35%,var(--border));background:color-mix(in srgb,var(--warning-soft) 35%,var(--surface))}.state.error-state{background:color-mix(in srgb,var(--danger-soft) 28%,var(--surface))}.state>a{margin-top:8px;font-weight:750;text-underline-offset:4px} .state.unavailable-state{border-style:solid;border-color:color-mix(in srgb,var(--warning) 35%,var(--border));background:color-mix(in srgb,var(--warning-soft) 35%,var(--surface))}.state.error-state{background:color-mix(in srgb,var(--danger-soft) 28%,var(--surface))}.state>a{margin-top:8px;font-weight:750;text-underline-offset:4px}
/* Admin navigation keeps all operational surfaces reachable by keyboard. */
.admin-nav{display:flex;flex-wrap:wrap;gap:8px;margin:0 0 18px}.admin-nav a{padding:9px 13px;border:1px solid var(--line);border-radius:999px;color:var(--text-muted);font-size:12px;text-decoration:none}.admin-nav a:hover,.admin-nav a.active{border-color:var(--focus);color:var(--deep);background:var(--lime)}
/* Action hierarchy stays semantic across public and admin surfaces. */ /* Action hierarchy stays semantic across public and admin surfaces. */
[data-action]{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:42px;padding:0 18px;border:1px solid transparent;border-radius:10px;font-weight:750;text-decoration:none;transition:background-color var(--motion-fast) var(--ease-out),border-color var(--motion-fast) var(--ease-out),color var(--motion-fast) var(--ease-out),transform var(--motion-fast) var(--ease-out)}[data-action]:hover:not(:disabled){transform:translateY(-1px)}[data-action]:active:not(:disabled){transform:translateY(0)}[data-action="primary"]{background:var(--lime);color:var(--deep)}[data-action="secondary"]{border-color:var(--border);background:var(--surface-soft);color:var(--text-muted)}[data-action="inverse"]{border-color:#ffffff38;background:var(--deep);color:var(--white)}[data-action="danger"]{background:var(--danger-soft);color:var(--danger)}[data-action="quiet-danger"]{border-color:color-mix(in srgb,var(--danger) 35%,transparent);background:transparent;color:var(--danger)}[data-action="quiet"]{background:transparent;color:#d5dfdc;text-decoration:underline;text-underline-offset:4px}[data-action]:disabled{opacity:.55;cursor:not-allowed} [data-action]{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:42px;padding:0 18px;border:1px solid transparent;border-radius:10px;font-weight:750;text-decoration:none;transition:background-color var(--motion-fast) var(--ease-out),border-color var(--motion-fast) var(--ease-out),color var(--motion-fast) var(--ease-out),transform var(--motion-fast) var(--ease-out)}[data-action]:hover:not(:disabled){transform:translateY(-1px)}[data-action]:active:not(:disabled){transform:translateY(0)}[data-action="primary"]{background:var(--lime);color:var(--deep)}[data-action="secondary"]{border-color:var(--border);background:var(--surface-soft);color:var(--text-muted)}[data-action="inverse"]{border-color:#ffffff38;background:var(--deep);color:var(--white)}[data-action="danger"]{background:var(--danger-soft);color:var(--danger)}[data-action="quiet-danger"]{border-color:color-mix(in srgb,var(--danger) 35%,transparent);background:transparent;color:var(--danger)}[data-action="quiet"]{background:transparent;color:#d5dfdc;text-decoration:underline;text-underline-offset:4px}[data-action]:disabled{opacity:.55;cursor:not-allowed}