Initial commit
This commit is contained in:
127
src/components/sections/hero/HeroBillboard.tsx
Normal file
127
src/components/sections/hero/HeroBillboard.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
"use client";
|
||||
|
||||
import TextBox from "@/components/Textbox";
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { Plus } from "lucide-react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
|
||||
interface HeroBillboardProps {
|
||||
title: string;
|
||||
description: string;
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
buttons?: ButtonConfig[];
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
frameStyle?: "card" | "browser";
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
titleClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
tagClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
mediaWrapperClassName?: string;
|
||||
imageClassName?: string;
|
||||
browserBarClassName?: string;
|
||||
addressBarClassName?: string;
|
||||
}
|
||||
|
||||
const HeroBillboard = ({
|
||||
title,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
imageAlt = "",
|
||||
videoAriaLabel = "Hero video",
|
||||
frameStyle = "card",
|
||||
ariaLabel = "Hero section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
imageClassName = "",
|
||||
browserBarClassName = "",
|
||||
addressBarClassName = "",
|
||||
}: HeroBillboardProps) => {
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("w-full py-hero-page-padding", className)}
|
||||
>
|
||||
<div className={cls("w-content-width mx-auto flex flex-col gap-14 md:gap-15", 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 gap-4 mt-3", buttonContainerClassName)}
|
||||
buttonClassName={buttonClassName}
|
||||
buttonTextClassName={buttonTextClassName}
|
||||
center={true}
|
||||
/>
|
||||
{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("rounded-none!", 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", imageClassName)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroBillboard.displayName = "HeroBillboard";
|
||||
|
||||
export default HeroBillboard;
|
||||
126
src/components/sections/hero/HeroBillboardCarousel.tsx
Normal file
126
src/components/sections/hero/HeroBillboardCarousel.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
"use client";
|
||||
|
||||
import TextBox from "@/components/Textbox";
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
import AutoCarousel from "@/components/cardStack/layouts/carousels/AutoCarousel";
|
||||
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;
|
||||
}
|
||||
|
||||
interface HeroBillboardCarouselProps {
|
||||
title: string;
|
||||
description: string;
|
||||
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,
|
||||
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(
|
||||
"w-full py-hero-page-padding md:h-svh md:py-0",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className={cls(
|
||||
"mx-auto flex flex-col gap-14 md:gap-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 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;
|
||||
112
src/components/sections/hero/HeroBillboardCarouselSplit.tsx
Normal file
112
src/components/sections/hero/HeroBillboardCarouselSplit.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
"use client";
|
||||
|
||||
import TextBox from "@/components/Textbox";
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
import AutoCarousel from "@/components/cardStack/layouts/carousels/AutoCarousel";
|
||||
import { cls } from "@/lib/utils";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
|
||||
export interface MediaItem {
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
}
|
||||
|
||||
interface HeroBillboardCarouselSplitProps {
|
||||
title: string;
|
||||
buttons?: ButtonConfig[];
|
||||
mediaItems: MediaItem[];
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
titleClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
mediaWrapperClassName?: string;
|
||||
}
|
||||
|
||||
const HeroBillboardCarouselSplit = ({
|
||||
title,
|
||||
buttons,
|
||||
mediaItems,
|
||||
ariaLabel = "Hero section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
}: HeroBillboardCarouselSplitProps) => {
|
||||
const renderCarouselItem = (item: MediaItem, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className="w-full aspect-square overflow-hidden rounded-theme-capped card p-2"
|
||||
>
|
||||
<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
|
||||
)}
|
||||
>
|
||||
<div className={cls(
|
||||
"mx-auto flex flex-col gap-14 md:gap-10 pt-hero-page-padding-half",
|
||||
"w-full md:w-content-width md:h-full md:items-center md:justify-center",
|
||||
containerClassName
|
||||
)}>
|
||||
<TextBox
|
||||
title={title}
|
||||
description=""
|
||||
buttons={buttons}
|
||||
textboxLayout="split-actions"
|
||||
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)}
|
||||
buttonContainerClassName={cls("flex 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>
|
||||
);
|
||||
};
|
||||
|
||||
HeroBillboardCarouselSplit.displayName = "HeroBillboardCarouselSplit";
|
||||
|
||||
export default HeroBillboardCarouselSplit;
|
||||
166
src/components/sections/hero/HeroBillboardGallery.tsx
Normal file
166
src/components/sections/hero/HeroBillboardGallery.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
"use client";
|
||||
|
||||
import TextBox from "@/components/Textbox";
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
import AutoCarousel from "@/components/cardStack/layouts/carousels/AutoCarousel";
|
||||
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;
|
||||
}
|
||||
|
||||
interface HeroBillboardGalleryProps {
|
||||
title: string;
|
||||
description: string;
|
||||
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,
|
||||
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(
|
||||
"w-full py-hero-page-padding md:h-svh md:py-0",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className={cls(
|
||||
"mx-auto flex flex-col gap-14",
|
||||
"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 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;
|
||||
196
src/components/sections/hero/HeroBillboardMetrics.tsx
Normal file
196
src/components/sections/hero/HeroBillboardMetrics.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
"use client";
|
||||
|
||||
import TextBox from "@/components/Textbox";
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { Plus } from "lucide-react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
|
||||
interface Metric {
|
||||
id: string;
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface HeroBillboardMetricsProps {
|
||||
title: string;
|
||||
description: string;
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
buttons?: ButtonConfig[];
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
frameStyle?: "card" | "browser";
|
||||
metricsLabel: string;
|
||||
metrics: Metric[];
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
titleClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
tagClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
mediaWrapperClassName?: string;
|
||||
imageClassName?: string;
|
||||
browserBarClassName?: string;
|
||||
addressBarClassName?: string;
|
||||
metricsContainerClassName?: string;
|
||||
metricsLabelClassName?: string;
|
||||
metricsGridClassName?: string;
|
||||
metricClassName?: string;
|
||||
metricValueClassName?: string;
|
||||
metricLabelClassName?: string;
|
||||
}
|
||||
|
||||
const HeroBillboardMetrics = ({
|
||||
title,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
imageAlt = "",
|
||||
videoAriaLabel = "Hero video",
|
||||
frameStyle = "card",
|
||||
metricsLabel,
|
||||
metrics,
|
||||
ariaLabel = "Hero section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
imageClassName = "",
|
||||
browserBarClassName = "",
|
||||
addressBarClassName = "",
|
||||
metricsContainerClassName = "",
|
||||
metricsLabelClassName = "",
|
||||
metricsGridClassName = "",
|
||||
metricClassName = "",
|
||||
metricValueClassName = "",
|
||||
metricLabelClassName = "",
|
||||
}: HeroBillboardMetricsProps) => {
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("w-full py-hero-page-padding", className)}
|
||||
>
|
||||
<div className={cls("w-content-width mx-auto flex flex-col gap-12 md:gap-11", containerClassName)}>
|
||||
<div className="flex flex-col gap-14 md:gap-15" >
|
||||
<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 gap-4 mt-3", buttonContainerClassName)}
|
||||
buttonClassName={buttonClassName}
|
||||
buttonTextClassName={buttonTextClassName}
|
||||
center={true}
|
||||
/>
|
||||
{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("rounded-none!", 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", imageClassName)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={cls(
|
||||
"relative w-full flex flex-col md:flex-row md:items-center justify-between gap-4 md:gap-10 border-t border-accent pt-10",
|
||||
metricsContainerClassName
|
||||
)}>
|
||||
<div className="relative w-full md:w-1/3" >
|
||||
<p className={cls(
|
||||
"text-base text-foreground/75 leading-tight text-balance",
|
||||
metricsLabelClassName
|
||||
)}>
|
||||
{metricsLabel}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={cls(
|
||||
"w-full md:w-1/2 grid grid-cols-2 gap-4 md:gap-6",
|
||||
metrics.length === 2 && "md:grid-cols-2",
|
||||
metrics.length === 3 && "md:grid-cols-3",
|
||||
metricsGridClassName
|
||||
)}>
|
||||
{metrics.map((metric, index) => (
|
||||
<div
|
||||
key={metric.id}
|
||||
className={cls(
|
||||
"relative card rounded-theme-capped flex flex-col gap-0 p-4",
|
||||
metrics.length === 3 && index === 2 && "col-span-2 md:col-span-1",
|
||||
metricClassName
|
||||
)}
|
||||
>
|
||||
<h3 className={cls(
|
||||
"relative w-full min-w-0 text-4xl font-medium text-foreground truncate",
|
||||
metricValueClassName
|
||||
)}>
|
||||
{metric.value}
|
||||
</h3>
|
||||
<p className={cls(
|
||||
"relative w-full min-w-0 text-sm text-foreground/70 truncate",
|
||||
metricLabelClassName
|
||||
)}>
|
||||
{metric.label}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroBillboardMetrics.displayName = "HeroBillboardMetrics";
|
||||
|
||||
export default HeroBillboardMetrics;
|
||||
@@ -0,0 +1,98 @@
|
||||
"use client";
|
||||
|
||||
|
||||
import TextBox from "@/components/Textbox";
|
||||
import AngledCarousel from "@/components/cardStack/layouts/carousels/AngledCarousel";
|
||||
import { cls } from "@/lib/utils";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
|
||||
interface CarouselItem {
|
||||
id: string;
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
}
|
||||
|
||||
interface HeroBillboardRotatedCarouselProps {
|
||||
title: string;
|
||||
description: string;
|
||||
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,
|
||||
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("w-full h-fit flex items-center justify-center py-hero-page-padding", className)}
|
||||
>
|
||||
<div className={cls("w-full flex flex-col gap-14 md:gap-15", 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 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;
|
||||
139
src/components/sections/hero/HeroBillboardScroll.tsx
Normal file
139
src/components/sections/hero/HeroBillboardScroll.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
"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 { cls } from "@/lib/utils";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
|
||||
interface HeroBillboardScrollProps {
|
||||
title: string;
|
||||
description: string;
|
||||
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,
|
||||
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)}
|
||||
>
|
||||
<div
|
||||
className={cls("py-hero-page-padding w-full relative", 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 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;
|
||||
136
src/components/sections/hero/HeroBillboardSplit.tsx
Normal file
136
src/components/sections/hero/HeroBillboardSplit.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
"use client";
|
||||
|
||||
import TextBox from "@/components/Textbox";
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { Plus, LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
|
||||
interface HeroBillboardSplitProps {
|
||||
title: string;
|
||||
description: string;
|
||||
tag: string;
|
||||
tagIcon?: LucideIcon;
|
||||
buttons: ButtonConfig[];
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
frameStyle?: "browser" | "card";
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
titleClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
tagClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
mediaWrapperClassName?: string;
|
||||
browserBarClassName?: string;
|
||||
addressBarClassName?: string;
|
||||
imageClassName?: string;
|
||||
}
|
||||
|
||||
const HeroBillboardSplit = ({
|
||||
title,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
imageAlt = "",
|
||||
videoAriaLabel = "Hero media",
|
||||
frameStyle = "browser",
|
||||
ariaLabel = "Hero section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
browserBarClassName = "",
|
||||
addressBarClassName = "",
|
||||
imageClassName = "",
|
||||
}: HeroBillboardSplitProps) => {
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls(
|
||||
"w-full h-fit py-hero-page-padding",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className={cls(
|
||||
"relative w-content-width mx-auto flex flex-col gap-14 md:gap-10",
|
||||
containerClassName
|
||||
)}>
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
buttons={buttons}
|
||||
textboxLayout="split"
|
||||
className={cls(
|
||||
"w-content-width mx-auto",
|
||||
textBoxClassName
|
||||
)}
|
||||
titleClassName={titleClassName}
|
||||
descriptionClassName={descriptionClassName}
|
||||
tagClassName={tagClassName}
|
||||
buttonContainerClassName={buttonContainerClassName}
|
||||
buttonClassName={buttonClassName}
|
||||
buttonTextClassName={buttonTextClassName}
|
||||
/>
|
||||
|
||||
{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("rounded-none!", 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", imageClassName)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroBillboardSplit.displayName = "HeroBillboardSplit";
|
||||
|
||||
export default HeroBillboardSplit;
|
||||
123
src/components/sections/hero/HeroChatPrompt.tsx
Normal file
123
src/components/sections/hero/HeroChatPrompt.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
"use client";
|
||||
|
||||
import TextBox from "@/components/Textbox";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { ArrowUp } from "lucide-react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
|
||||
interface FeatureTag {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: LucideIcon;
|
||||
}
|
||||
|
||||
interface HeroChatPromptProps {
|
||||
title: string;
|
||||
description: string;
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
promptText?: string;
|
||||
featureTags: FeatureTag[];
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
titleClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
tagClassName?: string;
|
||||
promptContainerClassName?: string;
|
||||
promptTextClassName?: string;
|
||||
promptButtonClassName?: string;
|
||||
featureTagsClassName?: string;
|
||||
featureTagClassName?: string;
|
||||
}
|
||||
|
||||
const HeroChatPrompt = ({
|
||||
title,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
promptText = "Ask me anything...",
|
||||
featureTags,
|
||||
ariaLabel = "Hero section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
promptContainerClassName = "",
|
||||
promptTextClassName = "",
|
||||
promptButtonClassName = "",
|
||||
featureTagsClassName = "",
|
||||
featureTagClassName = "",
|
||||
}: HeroChatPromptProps) => {
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("relative w-full h-svh flex items-center justify-center", className)}
|
||||
>
|
||||
<div className={cls("w-content-width mx-auto flex flex-col gap-8", containerClassName)}>
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
className={cls("flex flex-col gap-3 md:gap-1", textBoxClassName)}
|
||||
titleClassName={cls("text-6xl font-medium text-balance", titleClassName)}
|
||||
descriptionClassName={cls("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)}
|
||||
center={true}
|
||||
/>
|
||||
|
||||
<div className={cls(
|
||||
"w-full md:w-50 mx-auto card p-6 rounded-theme-capped flex flex-col gap-10",
|
||||
promptContainerClassName
|
||||
)}>
|
||||
<p className={cls(
|
||||
"relative z-1 flex-1 text-lg text-foreground truncate",
|
||||
promptTextClassName
|
||||
)}>
|
||||
{promptText}
|
||||
</p>
|
||||
<div className="relative z-1 w-full flex justify-end" >
|
||||
<div
|
||||
className={cls(
|
||||
"h-8 w-[var(--height-8)] rounded-theme primary-button flex items-center justify-center",
|
||||
promptButtonClassName
|
||||
)}
|
||||
>
|
||||
<ArrowUp className="w-1/2 h-1/2 text-background" strokeWidth={1.5} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={cls(
|
||||
"w-full md:w-50 mx-auto flex flex-wrap items-center justify-center gap-3",
|
||||
featureTagsClassName
|
||||
)}>
|
||||
{featureTags.map((featureTag) => {
|
||||
const FeatureIcon = featureTag.icon;
|
||||
return (
|
||||
<div
|
||||
key={featureTag.id}
|
||||
className={cls(
|
||||
"px-4 py-2 text-sm card rounded-theme flex items-center gap-2",
|
||||
featureTagClassName
|
||||
)}
|
||||
>
|
||||
<FeatureIcon className="relative z-1 h-[1em] w-auto" />
|
||||
<span className="relative z-1" >{featureTag.label}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroChatPrompt.displayName = "HeroChatPrompt";
|
||||
|
||||
export default HeroChatPrompt;
|
||||
185
src/components/sections/hero/HeroChatPromptFeatures.tsx
Normal file
185
src/components/sections/hero/HeroChatPromptFeatures.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
"use client";
|
||||
|
||||
import TextBox from "@/components/Textbox";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { ArrowUp } from "lucide-react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
|
||||
interface FeatureTag {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: LucideIcon;
|
||||
}
|
||||
|
||||
interface FeatureHighlight {
|
||||
id: string;
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
}
|
||||
|
||||
interface HeroChatPromptFeaturesProps {
|
||||
title: string;
|
||||
description: string;
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
promptText?: string;
|
||||
featureTags: FeatureTag[];
|
||||
featureHighlights: FeatureHighlight[];
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
titleClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
tagClassName?: string;
|
||||
promptContainerClassName?: string;
|
||||
promptTextClassName?: string;
|
||||
promptButtonClassName?: string;
|
||||
featureTagsClassName?: string;
|
||||
featureTagClassName?: string;
|
||||
featureHighlightsClassName?: string;
|
||||
featureHighlightClassName?: string;
|
||||
featureHighlightIconClassName?: string;
|
||||
featureHighlightTitleClassName?: string;
|
||||
featureHighlightSubtitleClassName?: string;
|
||||
}
|
||||
|
||||
const HeroChatPromptFeatures = ({
|
||||
title,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
promptText = "Ask me anything...",
|
||||
featureTags,
|
||||
featureHighlights,
|
||||
ariaLabel = "Hero section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
promptContainerClassName = "",
|
||||
promptTextClassName = "",
|
||||
promptButtonClassName = "",
|
||||
featureTagsClassName = "",
|
||||
featureTagClassName = "",
|
||||
featureHighlightsClassName = "",
|
||||
featureHighlightClassName = "",
|
||||
featureHighlightIconClassName = "",
|
||||
featureHighlightTitleClassName = "",
|
||||
featureHighlightSubtitleClassName = "",
|
||||
}: HeroChatPromptFeaturesProps) => {
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("relative pt-hero-page-padding md:pt-0 w-full h-fit md:h-svh flex items-center justify-center", className)}
|
||||
>
|
||||
<div className={cls("w-content-width mx-auto flex flex-col gap-8", containerClassName)}>
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
className={cls("flex flex-col gap-3 md:gap-1", textBoxClassName)}
|
||||
titleClassName={cls("text-6xl font-medium text-balance", titleClassName)}
|
||||
descriptionClassName={cls("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)}
|
||||
center={true}
|
||||
/>
|
||||
|
||||
<div className={cls(
|
||||
"w-full md:w-50 mx-auto flex flex-wrap items-center justify-center gap-3",
|
||||
featureTagsClassName
|
||||
)}>
|
||||
{featureTags.map((featureTag) => {
|
||||
const FeatureIcon = featureTag.icon;
|
||||
return (
|
||||
<div
|
||||
key={featureTag.id}
|
||||
className={cls(
|
||||
"px-4 py-2 text-sm card rounded-theme flex items-center gap-2",
|
||||
featureTagClassName
|
||||
)}
|
||||
>
|
||||
<FeatureIcon className="relative z-1 h-[1em] w-auto" />
|
||||
<span className="relative z-1" >{featureTag.label}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className={cls(
|
||||
"w-full md:w-50 mx-auto card p-6 rounded-theme-capped flex flex-col gap-10",
|
||||
promptContainerClassName
|
||||
)}>
|
||||
<p className={cls(
|
||||
"relative z-1 flex-1 text-lg text-foreground truncate",
|
||||
promptTextClassName
|
||||
)}>
|
||||
{promptText}
|
||||
</p>
|
||||
<div className="relative z-1 w-full flex justify-end" >
|
||||
<div
|
||||
className={cls(
|
||||
"h-8 w-[var(--height-8)] rounded-theme primary-button flex items-center justify-center",
|
||||
promptButtonClassName
|
||||
)}
|
||||
>
|
||||
<ArrowUp className="w-1/2 h-1/2 text-background" strokeWidth={1.5} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative h-px w-full md:w-50 mx-auto bg-accent" />
|
||||
|
||||
<div className={cls(
|
||||
"w-full md:w-50 mx-auto grid grid-cols-1 gap-6",
|
||||
featureHighlights.length === 2 && "md:grid-cols-2",
|
||||
featureHighlights.length === 3 && "md:grid-cols-3",
|
||||
featureHighlights.length === 4 && "md:grid-cols-2",
|
||||
featureHighlightsClassName
|
||||
)}>
|
||||
{featureHighlights.map((highlight) => {
|
||||
const HighlightIcon = highlight.icon;
|
||||
return (
|
||||
<div
|
||||
key={highlight.id}
|
||||
className={cls(
|
||||
"relative w-full min-w-0 flex items-center gap-4 p-3 card rounded-theme-capped",
|
||||
featureHighlightClassName
|
||||
)}
|
||||
>
|
||||
<div className={cls(
|
||||
"relative shrink-0 h-10 w-auto aspect-square rounded-theme card flex items-center justify-center",
|
||||
featureHighlightIconClassName
|
||||
)}>
|
||||
<HighlightIcon className="relative z-1 h-45/100 w-45/100 text-foreground" strokeWidth={1.5} />
|
||||
</div>
|
||||
<div className="relative w-full min-w-0 flex flex-col">
|
||||
<h3 className={cls(
|
||||
"relative w-full z-1 text-base font-medium text-foreground leading-tight truncate",
|
||||
featureHighlightTitleClassName
|
||||
)}>
|
||||
{highlight.title}
|
||||
</h3>
|
||||
<p className={cls(
|
||||
"relative w-full z-1 text-sm text-foreground/70 leading-tight truncate",
|
||||
featureHighlightSubtitleClassName
|
||||
)}>
|
||||
{highlight.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroChatPromptFeatures.displayName = "HeroChatPromptFeatures";
|
||||
|
||||
export default HeroChatPromptFeatures;
|
||||
91
src/components/sections/hero/HeroGlobeOverlay.tsx
Normal file
91
src/components/sections/hero/HeroGlobeOverlay.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
"use client";
|
||||
|
||||
import TextBox from "@/components/Textbox";
|
||||
import { Globe } from "@/components/shared/Globe";
|
||||
import { cls } from "@/lib/utils";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
import type { COBEOptions } from "cobe";
|
||||
|
||||
interface HeroGlobeOverlayProps {
|
||||
title: string;
|
||||
description: string;
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
buttons?: ButtonConfig[];
|
||||
globeConfig?: COBEOptions;
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
titleClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
tagClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
globeWrapperClassName?: string;
|
||||
globeClassName?: string;
|
||||
}
|
||||
|
||||
const HeroGlobeOverlay = ({
|
||||
title,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
globeConfig,
|
||||
ariaLabel = "Hero section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
globeWrapperClassName = "",
|
||||
globeClassName = "",
|
||||
}: HeroGlobeOverlayProps) => {
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("w-full h-svh relative overflow-hidden flex items-center justify-center", className)}
|
||||
>
|
||||
<div className={cls("w-content-width mx-auto flex flex-col items-center", containerClassName)}>
|
||||
<div className={cls(
|
||||
"relative w-full aspect-[16/12] md:w-[70%] md:aspect-video mask-fade-bottom-large",
|
||||
globeWrapperClassName
|
||||
)}>
|
||||
<Globe config={globeConfig} className={cls("w-full -mt-[6%] md:-mt-[var(--width-5)]", globeClassName)} />
|
||||
</div>
|
||||
|
||||
<div className="relative -mt-[15%] md:-mt-[10%] z-10 w-full">
|
||||
<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-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 gap-4 mt-3", buttonContainerClassName)}
|
||||
buttonClassName={buttonClassName}
|
||||
buttonTextClassName={buttonTextClassName}
|
||||
center={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroGlobeOverlay.displayName = "HeroGlobeOverlay";
|
||||
|
||||
export default HeroGlobeOverlay;
|
||||
127
src/components/sections/hero/HeroLogo.tsx
Normal file
127
src/components/sections/hero/HeroLogo.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
"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 { 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;
|
||||
logoLineHeight?: number;
|
||||
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,
|
||||
logoLineHeight = 1.1,
|
||||
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", 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", 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",
|
||||
blurClassName
|
||||
)}
|
||||
style={{ maskImage: MASK_GRADIENT }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<div className={cls("relative z-20 w-content-width mx-auto h-full 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 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 lineHeight={logoLineHeight} className={cls("font-bold text-background", logoClassName)}>
|
||||
{logoText}
|
||||
</FillWidthText>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroLogo.displayName = "HeroLogo";
|
||||
|
||||
export default HeroLogo;
|
||||
119
src/components/sections/hero/HeroLogoBillboard.tsx
Normal file
119
src/components/sections/hero/HeroLogoBillboard.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
"use client";
|
||||
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
import FillWidthText from "@/components/shared/FillWidthText/FillWidthText";
|
||||
import TextAnimation from "@/components/text/TextAnimation";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { Plus } from "lucide-react";
|
||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||
|
||||
interface HeroLogoBillboardProps {
|
||||
logoText: string;
|
||||
description: string;
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
frameStyle?: "card" | "browser";
|
||||
logoLineHeight?: number;
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
logoContainerClassName?: string;
|
||||
logoClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
mediaWrapperClassName?: string;
|
||||
imageClassName?: string;
|
||||
browserBarClassName?: string;
|
||||
addressBarClassName?: string;
|
||||
}
|
||||
|
||||
const HeroLogoBillboard = ({
|
||||
logoText,
|
||||
description,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
imageAlt = "",
|
||||
videoAriaLabel = "Hero video",
|
||||
frameStyle = "card",
|
||||
logoLineHeight = 1.1,
|
||||
ariaLabel = "Hero section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
logoContainerClassName = "",
|
||||
logoClassName = "",
|
||||
descriptionClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
imageClassName = "",
|
||||
browserBarClassName = "",
|
||||
addressBarClassName = "",
|
||||
}: HeroLogoBillboardProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("w-full py-hero-page-padding", className)}
|
||||
>
|
||||
<div className={cls("w-content-width mx-auto flex flex-col gap-14 md:gap-15", containerClassName)}>
|
||||
<div className={cls("w-full flex flex-col items-end gap-6 md:gap-8", logoContainerClassName)}>
|
||||
<div className="relative w-full flex">
|
||||
<FillWidthText lineHeight={logoLineHeight} 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>
|
||||
|
||||
{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;
|
||||
143
src/components/sections/hero/HeroLogoBillboardSplit.tsx
Normal file
143
src/components/sections/hero/HeroLogoBillboardSplit.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
"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 { 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";
|
||||
|
||||
interface HeroLogoBillboardSplitProps {
|
||||
logoText: string;
|
||||
description: string;
|
||||
buttons: ButtonConfig[];
|
||||
layoutOrder: "default" | "reverse";
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
frameStyle?: "card" | "browser";
|
||||
logoLineHeight?: number;
|
||||
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,
|
||||
buttons,
|
||||
layoutOrder,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
imageAlt = "",
|
||||
videoAriaLabel = "Hero video",
|
||||
frameStyle = "card",
|
||||
logoLineHeight = 1.1,
|
||||
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("w-full py-hero-page-padding", className)}
|
||||
>
|
||||
<div className={cls("w-content-width mx-auto flex flex-col gap-6 md:gap-15", 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 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 lineHeight={logoLineHeight} 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;
|
||||
107
src/components/sections/hero/HeroLogoCarousel.tsx
Normal file
107
src/components/sections/hero/HeroLogoCarousel.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
"use client";
|
||||
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
import FillWidthText from "@/components/shared/FillWidthText/FillWidthText";
|
||||
import TextAnimation from "@/components/text/TextAnimation";
|
||||
import AutoCarousel from "@/components/cardStack/layouts/carousels/AutoCarousel";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||
|
||||
export interface MediaItem {
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
}
|
||||
|
||||
interface HeroLogoCarouselProps {
|
||||
logoText: string;
|
||||
description: string;
|
||||
mediaItems: MediaItem[];
|
||||
logoLineHeight?: number;
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
logoContainerClassName?: string;
|
||||
logoClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
carouselWrapperClassName?: string;
|
||||
}
|
||||
|
||||
const HeroLogoCarousel = ({
|
||||
logoText,
|
||||
description,
|
||||
mediaItems,
|
||||
logoLineHeight = 1.1,
|
||||
ariaLabel = "Hero section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
logoContainerClassName = "",
|
||||
logoClassName = "",
|
||||
descriptionClassName = "",
|
||||
carouselWrapperClassName = "",
|
||||
}: HeroLogoCarouselProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const renderCarouselItem = (item: MediaItem, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className="w-full aspect-[3/5] overflow-hidden rounded-theme-capped card p-2"
|
||||
>
|
||||
<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("w-full py-hero-page-padding", className)}
|
||||
>
|
||||
<div className={cls("w-content-width mx-auto flex flex-col gap-14 md:gap-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 lineHeight={logoLineHeight} 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>
|
||||
|
||||
<div className={cls("w-full -mx-[var(--content-padding)]", carouselWrapperClassName)}>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
HeroLogoCarousel.displayName = "HeroLogoCarousel";
|
||||
|
||||
export default HeroLogoCarousel;
|
||||
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", 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", 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",
|
||||
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-full 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 gap-4 mt-4", buttonContainerClassName)}
|
||||
buttonClassName={cls("", buttonClassName)}
|
||||
buttonTextClassName={cls("text-base", buttonTextClassName)}
|
||||
center={textPosition === "center"}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroOverlay.displayName = "HeroOverlay";
|
||||
|
||||
export default HeroOverlay;
|
||||
214
src/components/sections/hero/HeroShowcaseSplitOverlay.tsx
Normal file
214
src/components/sections/hero/HeroShowcaseSplitOverlay.tsx
Normal file
@@ -0,0 +1,214 @@
|
||||
"use client";
|
||||
|
||||
import { Fragment } from "react";
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
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";
|
||||
|
||||
interface HeroShowcaseSplitOverlayProps {
|
||||
title: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
buttons: ButtonConfig[];
|
||||
showcaseImageSrc?: string;
|
||||
showcaseVideoSrc?: string;
|
||||
showcaseImageAlt?: string;
|
||||
showcaseVideoAriaLabel?: string;
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
showDimOverlay?: boolean;
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
titleClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
tagsContainerClassName?: string;
|
||||
tagClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
showcaseWrapperClassName?: string;
|
||||
showcaseImageClassName?: string;
|
||||
mediaWrapperClassName?: string;
|
||||
imageClassName?: string;
|
||||
dimOverlayClassName?: string;
|
||||
}
|
||||
|
||||
const HeroShowcaseSplitOverlay = ({
|
||||
title,
|
||||
description,
|
||||
tags,
|
||||
buttons,
|
||||
showcaseImageSrc,
|
||||
showcaseVideoSrc,
|
||||
showcaseImageAlt = "",
|
||||
showcaseVideoAriaLabel = "Showcase video",
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
imageAlt = "",
|
||||
videoAriaLabel = "Hero video",
|
||||
showDimOverlay = false,
|
||||
ariaLabel = "Hero section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagsContainerClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
showcaseWrapperClassName = "",
|
||||
showcaseImageClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
imageClassName = "",
|
||||
dimOverlayClassName = "",
|
||||
}: HeroShowcaseSplitOverlayProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("relative w-full h-fit md:h-svh overflow-hidden", className)}
|
||||
>
|
||||
{/* Background Media with mask fade */}
|
||||
<div className={cls("absolute inset-0 w-full h-full opacity-50 mask-fade-bottom-long", mediaWrapperClassName)}>
|
||||
{showDimOverlay && (
|
||||
<div className={cls("absolute inset-0 z-[1] bg-background/20", dimOverlayClassName)} />
|
||||
)}
|
||||
<MediaContent
|
||||
imageSrc={imageSrc}
|
||||
videoSrc={videoSrc}
|
||||
imageAlt={imageAlt}
|
||||
videoAriaLabel={videoAriaLabel}
|
||||
imageClassName={cls("w-full h-full object-cover !rounded-none", imageClassName)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Mobile Layout */}
|
||||
<div className={cls(
|
||||
"relative z-10 w-content-width mx-auto h-full flex md:hidden flex-col gap-10 justify-between pt-hero-page-padding-1_5 pb-hero-page-padding",
|
||||
containerClassName
|
||||
)}>
|
||||
<div className="relative flex flex-col gap-6 w-full">
|
||||
<TextAnimation
|
||||
type={theme.defaultTextAnimation}
|
||||
text={title}
|
||||
variant="words-trigger"
|
||||
className={cls("text-5xl font-medium text-foreground text-balance text-start leading-[1]", titleClassName)}
|
||||
/>
|
||||
<div className={cls("flex 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">
|
||||
<TextAnimation
|
||||
type={theme.defaultTextAnimation}
|
||||
text={description}
|
||||
variant="words-trigger"
|
||||
className={cls("text-base text-foreground/75 text-balance text-start leading-tight", descriptionClassName)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className={cls(
|
||||
"relative w-full aspect-square overflow-hidden rounded-theme-capped card p-4",
|
||||
showcaseWrapperClassName
|
||||
)}>
|
||||
<MediaContent
|
||||
imageSrc={showcaseImageSrc}
|
||||
videoSrc={showcaseVideoSrc}
|
||||
imageAlt={showcaseImageAlt}
|
||||
videoAriaLabel={showcaseVideoAriaLabel}
|
||||
imageClassName={cls("h-full w-full object-cover z-1", showcaseImageClassName)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={cls("w-full card rounded-theme-capped flex flex-row items-center justify-between gap-2 px-6 py-3", tagsContainerClassName)}>
|
||||
{tags.slice(0, 4).map((tag, index) => (
|
||||
<Fragment key={`${tag}-${index}`}>
|
||||
<span className={cls("text-sm text-foreground truncate", tagClassName)}>
|
||||
{tag}
|
||||
</span>
|
||||
{index < Math.min(tags.length, 4) - 1 && (
|
||||
<span className="text-accent">·</span>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop Layout */}
|
||||
<div className={cls(
|
||||
"relative z-10 w-content-width mx-auto h-full hidden md:flex flex-col justify-between pt-hero-page-padding-1_5 pb-hero-page-padding",
|
||||
containerClassName
|
||||
)}>
|
||||
<div className="relative w-full flex flex-row gap-[var(--width-10)] 2xl:gap-[var(--width-15)] items-center">
|
||||
<div className={cls(
|
||||
"relative w-2/5 aspect-video overflow-hidden rounded-theme-capped card p-4",
|
||||
showcaseWrapperClassName
|
||||
)}>
|
||||
<MediaContent
|
||||
imageSrc={showcaseImageSrc}
|
||||
videoSrc={showcaseVideoSrc}
|
||||
imageAlt={showcaseImageAlt}
|
||||
videoAriaLabel={showcaseVideoAriaLabel}
|
||||
imageClassName={cls("h-full w-full object-cover z-1", showcaseImageClassName)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative flex flex-col gap-6 w-3/5">
|
||||
<TextAnimation
|
||||
type={theme.defaultTextAnimation}
|
||||
text={title}
|
||||
variant="words-trigger"
|
||||
className={cls("text-7xl font-medium text-foreground text-balance text-start leading-[1]", titleClassName)}
|
||||
/>
|
||||
<div className={cls("flex 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>
|
||||
|
||||
<div className="relative w-full flex flex-row gap-10 items-end justify-between">
|
||||
<div className={cls("flex flex-col gap-2", tagsContainerClassName)}>
|
||||
{tags.slice(0, 6).map((tag, index) => (
|
||||
<span
|
||||
key={`${tag}-${index}`}
|
||||
className={cls("text-base text-foreground", tagClassName)}
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="relative w-1/2">
|
||||
<TextAnimation
|
||||
type={theme.defaultTextAnimation}
|
||||
text={description}
|
||||
variant="words-trigger"
|
||||
start="top 100%"
|
||||
className={cls("text-lg text-foreground/75 text-balance text-start leading-[1.4]", descriptionClassName)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroShowcaseSplitOverlay.displayName = "HeroShowcaseSplitOverlay";
|
||||
|
||||
export default HeroShowcaseSplitOverlay;
|
||||
127
src/components/sections/hero/HeroSplit.tsx
Normal file
127
src/components/sections/hero/HeroSplit.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
"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";
|
||||
|
||||
interface HeroSplitProps {
|
||||
title: string;
|
||||
description: string;
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
buttons?: ButtonConfig[];
|
||||
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;
|
||||
}
|
||||
|
||||
const HeroSplit = ({
|
||||
title,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
imageAlt = "",
|
||||
videoAriaLabel = "Hero video",
|
||||
ariaLabel = "Hero section",
|
||||
imagePosition = "right",
|
||||
fixedMediaHeight = true,
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
imageClassName = "",
|
||||
}: 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("w-full h-fit py-hero-page-padding md:py-0 md:h-svh flex items-center", className)}
|
||||
>
|
||||
<div className={cls("w-content-width mx-auto flex flex-col md:flex-row gap-13 md:gap-15 items-center", 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}
|
||||
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 gap-4 mt-4", buttonContainerClassName)}
|
||||
buttonClassName={cls("", buttonClassName)}
|
||||
buttonTextClassName={cls("text-base", buttonTextClassName)}
|
||||
center={true}
|
||||
/>
|
||||
{/* Desktop */}
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
buttons={buttons}
|
||||
className={cls("hidden md:flex flex-col gap-3", 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 gap-4 mt-4", buttonContainerClassName)}
|
||||
buttonClassName={cls("", buttonClassName)}
|
||||
buttonTextClassName={cls("text-base", buttonTextClassName)}
|
||||
center={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{imagePosition === "right" && mediaContent}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroSplit.displayName = "HeroSplit";
|
||||
|
||||
export default HeroSplit;
|
||||
140
src/components/sections/hero/HeroSplitAvatars.tsx
Normal file
140
src/components/sections/hero/HeroSplitAvatars.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
"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";
|
||||
import type { Avatar } from "@/components/shared/AvatarGroup";
|
||||
|
||||
interface HeroSplitAvatarsProps {
|
||||
title: string;
|
||||
description: string;
|
||||
avatars: Avatar[];
|
||||
avatarText: string;
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
buttons?: ButtonConfig[];
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
ariaLabel?: string;
|
||||
imagePosition?: "left" | "right";
|
||||
fixedMediaHeight?: boolean;
|
||||
avatarGroupClassName?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
titleClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
tagClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
mediaWrapperClassName?: string;
|
||||
imageClassName?: string;
|
||||
}
|
||||
|
||||
const HeroSplitAvatars = ({
|
||||
title,
|
||||
description,
|
||||
avatars,
|
||||
avatarText,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
imageAlt = "",
|
||||
videoAriaLabel = "Hero video",
|
||||
ariaLabel = "Hero section",
|
||||
imagePosition = "right",
|
||||
fixedMediaHeight = true,
|
||||
avatarGroupClassName = "",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
imageClassName = "",
|
||||
}: HeroSplitAvatarsProps) => {
|
||||
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("w-full h-fit py-hero-page-padding md:py-0 md:h-svh flex items-center", className)}
|
||||
>
|
||||
<div className={cls("w-content-width mx-auto flex flex-col md:flex-row gap-13 md:gap-15 items-center", 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 gap-4 mt-4", 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("!mt-5", avatarGroupClassName)}
|
||||
className={cls("hidden md:flex flex-col gap-3", 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 gap-4 mt-4", buttonContainerClassName)}
|
||||
buttonClassName={cls("", buttonClassName)}
|
||||
buttonTextClassName={cls("text-base", buttonTextClassName)}
|
||||
center={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{imagePosition === "right" && mediaContent}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroSplitAvatars.displayName = "HeroSplitAvatars";
|
||||
|
||||
export default HeroSplitAvatars;
|
||||
115
src/components/sections/hero/HeroSplitGlobe.tsx
Normal file
115
src/components/sections/hero/HeroSplitGlobe.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
"use client";
|
||||
|
||||
import TextBox from "@/components/Textbox";
|
||||
import { Globe } from "@/components/shared/Globe";
|
||||
import { cls } from "@/lib/utils";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
import type { COBEOptions } from "cobe";
|
||||
|
||||
interface HeroSplitGlobeProps {
|
||||
title: string;
|
||||
description: string;
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
buttons?: ButtonConfig[];
|
||||
globeConfig?: COBEOptions;
|
||||
ariaLabel?: string;
|
||||
imagePosition?: "left" | "right";
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
titleClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
tagClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
globeWrapperClassName?: string;
|
||||
globeClassName?: string;
|
||||
}
|
||||
|
||||
const HeroSplitGlobe = ({
|
||||
title,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
globeConfig,
|
||||
ariaLabel = "Hero section",
|
||||
imagePosition = "right",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
globeWrapperClassName = "",
|
||||
globeClassName = "",
|
||||
}: HeroSplitGlobeProps) => {
|
||||
const globeContent = (
|
||||
<div className={cls(
|
||||
"relative w-full h-fit aspect-square md:aspect-auto md:w-1/2 overflow-hidden rounded-theme-capped card p-4 md:h-[65vh]",
|
||||
globeWrapperClassName
|
||||
)}>
|
||||
<div className="relative h-full aspect-square max-w-full max-h-full mx-auto flex items-center justify-center">
|
||||
<Globe config={globeConfig} className={cls("absolute top-1/2 left-1/2 -translate-1/2", globeClassName)} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("w-full h-fit py-hero-page-padding md:py-0 md:h-svh flex items-center", className)}
|
||||
>
|
||||
<div className={cls("w-content-width mx-auto flex flex-col md:flex-row gap-13 md:gap-15 items-center", containerClassName)}>
|
||||
{imagePosition === "left" && globeContent}
|
||||
|
||||
<div className={cls("w-full md:w-1/2")}>
|
||||
{/* Mobile */}
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
buttons={buttons}
|
||||
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 gap-4 mt-4", buttonContainerClassName)}
|
||||
buttonClassName={cls("", buttonClassName)}
|
||||
buttonTextClassName={cls("text-base", buttonTextClassName)}
|
||||
center={true}
|
||||
/>
|
||||
{/* Desktop */}
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
buttons={buttons}
|
||||
className={cls("hidden md:flex flex-col gap-3", 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 gap-4 mt-4", buttonContainerClassName)}
|
||||
buttonClassName={cls("", buttonClassName)}
|
||||
buttonTextClassName={cls("text-base", buttonTextClassName)}
|
||||
center={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{imagePosition === "right" && globeContent}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroSplitGlobe.displayName = "HeroSplitGlobe";
|
||||
|
||||
export default HeroSplitGlobe;
|
||||
148
src/components/sections/hero/HeroSplitGlobeKpi.tsx
Normal file
148
src/components/sections/hero/HeroSplitGlobeKpi.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
"use client";
|
||||
|
||||
import TextBox from "@/components/Textbox";
|
||||
import { Globe } from "@/components/shared/Globe";
|
||||
import { cls } from "@/lib/utils";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
import type { COBEOptions } from "cobe";
|
||||
|
||||
interface KpiItem {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface HeroSplitGlobeKpiProps {
|
||||
title: string;
|
||||
description: string;
|
||||
kpis: [KpiItem, KpiItem, KpiItem];
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
buttons?: ButtonConfig[];
|
||||
globeConfig?: COBEOptions;
|
||||
ariaLabel?: string;
|
||||
globePosition?: "left" | "right";
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
titleClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
tagClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
globeWrapperClassName?: string;
|
||||
globeClassName?: string;
|
||||
kpiClassName?: string;
|
||||
kpiValueClassName?: string;
|
||||
kpiLabelClassName?: string;
|
||||
}
|
||||
|
||||
const HeroSplitGlobeKpi = ({
|
||||
title,
|
||||
description,
|
||||
kpis,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
globeConfig,
|
||||
ariaLabel = "Hero section",
|
||||
globePosition = "right",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
globeWrapperClassName = "",
|
||||
globeClassName = "",
|
||||
kpiClassName = "",
|
||||
kpiValueClassName = "",
|
||||
kpiLabelClassName = "",
|
||||
}: HeroSplitGlobeKpiProps) => {
|
||||
const globeContent = (
|
||||
<div className={cls(
|
||||
"relative w-full h-fit aspect-square md:aspect-auto md:w-1/2 md:h-[65vh]",
|
||||
globeWrapperClassName
|
||||
)}>
|
||||
<div className="relative h-full aspect-square max-w-full max-h-full mx-auto flex items-center justify-center" >
|
||||
<Globe config={globeConfig} className={cls("absolute top-1/2 left-1/2 -translate-1/2", globeClassName)} />
|
||||
|
||||
{kpis.map((kpi, index) => (
|
||||
<div
|
||||
key={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-[10%] md:left-[5%]",
|
||||
index === 1 && "top-[35%] right-[2.5%] md:top-[35%]",
|
||||
index === 2 && "bottom-[7.5%] left-[10%] md:left-[20%] 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>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("w-full h-fit py-hero-page-padding md:py-0 md:h-svh flex items-center", className)}
|
||||
>
|
||||
<div className={cls("w-content-width mx-auto flex flex-col md:flex-row gap-13 md:gap-15 items-center", containerClassName)}>
|
||||
{globePosition === "left" && globeContent}
|
||||
|
||||
<div className={cls("w-full md:w-1/2")}>
|
||||
{/* Mobile */}
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
buttons={buttons}
|
||||
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 gap-4 mt-4", buttonContainerClassName)}
|
||||
buttonClassName={cls("", buttonClassName)}
|
||||
buttonTextClassName={cls("text-base", buttonTextClassName)}
|
||||
center={true}
|
||||
/>
|
||||
{/* Desktop */}
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
buttons={buttons}
|
||||
className={cls("hidden md:flex flex-col gap-3", 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 gap-4 mt-4", buttonContainerClassName)}
|
||||
buttonClassName={cls("", buttonClassName)}
|
||||
buttonTextClassName={cls("text-base", buttonTextClassName)}
|
||||
center={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{globePosition === "right" && globeContent}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroSplitGlobeKpi.displayName = "HeroSplitGlobeKpi";
|
||||
|
||||
export default HeroSplitGlobeKpi;
|
||||
159
src/components/sections/hero/HeroSplitKpi.tsx
Normal file
159
src/components/sections/hero/HeroSplitKpi.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
"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";
|
||||
|
||||
interface KpiItem {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface HeroSplitKpiProps {
|
||||
title: string;
|
||||
description: string;
|
||||
kpis: [KpiItem, KpiItem, KpiItem];
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
buttons?: ButtonConfig[];
|
||||
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;
|
||||
kpiClassName?: string;
|
||||
kpiValueClassName?: string;
|
||||
kpiLabelClassName?: string;
|
||||
}
|
||||
|
||||
const HeroSplitKpi = ({
|
||||
title,
|
||||
description,
|
||||
kpis,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
imageAlt = "",
|
||||
videoAriaLabel = "Hero video",
|
||||
ariaLabel = "Hero section",
|
||||
imagePosition = "right",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
imageClassName = "",
|
||||
kpiClassName = "",
|
||||
kpiValueClassName = "",
|
||||
kpiLabelClassName = "",
|
||||
}: HeroSplitKpiProps) => {
|
||||
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}
|
||||
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
|
||||
aria-label={ariaLabel}
|
||||
className={cls("w-full h-fit py-hero-page-padding md:py-0 md:h-svh flex items-center", className)}
|
||||
>
|
||||
<div className={cls("w-content-width mx-auto flex flex-col md:flex-row gap-13 md:gap-15 items-center", 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}
|
||||
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 gap-4 mt-4", buttonContainerClassName)}
|
||||
buttonClassName={cls("", buttonClassName)}
|
||||
buttonTextClassName={cls("text-base", buttonTextClassName)}
|
||||
center={true}
|
||||
/>
|
||||
{/* Desktop */}
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
buttons={buttons}
|
||||
className={cls("hidden md:flex flex-col gap-3", 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 gap-4 mt-4", buttonContainerClassName)}
|
||||
buttonClassName={cls("", buttonClassName)}
|
||||
buttonTextClassName={cls("text-base", buttonTextClassName)}
|
||||
center={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{imagePosition === "right" && mediaContent}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroSplitKpi.displayName = "HeroSplitKpi";
|
||||
|
||||
export default HeroSplitKpi;
|
||||
116
src/components/sections/hero/HeroSplitLarge.tsx
Normal file
116
src/components/sections/hero/HeroSplitLarge.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
"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";
|
||||
|
||||
interface HeroSplitLargeProps {
|
||||
title: string;
|
||||
description: string;
|
||||
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;
|
||||
mediaWrapperClassName?: string;
|
||||
imageClassName?: string;
|
||||
}
|
||||
|
||||
const HeroSplitLarge = ({
|
||||
title,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
imageAlt = "",
|
||||
videoAriaLabel = "Hero video",
|
||||
ariaLabel = "Hero section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
imageClassName = "",
|
||||
}: HeroSplitLargeProps) => {
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("relative w-full h-svh pt-hero-page-padding md:pt-0 flex flex-col gap-15 md:flex-row md:gap-0 items-center", className)}
|
||||
>
|
||||
<div className={cls("w-content-width mx-auto flex flex-col md:flex-row gap-0 md:gap-20 items-center", containerClassName)}>
|
||||
<div className="w-full md:w-1/2">
|
||||
{/* Mobile */}
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
buttons={buttons}
|
||||
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 gap-4 mt-4", buttonContainerClassName)}
|
||||
buttonClassName={cls("", buttonClassName)}
|
||||
buttonTextClassName={cls("text-base", buttonTextClassName)}
|
||||
center={true}
|
||||
/>
|
||||
{/* Desktop */}
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
buttons={buttons}
|
||||
className={cls("hidden md:flex flex-col gap-3", 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 gap-4 mt-4", buttonContainerClassName)}
|
||||
buttonClassName={cls("", buttonClassName)}
|
||||
buttonTextClassName={cls("text-base", buttonTextClassName)}
|
||||
center={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative w-full md:w-1/2" />
|
||||
</div>
|
||||
<div className={cls(
|
||||
"relative! md:absolute! z-0 md:top-0 md:right-0 w-full md:w-[calc(50%-2.5rem)] h-full overflow-hidden",
|
||||
mediaWrapperClassName
|
||||
)}>
|
||||
<MediaContent
|
||||
imageSrc={imageSrc}
|
||||
videoSrc={videoSrc}
|
||||
imageAlt={imageAlt}
|
||||
videoAriaLabel={videoAriaLabel}
|
||||
imageClassName={cls("h-full min-h-0 rounded-none!", imageClassName)}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroSplitLarge.displayName = "HeroSplitLarge";
|
||||
|
||||
export default HeroSplitLarge;
|
||||
@@ -0,0 +1,173 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback } from "react";
|
||||
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 { 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;
|
||||
logoLineHeight?: number;
|
||||
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,
|
||||
logoLineHeight = 1.1,
|
||||
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", 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", 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",
|
||||
blurClassName
|
||||
)}
|
||||
style={{ maskImage: MASK_GRADIENT }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<div className={cls("relative z-20 w-content-width mx-auto h-full 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 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 lineHeight={logoLineHeight} className={cls("text-background", 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