Files
klz-cables.com/components/content/Hero.tsx
2026-01-06 13:55:04 +01:00

438 lines
13 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';
import { Button } from '../ui/Button';
// Hero height options
type HeroHeight = 'sm' | 'md' | 'lg' | 'xl' | 'full' | 'screen';
// Hero variant options
type HeroVariant = 'default' | 'dark' | 'primary' | 'gradient';
interface HeroProps {
title: string;
subtitle?: string;
backgroundImage?: string;
backgroundAlt?: string;
height?: HeroHeight;
variant?: HeroVariant;
ctaText?: string;
ctaLink?: string;
ctaVariant?: 'primary' | 'secondary' | 'outline';
overlay?: boolean;
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, 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
const getVariantStyles = (variant: HeroVariant) => {
switch (variant) {
case 'dark':
return 'bg-gray-900 text-white';
case 'primary':
return 'bg-primary text-white';
case 'gradient':
return 'bg-gradient-to-br from-primary to-secondary text-white';
default:
return 'bg-gray-800 text-white';
}
};
// Helper function to get overlay opacity
const getOverlayOpacity = (opacity?: number) => {
if (opacity === undefined) return 'bg-black/50';
if (opacity >= 1) return 'bg-black';
if (opacity <= 0) return 'bg-transparent';
return `bg-black/${Math.round(opacity * 100)}`;
};
export const Hero: React.FC<HeroProps> = ({
title,
subtitle,
backgroundImage,
backgroundAlt = '',
height = 'md',
variant = 'default',
ctaText,
ctaLink,
ctaVariant = 'primary',
overlay = true,
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',
getHeightStyles(height, fullScreenRowPosition),
textAlignClass,
className
)}
style={{
backgroundColor: backgroundColor || undefined,
...customPaddingStyle,
}}
>
{/* 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={cn(
'object-cover',
hasParallax && 'transform-gpu'
)}
sizes="100vw"
/>
</div>
)}
{/* Background Variant (if no image) */}
{!hasBackground && !backgroundColor && (
<div className={cn(
'absolute inset-0 z-0',
getVariantStyles(variant)
)} />
)}
{/* 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(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="none"
className={cn(
'px-4 sm:px-6 md:px-8',
// Add padding for full-height heroes
height === 'full' || height === 'screen' ? 'py-12 md:py-20' : 'py-8 md:py-12'
)}
>
{/* Title */}
<h1
className={cn(
'font-bold leading-tight mb-4',
'text-3xl sm:text-4xl md:text-5xl lg:text-6xl',
'tracking-tight',
textColorClass,
// Enhanced contrast for overlays
(hasBackground || hasColorOverlay || variant !== 'default') && 'drop-shadow-lg'
)}
>
{title}
</h1>
{/* Subtitle */}
{subtitle && (
<p
className={cn(
'text-lg sm:text-xl md:text-2xl',
'mb-8 max-w-3xl mx-auto',
'leading-relaxed',
subtitleTextColorClass,
(hasBackground || hasColorOverlay || variant !== 'default') && 'drop-shadow-md'
)}
>
{subtitle}
</p>
)}
{/* CTA Button */}
{hasCTA && (
<div className="flex justify-center">
<Button
variant={ctaVariant}
size="lg"
onClick={() => {
if (ctaLink) {
// Handle both internal and external links
if (ctaLink.startsWith('http')) {
window.open(ctaLink, '_blank');
} else {
// For Next.js routing, you'd use the router
// This is a fallback for external links
window.location.href = ctaLink;
}
}
}}
className="animate-fade-in-up"
>
{ctaText}
</Button>
</div>
)}
{/* Additional Content */}
{children && (
<div className="mt-8">
{children}
</div>
)}
</Container>
</div>
</section>
);
};
// Sub-components for more complex hero layouts
export const HeroContent: React.FC<{
title: string;
subtitle?: string;
children?: React.ReactNode;
className?: string;
}> = ({ title, subtitle, children, className = '' }) => (
<div className={cn('space-y-4 md:space-y-6', className)}>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold">{title}</h2>
{subtitle && <p className="text-lg md:text-xl text-gray-200">{subtitle}</p>}
{children}
</div>
);
export const HeroActions: React.FC<{
children: React.ReactNode;
className?: string;
}> = ({ children, className = '' }) => (
<div className={cn('flex flex-wrap gap-3 justify-center', className)}>
{children}
</div>
);
export default Hero;