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:
38
apps/web/src/components/ui/smart-animate-text.tsx
Normal file
38
apps/web/src/components/ui/smart-animate-text.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user