167 lines
6.5 KiB
TypeScript
167 lines
6.5 KiB
TypeScript
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
|
|
avatar_url: string | null
|
|
}
|
|
|
|
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: '/icons/healthcare/icons8-powerchart-50.png' },
|
|
{ path: '/profile', label: 'Profile', icon: '/icons/general/icons8-user-50.png' },
|
|
{ path: '/insights', label: 'Insights', icon: '/icons/general/icons8-idea-50.png', disabled: true },
|
|
{ path: '/sources', label: 'Sources', icon: '/icons/general/icons8-document-50.png' },
|
|
]
|
|
|
|
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">
|
|
<img src={item.icon} alt={item.label} />
|
|
</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">
|
|
<img src="/icons/user/icons8-add-user-group-woman-man-50.png" alt="Admin" />
|
|
</span>
|
|
<span className="sidebar-label">Manage Users</span>
|
|
</Link>
|
|
</div>
|
|
)}
|
|
|
|
<div className="sidebar-footer">
|
|
<div className="sidebar-user">
|
|
<div className="sidebar-avatar">
|
|
{user.avatar_url ? (
|
|
<img src={user.avatar_url} alt={displayName} className="avatar-img" />
|
|
) : (
|
|
<div className="avatar-placeholder">{displayName[0].toUpperCase()}</div>
|
|
)}
|
|
</div>
|
|
<div className="sidebar-user-info">
|
|
<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>
|
|
<div className="sidebar-actions">
|
|
<button
|
|
className="sidebar-btn"
|
|
onClick={toggleTheme}
|
|
title={theme === 'dark' ? 'Light mode' : 'Dark mode'}
|
|
>
|
|
{theme === 'dark' ? (
|
|
<img src="/icons/general/icons8-sun-50.png" alt="Light" className="theme-icon" />
|
|
) : (
|
|
<img src="/icons/general/icons8-waxing-crescent-50.png" alt="Dark" className="theme-icon" />
|
|
)}
|
|
</button>
|
|
<button
|
|
className="sidebar-btn"
|
|
onClick={handleLogout}
|
|
title="Logout"
|
|
>
|
|
<img src="/icons/general/icons8-cancel-50.png" alt="Logout" className="theme-icon" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
{/* Main Content */}
|
|
<main className="main-content">
|
|
{children}
|
|
</main>
|
|
</div>
|
|
)
|
|
}
|