rework of the event admin UI
This commit is contained in:
@@ -1,9 +1,18 @@
|
||||
import React from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { Link, NavLink, useLocation } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LayoutDashboard, CalendarDays, Camera, Settings } from 'lucide-react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from '@/components/ui/sheet';
|
||||
import {
|
||||
ADMIN_HOME_PATH,
|
||||
ADMIN_EVENTS_PATH,
|
||||
@@ -19,6 +28,7 @@ import { UserMenu } from './UserMenu';
|
||||
import { useEventContext } from '../context/EventContext';
|
||||
import { EventSwitcher, EventMenuBar } from './EventNav';
|
||||
import { useAuth } from '../auth/context';
|
||||
import { CommandShelf } from './CommandShelf';
|
||||
|
||||
type NavItem = {
|
||||
key: string;
|
||||
@@ -30,14 +40,24 @@ type NavItem = {
|
||||
prefetchKey?: string;
|
||||
};
|
||||
|
||||
type PageTab = {
|
||||
key: string;
|
||||
label: string;
|
||||
href: string;
|
||||
badge?: React.ReactNode;
|
||||
};
|
||||
|
||||
interface AdminLayoutProps {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
actions?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
disableCommandShelf?: boolean;
|
||||
tabs?: PageTab[];
|
||||
currentTabKey?: string;
|
||||
}
|
||||
|
||||
export function AdminLayout({ title, subtitle, actions, children }: AdminLayoutProps) {
|
||||
export function AdminLayout({ title, subtitle, actions, children, disableCommandShelf, tabs, currentTabKey }: AdminLayoutProps) {
|
||||
const { t } = useTranslation('common');
|
||||
const prefetchedPathsRef = React.useRef<Set<string>>(new Set());
|
||||
const { events } = useEventContext();
|
||||
@@ -167,7 +187,7 @@ export function AdminLayout({ title, subtitle, actions, children }: AdminLayoutP
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<EventSwitcher />
|
||||
{disableCommandShelf ? <EventSwitcher compact /> : null}
|
||||
{actions}
|
||||
<NotificationCenter />
|
||||
<UserMenu />
|
||||
@@ -203,7 +223,8 @@ export function AdminLayout({ title, subtitle, actions, children }: AdminLayoutP
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
<EventMenuBar />
|
||||
{disableCommandShelf ? <EventMenuBar /> : <CommandShelf />}
|
||||
{tabs && tabs.length ? <PageTabsNav tabs={tabs} currentKey={currentTabKey} /> : null}
|
||||
</header>
|
||||
|
||||
<main className="relative z-10 mx-auto w-full max-w-5xl flex-1 px-4 pb-[calc(env(safe-area-inset-bottom,0)+5.5rem)] pt-5 sm:px-6 md:pb-16">
|
||||
@@ -216,6 +237,116 @@ export function AdminLayout({ title, subtitle, actions, children }: AdminLayoutP
|
||||
);
|
||||
}
|
||||
|
||||
function PageTabsNav({ tabs, currentKey }: { tabs: PageTab[]; currentKey?: string }) {
|
||||
const location = useLocation();
|
||||
const { t } = useTranslation('common');
|
||||
const [mobileOpen, setMobileOpen] = React.useState(false);
|
||||
|
||||
const isActive = (tab: PageTab): boolean => {
|
||||
if (currentKey) {
|
||||
return tab.key === currentKey;
|
||||
}
|
||||
return location.pathname === tab.href || location.pathname.startsWith(tab.href);
|
||||
};
|
||||
|
||||
const activeTab = React.useMemo(() => tabs.find((tab) => isActive(tab)), [tabs, location.pathname, currentKey]);
|
||||
|
||||
return (
|
||||
<div className="border-t border-slate-200/70 bg-white/80 backdrop-blur dark:border-white/10 dark:bg-slate-950/70">
|
||||
<div className="mx-auto flex w-full max-w-6xl flex-col gap-2 px-4 py-2 sm:px-6">
|
||||
<div className="hidden gap-2 md:flex">
|
||||
{tabs.map((tab) => {
|
||||
const active = isActive(tab);
|
||||
return (
|
||||
<Link
|
||||
key={tab.key}
|
||||
to={tab.href}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-2xl px-4 py-2 text-sm font-semibold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-400/60',
|
||||
active
|
||||
? 'bg-rose-600 text-white shadow shadow-rose-300/40'
|
||||
: 'bg-white text-slate-600 hover:text-slate-900 dark:bg-white/5 dark:text-slate-300 dark:hover:text-white'
|
||||
)}
|
||||
>
|
||||
<span>{tab.label}</span>
|
||||
{tab.badge !== undefined ? (
|
||||
<Badge
|
||||
variant={active ? 'secondary' : 'outline'}
|
||||
className={cn(
|
||||
active ? 'bg-white/20 text-white' : 'text-slate-600 dark:text-slate-300',
|
||||
'rounded-full text-[11px]'
|
||||
)}
|
||||
>
|
||||
{tab.badge}
|
||||
</Badge>
|
||||
) : null}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="md:hidden">
|
||||
<Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center justify-between rounded-2xl border border-slate-200/70 bg-white px-3 py-2 text-left text-sm font-semibold text-slate-700 shadow-sm dark:border-white/10 dark:bg-white/10 dark:text-slate-200"
|
||||
>
|
||||
<span>
|
||||
{activeTab?.label ?? t('navigation.tabs.active', { defaultValue: 'Bereich wählen' })}
|
||||
</span>
|
||||
<span className="text-xs uppercase tracking-[0.3em] text-rose-500">
|
||||
{t('navigation.tabs.open', { defaultValue: 'Tabs' })}
|
||||
</span>
|
||||
</button>
|
||||
</SheetTrigger>
|
||||
<SheetContent
|
||||
side="bottom"
|
||||
className="rounded-t-3xl border-t border-slate-200/70 bg-white/95 pb-6 pt-6 dark:border-white/10 dark:bg-slate-950/95"
|
||||
>
|
||||
<SheetHeader className="px-4 pt-0 text-left">
|
||||
<SheetTitle className="text-lg font-semibold text-slate-900 dark:text-white">
|
||||
{t('navigation.tabs.title', { defaultValue: 'Bereich auswählen' })}
|
||||
</SheetTitle>
|
||||
<SheetDescription>
|
||||
{t('navigation.tabs.subtitle', { defaultValue: 'Wechsle schnell zwischen Event-Bereichen.' })}
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="mt-4 grid gap-2 px-4">
|
||||
{tabs.map((tab) => {
|
||||
const active = isActive(tab);
|
||||
return (
|
||||
<Link
|
||||
key={`sheet-${tab.key}`}
|
||||
to={tab.href}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
className={cn(
|
||||
'flex items-center justify-between rounded-2xl border px-4 py-3 text-sm font-medium shadow-sm transition',
|
||||
active
|
||||
? 'border-rose-200 bg-rose-50 text-rose-700'
|
||||
: 'border-slate-200 bg-white text-slate-700 dark:border-white/10 dark:bg-white/5 dark:text-slate-200'
|
||||
)}
|
||||
>
|
||||
<span>{tab.label}</span>
|
||||
{tab.badge !== undefined ? (
|
||||
<Badge
|
||||
variant={active ? 'secondary' : 'outline'}
|
||||
className={cn(active ? 'bg-white/30 text-rose-700' : 'text-slate-600 dark:text-slate-200', 'rounded-full text-[11px]')}
|
||||
>
|
||||
{tab.badge}
|
||||
</Badge>
|
||||
) : null}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TenantMobileNav({
|
||||
items,
|
||||
onPrefetch,
|
||||
|
||||
Reference in New Issue
Block a user