init project

This commit is contained in:
root
2026-04-23 00:21:02 -04:00
parent 23826c5528
commit 9191fd32f0
64 changed files with 10229 additions and 0 deletions
+194
View File
@@ -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>
)
}
+92
View File
@@ -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>
)
}