coupon code system eingeführt. coupons werden vom super admin gemanaged. coupons werden mit paddle synchronisiert und dort validiert. plus: einige mobil-optimierungen im tenant admin pwa.
This commit is contained in:
@@ -1,33 +1,33 @@
|
||||
import React from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
LayoutDashboard,
|
||||
CalendarDays,
|
||||
Sparkles,
|
||||
CreditCard,
|
||||
Settings as SettingsIcon,
|
||||
} from 'lucide-react';
|
||||
import { LayoutDashboard, CalendarDays, Camera, Settings } from 'lucide-react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
ADMIN_HOME_PATH,
|
||||
ADMIN_EVENTS_PATH,
|
||||
ADMIN_EVENT_VIEW_PATH,
|
||||
ADMIN_EVENT_PHOTOS_PATH,
|
||||
ADMIN_SETTINGS_PATH,
|
||||
ADMIN_BILLING_PATH,
|
||||
ADMIN_ENGAGEMENT_PATH,
|
||||
} from '../constants';
|
||||
import { LanguageSwitcher } from './LanguageSwitcher';
|
||||
import { registerApiErrorListener } from '../lib/apiError';
|
||||
import { getDashboardSummary, getEvents, getTenantPackagesOverview } from '../api';
|
||||
import { NotificationCenter } from './NotificationCenter';
|
||||
import { UserMenu } from './UserMenu';
|
||||
import { useEventContext } from '../context/EventContext';
|
||||
import { EventSwitcher, EventMenuBar } from './EventNav';
|
||||
|
||||
const navItems = [
|
||||
{ to: ADMIN_HOME_PATH, labelKey: 'navigation.dashboard', icon: LayoutDashboard, end: true },
|
||||
{ to: ADMIN_EVENTS_PATH, labelKey: 'navigation.events', icon: CalendarDays },
|
||||
{ to: ADMIN_ENGAGEMENT_PATH, labelKey: 'navigation.engagement', icon: Sparkles },
|
||||
{ to: ADMIN_BILLING_PATH, labelKey: 'navigation.billing', icon: CreditCard },
|
||||
{ to: ADMIN_SETTINGS_PATH, labelKey: 'navigation.settings', icon: SettingsIcon },
|
||||
];
|
||||
type NavItem = {
|
||||
key: string;
|
||||
to: string;
|
||||
label: string;
|
||||
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
|
||||
end?: boolean;
|
||||
highlight?: boolean;
|
||||
prefetchKey?: string;
|
||||
};
|
||||
|
||||
interface AdminLayoutProps {
|
||||
title: string;
|
||||
@@ -39,6 +39,51 @@ interface AdminLayoutProps {
|
||||
export function AdminLayout({ title, subtitle, actions, children }: AdminLayoutProps) {
|
||||
const { t } = useTranslation('common');
|
||||
const prefetchedPathsRef = React.useRef<Set<string>>(new Set());
|
||||
const { events } = useEventContext();
|
||||
const singleEvent = events.length === 1 ? events[0] : null;
|
||||
const eventsPath = singleEvent?.slug ? ADMIN_EVENT_VIEW_PATH(singleEvent.slug) : ADMIN_EVENTS_PATH;
|
||||
const eventsLabel = events.length === 1
|
||||
? t('navigation.event', { defaultValue: 'Event' })
|
||||
: t('navigation.events');
|
||||
|
||||
const photosPath = singleEvent?.slug ? ADMIN_EVENT_PHOTOS_PATH(singleEvent.slug) : ADMIN_EVENTS_PATH;
|
||||
const photosLabel = t('navigation.photos', { defaultValue: 'Fotos' });
|
||||
const settingsLabel = t('navigation.settings');
|
||||
|
||||
const navItems = React.useMemo<NavItem[]>(() => [
|
||||
{
|
||||
key: 'dashboard',
|
||||
to: ADMIN_HOME_PATH,
|
||||
label: t('navigation.dashboard'),
|
||||
icon: LayoutDashboard,
|
||||
end: true,
|
||||
prefetchKey: ADMIN_HOME_PATH,
|
||||
},
|
||||
{
|
||||
key: 'events',
|
||||
to: eventsPath,
|
||||
label: eventsLabel,
|
||||
icon: CalendarDays,
|
||||
end: Boolean(singleEvent?.slug),
|
||||
highlight: events.length === 1,
|
||||
prefetchKey: ADMIN_EVENTS_PATH,
|
||||
},
|
||||
{
|
||||
key: 'photos',
|
||||
to: photosPath,
|
||||
label: photosLabel,
|
||||
icon: Camera,
|
||||
end: Boolean(singleEvent?.slug),
|
||||
prefetchKey: singleEvent?.slug ? undefined : ADMIN_EVENTS_PATH,
|
||||
},
|
||||
{
|
||||
key: 'settings',
|
||||
to: ADMIN_SETTINGS_PATH,
|
||||
label: settingsLabel,
|
||||
icon: Settings,
|
||||
prefetchKey: ADMIN_SETTINGS_PATH,
|
||||
},
|
||||
], [eventsLabel, eventsPath, photosPath, photosLabel, settingsLabel, singleEvent, events.length, t]);
|
||||
|
||||
const prefetchers = React.useMemo(() => ({
|
||||
[ADMIN_HOME_PATH]: () =>
|
||||
@@ -48,7 +93,6 @@ export function AdminLayout({ title, subtitle, actions, children }: AdminLayoutP
|
||||
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(),
|
||||
}), []);
|
||||
@@ -109,35 +153,43 @@ export function AdminLayout({ title, subtitle, actions, children }: AdminLayoutP
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<EventSwitcher />
|
||||
{actions}
|
||||
<LanguageSwitcher />
|
||||
<NotificationCenter />
|
||||
<UserMenu />
|
||||
</div>
|
||||
</div>
|
||||
<nav className="hidden border-t border-slate-200/60 dark:border-white/5 sm:block">
|
||||
<div className="mx-auto flex w-full max-w-6xl items-center gap-1 px-4 py-2 sm:px-6">
|
||||
{navItems.map(({ to, labelKey, icon: Icon, end }) => (
|
||||
{navItems.map((item) => (
|
||||
<NavLink
|
||||
key={to}
|
||||
to={to}
|
||||
end={end}
|
||||
onPointerEnter={() => triggerPrefetch(to)}
|
||||
onFocus={() => triggerPrefetch(to)}
|
||||
onTouchStart={() => triggerPrefetch(to)}
|
||||
key={item.key}
|
||||
to={item.to}
|
||||
end={item.end}
|
||||
onPointerEnter={() => triggerPrefetch(item.prefetchKey ?? item.to)}
|
||||
onFocus={() => triggerPrefetch(item.prefetchKey ?? item.to)}
|
||||
onTouchStart={() => triggerPrefetch(item.prefetchKey ?? item.to)}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
'flex items-center gap-2 rounded-2xl px-3 py-2 text-xs font-semibold uppercase tracking-wide transition-all duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-400/60',
|
||||
isActive
|
||||
? 'bg-rose-600 text-white shadow shadow-rose-300/40'
|
||||
: 'text-slate-500 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white'
|
||||
: cn(
|
||||
item.highlight
|
||||
? 'text-rose-600 dark:text-rose-200'
|
||||
: 'text-slate-500 dark:text-slate-300',
|
||||
'hover:text-slate-900 dark:hover:text-white'
|
||||
)
|
||||
)
|
||||
}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
{t(labelKey)}
|
||||
<item.icon className="h-4 w-4" />
|
||||
{item.label}
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
<EventMenuBar />
|
||||
</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">
|
||||
@@ -154,7 +206,7 @@ function TenantMobileNav({
|
||||
items,
|
||||
onPrefetch,
|
||||
}: {
|
||||
items: typeof navItems;
|
||||
items: NavItem[];
|
||||
onPrefetch: (path: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation('common');
|
||||
@@ -167,25 +219,30 @@ function TenantMobileNav({
|
||||
/>
|
||||
<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 }) => (
|
||||
{items.map((item) => (
|
||||
<NavLink
|
||||
key={to}
|
||||
to={to}
|
||||
end={end}
|
||||
onPointerEnter={() => onPrefetch(to)}
|
||||
onFocus={() => onPrefetch(to)}
|
||||
onTouchStart={() => onPrefetch(to)}
|
||||
key={item.key}
|
||||
to={item.to}
|
||||
end={item.end}
|
||||
onPointerEnter={() => onPrefetch(item.prefetchKey ?? item.to)}
|
||||
onFocus={() => onPrefetch(item.prefetchKey ?? item.to)}
|
||||
onTouchStart={() => onPrefetch(item.prefetchKey ?? item.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'
|
||||
: cn(
|
||||
item.highlight
|
||||
? 'text-rose-600 dark:text-rose-200'
|
||||
: 'text-slate-600 dark:text-slate-300',
|
||||
'hover:text-rose-700 dark:hover:text-rose-200'
|
||||
)
|
||||
)
|
||||
}
|
||||
>
|
||||
<Icon className="h-5 w-5" />
|
||||
<span>{t(labelKey)}</span>
|
||||
<item.icon className="h-5 w-5" />
|
||||
<span>{item.label}</span>
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user