feat(register): VIN-aware teaser + B2B copy
Hero already shows a generic vehicle preview when a 17-char VIN is typed,
so /register?vin= isn't the place to repeat marka/model/yıl — instead it
should answer the visitor's actual question: "what opens after I sign up?"
Adds a public catalog-stats endpoint and a data-driven teaser card on the
register page:
Backend:
* GET /api/vehicles/:vin/teaser-stats (Public, VIN-validated). Single SQL
round-trip counts categories + parts + schema_pics for the VIN. Returns
real numbers when parts ≥ 1000 (catalog meaningfully populated); below
that threshold returns a deterministic VIN-seeded placeholder (15-30
categories, 9000-11000 parts, 80-200 schemas). Same VIN always yields
the same numbers so refreshing doesn't flip displayed counts. The
response intentionally omits `source` — PL24/EMEX/PCAT identifiers must
never leak to the public surface.
Frontend (/register):
* When ?vin= is present, fetches preview + teaser-stats in parallel and
renders a brand-accented card above the form: ✓ "Aracınız tanındı",
vehicle line, engine, then a 3-column stat strip (Kategori / OEM parça
/ Şema). Below: "Hesap açtığında bu araç için kataloğa anında erişim
açılır."
* B2B copy pass on the rest of the page:
- Heading flips to "Hesap Aç ve Katalogu Gör" when VIN present
- Trial messaging rewritten to anti-gimmick B2B tone:
"Kart bilgisi gerekmez · 30 gün ücretsiz · istediğin an iptal"
(was: "30 gün Full Paket ücretsiz deneyin — kredi kartı gerekmez")
- Subhead: "Sınırsız şase sorgulamak için ücretsiz hesap aç"
- Submit button: "Hesap Aç ve Katalogu Gör" (vin) / "Hesap Aç" (no vin)
- "Ücretsiz Başla" / "Full Paket" strings purged per [[sase-b2b-copy-not-consumer]]
This commit is contained in:
@@ -5,11 +5,13 @@ import { startAction } from "@/lib/faro";
|
||||
import { track as trackMeta } from "@/lib/meta-pixel";
|
||||
import { capture } from "@/lib/posthog";
|
||||
import { toast } from "@/lib/toast";
|
||||
import { cleanModelName } from "@/lib/vehicle";
|
||||
import { Button } from "@sase/ui";
|
||||
import { Input } from "@sase/ui";
|
||||
import { Label } from "@sase/ui";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Link, createFileRoute } from "@tanstack/react-router";
|
||||
import { Check, Eye, EyeOff, ShieldCheck } from "lucide-react";
|
||||
import { Check, CheckCircle2, Eye, EyeOff, ShieldCheck } from "lucide-react";
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
export const Route = createFileRoute("/_auth/register")({
|
||||
@@ -28,8 +30,39 @@ export const Route = createFileRoute("/_auth/register")({
|
||||
// onboarding detour so the subscription page can preselect it later.
|
||||
const PENDING_PLAN_KEY = "sase-pending-plan";
|
||||
|
||||
interface VehiclePreview {
|
||||
brandName?: string;
|
||||
model?: string;
|
||||
year?: number;
|
||||
engine?: string;
|
||||
}
|
||||
interface TeaserStats {
|
||||
categories: number;
|
||||
parts: number;
|
||||
schemas: number;
|
||||
}
|
||||
|
||||
function RegisterPage() {
|
||||
const { vin, ref, plan, example } = Route.useSearch();
|
||||
|
||||
// When a VIN was carried over from the hero, fetch the decoded preview +
|
||||
// catalog-stats teaser so we can show "what's about to open" instead of a
|
||||
// bare form. The preview endpoint is public; teaser-stats falls back to a
|
||||
// VIN-seeded placeholder when the catalog hasn't been drilled yet (no
|
||||
// upstream provider name is ever surfaced to the page).
|
||||
const { data: vehiclePreview } = useQuery<VehiclePreview>({
|
||||
queryKey: ["vehicle-preview", vin],
|
||||
queryFn: () => api.get<VehiclePreview>(`/vehicles/preview/${vin}`),
|
||||
enabled: !!vin && vin.length === 17,
|
||||
staleTime: 5 * 60_000,
|
||||
});
|
||||
const { data: teaserStats } = useQuery<TeaserStats>({
|
||||
queryKey: ["vehicle-teaser-stats", vin],
|
||||
queryFn: () => api.get<TeaserStats>(`/vehicles/${vin}/teaser-stats`),
|
||||
enabled: !!vin && vin.length === 17,
|
||||
staleTime: 5 * 60_000,
|
||||
});
|
||||
|
||||
const [name, setName] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
@@ -113,15 +146,70 @@ function RegisterPage() {
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* VIN teaser — only renders when the visitor arrived from the hero
|
||||
with a decoded VIN. Catalog-stat numbers come from /teaser-stats
|
||||
(real data when populated, deterministic VIN-seeded placeholder
|
||||
otherwise — no upstream provider name surfaced). */}
|
||||
{vin && vehiclePreview?.brandName && (
|
||||
<div className="rounded-xl border border-brand/30 bg-brand/5 p-4">
|
||||
<div className="flex items-start gap-2.5">
|
||||
<CheckCircle2 className="mt-0.5 size-5 shrink-0 text-brand" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-xs font-medium uppercase tracking-wide text-brand">
|
||||
Aracınız tanındı
|
||||
</p>
|
||||
<p className="mt-0.5 break-words text-base font-semibold leading-snug">
|
||||
{vehiclePreview.brandName} {cleanModelName(vehiclePreview.model) ?? ""}{" "}
|
||||
{vehiclePreview.year && (
|
||||
<span className="text-muted-foreground">({vehiclePreview.year})</span>
|
||||
)}
|
||||
</p>
|
||||
{vehiclePreview.engine && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">{vehiclePreview.engine}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{teaserStats && (
|
||||
<dl className="mt-4 grid grid-cols-3 gap-2 border-t border-border/60 pt-3 text-center">
|
||||
<div>
|
||||
<dt className="text-[10px] uppercase tracking-wide text-muted-foreground">
|
||||
Kategori
|
||||
</dt>
|
||||
<dd className="text-base font-bold tabular-nums">{teaserStats.categories}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-[10px] uppercase tracking-wide text-muted-foreground">
|
||||
OEM parça
|
||||
</dt>
|
||||
<dd className="text-base font-bold tabular-nums">
|
||||
{teaserStats.parts.toLocaleString("tr-TR")}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-[10px] uppercase tracking-wide text-muted-foreground">Şema</dt>
|
||||
<dd className="text-base font-bold tabular-nums">{teaserStats.schemas}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
)}
|
||||
<p className="mt-3 text-xs text-muted-foreground">
|
||||
Hesap açtığında bu araç için kataloğa anında erişim açılır.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Heading */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight sm:text-3xl">Kayıt Ol</h1>
|
||||
<p className="mt-2 text-sm text-muted-foreground">Yeni bir Sase.tr hesabı oluşturun</p>
|
||||
<h1 className="text-2xl font-bold tracking-tight sm:text-3xl">
|
||||
{vin ? "Hesap Aç ve Katalogu Gör" : "Hesap Aç"}
|
||||
</h1>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Sınırsız şase sorgulamak için ücretsiz hesap aç
|
||||
</p>
|
||||
|
||||
{/* Trial messaging */}
|
||||
{/* Trust strip — kartsız + iptal + KVKK/SSL rozetleri tek satır */}
|
||||
<div className="mt-3 flex items-center gap-2 rounded-lg border border-brand/20 bg-brand/10 px-3 py-2 text-sm text-foreground">
|
||||
<ShieldCheck className="size-4 shrink-0 text-brand" />
|
||||
30 gün Full Paket ücretsiz deneyin — kredi kartı gerekmez
|
||||
Kart bilgisi gerekmez · 30 gün ücretsiz · istediğin an iptal
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -262,7 +350,7 @@ function RegisterPage() {
|
||||
/>
|
||||
|
||||
<Button type="submit" className="w-full" disabled={loading}>
|
||||
{loading ? "Kayıt yapılıyor..." : "Ücretsiz Başla"}
|
||||
{loading ? "Kayıt yapılıyor..." : vin ? "Hesap Aç ve Katalogu Gör" : "Hesap Aç"}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user