Merge version_1_1780757373420 into main #2
@@ -1,145 +1,107 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { AnimatePresence, motion, useScroll, useTransform } from "motion/react";
|
||||
import ImageOrVideo from "@/components/ui/ImageOrVideo";
|
||||
import AutoFillText from "@/components/ui/AutoFillText";
|
||||
import { useButtonClick } from "@/hooks/useButtonClick";
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { cn } from '@/utils/cn'; // Assuming this utility exists
|
||||
|
||||
const StaggerText = ({ text }: { text: string }) => (
|
||||
<span className="truncate overflow-hidden">
|
||||
{[...text].map((char, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="inline-block transition-transform duration-400 ease-out md:group-hover:-translate-y-[1.25em]"
|
||||
style={{ textShadow: "0 1.25em currentColor", transitionDelay: `${index * 0.01}s`, whiteSpace: char === " " ? "pre" : undefined }}
|
||||
>
|
||||
{char}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
);
|
||||
|
||||
type HeroVideoExpandProps = {
|
||||
interface HeroVideoExpandProps {
|
||||
title: string;
|
||||
primaryButton: { text: string; href: string };
|
||||
secondaryButton: { text: string; href: string };
|
||||
description: string;
|
||||
videoSrc: string;
|
||||
thumbnailSrc?: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
onComplete?: () => void;
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const HeroVideoExpand = ({
|
||||
title,
|
||||
description,
|
||||
videoSrc,
|
||||
imageSrc,
|
||||
thumbnailSrc,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
onComplete,
|
||||
className,
|
||||
}: HeroVideoExpandProps) => {
|
||||
const [showLoader, setShowLoader] = useState(true);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const handlePrimaryClick = useButtonClick(primaryButton.href);
|
||||
const handleSecondaryClick = useButtonClick(secondaryButton.href);
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [hasPlayedOnce, setHasPlayedOnce] = useState(false);
|
||||
|
||||
const sectionRef = useRef<HTMLElement>(null);
|
||||
const { scrollYProgress } = useScroll({
|
||||
target: sectionRef,
|
||||
offset: ["start start", "end start"],
|
||||
});
|
||||
const videoY = useTransform(scrollYProgress, [0, 1], ["0px", "150px"]);
|
||||
const videoScale = useTransform(scrollYProgress, [0, 1], [1, 1.1]);
|
||||
const handlePlayClick = () => {
|
||||
if (videoRef.current) {
|
||||
videoRef.current.play();
|
||||
setIsPlaying(true);
|
||||
setHasPlayedOnce(true);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const expandTimer = setTimeout(() => setExpanded(true), 600);
|
||||
const hideTimer = setTimeout(() => {
|
||||
setShowLoader(false);
|
||||
onComplete?.();
|
||||
}, 1500);
|
||||
return () => {
|
||||
clearTimeout(expandTimer);
|
||||
clearTimeout(hideTimer);
|
||||
};
|
||||
}, []);
|
||||
const video = videoRef.current;
|
||||
if (video) {
|
||||
const handleEnded = () => {
|
||||
setIsPlaying(false);
|
||||
if (onComplete) {
|
||||
onComplete();
|
||||
}
|
||||
};
|
||||
video.addEventListener('ended', handleEnded);
|
||||
return () => {
|
||||
video.removeEventListener('ended', handleEnded);
|
||||
};
|
||||
}
|
||||
}, [onComplete]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AnimatePresence>
|
||||
{showLoader && (
|
||||
<motion.div
|
||||
key="loader"
|
||||
className="fixed inset-0 z-100 bg-background"
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<motion.div
|
||||
className="absolute inset-0"
|
||||
initial={{ opacity: 0, clipPath: "inset(25% 20% 25% 20% round 24px)" }}
|
||||
animate={
|
||||
expanded
|
||||
? { opacity: 1, clipPath: "inset(0% 0% 0% 0% round 0px)" }
|
||||
: { opacity: 1, clipPath: "inset(25% 20% 25% 20% round 24px)" }
|
||||
}
|
||||
transition={{ duration: expanded ? 1.4 : 1.2, ease: [0.76, 0, 0.24, 1] }}
|
||||
>
|
||||
<ImageOrVideo imageSrc={imageSrc} videoSrc={videoSrc} className="rounded-none" />
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<section ref={sectionRef} aria-label="Hero section" className="relative w-full h-svh overflow-hidden mb-20">
|
||||
<motion.div className="absolute inset-0" style={{ y: videoY, scale: videoScale }}>
|
||||
<ImageOrVideo
|
||||
imageSrc={imageSrc}
|
||||
videoSrc={videoSrc}
|
||||
className="absolute inset-0 w-full h-full object-cover rounded-none"
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
<div className="absolute inset-0 bg-linear-to-t from-background/60 via-transparent to-background/0" />
|
||||
|
||||
<div className="absolute inset-0 z-10 flex flex-col justify-end pb-8 md:pb-12 xl:pb-16 2xl:pb-20">
|
||||
<div className="w-content-width mx-auto flex flex-col md:flex-row md:items-end md:justify-between gap-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={!showLoader ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
|
||||
transition={{ duration: 1.2, ease: "easeOut" }}
|
||||
className="w-full"
|
||||
>
|
||||
<AutoFillText className="font-medium text-white" paddingY="0">{title}</AutoFillText>
|
||||
</motion.div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={!showLoader ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
|
||||
transition={{ duration: 1.2, delay: 0.1, ease: "easeOut" }}
|
||||
className="w-1/2 md:w-auto"
|
||||
>
|
||||
<a
|
||||
href={primaryButton.href}
|
||||
onClick={handlePrimaryClick}
|
||||
className="group w-1/2 md:w-auto h-14 xl:h-16 2xl:h-18 px-8 xl:px-10 2xl:px-12 text-lg xl:text-xl font-medium text-nowrap inline-flex items-center justify-center rounded-2xl cursor-pointer primary-button text-primary-cta-text"
|
||||
>
|
||||
<StaggerText text={primaryButton.text} />
|
||||
</a>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={!showLoader ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
|
||||
transition={{ duration: 1.2, delay: 0.2, ease: "easeOut" }}
|
||||
className="w-1/2 md:w-auto"
|
||||
>
|
||||
<a
|
||||
href={secondaryButton.href}
|
||||
onClick={handleSecondaryClick}
|
||||
className="group w-1/2 md:w-auto h-14 xl:h-16 2xl:h-18 px-8 xl:px-10 2xl:px-12 text-lg xl:text-xl font-medium text-nowrap inline-flex items-center justify-center rounded-2xl cursor-pointer secondary-button text-secondary-cta-text"
|
||||
>
|
||||
<StaggerText text={secondaryButton.text} />
|
||||
</a>
|
||||
</motion.div>
|
||||
</div>
|
||||
<section className={cn('relative w-full h-screen flex items-center justify-center overflow-hidden', className)}>
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={videoSrc}
|
||||
poster={thumbnailSrc}
|
||||
loop={false}
|
||||
muted={false} // Adjust as needed
|
||||
className="absolute inset-0 w-full h-full object-cover transition-opacity duration-500"
|
||||
onPlay={() => setIsPlaying(true)}
|
||||
onPause={() => setIsPlaying(false)}
|
||||
onEnded={() => {
|
||||
setIsPlaying(false);
|
||||
// onComplete is handled by useEffect
|
||||
}}
|
||||
/>
|
||||
{!isPlaying && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="absolute inset-0 bg-black/60 flex flex-col items-center justify-center p-4 text-center z-10"
|
||||
>
|
||||
<h1 className="text-4xl md:text-6xl font-bold text-white mb-4">{title}</h1>
|
||||
<p className="text-lg md:text-xl text-white/80 max-w-2xl mb-8">{description}</p>
|
||||
<div className="flex gap-4">
|
||||
{primaryButton && (
|
||||
<a href={primaryButton.href} className="px-6 py-3 bg-primary-500 text-white rounded-md text-lg hover:bg-primary-600 transition-colors">
|
||||
{primaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<a href={secondaryButton.href} className="px-6 py-3 border border-white text-white rounded-md text-lg hover:bg-white/10 transition-colors">
|
||||
{secondaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
{!hasPlayedOnce && (
|
||||
<button
|
||||
onClick={handlePlayClick}
|
||||
className="mt-8 px-8 py-4 bg-white text-gray-900 rounded-full text-xl font-semibold flex items-center gap-2 hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
<svg className="h-6 w-6 fill-current" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 5V19L19 12L8 5Z" fill="currentColor"/>
|
||||
</svg>
|
||||
Play Video
|
||||
</button>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,21 +1,187 @@
|
||||
import { resolveIcon } from "./resolve-icon";
|
||||
import type { IconInput } from "./resolve-icon";
|
||||
import {
|
||||
Activity,
|
||||
ArrowRight,
|
||||
Calculator,
|
||||
Calendar,
|
||||
Camera,
|
||||
CheckCircle,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Circle,
|
||||
Cloud,
|
||||
Code,
|
||||
CreditCard,
|
||||
Database,
|
||||
Diamond,
|
||||
DollarSign,
|
||||
Droplet,
|
||||
Eye,
|
||||
FileText,
|
||||
Fingerprint,
|
||||
FlaskConical,
|
||||
Flower,
|
||||
Footprints,
|
||||
GalleryVertical,
|
||||
Gem,
|
||||
Gift,
|
||||
Globe,
|
||||
Hammer,
|
||||
HardHat,
|
||||
Heart,
|
||||
HelpCircle,
|
||||
Home,
|
||||
Image,
|
||||
Info,
|
||||
Key,
|
||||
Laptop,
|
||||
LayoutGrid,
|
||||
Leaf,
|
||||
Lightbulb,
|
||||
Link,
|
||||
Loader,
|
||||
Lock,
|
||||
LucideIcon,
|
||||
Mail,
|
||||
MapPin,
|
||||
Megaphone,
|
||||
Menu,
|
||||
MessageSquare,
|
||||
Mic,
|
||||
Minus,
|
||||
Monitor,
|
||||
Moon,
|
||||
Mountain,
|
||||
Package,
|
||||
Pencil,
|
||||
Phone,
|
||||
PieChart,
|
||||
Pin,
|
||||
Play,
|
||||
Plus,
|
||||
Rocket,
|
||||
Rss,
|
||||
Scissors,
|
||||
Search,
|
||||
Send,
|
||||
Settings,
|
||||
Shield,
|
||||
ShoppingCart,
|
||||
Sparkle,
|
||||
Sprout,
|
||||
Star,
|
||||
Sun,
|
||||
Tablet,
|
||||
Tag,
|
||||
Target,
|
||||
Terminal,
|
||||
ThumbsUp,
|
||||
TreeDeciduous,
|
||||
TrendingUp,
|
||||
Truck,
|
||||
User,
|
||||
Utensils,
|
||||
Video,
|
||||
Wallet,
|
||||
Wand,
|
||||
Wifi,
|
||||
X,
|
||||
Zap,
|
||||
} from 'lucide-react';
|
||||
|
||||
const DynamicIcon = ({
|
||||
icon,
|
||||
size,
|
||||
className,
|
||||
strokeWidth,
|
||||
}: {
|
||||
icon: IconInput;
|
||||
size?: number;
|
||||
className?: string;
|
||||
strokeWidth?: number;
|
||||
}) => {
|
||||
const Icon = resolveIcon(icon);
|
||||
return <Icon size={size} className={className} strokeWidth={strokeWidth} />;
|
||||
};
|
||||
|
||||
export default DynamicIcon;
|
||||
export { resolveIcon };
|
||||
export type { IconInput };
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export function resolveIcon(iconName: string | LucideIcon): LucideIcon {
|
||||
if (typeof iconName === 'string') {
|
||||
switch (iconName) {
|
||||
case 'Activity': return Activity;
|
||||
case 'ArrowRight': return ArrowRight;
|
||||
case 'Calculator': return Calculator;
|
||||
case 'Calendar': return Calendar;
|
||||
case 'Camera': return Camera;
|
||||
case 'CheckCircle': return CheckCircle;
|
||||
case 'ChevronDown': return ChevronDown;
|
||||
case 'ChevronRight': return ChevronRight;
|
||||
case 'Circle': return Circle;
|
||||
case 'Cloud': return Cloud;
|
||||
case 'Code': return Code;
|
||||
case 'CreditCard': return CreditCard;
|
||||
case 'Database': return Database;
|
||||
case 'Diamond': return Diamond;
|
||||
case 'DollarSign': return DollarSign;
|
||||
case 'Droplet': return Droplet;
|
||||
case 'Eye': return Eye;
|
||||
case 'FileText': return FileText;
|
||||
case 'Fingerprint': return Fingerprint;
|
||||
case 'FlaskConical': return FlaskConical;
|
||||
case 'Flower': return Flower;
|
||||
case 'Footprints': return Footprints;
|
||||
case 'GalleryVertical': return GalleryVertical;
|
||||
case 'Gem': return Gem;
|
||||
case 'Gift': return Gift;
|
||||
case 'Globe': return Globe;
|
||||
case 'Hammer': return Hammer;
|
||||
case 'HardHat': return HardHat;
|
||||
case 'Heart': return Heart;
|
||||
case 'HelpCircle': return HelpCircle;
|
||||
case 'Home': return Home;
|
||||
case 'Image': return Image;
|
||||
case 'Info': return Info;
|
||||
case 'Key': return Key;
|
||||
case 'Laptop': return Laptop;
|
||||
case 'LayoutGrid': return LayoutGrid;
|
||||
case 'Leaf': return Leaf;
|
||||
case 'Lightbulb': return Lightbulb;
|
||||
case 'Link': return Link;
|
||||
case 'Loader': return Loader;
|
||||
case 'Lock': return Lock;
|
||||
case 'Mail': return Mail;
|
||||
case 'MapPin': return MapPin;
|
||||
case 'Megaphone': return Megaphone;
|
||||
case 'Menu': return Menu;
|
||||
case 'MessageSquare': return MessageSquare;
|
||||
case 'Mic': return Mic;
|
||||
case 'Minus': return Minus;
|
||||
case 'Monitor': return Monitor;
|
||||
case 'Moon': return Moon;
|
||||
case 'Mountain': return Mountain;
|
||||
case 'Package': return Package;
|
||||
case 'Pencil': return Pencil;
|
||||
case 'Phone': return Phone;
|
||||
case 'PieChart': return PieChart;
|
||||
case 'Pin': return Pin;
|
||||
case 'Play': return Play;
|
||||
case 'Plus': return Plus;
|
||||
case 'Rocket': return Rocket;
|
||||
case 'Rss': return Rss;
|
||||
case 'Scissors': return Scissors;
|
||||
case 'Search': return Search;
|
||||
case 'Send': return Send;
|
||||
case 'Settings': return Settings;
|
||||
case 'Shield': return Shield;
|
||||
case 'ShoppingCart': return ShoppingCart;
|
||||
case 'Sparkle': return Sparkle;
|
||||
case 'Sprout': return Sprout;
|
||||
case 'Star': return Star;
|
||||
case 'Sun': return Sun;
|
||||
case 'Tablet': return Tablet;
|
||||
case 'Tag': return Tag;
|
||||
case 'Target': return Target;
|
||||
case 'Terminal': return Terminal;
|
||||
case 'ThumbsUp': return ThumbsUp;
|
||||
case 'TreeDeciduous': return TreeDeciduous;
|
||||
case 'TrendingUp': return TrendingUp;
|
||||
case 'Truck': return Truck;
|
||||
case 'User': return User;
|
||||
case 'Utensils': return Utensils;
|
||||
case 'Video': return Video;
|
||||
case 'Wallet': return Wallet;
|
||||
case 'Wand': return Wand;
|
||||
case 'Wifi': return Wifi;
|
||||
case 'X': return X;
|
||||
case 'Zap': return Zap;
|
||||
default:
|
||||
// console.warn(`Icon "${iconName}" not found, falling back to Circle.`);
|
||||
return Circle;
|
||||
}
|
||||
}
|
||||
return iconName;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user