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

@@ -1,4 +1,4 @@
import React from "react";
import React from "react";
import { useNavigate, useLocation } from "react-router-dom";
import { useTranslation } from "react-i18next";
import {
@@ -8,6 +8,7 @@ import {
ArrowLeft,
CreditCard,
AlertTriangle,
Info,
Loader2,
} from "lucide-react";
@@ -17,20 +18,22 @@ import {
OnboardingCTAList,
useOnboardingProgress,
} from "..";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { FrostedSurface } from "../../components/tenant";
import { ADMIN_BILLING_PATH, ADMIN_WELCOME_EVENT_PATH, ADMIN_WELCOME_PACKAGES_PATH } from "../../constants";
import { useTenantPackages } from "../hooks/useTenantPackages";
import {
assignFreeTenantPackage,
createTenantPaddleCheckout,
} from "../../api";
import { cn } from "@/lib/utils";
type PaddleCheckoutProps = {
packageId: number;
onSuccess: () => void;
t: ReturnType<typeof useTranslation>["t"];
className?: string;
};
function useLocaleFormats(locale: string) {
@@ -69,7 +72,7 @@ function humanizeFeature(t: ReturnType<typeof useTranslation>["t"], key: string)
.join(" ");
}
function PaddleCheckout({ packageId, onSuccess, t }: PaddleCheckoutProps) {
function PaddleCheckout({ packageId, onSuccess, t, className }: PaddleCheckoutProps) {
const [status, setStatus] = React.useState<'idle' | 'processing' | 'success' | 'error'>('idle');
const [error, setError] = React.useState<string | null>(null);
@@ -89,14 +92,32 @@ function PaddleCheckout({ packageId, onSuccess, t }: PaddleCheckoutProps) {
}, [packageId, onSuccess, t]);
return (
<div className="space-y-3 rounded-2xl border border-brand-rose-soft bg-white/85 p-6 shadow-inner">
<p className="text-sm font-medium text-brand-slate">{t('summary.paddle.heading')}</p>
{error && (
<Alert variant="destructive">
<AlertTitle>{t('summary.paddle.errorTitle')}</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
<FrostedSurface
className={cn(
"space-y-4 rounded-2xl border border-white/20 p-6 text-slate-900 shadow-md shadow-rose-200/20 dark:border-slate-800/70 dark:bg-slate-950/80 dark:text-slate-100",
className
)}
>
<div className="space-y-1">
<p className="text-xs font-semibold uppercase tracking-[0.35em] text-rose-400 dark:text-rose-200">
{t('summary.paddle.sectionTitle')}
</p>
<p className="text-base font-semibold text-slate-900 dark:text-slate-100">
{t('summary.paddle.heading')}
</p>
</div>
{error ? (
<div
role="alert"
className="flex items-start gap-3 rounded-2xl border border-rose-300/40 bg-rose-100/15 p-4 text-sm text-rose-700 shadow-inner shadow-rose-200/20 dark:border-rose-500/30 dark:bg-rose-500/10 dark:text-rose-200"
>
<AlertTriangle className="size-4 shrink-0 text-current" />
<div className="space-y-1">
<p className="text-sm font-semibold uppercase tracking-wide">{t('summary.paddle.errorTitle')}</p>
<p className="text-sm text-rose-600/80 dark:text-rose-200/80">{error}</p>
</div>
</div>
) : null}
<Button
size="lg"
className="w-full rounded-full bg-brand-rose text-white shadow-lg shadow-rose-400/40 hover:bg-[var(--brand-rose-strong)] disabled:opacity-60"
@@ -115,8 +136,8 @@ function PaddleCheckout({ packageId, onSuccess, t }: PaddleCheckoutProps) {
</>
)}
</Button>
<p className="text-xs text-brand-navy/70">{t('summary.paddle.hint')}</p>
</div>
<p className="text-xs text-slate-600 dark:text-slate-400">{t('summary.paddle.hint')}</p>
</FrostedSurface>
);
}
@@ -216,101 +237,177 @@ export default function WelcomeOrderSummaryPage() {
icon={Receipt}
>
{packagesState.status === "loading" && (
<div className="flex items-center gap-3 rounded-2xl bg-slate-50 p-6 text-sm text-brand-navy/80">
<CreditCard className="size-5 text-brand-rose" />
<FrostedSurface className="flex items-center gap-3 rounded-2xl border border-white/20 p-6 text-sm text-slate-600 shadow-md shadow-rose-200/20 dark:border-slate-800/70 dark:bg-slate-950/80 dark:text-slate-300">
<Loader2 className="size-5 animate-spin text-rose-400" />
{t("summary.state.loading")}
</div>
</FrostedSurface>
)}
{packagesState.status === "error" && (
<Alert variant="destructive">
<AlertTriangle className="size-4" />
<AlertTitle>{t("summary.state.errorTitle")}</AlertTitle>
<AlertDescription>
{packagesState.message ?? t("summary.state.errorDescription")}
</AlertDescription>
</Alert>
<FrostedSurface
role="alert"
className="flex items-start gap-3 rounded-2xl border border-rose-300/40 bg-rose-100/15 p-6 text-sm text-rose-700 shadow-md shadow-rose-200/30 dark:border-rose-500/30 dark:bg-rose-500/10 dark:text-rose-200"
>
<AlertTriangle className="size-4 shrink-0 text-current" />
<div className="space-y-1">
<p className="text-sm font-semibold uppercase tracking-wide">
{t("summary.state.errorTitle")}
</p>
<p className="text-sm text-rose-600/80 dark:text-rose-200/80">
{packagesState.message ?? t("summary.state.errorDescription")}
</p>
</div>
</FrostedSurface>
)}
{packagesState.status === "success" && !packageDetails && (
<Alert>
<AlertTitle>{t("summary.state.missingTitle")}</AlertTitle>
<AlertDescription>{t("summary.state.missingDescription")}</AlertDescription>
</Alert>
<FrostedSurface
role="alert"
className="flex items-start gap-3 rounded-2xl border border-white/20 bg-white/15 p-6 text-sm text-slate-800 shadow-md shadow-rose-200/20 dark:border-slate-800/70 dark:bg-slate-950/80 dark:text-slate-200"
>
<Info className="size-4 shrink-0 text-current" />
<div className="space-y-1">
<p className="text-sm font-semibold uppercase tracking-wide">
{t("summary.state.missingTitle")}
</p>
<p className="text-sm text-slate-700/90 dark:text-slate-300/80">
{t("summary.state.missingDescription")}
</p>
</div>
</FrostedSurface>
)}
{packagesState.status === "success" && packageDetails && (
<div className="grid gap-4">
<div className="rounded-3xl border border-brand-rose-soft bg-brand-card p-6 shadow-md shadow-rose-100/40">
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div>
<p className="text-xs uppercase tracking-[0.35em] text-brand-rose">
{t(isSubscription ? "summary.details.subscription" : "summary.details.creditPack")}
</p>
<h3 className="text-2xl font-semibold text-brand-slate">{packageDetails.name}</h3>
</div>
{priceText && (
<Badge className="rounded-full bg-rose-100 px-4 py-2 text-base font-semibold text-rose-600">
{priceText}
</Badge>
)}
</div>
<dl className="mt-6 grid gap-3 text-sm text-brand-navy/80 md:grid-cols-2">
<div>
<dt className="font-semibold text-brand-slate">{t("summary.details.section.photosTitle")}</dt>
<dd>
{packageDetails.max_photos
? t("summary.details.section.photosValue", {
count: packageDetails.max_photos,
days: packageDetails.gallery_days ?? t("summary.details.infinity"),
})
: t("summary.details.section.photosUnlimited")}
</dd>
</div>
<div>
<dt className="font-semibold text-brand-slate">{t("summary.details.section.guestsTitle")}</dt>
<dd>
{packageDetails.max_guests
? t("summary.details.section.guestsValue", { count: packageDetails.max_guests })
: t("summary.details.section.guestsUnlimited")}
</dd>
</div>
<div>
<dt className="font-semibold text-brand-slate">{t("summary.details.section.featuresTitle")}</dt>
<dd>{featuresList.length ? featuresList.join(", ") : t("summary.details.section.featuresNone")}</dd>
</div>
<div>
<dt className="font-semibold text-brand-slate">{t("summary.details.section.statusTitle")}</dt>
<dd>{activePackage ? t("summary.details.section.statusActive") : t("summary.details.section.statusInactive")}</dd>
</div>
</dl>
{detailBadges.length > 0 && (
<div className="mt-4 flex flex-wrap gap-2 text-xs uppercase tracking-wide text-brand-rose">
{detailBadges.map((badge) => (
<span key={badge} className="rounded-full bg-brand-rose-soft px-3 py-1">
{badge}
<FrostedSurface className="relative overflow-hidden rounded-3xl border border-white/20 p-6 text-white shadow-xl shadow-rose-300/25 dark:border-slate-800/70 dark:bg-slate-950/85">
<div
aria-hidden
className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_at_top,_rgba(255,137,170,0.35),_transparent_65%),radial-gradient(ellipse_at_bottom,_rgba(99,102,241,0.3),_transparent_65%)]"
/>
<div aria-hidden className="absolute inset-0 bg-gradient-to-br from-slate-950/65 via-slate-900/10 to-transparent mix-blend-overlay" />
<div className="relative z-10 flex flex-col gap-6">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="space-y-3">
<Badge className="w-fit rounded-full border border-white/40 bg-white/20 px-4 py-1 text-[10px] font-semibold uppercase tracking-[0.4em] text-white shadow-inner shadow-white/20">
{t(isSubscription ? "summary.details.subscription" : "summary.details.creditPack")}
</Badge>
<h3 className="font-display text-2xl font-semibold tracking-tight text-white md:text-3xl">
{packageDetails.name}
</h3>
</div>
<div className="flex flex-col items-start gap-2 text-left sm:items-end sm:text-right">
{priceText ? (
<Badge className="rounded-full border border-white/40 bg-white/15 px-4 py-2 text-base font-semibold text-white shadow-inner shadow-white/20">
{priceText}
</Badge>
) : null}
<span className="text-xs font-semibold uppercase tracking-[0.35em] text-white/70">
{activePackage
? t("summary.details.section.statusActive")
: t("summary.details.section.statusInactive")}
</span>
))}
</div>
</div>
)}
</div>
<dl className="grid gap-4 text-sm text-white/85 md:grid-cols-2">
<div className="space-y-1">
<dt className="text-xs font-semibold uppercase tracking-wide text-white/70">
{t("summary.details.section.photosTitle")}
</dt>
<dd>
{packageDetails.max_photos
? t("summary.details.section.photosValue", {
count: packageDetails.max_photos,
days: packageDetails.gallery_days ?? t("summary.details.infinity"),
})
: t("summary.details.section.photosUnlimited")}
</dd>
</div>
<div className="space-y-1">
<dt className="text-xs font-semibold uppercase tracking-wide text-white/70">
{t("summary.details.section.guestsTitle")}
</dt>
<dd>
{packageDetails.max_guests
? t("summary.details.section.guestsValue", { count: packageDetails.max_guests })
: t("summary.details.section.guestsUnlimited")}
</dd>
</div>
<div className="space-y-1">
<dt className="text-xs font-semibold uppercase tracking-wide text-white/70">
{t("summary.details.section.featuresTitle")}
</dt>
<dd>
{featuresList.length ? (
<div className="flex flex-wrap gap-2">
{featuresList.map((feature) => (
<Badge
key={feature}
className="rounded-full border border-white/30 bg-white/20 px-3 py-1 text-[11px] font-medium uppercase tracking-wide text-white"
>
{feature}
</Badge>
))}
</div>
) : (
<span className="text-white/70">{t("summary.details.section.featuresNone")}</span>
)}
</dd>
</div>
<div className="space-y-1">
<dt className="text-xs font-semibold uppercase tracking-wide text-white/70">
{t("summary.details.section.statusTitle")}
</dt>
<dd>
<Badge className="rounded-full border border-white/30 bg-white/20 px-3 py-1 text-[11px] font-medium uppercase tracking-wide text-white">
{activePackage
? t("summary.details.section.statusActive")
: t("summary.details.section.statusInactive")}
</Badge>
</dd>
</div>
</dl>
{detailBadges.length > 0 ? (
<div className="flex flex-wrap gap-2">
{detailBadges.map((badge) => (
<Badge
key={badge}
className="rounded-full border border-white/30 bg-white/20 px-3 py-1 text-[11px] font-medium uppercase tracking-wide text-white"
>
{badge}
</Badge>
))}
</div>
) : null}
</div>
</FrostedSurface>
{!activePackage && (
<Alert className="rounded-2xl border-brand-rose-soft bg-brand-rose-soft/60 text-rose-700">
<ShieldCheck className="size-4" />
<AlertTitle>{t("summary.status.pendingTitle")}</AlertTitle>
<AlertDescription>{t("summary.status.pendingDescription")}</AlertDescription>
</Alert>
<FrostedSurface
role="status"
className="flex items-start gap-3 rounded-2xl border border-amber-200/40 bg-amber-100/15 p-5 text-sm text-amber-800 shadow-md shadow-amber-200/20 dark:border-amber-300/30 dark:bg-amber-500/10 dark:text-amber-100"
>
<ShieldCheck className="size-4 shrink-0 text-current" />
<div className="space-y-1">
<p className="text-sm font-semibold uppercase tracking-wide">
{t("summary.status.pendingTitle")}
</p>
<p className="text-sm text-amber-700/90 dark:text-amber-100/80">
{t("summary.status.pendingDescription")}
</p>
</div>
</FrostedSurface>
)}
{packageDetails.price === 0 && (
<div className="rounded-2xl border border-emerald-200 bg-emerald-50/40 p-6">
<p className="text-sm text-emerald-700">{t("summary.free.description")}</p>
<FrostedSurface className="space-y-3 rounded-2xl border border-emerald-200/40 bg-emerald-100/15 p-6 text-sm text-emerald-900 shadow-md shadow-emerald-200/20 dark:border-emerald-400/30 dark:bg-emerald-500/10 dark:text-emerald-100">
<p>{t("summary.free.description")}</p>
{freeAssignStatus === "success" ? (
<Alert className="mt-4 border-emerald-200 bg-white text-emerald-600">
<AlertTitle>{t("summary.free.successTitle")}</AlertTitle>
<AlertDescription>{t("summary.free.successDescription")}</AlertDescription>
</Alert>
<div className="space-y-1 rounded-xl border border-emerald-200/60 bg-emerald-50/30 p-4 text-sm text-emerald-800 dark:border-emerald-400/30 dark:bg-emerald-500/20 dark:text-emerald-50">
<p className="font-semibold">{t("summary.free.successTitle")}</p>
<p>{t("summary.free.successDescription")}</p>
</div>
) : (
<Button
onClick={async () => {
@@ -333,7 +430,7 @@ export default function WelcomeOrderSummaryPage() {
}
}}
disabled={freeAssignStatus === "loading"}
className="mt-4 rounded-full bg-brand-teal text-white shadow-md shadow-emerald-300/40 hover:opacity-90"
className="mt-2 w-full rounded-full bg-gradient-to-r from-[#34d399] via-[#10b981] to-[#059669] text-white shadow-lg shadow-emerald-300/30 hover:from-[#22c783] hover:via-[#0fa776] hover:to-[#047857]"
>
{freeAssignStatus === "loading" ? (
<>
@@ -345,37 +442,37 @@ export default function WelcomeOrderSummaryPage() {
)}
</Button>
)}
{freeAssignStatus === "error" && freeAssignError && (
<Alert variant="destructive" className="mt-3">
<AlertTitle>{t("summary.free.failureTitle")}</AlertTitle>
<AlertDescription>{freeAssignError}</AlertDescription>
</Alert>
)}
</div>
{freeAssignStatus === "error" && freeAssignError ? (
<div
role="alert"
className="rounded-xl border border-rose-300/40 bg-rose-100/15 p-4 text-sm text-rose-700 shadow-inner shadow-rose-200/20 dark:border-rose-500/30 dark:bg-rose-500/10 dark:text-rose-200"
>
<p className="font-semibold">{t("summary.free.failureTitle")}</p>
<p>{freeAssignError}</p>
</div>
) : null}
</FrostedSurface>
)}
{requiresPayment && (
<div className="space-y-4">
<h4 className="text-lg font-semibold text-brand-slate">{t('summary.paddle.sectionTitle')}</h4>
<PaddleCheckout
packageId={packageDetails.id}
onSuccess={() => {
markStep({ packageSelected: true });
navigate(ADMIN_WELCOME_EVENT_PATH, { replace: true });
}}
t={t}
/>
</div>
<PaddleCheckout
packageId={packageDetails.id}
onSuccess={() => {
markStep({ packageSelected: true });
navigate(ADMIN_WELCOME_EVENT_PATH, { replace: true });
}}
t={t}
/>
)}
<div className="flex flex-col gap-3 rounded-2xl bg-brand-card p-6 shadow-inner shadow-rose-100/40">
<h4 className="text-lg font-semibold text-brand-slate">{t("summary.nextStepsTitle")}</h4>
<ol className="list-decimal space-y-2 pl-5 text-sm text-brand-navy/80">
<FrostedSurface className="flex flex-col gap-3 rounded-2xl border border-white/20 p-6 text-slate-900 shadow-inner shadow-rose-200/10 dark:border-slate-800/70 dark:bg-slate-950/80 dark:text-slate-100">
<h4 className="text-lg font-semibold text-slate-900 dark:text-slate-100">{t("summary.nextStepsTitle")}</h4>
<ol className="list-decimal space-y-2 pl-5 text-sm text-slate-600 dark:text-slate-300">
{nextSteps.map((step, index) => (
<li key={`${step}-${index}`}>{step}</li>
))}
</ol>
</div>
</FrostedSurface>
</div>
)}
</WelcomeStepCard>