chore : move all to root

This commit is contained in:
Thibault Pouch
2026-02-26 16:16:44 +01:00
parent 308a758e79
commit c2d94a349c
44 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,150 @@
import { NavLink, Outlet, useNavigate } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext';
import { useCallback } from 'react';
const INTRANET_LINKS = [
{ to: '/intranet', label: 'Dashboard', icon: '[>]', end: true },
{ to: '/intranet/bugs', label: 'Bug Reports', icon: '[!]', end: false },
{ to: '/intranet/feed', label: 'Team Feed', icon: '[~]', end: false },
{ to: '/intranet/events', label: 'Events', icon: '[E]', end: false },
{ to: '/intranet/users', label: 'Users', icon: '[U]', end: false },
{ to: '/intranet/moderation', label: 'Moderation', icon: '[M]', end: false },
];
export function IntranetLayout() {
const { user, logout } = useAuth();
const navigate = useNavigate();
const handleLogout = useCallback(() => {
logout();
navigate('/');
}, [logout, navigate]);
return (
<div style={{ display: 'flex', minHeight: '100vh', background: 'var(--color-bg)' }}>
{/* Sidebar */}
<aside
style={{
width: '220px',
flexShrink: 0,
background: 'var(--color-bg-alt)',
borderRight: '2px solid var(--color-border)',
display: 'flex',
flexDirection: 'column',
padding: '1.5rem 0',
}}
>
{/* Logo */}
<div style={{ padding: '0 1.25rem 1.25rem', borderBottom: '1px solid var(--color-border)' }}>
<div
style={{
fontFamily: 'var(--font-heading)',
color: 'var(--color-yellow)',
fontSize: '1.3rem',
letterSpacing: '0.08em',
}}
>
INTRANET
</div>
<div style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text-muted)', fontSize: '0.62rem', marginTop: '2px' }}>
CROWMATE STUDIO
</div>
</div>
{/* Nav links */}
<nav style={{ flex: 1, padding: '0.75rem 0' }}>
{INTRANET_LINKS.map(({ to, label, icon, end }) => (
<NavLink
key={to}
to={to}
end={end}
style={({ isActive }) => ({
display: 'flex',
alignItems: 'center',
gap: '0.6rem',
padding: '0.55rem 1.25rem',
fontFamily: 'var(--font-mono)',
fontSize: '0.78rem',
color: isActive ? 'var(--color-yellow)' : 'var(--color-text-muted)',
background: isActive ? 'rgba(37,99,235,0.08)' : 'transparent',
borderLeft: isActive ? '3px solid var(--color-yellow)' : '3px solid transparent',
textDecoration: 'none',
transition: 'color 0.1s, background 0.1s',
letterSpacing: '0.05em',
})}
>
<span style={{ opacity: 0.6, fontSize: '0.68rem' }}>{icon}</span>
{label}
</NavLink>
))}
</nav>
{/* User info */}
<div
style={{
padding: '1rem 1.25rem',
borderTop: '1px solid var(--color-border)',
fontFamily: 'var(--font-mono)',
}}
>
<div style={{ color: 'var(--color-text-dim)', fontSize: '0.75rem', marginBottom: '0.4rem' }}>
{user?.username}
</div>
<div
style={{
display: 'inline-block',
background: 'rgba(255,255,0,0.08)',
border: '1px solid var(--color-yellow)',
color: 'var(--color-yellow)',
fontSize: '0.6rem',
padding: '0.1rem 0.4rem',
letterSpacing: '0.1em',
textTransform: 'uppercase',
marginBottom: '0.75rem',
}}
>
{user?.role}
</div>
<div style={{ display: 'flex', gap: '0.5rem' }}>
<button
onClick={handleLogout}
style={{
background: 'transparent',
border: '1px solid var(--color-red)',
color: 'var(--color-red)',
fontFamily: 'var(--font-mono)',
fontSize: '0.63rem',
padding: '0.2rem 0.5rem',
cursor: 'pointer',
letterSpacing: '0.05em',
}}
>
Logout
</button>
<NavLink
to="/"
style={{
background: 'transparent',
border: '1px solid var(--color-border)',
color: 'var(--color-text-muted)',
fontFamily: 'var(--font-mono)',
fontSize: '0.63rem',
padding: '0.2rem 0.5rem',
letterSpacing: '0.05em',
textDecoration: 'none',
display: 'inline-block',
}}
>
Public
</NavLink>
</div>
</div>
</aside>
{/* Main content */}
<main style={{ flex: 1, overflowY: 'auto', padding: '2rem', background: 'var(--color-bg)' }}>
<Outlet />
</main>
</div>
);
}

