feat: smart VIN decode on landing page, pass VIN through registration flow

- Landing hero VIN search now decodes directly for logged-in users
- Unauthenticated users redirected to register with VIN preserved in search params
- Added loading states for decode button
- Tightened section spacing across landing page

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sase Dev
2026-02-17 19:20:08 +00:00
parent f2126754a6
commit 87f03f66ac
2 changed files with 122 additions and 49 deletions

View File

@@ -11,13 +11,20 @@ import { ShieldCheck } from "lucide-react";
export const Route = createFileRoute("/_auth/register")({
component: RegisterPage,
validateSearch: (search: Record<string, unknown>): { vin?: string } => ({
vin: (search.vin as string) || undefined,
}),
});
function RegisterPage() {
const { vin } = Route.useSearch();
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const redirectUrl = vin
? `/dashboard/subscription?welcome=1&vin=${encodeURIComponent(vin)}`
: "/dashboard/subscription?welcome=1";
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
@@ -28,7 +35,7 @@ function RegisterPage() {
await signUp.email({ name, email, password });
capture("user_signed_up", { method: "email" });
toast.success("Hesap oluşturuldu!");
window.location.href = "/dashboard/subscription?welcome=1";
window.location.href = redirectUrl;
} catch {
toast.error("Kayıt başarısız. Bu e-posta zaten kullanılıyor olabilir.");
} finally {
@@ -114,7 +121,7 @@ function RegisterPage() {
onClick={() => {
startAction("register", { method: "google" });
capture("user_signed_up", { method: "google" });
signIn.social({ provider: "google", callbackURL: "/dashboard/subscription?welcome=1" });
signIn.social({ provider: "google", callbackURL: redirectUrl });
}}
>
Google ile Kayıt Ol

View File

@@ -31,6 +31,10 @@ import { DashboardDemo } from "@/remotion/DashboardDemo";
import { EcommerceDemo } from "@/remotion/EcommerceDemo";
import { getUserSettings, setUserSetting } from "@/lib/user-settings";
import { useAuth } from "@/hooks/use-auth";
import { useAuthStore } from "@/stores/auth.store";
import { api, ApiError } from "@/lib/api-client";
import { toast } from "@/lib/toast";
import { capture } from "@/lib/posthog";
// ─── DATA ────────────────────────────────────────────────────────────────────
@@ -349,6 +353,7 @@ function HomePage() {
} | null>(null);
const [vinLoading, setVinLoading] = useState(false);
const [vinError, setVinError] = useState(false);
const [decodeLoading, setDecodeLoading] = useState(false);
const toggleTheme = () => {
const next = isDark ? "light" : "dark";
@@ -398,9 +403,54 @@ function HomePage() {
return () => controller.abort();
}, [vin]);
const handleVinSearch = () => {
if (vin.trim().length > 0) {
navigate({ to: "/register" });
const handleVinSearch = async () => {
const trimmed = vin.trim();
if (!trimmed) return;
// Read latest auth state directly from store (avoids stale closure)
const { user, isLoading } = useAuthStore.getState();
// If session still loading, wait for it to resolve
if (isLoading) {
setDecodeLoading(true);
await new Promise<void>((resolve) => {
const unsub = useAuthStore.subscribe((state) => {
if (!state.isLoading) {
unsub();
resolve();
}
});
});
}
const loggedIn = !!useAuthStore.getState().user;
if (!loggedIn) {
setDecodeLoading(false);
navigate({ to: "/register", search: { vin: trimmed } });
return;
}
setDecodeLoading(true);
try {
const vehicle = await api.post<{ id: string }>("/vehicles/decode", { vin: trimmed });
capture("vin_decoded", { vin: trimmed, source: "landing" });
navigate({ to: "/dashboard/vehicles/$id", params: { id: vehicle.id } });
} catch (err) {
if (err instanceof ApiError) {
if (err.status === 403) {
toast.error(err.message);
navigate({ to: "/dashboard/subscription" });
} else if (err.status === 400) {
toast.error(err.message);
} else {
toast.error("Bir hata oluştu. Lütfen tekrar deneyin.");
}
} else {
toast.error("Bir hata oluştu. Lütfen tekrar deneyin.");
}
} finally {
setDecodeLoading(false);
}
};
@@ -541,7 +591,7 @@ function HomePage() {
</header>
{/* ─── 2. HERO SECTION ──────────────────────────────────────────── */}
<section className="relative overflow-hidden px-4 pb-20 pt-24 sm:px-6 sm:pt-32 lg:pt-40">
<section className="relative overflow-hidden px-4 pb-16 pt-16 sm:px-6 sm:pt-24 lg:pt-28">
{/* Decorative glow */}
<div className="pointer-events-none absolute left-1/2 top-0 -translate-x-1/2">
<div className="h-[600px] w-[800px] rounded-full bg-foreground/[0.03] blur-3xl" />
@@ -549,13 +599,13 @@ function HomePage() {
<div className="relative mx-auto max-w-4xl text-center">
{/* Pill badge — loss framing */}
<div className="mb-8 inline-flex items-center gap-2 rounded-full border border-border bg-muted px-4 py-1.5 text-xs text-muted-foreground">
<div className="mb-4 inline-flex items-center gap-2 rounded-full border border-border bg-muted px-4 py-1.5 text-xs text-muted-foreground">
<span className="size-2 rounded-full bg-amber-500" />
Her yanlış parça siparişi ortalama 450 TL'ye mal olur
</div>
{/* Social proof badge */}
<div className="mt-3 inline-flex items-center gap-3 rounded-full border border-border bg-muted/50 px-4 py-2 text-xs text-muted-foreground">
<div className="mt-3 inline-flex items-center gap-3 rounded-full border border-border bg-muted px-4 py-1.5 text-xs text-muted-foreground">
<div className="flex -space-x-2">
{["AY", "MK", "ÖD", "BT"].map((initials) => (
<div key={initials} className="flex size-6 items-center justify-center rounded-full border-2 border-background bg-muted text-[10px] font-medium">
@@ -569,9 +619,7 @@ function HomePage() {
<span>500+ İşletme Güveniyor</span>
</div>
<h1 className="font-[family-name:var(--font-display)] text-4xl font-bold leading-[1.1] tracking-tight sm:text-5xl lg:text-7xl">
Müşteriniz Şase Verdi —
<br />
<h1 className="mt-6 font-[family-name:var(--font-display)] text-4xl font-bold leading-[1.1] tracking-tight sm:text-5xl lg:text-7xl">
Doğru Parçayı 30 Saniyede Bulun
</h1>
@@ -581,7 +629,7 @@ function HomePage() {
</p>
{/* VIN Input */}
<div className="mx-auto mt-10 flex max-w-xl flex-col gap-3 sm:flex-row">
<div className="mx-auto mt-8 flex max-w-xl flex-col gap-3 sm:flex-row">
<div className="relative flex-1">
<Search className="absolute left-4 top-1/2 size-5 -translate-y-1/2 text-muted-foreground" />
<Input
@@ -595,11 +643,21 @@ function HomePage() {
</div>
<Button
onClick={handleVinSearch}
disabled={decodeLoading}
data-faro-user-action-name="hero-vin-search"
className="h-12 rounded-full bg-foreground px-8 text-sm font-semibold text-background hover:bg-foreground/90 sm:h-14 sm:text-base"
>
Ara
<ArrowRight className="ml-2 size-4" />
{decodeLoading ? (
<>
<Loader2 className="mr-2 size-4 animate-spin" />
Çözülüyor...
</>
) : (
<>
Ara
<ArrowRight className="ml-2 size-4" />
</>
)}
</Button>
</div>
@@ -642,15 +700,24 @@ function HomePage() {
)}
</div>
<div className="mt-3">
<Link to="/register">
<Button
size="sm"
className="rounded-full bg-emerald-600 text-white hover:bg-emerald-700"
>
Tam parça kataloğuna eriş
<ArrowRight className="ml-1.5 size-3.5" />
</Button>
</Link>
<Button
size="sm"
onClick={handleVinSearch}
disabled={decodeLoading}
className="rounded-full bg-emerald-500 text-white hover:bg-emerald-600"
>
{decodeLoading ? (
<>
<Loader2 className="mr-1.5 size-3.5 animate-spin" />
Çözülüyor...
</>
) : (
<>
{isAuthenticated ? "Parça kataloğunu aç" : "Tam parça kataloğuna eriş"}
<ArrowRight className="ml-1.5 size-3.5" />
</>
)}
</Button>
</div>
</div>
)}
@@ -674,7 +741,7 @@ function HomePage() {
</p>
{/* Metric badges */}
<div className="mt-10 flex flex-wrap items-center justify-center gap-4 sm:gap-6">
<div className="mt-8 flex flex-wrap items-center justify-center gap-4 sm:gap-6">
{[
{ icon: Zap, label: "Ortalama 1.2sn Sonuç" },
{ icon: Car, label: "27 Marka Desteği" },
@@ -718,7 +785,7 @@ function HomePage() {
</section>
{/* ─── 4. ÇÖZÜMLER / ÖZELLİKLER ────────────────────────────────── */}
<section id="features" className="px-4 py-24 sm:px-6">
<section id="features" className="px-4 py-20 sm:px-6">
<div className="mx-auto max-w-7xl">
<div className="text-center">
<SectionBadge>Çözümlerimiz</SectionBadge>
@@ -730,7 +797,7 @@ function HomePage() {
</p>
</div>
<div className="mt-16 space-y-8">
<div className="mt-12 space-y-8">
{FEATURES.map((feature, idx) => {
const reversed = idx === 1;
const Icon = feature.icon;
@@ -744,9 +811,8 @@ function HomePage() {
>
{/* Text */}
<div className="flex-1 space-y-5">
<div className="inline-flex items-center gap-2 rounded-lg bg-muted px-3 py-1.5 text-sm text-muted-foreground">
<Icon className="size-4" />
{feature.title}
<div className="inline-flex rounded-lg bg-muted p-2.5">
<Icon className="size-5 text-muted-foreground" />
</div>
<h3 className="font-[family-name:var(--font-display)] text-2xl font-bold sm:text-3xl">
{feature.title}
@@ -791,7 +857,7 @@ function HomePage() {
</section>
{/* ─── 5. KARŞILAŞTIRMA TABLOSU (YENİ) ─────────────────────────── */}
<section className="bg-surface-alt px-4 py-24 sm:px-6">
<section className="bg-surface-alt px-4 py-20 sm:px-6">
<div className="mx-auto max-w-4xl">
<div className="text-center">
<SectionBadge>Karşılaştırma</SectionBadge>
@@ -801,7 +867,7 @@ function HomePage() {
</div>
{/* Desktop: table */}
<div className="mt-16 hidden overflow-hidden rounded-2xl border border-border bg-surface sm:block">
<div className="mt-12 hidden overflow-hidden rounded-2xl border border-border bg-surface sm:block">
<table className="w-full">
<thead>
<tr className="border-b border-border">
@@ -864,7 +930,7 @@ function HomePage() {
</section>
{/* ─── 6. BENTO GRID — NASIL ÇALIŞIR ────────────────────────────── */}
<section id="how-it-works" className="px-4 py-24 sm:px-6">
<section id="how-it-works" className="px-4 py-20 sm:px-6">
<div className="mx-auto max-w-7xl">
<div className="text-center">
<SectionBadge>Nasıl Çalışır</SectionBadge>
@@ -873,7 +939,7 @@ function HomePage() {
</h2>
</div>
<div className="mt-16 grid grid-cols-1 gap-4 md:grid-cols-3">
<div className="mt-12 grid grid-cols-1 gap-4 sm:gap-6 md:grid-cols-3">
{BENTO_ITEMS.map((item) => {
const Icon = item.icon;
const wide = item.wide;
@@ -902,7 +968,7 @@ function HomePage() {
</section>
{/* ─── 7. KULLANIM ALANLARI ─────────────────────────────────────── */}
<section className="bg-surface-alt px-4 py-24 sm:px-6">
<section className="bg-surface-alt px-4 py-20 sm:px-6">
<div className="mx-auto max-w-7xl">
<div className="text-center">
<SectionBadge>Kullanım Alanları</SectionBadge>
@@ -911,7 +977,7 @@ function HomePage() {
</h2>
</div>
<div className="mt-16 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
<div className="mt-12 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{USE_CASES.map((uc) => {
const Icon = uc.icon;
return (
@@ -957,7 +1023,7 @@ function HomePage() {
</section>
{/* ─── 8. RAKAMLARLA SASE.TR ────────────────────────────────────── */}
<section className="bg-[#09090b] px-4 py-24 text-white sm:px-6">
<section className="bg-[#09090b] px-4 py-20 text-white sm:px-6">
<div className="mx-auto max-w-7xl">
<div className="text-center">
<span className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-4 py-1.5 text-xs font-medium text-neutral-400">
@@ -968,7 +1034,7 @@ function HomePage() {
</h2>
</div>
<div className="mt-16 grid grid-cols-2 gap-4 sm:gap-6 lg:grid-cols-4">
<div className="mt-12 grid grid-cols-2 gap-4 sm:gap-6 lg:grid-cols-4">
{STATS.map((stat) => (
<div
key={stat.label}
@@ -987,7 +1053,7 @@ function HomePage() {
</section>
{/* ─── 9. DASHBOARD SHOWCASE ────────────────────────────────────── */}
<section className="relative overflow-hidden bg-surface-alt px-4 py-24 sm:px-6">
<section className="relative overflow-hidden bg-surface-alt px-4 py-20 sm:px-6">
{/* Decorative circles */}
<div className="pointer-events-none absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
<div className="size-[600px] rounded-full border border-border/50" />
@@ -1039,7 +1105,7 @@ function HomePage() {
</section>
{/* ─── 9.5. E-TİCARET ENTEGRASYONU ────────────────────────────── */}
<section className="px-4 py-24 sm:px-6">
<section className="px-4 py-20 sm:px-6">
<div className="mx-auto max-w-7xl">
<div className="text-center">
<SectionBadge>E-Ticaret Entegrasyonu</SectionBadge>
@@ -1053,7 +1119,7 @@ function HomePage() {
</p>
</div>
<div className="mt-16 overflow-hidden rounded-2xl bg-surface p-6 sm:p-8 lg:p-12">
<div className="mt-12 overflow-hidden rounded-2xl bg-surface p-6 sm:p-8 lg:p-12">
<div className="flex flex-col gap-8 lg:flex-row lg:items-center lg:gap-12">
{/* Left — Video */}
<div className="flex-1">
@@ -1124,7 +1190,7 @@ function HomePage() {
].map((stat) => (
<div
key={stat.label}
className="rounded-2xl border border-border bg-surface p-6 text-center"
className="rounded-2xl border border-border bg-surface p-6 sm:p-8 text-center"
>
<p className="font-[family-name:var(--font-display)] text-3xl font-bold text-emerald-500 sm:text-4xl">
{stat.value}
@@ -1139,7 +1205,7 @@ function HomePage() {
</section>
{/* ─── 9.6. TESTIMONIALS ────────────────────────────────────── */}
<section className="bg-surface-alt px-4 py-24 sm:px-6">
<section className="bg-surface-alt px-4 py-20 sm:px-6">
<div className="mx-auto max-w-7xl">
<div className="text-center">
<SectionBadge>Müşteri Yorumları</SectionBadge>
@@ -1148,7 +1214,7 @@ function HomePage() {
</h2>
</div>
<div className="mt-16 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div className="mt-12 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{TESTIMONIALS.map((t) => (
<div key={t.name} className="rounded-2xl border border-border bg-surface p-6">
<div className="flex gap-0.5">
@@ -1175,7 +1241,7 @@ function HomePage() {
</section>
{/* ─── 10. PRICING ─────────────────────────────────────────────── */}
<section className="px-4 py-24 sm:px-6">
<section className="px-4 py-20 sm:px-6">
<div className="mx-auto max-w-6xl">
<div className="text-center">
<SectionBadge>Fiyatlandırma</SectionBadge>
@@ -1188,7 +1254,7 @@ function HomePage() {
</div>
{/* Horizontally scrollable on mobile, grid on desktop */}
<div className="mt-12 flex gap-4 overflow-x-auto pb-4 sm:pb-0 lg:grid lg:grid-cols-4 lg:overflow-visible">
<div className="mt-12 flex gap-4 overflow-x-auto pb-4 sm:grid sm:grid-cols-2 sm:overflow-visible sm:pb-0 lg:grid-cols-4">
{[
{
name: "1 Marka",
@@ -1280,11 +1346,11 @@ function HomePage() {
</section>
{/* ─── 11. REFERRAL BANNER (YENİ) ───────────────────────────────── */}
<section className="px-4 sm:px-6">
<section className="px-4 py-10 sm:px-6">
<div className="mx-auto max-w-4xl overflow-hidden rounded-2xl border border-border bg-surface p-8 sm:p-12">
<div className="flex flex-col items-center gap-6 text-center sm:flex-row sm:text-left">
<div className="flex size-14 shrink-0 items-center justify-center rounded-2xl bg-muted">
<Gift className="size-7 text-muted-foreground" />
<div className="flex size-14 shrink-0 items-center justify-center rounded-xl bg-muted">
<Gift className="size-6 text-muted-foreground" />
</div>
<div className="flex-1">
<h3 className="font-[family-name:var(--font-display)] text-xl font-bold sm:text-2xl">
@@ -1305,7 +1371,7 @@ function HomePage() {
</section>
{/* ─── 12. SON CTA ──────────────────────────────────────────────── */}
<section className="relative overflow-hidden bg-surface-alt px-4 py-24 sm:px-6 sm:py-32">
<section className="relative overflow-hidden bg-surface-alt px-4 py-20 sm:px-6 sm:py-24">
{/* SVG grid pattern */}
<div className="pointer-events-none absolute inset-0 opacity-[0.03]">
<svg width="100%" height="100%">