fixed event join token handling in the event admin. created new seeders with new tenants and package purchases. added new playwright test scenarios.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CalendarDays, Camera, CreditCard, Sparkles, Users, Plus, Settings } from 'lucide-react';
|
||||
import { CalendarDays, Camera, Sparkles, Users, Plus, Settings } from 'lucide-react';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -11,7 +11,6 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
||||
import { AdminLayout } from '../components/AdminLayout';
|
||||
import {
|
||||
DashboardSummary,
|
||||
getCreditBalance,
|
||||
getDashboardSummary,
|
||||
getEvents,
|
||||
getTenantPackagesOverview,
|
||||
@@ -35,7 +34,6 @@ import { useOnboardingProgress } from '../onboarding';
|
||||
interface DashboardState {
|
||||
summary: DashboardSummary | null;
|
||||
events: TenantEvent[];
|
||||
credits: number;
|
||||
activePackage: TenantPackageSummary | null;
|
||||
loading: boolean;
|
||||
errorKey: string | null;
|
||||
@@ -46,11 +44,23 @@ export default function DashboardPage() {
|
||||
const location = useLocation();
|
||||
const { user } = useAuth();
|
||||
const { progress, markStep } = useOnboardingProgress();
|
||||
const { t, i18n } = useTranslation(['dashboard', 'common']);
|
||||
const { t, i18n } = useTranslation('dashboard', { keyPrefix: 'dashboard' });
|
||||
const { t: tc } = useTranslation('common');
|
||||
|
||||
const translate = React.useCallback(
|
||||
(key: string, options?: Record<string, unknown>) => {
|
||||
const value = t(key, options);
|
||||
if (value === `dashboard.${key}`) {
|
||||
const fallback = i18n.t(`dashboard:${key}`, options);
|
||||
return fallback === `dashboard:${key}` ? value : fallback;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
[t, i18n],
|
||||
);
|
||||
const [state, setState] = React.useState<DashboardState>({
|
||||
summary: null,
|
||||
events: [],
|
||||
credits: 0,
|
||||
activePackage: null,
|
||||
loading: true,
|
||||
errorKey: null,
|
||||
@@ -60,10 +70,9 @@ export default function DashboardPage() {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
try {
|
||||
const [summary, events, credits, packages] = await Promise.all([
|
||||
const [summary, events, packages] = await Promise.all([
|
||||
getDashboardSummary().catch(() => null),
|
||||
getEvents().catch(() => [] as TenantEvent[]),
|
||||
getCreditBalance().catch(() => ({ balance: 0 })),
|
||||
getTenantPackagesOverview().catch(() => ({ packages: [], activePackage: null })),
|
||||
]);
|
||||
|
||||
@@ -71,12 +80,11 @@ export default function DashboardPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
const fallbackSummary = buildSummaryFallback(events, credits.balance ?? 0, packages.activePackage);
|
||||
const fallbackSummary = buildSummaryFallback(events, packages.activePackage);
|
||||
|
||||
setState({
|
||||
summary: summary ?? fallbackSummary,
|
||||
events,
|
||||
credits: credits.balance ?? 0,
|
||||
activePackage: packages.activePackage,
|
||||
loading: false,
|
||||
errorKey: null,
|
||||
@@ -97,7 +105,7 @@ export default function DashboardPage() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const { summary, events, credits, activePackage, loading, errorKey } = state;
|
||||
const { summary, events, activePackage, loading, errorKey } = state;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (loading) {
|
||||
@@ -112,10 +120,10 @@ export default function DashboardPage() {
|
||||
}
|
||||
}, [loading, events.length, progress.eventCreated, navigate, location.pathname, markStep]);
|
||||
|
||||
const greetingName = user?.name ?? t('dashboard.welcome.fallbackName');
|
||||
const greetingTitle = t('dashboard.welcome.greeting', { name: greetingName });
|
||||
const subtitle = t('dashboard.welcome.subtitle');
|
||||
const errorMessage = errorKey ? t(`dashboard.errors.${errorKey}`) : null;
|
||||
const greetingName = user?.name ?? translate('welcome.fallbackName');
|
||||
const greetingTitle = translate('welcome.greeting', { name: greetingName });
|
||||
const subtitle = translate('welcome.subtitle');
|
||||
const errorMessage = errorKey ? translate(`errors.${errorKey}`) : null;
|
||||
const dateLocale = i18n.language?.startsWith('en') ? 'en-GB' : 'de-DE';
|
||||
|
||||
const upcomingEvents = getUpcomingEvents(events);
|
||||
@@ -127,10 +135,10 @@ export default function DashboardPage() {
|
||||
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" /> {t('dashboard.actions.newEvent')}
|
||||
<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" /> {t('dashboard.actions.allEvents')}
|
||||
<CalendarDays className="h-4 w-4" /> {translate('actions.allEvents')}
|
||||
</Button>
|
||||
{events.length === 0 && (
|
||||
<Button
|
||||
@@ -138,7 +146,7 @@ export default function DashboardPage() {
|
||||
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" /> {t('dashboard.actions.guidedSetup')}
|
||||
<Sparkles className="h-4 w-4" /> {translate('actions.guidedSetup')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
@@ -162,24 +170,23 @@ export default function DashboardPage() {
|
||||
<CardHeader className="space-y-3">
|
||||
<CardTitle className="flex items-center gap-2 text-xl text-slate-900">
|
||||
<Sparkles className="h-5 w-5 text-brand-rose" />
|
||||
{t('dashboard.welcomeCard.title')}
|
||||
{translate('welcomeCard.title')}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-sm text-slate-600">
|
||||
Lerne die Storytelling-Elemente kennen, wähle dein Paket und erstelle dein erstes Event mit
|
||||
geführten Schritten.
|
||||
{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>{t('dashboard.welcomeCard.body1')}</p>
|
||||
<p>{t('dashboard.welcomeCard.body2')}</p>
|
||||
<p>{translate('welcomeCard.body1')}</p>
|
||||
<p>{translate('welcomeCard.body2')}</p>
|
||||
</div>
|
||||
<Button
|
||||
size="lg"
|
||||
className="rounded-full bg-rose-500 text-white shadow-lg shadow-rose-400/40 hover:bg-rose-500/90"
|
||||
onClick={() => navigate(ADMIN_WELCOME_BASE_PATH)}
|
||||
>
|
||||
{t('dashboard.welcomeCard.cta')}
|
||||
{translate('welcomeCard.cta')}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -190,74 +197,75 @@ export default function DashboardPage() {
|
||||
<div>
|
||||
<CardTitle className="flex items-center gap-2 text-xl text-slate-900">
|
||||
<Sparkles className="h-5 w-5 text-brand-rose" />
|
||||
{t('dashboard.overview.title')}
|
||||
{translate('overview.title')}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-sm text-slate-600">
|
||||
{t('dashboard.overview.description')}
|
||||
{translate('overview.description')}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Badge className="bg-brand-rose-soft text-brand-rose">
|
||||
{activePackage?.package_name ?? t('dashboard.overview.noPackage')}
|
||||
{activePackage?.package_name ?? translate('overview.noPackage')}
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<CardContent className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
<StatCard
|
||||
label={t('dashboard.overview.stats.activeEvents')}
|
||||
label={translate('overview.stats.activeEvents')}
|
||||
value={summary?.active_events ?? publishedEvents.length}
|
||||
hint={t('dashboard.overview.stats.publishedHint', { count: publishedEvents.length })}
|
||||
hint={translate('overview.stats.publishedHint', { count: publishedEvents.length })}
|
||||
icon={<CalendarDays className="h-5 w-5 text-brand-rose" />}
|
||||
/>
|
||||
<StatCard
|
||||
label={t('dashboard.overview.stats.newPhotos')}
|
||||
label={translate('overview.stats.newPhotos')}
|
||||
value={summary?.new_photos ?? 0}
|
||||
icon={<Camera className="h-5 w-5 text-fuchsia-500" />}
|
||||
/>
|
||||
<StatCard
|
||||
label={t('dashboard.overview.stats.taskProgress')}
|
||||
label={translate('overview.stats.taskProgress')}
|
||||
value={`${Math.round(summary?.task_progress ?? 0)}%`}
|
||||
icon={<Users className="h-5 w-5 text-amber-500" />}
|
||||
/>
|
||||
<StatCard
|
||||
label={t('dashboard.overview.stats.credits')}
|
||||
value={credits}
|
||||
hint={credits <= 1 ? t('dashboard.overview.stats.lowCredits') : undefined}
|
||||
icon={<CreditCard className="h-5 w-5 text-sky-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>
|
||||
|
||||
<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">{t('dashboard.quickActions.title')}</CardTitle>
|
||||
<CardTitle className="text-xl text-slate-900">{translate('quickActions.title')}</CardTitle>
|
||||
<CardDescription className="text-sm text-slate-600">
|
||||
{t('dashboard.quickActions.description')}
|
||||
{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={t('dashboard.quickActions.createEvent.label')}
|
||||
description={t('dashboard.quickActions.createEvent.description')}
|
||||
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={t('dashboard.quickActions.moderatePhotos.label')}
|
||||
description={t('dashboard.quickActions.moderatePhotos.description')}
|
||||
label={translate('quickActions.moderatePhotos.label')}
|
||||
description={translate('quickActions.moderatePhotos.description')}
|
||||
onClick={() => navigate(ADMIN_EVENTS_PATH)}
|
||||
/>
|
||||
<QuickAction
|
||||
icon={<Users className="h-5 w-5" />}
|
||||
label={t('dashboard.quickActions.organiseTasks.label')}
|
||||
description={t('dashboard.quickActions.organiseTasks.description')}
|
||||
label={translate('quickActions.organiseTasks.label')}
|
||||
description={translate('quickActions.organiseTasks.description')}
|
||||
onClick={() => navigate(ADMIN_TASKS_PATH)}
|
||||
/>
|
||||
<QuickAction
|
||||
icon={<CreditCard className="h-5 w-5" />}
|
||||
label={t('dashboard.quickActions.manageCredits.label')}
|
||||
description={t('dashboard.quickActions.manageCredits.description')}
|
||||
icon={<Sparkles className="h-5 w-5" />}
|
||||
label={translate('quickActions.managePackages.label')}
|
||||
description={translate('quickActions.managePackages.description')}
|
||||
onClick={() => navigate(ADMIN_BILLING_PATH)}
|
||||
/>
|
||||
</CardContent>
|
||||
@@ -266,21 +274,21 @@ export default function DashboardPage() {
|
||||
<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">{t('dashboard.upcoming.title')}</CardTitle>
|
||||
<CardTitle className="text-xl text-slate-900">{translate('upcoming.title')}</CardTitle>
|
||||
<CardDescription className="text-sm text-slate-600">
|
||||
{t('dashboard.upcoming.description')}
|
||||
{translate('upcoming.description')}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button variant="outline" onClick={() => navigate(ADMIN_SETTINGS_PATH)}>
|
||||
<Settings className="h-4 w-4" />
|
||||
{t('dashboard.upcoming.settings')}
|
||||
{translate('upcoming.settings')}
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{upcomingEvents.length === 0 ? (
|
||||
<EmptyState
|
||||
message={t('dashboard.upcoming.empty.message')}
|
||||
ctaLabel={t('dashboard.upcoming.empty.cta')}
|
||||
message={translate('upcoming.empty.message')}
|
||||
ctaLabel={translate('upcoming.empty.cta')}
|
||||
onCta={() => navigate(adminPath('/events/new'))}
|
||||
/>
|
||||
) : (
|
||||
@@ -291,10 +299,10 @@ export default function DashboardPage() {
|
||||
onView={() => navigate(ADMIN_EVENT_VIEW_PATH(event.slug))}
|
||||
locale={dateLocale}
|
||||
labels={{
|
||||
live: t('dashboard.upcoming.status.live'),
|
||||
planning: t('dashboard.upcoming.status.planning'),
|
||||
open: t('common:actions.open'),
|
||||
noDate: t('dashboard.upcoming.status.noDate'),
|
||||
live: translate('upcoming.status.live'),
|
||||
planning: translate('upcoming.status.planning'),
|
||||
open: tc('actions.open'),
|
||||
noDate: translate('upcoming.status.noDate'),
|
||||
}}
|
||||
/>
|
||||
))
|
||||
@@ -309,7 +317,6 @@ export default function DashboardPage() {
|
||||
|
||||
function buildSummaryFallback(
|
||||
events: TenantEvent[],
|
||||
balance: number,
|
||||
activePackage: TenantPackageSummary | null
|
||||
): DashboardSummary {
|
||||
const activeEvents = events.filter((event) => Boolean(event.is_active || event.status === 'published'));
|
||||
@@ -319,7 +326,6 @@ function buildSummaryFallback(
|
||||
active_events: activeEvents.length,
|
||||
new_photos: totalPhotos,
|
||||
task_progress: 0,
|
||||
credit_balance: balance,
|
||||
upcoming_events: activeEvents.length,
|
||||
active_package: activePackage
|
||||
? {
|
||||
@@ -471,10 +477,3 @@ function DashboardSkeleton() {
|
||||
);
|
||||
}
|
||||
|
||||
function renderName(name: TenantEvent['name']): string {
|
||||
if (typeof name === 'string') {
|
||||
return name;
|
||||
}
|
||||
return name?.de ?? name?.en ?? Object.values(name ?? {})[0] ?? 'Unbenanntes Event';
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user