feat(web): align pricing cards with subscription page
Bring the public pricing cards in line with the in-app subscription page and
strengthen the CTA:
- Move the "Popüler" badge from 2 Marka to Full Paket (one consistent story
across both surfaces).
- CTA "Başla" -> "Ücretsiz Başla" (leans on the free trial the flow grants).
- Yearly line now uses a consistent ₺ format ("veya ₺2.000,00/yıl") plus a
"· 2 ay bedava" savings cue instead of the bare, differently-formatted
"veya 2.000,00 TL/yıl".
- Plan-decision helper above the grid ("Hangi planı seçmeliyim? …").
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
21
apps/web/src/routes/__tests__/pricing-plans.test.tsx
Normal file
21
apps/web/src/routes/__tests__/pricing-plans.test.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Guards that the public pricing page emphasises the same plan as the in-app
|
||||
* subscription page. The "Popüler" badge was moved from 2 Marka to Full Paket
|
||||
* so both surfaces tell one story.
|
||||
*/
|
||||
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { plans } from "@/routes/pricing";
|
||||
|
||||
describe("pricing plans", () => {
|
||||
test("Full Paket is the only popular plan (aligned with subscription)", () => {
|
||||
expect(plans.find((p) => p.key === "full")?.popular).toBe(true);
|
||||
expect(plans.find((p) => p.key === "brand2")?.popular).toBeFalsy();
|
||||
expect(plans.filter((p) => p.popular).map((p) => p.key)).toEqual(["full"]);
|
||||
});
|
||||
|
||||
test("still lists all four tiers in ascending brand order", () => {
|
||||
expect(plans.map((p) => p.key)).toEqual(["brand1", "brand2", "brand3", "full"]);
|
||||
});
|
||||
});
|
||||
@@ -6,13 +6,13 @@ import { Button } from "@sase/ui";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@sase/ui";
|
||||
import { Badge } from "@sase/ui";
|
||||
import { Link, createFileRoute } from "@tanstack/react-router";
|
||||
import { Check } from "lucide-react";
|
||||
import { Check, Info } from "lucide-react";
|
||||
|
||||
export const Route = createFileRoute("/pricing")({
|
||||
component: PricingPage,
|
||||
});
|
||||
|
||||
const plans = [
|
||||
export const plans = [
|
||||
{
|
||||
key: "brand1",
|
||||
brandLimit: 1,
|
||||
@@ -25,7 +25,6 @@ const plans = [
|
||||
brandLimit: 2,
|
||||
priceMonthly: 35000,
|
||||
priceYearly: 350000,
|
||||
popular: true,
|
||||
features: ["brandSelection", "vinSearch", "partsCatalog", "schemaViewer", "prioritySupport"],
|
||||
},
|
||||
{
|
||||
@@ -40,6 +39,7 @@ const plans = [
|
||||
brandLimit: FULL_PLAN_BRAND_LIMIT,
|
||||
priceMonthly: 99900,
|
||||
priceYearly: 999000,
|
||||
popular: true,
|
||||
features: [
|
||||
"allBrands",
|
||||
"vinSearch",
|
||||
@@ -86,7 +86,12 @@ function PricingPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 grid gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||||
<div className="mx-auto mt-10 flex max-w-2xl items-start justify-center gap-2 rounded-lg border border-border bg-muted/30 px-4 py-2.5 text-sm text-muted-foreground">
|
||||
<Info className="mt-0.5 h-4 w-4 shrink-0 text-primary" />
|
||||
<span>{t("subscription.planHelper")}</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 grid gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||||
{plans.map((plan) => (
|
||||
<Card
|
||||
key={plan.key}
|
||||
@@ -107,9 +112,8 @@ function PricingPage() {
|
||||
<span className="text-muted-foreground">{t("common.perMonth")}</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("pricing.orYearly", {
|
||||
price: formatTRY(plan.priceYearly).replace(/[₺\s]/g, "").trim(),
|
||||
})}
|
||||
{t("pricing.orYearly", { price: formatTRY(plan.priceYearly) })}{" "}
|
||||
<span className="font-medium text-primary">{t("pricing.yearlySave")}</span>
|
||||
</p>
|
||||
<ul className="space-y-2 text-sm">
|
||||
{plan.features.map((f) => {
|
||||
|
||||
Reference in New Issue
Block a user