View File

@@ -0,0 +1,31 @@
import { Outlet, useLocation } from 'react-router-dom';
import { useEffect, useRef } from 'react';
import { Navbar } from '../shared/Navbar';
import { Footer } from '../shared/Footer';
import { DevRoleSwitcher } from '../shared/DevRoleSwitcher';
export function PublicLayout() {
const location = useLocation();
const mainRef = useRef<HTMLDivElement>(null);
// Scroll to top and add page-enter animation on route change
useEffect(() => {
window.scrollTo({ top: 0, behavior: 'instant' });
const el = mainRef.current;
if (!el) return;
el.classList.remove('page-enter');
void el.offsetWidth; // reflow to restart animation
el.classList.add('page-enter');
}, [location.pathname]);
return (
<div style={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
<Navbar />
<main ref={mainRef} style={{ flex: 1 }}>
<Outlet />
</main>
<Footer />
<DevRoleSwitcher />
</div>
);
}

View File

@@ -0,0 +1,118 @@
import { useAuth } from '../../contexts/AuthContext';
import type { UserRole } from '../../types';
/**
* Developer-only overlay to quickly switch user roles for testing.
* Only visible in development mode.
*/
export function DevRoleSwitcher() {
if (import.meta.env.PROD) return null;
return <DevRoleSwitcherInner />;
}
function DevRoleSwitcherInner() {
const { user, isAuthenticated, devSetRole, login, logout } = useAuth();
const ROLES: UserRole[] = ['user', 'dev', 'com'];
const DEV_ACCOUNTS = [
{ label: 'Dev/Admin (Kestrel)', email: 'kestrel@crowmate.dev' },
{ label: 'Com Staff (Vesper)', email: 'vesper@crowmate.dev' },
{ label: 'User (GlitchHunter)', email: 'glitch@mail.com' },
];
return (
<div
style={{
position: 'fixed',
bottom: '1rem',
right: '1rem',
background: 'var(--color-surface)',
border: '2px solid var(--color-yellow)',
padding: '0.75rem',
zIndex: 9999,
fontSize: '0.7rem',
fontFamily: 'var(--font-mono)',
color: 'var(--color-text)',
maxWidth: '220px',
borderRadius: '8px',
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
}}
>
<div style={{ marginBottom: '0.5rem', fontWeight: 'bold', letterSpacing: '0.1em' }}>
[DEV] Auth Switcher
</div>
{isAuthenticated ? (
<>
<div style={{ color: 'var(--color-text-muted)', marginBottom: '0.4rem' }}>
Logged as: <strong style={{ color: 'var(--color-text)' }}>{user?.username}</strong>
</div>
<div style={{ color: 'var(--color-text-muted)', marginBottom: '0.5rem' }}>
Role: <strong style={{ color: 'var(--color-green)' }}>{user?.role}</strong>
</div>
<div style={{ display: 'flex', gap: '0.3rem', marginBottom: '0.5rem', flexWrap: 'wrap' }}>
{ROLES.map((r) => (
<button
key={r}
onClick={() => devSetRole(r)}
style={{
background: user?.role === r ? 'var(--color-amber)' : 'transparent',
border: '1px solid var(--color-amber)',
color: user?.role === r ? '#000' : 'var(--color-amber)',
padding: '0.1rem 0.4rem',
cursor: 'pointer',
fontFamily: 'var(--font-mono)',
fontSize: '0.65rem',
}}
>
{r}
</button>
))}
</div>
<button
onClick={logout}
style={{
background: 'transparent',
border: '1px solid var(--color-red)',
color: 'var(--color-red)',
padding: '0.2rem 0.5rem',
cursor: 'pointer',
fontFamily: 'var(--font-mono)',
fontSize: '0.65rem',
width: '100%',
}}
>
Logout
</button>
</>
) : (
<>
<div style={{ color: 'var(--color-text-muted)', marginBottom: '0.5rem' }}>Quick login:</div>
{DEV_ACCOUNTS.map(({ label, email }) => (
<button
key={email}
onClick={() => login(email, 'password')}
style={{
background: 'transparent',
border: '1px solid var(--color-border)',
color: 'var(--color-text-dim)',
padding: '0.2rem 0.4rem',
cursor: 'pointer',
fontFamily: 'var(--font-mono)',
fontSize: '0.63rem',
width: '100%',
marginBottom: '0.2rem',
textAlign: 'left',
}}
>
{label}
</button>
))}
</>
)}
</div>
);
}

View File

@@ -0,0 +1,103 @@
import { Link } from 'react-router-dom';
export function Footer() {
const year = new Date().getFullYear();
return (
<footer
style={{
background: 'var(--color-surface-alt)',
borderTop: '1px solid var(--color-border)',
padding: '2.5rem 1.5rem 2rem',
marginTop: 'auto',
}}
>
<div className="max-w-7xl mx-auto">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-8 mb-8">
{/* Brand */}
<div>
<div
style={{
fontFamily: 'var(--font-heading)',
color: 'var(--color-green)',
fontSize: '1.1rem',
letterSpacing: '0.1em',
marginBottom: '0.5rem',
}}
>
CROWMATE STUDIO
</div>
<p style={{ color: 'var(--color-text-muted)', fontSize: '0.75rem', lineHeight: 1.7 }}>
Building strange worlds for strange people.
</p>
</div>
{/* Navigation */}
<div>
<div className="section-label" style={{ marginBottom: '0.75rem' }}>Navigate</div>
<ul style={{ listStyle: 'none', margin: 0, padding: 0, display: 'flex', flexDirection: 'column', gap: '0.4rem' }}>
{[
{ to: '/', label: 'Home' },
{ to: '/studio', label: 'Studio' },
{ to: '/forum', label: 'Forum' },
{ to: '/bugs', label: 'Bug Reports' },
].map(({ to, label }) => (
<li key={to}>
<Link
to={to}
style={{ color: 'var(--color-text-muted)', fontSize: '0.8rem' }}
>
&gt; {label}
</Link>
</li>
))}
</ul>
</div>
{/* Social */}
<div>
<div className="section-label" style={{ marginBottom: '0.75rem' }}>Connect</div>
<ul style={{ listStyle: 'none', margin: 0, padding: 0, display: 'flex', flexDirection: 'column', gap: '0.4rem' }}>
{[
{ href: '#', label: 'Twitter / X' },
{ href: '#', label: 'Discord' },
{ href: '#', label: 'YouTube' },
{ href: '#', label: 'Steam Page' },
].map(({ href, label }) => (
<li key={label}>
<a
href={href}
style={{ color: 'var(--color-text-muted)', fontSize: '0.8rem' }}
rel="noopener noreferrer"
>
&gt; {label}
</a>
</li>
))}
</ul>
</div>
</div>
{/* Bottom bar */}
<div
style={{
borderTop: '1px solid var(--color-border)',
paddingTop: '1.25rem',
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'space-between',
alignItems: 'center',
gap: '0.5rem',
}}
>
<span style={{ color: 'var(--color-text-muted)', fontSize: '0.7rem', fontFamily: 'var(--font-mono)' }}>
&copy; {year} CrowMate Studio. All rights reserved.
</span>
<span style={{ color: 'var(--color-text-muted)', fontSize: '0.7rem', fontFamily: 'var(--font-mono)', opacity: 0.5 }}>
HEADLESS HAZARD v0.9.3-alpha
</span>
</div>
</div>
</footer>
);
}

View 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',
})}
>
&#9646; 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,
})}
>
&#9646; INTRANET
</NavLink>
</div>
)}
</div>
</div>
)}
</header>
);
}

