der tenant admin hat eine neue, mobil unterstützende UI, login redirect funktioniert, typescript fehler wurden bereinigt. Neue Blog Posts von ChatGPT eingebaut, übersetzt von Gemini 2.5

This commit is contained in:
Codex Agent
2025-11-05 19:27:10 +01:00
parent adb93b5f9d
commit c6ac04eb15
44 changed files with 1995 additions and 1949 deletions

View File

@@ -0,0 +1,64 @@
import React from 'react';
import { CheckCircle2, Circle } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
type ChecklistAction = {
label: string;
onClick: () => void;
disabled?: boolean;
};
type ChecklistRowProps = {
icon: React.ReactNode;
label: string;
hint?: string;
completed: boolean;
status: { complete: string; pending: string };
action?: ChecklistAction;
className?: string;
};
export function ChecklistRow({ icon, label, hint, completed, status, action, className }: ChecklistRowProps) {
return (
<div
className={cn(
'flex flex-col gap-3 rounded-2xl border border-white/20 bg-white/90 p-4 text-slate-900 transition duration-200 ease-out hover:-translate-y-0.5 hover:shadow-md hover:shadow-rose-300/20 md:flex-row md:items-center md:justify-between',
'dark:border-slate-800/70 dark:bg-slate-950/80 dark:text-slate-100',
className
)}
>
<div className="flex items-start gap-3">
<div
className={cn(
'flex h-11 w-11 items-center justify-center rounded-full text-base transition-colors duration-200',
completed ? 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-300' : 'bg-slate-200/80 text-slate-500 dark:bg-slate-800/50'
)}
>
{icon}
</div>
<div className="space-y-1">
<p className="text-sm font-semibold leading-tight">{label}</p>
{hint ? <p className="text-xs text-slate-600 dark:text-slate-400">{hint}</p> : null}
</div>
</div>
<div className="flex flex-wrap items-center gap-2">
<span
className={cn(
'inline-flex items-center gap-1 text-xs font-medium',
completed ? 'text-emerald-600 dark:text-emerald-300' : 'text-slate-500 dark:text-slate-400'
)}
>
{completed ? <CheckCircle2 className="h-4 w-4" /> : <Circle className="h-4 w-4" />}
{completed ? status.complete : status.pending}
</span>
{action ? (
<Button size="sm" variant="outline" onClick={action.onClick} disabled={action.disabled}>
{action.label}
</Button>
) : null}
</div>
</div>
);
}

View File

@@ -0,0 +1,32 @@
import React from 'react';
import { Card } from '@/components/ui/card';
import { cn } from '@/lib/utils';
export const frostedCardClass = cn(
'border border-white/15 bg-white/92 text-slate-900 shadow-lg shadow-rose-400/10 backdrop-blur-lg',
'dark:border-slate-800/70 dark:bg-slate-950/85 dark:text-slate-100'
);
type FrostedCardProps = React.ComponentProps<typeof Card>;
export function FrostedCard({ className, ...props }: FrostedCardProps) {
return <Card className={cn(frostedCardClass, className)} {...props} />;
}
type FrostedSurfaceProps = React.HTMLAttributes<HTMLDivElement> & {
asChild?: boolean;
};
export function FrostedSurface({ className, ...props }: FrostedSurfaceProps) {
return (
<div
className={cn(
'rounded-2xl border border-white/15 bg-white/88 text-slate-900 shadow-lg shadow-rose-300/10 backdrop-blur-lg',
'dark:border-slate-800/70 dark:bg-slate-950/80 dark:text-slate-100',
className
)}
{...props}
/>
);
}

View File

