Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 5s
Build & Deploy / 🏗️ Build (push) Failing after 14s
Build & Deploy / 🧪 QA (push) Failing after 1m48s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
101 lines
4.8 KiB
TypeScript
101 lines
4.8 KiB
TypeScript
'use client';
|
|
|
|
import React, { useRef, useState } from 'react';
|
|
|
|
interface CarouselItem {
|
|
title: string;
|
|
content: string;
|
|
icon?: React.ReactNode;
|
|
}
|
|
|
|
interface CarouselProps {
|
|
items: CarouselItem[];
|
|
className?: string;
|
|
}
|
|
|
|
export const Carousel: React.FC<CarouselProps> = ({ items, className = '' }) => {
|
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
|
|
const scrollTo = (index: number) => {
|
|
if (!scrollRef.current) return;
|
|
const width = scrollRef.current.clientWidth;
|
|
scrollRef.current.scrollTo({ left: index * width, behavior: 'smooth' });
|
|
setActiveIndex(index);
|
|
};
|
|
|
|
const handleScroll = () => {
|
|
if (!scrollRef.current) return;
|
|
const width = scrollRef.current.clientWidth;
|
|
const index = Math.round(scrollRef.current.scrollLeft / width);
|
|
if (index !== activeIndex) setActiveIndex(index);
|
|
};
|
|
|
|
// Icons helper (default icon if none provided)
|
|
const DefaultIcon = () => (
|
|
<svg className="w-6 h-6 text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
</svg>
|
|
);
|
|
|
|
return (
|
|
<div className={`not-prose my-16 px-12 md:px-16 ${className}`}>
|
|
<div className="relative group">
|
|
{/* Scroll Container */}
|
|
<div
|
|
ref={scrollRef}
|
|
onScroll={handleScroll}
|
|
className="flex overflow-x-auto snap-x snap-mandatory scrollbar-hide rounded-2xl border border-slate-200 bg-slate-50 gap-4 p-4"
|
|
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
|
|
>
|
|
{(items || []).map((item, index) => (
|
|
<div
|
|
key={index}
|
|
className="min-w-[85%] md:min-w-[45%] snap-center p-8 md:p-10 flex flex-col gap-6 items-start bg-white rounded-xl border border-slate-100 shadow-sm"
|
|
>
|
|
<div className="flex-shrink-0 w-12 h-12 bg-slate-50 rounded-xl border border-slate-100 flex items-center justify-center">
|
|
{item.icon || <DefaultIcon />}
|
|
</div>
|
|
<div className="flex-1">
|
|
<h4 className="text-lg font-bold text-slate-900 mb-2">{item.title}</h4>
|
|
{item.content && (
|
|
<p className="text-sm text-slate-600 leading-relaxed m-0">{item.content}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Nav Buttons (Outside) */}
|
|
<button
|
|
onClick={() => scrollTo(Math.max(0, activeIndex - 1))}
|
|
disabled={activeIndex === 0}
|
|
className="absolute -left-12 md:-left-16 top-1/2 -translate-y-1/2 p-3 bg-white rounded-full shadow-sm border border-slate-200 text-slate-600 hover:text-slate-900 disabled:opacity-30 transition-all hidden md:block"
|
|
>
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg>
|
|
</button>
|
|
<button
|
|
onClick={() => scrollTo(Math.min((items?.length || 0) - 1, activeIndex + 1))}
|
|
disabled={activeIndex === (items?.length || 0) - 1}
|
|
className="absolute -right-12 md:-right-16 top-1/2 -translate-y-1/2 p-3 bg-white rounded-full shadow-sm border border-slate-200 text-slate-600 hover:text-slate-900 disabled:opacity-30 transition-all hidden md:block"
|
|
>
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Dots */}
|
|
<div className="flex justify-center mt-6 gap-3">
|
|
{(items || []).map((_, index) => (
|
|
<button
|
|
key={index}
|
|
onClick={() => scrollTo(index)}
|
|
className={`h-1.5 transition-all duration-300 rounded-full ${index === activeIndex ? 'w-8 bg-slate-900' : 'w-2 bg-slate-200 hover:bg-slate-300'
|
|
}`}
|
|
aria-label={`Go to slide ${index + 1}`}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|