325 lines
10 KiB
TypeScript
325 lines
10 KiB
TypeScript
"use client";
|
|
import { motion } from "motion/react";
|
|
import React, { useState } from "react";
|
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
const defaultFeatures = [
|
|
{
|
|
number: "01",
|
|
title: "Instant Implementation",
|
|
description:
|
|
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et",
|
|
image: "https://deifkwefumgah.cloudfront.net/shadcnblocks/block/guri3/img11.jpeg",
|
|
imageOrder: "order-1",
|
|
contentOrder: "order-2",
|
|
},
|
|
{
|
|
number: "02",
|
|
title: "One time payment",
|
|
description:
|
|
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et",
|
|
image: "https://deifkwefumgah.cloudfront.net/shadcnblocks/block/guri3/img12.jpeg",
|
|
imageOrder: "order-2",
|
|
contentOrder: "order-1",
|
|
},
|
|
{
|
|
number: "03",
|
|
title: "Developer Friendly",
|
|
description:
|
|
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et",
|
|
image: "https://deifkwefumgah.cloudfront.net/shadcnblocks/block/guri3/img14.jpeg",
|
|
imageOrder: "order-1",
|
|
contentOrder: "order-2",
|
|
},
|
|
];
|
|
|
|
interface Feature270Item {
|
|
number: string;
|
|
title: string;
|
|
description: string;
|
|
image: string;
|
|
imageOrder: string;
|
|
contentOrder: string;
|
|
}
|
|
|
|
interface Feature270Props {
|
|
className?: string;
|
|
heading?: string;
|
|
features?: Feature270Item[];
|
|
headingAlign?: "left" | "right" | "center";
|
|
contentAlign?: "left" | "center" | "right";
|
|
showNumbers?: boolean;
|
|
/** وقتی false باشد، دکمه/لینک «Learn More» روی کارتها نمایش داده نمیشود. */
|
|
showLearnMore?: boolean;
|
|
}
|
|
|
|
const Feature270 = ({
|
|
className,
|
|
heading = "Why Choose Shadcn Blocks?",
|
|
features = defaultFeatures,
|
|
headingAlign = "left",
|
|
contentAlign = "left",
|
|
showNumbers = true,
|
|
showLearnMore = true,
|
|
}: Feature270Props) => {
|
|
const headingClass = {
|
|
left: "text-left",
|
|
right: "text-right",
|
|
center: "text-center",
|
|
}[headingAlign];
|
|
const contentClass = {
|
|
left: "text-left",
|
|
center: "text-center",
|
|
right: "text-right",
|
|
}[contentAlign];
|
|
|
|
return (
|
|
<section className={cn("relative w-full py-32", className)}>
|
|
<div className="container mx-auto px-4 md:px-6">
|
|
<div className={cn("w-full space-y-5 lg:w-3/5 mb-12", headingAlign === "right" && "lg:mr-0 lg:ml-auto")}>
|
|
<h2 className={cn("text-heading-2 mb-6 w-full max-w-lg font-semibold tracking-tight", headingClass)}>
|
|
{heading}
|
|
</h2>
|
|
</div>
|
|
<div className="relative grid grid-cols-1 auto-rows-auto items-stretch gap-6 md:gap-3 md:min-h-[68rem] md:grid-cols-2 lg:min-h-[32rem] lg:grid-cols-3">
|
|
{features.map((feature, index) => {
|
|
const cardContent = (
|
|
<div className="flex flex-col h-full min-h-[26rem]">
|
|
<div className={feature.imageOrder}>
|
|
<img
|
|
src={feature.image}
|
|
height="1000"
|
|
width="1000"
|
|
className="h-70 w-full rounded-3xl object-cover group-hover/card:shadow-xl"
|
|
alt="thumbnail"
|
|
/>
|
|
</div>
|
|
<div className={cn("mt-4 w-full p-3 flex-1 flex flex-col min-h-[8rem] gap-0", feature.contentOrder, contentClass)}>
|
|
{showNumbers && (
|
|
<p className="text-caption font-medium text-muted-foreground tracking-tighter">
|
|
{feature.number}
|
|
</p>
|
|
)}
|
|
<h3 className="text-heading-3 mt-3 mb-2 font-semibold leading-tight tracking-tight text-foreground">
|
|
{feature.title}
|
|
</h3>
|
|
<p className="text-caption leading-relaxed text-muted-foreground flex-1">
|
|
{feature.description}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
const cardClassName = "w-full h-full min-h-[28rem] rounded-3xl !bg-muted/70 p-4 flex flex-col";
|
|
return (
|
|
<PinContainer
|
|
key={index}
|
|
title={showLearnMore ? "Learn More" : undefined}
|
|
href={showLearnMore ? "https://www.shadcnblocks.com/blocks" : undefined}
|
|
className={cardClassName}
|
|
showPerspective={showLearnMore}
|
|
>
|
|
{cardContent}
|
|
</PinContainer>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
export { Feature270 };
|
|
|
|
// Below is the modified component from Aceternity UI
|
|
// Original source: npx shadcn@latest add https://ui.aceternity.com/registry/3d-pin.json
|
|
// Modified to follow our coding standards and design system
|
|
// We respect copyright and attribution to the original creators
|
|
|
|
export const PinContainer = ({
|
|
children,
|
|
title,
|
|
href,
|
|
className,
|
|
containerClassName,
|
|
showPerspective = true,
|
|
}: {
|
|
children: React.ReactNode;
|
|
title?: string;
|
|
href?: string;
|
|
className?: string;
|
|
containerClassName?: string;
|
|
/** وقتی false باشد، دکمه/برچسب «Learn More» روی هاور نمایش داده نمیشود. افکت سهبعدی هاور حفظ میشود. */
|
|
showPerspective?: boolean;
|
|
}) => {
|
|
const [transform, setTransform] = useState(
|
|
"translate(-50%,-50%) rotateX(0deg)",
|
|
);
|
|
|
|
const onMouseEnter = () => {
|
|
setTransform("translate(-50%,-50%) rotateX(40deg) scale(0.8)");
|
|
};
|
|
const onMouseLeave = () => {
|
|
setTransform("translate(-50%,-50%) rotateX(0deg) scale(1)");
|
|
};
|
|
|
|
const wrapperClassName = cn(
|
|
"group/pin relative z-10",
|
|
href ? "cursor-pointer" : "cursor-default",
|
|
containerClassName,
|
|
);
|
|
|
|
const outerTransform = showPerspective
|
|
? "rotateX(70deg) translateZ(0deg)"
|
|
: "rotateX(0deg) translateZ(0deg)";
|
|
|
|
const content = showPerspective ? (
|
|
<>
|
|
<div
|
|
style={{
|
|
perspective: "1000px",
|
|
transform: outerTransform,
|
|
}}
|
|
className="absolute top-1/2 left-1/2 mt-4 ml-[0.09375rem] w-full -translate-x-1/2 -translate-y-1/2"
|
|
>
|
|
<div
|
|
style={{
|
|
transform: transform,
|
|
}}
|
|
className="absolute top-1/2 left-1/2 flex w-full items-start justify-start overflow-hidden rounded-2xl transition duration-700"
|
|
>
|
|
<div className={cn("relative z-10", className)}>{children}</div>
|
|
</div>
|
|
</div>
|
|
{title != null && <PinPerspective title={title} />}
|
|
</>
|
|
) : (
|
|
<div className={cn("relative z-10", className)}>{children}</div>
|
|
);
|
|
|
|
if (href) {
|
|
return (
|
|
<a
|
|
className={wrapperClassName}
|
|
onMouseEnter={onMouseEnter}
|
|
onMouseLeave={onMouseLeave}
|
|
href={href}
|
|
>
|
|
{content}
|
|
</a>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className={wrapperClassName}
|
|
onMouseEnter={onMouseEnter}
|
|
onMouseLeave={onMouseLeave}
|
|
>
|
|
{content}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const PinPerspective = ({
|
|
title,
|
|
}: {
|
|
title?: string;
|
|
href?: string;
|
|
}) => {
|
|
return (
|
|
<motion.div className="pointer-events-none z-20 flex h-80 w-96 items-center justify-center opacity-0 transition duration-500 group-hover/pin:opacity-100">
|
|
<div className="inset-0 -mt-7 h-full w-full flex-none">
|
|
<div className="absolute inset-x-0 top-0 flex justify-center">
|
|
<span className="relative z-10 flex items-center space-x-2 rounded-full bg-foreground px-4 py-0.5 ring-1 ring-border">
|
|
<span className="text-caption relative z-20 inline-block py-0.5 font-bold text-background">
|
|
{title}
|
|
</span>
|
|
|
|
<span className="absolute -bottom-0 left-[1.125rem] h-px w-[calc(100%-2.25rem)] bg-gradient-to-r from-secondary/0 via-secondary to-secondary/0 transition-opacity duration-500 group-hover/btn:opacity-40"></span>
|
|
</span>
|
|
</div>
|
|
|
|
<div
|
|
style={{
|
|
perspective: "1000px",
|
|
transform: "rotateX(70deg) translateZ(0)",
|
|
}}
|
|
className="absolute top-1/2 left-1/2 mt-4 ml-[0.09375rem] -translate-x-1/2 -translate-y-1/2"
|
|
>
|
|
<>
|
|
<motion.div
|
|
initial={{
|
|
opacity: 0,
|
|
scale: 0,
|
|
x: "-50%",
|
|
y: "-50%",
|
|
}}
|
|
animate={{
|
|
opacity: [0, 1, 0.5, 0],
|
|
scale: 1,
|
|
|
|
z: 0,
|
|
}}
|
|
transition={{
|
|
duration: 6,
|
|
repeat: Infinity,
|
|
delay: 0,
|
|
}}
|
|
className="absolute top-1/2 left-1/2 h-[11.25rem] w-[11.25rem] rounded-[50%] bg-primary/[0.08] shadow-lg"
|
|
></motion.div>
|
|
<motion.div
|
|
initial={{
|
|
opacity: 0,
|
|
scale: 0,
|
|
x: "-50%",
|
|
y: "-50%",
|
|
}}
|
|
animate={{
|
|
opacity: [0, 1, 0.5, 0],
|
|
scale: 1,
|
|
|
|
z: 0,
|
|
}}
|
|
transition={{
|
|
duration: 6,
|
|
repeat: Infinity,
|
|
delay: 2,
|
|
}}
|
|
className="absolute top-1/2 left-1/2 h-[11.25rem] w-[11.25rem] rounded-[50%] bg-primary/[0.08] shadow-lg"
|
|
></motion.div>
|
|
<motion.div
|
|
initial={{
|
|
opacity: 0,
|
|
scale: 0,
|
|
x: "-50%",
|
|
y: "-50%",
|
|
}}
|
|
animate={{
|
|
opacity: [0, 1, 0.5, 0],
|
|
scale: 1,
|
|
|
|
z: 0,
|
|
}}
|
|
transition={{
|
|
duration: 6,
|
|
repeat: Infinity,
|
|
delay: 4,
|
|
}}
|
|
className="absolute top-1/2 left-1/2 h-[11.25rem] w-[11.25rem] rounded-[50%] bg-primary/[0.08] shadow-lg"
|
|
></motion.div>
|
|
</>
|
|
</div>
|
|
|
|
<>
|
|
<motion.div className="absolute right-1/2 bottom-1/2 h-20 w-px translate-y-[14px] bg-gradient-to-b from-transparent to-secondary blur-[2px] group-hover/pin:h-40" />
|
|
<motion.div className="absolute right-1/2 bottom-1/2 h-20 w-px translate-y-[14px] bg-gradient-to-b from-transparent to-secondary group-hover/pin:h-40" />
|
|
<motion.div className="absolute right-1/2 bottom-1/2 z-40 h-[4px] w-[4px] translate-x-[1.5px] translate-y-[14px] rounded-full bg-secondary blur-[3px]" />
|
|
<motion.div className="absolute right-1/2 bottom-1/2 z-40 h-[2px] w-[2px] translate-x-[0.5px] translate-y-[14px] rounded-full bg-secondary" />
|
|
</>
|
|
</div>
|
|
</motion.div>
|
|
);
|
|
};
|
|
|