Files
ar.badieiandCursor c2376b3b9e
Build & Push Docker Image / build (push) Has been cancelled
feat: redesign landing page with brand system and section blocks
Refresh the homepage around new hero/services/roadmap sections, shared design tokens, and updated brand assets.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 17:00:55 +03:30

169 lines
4.4 KiB
TypeScript

"use client";
import { useMemo, useRef } from "react";
import AutoScroll from "embla-carousel-auto-scroll";
import { Icon } from "@iconify/react";
import {
Carousel,
CarouselContent,
CarouselItem,
} from "@/components/ui/carousel";
import { Container } from "@/components/container";
import { MotionReveal } from "@/components/motion-reveal";
import { layout } from "@/lib/layout";
import { type } from "@/lib/typography";
import { cn } from "@/lib/utils";
const NORMAL_SPEED = 1;
const HOVER_SPEED = 0.35;
interface Logo {
id: string;
description: string;
icon: string;
className?: string;
}
interface Logos3Props {
heading?: string;
logos?: Logo[];
className?: string;
}
const defaultLogos: Logo[] = [
{
id: "logo-1",
description: "Next.js",
icon: "logos:nextjs-icon",
className: "size-8",
},
{
id: "logo-2",
description: "React",
icon: "logos:react",
className: "size-8",
},
{
id: "logo-3",
description: "TypeScript",
icon: "logos:typescript-icon",
className: "size-8",
},
{
id: "logo-4",
description: "Tailwind CSS",
icon: "logos:tailwindcss-icon",
className: "size-8",
},
{
id: "logo-5",
description: "WordPress",
icon: "skill-icons:wordpress",
className: "size-8",
},
{
id: "logo-6",
description: "Google",
icon: "logos:google-icon",
className: "size-8",
},
{
id: "logo-7",
description: "Figma",
icon: "logos:figma",
className: "size-8",
},
{
id: "logo-8",
description: "Vercel",
icon: "logos:vercel-icon",
className: "size-8",
},
];
export function Logos3({
heading = "همکاری اخیر ما با برندهای معتبر",
logos = defaultLogos,
className,
}: Logos3Props) {
const speedRef = useRef(NORMAL_SPEED);
// Embla reads `speed` each animation frame via numeric coercion (`valueOf`),
// so hover can slow the scroll without stopping or re-initializing the plugin.
const autoScrollPlugin = useMemo(
() =>
AutoScroll({
playOnInit: true,
speed: {
valueOf: () => speedRef.current,
} as unknown as number,
stopOnInteraction: false,
stopOnMouseEnter: false,
startDelay: 0,
}),
[]
);
return (
<section
className={cn(
"section-shell bg-section-muted",
layout.scrollMargin,
layout.sectionY,
className
)}
>
<Container className="section-content flex flex-col items-center text-center">
<MotionReveal>
<h2 className={cn(type.h2, "text-balance")}>{heading}</h2>
</MotionReveal>
</Container>
<Container className="section-content pt-4 md:pt-14 lg:pt-16">
<MotionReveal>
<div
className={cn(
"relative flex items-center justify-center overflow-x-clip",
// Fade logos via mask (no painted overlay) so edges match translucent section surfaces
"[mask-image:linear-gradient(to_right,transparent,black_12%,black_88%,transparent)]",
"[-webkit-mask-image:linear-gradient(to_right,transparent,black_12%,black_88%,transparent)]"
)}
onMouseEnter={() => {
speedRef.current = HOVER_SPEED;
}}
onMouseLeave={() => {
speedRef.current = NORMAL_SPEED;
}}
>
<Carousel
opts={{ loop: true, direction: "rtl" }}
plugins={[autoScrollPlugin]}
>
<CarouselContent className="ml-0">
{logos.map((logo) => (
<CarouselItem
key={logo.id}
className="flex basis-1/3 justify-center pl-0 md:basis-1/5 lg:basis-1/6"
>
<div className="mx-2 flex shrink-0 items-center justify-center sm:mx-8 md:mx-10">
<Icon
icon={logo.icon}
className={cn(
"shrink-0 opacity-70 transition-opacity duration-200 hover:opacity-100",
"dark:opacity-80 dark:hover:opacity-100",
logo.className
)}
aria-label={logo.description}
/>
</div>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
</div>
</MotionReveal>
</Container>
</section>
);
}