Untitled
unknown
plain_text
10 months ago
17 kB
6
Indexable
"use client"
import { motion } from "framer-motion"
import type { ClientConfig } from "@/lib/client-config"
import { getR2ImageUrl } from "@/lib/r2"
import { ChevronDown, Heart, MousePointer, ArrowDown } from "lucide-react"
interface PortraitGalleryHeroProps {
config: ClientConfig
}
export function PortraitGalleryHero({ config }: PortraitGalleryHeroProps) {
const heroConfig = config.welcomeHero || {}
const images = heroConfig.images || [config.highlightImage]
// Limit to 3 images maximum
const displayImages = images.slice(0, 3)
// Determine layout based on number of images
const layoutClass =
displayImages.length === 1
? "grid-cols-1"
: displayImages.length === 2
? "grid-cols-1 md:grid-cols-2"
: "grid-cols-1 md:grid-cols-3"
// Visual settings
const overlayOpacity = heroConfig.overlayOpacity ?? 0.3
const overlayColor = heroConfig.overlayColor || "black"
const overlayGradient = heroConfig.overlayGradient || "gradient-to-b from-black/40 via-transparent to-black/70"
const textColor = heroConfig.textColor === "dark" ? "text-gray-800" : "text-white"
// Animation settings
const animationStyle = heroConfig.animationStyle || "fade"
const animationSpeed = heroConfig.animationSpeed || "medium"
const animationDuration = animationSpeed === "slow" ? 1.5 : animationSpeed === "fast" ? 0.6 : 1
const animationDelay = animationSpeed === "slow" ? 0.3 : animationSpeed === "fast" ? 0.1 : 0.2
// Typography settings
const titleStyle = heroConfig.titleStyle || "elegant"
const titleSize = heroConfig.titleSize || "medium"
const titleFont = heroConfig.titleFont || ""
const subtitleFont = heroConfig.subtitleFont || ""
// Content layout settings
const contentAlignment = heroConfig.contentAlignment || "center"
const contentPosition = heroConfig.contentPosition || "center"
const contentWidth = heroConfig.contentWidth || "medium"
// Create dynamic classes based on settings
const titleStyleClasses = {
classic: "font-medium",
elegant: "font-light tracking-wide",
minimal: "font-thin tracking-wider",
script: "font-serif italic",
bold: "font-bold",
outlined: "text-transparent bg-clip-text bg-gradient-to-r from-white to-gray-100 [-webkit-text-stroke:1px_rgba(255,255,255,0.7)]"
}[titleStyle]
const titleSizeClasses = {
small: "text-2xl sm:text-3xl md:text-4xl lg:text-5xl",
medium: "text-3xl sm:text-4xl md:text-5xl lg:text-6xl",
large: "text-4xl sm:text-5xl md:text-6xl lg:text-7xl",
xlarge: "text-5xl sm:text-6xl md:text-7xl lg:text-8xl"
}[titleSize]
const contentWidthClasses = {
narrow: "max-w-xl",
medium: "max-w-3xl",
wide: "max-w-5xl",
full: "max-w-full px-6"
}[contentWidth]
const contentAlignmentClasses = {
center: "items-center text-center",
left: "items-start text-left",
right: "items-end text-right"
}[contentAlignment]
const contentPositionClasses = {
center: "justify-center",
top: "justify-start pt-24",
bottom: "justify-end pb-24"
}[contentPosition]
// Scroll indicator style
const scrollIndicatorStyle = heroConfig.scrollIndicatorStyle || "arrow"
// Logo settings
const showLogo = heroConfig.showLogo ?? false
const logoUrl = heroConfig.logoUrl || ""
const logoPosition = heroConfig.logoPosition || "center"
const logoSize = heroConfig.logoSize || "medium"
const logoSizeClasses = {
small: "h-10 w-auto",
medium: "h-16 w-auto",
large: "h-24 w-auto"
}[logoSize]
const logoPositionClasses = {
center: "mx-auto mb-8",
"top-left": "absolute top-8 left-8 z-30",
"top-right": "absolute top-8 right-8 z-30",
"bottom-left": "absolute bottom-8 left-8 z-30",
"bottom-right": "absolute bottom-8 right-8 z-30"
}[logoPosition]
// Photographer info settings
const showPhotographerInfo = heroConfig.showPhotographerInfo ?? true
const photographerInfoPosition = heroConfig.photographerInfoPosition || "bottom"
const photographerInfoStyle = heroConfig.photographerInfoStyle || "minimal"
const photographerPositionClasses = {
bottom: "bottom-8 left-0 right-0 text-center",
top: "top-8 left-0 right-0 text-center",
side: "bottom-8 right-8 text-right",
overlay: "bottom-1/4 left-0 right-0 text-center"
}[photographerInfoPosition]
// Get decorative elements configuration
const decorativeElements = heroConfig.decorativeElements || {}
const showCorners = decorativeElements.corners !== false
const showBorders = decorativeElements.borders !== false
const showDividers = decorativeElements.dividers ?? true
const showFloral = decorativeElements.floral ?? false
const backgroundPattern = decorativeElements.backgroundPattern || "/decorative/floral-pattern.png"
// Animation variants based on style
const imageAnimationVariants = {
fade: {
initial: { opacity: 0, scale: 1.05 },
animate: { opacity: 1, scale: 1 }
},
slide: {
initial: { opacity: 0, x: -50 },
animate: { opacity: 1, x: 0 }
},
reveal: {
initial: { opacity: 1, clipPath: "polygon(0 0, 0 0, 0 100%, 0% 100%)" },
animate: { opacity: 1, clipPath: "polygon(0 0, 100% 0, 100% 100%, 0 100%)" }
},
parallax: {
initial: { opacity: 0, y: 50 },
animate: { opacity: 1, y: 0 }
},
none: {
initial: { opacity: 1 },
animate: { opacity: 1 }
}
}[animationStyle]
// Choose animation for title based on style
const titleAnimationVariants = {
fade: {
initial: { opacity: 0, y: 20 },
animate: { opacity: 1, y: 0 }
},
slide: {
initial: { opacity: 0, x: -30 },
animate: { opacity: 1, x: 0 }
},
reveal: {
initial: { opacity: 0, y: 50 },
animate: { opacity: 1, y: 0 }
},
typing: {
initial: { width: "0%" },
animate: { width: "100%" }
},
none: {
initial: { opacity: 1 },
animate: { opacity: 1 }
}
}[animationStyle === "typing" ? "typing" : animationStyle]
return (
<div className="relative h-screen w-full overflow-hidden bg-black">
{/* Decorative background pattern */}
{backgroundPattern && (
<div
className="absolute inset-0 bg-repeat opacity-5"
style={{ backgroundImage: `url('${backgroundPattern}')` }}
></div>
)}
{/* Image Grid */}
<div className={`grid ${layoutClass} h-full w-full`}>
{displayImages.map((image, index) => {
const imageUrl = image.startsWith("http")
? image
: getR2ImageUrl(config.id, image.split("/").pop() || "")
return (
<div key={index} className="relative h-full w-full overflow-hidden">
<motion.div
initial={imageAnimationVariants.initial}
animate={imageAnimationVariants.animate}
transition={{
duration: animationDuration,
delay: index * animationDelay
}}
className="absolute inset-0"
>
<img
src={imageUrl || "/placeholder.svg"}
alt={`Wedding portrait ${index + 1}`}
className="h-full w-full object-cover object-center"
/>
<div
className={`absolute inset-0 bg-${overlayColor} bg-${overlayGradient}`}
style={{ opacity: overlayOpacity }}
/>
</motion.div>
</div>
)
})}
</div>
{/* Decorative frame - only show if borders are enabled */}
{showBorders && (
<motion.div
className="absolute inset-4 border border-white/10 pointer-events-none z-10"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: animationDuration, delay: animationDelay }}
/>
)}
{/* Logo - show only if configured */}
{showLogo && logoUrl && (
<motion.div
className={`z-30 ${logoPositionClasses}`}
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: animationDuration, delay: animationDelay }}
>
<img
src={logoUrl}
alt="Logo"
className={logoSizeClasses}
/>
</motion.div>
)}
{/* Content overlay */}
<div className={`absolute inset-0 flex flex-col ${contentAlignmentClasses} ${contentPositionClasses} z-20 px-4`}>
<div className={contentWidthClasses}>
{/* Decorative heart element */}
{showFloral && (
<motion.div
className="mb-6 flex justify-center"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: animationDuration, delay: animationDelay * 3 }}
>
<Heart className="h-8 w-8 text-rose-300/80" />
</motion.div>
)}
{/* Title with animation based on selected style */}
{animationStyle === "typing" ? (
<div className="overflow-hidden">
<motion.h1
className={`${textColor} ${titleSizeClasses} ${titleStyleClasses}`}
initial={titleAnimationVariants.initial}
animate={titleAnimationVariants.animate}
transition={{
duration: animationDuration * 2,
delay: animationDelay * 4,
ease: "easeInOut"
}}
style={{ fontFamily: titleFont, whiteSpace: "nowrap", overflow: "hidden" }}
>
{config.title}
</motion.h1>
</div>
) : (
<motion.h1
className={`${textColor} ${titleSizeClasses} ${titleStyleClasses} mb-6`}
initial={titleAnimationVariants.initial}
animate={titleAnimationVariants.animate}
transition={{
duration: animationDuration,
delay: animationDelay * 4
}}
style={{ fontFamily: titleFont }}
>
{config.title}
</motion.h1>
)}
{/* Elegant divider - show only if dividers are enabled */}
{showDividers && (
<motion.div
className="my-6 flex items-center justify-center gap-4"
initial={{ opacity: 0, scaleX: 0 }}
animate={{ opacity: 1, scaleX: 1 }}
transition={{ duration: animationDuration, delay: animationDelay * 5 }}
>
<div className="h-px w-16 bg-white/60"></div>
<div className="w-2 h-2 rounded-full bg-rose-300/80"></div>
<div className="h-px w-16 bg-white/60"></div>
</motion.div>
)}
{/* Subtitle */}
{heroConfig.subtitle && (
<motion.p
className={`${textColor} max-w-xl mx-auto text-lg sm:text-xl tracking-wide`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: animationDuration, delay: animationDelay * 6 }}
style={{ fontFamily: subtitleFont }}
>
{heroConfig.subtitle}
</motion.p>
)}
{/* Date */}
<motion.div
className={`${textColor} mt-8 text-lg sm:text-xl tracking-widest`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: animationDuration, delay: animationDelay * 7 }}
>
{config.date}
</motion.div>
</div>
</div>
{/* Photographer credit - based on position and style */}
{showPhotographerInfo && (
<motion.div
className={`absolute ${photographerPositionClasses} z-20`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: animationDuration, delay: animationDelay * 8 }}
>
{photographerInfoStyle === "signature" ? (
<div className="inline-flex items-center px-3 py-1.5 bg-black/20 backdrop-blur-sm rounded-full">
{logoUrl && (
<img src={logoUrl} alt="Logo" className="w-4 h-4 mr-2" />
)}
<span className="text-white text-xs font-medium tracking-wide">
{config.photographerName || config.photographerCredit}
</span>
</div>
) : photographerInfoStyle === "detailed" ? (
<div className="flex items-center justify-center gap-2">
<div className="px-4 py-2 bg-black/30 backdrop-blur-sm rounded-md">
<div className="flex items-center gap-2">
{logoUrl && (
<img src={logoUrl} alt="Logo" className="w-5 h-5" />
)}
<div>
<p className="text-white text-xs font-medium">{config.photographerName || ""}</p>
<p className="text-white/80 text-xs">{config.photographerCredit}</p>
</div>
</div>
</div>
</div>
) : (
// Default minimal style
<p className="text-white/70 text-xs tracking-widest">{config.photographerCredit}</p>
)}
</motion.div>
)}
{/* Scroll Indicator with style variants */}
{(heroConfig.scrollIndicator ?? true) && (
<motion.div
className="absolute bottom-20 left-0 right-0 flex justify-center z-20"
initial={{ opacity: 0, y: -10 }}
animate={{
opacity: 1,
y: 0,
transition: {
duration: animationDuration * 0.8,
delay: animationDelay * 9,
repeat: Number.POSITIVE_INFINITY,
repeatType: "reverse",
repeatDelay: 0.5,
},
}}
>
{scrollIndicatorStyle === "dot" ? (
<div className="flex flex-col items-center gap-1">
<div className={`h-2 w-2 rounded-full ${textColor} opacity-80`}></div>
<div className={`h-2 w-2 rounded-full ${textColor} opacity-60`}></div>
<div className={`h-2 w-2 rounded-full ${textColor} opacity-40`}></div>
</div>
) : scrollIndicatorStyle === "icon" ? (
<div className="p-2 border border-white/20 rounded-full">
<MousePointer className="h-4 w-4 text-white/70" />
</div>
) : scrollIndicatorStyle === "text" ? (
<p className="text-sm uppercase tracking-widest text-white/70">Scroll</p>
) : (
// Default arrow style
<div className="p-2 border border-white/20 rounded-full">
<ChevronDown className="h-5 w-5 text-white/70" />
</div>
)}
</motion.div>
)}
{/* Corner decorations - only show if corners are enabled */}
{showCorners && (
<div className="absolute inset-0 pointer-events-none z-10">
<motion.img
src="/decorative/floral-corner-1.png"
alt="Decorative corner"
className="absolute top-8 left-8 w-16 h-16 opacity-40"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 0.4, scale: 1 }}
transition={{ duration: animationDuration, delay: animationDelay * 6 }}
/>
<motion.img
src="/decorative/floral-corner-1.png"
alt="Decorative corner"
className="absolute top-8 right-8 w-16 h-16 opacity-40 transform -scale-x-100"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 0.4, scale: 1 }}
transition={{ duration: animationDuration, delay: animationDelay * 7 }}
/>
<motion.img
src="/decorative/floral-corner-1.png"
alt="Decorative corner"
className="absolute bottom-8 left-8 w-16 h-16 opacity-40 transform -scale-y-100"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 0.4, scale: 1 }}
transition={{ duration: animationDuration, delay: animationDelay * 8 }}
/>
<motion.img
src="/decorative/floral-corner-1.png"
alt="Decorative corner"
className="absolute bottom-8 right-8 w-16 h-16 opacity-40 transform -scale-x-100 -scale-y-100"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 0.4, scale: 1 }}
transition={{ duration: animationDuration, delay: animationDelay * 9 }}
/>
</div>
)}
</div>
)
}Editor is loading...
Leave a Comment