From 6f88658c60147a427da3783b89702554c5299f61 Mon Sep 17 00:00:00 2001 From: Halit Aksoy Date: Sat, 17 Oct 2026 02:59:26 +0300 Subject: [PATCH] feat(frontend): simplify the header as in the updated design The header is now the logo, the page's actions, how to play, settings and the account. The theme swatches move into a settings menu; the account shows the player's initials and is on every page through an AuthUserContext. On phones the home page's actions leave the header for a row of two buttons. Checked menu items get a check at their end. --- TODO.md | 1 + apps/frontend/src/MancalaApp.tsx | 5 +- apps/frontend/src/components/BotLevelMenu.tsx | 8 +- apps/frontend/src/components/Icons.tsx | 8 +- .../src/components/headerbar/AccountMenu.tsx | 11 +-- .../src/components/headerbar/AppHeader.tsx | 29 ++++--- .../src/components/headerbar/SettingsMenu.tsx | 37 +++++++++ .../components/headerbar/ThemeSwatches.tsx | 26 ------ apps/frontend/src/context/AuthUserContext.ts | 5 ++ apps/frontend/src/routes/Home.tsx | 13 ++- apps/frontend/src/styles/app.css | 80 ++++++++++++++----- apps/frontend/src/theme/themes.ts | 5 ++ docs/specs/theme-polish/design.md | 14 ++++ docs/specs/theme-polish/requirements.md | 13 ++- docs/specs/theme-polish/tasks.md | 2 + packages/core/src/localization/en.ts | 6 ++ packages/core/src/localization/tr.ts | 6 ++ 17 files changed, 191 insertions(+), 78 deletions(-) create mode 100644 apps/frontend/src/components/headerbar/SettingsMenu.tsx delete mode 100644 apps/frontend/src/components/headerbar/ThemeSwatches.tsx create mode 100644 apps/frontend/src/context/AuthUserContext.ts diff --git a/TODO.md b/TODO.md index d75042a..89cc2e9 100644 --- a/TODO.md +++ b/TODO.md @@ -79,6 +79,7 @@ See `docs/specs/tutorial`. See `docs/specs/web-redesign`. - [ ] Bring the new design to the mobile app (themes, board, game screen) +- [ ] The design's "Sound effects" switch in the settings menu, once the app has sounds ## Rematch diff --git a/apps/frontend/src/MancalaApp.tsx b/apps/frontend/src/MancalaApp.tsx index 4c0be81..168e5eb 100644 --- a/apps/frontend/src/MancalaApp.tsx +++ b/apps/frontend/src/MancalaApp.tsx @@ -22,6 +22,7 @@ import Util from './util/Util'; import { server } from './const/config'; import { useTranslation } from 'react-i18next'; import { applyThemeClass } from './theme/themes'; +import { AuthUserContext } from './context/AuthUserContext'; import './styles/app.css'; const context = initContext(); @@ -122,7 +123,7 @@ const MancalaApp: FunctionComponent = () => { return ( <> - {hasSession && + {hasSession && } /> @@ -138,7 +139,7 @@ const MancalaApp: FunctionComponent = () => { } /> - } + } {Util.getTextByConnectionState(context, connectionState, t)} diff --git a/apps/frontend/src/components/BotLevelMenu.tsx b/apps/frontend/src/components/BotLevelMenu.tsx index 6a43278..6625fa5 100644 --- a/apps/frontend/src/components/BotLevelMenu.tsx +++ b/apps/frontend/src/components/BotLevelMenu.tsx @@ -8,13 +8,13 @@ import { BotIcon } from "./Icons"; import "@szhsin/react-menu/dist/index.css"; import "@szhsin/react-menu/dist/transitions/slide.css" -// "Play with bot" in the header: a menu of the levels, the one chosen last marked. -const BotLevelMenu: FunctionComponent<{ context: Context, onChoose: (level: BotLevel) => void }> = ({ context, onChoose }) => { +// "Play with bot": a menu of the levels, the one chosen last marked. `className` styles the button: the header's hides on phones, the home page's row of actions shows it there. +const BotLevelMenu: FunctionComponent<{ context: Context, onChoose: (level: BotLevel) => void, className?: string, label?: string }> = ({ context, onChoose, className, label }) => { const { t } = useTranslation(); const lastLevel = getLastBotLevel(context.storage); const menuButton = ( - ); const choose = (level: BotLevel) => { diff --git a/apps/frontend/src/components/Icons.tsx b/apps/frontend/src/components/Icons.tsx index 37f93d1..99f7a93 100644 --- a/apps/frontend/src/components/Icons.tsx +++ b/apps/frontend/src/components/Icons.tsx @@ -24,10 +24,6 @@ export const PlusIcon = ({ size = 20 }: IconProps) => ( ); -export const ProfileIcon = ({ size = 20 }: IconProps) => ( - -); - export const LeaveIcon = ({ size = 20 }: IconProps) => ( ); @@ -72,6 +68,10 @@ export const BotIcon = ({ size = 20 }: IconProps) => ( ); +export const SettingsIcon = ({ size = 22 }: IconProps) => ( + +); + export const HelpIcon = ({ size = 20 }: IconProps) => ( ); diff --git a/apps/frontend/src/components/headerbar/AccountMenu.tsx b/apps/frontend/src/components/headerbar/AccountMenu.tsx index f971cf6..5fc2446 100644 --- a/apps/frontend/src/components/headerbar/AccountMenu.tsx +++ b/apps/frontend/src/components/headerbar/AccountMenu.tsx @@ -4,7 +4,7 @@ import { FunctionComponent } from "react"; import { AuthUser, getPlayerDisplayName } from "@mancala/core"; import { useTranslation } from "react-i18next"; import { Context } from "../../context/context"; -import { ProfileIcon } from "../Icons"; +import { getInitials } from "../../util/PlayerText"; import "@szhsin/react-menu/dist/index.css"; import "@szhsin/react-menu/dist/transitions/slide.css" @@ -12,9 +12,10 @@ import "@szhsin/react-menu/dist/transitions/slide.css" const AccountMenu: FunctionComponent<{ context: Context, authUser: AuthUser }> = ({ context, authUser }) => { const { t } = useTranslation(); const authService = context.authService; - const menuButton = ( - ); const menuItems = authUser.isGuest ? [ @@ -25,7 +26,7 @@ const AccountMenu: FunctionComponent<{ context: Context, authUser: AuthUser }> = ]; return ( - {getPlayerDisplayName(authUser, t)} + {name} {menuItems} ); diff --git a/apps/frontend/src/components/headerbar/AppHeader.tsx b/apps/frontend/src/components/headerbar/AppHeader.tsx index bb22127..a8dca1f 100644 --- a/apps/frontend/src/components/headerbar/AppHeader.tsx +++ b/apps/frontend/src/components/headerbar/AppHeader.tsx @@ -1,30 +1,33 @@ import * as React from "react"; -import { FunctionComponent } from "react"; +import { FunctionComponent, useContext } from "react"; import { Link } from "react-router-dom"; -import { AuthUser } from "@mancala/core"; import { useTranslation } from "react-i18next"; import { Context } from "../../context/context"; +import { AuthUserContext } from "../../context/AuthUserContext"; import { HelpIcon, LogoIcon } from "../Icons"; import AccountMenu from "./AccountMenu"; -import ThemeSwatches from "./ThemeSwatches"; +import SettingsMenu from "./SettingsMenu"; -// The header of every page: logo and name, theme swatches, how to play, the account menu when a user is given, and -// the page's main action (children). How to play opens the rules page, or calls `onHelp` (the game page shows the -// rules over the game instead). -const AppHeader: FunctionComponent<{ context: Context, authUser?: AuthUser, onHelp?: () => void }> = ({ context, authUser, onHelp, children }) => { +// The header of every page: logo and name, the page's actions (children), how to play, settings (the themes) and the +// account menu once the session has started. How to play opens the rules page, or calls `onHelp` (the game page +// shows the rules over the game instead). +const AppHeader: FunctionComponent<{ context: Context, onHelp?: () => void }> = ({ context, onHelp, children }) => { const { t } = useTranslation(); + const authUser = useContext(AuthUserContext); return ( -
+
{t("Mancala")} - - {onHelp - ? - : } - {authUser && } {children} +
+ {onHelp + ? + : } + + {authUser && } +
); }; diff --git a/apps/frontend/src/components/headerbar/SettingsMenu.tsx b/apps/frontend/src/components/headerbar/SettingsMenu.tsx new file mode 100644 index 0000000..931de6a --- /dev/null +++ b/apps/frontend/src/components/headerbar/SettingsMenu.tsx @@ -0,0 +1,37 @@ +import { Menu, MenuHeader, MenuItem } from "@szhsin/react-menu"; +import * as React from "react"; +import { FunctionComponent } from "react"; +import { useTranslation } from "react-i18next"; +import { Context } from "../../context/context"; +import { getWebTheme, webThemes } from "../../theme/themes"; +import { SettingsIcon } from "../Icons"; +import "@szhsin/react-menu/dist/index.css"; +import "@szhsin/react-menu/dist/transitions/slide.css" + +// Settings in the header: the three themes, each with its swatch, the chosen one checked. +const SettingsMenu: FunctionComponent<{ context: Context }> = ({ context }) => { + const { t } = useTranslation(); + const current = getWebTheme(context.themeManager.theme); + const menuButton = ({ open }: { open: boolean }) => ( + + ); + return ( + + {t("Theme")} + {webThemes.map((theme) => ( + (context.themeManager.theme = theme)}> + + {t(theme.nameKey)} + + ))} + + ); +}; + +export default SettingsMenu; diff --git a/apps/frontend/src/components/headerbar/ThemeSwatches.tsx b/apps/frontend/src/components/headerbar/ThemeSwatches.tsx deleted file mode 100644 index add43d9..0000000 --- a/apps/frontend/src/components/headerbar/ThemeSwatches.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import * as React from "react"; -import { FunctionComponent } from "react"; -import { useTranslation } from "react-i18next"; -import { Context } from "../../context/context"; -import { getWebTheme, webThemes } from "../../theme/themes"; - -const ThemeSwatches: FunctionComponent<{ context: Context }> = ({ context }) => { - const { t } = useTranslation(); - const current = getWebTheme(context.themeManager.theme); - return ( -
- {webThemes.map((theme) => ( - - ))} -
- ); -}; - -export default ThemeSwatches; diff --git a/apps/frontend/src/context/AuthUserContext.ts b/apps/frontend/src/context/AuthUserContext.ts new file mode 100644 index 0000000..50b0951 --- /dev/null +++ b/apps/frontend/src/context/AuthUserContext.ts @@ -0,0 +1,5 @@ +import * as React from "react"; +import { AuthUser } from "@mancala/core"; + +// The signed-in user (a guest too), once the session has started; the header's account menu reads it on every page. +export const AuthUserContext = React.createContext(undefined); diff --git a/apps/frontend/src/routes/Home.tsx b/apps/frontend/src/routes/Home.tsx index cb7f733..2cfaaba 100644 --- a/apps/frontend/src/routes/Home.tsx +++ b/apps/frontend/src/routes/Home.tsx @@ -52,13 +52,18 @@ const Home: FunctionComponent<{ return ( - - -
+ {/* On phones the header's actions move here. */} +
+ + +
{activeGame && authUser && ( .btn { width: 100%; } + /* Dialogs (sweetalert) */ .swal-modal { background: var(--surface); border-radius: 24px; font-family: 'Bricolage Grotesque', system-ui, sans-serif; } @@ -228,17 +274,13 @@ a { color: inherit; } .hdr { padding: 10px 16px; gap: 8px; } .logo { font-size: 20px; gap: 8px; } .logo svg { width: 28px; height: 28px; } - .themes .sw { width: 40px; height: 40px; } .hide-m { display: none !important; } + .show-m { display: grid !important; } + .hdr-tools { margin-left: 4px; gap: 2px; } .ic-m { width: 44px; padding: 0; } .wrap { padding: 16px 16px 32px; gap: 16px; } } -/* The account button makes the header too wide for the name on phones. */ -@media (max-width: 420px) { - .hdr.has-account .logo span { display: none; } -} - @media (max-width: 360px) { .logo span { display: none; } } diff --git a/apps/frontend/src/theme/themes.ts b/apps/frontend/src/theme/themes.ts index 30d118e..a4f8443 100644 --- a/apps/frontend/src/theme/themes.ts +++ b/apps/frontend/src/theme/themes.ts @@ -7,6 +7,8 @@ export type WebTheme = Theme & { className: string; // Translation key of the theme's name. labelKey: string; + // The name alone ("Walnut"), as the settings menu lists it. + nameKey: string; // Colours of the theme's swatch. swatch: { board: string, accent: string }; }; @@ -16,6 +18,7 @@ const walnut: WebTheme = { name: "Ceviz", className: "t-ceviz", labelKey: "ThemeWalnut", + nameKey: "ThemeWalnutName", swatch: { board: "#6e4a30", accent: "#f2b84b" }, themePreviewColor: "#6e4a30", background: "#1f1712", @@ -39,6 +42,7 @@ const night: WebTheme = { name: "Gece", className: "t-gece", labelKey: "ThemeNight", + nameKey: "ThemeNightName", swatch: { board: "#1d2a4d", accent: "#e2b36a" }, themePreviewColor: "#1d2a4d", background: "#0c1220", @@ -62,6 +66,7 @@ const jade: WebTheme = { name: "Yeşim", className: "t-yesim", labelKey: "ThemeJade", + nameKey: "ThemeJadeName", swatch: { board: "#cfd2c3", accent: "#2a7650" }, themePreviewColor: "#cfd2c3", background: "#e9ebe3", diff --git a/docs/specs/theme-polish/design.md b/docs/specs/theme-polish/design.md index 111bede..05a02d2 100644 --- a/docs/specs/theme-polish/design.md +++ b/docs/specs/theme-polish/design.md @@ -27,6 +27,20 @@ See `requirements.md`; requirement ids (R1.1, …) are referenced below. ## Web app +### Header (R3) + +- `AppHeader`: logo, the page's actions (children), how to play, `SettingsMenu`, `AccountMenu`. The signed-in user + comes from an `AuthUserContext` that `MancalaApp` provides, so pages need not pass it down. +- `SettingsMenu` (new) replaces `ThemeSwatches`: a `@szhsin/react-menu` menu like the account menu, with a "Theme" + header and one checkbox item per theme (swatch, name). Checked items in every menu (the theme, the last bot level) + show a check at their end instead of the library's mark in front. +- Icon buttons in the header use `.ibtn` (round, no outline, a surface on hover and while open); the account button + shows `getInitials` in an accent circle. +- Home: the header's actions get `hide-m`; a `.qa.show-m` row at the top of the page holds New Game and the bot menu + (`BotLevelMenu` with a full button) below 640 px. + +### Messages and the theme cookie + - `theme/themes.ts`: `saveThemeCookie(theme)`, called with `applyThemeClass` at start and on every theme change. The cookie lasts a year, `SameSite=Lax`, `Secure` on https, and on a real domain is set for the last two labels of the host (`playmancala.app`). diff --git a/docs/specs/theme-polish/requirements.md b/docs/specs/theme-polish/requirements.md index d7e312e..8f6cede 100644 --- a/docs/specs/theme-polish/requirements.md +++ b/docs/specs/theme-polish/requirements.md @@ -1,7 +1,7 @@ # Theme polish: requirements Status: implemented on branch `feature/theme-polish` (based on `develop`); checked in Chrome against the local stack. -Scope: the Keycloak login theme and the web app's short messages. Follows `web-redesign`. +Scope: the Keycloak login theme, the web app's short messages and its header. Follows `web-redesign`. ## Goals @@ -24,5 +24,16 @@ Yeşim), and messages appear where they are noticed without covering the game. - R2.2 They appear at the top centre, just below the header, not in the bottom right corner. - R2.3 Several messages stack without covering the header; each goes away by itself or when clicked. +### R3. Header +- R3.1 Every page's header has, in this order: the logo and name, the page's actions, how to play, settings and the + account (the design canvas's updated header). +- R3.2 How to play, settings and the account are round icon buttons; the account shows the player's initials. +- R3.3 Settings opens a menu with the three themes, each with its swatch and name, the chosen one checked. The theme + swatches are no longer in the header. +- R3.4 The account menu (log in, sign up, log out) is on every page, not only the home page. +- R3.5 At phone width the home page's actions (New Game, Play the bot) leave the header for a row of two buttons at the + top of the page; the game page keeps its action as an icon. + ## Out of scope (TODO) +- The design's "Sound effects" switch in settings: the app has no sounds yet. - Keycloak's email theme (password reset emails). diff --git a/docs/specs/theme-polish/tasks.md b/docs/specs/theme-polish/tasks.md index d6ce749..ed1222c 100644 --- a/docs/specs/theme-polish/tasks.md +++ b/docs/specs/theme-polish/tasks.md @@ -8,3 +8,5 @@ Work through the tasks in order; each ends with its checks passing and a commit. - [x] 4. **Login theme**: the script, the three themes' styles, logo and board masks; checked in Chrome in all three themes, at desktop and phone width, with and without the cookie. - [x] 5. **Docs**: `AGENTS.md` (specs list, the cookie), `apps/keycloak/README.md`, `TODO.md`. +- [x] 6. **Header**: the new order, the settings menu with the themes, the account on every page, the home page's + actions on phones; checked in Chrome in all three themes at desktop and phone width. diff --git a/packages/core/src/localization/en.ts b/packages/core/src/localization/en.ts index 36c277c..c5969bd 100644 --- a/packages/core/src/localization/en.ts +++ b/packages/core/src/localization/en.ts @@ -194,4 +194,10 @@ export const en = { MovedFirst: "Moved first", EndedOnTime: "time", EndedByLeaving: "left", + Settings: "Settings", + Theme: "Theme", + ThemeWalnutName: "Walnut", + ThemeNightName: "Night", + ThemeJadeName: "Jade", + PlayBot: "Play the bot", } \ No newline at end of file diff --git a/packages/core/src/localization/tr.ts b/packages/core/src/localization/tr.ts index 83d6bdc..17f7895 100644 --- a/packages/core/src/localization/tr.ts +++ b/packages/core/src/localization/tr.ts @@ -194,4 +194,10 @@ export const tr = { MovedFirst: "İlk hamle", EndedOnTime: "süre", EndedByLeaving: "ayrıldı", + Settings: "Ayarlar", + Theme: "Tema", + ThemeWalnutName: "Ceviz", + ThemeNightName: "Gece", + ThemeJadeName: "Yeşim", + PlayBot: "Bota karşı", }; \ No newline at end of file