113 lines
4.5 KiB
TypeScript

import { NavLink, Outlet } from 'react-router'
import { useEffect } from 'react'
import { currentUser, getAccessToken } from '../auth/keycloak'
import { LEARNER_NAV_ITEMS, hasAnyRole, hasLearnerPortalAccess } from '../learner-portal'
export function LearnerLayout() {
const user = currentUser()
const roles = user.roles ?? []
useEffect(() => {
// Re-enter bootstrap auth flow when the token is missing.
if (!getAccessToken()) {
window.location.reload()
}
}, [])
if (!hasLearnerPortalAccess(roles)) {
return (
<main className="mx-auto max-w-3xl px-4 py-14">
<h1 className="text-3xl font-semibold text-stone-900">Learner Portal Access Restricted</h1>
<p className="mt-2 text-sm text-stone-600">
Your account is authenticated but does not currently have learner portal access.
</p>
</main>
)
}
return (
<main data-testid="learner-layout" className="min-h-screen bg-gradient-to-b from-white via-slate-50 to-sky-50 text-slate-900">
<div className="mx-auto grid max-w-7xl gap-6 px-4 py-8 md:grid-cols-[260px_1fr] md:px-6 md:py-10">
<aside className="h-fit rounded-[24px] border border-stone-200/80 bg-white/90 p-4 shadow-[0_16px_40px_rgba(65,48,31,0.07)] backdrop-blur">
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-slate-500">Learner Area</p>
<p className="mt-1 text-sm text-stone-700">{user.username || user.email || 'Authenticated user'}</p>
<nav data-testid="learner-nav-mobile" className="mt-4 flex gap-2 overflow-x-auto pb-1 md:hidden" aria-label="Learner mobile navigation">
{LEARNER_NAV_ITEMS.map(item => {
const allowed = !item.minAnyRoles || hasAnyRole(roles, item.minAnyRoles)
if (!allowed) {
return (
<span
key={`mobile-${item.path}`}
className="inline-flex shrink-0 rounded-lg border border-slate-200 bg-stone-50 px-3 py-2 text-xs text-stone-400"
aria-disabled="true"
>
{item.label}
</span>
)
}
return (
<NavLink
key={`mobile-${item.path}`}
to={item.path}
end={item.path === '/learn'}
data-testid={`learner-nav-mobile-${item.path.replace(/^\//, '').replace(/\//g, '-') || 'home'}`}
className={({ isActive }) =>
`inline-flex shrink-0 rounded-lg px-3 py-2 text-xs font-medium transition ${
isActive
? 'bg-indigo-50 text-indigo-800'
: 'text-slate-700 hover:bg-stone-100 hover:text-stone-900'
}`
}
>
{item.label}
</NavLink>
)
})}
</nav>
<nav data-testid="learner-nav-desktop" className="mt-4 hidden space-y-1 md:block" aria-label="Learner desktop navigation">
{LEARNER_NAV_ITEMS.map(item => {
const allowed = !item.minAnyRoles || hasAnyRole(roles, item.minAnyRoles)
if (!allowed) {
return (
<span
key={item.path}
className="block rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm text-slate-400"
aria-disabled="true"
>
{item.label} (restricted)
</span>
)
}
return (
<NavLink
key={item.path}
to={item.path}
end={item.path === '/learn'}
data-testid={`learner-nav-desktop-${item.path.replace(/^\//, '').replace(/\//g, '-') || 'home'}`}
className={({ isActive }) =>
`block rounded-lg px-3 py-2 text-sm font-medium transition ${
isActive
? 'bg-sky-100 text-sky-800'
: 'text-slate-700 hover:bg-slate-100 hover:text-slate-900'
}`
}
>
{item.label}
</NavLink>
)
})}
</nav>
</aside>
<section data-testid="learner-layout-content" className="rounded-2xl border border-slate-200 bg-white p-4 shadow-sm md:p-6">
<Outlet />
</section>
</div>
</main>
)
}