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:
@@ -11,6 +11,7 @@ import {
|
||||
} 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();
|
||||
@@ -55,25 +56,25 @@ export default function WelcomeEventSetupPage() {
|
||||
icon: ArrowRight,
|
||||
},
|
||||
].map((item) => (
|
||||
<div
|
||||
<FrostedSurface
|
||||
key={item.id}
|
||||
className="flex flex-col gap-3 rounded-2xl border border-brand-rose-soft bg-brand-card p-5 shadow-brand-primary"
|
||||
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-brand-rose-soft text-brand-rose shadow-inner">
|
||||
<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-brand-slate">{item.title}</h3>
|
||||
<p className="text-sm text-brand-navy/80">{item.copy}</p>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<div className="mt-6 flex flex-col items-start gap-3 rounded-3xl border border-brand-rose-soft bg-brand-sky-soft/40 p-6 text-brand-navy">
|
||||
<h4 className="text-lg font-semibold text-brand-rose">{t("eventSetup.cta.heading")}</h4>
|
||||
<p className="text-sm text-brand-navy/80">{t("eventSetup.cta.description")}</p>
|
||||
<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-brand-rose text-white shadow-lg shadow-rose-400/40 hover:bg-[var(--brand-rose-strong)]"
|
||||
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);
|
||||
@@ -82,7 +83,7 @@ export default function WelcomeEventSetupPage() {
|
||||
{t("eventSetup.cta.button")}
|
||||
<ArrowRight className="ml-2 size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</FrostedSurface>
|
||||
</WelcomeStepCard>
|
||||
|
||||
<OnboardingCTAList
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Sparkles, Users, Camera, CalendarDays, ChevronRight } from "lucide-react";
|
||||
import { Sparkles, Users, Camera, CalendarDays, ChevronRight, CreditCard, UserPlus, Palette } from "lucide-react";
|
||||
|
||||
import {
|
||||
TenantWelcomeLayout,
|
||||
@@ -11,16 +11,52 @@ import {
|
||||
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 } = useOnboardingProgress();
|
||||
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")}
|
||||
@@ -60,6 +96,32 @@ export default function WelcomeLandingPage() {
|
||||
]}
|
||||
/>
|
||||
|
||||
<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={[
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} 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";
|
||||
@@ -115,16 +116,16 @@ export default function WelcomePackagesPage() {
|
||||
const renderPackageList = () => {
|
||||
if (packagesState.status === "loading") {
|
||||
return (
|
||||
<div className="flex items-center gap-3 rounded-2xl bg-brand-sky-soft/60 p-6 text-sm text-brand-navy/80">
|
||||
<Loader2 className="size-5 animate-spin text-brand-rose" />
|
||||
<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")}
|
||||
</div>
|
||||
</FrostedSurface>
|
||||
);
|
||||
}
|
||||
|
||||
if (packagesState.status === "error") {
|
||||
return (
|
||||
<Alert variant="destructive">
|
||||
<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>
|
||||
@@ -134,7 +135,7 @@ export default function WelcomePackagesPage() {
|
||||
|
||||
if (packagesState.status === "success" && packagesState.catalog.length === 0) {
|
||||
return (
|
||||
<Alert variant="default" className="border-brand-rose-soft bg-brand-card/60 text-brand-navy">
|
||||
<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>
|
||||
@@ -170,59 +171,70 @@ export default function WelcomePackagesPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
<FrostedSurface
|
||||
key={pkg.id}
|
||||
className="flex flex-col gap-4 rounded-2xl border border-brand-rose-soft bg-brand-card p-6 shadow-brand-primary"
|
||||
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-brand-rose">
|
||||
<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-brand-slate">{pkg.name}</h3>
|
||||
<h3 className="text-xl font-semibold text-slate-900 dark:text-slate-100">{pkg.name}</h3>
|
||||
</div>
|
||||
<span className="text-lg font-medium text-brand-rose">{priceText}</span>
|
||||
<span className="text-lg font-medium text-rose-400 dark:text-rose-200">{priceText}</span>
|
||||
</div>
|
||||
<p className="text-sm text-brand-navy/80">
|
||||
<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-brand-rose">
|
||||
<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-brand-rose-soft px-3 py-1">
|
||||
<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-brand-rose-soft px-3 py-1">
|
||||
<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-brand-sky-soft px-3 py-1 text-brand-navy">
|
||||
{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>
|
||||
<Button
|
||||
size="lg"
|
||||
className="mt-auto rounded-full bg-brand-rose text-white shadow-lg shadow-rose-300/40 hover:bg-[var(--brand-rose-strong)]"
|
||||
onClick={() => handleSelectPackage(pkg)}
|
||||
>
|
||||
{t("packages.card.select")}
|
||||
<ArrowRight className="ml-2 size-4" />
|
||||
</Button>
|
||||
{purchased && (
|
||||
<p className="text-xs text-brand-rose">
|
||||
{t("packages.card.purchased", {
|
||||
date: purchased.purchased_at
|
||||
? dateFormatter.format(new Date(purchased.purchased_at))
|
||||
: t("packages.card.purchasedUnknown"),
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user