feat: add responsive carousel with mobile-optimized layout and controls

This commit is contained in:
2025-09-19 01:01:46 +02:00
parent 0dedde3592
commit 43d995bbc2
2 changed files with 73 additions and 17 deletions

View File

@@ -1,6 +1,7 @@
'use client' 'use client'
import { useEffect, useMemo, useState, useRef } from 'react' import { useEffect, useMemo, useState } from 'react'
import { useResponsiveCarousel } from '@/hooks/useResponsiveCarousel';
import Image from 'next/image' import Image from 'next/image'
import { motion, AnimatePresence } from 'framer-motion' import { motion, AnimatePresence } from 'framer-motion'
import { wrap } from 'popmotion' import { wrap } from 'popmotion'
@@ -24,16 +25,13 @@ const galleryItems = [
] ]
// 🔧 Carousel Config // 🔧 Carousel Config
const VISIBLE = 4 const VISIBLE = 4;
const GAP = 300 // spacing for larger cards const AUTOPLAY_MS = 3200;
const ROT_Y = 18
const DEPTH = 210
const SCALE_DROP = 0.12
const AUTOPLAY_MS = 3200
export function ClickableGallery() { export function ClickableGallery() {
const [active, setActive] = useState(0) const [active, setActive] = useState(0);
const [hovering, setHovering] = useState(false) const [hovering, setHovering] = useState(false);
const { GAP, ROT_Y, DEPTH, SCALE_DROP } = useResponsiveCarousel();
// autoplay // autoplay
useEffect(() => { useEffect(() => {
@@ -59,7 +57,7 @@ export function ClickableGallery() {
</div> </div>
</FadeIn> </FadeIn>
<FadeIn transition={{ duration: 0.8, delay: 0.2 }}> <FadeIn transition={{ duration: 0.8, delay: 0.2 }}>
<div className="mx-auto max-w-4xl mt-6"> <div className="mx-auto max-w-4xl mt-6 lg:px-0 px-4">
<P className="text-center" color="primary"> <P className="text-center" color="primary">
The future isnt about more tools. Its about one intelligent partner that can do it all. This is your gateway to creativity, automation, and discovery. The future isnt about more tools. Its about one intelligent partner that can do it all. This is your gateway to creativity, automation, and discovery.
</P> </P>
@@ -72,7 +70,7 @@ export function ClickableGallery() {
onMouseEnter={() => setHovering(true)} onMouseEnter={() => setHovering(true)}
onMouseLeave={() => setHovering(false)} onMouseLeave={() => setHovering(false)}
> >
<div className="relative w-full max-w-[1800px] h-[500px]" style={{ perspective: '1600px' }}> <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' }}> <div className="absolute inset-0" style={{ transformStyle: 'preserve-3d' }}>
<AnimatePresence initial={false}> <AnimatePresence initial={false}>
{indices.map((idx, i) => { {indices.map((idx, i) => {
@@ -119,7 +117,8 @@ export function ClickableGallery() {
</div> </div>
{/* Arrows */} {/* Arrows */}
<div className="absolute inset-y-0 left-8 flex items-center z-50"> {/* Arrows */}
<div className="absolute inset-y-0 left-8 hidden md:flex items-center z-50">
<button <button
onClick={prev} onClick={prev}
className="bg-transparent rounded-full p-2 shadow-lg backdrop-blur-md" className="bg-transparent rounded-full p-2 shadow-lg backdrop-blur-md"
@@ -128,7 +127,7 @@ export function ClickableGallery() {
<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"/>' }} /> <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> </button>
</div> </div>
<div className="absolute inset-y-0 right-8 flex items-center z-50"> <div className="absolute inset-y-0 right-8 hidden md:flex items-center z-50">
<button <button
onClick={next} onClick={next}
className="bg-transparent rounded-full p-2 shadow-lg backdrop-blur-md" className="bg-transparent rounded-full p-2 shadow-lg backdrop-blur-md"
@@ -138,8 +137,8 @@ export function ClickableGallery() {
</button> </button>
</div> </div>
{/* Foreground pill */} {/* Foreground pill (Desktop) */}
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-[60]"> <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-black/30 shadow-[0_8px_40px_rgba(0,0,0,0.15)] px-12 backdrop-blur"> <div className="flex items-center justify-between w-[1040px] gap-6 rounded-2xl bg-black/30 shadow-[0_8px_40px_rgba(0,0,0,0.15)] px-12 backdrop-blur">
<CT as="h4" className="max-w-[820px] h-[72px] text-white flex items-center"> <CT as="h4" className="max-w-[820px] h-[72px] text-white flex items-center">
<TypeAnimation <TypeAnimation
@@ -150,12 +149,30 @@ export function ClickableGallery() {
repeat={0} repeat={0}
/> />
</CT> </CT>
<Button href="#" color="cyan" className="text-sm px-4 py-2 lg:text-base"> <Button href="#" color="cyan" className="text-sm px-4 py-2 lg:text-base whitespace-nowrap">
Start Start
</Button> </Button>
</div> </div>
</div> </div>
</section> </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-white/10 bg-opacity-80 p-4 backdrop-blur-md">
<CT as="h4" className="w-full text-left h-[72px] text-white leading-tight flex items-center">
<TypeAnimation
key={active}
sequence={[galleryItems[active].text]}
wrapper="span"
speed={50}
repeat={0}
/>
</CT>
<Button href="#" color="cyan" className="text-xs px-3 py-1.5 whitespace-nowrap">
Start
</Button>
</div>
</div>
</FadeIn> </FadeIn>
</div> </div>
); );

View File

@@ -0,0 +1,39 @@
'use client'
import { useState, useEffect } from 'react';
// 🔧 Carousel Config
const desktopConfig = {
GAP: 300,
ROT_Y: 18,
DEPTH: 210,
SCALE_DROP: 0.12,
};
const mobileConfig = {
GAP: 100, // Smaller gap for mobile
ROT_Y: 0, // Flatter view on mobile
DEPTH: 150, // Less depth
SCALE_DROP: 0.1, // Less aggressive scaling
};
export const useResponsiveCarousel = () => {
const [config, setConfig] = useState(desktopConfig);
useEffect(() => {
const checkScreenSize = () => {
if (window.innerWidth < 768) {
setConfig(mobileConfig);
} else {
setConfig(desktopConfig);
}
};
checkScreenSize();
window.addEventListener('resize', checkScreenSize);
return () => window.removeEventListener('resize', checkScreenSize);
}, []);
return config;
};