feat: add responsive carousel with mobile-optimized layout and controls
This commit is contained in:
@@ -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 isn’t about more tools. It’s about one intelligent partner that can do it all. This is your gateway to creativity, automation, and discovery.
|
The future isn’t about more tools. It’s 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>
|
||||||
);
|
);
|
||||||
|
39
src/hooks/useResponsiveCarousel.ts
Normal file
39
src/hooks/useResponsiveCarousel.ts
Normal 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;
|
||||||
|
};
|
Reference in New Issue
Block a user