feat: unify tenant admin ui and add photo moderation
This commit is contained in:
@@ -27,6 +27,10 @@ import {
|
||||
FrostedSurface,
|
||||
tenantHeroPrimaryButtonClass,
|
||||
tenantHeroSecondaryButtonClass,
|
||||
SectionCard,
|
||||
SectionHeader,
|
||||
StatCarousel,
|
||||
ActionGrid,
|
||||
} from '../components/tenant';
|
||||
import type { ChecklistStep } from '../components/tenant';
|
||||
|
||||
@@ -267,6 +271,44 @@ export default function DashboardPage() {
|
||||
}, [summary, events]);
|
||||
|
||||
const primaryEventSlug = readiness.primaryEventSlug;
|
||||
const statItems = React.useMemo(
|
||||
() => ([
|
||||
{
|
||||
key: 'activeEvents',
|
||||
label: translate('overview.stats.activeEvents'),
|
||||
value: summary?.active_events ?? publishedEvents.length,
|
||||
hint: translate('overview.stats.publishedHint', { count: publishedEvents.length }),
|
||||
icon: <CalendarDays className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
key: 'newPhotos',
|
||||
label: translate('overview.stats.newPhotos'),
|
||||
value: summary?.new_photos ?? 0,
|
||||
icon: <Camera className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
key: 'taskProgress',
|
||||
label: translate('overview.stats.taskProgress'),
|
||||
value: `${Math.round(summary?.task_progress ?? 0)}%`,
|
||||
icon: <Users className="h-4 w-4" />,
|
||||
},
|
||||
activePackage
|
||||
? {
|
||||
key: 'package',
|
||||
label: translate('overview.stats.activePackage', 'Aktives Paket'),
|
||||
value: activePackage.package_name,
|
||||
icon: <PackageIcon className="h-4 w-4" />,
|
||||
}
|
||||
: null,
|
||||
].filter(Boolean) as {
|
||||
key: string;
|
||||
label: string;
|
||||
value: string | number;
|
||||
hint?: string;
|
||||
icon?: React.ReactNode;
|
||||
}[]),
|
||||
[summary, publishedEvents.length, translate, activePackage],
|
||||
);
|
||||
|
||||
const onboardingChecklist = React.useMemo<ChecklistStep[]>(() => {
|
||||
const steps: ChecklistStep[] = [
|
||||
@@ -428,7 +470,7 @@ export default function DashboardPage() {
|
||||
</Button>
|
||||
);
|
||||
const heroAside = (
|
||||
<FrostedSurface className="w-full rounded-2xl border-white/30 bg-white/90 p-5 text-slate-900 shadow-lg shadow-rose-300/20 backdrop-blur">
|
||||
<FrostedSurface className="w-full rounded-2xl border-slate-200 bg-white p-5 text-slate-900 shadow-lg shadow-rose-300/20 dark:border-white/20 dark:bg-white/10">
|
||||
<div className="flex items-center justify-between text-sm font-medium text-slate-700">
|
||||
<span>{onboardingCardTitle}</span>
|
||||
<span>
|
||||
@@ -441,32 +483,58 @@ export default function DashboardPage() {
|
||||
);
|
||||
const readinessCompleteLabel = translate('readiness.complete', 'Erledigt');
|
||||
const readinessPendingLabel = translate('readiness.pending', 'Noch offen');
|
||||
const quickActionItems = React.useMemo(
|
||||
() => [
|
||||
{
|
||||
key: 'create',
|
||||
label: translate('quickActions.createEvent.label'),
|
||||
description: translate('quickActions.createEvent.description'),
|
||||
icon: <Plus className="h-5 w-5" />,
|
||||
onClick: () => navigate(ADMIN_EVENT_CREATE_PATH),
|
||||
},
|
||||
{
|
||||
key: 'photos',
|
||||
label: translate('quickActions.moderatePhotos.label'),
|
||||
description: translate('quickActions.moderatePhotos.description'),
|
||||
icon: <Camera className="h-5 w-5" />,
|
||||
onClick: () => navigate(ADMIN_EVENTS_PATH),
|
||||
},
|
||||
{
|
||||
key: 'tasks',
|
||||
label: translate('quickActions.organiseTasks.label'),
|
||||
description: translate('quickActions.organiseTasks.description'),
|
||||
icon: <ClipboardList className="h-5 w-5" />,
|
||||
onClick: () => navigate(buildEngagementTabPath('tasks')),
|
||||
},
|
||||
{
|
||||
key: 'packages',
|
||||
label: translate('quickActions.managePackages.label'),
|
||||
description: translate('quickActions.managePackages.description'),
|
||||
icon: <Sparkles className="h-5 w-5" />,
|
||||
onClick: () => navigate(ADMIN_BILLING_PATH),
|
||||
},
|
||||
{
|
||||
key: 'marketing',
|
||||
label: marketingDashboardLabel,
|
||||
description: marketingDashboardDescription,
|
||||
icon: <ArrowUpRight className="h-5 w-5" />,
|
||||
onClick: () => window.location.assign('/dashboard'),
|
||||
},
|
||||
],
|
||||
[translate, navigate, marketingDashboardLabel, marketingDashboardDescription],
|
||||
);
|
||||
|
||||
const actions = (
|
||||
<>
|
||||
<Button
|
||||
className="bg-brand-rose text-white shadow-lg shadow-rose-400/40 hover:bg-[var(--brand-rose-strong)]"
|
||||
onClick={() => navigate(ADMIN_EVENT_CREATE_PATH)}
|
||||
>
|
||||
<Plus className="h-4 w-4" /> {translate('actions.newEvent')}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => navigate(ADMIN_EVENTS_PATH)} className="border-brand-rose-soft text-brand-rose">
|
||||
<CalendarDays className="h-4 w-4" /> {translate('actions.allEvents')}
|
||||
</Button>
|
||||
{events.length === 0 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => navigate(ADMIN_WELCOME_BASE_PATH)}
|
||||
className="border-brand-rose-soft text-brand-rose hover:bg-brand-rose-soft/40"
|
||||
>
|
||||
<Sparkles className="h-4 w-4" /> {translate('actions.guidedSetup')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
const layoutActions = (
|
||||
<Button
|
||||
className="rounded-full bg-brand-rose px-4 text-white shadow-lg shadow-rose-400/40 hover:bg-[var(--brand-rose-strong)]"
|
||||
onClick={() => navigate(ADMIN_EVENT_CREATE_PATH)}
|
||||
>
|
||||
<Plus className="h-4 w-4" /> {translate('actions.newEvent')}
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<AdminLayout title={greetingTitle} subtitle={subtitle} actions={actions}>
|
||||
<AdminLayout title={greetingTitle} subtitle={subtitle} actions={layoutActions}>
|
||||
{errorMessage && (
|
||||
<Alert variant="destructive">
|
||||
<AlertTitle>{t('dashboard.alerts.errorTitle')}</AlertTitle>
|
||||
@@ -481,17 +549,17 @@ export default function DashboardPage() {
|
||||
<TenantHeroCard
|
||||
badge={heroBadge}
|
||||
title={greetingTitle}
|
||||
description={subtitle}
|
||||
supporting={[heroDescription, heroSupportingCopy]}
|
||||
description={heroDescription}
|
||||
supporting={[heroSupportingCopy]}
|
||||
primaryAction={heroPrimaryAction}
|
||||
secondaryAction={heroSecondaryAction}
|
||||
aside={heroAside}
|
||||
/>
|
||||
|
||||
{events.length === 0 && (
|
||||
<Card className="border-0 bg-brand-card shadow-brand-primary">
|
||||
<CardHeader className="space-y-3">
|
||||
<CardTitle className="flex items-center gap-2 text-xl text-slate-900">
|
||||
<Card className="border-none bg-white/90 shadow-lg shadow-rose-100/50">
|
||||
<CardHeader className="space-y-2">
|
||||
<CardTitle className="flex items-center gap-2 text-base font-semibold text-slate-900">
|
||||
<Sparkles className="h-5 w-5 text-brand-rose" />
|
||||
{translate('welcomeCard.title')}
|
||||
</CardTitle>
|
||||
@@ -499,14 +567,12 @@ export default function DashboardPage() {
|
||||
{translate('welcomeCard.summary')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div className="space-y-2 text-sm text-slate-600">
|
||||
<p>{translate('welcomeCard.body1')}</p>
|
||||
<p>{translate('welcomeCard.body2')}</p>
|
||||
</div>
|
||||
<CardContent className="flex flex-col gap-3 text-sm text-slate-600">
|
||||
<p>{translate('welcomeCard.body1')}</p>
|
||||
<p>{translate('welcomeCard.body2')}</p>
|
||||
<Button
|
||||
size="lg"
|
||||
className="rounded-full bg-rose-500 text-white shadow-lg shadow-rose-400/40 hover:bg-rose-500/90"
|
||||
size="sm"
|
||||
className="self-start rounded-full bg-brand-rose px-5 text-white shadow-md shadow-rose-300/40"
|
||||
onClick={() => navigate(ADMIN_WELCOME_BASE_PATH)}
|
||||
>
|
||||
{translate('welcomeCard.cta')}
|
||||
@@ -515,47 +581,19 @@ export default function DashboardPage() {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Card className="border-0 bg-brand-card shadow-brand-primary">
|
||||
<CardHeader className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<CardTitle className="flex items-center gap-2 text-xl text-slate-900">
|
||||
<Sparkles className="h-5 w-5 text-brand-rose" />
|
||||
{translate('overview.title')}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-sm text-slate-600">
|
||||
{translate('overview.description')}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Badge className="bg-brand-rose-soft text-brand-rose">
|
||||
{activePackage?.package_name ?? translate('overview.noPackage')}
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
<StatCard
|
||||
label={translate('overview.stats.activeEvents')}
|
||||
value={summary?.active_events ?? publishedEvents.length}
|
||||
hint={translate('overview.stats.publishedHint', { count: publishedEvents.length })}
|
||||
icon={<CalendarDays className="h-5 w-5 text-brand-rose" />}
|
||||
/>
|
||||
<StatCard
|
||||
label={translate('overview.stats.newPhotos')}
|
||||
value={summary?.new_photos ?? 0}
|
||||
icon={<Camera className="h-5 w-5 text-fuchsia-500" />}
|
||||
/>
|
||||
<StatCard
|
||||
label={translate('overview.stats.taskProgress')}
|
||||
value={`${Math.round(summary?.task_progress ?? 0)}%`}
|
||||
icon={<Users className="h-5 w-5 text-amber-500" />}
|
||||
/>
|
||||
{activePackage ? (
|
||||
<StatCard
|
||||
label={translate('overview.stats.activePackage')}
|
||||
value={activePackage.package_name}
|
||||
icon={<Sparkles className="h-5 w-5 text-sky-500" />}
|
||||
/>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<SectionCard className="space-y-3">
|
||||
<SectionHeader
|
||||
eyebrow={translate('overview.title')}
|
||||
title={translate('overview.title')}
|
||||
description={translate('overview.description')}
|
||||
endSlot={(
|
||||
<Badge className="bg-brand-rose-soft text-brand-rose">
|
||||
{activePackage?.package_name ?? translate('overview.noPackage')}
|
||||
</Badge>
|
||||
)}
|
||||
/>
|
||||
<StatCarousel items={statItems} />
|
||||
</SectionCard>
|
||||
|
||||
{primaryEventLimits ? (
|
||||
<Card className="border-0 bg-brand-card shadow-brand-primary">
|
||||
@@ -627,48 +665,14 @@ export default function DashboardPage() {
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
<Card className="border-0 bg-brand-card shadow-brand-primary">
|
||||
<CardHeader className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-xl text-slate-900">{translate('quickActions.title')}</CardTitle>
|
||||
<CardDescription className="text-sm text-slate-600">
|
||||
{translate('quickActions.description')}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<QuickAction
|
||||
icon={<Plus className="h-5 w-5" />}
|
||||
label={translate('quickActions.createEvent.label')}
|
||||
description={translate('quickActions.createEvent.description')}
|
||||
onClick={() => navigate(ADMIN_EVENT_CREATE_PATH)}
|
||||
/>
|
||||
<QuickAction
|
||||
icon={<Camera className="h-5 w-5" />}
|
||||
label={translate('quickActions.moderatePhotos.label')}
|
||||
description={translate('quickActions.moderatePhotos.description')}
|
||||
onClick={() => navigate(ADMIN_EVENTS_PATH)}
|
||||
/>
|
||||
<QuickAction
|
||||
icon={<Users className="h-5 w-5" />}
|
||||
label={translate('quickActions.organiseTasks.label')}
|
||||
description={translate('quickActions.organiseTasks.description')}
|
||||
onClick={() => navigate(buildEngagementTabPath('tasks'))}
|
||||
/>
|
||||
<QuickAction
|
||||
icon={<Sparkles className="h-5 w-5" />}
|
||||
label={translate('quickActions.managePackages.label')}
|
||||
description={translate('quickActions.managePackages.description')}
|
||||
onClick={() => navigate(ADMIN_BILLING_PATH)}
|
||||
/>
|
||||
<QuickAction
|
||||
icon={<ArrowUpRight className="h-5 w-5" />}
|
||||
label={marketingDashboardLabel}
|
||||
description={marketingDashboardDescription}
|
||||
onClick={() => window.location.assign('/dashboard')}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<SectionCard className="space-y-3">
|
||||
<SectionHeader
|
||||
eyebrow={translate('quickActions.title')}
|
||||
title={translate('quickActions.title')}
|
||||
description={translate('quickActions.description')}
|
||||
/>
|
||||
<ActionGrid items={quickActionItems} />
|
||||
</SectionCard>
|
||||
|
||||
<TenantOnboardingChecklistCard
|
||||
title={onboardingCardTitle}
|
||||
@@ -683,20 +687,20 @@ export default function DashboardPage() {
|
||||
fallbackActionLabel={onboardingFallbackCta}
|
||||
/>
|
||||
|
||||
<Card className="border-0 bg-brand-card shadow-brand-primary">
|
||||
<CardHeader className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
|
||||
<section className="space-y-4 rounded-3xl border border-slate-200 bg-white p-5 shadow-sm dark:border-white/10 dark:bg-white/5 dark:shadow-inner">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-xl text-slate-900">{translate('upcoming.title')}</CardTitle>
|
||||
<CardDescription className="text-sm text-slate-600">
|
||||
{translate('upcoming.description')}
|
||||
</CardDescription>
|
||||
<p className="text-xs uppercase tracking-[0.35em] text-rose-500 dark:text-rose-200">
|
||||
{translate('upcoming.title')}
|
||||
</p>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300">{translate('upcoming.description')}</p>
|
||||
</div>
|
||||
<Button variant="outline" onClick={() => navigate(ADMIN_SETTINGS_PATH)}>
|
||||
<Button variant="outline" size="sm" onClick={() => navigate(ADMIN_SETTINGS_PATH)}>
|
||||
<Settings className="h-4 w-4" />
|
||||
{translate('upcoming.settings')}
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{upcomingEvents.length === 0 ? (
|
||||
<EmptyState
|
||||
message={translate('upcoming.empty.message')}
|
||||
@@ -719,8 +723,8 @@ export default function DashboardPage() {
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</AdminLayout>
|
||||
@@ -933,30 +937,6 @@ function StatCard({
|
||||
);
|
||||
}
|
||||
|
||||
function QuickAction({
|
||||
icon,
|
||||
label,
|
||||
description,
|
||||
onClick,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
description: string;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className="group flex flex-col items-start gap-2 rounded-2xl border border-slate-100 bg-white/85 p-4 text-left shadow-sm transition duration-200 ease-out hover:-translate-y-0.5 hover:border-brand-rose-soft hover:shadow-md focus:outline-none focus:ring-2 focus:ring-brand-rose/40 dark:border-slate-800/70 dark:bg-slate-950/80"
|
||||
>
|
||||
<span className="rounded-full bg-brand-rose-soft p-2 text-brand-rose shadow-sm shadow-rose-200/60 transition-transform duration-200 group-hover:scale-105">{icon}</span>
|
||||
<span className="text-sm font-semibold text-slate-900 dark:text-slate-100">{label}</span>
|
||||
<span className="text-xs text-slate-600 dark:text-slate-400">{description}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function UpcomingEventRow({
|
||||
event,
|
||||
onView,
|
||||
@@ -979,7 +959,7 @@ function UpcomingEventRow({
|
||||
: labels.noDate;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 rounded-2xl border border-sky-100 bg-white/90 p-4 shadow-sm sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex flex-col gap-2 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-white/30 dark:border-white/10 dark:bg-white/5 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-xs font-medium uppercase tracking-wide text-slate-500">{formattedDate}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -990,7 +970,7 @@ function UpcomingEventRow({
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={onView}
|
||||
className="border-brand-rose-soft text-brand-rose hover:bg-brand-rose-soft/40"
|
||||
className="rounded-full border-brand-rose-soft text-brand-rose hover:bg-brand-rose-soft/40"
|
||||
>
|
||||
{labels.open}
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user