445 lines
12 KiB
TypeScript
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 };
|