129 lines
4.7 KiB
TypeScript
129 lines
4.7 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { NavLink, Navigate, Outlet, useNavigate } from 'react-router-dom';
|
|
import { LogOut, Sun, Moon } from 'lucide-react';
|
|
import { cn } from '../lib/cn';
|
|
import { useAuth } from '../auth/context';
|
|
import { onAuthErrorAll, orderBookingClient } from '../api/clients';
|
|
import { APP_ID } from '../api/config';
|
|
import { SCREENS } from './tabs';
|
|
|
|
/** Auth-guarded shell: slim navy top bar + fixed bottom tab nav + routed <Outlet>. */
|
|
export function ConsoleLayout() {
|
|
const { authed, logout } = useAuth();
|
|
const navigate = useNavigate();
|
|
const user = orderBookingClient.currentUser();
|
|
|
|
const [isDark, setIsDark] = useState(() => document.documentElement.classList.contains('dark'));
|
|
|
|
useEffect(() => {
|
|
const saved = localStorage.getItem('theme');
|
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
if (saved === 'dark' || (!saved && prefersDark)) {
|
|
document.documentElement.classList.add('dark');
|
|
setIsDark(true);
|
|
} else {
|
|
document.documentElement.classList.remove('dark');
|
|
setIsDark(false);
|
|
}
|
|
}, []);
|
|
|
|
const toggleTheme = () => {
|
|
if (isDark) {
|
|
document.documentElement.classList.remove('dark');
|
|
localStorage.setItem('theme', 'light');
|
|
setIsDark(false);
|
|
} else {
|
|
document.documentElement.classList.add('dark');
|
|
localStorage.setItem('theme', 'dark');
|
|
setIsDark(true);
|
|
}
|
|
};
|
|
|
|
// A 401 from any workflow client bounces back to login.
|
|
useEffect(() => {
|
|
onAuthErrorAll(() => {
|
|
logout();
|
|
navigate('/login', { replace: true });
|
|
});
|
|
}, [logout, navigate]);
|
|
|
|
if (!authed) return <Navigate to="/login" replace />;
|
|
|
|
return (
|
|
<div className="h-[100dvh] overflow-hidden bg-app flex flex-col">
|
|
<header className="sticky top-0 z-20 shrink-0 flex items-center justify-between gap-3 px-4 h-14 pt-[env(safe-area-inset-top)] bg-navy-grad shadow-md">
|
|
<div className="flex flex-col justify-center leading-none flex-1">
|
|
<span className="text-base font-extrabold text-on-navy tracking-[-0.01em] leading-none">Krishna Sales</span>
|
|
<span className="text-2xs text-on-navy-muted">Sandbox {APP_ID}{user?.name ? ` · ${user.name}` : ''}</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1">
|
|
<button
|
|
type="button"
|
|
aria-label="Toggle Theme"
|
|
onClick={toggleTheme}
|
|
className="inline-flex items-center justify-center size-9 rounded-md text-on-navy-muted hover:text-white hover:bg-white/10 transition-colors duration-150 cursor-pointer"
|
|
>
|
|
{isDark ? <Sun size={18} /> : <Moon size={18} />}
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
aria-label="Sign out"
|
|
onClick={() => {
|
|
logout();
|
|
navigate('/login', { replace: true });
|
|
}}
|
|
className="inline-flex items-center justify-center size-9 rounded-md text-on-navy-muted hover:text-white hover:bg-white/10 transition-colors duration-150 cursor-pointer"
|
|
>
|
|
<LogOut size={18} />
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<main className="flex-1 overflow-y-auto px-4 pt-4 pb-24">
|
|
<div className="max-w-[720px] w-full mx-auto flex flex-col gap-5">
|
|
<Outlet />
|
|
</div>
|
|
</main>
|
|
|
|
<nav
|
|
className="fixed bottom-0 inset-x-0 z-20 h-16 pb-[env(safe-area-inset-bottom)] bg-card border-t border-border-subtle grid shadow-[0_-2px_16px_rgba(11,27,59,0.06)]"
|
|
style={{ gridTemplateColumns: `repeat(${SCREENS.length}, minmax(0, 1fr))` }}
|
|
>
|
|
{SCREENS.map((t) => {
|
|
const Icon = t.icon;
|
|
return (
|
|
<NavLink
|
|
key={t.key}
|
|
to={`/${t.key}`}
|
|
className="flex flex-col items-center justify-center gap-1 no-underline min-h-[48px]"
|
|
>
|
|
{({ isActive }) => (
|
|
<>
|
|
<span
|
|
className={cn(
|
|
'flex items-center justify-center h-7 w-14 rounded-pill transition-colors duration-150',
|
|
isActive ? 'bg-sunrise-100 text-sunrise-600' : 'text-faint',
|
|
)}
|
|
>
|
|
<Icon size={20} />
|
|
</span>
|
|
<span
|
|
className={cn(
|
|
'text-2xs leading-none transition-colors duration-150',
|
|
isActive ? 'font-semibold text-sunrise-600' : 'font-medium text-faint',
|
|
)}
|
|
>
|
|
{t.label}
|
|
</span>
|
|
</>
|
|
)}
|
|
</NavLink>
|
|
);
|
|
})}
|
|
</nav>
|
|
</div>
|
|
);
|
|
}
|