überarbeitung des event-admins fortgesetzt
This commit is contained in:
@@ -1,117 +0,0 @@
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ClipboardCheck, Sparkles, Globe, ArrowRight } from "lucide-react";
|
||||
|
||||
import {
|
||||
TenantWelcomeLayout,
|
||||
WelcomeStepCard,
|
||||
OnboardingCTAList,
|
||||
useOnboardingProgress,
|
||||
} from "..";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ADMIN_EVENT_CREATE_PATH, ADMIN_EVENTS_PATH, ADMIN_HOME_PATH } from "../../constants";
|
||||
import { FrostedSurface } from "../../components/tenant";
|
||||
|
||||
export default function WelcomeEventSetupPage() {
|
||||
const navigate = useNavigate();
|
||||
const { markStep } = useOnboardingProgress();
|
||||
const { t } = useTranslation("onboarding");
|
||||
|
||||
React.useEffect(() => {
|
||||
markStep({ lastStep: "event-setup" });
|
||||
}, [markStep]);
|
||||
|
||||
return (
|
||||
<TenantWelcomeLayout
|
||||
eyebrow={t("eventSetup.layout.eyebrow")}
|
||||
title={t("eventSetup.layout.title")}
|
||||
subtitle={t("eventSetup.layout.subtitle")}
|
||||
>
|
||||
<WelcomeStepCard
|
||||
step={4}
|
||||
totalSteps={4}
|
||||
title={t("eventSetup.step.title")}
|
||||
description={t("eventSetup.step.description")}
|
||||
icon={ClipboardCheck}
|
||||
>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{[
|
||||
{
|
||||
id: "story",
|
||||
title: t("eventSetup.tiles.story.title"),
|
||||
copy: t("eventSetup.tiles.story.copy"),
|
||||
icon: Sparkles,
|
||||
},
|
||||
{
|
||||
id: "team",
|
||||
title: t("eventSetup.tiles.team.title"),
|
||||
copy: t("eventSetup.tiles.team.copy"),
|
||||
icon: Globe,
|
||||
},
|
||||
{
|
||||
id: "launch",
|
||||
title: t("eventSetup.tiles.launch.title"),
|
||||
copy: t("eventSetup.tiles.launch.copy"),
|
||||
icon: ArrowRight,
|
||||
},
|
||||
].map((item) => (
|
||||
<FrostedSurface
|
||||
key={item.id}
|
||||
className="flex flex-col gap-3 border border-white/20 p-5 text-slate-900 shadow-md shadow-rose-200/20 dark:border-slate-800/70 dark:bg-slate-950/80 dark:text-slate-100"
|
||||
>
|
||||
<span className="flex size-10 items-center justify-center rounded-full bg-rose-100/80 text-rose-500 shadow-inner shadow-rose-200/60 dark:bg-rose-500/20 dark:text-rose-200">
|
||||
<item.icon className="size-5" />
|
||||
</span>
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-slate-100">{item.title}</h3>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-400">{item.copy}</p>
|
||||
</FrostedSurface>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<FrostedSurface className="mt-6 flex flex-col items-start gap-3 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">
|
||||
<h4 className="text-lg font-semibold text-rose-400 dark:text-rose-200">{t("eventSetup.cta.heading")}</h4>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-400">{t("eventSetup.cta.description")}</p>
|
||||
<Button
|
||||
size="lg"
|
||||
className="mt-2 rounded-full bg-gradient-to-r from-[#ff5f87] via-[#ec4899] to-[#6366f1] text-white shadow-lg shadow-rose-300/30 hover:from-[#ff4470] hover:via-[#ec4899] hover:to-[#4f46e5]"
|
||||
onClick={() => {
|
||||
markStep({ lastStep: "event-create-intent" });
|
||||
navigate(ADMIN_EVENT_CREATE_PATH);
|
||||
}}
|
||||
>
|
||||
{t("eventSetup.cta.button")}
|
||||
<ArrowRight className="ml-2 size-4" />
|
||||
</Button>
|
||||
</FrostedSurface>
|
||||
</WelcomeStepCard>
|
||||
|
||||
<OnboardingCTAList
|
||||
actions={[
|
||||
{
|
||||
id: "back",
|
||||
label: t("eventSetup.actions.back.label"),
|
||||
description: t("eventSetup.actions.back.description"),
|
||||
buttonLabel: t("eventSetup.actions.back.button"),
|
||||
onClick: () => navigate(-1),
|
||||
variant: "secondary",
|
||||
},
|
||||
{
|
||||
id: "dashboard",
|
||||
label: t("eventSetup.actions.dashboard.label"),
|
||||
description: t("eventSetup.actions.dashboard.description"),
|
||||
buttonLabel: t("eventSetup.actions.dashboard.button"),
|
||||
onClick: () => navigate(ADMIN_HOME_PATH),
|
||||
},
|
||||
{
|
||||
id: "events",
|
||||
label: t("eventSetup.actions.events.label"),
|
||||
description: t("eventSetup.actions.events.description"),
|
||||
buttonLabel: t("eventSetup.actions.events.button"),
|
||||
onClick: () => navigate(ADMIN_EVENTS_PATH),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</TenantWelcomeLayout>
|
||||
);
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Sparkles, Users, Camera, CalendarDays, ChevronRight, CreditCard, UserPlus, Palette } from "lucide-react";
|
||||
|
||||
import {
|
||||
TenantWelcomeLayout,
|
||||
WelcomeHero,
|
||||
OnboardingHighlightsGrid,
|
||||
OnboardingCTAList,
|
||||
useOnboardingProgress,
|
||||
} from "..";
|
||||
import { ADMIN_WELCOME_PACKAGES_PATH, ADMIN_EVENTS_PATH } from "../../constants";
|
||||
import { ChecklistRow, FrostedSurface } from "../../components/tenant";
|
||||
|
||||
export default function WelcomeLandingPage() {
|
||||
const navigate = useNavigate();
|
||||
const { markStep, progress } = useOnboardingProgress();
|
||||
const { t } = useTranslation("onboarding");
|
||||
|
||||
React.useEffect(() => {
|
||||
markStep({ welcomeSeen: true, lastStep: "landing" });
|
||||
}, [markStep]);
|
||||
|
||||
const progressStatus = React.useMemo(
|
||||
() => ({
|
||||
complete: t("landingProgress.status.complete"),
|
||||
pending: t("landingProgress.status.pending"),
|
||||
}),
|
||||
[t]
|
||||
);
|
||||
|
||||
const progressSteps = React.useMemo(
|
||||
() => [
|
||||
{
|
||||
key: "package",
|
||||
icon: <CreditCard className="h-5 w-5" />,
|
||||
label: t("landingProgress.steps.package.title"),
|
||||
hint: t("landingProgress.steps.package.hint"),
|
||||
completed: progress.packageSelected,
|
||||
},
|
||||
{
|
||||
key: "invite",
|
||||
icon: <UserPlus className="h-5 w-5" />,
|
||||
label: t("landingProgress.steps.invite.title"),
|
||||
hint: t("landingProgress.steps.invite.hint"),
|
||||
completed: progress.inviteCreated,
|
||||
},
|
||||
{
|
||||
key: "branding",
|
||||
icon: <Palette className="h-5 w-5" />,
|
||||
label: t("landingProgress.steps.branding.title"),
|
||||
hint: t("landingProgress.steps.branding.hint"),
|
||||
completed: progress.brandingConfigured,
|
||||
},
|
||||
],
|
||||
[progress.packageSelected, progress.inviteCreated, progress.brandingConfigured, t]
|
||||
);
|
||||
|
||||
return (
|
||||
<TenantWelcomeLayout
|
||||
eyebrow={t("layout.eyebrow")}
|
||||
title={t("layout.title")}
|
||||
subtitle={t("layout.subtitle")}
|
||||
footer={
|
||||
<>
|
||||
<span className="text-brand-navy/80">{t("layout.alreadyFamiliar")}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-1 text-sm font-semibold text-brand-rose hover:text-[var(--brand-rose-strong)]"
|
||||
onClick={() => navigate(ADMIN_EVENTS_PATH)}
|
||||
>
|
||||
{t("layout.jumpToDashboard")}
|
||||
<ChevronRight className="size-4" />
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<WelcomeHero
|
||||
eyebrow={t("hero.eyebrow")}
|
||||
title={t("hero.title")}
|
||||
scriptTitle={t("hero.scriptTitle")}
|
||||
description={t("hero.description")}
|
||||
actions={[
|
||||
{
|
||||
label: t("hero.primary.label"),
|
||||
onClick: () => navigate(ADMIN_WELCOME_PACKAGES_PATH),
|
||||
icon: Sparkles,
|
||||
},
|
||||
{
|
||||
label: t("hero.secondary.label"),
|
||||
onClick: () => navigate(ADMIN_EVENTS_PATH),
|
||||
icon: CalendarDays,
|
||||
variant: "outline",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<FrostedSurface className="space-y-4 rounded-3xl 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">
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.35em] text-rose-300 dark:text-rose-200">
|
||||
{t("landingProgress.eyebrow")}
|
||||
</p>
|
||||
<h2 className="text-xl font-semibold text-slate-900 dark:text-slate-100">
|
||||
{t("landingProgress.title")}
|
||||
</h2>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300">
|
||||
{t("landingProgress.description")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
{progressSteps.map((step) => (
|
||||
<ChecklistRow
|
||||
key={step.key}
|
||||
icon={step.icon}
|
||||
label={step.label}
|
||||
hint={step.hint}
|
||||
completed={step.completed}
|
||||
status={progressStatus}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</FrostedSurface>
|
||||
|
||||
<OnboardingHighlightsGrid
|
||||
items={[
|
||||
{
|
||||
id: "gallery",
|
||||
icon: Camera,
|
||||
title: t("highlights.gallery.title"),
|
||||
description: t("highlights.gallery.description"),
|
||||
badge: t("highlights.gallery.badge"),
|
||||
},
|
||||
{
|
||||
id: "team",
|
||||
icon: Users,
|
||||
title: t("highlights.team.title"),
|
||||
description: t("highlights.team.description"),
|
||||
},
|
||||
{
|
||||
id: "story",
|
||||
icon: Sparkles,
|
||||
title: t("highlights.story.title"),
|
||||
description: t("highlights.story.description"),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<OnboardingCTAList
|
||||
actions={[
|
||||
{
|
||||
id: "choose-package",
|
||||
label: t("ctaList.choosePackage.label"),
|
||||
description: t("ctaList.choosePackage.description"),
|
||||
onClick: () => navigate(ADMIN_WELCOME_PACKAGES_PATH),
|
||||
icon: Sparkles,
|
||||
buttonLabel: t("ctaList.choosePackage.button"),
|
||||
},
|
||||
{
|
||||
id: "create-event",
|
||||
label: t("ctaList.createEvent.label"),
|
||||
description: t("ctaList.createEvent.description"),
|
||||
onClick: () => navigate(ADMIN_EVENTS_PATH),
|
||||
icon: CalendarDays,
|
||||
buttonLabel: t("ctaList.createEvent.button"),
|
||||
variant: "secondary",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</TenantWelcomeLayout>
|
||||
);
|
||||
}
|
||||
@@ -1,511 +0,0 @@
|
||||
import React from "react";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Receipt,
|
||||
ShieldCheck,
|
||||
ArrowRight,
|
||||
ArrowLeft,
|
||||
CreditCard,
|
||||
AlertTriangle,
|
||||
Info,
|
||||
Loader2,
|
||||
} from "lucide-react";
|
||||
|
||||
import {
|
||||
TenantWelcomeLayout,
|
||||
WelcomeStepCard,
|
||||
OnboardingCTAList,
|
||||
useOnboardingProgress,
|
||||
} from "..";
|
||||
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) {
|
||||
const currencyFormatter = React.useMemo(
|
||||
() =>
|
||||
new Intl.NumberFormat(locale, {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
minimumFractionDigits: 0,
|
||||
}),
|
||||
[locale]
|
||||
);
|
||||
|
||||
const dateFormatter = React.useMemo(
|
||||
() =>
|
||||
new Intl.DateTimeFormat(locale, {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
}),
|
||||
[locale]
|
||||
);
|
||||
|
||||
return { currencyFormatter, dateFormatter };
|
||||
}
|
||||
|
||||
function humanizeFeature(t: ReturnType<typeof useTranslation>["t"], key: string): string {
|
||||
const translationKey = `summary.details.features.${key}`;
|
||||
const translated = t(translationKey);
|
||||
if (translated !== translationKey) {
|
||||
return translated;
|
||||
}
|
||||
return key
|
||||
.split("_")
|
||||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
const handleCheckout = React.useCallback(async () => {
|
||||
try {
|
||||
setStatus('processing');
|
||||
setError(null);
|
||||
const { checkout_url } = await createTenantPaddleCheckout(packageId);
|
||||
window.open(checkout_url, '_blank', 'noopener');
|
||||
setStatus('success');
|
||||
onSuccess();
|
||||
} catch (err) {
|
||||
console.error('[Onboarding] Paddle checkout failed', err);
|
||||
setStatus('error');
|
||||
setError(err instanceof Error ? err.message : t('summary.paddle.genericError'));
|
||||
}
|
||||
}, [packageId, onSuccess, t]);
|
||||
|
||||
return (
|
||||
<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"
|
||||
disabled={status === 'processing'}
|
||||
onClick={handleCheckout}
|
||||
>
|
||||
{status === 'processing' ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 size-4 animate-spin" />
|
||||
{t('summary.paddle.processing')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CreditCard className="mr-2 size-4" />
|
||||
{t('summary.paddle.cta')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<p className="text-xs text-slate-600 dark:text-slate-400">{t('summary.paddle.hint')}</p>
|
||||
</FrostedSurface>
|
||||
);
|
||||
}
|
||||
|
||||
export default function WelcomeOrderSummaryPage() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { progress, markStep } = useOnboardingProgress();
|
||||
const packagesState = useTenantPackages();
|
||||
const { t, i18n } = useTranslation("onboarding");
|
||||
const locale = i18n.language?.startsWith("en") ? "en-GB" : "de-DE";
|
||||
const { currencyFormatter } = useLocaleFormats(locale);
|
||||
|
||||
const packageIdFromState =
|
||||
typeof location.state === "object" && location.state !== null && "packageId" in location.state
|
||||
? (location.state as { packageId?: number | string | null }).packageId
|
||||
: undefined;
|
||||
const selectedPackageId = progress.selectedPackage?.id ?? packageIdFromState ?? null;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!selectedPackageId && packagesState.status !== "loading") {
|
||||
navigate(ADMIN_WELCOME_PACKAGES_PATH, { replace: true });
|
||||
}
|
||||
}, [selectedPackageId, packagesState.status, navigate]);
|
||||
|
||||
React.useEffect(() => {
|
||||
markStep({ lastStep: "summary" });
|
||||
}, [markStep]);
|
||||
|
||||
const packageDetails =
|
||||
packagesState.status === "success" && selectedPackageId
|
||||
? packagesState.catalog.find((pkg) => pkg.id === selectedPackageId)
|
||||
: null;
|
||||
|
||||
const activePackage =
|
||||
packagesState.status === "success"
|
||||
? packagesState.purchasedPackages.find((pkg) => pkg.package_id === selectedPackageId)
|
||||
: null;
|
||||
|
||||
const isSubscription = Boolean(packageDetails?.features?.subscription);
|
||||
const requiresPayment = Boolean(packageDetails && packageDetails.price > 0);
|
||||
|
||||
const [freeAssignStatus, setFreeAssignStatus] = React.useState<"idle" | "loading" | "success" | "error">("idle");
|
||||
const [freeAssignError, setFreeAssignError] = React.useState<string | null>(null);
|
||||
|
||||
const priceText =
|
||||
progress.selectedPackage?.priceText ??
|
||||
(packageDetails && typeof packageDetails.price === "number"
|
||||
? currencyFormatter.format(packageDetails.price)
|
||||
: null);
|
||||
|
||||
const detailBadges = React.useMemo(() => {
|
||||
if (!packageDetails) {
|
||||
return [] as string[];
|
||||
}
|
||||
const badges: string[] = [];
|
||||
if (packageDetails.max_photos) {
|
||||
badges.push(t("summary.details.photos", { count: packageDetails.max_photos }));
|
||||
}
|
||||
if (packageDetails.gallery_days) {
|
||||
badges.push(t("summary.details.galleryDays", { count: packageDetails.gallery_days }));
|
||||
}
|
||||
if (packageDetails.max_guests) {
|
||||
badges.push(t("summary.details.guests", { count: packageDetails.max_guests }));
|
||||
}
|
||||
return badges;
|
||||
}, [packageDetails, t]);
|
||||
|
||||
const featuresList = React.useMemo(() => {
|
||||
if (!packageDetails) {
|
||||
return [] as string[];
|
||||
}
|
||||
return Object.entries(packageDetails.features ?? {})
|
||||
.filter(([, enabled]) => Boolean(enabled))
|
||||
.map(([feature]) => humanizeFeature(t, feature));
|
||||
}, [packageDetails, t]);
|
||||
|
||||
const nextSteps = t("summary.nextSteps", { returnObjects: true }) as string[];
|
||||
|
||||
return (
|
||||
<TenantWelcomeLayout
|
||||
eyebrow={t("summary.layout.eyebrow")}
|
||||
title={t("summary.layout.title")}
|
||||
subtitle={t("summary.layout.subtitle")}
|
||||
footer={
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-2 text-sm font-medium text-brand-rose hover:text-rose-600"
|
||||
onClick={() => navigate(ADMIN_WELCOME_PACKAGES_PATH)}
|
||||
>
|
||||
<ArrowLeft className="size-4" />
|
||||
{t("summary.footer.back")}
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<WelcomeStepCard
|
||||
step={3}
|
||||
totalSteps={4}
|
||||
title={t("summary.step.title")}
|
||||
description={t("summary.step.description")}
|
||||
icon={Receipt}
|
||||
>
|
||||
{packagesState.status === "loading" && (
|
||||
<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")}
|
||||
</FrostedSurface>
|
||||
)}
|
||||
|
||||
{packagesState.status === "error" && (
|
||||
<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 && (
|
||||
<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">
|
||||
<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>
|
||||
|
||||
<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 && (
|
||||
<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 && (
|
||||
<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" ? (
|
||||
<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 () => {
|
||||
if (!packageDetails) {
|
||||
return;
|
||||
}
|
||||
setFreeAssignStatus("loading");
|
||||
setFreeAssignError(null);
|
||||
try {
|
||||
await assignFreeTenantPackage(packageDetails.id);
|
||||
setFreeAssignStatus("success");
|
||||
markStep({ packageSelected: true });
|
||||
navigate(ADMIN_WELCOME_EVENT_PATH, { replace: true });
|
||||
} catch (error) {
|
||||
console.error("[Onboarding] Free package assignment failed", error);
|
||||
setFreeAssignStatus("error");
|
||||
setFreeAssignError(
|
||||
error instanceof Error ? error.message : t("summary.free.errorMessage")
|
||||
);
|
||||
}
|
||||
}}
|
||||
disabled={freeAssignStatus === "loading"}
|
||||
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" ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 size-4 animate-spin" />
|
||||
{t("summary.free.progress")}
|
||||
</>
|
||||
) : (
|
||||
t("summary.free.activate")
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
{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 && (
|
||||
<PaddleCheckout
|
||||
packageId={packageDetails.id}
|
||||
onSuccess={() => {
|
||||
markStep({ packageSelected: true });
|
||||
navigate(ADMIN_WELCOME_EVENT_PATH, { replace: true });
|
||||
}}
|
||||
t={t}
|
||||
/>
|
||||
)}
|
||||
|
||||
<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>
|
||||
</FrostedSurface>
|
||||
</div>
|
||||
)}
|
||||
</WelcomeStepCard>
|
||||
|
||||
<OnboardingCTAList
|
||||
actions={[
|
||||
{
|
||||
id: "checkout",
|
||||
label: t("summary.cta.billing.label"),
|
||||
description: t("summary.cta.billing.description"),
|
||||
buttonLabel: t("summary.cta.billing.button"),
|
||||
href: ADMIN_BILLING_PATH,
|
||||
icon: CreditCard,
|
||||
variant: "secondary",
|
||||
},
|
||||
{
|
||||
id: "continue-to-setup",
|
||||
label: t("summary.cta.setup.label"),
|
||||
description: t("summary.cta.setup.description"),
|
||||
buttonLabel: t("summary.cta.setup.button"),
|
||||
onClick: () => {
|
||||
markStep({ lastStep: "event-setup" });
|
||||
navigate(ADMIN_WELCOME_EVENT_PATH);
|
||||
},
|
||||
icon: ArrowRight,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</TenantWelcomeLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export { PaddleCheckout };
|
||||
@@ -1,283 +0,0 @@
|
||||
import React from "react";
|
||||
import { CreditCard, ShoppingBag, ArrowRight, Loader2, AlertCircle } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import {
|
||||
TenantWelcomeLayout,
|
||||
WelcomeStepCard,
|
||||
OnboardingCTAList,
|
||||
useOnboardingProgress,
|
||||
} from "..";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { FrostedSurface } from "../../components/tenant/frosted-surface";
|
||||
import { ADMIN_BILLING_PATH, ADMIN_WELCOME_SUMMARY_PATH } from "../../constants";
|
||||
import { useTenantPackages } from "../hooks/useTenantPackages";
|
||||
import { Package } from "../../api";
|
||||
|
||||
const DEFAULT_CURRENCY = "EUR";
|
||||
|
||||
function useLocaleFormats(locale: string) {
|
||||
const currencyFormatter = React.useMemo(
|
||||
() =>
|
||||
new Intl.NumberFormat(locale, {
|
||||
style: "currency",
|
||||
currency: DEFAULT_CURRENCY,
|
||||
minimumFractionDigits: 0,
|
||||
}),
|
||||
[locale]
|
||||
);
|
||||
|
||||
const dateFormatter = React.useMemo(
|
||||
() =>
|
||||
new Intl.DateTimeFormat(locale, {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
}),
|
||||
[locale]
|
||||
);
|
||||
|
||||
return { currencyFormatter, dateFormatter };
|
||||
}
|
||||
|
||||
function formatFeatureLabel(t: ReturnType<typeof useTranslation>["t"], key: string): string {
|
||||
const translationKey = `packages.features.${key}`;
|
||||
const translated = t(translationKey);
|
||||
if (translated !== translationKey) {
|
||||
return translated;
|
||||
}
|
||||
return key
|
||||
.split("_")
|
||||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
export default function WelcomePackagesPage() {
|
||||
const navigate = useNavigate();
|
||||
const { markStep } = useOnboardingProgress();
|
||||
const packagesState = useTenantPackages();
|
||||
const { t, i18n } = useTranslation("onboarding");
|
||||
const locale = i18n.language?.startsWith("en") ? "en-GB" : "de-DE";
|
||||
const { currencyFormatter, dateFormatter } = useLocaleFormats(locale);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (packagesState.status === "success") {
|
||||
const active = packagesState.activePackage;
|
||||
markStep({
|
||||
packageSelected: Boolean(active),
|
||||
lastStep: "packages",
|
||||
selectedPackage: active
|
||||
? {
|
||||
id: active.package_id,
|
||||
name: active.package_name,
|
||||
priceText:
|
||||
typeof active.price === "number"
|
||||
? currencyFormatter.format(active.price)
|
||||
: t("packages.card.onRequest"),
|
||||
isSubscription: Boolean(active.package_limits?.subscription),
|
||||
}
|
||||
: null,
|
||||
serverStep: active ? "package_selected" : undefined,
|
||||
meta: active
|
||||
? {
|
||||
packages: [active.package_id],
|
||||
is_active: active.active,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
}, [packagesState, markStep, currencyFormatter, t]);
|
||||
|
||||
const handleSelectPackage = React.useCallback(
|
||||
(pkg: Package) => {
|
||||
const priceText =
|
||||
typeof pkg.price === "number" ? currencyFormatter.format(pkg.price) : t("packages.card.onRequest");
|
||||
|
||||
markStep({
|
||||
packageSelected: true,
|
||||
lastStep: "packages",
|
||||
selectedPackage: {
|
||||
id: pkg.id,
|
||||
name: pkg.name,
|
||||
priceText,
|
||||
isSubscription: Boolean(pkg.features?.subscription),
|
||||
},
|
||||
serverStep: "package_selected",
|
||||
meta: { packages: [pkg.id] },
|
||||
});
|
||||
|
||||
navigate(ADMIN_WELCOME_SUMMARY_PATH, { state: { packageId: pkg.id } });
|
||||
},
|
||||
[currencyFormatter, markStep, navigate, t]
|
||||
);
|
||||
|
||||
const renderPackageList = () => {
|
||||
if (packagesState.status === "loading") {
|
||||
return (
|
||||
<FrostedSurface className="flex items-center gap-3 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("packages.state.loading")}
|
||||
</FrostedSurface>
|
||||
);
|
||||
}
|
||||
|
||||
if (packagesState.status === "error") {
|
||||
return (
|
||||
<Alert variant="destructive" className="border-rose-300/60 bg-rose-50/80 text-rose-700 dark:border-rose-500/40 dark:bg-rose-500/15 dark:text-rose-200">
|
||||
<AlertCircle className="size-4" />
|
||||
<AlertTitle>{t("packages.state.errorTitle")}</AlertTitle>
|
||||
<AlertDescription>{packagesState.message ?? t("packages.state.errorDescription")}</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (packagesState.status === "success" && packagesState.catalog.length === 0) {
|
||||
return (
|
||||
<Alert variant="default" className="border-white/20 bg-white/70 text-slate-700 shadow-sm dark:border-slate-800/60 dark:bg-slate-950/80 dark:text-slate-300">
|
||||
<AlertTitle>{t("packages.state.emptyTitle")}</AlertTitle>
|
||||
<AlertDescription>{t("packages.state.emptyDescription")}</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (packagesState.status !== "success") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{packagesState.catalog.map((pkg) => {
|
||||
const isActive = packagesState.activePackage?.package_id === pkg.id;
|
||||
const purchased = packagesState.purchasedPackages.find((tenantPkg) => tenantPkg.package_id === pkg.id);
|
||||
const featureLabels = Object.entries(pkg.features ?? {})
|
||||
.filter(([, enabled]) => Boolean(enabled))
|
||||
.map(([key]) => formatFeatureLabel(t, key));
|
||||
|
||||
const isSubscription = Boolean(pkg.features?.subscription);
|
||||
const priceText =
|
||||
typeof pkg.price === "number" ? currencyFormatter.format(pkg.price ?? 0) : t("packages.card.onRequest");
|
||||
|
||||
const badges: string[] = [];
|
||||
if (pkg.max_guests) {
|
||||
badges.push(t("packages.card.badges.guests", { count: pkg.max_guests }));
|
||||
}
|
||||
if (pkg.gallery_days) {
|
||||
badges.push(t("packages.card.badges.days", { count: pkg.gallery_days }));
|
||||
}
|
||||
if (pkg.max_photos) {
|
||||
badges.push(t("packages.card.badges.photos", { count: pkg.max_photos }));
|
||||
}
|
||||
|
||||
return (
|
||||
<FrostedSurface
|
||||
key={pkg.id}
|
||||
className="flex flex-col gap-4 border border-white/20 p-6 text-slate-900 shadow-md shadow-rose-200/20 transition-colors duration-200 dark:border-slate-800/70 dark:bg-slate-950/80 dark:text-slate-100"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-sm font-semibold uppercase tracking-[0.25em] text-rose-400 dark:text-rose-200">
|
||||
{t(isSubscription ? "packages.card.subscription" : "packages.card.creditPack")}
|
||||
</p>
|
||||
<h3 className="text-xl font-semibold text-slate-900 dark:text-slate-100">{pkg.name}</h3>
|
||||
</div>
|
||||
<span className="text-lg font-medium text-rose-400 dark:text-rose-200">{priceText}</span>
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300">
|
||||
{pkg.max_photos
|
||||
? t("packages.card.descriptionWithPhotos", { count: pkg.max_photos })
|
||||
: t("packages.card.description")}
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs uppercase tracking-wide text-rose-400 dark:text-rose-200">
|
||||
{badges.map((badge) => (
|
||||
<span key={badge} className="rounded-full bg-rose-100/80 px-3 py-1 shadow-inner shadow-rose-200/60 dark:bg-rose-500/20 dark:text-rose-100">
|
||||
{badge}
|
||||
</span>
|
||||
))}
|
||||
{featureLabels.map((feature) => (
|
||||
<span key={feature} className="rounded-full bg-rose-100/80 px-3 py-1 shadow-inner shadow-rose-200/60 dark:bg-rose-500/20 dark:text-rose-100">
|
||||
{feature}
|
||||
</span>
|
||||
))}
|
||||
{isActive ? (
|
||||
<span className="rounded-full bg-sky-100/80 px-3 py-1 text-sky-600 shadow-inner shadow-sky-200/40 dark:bg-sky-500/20 dark:text-sky-200">
|
||||
{t("packages.card.active")}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 pt-2">
|
||||
<Button
|
||||
size="lg"
|
||||
className="rounded-full bg-gradient-to-r from-[#ff5f87] via-[#ec4899] to-[#6366f1] text-white shadow-lg shadow-rose-300/30 hover:from-[#ff4470] hover:via-[#ec4899] hover:to-[#4f46e5]"
|
||||
onClick={() => handleSelectPackage(pkg)}
|
||||
disabled={isActive}
|
||||
>
|
||||
{isActive ? t("packages.card.active") : t("packages.card.select")}
|
||||
<ArrowRight className="ml-2 size-4" />
|
||||
</Button>
|
||||
{purchased ? (
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||||
{t("packages.card.purchased", {
|
||||
date: purchased.purchased_at
|
||||
? dateFormatter.format(new Date(purchased.purchased_at))
|
||||
: t("packages.card.purchasedUnknown"),
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="text-sm text-slate-600 hover:text-rose-400 dark:text-slate-400 dark:hover:text-rose-200"
|
||||
onClick={() => navigate(ADMIN_BILLING_PATH)}
|
||||
>
|
||||
<CreditCard className="mr-2 h-4 w-4" />
|
||||
{t("packages.card.viewBilling")}
|
||||
</Button>
|
||||
</div>
|
||||
</FrostedSurface>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<TenantWelcomeLayout
|
||||
eyebrow={t("packages.layout.eyebrow")}
|
||||
title={t("packages.layout.title")}
|
||||
subtitle={t("packages.layout.subtitle")}
|
||||
>
|
||||
<WelcomeStepCard
|
||||
step={2}
|
||||
totalSteps={4}
|
||||
title={t("packages.step.title")}
|
||||
description={t("packages.step.description")}
|
||||
icon={CreditCard}
|
||||
>
|
||||
{renderPackageList()}
|
||||
</WelcomeStepCard>
|
||||
|
||||
<OnboardingCTAList
|
||||
actions={[
|
||||
{
|
||||
id: "skip-to-billing",
|
||||
label: t("packages.cta.billing.label"),
|
||||
description: t("packages.cta.billing.description"),
|
||||
buttonLabel: t("packages.cta.billing.button"),
|
||||
href: ADMIN_BILLING_PATH,
|
||||
icon: ShoppingBag,
|
||||
variant: "secondary",
|
||||
},
|
||||
{
|
||||
id: "continue",
|
||||
label: t("packages.cta.summary.label"),
|
||||
description: t("packages.cta.summary.description"),
|
||||
buttonLabel: t("packages.cta.summary.button"),
|
||||
onClick: () => navigate(ADMIN_WELCOME_SUMMARY_PATH),
|
||||
icon: ArrowRight,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</TenantWelcomeLayout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user