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
40 changes: 40 additions & 0 deletions client/src/core/hooks/usePlayerHeader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useEffect, useState } from "react";
import { useNavigate, useLocation } from "react-router";
import { useCurrentUser } from "./useCurrentUser";

export function usePlayerHeader() {
const navigate = useNavigate();
const location = useLocation();
const [menuOpen, setMenuOpen] = useState(false);

const { user, loading, clearUser } = useCurrentUser();

// Close mobile menu on route change
useEffect(() => {
setMenuOpen(false);
}, [location.pathname]);

function toggleMenu() {
setMenuOpen(v => !v);
}

function closeMenu() {
setMenuOpen(false);
}

function handleLogout() {
localStorage.clear();
clearUser();
navigate("/");
}

return {
menuOpen,
toggleMenu,
closeMenu,
handleLogout,
userName: user?.fullName || "Player",
balance: user?.balance ?? 0,
loading
};
}
23 changes: 22 additions & 1 deletion client/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,25 @@
--color-jerneNavy: #2E4658;
--color-lightBG: #F2F2F2;
--color-greyBorder: #D9D9D9;
}
}

/* ===== LOGO RESPONSIVENESS ===== */
.logo {
height: 72px;
width: auto;
object-fit: contain;
}

/* Tablet */
@media (max-width: 900px) {
.logo {
height: 48px;
}
}

/* Mobile */
@media (max-width: 600px) {
.logo {
height: 36px;
}
}
8 changes: 2 additions & 6 deletions client/src/ui/components/Logo.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
interface LogoProps {
size?: number;
className?: string;
alt?: string;
}

export const Logo = ({
size = 48,
className = "",
alt = "Jer ne IF Logo",
alt = "Jerne IF Logo",
}: LogoProps) => {
return (
<img
src="/logo1.png"
alt={alt}
width={size}
height={size}
loading="eager"
draggable={false}
className={`object-contain select-none ${className}`}
className={`logo ${className}`}
/>
);
};
133 changes: 21 additions & 112 deletions client/src/ui/components/PlayerPageHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,118 +1,27 @@
import React, { useEffect, useState } from "react";
import { NavLink, useNavigate, useLocation } from "react-router";
import "../css/PlayerPageHeader.css";
import { User, LogOut, Menu, X } from "lucide-react";
import { useCurrentUser } from "../../core/hooks/useCurrentUser";

const navItems = [
{ label: "Board", path: "/player/board" },
{ label: "History", path: "/player/history" },
{ label: "Results", path: "/player/results" },
{ label: "My Transactions", path: "/player/transactions" },
];
import React from "react";
import { usePlayerHeader } from "../../core/hooks/usePlayerHeader";
import { PlayerPageHeaderView } from "./PlayerPageHeaderView";

export const PlayerPageHeader: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
const [menuOpen, setMenuOpen] = useState(false);

const { user, loading, clearUser } = useCurrentUser();

// close mobile menu on route change
useEffect(() => {
setMenuOpen(false);
}, [location.pathname]);

function handleLogout() {
localStorage.clear();
clearUser(); // clears global cache + subscribers
navigate("/");
}

const userName = user?.fullName || "Player";
const balance = user?.balance ?? 0;
const {
menuOpen,
toggleMenu,
closeMenu,
handleLogout,
userName,
balance,
loading
} = usePlayerHeader();

