UI: кнопка «Меню» (гамбургер) в нижней панели → боковое меню справа (плашка аккаунта + Выйти)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 16:39:20 +03:00
co-authored by Claude Opus 4.8
parent 6196c39d25
commit 460b57e792
4 changed files with 122 additions and 6 deletions
+5 -1
View File
@@ -1,3 +1,4 @@
import { useState } from "react";
import { Outlet, useLocation, useNavigate } from "react-router-dom";
import { useMe } from "../hooks/auth";
@@ -5,6 +6,7 @@ import { useServerEvents } from "../hooks/useServerEvents";
import { BottomBar } from "./BottomBar";
import { NotificationBell } from "./NotificationBell";
import { NotificationToaster } from "./NotificationToaster";
import { SideMenu } from "./SideMenu";
const TITLES: Record<string, string> = {
"/": "Forbidden Stars",
@@ -19,6 +21,7 @@ export function AppShell() {
const location = useLocation();
const navigate = useNavigate();
const { data: me } = useMe();
const [menuOpen, setMenuOpen] = useState(false);
useServerEvents(!!me); // живые обновления (SSE) только при наличии сессии
const title =
TITLES[location.pathname] ??
@@ -49,7 +52,8 @@ export function AppShell() {
<Outlet />
</main>
<BottomBar />
<SideMenu open={menuOpen} onClose={() => setMenuOpen(false)} />
<BottomBar onMenu={() => setMenuOpen(true)} />
</div>
);
}
+5 -5
View File
@@ -1,10 +1,10 @@
import { Plus, Settings, Users } from "lucide-react";
import { Menu, Plus, Users } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { useToast } from "../context/ToastContext";
import { useMe } from "../hooks/auth";
export function BottomBar() {
export function BottomBar({ onMenu }: { onMenu: () => void }) {
const { data: me } = useMe();
const navigate = useNavigate();
const toast = useToast();
@@ -29,9 +29,9 @@ export function BottomBar() {
<div className="spacer" />
<button className="slot" onClick={() => navigate("/account")}>
<Settings size={20} />
<span>Аккаунт</span>
<button className="slot" onClick={onMenu} aria-label="Меню">
<Menu size={20} />
<span>Меню</span>
</button>
<button
+51
View File
@@ -0,0 +1,51 @@
import { LogOut } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { useLogout, useMe } from "../hooks/auth";
import { useMyStats } from "../hooks/stats";
import { Avatar } from "./Avatar";
/** Боковое меню, выезжающее из правого края. Сверху — кликабельная плашка аккаунта
* (краткая сводка → /account), ниже — выход. Открытием управляет AppShell. */
export function SideMenu({ open, onClose }: { open: boolean; onClose: () => void }) {
const { data: me } = useMe();
const { data: stats } = useMyStats();
const logout = useLogout();
const navigate = useNavigate();
const goAccount = () => {
onClose();
navigate("/account");
};
const doLogout = async () => {
onClose();
await logout.mutateAsync().catch(() => {});
navigate("/login", { replace: true });
};
return (
<>
<div className={"drawer-overlay" + (open ? " open" : "")} onClick={onClose} />
<aside className={"side-menu" + (open ? " open" : "")} aria-hidden={!open}>
{me && (
<button className="side-account" onClick={goAccount}>
<Avatar url={me.avatar_url} nickname={me.nickname} size={48} />
<span className="side-account-info">
<b>{me.nickname}</b>
<span className="muted small">
игр {stats?.overall.games ?? 0} · побед {stats?.overall.wins ?? 0}
</span>
</span>
</button>
)}
<div className="side-menu-items">
<button className="side-menu-item danger" onClick={doLogout}>
<LogOut size={18} /> Выйти
</button>
</div>
</aside>
</>
);
}