Files
ForbiddenStarsApp/frontend/src/pages/admin/AdminAccountsPage.tsx
T
NotBigGhostandClaude Fable 5 28852f9d38 UI: заголовки страниц без дублирования верхней панели и вкладок
Единое правило: название страницы показывает верхняя панель (или активная
вкладка админки), тело страницы его не повторяет. Убраны дубли на общей
статистике, в справке и в админ-разделах (аккаунты, фракции, журнал);
пояснительные абзацы сохранены. Заголовки-сущности (имя группы, дата
партии) остаются h2 в теле.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 17:46:23 +03:00

105 lines
4.0 KiB
TypeScript

import { Pencil } from "lucide-react";
import { useState } from "react";
import { ApiError } from "../../api/client";
import { Spinner } from "../../components/Spinner";
import { useToast } from "../../context/ToastContext";
import { useAdminUpdateUser, useAdminUsers } from "../../hooks/admin";
// DEV-ТОЛЬКО: удаление аккаунтов. Импорт используется лишь под import.meta.env.DEV,
// поэтому в прод/тест-сборке вырезается тришейкингом (как и dev-вход).
import { DevDeleteAccountButton } from "./DevDeleteAccountButton";
export function AdminAccountsPage() {
const { data: users, isLoading } = useAdminUsers();
const update = useAdminUpdateUser();
const toast = useToast();
const [editId, setEditId] = useState<number | null>(null);
const [editName, setEditName] = useState("");
if (isLoading) return <Spinner />;
const startEdit = (id: number, nickname: string) => {
setEditId(id);
setEditName(nickname);
};
const saveName = async () => {
const name = editName.trim();
if (editId == null || !name) return;
try {
await update.mutateAsync({ userId: editId, nickname: name });
toast.show("Ник обновлён");
setEditId(null);
} catch (e) {
toast.show(e instanceof ApiError ? e.message : "Не удалось переименовать");
}
};
const toggleActive = async (id: number, isActive: boolean) => {
await update.mutateAsync({ userId: id, is_active: !isActive }).catch(() => {});
toast.show("Сохранено");
};
return (
<div>
{/* Название раздела уже в активной вкладке админки. */}
<div className="stack">
{(users ?? []).map((u) => (
<div key={u.id} className="card" style={{ margin: 0 }}>
{editId === u.id ? (
// Режим переименования.
<div className="row">
<input
value={editName}
autoFocus
onChange={(e) => setEditName(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") saveName();
if (e.key === "Escape") setEditId(null);
}}
/>
<button
className="btn btn-primary small"
disabled={update.isPending || !editName.trim() || editName.trim() === u.nickname}
onClick={saveName}
>
Сохранить
</button>
<button className="btn btn-ghost small" onClick={() => setEditId(null)}>
Отмена
</button>
</div>
) : (
<div className="row-between">
<div>
<b>{u.nickname}</b> {u.role === "admin" && <span className="badge">админ</span>}
<div className="muted small">
#{u.id} · {u.auth_provider} · {u.is_active ? "активен" : "отключён"}
</div>
</div>
{u.role !== "admin" && (
<div className="row" style={{ gap: 4 }}>
<button
className="btn btn-ghost small"
title="Переименовать"
onClick={() => startEdit(u.id, u.nickname)}
>
<Pencil size={16} />
</button>
{import.meta.env.DEV && (
<DevDeleteAccountButton userId={u.id} nickname={u.nickname} />
)}
<button className="btn small" onClick={() => toggleActive(u.id, u.is_active)}>
{u.is_active ? "Отключить" : "Включить"}
</button>
</div>
)}
</div>
)}
</div>
))}
</div>
</div>
);
}