201 lines
7.3 KiB
TypeScript
Raw Normal View History

"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import Link from "next/link";
import { ArrowRight, ChevronLeft, ChevronRight } from "lucide-react";
import { cn } from "@/shared/utils";
import { useInView } from "@/features/landing-page/hooks";
import {
personalConversionCards,
businessConversionCards,
type ConversionServiceCard,
} from "@/features/landing-page/data";
type Tab = "personal" | "business";
function ServiceConversionCard({ card }: { card: ConversionServiceCard }) {
return (
<Link href={card.href} className="group flex-shrink-0 w-[260px] sm:w-[280px] snap-start">
<article
data-service-card
className="h-full rounded-2xl bg-card border border-border/60 px-6 py-7 shadow-sm hover:shadow-md hover:border-primary/30 transition-all duration-300 group-hover:-translate-y-1 flex flex-col"
>
{/* Icon + Badge row */}
<div className="flex items-center justify-between mb-4">
<div className="text-primary">{card.icon}</div>
{card.badge && (
<span className="inline-flex items-center rounded-full bg-success/10 text-success px-2.5 py-0.5 text-xs font-semibold">
{card.badge}
</span>
)}
</div>
<p className="text-sm text-muted-foreground mb-1">{card.problemHook}</p>
<h3 className="text-lg font-bold text-foreground mb-1">{card.title}</h3>
<p className="text-sm text-muted-foreground mb-3 flex-grow">{card.keyBenefit}</p>
<span className="inline-flex items-center gap-1.5 text-sm font-semibold text-primary group-hover:gap-2.5 transition-all mt-auto">
{card.ctaLabel}
<ArrowRight className="h-4 w-4" />
</span>
</article>
</Link>
);
}
export function ServicesCarousel() {
const [activeTab, setActiveTab] = useState<Tab>("personal");
const carouselRef = useRef<HTMLDivElement>(null);
const itemWidthRef = useRef(0);
const isScrollingRef = useRef(false);
const autoScrollTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const [sectionRef, isInView] = useInView();
const cards = activeTab === "personal" ? personalConversionCards : businessConversionCards;
const computeItemWidth = useCallback(() => {
const container = carouselRef.current;
if (!container) return;
const card = container.querySelector<HTMLElement>("[data-service-card]");
if (!card) return;
const style = getComputedStyle(container);
const gap = Number.parseFloat(style.columnGap || style.gap || "0") || 24;
itemWidthRef.current = card.getBoundingClientRect().width + gap;
}, []);
const scrollByOne = useCallback((direction: 1 | -1) => {
const container = carouselRef.current;
if (!container || !itemWidthRef.current || isScrollingRef.current) return;
isScrollingRef.current = true;
container.scrollBy({
left: direction * itemWidthRef.current,
behavior: "smooth",
});
setTimeout(() => {
isScrollingRef.current = false;
}, 500);
}, []);
const startAutoScroll = useCallback(() => {
if (autoScrollTimerRef.current) clearInterval(autoScrollTimerRef.current);
autoScrollTimerRef.current = setInterval(() => scrollByOne(1), 5000);
}, [scrollByOne]);
const stopAutoScroll = useCallback(() => {
if (autoScrollTimerRef.current) {
clearInterval(autoScrollTimerRef.current);
autoScrollTimerRef.current = null;
}
}, []);
useEffect(() => {
computeItemWidth();
window.addEventListener("resize", computeItemWidth);
startAutoScroll();
return () => {
window.removeEventListener("resize", computeItemWidth);
stopAutoScroll();
};
}, [computeItemWidth, startAutoScroll, stopAutoScroll]);
// Reset scroll position when tab changes
useEffect(() => {
if (carouselRef.current) {
carouselRef.current.scrollTo({ left: 0, behavior: "smooth" });
}
computeItemWidth();
}, [activeTab, computeItemWidth]);
const handlePrev = useCallback(() => {
scrollByOne(-1);
startAutoScroll();
}, [scrollByOne, startAutoScroll]);
const handleNext = useCallback(() => {
scrollByOne(1);
startAutoScroll();
}, [scrollByOne, startAutoScroll]);
return (
<section
ref={sectionRef as React.RefObject<HTMLElement>}
className={cn(
"relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-surface-sunken/30 py-14 sm:py-16 transition-all duration-700",
isInView ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
)}
>
<div className="mx-auto max-w-6xl px-6 sm:px-10 lg:px-14">
{/* Header + Tabs */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-8">
<div>
<h2 className="text-2xl sm:text-3xl font-extrabold text-foreground">Our Services</h2>
<p className="mt-1 text-base text-muted-foreground">
Everything you need to stay connected in Japan
</p>
</div>
<div className="flex bg-muted rounded-full p-1 self-start">
<button
type="button"
onClick={() => setActiveTab("personal")}
className={cn(
"px-4 py-2 text-sm font-semibold rounded-full transition-all",
activeTab === "personal"
? "bg-foreground text-background shadow-sm"
: "text-muted-foreground hover:text-foreground"
)}
>
For You
</button>
<button
type="button"
onClick={() => setActiveTab("business")}
className={cn(
"px-4 py-2 text-sm font-semibold rounded-full transition-all",
activeTab === "business"
? "bg-foreground text-background shadow-sm"
: "text-muted-foreground hover:text-foreground"
)}
>
For Business
</button>
</div>
</div>
{/* Carousel */}
<div className="relative" onMouseEnter={stopAutoScroll} onMouseLeave={startAutoScroll}>
<div
ref={carouselRef}
className="flex gap-5 overflow-x-auto scroll-smooth pb-4 snap-x snap-mandatory"
style={{
scrollbarWidth: "none",
WebkitOverflowScrolling: "touch",
}}
>
{cards.map(card => (
<ServiceConversionCard key={card.title} card={card} />
))}
</div>
{/* Navigation buttons */}
<div className="flex justify-end gap-2 mt-4">
<button
type="button"
aria-label="Scroll left"
onClick={handlePrev}
className="h-9 w-9 rounded-full border border-border bg-card text-foreground shadow-sm hover:bg-muted transition-colors flex items-center justify-center"
>
<ChevronLeft className="h-4 w-4" />
</button>
<button
type="button"
aria-label="Scroll right"
onClick={handleNext}
className="h-9 w-9 rounded-full border border-border bg-card text-foreground shadow-sm hover:bg-muted transition-colors flex items-center justify-center"
>
<ChevronRight className="h-4 w-4" />
</button>
</div>
</div>
</div>
</section>
);
}