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,12 +1,4 @@
|
||||
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 {
|
||||
id: number
|
||||
@@ -23,62 +15,22 @@ interface Biomarker {
|
||||
methodology: string | null
|
||||
}
|
||||
|
||||
export function Dashboard() {
|
||||
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'
|
||||
)
|
||||
export function DashboardPage() {
|
||||
const [categories, setCategories] = useState<Category[]>([])
|
||||
const [biomarkers, setBiomarkers] = useState<Biomarker[]>([])
|
||||
const [expandedCategories, setExpandedCategories] = useState<Set<number>>(new Set())
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
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([
|
||||
fetch('/api/categories', { credentials: 'include' }).then(r => r.json()),
|
||||
fetch('/api/biomarkers', { credentials: 'include' }).then(r => r.json()),
|
||||
]).then(([cats, bms]) => {
|
||||
setCategories(cats)
|
||||
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) => {
|
||||
setExpandedCategories(prev => {
|
||||
@@ -97,69 +49,18 @@ export function Dashboard() {
|
||||
}
|
||||
|
||||
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 (
|
||||
<div style={{ padding: 'var(--space-lg)' }}>
|
||||
<header style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 'var(--space-xl)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-sm)' }}>
|
||||
<img src="/logo.svg" alt="zhealth" style={{ width: '32px', height: '32px' }} />
|
||||
<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>
|
||||
<div className="page">
|
||||
<header className="page-header">
|
||||
<h1>Dashboard</h1>
|
||||
<p className="text-secondary">View all biomarker categories and their reference markers</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
{/* User Welcome Card */}
|
||||
<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>
|
||||
<section>
|
||||
<h2 style={{ marginBottom: 'var(--space-md)' }}>Biomarker Categories</h2>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-sm)' }}>
|
||||
{categories.map(category => {
|
||||
const categoryBiomarkers = getBiomarkersForCategory(category.id)
|
||||
@@ -167,7 +68,6 @@ export function Dashboard() {
|
||||
|
||||
return (
|
||||
<div key={category.id} className="card" style={{ padding: 0 }}>
|
||||
{/* Category Header - Clickable */}
|
||||
<button
|
||||
className="collapsible-header"
|
||||
onClick={() => toggleCategory(category.id)}
|
||||
@@ -195,7 +95,6 @@ export function Dashboard() {
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Biomarkers List - Collapsible */}
|
||||
{isExpanded && (
|
||||
<div style={{ borderTop: '1px solid var(--border)', padding: 'var(--space-sm)' }}>
|
||||
{categoryBiomarkers.length === 0 ? (
|
||||
@@ -222,7 +121,7 @@ export function Dashboard() {
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</main>
|
||||
</section>
|
||||
</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 { useNavigate, Link } from 'react-router-dom'
|
||||
|
||||
interface Diet {
|
||||
id: number
|
||||
@@ -21,7 +20,6 @@ interface UserProfile {
|
||||
}
|
||||
|
||||
export function ProfilePage() {
|
||||
const navigate = useNavigate()
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
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)
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch current user and diets
|
||||
// Fetch current user and diets (Layout already ensures auth)
|
||||
Promise.all([
|
||||
fetch('/api/auth/me', { credentials: 'include' }).then(r => r.json()),
|
||||
fetch('/api/diets', { credentials: 'include' }).then(r => r.json()),
|
||||
])
|
||||
.then(([authData, dietsData]) => {
|
||||
if (!authData.user) {
|
||||
navigate('/login')
|
||||
return
|
||||
}
|
||||
if (!authData.user) return
|
||||
setDiets(dietsData)
|
||||
|
||||
// Now fetch full user profile
|
||||
// Fetch full user profile
|
||||
return fetch(`/api/users/${authData.user.id}`, { credentials: 'include' })
|
||||
.then(r => r.json())
|
||||
.then((profile: UserProfile) => {
|
||||
@@ -68,9 +63,8 @@ export function ProfilePage() {
|
||||
setDietId(diet?.id || null)
|
||||
})
|
||||
})
|
||||
.catch(() => navigate('/login'))
|
||||
.finally(() => setLoading(false))
|
||||
}, [navigate])
|
||||
}, [])
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
@@ -108,14 +102,14 @@ export function ProfilePage() {
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <div style={{ padding: 'var(--space-lg)' }}>Loading...</div>
|
||||
return <div className="page-loading">Loading...</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: 'var(--space-lg)', maxWidth: '600px', margin: '0 auto' }}>
|
||||
<header style={{ marginBottom: 'var(--space-xl)' }}>
|
||||
<Link to="/" className="text-secondary text-sm">← Back to Dashboard</Link>
|
||||
<h1 style={{ marginTop: 'var(--space-sm)' }}>Profile</h1>
|
||||
<div className="page" style={{ maxWidth: '600px' }}>
|
||||
<header className="page-header">
|
||||
<h1>Profile</h1>
|
||||
<p className="text-secondary">Manage your account and health information</p>
|
||||
</header>
|
||||
|
||||
<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