2026-03-19 19:59:01 -05:00
|
|
|
import { useState, useEffect } from 'react';
|
|
|
|
|
import { Dashboard } from './components/Dashboard';
|
|
|
|
|
import { Inventory } from './components/Inventory';
|
|
|
|
|
import { ProtocolChecker } from './components/ProtocolChecker';
|
|
|
|
|
import { Onboarding } from './components/Onboarding';
|
2026-04-04 23:11:51 -05:00
|
|
|
import { ProfileSettings } from './components/ProfileSettings';
|
2026-03-19 19:59:01 -05:00
|
|
|
import { LoginForm } from './components/auth/LoginForm';
|
|
|
|
|
import { SignUpForm } from './components/auth/SignUpForm';
|
|
|
|
|
import { EmailVerification } from './components/auth/EmailVerification';
|
|
|
|
|
import { ForgotPassword } from './components/auth/ForgotPassword';
|
|
|
|
|
import { ResetPassword } from './components/auth/ResetPassword';
|
2026-04-09 04:35:02 -05:00
|
|
|
import { PrivacyPolicy } from './components/PrivacyPolicy';
|
2026-03-19 19:59:01 -05:00
|
|
|
import { useSession, signOut } from './lib/auth-client';
|
2026-04-04 23:11:51 -05:00
|
|
|
import { LayoutDashboard, Package, FileCheck, LogOut, UserCircle } from 'lucide-react';
|
2026-03-19 19:59:01 -05:00
|
|
|
|
|
|
|
|
const logo = '/logo.png';
|
|
|
|
|
|
|
|
|
|
type AppView =
|
|
|
|
|
| 'loading'
|
|
|
|
|
| 'login'
|
|
|
|
|
| 'signup'
|
|
|
|
|
| 'verify-email'
|
|
|
|
|
| 'forgot-password'
|
|
|
|
|
| 'reset-password'
|
|
|
|
|
| 'onboarding'
|
|
|
|
|
| 'app';
|
|
|
|
|
|
2026-04-04 23:11:51 -05:00
|
|
|
type Tab = 'dashboard' | 'inventory' | 'protocol' | 'profile';
|
2026-03-19 19:59:01 -05:00
|
|
|
|
2026-04-09 04:35:02 -05:00
|
|
|
function isPrivacyRoute() {
|
|
|
|
|
return window.location.pathname === '/privacy';
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-19 19:59:01 -05:00
|
|
|
function isResetPasswordRoute() {
|
|
|
|
|
return (
|
|
|
|
|
window.location.pathname === '/reset-password' &&
|
|
|
|
|
Boolean(new URLSearchParams(window.location.search).get('token'))
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-03-18 17:10:16 -05:00
|
|
|
|
|
|
|
|
export default function App() {
|
2026-03-19 05:42:11 +00:00
|
|
|
const { data: session, isPending } = useSession();
|
2026-04-09 04:35:02 -05:00
|
|
|
const [isPrivacy] = useState(isPrivacyRoute);
|
2026-03-19 19:59:01 -05:00
|
|
|
const [view, setView] = useState<AppView>(
|
|
|
|
|
isResetPasswordRoute() ? 'reset-password' : 'loading'
|
|
|
|
|
);
|
|
|
|
|
const [activeTab, setActiveTab] = useState<Tab>('dashboard');
|
2026-04-10 21:58:15 -05:00
|
|
|
const [sidebarName, setSidebarName] = useState<string | null | undefined>(undefined);
|
2026-03-18 17:10:16 -05:00
|
|
|
|
2026-03-19 19:59:01 -05:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (view === 'reset-password') return;
|
|
|
|
|
if (isPending) return;
|
|
|
|
|
|
|
|
|
|
if (!session) {
|
2026-04-02 14:29:56 -05:00
|
|
|
if (view !== 'signup' && view !== 'forgot-password') setView('login');
|
2026-03-19 19:59:01 -05:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!session.user.emailVerified) {
|
|
|
|
|
setView('verify-email');
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-03-18 17:10:16 -05:00
|
|
|
|
2026-03-19 19:59:01 -05:00
|
|
|
// Check if lab profile exists
|
|
|
|
|
fetch('/api/profile', { credentials: 'include' }).then(r => {
|
|
|
|
|
setView(r.ok ? 'app' : 'onboarding');
|
|
|
|
|
});
|
|
|
|
|
}, [session, isPending, view]);
|
|
|
|
|
|
2026-04-09 04:35:02 -05:00
|
|
|
if (isPrivacy) return <PrivacyPolicy />;
|
|
|
|
|
|
2026-03-19 19:59:01 -05:00
|
|
|
if (view === 'reset-password') {
|
|
|
|
|
return (
|
|
|
|
|
<ResetPassword
|
|
|
|
|
onSuccess={() => {
|
|
|
|
|
window.history.replaceState({}, '', '/');
|
|
|
|
|
setView('login');
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (view === 'loading') {
|
2026-03-19 05:42:11 +00:00
|
|
|
return (
|
|
|
|
|
<div className="flex h-screen items-center justify-center bg-secondary">
|
|
|
|
|
<img src={logo} alt="LabWise" className="h-12 opacity-50 animate-pulse" />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!session) {
|
2026-03-19 19:59:01 -05:00
|
|
|
if (view === 'signup')
|
|
|
|
|
return (
|
|
|
|
|
<SignUpForm
|
|
|
|
|
onLogin={() => setView('login')}
|
|
|
|
|
onVerify={() => setView('verify-email')}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
if (view === 'forgot-password')
|
|
|
|
|
return <ForgotPassword onBack={() => setView('login')} />;
|
2026-03-19 05:42:11 +00:00
|
|
|
return (
|
2026-03-19 19:59:01 -05:00
|
|
|
<LoginForm
|
|
|
|
|
onSignUp={() => setView('signup')}
|
|
|
|
|
onForgotPassword={() => setView('forgot-password')}
|
|
|
|
|
/>
|
2026-03-19 05:42:11 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-19 19:59:01 -05:00
|
|
|
if (view === 'verify-email')
|
|
|
|
|
return <EmailVerification email={session.user.email} />;
|
|
|
|
|
|
|
|
|
|
if (view === 'onboarding')
|
|
|
|
|
return <Onboarding onComplete={() => setView('app')} />;
|
|
|
|
|
|
|
|
|
|
if (view !== 'app') return null;
|
|
|
|
|
|
|
|
|
|
const navItems = [
|
|
|
|
|
{ id: 'dashboard' as Tab, label: 'Dashboard', icon: LayoutDashboard },
|
|
|
|
|
{ id: 'inventory' as Tab, label: 'Inventory', icon: Package },
|
|
|
|
|
{ id: 'protocol' as Tab, label: 'Protocol Checker', icon: FileCheck },
|
|
|
|
|
];
|
|
|
|
|
|
2026-03-18 17:10:16 -05:00
|
|
|
return (
|
|
|
|
|
<div className="flex h-screen bg-secondary">
|
|
|
|
|
<aside className="w-64 bg-card border-r border-border flex flex-col">
|
|
|
|
|
<div className="p-6 border-b border-border flex items-center justify-center">
|
2026-04-09 14:20:18 -05:00
|
|
|
<button onClick={() => setActiveTab('dashboard')} className="cursor-pointer">
|
|
|
|
|
<img src={logo} alt="labwise" className="h-15" />
|
|
|
|
|
</button>
|
2026-03-18 17:10:16 -05:00
|
|
|
</div>
|
|
|
|
|
<nav className="flex-1 p-4">
|
2026-03-19 19:59:01 -05:00
|
|
|
{navItems.map(item => {
|
2026-03-18 17:10:16 -05:00
|
|
|
const Icon = item.icon;
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
key={item.id}
|
|
|
|
|
onClick={() => setActiveTab(item.id)}
|
|
|
|
|
className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg mb-2 transition-colors ${
|
|
|
|
|
activeTab === item.id
|
2026-03-19 19:59:01 -05:00
|
|
|
? 'bg-accent text-primary'
|
|
|
|
|
: 'text-muted-foreground hover:bg-muted'
|
2026-03-18 17:10:16 -05:00
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<Icon className="w-5 h-5" />
|
|
|
|
|
<span>{item.label}</span>
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</nav>
|
2026-04-04 23:11:51 -05:00
|
|
|
<div className="p-4 border-t border-border space-y-1">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setActiveTab('profile')}
|
|
|
|
|
className={`w-full flex items-center gap-3 px-4 py-2 rounded-lg transition-colors text-sm ${
|
|
|
|
|
activeTab === 'profile'
|
|
|
|
|
? 'bg-accent text-primary'
|
|
|
|
|
: 'text-muted-foreground hover:bg-muted'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<UserCircle className="w-4 h-4 shrink-0" />
|
|
|
|
|
<span className="truncate text-left">
|
2026-04-10 21:58:15 -05:00
|
|
|
{(sidebarName !== undefined ? sidebarName : session.user.name) || session.user.email}
|
2026-04-04 23:11:51 -05:00
|
|
|
</span>
|
|
|
|
|
</button>
|
2026-03-19 05:42:11 +00:00
|
|
|
<button
|
|
|
|
|
onClick={() => signOut()}
|
|
|
|
|
className="w-full flex items-center gap-3 px-4 py-2 rounded-lg text-muted-foreground hover:bg-muted transition-colors text-sm"
|
|
|
|
|
>
|
|
|
|
|
<LogOut className="w-4 h-4" />
|
|
|
|
|
<span>Sign out</span>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
2026-03-18 17:10:16 -05:00
|
|
|
</aside>
|
|
|
|
|
<main className="flex-1 overflow-auto">
|
2026-03-19 19:59:01 -05:00
|
|
|
{activeTab === 'dashboard' && <Dashboard setActiveTab={setActiveTab} />}
|
|
|
|
|
{activeTab === 'inventory' && <Inventory />}
|
|
|
|
|
{activeTab === 'protocol' && <ProtocolChecker />}
|
2026-04-10 21:58:15 -05:00
|
|
|
{activeTab === 'profile' && <ProfileSettings onNameChange={setSidebarName} />}
|
2026-03-18 17:10:16 -05:00
|
|
|
</main>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2026-03-19 19:59:01 -05:00
|
|
|
}
|