@@ -0,0 +1,74 @@
import React from 'react';
import { Card, CardContent } from '@/components/ui/card';
import { cn } from '@/lib/utils';
type TenantHeroCardProps = {
badge?: string;
title: string;
description?: string;
supporting?: string[];
primaryAction?: React.ReactNode;
secondaryAction?: React.ReactNode;
aside?: React.ReactNode;
children?: React.ReactNode;
className?: string;
};
export function TenantHeroCard({
badge,
title,
description,
supporting,
primaryAction,
secondaryAction,
aside,
children,
className,
}: TenantHeroCardProps) {
return (
<Card
className={cn(
'relative overflow-hidden border border-white/15 bg-white/95 text-slate-900 shadow-2xl shadow-rose-400/10 backdrop-blur-xl',
'dark:border-white/10 dark:bg-slate-900/95 dark:text-slate-100',
className
)}
>
<div
aria-hidden
className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_at_top,_rgba(255,137,170,0.3),_transparent_60%),radial-gradient(ellipse_at_bottom,_rgba(99,102,241,0.3),_transparent_65%)] motion-safe:animate-[aurora_18s_ease-in-out_infinite]"
/>
<div aria-hidden className="absolute inset-0 bg-gradient-to-br from-slate-950/80 via-slate-900/20 to-transparent mix-blend-overlay" />
<CardContent className="relative z-10 flex flex-col gap-8 px-6 py-8 lg:flex-row lg:items-start lg:justify-between lg:px-10 lg:py-12">
<div className="max-w-2xl space-y-6 text-white">
{badge ? (
<span className="inline-flex items-center gap-2 rounded-full border border-white/40 bg-white/15 px-4 py-1 text-xs font-semibold uppercase tracking-[0.35em]">
{badge}
</span>
) : null}
<div className="space-y-3">
<h1 className="font-display text-3xl tracking-tight sm:text-4xl">{title}</h1>
{description ? <p className="text-sm text-white/80 sm:text-base">{description}</p> : null}
{supporting?.map((paragraph) => (
<p key={paragraph} className="text-sm text-white/75 sm:text-base">
{paragraph}
</p>
))}
{children}
</div>
{(primaryAction || secondaryAction) && (
<div className="flex flex-wrap gap-2">
{primaryAction}
{secondaryAction}
</div>
)}
</div>
{aside ? <div className="w-full max-w-sm">{aside}</div> : null}
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,6 @@
export { TenantHeroCard } from './hero-card';
export { FrostedCard, FrostedSurface, frostedCardClass } from './frosted-surface';
export { ChecklistRow } from './checklist-row';
export type { ChecklistStep } from './onboarding-checklist-card';
export { TenantOnboardingChecklistCard } from './onboarding-checklist-card';

View File

@@ -0,0 +1,94 @@
import React from 'react';
import type { LucideIcon } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';
import { FrostedCard } from './frosted-surface';
import { ChecklistRow } from './checklist-row';
export type ChecklistStep = {
key: string;
title: string;
description: string;
done: boolean;
ctaLabel?: string | null;
onAction?: () => void;
icon: LucideIcon;
};
type TenantOnboardingChecklistCardProps = {
title: string;
description?: string;
steps: ChecklistStep[];
completedLabel: string;
pendingLabel: string;
completionPercent: number;
completedCount: number;
totalCount: number;
emptyCopy?: string;
fallbackActionLabel?: string;
};
export function TenantOnboardingChecklistCard({
title,
description,
steps,
completedLabel,
pendingLabel,
completionPercent,
completedCount,
totalCount,
emptyCopy,
fallbackActionLabel,
}: TenantOnboardingChecklistCardProps) {
return (
<FrostedCard className="relative overflow-hidden">
<CardHeader className="relative flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div
aria-hidden
className="pointer-events-none absolute -top-12 -right-16 h-32 w-32 rounded-full bg-rose-200/40 blur-3xl"
/>
<div className="relative">
<CardTitle>{title}</CardTitle>
{description ? <CardDescription>{description}</CardDescription> : null}
</div>
<Badge className="bg-brand-rose-soft text-brand-rose">
{completionPercent}% · {completedCount}/{totalCount}
</Badge>
</CardHeader>
<CardContent className="space-y-4">
<Progress value={completionPercent} className="h-2 bg-rose-100 motion-safe:animate-pulse" />
<div className="space-y-3">
{steps.map((step) => {
const Icon = step.icon;
return (
<ChecklistRow
key={step.key}
icon={<Icon className="h-5 w-5" />}
label={step.title}
hint={step.description}
completed={step.done}
status={{ complete: completedLabel, pending: pendingLabel }}
action={
step.done || (!step.ctaLabel && !fallbackActionLabel)
? undefined
: {
label: step.ctaLabel ?? fallbackActionLabel ?? '',
onClick: () => step.onAction?.(),
disabled: !step.onAction,
}
}
/>
);
})}
</div>
{steps.length === 0 && emptyCopy ? (
<p className="text-sm text-slate-600 dark:text-slate-300">{emptyCopy}</p>
) : null}
</CardContent>
</FrostedCard>
);
}