Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions apps/web/src/components/mobile-menu.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>(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 (
<div ref={ref} className="relative sm:hidden">
<Button
variant="ghost"
size="icon"
aria-label={label}
aria-haspopup="menu"
aria-expanded={open}
onClick={() => setOpen((o) => !o)}
>
{open ? <IconX size={20} stroke={1.75} /> : <IconMenu2 size={20} stroke={1.75} />}
</Button>
{open && (
<div
role="menu"
onClick={() => 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}
</div>
)}
</div>
);
}
43 changes: 41 additions & 2 deletions apps/web/src/components/site-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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();
Expand Down Expand Up @@ -62,8 +68,10 @@ export async function SiteHeader({ user }: { user: HeaderUser | null }) {
<LanguageSwitcher locale={locale} />
<ThemeToggle />
<span className="mx-1 hidden h-5 w-px bg-border sm:block" aria-hidden />

{/* Desktop auth actions */}
{user ? (
<div className="flex items-center gap-2">
<div className="hidden items-center gap-2 sm:flex">
{user.avatar && (
<img
src={user.avatar}
Expand All @@ -79,13 +87,44 @@ export async function SiteHeader({ user }: { user: HeaderUser | null }) {
<LogoutButton label={t.header.logout} />
</div>
) : (
<Button asChild size="sm">
<Button asChild size="sm" className="hidden sm:inline-flex">
<a href={loginHref}>
<IconBrandDiscord size={16} stroke={1.75} />
{t.header.login}
</a>
</Button>
)}

{/* Mobile menu: nav links + auth actions that don't fit the bar */}
<MobileMenu label={t.header.menu}>
<a href="/pricing" className={MENU_ROW}>
{t.pricing.nav}
</a>
<a href="/stats" className={MENU_ROW}>
{t.stats.nav}
</a>
<a href="https://forms.msk-scripts.de/msk-forms" className={MENU_ROW}>
MSK Scripts Hub
</a>
<div className="my-1 h-px bg-border" aria-hidden />
{user ? (
<>
<a href={dashboardHref} className={MENU_ROW}>
{t.header.dashboard}
</a>
<form action={logoutAction}>
<button type="submit" className={`${MENU_ROW} w-full text-start`}>
{t.header.logout}
</button>
</form>
</>
) : (
<a href={loginHref} className={MENU_ROW}>
<IconBrandDiscord size={16} stroke={1.75} />
{t.header.login}
</a>
)}
</MobileMenu>
</div>
</div>
</header>
Expand Down
14 changes: 7 additions & 7 deletions apps/web/src/i18n/dictionaries.ts
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down