omnidiag / frontend /src /App.jsx
yahyoha's picture
fix(admin): auto-navigate to Admin Dashboard after login from admin button click
1d8c897
Raw
History Blame Contribute Delete
8.52 kB
import { useState } from 'react';
import {
Heart,
FlaskConical,
Stethoscope,
Shield,
Table2,
ArrowLeftRight,
LogIn,
LogOut,
Moon,
Sun,
Menu,
} from 'lucide-react';
import { DiseaseProvider } from './context/DiseaseContext';
import { AuthProvider, useAuth } from './context/AuthContext';
import { ThemeProvider, useTheme } from './context/ThemeContext';
import DiseaseSelector from './components/DiseaseSelector';
import EngineeringMode from './components/EngineeringMode';
import ClinicalEmrMode from './components/ClinicalEmrMode';
import AdminDashboard from './components/AdminDashboard';
import BatchUpload from './components/BatchUpload';
import ComparisonMode from './components/ComparisonMode';
import OfflineBanner from './components/OfflineBanner';
import InstallPrompt from './components/InstallPrompt';
import LoginModal from './components/LoginModal';
const CLINICAL_VIEWS = [
{ id: 'engineering', label: 'Engineering Mode', icon: FlaskConical, component: EngineeringMode },
{ id: 'clinical', label: 'Clinical EMR Mode', icon: Stethoscope, component: ClinicalEmrMode },
{ id: 'batch', label: 'Batch Prediction', icon: Table2, component: BatchUpload },
{ id: 'comparison', label: 'Before/After Compare', icon: ArrowLeftRight, component: ComparisonMode },
];
function AppContent() {
const [activeView, setActiveView] = useState('engineering');
const [sidebarOpen, setSidebarOpen] = useState(false);
const [showLogin, setShowLogin] = useState(false);
const [pendingView, setPendingView] = useState(null);
const { user, isAdmin, logout } = useAuth();
const { dark, toggle: toggleTheme } = useTheme();
const isAdminView = activeView === 'admin';
let ActiveComponent = CLINICAL_VIEWS.find((v) => v.id === activeView)?.component || EngineeringMode;
if (isAdminView) ActiveComponent = AdminDashboard;
function handleAdminClick() {
if (user) {
setActiveView('admin');
setSidebarOpen(false);
} else {
setPendingView('admin');
setShowLogin(true);
}
}
function handleLoginSuccess() {
if (pendingView) {
setActiveView(pendingView);
setPendingView(null);
}
}
return (
<div className="min-h-screen bg-clinical-bg flex dark:bg-slate-950">
{/* Skip to main content — WCAG 2.1 SC 2.4.1 */}
<a href="#main-content" className="skip-link">Skip to main content</a>
<OfflineBanner />
{/* Mobile sidebar overlay */}
{sidebarOpen && (
<div
className="fixed inset-0 bg-black/30 z-30 lg:hidden"
onClick={() => setSidebarOpen(false)}
/>
)}
{/* ── Sidebar ── */}
<aside
aria-label="Navigation"
className={`
fixed lg:static inset-y-0 left-0 z-40 w-64 bg-white border-r border-clinical-border
dark:bg-slate-900 dark:border-slate-700
transform transition-transform duration-200 ease-in-out flex flex-col
${sidebarOpen ? 'translate-x-0' : '-translate-x-full'}
lg:translate-x-0 lg:flex
`}
>
{/* Logo */}
<div className="h-16 flex items-center gap-3 px-6 border-b border-clinical-border flex-shrink-0">
<div className="w-9 h-9 rounded-lg bg-primary-600 flex items-center justify-center">
<Heart className="w-5 h-5 text-white" />
</div>
<div>
<h1 className="text-base font-bold text-gray-900 leading-tight">OmniDiag</h1>
<p className="text-[10px] text-gray-500 leading-tight">Multi-Disease Diagnostic Platform</p>
</div>
</div>
{/* Disease selector — only relevant for clinical views */}
{!isAdminView && <DiseaseSelector />}
{/* Navigation */}
<nav className="p-4 space-y-1 flex-1 overflow-y-auto">
{CLINICAL_VIEWS.map((view) => {
const Icon = view.icon;
const isActive = activeView === view.id;
return (
<button
key={view.id}
onClick={() => { setActiveView(view.id); setSidebarOpen(false); }}
className={isActive ? 'sidebar-link-active w-full text-left' : 'sidebar-link-inactive w-full text-left'}
>
<Icon className={`w-5 h-5 ${isActive ? 'text-primary-600' : 'text-gray-400'}`} />
{view.label}
</button>
);
})}
{/* Admin section divider */}
<div className="pt-2 mt-2 border-t border-clinical-border">
<button
onClick={handleAdminClick}
className={activeView === 'admin' ? 'sidebar-link-active w-full text-left' : 'sidebar-link-inactive w-full text-left'}
>
<Shield className={`w-5 h-5 ${activeView === 'admin' ? 'text-primary-600' : 'text-gray-400'}`} />
Admin Dashboard
{!user && <span className="ml-auto text-[10px] text-gray-400">Sign in</span>}
</button>
</div>
</nav>
{/* Footer */}
<div className="p-4 border-t border-clinical-border space-y-2 flex-shrink-0">
{user ? (
<div className="flex items-center justify-between">
<div className="min-w-0">
<p className="text-xs font-medium text-gray-900 truncate">{user.full_name}</p>
<p className="text-[10px] text-gray-500 truncate">{user.email}</p>
</div>
<button
onClick={logout}
title="Sign out"
className="p-1.5 rounded-lg hover:bg-red-50 text-gray-400 hover:text-red-600 transition-colors flex-shrink-0"
>
<LogOut className="w-4 h-4" />
</button>
</div>
) : (
<button
onClick={() => setShowLogin(true)}
className="btn-secondary w-full text-xs flex items-center gap-2"
>
<LogIn className="w-4 h-4" /> Sign In
</button>
)}
{/* Dark mode toggle */}
<button
onClick={toggleTheme}
aria-label={dark ? 'Switch to light mode' : 'Switch to dark mode'}
className="btn-secondary w-full text-xs flex items-center gap-2"
>
{dark ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
{dark ? 'Light Mode' : 'Dark Mode'}
</button>
<InstallPrompt />
<p className="text-[10px] text-gray-400 text-center dark:text-slate-600">
OmniDiag v4.0.0 &middot; Powered by XGBoost + SHAP
</p>
</div>
</aside>
{/* ── Main Content ── */}
<div className="flex-1 min-w-0">
{/* Mobile top bar */}
<header className="h-16 bg-white border-b border-clinical-border flex items-center justify-between px-4 lg:hidden dark:bg-slate-900 dark:border-slate-700">
<button
onClick={() => setSidebarOpen(true)}
className="p-2 rounded-lg hover:bg-gray-100 transition-colors"
>
<Menu className="w-5 h-5 text-gray-600" />
</button>
<div className="flex items-center gap-2">
<Heart className="w-5 h-5 text-primary-600" />
<span className="text-sm font-bold text-gray-900">OmniDiag</span>
</div>
<div className="flex items-center gap-2">
<InstallPrompt />
</div>
</header>
{/* Page content */}
<main id="main-content" className="p-4 lg:p-8 max-w-7xl mx-auto" role="main">
{isAdminView && !isAdmin ? (
<div className="flex flex-col items-center justify-center py-24 text-center">
<Shield className="w-12 h-12 text-gray-300 mb-4" />
<p className="text-gray-500 text-sm">Admin access required.</p>
<button onClick={() => setShowLogin(true)} className="btn-primary mt-4 text-sm">
Sign In as Admin
</button>
</div>
) : (
<ActiveComponent />
)}
</main>
</div>
{showLogin && <LoginModal onClose={() => setShowLogin(false)} onSuccess={handleLoginSuccess} />}
</div>
);
}
export default function App() {
return (
<ThemeProvider>
<AuthProvider>
<DiseaseProvider>
<AppContent />
</DiseaseProvider>
</AuthProvider>
</ThemeProvider>
);
}