stage 1 of oauth removal, switch to sanctum pat tokens

This commit is contained in:
Codex Agent
2025-11-06 20:35:58 +01:00
parent c9783bd57b
commit 776da57ca9
47 changed files with 1571 additions and 2555 deletions

View File

@@ -19,6 +19,7 @@ import {
} from 'lucide-react';
import { LanguageSwitcher } from './LanguageSwitcher';
import { registerApiErrorListener } from '../lib/apiError';
import { getDashboardSummary, getEvents, getTenantPackagesOverview } from '../api';
const navItems = [
{ to: ADMIN_HOME_PATH, labelKey: 'navigation.dashboard', icon: LayoutDashboard, end: true },
@@ -37,6 +38,39 @@ interface AdminLayoutProps {
export function AdminLayout({ title, subtitle, actions, children }: AdminLayoutProps) {
const { t } = useTranslation('common');
const prefetchedPathsRef = React.useRef<Set<string>>(new Set());
const prefetchers = React.useMemo(() => ({
[ADMIN_HOME_PATH]: () =>
Promise.all([
getDashboardSummary(),
getEvents(),
getTenantPackagesOverview(),
]).then(() => undefined),
[ADMIN_EVENTS_PATH]: () => getEvents().then(() => undefined),
[ADMIN_ENGAGEMENT_PATH]: () => getEvents().then(() => undefined),
[ADMIN_BILLING_PATH]: () => getTenantPackagesOverview().then(() => undefined),
[ADMIN_SETTINGS_PATH]: () => Promise.resolve(),
}), []);
const triggerPrefetch = React.useCallback(
(path: string) => {
if (prefetchedPathsRef.current.has(path)) {
return;
}
const runner = prefetchers[path as keyof typeof prefetchers];
if (!runner) {
return;
}
prefetchedPathsRef.current.add(path);
Promise.resolve(runner()).catch(() => {
prefetchedPathsRef.current.delete(path);
});
},
[prefetchers],
);
React.useEffect(() => {
document.body.classList.add('tenant-admin-theme');
@@ -78,18 +112,21 @@ export function AdminLayout({ title, subtitle, actions, children }: AdminLayoutP
{actions}
</div>
</div>
<nav className="hidden items-center gap-2 border-t border-white/20 px-6 py-4 md:flex">
<nav className="hidden items-center gap-2 border-t border-slate-200/70 px-6 py-4 md:flex">
{navItems.map(({ to, labelKey, icon: Icon, end }) => (
<NavLink
key={to}
to={to}
end={end}
onPointerEnter={() => triggerPrefetch(to)}
onFocus={() => triggerPrefetch(to)}
onTouchStart={() => triggerPrefetch(to)}
className={({ isActive }) =>
cn(
'flex items-center gap-2 rounded-full px-4 py-2 text-sm font-medium transition-all duration-200',
'flex items-center gap-2 rounded-full px-4 py-2 text-sm font-semibold transition-all duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-300 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950',
isActive
? 'bg-gradient-to-r from-[#ff5f87] via-[#ec4899] to-[#6366f1] text-white shadow-lg shadow-rose-300/30'
: 'bg-white/70 text-slate-600 hover:bg-white hover:text-slate-900'
? 'bg-rose-600 text-white shadow-md shadow-rose-400/30'
: 'border border-slate-200/80 bg-white text-slate-700 hover:bg-rose-50/80 hover:text-rose-700'
)
}
>
@@ -101,7 +138,7 @@ export function AdminLayout({ title, subtitle, actions, children }: AdminLayoutP
</div>
</header>
<main className="relative z-10 mx-auto w-full max-w-6xl flex-1 px-4 pb-28 pt-6 sm:px-6">
<main className="relative z-10 mx-auto w-full max-w-6xl flex-1 px-4 pb-[calc(env(safe-area-inset-bottom,0)+6.5rem)] pt-6 sm:px-6 md:pb-16">
<div className="grid gap-6">{children}</div>
</main>
@@ -115,26 +152,35 @@ function TenantMobileNav({ items }: { items: typeof navItems }) {
const { t } = useTranslation('common');
return (
<nav className="sticky bottom-4 z-40 px-4 pb-2 md:hidden">
<div className="mx-auto flex w-full max-w-md items-center justify-around rounded-full border border-white/20 bg-white/90 p-2 text-slate-600 shadow-2xl shadow-rose-300/20 backdrop-blur-xl">
{items.map(({ to, labelKey, icon: Icon, end }) => (
<NavLink
key={to}
to={to}
end={end}
className={({ isActive }) =>
cn(
'flex flex-col items-center gap-1 rounded-full px-3 py-2 text-xs font-medium transition',
isActive
? 'bg-gradient-to-r from-[#ff5f87] via-[#ec4899] to-[#6366f1] text-white shadow-lg shadow-rose-300/30'
: 'text-slate-600 hover:text-slate-900'
)
}
>
<Icon className="h-5 w-5" />
<span>{t(labelKey)}</span>
</NavLink>
))}
<nav className="md:hidden" aria-label={t('navigation.mobile', { defaultValue: 'Tenant Navigation' })}>
<div
aria-hidden
className="pointer-events-none fixed inset-x-0 bottom-0 z-30 h-16 bg-gradient-to-t from-slate-950/35 via-transparent to-transparent dark:from-black/60"
/>
<div className="fixed inset-x-0 bottom-0 z-40 border-t border-slate-200/80 bg-white/95 px-4 pb-[calc(env(safe-area-inset-bottom,0)+0.75rem)] pt-3 shadow-2xl shadow-rose-300/15 backdrop-blur supports-[backdrop-filter]:bg-white/90 dark:border-slate-800/70 dark:bg-slate-950/90">
<div className="mx-auto flex max-w-xl items-center justify-around gap-1">
{items.map(({ to, labelKey, icon: Icon, end }) => (
<NavLink
key={to}
to={to}
end={end}
onPointerEnter={() => triggerPrefetch(to)}
onFocus={() => triggerPrefetch(to)}
onTouchStart={() => triggerPrefetch(to)}
className={({ isActive }) =>
cn(
'flex flex-col items-center gap-1 rounded-xl px-3 py-2 text-xs font-semibold text-slate-600 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-300 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-slate-300 dark:focus-visible:ring-offset-slate-950',
isActive
? 'bg-rose-600 text-white shadow-md shadow-rose-400/25'
: 'hover:text-rose-700 dark:hover:text-rose-200'
)
}
>
<Icon className="h-5 w-5" />
<span>{t(labelKey)}</span>
</NavLink>
))}
</div>
</div>
</nav>
);