init project
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
import { useEffect, useState, type ReactNode } from 'react'
|
||||
import { Link, NavLink, Outlet, useNavigate } from 'react-router-dom'
|
||||
import { fetchNavMenu } from '../api/session'
|
||||
import type { NavItem } from '../api/types'
|
||||
import { useAuth } from '../auth/AuthProvider'
|
||||
import {
|
||||
isExternalNavUrl,
|
||||
spaPathFromCiUrl,
|
||||
} from '../lib/ciSpaPaths'
|
||||
|
||||
function normalizeNavItems(payload: unknown): NavItem[] {
|
||||
if (Array.isArray(payload)) {
|
||||
return payload as NavItem[]
|
||||
}
|
||||
if (
|
||||
payload &&
|
||||
typeof payload === 'object' &&
|
||||
'data' in payload &&
|
||||
Array.isArray((payload as { data: unknown }).data)
|
||||
) {
|
||||
return (payload as { data: NavItem[] }).data
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
function NavTree({ items }: { items: NavItem[] }) {
|
||||
return (
|
||||
<ul className="nav flex-column gap-1 ps-0 mb-0 list-unstyled">
|
||||
{items.map((item) => (
|
||||
<NavBranch key={item.id} item={item} depth={0} />
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
function NavBranch({ item, depth }: { item: NavItem; depth: number }) {
|
||||
const enabled = item.is_enabled !== 0
|
||||
if (!enabled) return null
|
||||
|
||||
const label = item.label?.trim() || 'Menu'
|
||||
const url = item.url?.trim() || ''
|
||||
const external = isExternalNavUrl(url)
|
||||
const spa = spaPathFromCiUrl(url)
|
||||
const pad = 8 + depth * 10
|
||||
|
||||
const icon = item.icon_class ? (
|
||||
<i className={`${item.icon_class} me-2`} aria-hidden />
|
||||
) : null
|
||||
|
||||
const linkInner = (
|
||||
<>
|
||||
{icon}
|
||||
{label}
|
||||
</>
|
||||
)
|
||||
|
||||
const linkClass = ({ isActive }: { isActive: boolean }) =>
|
||||
`nav-link rounded py-2 px-2 ${isActive ? 'active fw-semibold' : ''}`
|
||||
|
||||
let main: ReactNode
|
||||
if (external) {
|
||||
main = (
|
||||
<a
|
||||
className="nav-link rounded py-2 px-2"
|
||||
href={url}
|
||||
target={item.target || '_self'}
|
||||
rel={item.target === '_blank' ? 'noreferrer noopener' : undefined}
|
||||
style={{ paddingLeft: pad }}
|
||||
>
|
||||
{linkInner}
|
||||
</a>
|
||||
)
|
||||
} else if (spa) {
|
||||
main = (
|
||||
<NavLink end to={spa} className={linkClass} style={{ paddingLeft: pad }}>
|
||||
{linkInner}
|
||||
</NavLink>
|
||||
)
|
||||
} else {
|
||||
main = (
|
||||
<span className="nav-link disabled" style={{ paddingLeft: pad }}>
|
||||
{linkInner}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const children = (item.children ?? []).filter((c) => c.is_enabled !== 0)
|
||||
|
||||
return (
|
||||
<li>
|
||||
{main}
|
||||
{children.length > 0 ? (
|
||||
<div className="mt-1">
|
||||
{children.map((ch) => (
|
||||
<NavBranch key={ch.id} item={ch} depth={depth + 1} />
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
export function ManagementLayout() {
|
||||
const { user, logout } = useAuth()
|
||||
const navigate = useNavigate()
|
||||
const [items, setItems] = useState<NavItem[]>([])
|
||||
const [menuError, setMenuError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
;(async () => {
|
||||
try {
|
||||
const res = await fetchNavMenu()
|
||||
const list = res.data?.items
|
||||
if (!cancelled && list != null) {
|
||||
setItems(normalizeNavItems(list))
|
||||
setMenuError(null)
|
||||
}
|
||||
} catch (e) {
|
||||
if (!cancelled) {
|
||||
setItems([])
|
||||
setMenuError(e instanceof Error ? e.message : 'Unable to load menu.')
|
||||
}
|
||||
}
|
||||
})()
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="management-app d-flex flex-column min-vh-100">
|
||||
<header className="navbar navbar-dark border-bottom shadow-sm px-3 py-2">
|
||||
<div className="container-fluid px-2 d-flex flex-wrap align-items-center justify-content-between gap-2">
|
||||
<Link className="navbar-brand fw-semibold mb-0" to="/app/home">
|
||||
Al Rahma Sunday School
|
||||
</Link>
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<span className="navbar-text small mb-0">{user?.name}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-light btn-sm"
|
||||
onClick={() => {
|
||||
logout()
|
||||
navigate('/login', { replace: true })
|
||||
}}
|
||||
>
|
||||
Log out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="container-fluid flex-grow-1 py-3">
|
||||
<div className="row g-3">
|
||||
<aside className="col-12 col-md-3 col-xl-2">
|
||||
<div className="border rounded bg-white shadow-sm">
|
||||
<div className="p-3 small text-muted border-bottom">Navigation</div>
|
||||
<nav className="p-2 pb-3">
|
||||
<NavLink
|
||||
to="/app/home"
|
||||
className={({ isActive }) =>
|
||||
`nav-link rounded mb-2 ${isActive ? 'active fw-semibold' : ''}`
|
||||
}
|
||||
>
|
||||
<i className="bi bi-house-door me-2" aria-hidden />
|
||||
Home
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/app/browse"
|
||||
className={({ isActive }) =>
|
||||
`nav-link rounded mb-2 ${isActive ? 'active fw-semibold' : ''}`
|
||||
}
|
||||
>
|
||||
<i className="bi bi-diagram-3 me-2" aria-hidden />
|
||||
CI views
|
||||
</NavLink>
|
||||
{menuError ? (
|
||||
<div className="alert alert-warning small py-2">{menuError}</div>
|
||||
) : (
|
||||
<NavTree items={items} />
|
||||
)}
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main className="col-12 col-md-9 col-xl-10 management-main">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { Link, Outlet } from 'react-router-dom'
|
||||
import { useAuth } from '../auth/AuthProvider'
|
||||
|
||||
export function PublicLayout() {
|
||||
const { isAuthenticated, user, logout } = useAuth()
|
||||
|
||||
return (
|
||||
<div className="d-flex flex-column min-vh-100 public-shell">
|
||||
<nav
|
||||
className="navbar navbar-expand-lg navbar-dark shadow-sm"
|
||||
style={{ backgroundColor: '#2c9b09' }}
|
||||
>
|
||||
<div className="container">
|
||||
<Link className="navbar-brand fw-semibold" to="/">
|
||||
Al Rahma Sunday School
|
||||
</Link>
|
||||
<button
|
||||
className="navbar-toggler"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#publicNav"
|
||||
aria-controls="publicNav"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
>
|
||||
<span className="navbar-toggler-icon" />
|
||||
</button>
|
||||
<div className="collapse navbar-collapse justify-content-end" id="publicNav">
|
||||
<ul className="navbar-nav align-items-lg-center gap-lg-2">
|
||||
<li className="nav-item">
|
||||
<Link className="nav-link" to="/">
|
||||
Home
|
||||
</Link>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<Link className="nav-link" to="/docs">
|
||||
API docs
|
||||
</Link>
|
||||
</li>
|
||||
{isAuthenticated ? (
|
||||
<>
|
||||
<li className="nav-item">
|
||||
<span className="navbar-text small px-2">{user?.name}</span>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<Link className="btn btn-outline-light btn-sm" to="/app/home">
|
||||
Portal
|
||||
</Link>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-link nav-link text-white text-decoration-none"
|
||||
onClick={() => {
|
||||
logout()
|
||||
}}
|
||||
>
|
||||
Sign out
|
||||
</button>
|
||||
</li>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<li className="nav-item">
|
||||
<Link className="nav-link" to="/login">
|
||||
Sign in
|
||||
</Link>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<Link className="btn btn-outline-light btn-sm" to="/register">
|
||||
Register
|
||||
</Link>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main className="flex-grow-1 d-flex flex-column">
|
||||
<Outlet />
|
||||
</main>
|
||||
|
||||
<footer className="border-top bg-white py-4 mt-auto">
|
||||
<div className="container text-center text-muted small">
|
||||
<p className="mb-0">Al Rahma Sunday School</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user