Files
reui-blocks/blocks/hero187.tsx
T
2026-07-22 08:52:46 +03:30

235 lines
7.2 KiB
TypeScript

"use client";
import Autoplay from "embla-carousel-autoplay";
import {
ArrowRight,
Blend,
ChartNoAxesColumn,
CircleDot,
Diamond,
} from "lucide-react";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import type { CarouselApi } from "@/components/ui/carousel";
import {
Carousel,
CarouselContent,
CarouselItem,
} from "@/components/ui/carousel";
import type { HeroFeatureIconsProps, Image } from "@/lib/hero-feature-icons";
import { cn } from "@/lib/utils";
const DEFAULT_FEATURES = [
{
title: "گردش‌کارهای سفارشی",
description: "پیشرفت را در جریان‌های کاری سفارشی تیم خود دنبال کنید.",
icon: CircleDot,
},
{
title: "نقاط عطف",
description: "پروژه‌ها را به فازهای مشخص تقسیم کنید.",
icon: Diamond,
},
{
title: "پروژه‌های بین‌تیمی",
description: "همکاری بین تیم‌ها و بخش‌ها.",
icon: Blend,
},
{
title: "بینش پیشرفت",
description: "دامنه، سرعت و پیشرفت را در طول زمان رصد کنید.",
icon: ChartNoAxesColumn,
},
];
type HeroImage = Image & { label?: string };
const DEFAULT_IMAGES: [HeroImage, ...HeroImage[]] = [
{
src: "/block-media/shadcnblocks/block/placeholder-1.svg",
alt: "تصویر نمونه",
label: "کانبان",
},
{
src: "/block-media/shadcnblocks/block/placeholder-2.svg",
alt: "تصویر نمونه",
label: "مسائل",
},
{
src: "/block-media/shadcnblocks/block/placeholder-3.svg",
alt: "تصویر نمونه",
label: "افزودن مسئله",
},
];
const Hero187 = ({
heading = "مجموعه بلاک‌های آماده رابط کاربری",
description = "صدها بلاک و کامپوننت آماده مبتنی بر رابط کاربری، استایل‌دهی مدرن و ری یو آی که مستقیماً در پروژه‌های شما قابل استفاده هستند.",
buttonPrimary = { text: "شروع رایگان", href: "#" },
buttonSecondary = { text: "مستندات", href: "#" },
features = DEFAULT_FEATURES,
images = DEFAULT_IMAGES,
className,
}: HeroFeatureIconsProps) => {
const [currentSlide, setCurrentSlide] = useState(0);
const [api, setApi] = useState<CarouselApi>();
useEffect(() => {
if (!api) {
return;
}
api.on("select", () => {
setCurrentSlide(api.selectedScrollSnap());
});
}, [api]);
const activeImages = images ?? DEFAULT_IMAGES;
return (
<section
className={cn(
"relative overflow-hidden py-32 dark:bg-gray-900",
className,
)}
>
{/* Gradient border */}
<div className="relative container grid gap-12 lg:grid-cols-[1fr_1.2fr]">
<div className="absolute inset-x-0 bottom-0 z-10 h-px bg-linear-to-r from-transparent via-border to-border" />
{/* Left side - Content */}
<div className="relative z-10 space-y-8 lg:space-y-12">
<div>
<h1 className="text-3xl font-semibold tracking-tight sm:text-xl md:text-2xll lg:text-2xll">
{heading}
</h1>
<p className="mt-6 text-xl font-medium text-muted-foreground">
{description}
</p>
</div>
{/* Features */}
<div className="grid grid-cols-2 gap-5">
{features?.map((feature) => {
const Icon = feature.icon;
return (
<div key={feature.title} className="flex gap-2.5 lg:gap-5">
<Icon className="mt-1 size-4 shrink-0 lg:size-5" />
<div>
<h2 className="font-semibold">{feature.title}</h2>
<p className="text-sm text-muted-foreground">
{feature.description}
</p>
</div>
</div>
);
})}
</div>
<div className="flex flex-wrap items-center gap-4">
{buttonPrimary && (
<Button asChild aria-label={buttonPrimary.text}>
<a href={buttonPrimary.href}>{buttonPrimary.text}</a>
</Button>
)}
{buttonSecondary && (
<Button asChild variant="outline" className="max-sm:hidden">
<a href={buttonSecondary.href}>
<span className="flex items-center gap-2 text-start whitespace-pre-wrap">
{buttonSecondary.text}
<ArrowRight className="size-4 rtl:rotate-180" />
</span>
</a>
</Button>
)}
</div>
<SlideIndicator
currentSlide={currentSlide}
images={activeImages}
className="mb-4! max-lg:hidden"
api={api}
/>
</div>
{/* Outer-edge carousel: bleed with -me so RTL/LTR both extend away from copy */}
<div className="relative min-w-0 -me-[max(2rem,calc((100vw-80rem)/2+2rem))] max-lg:translate-x-10 rtl:max-lg:-translate-x-10">
<Carousel
dir="rtl"
className="size-full [&>div]:size-full"
setApi={setApi}
opts={{
loop: true,
direction: "rtl",
align: "start",
}}
plugins={[Autoplay({ delay: 4000, stopOnInteraction: true })]}
>
<CarouselContent className="size-full">
{activeImages.map((image, index) => (
<CarouselItem key={index} className="overflow-hidden">
<img
src={image.src}
alt={image.alt}
className="size-full min-h-[30rem] overflow-hidden rounded-t-xl object-contain"
/>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
</div>
</div>
<SlideIndicator
currentSlide={currentSlide}
images={activeImages}
className="mt-6 mb-8 lg:hidden"
api={api}
/>
</section>
);
};
interface SlideIndicatorProps {
currentSlide: number;
images: HeroImage[];
className?: string;
api: CarouselApi | null;
}
const SlideIndicator = ({
currentSlide,
images,
className,
api,
}: SlideIndicatorProps) => {
return (
<div
className={cn("flex flex-col items-center gap-2 font-medium", className)}
>
<div className="">
<span className="text-foreground-700">
{currentSlide + 1} از {images.length} {" "}
</span>
<span className="text-primary">{images[currentSlide].label}</span>
</div>
<div className="flex gap-2">
{images.map((_, index) => (
<button
key={index}
onClick={() => api?.scrollTo(index)}
aria-label={`رفتن به اسلاید ${index + 1}`}
className={cn(
"h-0.5 w-6 rounded-full transition-colors",
index === currentSlide
? "bg-primary"
: "bg-primary/20 hover:bg-primary/40",
)}
/>
))}
</div>
</div>
);
};
export { Hero187 };