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:
Codex Agent
2025-11-09 20:26:50 +01:00
parent f3c44be76d
commit 082b78cd43
80 changed files with 4855 additions and 435 deletions

View File

@@ -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>