feat(web): landing marka karuselinde logolar + tıklayınca animasyonlu marka adı

- Kayan şeritte metin yerine CarBrandLogo (cardog-icons + statik SVG'ler)
- Logoya tıklayınca seçilen marka adı SmartAnimateText ile karakter karakter yazılır
- globals.css: char-in keyframe + animate-char-in utility

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-25 23:37:24 +03:00
parent 44bea9c5bc
commit 96171c0726
3 changed files with 91 additions and 11 deletions

View File

@@ -0,0 +1,38 @@
import { cn } from "@sase/ui";
interface SmartAnimateTextProps {
/** Metin — karakter karakter animasyonla görünür. */
text: string;
className?: string;
/** Her karaktere uygulanan ek sınıf. */
charClassName?: string;
/** Karakterler arası gecikme (ms). */
stagger?: number;
}
/**
* Metni karakter karakter (blur + yukarı kayma + fade) animasyonla yazar.
* Animasyonu yeniden tetiklemek için bileşene değişen bir `key` verin.
*/
export function SmartAnimateText({
text,
className,
charClassName,
stagger = 35,
}: SmartAnimateTextProps) {
return (
<span className={cn("inline-flex", className)} aria-label={text}>
{Array.from(text).map((char, i) => (
<span
// biome-ignore lint/suspicious/noArrayIndexKey: konum tabanlı animasyon sırası gerekli
key={`${char}-${i}`}
aria-hidden="true"
className={cn("inline-block animate-char-in", charClassName)}
style={{ animationDelay: `${i * stagger}ms`, animationFillMode: "backwards" }}
>
{char === " " ? " " : char}
</span>
))}
</span>
);
}

View File

@@ -152,6 +152,22 @@
}
}
@keyframes char-in {
0% {
opacity: 0;
transform: translateY(0.45em) scale(0.92);
filter: blur(5px);
}
60% {
opacity: 1;
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
filter: blur(0);
}
}
@keyframes accordion-down {
from {
height: 0;
@@ -182,6 +198,9 @@
.animate-fade-in {
animation: fade-in 0.4s ease-out;
}
.animate-char-in {
animation: char-in 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}
.carousel-track:hover .animate-scroll-left {
animation-play-state: paused;
}

View File

@@ -1,3 +1,5 @@
import { CarBrandLogo } from "@/components/ui/car-brand-logo";
import { SmartAnimateText } from "@/components/ui/smart-animate-text";
import { KEYS_17, dynamicKeys } from "@/lib/keys";
import { Button, Input, Separator } from "@sase/ui";
import { Link, createFileRoute, useNavigate } from "@tanstack/react-router";
@@ -433,6 +435,8 @@ export function HomePage() {
const { isAuthenticated } = useAuth();
const [vin, setVin] = useState("");
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
// Karusel: tıklanan marka adı (n = animasyonu her tıklamada yeniden tetiklemek için)
const [selectedBrand, setSelectedBrand] = useState<{ name: string; n: number } | null>(null);
const [isDark, setIsDark] = useState(() => {
const theme = getUserSettings().theme ?? "dark";
if (theme === "system") {
@@ -965,8 +969,18 @@ export function HomePage() {
{/* ─── 3. MARKA CAROUSEL ────────────────────────────────────────── */}
<section className="border-y border-border py-12">
<div className="mx-auto max-w-7xl px-4 sm:px-6">
<p className="mb-8 text-center text-sm text-muted-foreground">
Desteklenen 50+ Otomobil Markası
<p className="mb-8 flex h-8 items-center justify-center text-center">
{selectedBrand ? (
<SmartAnimateText
key={selectedBrand.n}
text={selectedBrand.name}
className="font-[family-name:var(--font-display)] text-2xl font-bold text-brand"
/>
) : (
<span className="text-sm text-muted-foreground">
Desteklenen 50+ Otomobil Markası
</span>
)}
</p>
</div>
<div className="carousel-track relative overflow-hidden">
@@ -974,15 +988,24 @@ export function HomePage() {
<div className="pointer-events-none absolute left-0 top-0 z-10 h-full w-24 bg-gradient-to-r from-background to-transparent" />
<div className="pointer-events-none absolute right-0 top-0 z-10 h-full w-24 bg-gradient-to-l from-background to-transparent" />
<div className="animate-scroll-left flex w-max gap-8 whitespace-nowrap">
{[...BRANDS.map((b) => `${b}::a`), ...BRANDS.map((b) => `${b}::b`)].map((id) => (
<span
key={id}
className="inline-block text-sm font-medium text-muted-foreground transition hover:text-foreground"
>
{id.split("::")[0]}
</span>
))}
<div className="animate-scroll-left flex w-max items-center gap-12 whitespace-nowrap">
{[...BRANDS.map((b) => `${b}::a`), ...BRANDS.map((b) => `${b}::b`)].map((id) => {
const brand = id.split("::")[0];
return (
<button
type="button"
key={id}
onClick={() =>
setSelectedBrand((prev) => ({ name: brand, n: (prev?.n ?? 0) + 1 }))
}
title={brand}
aria-label={`${brand} markasını göster`}
className="inline-flex shrink-0 cursor-pointer items-center opacity-60 grayscale transition hover:opacity-100 hover:grayscale-0 focus-visible:opacity-100 focus-visible:grayscale-0 focus-visible:outline-none"
>
<CarBrandLogo brandName={brand} size={44} />
</button>
);
})}
</div>
</div>
</section>