feat: add responsive carousel and dark theme to agents gallery section

This commit is contained in:
2025-10-22 18:04:52 +02:00
parent 8ea15271d3
commit 31f5e53a71
6 changed files with 310 additions and 57 deletions

View File

@@ -1,58 +1,178 @@
import { motion } from 'framer-motion'
import { Container } from '../../components/Container'
'use client'
import { useEffect, useMemo, useState } from 'react'
import { useResponsiveCarousel } from '@/hooks/useResponsiveCarousel';
import { motion, AnimatePresence } from 'framer-motion'
import { wrap } from 'popmotion'
import { Button } from '@/components/Button';
import { SectionHeader, P, Eyebrow } from '@/components/Texts';
import { TypeAnimation } from 'react-type-animation'
import { FadeIn } from '@/components/FadeIn';
const galleryItems = [
{ text: 'Navigate and interact with any web interface', image: '/images/gallery/interface.jpg' },
{ text: 'Process documents across all formats', image: '/images/gallery/docs.jpg' },
{ text: 'Execute multi-step workflows autonomously', image: '/images/gallery/flow.jpg' },
{ text: 'Manage calendars, emails, and tasks', image: '/images/gallery/calendar.jpg' },
{ text: 'Perform deep semantic search across all data sources', image: '/images/gallery/data.jpg' },
{ text: 'Identify patterns in complex datasets', image: '/images/gallery/datasets.jpg' },
{ text: 'Navigate and interact with any web interface', image: '/images/gallery/interface.jpg', width: 448, height: 277 },
{ text: 'Process documents across all formats', image: '/images/gallery/docs.jpg', width: 448, height: 277 },
{ text: 'Execute multi-step workflows autonomously', image: '/images/gallery/flow.jpg', width: 448, height: 277 },
{ text: 'Manage calendars, emails, and tasks', image: '/images/gallery/calendar.jpg', width: 448, height: 277 },
{ text: 'Perform deep semantic search across all data sources', image: '/images/gallery/data.jpg', width: 448, height: 277 },
{ text: 'Identify patterns in complex datasets', image: '/images/gallery/datasets.jpg', width: 448, height: 277 },
{ text: 'Provide real-time market intelligence', image: '/images/gallery/market.jpg', width: 448, height: 277 },
{ text: 'Generate and debug code in multiple languages', image: '/images/gallery/code.jpg', width: 448, height: 277 },
{ text: 'Create consistent branded content', image: '/images/gallery/branding.jpg', width: 448, height: 277 },
{ text: 'Translate and localize materials', image: '/images/gallery/translate.jpg', width: 448, height: 277 },
{ text: 'Transform and migrate data structures', image: '/images/gallery/structure.jpg', width: 448, height: 277 },
]
export function GallerySection() {
return (
<section className="bg-gray-50 py-20 lg:py-32">
<Container>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8 }}
className="mx-auto max-w-3xl text-center mb-16"
>
<h2 className="text-3xl lg:text-4xl font-medium tracking-tight text-gray-900">
Agents with Endless Possibilities.
</h2>
<p className="mt-6 text-lg text-gray-600">
Your private agent coordinates a team of specialists that spin up on demand, collaborate across your world, and deliver end-to-end results. Many agents, one intelligenceyours.
</p>
</motion.div>
// 🔧 Carousel Config
const VISIBLE = 4;
const AUTOPLAY_MS = 3200;
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{galleryItems.map((item, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.1 }}
className="group relative overflow-hidden rounded-2xl bg-white border border-gray-200 hover:border-cyan-500 hover:shadow-lg transition-all duration-300"
>
<div className="aspect-video overflow-hidden">
<img
src={item.image}
alt={item.text}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
/>
</div>
<div className="p-6">
<p className="text-sm font-medium text-gray-900">{item.text}</p>
</div>
</motion.div>
))}
</div>
</Container>
</section>
export function GallerySection() {
const [active, setActive] = useState(0);
const [hovering, setHovering] = useState(false);
const { GAP, ROT_Y, DEPTH, SCALE_DROP } = useResponsiveCarousel();
// autoplay
useEffect(() => {
if (hovering) return
const id = setInterval(() => setActive((i) => wrap(0, galleryItems.length, i + 1)), AUTOPLAY_MS)
return () => clearInterval(id)
}, [hovering])
const indices = useMemo(
() => [...Array(VISIBLE * 2 + 1)].map((_, i) => wrap(0, galleryItems.length, active + i - VISIBLE)),
[active]
)
const next = () => setActive((i) => wrap(0, galleryItems.length, i + 1))
const prev = () => setActive((i) => wrap(0, galleryItems.length, i - 1))
return (
<div className="bg-[#FAFAFA]">
<div className="relative isolate pt-8 pb-0 text-center w-full">
<FadeIn transition={{ duration: 0.8, delay: 0.1 }}>
<div className="mx-auto max-w-5xl lg:mt-12">
<Eyebrow color="accent">Use Cases</Eyebrow>
<SectionHeader className="text-center" color="dark">Agents with Endless Possibilities.</SectionHeader>
</div>
</FadeIn>
<FadeIn transition={{ duration: 0.8, delay: 0.2 }}>
<div className="mx-auto max-w-4xl mt-6 lg:px-0 px-4">
<P className="text-center" color="dark">
Your private agent coordinates a team of specialists that spin up on demand, collaborate across your world, and deliver end-to-end results.
Many agents, one intelligenceyours.
</P>
</div>
</FadeIn>
</div>
<FadeIn transition={{ duration: 1, delay: 0.4 }}>
<section
className="relative w-full flex items-center justify-center overflow-hidden -mt-8 pt-0 pb-0"
onMouseEnter={() => setHovering(true)}
onMouseLeave={() => setHovering(false)}
>
<div className="relative w-full max-w-[1800px] h-[300px] md:h-[500px]" style={{ perspective: '1600px' }}>
<div className="absolute inset-0" style={{ transformStyle: 'preserve-3d' }}>
<AnimatePresence initial={false}>
{indices.map((idx, i) => {
const distance = i - VISIBLE;
const item = galleryItems[idx];
const x = distance * GAP;
const z = -Math.abs(distance) * DEPTH;
const r = distance * ROT_Y;
const s = 1 - Math.abs(distance) * SCALE_DROP;
const o = distance === 0 ? 1 : 0.80;
const zIndex = 100 - Math.abs(distance);
return (
<motion.div
key={`${idx}-${i}`}
className={`absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 will-change-transform overflow-hidden ${distance === 0 ? 'rounded-xl' : ''}`}
initial={{ opacity: 0 }}
animate={{
transform: `translateX(${x}px) translateZ(${z}px) rotateY(${r}deg) scale(${s})`,
zIndex,
opacity: o,
boxShadow: distance === 0 ? '0 0 20px 5px rgba(0, 0, 0, 0.1)' : 'none',
}}
exit={{ opacity: 0 }}
transition={{ type: 'spring', stiffness: 220, damping: 26 }}
onClick={() => setActive(idx)}
>
<div className="relative bg-gray-100 flex items-center justify-center">
<img
src={item.image}
alt={item.text}
width={item.width}
height={item.height}
className="object-contain"
/>
</div>
</motion.div>
);
})}
</AnimatePresence>
</div>
</div>
{/* Arrows */}
<div className="absolute inset-y-0 left-8 hidden md:flex items-center z-50">
<button
onClick={prev}
className="bg-white/50 rounded-full p-2 shadow-lg backdrop-blur-md text-black"
aria-label="Previous"
>
<svg className="size-8" viewBox="0 0 24 24" fill="none" dangerouslySetInnerHTML={{ __html: '<path d="M15 19L8 12l7-7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>' }} />
</button>
</div>
<div className="absolute inset-y-0 right-8 hidden md:flex items-center z-50">
<button
onClick={next}
className="bg-white/50 rounded-full p-2 shadow-lg backdrop-blur-md text-black"
aria-label="Next"
>
<svg className="size-8" viewBox="0 0 24 24" fill="none" dangerouslySetInnerHTML={{ __html: '<path d="M9 5l7 7-7 7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>' }} />
</button>
</div>
{/* Foreground pill (Desktop) */}
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-[60] hidden md:block">
<div className="flex items-center justify-between w-[1040px] gap-6 rounded-2xl bg-gray-100/80 shadow-[0_8px_40px_rgba(0,0,0,0.15)] px-12 backdrop-blur">
<P as="h4" className="max-w-[820px] h-[72px] flex items-center" color="dark">
<TypeAnimation
key={active}
sequence={[galleryItems[active].text]}
wrapper="span"
speed={50}
repeat={0}
/>
</P>
<Button href="#" color="cyan" className="text-sm px-4 py-2 lg:text-base whitespace-nowrap">
Start
</Button>
</div>
</div>
</section>
{/* Text box (Mobile) */}
<div className="md:hidden w-full px-4 -mt-12 mb-16">
<div className="flex flex-row items-center justify-between w-full gap-x-4 rounded-2xl bg-gray-100/80 p-4 backdrop-blur-md">
<P as="h4" className="w-full text-left h-[72px] leading-tight flex items-center" color="dark">
<TypeAnimation
key={active}
sequence={[galleryItems[active].text]}
wrapper="span"
speed={50}
repeat={0}
/>
</P>
<Button href="#" color="cyan" className="text-xs px-3 py-1.5 whitespace-nowrap">
Start
</Button>
</div>
</div>
</FadeIn>
</div>
);
}