281 lines
7.9 KiB
TypeScript
281 lines
7.9 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef } from "react";
|
|
import { motion } from "framer-motion";
|
|
import { Palette, Monitor, FileImage, ArrowRight, LucideIcon } from "lucide-react";
|
|
import Link from "next/link";
|
|
import gsap from "gsap";
|
|
|
|
const categories = [
|
|
{
|
|
icon: Monitor,
|
|
title: "Digital Art",
|
|
description:
|
|
"Create stunning digital masterpieces using any software of your choice.",
|
|
color: "from-violet-500 to-purple-600",
|
|
bgColor: "bg-violet-500",
|
|
},
|
|
{
|
|
icon: Palette,
|
|
title: "Paintings",
|
|
description:
|
|
"Traditional paintings in any medium - oil, acrylic, watercolor.",
|
|
color: "from-orange-500 to-red-500",
|
|
bgColor: "bg-orange-500",
|
|
},
|
|
{
|
|
icon: FileImage,
|
|
title: "Poster Making",
|
|
description:
|
|
"Design impactful posters that communicate powerful messages.",
|
|
color: "from-lime-500 to-green-500",
|
|
bgColor: "bg-lime-500",
|
|
},
|
|
];
|
|
|
|
// BounceCards Component for Category Cards
|
|
interface CategoryCard {
|
|
icon: LucideIcon;
|
|
title: string;
|
|
description: string;
|
|
color: string;
|
|
bgColor: string;
|
|
}
|
|
|
|
interface BounceCardsProps {
|
|
className?: string;
|
|
cards: CategoryCard[];
|
|
containerWidth?: number;
|
|
containerHeight?: number;
|
|
animationDelay?: number;
|
|
animationStagger?: number;
|
|
easeType?: string;
|
|
transformStyles?: string[];
|
|
enableHover?: boolean;
|
|
}
|
|
|
|
function BounceCards({
|
|
className = "",
|
|
cards,
|
|
containerWidth = 500,
|
|
containerHeight = 400,
|
|
animationDelay = 0.5,
|
|
animationStagger = 0.08,
|
|
easeType = "elastic.out(1, 0.8)",
|
|
transformStyles = [
|
|
"rotate(-8deg) translate(-200px)",
|
|
"rotate(0deg)",
|
|
"rotate(8deg) translate(200px)",
|
|
],
|
|
enableHover = true,
|
|
}: BounceCardsProps) {
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
const ctx = gsap.context(() => {
|
|
gsap.fromTo(
|
|
".card",
|
|
{ scale: 0 },
|
|
{
|
|
scale: 1,
|
|
stagger: animationStagger,
|
|
ease: easeType,
|
|
delay: animationDelay,
|
|
}
|
|
);
|
|
}, containerRef);
|
|
return () => ctx.revert();
|
|
}, [animationDelay, animationStagger, easeType]);
|
|
|
|
const getNoRotationTransform = (transformStr: string): string => {
|
|
const hasRotate = /rotate\([\s\S]*?\)/.test(transformStr);
|
|
if (hasRotate) {
|
|
return transformStr.replace(/rotate\([\s\S]*?\)/, "rotate(0deg)");
|
|
} else if (transformStr === "none") {
|
|
return "rotate(0deg)";
|
|
} else {
|
|
return `${transformStr} rotate(0deg)`;
|
|
}
|
|
};
|
|
|
|
const getPushedTransform = (
|
|
baseTransform: string,
|
|
offsetX: number
|
|
): string => {
|
|
const translateRegex = /translate\(([-0-9.]+)px\)/;
|
|
const match = baseTransform.match(translateRegex);
|
|
if (match) {
|
|
const currentX = parseFloat(match[1]);
|
|
const newX = currentX + offsetX;
|
|
return baseTransform.replace(translateRegex, `translate(${newX}px)`);
|
|
} else {
|
|
return baseTransform === "none"
|
|
? `translate(${offsetX}px)`
|
|
: `${baseTransform} translate(${offsetX}px)`;
|
|
}
|
|
};
|
|
|
|
const pushSiblings = (hoveredIdx: number) => {
|
|
const q = gsap.utils.selector(containerRef);
|
|
if (!enableHover || !containerRef.current) return;
|
|
|
|
cards.forEach((_, i) => {
|
|
const selector = q(`.card-${i}`);
|
|
gsap.killTweensOf(selector);
|
|
|
|
const baseTransform = transformStyles[i] || "none";
|
|
|
|
if (i === hoveredIdx) {
|
|
const noRotation = getNoRotationTransform(baseTransform);
|
|
gsap.to(selector, {
|
|
transform: noRotation,
|
|
zIndex: 10,
|
|
duration: 0.4,
|
|
ease: "back.out(1.4)",
|
|
overwrite: "auto",
|
|
});
|
|
} else {
|
|
const offsetX = i < hoveredIdx ? -80 : 80;
|
|
const pushedTransform = getPushedTransform(baseTransform, offsetX);
|
|
|
|
const distance = Math.abs(hoveredIdx - i);
|
|
const delay = distance * 0.05;
|
|
|
|
gsap.to(selector, {
|
|
transform: pushedTransform,
|
|
zIndex: 1,
|
|
duration: 0.4,
|
|
ease: "back.out(1.4)",
|
|
delay,
|
|
overwrite: "auto",
|
|
});
|
|
}
|
|
});
|
|
};
|
|
|
|
const resetSiblings = () => {
|
|
if (!enableHover || !containerRef.current) return;
|
|
const q = gsap.utils.selector(containerRef);
|
|
|
|
cards.forEach((_, i) => {
|
|
const selector = q(`.card-${i}`);
|
|
gsap.killTweensOf(selector);
|
|
|
|
const baseTransform = transformStyles[i] || "none";
|
|
gsap.to(selector, {
|
|
transform: baseTransform,
|
|
zIndex: cards.length - i,
|
|
duration: 0.4,
|
|
ease: "back.out(1.4)",
|
|
overwrite: "auto",
|
|
});
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className={`relative flex items-center justify-center ${className}`}
|
|
ref={containerRef}
|
|
style={{
|
|
width: containerWidth,
|
|
height: containerHeight,
|
|
}}
|
|
>
|
|
{cards.map((card, idx) => (
|
|
<div
|
|
key={idx}
|
|
className={`card card-${idx} absolute w-[220px] h-[280px] border-4 border-zinc-800 rounded-[24px] overflow-hidden bg-zinc-900 p-6 flex flex-col cursor-pointer`}
|
|
style={{
|
|
boxShadow: "0 8px 40px rgba(0, 0, 0, 0.5)",
|
|
transform: transformStyles[idx] || "none",
|
|
zIndex: cards.length - idx,
|
|
}}
|
|
onMouseEnter={() => pushSiblings(idx)}
|
|
onMouseLeave={resetSiblings}
|
|
>
|
|
{/* Icon */}
|
|
<div
|
|
className={`w-14 h-14 rounded-xl bg-gradient-to-br ${card.color} flex items-center justify-center mb-4 flex-shrink-0`}
|
|
>
|
|
<card.icon className="w-7 h-7 text-white" />
|
|
</div>
|
|
|
|
{/* Title */}
|
|
<h3 className="text-xl font-bold text-white mb-2">{card.title}</h3>
|
|
|
|
{/* Description */}
|
|
<p className="text-zinc-400 text-sm leading-relaxed flex-grow">
|
|
{card.description}
|
|
</p>
|
|
|
|
{/* Submit Link */}
|
|
<div className="mt-4 pt-4 border-t border-zinc-800">
|
|
<Link
|
|
href="/submit"
|
|
className="inline-flex items-center gap-2 text-lime-400 text-sm font-semibold hover:text-lime-300 transition-colors"
|
|
>
|
|
Submit Now
|
|
<ArrowRight className="w-4 h-4" />
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function Categories() {
|
|
const transformStyles = [
|
|
"rotate(-8deg) translate(-220px)",
|
|
"rotate(0deg)",
|
|
"rotate(8deg) translate(220px)",
|
|
];
|
|
|
|
return (
|
|
<section className="py-24 bg-black overflow-hidden">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
{/* Section Header */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.6 }}
|
|
className="text-center mb-16"
|
|
>
|
|
<span className="inline-block bg-lime-400 text-black px-4 py-1.5 text-xs font-bold tracking-wider rounded-full mb-4">
|
|
CATEGORIES
|
|
</span>
|
|
<h2 className="text-4xl sm:text-5xl font-black tracking-tight text-white mb-4">
|
|
Three Ways to
|
|
<span className="text-lime-400"> Express</span>
|
|
</h2>
|
|
<p className="text-zinc-400 text-lg max-w-2xl mx-auto">
|
|
Choose your canvas, unleash your creativity. Each category offers a
|
|
unique way to showcase your artistic vision.
|
|
</p>
|
|
</motion.div>
|
|
|
|
{/* BounceCards with Category Cards */}
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
whileInView={{ opacity: 1 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.6, delay: 0.2 }}
|
|
className="flex justify-center"
|
|
>
|
|
<BounceCards
|
|
cards={categories}
|
|
containerWidth={700}
|
|
containerHeight={380}
|
|
animationDelay={0.3}
|
|
animationStagger={0.1}
|
|
easeType="elastic.out(1, 0.8)"
|
|
transformStyles={transformStyles}
|
|
enableHover={true}
|
|
/>
|
|
</motion.div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|