Frontend: hamburger menu v hlavicke na mobile
Hlavicka GameListu sa na uzkych displejoch nezmestila do jedneho riadku. Pod sm: breakpointom sa polozky zbalia do dropdownu. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,100 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
username?: string;
|
||||||
|
onHistory: () => void;
|
||||||
|
onDonate: () => void;
|
||||||
|
onLogout: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Header navigation: a row of links from `sm:` up, a hamburger dropdown below it. */
|
||||||
|
export default function HeaderMenu({ username, onHistory, onDonate, onLogout }: Props) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
const onPointerDown = (e: PointerEvent) => {
|
||||||
|
if (!ref.current?.contains(e.target as Node)) setOpen(false);
|
||||||
|
};
|
||||||
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') setOpen(false);
|
||||||
|
};
|
||||||
|
document.addEventListener('pointerdown', onPointerDown);
|
||||||
|
document.addEventListener('keydown', onKeyDown);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('pointerdown', onPointerDown);
|
||||||
|
document.removeEventListener('keydown', onKeyDown);
|
||||||
|
};
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
const pick = (action: () => void) => () => {
|
||||||
|
setOpen(false);
|
||||||
|
action();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="hidden sm:flex items-center gap-3 text-sm">
|
||||||
|
<span className="text-green-dim">{username}</span>
|
||||||
|
<button onClick={onHistory} className="text-gold hover:text-gold-bright">
|
||||||
|
História
|
||||||
|
</button>
|
||||||
|
<button onClick={onDonate} className="text-gold hover:text-gold-bright">
|
||||||
|
Na kávu
|
||||||
|
</button>
|
||||||
|
<button onClick={onLogout} className="text-green-dim hover:text-gold">
|
||||||
|
Odhlásiť
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ref={ref} className="relative sm:hidden">
|
||||||
|
<button
|
||||||
|
onClick={() => setOpen((o) => !o)}
|
||||||
|
aria-label="Menu"
|
||||||
|
aria-expanded={open}
|
||||||
|
aria-haspopup="menu"
|
||||||
|
className="flex flex-col justify-center gap-[5px] w-9 h-9 items-center rounded-lg border border-gold/20 text-gold hover:border-gold/50 transition-colors"
|
||||||
|
>
|
||||||
|
<span className="block w-4 h-px bg-current" />
|
||||||
|
<span className="block w-4 h-px bg-current" />
|
||||||
|
<span className="block w-4 h-px bg-current" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{open && (
|
||||||
|
<div
|
||||||
|
role="menu"
|
||||||
|
className="absolute right-0 top-11 z-40 w-44 bg-header border border-[#142018] rounded-xl py-1 shadow-[0_20px_60px_rgba(0,0,0,.6)]"
|
||||||
|
>
|
||||||
|
{username && (
|
||||||
|
<p className="px-4 py-2 text-xs text-green-dim border-b border-[#142018] truncate">
|
||||||
|
{username}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
role="menuitem"
|
||||||
|
onClick={pick(onHistory)}
|
||||||
|
className="block w-full text-left px-4 py-2.5 text-sm text-gold hover:bg-gold/10"
|
||||||
|
>
|
||||||
|
História
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
role="menuitem"
|
||||||
|
onClick={pick(onDonate)}
|
||||||
|
className="block w-full text-left px-4 py-2.5 text-sm text-gold hover:bg-gold/10"
|
||||||
|
>
|
||||||
|
Na kávu
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
role="menuitem"
|
||||||
|
onClick={pick(onLogout)}
|
||||||
|
className="block w-full text-left px-4 py-2.5 text-sm text-green-dim hover:bg-gold/10 hover:text-gold"
|
||||||
|
>
|
||||||
|
Odhlásiť
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { useGameStore } from '../store/gameStore';
|
import { useGameStore } from '../store/gameStore';
|
||||||
import { emit, socket, setAuthToken } from '../lib/socket';
|
import { emit, socket, setAuthToken } from '../lib/socket';
|
||||||
import { trackEvent } from '../lib/track';
|
import { trackEvent } from '../lib/track';
|
||||||
|
import HeaderMenu from '../components/HeaderMenu';
|
||||||
import NameModal from '../components/NameModal';
|
import NameModal from '../components/NameModal';
|
||||||
import RulesModal from '../components/RulesModal';
|
import RulesModal from '../components/RulesModal';
|
||||||
|
|
||||||
@@ -33,18 +34,12 @@ export default function GameList() {
|
|||||||
<div className="max-w-md mx-auto p-4 pt-8 min-h-screen">
|
<div className="max-w-md mx-auto p-4 pt-8 min-h-screen">
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<h1 className="font-serif text-2xl text-gold tracking-wide">Bridžik</h1>
|
<h1 className="font-serif text-2xl text-gold tracking-wide">Bridžik</h1>
|
||||||
<div className="flex items-center gap-3 text-sm">
|
<HeaderMenu
|
||||||
<span className="text-green-dim">{account?.username}</span>
|
username={account?.username}
|
||||||
<button onClick={() => navigate('/history')} className="text-gold hover:text-gold-bright">
|
onHistory={() => navigate('/history')}
|
||||||
História
|
onDonate={() => navigate('/donate')}
|
||||||
</button>
|
onLogout={handleLogout}
|
||||||
<button onClick={() => navigate('/donate')} className="text-gold hover:text-gold-bright">
|
/>
|
||||||
Na kávu
|
|
||||||
</button>
|
|
||||||
<button onClick={handleLogout} className="text-green-dim hover:text-gold">
|
|
||||||
Odhlásiť
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-3 mb-6">
|
<div className="flex flex-col gap-3 mb-6">
|
||||||
|
|||||||
Reference in New Issue
Block a user