Fix admin SQL count parsing for local SpacetimeDB
This commit is contained in:
108
apps/admin-web/src/app/AdminShell.tsx
Normal file
108
apps/admin-web/src/app/AdminShell.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import {
|
||||
Bug,
|
||||
LayoutDashboard,
|
||||
LogOut,
|
||||
ShieldCheck,
|
||||
TicketPercent,
|
||||
} from 'lucide-react';
|
||||
import type {ReactNode} from 'react';
|
||||
|
||||
import type {AdminSessionPayload} from '../api/adminApiTypes';
|
||||
import type {AdminRouteId} from './adminRoutes';
|
||||
import {adminRoutes} from './adminRoutes';
|
||||
|
||||
interface AdminShellProps {
|
||||
admin: AdminSessionPayload;
|
||||
routeId: AdminRouteId;
|
||||
children: ReactNode;
|
||||
onRouteChange: (routeId: AdminRouteId) => void;
|
||||
onLogout: () => void;
|
||||
}
|
||||
|
||||
const routeIcons = {
|
||||
overview: LayoutDashboard,
|
||||
debug: Bug,
|
||||
redeem: TicketPercent,
|
||||
} satisfies Record<AdminRouteId, typeof LayoutDashboard>;
|
||||
|
||||
export function AdminShell({
|
||||
admin,
|
||||
routeId,
|
||||
children,
|
||||
onRouteChange,
|
||||
onLogout,
|
||||
}: AdminShellProps) {
|
||||
return (
|
||||
<div className="admin-shell">
|
||||
<aside className="admin-sidebar">
|
||||
<div className="admin-brand">
|
||||
<div className="admin-brand-icon">
|
||||
<ShieldCheck size={20} aria-hidden="true" />
|
||||
</div>
|
||||
<div>
|
||||
<strong>陶泥后台</strong>
|
||||
<span>Admin</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav className="admin-nav" aria-label="后台导航">
|
||||
{adminRoutes.map((route) => {
|
||||
const Icon = routeIcons[route.id];
|
||||
return (
|
||||
<button
|
||||
className="admin-nav-button"
|
||||
data-active={route.id === routeId}
|
||||
key={route.id}
|
||||
title={route.label}
|
||||
type="button"
|
||||
onClick={() => onRouteChange(route.id)}
|
||||
>
|
||||
<Icon size={18} aria-hidden="true" />
|
||||
<span>{route.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div className="admin-main">
|
||||
<header className="admin-topbar">
|
||||
<div className="admin-user">
|
||||
<span>{admin.displayName || admin.username}</span>
|
||||
<small>{admin.roles.join(' / ')}</small>
|
||||
</div>
|
||||
<button
|
||||
className="admin-icon-button"
|
||||
title="退出登录"
|
||||
type="button"
|
||||
onClick={onLogout}
|
||||
>
|
||||
<LogOut size={18} aria-hidden="true" />
|
||||
<span>退出</span>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<main className="admin-content">{children}</main>
|
||||
</div>
|
||||
|
||||
<nav className="admin-bottom-nav" aria-label="后台导航">
|
||||
{adminRoutes.map((route) => {
|
||||
const Icon = routeIcons[route.id];
|
||||
return (
|
||||
<button
|
||||
className="admin-bottom-nav-button"
|
||||
data-active={route.id === routeId}
|
||||
key={route.id}
|
||||
title={route.label}
|
||||
type="button"
|
||||
onClick={() => onRouteChange(route.id)}
|
||||
>
|
||||
<Icon size={19} aria-hidden="true" />
|
||||
<span>{route.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user