Compare commits
15 Commits
version_2_
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 014f2cf1c6 | |||
|
|
d9c74ddab9 | ||
|
|
eaa26d9599 | ||
|
|
688d670892 | ||
| 6d3a8c3e19 | |||
|
|
b76d2874f6 | ||
|
|
40d71e0f0a | ||
|
|
e0e655c575 | ||
| 9bad83cd4d | |||
|
|
9bbe263701 | ||
| 253bf96646 | |||
|
|
c46d47952f | ||
| 725ae37201 | |||
|
|
dad69dac5b | ||
| 0c1d2ac648 |
@@ -1,9 +1,11 @@
|
||||
import { motion } from "motion/react";
|
||||
import { Info } from "lucide-react";
|
||||
import { useRef } from "react";
|
||||
import TextAnimation from "@/components/ui/TextAnimation";
|
||||
import ImageOrVideo from "@/components/ui/ImageOrVideo";
|
||||
import GridOrCarousel from "@/components/ui/GridOrCarousel";
|
||||
import Button from "@/components/ui/Button";
|
||||
import { useParallax } from "@/hooks/useParallax";
|
||||
|
||||
type FeatureItem = {
|
||||
title: string;
|
||||
@@ -19,6 +21,47 @@ interface FeaturesRevealCardsProps {
|
||||
items: FeatureItem[];
|
||||
}
|
||||
|
||||
const ParallaxFeatureCard = ({ item, index }: { item: FeatureItem; index: number }) => {
|
||||
const cardRef = useRef<HTMLDivElement>(null);
|
||||
const transform = useParallax(cardRef, { maxRotate: 5 });
|
||||
|
||||
return (
|
||||
<div ref={cardRef} className="group relative overflow-hidden aspect-6/7 rounded parallax-card-container">
|
||||
<div className="w-full h-full parallax-card" style={{ transform }}>
|
||||
<ImageOrVideo imageSrc={item.imageSrc} videoSrc={item.videoSrc} className="absolute inset-0" />
|
||||
|
||||
<div className="absolute top-5 left-5 z-20 perspective-[1000px]">
|
||||
<div className="relative size-8 transform-3d transition-transform duration-400 group-hover:rotate-y-180">
|
||||
<div className="absolute inset-0 flex items-center justify-center rounded bg-background backface-hidden">
|
||||
<span className="text-sm font-medium text-foreground">{index + 1}</span>
|
||||
</div>
|
||||
<div className="absolute inset-0 flex items-center justify-center rounded bg-background backface-hidden rotate-y-180">
|
||||
<Info className="h-1/2 w-1/2 text-foreground" strokeWidth={1.5} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute inset-x-0 bottom-0 h-2/5 backdrop-blur-xl mask-fade-top-overlay" aria-hidden="true" />
|
||||
|
||||
<div className="absolute inset-x-0 bottom-0 z-10 p-1">
|
||||
<div className="relative flex flex-col gap-1 p-3">
|
||||
<div className="absolute inset-0 -z-10 card rounded translate-y-full opacity-0 transition-all duration-400 ease-out group-hover:translate-y-0 group-hover:opacity-100" />
|
||||
|
||||
<h3 className="text-2xl font-semibold leading-tight text-background transition-colors duration-400 group-hover:text-foreground">
|
||||
{item.title}
|
||||
</h3>
|
||||
<div className="grid grid-rows-[0fr] transition-all duration-400 ease-out group-hover:grid-rows-[1fr]">
|
||||
<p className="overflow-hidden text-sm leading-tight text-foreground opacity-0 transition-opacity duration-400 group-hover:opacity-100">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FeaturesRevealCards = ({
|
||||
tag,
|
||||
title,
|
||||
@@ -63,37 +106,7 @@ const FeaturesRevealCards = ({
|
||||
>
|
||||
<GridOrCarousel>
|
||||
{items.map((item, index) => (
|
||||
<div key={item.title} className="group relative overflow-hidden aspect-6/7 rounded">
|
||||
<ImageOrVideo imageSrc={item.imageSrc} videoSrc={item.videoSrc} className="absolute inset-0" />
|
||||
|
||||
<div className="absolute top-5 left-5 z-20 perspective-[1000px]">
|
||||
<div className="relative size-8 transform-3d transition-transform duration-400 group-hover:rotate-y-180">
|
||||
<div className="absolute inset-0 flex items-center justify-center rounded bg-background backface-hidden">
|
||||
<span className="text-sm font-medium text-foreground">{index + 1}</span>
|
||||
</div>
|
||||
<div className="absolute inset-0 flex items-center justify-center rounded bg-background backface-hidden rotate-y-180">
|
||||
<Info className="h-1/2 w-1/2 text-foreground" strokeWidth={1.5} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute inset-x-0 bottom-0 h-2/5 backdrop-blur-xl mask-fade-top-overlay" aria-hidden="true" />
|
||||
|
||||
<div className="absolute inset-x-0 bottom-0 z-10 p-1">
|
||||
<div className="relative flex flex-col gap-1 p-3">
|
||||
<div className="absolute inset-0 -z-10 card rounded translate-y-full opacity-0 transition-all duration-400 ease-out group-hover:translate-y-0 group-hover:opacity-100" />
|
||||
|
||||
<h3 className="text-2xl font-semibold leading-tight text-background transition-colors duration-400 group-hover:text-foreground">
|
||||
{item.title}
|
||||
</h3>
|
||||
<div className="grid grid-rows-[0fr] transition-all duration-400 ease-out group-hover:grid-rows-[1fr]">
|
||||
<p className="overflow-hidden text-sm leading-tight text-foreground opacity-0 transition-opacity duration-400 group-hover:opacity-100">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ParallaxFeatureCard key={item.title} item={item} index={index} />
|
||||
))}
|
||||
</GridOrCarousel>
|
||||
</motion.div>
|
||||
|
||||
@@ -38,11 +38,6 @@ const HeroAnimated = ({
|
||||
className="hero-animated-container"
|
||||
onMouseMove={handleMouseMove}
|
||||
>
|
||||
<div className="hero-animated-image-container">
|
||||
<div className="hero-animated-parallax-wrapper" style={parallaxStyle}>
|
||||
<ImageOrVideo imageSrc={imageSrc} videoSrc={videoSrc} className="hero-animated-image" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="hero-animated-content w-content-width mx-auto">
|
||||
<div className="flex flex-col items-center gap-3 md:gap-5">
|
||||
<span className="px-3 py-1 mb-1 text-sm card rounded">{tag}</span>
|
||||
@@ -59,11 +54,16 @@ const HeroAnimated = ({
|
||||
className="max-w-8/10 text-lg md:text-xl leading-tight text-center"
|
||||
/>
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-2">
|
||||
<Button text={primaryButton.text} href={primaryButton.href} variant="primary" animate />
|
||||
<Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary" animate delay={0.1} />
|
||||
<Button text={primaryButton.text} href={primaryButton.href} variant="primary" animate className="btn-3d" />
|
||||
<Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary" animate delay={0.1} className="btn-3d" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hero-animated-image-container">
|
||||
<div className="hero-animated-parallax-wrapper" style={parallaxStyle}>
|
||||
<ImageOrVideo imageSrc={imageSrc} videoSrc={videoSrc} className="hero-animated-image" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
36
src/hooks/useParallax.ts
Normal file
36
src/hooks/useParallax.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useState, useEffect, type RefObject } from "react";
|
||||
|
||||
export const useParallax = (ref: RefObject<HTMLElement | null>, options?: { maxRotate?: number }) => {
|
||||
const [transform, setTransform] = useState("rotateX(0deg) rotateY(0deg) scale3d(1, 1, 1)");
|
||||
const maxRotate = options?.maxRotate || 5;
|
||||
|
||||
useEffect(() => {
|
||||
const element = ref.current;
|
||||
if (!element) return;
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
const { left, top, width, height } = element.getBoundingClientRect();
|
||||
const x = e.clientX - left;
|
||||
const y = e.clientY - top;
|
||||
|
||||
const rotateX = ((y / height) - 0.5) * -maxRotate * 2;
|
||||
const rotateY = ((x / width) - 0.5) * maxRotate * 2;
|
||||
|
||||
setTransform(`rotateX(${rotateX}deg) rotateY(${rotateY}deg) scale3d(1.05, 1.05, 1.05)`);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setTransform("rotateX(0deg) rotateY(0deg) scale3d(1, 1, 1)");
|
||||
};
|
||||
|
||||
element.addEventListener("mousemove", handleMouseMove);
|
||||
element.addEventListener("mouseleave", handleMouseLeave);
|
||||
|
||||
return () => {
|
||||
element.removeEventListener("mousemove", handleMouseMove);
|
||||
element.removeEventListener("mouseleave", handleMouseLeave);
|
||||
};
|
||||
}, [ref, maxRotate]);
|
||||
|
||||
return transform;
|
||||
};
|
||||
@@ -151,6 +151,17 @@ h6 {
|
||||
font-family: var(--font-manrope), sans-serif;
|
||||
}
|
||||
|
||||
#hero {
|
||||
padding-top: 4rem;
|
||||
}
|
||||
|
||||
.hero-animated-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
/* WEBILD_CARD_STYLE */
|
||||
/* @cards/gradient-mesh */
|
||||
.card {
|
||||
@@ -183,3 +194,13 @@ h6 {
|
||||
2.10837px 3.16256px 9.48767px color-mix(in srgb, var(--color-accent) 10%, transparent);
|
||||
border: 1px solid var(--color-secondary-cta);
|
||||
}
|
||||
|
||||
.btn-3d {
|
||||
transition: transform 0.1s ease-out, border-bottom-width 0.1s ease-out;
|
||||
border-bottom: 4px solid rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.btn-3d:active {
|
||||
transform: translateY(2px);
|
||||
border-bottom-width: 2px;
|
||||
}
|
||||
|
||||
@@ -168,3 +168,12 @@
|
||||
.animate-marquee-horizontal-reverse {
|
||||
animation: marquee-horizontal-reverse 15s linear infinite;
|
||||
}
|
||||
|
||||
.parallax-card-container {
|
||||
perspective: 1000px;
|
||||
}
|
||||
|
||||
.parallax-card {
|
||||
transform-style: preserve-3d;
|
||||
transition: transform 0.1s ease-out;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user