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

307 lines
9.2 KiB
TypeScript

"use client";
import type { MotionValue } from "framer-motion";
import {
AnimatePresence,
motion,
useMotionValueEvent,
useScroll,
useTransform,
} from "framer-motion";
import React, { useRef, useState } from "react";
import { cn } from "@/lib/utils";
interface Statistics {
value: string;
label: string;
}
interface Feature {
id: string;
title: string;
subtitle: string;
statistics: Statistics;
visual: string;
}
interface Testimonial {
quote: string;
companyLogo: string;
author: string;
designation: string;
}
interface TestimonialCardProps {
testimonial: Testimonial;
}
const TestimonialCard = ({ testimonial }: TestimonialCardProps) => {
return (
<div
className="hidden h-96 min-w-[14rem] flex-[1/2] shrink-0 flex-col items-start justify-end rounded-lg border bg-muted p-6 text-start sm:flex lg:h-full lg:min-w-[16rem]"
>
<img
src={testimonial.companyLogo}
alt={testimonial.author}
className="h-8 dark:invert"
/>
<p className="mt-8 text-lg leading-tight text-pretty">
«{testimonial.quote}»
</p>
<p className="mt-4 text-sm font-medium">
{testimonial.author}{" "}
<span className="font-light">{testimonial.designation}</span>
</p>
</div>
);
};
interface ScrollProgressIndicatorProps {
totalFeatures: number;
activeIndex: number;
scrollYProgress: MotionValue<number>;
}
const ScrollProgressIndicator = ({
totalFeatures,
activeIndex,
scrollYProgress,
}: ScrollProgressIndicatorProps) => {
const segmentStart = activeIndex / totalFeatures;
const segmentEnd = (activeIndex + 1) / totalFeatures;
const width = useTransform(
scrollYProgress,
[segmentStart, segmentEnd],
["0%", "100%"],
);
return (
<div className="mt-14 hidden w-fit items-center justify-center gap-3 rounded-full bg-muted px-6 py-4 lg:flex">
{Array.from({ length: totalFeatures }).map((_, index) => {
return (
<div
key={`scroll-progress-indicator-${index}`}
className={cn(
"relative h-1.5 w-1.5 rounded-full bg-foreground/20",
activeIndex === index && "w-10",
activeIndex > index && "bg-foreground",
)}
>
{activeIndex === index ? (
<motion.div
style={{ width }}
className="absolute start-0 top-0 h-full rounded-full bg-foreground"
/>
) : null}
</div>
);
})}
</div>
);
};
interface FeatureCardProps {
feature: Feature;
index: number;
testimonial: Testimonial;
totalFeatures: number;
scrollYProgress: MotionValue<number>;
}
const FeatureCard = ({
feature,
index,
testimonial,
totalFeatures,
scrollYProgress,
}: FeatureCardProps) => {
return (
<div className="flex w-full min-w-0 flex-col-reverse justify-between gap-8 px-4 py-4 text-start sm:px-10 lg:min-h-[28rem] lg:flex-row lg:items-stretch lg:border-s">
<div className="flex min-w-0 flex-[2/5] flex-col gap-8 lg:gap-0">
<div className="flex min-w-0 flex-col gap-1">
<p className="text-xl font-semibold text-balance break-words">
{feature.title}
</p>
<p className="font-light text-pretty">{feature.subtitle}</p>
</div>
<ScrollProgressIndicator
totalFeatures={totalFeatures}
activeIndex={index}
scrollYProgress={scrollYProgress}
/>
<div className="flex flex-col gap-1 lg:mt-auto">
<p className="text-2xl font-medium">{feature.statistics.value}</p>
<p className="text-sm font-light">{feature.statistics.label}</p>
</div>
</div>
<div className="flex h-96 w-full min-w-[18rem] flex-[3/5] items-center gap-6 sm:min-w-[24rem] lg:h-[28rem]">
<img
src={feature.visual}
alt={feature.title}
className={cn(
"h-full w-full min-w-0 overflow-hidden rounded-lg border object-cover",
index === 0 && "flex-[1/2]",
)}
/>
{index === 0 ? <TestimonialCard testimonial={testimonial} /> : null}
</div>
</div>
);
};
interface Feature322Props {
features?: Feature[];
testimonial?: Testimonial;
className?: string;
}
const DEFAULT_FEATURES: Feature[] = [
{
id: "performance",
title: "طبقه‌بندی خودکار",
subtitle: "درخواست‌ها را خودکار دسته‌بندی کنید و به تیم مناسب بفرستید.",
statistics: {
value: "اتوماسیون AI",
label: "دستیار کسب‌وکار",
},
visual:
"/block-media/shadcnblocks/block/photos/christopher-gower-vjMgqUkS8q8-unsplash.jpg",
},
{
id: "security",
title: "پیش‌بینی ریزش",
subtitle: "مشتریان در معرض ریزش را زودتر شناسایی کنید.",
statistics: {
value: "هوش مصنوعی",
label: "پیشنهاد هوشمند",
},
visual:
"/block-media/shadcnblocks/block/photos/daniel-leone-g30P1zcOzXo-unsplash.jpg",
},
{
id: "analytics",
title: "جستجوی معنایی",
subtitle: "متن‌های طولانی را در چند ثانیه به نکات کلیدی تبدیل کنید.",
statistics: {
value: "اتوماسیون AI",
label: "دستیار کسب‌وکار",
},
visual:
"/block-media/shadcnblocks/block/photos/jason-leung-6uoj7DL6BFk-unsplash.jpg",
},
{
id: "integration",
title: "تولید محتوا",
subtitle: "بهترین پاسخ را بر اساس زمینه مکالمه پیشنهاد دهید.",
statistics: {
value: "هوش مصنوعی",
label: "پیشنهاد هوشمند",
},
visual:
"/block-media/shadcnblocks/block/photos/ivan-bandura-Kj2tYAl4HZg-unsplash.jpg",
},
];
const DEFAULT_TESTIMONIAL: Testimonial = {
quote:
"مدل‌های هوشمند الگوهای پنهان در داده فروش و پشتیبانی را پیدا می‌کنند. تیم‌ها با پیشنهادهای عملی، سریع‌تر اقدام می‌کنند.",
companyLogo:
"/block-media/shadcnblocks/block/logos/company/fictional-company-logo-4.svg",
author: "اتوماسیون AI",
designation: "با هوش مصنوعی سریع‌تر تصمیم بگیرید",
};
function getActiveFeatureIndex(progress: number, total: number) {
if (total <= 0) return 0;
if (progress >= 1) return total - 1;
return Math.min(total - 1, Math.floor(progress * total));
}
const Feature322 = ({
className,
features = DEFAULT_FEATURES,
testimonial = DEFAULT_TESTIMONIAL,
}: Feature322Props) => {
const scrollRef = useRef<HTMLElement>(null);
const { scrollYProgress } = useScroll({
container: scrollRef,
});
const [activeIndex, setActiveIndex] = useState(0);
useMotionValueEvent(scrollYProgress, "change", (latest) => {
const nextIndex = getActiveFeatureIndex(latest, features.length);
setActiveIndex((prev) => (prev === nextIndex ? prev : nextIndex));
});
const activeFeature = features[activeIndex] ?? features[0];
return (
<section
ref={scrollRef}
dir="rtl"
lang="fa"
className={cn(
"relative h-svh w-full min-w-[20rem] max-w-full overflow-x-clip overflow-y-auto sm:min-w-[36rem] lg:min-w-[64rem]",
className,
)}
>
<div className="container min-w-0 w-full max-w-full">
{/* Desktop: sticky stage + scroll spacer */}
<div className="relative hidden lg:block">
<div className="sticky top-0 flex h-svh w-full min-w-0 flex-col items-center justify-center">
<div className="relative w-full min-w-0 max-w-full">
<AnimatePresence mode="wait" initial={false}>
{activeFeature ? (
<motion.div
key={activeFeature.id}
initial={{ opacity: 0, x: -12 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 12 }}
transition={{ duration: 0.35, ease: "easeOut" }}
className="w-full min-w-0"
>
<FeatureCard
index={activeIndex}
feature={activeFeature}
testimonial={testimonial}
totalFeatures={features.length}
scrollYProgress={scrollYProgress}
/>
</motion.div>
) : null}
</AnimatePresence>
</div>
</div>
<div
style={{ height: `${Math.max(features.length, 1) * 75}vh` }}
className="pointer-events-none w-full"
aria-hidden
/>
</div>
{/* Mobile / tablet: stacked cards, no crossfade thrash */}
<div className="flex w-full min-w-0 flex-col items-center justify-center gap-10 py-16 lg:hidden">
{features.map((feature, index) => (
<FeatureCard
key={feature.id}
index={index}
feature={feature}
testimonial={testimonial}
totalFeatures={features.length}
scrollYProgress={scrollYProgress}
/>
))}
</div>
</div>
</section>
);
};
export { Feature322 };