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:
@@ -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
|
||||
|
||||
@@ -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%">
|
||||
|
||||
Reference in New Issue
Block a user