530 lines
14 KiB
TypeScript
530 lines
14 KiB
TypeScript
'use client';
|
|
|
|
import React, { useEffect, useRef } from 'react';
|
|
import Image from 'next/image';
|
|
import { cn } from '../../lib/utils';
|
|
import { Container } from '../ui/Container';
|
|
|
|
// Section background options
|
|
type SectionBackground = 'default' | 'light' | 'dark' | 'primary' | 'secondary' | 'gradient';
|
|
|
|
// Section padding options
|
|
type SectionPadding = 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
|
|
|
|
interface SectionProps {
|
|
children: React.ReactNode;
|
|
background?: SectionBackground;
|
|
padding?: SectionPadding;
|
|
fullWidth?: boolean;
|
|
className?: string;
|
|
id?: string;
|
|
as?: React.ElementType;
|
|
// Additional props for background images and overlays
|
|
backgroundImage?: string;
|
|
backgroundAlt?: string;
|
|
colorOverlay?: string;
|
|
overlayOpacity?: number;
|
|
backgroundColor?: string;
|
|
// WordPress Salient-specific props
|
|
enableGradient?: boolean;
|
|
gradientDirection?: 'left_to_right' | 'right_to_left' | 'top_to_bottom' | 'bottom_to_top';
|
|
colorOverlay2?: string;
|
|
parallaxBg?: boolean;
|
|
parallaxBgSpeed?: 'slow' | 'fast' | 'medium';
|
|
bgImageAnimation?: 'none' | 'zoom-out-reveal' | 'fade-in';
|
|
topPadding?: string;
|
|
bottomPadding?: string;
|
|
textAlignment?: 'left' | 'center' | 'right';
|
|
textColor?: 'light' | 'dark';
|
|
shapeType?: string;
|
|
scenePosition?: 'center' | 'top' | 'bottom';
|
|
fullScreenRowPosition?: 'middle' | 'top' | 'bottom';
|
|
// Additional styling
|
|
borderRadius?: string;
|
|
boxShadow?: boolean;
|
|
// Video background props
|
|
videoBg?: string;
|
|
videoMp4?: string;
|
|
videoWebm?: string;
|
|
}
|
|
|
|
// Helper function to get background styles
|
|
const getBackgroundStyles = (background: SectionBackground) => {
|
|
switch (background) {
|
|
case 'light':
|
|
return 'bg-gray-50';
|
|
case 'dark':
|
|
return 'bg-gray-900 text-white';
|
|
case 'primary':
|
|
return 'bg-primary text-white';
|
|
case 'secondary':
|
|
return 'bg-secondary text-white';
|
|
case 'gradient':
|
|
return 'bg-gradient-to-br from-primary to-secondary text-white';
|
|
default:
|
|
return 'bg-white';
|
|
}
|
|
};
|
|
|
|
// Helper function to get padding styles
|
|
const getPaddingStyles = (padding: SectionPadding) => {
|
|
switch (padding) {
|
|
case 'none':
|
|
return 'py-0';
|
|
case 'sm':
|
|
return 'py-4 sm:py-6';
|
|
case 'md':
|
|
return 'py-8 sm:py-12';
|
|
case 'lg':
|
|
return 'py-12 sm:py-16';
|
|
case 'xl':
|
|
return 'py-16 sm:py-20 md:py-24';
|
|
case '2xl':
|
|
return 'py-20 sm:py-24 md:py-32';
|
|
default:
|
|
return 'py-12 sm:py-16';
|
|
}
|
|
};
|
|
|
|
export const Section: React.FC<SectionProps> = ({
|
|
children,
|
|
background = 'default',
|
|
padding = 'md',
|
|
fullWidth = false,
|
|
className = '',
|
|
id,
|
|
as: Component = 'section',
|
|
backgroundImage,
|
|
backgroundAlt = '',
|
|
colorOverlay,
|
|
overlayOpacity = 0.5,
|
|
backgroundColor,
|
|
enableGradient = false,
|
|
gradientDirection = 'left_to_right',
|
|
colorOverlay2,
|
|
parallaxBg = false,
|
|
parallaxBgSpeed = 'medium',
|
|
bgImageAnimation = 'none',
|
|
topPadding,
|
|
bottomPadding,
|
|
textAlignment = 'left',
|
|
textColor = 'dark',
|
|
shapeType,
|
|
scenePosition = 'center',
|
|
fullScreenRowPosition,
|
|
borderRadius,
|
|
boxShadow = false,
|
|
videoBg,
|
|
videoMp4,
|
|
videoWebm,
|
|
}) => {
|
|
const hasBackgroundImage = !!backgroundImage;
|
|
const hasColorOverlay = !!colorOverlay;
|
|
const hasCustomBg = !!backgroundColor;
|
|
const hasGradient = !!enableGradient;
|
|
const hasParallax = !!parallaxBg;
|
|
const hasVideo = !!(videoMp4?.trim()) || !!(videoWebm?.trim());
|
|
const sectionRef = useRef<HTMLDivElement>(null);
|
|
const videoRef = useRef<HTMLVideoElement>(null);
|
|
|
|
// Get text alignment
|
|
const textAlignClass = {
|
|
left: 'text-left',
|
|
center: 'text-center',
|
|
right: 'text-right',
|
|
}[textAlignment];
|
|
|
|
// Get text color
|
|
const textColorClass = textColor === 'light' ? 'text-white' : 'text-gray-900';
|
|
|
|
// Get gradient direction
|
|
const gradientDirectionClass = {
|
|
'left_to_right': 'bg-gradient-to-r',
|
|
'right_to_left': 'bg-gradient-to-l',
|
|
'top_to_bottom': 'bg-gradient-to-b',
|
|
'bottom_to_top': 'bg-gradient-to-t',
|
|
}[gradientDirection];
|
|
|
|
// Get parallax speed
|
|
const parallaxSpeedClass = {
|
|
slow: 'parallax-slow',
|
|
medium: 'parallax-medium',
|
|
fast: 'parallax-fast',
|
|
}[parallaxBgSpeed];
|
|
|
|
// Get background animation
|
|
const bgAnimationClass = {
|
|
none: '',
|
|
'zoom-out-reveal': 'animate-zoom-out',
|
|
'fade-in': 'animate-fade-in',
|
|
}[bgImageAnimation];
|
|
|
|
// Calculate padding from props
|
|
const customPaddingStyle = {
|
|
paddingTop: topPadding || undefined,
|
|
paddingBottom: bottomPadding || undefined,
|
|
};
|
|
|
|
// Base classes
|
|
const baseClasses = cn(
|
|
'w-full relative overflow-hidden',
|
|
getPaddingStyles(padding),
|
|
textAlignClass,
|
|
textColorClass,
|
|
boxShadow && 'shadow-xl',
|
|
borderRadius && `rounded-${borderRadius}`,
|
|
className
|
|
);
|
|
|
|
// Background style (for solid colors)
|
|
const backgroundStyle = hasCustomBg ? { backgroundColor, ...customPaddingStyle } : customPaddingStyle;
|
|
|
|
// Content wrapper classes
|
|
const contentWrapperClasses = cn(
|
|
'relative z-20 w-full',
|
|
!fullWidth && 'container mx-auto px-4 md:px-6'
|
|
);
|
|
|
|
// Parallax effect handler
|
|
useEffect(() => {
|
|
if (!hasParallax || !sectionRef.current) return;
|
|
|
|
const handleScroll = () => {
|
|
if (!sectionRef.current) return;
|
|
|
|
const rect = sectionRef.current.getBoundingClientRect();
|
|
const viewportHeight = window.innerHeight;
|
|
|
|
// Calculate offset based on scroll position
|
|
const scrollProgress = (viewportHeight - rect.top) / (viewportHeight + rect.height);
|
|
const offset = scrollProgress * 50; // Max 50px offset
|
|
|
|
// Apply to CSS variable
|
|
sectionRef.current.style.setProperty('--parallax-offset', `${offset}px`);
|
|
};
|
|
|
|
handleScroll(); // Initial call
|
|
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
|
|
return () => window.removeEventListener('scroll', handleScroll);
|
|
}, [hasParallax]);
|
|
|
|
const content = (
|
|
<div ref={sectionRef} className={baseClasses} id={id} style={backgroundStyle}>
|
|
{/* Video Background */}
|
|
{hasVideo && (
|
|
<div className="absolute inset-0 z-0">
|
|
<video
|
|
ref={videoRef}
|
|
autoPlay
|
|
muted
|
|
loop
|
|
playsInline
|
|
className="absolute inset-0 w-full h-full object-cover"
|
|
style={{ opacity: 1 }}
|
|
>
|
|
{videoWebm && <source src={videoWebm} type="video/webm" />}
|
|
{videoMp4 && <source src={videoMp4} type="video/mp4" />}
|
|
</video>
|
|
</div>
|
|
)}
|
|
|
|
{/* Background Image with Parallax (fallback if no video) */}
|
|
{hasBackgroundImage && !hasVideo && (
|
|
<div className={cn(
|
|
'absolute inset-0 z-0',
|
|
hasParallax && parallaxSpeedClass,
|
|
bgAnimationClass
|
|
)}>
|
|
<Image
|
|
src={backgroundImage}
|
|
alt={backgroundAlt || ''}
|
|
fill
|
|
className={cn(
|
|
'object-cover',
|
|
hasParallax && 'transform-gpu'
|
|
)}
|
|
sizes="100vw"
|
|
priority={false}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Background Variant (if no image) */}
|
|
{!hasBackgroundImage && !hasCustomBg && (
|
|
<div className={cn(
|
|
'absolute inset-0 z-0',
|
|
getBackgroundStyles(background)
|
|
)} />
|
|
)}
|
|
|
|
{/* Gradient Overlay */}
|
|
{hasGradient && (
|
|
<div
|
|
className={cn(
|
|
'absolute inset-0 z-10',
|
|
gradientDirectionClass,
|
|
'from-transparent via-transparent to-transparent'
|
|
)}
|
|
style={{
|
|
opacity: overlayOpacity * 0.3,
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{/* Color Overlay (from WordPress color_overlay) */}
|
|
{hasColorOverlay && (
|
|
<div
|
|
className="absolute inset-0 z-10"
|
|
style={{
|
|
backgroundColor: colorOverlay,
|
|
opacity: overlayOpacity
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{/* Second Color Overlay (for gradients) */}
|
|
{colorOverlay2 && (
|
|
<div
|
|
className="absolute inset-0 z-10"
|
|
style={{
|
|
backgroundColor: colorOverlay2,
|
|
opacity: overlayOpacity * 0.5
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{/* Shape Divider (bottom) */}
|
|
{shapeType && (
|
|
<div className="absolute bottom-0 left-0 right-0 z-10">
|
|
<svg
|
|
className="w-full h-16 md:h-24 lg:h-32"
|
|
viewBox="0 0 1200 120"
|
|
preserveAspectRatio="none"
|
|
>
|
|
{shapeType === 'waves_opacity_alt' && (
|
|
<path
|
|
d="M0,0V46.29c47.79,22.2,103.59,32.17,158,28,70.36-5.37,136.33-33.31,206.8-37.5C438.64,32.43,512.34,53.67,583,72.05c69.27,18,138.3,24.88,209.4,13.08,36.15-6,69.85-17.8,104.45-29.34C989.49,25,1113-14.29,1200,52.47V0Z"
|
|
opacity=".25"
|
|
fill="#ffffff"
|
|
/>
|
|
)}
|
|
{shapeType === 'mountains' && (
|
|
<path
|
|
d="M0,0V60c100,0,150,20,200,20s100-20,200-20s150,20,200,20s100-20,200-20s150,20,200,20s100-20,200-20V0Z"
|
|
opacity=".25"
|
|
fill="#ffffff"
|
|
/>
|
|
)}
|
|
</svg>
|
|
</div>
|
|
)}
|
|
|
|
{/* Content */}
|
|
<div className={contentWrapperClasses}>
|
|
{fullWidth ? children : (
|
|
<Container maxWidth="6xl" padding="none">
|
|
{children}
|
|
</Container>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
if (Component !== 'section') {
|
|
return (
|
|
<Component className={baseClasses} id={id} style={backgroundStyle}>
|
|
{/* Video Background */}
|
|
{hasVideo && (
|
|
<div className="absolute inset-0 z-0">
|
|
<video
|
|
ref={videoRef}
|
|
autoPlay
|
|
muted
|
|
loop
|
|
playsInline
|
|
className="absolute inset-0 w-full h-full object-cover"
|
|
style={{ opacity: 1 }}
|
|
>
|
|
{videoWebm && <source src={videoWebm} type="video/webm" />}
|
|
{videoMp4 && <source src={videoMp4} type="video/mp4" />}
|
|
</video>
|
|
</div>
|
|
)}
|
|
|
|
{/* Background Image with Parallax (fallback if no video) */}
|
|
{hasBackgroundImage && !hasVideo && (
|
|
<div className={cn(
|
|
'absolute inset-0 z-0',
|
|
hasParallax && parallaxSpeedClass,
|
|
bgAnimationClass
|
|
)}>
|
|
<Image
|
|
src={backgroundImage}
|
|
alt={backgroundAlt || ''}
|
|
fill
|
|
className={cn(
|
|
'object-cover',
|
|
hasParallax && 'transform-gpu'
|
|
)}
|
|
sizes="100vw"
|
|
priority={false}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Background Variant (if no image) */}
|
|
{!hasBackgroundImage && !hasCustomBg && (
|
|
<div className={cn(
|
|
'absolute inset-0 z-0',
|
|
getBackgroundStyles(background)
|
|
)} />
|
|
)}
|
|
|
|
{/* Gradient Overlay */}
|
|
{hasGradient && (
|
|
<div
|
|
className={cn(
|
|
'absolute inset-0 z-10',
|
|
gradientDirectionClass,
|
|
'from-transparent via-transparent to-transparent'
|
|
)}
|
|
style={{
|
|
opacity: overlayOpacity * 0.3,
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{/* Color Overlay */}
|
|
{hasColorOverlay && (
|
|
<div
|
|
className="absolute inset-0 z-10"
|
|
style={{
|
|
backgroundColor: colorOverlay,
|
|
opacity: overlayOpacity
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{/* Second Color Overlay */}
|
|
{colorOverlay2 && (
|
|
<div
|
|
className="absolute inset-0 z-10"
|
|
style={{
|
|
backgroundColor: colorOverlay2,
|
|
opacity: overlayOpacity * 0.5
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{/* Shape Divider */}
|
|
{shapeType && (
|
|
<div className="absolute bottom-0 left-0 right-0 z-10">
|
|
<svg
|
|
className="w-full h-16 md:h-24 lg:h-32"
|
|
viewBox="0 0 1200 120"
|
|
preserveAspectRatio="none"
|
|
>
|
|
{shapeType === 'waves_opacity_alt' && (
|
|
<path
|
|
d="M0,0V46.29c47.79,22.2,103.59,32.17,158,28,70.36-5.37,136.33-33.31,206.8-37.5C438.64,32.43,512.34,53.67,583,72.05c69.27,18,138.3,24.88,209.4,13.08,36.15-6,69.85-17.8,104.45-29.34C989.49,25,1113-14.29,1200,52.47V0Z"
|
|
opacity=".25"
|
|
fill="#ffffff"
|
|
/>
|
|
)}
|
|
{shapeType === 'mountains' && (
|
|
<path
|
|
d="M0,0V60c100,0,150,20,200,20s100-20,200-20s150,20,200,20s100-20,200-20s150,20,200,20s100-20,200-20V0Z"
|
|
opacity=".25"
|
|
fill="#ffffff"
|
|
/>
|
|
)}
|
|
</svg>
|
|
</div>
|
|
)}
|
|
|
|
<div className={contentWrapperClasses}>
|
|
{fullWidth ? children : (
|
|
<Container maxWidth="6xl" padding="none">
|
|
{children}
|
|
</Container>
|
|
)}
|
|
</div>
|
|
</Component>
|
|
);
|
|
}
|
|
|
|
return content;
|
|
};
|
|
|
|
// Sub-components for common section patterns
|
|
export const SectionHeader: React.FC<{
|
|
title: string;
|
|
subtitle?: string;
|
|
align?: 'left' | 'center' | 'right';
|
|
className?: string;
|
|
}> = ({ title, subtitle, align = 'center', className = '' }) => {
|
|
const alignment = {
|
|
left: 'text-left',
|
|
center: 'text-center',
|
|
right: 'text-right',
|
|
}[align];
|
|
|
|
return (
|
|
<div className={cn('mb-8 md:mb-12', alignment, className)}>
|
|
<h2 className={cn(
|
|
'text-3xl md:text-4xl font-bold mb-3',
|
|
'leading-tight tracking-tight'
|
|
)}>
|
|
{title}
|
|
</h2>
|
|
{subtitle && (
|
|
<p className={cn(
|
|
'text-lg md:text-xl',
|
|
'max-w-3xl mx-auto',
|
|
'opacity-90'
|
|
)}>
|
|
{subtitle}
|
|
</p>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const SectionContent: React.FC<{
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
}> = ({ children, className = '' }) => (
|
|
<div className={cn('space-y-6 md:space-y-8', className)}>
|
|
{children}
|
|
</div>
|
|
);
|
|
|
|
export const SectionGrid: React.FC<{
|
|
children: React.ReactNode;
|
|
cols?: 1 | 2 | 3 | 4;
|
|
gap?: 'sm' | 'md' | 'lg' | 'xl';
|
|
className?: string;
|
|
}> = ({ children, cols = 3, gap = 'md', className = '' }) => {
|
|
const gapClasses = {
|
|
sm: 'gap-4 md:gap-6',
|
|
md: 'gap-6 md:gap-8',
|
|
lg: 'gap-8 md:gap-12',
|
|
xl: 'gap-10 md:gap-16',
|
|
}[gap];
|
|
|
|
const colClasses = {
|
|
1: 'grid-cols-1',
|
|
2: 'grid-cols-1 md:grid-cols-2',
|
|
3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
|
4: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4',
|
|
}[cols];
|
|
|
|
return (
|
|
<div className={cn('grid', colClasses, gapClasses, className)}>
|
|
{children}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Section; |