return (
<header className="player-header">
{/* LEFT */}
<div className="player-header_left">
<div className="player-header_logo">
<img src="../../../src/assets/logo1.png" alt="Jerne IF" />
</div>

<button
type="button"
className="player-header_burger"
onClick={() => setMenuOpen(v => !v)}
>
{menuOpen ? <X size={22} /> : <Menu size={22} />}
</button>
</div>

{/* DESKTOP NAV */}
<nav className="player-header_nav player-header_nav--desktop">
<ul className="player-header_nav-list">
{navItems.map(item => (
<li key={item.path}>
<NavLink
to={item.path}
className={({ isActive }) =>
"player-header_nav-link" +
(isActive ? " player-header_nav-link--active" : "")
}
>
{item.label}
</NavLink>
</li>
))}
</ul>
</nav>

{/* USER */}
<div className="player-header_user-card">
<User size={20} />
<div className="player-header_user-text">
<div className="player-header_user-name">
{loading ? "…" : userName}
</div>
<div className="player-header_user-balance">
Balance: {loading ? "…" : balance} DKK
</div>
</div>
</div>

<button
type="button"
className="player-header_logout-btn"
onClick={handleLogout}
>
<LogOut size={20} />
</button>

{/* MOBILE MENU */}
<div
className={
"player-header_mobile-menu" +
(menuOpen ? " player-header_mobile-menu--open" : "")
}
>
{navItems.map(item => (
<NavLink
key={item.path}
to={item.path}
className="player-header_mobile-link"
>
{item.label}
</NavLink>
))}
</div>

{menuOpen && (
<div
className="player-header_backdrop"
onClick={() => setMenuOpen(false)}
/>
)}
</header>
<PlayerPageHeaderView
menuOpen={menuOpen}
onToggleMenu={toggleMenu}
onCloseMenu={closeMenu}
onLogout={handleLogout}
userName={userName}
balance={balance}
loading={loading}
/>
);
};
120 changes: 120 additions & 0 deletions client/src/ui/components/PlayerPageHeaderView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { NavLink } from "react-router";
import { User, LogOut, Menu, X } from "lucide-react";
import "../css/PlayerPageHeader.css";
import {Logo} from "./Logo.tsx";

const navItems = [
{ label: "Board", path: "/player/board" },
{ label: "History", path: "/player/history" },
{ label: "Results", path: "/player/results" },
{ label: "My Transactions", path: "/player/transactions" },
];

interface Props {
menuOpen: boolean;
loading: boolean;
userName: string;
balance: number;
onToggleMenu: () => void;
onCloseMenu: () => void;
onLogout: () => void;
}

export const PlayerPageHeaderView = ({
menuOpen,
loading,
userName,
balance,
onToggleMenu,
onCloseMenu,
onLogout
}: Props) => {
return (
<header className="player-header">
{/* LEFT */}
<div className="player-header_left">
<button
type="button"
className="player-header_burger"
onClick={onToggleMenu}
>
{menuOpen ? <X size={22} /> : <Menu size={22} />}
</button>

<Logo/>
</div>

{/* DESKTOP NAV */}
<nav className="player-header_nav player-header_nav--desktop">
<ul className="player-header_nav-list">
{navItems.map(item => (
<li key={item.path}>
<NavLink
to={item.path}
className={({ isActive }) =>
"player-header_nav-link" +
(isActive ? " player-header_nav-link--active" : "")
}
>
{item.label}
</NavLink>
</li>
))}
</ul>
</nav>

{/* USER */}
<div className="player-header_user-card">
<User size={20} />
<div className="player-header_user-text">
<div className="player-header_user-name">
{loading ? "…" : userName}
</div>
<div className="player-header_user-balance">
Balance: {loading ? "…" : balance} DKK
</div>
</div>
</div>

<button
type="button"
className="player-header_logout-btn"
onClick={onLogout}
>
<LogOut size={20} />
</button>

{/* MOBILE MENU */}
<div
className={
"player-header_mobile-menu" +
(menuOpen ? " player-header_mobile-menu--open" : "")
}
>
{navItems.map(item => (
<NavLink
key={item.path}
to={item.path}
className="player-header_mobile-link"
onClick={onCloseMenu}
>
{item.label}
</NavLink>
))}
<button
className="player-header_mobile-link player-header_mobile-logout"
onClick={onLogout}
>
Log out
</button>
</div>

{menuOpen && (
<div
className="player-header_backdrop"
onClick={onCloseMenu}
/>
)}
</header>
);
};
Loading