fix teacher, parent and admin pages
This commit is contained in:
+102
-29
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user