255 lines
8.2 KiB
TypeScript
255 lines
8.2 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
import { cn } from '../../lib/utils';
|
|
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
|
|
|
export interface Slide {
|
|
id: string;
|
|
title?: string;
|
|
subtitle?: string;
|
|
description?: string;
|
|
image?: string;
|
|
ctaText?: string;
|
|
ctaLink?: string;
|
|
}
|
|
|
|
export interface SliderProps {
|
|
slides: Slide[];
|
|
autoplay?: boolean;
|
|
autoplayInterval?: number;
|
|
showControls?: boolean;
|
|
showIndicators?: boolean;
|
|
variant?: 'default' | 'fullscreen' | 'compact';
|
|
className?: string;
|
|
}
|
|
|
|
/**
|
|
* Slider Component
|
|
* Responsive carousel for WPBakery nectar_slider/nectar_carousel patterns
|
|
* Supports autoplay, manual controls, and multiple variants
|
|
*/
|
|
export const Slider: React.FC<SliderProps> = ({
|
|
slides,
|
|
autoplay = false,
|
|
autoplayInterval = 5000,
|
|
showControls = true,
|
|
showIndicators = true,
|
|
variant = 'default',
|
|
className = ''
|
|
}) => {
|
|
const [currentIndex, setCurrentIndex] = useState(0);
|
|
const [isTransitioning, setIsTransitioning] = useState(false);
|
|
|
|
// Handle autoplay
|
|
useEffect(() => {
|
|
if (!autoplay || slides.length <= 1) return;
|
|
|
|
const interval = setInterval(() => {
|
|
nextSlide();
|
|
}, autoplayInterval);
|
|
|
|
return () => clearInterval(interval);
|
|
}, [autoplay, autoplayInterval, currentIndex, slides.length]);
|
|
|
|
const nextSlide = useCallback(() => {
|
|
if (isTransitioning || slides.length <= 1) return;
|
|
setIsTransitioning(true);
|
|
setCurrentIndex((prev) => (prev + 1) % slides.length);
|
|
setTimeout(() => setIsTransitioning(false), 300);
|
|
}, [slides.length, isTransitioning]);
|
|
|
|
const prevSlide = useCallback(() => {
|
|
if (isTransitioning || slides.length <= 1) return;
|
|
setIsTransitioning(true);
|
|
setCurrentIndex((prev) => (prev - 1 + slides.length) % slides.length);
|
|
setTimeout(() => setIsTransitioning(false), 300);
|
|
}, [slides.length, isTransitioning]);
|
|
|
|
const goToSlide = useCallback((index: number) => {
|
|
if (isTransitioning || slides.length <= 1) return;
|
|
setIsTransitioning(true);
|
|
setCurrentIndex(index);
|
|
setTimeout(() => setIsTransitioning(false), 300);
|
|
}, [slides.length, isTransitioning]);
|
|
|
|
// Variant-specific styles
|
|
const variantStyles = {
|
|
default: 'rounded-xl overflow-hidden shadow-lg',
|
|
fullscreen: 'w-full h-full rounded-none',
|
|
compact: 'rounded-lg overflow-hidden shadow-md'
|
|
};
|
|
|
|
const heightStyles = {
|
|
default: 'h-96 md:h-[500px]',
|
|
fullscreen: 'h-screen',
|
|
compact: 'h-64 md:h-80'
|
|
};
|
|
|
|
return (
|
|
<div className={cn(
|
|
'relative w-full bg-gray-900',
|
|
heightStyles[variant],
|
|
variantStyles[variant],
|
|
className
|
|
)}>
|
|
{/* Slides Container */}
|
|
<div className="relative w-full h-full overflow-hidden">
|
|
{slides.map((slide, index) => (
|
|
<div
|
|
key={slide.id}
|
|
className={cn(
|
|
'absolute inset-0 w-full h-full transition-opacity duration-500',
|
|
currentIndex === index ? 'opacity-100 z-10' : 'opacity-0 z-0'
|
|
)}
|
|
>
|
|
{/* Background Image */}
|
|
{slide.image && (
|
|
<div className="absolute inset-0">
|
|
<div
|
|
className="w-full h-full bg-cover bg-center"
|
|
style={{ backgroundImage: `url(${slide.image})` }}
|
|
/>
|
|
{/* Overlay for better text readability */}
|
|
<div className="absolute inset-0 bg-gradient-to-b from-black/40 via-black/20 to-black/60" />
|
|
</div>
|
|
)}
|
|
|
|
{/* Content */}
|
|
<div className="relative z-10 h-full flex flex-col items-center justify-center px-4 md:px-8 text-white text-center">
|
|
<div className="max-w-4xl space-y-4 md:space-y-6">
|
|
{slide.subtitle && (
|
|
<p className={cn(
|
|
'text-sm md:text-base uppercase tracking-wider font-semibold',
|
|
'text-white/90',
|
|
variant === 'compact' && 'text-xs'
|
|
)}>
|
|
{slide.subtitle}
|
|
</p>
|
|
)}
|
|
|
|
{slide.title && (
|
|
<h2 className={cn(
|
|
'text-3xl md:text-5xl font-bold leading-tight',
|
|
'text-white drop-shadow-lg',
|
|
variant === 'compact' && 'text-2xl md:text-3xl'
|
|
)}>
|
|
{slide.title}
|
|
</h2>
|
|
)}
|
|
|
|
{slide.description && (
|
|
<p className={cn(
|
|
'text-lg md:text-xl leading-relaxed',
|
|
'text-white/90 max-w-2xl mx-auto',
|
|
variant === 'compact' && 'text-base md:text-lg'
|
|
)}>
|
|
{slide.description}
|
|
</p>
|
|
)}
|
|
|
|
{slide.ctaText && slide.ctaLink && (
|
|
<a
|
|
href={slide.ctaLink}
|
|
className={cn(
|
|
'inline-flex items-center justify-center',
|
|
'px-6 py-3 md:px-8 md:py-4',
|
|
'bg-primary hover:bg-primary-dark',
|
|
'text-white font-semibold rounded-lg',
|
|
'transition-all duration-200',
|
|
'hover:scale-105 active:scale-95',
|
|
'shadow-lg hover:shadow-xl'
|
|
)}
|
|
>
|
|
{slide.ctaText}
|
|
</a>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Navigation Controls */}
|
|
{showControls && slides.length > 1 && (
|
|
<>
|
|
<button
|
|
onClick={prevSlide}
|
|
className={cn(
|
|
'absolute left-4 top-1/2 -translate-y-1/2',
|
|
'z-20 p-2 md:p-3',
|
|
'bg-white/20 hover:bg-white/30 backdrop-blur-sm',
|
|
'text-white rounded-full',
|
|
'transition-all duration-200',
|
|
'hover:scale-110 active:scale-95',
|
|
'focus:outline-none focus:ring-2 focus:ring-white/50'
|
|
)}
|
|
aria-label="Previous slide"
|
|
>
|
|
<ChevronLeft className="w-5 h-5 md:w-6 md:h-6" />
|
|
</button>
|
|
|
|
<button
|
|
onClick={nextSlide}
|
|
className={cn(
|
|
'absolute right-4 top-1/2 -translate-y-1/2',
|
|
'z-20 p-2 md:p-3',
|
|
'bg-white/20 hover:bg-white/30 backdrop-blur-sm',
|
|
'text-white rounded-full',
|
|
'transition-all duration-200',
|
|
'hover:scale-110 active:scale-95',
|
|
'focus:outline-none focus:ring-2 focus:ring-white/50'
|
|
)}
|
|
aria-label="Next slide"
|
|
>
|
|
<ChevronRight className="w-5 h-5 md:w-6 md:h-6" />
|
|
</button>
|
|
</>
|
|
)}
|
|
|
|
{/* Indicators */}
|
|
{showIndicators && slides.length > 1 && (
|
|
<div className={cn(
|
|
'absolute bottom-4 left-1/2 -translate-x-1/2',
|
|
'z-20 flex gap-2',
|
|
'bg-black/20 backdrop-blur-sm px-3 py-2 rounded-full'
|
|
)}>
|
|
{slides.map((_, index) => (
|
|
<button
|
|
key={index}
|
|
onClick={() => goToSlide(index)}
|
|
className={cn(
|
|
'w-2 h-2 md:w-3 md:h-3 rounded-full',
|
|
'transition-all duration-200',
|
|
currentIndex === index
|
|
? 'bg-white scale-125'
|
|
: 'bg-white/40 hover:bg-white/60 hover:scale-110'
|
|
)}
|
|
aria-label={`Go to slide ${index + 1}`}
|
|
aria-current={currentIndex === index}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Slide Counter (optional, for accessibility) */}
|
|
<div className={cn(
|
|
'absolute top-4 right-4',
|
|
'z-20 px-3 py-1',
|
|
'bg-black/30 backdrop-blur-sm',
|
|
'text-white text-sm font-medium rounded-full'
|
|
)}>
|
|
{currentIndex + 1} / {slides.length}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// Helper function to convert WPBakery slider HTML to Slide array
|
|
export function parseWpSlider(content: string): Slide[] {
|
|
// This would parse nectar_slider or similar WPBakery slider patterns
|
|
// For now, returns empty array - can be enhanced based on actual WP content
|
|
return [];
|
|
}
|
|
|
|
export default Slider; |