Merge pull request 'dev' (#51) from dev into main

Reviewed-on: #51
This commit was merged in pull request #51.
This commit is contained in:
2026-05-25 19:26:36 +00:00
6 changed files with 120 additions and 58 deletions

View File

@@ -6,7 +6,7 @@
<title>Şase Numarası Sorgulama &amp; OEM Parça Kataloğu | Sase.tr</title> <title>Şase Numarası Sorgulama &amp; OEM Parça Kataloğu | Sase.tr</title>
<meta <meta
name="description" name="description"
content="Araç şase numarasını (VIN) girin, saniyeler içinde OEM parça kodlarına ulaşın. 27+ marka, 243.000+ parça. Yanlış sipariş yapmadan doğru parçayı bulun." content="Araç şase numarasını (VIN) girin, saniyeler içinde OEM parça kodlarına ulaşın. 50+ marka, 1M+ parça. Yanlış sipariş yapmadan doğru parçayı bulun."
/> />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" /> <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" /> <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
@@ -16,7 +16,7 @@
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:url" content="https://sase.tr" /> <meta property="og:url" content="https://sase.tr" />
<meta property="og:title" content="Şase Numarası Sorgulama &amp; OEM Parça Kataloğu | Sase.tr" /> <meta property="og:title" content="Şase Numarası Sorgulama &amp; OEM Parça Kataloğu | Sase.tr" />
<meta property="og:description" content="Araç şase numarasını girin, saniyeler içinde doğru OEM parça kodlarına ulaşın. 27+ marka, 243.000+ parça." /> <meta property="og:description" content="Araç şase numarasını girin, saniyeler içinde doğru OEM parça kodlarına ulaşın. 50+ marka, 1M+ parça." />
<meta property="og:image" content="https://sase.tr/og-image.png" /> <meta property="og:image" content="https://sase.tr/og-image.png" />
<meta property="og:locale" content="tr_TR" /> <meta property="og:locale" content="tr_TR" />
<meta property="og:site_name" content="Sase.tr" /> <meta property="og:site_name" content="Sase.tr" />
@@ -24,7 +24,7 @@
<!-- Twitter Card --> <!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Şase Numarası Sorgulama &amp; OEM Parça Kataloğu | Sase.tr" /> <meta name="twitter:title" content="Şase Numarası Sorgulama &amp; OEM Parça Kataloğu | Sase.tr" />
<meta name="twitter:description" content="Araç şase numarasını girin, saniyeler içinde doğru OEM parça kodlarına ulaşın. 27+ marka, 243.000+ parça." /> <meta name="twitter:description" content="Araç şase numarasını girin, saniyeler içinde doğru OEM parça kodlarına ulaşın. 50+ marka, 1M+ parça." />
<meta name="twitter:image" content="https://sase.tr/og-image.png" /> <meta name="twitter:image" content="https://sase.tr/og-image.png" />
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />

View File

@@ -2,7 +2,7 @@ import { useEffect } from "react";
const DEFAULT_TITLE = "Şase Numarası Sorgulama & OEM Parça Kataloğu | Sase.tr"; const DEFAULT_TITLE = "Şase Numarası Sorgulama & OEM Parça Kataloğu | Sase.tr";
const DEFAULT_DESCRIPTION = const DEFAULT_DESCRIPTION =
"Araç şase numarasını (VIN) girin, saniyeler içinde OEM parça kodlarına ulaşın. 27+ marka, 243.000+ parça. Yanlış sipariş yapmadan doğru parçayı bulun."; "Araç şase numarasını (VIN) girin, saniyeler içinde OEM parça kodlarına ulaşın. 50+ marka, 1M+ parça. Yanlış sipariş yapmadan doğru parçayı bulun.";
const DEFAULT_CANONICAL = "https://sase.tr"; const DEFAULT_CANONICAL = "https://sase.tr";
interface PageMetaOptions { interface PageMetaOptions {

View File

@@ -109,11 +109,11 @@ function AuthLayout() {
<circle cx="7" cy="17" r="2" /> <circle cx="7" cy="17" r="2" />
<circle cx="17" cy="17" r="2" /> <circle cx="17" cy="17" r="2" />
</svg> </svg>
27 Marka 50+ Marka
</span> </span>
<span className="inline-flex items-center gap-1.5 rounded-full border border-border bg-surface/60 px-3 py-1.5 text-xs text-foreground/80 backdrop-blur-sm"> <span className="inline-flex items-center gap-1.5 rounded-full border border-border bg-surface/60 px-3 py-1.5 text-xs text-foreground/80 backdrop-blur-sm">
<Database className="size-3" /> <Database className="size-3" />
<span className="tabular">243K+ OEM Parça</span> <span className="tabular">1M+ OEM Parça</span>
</span> </span>
<span className="inline-flex items-center gap-1.5 rounded-full border border-border bg-surface/60 px-3 py-1.5 text-xs text-foreground/80 backdrop-blur-sm"> <span className="inline-flex items-center gap-1.5 rounded-full border border-border bg-surface/60 px-3 py-1.5 text-xs text-foreground/80 backdrop-blur-sm">
<ShieldCheck className="size-3" /> <ShieldCheck className="size-3" />

View File

@@ -1,3 +1,4 @@
import { api } from "@/lib/api-client";
import { signIn, signUp } from "@/lib/auth-client"; import { signIn, signUp } from "@/lib/auth-client";
import { startAction } from "@/lib/faro"; import { startAction } from "@/lib/faro";
import { track as trackMeta } from "@/lib/meta-pixel"; import { track as trackMeta } from "@/lib/meta-pixel";
@@ -7,7 +8,7 @@ import { Button } from "@sase/ui";
import { Input } from "@sase/ui"; import { Input } from "@sase/ui";
import { Label } from "@sase/ui"; import { Label } from "@sase/ui";
import { Link, createFileRoute } from "@tanstack/react-router"; import { Link, createFileRoute } from "@tanstack/react-router";
import { ShieldCheck } from "lucide-react"; import { Check, Eye, EyeOff, ShieldCheck } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
export const Route = createFileRoute("/_auth/register")({ export const Route = createFileRoute("/_auth/register")({
@@ -25,6 +26,13 @@ function RegisterPage() {
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [refCode, setRefCode] = useState(ref || ""); const [refCode, setRefCode] = useState(ref || "");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [showRefCode, setShowRefCode] = useState(!!ref);
const passwordRules = [
{ ok: password.length >= 8, label: "8+ karakter" },
{ ok: /[A-Z]/.test(password), label: "1 büyük harf" },
{ ok: /[0-9]/.test(password), label: "1 rakam" },
];
// Land new users straight on the VIN search (value-first). The activation // Land new users straight on the VIN search (value-first). The activation
// "motion" + trial provisioning now runs as a modal on the search page // "motion" + trial provisioning now runs as a modal on the search page
// (see WelcomeOnboardingModal), so we carry welcome/vin/ref across. // (see WelcomeOnboardingModal), so we carry welcome/vin/ref across.
@@ -40,16 +48,40 @@ function RegisterPage() {
setLoading(true); setLoading(true);
try { try {
await signUp.email({ name, email, password, callbackURL: redirectUrl }); // better-auth returns { error } rather than throwing (see login.tsx).
capture("user_signed_up", { method: "email" }); // The old code awaited without checking error, so failed signups still
trackMeta("CompleteRegistration", { method: "email" }); // reported success, fired user_signed_up, and redirected to the dashboard.
const { error } = await signUp.email({ name, email, password, callbackURL: redirectUrl });
if (error) {
let exists = false;
try {
({ exists } = await api.post<{ exists: boolean }>("/users/check-email", { email }));
} catch {
// fall through to a generic message
}
if (exists) {
toast.error("Bu e-posta zaten kayıtlı", {
description: "Bunun yerine giriş yapmayı deneyin.",
});
} else {
toast.error("Kayıt başarısız", {
description:
error.message ?? "Şifre en az 8 karakter, 1 büyük harf ve 1 rakam içermelidir.",
});
}
return;
}
// The referral code travels via `?ref=` in redirectUrl; the welcome // The referral code travels via `?ref=` in redirectUrl; the welcome
// onboarding modal on the search page is the single place that applies it // onboarding modal on the search page is the single place that applies it
// (covers both email and Google OAuth signups). // (covers both email and Google OAuth signups).
capture("user_signed_up", { method: "email" });
trackMeta("CompleteRegistration", { method: "email" });
toast.success("Hesap oluşturuldu!"); toast.success("Hesap oluşturuldu!");
window.location.href = redirectUrl; window.location.href = redirectUrl;
} catch { } catch {
toast.error("Kayıt başarısız. Bu e-posta zaten kullanılıyor olabilir."); toast.error("Bağlantı hatası", { description: "Lütfen tekrar deneyin." });
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -137,38 +169,74 @@ function RegisterPage() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="password">Şifre</Label> <Label htmlFor="password">Şifre</Label>
<Input <div className="relative">
id="password" <Input
type="password" id="password"
placeholder="En az 8 karakter, 1 büyük harf, 1 rakam" type={showPassword ? "text" : "password"}
value={password} placeholder="En az 8 karakter"
onChange={(e) => setPassword(e.target.value)} value={password}
required onChange={(e) => setPassword(e.target.value)}
minLength={8} required
/> minLength={8}
</div> className="pr-10"
<div className="space-y-2"> />
<Label htmlFor="refCode"> <button
Referans Kodu <span className="text-muted-foreground">(opsiyonel)</span> type="button"
</Label> onClick={() => setShowPassword((v) => !v)}
<Input className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground transition-colors hover:text-foreground"
id="refCode" aria-label={showPassword ? "Şifreyi gizle" : "Şifreyi göster"}
type="text" >
placeholder="örn. MEU8EHF7" {showPassword ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
value={refCode} </button>
onChange={(e) => setRefCode(e.target.value.toUpperCase())} </div>
maxLength={20} {password.length > 0 && (
autoComplete="off" <ul className="flex flex-wrap gap-x-3 gap-y-1 text-xs">
className="font-mono uppercase" {passwordRules.map((rule) => (
/> <li
key={rule.label}
className={`flex items-center gap-1 ${
rule.ok ? "text-emerald-600 dark:text-emerald-400" : "text-muted-foreground"
}`}
>
<Check className={`size-3 ${rule.ok ? "opacity-100" : "opacity-30"}`} />
{rule.label}
</li>
))}
</ul>
)}
</div> </div>
{showRefCode ? (
<div className="space-y-2">
<Label htmlFor="refCode">
Referans Kodu <span className="text-muted-foreground">(opsiyonel)</span>
</Label>
<Input
id="refCode"
type="text"
placeholder="örn. MEU8EHF7"
value={refCode}
onChange={(e) => setRefCode(e.target.value.toUpperCase())}
maxLength={20}
autoComplete="off"
className="font-mono uppercase"
/>
</div>
) : (
<button
type="button"
onClick={() => setShowRefCode(true)}
className="text-sm text-muted-foreground transition-colors hover:text-foreground hover:underline"
>
Referans kodun var mı?
</button>
)}
<Button type="submit" className="w-full" disabled={loading}> <Button type="submit" className="w-full" disabled={loading}>
{loading ? "Kayıt yapılıyor..." : "Kayıt Ol"} {loading ? "Kayıt yapılıyor..." : "Ücretsiz Başla"}
</Button> </Button>
</form> </form>
<p className="text-center text-xs text-muted-foreground"> <p className="text-center text-xs text-muted-foreground">
Kayıt olunca hemen VIN aramaya başlayın Kayıt olunca hemen VIN aramaya başlarsın · Spam göndermeyiz
</p> </p>
<p className="text-center text-sm"> <p className="text-center text-sm">

View File

@@ -1,6 +1,6 @@
import { usePageMeta } from "@/hooks/use-page-meta";
import { SiteHeader } from "@/components/site-header"; import { SiteHeader } from "@/components/site-header";
import { useBlogPost } from "@/hooks/use-blog"; import { useBlogPost } from "@/hooks/use-blog";
import { usePageMeta } from "@/hooks/use-page-meta";
import { Button } from "@sase/ui"; import { Button } from "@sase/ui";
import { Link, createFileRoute } from "@tanstack/react-router"; import { Link, createFileRoute } from "@tanstack/react-router";
import { ChevronRight } from "lucide-react"; import { ChevronRight } from "lucide-react";
@@ -97,7 +97,7 @@ const POSTS: Record<string, BlogPost> = {
<p> <p>
Sase.tr platformunda VIN numaranızı girerek saniyeler içinde aracınızın tam teknik Sase.tr platformunda VIN numaranızı girerek saniyeler içinde aracınızın tam teknik
bilgilerine ve orijinal parça kataloğuna erişebilirsiniz. 27'den fazla markayı destekleyen bilgilerine ve orijinal parça kataloğuna erişebilirsiniz. 50'den fazla markayı destekleyen
platformumuzla yanlış parça siparişlerine son verin. platformumuzla yanlış parça siparişlerine son verin.
</p> </p>
</div> </div>
@@ -252,8 +252,8 @@ const POSTS: Record<string, BlogPost> = {
<p> <p>
Sase.tr, Türkiye'nin yedek parça sektörüne özgü geliştirilen bu dijital dönüşümün Sase.tr, Türkiye'nin yedek parça sektörüne özgü geliştirilen bu dijital dönüşümün
öncüsüdür. VIN/şase numarası sorgulama, çoklu katalog entegrasyonu ve interaktif şema öncüsüdür. VIN/şase numarası sorgulama, çoklu katalog entegrasyonu ve interaktif şema
görüntüleme özellikleriyle geleneksel yapış biçimlerini dönüştürmektedir. Platform, 27+ görüntüleme özellikleriyle geleneksel yapış biçimlerini dönüştürmektedir. Platform, 50+
marka ve 243.000'den fazla OEM parça numarasıyla sektörün en kapsamlı dijital kataloğunu marka ve 1 milyondan fazla OEM parça numarasıyla sektörün en kapsamlı dijital kataloğunu
sunmaktadır. sunmaktadır.
</p> </p>
</div> </div>
@@ -331,7 +331,7 @@ const POSTS: Record<string, BlogPost> = {
Parça bulamadığınızda kategori ağacında bir seviye yukarı çıkarak daha geniş bir arama Parça bulamadığınızda kategori ağacında bir seviye yukarı çıkarak daha geniş bir arama
yapabilirsiniz. yapabilirsiniz.
</li> </li>
<li>27+ markanın tamamına erişmek için Full Paket aboneliği en avantajlı seçenektir.</li> <li>50+ markanın tamamına erişmek için Full Paket aboneliği en avantajlı seçenektir.</li>
</ul> </ul>
<h2 className="text-xl font-semibold text-foreground">Sonuç</h2> <h2 className="text-xl font-semibold text-foreground">Sonuç</h2>
@@ -355,18 +355,14 @@ const mdComponents = {
h3: (props: React.ComponentProps<"h3">) => ( h3: (props: React.ComponentProps<"h3">) => (
<h3 className="text-lg font-semibold text-foreground" {...props} /> <h3 className="text-lg font-semibold text-foreground" {...props} />
), ),
ul: (props: React.ComponentProps<"ul">) => ( ul: (props: React.ComponentProps<"ul">) => <ul className="list-disc space-y-2 pl-6" {...props} />,
<ul className="list-disc space-y-2 pl-6" {...props} />
),
ol: (props: React.ComponentProps<"ol">) => ( ol: (props: React.ComponentProps<"ol">) => (
<ol className="list-decimal space-y-2 pl-6" {...props} /> <ol className="list-decimal space-y-2 pl-6" {...props} />
), ),
strong: (props: React.ComponentProps<"strong">) => ( strong: (props: React.ComponentProps<"strong">) => (
<strong className="text-foreground" {...props} /> <strong className="text-foreground" {...props} />
), ),
a: (props: React.ComponentProps<"a">) => ( a: (props: React.ComponentProps<"a">) => <a className="text-foreground underline" {...props} />,
<a className="text-foreground underline" {...props} />
),
}; };
function MarkdownBody({ markdown }: { markdown: string }) { function MarkdownBody({ markdown }: { markdown: string }) {
@@ -406,9 +402,7 @@ function BlogPostPage() {
else if (isLoading) body = <p className="text-muted-foreground">Yükleniyor</p>; else if (isLoading) body = <p className="text-muted-foreground">Yükleniyor</p>;
else else
body = ( body = (
<p className="text-muted-foreground"> <p className="text-muted-foreground">{isError ? "Yazı bulunamadı." : "Yazı yüklenemedi."}</p>
{isError ? "Yazı bulunamadı." : "Yazı yüklenemedi."}
</p>
); );
return ( return (

View File

@@ -256,7 +256,7 @@ const BENTO_ITEMS = [
}, },
{ {
icon: Car, icon: Car,
title: "27+ Marka", title: "50+ Marka",
description: "Avrupa, Asya ve Amerikan markaları tek çatı altında.", description: "Avrupa, Asya ve Amerikan markaları tek çatı altında.",
stat: null, stat: null,
wide: false, wide: false,
@@ -294,7 +294,7 @@ const BENTO_ITEMS = [
const STATS = [ const STATS = [
{ value: "1.2sn", label: "Ortalama sorgu süresi" }, { value: "1.2sn", label: "Ortalama sorgu süresi" },
{ value: "27", label: "Desteklenen marka" }, { value: "27", label: "Desteklenen marka" },
{ value: "243K+", label: "OEM parça numarası" }, { value: "1M+", label: "OEM parça numarası" },
{ value: "%99.9", label: "Platform erişilebilirlik" }, { value: "%99.9", label: "Platform erişilebilirlik" },
]; ];
@@ -385,7 +385,7 @@ export function HomePage() {
usePageMeta({ usePageMeta({
title: "Şase Numarası Sorgulama & OEM Parça Kataloğu | Sase.tr", title: "Şase Numarası Sorgulama & OEM Parça Kataloğu | Sase.tr",
description: description:
"Araç şase numarasını (VIN) girin, saniyeler içinde OEM parça kodlarına ulaşın. 27+ marka, 243.000+ parça. Yanlış sipariş yapmadan doğru parçayı bulun.", "Araç şase numarasını (VIN) girin, saniyeler içinde OEM parça kodlarına ulaşın. 50+ marka, 1M+ parça. Yanlış sipariş yapmadan doğru parçayı bulun.",
canonical: "https://sase.tr", canonical: "https://sase.tr",
}); });
@@ -947,8 +947,8 @@ export function HomePage() {
<div className="mt-8 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: Zap, label: "Ortalama 1.2sn Sonuç" },
{ icon: Car, label: "27 Marka Desteği" }, { icon: Car, label: "50+ Marka Desteği" },
{ icon: Database, label: "243.000+ OEM Parça" }, { icon: Database, label: "1M+ OEM Parça" },
].map(({ icon: Icon, label }) => ( ].map(({ icon: Icon, label }) => (
<div <div
key={label} key={label}
@@ -966,7 +966,7 @@ export function HomePage() {
<section className="border-y border-border py-12"> <section className="border-y border-border py-12">
<div className="mx-auto max-w-7xl px-4 sm:px-6"> <div className="mx-auto max-w-7xl px-4 sm:px-6">
<p className="mb-8 text-center text-sm text-muted-foreground"> <p className="mb-8 text-center text-sm text-muted-foreground">
Desteklenen 27+ Otomobil Markası Desteklenen 50+ Otomobil Markası
</p> </p>
</div> </div>
<div className="carousel-track relative overflow-hidden"> <div className="carousel-track relative overflow-hidden">
@@ -1669,7 +1669,7 @@ export function HomePage() {
</p> </p>
<ul className="mt-5 grid grid-cols-1 gap-x-6 gap-y-3 sm:grid-cols-2"> <ul className="mt-5 grid grid-cols-1 gap-x-6 gap-y-3 sm:grid-cols-2">
{[ {[
"Tüm 27+ markaya erişim", "Tüm 50+ markaya erişim",
"Sınırsız VIN arama", "Sınırsız VIN arama",
"OEM parça kataloğu", "OEM parça kataloğu",
"İnteraktif şema görüntüleyici", "İnteraktif şema görüntüleyici",