View File

@@ -0,0 +1,25 @@
export function PageLoader() {
return (
<div
className="fixed inset-0 flex items-center justify-center"
style={{ background: 'var(--color-bg)' }}
role="status"
aria-label="Loading"
>
<div className="text-center">
<div
className="text-4xl font-bold mb-4 cursor-blink"
style={{
fontFamily: 'var(--font-mono)',
color: 'var(--color-green)',
}}
>
LOADING
</div>
<div style={{ color: 'var(--color-text-muted)', fontSize: '0.75rem', letterSpacing: '0.2em' }}>
CROWMATE STUDIO / HEADLESS HAZARD
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,29 @@
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext';
interface ProtectedRouteProps {
children: React.ReactNode;
/** If true, requires staff role (dev or com). */
staffOnly?: boolean;
/** Redirect destination when access is denied. Defaults to /login. */
redirectTo?: string;
}
export function ProtectedRoute({
children,
staffOnly = false,
redirectTo = '/login',
}: ProtectedRouteProps) {
const { isAuthenticated, isStaff } = useAuth();
const location = useLocation();
if (!isAuthenticated) {
return <Navigate to={redirectTo} state={{ from: location }} replace />;
}
if (staffOnly && !isStaff) {
return <Navigate to="/" replace />;
}
return <>{children}</>;
}