From e558e19512e391bc49aaebae2f1067d451d90609 Mon Sep 17 00:00:00 2001 From: abhishekbhakat Date: Fri, 19 Dec 2025 21:25:23 +0530 Subject: [PATCH] feat: Introduce a shared Layout component, add new Insights and Sources pages, and refactor Dashboard and Profile pages to integrate with the new layout. --- frontend/src/App.tsx | 18 ++- frontend/src/components/Layout.tsx | 148 +++++++++++++++++++++++ frontend/src/index.css | 182 +++++++++++++++++++++++++++++ frontend/src/pages/Dashboard.tsx | 125 ++------------------ frontend/src/pages/Insights.tsx | 18 +++ frontend/src/pages/Profile.tsx | 24 ++-- frontend/src/pages/Sources.tsx | 41 +++++++ 7 files changed, 424 insertions(+), 132 deletions(-) create mode 100644 frontend/src/components/Layout.tsx create mode 100644 frontend/src/pages/Insights.tsx create mode 100644 frontend/src/pages/Sources.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index cd84e1d..753d53a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,19 +1,29 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' import { LoginPage } from './pages/Login' import { SignupPage } from './pages/Signup' -import { Dashboard } from './pages/Dashboard' +import { DashboardPage } from './pages/Dashboard' import { ProfilePage } from './pages/Profile' +import { InsightsPage } from './pages/Insights' +import { SourcesPage } from './pages/Sources' import { AdminPage } from './pages/Admin' +import { Layout } from './components/Layout' function App() { return ( + {/* Public routes */} } /> } /> - } /> - } /> - } /> + + {/* Protected routes with Layout */} + } /> + } /> + } /> + } /> + } /> + + {/* Fallback */} } /> diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx new file mode 100644 index 0000000..2eaafb7 --- /dev/null +++ b/frontend/src/components/Layout.tsx @@ -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(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
Loading...
+ } + + 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 ( +
+ {/* Sidebar */} + + + {/* Main Content */} +
+ {children} +
+
+ ) +} diff --git a/frontend/src/index.css b/frontend/src/index.css index 3341371..d62e6d6 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -420,4 +420,186 @@ select.input { .biomarker-unit { color: var(--text-secondary); 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); } \ No newline at end of file diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 4569e52..005acfe 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -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(null) - const [loading, setLoading] = useState(true) - const [theme, setTheme] = useState(() => - document.documentElement.getAttribute('data-theme') || 'light' - ) +export function DashboardPage() { const [categories, setCategories] = useState([]) const [biomarkers, setBiomarkers] = useState([]) const [expandedCategories, setExpandedCategories] = useState>(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
Loading...
+ return
Loading biomarkers...
} - if (!user) { - return null - } - - // Display name or username fallback - const displayName = user.name || user.username - return ( -
-
-
- zhealth -

zhealth

-
-
- - -
+
+
+

Dashboard

+

View all biomarker categories and their reference markers

-
- {/* User Welcome Card */} -
-
-
-

Welcome, {displayName}

-

- Role: {user.role} -

-
- - Edit Profile - -
-
- - {/* Admin Section */} - {user.role === 'admin' && ( -
-

Admin

-

- Manage users and system settings -

- - Manage Users - -
- )} - - {/* Biomarker Categories */} -

Biomarkers

+
+

Biomarker Categories

{categories.map(category => { const categoryBiomarkers = getBiomarkersForCategory(category.id) @@ -167,7 +68,6 @@ export function Dashboard() { return (
- {/* Category Header - Clickable */} - {/* Biomarkers List - Collapsible */} {isExpanded && (
{categoryBiomarkers.length === 0 ? ( @@ -222,7 +121,7 @@ export function Dashboard() { ) })}
-
+
) } diff --git a/frontend/src/pages/Insights.tsx b/frontend/src/pages/Insights.tsx new file mode 100644 index 0000000..c187bce --- /dev/null +++ b/frontend/src/pages/Insights.tsx @@ -0,0 +1,18 @@ +export function InsightsPage() { + return ( +
+
+

Insights

+

AI-powered analysis of your health data

+
+ +
+
🚀
+

Coming Soon

+

+ AI-generated health insights and recommendations based on your biomarker data. +

+
+
+ ) +} diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx index a2916b7..4c70117 100644 --- a/frontend/src/pages/Profile.tsx +++ b/frontend/src/pages/Profile.tsx @@ -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(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
Loading...
+ return
Loading...
} return ( -
-
- ← Back to Dashboard -

Profile

+
+
+

Profile

+

Manage your account and health information

diff --git a/frontend/src/pages/Sources.tsx b/frontend/src/pages/Sources.tsx new file mode 100644 index 0000000..edcacc0 --- /dev/null +++ b/frontend/src/pages/Sources.tsx @@ -0,0 +1,41 @@ +export function SourcesPage() { + return ( +
+
+

Sources

+

Upload and manage your health data sources

+
+ +
+

Upload Data

+

+ Upload lab reports in PDF, CSV, or Excel format to import your biomarker data. +

+ +
+
📤
+

+ Drag & drop files here, or click to browse +

+

+ Supported: PDF, CSV, XLSX +

+
+
+ +
+

Recent Uploads

+

No files uploaded yet.

+
+
+ ) +}