fix teacher, parent and admin pages
This commit is contained in:
+286
-36
@@ -1,6 +1,9 @@
|
||||
import { useState } from 'react'
|
||||
import { Link, NavLink, Outlet, useNavigate } from 'react-router-dom'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Link, NavLink, Outlet, useLocation, useNavigate } from 'react-router-dom'
|
||||
import { useAuth } from '../auth/AuthProvider'
|
||||
import { CiPublicFooter } from '../components/CiPartials'
|
||||
import { readPortalStylePrefs, resolvePortalColorMode, type PortalStylePrefs } from '../lib/portalAppearance'
|
||||
import { isParentPortalRole, isTeacherPortalRole } from '../lib/portalRoles'
|
||||
|
||||
const parentNavItems = [
|
||||
{ to: '/app/parent/home', icon: 'bi bi-house-door', label: 'Dashboard' },
|
||||
@@ -16,20 +19,152 @@ const parentNavItems = [
|
||||
{ to: '/app/parent/scores', icon: 'bi bi-clipboard-data', label: 'Scores' },
|
||||
] as const
|
||||
|
||||
/** Teacher top shortcuts (aligned with legacy teacher navbar). */
|
||||
const teacherNavItems = [
|
||||
{ to: '/app/teacher_dashboard', icon: 'bi bi-house-door', label: 'Dashboard' },
|
||||
{ to: '/app/teacher/showupdate-attendance', icon: 'bi bi-calendar-check', label: 'Attendance' },
|
||||
{ to: '/app/teacher/inventory/book-distribute', icon: 'bi bi-book', label: 'Books' },
|
||||
{ to: '/app/teacher/calendar', icon: 'bi bi-calendar2-week', label: 'Calendar' },
|
||||
{ to: '/app/teacher/class-view', icon: 'bi bi-pencil-square', label: 'Class' },
|
||||
{ to: '/app/teacher/class-progress-submit', icon: 'bi bi-card-checklist', label: 'Class Progress' },
|
||||
{ to: '/app/teacher/competition-scores', icon: 'bi bi-trophy', label: 'Competitions' },
|
||||
{ to: '/app/teacher/exam-drafts', icon: 'bi bi-file-earmark-text', label: 'Exam Drafts' },
|
||||
{ to: '/app/teacher/print-requests', icon: 'bi bi-printer', label: 'Print' },
|
||||
{ to: '/app/student/score-card/list', icon: 'bi bi-card-text', label: 'Score Card' },
|
||||
{ to: '/app/teacher/scores', icon: 'bi bi-bar-chart-line', label: 'Scores' },
|
||||
{ to: '/app/teacher/absence-vacation', icon: 'bi bi-megaphone', label: 'TimeOff' },
|
||||
] as const
|
||||
|
||||
function initialsFromName(name: string | undefined | null): string {
|
||||
const t = name?.trim()
|
||||
if (!t) return '?'
|
||||
const parts = t.split(/\s+/).filter(Boolean)
|
||||
if (parts.length >= 2) {
|
||||
const a = parts[0][0] ?? ''
|
||||
const b = parts[parts.length - 1][0] ?? ''
|
||||
return (a + b).toUpperCase()
|
||||
}
|
||||
if (parts[0].length >= 2) return parts[0].slice(0, 2).toUpperCase()
|
||||
return (parts[0][0] ?? '?').toUpperCase()
|
||||
}
|
||||
|
||||
function formatRoleLabel(slug: string): string {
|
||||
const t = slug.trim()
|
||||
if (!t) return ''
|
||||
return t.replace(/_/g, ' ').replace(/\b\w/g, (letter) => letter.toUpperCase())
|
||||
}
|
||||
|
||||
export function MainLayout() {
|
||||
const { user, logout, selectedRole, roles } = useAuth()
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const [navOpen, setNavOpen] = useState(false)
|
||||
const [portalStyle, setPortalStyle] = useState<PortalStylePrefs>(readPortalStylePrefs)
|
||||
const [osPrefersDark, setOsPrefersDark] = useState(() =>
|
||||
typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: dark)').matches,
|
||||
)
|
||||
const [userMenuOpen, setUserMenuOpen] = useState(false)
|
||||
const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const clusterRef = useRef<HTMLDivElement>(null)
|
||||
const userMenuTriggerRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
const clearCloseTimer = useCallback(() => {
|
||||
if (closeTimerRef.current) {
|
||||
clearTimeout(closeTimerRef.current)
|
||||
closeTimerRef.current = null
|
||||
}
|
||||
}, [])
|
||||
|
||||
const openUserMenu = useCallback(() => {
|
||||
clearCloseTimer()
|
||||
setUserMenuOpen(true)
|
||||
}, [clearCloseTimer])
|
||||
|
||||
const closeUserMenu = useCallback(() => {
|
||||
clearCloseTimer()
|
||||
const root = clusterRef.current
|
||||
if (root?.contains(document.activeElement)) {
|
||||
userMenuTriggerRef.current?.focus()
|
||||
}
|
||||
setUserMenuOpen(false)
|
||||
}, [clearCloseTimer])
|
||||
|
||||
const scheduleCloseUserMenu = useCallback(() => {
|
||||
clearCloseTimer()
|
||||
closeTimerRef.current = setTimeout(() => {
|
||||
const root = clusterRef.current
|
||||
if (root?.contains(document.activeElement)) {
|
||||
closeTimerRef.current = null
|
||||
return
|
||||
}
|
||||
closeUserMenu()
|
||||
closeTimerRef.current = null
|
||||
}, 200)
|
||||
}, [clearCloseTimer, closeUserMenu])
|
||||
|
||||
useEffect(() => () => clearCloseTimer(), [clearCloseTimer])
|
||||
|
||||
const onClusterBlurCapture = useCallback(() => {
|
||||
window.setTimeout(() => {
|
||||
const root = clusterRef.current
|
||||
if (!root || !root.contains(document.activeElement)) {
|
||||
closeUserMenu()
|
||||
}
|
||||
}, 0)
|
||||
}, [closeUserMenu])
|
||||
|
||||
const onAvatarClick = useCallback(() => {
|
||||
if (typeof window === 'undefined') return
|
||||
if (window.matchMedia('(hover: none) and (pointer: coarse)').matches) {
|
||||
setUserMenuOpen((open) => !open)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const closeMenus = useCallback(() => {
|
||||
closeUserMenu()
|
||||
setNavOpen(false)
|
||||
}, [closeUserMenu])
|
||||
|
||||
useEffect(() => {
|
||||
const sync = () => setPortalStyle(readPortalStylePrefs())
|
||||
window.addEventListener('alrahma-portal-style', sync)
|
||||
return () => window.removeEventListener('alrahma-portal-style', sync)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const mq = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
const onChange = () => setOsPrefersDark(mq.matches)
|
||||
mq.addEventListener('change', onChange)
|
||||
return () => mq.removeEventListener('change', onChange)
|
||||
}, [])
|
||||
|
||||
const role = selectedRole ?? roles[0] ?? ''
|
||||
const canSwitchRole = roles.length > 1
|
||||
const dashboardPath =
|
||||
role === 'parent'
|
||||
isParentPortalRole(role)
|
||||
? '/app/parent/home'
|
||||
: role === 'teacher' || role === 'teacher_assistant'
|
||||
? '/app/teacher/dashboard'
|
||||
: isTeacherPortalRole(role)
|
||||
? '/app/teacher_dashboard'
|
||||
: '/app/home'
|
||||
|
||||
const mainNavItems = isParentPortalRole(role)
|
||||
? parentNavItems
|
||||
: isTeacherPortalRole(role)
|
||||
? teacherNavItems
|
||||
: []
|
||||
|
||||
const initials = initialsFromName(user?.name)
|
||||
const resolvedColorMode = resolvePortalColorMode(portalStyle, osPrefersDark)
|
||||
|
||||
return (
|
||||
<div className="main-layout-app d-flex flex-column min-vh-100">
|
||||
<div
|
||||
className="main-layout-app d-flex flex-column min-vh-100"
|
||||
data-portal-density={portalStyle.density}
|
||||
data-portal-font-scale={portalStyle.fontScale}
|
||||
data-portal-font-family={portalStyle.fontFamily}
|
||||
data-portal-accent={portalStyle.accent}
|
||||
data-portal-color-mode={resolvedColorMode}
|
||||
>
|
||||
<header className="navbar navbar-expand-lg custom-navbar sticky-top shadow-sm px-3">
|
||||
<div className="container-fluid">
|
||||
<Link className="navbar-brand fw-semibold d-inline-flex align-items-center gap-2" to={dashboardPath}>
|
||||
@@ -47,32 +182,152 @@ export function MainLayout() {
|
||||
<span className="navbar-toggler-icon" />
|
||||
</button>
|
||||
<div id="mainLayoutNavbar" className={`collapse navbar-collapse ${navOpen ? 'show' : ''}`}>
|
||||
<nav className="ms-lg-auto me-lg-3 my-3 my-lg-0 main-layout-menu parent-navbar-menu" aria-label="Parent navigation">
|
||||
{parentNavItems.map((item) => (
|
||||
<NavLink
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
className={({ isActive }) => `nav-link parent-navbar-link ${isActive ? 'active fw-semibold' : ''}`}
|
||||
onClick={() => setNavOpen(false)}
|
||||
>
|
||||
<i className={item.icon} aria-hidden />
|
||||
<span>{item.label}</span>
|
||||
</NavLink>
|
||||
))}
|
||||
<nav
|
||||
className="ms-lg-auto me-lg-2 my-2 my-lg-0 main-layout-menu"
|
||||
aria-label={
|
||||
isTeacherPortalRole(role)
|
||||
? 'Teacher portal navigation'
|
||||
: isParentPortalRole(role)
|
||||
? 'Parent portal navigation'
|
||||
: 'Portal navigation'
|
||||
}
|
||||
>
|
||||
<ul className="navbar-nav list-unstyled parent-navbar-menu mb-0">
|
||||
{mainNavItems.map((item) => (
|
||||
<li key={item.to} className="nav-item">
|
||||
<NavLink
|
||||
to={item.to}
|
||||
end={
|
||||
item.to === '/app/teacher_dashboard' ||
|
||||
item.to === '/app/parent/home' ||
|
||||
item.to === '/app/student/score-card/list'
|
||||
}
|
||||
className={({ isActive }) =>
|
||||
`nav-link parent-navbar-link ${isActive ? 'active fw-semibold' : ''}`
|
||||
}
|
||||
onClick={() => setNavOpen(false)}
|
||||
>
|
||||
<i className={item.icon} aria-hidden />
|
||||
<span>{item.label}</span>
|
||||
</NavLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<span className="navbar-text small">{user?.name}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-light btn-sm"
|
||||
onClick={() => {
|
||||
logout()
|
||||
navigate('/login', { replace: true })
|
||||
}}
|
||||
{user ? (
|
||||
<div
|
||||
ref={clusterRef}
|
||||
className="main-layout-user-cluster d-flex align-items-center ms-lg-2"
|
||||
onMouseEnter={openUserMenu}
|
||||
onMouseLeave={scheduleCloseUserMenu}
|
||||
onFocusCapture={openUserMenu}
|
||||
onBlurCapture={onClusterBlurCapture}
|
||||
>
|
||||
Log out
|
||||
</button>
|
||||
</div>
|
||||
<div className="main-layout-user-avatar-wrap">
|
||||
<button
|
||||
ref={userMenuTriggerRef}
|
||||
type="button"
|
||||
className="main-layout-user-trigger"
|
||||
id="main-layout-user-trigger"
|
||||
aria-haspopup="true"
|
||||
aria-expanded={userMenuOpen}
|
||||
aria-controls="main-layout-user-menu"
|
||||
onClick={onAvatarClick}
|
||||
>
|
||||
<div className="main-layout-user-avatar rounded-circle text-white fw-semibold d-flex align-items-center justify-content-center">
|
||||
<span className="main-layout-user-initials">{initials}</span>
|
||||
</div>
|
||||
</button>
|
||||
{canSwitchRole ? (
|
||||
<Link
|
||||
to="/app/select-role"
|
||||
state={{ from: location.pathname }}
|
||||
className="main-layout-user-avatar-chip main-layout-user-avatar-chip--roles"
|
||||
title="Switch role"
|
||||
aria-label="Switch role"
|
||||
onClick={closeMenus}
|
||||
>
|
||||
<i className="bi bi-arrow-left-right" aria-hidden />
|
||||
</Link>
|
||||
) : null}
|
||||
<Link
|
||||
to="/app/portal-style"
|
||||
className="main-layout-user-avatar-chip main-layout-user-avatar-chip--style"
|
||||
title="Style edit"
|
||||
aria-label="Style edit"
|
||||
onClick={closeMenus}
|
||||
>
|
||||
<i className="bi bi-palette" aria-hidden />
|
||||
</Link>
|
||||
</div>
|
||||
<div
|
||||
id="main-layout-user-menu"
|
||||
role="region"
|
||||
aria-labelledby="main-layout-user-trigger"
|
||||
aria-hidden={!userMenuOpen}
|
||||
className={`main-layout-user-dropdown card border-0 shadow-sm py-2 ${userMenuOpen ? 'is-open' : ''}`}
|
||||
>
|
||||
<div className="px-3 pb-1">
|
||||
<div className="main-layout-user-dropdown-header">Signed in as</div>
|
||||
<div className="small text-dark text-truncate fw-semibold" title={user.name}>
|
||||
{user.name}
|
||||
</div>
|
||||
{role ? (
|
||||
<>
|
||||
<div className="main-layout-user-dropdown-header mt-2">Role</div>
|
||||
<div
|
||||
className="small text-dark text-truncate fw-semibold main-layout-user-dropdown-role"
|
||||
title={formatRoleLabel(role)}
|
||||
>
|
||||
{formatRoleLabel(role)}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="dropdown-divider mx-2 my-1" />
|
||||
{canSwitchRole ? (
|
||||
<Link
|
||||
className="dropdown-item d-flex align-items-center gap-2 py-2"
|
||||
to="/app/select-role"
|
||||
state={{ from: location.pathname }}
|
||||
onClick={closeMenus}
|
||||
>
|
||||
<i className="bi bi-arrow-left-right" aria-hidden />
|
||||
Switch role
|
||||
</Link>
|
||||
) : null}
|
||||
<Link
|
||||
className="dropdown-item d-flex align-items-center gap-2 py-2"
|
||||
to="/app/portal-style"
|
||||
onClick={closeMenus}
|
||||
>
|
||||
<i className="bi bi-palette" aria-hidden />
|
||||
Style edit
|
||||
</Link>
|
||||
<Link
|
||||
className="dropdown-item d-flex align-items-center gap-2 py-2"
|
||||
to="/app/account"
|
||||
onClick={closeMenus}
|
||||
>
|
||||
<i className="bi bi-gear" aria-hidden />
|
||||
Settings
|
||||
</Link>
|
||||
<div className="dropdown-divider mx-2 my-1" />
|
||||
<button
|
||||
type="button"
|
||||
className="dropdown-item d-flex align-items-center gap-2 py-2 text-danger"
|
||||
onClick={() => {
|
||||
closeMenus()
|
||||
logout()
|
||||
navigate('/login', { replace: true })
|
||||
}}
|
||||
>
|
||||
<i className="bi bi-box-arrow-right" aria-hidden />
|
||||
Log out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -81,12 +336,7 @@ export function MainLayout() {
|
||||
<Outlet />
|
||||
</main>
|
||||
|
||||
<footer className="footer py-4 mt-auto border-top">
|
||||
<div className="container text-muted small d-flex flex-wrap justify-content-between gap-2">
|
||||
<span>Al Rahma Sunday School</span>
|
||||
<span>Parent and Teacher Portal</span>
|
||||
</div>
|
||||
</footer>
|
||||
<CiPublicFooter />
|
||||
|
||||
<div className="modal fade" id="familyCardModal" tabIndex={-1} aria-hidden="true">
|
||||
<div className="modal-dialog modal-xl modal-dialog-scrollable">
|
||||
|
||||
Reference in New Issue
Block a user