wip
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
import React from 'react';
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Container } from '../ui/Container';
|
||||
import { Button } from '../ui/Button';
|
||||
|
||||
// Hero height options
|
||||
type HeroHeight = 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
||||
type HeroHeight = 'sm' | 'md' | 'lg' | 'xl' | 'full' | 'screen';
|
||||
|
||||
// Hero variant options
|
||||
type HeroVariant = 'default' | 'dark' | 'primary' | 'gradient';
|
||||
@@ -24,24 +26,51 @@ interface HeroProps {
|
||||
overlayOpacity?: number;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
// Additional props for background color and overlay
|
||||
backgroundColor?: string;
|
||||
colorOverlay?: string;
|
||||
overlayStrength?: number;
|
||||
// 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';
|
||||
// Video background props
|
||||
videoBg?: string;
|
||||
videoMp4?: string;
|
||||
videoWebm?: string;
|
||||
}
|
||||
|
||||
// Helper function to get height styles
|
||||
const getHeightStyles = (height: HeroHeight) => {
|
||||
switch (height) {
|
||||
case 'sm':
|
||||
return 'min-h-[300px] md:min-h-[400px]';
|
||||
case 'md':
|
||||
return 'min-h-[400px] md:min-h-[500px]';
|
||||
case 'lg':
|
||||
return 'min-h-[500px] md:min-h-[600px]';
|
||||
case 'xl':
|
||||
return 'min-h-[600px] md:min-h-[700px]';
|
||||
case 'full':
|
||||
return 'min-h-screen';
|
||||
default:
|
||||
return 'min-h-[500px] md:min-h-[600px]';
|
||||
const getHeightStyles = (height: HeroHeight, fullScreenRowPosition?: string) => {
|
||||
const baseHeight = {
|
||||
sm: 'min-h-[300px] md:min-h-[400px]',
|
||||
md: 'min-h-[400px] md:min-h-[500px]',
|
||||
lg: 'min-h-[500px] md:min-h-[600px]',
|
||||
xl: 'min-h-[600px] md:min-h-[700px]',
|
||||
full: 'min-h-screen',
|
||||
screen: 'min-h-screen'
|
||||
}[height] || 'min-h-[500px] md:min-h-[600px]';
|
||||
|
||||
// Handle full screen positioning
|
||||
if (fullScreenRowPosition === 'middle') {
|
||||
return `${baseHeight} flex items-center justify-center`;
|
||||
} else if (fullScreenRowPosition === 'top') {
|
||||
return `${baseHeight} items-start justify-center pt-12`;
|
||||
} else if (fullScreenRowPosition === 'bottom') {
|
||||
return `${baseHeight} items-end justify-center pb-12`;
|
||||
}
|
||||
|
||||
return baseHeight;
|
||||
};
|
||||
|
||||
// Helper function to get variant styles
|
||||
@@ -80,57 +109,241 @@ export const Hero: React.FC<HeroProps> = ({
|
||||
overlayOpacity,
|
||||
children,
|
||||
className = '',
|
||||
backgroundColor,
|
||||
colorOverlay,
|
||||
overlayStrength,
|
||||
enableGradient = false,
|
||||
gradientDirection = 'left_to_right',
|
||||
colorOverlay2,
|
||||
parallaxBg = false,
|
||||
parallaxBgSpeed = 'medium',
|
||||
bgImageAnimation = 'none',
|
||||
topPadding,
|
||||
bottomPadding,
|
||||
textAlignment = 'center',
|
||||
textColor = 'light',
|
||||
shapeType,
|
||||
scenePosition = 'center',
|
||||
fullScreenRowPosition,
|
||||
videoBg,
|
||||
videoMp4,
|
||||
videoWebm,
|
||||
}) => {
|
||||
const hasBackground = !!backgroundImage;
|
||||
const hasCTA = !!ctaText && !!ctaLink;
|
||||
const hasColorOverlay = !!colorOverlay;
|
||||
const hasGradient = !!enableGradient;
|
||||
const hasParallax = !!parallaxBg;
|
||||
const hasVideo = !!(videoMp4?.trim()) || !!(videoWebm?.trim());
|
||||
const heroRef = useRef<HTMLElement>(null);
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
|
||||
// Calculate overlay opacity
|
||||
const overlayOpacityValue = overlayOpacity ?? (overlayStrength !== undefined ? overlayStrength : 0.5);
|
||||
|
||||
// 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';
|
||||
const subtitleTextColorClass = textColor === 'light' ? 'text-gray-100' : 'text-gray-600';
|
||||
|
||||
// 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,
|
||||
};
|
||||
|
||||
// Parallax effect handler
|
||||
useEffect(() => {
|
||||
if (!hasParallax || !heroRef.current) return;
|
||||
|
||||
const handleScroll = () => {
|
||||
if (!heroRef.current) return;
|
||||
|
||||
const rect = heroRef.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
|
||||
heroRef.current.style.setProperty('--parallax-offset', `${offset}px`);
|
||||
};
|
||||
|
||||
handleScroll(); // Initial call
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, [hasParallax]);
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={heroRef}
|
||||
className={cn(
|
||||
'relative w-full overflow-hidden flex items-center justify-center',
|
||||
getHeightStyles(height),
|
||||
'relative w-full overflow-hidden',
|
||||
getHeightStyles(height, fullScreenRowPosition),
|
||||
textAlignClass,
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: backgroundColor || undefined,
|
||||
...customPaddingStyle,
|
||||
}}
|
||||
>
|
||||
{/* Background Image */}
|
||||
{hasBackground && (
|
||||
{/* 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) */}
|
||||
{hasBackground && !hasVideo && (
|
||||
<div className={cn(
|
||||
'absolute inset-0 z-0',
|
||||
hasParallax && parallaxSpeedClass,
|
||||
bgAnimationClass
|
||||
)}>
|
||||
<Image
|
||||
src={backgroundImage}
|
||||
alt={backgroundAlt || title}
|
||||
fill
|
||||
priority
|
||||
className="object-cover"
|
||||
className={cn(
|
||||
'object-cover',
|
||||
hasParallax && 'transform-gpu'
|
||||
)}
|
||||
sizes="100vw"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Background Variant (if no image) */}
|
||||
{!hasBackground && (
|
||||
{!hasBackground && !backgroundColor && (
|
||||
<div className={cn(
|
||||
'absolute inset-0 z-0',
|
||||
getVariantStyles(variant)
|
||||
)} />
|
||||
)}
|
||||
|
||||
{/* Overlay */}
|
||||
{overlay && hasBackground && (
|
||||
{/* Gradient Overlay */}
|
||||
{hasGradient && (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute inset-0 z-10',
|
||||
gradientDirectionClass,
|
||||
'from-transparent via-transparent to-transparent'
|
||||
)}
|
||||
style={{
|
||||
opacity: overlayOpacityValue * 0.3,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Color Overlay (from WordPress color_overlay) */}
|
||||
{hasColorOverlay && (
|
||||
<div
|
||||
className="absolute inset-0 z-10"
|
||||
style={{
|
||||
backgroundColor: colorOverlay,
|
||||
opacity: overlayOpacityValue
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Second Color Overlay (for gradients) */}
|
||||
{colorOverlay2 && (
|
||||
<div
|
||||
className="absolute inset-0 z-10"
|
||||
style={{
|
||||
backgroundColor: colorOverlay2,
|
||||
opacity: overlayOpacityValue * 0.5
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Standard Overlay */}
|
||||
{overlay && hasBackground && !hasColorOverlay && (
|
||||
<div className={cn(
|
||||
'absolute inset-0 z-10',
|
||||
getOverlayOpacity(overlayOpacity)
|
||||
getOverlayOpacity(overlayOpacityValue)
|
||||
)} />
|
||||
)}
|
||||
|
||||
{/* 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="relative z-20 w-full">
|
||||
<Container
|
||||
maxWidth="6xl"
|
||||
padding="lg"
|
||||
padding="none"
|
||||
className={cn(
|
||||
'text-center',
|
||||
'px-4 sm:px-6 md:px-8',
|
||||
// Add padding for full-height heroes
|
||||
height === 'full' && 'py-12 md:py-20'
|
||||
height === 'full' || height === 'screen' ? 'py-12 md:py-20' : 'py-8 md:py-12'
|
||||
)}
|
||||
>
|
||||
{/* Title */}
|
||||
@@ -139,8 +352,9 @@ export const Hero: React.FC<HeroProps> = ({
|
||||
'font-bold leading-tight mb-4',
|
||||
'text-3xl sm:text-4xl md:text-5xl lg:text-6xl',
|
||||
'tracking-tight',
|
||||
// Ensure text contrast
|
||||
hasBackground || variant !== 'default' ? 'text-white' : 'text-gray-900'
|
||||
textColorClass,
|
||||
// Enhanced contrast for overlays
|
||||
(hasBackground || hasColorOverlay || variant !== 'default') && 'drop-shadow-lg'
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
@@ -153,7 +367,8 @@ export const Hero: React.FC<HeroProps> = ({
|
||||
'text-lg sm:text-xl md:text-2xl',
|
||||
'mb-8 max-w-3xl mx-auto',
|
||||
'leading-relaxed',
|
||||
hasBackground || variant !== 'default' ? 'text-gray-100' : 'text-gray-600'
|
||||
subtitleTextColorClass,
|
||||
(hasBackground || hasColorOverlay || variant !== 'default') && 'drop-shadow-md'
|
||||
)}
|
||||
>
|
||||
{subtitle}
|
||||
|
||||
Reference in New Issue
Block a user