haixunMaster/haixun-backend/web/src/components/Layout.tsx

117 lines
3.9 KiB
TypeScript
Raw Normal View History

2026-06-23 10:10:22 +00:00
import { NavLink, Outlet, useLocation } from 'react-router-dom'
import { navApps } from '../lib/acAssets'
import type { AcAppKey } from '../lib/acAssets'
2026-06-23 09:54:27 +00:00
import { useAuth } from '../auth/AuthContext'
2026-06-23 10:10:22 +00:00
import { AuthTicketIcon, SceneDecor } from './AuthDecor'
import { AcIcon } from './AcIcon'
2026-06-23 09:54:27 +00:00
import { MobileBottomNav } from './MobileBottomNav'
import { ThemeToggle } from './ThemeToggle'
2026-06-23 10:10:22 +00:00
function AppTile({
to,
label,
icon,
end,
matchPrefix,
}: {
to: string
label: string
icon: AcAppKey
end?: boolean
matchPrefix?: string
}) {
const { pathname } = useLocation()
2026-06-23 09:54:27 +00:00
return (
<NavLink
to={to}
end={end}
2026-06-23 10:10:22 +00:00
className={({ isActive }) => {
const prefixActive = matchPrefix ? pathname.startsWith(matchPrefix) : false
const active = isActive || prefixActive
return `ac-app-tile ${active ? 'ac-app-tile--active' : ''}`
}}
2026-06-23 09:54:27 +00:00
>
2026-06-23 10:10:22 +00:00
<AcIcon app={icon} size="md" />
<span>{label}</span>
2026-06-23 09:54:27 +00:00
</NavLink>
)
}
export function Layout() {
const { member, uid, logout } = useAuth()
return (
2026-06-23 10:10:22 +00:00
<div className="hx-scene ac-app-shell flex min-h-screen flex-col">
<SceneDecor />
<header className="ac-app-header">
<div className="ac-app-header-brand">
<AuthTicketIcon className="ac-app-header-icon" />
<div className="min-w-0">
<p className="display-en text-[10px] font-semibold tracking-[0.16em] text-accent uppercase sm:text-[11px]">
Haixun Patrol
</p>
<h1 className="truncate text-lg font-bold leading-snug text-ink sm:text-2xl"></h1>
</div>
2026-06-23 09:54:27 +00:00
</div>
2026-06-23 10:10:22 +00:00
<div className="flex shrink-0 items-center gap-2">
<span className="ac-role-chip hidden px-3 py-1 text-xs sm:inline">
{member?.roles?.[0] || 'member'}
</span>
<ThemeToggle compact />
</div>
</header>
2026-06-23 09:54:27 +00:00
2026-06-23 10:10:22 +00:00
<div className="relative z-10 mx-auto flex w-full max-w-7xl flex-1 flex-col gap-4 p-3 sm:p-4 lg:flex-row lg:gap-5 lg:p-5">
<aside className="ac-pocket-device hidden self-start lg:sticky lg:top-[5.5rem] lg:block">
<div className="ac-pocket-screen">
<div className="ac-pocket-status">
<span className="display-en tracking-[0.14em]">PATROL PAD</span>
<span className="ac-pocket-status-dots" aria-hidden>
<span />
<span />
<span />
</span>
2026-06-23 09:54:27 +00:00
</div>
2026-06-23 10:10:22 +00:00
<div className="ac-pocket-scroll">
<div className="ac-pocket-body">
<nav className="grid grid-cols-2 gap-2.5" aria-label="側欄導覽">
{navApps.map((item) => (
<AppTile
key={item.to}
to={item.to}
label={item.label}
icon={item.icon}
end={'end' in item ? item.end : undefined}
matchPrefix={'matchPrefix' in item ? item.matchPrefix : undefined}
/>
))}
</nav>
<div className="ac-slot ac-pocket-slot mt-4">
<p className="ac-pocket-slot-label"></p>
<div className="ac-pocket-slot-email truncate">{member?.email}</div>
<p className="ac-pocket-slot-uid truncate">{uid || member?.uid}</p>
<button
type="button"
onClick={() => logout()}
className="ac-btn-secondary mt-3 w-full py-2.5 text-[15px] font-semibold"
>
</button>
</div>
</div>
2026-06-23 09:54:27 +00:00
</div>
</div>
2026-06-23 10:10:22 +00:00
</aside>
2026-06-23 09:54:27 +00:00
2026-06-23 10:10:22 +00:00
<main className="auth-ticket ac-app-main-panel layout-main">
<div className="ac-app-main-inner ac-dialog-texture">
<Outlet />
2026-06-23 09:54:27 +00:00
</div>
</main>
</div>
<MobileBottomNav />
</div>
)
}