From 490bb2c64f5168524b5cde3f9eda8cd1efdf5f04 Mon Sep 17 00:00:00 2001 From: Musiker15 Date: Fri, 3 Jul 2026 14:41:34 +0200 Subject: [PATCH] fix(mobile): collapse header actions into a hamburger menu On phones the header crammed the logo, language switcher, theme toggle, avatar, Dashboard and Log out into one row and ran off the right edge. The nav links and auth actions (Dashboard / Log out, or Log in) now live behind a hamburger menu on mobile; the bar keeps only the wordmark, language switcher and theme toggle. The desktop header is unchanged (sm+). New MobileMenu component (outside click / Escape / click-to-close) + header.menu label in all 7 locales. --- apps/web/src/components/mobile-menu.tsx | 56 +++++++++++++++++++++++++ apps/web/src/components/site-header.tsx | 43 ++++++++++++++++++- apps/web/src/i18n/dictionaries.ts | 14 +++---- 3 files changed, 104 insertions(+), 9 deletions(-) create mode 100644 apps/web/src/components/mobile-menu.tsx diff --git a/apps/web/src/components/mobile-menu.tsx b/apps/web/src/components/mobile-menu.tsx new file mode 100644 index 0000000..8d5aec4 --- /dev/null +++ b/apps/web/src/components/mobile-menu.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { IconMenu2, IconX } from "@tabler/icons-react"; +import { useEffect, useRef, useState } from "react"; + +import { Button } from "@/components/ui/button"; + +/** + * Mobile-only header menu (`sm:hidden`). Holds the nav links and auth actions + * that don't fit the bar on a phone, behind a hamburger. Closes on outside + * click, Escape, or any click inside (so navigating a link dismisses it). + */ +export function MobileMenu({ children, label }: { children: React.ReactNode; label: string }) { + const [open, setOpen] = useState(false); + const ref = useRef(null); + + useEffect(() => { + if (!open) return; + function onPointer(e: MouseEvent) { + if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); + } + function onKey(e: KeyboardEvent) { + if (e.key === "Escape") setOpen(false); + } + document.addEventListener("mousedown", onPointer); + document.addEventListener("keydown", onKey); + return () => { + document.removeEventListener("mousedown", onPointer); + document.removeEventListener("keydown", onKey); + }; + }, [open]); + + return ( +
+ + {open && ( +
setOpen(false)} + className="absolute end-0 z-50 mt-1.5 w-56 rounded-md border border-border bg-popover p-2 text-popover-foreground shadow-md" + > + {children} +
+ )} +
+ ); +} diff --git a/apps/web/src/components/site-header.tsx b/apps/web/src/components/site-header.tsx index 5012c56..dec36d0 100644 --- a/apps/web/src/components/site-header.tsx +++ b/apps/web/src/components/site-header.tsx @@ -2,9 +2,11 @@ import { IconBrandDiscord } from "@tabler/icons-react"; import { LanguageSwitcher } from "@/components/language-switcher"; import { LogoutButton } from "@/components/logout-button"; +import { MobileMenu } from "@/components/mobile-menu"; import { ThemeToggle } from "@/components/theme-toggle"; import { Wordmark } from "@/components/landing/wordmark"; import { Button } from "@/components/ui/button"; +import { logoutAction } from "@/lib/auth-actions"; import { isPrimaryHostname, requestHostname } from "@/lib/custom-domain"; import { resolveHostOAuth } from "@/lib/guild-oauth"; import { appBaseUrl } from "@/lib/url"; @@ -15,6 +17,10 @@ export interface HeaderUser { avatar: string | null; } +/** Shared style for a row inside the mobile menu. */ +const MENU_ROW = + "flex items-center gap-2 rounded-sm px-2.5 py-2 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"; + export async function SiteHeader({ user }: { user: HeaderUser | null }) { const t = await getDict(); const locale = await getLocale(); @@ -62,8 +68,10 @@ export async function SiteHeader({ user }: { user: HeaderUser | null }) { + + {/* Desktop auth actions */} {user ? ( -
+
{user.avatar && (
) : ( - )} + + {/* Mobile menu: nav links + auth actions that don't fit the bar */} + + + {t.pricing.nav} + + + {t.stats.nav} + + + MSK Scripts Hub + +
+ {user ? ( + <> + + {t.header.dashboard} + +
+ +
+ + ) : ( + + + {t.header.login} + + )} +
diff --git a/apps/web/src/i18n/dictionaries.ts b/apps/web/src/i18n/dictionaries.ts index 56931c9..4e69d05 100644 --- a/apps/web/src/i18n/dictionaries.ts +++ b/apps/web/src/i18n/dictionaries.ts @@ -1,7 +1,7 @@ export type Locale = "en" | "de" | "hu" | "fr" | "es" | "pt" | "pl"; const en = { - header: { login: "Log in", dashboard: "Dashboard", logout: "Log out", toggleTheme: "Toggle theme", language: "Language" }, + header: { login: "Log in", dashboard: "Dashboard", logout: "Log out", toggleTheme: "Toggle theme", language: "Language", menu: "Menu" }, stats: { nav: "Stats", eyebrow: "Platform stats", @@ -549,7 +549,7 @@ const en = { export type Dictionary = typeof en; const de: Dictionary = { - header: { login: "Anmelden", dashboard: "Dashboard", logout: "Abmelden", toggleTheme: "Theme wechseln", language: "Sprache" }, + header: { login: "Anmelden", dashboard: "Dashboard", logout: "Abmelden", toggleTheme: "Theme wechseln", language: "Sprache", menu: "Menü" }, stats: { nav: "Statistik", eyebrow: "Plattform-Statistik", @@ -1093,7 +1093,7 @@ const de: Dictionary = { }; const hu: Dictionary = { - header: { login: "Bejelentkezés", dashboard: "Vezérlőpult", logout: "Kijelentkezés", toggleTheme: "Téma váltása", language: "Nyelv" }, + header: { login: "Bejelentkezés", dashboard: "Vezérlőpult", logout: "Kijelentkezés", toggleTheme: "Téma váltása", language: "Nyelv", menu: "Menü" }, stats: { nav: "Statisztika", eyebrow: "Platformstatisztika", @@ -1637,7 +1637,7 @@ const hu: Dictionary = { }; const fr: Dictionary = { - header: { login: "Connexion", dashboard: "Tableau de bord", logout: "Déconnexion", toggleTheme: "Changer de thème", language: "Langue" }, + header: { login: "Connexion", dashboard: "Tableau de bord", logout: "Déconnexion", toggleTheme: "Changer de thème", language: "Langue", menu: "Menu" }, stats: { nav: "Statistiques", eyebrow: "Statistiques de la plateforme", @@ -2182,7 +2182,7 @@ const fr: Dictionary = { const es: Dictionary = { - header: { login: "Iniciar sesión", dashboard: "Dashboard", logout: "Cerrar sesión", toggleTheme: "Cambiar tema", language: "Idioma" }, + header: { login: "Iniciar sesión", dashboard: "Dashboard", logout: "Cerrar sesión", toggleTheme: "Cambiar tema", language: "Idioma", menu: "Menú" }, stats: { nav: "Estadísticas", eyebrow: "Estadísticas de la plataforma", @@ -2727,7 +2727,7 @@ const es: Dictionary = { const pt: Dictionary = { - header: { login: "Entrar", dashboard: "Painel", logout: "Sair", toggleTheme: "Alternar tema", language: "Idioma" }, + header: { login: "Entrar", dashboard: "Painel", logout: "Sair", toggleTheme: "Alternar tema", language: "Idioma", menu: "Menu" }, stats: { nav: "Estatísticas", eyebrow: "Estatísticas da plataforma", @@ -3272,7 +3272,7 @@ const pt: Dictionary = { const pl: Dictionary = { - header: { login: "Zaloguj się", dashboard: "Panel", logout: "Wyloguj się", toggleTheme: "Zmień motyw", language: "Język" }, + header: { login: "Zaloguj się", dashboard: "Panel", logout: "Wyloguj się", toggleTheme: "Zmień motyw", language: "Język", menu: "Menu" }, stats: { nav: "Statystyki", eyebrow: "Statystyki platformy",