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>
|
|
|
|
|
)
|
|
|
|
|
}
|