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:
2025-12-19 21:25:23 +05:30
parent 10e6d2c58a
commit e558e19512
7 changed files with 424 additions and 132 deletions

View File

@@ -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>
)
}

View 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>
)
}

View File

@@ -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}>

View 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>
)
}