forked from sagnik/Project_Velocity
The complete code integration is done. Co-authored-by: Sagnik <sagnik7896@gmail.com> Reviewed-on: sagnik/Project_Velocity#18
256 lines
9.5 KiB
TypeScript
256 lines
9.5 KiB
TypeScript
import { useEffect } from 'react';
|
|
import { Routes, Route, Navigate, useNavigate, useLocation } from 'react-router-dom';
|
|
import { AnimatePresence, motion } from 'framer-motion';
|
|
import { useStore } from '@/store/useStore';
|
|
import { Sidebar } from '@/components/layout/Sidebar';
|
|
import { LoginScreen } from '@/components/layout/LoginScreen';
|
|
import { Dashboard } from '@/components/modules/Dashboard';
|
|
import { Oracle } from '@/components/modules/Oracle';
|
|
import { Sentinel } from '@/components/modules/Sentinel';
|
|
import { Inventory } from '@/components/modules/Inventory';
|
|
import { Settings } from '@/components/modules/Settings';
|
|
import { Catalyst } from '@/components/modules/Catalyst';
|
|
import { NotificationCenter } from '@/components/layout/NotificationCenter';
|
|
import { useCrmBootstrap } from '@/hooks/useCrmBootstrap';
|
|
import type { ModuleId } from '@/types';
|
|
|
|
import {
|
|
MoreVertical,
|
|
LogOut,
|
|
Settings2
|
|
} from 'lucide-react';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
|
|
// ── Route map ─────────────────────────────────────────────────────────────────
|
|
// Single source of truth: module id → URL path → page title → component
|
|
|
|
export const MODULE_ROUTES: Array<{
|
|
id: ModuleId;
|
|
path: string;
|
|
title: string;
|
|
component: React.ComponentType;
|
|
}> = [
|
|
{ id: 'dashboard', path: '/dashboard', title: 'Dashboard', component: Dashboard },
|
|
{ id: 'oracle', path: '/oracle', title: 'The Oracle', component: Oracle },
|
|
{ id: 'sentinel', path: '/sentinel', title: 'The Sentinel', component: Sentinel },
|
|
{ id: 'inventory', path: '/inventory', title: 'Inventory', component: Inventory },
|
|
{ id: 'catalyst', path: '/catalyst', title: 'The Catalyst', component: Catalyst },
|
|
{ id: 'settings', path: '/settings', title: 'Settings', component: Settings },
|
|
];
|
|
|
|
export const PATH_TO_MODULE = Object.fromEntries(
|
|
MODULE_ROUTES.map((r) => [r.path, r.id])
|
|
) as Record<string, ModuleId>;
|
|
|
|
// ── Protected Route guard ─────────────────────────────────────────────────────
|
|
|
|
function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
|
const { isAuthenticated } = useStore();
|
|
if (!isAuthenticated) return <Navigate to="/login" replace />;
|
|
return <>{children}</>;
|
|
}
|
|
|
|
// ── Sync URL → Zustand activeModule ──────────────────────────────────────────
|
|
// This keeps the Zustand store in sync with the URL so existing module
|
|
// components that read `activeModule` from the store continue to work.
|
|
|
|
function RouteModuleSync() {
|
|
const { pathname } = useLocation();
|
|
const { setActiveModule } = useStore();
|
|
|
|
useEffect(() => {
|
|
const moduleId = PATH_TO_MODULE[pathname];
|
|
if (moduleId) setActiveModule(moduleId);
|
|
}, [pathname, setActiveModule]);
|
|
|
|
return null;
|
|
}
|
|
|
|
// ── Main authenticated layout ─────────────────────────────────────────────────
|
|
|
|
function MainLayout() {
|
|
const { activeModule, setActiveModule, sidebarExpanded, logout } = useStore();
|
|
useCrmBootstrap();
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
|
|
// Current route title
|
|
const currentRoute = MODULE_ROUTES.find((r) => r.path === location.pathname);
|
|
const pageTitle = currentRoute?.title ?? 'Velocity';
|
|
|
|
// Navigate to settings from dropdown (keeps router in sync)
|
|
const goToSettings = () => {
|
|
setActiveModule('settings');
|
|
navigate('/settings');
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen flex" style={{ background: '#000' }}>
|
|
{/* Sync URL → store */}
|
|
<RouteModuleSync />
|
|
|
|
{/* Sidebar */}
|
|
<Sidebar />
|
|
|
|
{/* Main Content Area */}
|
|
<motion.main
|
|
className="flex-1 h-screen flex flex-col overflow-hidden"
|
|
initial={{ marginLeft: 72 }}
|
|
animate={{ marginLeft: sidebarExpanded ? 232 : 72 }}
|
|
transition={{
|
|
type: 'spring',
|
|
stiffness: 320,
|
|
damping: 32,
|
|
mass: 0.8
|
|
}}
|
|
>
|
|
{/* Top Bar */}
|
|
<header className="flex-none z-40 px-6 py-4">
|
|
<div
|
|
className="flex items-center justify-between px-5 py-3 rounded-2xl"
|
|
style={{
|
|
background: 'hsl(var(--surface))',
|
|
border: '1px solid hsl(var(--border-subtle))',
|
|
}}
|
|
>
|
|
<motion.div
|
|
key={activeModule}
|
|
initial={{ opacity: 0, x: -12 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
transition={{ duration: 0.25 }}
|
|
>
|
|
<h1 className="text-base font-semibold text-white tracking-tight">{pageTitle}</h1>
|
|
<p className="text-xs" style={{ color: 'hsl(var(--muted-fg))' }}>Project Velocity · v.1.1</p>
|
|
</motion.div>
|
|
|
|
{/* User Profile */}
|
|
<div className="flex items-center gap-2">
|
|
{/* Active Notification Center */}
|
|
<NotificationCenter />
|
|
|
|
<div className="w-px h-6 bg-white/10" />
|
|
|
|
<div className="text-right">
|
|
|
|
<p className="text-sm font-medium text-white">Ahmed Al-Farsi</p>
|
|
<p className="text-xs" style={{ color: 'hsl(var(--muted-fg))' }}>Sales Director</p>
|
|
</div>
|
|
<div
|
|
className="w-9 h-9 rounded-xl flex items-center justify-center text-sm font-semibold"
|
|
style={{
|
|
background: 'hsl(var(--accent))',
|
|
color: 'hsl(var(--accent-fg))',
|
|
}}
|
|
>
|
|
AA
|
|
</div>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<button className="h-9 w-9 flex items-center justify-center rounded-lg hover:bg-white/5 text-zinc-400 hover:text-white transition-colors outline-none">
|
|
<MoreVertical className="w-5 h-5" />
|
|
</button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="w-48 bg-[#0A0B10] border-white/10 text-zinc-200">
|
|
<DropdownMenuItem
|
|
className="cursor-pointer focus:bg-white/5"
|
|
onClick={goToSettings}
|
|
>
|
|
<Settings2 className="mr-2 h-4 w-4 text-zinc-400" />
|
|
<span>Settings</span>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator className="bg-white/10" />
|
|
<DropdownMenuItem className="text-red-400 focus:text-red-400 focus:bg-red-500/10 cursor-pointer" onClick={() => logout()}>
|
|
<LogOut className="mr-2 h-4 w-4" />
|
|
<span>Log out</span>
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Module Content — animated on route change */}
|
|
<div className="flex-1 overflow-y-auto custom-scrollbar">
|
|
<div className="px-8 pb-8 min-h-full relative">
|
|
<AnimatePresence mode="wait">
|
|
<motion.div
|
|
key={location.pathname}
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -20 }}
|
|
transition={{
|
|
duration: 0.3,
|
|
ease: [0.4, 0, 0.2, 1]
|
|
}}
|
|
>
|
|
{/* Nested module routes rendered here */}
|
|
<Routes>
|
|
{MODULE_ROUTES.map(({ path, component: Component }) => (
|
|
<Route key={path} path={path} element={<Component />} />
|
|
))}
|
|
{/* Default: redirect / → /dashboard */}
|
|
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
|
{/* Catch-all: any unknown path → dashboard */}
|
|
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
|
</Routes>
|
|
</motion.div>
|
|
</AnimatePresence>
|
|
</div>
|
|
</div>
|
|
</motion.main>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ── Root App ──────────────────────────────────────────────────────────────────
|
|
|
|
function App() {
|
|
const { isAuthenticated } = useStore();
|
|
|
|
return (
|
|
<AnimatePresence mode="wait">
|
|
{!isAuthenticated ? (
|
|
<motion.div
|
|
key="login"
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
transition={{ duration: 0.5 }}
|
|
>
|
|
<Routes>
|
|
<Route path="/login" element={<LoginScreen />} />
|
|
<Route path="*" element={<LoginScreen />} />
|
|
</Routes>
|
|
</motion.div>
|
|
) : (
|
|
<motion.div
|
|
key="app"
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
transition={{ duration: 0.5 }}
|
|
>
|
|
<Routes>
|
|
<Route
|
|
path="/*"
|
|
element={
|
|
<ProtectedRoute>
|
|
<MainLayout />
|
|
</ProtectedRoute>
|
|
}
|
|
/>
|
|
</Routes>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
);
|
|
}
|
|
|
|
export default App;
|