Initial commit
This commit is contained in:
161
src/components/sections/hero/HeroBillboard.tsx
Normal file
161
src/components/sections/hero/HeroBillboard.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
"use client";
|
||||
|
||||
import TextBox from "@/components/Textbox";
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
|
||||
import LogoMarquee, { type MarqueeItem } from "@/components/shared/LogoMarquee";
|
||||
import { cls } from "@/lib/utils";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
import type { Avatar } from "@/components/shared/AvatarGroup";
|
||||
|
||||
type HeroBillboardBackgroundProps = Extract<
|
||||
HeroBackgroundVariantProps,
|
||||
| { variant: "plain" }
|
||||
| { variant: "animated-grid" }
|
||||
| { variant: "canvas-reveal" }
|
||||
| { variant: "cell-wave" }
|
||||
| { variant: "downward-rays-animated" }
|
||||
| { variant: "downward-rays-animated-grid" }
|
||||
| { variant: "downward-rays-static" }
|
||||
| { variant: "downward-rays-static-grid" }
|
||||
| { variant: "gradient-bars" }
|
||||
| { variant: "radial-gradient" }
|
||||
| { variant: "rotated-rays-animated" }
|
||||
| { variant: "rotated-rays-animated-grid" }
|
||||
| { variant: "rotated-rays-static" }
|
||||
| { variant: "rotated-rays-static-grid" }
|
||||
| { variant: "sparkles-gradient" }
|
||||
>;
|
||||
|
||||
interface HeroBillboardProps {
|
||||
title: string;
|
||||
description: string;
|
||||
background: HeroBillboardBackgroundProps;
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
buttons?: ButtonConfig[];
|
||||
avatars?: Avatar[];
|
||||
avatarText?: string;
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
marqueeItems?: MarqueeItem[];
|
||||
marqueeSpeed?: number;
|
||||
showMarqueeCard?: boolean;
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
titleClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
tagClassName?: string;
|
||||
avatarGroupClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
mediaWrapperClassName?: string;
|
||||
imageClassName?: string;
|
||||
marqueeClassName?: string;
|
||||
marqueeItemClassName?: string;
|
||||
marqueeCardClassName?: string;
|
||||
marqueeImageClassName?: string;
|
||||
marqueeTextClassName?: string;
|
||||
marqueeIconClassName?: string;
|
||||
}
|
||||
|
||||
const HeroBillboard = ({
|
||||
title,
|
||||
description,
|
||||
background,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
avatars,
|
||||
avatarText,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
imageAlt = "",
|
||||
videoAriaLabel = "Hero video",
|
||||
marqueeItems,
|
||||
marqueeSpeed = 30,
|
||||
showMarqueeCard = true,
|
||||
ariaLabel = "Hero section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
avatarGroupClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
imageClassName = "",
|
||||
marqueeClassName = "",
|
||||
marqueeItemClassName = "",
|
||||
marqueeCardClassName = "",
|
||||
marqueeImageClassName = "",
|
||||
marqueeTextClassName = "",
|
||||
marqueeIconClassName = "",
|
||||
}: HeroBillboardProps) => {
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("relative w-full py-hero-page-padding", className)}
|
||||
>
|
||||
<HeroBackgrounds {...background} />
|
||||
<div className={cls("w-content-width mx-auto flex flex-col gap-14 md:gap-15 relative z-10", containerClassName)}>
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
buttons={buttons}
|
||||
avatars={avatars}
|
||||
avatarText={avatarText}
|
||||
avatarsAbove={true}
|
||||
className={cls("flex flex-col gap-3 md:gap-1", textBoxClassName)}
|
||||
titleClassName={cls("text-6xl font-medium text-balance", titleClassName)}
|
||||
descriptionClassName={cls("text-base md:text-lg leading-[1.2]", descriptionClassName)}
|
||||
tagClassName={cls("px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-3", tagClassName)}
|
||||
avatarGroupClassName={avatarGroupClassName}
|
||||
buttonContainerClassName={cls("flex flex-wrap gap-4 mt-3", buttonContainerClassName)}
|
||||
buttonClassName={buttonClassName}
|
||||
buttonTextClassName={buttonTextClassName}
|
||||
center={true}
|
||||
/>
|
||||
<div className="flex flex-col gap-6" >
|
||||
<div className={cls("w-full overflow-hidden rounded-theme-capped card p-4", mediaWrapperClassName)}>
|
||||
<MediaContent
|
||||
imageSrc={imageSrc}
|
||||
videoSrc={videoSrc}
|
||||
imageAlt={imageAlt}
|
||||
videoAriaLabel={videoAriaLabel}
|
||||
imageClassName={cls("z-1", imageClassName)}
|
||||
/>
|
||||
</div>
|
||||
{marqueeItems && marqueeItems.length > 0 && (
|
||||
<LogoMarquee
|
||||
items={marqueeItems}
|
||||
speed={marqueeSpeed}
|
||||
showCard={showMarqueeCard}
|
||||
className={cls("w-content-width mx-auto z-10", marqueeClassName)}
|
||||
itemClassName={marqueeItemClassName}
|
||||
cardClassName={marqueeCardClassName}
|
||||
imageClassName={marqueeImageClassName}
|
||||
textClassName={marqueeTextClassName}
|
||||
iconClassName={marqueeIconClassName}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroBillboard.displayName = "HeroBillboard";
|
||||
|
||||
export default HeroBillboard;
|
||||
149
src/components/sections/hero/HeroBillboardCarousel.tsx
Normal file
149
src/components/sections/hero/HeroBillboardCarousel.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
"use client";
|
||||
|
||||
import TextBox from "@/components/Textbox";
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
import AutoCarousel from "@/components/cardStack/layouts/carousels/AutoCarousel";
|
||||
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
|
||||
import { cls } from "@/lib/utils";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
|
||||
export interface MediaItem {
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
}
|
||||
|
||||
type HeroBillboardCarouselBackgroundProps = Extract<
|
||||
HeroBackgroundVariantProps,
|
||||
| { variant: "plain" }
|
||||
| { variant: "animated-grid" }
|
||||
| { variant: "canvas-reveal" }
|
||||
| { variant: "cell-wave" }
|
||||
| { variant: "downward-rays-animated" }
|
||||
| { variant: "downward-rays-animated-grid" }
|
||||
| { variant: "downward-rays-static" }
|
||||
| { variant: "downward-rays-static-grid" }
|
||||
| { variant: "gradient-bars" }
|
||||
| { variant: "radial-gradient" }
|
||||
| { variant: "rotated-rays-animated" }
|
||||
| { variant: "rotated-rays-animated-grid" }
|
||||
| { variant: "rotated-rays-static" }
|
||||
| { variant: "rotated-rays-static-grid" }
|
||||
| { variant: "sparkles-gradient" }
|
||||
>;
|
||||
|
||||
interface HeroBillboardCarouselProps {
|
||||
title: string;
|
||||
description: string;
|
||||
background: HeroBillboardCarouselBackgroundProps;
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
buttons?: ButtonConfig[];
|
||||
mediaItems: MediaItem[];
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
titleClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
tagClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
mediaWrapperClassName?: string;
|
||||
}
|
||||
|
||||
const HeroBillboardCarousel = ({
|
||||
title,
|
||||
description,
|
||||
background,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
mediaItems,
|
||||
ariaLabel = "Hero section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
}: HeroBillboardCarouselProps) => {
|
||||
const renderCarouselItem = (item: MediaItem, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className="w-full aspect-[4/5] overflow-hidden rounded-theme-capped card p-2 shadow-lg"
|
||||
>
|
||||
<MediaContent
|
||||
imageSrc={item.imageSrc}
|
||||
videoSrc={item.videoSrc}
|
||||
imageAlt={item.imageAlt || ""}
|
||||
videoAriaLabel={item.videoAriaLabel || "Carousel media"}
|
||||
imageClassName="z-1 h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls(
|
||||
"relative w-full py-hero-page-padding md:h-svh md:py-0",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<HeroBackgrounds {...background} />
|
||||
<div className={cls(
|
||||
"mx-auto flex flex-col gap-14 md:gap-10 relative z-10",
|
||||
"w-full md:w-content-width md:h-full md:items-center md:justify-center",
|
||||
containerClassName
|
||||
)}>
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
buttons={buttons}
|
||||
className={cls(
|
||||
"flex flex-col gap-3 md:gap-1 w-content-width mx-auto",
|
||||
textBoxClassName
|
||||
)}
|
||||
titleClassName={cls("text-6xl font-medium text-balance", titleClassName)}
|
||||
descriptionClassName={cls("text-base md:text-lg leading-[1.2]", descriptionClassName)}
|
||||
tagClassName={cls("px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-3", tagClassName)}
|
||||
buttonContainerClassName={cls("flex flex-wrap gap-4 mt-3", buttonContainerClassName)}
|
||||
buttonClassName={buttonClassName}
|
||||
buttonTextClassName={buttonTextClassName}
|
||||
center={true}
|
||||
/>
|
||||
|
||||
<div className={cls("w-full -mx-[var(--content-padding)]", mediaWrapperClassName)}>
|
||||
<AutoCarousel
|
||||
title=""
|
||||
description=""
|
||||
textboxLayout="default"
|
||||
animationType="none"
|
||||
className="py-0"
|
||||
carouselClassName="py-0"
|
||||
containerClassName="!w-full"
|
||||
itemClassName="!w-55 md:!w-carousel-item-4"
|
||||
ariaLabel="Hero carousel"
|
||||
showTextBox={false}
|
||||
>
|
||||
{mediaItems?.map(renderCarouselItem)}
|
||||
</AutoCarousel>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroBillboardCarousel.displayName = "HeroBillboardCarousel";
|
||||
|
||||
export default HeroBillboardCarousel;
|
||||
126
src/components/sections/hero/HeroBillboardDashboard.tsx
Normal file
126
src/components/sections/hero/HeroBillboardDashboard.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
"use client";
|
||||
|
||||
import TextBox from "@/components/Textbox";
|
||||
import Dashboard from "@/components/shared/Dashboard";
|
||||
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
|
||||
import { cls } from "@/lib/utils";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
import type { DashboardSidebarItem, DashboardStat, DashboardListItem } from "@/components/shared/Dashboard";
|
||||
import type { ChartDataItem } from "@/components/bento/BentoLineChart/utils";
|
||||
|
||||
type HeroBillboardDashboardBackgroundProps = Extract<
|
||||
HeroBackgroundVariantProps,
|
||||
| { variant: "plain" }
|
||||
| { variant: "animated-grid" }
|
||||
| { variant: "canvas-reveal" }
|
||||
| { variant: "cell-wave" }
|
||||
| { variant: "downward-rays-animated" }
|
||||
| { variant: "downward-rays-animated-grid" }
|
||||
| { variant: "downward-rays-static" }
|
||||
| { variant: "downward-rays-static-grid" }
|
||||
| { variant: "gradient-bars" }
|
||||
| { variant: "radial-gradient" }
|
||||
| { variant: "rotated-rays-animated" }
|
||||
| { variant: "rotated-rays-animated-grid" }
|
||||
| { variant: "rotated-rays-static" }
|
||||
| { variant: "rotated-rays-static-grid" }
|
||||
| { variant: "sparkles-gradient" }
|
||||
>;
|
||||
|
||||
interface HeroBillboardDashboardProps {
|
||||
title: string;
|
||||
description: string;
|
||||
background: HeroBillboardDashboardBackgroundProps;
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
buttons?: ButtonConfig[];
|
||||
ariaLabel?: string;
|
||||
dashboard: {
|
||||
title: string;
|
||||
stats: [DashboardStat, DashboardStat, DashboardStat];
|
||||
logoIcon: LucideIcon;
|
||||
sidebarItems: DashboardSidebarItem[];
|
||||
searchPlaceholder?: string;
|
||||
buttons: ButtonConfig[];
|
||||
chartTitle?: string;
|
||||
chartData?: ChartDataItem[];
|
||||
listItems: DashboardListItem[];
|
||||
listTitle?: string;
|
||||
imageSrc: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
sidebarClassName?: string;
|
||||
statClassName?: string;
|
||||
chartClassName?: string;
|
||||
listClassName?: string;
|
||||
};
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
titleClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
tagClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
dashboardClassName?: string;
|
||||
}
|
||||
|
||||
const HeroBillboardDashboard = ({
|
||||
title,
|
||||
description,
|
||||
background,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
ariaLabel = "Hero section",
|
||||
dashboard,
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
dashboardClassName = "",
|
||||
}: HeroBillboardDashboardProps) => {
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("relative w-full py-hero-page-padding", className)}
|
||||
>
|
||||
<HeroBackgrounds {...background} />
|
||||
<div className={cls("w-content-width mx-auto flex flex-col gap-14 md:gap-15 relative z-10", containerClassName)}>
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
buttons={buttons}
|
||||
className={cls("flex flex-col gap-3 md:gap-1", textBoxClassName)}
|
||||
titleClassName={cls("text-6xl font-medium text-balance", titleClassName)}
|
||||
descriptionClassName={cls("text-base md:text-lg leading-[1.2]", descriptionClassName)}
|
||||
tagClassName={cls("px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-3", tagClassName)}
|
||||
buttonContainerClassName={cls("flex flex-wrap gap-4 mt-3", buttonContainerClassName)}
|
||||
buttonClassName={buttonClassName}
|
||||
buttonTextClassName={buttonTextClassName}
|
||||
center={true}
|
||||
/>
|
||||
<Dashboard
|
||||
{...dashboard}
|
||||
className={cls(dashboard.className, dashboardClassName)}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroBillboardDashboard.displayName = "HeroBillboardDashboard";
|
||||
|
||||
export default HeroBillboardDashboard;
|
||||
189
src/components/sections/hero/HeroBillboardGallery.tsx
Normal file
189
src/components/sections/hero/HeroBillboardGallery.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
"use client";
|
||||
|
||||
import TextBox from "@/components/Textbox";
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
import AutoCarousel from "@/components/cardStack/layouts/carousels/AutoCarousel";
|
||||
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
|
||||
import { cls } from "@/lib/utils";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
|
||||
export interface MediaItem {
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
}
|
||||
|
||||
type HeroBillboardGalleryBackgroundProps = Extract<
|
||||
HeroBackgroundVariantProps,
|
||||
| { variant: "plain" }
|
||||
| { variant: "animated-grid" }
|
||||
| { variant: "canvas-reveal" }
|
||||
| { variant: "cell-wave" }
|
||||
| { variant: "downward-rays-animated" }
|
||||
| { variant: "downward-rays-animated-grid" }
|
||||
| { variant: "downward-rays-static" }
|
||||
| { variant: "downward-rays-static-grid" }
|
||||
| { variant: "gradient-bars" }
|
||||
| { variant: "radial-gradient" }
|
||||
| { variant: "rotated-rays-animated" }
|
||||
| { variant: "rotated-rays-animated-grid" }
|
||||
| { variant: "rotated-rays-static" }
|
||||
| { variant: "rotated-rays-static-grid" }
|
||||
| { variant: "sparkles-gradient" }
|
||||
>;
|
||||
|
||||
interface HeroBillboardGalleryProps {
|
||||
title: string;
|
||||
description: string;
|
||||
background: HeroBillboardGalleryBackgroundProps;
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
buttons?: ButtonConfig[];
|
||||
mediaItems: MediaItem[];
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
titleClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
tagClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
mediaWrapperClassName?: string;
|
||||
imageClassName?: string;
|
||||
}
|
||||
|
||||
const HeroBillboardGallery = ({
|
||||
title,
|
||||
description,
|
||||
background,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
mediaItems,
|
||||
ariaLabel = "Hero section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
imageClassName = "",
|
||||
}: HeroBillboardGalleryProps) => {
|
||||
const renderCarouselItem = (item: MediaItem, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className="w-full aspect-[4/5] overflow-hidden rounded-theme-capped card p-2 shadow-lg"
|
||||
>
|
||||
<MediaContent
|
||||
imageSrc={item.imageSrc}
|
||||
videoSrc={item.videoSrc}
|
||||
imageAlt={item.imageAlt || ""}
|
||||
videoAriaLabel={item.videoAriaLabel || "Gallery media"}
|
||||
imageClassName="h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const itemCount = mediaItems?.length || 0;
|
||||
const desktopWidthClass = itemCount === 3 ? "md:w-[24%]" : itemCount === 4 ? "md:w-[24%]" : "md:w-[23%]";
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls(
|
||||
"relative w-full py-hero-page-padding md:h-svh md:py-0",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<HeroBackgrounds {...background} />
|
||||
<div className={cls(
|
||||
"mx-auto flex flex-col gap-14 relative z-10",
|
||||
"w-full md:w-content-width md:h-full md:items-center md:justify-center",
|
||||
containerClassName
|
||||
)}>
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
buttons={buttons}
|
||||
className={cls(
|
||||
"flex flex-col gap-3 md:gap-1 w-content-width mx-auto",
|
||||
textBoxClassName
|
||||
)}
|
||||
titleClassName={cls("text-6xl font-medium text-balance", titleClassName)}
|
||||
descriptionClassName={cls("text-base md:text-lg leading-[1.2]", descriptionClassName)}
|
||||
tagClassName={cls("px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-3", tagClassName)}
|
||||
buttonContainerClassName={cls("flex flex-wrap gap-4 mt-3", buttonContainerClassName)}
|
||||
buttonClassName={buttonClassName}
|
||||
buttonTextClassName={buttonTextClassName}
|
||||
center={true}
|
||||
/>
|
||||
|
||||
<div className={cls("w-full", mediaWrapperClassName)}>
|
||||
<div className="block md:hidden -mx-[var(--content-padding)]">
|
||||
<AutoCarousel
|
||||
title=""
|
||||
description=""
|
||||
textboxLayout="default"
|
||||
animationType="none"
|
||||
className="py-0"
|
||||
carouselClassName="py-0"
|
||||
containerClassName="!w-full"
|
||||
itemClassName="!w-55"
|
||||
ariaLabel="Hero gallery carousel"
|
||||
showTextBox={false}
|
||||
>
|
||||
{mediaItems?.slice(0, 5).map(renderCarouselItem)}
|
||||
</AutoCarousel>
|
||||
</div>
|
||||
|
||||
<div className="hidden md:flex justify-center items-center pt-2">
|
||||
<div className="relative flex items-center justify-center w-full">
|
||||
{mediaItems?.slice(0, 5).map((item, index) => {
|
||||
const rotations = ["-rotate-6", "rotate-6", "-rotate-6", "rotate-6", "-rotate-6"];
|
||||
const zIndexes = ["z-10", "z-20", "z-30", "z-40", "z-50"];
|
||||
const translates = ["-translate-y-5", "translate-y-5", "-translate-y-5", "translate-y-5", "-translate-y-5"];
|
||||
const marginClass = index > 0 ? "-ml-12 md:-ml-15" : "";
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={cls(
|
||||
"relative aspect-[4/5] overflow-hidden rounded-theme-capped card p-2 shadow-lg transition-transform duration-500 ease-out hover:scale-110",
|
||||
desktopWidthClass,
|
||||
rotations[index],
|
||||
zIndexes[index],
|
||||
translates[index],
|
||||
marginClass
|
||||
)}
|
||||
>
|
||||
<MediaContent
|
||||
imageSrc={item.imageSrc}
|
||||
videoSrc={item.videoSrc}
|
||||
imageAlt={item.imageAlt || ""}
|
||||
videoAriaLabel={item.videoAriaLabel || "Gallery media"}
|
||||
imageClassName={cls("z-1 h-full object-cover", imageClassName)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroBillboardGallery.displayName = "HeroBillboardGallery";
|
||||
|
||||
export default HeroBillboardGallery;
|
||||
121
src/components/sections/hero/HeroBillboardRotatedCarousel.tsx
Normal file
121
src/components/sections/hero/HeroBillboardRotatedCarousel.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
"use client";
|
||||
|
||||
|
||||
import TextBox from "@/components/Textbox";
|
||||
import AngledCarousel from "@/components/cardStack/layouts/carousels/AngledCarousel";
|
||||
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
|
||||
import { cls } from "@/lib/utils";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
|
||||
type HeroBillboardRotatedCarouselBackgroundProps = Extract<
|
||||
HeroBackgroundVariantProps,
|
||||
| { variant: "plain" }
|
||||
| { variant: "animated-grid" }
|
||||
| { variant: "canvas-reveal" }
|
||||
| { variant: "cell-wave" }
|
||||
| { variant: "downward-rays-animated" }
|
||||
| { variant: "downward-rays-animated-grid" }
|
||||
| { variant: "downward-rays-static" }
|
||||
| { variant: "downward-rays-static-grid" }
|
||||
| { variant: "gradient-bars" }
|
||||
| { variant: "radial-gradient" }
|
||||
| { variant: "rotated-rays-animated" }
|
||||
| { variant: "rotated-rays-animated-grid" }
|
||||
| { variant: "rotated-rays-static" }
|
||||
| { variant: "rotated-rays-static-grid" }
|
||||
| { variant: "sparkles-gradient" }
|
||||
>;
|
||||
|
||||
interface CarouselItem {
|
||||
id: string;
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
}
|
||||
|
||||
interface HeroBillboardRotatedCarouselProps {
|
||||
title: string;
|
||||
description: string;
|
||||
background: HeroBillboardRotatedCarouselBackgroundProps;
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
buttons?: ButtonConfig[];
|
||||
carouselItems: CarouselItem[];
|
||||
autoPlay?: boolean;
|
||||
autoPlayInterval?: number;
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
titleClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
tagClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
carouselClassName?: string;
|
||||
}
|
||||
|
||||
const HeroBillboardRotatedCarousel = ({
|
||||
title,
|
||||
description,
|
||||
background,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
carouselItems,
|
||||
autoPlay = true,
|
||||
autoPlayInterval = 4000,
|
||||
ariaLabel = "Hero section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
carouselClassName = "",
|
||||
}: HeroBillboardRotatedCarouselProps) => {
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("relative w-full h-fit md:min-h-svh flex items-center justify-center py-hero-page-padding", className)}
|
||||
>
|
||||
<HeroBackgrounds {...background} />
|
||||
<div className={cls("w-full flex flex-col gap-14 md:gap-15 relative z-10", containerClassName)}>
|
||||
<div className="w-content-width mx-auto" >
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
buttons={buttons}
|
||||
className={cls("flex flex-col gap-3 md:gap-1", textBoxClassName)}
|
||||
titleClassName={cls("text-6xl font-medium text-balance", titleClassName)}
|
||||
descriptionClassName={cls("text-base md:text-lg leading-[1.2]", descriptionClassName)}
|
||||
tagClassName={cls("px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-3", tagClassName)}
|
||||
buttonContainerClassName={cls("flex flex-wrap gap-4 mt-3", buttonContainerClassName)}
|
||||
buttonClassName={buttonClassName}
|
||||
buttonTextClassName={buttonTextClassName}
|
||||
center={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AngledCarousel
|
||||
items={carouselItems}
|
||||
autoPlay={autoPlay}
|
||||
autoPlayInterval={autoPlayInterval}
|
||||
className={carouselClassName}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroBillboardRotatedCarousel.displayName = "HeroBillboardRotatedCarousel";
|
||||
|
||||
export default HeroBillboardRotatedCarousel;
|
||||
162
src/components/sections/hero/HeroBillboardScroll.tsx
Normal file
162
src/components/sections/hero/HeroBillboardScroll.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
"use client";
|
||||
|
||||
import { useRef } from "react";
|
||||
import { useScroll, useTransform, motion } from "motion/react";
|
||||
import TextBox from "@/components/Textbox";
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
|
||||
import { cls } from "@/lib/utils";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
|
||||
type HeroBillboardScrollBackgroundProps = Extract<
|
||||
HeroBackgroundVariantProps,
|
||||
| { variant: "plain" }
|
||||
| { variant: "animated-grid" }
|
||||
| { variant: "canvas-reveal" }
|
||||
| { variant: "cell-wave" }
|
||||
| { variant: "downward-rays-animated" }
|
||||
| { variant: "downward-rays-animated-grid" }
|
||||
| { variant: "downward-rays-static" }
|
||||
| { variant: "downward-rays-static-grid" }
|
||||
| { variant: "gradient-bars" }
|
||||
| { variant: "radial-gradient" }
|
||||
| { variant: "rotated-rays-animated" }
|
||||
| { variant: "rotated-rays-animated-grid" }
|
||||
| { variant: "rotated-rays-static" }
|
||||
| { variant: "rotated-rays-static-grid" }
|
||||
| { variant: "sparkles-gradient" }
|
||||
>;
|
||||
|
||||
interface HeroBillboardScrollProps {
|
||||
title: string;
|
||||
description: string;
|
||||
background: HeroBillboardScrollBackgroundProps;
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
buttons?: ButtonConfig[];
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
titleClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
tagClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
cardWrapperClassName?: string;
|
||||
cardInnerClassName?: string;
|
||||
imageClassName?: string;
|
||||
}
|
||||
|
||||
const HeroBillboardScroll = ({
|
||||
title,
|
||||
description,
|
||||
background,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
imageAlt = "",
|
||||
videoAriaLabel = "Hero video",
|
||||
ariaLabel = "Hero section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
cardWrapperClassName = "",
|
||||
cardInnerClassName = "",
|
||||
imageClassName = "",
|
||||
}: HeroBillboardScrollProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const { scrollYProgress } = useScroll({
|
||||
target: containerRef,
|
||||
});
|
||||
|
||||
const rotate = useTransform(scrollYProgress, [0, 1], [20, 0]);
|
||||
const scale = useTransform(scrollYProgress, [0, 1], [1.05, 1]);
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
ref={containerRef}
|
||||
className={cls("relative h-fit flex items-center justify-center", className)}
|
||||
>
|
||||
<HeroBackgrounds {...background} />
|
||||
<div
|
||||
className={cls("py-hero-page-padding w-full relative z-10", containerClassName)}
|
||||
style={{
|
||||
perspective: "1000px",
|
||||
}}
|
||||
>
|
||||
<div className="w-content-width mx-auto">
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
buttons={buttons}
|
||||
className={cls("flex flex-col gap-3 md:gap-1", textBoxClassName)}
|
||||
titleClassName={cls("text-6xl font-medium text-balance", titleClassName)}
|
||||
descriptionClassName={cls("text-base md:text-lg leading-[1.2]", descriptionClassName)}
|
||||
tagClassName={cls("px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-3", tagClassName)}
|
||||
buttonContainerClassName={cls("flex flex-wrap gap-4 mt-3", buttonContainerClassName)}
|
||||
buttonClassName={buttonClassName}
|
||||
buttonTextClassName={buttonTextClassName}
|
||||
center={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cls("relative w-content-width h-[50svh] mt-8 mx-auto md:hidden", cardWrapperClassName)}
|
||||
style={{
|
||||
transform: "rotateX(20deg)",
|
||||
}}
|
||||
>
|
||||
<div className={cls("h-full w-full overflow-hidden rounded-theme-capped card p-4", cardInnerClassName)}>
|
||||
<MediaContent
|
||||
imageSrc={imageSrc}
|
||||
videoSrc={videoSrc}
|
||||
imageAlt={imageAlt}
|
||||
videoAriaLabel={videoAriaLabel}
|
||||
imageClassName={cls("z-1 h-full w-full object-cover object-left-top", imageClassName)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
style={{
|
||||
rotateX: rotate,
|
||||
scale,
|
||||
}}
|
||||
className={cls("hidden md:block relative w-content-width mt-8 h-[75svh] mx-auto", cardWrapperClassName)}
|
||||
>
|
||||
<div className={cls("h-full w-full overflow-hidden rounded-theme-capped card p-4", cardInnerClassName)}>
|
||||
<MediaContent
|
||||
imageSrc={imageSrc}
|
||||
videoSrc={videoSrc}
|
||||
imageAlt={imageAlt}
|
||||
videoAriaLabel={videoAriaLabel}
|
||||
imageClassName={cls("z-1 h-full w-full object-cover object-left-top", imageClassName)}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroBillboardScroll.displayName = "HeroBillboardScroll";
|
||||
|
||||
export default HeroBillboardScroll;
|
||||
128
src/components/sections/hero/HeroCentered.tsx
Normal file
128
src/components/sections/hero/HeroCentered.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
"use client";
|
||||
|
||||
import TextBox from "@/components/Textbox";
|
||||
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
|
||||
import LogoMarquee, { type MarqueeItem } from "@/components/shared/LogoMarquee";
|
||||
import { cls } from "@/lib/utils";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
import type { Avatar } from "@/components/shared/AvatarGroup";
|
||||
|
||||
type HeroCenteredBackgroundProps = Extract<
|
||||
HeroBackgroundVariantProps,
|
||||
| { variant: "plain" }
|
||||
| { variant: "animated-grid" }
|
||||
| { variant: "canvas-reveal" }
|
||||
| { variant: "cell-wave" }
|
||||
| { variant: "downward-rays-animated" }
|
||||
| { variant: "downward-rays-animated-grid" }
|
||||
| { variant: "downward-rays-static" }
|
||||
| { variant: "downward-rays-static-grid" }
|
||||
| { variant: "gradient-bars" }
|
||||
| { variant: "radial-gradient" }
|
||||
| { variant: "rotated-rays-animated" }
|
||||
| { variant: "rotated-rays-animated-grid" }
|
||||
| { variant: "rotated-rays-static" }
|
||||
| { variant: "rotated-rays-static-grid" }
|
||||
| { variant: "sparkles-gradient" }
|
||||
>;
|
||||
|
||||
interface HeroCenteredProps {
|
||||
title: string;
|
||||
description: string;
|
||||
background: HeroCenteredBackgroundProps;
|
||||
avatars: Avatar[];
|
||||
avatarText?: string;
|
||||
buttons?: ButtonConfig[];
|
||||
marqueeItems?: MarqueeItem[];
|
||||
marqueeSpeed?: number;
|
||||
showMarqueeCard?: boolean;
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
titleClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
avatarGroupClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
marqueeClassName?: string;
|
||||
marqueeItemClassName?: string;
|
||||
marqueeCardClassName?: string;
|
||||
marqueeImageClassName?: string;
|
||||
marqueeTextClassName?: string;
|
||||
marqueeIconClassName?: string;
|
||||
}
|
||||
|
||||
const HeroCentered = ({
|
||||
title,
|
||||
description,
|
||||
background,
|
||||
avatars,
|
||||
avatarText,
|
||||
buttons,
|
||||
marqueeItems,
|
||||
marqueeSpeed = 30,
|
||||
showMarqueeCard = true,
|
||||
ariaLabel = "Hero section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
avatarGroupClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
marqueeClassName = "",
|
||||
marqueeItemClassName = "",
|
||||
marqueeCardClassName = "",
|
||||
marqueeImageClassName = "",
|
||||
marqueeTextClassName = "",
|
||||
marqueeIconClassName = "",
|
||||
}: HeroCenteredProps) => {
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("relative w-full h-svh md:h-screen flex flex-col items-center justify-center", className)}
|
||||
>
|
||||
<HeroBackgrounds {...background} />
|
||||
<div className={cls("w-content-width mx-auto relative z-10", containerClassName)}>
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
avatars={avatars}
|
||||
avatarText={avatarText}
|
||||
avatarsAbove={true}
|
||||
buttons={buttons}
|
||||
className={cls("md:max-w-7/10 mx-auto flex flex-col gap-3 md:gap-3", textBoxClassName)}
|
||||
titleClassName={cls("text-7xl font-medium text-balance", titleClassName)}
|
||||
descriptionClassName={cls("text-lg md:text-xl leading-tight", descriptionClassName)}
|
||||
avatarGroupClassName={avatarGroupClassName}
|
||||
buttonContainerClassName={cls("flex flex-wrap gap-4 mt-3", buttonContainerClassName)}
|
||||
buttonClassName={buttonClassName}
|
||||
buttonTextClassName={buttonTextClassName}
|
||||
center={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{marqueeItems && marqueeItems.length > 0 && (
|
||||
<LogoMarquee
|
||||
items={marqueeItems}
|
||||
speed={marqueeSpeed}
|
||||
showCard={showMarqueeCard}
|
||||
className={cls("absolute bottom-6 left-1/2 -translate-x-1/2 w-content-width z-10", marqueeClassName)}
|
||||
itemClassName={marqueeItemClassName}
|
||||
cardClassName={marqueeCardClassName}
|
||||
imageClassName={marqueeImageClassName}
|
||||
textClassName={marqueeTextClassName}
|
||||
iconClassName={marqueeIconClassName}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroCentered.displayName = "HeroCentered";
|
||||
|
||||
export default HeroCentered;
|
||||
125
src/components/sections/hero/HeroLogo.tsx
Normal file
125
src/components/sections/hero/HeroLogo.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
"use client";
|
||||
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
import FillWidthText, { hasDescenders } from "@/components/shared/FillWidthText/FillWidthText";
|
||||
import TextAnimation from "@/components/text/TextAnimation";
|
||||
import Button from "@/components/button/Button";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { getButtonProps } from "@/lib/buttonUtils";
|
||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
|
||||
const MASK_GRADIENT = "linear-gradient(to bottom, transparent, black 60%)";
|
||||
|
||||
interface HeroLogoProps {
|
||||
logoText: string;
|
||||
description: string;
|
||||
buttons: ButtonConfig[];
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
showDimOverlay?: boolean;
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
contentContainerClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
logoContainerClassName?: string;
|
||||
logoClassName?: string;
|
||||
mediaWrapperClassName?: string;
|
||||
imageClassName?: string;
|
||||
blurClassName?: string;
|
||||
dimOverlayClassName?: string;
|
||||
}
|
||||
|
||||
const HeroLogo = ({
|
||||
logoText,
|
||||
description,
|
||||
buttons,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
imageAlt = "",
|
||||
videoAriaLabel = "Hero video",
|
||||
showDimOverlay = false,
|
||||
ariaLabel = "Hero section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
contentContainerClassName = "",
|
||||
descriptionClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
logoContainerClassName = "",
|
||||
logoClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
imageClassName = "",
|
||||
blurClassName = "",
|
||||
dimOverlayClassName = "",
|
||||
}: HeroLogoProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("relative w-full h-svh overflow-hidden flex flex-col justify-end", className)}
|
||||
>
|
||||
<div className={cls("absolute inset-0 w-full h-full", mediaWrapperClassName)}>
|
||||
{showDimOverlay && (
|
||||
<div className={cls("absolute top-0 left-0 w-full h-full bg-background/20 pointer-events-none select-none", dimOverlayClassName)} />
|
||||
)}
|
||||
<MediaContent
|
||||
imageSrc={imageSrc}
|
||||
videoSrc={videoSrc}
|
||||
imageAlt={imageAlt}
|
||||
videoAriaLabel={videoAriaLabel}
|
||||
imageClassName={cls("w-full h-full object-cover !rounded-none", imageClassName)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cls(
|
||||
"absolute z-10 backdrop-blur-xl opacity-100 w-full h-[50svh] md:h-[75svh] left-0 bottom-0 pointer-events-none select-none",
|
||||
blurClassName
|
||||
)}
|
||||
style={{ maskImage: MASK_GRADIENT }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<div className={cls("relative z-20 w-content-width mx-auto h-fit flex items-end", containerClassName)}>
|
||||
<div className={cls("w-full flex flex-col", logoContainerClassName)}>
|
||||
<div className={cls("w-full flex flex-col md:flex-row md:justify-between items-start md:items-end gap-3 md:gap-6", contentContainerClassName)}>
|
||||
<div className="w-full md:w-1/2" >
|
||||
<TextAnimation
|
||||
type={theme.defaultTextAnimation}
|
||||
text={description}
|
||||
variant="words-trigger"
|
||||
start="top 100%"
|
||||
className={cls("text-lg md:text-2xl text-background text-balance font-medium leading-[1.2] md:max-w-1/2", descriptionClassName)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full md:w-1/2 flex justify-start md:justify-end" >
|
||||
<div className={cls("flex flex-wrap gap-4", buttonContainerClassName)}>
|
||||
{buttons.slice(0, 2).map((button, index) => (
|
||||
<Button key={index} {...getButtonProps(button, index, theme.defaultButtonVariant, cls("", buttonClassName), cls("text-base", buttonTextClassName))} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex">
|
||||
<FillWidthText className={cls("font-bold text-background", !hasDescenders(logoText) && "my-10", logoClassName)}>
|
||||
{logoText}
|
||||
</FillWidthText>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroLogo.displayName = "HeroLogo";
|
||||
|
||||
export default HeroLogo;
|
||||
160
src/components/sections/hero/HeroLogoBillboard.tsx
Normal file
160
src/components/sections/hero/HeroLogoBillboard.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
"use client";
|
||||
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
import FillWidthText from "@/components/shared/FillWidthText/FillWidthText";
|
||||
import TextAnimation from "@/components/text/TextAnimation";
|
||||
import Button from "@/components/button/Button";
|
||||
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { getButtonProps } from "@/lib/buttonUtils";
|
||||
import { Plus } from "lucide-react";
|
||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||
import type { ButtonConfig } from "@/components/cardStack/types";
|
||||
|
||||
type HeroLogoBillboardBackgroundProps = Extract<
|
||||
HeroBackgroundVariantProps,
|
||||
| { variant: "plain" }
|
||||
| { variant: "animated-grid" }
|
||||
| { variant: "canvas-reveal" }
|
||||
| { variant: "cell-wave" }
|
||||
| { variant: "downward-rays-animated" }
|
||||
| { variant: "downward-rays-animated-grid" }
|
||||
| { variant: "downward-rays-static" }
|
||||
| { variant: "downward-rays-static-grid" }
|
||||
| { variant: "glowing-orb" }
|
||||
| { variant: "gradient-bars" }
|
||||
| { variant: "radial-gradient" }
|
||||
| { variant: "rotated-rays-animated" }
|
||||
| { variant: "rotated-rays-animated-grid" }
|
||||
| { variant: "rotated-rays-static" }
|
||||
| { variant: "rotated-rays-static-grid" }
|
||||
| { variant: "sparkles-gradient" }
|
||||
>;
|
||||
|
||||
interface HeroLogoBillboardProps {
|
||||
logoText: string;
|
||||
description: string;
|
||||
buttons: ButtonConfig[];
|
||||
background: HeroLogoBillboardBackgroundProps;
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
frameStyle?: "card" | "browser";
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
logoContainerClassName?: string;
|
||||
logoClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
mediaWrapperClassName?: string;
|
||||
imageClassName?: string;
|
||||
browserBarClassName?: string;
|
||||
addressBarClassName?: string;
|
||||
}
|
||||
|
||||
const HeroLogoBillboard = ({
|
||||
logoText,
|
||||
description,
|
||||
buttons,
|
||||
background,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
imageAlt = "",
|
||||
videoAriaLabel = "Hero video",
|
||||
frameStyle = "card",
|
||||
ariaLabel = "Hero section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
logoContainerClassName = "",
|
||||
logoClassName = "",
|
||||
descriptionClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
imageClassName = "",
|
||||
browserBarClassName = "",
|
||||
addressBarClassName = "",
|
||||
}: HeroLogoBillboardProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("relative w-full py-hero-page-padding", className)}
|
||||
>
|
||||
<HeroBackgrounds {...background} />
|
||||
<div className={cls("w-content-width mx-auto flex flex-col gap-14 md:gap-15 relative z-10", containerClassName)}>
|
||||
<div className={cls("w-full flex flex-col items-end gap-6 md:gap-8", logoContainerClassName)}>
|
||||
<div className="relative w-full flex">
|
||||
<FillWidthText className={cls("text-foreground", logoClassName)}>
|
||||
{logoText}
|
||||
</FillWidthText>
|
||||
</div>
|
||||
<div className="relative w-full md:w-1/2" >
|
||||
<TextAnimation
|
||||
type={theme.defaultTextAnimation}
|
||||
text={description}
|
||||
variant="words-trigger"
|
||||
start="top 100%"
|
||||
className={cls("text-lg md:text-3xl text-foreground/75 text-balance text-end leading-[1.2]", descriptionClassName)}
|
||||
/>
|
||||
</div>
|
||||
<div className={cls("flex flex-wrap justify-end gap-3", buttonContainerClassName)}>
|
||||
{buttons.slice(0, 2).map((button, index) => (
|
||||
<Button
|
||||
key={`${button.text}-${index}`}
|
||||
{...getButtonProps(button, index, theme.defaultButtonVariant, buttonClassName, buttonTextClassName)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{frameStyle === "browser" ? (
|
||||
<div className={cls("w-full overflow-hidden rounded-theme-capped card", mediaWrapperClassName)}>
|
||||
<div className={cls("relative z-1 bg-background border-b border-foreground/10 px-4 py-3 flex items-center gap-4", browserBarClassName)}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-3 w-auto aspect-square rounded-theme bg-accent" />
|
||||
<div className="h-3 w-auto aspect-square rounded-theme bg-accent" />
|
||||
<div className="h-3 w-auto aspect-square rounded-theme bg-accent" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<div className={cls("w-15 md:w-10 h-8 rounded-theme bg-accent/10", addressBarClassName)} />
|
||||
<div className="w-15 md:w-10 h-8 rounded-theme bg-accent/10" />
|
||||
<div className="hidden md:block w-10 h-8 rounded-theme bg-accent/10" />
|
||||
</div>
|
||||
<Plus className="h-[var(--text-sm)] w-auto text-foreground" />
|
||||
</div>
|
||||
<div className="relative z-1 p-0">
|
||||
<MediaContent
|
||||
imageSrc={imageSrc}
|
||||
videoSrc={videoSrc}
|
||||
imageAlt={imageAlt}
|
||||
videoAriaLabel={videoAriaLabel}
|
||||
imageClassName={cls("z-1 rounded-none! aspect-square md:aspect-video", imageClassName)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={cls("w-full overflow-hidden rounded-theme-capped card p-4", mediaWrapperClassName)}>
|
||||
<MediaContent
|
||||
imageSrc={imageSrc}
|
||||
videoSrc={videoSrc}
|
||||
imageAlt={imageAlt}
|
||||
videoAriaLabel={videoAriaLabel}
|
||||
imageClassName={cls("z-1 aspect-square md:aspect-video", imageClassName)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroLogoBillboard.displayName = "HeroLogoBillboard";
|
||||
|
||||
export default HeroLogoBillboard;
|
||||
165
src/components/sections/hero/HeroLogoBillboardSplit.tsx
Normal file
165
src/components/sections/hero/HeroLogoBillboardSplit.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
"use client";
|
||||
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
import FillWidthText from "@/components/shared/FillWidthText/FillWidthText";
|
||||
import TextAnimation from "@/components/text/TextAnimation";
|
||||
import Button from "@/components/button/Button";
|
||||
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { getButtonProps } from "@/lib/buttonUtils";
|
||||
import { Plus } from "lucide-react";
|
||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
|
||||
type HeroLogoBillboardSplitBackgroundProps = Extract<
|
||||
HeroBackgroundVariantProps,
|
||||
| { variant: "plain" }
|
||||
| { variant: "animated-grid" }
|
||||
| { variant: "canvas-reveal" }
|
||||
| { variant: "cell-wave" }
|
||||
| { variant: "downward-rays-animated" }
|
||||
| { variant: "downward-rays-animated-grid" }
|
||||
| { variant: "downward-rays-static" }
|
||||
| { variant: "downward-rays-static-grid" }
|
||||
| { variant: "glowing-orb" }
|
||||
| { variant: "gradient-bars" }
|
||||
| { variant: "radial-gradient" }
|
||||
| { variant: "rotated-rays-animated" }
|
||||
| { variant: "rotated-rays-animated-grid" }
|
||||
| { variant: "rotated-rays-static" }
|
||||
| { variant: "rotated-rays-static-grid" }
|
||||
| { variant: "sparkles-gradient" }
|
||||
>;
|
||||
|
||||
interface HeroLogoBillboardSplitProps {
|
||||
logoText: string;
|
||||
description: string;
|
||||
background: HeroLogoBillboardSplitBackgroundProps;
|
||||
buttons: ButtonConfig[];
|
||||
layoutOrder: "default" | "reverse";
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
frameStyle?: "card" | "browser";
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
logoContainerClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
logoClassName?: string;
|
||||
mediaWrapperClassName?: string;
|
||||
imageClassName?: string;
|
||||
browserBarClassName?: string;
|
||||
addressBarClassName?: string;
|
||||
}
|
||||
|
||||
const HeroLogoBillboardSplit = ({
|
||||
logoText,
|
||||
description,
|
||||
background,
|
||||
buttons,
|
||||
layoutOrder,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
imageAlt = "",
|
||||
videoAriaLabel = "Hero video",
|
||||
frameStyle = "card",
|
||||
ariaLabel = "Hero section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
logoContainerClassName = "",
|
||||
descriptionClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
logoClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
imageClassName = "",
|
||||
browserBarClassName = "",
|
||||
addressBarClassName = "",
|
||||
}: HeroLogoBillboardSplitProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("relative w-full py-hero-page-padding", className)}
|
||||
>
|
||||
<HeroBackgrounds {...background} />
|
||||
<div className={cls("w-content-width mx-auto flex flex-col gap-6 md:gap-15 relative z-10", containerClassName)}>
|
||||
<div className={cls(
|
||||
"w-full flex gap-6 md:gap-8",
|
||||
layoutOrder === "default" ? "flex-col" : "flex-col-reverse",
|
||||
logoContainerClassName
|
||||
)}>
|
||||
<div className="relative flex flex-col gap-3 md:flex-row justify-between md:items-end w-full" >
|
||||
<div className="relative flex flex-col gap-4 w-full md:w-1/2" >
|
||||
<TextAnimation
|
||||
type={theme.defaultTextAnimation}
|
||||
text={description}
|
||||
variant="words-trigger"
|
||||
start="top 100%"
|
||||
className={cls("text-lg md:text-3xl text-foreground/75 text-balance text-start leading-[1.2]", descriptionClassName)}
|
||||
/>
|
||||
</div>
|
||||
<div className={cls("flex flex-wrap gap-4", buttonContainerClassName)}>
|
||||
{buttons.slice(0, 2).map((button, index) => (
|
||||
<Button key={`${button.text}-${index}`} {...getButtonProps(button, index, theme.defaultButtonVariant, buttonClassName, buttonTextClassName)} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative w-full flex">
|
||||
<FillWidthText className={cls("text-foreground", logoClassName)}>
|
||||
{logoText}
|
||||
</FillWidthText>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{frameStyle === "browser" ? (
|
||||
<div className={cls("w-full overflow-hidden rounded-theme-capped card", mediaWrapperClassName)}>
|
||||
<div className={cls("relative z-1 bg-background border-b border-foreground/10 px-4 py-3 flex items-center gap-4", browserBarClassName)}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-3 w-auto aspect-square rounded-theme bg-accent" />
|
||||
<div className="h-3 w-auto aspect-square rounded-theme bg-accent" />
|
||||
<div className="h-3 w-auto aspect-square rounded-theme bg-accent" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<div className={cls("w-15 md:w-10 h-8 rounded-theme bg-accent/10", addressBarClassName)} />
|
||||
<div className="w-15 md:w-10 h-8 rounded-theme bg-accent/10" />
|
||||
<div className="hidden md:block w-10 h-8 rounded-theme bg-accent/10" />
|
||||
</div>
|
||||
<Plus className="h-[var(--text-sm)] w-auto text-foreground" />
|
||||
</div>
|
||||
<div className="relative z-1 p-0">
|
||||
<MediaContent
|
||||
imageSrc={imageSrc}
|
||||
videoSrc={videoSrc}
|
||||
imageAlt={imageAlt}
|
||||
videoAriaLabel={videoAriaLabel}
|
||||
imageClassName={cls("z-1 rounded-none! aspect-square md:aspect-video!", imageClassName)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={cls("w-full overflow-hidden rounded-theme-capped card p-4", mediaWrapperClassName)}>
|
||||
<MediaContent
|
||||
imageSrc={imageSrc}
|
||||
videoSrc={videoSrc}
|
||||
imageAlt={imageAlt}
|
||||
videoAriaLabel={videoAriaLabel}
|
||||
imageClassName={cls("z-1 aspect-square md:aspect-video", imageClassName)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroLogoBillboardSplit.displayName = "HeroLogoBillboardSplit";
|
||||
|
||||
export default HeroLogoBillboardSplit;
|
||||
142
src/components/sections/hero/HeroOverlay.tsx
Normal file
142
src/components/sections/hero/HeroOverlay.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
"use client";
|
||||
|
||||
import TextBox from "@/components/Textbox";
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
import { cls } from "@/lib/utils";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
|
||||
const RADIAL_MASK_GRADIENT = "radial-gradient(circle, black 20%, transparent 70%)";
|
||||
|
||||
interface HeroOverlayProps {
|
||||
title: string;
|
||||
description: string;
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
buttons?: ButtonConfig[];
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
textPosition?: "center" | "bottom-left";
|
||||
showDimOverlay?: boolean;
|
||||
showBlur?: boolean;
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
titleClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
tagClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
mediaWrapperClassName?: string;
|
||||
imageClassName?: string;
|
||||
blurClassName?: string;
|
||||
dimOverlayClassName?: string;
|
||||
}
|
||||
|
||||
const HeroOverlay = ({
|
||||
title,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
imageAlt = "",
|
||||
videoAriaLabel = "Hero video",
|
||||
textPosition = "bottom-left",
|
||||
showDimOverlay = false,
|
||||
showBlur = true,
|
||||
ariaLabel = "Hero section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
imageClassName = "",
|
||||
blurClassName = "",
|
||||
dimOverlayClassName = "",
|
||||
}: HeroOverlayProps) => {
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("relative w-full h-svh overflow-hidden flex flex-col justify-end", className)}
|
||||
>
|
||||
<div className={cls("absolute inset-0 w-full h-full", mediaWrapperClassName)}>
|
||||
{showDimOverlay && (
|
||||
<div className={cls("absolute top-0 left-0 w-full h-full bg-background/20 pointer-events-none select-none", dimOverlayClassName)} />
|
||||
)}
|
||||
<MediaContent
|
||||
imageSrc={imageSrc}
|
||||
videoSrc={videoSrc}
|
||||
imageAlt={imageAlt}
|
||||
videoAriaLabel={videoAriaLabel}
|
||||
imageClassName={cls("w-full h-full object-cover !rounded-none", imageClassName)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showBlur && (
|
||||
<div
|
||||
className={cls(
|
||||
"absolute z-10 backdrop-blur-sm opacity-100 pointer-events-auto select-none",
|
||||
textPosition === "center"
|
||||
? "w-[100vw] h-[80vw] left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2"
|
||||
: "w-[150vw] h-[150vw] left-0 bottom-0 -translate-x-1/2 translate-y-1/2",
|
||||
blurClassName
|
||||
)}
|
||||
style={{ maskImage: RADIAL_MASK_GRADIENT }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className={cls(
|
||||
"relative z-10 w-content-width mx-auto h-fit flex",
|
||||
textPosition === "center" ? "items-center justify-center" : "items-end pb-[var(--width-10)] md:pb-hero-page-padding",
|
||||
containerClassName
|
||||
)}>
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
buttons={buttons}
|
||||
className={cls(
|
||||
"flex flex-col gap-3 md:gap-3 text-background",
|
||||
textPosition === "center" ? "w-full" : "w-full md:w-1/2",
|
||||
textBoxClassName
|
||||
)}
|
||||
titleClassName={cls(
|
||||
"text-7xl 2xl:text-8xl font-medium text-balance",
|
||||
textPosition === "center" ? "text-center" : "text-left",
|
||||
titleClassName
|
||||
)}
|
||||
descriptionClassName={cls(
|
||||
"text-lg md:text-xl leading-[1.2]",
|
||||
textPosition === "center" ? "text-center" : "text-left",
|
||||
descriptionClassName
|
||||
)}
|
||||
tagClassName={cls(
|
||||
"w-fit px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-3",
|
||||
tagClassName
|
||||
)}
|
||||
buttonContainerClassName={cls("flex flex-wrap gap-4 mt-4", buttonContainerClassName)}
|
||||
buttonClassName={cls("", buttonClassName)}
|
||||
buttonTextClassName={cls("text-base", buttonTextClassName)}
|
||||
center={textPosition === "center"}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroOverlay.displayName = "HeroOverlay";
|
||||
|
||||
export default HeroOverlay;
|
||||
198
src/components/sections/hero/HeroPersonalLinks.tsx
Normal file
198
src/components/sections/hero/HeroPersonalLinks.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import Textbox from "@/components/Textbox";
|
||||
import Button from "@/components/button/Button";
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { getButtonProps } from "@/lib/buttonUtils";
|
||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||
import { useButtonClick } from "@/components/button/useButtonClick";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
|
||||
type TitleSegment =
|
||||
| { type: "text"; content: string }
|
||||
| { type: "image"; src: string; alt?: string };
|
||||
|
||||
type HeroPersonalLinksBackgroundProps = Extract<
|
||||
HeroBackgroundVariantProps,
|
||||
| { variant: "plain" }
|
||||
| { variant: "animated-grid" }
|
||||
| { variant: "canvas-reveal" }
|
||||
| { variant: "cell-wave" }
|
||||
| { variant: "downward-rays-animated" }
|
||||
| { variant: "downward-rays-animated-grid" }
|
||||
| { variant: "downward-rays-static" }
|
||||
| { variant: "downward-rays-static-grid" }
|
||||
| { variant: "gradient-bars" }
|
||||
| { variant: "radial-gradient" }
|
||||
| { variant: "rotated-rays-animated" }
|
||||
| { variant: "rotated-rays-animated-grid" }
|
||||
| { variant: "rotated-rays-static" }
|
||||
| { variant: "rotated-rays-static-grid" }
|
||||
| { variant: "sparkles-gradient" }
|
||||
>;
|
||||
|
||||
interface SocialLink {
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
interface LinkCard {
|
||||
icon?: LucideIcon;
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
imageClassName?: string;
|
||||
title: string;
|
||||
description: string;
|
||||
button: ButtonConfig;
|
||||
}
|
||||
|
||||
interface HeroPersonalLinksProps {
|
||||
background: HeroPersonalLinksBackgroundProps;
|
||||
title: string;
|
||||
titleSegments?: TitleSegment[];
|
||||
socialLinks?: SocialLink[];
|
||||
linkCards: LinkCard[];
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
textboxClassName?: string;
|
||||
titleClassName?: string;
|
||||
titleImageWrapperClassName?: string;
|
||||
titleImageClassName?: string;
|
||||
socialLinksClassName?: string;
|
||||
socialLinkClassName?: string;
|
||||
linkCardsClassName?: string;
|
||||
linkCardClassName?: string;
|
||||
linkCardIconClassName?: string;
|
||||
linkCardTitleClassName?: string;
|
||||
linkCardDescriptionClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
}
|
||||
|
||||
const SocialLinkButton = ({ social, className }: { social: SocialLink; className?: string }) => {
|
||||
const handleClick = useButtonClick(social.href);
|
||||
const Icon = social.icon;
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
className={cls(
|
||||
"flex items-center gap-2 px-4 py-2 rounded-theme card text-sm text-foreground hover:opacity-80 transition-opacity duration-300 ease-out cursor-pointer",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Icon className="h-[1em] w-auto aspect-square" />
|
||||
<span>{social.label}</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const HeroPersonalLinks = ({
|
||||
background,
|
||||
title,
|
||||
titleSegments,
|
||||
socialLinks,
|
||||
linkCards,
|
||||
ariaLabel = "Personal links section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textboxClassName = "",
|
||||
titleClassName = "",
|
||||
titleImageWrapperClassName = "",
|
||||
titleImageClassName = "",
|
||||
socialLinksClassName = "",
|
||||
socialLinkClassName = "",
|
||||
linkCardsClassName = "",
|
||||
linkCardClassName = "",
|
||||
linkCardIconClassName = "",
|
||||
linkCardTitleClassName = "",
|
||||
linkCardDescriptionClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
}: HeroPersonalLinksProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("relative w-full min-h-screen flex items-center justify-center py-20", className)}
|
||||
>
|
||||
<HeroBackgrounds {...background} />
|
||||
<div className={cls("w-content-width md:w-35 mx-auto flex flex-col items-center gap-8 relative z-10", containerClassName)}>
|
||||
<Textbox
|
||||
title={title}
|
||||
titleSegments={titleSegments}
|
||||
description=""
|
||||
textboxLayout="inline-image"
|
||||
center
|
||||
className={textboxClassName}
|
||||
titleClassName={titleClassName}
|
||||
titleImageWrapperClassName={titleImageWrapperClassName}
|
||||
titleImageClassName={titleImageClassName}
|
||||
/>
|
||||
|
||||
{socialLinks && socialLinks.length > 0 && (
|
||||
<div className={cls("flex flex-wrap justify-center gap-3", socialLinksClassName)}>
|
||||
{socialLinks.map((social, index) => (
|
||||
<SocialLinkButton
|
||||
key={index}
|
||||
social={social}
|
||||
className={socialLinkClassName}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={cls("w-full flex flex-col gap-4 mt-4", linkCardsClassName)}>
|
||||
{linkCards.map((card, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cls("w-full card rounded-theme-capped p-5 flex items-center gap-5", linkCardClassName)}
|
||||
>
|
||||
<div className={cls("relative h-10 w-auto aspect-square card shadow rounded-theme flex items-center justify-center flex-shrink-0 overflow-hidden", linkCardIconClassName)}>
|
||||
{card.videoSrc ? (
|
||||
<MediaContent
|
||||
videoSrc={card.videoSrc}
|
||||
videoAriaLabel={card.videoAriaLabel}
|
||||
imageClassName={cls("w-full h-full object-cover", card.imageClassName)}
|
||||
/>
|
||||
) : card.imageSrc ? (
|
||||
<MediaContent
|
||||
imageSrc={card.imageSrc}
|
||||
imageAlt={card.imageAlt}
|
||||
imageClassName={cls("w-full h-full object-cover", card.imageClassName)}
|
||||
/>
|
||||
) : card.icon ? (
|
||||
<card.icon className="h-4/10 w-4/10 text-foreground" strokeWidth={1.5} />
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className={cls("font-medium text-foreground", linkCardTitleClassName)}>{card.title}</h3>
|
||||
<p className={cls("text-sm text-foreground/60 truncate", linkCardDescriptionClassName)}>{card.description}</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
{...getButtonProps(card.button, 0, theme.defaultButtonVariant, buttonClassName, buttonTextClassName)}
|
||||
className={cls("flex-shrink-0", buttonClassName)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroPersonalLinks.displayName = "HeroPersonalLinks";
|
||||
|
||||
export default memo(HeroPersonalLinks);
|
||||
111
src/components/sections/hero/HeroSignup.tsx
Normal file
111
src/components/sections/hero/HeroSignup.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
"use client";
|
||||
|
||||
import TextBox from "@/components/Textbox";
|
||||
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
|
||||
import EmailSignupForm from "@/components/form/EmailSignupForm";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { LucideIcon } from "lucide-react";
|
||||
|
||||
type HeroSignupBackgroundProps = Extract<
|
||||
HeroBackgroundVariantProps,
|
||||
| { variant: "plain" }
|
||||
| { variant: "animated-grid" }
|
||||
| { variant: "canvas-reveal" }
|
||||
| { variant: "cell-wave" }
|
||||
| { variant: "downward-rays-animated" }
|
||||
| { variant: "downward-rays-animated-grid" }
|
||||
| { variant: "downward-rays-static" }
|
||||
| { variant: "downward-rays-static-grid" }
|
||||
| { variant: "glowing-orb" }
|
||||
| { variant: "glowing-orb-sparkles" }
|
||||
| { variant: "gradient-bars" }
|
||||
| { variant: "radial-gradient" }
|
||||
| { variant: "rotated-rays-animated" }
|
||||
| { variant: "rotated-rays-animated-grid" }
|
||||
| { variant: "rotated-rays-static" }
|
||||
| { variant: "rotated-rays-static-grid" }
|
||||
| { variant: "sparkles-gradient" }
|
||||
>;
|
||||
|
||||
interface HeroSignupProps {
|
||||
title: string;
|
||||
description: string;
|
||||
background: HeroSignupBackgroundProps;
|
||||
tag: string;
|
||||
tagIcon?: LucideIcon;
|
||||
inputPlaceholder?: string;
|
||||
buttonText?: string;
|
||||
onSubmit?: (email: string) => void;
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
titleClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
tagClassName?: string;
|
||||
formWrapperClassName?: string;
|
||||
formClassName?: string;
|
||||
inputClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
}
|
||||
|
||||
const HeroSignup = ({
|
||||
title,
|
||||
description,
|
||||
background,
|
||||
tag,
|
||||
tagIcon,
|
||||
inputPlaceholder = "Enter your email",
|
||||
buttonText = "Get Started",
|
||||
onSubmit,
|
||||
ariaLabel = "Hero section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
formWrapperClassName = "",
|
||||
formClassName = "",
|
||||
inputClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
}: HeroSignupProps) => {
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("relative w-full h-svh md:h-screen flex items-center justify-center", className)}
|
||||
>
|
||||
<HeroBackgrounds {...background} />
|
||||
<div className={cls("w-content-width mx-auto relative z-10", containerClassName)}>
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
className={cls("md:max-w-6/10 xl:max-w-45/100 mx-auto flex flex-col gap-3 md:gap-6", textBoxClassName)}
|
||||
titleClassName={cls("text-7xl font-medium text-balance", titleClassName)}
|
||||
descriptionClassName={cls("text-lg md:text-xl leading-tight text-balance", descriptionClassName)}
|
||||
tagClassName={cls("mb-0", tagClassName)}
|
||||
center={true}
|
||||
/>
|
||||
<div className={cls("md:max-w-6/10 xl:max-w-45/100 mx-auto mt-6", formWrapperClassName)}>
|
||||
<EmailSignupForm
|
||||
inputPlaceholder={inputPlaceholder}
|
||||
buttonText={buttonText}
|
||||
onSubmit={onSubmit}
|
||||
className={cls("w-full mx-auto", formClassName)}
|
||||
inputClassName={inputClassName}
|
||||
buttonClassName={buttonClassName}
|
||||
buttonTextClassName={buttonTextClassName}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroSignup.displayName = "HeroSignup";
|
||||
|
||||
export default HeroSignup;
|
||||
197
src/components/sections/hero/HeroSplit.tsx
Normal file
197
src/components/sections/hero/HeroSplit.tsx
Normal file
@@ -0,0 +1,197 @@
|
||||
"use client";
|
||||
|
||||
import TextBox from "@/components/Textbox";
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
|
||||
import LogoMarquee, { type MarqueeItem } from "@/components/shared/LogoMarquee";
|
||||
import { cls } from "@/lib/utils";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
import type { Avatar } from "@/components/shared/AvatarGroup";
|
||||
|
||||
type HeroSplitBackgroundProps = Extract<
|
||||
HeroBackgroundVariantProps,
|
||||
| { variant: "plain" }
|
||||
| { variant: "animated-grid" }
|
||||
| { variant: "canvas-reveal" }
|
||||
| { variant: "cell-wave" }
|
||||
| { variant: "downward-rays-animated" }
|
||||
| { variant: "downward-rays-animated-grid" }
|
||||
| { variant: "downward-rays-static" }
|
||||
| { variant: "downward-rays-static-grid" }
|
||||
| { variant: "glowing-orb" }
|
||||
| { variant: "gradient-bars" }
|
||||
| { variant: "radial-gradient" }
|
||||
| { variant: "rotated-rays-animated" }
|
||||
| { variant: "rotated-rays-animated-grid" }
|
||||
| { variant: "rotated-rays-static" }
|
||||
| { variant: "rotated-rays-static-grid" }
|
||||
| { variant: "sparkles-gradient" }
|
||||
>;
|
||||
|
||||
interface HeroSplitProps {
|
||||
title: string;
|
||||
description: string;
|
||||
background: HeroSplitBackgroundProps;
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
buttons?: ButtonConfig[];
|
||||
avatars?: Avatar[];
|
||||
avatarText?: string;
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
ariaLabel?: string;
|
||||
imagePosition?: "left" | "right";
|
||||
fixedMediaHeight?: boolean;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
titleClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
tagClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
mediaWrapperClassName?: string;
|
||||
imageClassName?: string;
|
||||
avatarGroupClassName?: string;
|
||||
marqueeItems?: MarqueeItem[];
|
||||
marqueeSpeed?: number;
|
||||
showMarqueeCard?: boolean;
|
||||
marqueeClassName?: string;
|
||||
marqueeItemClassName?: string;
|
||||
marqueeCardClassName?: string;
|
||||
marqueeImageClassName?: string;
|
||||
marqueeTextClassName?: string;
|
||||
marqueeIconClassName?: string;
|
||||
}
|
||||
|
||||
const HeroSplit = ({
|
||||
title,
|
||||
description,
|
||||
background,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
avatars,
|
||||
avatarText,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
imageAlt = "",
|
||||
videoAriaLabel = "Hero video",
|
||||
ariaLabel = "Hero section",
|
||||
imagePosition = "right",
|
||||
fixedMediaHeight = true,
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
imageClassName = "",
|
||||
avatarGroupClassName = "",
|
||||
marqueeItems,
|
||||
marqueeSpeed = 30,
|
||||
showMarqueeCard = true,
|
||||
marqueeClassName = "",
|
||||
marqueeItemClassName = "",
|
||||
marqueeCardClassName = "",
|
||||
marqueeImageClassName = "",
|
||||
marqueeTextClassName = "",
|
||||
marqueeIconClassName = "",
|
||||
}: HeroSplitProps) => {
|
||||
const mediaContent = (
|
||||
<div className={cls(
|
||||
"w-full h-fit md:w-1/2 overflow-hidden rounded-theme-capped card p-4 md:max-h-[75svh]",
|
||||
fixedMediaHeight && "h-100 md:h-[65vh]",
|
||||
mediaWrapperClassName
|
||||
)}>
|
||||
<MediaContent
|
||||
imageSrc={imageSrc}
|
||||
videoSrc={videoSrc}
|
||||
imageAlt={imageAlt}
|
||||
videoAriaLabel={videoAriaLabel}
|
||||
imageClassName={cls("h-full min-h-0", imageClassName)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("relative w-full h-fit py-hero-page-padding md:py-0 md:h-svh flex items-center", className)}
|
||||
>
|
||||
<HeroBackgrounds {...background} />
|
||||
<div className={cls("w-content-width mx-auto flex flex-col md:flex-row gap-13 md:gap-15 items-center relative z-10", containerClassName)}>
|
||||
{imagePosition === "left" && mediaContent}
|
||||
|
||||
<div className={cls("w-full md:w-1/2")}>
|
||||
{/* Mobile */}
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
buttons={buttons}
|
||||
avatars={avatars}
|
||||
avatarText={avatarText}
|
||||
avatarGroupClassName={cls("!mt-5", avatarGroupClassName)}
|
||||
className={cls("flex flex-col gap-3 md:hidden", textBoxClassName)}
|
||||
titleClassName={cls("text-7xl 2xl:text-8xl font-medium text-center md:text-left text-balance", titleClassName)}
|
||||
descriptionClassName={cls("max-w-8/10 text-lg md:text-xl leading-[1.2] text-center md:text-left", descriptionClassName)}
|
||||
tagClassName={cls("w-fit px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-3", tagClassName)}
|
||||
buttonContainerClassName={cls("flex flex-wrap gap-4 mt-2", buttonContainerClassName)}
|
||||
buttonClassName={cls("", buttonClassName)}
|
||||
buttonTextClassName={cls("text-base", buttonTextClassName)}
|
||||
center={true}
|
||||
/>
|
||||
{/* Desktop */}
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
buttons={buttons}
|
||||
avatars={avatars}
|
||||
avatarText={avatarText}
|
||||
avatarGroupClassName={cls("", avatarGroupClassName)}
|
||||
className={cls("hidden md:flex flex-col gap-3 md:gap-4", textBoxClassName)}
|
||||
titleClassName={cls("text-7xl 2xl:text-8xl font-medium text-center md:text-left text-balance", titleClassName)}
|
||||
descriptionClassName={cls("max-w-8/10 text-lg md:text-xl leading-[1.2] text-center md:text-left", descriptionClassName)}
|
||||
tagClassName={cls("w-fit px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-3", tagClassName)}
|
||||
buttonContainerClassName={cls("flex flex-wrap gap-4 mt-2", buttonContainerClassName)}
|
||||
buttonClassName={cls("", buttonClassName)}
|
||||
buttonTextClassName={cls("text-base", buttonTextClassName)}
|
||||
center={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{imagePosition === "right" && mediaContent}
|
||||
</div>
|
||||
|
||||
{marqueeItems && marqueeItems.length > 0 && (
|
||||
<LogoMarquee
|
||||
items={marqueeItems}
|
||||
speed={marqueeSpeed}
|
||||
showCard={showMarqueeCard}
|
||||
className={cls("absolute bottom-6 left-1/2 -translate-x-1/2 w-content-width z-10", marqueeClassName)}
|
||||
itemClassName={marqueeItemClassName}
|
||||
cardClassName={marqueeCardClassName}
|
||||
imageClassName={marqueeImageClassName}
|
||||
textClassName={marqueeTextClassName}
|
||||
iconClassName={marqueeIconClassName}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroSplit.displayName = "HeroSplit";
|
||||
|
||||
export default HeroSplit;
|
||||
180
src/components/sections/hero/HeroSplitDualMedia.tsx
Normal file
180
src/components/sections/hero/HeroSplitDualMedia.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
"use client";
|
||||
|
||||
import TextBox from "@/components/Textbox";
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { Star } from "lucide-react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
|
||||
type HeroSplitDualMediaBackgroundProps = Extract<
|
||||
HeroBackgroundVariantProps,
|
||||
| { variant: "plain" }
|
||||
| { variant: "animated-grid" }
|
||||
| { variant: "canvas-reveal" }
|
||||
| { variant: "cell-wave" }
|
||||
| { variant: "downward-rays-animated" }
|
||||
| { variant: "downward-rays-animated-grid" }
|
||||
| { variant: "downward-rays-static" }
|
||||
| { variant: "downward-rays-static-grid" }
|
||||
| { variant: "glowing-orb" }
|
||||
| { variant: "gradient-bars" }
|
||||
| { variant: "radial-gradient" }
|
||||
| { variant: "rotated-rays-animated" }
|
||||
| { variant: "rotated-rays-animated-grid" }
|
||||
| { variant: "rotated-rays-static" }
|
||||
| { variant: "rotated-rays-static-grid" }
|
||||
| { variant: "sparkles-gradient" }
|
||||
>;
|
||||
|
||||
interface MediaItem {
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
}
|
||||
|
||||
interface HeroSplitDualMediaProps {
|
||||
title: string;
|
||||
description: string;
|
||||
background: HeroSplitDualMediaBackgroundProps;
|
||||
tag: string;
|
||||
tagIcon?: LucideIcon;
|
||||
buttons?: ButtonConfig[];
|
||||
mediaItems: [MediaItem, MediaItem];
|
||||
rating: number;
|
||||
ratingText: string;
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
titleClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
tagClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
mediaWrapperClassName?: string;
|
||||
mediaItemClassName?: string;
|
||||
imageClassName?: string;
|
||||
ratingClassName?: string;
|
||||
ratingTextClassName?: string;
|
||||
}
|
||||
|
||||
const HeroSplitDualMedia = ({
|
||||
title,
|
||||
description,
|
||||
background,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
mediaItems,
|
||||
rating,
|
||||
ratingText,
|
||||
ariaLabel = "Hero section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
mediaItemClassName = "",
|
||||
imageClassName = "",
|
||||
ratingClassName = "",
|
||||
ratingTextClassName = "",
|
||||
}: HeroSplitDualMediaProps) => {
|
||||
const mediaContent = (
|
||||
<div className={cls("w-full md:w-1/2 grid grid-cols-2 gap-4", mediaWrapperClassName)}>
|
||||
{mediaItems.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cls(
|
||||
"w-full h-100 md:h-[55vh] overflow-hidden rounded-theme-capped card p-3",
|
||||
mediaItemClassName
|
||||
)}
|
||||
>
|
||||
<MediaContent
|
||||
imageSrc={item.imageSrc}
|
||||
videoSrc={item.videoSrc}
|
||||
imageAlt={item.imageAlt}
|
||||
videoAriaLabel={item.videoAriaLabel}
|
||||
imageClassName={cls("h-full w-full object-cover", imageClassName)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
const ratingElement = (
|
||||
<div className={cls("w-full min-w-0 md:w-75/100 flex flex-col gap-6 mt-8", ratingClassName)}>
|
||||
<div className="w-full h-px bg-background-accent" />
|
||||
<div className="w-full min-w-0 flex items-center justify-center md:justify-start gap-3 text-base md:text-xl">
|
||||
<div className="flex items-center gap-1">
|
||||
{Array.from({ length: rating }).map((_, index) => (
|
||||
<Star
|
||||
key={index}
|
||||
className="h-[1em] w-auto aspect-square text-primary-cta fill-primary-cta"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="h-[1em] w-px bg-background-accent" />
|
||||
<p className={cls("text-foreground truncate", ratingTextClassName)}>{ratingText}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("relative w-full h-fit py-hero-page-padding md:py-0 md:h-svh flex items-center", className)}
|
||||
>
|
||||
<HeroBackgrounds {...background} />
|
||||
<div className={cls("w-content-width mx-auto flex flex-col md:flex-row gap-13 md:gap-15 items-center relative z-10", containerClassName)}>
|
||||
<div className="w-full md:w-1/2">
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
buttons={buttons}
|
||||
className={cls("flex flex-col gap-3 md:gap-4 md:hidden", textBoxClassName)}
|
||||
titleClassName={cls("text-7xl 2xl:text-8xl font-medium text-center md:text-left text-balance", titleClassName)}
|
||||
descriptionClassName={cls("max-w-8/10 text-lg md:text-xl leading-[1.2] text-center md:text-left", descriptionClassName)}
|
||||
tagClassName={cls("w-fit px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-3", tagClassName)}
|
||||
buttonContainerClassName={cls("flex flex-wrap gap-4 mt-2", buttonContainerClassName)}
|
||||
buttonClassName={buttonClassName}
|
||||
buttonTextClassName={cls("text-base", buttonTextClassName)}
|
||||
center={true}
|
||||
/>
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
buttons={buttons}
|
||||
className={cls("hidden md:flex flex-col gap-3 md:gap-4", textBoxClassName)}
|
||||
titleClassName={cls("text-7xl 2xl:text-8xl font-medium text-center md:text-left text-balance", titleClassName)}
|
||||
descriptionClassName={cls("max-w-8/10 text-lg md:text-xl leading-[1.2] text-center md:text-left", descriptionClassName)}
|
||||
tagClassName={cls("w-fit px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-3", tagClassName)}
|
||||
buttonContainerClassName={cls("flex flex-wrap gap-4 mt-2", buttonContainerClassName)}
|
||||
buttonClassName={buttonClassName}
|
||||
buttonTextClassName={cls("text-base", buttonTextClassName)}
|
||||
center={false}
|
||||
/>
|
||||
{ratingElement}
|
||||
</div>
|
||||
|
||||
{mediaContent}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroSplitDualMedia.displayName = "HeroSplitDualMedia";
|
||||
|
||||
export default HeroSplitDualMedia;
|
||||
310
src/components/sections/hero/HeroSplitKpi.tsx
Normal file
310
src/components/sections/hero/HeroSplitKpi.tsx
Normal file
@@ -0,0 +1,310 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import TextBox from "@/components/Textbox";
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
|
||||
import LogoMarquee, { type MarqueeItem } from "@/components/shared/LogoMarquee";
|
||||
import { cls } from "@/lib/utils";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
import type { Avatar } from "@/components/shared/AvatarGroup";
|
||||
|
||||
const MOBILE_BREAKPOINT = 768;
|
||||
|
||||
const useKpiAnimation = (enableAnimation: boolean = true) => {
|
||||
const sectionRef = useRef<HTMLElement>(null);
|
||||
const boxRef1 = useRef<HTMLDivElement>(null);
|
||||
const boxRef2 = useRef<HTMLDivElement>(null);
|
||||
const boxRef3 = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enableAnimation) return;
|
||||
|
||||
const isMobile = window.innerWidth <= MOBILE_BREAKPOINT;
|
||||
if (isMobile) return;
|
||||
|
||||
let mouseX = 0;
|
||||
let mouseY = 0;
|
||||
|
||||
let box1X = 0, box1Y = 0;
|
||||
let box2X = 0, box2Y = 0;
|
||||
let box3X = 0, box3Y = 0;
|
||||
|
||||
const speed = 0.025;
|
||||
|
||||
const handleMouseMove = (event: MouseEvent): void => {
|
||||
mouseX = (event.clientX / window.innerWidth) * 100 - 50;
|
||||
mouseY = (event.clientY / window.innerHeight) * 100 - 50;
|
||||
};
|
||||
|
||||
const animate = (): void => {
|
||||
// Box 1 movement
|
||||
const distX1 = (mouseX * -0.25) - box1X;
|
||||
const distY1 = (mouseY * -0.25) - box1Y;
|
||||
box1X += distX1 * speed;
|
||||
box1Y += distY1 * speed;
|
||||
|
||||
// Box 2 movement
|
||||
const distX2 = (mouseX * -0.5) - box2X;
|
||||
const distY2 = (mouseY * -0.5) - box2Y;
|
||||
box2X += distX2 * speed;
|
||||
box2Y += distY2 * speed;
|
||||
|
||||
// Box 3 movement
|
||||
const distX3 = (mouseX * 0.25) - box3X;
|
||||
const distY3 = (mouseY * 0.25) - box3Y;
|
||||
box3X += distX3 * speed;
|
||||
box3Y += distY3 * speed;
|
||||
|
||||
// Apply transforms
|
||||
if (boxRef1.current) {
|
||||
boxRef1.current.style.transform = `translate(${box1X}px, ${box1Y}px)`;
|
||||
}
|
||||
if (boxRef2.current) {
|
||||
boxRef2.current.style.transform = `translate(${box2X}px, ${box2Y}px)`;
|
||||
}
|
||||
if (boxRef3.current) {
|
||||
boxRef3.current.style.transform = `translate(${box3X}px, ${box3Y}px)`;
|
||||
}
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
animate();
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
};
|
||||
}, [enableAnimation]);
|
||||
|
||||
return { sectionRef, boxRef1, boxRef2, boxRef3 };
|
||||
};
|
||||
|
||||
type HeroSplitKpiBackgroundProps = Extract<
|
||||
HeroBackgroundVariantProps,
|
||||
| { variant: "plain" }
|
||||
| { variant: "animated-grid" }
|
||||
| { variant: "canvas-reveal" }
|
||||
| { variant: "cell-wave" }
|
||||
| { variant: "downward-rays-animated" }
|
||||
| { variant: "downward-rays-animated-grid" }
|
||||
| { variant: "downward-rays-static" }
|
||||
| { variant: "downward-rays-static-grid" }
|
||||
| { variant: "glowing-orb" }
|
||||
| { variant: "gradient-bars" }
|
||||
| { variant: "radial-gradient" }
|
||||
| { variant: "rotated-rays-animated" }
|
||||
| { variant: "rotated-rays-animated-grid" }
|
||||
| { variant: "rotated-rays-static" }
|
||||
| { variant: "rotated-rays-static-grid" }
|
||||
| { variant: "sparkles-gradient" }
|
||||
>;
|
||||
|
||||
interface KpiItem {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface HeroSplitKpiProps {
|
||||
title: string;
|
||||
description: string;
|
||||
background: HeroSplitKpiBackgroundProps;
|
||||
kpis: [KpiItem, KpiItem, KpiItem];
|
||||
enableKpiAnimation: boolean;
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
buttons?: ButtonConfig[];
|
||||
avatars?: Avatar[];
|
||||
avatarText?: string;
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
ariaLabel?: string;
|
||||
imagePosition?: "left" | "right";
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
titleClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
tagClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
mediaWrapperClassName?: string;
|
||||
imageClassName?: string;
|
||||
avatarGroupClassName?: string;
|
||||
kpiClassName?: string;
|
||||
kpiValueClassName?: string;
|
||||
kpiLabelClassName?: string;
|
||||
marqueeItems?: MarqueeItem[];
|
||||
marqueeSpeed?: number;
|
||||
showMarqueeCard?: boolean;
|
||||
marqueeClassName?: string;
|
||||
marqueeItemClassName?: string;
|
||||
marqueeCardClassName?: string;
|
||||
marqueeImageClassName?: string;
|
||||
marqueeTextClassName?: string;
|
||||
marqueeIconClassName?: string;
|
||||
}
|
||||
|
||||
const HeroSplitKpi = ({
|
||||
title,
|
||||
description,
|
||||
background,
|
||||
kpis,
|
||||
enableKpiAnimation,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
avatars,
|
||||
avatarText,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
imageAlt = "",
|
||||
videoAriaLabel = "Hero video",
|
||||
ariaLabel = "Hero section",
|
||||
imagePosition = "right",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
imageClassName = "",
|
||||
avatarGroupClassName = "",
|
||||
kpiClassName = "",
|
||||
kpiValueClassName = "",
|
||||
kpiLabelClassName = "",
|
||||
marqueeItems,
|
||||
marqueeSpeed = 30,
|
||||
showMarqueeCard = true,
|
||||
marqueeClassName = "",
|
||||
marqueeItemClassName = "",
|
||||
marqueeCardClassName = "",
|
||||
marqueeImageClassName = "",
|
||||
marqueeTextClassName = "",
|
||||
marqueeIconClassName = "",
|
||||
}: HeroSplitKpiProps) => {
|
||||
const { sectionRef, boxRef1, boxRef2, boxRef3 } = useKpiAnimation(enableKpiAnimation);
|
||||
const boxRefs = [boxRef1, boxRef2, boxRef3];
|
||||
const mediaContent = (
|
||||
<div
|
||||
className={cls(
|
||||
"relative w-full h-fit md:w-1/2 aspect-square md:aspect-auto md:h-[65vh]",
|
||||
mediaWrapperClassName
|
||||
)}
|
||||
>
|
||||
<div className="relative h-full scale-75 w-full overflow-hidden rounded-theme-capped card p-4">
|
||||
<MediaContent
|
||||
imageSrc={imageSrc}
|
||||
videoSrc={videoSrc}
|
||||
imageAlt={imageAlt}
|
||||
videoAriaLabel={videoAriaLabel}
|
||||
imageClassName={cls("h-full min-h-0", imageClassName)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{kpis.map((kpi, index) => (
|
||||
<div
|
||||
key={index}
|
||||
ref={boxRefs[index]}
|
||||
className={cls(
|
||||
"absolute! card backdrop-blur-xs rounded-theme-capped px-4 py-3 md:px-6 md:py-4 flex flex-col items-center",
|
||||
index === 0 && "top-[5%] left-[5%] md:top-[0%] md:left-[0%]",
|
||||
index === 1 && "top-[35%] right-[2.5%] md:top-[35%]",
|
||||
index === 2 && "bottom-[7.5%] left-[10%] md:left-[7.5%] md:bottom-[0%]",
|
||||
kpiClassName
|
||||
)}
|
||||
>
|
||||
<p className={cls("text-2xl md:text-4xl font-medium text-foreground", kpiValueClassName)}>
|
||||
{kpi.value}
|
||||
</p>
|
||||
<p className={cls("text-sm md:text-base text-foreground/70", kpiLabelClassName)}>
|
||||
{kpi.label}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={sectionRef}
|
||||
aria-label={ariaLabel}
|
||||
className={cls("relative w-full h-fit py-hero-page-padding md:py-0 md:h-svh flex items-center", className)}
|
||||
>
|
||||
<HeroBackgrounds {...background} />
|
||||
<div className={cls("w-content-width mx-auto flex flex-col md:flex-row gap-13 md:gap-15 items-center relative z-10", containerClassName)}>
|
||||
{imagePosition === "left" && mediaContent}
|
||||
|
||||
<div className={cls("w-full md:w-1/2")}>
|
||||
{/* Mobile */}
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
buttons={buttons}
|
||||
avatars={avatars}
|
||||
avatarText={avatarText}
|
||||
avatarGroupClassName={cls("!mt-5", avatarGroupClassName)}
|
||||
className={cls("flex flex-col gap-3 md:hidden", textBoxClassName)}
|
||||
titleClassName={cls("text-7xl 2xl:text-8xl font-medium text-center md:text-left text-balance", titleClassName)}
|
||||
descriptionClassName={cls("max-w-8/10 text-lg md:text-xl leading-[1.2] text-center md:text-left", descriptionClassName)}
|
||||
tagClassName={cls("w-fit px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-3", tagClassName)}
|
||||
buttonContainerClassName={cls("flex flex-wrap gap-4 mt-2", buttonContainerClassName)}
|
||||
buttonClassName={cls("", buttonClassName)}
|
||||
buttonTextClassName={cls("text-base", buttonTextClassName)}
|
||||
center={true}
|
||||
/>
|
||||
{/* Desktop */}
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
buttons={buttons}
|
||||
avatars={avatars}
|
||||
avatarText={avatarText}
|
||||
avatarGroupClassName={cls("", avatarGroupClassName)}
|
||||
className={cls("hidden md:flex flex-col gap-3 md:gap-4", textBoxClassName)}
|
||||
titleClassName={cls("text-7xl 2xl:text-8xl font-medium text-center md:text-left text-balance", titleClassName)}
|
||||
descriptionClassName={cls("max-w-8/10 text-lg md:text-xl leading-[1.2] text-center md:text-left", descriptionClassName)}
|
||||
tagClassName={cls("w-fit px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-3", tagClassName)}
|
||||
buttonContainerClassName={cls("flex flex-wrap gap-4 mt-2", buttonContainerClassName)}
|
||||
buttonClassName={cls("", buttonClassName)}
|
||||
buttonTextClassName={cls("text-base", buttonTextClassName)}
|
||||
center={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{imagePosition === "right" && mediaContent}
|
||||
</div>
|
||||
|
||||
{marqueeItems && marqueeItems.length > 0 && (
|
||||
<LogoMarquee
|
||||
items={marqueeItems}
|
||||
speed={marqueeSpeed}
|
||||
showCard={showMarqueeCard}
|
||||
className={cls("absolute bottom-6 left-1/2 -translate-x-1/2 w-content-width z-10", marqueeClassName)}
|
||||
itemClassName={marqueeItemClassName}
|
||||
cardClassName={marqueeCardClassName}
|
||||
imageClassName={marqueeImageClassName}
|
||||
textClassName={marqueeTextClassName}
|
||||
iconClassName={marqueeIconClassName}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroSplitKpi.displayName = "HeroSplitKpi";
|
||||
|
||||
export default HeroSplitKpi;
|
||||
@@ -0,0 +1,171 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback } from "react";
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
import FillWidthText, { hasDescenders } from "@/components/shared/FillWidthText/FillWidthText";
|
||||
import TextAnimation from "@/components/text/TextAnimation";
|
||||
import Button from "@/components/button/Button";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { getButtonProps } from "@/lib/buttonUtils";
|
||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||
import { useCarouselFullscreen } from "./useCarouselFullscreen";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
|
||||
const MASK_GRADIENT = "linear-gradient(to bottom, transparent, black 60%)";
|
||||
|
||||
interface CarouselSlide {
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
}
|
||||
|
||||
interface HeroCarouselLogoProps {
|
||||
logoText: string;
|
||||
description: string;
|
||||
buttons: ButtonConfig[];
|
||||
slides: CarouselSlide[];
|
||||
autoplayDelay?: number;
|
||||
showDimOverlay?: boolean;
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
contentContainerClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
logoContainerClassName?: string;
|
||||
logoClassName?: string;
|
||||
mediaWrapperClassName?: string;
|
||||
imageClassName?: string;
|
||||
blurClassName?: string;
|
||||
dimOverlayClassName?: string;
|
||||
progressBarClassName?: string;
|
||||
}
|
||||
|
||||
const HeroCarouselLogo = ({
|
||||
logoText,
|
||||
description,
|
||||
buttons,
|
||||
slides,
|
||||
autoplayDelay = 3000,
|
||||
showDimOverlay = false,
|
||||
ariaLabel = "Hero section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
contentContainerClassName = "",
|
||||
descriptionClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
logoContainerClassName = "",
|
||||
logoClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
imageClassName = "",
|
||||
blurClassName = "",
|
||||
dimOverlayClassName = "",
|
||||
progressBarClassName = "",
|
||||
}: HeroCarouselLogoProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const { currentSlide, progressRefs, goToSlide } = useCarouselFullscreen({
|
||||
totalSlides: slides.length,
|
||||
autoplayDelay,
|
||||
});
|
||||
|
||||
const setProgressRef = useCallback(
|
||||
(el: HTMLDivElement | null, index: number) => {
|
||||
progressRefs.current[index] = el;
|
||||
},
|
||||
[progressRefs]
|
||||
);
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("relative w-full h-svh overflow-hidden flex flex-col justify-end", className)}
|
||||
>
|
||||
<div className={cls("absolute inset-0 w-full h-full", mediaWrapperClassName)}>
|
||||
{showDimOverlay && (
|
||||
<div className={cls("absolute top-0 left-0 w-full h-full bg-background/20 z-10 pointer-events-none select-none", dimOverlayClassName)} />
|
||||
)}
|
||||
{slides.map((slide, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cls(
|
||||
"absolute inset-0 transition-opacity duration-600",
|
||||
currentSlide === index ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
aria-hidden={currentSlide !== index}
|
||||
>
|
||||
<MediaContent
|
||||
imageSrc={slide.imageSrc}
|
||||
videoSrc={slide.videoSrc}
|
||||
imageAlt={slide.imageAlt || ""}
|
||||
videoAriaLabel={slide.videoAriaLabel || "Hero video"}
|
||||
imageClassName={cls("w-full h-full object-cover !rounded-none", imageClassName)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cls(
|
||||
"absolute z-10 backdrop-blur-xl opacity-100 w-full h-[50svh] md:h-[75svh] left-0 bottom-0 pointer-events-none select-none",
|
||||
blurClassName
|
||||
)}
|
||||
style={{ maskImage: MASK_GRADIENT }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<div className={cls("relative z-20 w-content-width mx-auto h-fit flex items-end", containerClassName)}>
|
||||
<div className={cls("w-full flex flex-col", logoContainerClassName)}>
|
||||
<div className={cls("w-full flex flex-col md:flex-row md:justify-between items-start md:items-end gap-3 md:gap-6", contentContainerClassName)}>
|
||||
<div className="w-full md:w-1/2">
|
||||
<TextAnimation
|
||||
type={theme.defaultTextAnimation}
|
||||
text={description}
|
||||
variant="words-trigger"
|
||||
className={cls("text-lg md:text-2xl text-background text-balance font-medium leading-[1.2] md:max-w-1/2", descriptionClassName)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full md:w-1/2 flex justify-start md:justify-end">
|
||||
<div className={cls("flex flex-wrap gap-4", buttonContainerClassName)}>
|
||||
{buttons.slice(0, 2).map((button, index) => (
|
||||
<Button key={index} {...getButtonProps(button, index, theme.defaultButtonVariant, cls("", buttonClassName), cls("text-base", buttonTextClassName))} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex">
|
||||
<FillWidthText className={cls("text-background", !hasDescenders(logoText) && "my-10", logoClassName)}>
|
||||
{logoText}
|
||||
</FillWidthText>
|
||||
</div>
|
||||
<div className="w-full flex gap-3 pb-12 pt-6">
|
||||
{Array.from({ length: slides.length }, (_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className={cls("relative cursor-pointer h-1 w-full rounded-theme overflow-hidden bg-white/10 backdrop-blur-sm", progressBarClassName)}
|
||||
onClick={() => goToSlide(index)}
|
||||
aria-label={`Go to slide ${index + 1}`}
|
||||
aria-current={currentSlide === index ? "true" : "false"}
|
||||
>
|
||||
<div
|
||||
ref={(el) => setProgressRef(el, index)}
|
||||
className="absolute inset-0 bg-white rounded-theme"
|
||||
style={{ transform: "translateX(-100%)" }}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroCarouselLogo.displayName = "HeroCarouselLogo";
|
||||
|
||||
export default HeroCarouselLogo;
|
||||
@@ -0,0 +1,103 @@
|
||||
import { useEffect, useRef, useState, useCallback } from "react";
|
||||
|
||||
interface UseCarouselFullscreenProps {
|
||||
totalSlides: number;
|
||||
autoplayDelay?: number;
|
||||
}
|
||||
|
||||
interface UseCarouselFullscreenReturn {
|
||||
currentSlide: number;
|
||||
progressRefs: React.MutableRefObject<(HTMLDivElement | null)[]>;
|
||||
goToSlide: (index: number) => void;
|
||||
}
|
||||
|
||||
export const useCarouselFullscreen = ({
|
||||
totalSlides,
|
||||
autoplayDelay = 3000,
|
||||
}: UseCarouselFullscreenProps): UseCarouselFullscreenReturn => {
|
||||
const [currentSlide, setCurrentSlide] = useState(0);
|
||||
const progressRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
const animationFrameRef = useRef<number | null>(null);
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const previousSlideRef = useRef<number>(0);
|
||||
|
||||
const resetProgressBars = useCallback(
|
||||
(fromIndex: number, isLooping: boolean = false) => {
|
||||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
}
|
||||
|
||||
progressRefs.current.forEach((bar, index) => {
|
||||
if (bar) {
|
||||
if (isLooping || index >= fromIndex) {
|
||||
bar.style.transform = "translateX(-100%)";
|
||||
} else {
|
||||
bar.style.transform = "translateX(0%)";
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const animateProgress = useCallback(
|
||||
(index: number, prevIndex: number) => {
|
||||
const isLooping = prevIndex === totalSlides - 1 && index === 0;
|
||||
resetProgressBars(index, isLooping);
|
||||
|
||||
if (!progressRefs.current[index]) return;
|
||||
|
||||
let startTime: number | null = null;
|
||||
|
||||
const animate = (timestamp: number) => {
|
||||
if (!startTime) startTime = timestamp;
|
||||
const elapsed = timestamp - startTime;
|
||||
const progress = Math.min(elapsed / autoplayDelay, 1);
|
||||
|
||||
const translateValue = -100 + progress * 100;
|
||||
if (progressRefs.current[index]) {
|
||||
progressRefs.current[index]!.style.transform = `translateX(${translateValue}%)`;
|
||||
}
|
||||
|
||||
if (progress < 1) {
|
||||
animationFrameRef.current = requestAnimationFrame(animate);
|
||||
}
|
||||
};
|
||||
|
||||
animationFrameRef.current = requestAnimationFrame(animate);
|
||||
},
|
||||
[autoplayDelay, resetProgressBars, totalSlides]
|
||||
);
|
||||
|
||||
const goToSlide = useCallback((index: number) => {
|
||||
setCurrentSlide(index);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
animateProgress(currentSlide, previousSlideRef.current);
|
||||
previousSlideRef.current = currentSlide;
|
||||
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
}
|
||||
|
||||
intervalRef.current = setInterval(() => {
|
||||
setCurrentSlide((prev) => (prev + 1) % totalSlides);
|
||||
}, autoplayDelay);
|
||||
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
}
|
||||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
}
|
||||
};
|
||||
}, [currentSlide, totalSlides, autoplayDelay, animateProgress]);
|
||||
|
||||
return {
|
||||
currentSlide,
|
||||
progressRefs,
|
||||
goToSlide,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user