Files
reui-blocks/blocks/feature323.tsx
T
2026-07-23 06:55:21 +03:30

445 lines
12 KiB
TypeScript

"use client";
import { AnimatePresence, motion } from "framer-motion";
import {
ChevronDown,
ChevronLeft,
ChevronRight,
ChevronUp,
PlusCircle,
} from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { useMediaQuery } from "@/hooks/useMediaQuery";
import { cn } from "@/lib/utils";
interface FeatureItem {
id: string;
image: string;
title: string;
description: string;
}
interface ControlsProps {
handleNext: () => void;
handlePrevious: () => void;
isPrevioIRRisabled: boolean;
isNextDisabled: boolean;
}
const Controls = ({
handleNext,
handlePrevious,
isPrevioIRRisabled,
isNextDisabled,
}: ControlsProps) => {
return (
<div className="hidden flex-col items-start gap-8 lg:flex">
<Button
variant="outline"
size="icon"
className="rounded-full !bg-background/50 hover:!bg-background/100 [&_svg:not([class*='size-'])]:size-6"
onClick={handlePrevious}
disabled={isPrevioIRRisabled}
>
<ChevronUp />
</Button>
<Button
variant="outline"
size="icon"
className="rounded-full !bg-background/50 hover:!bg-background/100 [&_svg:not([class*='size-'])]:size-6"
onClick={handleNext}
disabled={isNextDisabled}
>
<ChevronDown />
</Button>
</div>
);
};
interface FeatureCardProps {
feature: FeatureItem;
isActive: boolean;
onClick: () => void;
}
const FeatureCard = ({ feature, isActive, onClick }: FeatureCardProps) => {
const variants = {
initial: {
opacity: 0,
},
animate: {
opacity: 1,
},
exit: {
opacity: 0,
},
};
return (
<AnimatePresence mode="popLayout">
<motion.div
layout
transition={{
layout: {
duration: 0.4,
ease: "easeOut",
},
}}
style={{
borderRadius: "24px",
}}
className="flex min-w-0 max-w-full cursor-pointer items-start gap-4 overflow-hidden bg-background text-start md:w-fit md:max-w-sm"
onClick={onClick}
>
{isActive ? (
<motion.div
layout
variants={variants}
initial="initial"
animate="animate"
exit="exit"
key={`feature-description-active-${feature.id}`}
transition={{
duration: 0.4,
delay: 0.3,
ease: "easeOut",
}}
className="min-w-0 p-6 text-sm md:p-8 md:text-base"
>
<p className="text-pretty">
<span className="font-semibold">{feature.title}.</span>{" "}
<span>{feature.description}</span>
</p>
</motion.div>
) : (
<motion.div
layout
variants={variants}
initial="initial"
animate="animate"
exit="exit"
key={`feature-description-inactive-${feature.id}`}
transition={{
duration: 0.4,
delay: 0.2,
ease: "easeOut",
}}
className={cn(
"flex h-fit shrink-0 items-center gap-4 text-sm md:py-3.5 md:ps-3 md:pe-6 md:text-base",
!isActive && "h-0 w-0 md:h-auto md:w-auto",
)}
style={{
height: "auto",
lineHeight: "normal",
}}
>
<PlusCircle strokeWidth={1.5} className="shrink-0" />
<p className="min-w-0 font-semibold">{feature.title}</p>
</motion.div>
)}
</motion.div>
</AnimatePresence>
);
};
interface FeaturesDesktopProps {
features: FeatureItem[];
handleNext: () => void;
handlePrevious: () => void;
activeIndex: number;
handleFeatureClick: (index: number) => void;
isPrevioIRRisabled: boolean;
isNextDisabled: boolean;
}
const FeaturesDesktop = ({
features,
handleNext,
handlePrevious,
activeIndex,
handleFeatureClick,
isPrevioIRRisabled,
isNextDisabled,
}: FeaturesDesktopProps) => {
return (
<div className="relative z-10 hidden min-w-0 items-center gap-8 md:flex">
<Controls
handleNext={handleNext}
handlePrevious={handlePrevious}
isPrevioIRRisabled={isPrevioIRRisabled}
isNextDisabled={isNextDisabled}
/>
<div className="flex min-w-0 flex-col gap-4">
{features.map((feature, index) => {
return (
<FeatureCard
key={feature.id}
feature={feature}
isActive={index === activeIndex}
onClick={() => handleFeatureClick(index)}
/>
);
})}
</div>
</div>
);
};
interface FeatureMobileProps {
features: FeatureItem[];
handleNext: () => void;
handlePrevious: () => void;
activeIndex: number;
direction: 1 | -1;
isPrevioIRRisabled: boolean;
isNextDisabled: boolean;
}
const FeaturesMobile = ({
features,
handleNext,
handlePrevious,
activeIndex,
direction,
isPrevioIRRisabled,
isNextDisabled,
}: FeatureMobileProps) => {
const variants = {
initial: (dir: 1 | -1) => ({
opacity: 0,
scale: 0.9,
x: `${dir * -20}%`,
}),
animate: {
opacity: 1,
scale: 1,
x: 0,
},
exit: (dir: 1 | -1) => ({
opacity: 0,
scale: 0.9,
x: `${dir * 20}%`,
}),
};
return (
<div className="absolute inset-x-0 bottom-6 z-10 flex w-full min-w-0 items-end justify-between gap-3 px-4 sm:gap-6 sm:px-6 md:hidden">
<Button
variant="outline"
size="icon"
className="shrink-0 rounded-full !bg-background/100 [&_svg:not([class*='size-'])]:size-6"
onClick={handlePrevious}
disabled={isPrevioIRRisabled}
>
<ChevronLeft className="rtl:rotate-180" />
</Button>
<AnimatePresence mode="popLayout" custom={direction}>
<motion.div
key={`feature-mobile-${features[activeIndex]?.id}`}
variants={variants}
initial="initial"
animate="animate"
exit="exit"
custom={direction}
transition={{
duration: 0.2,
ease: "easeOut",
}}
className="h-full min-w-0 flex-1 object-cover"
>
<FeatureCard
feature={features[activeIndex]}
isActive={true}
onClick={() => {}}
/>
</motion.div>
</AnimatePresence>
<Button
variant="outline"
size="icon"
className="shrink-0 rounded-full !bg-background/100 [&_svg:not([class*='size-'])]:size-6"
onClick={handleNext}
disabled={isNextDisabled}
>
<ChevronRight className="rtl:rotate-180" />
</Button>
</div>
);
};
const DEFAULT_FEATURES: FeatureItem[] = [
{
id: "smart-watches",
image:
"/block-media/shadcnblocks/block/ecommerce/electronics/klim-musalimov-DAGss-dkVOs-unsplash-2.jpg",
title: "تقویم تیمی",
description:
"برنامه تیم را هماهنگ کنید و تداخل وظایف را کاهش دهید.",
},
{
id: "fitness-tracking",
image:
"/block-media/shadcnblocks/block/ecommerce/electronics/klim-musalimov-IGO10LkxP_g-unsplash-2.jpg",
title: "اشتراک فایل",
description:
"نسخه درست فایل همیشه در دسترس همه اعضا باشد.",
},
{
id: "gaming-consoles",
image:
"/block-media/shadcnblocks/block/ecommerce/electronics/dennis-cortes-cL7xovIO7sw-unsplash-3.jpg",
title: "منشن و بحث",
description:
"بحث را کنار هر تسک نگه دارید تا تصمیم‌ها گم نشود.",
},
{
id: "premium-laptops",
image:
"/block-media/shadcnblocks/block/ecommerce/electronics/Sleek-Laptop-Silhouette-1.png",
title: "ردیابی زمان",
description:
"وضعیت هر کار را در یک نما ببینید و گلوگاه‌ها را زودتر پیدا کنید.",
},
{
id: "phone-accessories",
image:
"/block-media/shadcnblocks/block/ecommerce/other/jascent-leung-fCO3tBcnkhg-unsplash-1.jpg",
title: "نقش و دسترسی",
description:
"برنامه تیم را هماهنگ کنید و تداخل وظایف را کاهش دهید.",
},
{
id: "work-anywhere",
image:
"/block-media/shadcnblocks/block/ecommerce/electronics/Hands-Typing-on-Laptop-1.png",
title: "چک‌لیست تحویل",
description:
"نسخه درست فایل همیشه در دسترس همه اعضا باشد.",
},
];
interface Feature323Props {
heading?: string;
features?: FeatureItem[];
className?: string;
}
const Feature323 = ({
className,
heading = "همکاری از راه دور را ساده کنید",
features = DEFAULT_FEATURES,
}: Feature323Props) => {
const [activeIndex, setActiveIndex] = useState(0);
const [direction, setDirection] = useState<1 | -1>(1);
const isMobile = useMediaQuery("(max-width: 768px)");
const handleNext = () => {
setDirection(1);
if (activeIndex !== features.length - 1) {
setActiveIndex((prevIndex) => prevIndex + 1);
}
};
const handlePrevious = () => {
setDirection(-1);
if (activeIndex !== 0) {
setActiveIndex((prevIndex) => prevIndex - 1);
}
};
const handleFeatureClick = (index: number) => {
setDirection(index > activeIndex ? 1 : -1);
setActiveIndex(index);
};
const xOffset = !isMobile ? 20 : 10;
const yOffset = !isMobile ? 10 : 5;
const scale = !isMobile ? 0.85 : 0.9;
const variants = {
initial: (dir: 1 | -1) => ({
opacity: 0,
scale: scale,
filter: "blur(20px)",
x: `${dir * -xOffset}%`,
y: `${dir * yOffset}%`,
}),
animate: {
opacity: 1,
scale: 1,
filter: "blur(0px)",
x: 0,
y: 0,
},
exit: (dir: 1 | -1) => ({
opacity: 0,
scale: scale,
x: `${dir * xOffset}%`,
y: `${dir * -yOffset}%`,
filter: "blur(20px)",
}),
};
return (
<section
dir="rtl"
lang="fa"
className={cn("relative w-full min-w-0 overflow-hidden py-32", className)}
>
<div className="container min-w-0 space-y-20">
<div className="mx-auto w-full min-w-0 max-w-5xl">
<h2 className="text-xl font-semibold text-balance break-words sm:text-2xll">
{heading}
</h2>
</div>
<div className="relative h-full min-h-[60vh] w-full min-w-0 max-w-full overflow-hidden rounded-4xl bg-muted px-4 py-8 sm:px-8 md:min-h-full md:py-20">
<FeaturesDesktop
features={features}
handleNext={handleNext}
handlePrevious={handlePrevious}
activeIndex={activeIndex}
handleFeatureClick={handleFeatureClick}
isPrevioIRRisabled={activeIndex === 0}
isNextDisabled={activeIndex === features.length - 1}
/>
<FeaturesMobile
features={features}
handleNext={handleNext}
handlePrevious={handlePrevious}
activeIndex={activeIndex}
direction={direction}
isPrevioIRRisabled={activeIndex === 0}
isNextDisabled={activeIndex === features.length - 1}
/>
<div className="absolute inset-0 z-0 flex h-full w-full min-w-0 items-center justify-center overflow-hidden lg:inset-y-0 lg:end-0 lg:start-auto lg:w-2/3 lg:mask-[linear-gradient(to_left,_transparent,_black_30%,_black)]">
<AnimatePresence mode="popLayout" custom={direction}>
<motion.img
key={`feature-image-${features[activeIndex]?.id}`}
variants={variants}
initial="initial"
animate="animate"
exit="exit"
custom={direction}
transition={{
duration: 0.4,
ease: "easeOut",
}}
src={features[activeIndex].image}
alt={features[activeIndex].title}
className="h-full w-full max-w-full object-cover"
/>
</AnimatePresence>
</div>
</div>
</div>
</section>
);
};
export { Feature323 };