feat(carousel): add clickable carousel indicators
This commit is contained in:
@ -1,3 +1,4 @@
|
|||||||
|
import ServiceCarousel from "@/components/ServiceCarousel";
|
||||||
import {
|
import {
|
||||||
Carousel,
|
Carousel,
|
||||||
CarouselContent,
|
CarouselContent,
|
||||||
@ -347,13 +348,7 @@ export default function Home() {
|
|||||||
Strategic excellence across every dimension of modern PR
|
Strategic excellence across every dimension of modern PR
|
||||||
</h5>
|
</h5>
|
||||||
<section className="flex flex-col items-start space-y-7">
|
<section className="flex flex-col items-start space-y-7">
|
||||||
<Carousel className="w-full h-fit ">
|
<ServiceCarousel />
|
||||||
<CarouselContent>
|
|
||||||
{serviceCarousel.map(({ id, content }) => (
|
|
||||||
<CarouselItem key={id}>{content}</CarouselItem>
|
|
||||||
))}
|
|
||||||
</CarouselContent>
|
|
||||||
</Carousel>
|
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
{/*Approach*/}
|
{/*Approach*/}
|
||||||
|
|||||||
139
components/ServiceCarousel.tsx
Normal file
139
components/ServiceCarousel.tsx
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
Carousel,
|
||||||
|
CarouselContent,
|
||||||
|
CarouselItem,
|
||||||
|
} from "@/components/ui/carousel";
|
||||||
|
import CurrentSlide from "@/hooks/CurrentSlide";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
export default function CarouselWrapper() {
|
||||||
|
const serviceCarousel = [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
<section className="lg:flex lg:flex-row-reverse lg:justify-between space-y-6 lg:items-center">
|
||||||
|
<div className="lg:w-1/2">
|
||||||
|
<Image
|
||||||
|
src={"/images/campaign-strategy.png"}
|
||||||
|
width={2464}
|
||||||
|
height={1672}
|
||||||
|
alt="campaign-strategy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4 lg:w-1/3 lg:space-y-12">
|
||||||
|
<p className="px-6 py-2 lg:px-9 lg:py-4 lg:text-xl text-[#7B2E45] font-lato border-2 border-[#7B2E45] rounded-full w-fit">
|
||||||
|
01
|
||||||
|
</p>
|
||||||
|
<h2 className="text-4xl lg:text-7xl text-white font-lato">
|
||||||
|
Campaign Strategy
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg lg:text-2xl font-lato text-white/60">
|
||||||
|
Comprehensive planning from research to execution, crafted to
|
||||||
|
win hearts and minds.
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-4 w-full">
|
||||||
|
<div className="bg-[#7B2E45]/80 rounded-full w-1/4 h-1 lg:w-[60px]" />
|
||||||
|
<div className="bg-[#7B2E45]/65 rounded-full w-1/5 h-1 lg:w-[50px]" />
|
||||||
|
<div className="bg-[#7B2E45]/50 rounded-full w-1/6 h-1 lg:w-[40px]" />
|
||||||
|
<div className="bg-[#7B2E45]/35 rounded-full w-1/7 h-1 lg:w-[30px]" />
|
||||||
|
<div className="bg-[#7B2E45]/20 rounded-full w-1/8 h-1 lg:w-[20px]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
<section className="lg:flex lg:flex-row-reverse lg:justify-between space-y-6 lg:items-center">
|
||||||
|
<div className="lg:w-1/2">
|
||||||
|
<Image
|
||||||
|
src={"/images/media-production.png"}
|
||||||
|
width={2464}
|
||||||
|
height={1672}
|
||||||
|
alt="media-production"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4 lg:w-1/3 lg:space-y-12">
|
||||||
|
<p className="px-6 py-2 lg:px-9 lg:py-4 lg:text-xl text-[#7B2E45] font-lato border-2 border-[#7B2E45] rounded-full w-fit">
|
||||||
|
02
|
||||||
|
</p>
|
||||||
|
<h2 className="text-4xl lg:text-7xl text-white font-lato">
|
||||||
|
Media Production
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg font-lato text-white/60">
|
||||||
|
Cinematic storytelling through video, photography, and
|
||||||
|
multimedia content that captivates.
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-4 w-full">
|
||||||
|
<div className="bg-[#7B2E45]/80 rounded-full w-1/4 h-1 lg:w-[60px]" />
|
||||||
|
<div className="bg-[#7B2E45]/65 rounded-full w-1/5 h-1 lg:w-[50px]" />
|
||||||
|
<div className="bg-[#7B2E45]/50 rounded-full w-1/6 h-1 lg:w-[40px]" />
|
||||||
|
<div className="bg-[#7B2E45]/35 rounded-full w-1/7 h-1 lg:w-[30px]" />
|
||||||
|
<div className="bg-[#7B2E45]/20 rounded-full w-1/8 h-1 lg:w-[20px]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
<section className="lg:flex lg:flex-row-reverse lg:justify-between space-y-6 lg:items-center">
|
||||||
|
<div className="lg:w-1/2">
|
||||||
|
<Image
|
||||||
|
src={"/images/research.png"}
|
||||||
|
width={2464}
|
||||||
|
height={1672}
|
||||||
|
alt="research"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4 lg:w-1/3 lg:space-y-12">
|
||||||
|
<p className="px-6 py-2 lg:px-9 lg:py-4 lg:text-xl text-[#7B2E45] font-lato border-2 border-[#7B2E45] rounded-full w-fit">
|
||||||
|
03
|
||||||
|
</p>
|
||||||
|
<h2 className="text-4xl lg:text-7xl text-white font-lato">
|
||||||
|
Research & Analytics
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg font-lato text-white/60">
|
||||||
|
Data-driven insights that inform every decision and optimize
|
||||||
|
every outcome
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-4 w-full">
|
||||||
|
<div className="bg-[#7B2E45]/80 rounded-full w-1/4 h-1 lg:w-[60px]" />
|
||||||
|
<div className="bg-[#7B2E45]/65 rounded-full w-1/5 h-1 lg:w-[50px]" />
|
||||||
|
<div className="bg-[#7B2E45]/50 rounded-full w-1/6 h-1 lg:w-[40px]" />
|
||||||
|
<div className="bg-[#7B2E45]/35 rounded-full w-1/7 h-1 lg:w-[30px]" />
|
||||||
|
<div className="bg-[#7B2E45]/20 rounded-full w-1/8 h-1 lg:w-[20px]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const [api, setApi] = useState<any>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Carousel setApi={setApi} className="w-full h-fit">
|
||||||
|
<CarouselContent>
|
||||||
|
{serviceCarousel.map(({ id, content }) => (
|
||||||
|
<CarouselItem key={id}>{content}</CarouselItem>
|
||||||
|
))}
|
||||||
|
</CarouselContent>
|
||||||
|
</Carousel>
|
||||||
|
|
||||||
|
{/* All slide reading logic happens inside this component */}
|
||||||
|
<CurrentSlide api={api} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
77
hooks/CurrentSlide.tsx
Normal file
77
hooks/CurrentSlide.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import type { CarouselApi } from "@/components/ui/carousel";
|
||||||
|
|
||||||
|
export default function CurrentSlide({
|
||||||
|
api,
|
||||||
|
total = 3,
|
||||||
|
}: {
|
||||||
|
api?: CarouselApi;
|
||||||
|
total?: number;
|
||||||
|
}) {
|
||||||
|
const [current, setCurrent] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!api) return;
|
||||||
|
|
||||||
|
// Set initial slide
|
||||||
|
setCurrent(api.selectedScrollSnap());
|
||||||
|
|
||||||
|
const handler = () => setCurrent(api.selectedScrollSnap());
|
||||||
|
api.on("select", handler);
|
||||||
|
|
||||||
|
return () => api.off("select", handler);
|
||||||
|
}, [api]);
|
||||||
|
|
||||||
|
const handleClick = (index: number) => {
|
||||||
|
if (!api) return;
|
||||||
|
api.scrollTo(index); // embla API command to jump to slide
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center w-full gap-8 py-6">
|
||||||
|
{Array.from({ length: total }).map((_, i) => {
|
||||||
|
const isActive = i === current;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={i}
|
||||||
|
onClick={() => handleClick(i)}
|
||||||
|
className="flex flex-col items-center gap-2 cursor-pointer group"
|
||||||
|
>
|
||||||
|
{/* Outer ring */}
|
||||||
|
<div
|
||||||
|
className={`
|
||||||
|
relative w-10 h-10 rounded-full border transition-all duration-300
|
||||||
|
${isActive ? "border-[#C74C65]" : "border-[#C74C65]/40"}
|
||||||
|
group-hover:border-[#C74C65]
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{/* Inner dot (only active) */}
|
||||||
|
{isActive && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<div className="w-3 h-3 rounded-full bg-[#C74C65]" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Slide number */}
|
||||||
|
<span
|
||||||
|
className={`
|
||||||
|
font-lato text-md tracking-widest transition-colors duration-300
|
||||||
|
${
|
||||||
|
isActive
|
||||||
|
? "text-[#C74C65]"
|
||||||
|
: "text-[#C74C65]/40 group-hover:text-[#C74C65]"
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{String(i + 1).padStart(2, "0")}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user