243 lines
7.4 KiB
TypeScript
243 lines
7.4 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";
|
|
|
|
// Extended Image type with optional label for carousel indicators
|
|
type ImageWithLabel = Image & { label?: string };
|
|
|
|
const DEFAULT_FEATURES = [
|
|
{
|
|
title: "گردشکارهای سفارشی",
|
|
description:
|
|
"پیشرفت وظایف را در گردشکارهای سفارشی تیم خود مدیریت کنید.",
|
|
icon: CircleDot,
|
|
},
|
|
{
|
|
title: "نقاط عطف",
|
|
description: "پروژهها را به فازهای مشخص تقسیم کنید.",
|
|
icon: Diamond,
|
|
},
|
|
{
|
|
title: "پروژههای بینتیمی",
|
|
description: "همکاری میان تیمها و واحدهای مختلف را سادهتر کنید.",
|
|
icon: Blend,
|
|
},
|
|
{
|
|
title: "بینش پیشرفت",
|
|
description: "وضعیت پروژه، سرعت اجرا و میزان پیشرفت را بهصورت مستمر بررسی کنید.",
|
|
icon: ChartNoAxesColumn,
|
|
},
|
|
];
|
|
|
|
const DEFAULT_IMAGES: [ImageWithLabel, ...ImageWithLabel[]] = [
|
|
{
|
|
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 Hero187a = ({
|
|
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", className)}>
|
|
<div className="container">
|
|
{/* Carousel Section */}
|
|
<div className="relative mb-16">
|
|
<Carousel
|
|
className="mx-auto"
|
|
setApi={setApi}
|
|
opts={{
|
|
loop: true,
|
|
direction: "rtl",
|
|
align: "start",
|
|
}}
|
|
plugins={[Autoplay({ delay: 4000, stopOnInteraction: true })]}
|
|
>
|
|
<CarouselContent>
|
|
{activeImages.map((image, index) => (
|
|
<CarouselItem key={index}>
|
|
<img
|
|
src={image.src}
|
|
alt={image.alt}
|
|
className="aspect-video w-full rounded-2xl object-contain"
|
|
/>
|
|
</CarouselItem>
|
|
))}
|
|
</CarouselContent>
|
|
</Carousel>
|
|
|
|
<SlideIndicator
|
|
currentSlide={currentSlide}
|
|
images={activeImages}
|
|
className="mt-8"
|
|
api={api}
|
|
/>
|
|
</div>
|
|
|
|
{/* Content Section */}
|
|
<div className="space-y-12 text-center">
|
|
<div className="mx-auto max-w-4xl">
|
|
<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 lg:text-xl">
|
|
{description}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Features */}
|
|
<div
|
|
className={cn(
|
|
"mx-auto grid max-w-6xl gap-8",
|
|
features?.length === 1 && "max-w-xs grid-cols-1",
|
|
features?.length === 2 && "max-w-2xl grid-cols-1 md:grid-cols-2",
|
|
features?.length === 3 && "max-w-4xl grid-cols-1 md:grid-cols-3",
|
|
features?.length === 4 &&
|
|
"grid-cols-1 md:grid-cols-2 lg:grid-cols-4",
|
|
(features?.length ?? 0) > 4 &&
|
|
"grid-cols-1 md:grid-cols-2 lg:grid-cols-4",
|
|
)}
|
|
>
|
|
{features?.map((feature) => {
|
|
const Icon = feature.icon;
|
|
return (
|
|
<div key={feature.title} className="space-y-4 text-center">
|
|
<div className="flex justify-center">
|
|
<Icon className="size-8 text-primary" />
|
|
</div>
|
|
<div>
|
|
<h2 className="text-lg font-semibold">{feature.title}</h2>
|
|
<p className="mt-2 text-sm text-muted-foreground">
|
|
{feature.description}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div className="flex flex-wrap items-center justify-center gap-4">
|
|
{buttonPrimary && (
|
|
<Button asChild size="lg" aria-label={buttonPrimary.text}>
|
|
<a href={buttonPrimary.href}>{buttonPrimary.text}</a>
|
|
</Button>
|
|
)}
|
|
{buttonSecondary && (
|
|
<Button
|
|
asChild
|
|
aria-label={buttonSecondary.text}
|
|
variant="outline"
|
|
size="lg"
|
|
>
|
|
<a href={buttonSecondary.href}>
|
|
<span className="flex items-center gap-2">
|
|
{buttonSecondary.text}
|
|
<ArrowRight className="size-4 rtl:rotate-180" />
|
|
</span>
|
|
</a>
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
interface SlideIndicatorProps {
|
|
currentSlide: number;
|
|
images: ImageWithLabel[];
|
|
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).toLocaleString("fa-IR")} از{" "}
|
|
{images.length.toLocaleString("fa-IR")} {" "}
|
|
</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).toLocaleString("fa-IR")}`}
|
|
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 { Hero187a };
|