160 lines
6.4 KiB
TypeScript
160 lines
6.4 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { NavLink, Navigate, Outlet, useNavigate } from 'react-router-dom';
|
|
import { LogOut, Menu, X } from 'lucide-react';
|
|
import { cn } from '../lib/cn';
|
|
import { useAuth } from '../auth/context';
|
|
import { onAuthErrorAll, orderBookingClient } from '../api/clients';
|
|
import { SCREENS } from './tabs';
|
|
import './screen.css';
|
|
|
|
/** 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 [isMenuOpen, setIsMenuOpen] = useState(false);
|
|
|
|
// 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="print:hidden sticky top-0 z-20 shrink-0 flex items-center justify-between gap-3 px-4 h-14 pt-[env(safe-area-inset-top)] top-navbar shadow-md">
|
|
<div className="flex items-center gap-3 flex-1">
|
|
<button
|
|
type="button"
|
|
aria-label="Toggle Menu"
|
|
onClick={() => setIsMenuOpen(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 nav-menu-icon"
|
|
>
|
|
<Menu size={20} />
|
|
</button>
|
|
<div className="flex flex-col justify-center leading-none">
|
|
<span className="text-base font-extrabold text-on-navy tracking-[-0.01em] leading-none nav-title">Krishna Sales</span>
|
|
{/* <span className="text-2xs text-on-navy-muted">Sandbox {APP_ID}{user?.name ? ` · ${user.name}` : ''}</span> */}
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<main className="flex-1 overflow-y-auto px-4 pt-4 pb-24">
|
|
<div className="w-full max-w-[1200px] mx-auto flex flex-col gap-5">
|
|
<Outlet />
|
|
</div>
|
|
</main>
|
|
|
|
<nav
|
|
className="print:hidden 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.filter(t => t.key !== 'analytics' && t.key !== 'all-daily' && t.key !== 'sales-report').length}, minmax(0, 1fr))` }}
|
|
>
|
|
{SCREENS.filter(t => t.key !== 'analytics' && t.key !== 'all-daily' && t.key !== 'sales-report').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 bottom-nav-icon',
|
|
isActive ? 'active' : 'text-faint',
|
|
)}
|
|
>
|
|
<Icon size={20} />
|
|
</span>
|
|
<span
|
|
className={cn(
|
|
'text-2xs leading-none transition-colors duration-150 bottom-nav-text',
|
|
isActive ? 'font-semibold active' : 'font-medium text-faint',
|
|
)}
|
|
>
|
|
{t.label}
|
|
</span>
|
|
</>
|
|
)}
|
|
</NavLink>
|
|
);
|
|
})}
|
|
</nav>
|
|
|
|
{/* Sidebar Overlay */}
|
|
{isMenuOpen && (
|
|
<div
|
|
className="fixed inset-0 z-40 bg-black/50 transition-opacity"
|
|
onClick={() => setIsMenuOpen(false)}
|
|
/>
|
|
)}
|
|
|
|
{/* Sidebar */}
|
|
<div
|
|
className={cn(
|
|
"fixed inset-y-0 left-0 z-50 w-64 bg-card border-r border-border-subtle shadow-xl transform transition-transform duration-300 ease-in-out flex flex-col",
|
|
isMenuOpen ? "translate-x-0" : "-translate-x-full"
|
|
)}
|
|
>
|
|
<div className="h-14 flex items-center justify-between px-4 border-b border-border-subtle pt-[env(safe-area-inset-top)] shrink-0">
|
|
<span className="font-semibold text-foreground">Krishna Field Sales</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => setIsMenuOpen(false)}
|
|
className="inline-flex items-center justify-center size-8 rounded-md text-faint hover:text-foreground hover:bg-black/5 transition-colors cursor-pointer"
|
|
>
|
|
<X size={20} />
|
|
</button>
|
|
</div>
|
|
<div className="flex-1 overflow-y-auto py-2">
|
|
{SCREENS.map((t) => {
|
|
const Icon = t.icon;
|
|
return (
|
|
<NavLink
|
|
key={t.key}
|
|
to={`/${t.key}`}
|
|
onClick={() => setIsMenuOpen(false)}
|
|
className={({ isActive }) => cn(
|
|
"flex items-center gap-3 px-4 py-3 mx-2 rounded-lg no-underline transition-colors duration-150 sidebar-item",
|
|
isActive ? "active" : ""
|
|
)}
|
|
>
|
|
<Icon size={20} className="shrink-0" />
|
|
<span>{t.label}</span>
|
|
</NavLink>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div className="p-4 border-t border-border-subtle shrink-0 flex flex-col gap-3 mb-[env(safe-area-inset-bottom)]">
|
|
{user && (
|
|
<div className="flex flex-col px-1">
|
|
|
|
<span className="text-sm font-medium text-foreground truncate">{user.name || 'User'}</span>
|
|
<span className="font-medium text-foreground truncate">{user.email || 'user@email.com'}</span>
|
|
</div>
|
|
)}
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setIsMenuOpen(false);
|
|
logout();
|
|
navigate('/login', { replace: true });
|
|
}}
|
|
className="flex items-center justify-center gap-2 w-full py-2.5 px-4 rounded-lg bg-black/5 hover:bg-black/10 text-foreground transition-colors font-medium text-sm cursor-pointer"
|
|
>
|
|
<LogOut size={16} />
|
|
<span>Sign out</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|