From 6196c39d250446b3d1dc1985a02765559c564897 Mon Sep 17 00:00:00 2001 From: NotBigGhost Date: Fri, 26 Jun 2026 16:36:54 +0300 Subject: [PATCH 01/15] =?UTF-8?q?UI:=20=D0=BA=D1=80=D1=83=D0=B3=20=D1=81?= =?UTF-8?q?=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=BF=D0=B0=D1=80?= =?UTF-8?q?=D1=82=D0=B8=D0=B8=20=E2=80=94=20=D0=BA=D1=80=D1=83=D0=BF=D0=BD?= =?UTF-8?q?=D0=B5=D0=B5,=20=D0=BF=D0=BE=D0=BB=D1=83=D0=BA=D1=80=D1=83?= =?UTF-8?q?=D0=B3,=20=D0=BF=D0=BE=20=D1=86=D0=B5=D0=BD=D1=82=D1=80=D1=83?= =?UTF-8?q?=20=D1=80=D0=B0=D0=B7=D0=B4=D0=B5=D0=BB=D1=8F=D0=B5=D1=82=20?= =?UTF-8?q?=D1=81=D0=BB=D0=BE=D1=82=D1=8B,=20=D1=81=D0=BC=D0=B5=D1=89?= =?UTF-8?q?=D1=91=D0=BD=20=D0=B2=D0=BD=D0=B8=D0=B7=20=D0=B8=20=D0=BD=D0=B0?= =?UTF-8?q?=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B4=D0=BD=D0=B8=D0=B9=20=D0=BF?= =?UTF-8?q?=D0=BB=D0=B0=D0=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- frontend/src/styles/layout.css | 14 +++++++++----- frontend/src/styles/tokens.css | 4 ++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/frontend/src/styles/layout.css b/frontend/src/styles/layout.css index c0fd8da..ea09fec 100644 --- a/frontend/src/styles/layout.css +++ b/frontend/src/styles/layout.css @@ -60,26 +60,30 @@ .bottom-bar .slot .name { color: var(--text); font-weight: 600; max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .bottom-bar .spacer { width: var(--fab-width); flex: 0 0 var(--fab-width); } +/* Круг создания партии: полукруг-купол, по центру разделяет слоты, смещён вниз + (база у нижней кромки панели) и поднят на передний план (z-index выше панели). */ .fab { position: absolute; left: 50%; transform: translateX(-50%); - bottom: calc(var(--bottombar-height) + var(--safe-bottom) - 2px); + bottom: calc(var(--safe-bottom) + 8px); width: var(--fab-width); height: var(--fab-height); border: 1px solid var(--accent); border-bottom: none; - border-top-left-radius: var(--fab-width); - border-top-right-radius: var(--fab-width); + /* Радиус = половина ширины → ровный полукруг (ширина = 2× высоты). */ + border-top-left-radius: calc(var(--fab-width) / 2); + border-top-right-radius: calc(var(--fab-width) / 2); border-bottom-left-radius: 0; border-bottom-right-radius: 0; display: grid; place-items: center; background: var(--accent); color: #fff; - box-shadow: 0 -4px 14px rgba(224, 83, 61, 0.45); + box-shadow: 0 -4px 16px rgba(224, 83, 61, 0.5); + z-index: 25; } -.fab svg { margin-top: 6px; } +.fab svg { margin-top: 8px; } .fab[aria-disabled="true"] { background: var(--surface-2); border-color: var(--border); diff --git a/frontend/src/styles/tokens.css b/frontend/src/styles/tokens.css index 5aedade..f341a39 100644 --- a/frontend/src/styles/tokens.css +++ b/frontend/src/styles/tokens.css @@ -15,8 +15,8 @@ /* Размеры */ --app-max-w: 480px; --bottombar-height: 64px; - --fab-width: 88px; - --fab-height: 44px; + --fab-width: 100px; + --fab-height: 50px; --radius: 14px; --radius-sm: 10px; --safe-bottom: env(safe-area-inset-bottom, 0px); From 460b57e792c8fe063167472cb2509e154bfa5514 Mon Sep 17 00:00:00 2001 From: NotBigGhost Date: Fri, 26 Jun 2026 16:39:20 +0300 Subject: [PATCH 02/15] =?UTF-8?q?UI:=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BA?= =?UTF-8?q?=D0=B0=20=C2=AB=D0=9C=D0=B5=D0=BD=D1=8E=C2=BB=20(=D0=B3=D0=B0?= =?UTF-8?q?=D0=BC=D0=B1=D1=83=D1=80=D0=B3=D0=B5=D1=80)=20=D0=B2=20=D0=BD?= =?UTF-8?q?=D0=B8=D0=B6=D0=BD=D0=B5=D0=B9=20=D0=BF=D0=B0=D0=BD=D0=B5=D0=BB?= =?UTF-8?q?=D0=B8=20=E2=86=92=20=D0=B1=D0=BE=D0=BA=D0=BE=D0=B2=D0=BE=D0=B5?= =?UTF-8?q?=20=D0=BC=D0=B5=D0=BD=D1=8E=20=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B0=20(=D0=BF=D0=BB=D0=B0=D1=88=D0=BA=D0=B0=20=D0=B0=D0=BA?= =?UTF-8?q?=D0=BA=D0=B0=D1=83=D0=BD=D1=82=D0=B0=20+=20=D0=92=D1=8B=D0=B9?= =?UTF-8?q?=D1=82=D0=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- frontend/src/components/AppShell.tsx | 6 ++- frontend/src/components/BottomBar.tsx | 10 ++--- frontend/src/components/SideMenu.tsx | 51 ++++++++++++++++++++++ frontend/src/styles/layout.css | 61 +++++++++++++++++++++++++++ 4 files changed, 122 insertions(+), 6 deletions(-) create mode 100644 frontend/src/components/SideMenu.tsx diff --git a/frontend/src/components/AppShell.tsx b/frontend/src/components/AppShell.tsx index 196fe46..1cb573c 100644 --- a/frontend/src/components/AppShell.tsx +++ b/frontend/src/components/AppShell.tsx @@ -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 = { "/": "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() { - + setMenuOpen(false)} /> + setMenuOpen(true)} /> ); } diff --git a/frontend/src/components/BottomBar.tsx b/frontend/src/components/BottomBar.tsx index 25d6a4c..f656d54 100644 --- a/frontend/src/components/BottomBar.tsx +++ b/frontend/src/components/BottomBar.tsx @@ -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() {
- + )} + +
+ +
+ + + ); +} diff --git a/frontend/src/styles/layout.css b/frontend/src/styles/layout.css index ea09fec..b942944 100644 --- a/frontend/src/styles/layout.css +++ b/frontend/src/styles/layout.css @@ -194,3 +194,64 @@ font-size: 13px; } .place-badge.first { background: var(--gold); color: #1a1a1a; border-color: var(--gold); } + +/* ─── Боковое меню (выезжает справа) ─────────────────────────────────────────── */ +.drawer-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + opacity: 0; + pointer-events: none; + transition: opacity 0.2s ease; + z-index: 60; +} +.drawer-overlay.open { opacity: 1; pointer-events: auto; } + +.side-menu { + position: fixed; + top: 0; + right: 0; + height: 100%; + width: min(300px, 84vw); + background: var(--surface); + border-left: 1px solid var(--border); + box-shadow: -10px 0 30px rgba(0, 0, 0, 0.5); + transform: translateX(100%); + transition: transform 0.22s ease; + z-index: 61; + display: flex; + flex-direction: column; + padding-top: var(--safe-top); +} +.side-menu.open { transform: translateX(0); } + +.side-account { + display: flex; + align-items: center; + gap: 12px; + width: 100%; + text-align: left; + padding: 16px; + background: none; + border: none; + border-bottom: 1px solid var(--border); + color: var(--text); +} +.side-account-info { display: flex; flex-direction: column; min-width: 0; } +.side-account-info b { font-size: 16px; overflow: hidden; text-overflow: ellipsis; } + +.side-menu-items { display: flex; flex-direction: column; padding: 8px; } +.side-menu-item { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + text-align: left; + padding: 12px 14px; + background: none; + border: none; + border-radius: var(--radius-sm); + color: var(--text); + font-weight: 600; +} +.side-menu-item.danger { color: var(--danger); } From d32237975d880007ba91f2d05d93082fe5ee44c2 Mon Sep 17 00:00:00 2001 From: NotBigGhost Date: Fri, 26 Jun 2026 16:40:49 +0300 Subject: [PATCH 03/15] =?UTF-8?q?UI:=20=D1=81=D0=B5=D0=BA=D1=80=D0=B5?= =?UTF-8?q?=D1=82=D0=BD=D1=8B=D0=B9=20=D0=B2=D1=85=D0=BE=D0=B4=20=D0=B2=20?= =?UTF-8?q?=D0=B0=D0=B4=D0=BC=D0=B8=D0=BD=D0=BA=D1=83=20=E2=80=94=20=D1=83?= =?UTF-8?q?=D0=B4=D0=B5=D1=80=D0=B6=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=BA=D0=BD?= =?UTF-8?q?=D0=BE=D0=BF=D0=BA=D0=B8=20=D0=BC=D0=B5=D0=BD=D1=8E=2010=D1=81;?= =?UTF-8?q?=20=D1=83=D0=B1=D1=80=D0=B0=D0=BD=D1=8B=20=D0=B2=D0=B8=D0=B4?= =?UTF-8?q?=D0=B8=D0=BC=D1=8B=D0=B5=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BA=D0=B8?= =?UTF-8?q?=20=D0=B2=D1=85=D0=BE=D0=B4=D0=B0=20=D0=B0=D0=B4=D0=BC=D0=B8?= =?UTF-8?q?=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- frontend/src/components/BottomBar.tsx | 34 ++++++++++++++++++++++++++- frontend/src/pages/AccountPage.tsx | 6 ----- frontend/src/pages/LoginPage.tsx | 10 -------- 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/frontend/src/components/BottomBar.tsx b/frontend/src/components/BottomBar.tsx index f656d54..f917ac6 100644 --- a/frontend/src/components/BottomBar.tsx +++ b/frontend/src/components/BottomBar.tsx @@ -1,9 +1,12 @@ import { Menu, Plus, Users } from "lucide-react"; +import { useRef } from "react"; import { useNavigate } from "react-router-dom"; import { useToast } from "../context/ToastContext"; import { useMe } from "../hooks/auth"; +const ADMIN_HOLD_MS = 10000; // секретный вход в админку — удержание кнопки меню 10с + export function BottomBar({ onMenu }: { onMenu: () => void }) { const { data: me } = useMe(); const navigate = useNavigate(); @@ -11,6 +14,28 @@ export function BottomBar({ onMenu }: { onMenu: () => void }) { const hasGroup = me?.active_group_id != null; const activeGroup = me?.groups.find((g) => g.id === me?.active_group_id); + // Долгое удержание кнопки меню → /admin/login; короткий тап → открыть боковое меню. + const holdTimer = useRef | null>(null); + const holdFired = useRef(false); + + const startHold = () => { + holdFired.current = false; + holdTimer.current = setTimeout(() => { + holdFired.current = true; + navigate("/admin/login"); + }, ADMIN_HOLD_MS); + }; + const clearHold = () => { + if (holdTimer.current) { + clearTimeout(holdTimer.current); + holdTimer.current = null; + } + }; + const endHold = () => { + clearHold(); + if (!holdFired.current) onMenu(); // обычный тап + }; + const onCreate = () => { if (!hasGroup) { toast.show("Вы не состоите в группе. Создайте или вступите в группу."); @@ -29,7 +54,14 @@ export function BottomBar({ onMenu }: { onMenu: () => void }) {
- diff --git a/frontend/src/pages/AccountPage.tsx b/frontend/src/pages/AccountPage.tsx index bae0b26..bf14762 100644 --- a/frontend/src/pages/AccountPage.tsx +++ b/frontend/src/pages/AccountPage.tsx @@ -158,12 +158,6 @@ export function AccountPage() { - -

- -

); } diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx index 2d94dfa..cb82bc0 100644 --- a/frontend/src/pages/LoginPage.tsx +++ b/frontend/src/pages/LoginPage.tsx @@ -93,16 +93,6 @@ export function LoginPage() { {showStub && !pendingTg && } {error &&

{error}

} - -

- -

); From 28c0358b45d4f3abf830acb8770cf7218e14b4b0 Mon Sep 17 00:00:00 2001 From: NotBigGhost Date: Fri, 26 Jun 2026 16:41:34 +0300 Subject: [PATCH 04/15] =?UTF-8?q?UI:=20=D0=B2=D0=BC=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D0=BE=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BA=D0=B8=20=C2=AB=D0=9D?= =?UTF-8?q?=D0=B0=D0=B7=D0=B0=D0=B4=C2=BB=20=E2=80=94=20=D0=BA=D0=BB=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=D0=B1=D0=B5=D0=BB=D1=8C=D0=BD=D0=BE=D0=B5=20=D0=BD?= =?UTF-8?q?=D0=B0=D0=B7=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=B8=D0=B3=D1=80?= =?UTF-8?q?=D1=8B,=20=D0=B2=D0=B5=D0=B4=D1=83=D1=89=D0=B5=D0=B5=20=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=B3=D0=BB=D0=B0=D0=B2=D0=BD=D1=83=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- frontend/src/components/AppShell.tsx | 10 +++------- frontend/src/styles/global.css | 8 ++++++++ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/AppShell.tsx b/frontend/src/components/AppShell.tsx index 1cb573c..a079818 100644 --- a/frontend/src/components/AppShell.tsx +++ b/frontend/src/components/AppShell.tsx @@ -32,13 +32,9 @@ export function AppShell() {
- {isHome ? ( - {title} - ) : ( - - )} +
{!isHome ? title : ""}
diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index 46c2deb..bb42ee7 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -121,6 +121,14 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent); } padding: 0 8px; } .top-bar-right { display: flex; align-items: center; } +.top-bar-home { + background: none; + border: none; + padding: 0; + color: var(--text); + font-weight: 700; + font-size: 17px; +} .notif-bell-wrap { position: relative; } .notif-bell { From c39c96d5dcc2bec0bdc101e1d54f9d24c36c589f Mon Sep 17 00:00:00 2001 From: NotBigGhost Date: Fri, 26 Jun 2026 16:42:05 +0300 Subject: [PATCH 05/15] =?UTF-8?q?UI:=20=D1=83=D0=B1=D1=80=D0=B0=D0=BD=20?= =?UTF-8?q?=D0=B2=D1=8B=D0=B1=D0=BE=D1=80=20=D0=B3=D1=80=D1=83=D0=BF=D0=BF?= =?UTF-8?q?=D1=8B=20=D0=B8=D0=B7=20=D0=B2=D0=BA=D0=BB=D0=B0=D0=B4=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=B0=D0=BA=D0=BA=D0=B0=D1=83=D0=BD=D1=82=D0=B0=20(?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D0=B0=D1=91=D1=82=D1=81=D1=8F=20=D0=BD=D0=B0?= =?UTF-8?q?=20=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D0=B5=20=D0=B3?= =?UTF-8?q?=D1=80=D1=83=D0=BF=D0=BF=D1=8B)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- frontend/src/pages/AccountPage.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/src/pages/AccountPage.tsx b/frontend/src/pages/AccountPage.tsx index bf14762..2619b87 100644 --- a/frontend/src/pages/AccountPage.tsx +++ b/frontend/src/pages/AccountPage.tsx @@ -3,7 +3,6 @@ import { useNavigate } from "react-router-dom"; import { ApiError } from "../api/client"; import { Avatar } from "../components/Avatar"; -import { GroupSwitcher } from "../components/GroupSwitcher"; import { ProfileStatsCard } from "../components/ProfileStatsCard"; import { Spinner } from "../components/Spinner"; import { useToast } from "../context/ToastContext"; @@ -153,8 +152,6 @@ export function AccountPage() { {stats && } - - From cd7e5d16b4f72bdc9a8efede2f75f7e694e5ecba Mon Sep 17 00:00:00 2001 From: NotBigGhost Date: Fri, 26 Jun 2026 16:43:10 +0300 Subject: [PATCH 06/15] =?UTF-8?q?UI:=20=D0=B4=D0=BE=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B3=D1=80=D1=83=D0=BF=D0=BF?= =?UTF-8?q?=D1=8B=20=E2=80=94=20=D0=B3=D0=BE=D1=80=D0=B8=D0=B7=D0=BE=D0=BD?= =?UTF-8?q?=D1=82=D0=B0=D0=BB=D1=8C=D0=BD=D1=8B=D0=B5=20=D1=82=D1=83=D0=BC?= =?UTF-8?q?=D0=B1=D0=BB=D0=B5=D1=80=D1=8B=20=D0=B2=D0=BC=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D0=BE=20=D0=B3=D0=B0=D0=BB=D0=BE=D1=87=D0=B5=D0=BA;=20=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=BE=D0=BA=D0=B0=20=D0=B1=D0=B0=D0=B7=D0=BE=D0=B2?= =?UTF-8?q?=D0=BE=D0=B9=20=D0=B8=D0=B3=D1=80=D1=8B=20=D1=81=D0=BA=D1=80?= =?UTF-8?q?=D1=8B=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- frontend/src/pages/GroupSettingsPage.tsx | 33 +++++++++++++----------- frontend/src/styles/global.css | 33 ++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 15 deletions(-) diff --git a/frontend/src/pages/GroupSettingsPage.tsx b/frontend/src/pages/GroupSettingsPage.tsx index ea05dfc..31a4f5f 100644 --- a/frontend/src/pages/GroupSettingsPage.tsx +++ b/frontend/src/pages/GroupSettingsPage.tsx @@ -72,21 +72,24 @@ export function GroupSettingsPage() {

Дополнения

- {(expansions ?? []).map((e) => ( - - ))} + {(expansions ?? []) + .filter((e) => !e.is_base) + .map((e) => ( + + ))}
{isOwner && (
- {row.was_random && row.faction_id !== "" && 🎲 случайная}
))} diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index 21b0ba5..6bfb96a 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -92,6 +92,27 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent); } .error-text { color: var(--danger); font-size: 13px; } +/* «Барабан» рандома фракции — мелькающее название вместо select */ +.faction-spin { + display: flex; + align-items: center; + justify-content: center; + padding: 10px 12px; + border: 1px solid var(--accent); + border-radius: var(--radius-sm); + background: var(--surface-2); + color: var(--text); + font-weight: 700; + white-space: nowrap; + overflow: hidden; + animation: faction-roll 0.45s ease-in-out infinite; +} +@keyframes faction-roll { + 0% { opacity: 0.5; transform: translateY(-3px); } + 50% { opacity: 1; transform: translateY(0); } + 100% { opacity: 0.5; transform: translateY(3px); } +} + /* Тумблер-переключатель (вкл/выкл) */ .switch { position: relative; display: inline-block; width: 44px; height: 26px; flex: 0 0 auto; } .switch input { From 7269afcd4e9cc9f99e1ceed558a2330d23554bbb Mon Sep 17 00:00:00 2001 From: NotBigGhost Date: Fri, 26 Jun 2026 16:57:35 +0300 Subject: [PATCH 09/15] =?UTF-8?q?UI:=20=D0=BF=D0=BE=D0=B4=D1=82=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D0=B6=D0=B4=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=B4=20=D0=BE=D1=82=D0=BC=D0=B5=D0=BD=D0=BE=D0=B9?= =?UTF-8?q?=20=D0=BD=D0=B5=D0=B7=D0=B0=D0=B2=D0=B5=D1=80=D1=88=D1=91=D0=BD?= =?UTF-8?q?=D0=BD=D0=BE=D0=B9=20=D0=B8=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=D0=BC=20=D1=81=D1=8B=D0=B3=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=D0=BD=D0=BE=D0=B9=20=D0=BF=D0=B0=D1=80=D1=82=D0=B8=D0=B8=20(?= =?UTF-8?q?=D0=BC=D0=BE=D0=B4=D0=B0=D0=BB=D0=BA=D0=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- frontend/src/components/ConfirmDialog.tsx | 41 +++++++++++++++++++++++ frontend/src/pages/MatchDetailPage.tsx | 26 +++++++++++++- frontend/src/styles/global.css | 25 ++++++++++++++ 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/ConfirmDialog.tsx diff --git a/frontend/src/components/ConfirmDialog.tsx b/frontend/src/components/ConfirmDialog.tsx new file mode 100644 index 0000000..bddc2c8 --- /dev/null +++ b/frontend/src/components/ConfirmDialog.tsx @@ -0,0 +1,41 @@ +/** Переиспользуемое модальное подтверждение (без нативного confirm()). */ +export function ConfirmDialog({ + open, + title, + message, + confirmLabel = "Подтвердить", + cancelLabel = "Отмена", + danger = false, + onConfirm, + onCancel, +}: { + open: boolean; + title: string; + message?: string; + confirmLabel?: string; + cancelLabel?: string; + danger?: boolean; + onConfirm: () => void; + onCancel: () => void; +}) { + if (!open) return null; + return ( +
+
e.stopPropagation()}> +
{title}
+ {message &&

{message}

} +
+ + +
+
+
+ ); +} diff --git a/frontend/src/pages/MatchDetailPage.tsx b/frontend/src/pages/MatchDetailPage.tsx index 32323a9..d68ec50 100644 --- a/frontend/src/pages/MatchDetailPage.tsx +++ b/frontend/src/pages/MatchDetailPage.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { ApiError } from "../api/client"; +import { ConfirmDialog } from "../components/ConfirmDialog"; import { MatchMedia } from "../components/MatchMedia"; import { Spinner } from "../components/Spinner"; import { formatDuration, formatTime } from "../domain/format"; @@ -39,6 +40,7 @@ export function MatchDetailPage() { const [winReason, setWinReason] = useState("objectives"); const [overall, setOverall] = useState(""); const [error, setError] = useState(null); + const [confirmRemove, setConfirmRemove] = useState(false); if (isLoading) return ; if (!match) return
Партия не найдена.
; @@ -271,10 +273,32 @@ export function MatchDetailPage() { )} {canModify && ( - )} + + setConfirmRemove(false)} + onConfirm={() => { + setConfirmRemove(false); + remove(); + }} + />
); } diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index 6bfb96a..b40c8f3 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -68,6 +68,7 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent); } .btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; } .btn-ghost { background: transparent; } .btn-danger { background: transparent; color: var(--danger); border-color: var(--danger); } +.btn-danger-solid { background: var(--danger); border-color: var(--danger); color: #fff; } .btn[disabled] { opacity: 0.5; pointer-events: none; } .row { display: flex; align-items: center; gap: 10px; } @@ -92,6 +93,30 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent); } .error-text { color: var(--danger); font-size: 13px; } +/* Модальное окно подтверждения */ +.modal-overlay { + position: fixed; + inset: 0; + z-index: 300; + background: rgba(0, 0, 0, 0.6); + display: flex; + align-items: center; + justify-content: center; + padding: 24px; +} +.modal { + width: 100%; + max-width: 360px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 18px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6); +} +.modal-title { font-weight: 700; font-size: 16px; margin-bottom: 8px; } +.modal-msg { color: var(--text-muted); font-size: 14px; margin: 0 0 16px; } +.modal-actions { display: flex; justify-content: flex-end; gap: 8px; } + /* «Барабан» рандома фракции — мелькающее название вместо select */ .faction-spin { display: flex; From 395b134c9b5ae08a9a5dd9d3c8a21f855f31f03b Mon Sep 17 00:00:00 2001 From: NotBigGhost Date: Fri, 26 Jun 2026 16:58:32 +0300 Subject: [PATCH 10/15] =?UTF-8?q?UI:=20=D0=BF=D0=BE=D0=BB=D0=B5=20=C2=AB?= =?UTF-8?q?=D0=9E=20=D1=81=D0=B5=D0=B1=D0=B5=C2=BB=20=D0=B1=D0=BE=D0=BB?= =?UTF-8?q?=D1=8C=D1=88=D0=B5=20=D0=BD=D0=B5=D0=BB=D1=8C=D0=B7=D1=8F=20?= =?UTF-8?q?=D1=80=D0=B0=D1=81=D1=82=D1=8F=D0=B3=D0=B8=D0=B2=D0=B0=D1=82?= =?UTF-8?q?=D1=8C=20(resize:=20none)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- frontend/src/pages/AccountPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/pages/AccountPage.tsx b/frontend/src/pages/AccountPage.tsx index 2619b87..7c8a336 100644 --- a/frontend/src/pages/AccountPage.tsx +++ b/frontend/src/pages/AccountPage.tsx @@ -124,6 +124,7 @@ export function AccountPage() { rows={3} maxLength={500} placeholder="Пара слов о себе…" + style={{ resize: "none" }} value={bioValue} onChange={(e) => setBio(e.target.value)} /> From 712dc529c34eba4aed9596cc78b0a797d789e520 Mon Sep 17 00:00:00 2001 From: NotBigGhost Date: Fri, 26 Jun 2026 16:59:28 +0300 Subject: [PATCH 11/15] =?UTF-8?q?UI:=20=D1=81=D0=BF=D0=B8=D1=81=D0=BE?= =?UTF-8?q?=D0=BA=20=D0=BF=D0=B0=D1=80=D1=82=D0=B8=D0=B9=20=D0=B3=D1=80?= =?UTF-8?q?=D1=83=D0=BF=D0=BF=D1=8B=20=E2=80=94=20=D0=B4=D0=B0=D1=82=D0=B0?= =?UTF-8?q?=20=D1=81=D0=B5=D1=80=D0=BE=D0=B9,=20=D0=BD=D0=B8=D0=BA=D0=B8?= =?UTF-8?q?=20=D0=B8=20=D0=BD=D0=BE=D0=BC=D0=B5=D1=80=D0=B0=20=D0=BC=D0=B5?= =?UTF-8?q?=D1=81=D1=82=20=D0=B1=D0=B5=D0=BB=D1=8B=D0=BC=20=D1=82=D0=B5?= =?UTF-8?q?=D0=BA=D1=81=D1=82=D0=BE=D0=BC=20(=D0=BA=D0=BE=D0=BD=D1=82?= =?UTF-8?q?=D1=80=D0=B0=D1=81=D1=82)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- frontend/src/components/MatchList.tsx | 2 +- frontend/src/styles/global.css | 1 + frontend/src/styles/layout.css | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/MatchList.tsx b/frontend/src/components/MatchList.tsx index 503e553..7aac38b 100644 --- a/frontend/src/components/MatchList.tsx +++ b/frontend/src/components/MatchList.tsx @@ -21,7 +21,7 @@ export function MatchListView({ items }: { items: MatchListItem[] }) { onClick={() => navigate(`/match/${m.id}`)} >
- {m.played_at} + {m.played_at} {inProgress ? ( идёт ) : ( diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index b40c8f3..4d2440d 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -50,6 +50,7 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent); } border-radius: var(--radius); padding: 14px; margin-bottom: 12px; + color: var(--text); /* карточки-кнопки (список партий) — белый текст по умолчанию */ } .btn { diff --git a/frontend/src/styles/layout.css b/frontend/src/styles/layout.css index 7a64523..069e90a 100644 --- a/frontend/src/styles/layout.css +++ b/frontend/src/styles/layout.css @@ -190,6 +190,7 @@ border-radius: 6px; background: var(--surface); border: 1px solid var(--border); + color: var(--text); /* номер места — белым для контраста */ font-weight: 700; font-size: 13px; } From cec3aa25a6ef9a663fc72c9983342cc1792fe5d4 Mon Sep 17 00:00:00 2001 From: NotBigGhost Date: Fri, 26 Jun 2026 17:44:28 +0300 Subject: [PATCH 12/15] =?UTF-8?q?=D0=9C=D0=B5=D0=BB=D0=BA=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20=D0=B2=D1=91=D1=80=D1=81?= =?UTF-8?q?=D1=82=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/MatchDetailPage.tsx | 2 +- frontend/src/styles/tokens.css | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/MatchDetailPage.tsx b/frontend/src/pages/MatchDetailPage.tsx index d68ec50..303dda0 100644 --- a/frontend/src/pages/MatchDetailPage.tsx +++ b/frontend/src/pages/MatchDetailPage.tsx @@ -215,7 +215,7 @@ export function MatchDetailPage() { {n} ))} - +
diff --git a/frontend/src/styles/tokens.css b/frontend/src/styles/tokens.css index f341a39..1d35adf 100644 --- a/frontend/src/styles/tokens.css +++ b/frontend/src/styles/tokens.css @@ -15,8 +15,8 @@ /* Размеры */ --app-max-w: 480px; --bottombar-height: 64px; - --fab-width: 100px; - --fab-height: 50px; + --fab-width: 125px; + --fab-height: 62.5px; --radius: 14px; --radius-sm: 10px; --safe-bottom: env(safe-area-inset-bottom, 0px); From 0745d07887d15cc33fdfe960d56fedc86c92fa7b Mon Sep 17 00:00:00 2001 From: NotBigGhost Date: Fri, 26 Jun 2026 17:48:08 +0300 Subject: [PATCH 13/15] =?UTF-8?q?UI:=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BA?= =?UTF-8?q?=D0=B0=20=C2=AB=D0=9C=D0=B5=D0=BD=D1=8E=C2=BB=20=D0=BD=D0=B0?= =?UTF-8?q?=D0=B4=D1=91=D0=B6=D0=BD=D0=BE=20=D0=BE=D1=82=D0=BA=D1=80=D1=8B?= =?UTF-8?q?=D0=B2=D0=B0=D0=B5=D1=82=D1=81=D1=8F=20=D0=BD=D0=B0=20=D0=BC?= =?UTF-8?q?=D0=BE=D0=B1=D0=B8=D0=BB=D1=8C=D0=BD=D1=8B=D1=85=20(onClick=20?= =?UTF-8?q?=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=BE=20pointerup=20+=20touch-ac?= =?UTF-8?q?tion)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Открытие бокового меню переведено на нативный onClick: на тач-устройствах pointerup часто не приходит из-за pointercancel, из-за чего меню не открывалось. Долгое удержание (10с → админка) оставлено на таймере и подавляет этот клик. Co-Authored-By: Claude Opus 4.8 --- frontend/src/components/BottomBar.tsx | 14 ++++++++++---- frontend/src/styles/layout.css | 1 + 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/BottomBar.tsx b/frontend/src/components/BottomBar.tsx index f917ac6..eab3c57 100644 --- a/frontend/src/components/BottomBar.tsx +++ b/frontend/src/components/BottomBar.tsx @@ -31,9 +31,14 @@ export function BottomBar({ onMenu }: { onMenu: () => void }) { holdTimer.current = null; } }; - const endHold = () => { - clearHold(); - if (!holdFired.current) onMenu(); // обычный тап + // Открытие меню — на нативном onClick (надёжно на мобильных, в отличие от pointerup, + // который не приходит при pointercancel). Долгий зажим подавляет этот клик. + const onMenuClick = () => { + if (holdFired.current) { + holdFired.current = false; + return; + } + onMenu(); }; const onCreate = () => { @@ -57,8 +62,9 @@ export function BottomBar({ onMenu }: { onMenu: () => void }) { From 6d75b4c13030f89d5af55ce95ac3e9ef2ae21590 Mon Sep 17 00:00:00 2001 From: NotBigGhost Date: Fri, 26 Jun 2026 18:14:54 +0300 Subject: [PATCH 15/15] =?UTF-8?q?=D0=9E=D1=84=D0=BE=D1=80=D0=BC=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BE=D1=84=D1=84=D0=BB=D0=B0=D0=B9?= =?UTF-8?q?=D0=BD-=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D1=8B=20?= =?UTF-8?q?=D1=81=D0=B0=D0=B9=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/vps/Caddyfile | 21 ++++++++++++ deploy/vps/README.md | 16 ++++++++-- deploy/vps/maintenance.html | 64 +++++++++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 deploy/vps/maintenance.html diff --git a/deploy/vps/Caddyfile b/deploy/vps/Caddyfile index e7f360b..678927f 100644 --- a/deploy/vps/Caddyfile +++ b/deploy/vps/Caddyfile @@ -11,6 +11,25 @@ # # SSE (/api/events): отдельный handle БЕЗ encode и с flush_interval -1 — иначе сжатие/ # буферизация задержат поток событий (реал-тайм перестанет работать). +# +# Туннель лёг (апстрим недоступен) → reverse_proxy отдаёт 502/503/504, и сниппет +# (offline) подменяет ответ на свою страницу-заглушку (/etc/caddy/maintenance/maintenance.html), +# вместо «зависшей загрузки» SPA. Файл заглушки — deploy/vps/maintenance.html. + +# Заглушка «сайт недоступен» при недоступности апстрима. Импортируется в каждый сайт. +(offline) { + handle_errors { + @down expression `{http.error.status_code} in [502, 503, 504]` + handle @down { + root * /etc/caddy/maintenance + rewrite * /maintenance.html + header Cache-Control "no-store" + file_server { + status 503 + } + } + } +} forbiddenstars.ru { tls /etc/caddy/certs/forbiddenstars.ru/fullchain.pem /etc/caddy/certs/forbiddenstars.ru/privkey.pem @@ -24,6 +43,7 @@ forbiddenstars.ru { encode zstd gzip reverse_proxy 127.0.0.1:9000 } + import offline } forbidden-stars.ru { @@ -38,4 +58,5 @@ forbidden-stars.ru { encode zstd gzip reverse_proxy 127.0.0.1:9001 } + import offline } diff --git a/deploy/vps/README.md b/deploy/vps/README.md index a546449..06307bb 100644 --- a/deploy/vps/README.md +++ b/deploy/vps/README.md @@ -85,16 +85,26 @@ rm -rf /root/certs-tmp > как «промежуточную» часть. Если дал один файл, где leaf и chain уже вместе, — это и есть > `fullchain.pem` (склейка не нужна). -## 6. Caddyfile -Скопируй `deploy/vps/Caddyfile` из репозитория в `/etc/caddy/Caddyfile`, затем: +## 6. Caddyfile + страница-заглушка +Скопируй `deploy/vps/Caddyfile` из репозитория в `/etc/caddy/Caddyfile` и положи страницу +«сайт недоступен» (показывается, когда туннель к хосту лежит — вместо зависшей «Загрузки» SPA): ```bash +mkdir -p /etc/caddy/maintenance +# с локальной машины: +scp deploy/vps/maintenance.html root@186.246.51.17:/etc/caddy/maintenance/maintenance.html +# на VPS: caddy validate --config /etc/caddy/Caddyfile systemctl reload caddy ``` +> Заглушка живёт в сниппете `(offline)` Caddyfile: при ответе апстрима 502/503/504 +> Caddy отдаёт `/etc/caddy/maintenance/maintenance.html` со статусом 503 и `Cache-Control: no-store`. +> Правки текста/вида — в `deploy/vps/maintenance.html`, затем повтори `scp` (reload Caddy не нужен, +> файл читается на каждый запрос). ## 7. Проверка 1. Подними туннель прода на Pi (см. `deploy/pi/README.md`) и/или dev/test на ПК (`run.ps1` при `LOCAL_PUBLIC=vps`). 2. Открой `https://forbiddenstars.ru` и `https://forbidden-stars.ru`. -3. Пока соответствующий туннель не поднят — Caddy отдаёт `502`, это ожидаемо. +3. Пока соответствующий туннель не поднят — Caddy отдаёт страницу-заглушку «Технические шоколадки» + (HTTP 503), это ожидаемо. Логи Caddy: `journalctl -u caddy -f`. diff --git a/deploy/vps/maintenance.html b/deploy/vps/maintenance.html new file mode 100644 index 0000000..d6a71fe --- /dev/null +++ b/deploy/vps/maintenance.html @@ -0,0 +1,64 @@ + + + + + + + + + Сайт недоступен — Forbidden Stars + + + +
+
🍫
+

Технические шоколадки

+

Сайт временно недоступен — мы уже чиним. Загляните через минутку.

+
Страница обновится сама, как только всё заработает
+
+ +