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

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