Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Commits merged: - fix(FN-186): fix pre-existing Biome formatting error in vehicles.service.spec.ts - feat(FN-186): complete Step 2 — update i18n tabSasetr keys from Sase.Tr to SASE - feat(FN-186): complete Step 1 — update all header/nav logo instances from Sase.tr to SASE with tracking-wider Files changed: apps/api/src/vehicles/vehicles.service.spec.ts | 38 ++++++++++++++------------ apps/web/src/messages/en.json | 2 +- apps/web/src/messages/tr.json | 2 +- apps/web/src/routes/_auth.tsx | 4 +-- apps/web/src/routes/about.tsx | 4 +-- apps/web/src/routes/blog.tsx | 4 +-- apps/web/src/routes/blog_/$slug.tsx | 4 +-- apps/web/src/routes/contact.tsx | 4 +-- apps/web/src/routes/dashboard.tsx | 6 ++-- apps/web/src/routes/demo.tsx | 4 +-- apps/web/src/routes/index.tsx | 4 +-- apps/web/src/routes/kvkk.tsx | 4 +-- apps/web/src/routes/pricing.tsx | 4 +-- apps/web/src/routes/privacy.tsx | 4 +-- apps/web/src/routes/terms.tsx | 4 +-- 15 files changed, 47 insertions(+), 45 deletions(-) Fusion-Task-Id: FN-186
424 lines
17 KiB
TypeScript
424 lines
17 KiB
TypeScript
import { usePageMeta } from "@/hooks/use-page-meta";
|
||
import { KEYS_16, KEYS_17 } from "@/lib/keys";
|
||
import { getUserSettings, setUserSetting } from "@/lib/user-settings";
|
||
import { Button, Input } from "@sase/ui";
|
||
import { Link, createFileRoute } from "@tanstack/react-router";
|
||
import {
|
||
ArrowRight,
|
||
Car,
|
||
FolderTree,
|
||
Loader2,
|
||
Lock,
|
||
Moon,
|
||
MousePointerClick,
|
||
Search,
|
||
Sun,
|
||
} from "lucide-react";
|
||
import { useEffect, useState } from "react";
|
||
|
||
export const Route = createFileRoute("/demo")({
|
||
component: DemoPage,
|
||
});
|
||
|
||
const EXAMPLE_CATEGORIES = [
|
||
{ name: "Motor", subcategories: ["Silindir Kapağı", "Krank Mili", "Piston", "Yağ Pompası"] },
|
||
{ name: "Şasi & Süspansiyon", subcategories: ["Amortisör", "Salıncak", "Rotil", "Viraj Demiri"] },
|
||
{ name: "Elektrik", subcategories: ["Alternatör", "Marş Motoru", "Kablo Tesisatı", "Sensörler"] },
|
||
{ name: "Karoseri", subcategories: ["Kapı Paneli", "Tampon", "Ayna", "Far"] },
|
||
{ name: "Klima & Isıtma", subcategories: ["Kompresör", "Kalorifer", "Radyatör", "Fan Motoru"] },
|
||
];
|
||
|
||
const EXAMPLE_SCHEMA_PARTS = [
|
||
{ code: "1J0 820 803F", name: "Klima Kompresörü", position: "A1" },
|
||
{ code: "1J0 819 031A", name: "Kalorifer Motoru", position: "B3" },
|
||
{ code: "1J0 698 151G", name: "Ön Fren Balatası", position: "C2" },
|
||
{ code: "1J0 407 271J", name: "Alt Salıncak", position: "D1" },
|
||
];
|
||
|
||
function DemoPage() {
|
||
usePageMeta({
|
||
title: "Demo — Sase.tr | Şase Sorgulamayı Deneyin",
|
||
description: "Ücretsiz demo ile şase numarası sorgulama ve OEM parça kataloğunu keşfedin.",
|
||
canonical: "https://sase.tr/demo",
|
||
});
|
||
|
||
const [vin, setVin] = useState("");
|
||
const [vinPreview, setVinPreview] = useState<{
|
||
make: string;
|
||
model: string;
|
||
year: string;
|
||
engine: string;
|
||
} | null>(null);
|
||
const [vinLoading, setVinLoading] = useState(false);
|
||
const [vinError, setVinError] = useState(false);
|
||
const [step, setStep] = useState<"vin" | "categories" | "schema">("vin");
|
||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
||
const [isDark, setIsDark] = useState(() => {
|
||
const theme = getUserSettings().theme ?? "dark";
|
||
if (theme === "system") {
|
||
return window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||
}
|
||
return theme === "dark";
|
||
});
|
||
|
||
const toggleTheme = () => {
|
||
const next = isDark ? "light" : "dark";
|
||
document.documentElement.classList.toggle("dark", next === "dark");
|
||
setUserSetting("theme", next);
|
||
setIsDark(next === "dark");
|
||
};
|
||
|
||
// NHTSA VIN decode
|
||
useEffect(() => {
|
||
if (vin.length !== 17) {
|
||
setVinPreview(null);
|
||
setVinError(false);
|
||
return;
|
||
}
|
||
|
||
const controller = new AbortController();
|
||
setVinLoading(true);
|
||
setVinError(false);
|
||
|
||
fetch(`/api/vehicles/preview/${vin}`, { signal: controller.signal })
|
||
.then((res) => {
|
||
if (!res.ok) throw new Error("Bulunamadı");
|
||
return res.json();
|
||
})
|
||
.then((data) => {
|
||
const r = data.data !== undefined ? data.data : data;
|
||
if (r?.brandName) {
|
||
setVinPreview({
|
||
make: r.brandName,
|
||
model: r.model || "—",
|
||
year: r.year ? String(r.year) : "—",
|
||
engine: r.engine || "—",
|
||
});
|
||
} else {
|
||
setVinError(true);
|
||
}
|
||
setVinLoading(false);
|
||
})
|
||
.catch((err) => {
|
||
if (err.name !== "AbortError") {
|
||
setVinError(true);
|
||
setVinLoading(false);
|
||
}
|
||
});
|
||
|
||
return () => controller.abort();
|
||
}, [vin]);
|
||
|
||
return (
|
||
<div className="min-h-screen bg-background text-foreground">
|
||
{/* Header */}
|
||
<header className="sticky top-0 z-50 border-b border-border bg-background/80 backdrop-blur-md">
|
||
<div className="mx-auto flex h-16 max-w-5xl items-center justify-between px-4 sm:px-6">
|
||
<Link to="/" className="text-xl font-bold tracking-wider">
|
||
SASE
|
||
</Link>
|
||
<div className="flex items-center gap-3">
|
||
<button
|
||
type="button"
|
||
onClick={toggleTheme}
|
||
className="inline-flex size-9 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||
aria-label="Tema değiştir"
|
||
>
|
||
{isDark ? <Sun className="size-4" /> : <Moon className="size-4" />}
|
||
</button>
|
||
<span className="inline-flex items-center gap-1.5 rounded-full border border-border bg-muted px-3 py-1 text-xs font-medium text-muted-foreground">
|
||
<span className="size-1.5 rounded-full bg-brand" />
|
||
Demo
|
||
</span>
|
||
<Link to="/register">
|
||
<Button
|
||
size="sm"
|
||
className="rounded-full bg-foreground text-background hover:bg-foreground/90"
|
||
>
|
||
Tam Erişim
|
||
<ArrowRight className="ml-1.5 size-3.5" />
|
||
</Button>
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<main id="main-content" className="mx-auto max-w-5xl px-4 py-12 sm:px-6">
|
||
{/* Step indicator */}
|
||
<div className="mb-8 flex items-center justify-center gap-2 text-sm text-muted-foreground">
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
setStep("vin");
|
||
setSelectedCategory(null);
|
||
}}
|
||
className={`rounded-full px-3 py-1 transition ${step === "vin" ? "bg-foreground text-background" : "bg-muted"}`}
|
||
>
|
||
1. VIN Girin
|
||
</button>
|
||
<div className="h-px w-6 bg-border" />
|
||
<button
|
||
type="button"
|
||
onClick={() => vinPreview && setStep("categories")}
|
||
className={`rounded-full px-3 py-1 transition ${step === "categories" ? "bg-foreground text-background" : "bg-muted"} ${!vinPreview ? "opacity-50 cursor-not-allowed" : ""}`}
|
||
disabled={!vinPreview}
|
||
>
|
||
2. Kategori Seçin
|
||
</button>
|
||
<div className="h-px w-6 bg-border" />
|
||
<button
|
||
type="button"
|
||
onClick={() => selectedCategory && setStep("schema")}
|
||
className={`rounded-full px-3 py-1 transition ${step === "schema" ? "bg-foreground text-background" : "bg-muted"} ${!selectedCategory ? "opacity-50 cursor-not-allowed" : ""}`}
|
||
disabled={!selectedCategory}
|
||
>
|
||
3. Şema & Parçalar
|
||
</button>
|
||
</div>
|
||
|
||
{/* Step 1: VIN Input */}
|
||
{step === "vin" && (
|
||
<div className="mx-auto max-w-xl space-y-6">
|
||
<div className="text-center">
|
||
<h1 className="font-[family-name:var(--font-display)] text-3xl font-bold tracking-tight sm:text-4xl">
|
||
VIN ile Araç Tanımlama
|
||
</h1>
|
||
<p className="mt-3 text-muted-foreground">
|
||
17 haneli VIN numaranızı girin, aracınızı tanıyalım.
|
||
</p>
|
||
</div>
|
||
|
||
<div className="relative">
|
||
<Search className="absolute left-4 top-1/2 size-5 -translate-y-1/2 text-muted-foreground" />
|
||
<Input
|
||
value={vin}
|
||
onChange={(e) => setVin(e.target.value.toUpperCase())}
|
||
placeholder="Örnek: WVWZZZ1JZ3W597935"
|
||
maxLength={17}
|
||
className="h-14 rounded-2xl border-border bg-muted pl-12 pr-4 font-mono text-foreground placeholder:text-muted-foreground/70 focus-visible:ring-ring"
|
||
/>
|
||
</div>
|
||
|
||
{/* Progress bar */}
|
||
<div className="flex gap-0.5">
|
||
{KEYS_17.map((k, i) => (
|
||
<div
|
||
key={k}
|
||
className={`h-1 flex-1 rounded-full transition-colors duration-200 ${
|
||
i < vin.length ? "bg-brand" : "bg-border"
|
||
}`}
|
||
/>
|
||
))}
|
||
</div>
|
||
|
||
{vinLoading && (
|
||
<div className="flex items-center justify-center gap-2 rounded-2xl border border-border bg-surface p-4">
|
||
<Loader2 className="size-5 animate-spin text-muted-foreground" />
|
||
<span className="text-sm text-muted-foreground">Araç bilgileri alınıyor...</span>
|
||
</div>
|
||
)}
|
||
|
||
{vinPreview && !vinLoading && (
|
||
<div className="animate-fade-in-up rounded-2xl border border-brand/30 bg-surface p-6">
|
||
<div className="flex items-center gap-3">
|
||
<Car className="size-6 text-brand" />
|
||
<div>
|
||
<p className="font-semibold text-foreground">
|
||
{vinPreview.make} {vinPreview.model}
|
||
</p>
|
||
<p className="text-sm text-muted-foreground">
|
||
{vinPreview.year} {vinPreview.engine !== "—" ? `• ${vinPreview.engine}` : ""}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<Button
|
||
onClick={() => setStep("categories")}
|
||
variant="brand"
|
||
className="mt-4 w-full rounded-full"
|
||
>
|
||
Parça Kataloğuna Devam Et
|
||
<ArrowRight className="ml-2 size-4" />
|
||
</Button>
|
||
</div>
|
||
)}
|
||
|
||
{vinError && !vinLoading && (
|
||
<div className="rounded-2xl border border-border bg-surface p-4 text-center text-sm text-muted-foreground">
|
||
VIN bilgisi bulunamadı. Lütfen kontrol edin.
|
||
</div>
|
||
)}
|
||
|
||
{!vin && (
|
||
<button
|
||
type="button"
|
||
onClick={() => setVin("WVWZZZ1JZ3W597935")}
|
||
className="mx-auto block text-sm text-muted-foreground underline underline-offset-4 transition hover:text-foreground"
|
||
>
|
||
Örnek VIN ile deneyin →
|
||
</button>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Step 2: Categories (static mockup) */}
|
||
{step === "categories" && (
|
||
<div className="space-y-6">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h2 className="font-[family-name:var(--font-display)] text-2xl font-bold">
|
||
Parça Kategorileri
|
||
</h2>
|
||
{vinPreview && (
|
||
<p className="mt-1 text-sm text-muted-foreground">
|
||
{vinPreview.make} {vinPreview.model} ({vinPreview.year})
|
||
</p>
|
||
)}
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
setStep("vin");
|
||
setSelectedCategory(null);
|
||
}}
|
||
className="text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground"
|
||
>
|
||
Farklı VIN dene
|
||
</button>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||
{EXAMPLE_CATEGORIES.map((cat) => (
|
||
<button
|
||
type="button"
|
||
key={cat.name}
|
||
onClick={() => {
|
||
setSelectedCategory(cat.name);
|
||
setStep("schema");
|
||
}}
|
||
className="group rounded-2xl border border-border bg-surface p-5 text-left transition hover:border-foreground/20"
|
||
>
|
||
<div className="mb-3 inline-flex rounded-lg bg-muted p-2.5">
|
||
<FolderTree className="size-5 text-muted-foreground" />
|
||
</div>
|
||
<h3 className="font-semibold">{cat.name}</h3>
|
||
<p className="mt-1 text-xs text-muted-foreground">
|
||
{cat.subcategories.join(" • ")}
|
||
</p>
|
||
<div className="mt-3 text-xs text-muted-foreground/70 transition group-hover:text-foreground">
|
||
{cat.subcategories.length} alt kategori →
|
||
</div>
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Step 3: Schema & Parts (static mockup with overlay) */}
|
||
{step === "schema" && (
|
||
<div className="space-y-6">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h2 className="font-[family-name:var(--font-display)] text-2xl font-bold">
|
||
{selectedCategory} — Şema & Parçalar
|
||
</h2>
|
||
{vinPreview && (
|
||
<p className="mt-1 text-sm text-muted-foreground">
|
||
{vinPreview.make} {vinPreview.model} ({vinPreview.year})
|
||
</p>
|
||
)}
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={() => setStep("categories")}
|
||
className="text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground"
|
||
>
|
||
Kategorilere dön
|
||
</button>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||
{/* Schema mockup with watermark */}
|
||
<div className="relative overflow-hidden rounded-2xl border border-border bg-surface">
|
||
<div className="p-4">
|
||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||
<MousePointerClick className="size-4" />
|
||
İnteraktif Şema
|
||
</div>
|
||
</div>
|
||
<div className="relative aspect-square bg-muted/50 p-6">
|
||
{/* Simplified schema grid */}
|
||
<div className="grid h-full grid-cols-4 grid-rows-4 gap-2">
|
||
{KEYS_16.map((k, i) => (
|
||
<div
|
||
key={k}
|
||
className={`flex items-center justify-center rounded-lg border border-border text-xs text-muted-foreground ${
|
||
[2, 5, 9, 13].includes(i)
|
||
? "border-brand/50 bg-brand/10 text-brand"
|
||
: "bg-muted/50"
|
||
}`}
|
||
>
|
||
{[2, 5, 9, 13].includes(i)
|
||
? EXAMPLE_SCHEMA_PARTS[[2, 5, 9, 13].indexOf(i)]?.position
|
||
: ""}
|
||
</div>
|
||
))}
|
||
</div>
|
||
{/* Watermark overlay */}
|
||
<div className="absolute inset-0 flex items-center justify-center bg-background/60 backdrop-blur-[2px]">
|
||
<div className="text-center">
|
||
<Lock className="mx-auto size-8 text-muted-foreground" />
|
||
<p className="mt-2 text-sm font-medium text-muted-foreground">
|
||
Tam şema erişimi için kayıt olun
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Parts list */}
|
||
<div className="space-y-3">
|
||
<h3 className="text-sm font-medium text-muted-foreground">OEM Parça Listesi</h3>
|
||
{EXAMPLE_SCHEMA_PARTS.map((part, idx) => (
|
||
<div
|
||
key={part.code}
|
||
className="flex items-center justify-between rounded-xl border border-border bg-surface p-4"
|
||
>
|
||
<div>
|
||
<span className="font-mono text-sm text-foreground">{part.code}</span>
|
||
<p className="mt-0.5 text-sm text-muted-foreground">{part.name}</p>
|
||
</div>
|
||
{idx < 2 ? (
|
||
<span className="rounded-full bg-brand/10 px-2 py-0.5 text-xs text-brand">
|
||
Görünür
|
||
</span>
|
||
) : (
|
||
<span className="rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground">
|
||
<Lock className="mr-1 inline size-3" />
|
||
Kilitli
|
||
</span>
|
||
)}
|
||
</div>
|
||
))}
|
||
|
||
{/* Signup overlay CTA */}
|
||
<div className="mt-6 rounded-2xl border-2 border-dashed border-border bg-surface p-6 text-center">
|
||
<h3 className="font-semibold">Tüm parçaları ve şemaları görün</h3>
|
||
<p className="mt-2 text-sm text-muted-foreground">
|
||
30 gün ücretsiz deneyin — kredi kartı gerekmez
|
||
</p>
|
||
<Link to="/register">
|
||
<Button className="mt-4 rounded-full bg-foreground text-background hover:bg-foreground/90">
|
||
Ücretsiz Kayıt Ol
|
||
<ArrowRight className="ml-2 size-4" />
|
||
</Button>
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</main>
|
||
</div>
|
||
);
|
||
}
|