Files
mintel.me/apps/web/src/components/Carousel.tsx
Marc Mintel b15c8408ff
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
fix(blog): optimize component share logic, typography, and modal layouts
2026-02-22 11:41:28 +01:00

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>
);
};