201 lines
6.2 KiB
TypeScript
201 lines
6.2 KiB
TypeScript
"use client";
|
|
|
|
import Autoplay from "embla-carousel-autoplay";
|
|
import { AnimatePresence, motion } from "framer-motion";
|
|
import React, { useCallback, useMemo } from "react";
|
|
|
|
import {
|
|
Carousel,
|
|
type CarouselApi,
|
|
CarouselContent,
|
|
CarouselItem,
|
|
} from "@/components/ui/carousel";
|
|
import { useIsMobile } from "@/hooks/use-mobile";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
interface Hero228Props {
|
|
className?: string;
|
|
}
|
|
|
|
const TESTIMONIALS = [
|
|
{
|
|
id: 1,
|
|
image: "/block-media/shadcnblocks/block/lummi/person1.jpeg",
|
|
name: "علی رضایی",
|
|
},
|
|
{
|
|
id: 2,
|
|
image: "/block-media/shadcnblocks/block/lummi/person2.jpeg",
|
|
name: "سارا محمدی",
|
|
},
|
|
{
|
|
id: 3,
|
|
image: "/block-media/shadcnblocks/block/lummi/person3.jpeg",
|
|
name: "رضا کریمی",
|
|
},
|
|
{
|
|
id: 4,
|
|
image: "/block-media/shadcnblocks/block/lummi/person4.jpeg",
|
|
name: "نرگس حسینی",
|
|
},
|
|
{
|
|
id: 5,
|
|
image: "/block-media/shadcnblocks/block/lummi/person5.jpeg",
|
|
name: "امیر نوری",
|
|
},
|
|
{
|
|
id: 6,
|
|
image: "/block-media/shadcnblocks/block/lummi/bw3.jpeg",
|
|
name: "الهام کاظمی",
|
|
},
|
|
{
|
|
id: 7,
|
|
image: "/block-media/shadcnblocks/block/lummi/bw11.jpeg",
|
|
name: "کیان موسوی",
|
|
},
|
|
] as const;
|
|
|
|
const Hero228 = ({ className }: Hero228Props) => {
|
|
const [api, setApi] = React.useState<CarouselApi>();
|
|
const [current, setCurrent] = React.useState(0);
|
|
|
|
React.useEffect(() => {
|
|
if (!api) return;
|
|
|
|
api.on("select", () => {
|
|
setCurrent(api.selectedScrollSnap());
|
|
});
|
|
}, [api]);
|
|
|
|
const isMobile = useIsMobile();
|
|
const slideCount = isMobile
|
|
? TESTIMONIALS.length
|
|
: TESTIMONIALS.length + 2;
|
|
|
|
const activeName =
|
|
TESTIMONIALS[current % TESTIMONIALS.length]?.name ?? TESTIMONIALS[0].name;
|
|
|
|
const getImageSrc = useCallback((index: number) => {
|
|
if (index === TESTIMONIALS.length) return TESTIMONIALS[0].image;
|
|
if (index === TESTIMONIALS.length + 1) return TESTIMONIALS[1].image;
|
|
if (index === TESTIMONIALS.length + 2) return TESTIMONIALS[2].image;
|
|
return TESTIMONIALS[index]?.image ?? TESTIMONIALS[0].image;
|
|
}, []);
|
|
|
|
// RTL track: current is on the physical right, then center, then left
|
|
const getRotation = useCallback(
|
|
(index: number) => {
|
|
if (index === current) {
|
|
return "md:relative md:z-0 md:translate-x-40 md:scale-75 md:rotate-45";
|
|
}
|
|
if (index === current + 1) {
|
|
return "md:relative md:z-10 md:rotate-0";
|
|
}
|
|
if (index === current + 2) {
|
|
return "md:relative md:z-0 md:-translate-x-40 md:scale-75 md:-rotate-45";
|
|
}
|
|
return "";
|
|
},
|
|
[current],
|
|
);
|
|
|
|
const scrollbarBars = useMemo(
|
|
() =>
|
|
[...Array(40)].map((_, item) => (
|
|
<motion.div
|
|
key={item}
|
|
initial={{
|
|
opacity: 0.2,
|
|
filter: "blur(1px)",
|
|
}}
|
|
animate={{
|
|
opacity: item % 5 === 0 ? 1 : 0.2,
|
|
filter: "blur(0px)",
|
|
}}
|
|
transition={{
|
|
duration: 0.2,
|
|
delay: item % 5 === 0 ? (item / 5) * 0.05 : 0,
|
|
ease: "easeOut",
|
|
}}
|
|
className={cn(
|
|
"w-px bg-foreground",
|
|
item % 5 === 0 ? "h-[15px]" : "h-[10px]",
|
|
)}
|
|
/>
|
|
)),
|
|
[],
|
|
);
|
|
|
|
return (
|
|
<section className={cn("relative w-full overflow-hidden bg-background py-32", className)}>
|
|
<div className="container flex min-w-0 flex-col items-center justify-center gap-4 text-center">
|
|
<h1 className="max-w-3xl text-balance text-2xll font-medium tracking-tighter text-foreground md:px-9 md:text-2xll">
|
|
<span className="font-playfair italic">همه</span> ابزارهای لازم برای{" "}
|
|
<span className="font-playfair italic">خلاق و</span> توسعه سریع
|
|
</h1>
|
|
<p className="mt-5 max-w-xl text-balance text-muted-foreground/80">
|
|
صدها بلاک و کامپوننت آماده مبتنی بر رابط کاربری، استایلدهی مدرن و ری یو آی که مستقیماً در پروژههای شما قابل استفاده هستند.
|
|
</p>
|
|
|
|
<Carousel
|
|
className="relative w-full min-w-0 max-w-5xl overflow-hidden"
|
|
opts={{
|
|
loop: true,
|
|
align: "start",
|
|
slidesToScroll: 1,
|
|
direction: "rtl",
|
|
}}
|
|
plugins={[
|
|
Autoplay({
|
|
delay: 1000,
|
|
stopOnInteraction: true,
|
|
}),
|
|
]}
|
|
setApi={setApi}
|
|
>
|
|
{/* Override -ml-4 / pl-4 — they break RTL track sizing */}
|
|
<CarouselContent className="-ml-0!">
|
|
{Array.from({ length: slideCount }).map((_, index) => (
|
|
<CarouselItem
|
|
key={index}
|
|
className="my-10 basis-full pl-0! md:basis-1/3"
|
|
>
|
|
<div
|
|
className={cn(
|
|
"h-105 w-full min-w-0 overflow-hidden transition-transform duration-500 ease-in-out",
|
|
getRotation(index),
|
|
)}
|
|
>
|
|
<img
|
|
src={getImageSrc(index)}
|
|
className="size-full object-cover"
|
|
alt={activeName}
|
|
/>
|
|
</div>
|
|
</CarouselItem>
|
|
))}
|
|
</CarouselContent>
|
|
<div className="absolute inset-x-0 bottom-0 flex w-full translate-y-full flex-col items-center justify-center gap-2">
|
|
<div className="flex gap-2">{scrollbarBars}</div>
|
|
<AnimatePresence mode="popLayout" initial={false}>
|
|
<motion.p
|
|
key={activeName}
|
|
className="w-full text-lg font-medium"
|
|
initial={{ opacity: 0, y: 20, scale: 0.9, filter: "blur(5px)" }}
|
|
animate={{ opacity: 1, y: 0, scale: 1, filter: "blur(0px)" }}
|
|
exit={{ opacity: 0, y: -20, scale: 0.9, filter: "blur(5px)" }}
|
|
transition={{ duration: 0.5 }}
|
|
>
|
|
{activeName}
|
|
</motion.p>
|
|
</AnimatePresence>
|
|
<div className="flex gap-2">{scrollbarBars}</div>
|
|
</div>
|
|
</Carousel>
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
export { Hero228 };
|