feat: Added frontend for The Catalyst tab (#10)

Added frontend for "The Catalyst" tab.

Co-authored-by: Sayan Datta <sayan@Sayans-MacBook-Air.local>
Reviewed-on: #10
This commit was merged in pull request #10.
This commit is contained in:
2026-03-27 22:35:25 +05:30
parent 023ba48da2
commit 5478f2815e
56 changed files with 80532 additions and 187100 deletions

View File

@@ -1,3 +1,5 @@
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';
@@ -7,6 +9,7 @@ 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 type { ModuleId } from '@/types';
import {
MoreVertical,
@@ -19,30 +22,75 @@ import {
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
} from "@/components/ui/dropdown-menu";
const moduleComponents: Record<ModuleId, React.ComponentType> = {
dashboard: Dashboard,
oracle: Oracle,
sentinel: Sentinel,
inventory: Inventory,
settings: Settings,
};
// ── Route map ─────────────────────────────────────────────────────────────────
// Single source of truth: module id → URL path → page title → component
const moduleTitles: Record<ModuleId, string> = {
dashboard: 'Dashboard',
oracle: 'The Oracle',
sentinel: 'The Sentinel',
inventory: 'Inventory',
settings: 'Settings',
};
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();
const ActiveComponent = moduleComponents[activeModule];
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 />
@@ -73,7 +121,7 @@ function MainLayout() {
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.25 }}
>
<h1 className="text-base font-semibold text-white tracking-tight">{moduleTitles[activeModule]}</h1>
<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>
@@ -101,7 +149,7 @@ function MainLayout() {
<DropdownMenuContent align="end" className="w-48 bg-[#0A0B10] border-white/10 text-zinc-200">
<DropdownMenuItem
className="cursor-pointer focus:bg-white/5"
onClick={() => setActiveModule('settings')}
onClick={goToSettings}
>
<Settings2 className="mr-2 h-4 w-4 text-zinc-400" />
<span>Settings</span>
@@ -117,11 +165,11 @@ function MainLayout() {
</div>
</header>
{/* Module Content */}
{/* Module Content — animated on route change */}
<div className="px-8 pb-8">
<AnimatePresence mode="wait">
<motion.div
key={activeModule}
key={location.pathname}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
@@ -130,7 +178,16 @@ function MainLayout() {
ease: [0.4, 0, 0.2, 1]
}}
>
<ActiveComponent />
{/* 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>
@@ -139,6 +196,8 @@ function MainLayout() {
);
}
// ── Root App ──────────────────────────────────────────────────────────────────
function App() {
const { isAuthenticated } = useStore();
@@ -152,7 +211,10 @@ function App() {
exit={{ opacity: 0 }}
transition={{ duration: 0.5 }}
>
<LoginScreen />
<Routes>
<Route path="/login" element={<LoginScreen />} />
<Route path="*" element={<LoginScreen />} />
</Routes>
</motion.div>
) : (
<motion.div
@@ -162,7 +224,16 @@ function App() {
exit={{ opacity: 0 }}
transition={{ duration: 0.5 }}
>
<MainLayout />
<Routes>
<Route
path="/*"
element={
<ProtectedRoute>
<MainLayout />
</ProtectedRoute>
}
/>
</Routes>
</motion.div>
)}
</AnimatePresence>