Fixed the Jerks of the Marquee effect
This commit is contained in:
@@ -6,13 +6,7 @@ import Link from "next/link";
|
||||
|
||||
export default function CTA() {
|
||||
return (
|
||||
<section className="py-24 bg-white relative overflow-hidden">
|
||||
{/* Background Elements */}
|
||||
<div className="absolute inset-0">
|
||||
<div className="absolute top-0 left-1/4 w-96 h-96 bg-lime-100 rounded-full blur-3xl opacity-50" />
|
||||
<div className="absolute bottom-0 right-1/4 w-96 h-96 bg-sky-100 rounded-full blur-3xl opacity-50" />
|
||||
</div>
|
||||
|
||||
<section className="py-24 bg-black relative overflow-hidden">
|
||||
<div className="relative max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
@@ -20,20 +14,20 @@ export default function CTA() {
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.8 }}
|
||||
>
|
||||
<div className="inline-flex items-center gap-2 bg-black text-white px-4 py-2 text-xs font-medium tracking-wider rounded-full mb-8">
|
||||
<Sparkles className="w-4 h-4 text-lime-400" />
|
||||
<div className="inline-flex items-center gap-2 bg-lime-400 text-black px-4 py-2 text-xs font-bold tracking-wider rounded-full mb-8">
|
||||
<Sparkles className="w-4 h-4" />
|
||||
LIMITED TIME
|
||||
</div>
|
||||
|
||||
<h2 className="text-4xl sm:text-5xl lg:text-6xl font-black tracking-tight text-black mb-6 leading-tight">
|
||||
<h2 className="text-4xl sm:text-5xl lg:text-6xl font-black tracking-tight text-white mb-6 leading-tight">
|
||||
Ready to Showcase
|
||||
<br />
|
||||
<span className="text-transparent bg-clip-text bg-gradient-to-r from-lime-400 to-lime-600">
|
||||
<span className="text-lime-400">
|
||||
Your Masterpiece?
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
<p className="text-zinc-600 text-lg sm:text-xl max-w-2xl mx-auto mb-10">
|
||||
<p className="text-zinc-400 text-lg sm:text-xl max-w-2xl mx-auto mb-10">
|
||||
Join hundreds of talented artists from SJEC in this creative
|
||||
celebration. Submit your artwork and let the world vote for your
|
||||
talent.
|
||||
@@ -49,7 +43,7 @@ export default function CTA() {
|
||||
</Link>
|
||||
<Link
|
||||
href="/rules"
|
||||
className="inline-flex items-center justify-center gap-2 bg-white text-black px-10 py-5 text-base font-bold tracking-wide border-2 border-black hover:bg-black hover:text-white transition-all rounded-full"
|
||||
className="inline-flex items-center justify-center gap-2 bg-transparent text-white px-10 py-5 text-base font-bold tracking-wide border-2 border-zinc-700 hover:border-lime-400 hover:text-lime-400 transition-all rounded-full"
|
||||
>
|
||||
VIEW RULES
|
||||
</Link>
|
||||
@@ -61,11 +55,11 @@ export default function CTA() {
|
||||
whileInView={{ opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: 0.3 }}
|
||||
className="mt-12 inline-flex items-center gap-3 bg-zinc-100 px-6 py-3 rounded-full"
|
||||
className="mt-12 inline-flex items-center gap-3 bg-zinc-900 border border-zinc-800 px-6 py-3 rounded-full"
|
||||
>
|
||||
<span className="w-3 h-3 bg-red-500 rounded-full animate-pulse" />
|
||||
<span className="text-zinc-700 font-medium">
|
||||
Submissions close on <span className="text-black font-bold">February 28, 2026</span>
|
||||
<span className="text-zinc-400 font-medium">
|
||||
Submissions close on <span className="text-lime-400 font-bold">February 28, 2026</span>
|
||||
</span>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
@@ -1,60 +1,235 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { Palette, Monitor, FileImage, ArrowRight } from "lucide-react";
|
||||
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. From illustrations to photo manipulations.",
|
||||
"Create stunning digital masterpieces using any software of your choice.",
|
||||
color: "from-violet-500 to-purple-600",
|
||||
bgColor: "bg-violet-50",
|
||||
bgColor: "bg-violet-500",
|
||||
},
|
||||
{
|
||||
icon: Palette,
|
||||
title: "Paintings",
|
||||
description:
|
||||
"Traditional paintings in any medium - oil, acrylic, watercolor. Capture the essence of classic artistry.",
|
||||
"Traditional paintings in any medium - oil, acrylic, watercolor.",
|
||||
color: "from-orange-500 to-red-500",
|
||||
bgColor: "bg-orange-50",
|
||||
bgColor: "bg-orange-500",
|
||||
},
|
||||
{
|
||||
icon: FileImage,
|
||||
title: "Poster Making",
|
||||
description:
|
||||
"Design impactful posters that communicate powerful messages. Blend creativity with purpose.",
|
||||
"Design impactful posters that communicate powerful messages.",
|
||||
color: "from-lime-500 to-green-500",
|
||||
bgColor: "bg-lime-50",
|
||||
bgColor: "bg-lime-500",
|
||||
},
|
||||
];
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.2,
|
||||
},
|
||||
},
|
||||
};
|
||||
// BounceCards Component for Category Cards
|
||||
interface CategoryCard {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
description: string;
|
||||
color: string;
|
||||
bgColor: string;
|
||||
}
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { opacity: 0, y: 30 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
duration: 0.6,
|
||||
},
|
||||
},
|
||||
};
|
||||
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">
|
||||
<span className="inline-flex items-center gap-2 text-lime-400 text-sm font-semibold">
|
||||
Submit Now
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</span>
|
||||
</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-white">
|
||||
<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
|
||||
@@ -67,63 +242,34 @@ export default function Categories() {
|
||||
<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-black mb-4">
|
||||
<h2 className="text-4xl sm:text-5xl font-black tracking-tight text-white mb-4">
|
||||
Three Ways to
|
||||
<span className="text-transparent bg-clip-text bg-gradient-to-r from-lime-400 to-lime-600">
|
||||
{" "}
|
||||
Express
|
||||
</span>
|
||||
<span className="text-lime-400"> Express</span>
|
||||
</h2>
|
||||
<p className="text-zinc-600 text-lg max-w-2xl mx-auto">
|
||||
<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>
|
||||
|
||||
{/* Categories Grid */}
|
||||
{/* BounceCards with Category Cards */}
|
||||
<motion.div
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
className="grid md:grid-cols-3 gap-8"
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
className="flex justify-center"
|
||||
>
|
||||
{categories.map((category) => (
|
||||
<motion.div
|
||||
key={category.title}
|
||||
variants={itemVariants}
|
||||
className={`group relative ${category.bgColor} rounded-3xl p-8 hover:shadow-2xl transition-all duration-500 overflow-hidden`}
|
||||
>
|
||||
{/* Background Gradient on Hover */}
|
||||
<div
|
||||
className={`absolute inset-0 bg-gradient-to-br ${category.color} opacity-0 group-hover:opacity-10 transition-opacity duration-500`}
|
||||
/>
|
||||
|
||||
{/* Icon */}
|
||||
<div
|
||||
className={`w-16 h-16 rounded-2xl bg-gradient-to-br ${category.color} flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300`}
|
||||
>
|
||||
<category.icon className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<h3 className="text-2xl font-bold text-black mb-3">
|
||||
{category.title}
|
||||
</h3>
|
||||
<p className="text-zinc-600 mb-6 leading-relaxed">
|
||||
{category.description}
|
||||
</p>
|
||||
|
||||
{/* Link */}
|
||||
<Link
|
||||
href="/submit"
|
||||
className="inline-flex items-center gap-2 text-black font-semibold group-hover:gap-4 transition-all"
|
||||
>
|
||||
Submit Now
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</Link>
|
||||
</motion.div>
|
||||
))}
|
||||
<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>
|
||||
|
||||
@@ -14,10 +14,12 @@ function TileGrid() {
|
||||
const handleMouseEnter = useCallback((index: number) => {
|
||||
const tile = tilesRef.current[index];
|
||||
if (tile) {
|
||||
gsap.killTweensOf(tile);
|
||||
gsap.to(tile, {
|
||||
rotateY: 180,
|
||||
duration: 0.4,
|
||||
ease: "power2.out",
|
||||
duration: 0.5,
|
||||
ease: "power2.inOut",
|
||||
overwrite: true,
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
@@ -25,10 +27,12 @@ function TileGrid() {
|
||||
const handleMouseLeave = useCallback((index: number) => {
|
||||
const tile = tilesRef.current[index];
|
||||
if (tile) {
|
||||
gsap.killTweensOf(tile);
|
||||
gsap.to(tile, {
|
||||
rotateY: 0,
|
||||
duration: 0.4,
|
||||
ease: "power2.out",
|
||||
duration: 0.5,
|
||||
ease: "power2.inOut",
|
||||
overwrite: true,
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
@@ -53,7 +57,10 @@ function TileGrid() {
|
||||
<div
|
||||
ref={(el) => { tilesRef.current[index] = el; }}
|
||||
className="w-full h-full relative"
|
||||
style={{ transformStyle: "preserve-3d" }}
|
||||
style={{
|
||||
transformStyle: "preserve-3d",
|
||||
willChange: "transform",
|
||||
}}
|
||||
>
|
||||
{/* Front - Black */}
|
||||
<div
|
||||
@@ -133,12 +140,12 @@ export default function Hero() {
|
||||
</h1>
|
||||
|
||||
<div ref={contentRef}>
|
||||
<p className="text-zinc-400 text-base sm:text-lg max-w-lg mx-auto mb-10 leading-relaxed font-light">
|
||||
<p className="text-zinc-300 text-base sm:text-lg max-w-lg mx-auto mb-10 leading-relaxed font-light drop-shadow-[0_2px_4px_rgba(0,0,0,0.8)]">
|
||||
From canvas to screen, showcase your creativity and let your
|
||||
artwork speak to the world.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center pointer-events-auto">
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center pointer-events-auto mb-12">
|
||||
<Link
|
||||
href="/register"
|
||||
className="inline-flex items-center justify-center gap-2 bg-lime-400 text-black px-10 py-4 text-sm font-bold tracking-wider hover:bg-lime-300 transition-all hover:scale-105 rounded-full shadow-[0_0_30px_rgba(163,230,53,0.3)]"
|
||||
@@ -152,15 +159,15 @@ export default function Hero() {
|
||||
EXPLORE GALLERY
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Scroll Indicator */}
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<span className="text-lime-400 text-[10px] tracking-[0.3em] uppercase drop-shadow-[0_0_10px_rgba(163,230,53,0.8)]">Scroll</span>
|
||||
<div className="w-px h-8 bg-gradient-to-b from-lime-400 to-transparent shadow-[0_0_10px_rgba(163,230,53,0.5)]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scroll Indicator - positioned above the marquee */}
|
||||
<div className="absolute bottom-28 left-1/2 -translate-x-1/2 flex flex-col items-center gap-2 text-zinc-500 z-10">
|
||||
<span className="text-[10px] tracking-[0.3em] uppercase">Scroll</span>
|
||||
<div className="w-px h-8 bg-gradient-to-b from-zinc-500 to-transparent" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Marquee */}
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "images.unsplash.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
Reference in New Issue
Block a user