krishnasales_mobile/src/screens/ConsoleLayout.tsx
2026-07-16 16:17:10 +05:30

160 lines
6.2 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="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">
<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="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.filter(t => t.key !== 'analytics').length}, minmax(0, 1fr))` }}
>
{SCREENS.filter(t => t.key !== 'analytics').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>
);
}