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:
117
app/src/App.tsx
117
app/src/App.tsx
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user