feat(FN-203): implement P0-1 through P0-6 subscription page fixes
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Commits merged: - feat(FN-203): complete Step 1 — implement P0-1 through P0-6 subscription page fixes Files changed: apps/web/src/messages/en.json | 7 + apps/web/src/messages/tr.json | 7 + .../src/routes/dashboard/subscription/index.tsx | 188 +++++++++++++++------ apps/web/vitest.config.ts | 3 + 4 files changed, 153 insertions(+), 52 deletions(-) Fusion-Task-Id: FN-203
This commit is contained in:
@@ -177,6 +177,13 @@
|
||||
"expired": "Expired"
|
||||
},
|
||||
"popular": "Popular",
|
||||
"yearlyDiscount": "17% off",
|
||||
"proceed": "Continue",
|
||||
"orderSummary": "Order Summary",
|
||||
"orderSummaryPlan": "Plan",
|
||||
"orderSummaryPeriod": "Period",
|
||||
"orderSummaryBrands": "Brands",
|
||||
"orderSummaryPrice": "Price",
|
||||
"trialTitle": "30-Day Full Package Trial",
|
||||
"trialDescription": "Free access to all brands for 30 days. No credit card required.",
|
||||
"startTrial": "Start Free Trial",
|
||||
|
||||
@@ -177,6 +177,13 @@
|
||||
"expired": "Süresi Doldu"
|
||||
},
|
||||
"popular": "Popüler",
|
||||
"yearlyDiscount": "17% indirim",
|
||||
"proceed": "Devam Et",
|
||||
"orderSummary": "Sipariş Özeti",
|
||||
"orderSummaryPlan": "Plan",
|
||||
"orderSummaryPeriod": "Dönem",
|
||||
"orderSummaryBrands": "Markalar",
|
||||
"orderSummaryPrice": "Tutar",
|
||||
"trialTitle": "30 Gün Full Paket Denemesi",
|
||||
"trialDescription": "Tüm markalara 30 gün boyunca ücretsiz erişim. Kredi kartı gerekmez.",
|
||||
"startTrial": "Ücretsiz Denemeyi Başlat",
|
||||
|
||||
@@ -552,43 +552,42 @@ function SubscriptionPage() {
|
||||
)}
|
||||
|
||||
{/* Trial CTA Card */}
|
||||
{eligibleForTrial &&
|
||||
(!subscription || subscription.status === "expired" || subscription.status === "trial") && (
|
||||
<Card className="relative overflow-hidden border-brand/25 bg-brand/5">
|
||||
<div className="pointer-events-none absolute -right-24 top-0 h-[300px] w-[300px] rounded-full bg-brand/15 blur-[100px]" />
|
||||
<CardHeader className="relative">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="h-5 w-5 text-brand" />
|
||||
<CardTitle className="text-foreground">{t("subscription.trialTitle")}</CardTitle>
|
||||
</div>
|
||||
<CardDescription className="text-muted-foreground">
|
||||
{t("subscription.trialDescription")}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="relative space-y-4">
|
||||
<ul className="space-y-2 text-sm">
|
||||
{["allBrands", "vinSearch", "partsCatalog", "schemaViewer"].map((f) => (
|
||||
<li key={f} className="flex items-center gap-2 text-foreground/85">
|
||||
<Check className="h-4 w-4 text-brand" />
|
||||
{t(`subscription.features.${f}`)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Button
|
||||
className="bg-brand text-brand-foreground hover:bg-brand/90"
|
||||
onClick={() => {
|
||||
startAction("trial-start");
|
||||
capture("trial_started");
|
||||
trialMutation.mutate();
|
||||
}}
|
||||
disabled={trialMutation.isPending}
|
||||
>
|
||||
<ShieldCheck className="mr-2 h-4 w-4" />
|
||||
{trialMutation.isPending ? t("common.loading") : t("subscription.startTrial")}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
{eligibleForTrial && (!subscription || subscription.status === "expired") && (
|
||||
<Card className="relative overflow-hidden border-brand/25 bg-brand/5">
|
||||
<div className="pointer-events-none absolute -right-24 top-0 h-[300px] w-[300px] rounded-full bg-brand/15 blur-[100px]" />
|
||||
<CardHeader className="relative">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="h-5 w-5 text-brand" />
|
||||
<CardTitle className="text-foreground">{t("subscription.trialTitle")}</CardTitle>
|
||||
</div>
|
||||
<CardDescription className="text-muted-foreground">
|
||||
{t("subscription.trialDescription")}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="relative space-y-4">
|
||||
<ul className="space-y-2 text-sm">
|
||||
{["allBrands", "vinSearch", "partsCatalog", "schemaViewer"].map((f) => (
|
||||
<li key={f} className="flex items-center gap-2 text-foreground/85">
|
||||
<Check className="h-4 w-4 text-brand" />
|
||||
{t(`subscription.features.${f}`)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Button
|
||||
className="bg-brand text-brand-foreground hover:bg-brand/90"
|
||||
onClick={() => {
|
||||
startAction("trial-start");
|
||||
capture("trial_started");
|
||||
trialMutation.mutate();
|
||||
}}
|
||||
disabled={trialMutation.isPending}
|
||||
>
|
||||
<ShieldCheck className="mr-2 h-4 w-4" />
|
||||
{trialMutation.isPending ? t("common.loading") : t("subscription.startTrial")}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* No Subscription Banner */}
|
||||
{!eligibleForTrial && !subscription && (
|
||||
@@ -640,12 +639,16 @@ function SubscriptionPage() {
|
||||
<Card
|
||||
key={plan.key}
|
||||
data-faro-user-action-name={`select-plan-${plan.key}`}
|
||||
className={`relative cursor-pointer transition-all hover:shadow-md ${
|
||||
isSelected ? "border-primary ring-2 ring-primary/20" : ""
|
||||
className={`relative transition-all hover:shadow-md ${
|
||||
isCurrentPlan ? "" : "cursor-pointer"
|
||||
} ${
|
||||
isSelected && !isCurrentPlan ? "border-primary ring-2 ring-primary/20" : ""
|
||||
} ${isCurrentPlan ? "border-green-500/50 bg-green-50/50 dark:bg-green-950/10" : ""} ${
|
||||
plan.popular ? "border-primary shadow-lg" : ""
|
||||
plan.popular && !isCurrentPlan
|
||||
? "border-primary ring-2 ring-primary/30 shadow-xl dark:shadow-primary/10 bg-primary/5"
|
||||
: ""
|
||||
}`}
|
||||
onClick={() => handleSelectPlan(plan.key)}
|
||||
onClick={isCurrentPlan ? undefined : () => handleSelectPlan(plan.key)}
|
||||
>
|
||||
{plan.popular && (
|
||||
<Badge className="absolute -top-3 left-1/2 -translate-x-1/2">
|
||||
@@ -674,6 +677,11 @@ function SubscriptionPage() {
|
||||
<span className="text-muted-foreground">
|
||||
{billingPeriod === "monthly" ? t("common.perMonth") : t("common.perYear")}
|
||||
</span>
|
||||
{billingPeriod === "yearly" && (
|
||||
<Badge variant="secondary" className="ml-2 align-middle">
|
||||
{t("subscription.yearlyDiscount")}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<ul className="space-y-2 text-sm">
|
||||
{plan.features.map((f) => (
|
||||
@@ -685,16 +693,23 @@ function SubscriptionPage() {
|
||||
</ul>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button
|
||||
className="w-full"
|
||||
variant={isSelected ? "default" : plan.popular ? "default" : "outline"}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleSelectPlan(plan.key);
|
||||
}}
|
||||
>
|
||||
{isSelected ? t("subscription.choosePlan") : t("subscription.subscribe")}
|
||||
</Button>
|
||||
{isCurrentPlan ? (
|
||||
<div className="flex w-full items-center justify-center rounded-md bg-muted py-2 text-sm font-medium text-muted-foreground">
|
||||
<Check className="mr-1.5 h-4 w-4" />
|
||||
{t("subscription.currentPlan")}
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
className="w-full"
|
||||
variant={isSelected ? "default" : plan.popular ? "default" : "outline"}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleSelectPlan(plan.key);
|
||||
}}
|
||||
>
|
||||
{isSelected ? t("subscription.proceed") : t("subscription.choosePlan")}
|
||||
</Button>
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
@@ -702,6 +717,66 @@ function SubscriptionPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Order Summary (shown when a plan is selected) */}
|
||||
{selectedPlanKey &&
|
||||
(() => {
|
||||
const selectedPlan = plans.find((p) => p.key === selectedPlanKey);
|
||||
const summaryPrice =
|
||||
billingPeriod === "monthly"
|
||||
? (selectedPlan?.priceMonthly ?? 0)
|
||||
: (selectedPlan?.priceYearly ?? 0);
|
||||
const periodLabel =
|
||||
billingPeriod === "monthly" ? t("common.perMonth") : t("common.perYear");
|
||||
return (
|
||||
<>
|
||||
<Separator />
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg">{t("subscription.orderSummary")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">
|
||||
{t("subscription.orderSummaryPlan")}
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
{t(`subscription.plans.${selectedPlanKey}.name`)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">
|
||||
{t("subscription.orderSummaryPeriod")}
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
{billingPeriod === "monthly" ? t("common.monthly") : t("common.yearly")}
|
||||
</span>
|
||||
</div>
|
||||
{selectedBrandIds.length > 0 && (
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">
|
||||
{t("subscription.orderSummaryBrands")}
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
{selectedBrandIds.length} {t("subscription.brandsSelected")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between font-semibold">
|
||||
<span>{t("subscription.orderSummaryPrice")}</span>
|
||||
<span>
|
||||
{formatTRY(summaryPrice)}
|
||||
<span className="text-xs font-normal text-muted-foreground">
|
||||
{periodLabel}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Brand Selector (shown when a plan is selected) */}
|
||||
{selectedPlanKey && (
|
||||
<>
|
||||
@@ -721,7 +796,16 @@ function SubscriptionPage() {
|
||||
</Suspense>
|
||||
<div className="flex justify-end">
|
||||
<Button size="lg" onClick={handleProceedToPayment}>
|
||||
{t("subscription.subscribe")} →
|
||||
{(() => {
|
||||
const proceedPlan = plans.find((p) => p.key === selectedPlanKey);
|
||||
const proceedPrice =
|
||||
billingPeriod === "monthly"
|
||||
? (proceedPlan?.priceMonthly ?? 0)
|
||||
: (proceedPlan?.priceYearly ?? 0);
|
||||
const proceedPeriodLabel =
|
||||
billingPeriod === "monthly" ? t("common.perMonth") : t("common.perYear");
|
||||
return `${formatTRY(proceedPrice)}${proceedPeriodLabel} ile ${t("subscription.proceed")} →`;
|
||||
})()}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,9 @@ export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "jsdom",
|
||||
env: {
|
||||
NODE_ENV: "development",
|
||||
},
|
||||
include: ["src/**/*.{test,spec}.{ts,tsx}"],
|
||||
setupFiles: ["./src/test-setup.ts"],
|
||||
coverage: {
|
||||
|
||||
Reference in New Issue
Block a user