chore : move all to root
This commit is contained in:
264
nest-front/src/components/shared/Navbar.tsx
Normal file
264
nest-front/src/components/shared/Navbar.tsx
Normal file
@@ -0,0 +1,264 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { Link, NavLink, useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
|
||||
const NAV_LINKS = [
|
||||
{ to: '/', label: 'Home', end: true },
|
||||
{ to: '/studio', label: 'Studio', end: false },
|
||||
{ to: '/events', label: 'Events', end: false },
|
||||
{ to: '/forum', label: 'Forum', end: false },
|
||||
{ to: '/bugs', label: 'Bugs', end: false },
|
||||
];
|
||||
|
||||
export function Navbar() {
|
||||
const { user, isAuthenticated, isStaff, logout } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
|
||||
const handleLogout = useCallback(() => {
|
||||
logout();
|
||||
setMenuOpen(false);
|
||||
navigate('/');
|
||||
}, [logout, navigate]);
|
||||
|
||||
const closeMenu = useCallback(() => setMenuOpen(false), []);
|
||||
|
||||
const navLinkStyle = ({ isActive }: { isActive: boolean }): React.CSSProperties => ({
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: '0.82rem',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.12em',
|
||||
textDecoration: 'none',
|
||||
color: isActive ? 'var(--color-yellow)' : 'var(--color-text-dim)',
|
||||
borderBottom: isActive ? '2px solid var(--color-yellow)' : '2px solid transparent',
|
||||
paddingBottom: '2px',
|
||||
transition: 'color 0.1s, border-color 0.1s',
|
||||
});
|
||||
|
||||
return (
|
||||
<header
|
||||
style={{
|
||||
background: 'var(--color-bg)',
|
||||
borderBottom: '2px solid var(--color-border)',
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 50,
|
||||
}}
|
||||
>
|
||||
<nav
|
||||
className="max-w-7xl mx-auto px-4 sm:px-6 h-14 flex items-center justify-between"
|
||||
role="navigation"
|
||||
aria-label="Main navigation"
|
||||
>
|
||||
{/* Logo */}
|
||||
<Link
|
||||
to="/"
|
||||
onClick={closeMenu}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'baseline',
|
||||
gap: '0.5rem',
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: 'var(--font-heading)',
|
||||
color: 'var(--color-yellow)',
|
||||
fontSize: '1.5rem',
|
||||
letterSpacing: '0.08em',
|
||||
}}
|
||||
>
|
||||
CROWMATE
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: 'var(--font-mono)',
|
||||
color: 'var(--color-text-muted)',
|
||||
fontSize: '0.62rem',
|
||||
letterSpacing: '0.15em',
|
||||
}}
|
||||
>
|
||||
STUDIO
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Nav */}
|
||||
<div className="hidden md:flex items-center gap-6">
|
||||
{NAV_LINKS.map(({ to, label, end }) => (
|
||||
<NavLink key={to} to={to} end={end} style={navLinkStyle}>
|
||||
{label}
|
||||
</NavLink>
|
||||
))}
|
||||
|
||||
{/* Intranet — visually separated, highlighted button */}
|
||||
{isStaff && (
|
||||
<>
|
||||
{/* Vertical divider */}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: '1px',
|
||||
height: '18px',
|
||||
background: 'var(--color-border)',
|
||||
margin: '0 0.25rem',
|
||||
}}
|
||||
/>
|
||||
<NavLink
|
||||
to="/intranet"
|
||||
style={({ isActive }) => ({
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: '0.75rem',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
textDecoration: 'none',
|
||||
background: isActive ? 'var(--color-yellow)' : 'var(--color-yellow)',
|
||||
color: 'var(--color-bg)',
|
||||
border: '2px solid var(--color-yellow)',
|
||||
padding: '0.2rem 0.65rem',
|
||||
fontWeight: 'bold',
|
||||
opacity: isActive ? 1 : 0.85,
|
||||
transition: 'opacity 0.1s',
|
||||
})}
|
||||
>
|
||||
▮ INTRANET
|
||||
</NavLink>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Desktop Auth */}
|
||||
<div className="hidden md:flex items-center gap-3">
|
||||
{isAuthenticated ? (
|
||||
<>
|
||||
<Link
|
||||
to="/account"
|
||||
style={{
|
||||
color: 'var(--color-text-dim)',
|
||||
fontSize: '0.78rem',
|
||||
fontFamily: 'var(--font-mono)',
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
[{user?.username}]
|
||||
</Link>
|
||||
<button className="btn-terminal btn-danger" onClick={handleLogout} style={{ padding: '0.3rem 0.85rem', fontSize: '0.75rem' }}>
|
||||
Logout
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Link to="/login" className="btn-terminal" style={{ padding: '0.3rem 0.85rem', fontSize: '0.75rem' }}>
|
||||
Login
|
||||
</Link>
|
||||
<Link to="/register" className="btn-terminal btn-amber" style={{ padding: '0.3rem 0.85rem', fontSize: '0.75rem' }}>
|
||||
Register
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mobile hamburger */}
|
||||
<button
|
||||
className="md:hidden"
|
||||
style={{ background: 'transparent', border: 'none', cursor: 'pointer', padding: '4px' }}
|
||||
onClick={() => setMenuOpen((v) => !v)}
|
||||
aria-label={menuOpen ? 'Close menu' : 'Open menu'}
|
||||
aria-expanded={menuOpen}
|
||||
>
|
||||
{/* Three-bar icon using block chars */}
|
||||
<div style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-cyan)', fontSize: '1.2rem', lineHeight: 1 }}>
|
||||
{menuOpen ? '✕' : '≡'}
|
||||
</div>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
{/* Mobile menu */}
|
||||
{menuOpen && (
|
||||
<div
|
||||
style={{
|
||||
background: 'var(--color-bg)',
|
||||
borderTop: '2px solid var(--color-border)',
|
||||
padding: '1rem 1.25rem',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.85rem' }}>
|
||||
{NAV_LINKS.map(({ to, label, end }) => (
|
||||
<NavLink
|
||||
key={to}
|
||||
to={to}
|
||||
end={end}
|
||||
onClick={closeMenu}
|
||||
style={navLinkStyle}
|
||||
>
|
||||
{label}
|
||||
</NavLink>
|
||||
))}
|
||||
|
||||
{/* Auth section */}
|
||||
<div
|
||||
style={{
|
||||
borderTop: '1px solid var(--color-border)',
|
||||
paddingTop: '0.85rem',
|
||||
display: 'flex',
|
||||
gap: '0.6rem',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
{isAuthenticated ? (
|
||||
<>
|
||||
<Link
|
||||
to="/account"
|
||||
style={{ color: 'var(--color-text-dim)', fontSize: '0.78rem', textDecoration: 'none', fontFamily: 'var(--font-mono)' }}
|
||||
onClick={closeMenu}
|
||||
>
|
||||
[{user?.username}]
|
||||
</Link>
|
||||
<button className="btn-terminal btn-danger" onClick={handleLogout} style={{ padding: '0.25rem 0.7rem', fontSize: '0.73rem' }}>
|
||||
Logout
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Link to="/login" className="btn-terminal" onClick={closeMenu} style={{ padding: '0.25rem 0.7rem', fontSize: '0.73rem' }}>
|
||||
Login
|
||||
</Link>
|
||||
<Link to="/register" className="btn-terminal btn-amber" onClick={closeMenu} style={{ padding: '0.25rem 0.7rem', fontSize: '0.73rem' }}>
|
||||
Register
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Intranet button — mobile: separated at the bottom */}
|
||||
{isStaff && (
|
||||
<div style={{ borderTop: '2px solid var(--color-yellow)', paddingTop: '0.85rem' }}>
|
||||
<NavLink
|
||||
to="/intranet"
|
||||
onClick={closeMenu}
|
||||
style={({ isActive }) => ({
|
||||
display: 'inline-block',
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: '0.8rem',
|
||||
textTransform: 'uppercase' as const,
|
||||
letterSpacing: '0.1em',
|
||||
textDecoration: 'none',
|
||||
background: 'var(--color-yellow)',
|
||||
color: 'var(--color-bg)',
|
||||
border: '2px solid var(--color-yellow)',
|
||||
padding: '0.3rem 0.85rem',
|
||||
fontWeight: 'bold',
|
||||
opacity: isActive ? 0.85 : 1,
|
||||
})}
|
||||
>
|
||||
▮ INTRANET
|
||||
</NavLink>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user