feat: Introduce a shared Layout component, add new Insights and Sources pages, and refactor Dashboard and Profile pages to integrate with the new layout.
This commit is contained in:
@@ -1,19 +1,29 @@
|
|||||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
|
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
|
||||||
import { LoginPage } from './pages/Login'
|
import { LoginPage } from './pages/Login'
|
||||||
import { SignupPage } from './pages/Signup'
|
import { SignupPage } from './pages/Signup'
|
||||||
import { Dashboard } from './pages/Dashboard'
|
import { DashboardPage } from './pages/Dashboard'
|
||||||
import { ProfilePage } from './pages/Profile'
|
import { ProfilePage } from './pages/Profile'
|
||||||
|
import { InsightsPage } from './pages/Insights'
|
||||||
|
import { SourcesPage } from './pages/Sources'
|
||||||
import { AdminPage } from './pages/Admin'
|
import { AdminPage } from './pages/Admin'
|
||||||
|
import { Layout } from './components/Layout'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
|
{/* Public routes */}
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
<Route path="/signup" element={<SignupPage />} />
|
<Route path="/signup" element={<SignupPage />} />
|
||||||
<Route path="/profile" element={<ProfilePage />} />
|
|
||||||
<Route path="/admin" element={<AdminPage />} />
|
{/* Protected routes with Layout */}
|
||||||
<Route path="/" element={<Dashboard />} />
|
<Route path="/" element={<Layout><DashboardPage /></Layout>} />
|
||||||
|
<Route path="/profile" element={<Layout><ProfilePage /></Layout>} />
|
||||||
|
<Route path="/insights" element={<Layout><InsightsPage /></Layout>} />
|
||||||
|
<Route path="/sources" element={<Layout><SourcesPage /></Layout>} />
|
||||||
|
<Route path="/admin" element={<Layout><AdminPage /></Layout>} />
|
||||||
|
|
||||||
|
{/* Fallback */}
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
|||||||
148
frontend/src/components/Layout.tsx
Normal file
148
frontend/src/components/Layout.tsx
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import { useEffect, useState, type ReactNode } from 'react'
|
||||||
|
import { useNavigate, useLocation, Link } from 'react-router-dom'
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
name: string | null
|
||||||
|
role: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LayoutProps {
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Layout({ children }: LayoutProps) {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const location = useLocation()
|
||||||
|
const [user, setUser] = useState<User | null>(null)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [theme, setTheme] = useState(() =>
|
||||||
|
document.documentElement.getAttribute('data-theme') || 'light'
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch('/api/auth/me', { credentials: 'include' })
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (!data.user) {
|
||||||
|
navigate('/login')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return fetch(`/api/users/${data.user.id}`, { credentials: 'include' })
|
||||||
|
.then(res => res.json())
|
||||||
|
})
|
||||||
|
.then((profile) => {
|
||||||
|
if (profile) {
|
||||||
|
setUser(profile)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => navigate('/login'))
|
||||||
|
.finally(() => setLoading(false))
|
||||||
|
}, [navigate])
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
await fetch('/api/auth/logout', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
})
|
||||||
|
navigate('/login')
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
const newTheme = theme === 'dark' ? 'light' : 'dark'
|
||||||
|
document.documentElement.setAttribute('data-theme', newTheme)
|
||||||
|
localStorage.setItem('theme', newTheme)
|
||||||
|
setTheme(newTheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div className="layout-loading">Loading...</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayName = user.name || user.username
|
||||||
|
|
||||||
|
const navItems = [
|
||||||
|
{ path: '/', label: 'Dashboard', icon: '📊' },
|
||||||
|
{ path: '/profile', label: 'Profile', icon: '👤' },
|
||||||
|
{ path: '/insights', label: 'Insights', icon: '💡', disabled: true },
|
||||||
|
{ path: '/sources', label: 'Sources', icon: '📄' },
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app-layout">
|
||||||
|
{/* Sidebar */}
|
||||||
|
<aside className="sidebar">
|
||||||
|
<div className="sidebar-header">
|
||||||
|
<img src="/logo.svg" alt="zhealth" className="sidebar-logo" />
|
||||||
|
<span className="sidebar-title">zhealth</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav className="sidebar-nav">
|
||||||
|
{navItems.map(item => (
|
||||||
|
<Link
|
||||||
|
key={item.path}
|
||||||
|
to={item.disabled ? '#' : item.path}
|
||||||
|
className={`sidebar-link ${location.pathname === item.path ? 'active' : ''} ${item.disabled ? 'disabled' : ''}`}
|
||||||
|
onClick={e => item.disabled && e.preventDefault()}
|
||||||
|
>
|
||||||
|
<span className="sidebar-icon">{item.icon}</span>
|
||||||
|
<span className="sidebar-label">{item.label}</span>
|
||||||
|
{item.disabled && <span className="sidebar-badge">Soon</span>}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Admin link for admins */}
|
||||||
|
{user.role === 'admin' && (
|
||||||
|
<div className="sidebar-section">
|
||||||
|
<div className="sidebar-section-title">Admin</div>
|
||||||
|
<Link
|
||||||
|
to="/admin"
|
||||||
|
className={`sidebar-link ${location.pathname === '/admin' ? 'active' : ''}`}
|
||||||
|
>
|
||||||
|
<span className="sidebar-icon">⚙️</span>
|
||||||
|
<span className="sidebar-label">Manage Users</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="sidebar-footer">
|
||||||
|
<div className="sidebar-user">
|
||||||
|
<div className="sidebar-user-name">{displayName}</div>
|
||||||
|
<div className="sidebar-user-role">
|
||||||
|
<span className={`indicator indicator-${user.role === 'admin' ? 'warning' : 'info'}`}>
|
||||||
|
{user.role}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="sidebar-actions">
|
||||||
|
<button
|
||||||
|
className="sidebar-btn"
|
||||||
|
onClick={toggleTheme}
|
||||||
|
title={theme === 'dark' ? 'Light mode' : 'Dark mode'}
|
||||||
|
>
|
||||||
|
{theme === 'dark' ? '☀️' : '🌙'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="sidebar-btn"
|
||||||
|
onClick={handleLogout}
|
||||||
|
title="Logout"
|
||||||
|
>
|
||||||
|
🚪
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<main className="main-content">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -421,3 +421,185 @@ select.input {
|
|||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* App Layout with Sidebar */
|
||||||
|
.app-layout {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
.sidebar {
|
||||||
|
width: 240px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: fixed;
|
||||||
|
height: 100vh;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
padding: var(--space-md);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-logo {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-nav {
|
||||||
|
flex: 1;
|
||||||
|
padding: var(--space-sm);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
padding: var(--space-sm) var(--space-md);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
transition: background-color 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-link:hover {
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-link.active {
|
||||||
|
background-color: var(--accent);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-link.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-link.disabled:hover {
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-label {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-badge {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
background: var(--border);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
border-radius: 9999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-section {
|
||||||
|
padding: var(--space-sm);
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-section-title {
|
||||||
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
padding: var(--space-xs) var(--space-md);
|
||||||
|
margin-bottom: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-footer {
|
||||||
|
padding: var(--space-md);
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-user {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-user-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-btn {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: var(--space-xs);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-btn:hover {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 240px;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Page wrapper */
|
||||||
|
.page {
|
||||||
|
padding: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: var(--space-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h1 {
|
||||||
|
margin-bottom: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-loading {
|
||||||
|
padding: var(--space-lg);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
@@ -1,12 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useNavigate, Link } from 'react-router-dom'
|
|
||||||
|
|
||||||
interface User {
|
|
||||||
id: number
|
|
||||||
username: string
|
|
||||||
name: string | null
|
|
||||||
role: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Category {
|
interface Category {
|
||||||
id: number
|
id: number
|
||||||
@@ -23,62 +15,22 @@ interface Biomarker {
|
|||||||
methodology: string | null
|
methodology: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Dashboard() {
|
export function DashboardPage() {
|
||||||
const navigate = useNavigate()
|
|
||||||
const [user, setUser] = useState<User | null>(null)
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
const [theme, setTheme] = useState(() =>
|
|
||||||
document.documentElement.getAttribute('data-theme') || 'light'
|
|
||||||
)
|
|
||||||
const [categories, setCategories] = useState<Category[]>([])
|
const [categories, setCategories] = useState<Category[]>([])
|
||||||
const [biomarkers, setBiomarkers] = useState<Biomarker[]>([])
|
const [biomarkers, setBiomarkers] = useState<Biomarker[]>([])
|
||||||
const [expandedCategories, setExpandedCategories] = useState<Set<number>>(new Set())
|
const [expandedCategories, setExpandedCategories] = useState<Set<number>>(new Set())
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// First get auth info, then fetch full user profile for name
|
|
||||||
fetch('/api/auth/me', { credentials: 'include' })
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(data => {
|
|
||||||
if (!data.user) {
|
|
||||||
navigate('/login')
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
// Fetch full user profile to get name
|
|
||||||
return fetch(`/api/users/${data.user.id}`, { credentials: 'include' })
|
|
||||||
.then(res => res.json())
|
|
||||||
})
|
|
||||||
.then((profile) => {
|
|
||||||
if (profile) {
|
|
||||||
setUser(profile)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => navigate('/login'))
|
|
||||||
.finally(() => setLoading(false))
|
|
||||||
|
|
||||||
// Fetch categories and biomarkers
|
|
||||||
Promise.all([
|
Promise.all([
|
||||||
fetch('/api/categories', { credentials: 'include' }).then(r => r.json()),
|
fetch('/api/categories', { credentials: 'include' }).then(r => r.json()),
|
||||||
fetch('/api/biomarkers', { credentials: 'include' }).then(r => r.json()),
|
fetch('/api/biomarkers', { credentials: 'include' }).then(r => r.json()),
|
||||||
]).then(([cats, bms]) => {
|
]).then(([cats, bms]) => {
|
||||||
setCategories(cats)
|
setCategories(cats)
|
||||||
setBiomarkers(bms)
|
setBiomarkers(bms)
|
||||||
|
setLoading(false)
|
||||||
})
|
})
|
||||||
}, [navigate])
|
}, [])
|
||||||
|
|
||||||
const handleLogout = async () => {
|
|
||||||
await fetch('/api/auth/logout', {
|
|
||||||
method: 'POST',
|
|
||||||
credentials: 'include',
|
|
||||||
})
|
|
||||||
navigate('/login')
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleTheme = () => {
|
|
||||||
const newTheme = theme === 'dark' ? 'light' : 'dark'
|
|
||||||
document.documentElement.setAttribute('data-theme', newTheme)
|
|
||||||
localStorage.setItem('theme', newTheme)
|
|
||||||
setTheme(newTheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleCategory = (categoryId: number) => {
|
const toggleCategory = (categoryId: number) => {
|
||||||
setExpandedCategories(prev => {
|
setExpandedCategories(prev => {
|
||||||
@@ -97,69 +49,18 @@ export function Dashboard() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <div style={{ padding: 'var(--space-lg)' }}>Loading...</div>
|
return <div className="page-loading">Loading biomarkers...</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display name or username fallback
|
|
||||||
const displayName = user.name || user.username
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: 'var(--space-lg)' }}>
|
<div className="page">
|
||||||
<header style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 'var(--space-xl)' }}>
|
<header className="page-header">
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-sm)' }}>
|
<h1>Dashboard</h1>
|
||||||
<img src="/logo.svg" alt="zhealth" style={{ width: '32px', height: '32px' }} />
|
<p className="text-secondary">View all biomarker categories and their reference markers</p>
|
||||||
<h1>zhealth</h1>
|
|
||||||
</div>
|
|
||||||
<div style={{ display: 'flex', gap: 'var(--space-sm)' }}>
|
|
||||||
<button
|
|
||||||
className="btn btn-secondary"
|
|
||||||
onClick={toggleTheme}
|
|
||||||
title={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
|
|
||||||
style={{ fontSize: '18px', padding: 'var(--space-sm)' }}
|
|
||||||
>
|
|
||||||
{theme === 'dark' ? '☀️' : '🌙'}
|
|
||||||
</button>
|
|
||||||
<button className="btn btn-secondary" onClick={handleLogout}>
|
|
||||||
Logout
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<section>
|
||||||
{/* User Welcome Card */}
|
<h2 style={{ marginBottom: 'var(--space-md)' }}>Biomarker Categories</h2>
|
||||||
<div className="card" style={{ marginBottom: 'var(--space-lg)' }}>
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
|
||||||
<div>
|
|
||||||
<h3 style={{ marginBottom: 'var(--space-sm)' }}>Welcome, {displayName}</h3>
|
|
||||||
<p className="text-secondary text-sm">
|
|
||||||
Role: <span className="indicator indicator-info">{user.role}</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Link to="/profile" className="btn btn-secondary">
|
|
||||||
Edit Profile
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Admin Section */}
|
|
||||||
{user.role === 'admin' && (
|
|
||||||
<div className="card" style={{ marginBottom: 'var(--space-lg)' }}>
|
|
||||||
<h3 style={{ marginBottom: 'var(--space-sm)' }}>Admin</h3>
|
|
||||||
<p className="text-secondary text-sm" style={{ marginBottom: 'var(--space-md)' }}>
|
|
||||||
Manage users and system settings
|
|
||||||
</p>
|
|
||||||
<Link to="/admin" className="btn btn-secondary">
|
|
||||||
Manage Users
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Biomarker Categories */}
|
|
||||||
<h2 style={{ marginBottom: 'var(--space-md)' }}>Biomarkers</h2>
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-sm)' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-sm)' }}>
|
||||||
{categories.map(category => {
|
{categories.map(category => {
|
||||||
const categoryBiomarkers = getBiomarkersForCategory(category.id)
|
const categoryBiomarkers = getBiomarkersForCategory(category.id)
|
||||||
@@ -167,7 +68,6 @@ export function Dashboard() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={category.id} className="card" style={{ padding: 0 }}>
|
<div key={category.id} className="card" style={{ padding: 0 }}>
|
||||||
{/* Category Header - Clickable */}
|
|
||||||
<button
|
<button
|
||||||
className="collapsible-header"
|
className="collapsible-header"
|
||||||
onClick={() => toggleCategory(category.id)}
|
onClick={() => toggleCategory(category.id)}
|
||||||
@@ -195,7 +95,6 @@ export function Dashboard() {
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Biomarkers List - Collapsible */}
|
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<div style={{ borderTop: '1px solid var(--border)', padding: 'var(--space-sm)' }}>
|
<div style={{ borderTop: '1px solid var(--border)', padding: 'var(--space-sm)' }}>
|
||||||
{categoryBiomarkers.length === 0 ? (
|
{categoryBiomarkers.length === 0 ? (
|
||||||
@@ -222,7 +121,7 @@ export function Dashboard() {
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
18
frontend/src/pages/Insights.tsx
Normal file
18
frontend/src/pages/Insights.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
export function InsightsPage() {
|
||||||
|
return (
|
||||||
|
<div className="page">
|
||||||
|
<header className="page-header">
|
||||||
|
<h1>Insights</h1>
|
||||||
|
<p className="text-secondary">AI-powered analysis of your health data</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="card" style={{ textAlign: 'center', padding: 'var(--space-xl)' }}>
|
||||||
|
<div style={{ fontSize: '48px', marginBottom: 'var(--space-md)' }}>🚀</div>
|
||||||
|
<h3>Coming Soon</h3>
|
||||||
|
<p className="text-secondary">
|
||||||
|
AI-generated health insights and recommendations based on your biomarker data.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useNavigate, Link } from 'react-router-dom'
|
|
||||||
|
|
||||||
interface Diet {
|
interface Diet {
|
||||||
id: number
|
id: number
|
||||||
@@ -21,7 +20,6 @@ interface UserProfile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ProfilePage() {
|
export function ProfilePage() {
|
||||||
const navigate = useNavigate()
|
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
|
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
|
||||||
@@ -39,19 +37,16 @@ export function ProfilePage() {
|
|||||||
const [dietId, setDietId] = useState<number | null>(null)
|
const [dietId, setDietId] = useState<number | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Fetch current user and diets
|
// Fetch current user and diets (Layout already ensures auth)
|
||||||
Promise.all([
|
Promise.all([
|
||||||
fetch('/api/auth/me', { credentials: 'include' }).then(r => r.json()),
|
fetch('/api/auth/me', { credentials: 'include' }).then(r => r.json()),
|
||||||
fetch('/api/diets', { credentials: 'include' }).then(r => r.json()),
|
fetch('/api/diets', { credentials: 'include' }).then(r => r.json()),
|
||||||
])
|
])
|
||||||
.then(([authData, dietsData]) => {
|
.then(([authData, dietsData]) => {
|
||||||
if (!authData.user) {
|
if (!authData.user) return
|
||||||
navigate('/login')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setDiets(dietsData)
|
setDiets(dietsData)
|
||||||
|
|
||||||
// Now fetch full user profile
|
// Fetch full user profile
|
||||||
return fetch(`/api/users/${authData.user.id}`, { credentials: 'include' })
|
return fetch(`/api/users/${authData.user.id}`, { credentials: 'include' })
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then((profile: UserProfile) => {
|
.then((profile: UserProfile) => {
|
||||||
@@ -68,9 +63,8 @@ export function ProfilePage() {
|
|||||||
setDietId(diet?.id || null)
|
setDietId(diet?.id || null)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch(() => navigate('/login'))
|
|
||||||
.finally(() => setLoading(false))
|
.finally(() => setLoading(false))
|
||||||
}, [navigate])
|
}, [])
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -108,14 +102,14 @@ export function ProfilePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <div style={{ padding: 'var(--space-lg)' }}>Loading...</div>
|
return <div className="page-loading">Loading...</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: 'var(--space-lg)', maxWidth: '600px', margin: '0 auto' }}>
|
<div className="page" style={{ maxWidth: '600px' }}>
|
||||||
<header style={{ marginBottom: 'var(--space-xl)' }}>
|
<header className="page-header">
|
||||||
<Link to="/" className="text-secondary text-sm">← Back to Dashboard</Link>
|
<h1>Profile</h1>
|
||||||
<h1 style={{ marginTop: 'var(--space-sm)' }}>Profile</h1>
|
<p className="text-secondary">Manage your account and health information</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
|
|||||||
41
frontend/src/pages/Sources.tsx
Normal file
41
frontend/src/pages/Sources.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
export function SourcesPage() {
|
||||||
|
return (
|
||||||
|
<div className="page">
|
||||||
|
<header className="page-header">
|
||||||
|
<h1>Sources</h1>
|
||||||
|
<p className="text-secondary">Upload and manage your health data sources</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="card">
|
||||||
|
<h3 style={{ marginBottom: 'var(--space-md)' }}>Upload Data</h3>
|
||||||
|
<p className="text-secondary text-sm" style={{ marginBottom: 'var(--space-lg)' }}>
|
||||||
|
Upload lab reports in PDF, CSV, or Excel format to import your biomarker data.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="upload-zone"
|
||||||
|
style={{
|
||||||
|
border: '2px dashed var(--border)',
|
||||||
|
borderRadius: 'var(--radius-md)',
|
||||||
|
padding: 'var(--space-xl)',
|
||||||
|
textAlign: 'center',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: '36px', marginBottom: 'var(--space-sm)' }}>📤</div>
|
||||||
|
<p className="text-secondary">
|
||||||
|
Drag & drop files here, or click to browse
|
||||||
|
</p>
|
||||||
|
<p className="text-secondary text-xs" style={{ marginTop: 'var(--space-sm)' }}>
|
||||||
|
Supported: PDF, CSV, XLSX
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card" style={{ marginTop: 'var(--space-lg)' }}>
|
||||||
|
<h3 style={{ marginBottom: 'var(--space-md)' }}>Recent Uploads</h3>
|
||||||
|
<p className="text-secondary text-sm">No files uploaded yet.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user