fix teacher, parent and admin pages

This commit is contained in:
root
2026-04-25 00:00:10 -04:00
parent 7fe34dde0d
commit 3e77fc92c7
275 changed files with 46412 additions and 3325 deletions
+102 -29
View File
@@ -1,5 +1,5 @@
import { useEffect, useState, type ReactNode } from 'react'
import { Link, NavLink, Outlet, useNavigate } from 'react-router-dom'
import { useEffect, useMemo, useState, type ReactNode } from 'react'
import { Link, NavLink, Outlet, useLocation, useNavigate } from 'react-router-dom'
import { fetchNavMenu } from '../api/session'
import type { NavItem } from '../api/types'
import { useAuth } from '../auth/AuthProvider'
@@ -9,31 +9,55 @@ import {
} from '../lib/ciSpaPaths'
function normalizeNavItems(payload: unknown): NavItem[] {
if (Array.isArray(payload)) {
return payload as NavItem[]
const rawItems = Array.isArray(payload)
? (payload as NavItem[])
: payload &&
typeof payload === 'object' &&
'data' in payload &&
Array.isArray((payload as { data: unknown }).data)
? ((payload as { data: NavItem[] }).data ?? [])
: []
if (rawItems.length === 0) return []
if (rawItems.some((item) => Array.isArray(item.children) && item.children.length > 0)) {
return rawItems
}
if (
payload &&
typeof payload === 'object' &&
'data' in payload &&
Array.isArray((payload as { data: unknown }).data)
) {
return (payload as { data: NavItem[] }).data
const byId = new Map<number, NavItem>()
for (const item of rawItems) {
byId.set(item.id, { ...item, children: [] })
}
return []
const tree: NavItem[] = []
for (const item of byId.values()) {
const parentId = item.menu_parent_id ?? null
if (parentId != null && byId.has(parentId)) {
byId.get(parentId)?.children.push(item)
} else {
tree.push(item)
}
}
return tree
}
function NavTree({ items }: { items: NavItem[] }) {
function branchContainsPath(item: NavItem, pathname: string): boolean {
const spa = item.url ? spaPathFromCiUrl(item.url) : null
if (spa && pathname.startsWith(spa)) return true
return (item.children ?? []).some((child) => branchContainsPath(child, pathname))
}
function NavTree({ items, pathname }: { items: NavItem[]; pathname: string }) {
return (
<ul className="nav flex-column gap-1 ps-0 mb-0 list-unstyled">
<ul className="nav flex-column gap-1 ps-0 mb-0 list-unstyled mgmt-nav-tree">
{items.map((item) => (
<NavBranch key={item.id} item={item} depth={0} />
<NavBranch key={item.id} item={item} depth={0} pathname={pathname} />
))}
</ul>
)
}
function NavBranch({ item, depth }: { item: NavItem; depth: number }) {
function NavBranch({ item, depth, pathname }: { item: NavItem; depth: number; pathname: string }) {
const enabled = item.is_enabled !== 0
if (!enabled) return null
@@ -47,10 +71,26 @@ function NavBranch({ item, depth }: { item: NavItem; depth: number }) {
<i className={`${item.icon_class} me-2`} aria-hidden />
) : null
const children = (item.children ?? []).filter((c) => c.is_enabled !== 0)
const hasChildren = children.length > 0
const [open, setOpen] = useState(() => hasChildren && branchContainsPath(item, pathname))
useEffect(() => {
if (hasChildren && branchContainsPath(item, pathname)) {
setOpen(true)
}
}, [hasChildren, item, pathname])
const linkInner = (
<>
{icon}
<span className="nav-label-chip">{label}</span>
{hasChildren ? (
<i
className={`bi ${open ? 'bi-chevron-down' : 'bi-chevron-right'} ms-auto`}
aria-hidden
/>
) : null}
</>
)
@@ -58,6 +98,12 @@ function NavBranch({ item, depth }: { item: NavItem; depth: number }) {
`nav-link rounded py-2 px-2 ${isActive ? 'active fw-semibold' : ''}`
let main: ReactNode
const handleBranchToggle = () => {
if (hasChildren) {
setOpen((value) => !value)
}
}
if (external) {
main = (
<a
@@ -66,33 +112,54 @@ function NavBranch({ item, depth }: { item: NavItem; depth: number }) {
target={item.target || '_self'}
rel={item.target === '_blank' ? 'noreferrer noopener' : undefined}
style={{ paddingLeft: pad }}
onClick={hasChildren ? (event) => {
event.preventDefault()
handleBranchToggle()
} : undefined}
>
{linkInner}
</a>
)
} else if (spa) {
} else if (spa && !hasChildren) {
main = (
<NavLink end to={spa} className={linkClass} style={{ paddingLeft: pad }}>
{linkInner}
</NavLink>
)
} else if (spa && hasChildren) {
main = (
<button
type="button"
className="nav-link rounded py-2 px-2 w-100 text-start d-flex align-items-center"
style={{ paddingLeft: pad }}
aria-expanded={open}
onClick={handleBranchToggle}
>
{linkInner}
</button>
)
} else {
main = (
<span className="nav-link disabled" style={{ paddingLeft: pad }}>
<button
type="button"
className="nav-link rounded py-2 px-2 w-100 text-start d-flex align-items-center border-0 bg-transparent"
style={{ paddingLeft: pad }}
aria-expanded={hasChildren ? open : undefined}
onClick={hasChildren ? handleBranchToggle : undefined}
disabled={!hasChildren}
>
{linkInner}
</span>
</button>
)
}
const children = (item.children ?? []).filter((c) => c.is_enabled !== 0)
return (
<li>
<li className={`mgmt-nav-branch mgmt-nav-depth-${Math.min(depth, 4)}`}>
{main}
{children.length > 0 ? (
<ul className="nav flex-column gap-1 ps-0 mb-0 mt-1 list-unstyled">
{children.length > 0 && open ? (
<ul className="nav flex-column gap-1 ps-0 mb-0 mt-1 list-unstyled mgmt-nav-subtree">
{children.map((ch) => (
<NavBranch key={ch.id} item={ch} depth={depth + 1} />
<NavBranch key={ch.id} item={ch} depth={depth + 1} pathname={pathname} />
))}
</ul>
) : null}
@@ -103,6 +170,7 @@ function NavBranch({ item, depth }: { item: NavItem; depth: number }) {
export function ManagementLayout() {
const { user, logout } = useAuth()
const navigate = useNavigate()
const location = useLocation()
const [items, setItems] = useState<NavItem[]>([])
const [menuError, setMenuError] = useState<string | null>(null)
const [sidebarOpen, setSidebarOpen] = useState(false)
@@ -129,6 +197,8 @@ export function ManagementLayout() {
}
}, [])
const navTree = useMemo(() => normalizeNavItems(items), [items])
return (
<div className={`management-app d-flex flex-column min-vh-100 ${sidebarOpen ? 'mgmt-sidebar-open' : ''}`}>
<header className="navbar navbar-dark sticky-top border-bottom shadow-sm px-3 py-2">
@@ -175,12 +245,15 @@ export function ManagementLayout() {
<div className="wrapper flex-grow-1">
<aside id="navbarManagement" className="mgmt-sidebar" data-mgmt-menu-mode="dark">
<div className="mgmt-sidebar-content">
<div className="px-2 py-2 small text-muted border-bottom mb-2">Navigation</div>
<div className="mgmt-sidebar-header px-2 py-2 mb-2">
<div className="mgmt-sidebar-eyebrow">Management</div>
<div className="mgmt-sidebar-title">Navigation</div>
</div>
<nav className="pb-3">
<NavLink
to="/app/home"
className={({ isActive }) =>
`nav-link rounded mb-2 ${isActive ? 'active fw-semibold' : ''}`
`nav-link rounded mb-2 mgmt-static-link ${isActive ? 'active fw-semibold' : ''}`
}
>
<i className="bi bi-house-door me-2" aria-hidden />
@@ -189,7 +262,7 @@ export function ManagementLayout() {
<NavLink
to="/app/browse"
className={({ isActive }) =>
`nav-link rounded mb-2 ${isActive ? 'active fw-semibold' : ''}`
`nav-link rounded mb-2 mgmt-static-link ${isActive ? 'active fw-semibold' : ''}`
}
>
<i className="bi bi-diagram-3 me-2" aria-hidden />
@@ -198,7 +271,7 @@ export function ManagementLayout() {
{menuError ? (
<div className="alert alert-warning small py-2">{menuError}</div>
) : (
<NavTree items={items} />
<NavTree items={navTree} pathname={location.pathname} />
)}
</nav>
</div>