Initial commit

This commit is contained in:
vitalijmulika
2025-12-25 13:52:13 +02:00
commit 4ef6fdd36a
307 changed files with 62808 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
"use client";
import { useState, useEffect, useRef, ReactNode } from "react";
interface AnimationContainerProps {
children: ReactNode;
className?: string;
animationDuration?: number;
animationType?: "full" | "fade";
style?: React.CSSProperties;
}
const AnimationContainer = ({
children,
className = "w-full h-fit flex flex-col gap-6",
animationDuration = 800,
animationType = "full",
style,
}: AnimationContainerProps) => {
const animationClass =
animationType === "full"
? "animation-container"
: "animation-container-fade";
const [activeClass, setActiveClass] = useState(animationClass);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const timer = setTimeout(() => {
setActiveClass("");
}, animationDuration);
return () => clearTimeout(timer);
}, [animationDuration]);
return (
<div
ref={containerRef}
className={`${className} ${activeClass}`.trim()}
style={style}
>
{children}
</div>
);
};
export default AnimationContainer;

View File

@@ -0,0 +1,102 @@
"use client";
import React, { memo } from "react";
import TextAnimation from "@/components/text/TextAnimation";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
interface Metric {
icon: LucideIcon;
label: string;
value: string;
}
interface AboutMetricProps {
title: string;
metrics: Metric[];
useInvertedBackground: "noInvert" | "invertDefault" | "invertCard";
ariaLabel?: string;
className?: string;
containerClassName?: string;
titleClassName?: string;
metricsContainerClassName?: string;
metricCardClassName?: string;
metricIconClassName?: string;
metricLabelClassName?: string;
metricValueClassName?: string;
}
const AboutMetric = ({
title,
metrics,
useInvertedBackground,
ariaLabel = "About metrics section",
className = "",
containerClassName = "",
titleClassName = "",
metricsContainerClassName = "",
metricCardClassName = "",
metricIconClassName = "",
metricLabelClassName = "",
metricValueClassName = "",
}: AboutMetricProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const gridColsMap = {
2: "md:grid-cols-2",
3: "md:grid-cols-3",
4: "md:grid-cols-4",
};
const gridCols = gridColsMap[metrics.length as keyof typeof gridColsMap] || "md:grid-cols-4";
return (
<section
aria-label={ariaLabel}
className={cls("relative py-20", useInvertedBackground === "invertCard" ? "w-content-width-expanded mx-auto rounded-theme-capped bg-foreground" : "w-full", useInvertedBackground === "invertDefault" && "bg-foreground", className)}
>
<div className={cls("w-content-width mx-auto flex flex-col gap-8", containerClassName)}>
<TextAnimation
type={theme.defaultTextAnimation}
text={title}
variant="words-trigger"
className={cls("text-2xl md:text-5xl font-medium leading-[1.175]", (useInvertedBackground === "invertDefault" || useInvertedBackground === "invertCard") && "text-background", titleClassName)}
/>
<div className={cls("grid grid-cols-1 gap-6", gridCols, metricsContainerClassName)}>
{metrics.map((metric, index) => {
const Icon = metric.icon;
return (
<div
key={index}
className={cls(
"h-fit card rounded-theme-capped px-6 py-8 md:py-10 flex flex-col items-center justify-center gap-3",
metricCardClassName
)}
>
<div className="relative z-1 w-full flex items-center justify-center gap-2">
<div className={cls("h-8 primary-button aspect-square rounded-theme flex items-center justify-center", metricIconClassName)}>
<Icon className="h-4/10 text-background" strokeWidth={1.5} />
</div>
<h3 className={cls("text-xl truncate", shouldUseLightText && "text-background", metricLabelClassName)}>
{metric.label}
</h3>
</div>
<div className="relative z-1 w-full flex items-center justify-center">
<h4 className={cls("text-6xl font-medium truncate", shouldUseLightText && "text-background", metricValueClassName)}>
{metric.value}
</h4>
</div>
</div>
);
})}
</div>
</div>
</section>
);
};
AboutMetric.displayName = "AboutMetric";
export default memo(AboutMetric);

View File

@@ -0,0 +1,214 @@
"use client";
import React, { memo, useMemo } from "react";
import TimelinePhoneView from "@/components/cardStack/layouts/timelines/TimelinePhoneView";
import TextAnimation from "@/components/text/TextAnimation";
import Button from "@/components/button/Button";
import Tag from "@/components/shared/Tag";
import { cls } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, TitleSegment } from "@/components/cardStack/types";
import type { TimelinePhoneViewItem } from "@/components/cardStack/hooks/usePhoneAnimations";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type AboutPhone = {
imageAlt?: string;
videoAriaLabel?: string;
} & (
| { imageSrc: string; videoSrc?: never }
| { videoSrc: string; imageSrc?: never }
);
interface AboutPhoneTimelineProps {
title: string;
titleSegments?: TitleSegment[];
description: string;
tag: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
phoneOne: AboutPhone;
phoneTwo: AboutPhone;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
desktopContainerClassName?: string;
mobileContainerClassName?: string;
desktopContentClassName?: string;
desktopWrapperClassName?: string;
mobileWrapperClassName?: string;
phoneFrameClassName?: string;
mobilePhoneFrameClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
contentClassName?: string;
tagClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
}
interface AboutContentProps {
title: string;
description: string;
tag: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
useInvertedBackground: "noInvert" | "invertDefault" | "invertCard";
contentClassName: string;
tagClassName: string;
titleClassName: string;
descriptionClassName: string;
buttonContainerClassName: string;
buttonClassName: string;
buttonTextClassName: string;
}
const AboutContent = ({
title,
description,
tag,
tagIcon: TagIcon,
buttons,
useInvertedBackground,
contentClassName,
tagClassName,
titleClassName,
descriptionClassName,
buttonContainerClassName,
buttonClassName,
buttonTextClassName,
}: AboutContentProps) => {
const theme = useTheme();
return (
<div className={cls("h-full w-full flex items-center justify-center px-10", contentClassName)}>
<div className="flex flex-col gap-3 md:gap-1 items-center mb-0 md:mb-[12.5vh] 2xl:mb-[14vh]">
<Tag
text={tag}
icon={TagIcon}
useInvertedBackground={useInvertedBackground}
className={cls("mb-1 md:mb-3", tagClassName)}
/>
<TextAnimation
type={theme.defaultTextAnimation}
text={title}
variant="trigger"
as="h2"
className={cls("text-6xl font-medium text-center", (useInvertedBackground === "invertDefault" || useInvertedBackground === "invertCard") && "text-background", titleClassName)}
/>
<TextAnimation
type={theme.defaultTextAnimation}
text={description}
variant="trigger"
as="p"
className={cls("text-lg leading-[1.2] text-center", (useInvertedBackground === "invertDefault" || useInvertedBackground === "invertCard") ? "text-background/75" : "text-foreground/75", descriptionClassName)}
/>
{buttons && buttons.length > 0 && (
<div className={cls("flex gap-4 mt-1 md:mt-3 justify-center", buttonContainerClassName)}>
{buttons.map((button, index) => (
<Button key={`${button.text}-${index}`} {...getButtonProps(button, index, theme.defaultButtonVariant, buttonClassName, buttonTextClassName)} />
))}
</div>
)}
</div>
</div>
);
};
const AboutPhoneTimeline = ({
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
phoneOne,
phoneTwo,
textboxLayout,
useInvertedBackground,
ariaLabel = "About section",
className = "",
containerClassName = "",
desktopContainerClassName = "",
mobileContainerClassName = "",
desktopContentClassName = "",
desktopWrapperClassName = "",
mobileWrapperClassName = "",
phoneFrameClassName = "",
mobilePhoneFrameClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
contentClassName = "",
tagClassName = "",
titleClassName = "",
descriptionClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
}: AboutPhoneTimelineProps) => {
const timelineItems: TimelinePhoneViewItem[] = useMemo(() => [{
trigger: 'about-trigger',
content: (
<AboutContent
title={title}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
useInvertedBackground={useInvertedBackground}
contentClassName={contentClassName}
tagClassName={tagClassName}
titleClassName={titleClassName}
descriptionClassName={descriptionClassName}
buttonContainerClassName={buttonContainerClassName}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
/>
),
imageOne: phoneOne.imageSrc,
videoOne: phoneOne.videoSrc,
imageAltOne: phoneOne.imageAlt || `${title} - Image 1`,
videoAriaLabelOne: phoneOne.videoAriaLabel || `${title} - Video 1`,
imageTwo: phoneTwo.imageSrc,
videoTwo: phoneTwo.videoSrc,
imageAltTwo: phoneTwo.imageAlt || `${title} - Image 2`,
videoAriaLabelTwo: phoneTwo.videoAriaLabel || `${title} - Video 2`,
}], [title, description, tag, tagIcon, buttons, phoneOne, phoneTwo, useInvertedBackground, contentClassName, tagClassName, titleClassName, descriptionClassName, buttonContainerClassName, buttonClassName, buttonTextClassName]);
return (
<TimelinePhoneView
items={timelineItems}
showTextBox={false}
title=""
titleSegments={titleSegments}
description=""
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
desktopContainerClassName={desktopContainerClassName}
mobileContainerClassName={mobileContainerClassName}
desktopContentClassName={desktopContentClassName}
desktopWrapperClassName={desktopWrapperClassName}
mobileWrapperClassName={mobileWrapperClassName}
phoneFrameClassName={phoneFrameClassName}
mobilePhoneFrameClassName={mobilePhoneFrameClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
ariaLabel={ariaLabel}
/>
);
};
AboutPhoneTimeline.displayName = "AboutPhoneTimeline";
export default memo(AboutPhoneTimeline);

View File

@@ -0,0 +1,130 @@
"use client";
import React, { memo } from "react";
import Image from "next/image";
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";
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
type HeadingSegment =
| { type: "text"; content: string }
| { type: "image"; src: string; alt?: string };
interface InlineImageSplitTextAboutProps {
heading: HeadingSegment[];
buttons?: ButtonConfig[];
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
headingClassName?: string;
imageWrapperClassName?: string;
imageClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
}
const InlineImageSplitTextAbout = ({
heading,
buttons,
useInvertedBackground,
ariaLabel = "About section",
className = "",
containerClassName = "",
headingClassName = "",
imageWrapperClassName = "",
imageClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
}: InlineImageSplitTextAboutProps) => {
const theme = useTheme();
return (
<section
aria-label={ariaLabel}
className={cls(
"relative py-20",
useInvertedBackground === "invertCard"
? "w-content-width-expanded mx-auto rounded-theme-capped bg-foreground"
: "w-full",
useInvertedBackground === "invertDefault" && "bg-foreground",
className
)}
>
<div
className={cls(
"w-content-width mx-auto flex flex-col gap-6 items-center",
containerClassName
)}
>
<h2
className={cls(
"text-4xl md:text-5xl font-medium text-center leading-[1.15] text-balance",
(useInvertedBackground === "invertDefault" ||
useInvertedBackground === "invertCard") &&
"text-background",
headingClassName
)}
>
{heading.map((segment, index) => {
const imageIndex = heading
.slice(0, index + 1)
.filter(s => s.type === "image").length - 1;
const element = segment.type === "text" ? (
<span key={index}>{segment.content}</span>
) : (
<span
key={index}
className={cls(
"inline-block relative primary-button -mt-[0.2em] h-[1.1em] w-auto aspect-square align-middle mx-1 p-0.5 rounded-theme",
imageIndex % 2 === 0 ? "-rotate-12" : "rotate-12",
imageWrapperClassName
)}
>
<div className="relative w-full h-full">
<Image
src={segment.src}
alt={segment.alt || ""}
width={24}
height={24}
className={cls(
"absolute inset-0 m-auto h-full w-full rounded-theme",
imageClassName
)}
unoptimized={segment.src.startsWith("http") || segment.src.startsWith("//")}
aria-hidden={!segment.alt || segment.alt === ""}
/>
</div>
</span>
);
return (
<React.Fragment key={index}>
{index > 0 && " "}
{element}
</React.Fragment>
);
})}
</h2>
{buttons && buttons.length > 0 && (
<div className={cls("flex gap-4", buttonContainerClassName)}>
{buttons.slice(0, 2).map((button, index) => (
<Button key={index} {...getButtonProps(button, index, theme.defaultButtonVariant, cls("px-8", buttonClassName), cls("text-base", buttonTextClassName))} />
))}
</div>
)}
</div>
</section>
);
};
InlineImageSplitTextAbout.displayName = "InlineImageSplitTextAbout";
export default memo(InlineImageSplitTextAbout);

View File

@@ -0,0 +1,98 @@
"use client";
import { memo } from "react";
import MediaContent from "@/components/shared/MediaContent";
import TextBox from "@/components/Textbox";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig } from "@/types/button";
interface MediaAboutProps {
title: string;
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
useInvertedBackground: "noInvert" | "invertDefault" | "invertCard";
ariaLabel?: string;
className?: string;
textBoxClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
}
const MediaAbout = ({
title,
description,
tag,
tagIcon,
buttons,
imageSrc,
videoSrc,
imageAlt,
videoAriaLabel,
useInvertedBackground,
ariaLabel = "About section",
className = "",
textBoxClassName = "",
titleClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
}: MediaAboutProps) => {
return (
<section
aria-label={ariaLabel}
className={cls("relative py-20", useInvertedBackground === "invertCard" ? "w-content-width-expanded mx-auto rounded-theme-capped bg-foreground" : "w-full", useInvertedBackground === "invertDefault" && "bg-foreground", className)}
>
<div className={cls("relative w-content-width mx-auto aspect-square md:aspect-video rounded-theme-capped overflow-hidden", mediaWrapperClassName)}>
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("w-full h-full object-cover", mediaClassName)}
/>
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
<div className="relative z-10 flex items-center justify-center h-full w-content-width md:w-45 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>
</div>
</section>
);
};
MediaAbout.displayName = "MediaAbout";
export default memo(MediaAbout);

View File

@@ -0,0 +1,125 @@
"use client";
import { memo } from "react";
import MediaContent from "@/components/shared/MediaContent";
import Button from "@/components/button/Button";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { ButtonConfig } from "@/types/button";
interface MediaSplitAboutProps {
title: string;
description: string;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
buttons?: ButtonConfig[];
imagePosition?: "left" | "right";
useInvertedBackground: "noInvert" | "invertDefault" | "invertCard";
ariaLabel?: string;
className?: string;
containerClassName?: string;
contentCardClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
mediaCardClassName?: string;
mediaClassName?: string;
}
const MediaSplitAbout = ({
title,
description,
imageSrc,
videoSrc,
imageAlt,
videoAriaLabel,
buttons,
imagePosition = "right",
useInvertedBackground,
ariaLabel = "About section",
className = "",
containerClassName = "",
contentCardClassName = "",
titleClassName = "",
descriptionClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
mediaCardClassName = "",
mediaClassName = "",
}: MediaSplitAboutProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const contentCard = (
<div className={cls("card rounded-theme-capped p-6 md:p-10 flex flex-col justify-between gap-10", contentCardClassName)}>
<div className="relative z-1 flex flex-col gap-4">
<h2 className={cls("text-4xl font-medium leading-tight", shouldUseLightText ? "text-background" : "text-foreground", titleClassName)}>
{title}
</h2>
<p className={cls("text-base md:text-lg leading-tight", shouldUseLightText ? "text-background/75" : "text-foreground/75", descriptionClassName)}>
{description}
</p>
</div>
{buttons && buttons.length > 0 && (
<div className={cls("relative z-1 flex gap-4", buttonContainerClassName)}>
{buttons.slice(0, 2).map((button, index) => (
<Button
key={index}
{...getButtonProps(
button,
index,
theme.defaultButtonVariant,
cls("px-8", buttonClassName),
cls("text-base", buttonTextClassName)
)}
/>
))}
</div>
)}
</div>
);
const mediaCard = (
<div className={cls("card md:aspect-square rounded-theme-capped overflow-hidden", mediaCardClassName)}>
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("relative z-1 w-full h-full object-cover", mediaClassName)}
/>
</div>
);
return (
<section
aria-label={ariaLabel}
className={cls("relative py-20", useInvertedBackground === "invertCard" ? "w-content-width-expanded mx-auto rounded-theme-capped bg-foreground" : "w-full", useInvertedBackground === "invertDefault" && "bg-foreground", className)}
>
<div className={cls("w-content-width mx-auto grid grid-cols-1 md:grid-cols-2 gap-6", containerClassName)}>
{imagePosition === "left" ? (
<>
{mediaCard}
{contentCard}
</>
) : (
<>
{contentCard}
{mediaCard}
</>
)}
</div>
</section>
);
};
MediaSplitAbout.displayName = "MediaSplitAbout";
export default memo(MediaSplitAbout);

View File

@@ -0,0 +1,163 @@
"use client";
import { memo, useState } from "react";
import MediaContent from "@/components/shared/MediaContent";
import AnimationContainer from "@/components/sections/AnimationContainer";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
interface TabOption {
id: string;
label: string;
description: string;
}
interface MediaSplitTabsAboutProps {
title: string;
description?: string;
tabs: TabOption[];
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
imagePosition?: "left" | "right";
useInvertedBackground: "noInvert" | "invertDefault" | "invertCard";
ariaLabel?: string;
className?: string;
containerClassName?: string;
contentCardClassName?: string;
titleClassName?: string;
titleDescriptionClassName?: string;
tabsContainerClassName?: string;
tabClassName?: string;
activeTabClassName?: string;
tabIndicatorClassName?: string;
descriptionClassName?: string;
mediaCardClassName?: string;
mediaClassName?: string;
}
const MediaSplitTabsAbout = ({
title,
description,
tabs,
imageSrc,
videoSrc,
imageAlt,
videoAriaLabel,
imagePosition = "right",
useInvertedBackground,
ariaLabel = "About section",
className = "",
containerClassName = "",
contentCardClassName = "",
titleClassName = "",
titleDescriptionClassName = "",
tabsContainerClassName = "",
tabClassName = "",
activeTabClassName = "",
tabIndicatorClassName = "",
descriptionClassName = "",
mediaCardClassName = "",
mediaClassName = "",
}: MediaSplitTabsAboutProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const [activeTab, setActiveTab] = useState(tabs[0]?.id || "");
const activeTabData = tabs.find((tab) => tab.id === activeTab);
const contentCard = (
<div className={cls("card rounded-theme-capped p-6 md:p-10 md:h-160 2xl:h-180 flex flex-col justify-between gap-3 md:gap-6", contentCardClassName)}>
<div className="relative z-1 flex flex-col gap-2">
<h2 className={cls("text-4xl font-medium leading-tight", shouldUseLightText ? "text-background" : "text-foreground", titleClassName)}>
{title}
</h2>
{description && (
<p className={cls("text-base md:text-lg leading-tight", shouldUseLightText ? "text-background" : "text-foreground", titleDescriptionClassName)}>
{description}
</p>
)}
</div>
<div className="relative z-1 flex flex-col gap-6">
<div className={cls("group flex flex-wrap gap-x-6 gap-y-1 md:gap-6", tabsContainerClassName)}>
{tabs.map((tab) => (
<button
key={tab.id}
type="button"
onClick={() => setActiveTab(tab.id)}
className={cls(
"flex items-center gap-2 text-lg md:text-xl transition-colors cursor-pointer",
activeTab === tab.id
? (shouldUseLightText ? "text-background" : "text-foreground")
: (shouldUseLightText ? "text-background/50" : "text-foreground/50"),
activeTab === tab.id && activeTabClassName,
tabClassName
)}
aria-pressed={activeTab === tab.id}
>
<span
className={cls(
"rounded-theme w-2 h-2 border border-accent group-hover:scale-125 group-hover:bg-accent transition-all duration-300",
activeTab === tab.id ? "bg-accent" : "bg-transparent",
tabIndicatorClassName
)}
aria-hidden="true"
/>
{tab.label}
</button>
))}
</div>
<div className="w-full h-px bg-accent" />
<AnimationContainer
key={activeTab}
className="w-full"
>
<p className={cls("text-base md:text-lg leading-tight", shouldUseLightText ? "text-background" : "text-foreground", descriptionClassName)}>
{activeTabData?.description}
</p>
</AnimationContainer>
</div>
</div>
);
const mediaCard = (
<div className={cls("card aspect-square md:aspect-auto md:h-160 2xl:h-180 rounded-theme-capped overflow-hidden", mediaCardClassName)}>
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("relative z-1 w-full h-full object-cover", mediaClassName)}
/>
</div>
);
return (
<section
aria-label={ariaLabel}
className={cls("relative py-20", useInvertedBackground === "invertCard" ? "w-content-width-expanded mx-auto rounded-theme-capped bg-foreground" : "w-full", useInvertedBackground === "invertDefault" && "bg-foreground", className)}
>
<div className={cls("w-content-width mx-auto grid grid-cols-1 md:grid-cols-10 gap-6", containerClassName)}>
{imagePosition === "left" ? (
<>
<div className="md:col-span-4">{mediaCard}</div>
<div className="md:col-span-6">{contentCard}</div>
</>
) : (
<>
<div className="md:col-span-6">{contentCard}</div>
<div className="md:col-span-4">{mediaCard}</div>
</>
)}
</div>
</section>
);
};
MediaSplitTabsAbout.displayName = "MediaSplitTabsAbout";
export default memo(MediaSplitTabsAbout);

View File

@@ -0,0 +1,95 @@
"use client";
import { memo } from "react";
import MediaContent from "@/components/shared/MediaContent";
import TextBox from "@/components/Textbox";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig } from "@/types/button";
interface ParallaxAboutProps {
title: string;
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
ariaLabel?: string;
className?: string;
textBoxClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
mediaClassName?: string;
}
const ParallaxAbout = ({
title,
description,
tag,
tagIcon,
buttons,
imageSrc,
videoSrc,
imageAlt,
videoAriaLabel,
ariaLabel = "About section",
className = "",
textBoxClassName = "",
titleClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
mediaClassName = "",
}: ParallaxAboutProps) => {
return (
<section
aria-label={ariaLabel}
className={cls("relative z-0 w-full h-svh", className)}
style={{
clipPath: "polygon(0% 0, 100% 0%, 100% 100%, 0 100%)",
}}
>
<div className="fixed inset-0 w-full h-full">
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("w-full h-full object-cover rounded-none!", mediaClassName)}
/>
</div>
<div className="relative z-10 flex items-center justify-center h-full w-content-width md:w-45 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>
</section>
);
};
ParallaxAbout.displayName = "ParallaxAbout";
export default memo(ParallaxAbout);

View File

@@ -0,0 +1,178 @@
"use client";
import { memo, Fragment } from "react";
import CardStackTextBox from "@/components/cardStack/CardStackTextBox";
import MediaContent from "@/components/shared/MediaContent";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig } from "@/types/button";
import type { TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
interface BulletPoint {
title: string;
description: string;
icon?: LucideIcon;
}
interface SplitAboutProps {
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
bulletPoints: BulletPoint[];
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
ariaLabel?: string;
imagePosition?: "left" | "right";
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
className?: string;
containerClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
contentClassName?: string;
bulletPointClassName?: string;
bulletTitleClassName?: string;
bulletDescriptionClassName?: string;
mediaWrapperClassName?: string;
imageClassName?: string;
}
const SplitAbout = ({
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
bulletPoints,
imageSrc,
videoSrc,
imageAlt = "",
videoAriaLabel = "About section video",
ariaLabel = "About section",
imagePosition = "right",
textboxLayout,
useInvertedBackground,
className = "",
containerClassName = "",
textBoxClassName = "",
titleClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
contentClassName = "",
bulletPointClassName = "",
bulletTitleClassName = "",
bulletDescriptionClassName = "",
mediaWrapperClassName = "",
imageClassName = "",
}: SplitAboutProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const mediaContent = (
<div className={cls("w-full md:w-6/10 2xl:w-7/10 overflow-hidden rounded-theme-capped card md:relative p-4", mediaWrapperClassName)}>
<div className="md:relative w-full md:h-full">
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("z-1 w-full h-auto object-cover rounded-theme-capped md:absolute md:inset-0 md:h-full", imageClassName)}
/>
</div>
</div>
);
return (
<section
aria-label={ariaLabel}
className={cls(
"relative py-20",
useInvertedBackground === "invertCard"
? "w-content-width-expanded mx-auto rounded-theme-capped bg-foreground"
: "w-full",
useInvertedBackground === "invertDefault" && "bg-foreground",
className
)}
>
<div className={cls("w-content-width mx-auto flex flex-col gap-8", containerClassName)}>
<CardStackTextBox
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
textBoxClassName={textBoxClassName}
titleClassName={titleClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
descriptionClassName={descriptionClassName}
tagClassName={tagClassName}
buttonContainerClassName={buttonContainerClassName}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
/>
<div className={cls("flex flex-col md:flex-row gap-6 md:items-stretch")}>
{imagePosition === "left" && mediaContent}
<div className={cls("w-full md:w-4/10 2xl:w-3/10 rounded-theme-capped card p-6 flex flex-col gap-6 justify-center", contentClassName)}>
{bulletPoints.map((point, index) => {
const Icon = point.icon;
return (
<Fragment key={index}>
<div className={cls("relative z-1 flex flex-col gap-2", bulletPointClassName)}>
{Icon && (
<div className="h-10 w-fit aspect-square rounded-theme primary-button flex items-center justify-center flex-shrink-0 mb-1">
<Icon className="h-[40%] w-[40%] text-background" strokeWidth={1.5} />
</div>
)}
<div className="flex flex-col gap-0">
<h3 className={cls("text-xl font-medium", shouldUseLightText && "text-background", bulletTitleClassName)}>
{point.title}
</h3>
<p className={cls("text-base leading-[1.4]", shouldUseLightText ? "text-background" : "text-foreground", bulletDescriptionClassName)}>
{point.description}
</p>
</div>
</div>
{index < bulletPoints.length - 1 && (
<div className="relative z-1 w-full border-b border-accent/10" />
)}
</Fragment>
);
})}
</div>
{imagePosition === "right" && mediaContent}
</div>
</div>
</section>
);
};
SplitAbout.displayName = "SplitAbout";
export default memo(SplitAbout);

View File

@@ -0,0 +1,129 @@
"use client";
import React, { memo } from "react";
import TextBox from "@/components/Textbox";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig } from "@/components/cardStack/types";
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
interface FeatureCard {
id: string;
title: string;
description: string;
label: string;
}
interface SplitAboutCardsProps {
title: string;
description: string;
tag?: string;
tagIcon?: LucideIcon;
features: FeatureCard[];
buttons?: ButtonConfig[];
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
featuresContainerClassName?: string;
featureCardClassName?: string;
featureTitleClassName?: string;
featureDescriptionClassName?: string;
featureLabelClassName?: string;
}
const SplitAboutCards = ({
title,
description,
tag,
tagIcon,
features,
buttons,
useInvertedBackground,
ariaLabel = "About section",
className = "",
containerClassName = "",
textBoxClassName = "",
titleClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
featuresContainerClassName = "",
featureCardClassName = "",
featureTitleClassName = "",
featureDescriptionClassName = "",
featureLabelClassName = "",
}: SplitAboutCardsProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const isOddCount = features.length % 2 !== 0;
return (
<section
aria-label={ariaLabel}
className={cls("relative py-20", useInvertedBackground === "invertCard" ? "w-content-width-expanded mx-auto rounded-theme-capped bg-foreground" : "w-full", useInvertedBackground === "invertDefault" && "bg-foreground", className)}
>
<div className={cls("w-content-width mx-auto flex flex-col gap-30", containerClassName)}>
<div className="flex flex-col lg:flex-row gap-6 lg:gap-15">
<div className="w-full lg:w-1/3">
<TextBox
title={title}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout="default"
useInvertedBackground={useInvertedBackground}
className={cls("gap-2! md:gap-3!", textBoxClassName)}
titleClassName={cls("text-5xl", titleClassName)}
descriptionClassName={descriptionClassName}
tagClassName={tagClassName}
buttonContainerClassName={buttonContainerClassName}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
/>
</div>
<div className={cls("w-full lg:w-2/3 grid grid-cols-1 md:grid-cols-2 gap-6", featuresContainerClassName)}>
{features.map((feature, index) => {
const isLastItemOdd = isOddCount && index === features.length - 1;
return (
<div
key={feature.id}
className={cls("card rounded-theme-capped p-6 flex flex-col gap-10", isLastItemOdd && "md:col-span-2", featureCardClassName)}
>
<div className="relative z-1 flex flex-col gap-1">
<h3 className={cls("text-2xl md:text-3xl font-medium", shouldUseLightText ? "text-background" : "text-foreground", featureTitleClassName)}>
{feature.title}
</h3>
<p className={cls("text-base leading-tight", shouldUseLightText ? "text-background/75" : "text-foreground/75", featureDescriptionClassName)}>
{feature.description}
</p>
</div>
<p className={cls("relative z-1 text-xl md:text-2xl font-medium", shouldUseLightText ? "text-background" : "text-foreground", featureLabelClassName)}>
{feature.label}
</p>
</div>
);
})}
</div>
</div>
</div>
</section>
);
};
SplitAboutCards.displayName = "SplitAboutCards";
export default memo(SplitAboutCards);

View File

@@ -0,0 +1,102 @@
"use client";
import React, { memo } from "react";
import TextAnimation from "@/components/text/TextAnimation";
import { cls } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
interface Metric {
label: string;
value: string;
}
interface SplitAboutMetricProps {
title: string;
description: string[];
metrics: [Metric, Metric];
showBorder?: boolean;
useInvertedBackground: "noInvert" | "invertDefault" | "invertCard";
ariaLabel?: string;
className?: string;
containerClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
metricsContainerClassName?: string;
metricClassName?: string;
metricValueClassName?: string;
metricLabelClassName?: string;
}
const SplitAboutMetric = ({
title,
description,
metrics,
showBorder = false,
useInvertedBackground,
ariaLabel = "About metrics section",
className = "",
containerClassName = "",
titleClassName = "",
descriptionClassName = "",
metricsContainerClassName = "",
metricClassName = "",
metricValueClassName = "",
metricLabelClassName = "",
}: SplitAboutMetricProps) => {
const theme = useTheme();
return (
<section
aria-label={ariaLabel}
className={cls("relative py-20", useInvertedBackground === "invertCard" ? "w-content-width-expanded mx-auto rounded-theme-capped bg-foreground" : "w-full", useInvertedBackground === "invertDefault" && "bg-foreground", className)}
>
<div className={cls("w-content-width mx-auto flex flex-col gap-30", containerClassName)}>
<div className="flex flex-col md:flex-row gap-3 md:gap-15">
<div className="w-full md:w-1/2">
<TextAnimation
type={theme.defaultTextAnimation}
text={title}
variant="trigger"
className={cls("text-7xl font-medium", (useInvertedBackground === "invertDefault" || useInvertedBackground === "invertCard") && "text-background", titleClassName)}
/>
</div>
<div className="w-full md:w-1/2 flex flex-col gap-12">
{description.map((desc, index) => (
<TextAnimation
key={index}
type={theme.defaultTextAnimation}
text={desc}
variant="words-trigger"
className={cls("text-base md:text-2xl leading-[1.3]", (useInvertedBackground === "invertDefault" || useInvertedBackground === "invertCard") ? "text-background/75" : "text-foreground/75", descriptionClassName)}
/>
))}
<div className="relative w-full border-b border-accent/50" />
<div className={cls("relative grid grid-cols-2 gap-8 md:gap-12", metricsContainerClassName)}>
{metrics.map((metric, index) => (
<div
key={index}
className={cls("w-full flex flex-col items-center text-center gap-0", metricClassName)}
>
<h3 className={cls("w-full text-9xl font-medium leading-[1.0] truncate", (useInvertedBackground === "invertDefault" || useInvertedBackground === "invertCard") && "text-background", metricValueClassName)}>
{metric.value}
</h3>
<p className={cls("text-sm truncate", (useInvertedBackground === "invertDefault" || useInvertedBackground === "invertCard") ? "text-background/80" : "text-foreground/80", metricLabelClassName)}>
{metric.label}
</p>
</div>
))}
</div>
</div>
</div>
{showBorder && <div className="w-full border-b border-foreground/10" />}
</div>
</section>
);
};
SplitAboutMetric.displayName = "SplitAboutMetric";
export default memo(SplitAboutMetric);

View File

@@ -0,0 +1,54 @@
"use client";
import React, { memo } from "react";
import TextAnimation from "@/components/text/TextAnimation";
import { cls } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
interface TagAboutProps {
tag: string;
description: string;
useInvertedBackground: "noInvert" | "invertDefault" | "invertCard";
ariaLabel?: string;
className?: string;
containerClassName?: string;
tagClassName?: string;
descriptionClassName?: string;
}
const TagAbout = ({
tag,
description,
useInvertedBackground,
ariaLabel = "About section",
className = "",
containerClassName = "",
tagClassName = "",
descriptionClassName = "",
}: TagAboutProps) => {
const theme = useTheme();
return (
<section
aria-label={ariaLabel}
className={cls("relative py-20", useInvertedBackground === "invertCard" ? "w-content-width-expanded mx-auto rounded-theme-capped bg-foreground" : "w-full", useInvertedBackground === "invertDefault" && "bg-foreground", className)}
>
<div className={cls("w-content-width mx-auto relative overflow-hidden", containerClassName)}>
<p className={cls("inline-block mr-15 text-base md:text-xl text-accent", tagClassName)}>
{tag}
</p>
<TextAnimation
type={theme.defaultTextAnimation}
text={description}
variant="words-trigger"
as="span"
className={cls(" !inline text-2xl md:text-5xl font-medium leading-[1.15]", (useInvertedBackground === "invertDefault" || useInvertedBackground === "invertCard") && "text-background", descriptionClassName)}
/>
</div>
</section>
);
};
TagAbout.displayName = "TagAbout";
export default memo(TagAbout);

View File

@@ -0,0 +1,211 @@
"use client";
import { memo } from "react";
import MediaContent from "@/components/shared/MediaContent";
import Tag from "@/components/shared/Tag";
import Button from "@/components/button/Button";
import CardStackTextBox from "@/components/cardStack/CardStackTextBox";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
interface TagMediaSplitAboutProps {
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
textboxLayout: TextboxLayout;
contentTag: string;
contentTagIcon?: LucideIcon;
contentTitle: string;
contentDescription: string;
contentButtons?: ButtonConfig[];
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
imagePosition?: "left" | "right";
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxTitleClassName?: string;
textBoxDescriptionClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
contentClassName?: string;
contentCardClassName?: string;
contentTagClassName?: string;
contentTitleClassName?: string;
contentDescriptionClassName?: string;
contentButtonContainerClassName?: string;
contentButtonClassName?: string;
contentButtonTextClassName?: string;
mediaCardClassName?: string;
mediaClassName?: string;
}
const TagMediaSplitAbout = ({
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
textboxLayout,
contentTag,
contentTagIcon,
contentTitle,
contentDescription,
contentButtons,
imageSrc,
videoSrc,
imageAlt,
videoAriaLabel,
imagePosition = "right",
useInvertedBackground,
ariaLabel = "About section",
className = "",
containerClassName = "",
textBoxTitleClassName = "",
textBoxDescriptionClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
contentClassName = "",
contentCardClassName = "",
contentTagClassName = "",
contentTitleClassName = "",
contentDescriptionClassName = "",
contentButtonContainerClassName = "",
contentButtonClassName = "",
contentButtonTextClassName = "",
mediaCardClassName = "",
mediaClassName = "",
}: TagMediaSplitAboutProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const contentCard = (
<div className={cls(
"flex flex-col justify-between gap-4 md:gap-6 card rounded-theme-capped p-6 md:p-10",
contentCardClassName
)}>
<Tag
text={contentTag}
icon={contentTagIcon}
useInvertedBackground={useInvertedBackground}
className={contentTagClassName}
/>
<div className="relative z-1 flex flex-col gap-4">
<h2 className={cls(
"text-4xl font-medium leading-tight",
shouldUseLightText ? "text-background" : "text-foreground",
contentTitleClassName
)}>
{contentTitle}
</h2>
<p className={cls(
"text-base md:text-lg leading-tight",
shouldUseLightText ? "text-background/75" : "text-foreground/75",
contentDescriptionClassName
)}>
{contentDescription}
</p>
{contentButtons && contentButtons.length > 0 && (
<div className={cls("relative z-1 flex gap-4", contentButtonContainerClassName)}>
{contentButtons.slice(0, 2).map((button, index) => (
<Button
key={`${button.text}-${index}`}
{...getButtonProps(button, index, theme.defaultButtonVariant, contentButtonClassName, contentButtonTextClassName)}
/>
))}
</div>
)}
</div>
</div>
);
const mediaCard = (
<div className={cls("card rounded-theme-capped overflow-hidden", mediaCardClassName)}>
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("relative z-1 w-full h-full md:aspect-square object-cover", mediaClassName)}
/>
</div>
);
return (
<section
aria-label={ariaLabel}
className={cls(
"relative py-20",
useInvertedBackground === "invertCard"
? "w-content-width-expanded mx-auto rounded-theme-capped bg-foreground"
: "w-full",
useInvertedBackground === "invertDefault" && "bg-foreground",
className
)}
>
<div className={cls("w-content-width mx-auto flex flex-col gap-10", containerClassName)}>
<CardStackTextBox
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
/>
<div className={cls(
"grid grid-cols-1 md:grid-cols-2 gap-6",
contentClassName
)}>
{imagePosition === "left" ? (
<>
{mediaCard}
{contentCard}
</>
) : (
<>
{contentCard}
{mediaCard}
</>
)}
</div>
</div>
</section>
);
};
TagMediaSplitAbout.displayName = "TagMediaSplitAbout";
export default memo(TagMediaSplitAbout);

View File

@@ -0,0 +1,135 @@
"use client";
import React, { memo } from "react";
import MediaContent from "@/components/shared/MediaContent";
import Tag from "@/components/shared/Tag";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
type MediaProps =
| {
imageSrc: string;
imageAlt?: string;
videoSrc?: never;
videoAriaLabel?: never;
}
| {
videoSrc: string;
videoAriaLabel?: string;
imageSrc?: never;
imageAlt?: never;
};
type TestimonialAboutCardProps = MediaProps & {
tag: string;
tagIcon?: LucideIcon;
title: string;
description: string;
subdescription: string;
icon: LucideIcon;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
contentClassName?: string;
tagClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
subdescriptionClassName?: string;
footerClassName?: string;
iconBoxClassName?: string;
iconClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
};
const TestimonialAboutCard = ({
tag,
tagIcon,
title,
description,
subdescription,
icon: Icon,
imageSrc,
videoSrc,
imageAlt = "",
videoAriaLabel = "Testimonial video",
useInvertedBackground,
ariaLabel = "Testimonial section",
className = "",
containerClassName = "",
cardClassName = "",
contentClassName = "",
tagClassName = "",
titleClassName = "",
descriptionClassName = "",
subdescriptionClassName = "",
footerClassName = "",
iconBoxClassName = "",
iconClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
}: TestimonialAboutCardProps) => {
return (
<section
aria-label={ariaLabel}
className={cls(
"relative py-20",
useInvertedBackground === "invertCard"
? "w-content-width-expanded mx-auto rounded-theme-capped bg-foreground"
: "w-full",
useInvertedBackground === "invertDefault" && "bg-foreground",
className
)}
>
<div className={cls("w-content-width mx-auto grid grid-cols-1 md:grid-cols-5 gap-6", containerClassName)}>
<div className={cls("relative md:col-span-3 card rounded-theme-capped p-8 md:p-12", cardClassName)}>
<div className={cls(
"absolute -top-7 -left-7 md:-top-8 md:-left-8 primary-button rounded-theme-capped h-14 md:h-16 w-auto aspect-square flex items-center justify-center",
iconBoxClassName
)}>
<Icon className={cls("h-5/10 text-background", iconClassName)} strokeWidth={1.5} />
</div>
<div className={cls("relative h-full flex flex-col justify-center gap-4 md:gap-6 py-8 md:py-4", contentClassName)}>
<Tag
text={tag}
icon={tagIcon}
className={cls("mb-1", tagClassName)}
/>
<h2 className={cls("text-3xl md:text-4xl font-medium text-foreground leading-tight", titleClassName)}>
{title}
</h2>
<div className={cls("flex items-center gap-2", footerClassName)}>
<span className={cls("text-base text-foreground", descriptionClassName)}>
{description}
</span>
<span className="text-accent"></span>
<span className={cls("text-base text-foreground/75", subdescriptionClassName)}>
{subdescription}
</span>
</div>
</div>
</div>
<div className={cls("md:col-span-2 card aspect-square rounded-theme-capped overflow-hidden", mediaWrapperClassName)}>
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("w-full h-full object-cover", mediaClassName)}
/>
</div>
</div>
</section>
);
};
TestimonialAboutCard.displayName = "TestimonialAboutCard";
export default memo(TestimonialAboutCard);

View File

@@ -0,0 +1,65 @@
"use client";
import React, { memo } from "react";
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 TextAboutProps {
title: string;
buttons?: ButtonConfig[];
useInvertedBackground: "noInvert" | "invertDefault" | "invertCard";
ariaLabel?: string;
className?: string;
containerClassName?: string;
titleClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
}
const TextAbout = ({
title,
buttons,
useInvertedBackground,
ariaLabel = "About section",
className = "",
containerClassName = "",
titleClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
}: TextAboutProps) => {
const theme = useTheme();
return (
<section
aria-label={ariaLabel}
className={cls("relative py-20", useInvertedBackground === "invertCard" ? "w-content-width-expanded mx-auto rounded-theme-capped bg-foreground" : "w-full", useInvertedBackground === "invertDefault" && "bg-foreground", className)}
>
<div className={cls("w-content-width mx-auto flex flex-col gap-6 items-center", containerClassName)}>
<TextAnimation
type={theme.defaultTextAnimation}
text={title}
variant="words-trigger"
className={cls("text-2xl md:text-5xl font-medium text-center leading-[1.175]", (useInvertedBackground === "invertDefault" || useInvertedBackground === "invertCard") && "text-background", titleClassName)}
/>
{buttons && buttons.length > 0 && (
<div className={cls("flex gap-4", buttonContainerClassName)}>
{buttons.slice(0, 2).map((button, index) => (
<Button key={index} {...getButtonProps(button, index, theme.defaultButtonVariant, cls("px-8", buttonClassName), cls("text-base", buttonTextClassName))} />
))}
</div>
)}
</div>
</section>
);
};
TextAbout.displayName = "TextAbout";
export default memo(TextAbout);

View File

@@ -0,0 +1,88 @@
"use client";
import React, { memo } from "react";
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 TextSplitAboutProps {
title: string;
description: string[];
buttons?: ButtonConfig[];
showBorder?: boolean;
useInvertedBackground: "noInvert" | "invertDefault" | "invertCard";
ariaLabel?: string;
className?: string;
containerClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
}
const TextSplitAbout = ({
title,
description,
buttons,
showBorder = false,
useInvertedBackground,
ariaLabel = "About section",
className = "",
containerClassName = "",
titleClassName = "",
descriptionClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
}: TextSplitAboutProps) => {
const theme = useTheme();
return (
<section
aria-label={ariaLabel}
className={cls("relative py-20", useInvertedBackground === "invertCard" ? "w-content-width-expanded mx-auto rounded-theme-capped bg-foreground" : "w-full", useInvertedBackground === "invertDefault" && "bg-foreground", className)}
>
<div className={cls("w-content-width mx-auto flex flex-col gap-30", containerClassName)}>
<div className="flex flex-col md:flex-row gap-3 md:gap-15">
<div className="w-full md:w-1/2">
<TextAnimation
type={theme.defaultTextAnimation}
text={title}
variant="trigger"
className={cls("text-7xl font-medium", (useInvertedBackground === "invertDefault" || useInvertedBackground === "invertCard") && "text-background", titleClassName)}
/>
</div>
<div className="w-full md:w-1/2 flex flex-col gap-6">
{description.map((desc, index) => (
<TextAnimation
key={index}
type={theme.defaultTextAnimation}
text={desc}
variant="words-trigger"
className={cls("text-base md:text-2xl leading-[1.3]", (useInvertedBackground === "invertDefault" || useInvertedBackground === "invertCard") ? "text-background/75" : "text-foreground/75", descriptionClassName)}
/>
))}
{buttons && buttons.length > 0 && (
<div className={cls("flex gap-4", buttonContainerClassName)}>
{buttons.slice(0, 2).map((button, index) => (
<Button key={index} {...getButtonProps(button, index, theme.defaultButtonVariant, cls("px-8", buttonClassName), cls("text-base", buttonTextClassName))} />
))}
</div>
)}
</div>
</div>
{showBorder && <div className="w-full border-b border-foreground/10" />}
</div>
</section>
);
};
TextSplitAbout.displayName = "TextSplitAbout";
export default memo(TextSplitAbout);

View File

@@ -0,0 +1,211 @@
"use client";
import React, { memo } from "react";
import CardStack from "@/components/cardStack/CardStack";
import Tag from "@/components/shared/Tag";
import MediaContent from "@/components/shared/MediaContent";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type MediaProps =
| {
imageSrc: string;
imageAlt?: string;
videoSrc?: never;
videoAriaLabel?: never;
}
| {
videoSrc: string;
videoAriaLabel?: string;
imageSrc?: never;
imageAlt?: never;
};
type BlogCard = MediaProps & {
id: string;
category: string;
title: string;
tags: string[];
onBlogClick?: () => void;
};
interface BlogCardEightProps {
blogs: BlogCard[];
carouselMode?: "auto" | "buttons";
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
cardTitleClassName?: string;
footerClassName?: string;
tagClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
}
interface BlogCardItemProps {
blog: BlogCard;
cardClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
cardTitleClassName?: string;
footerClassName?: string;
tagClassName?: string;
}
const BlogCardItem = memo(({
blog,
cardClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
cardTitleClassName = "",
footerClassName = "",
tagClassName = "",
}: BlogCardItemProps) => {
return (
<article
className={cls("relative h-full card group flex flex-col gap-4 cursor-pointer p-4 rounded-theme-capped", cardClassName)}
onClick={blog.onBlogClick}
role="article"
aria-label={blog.title}
>
<div className="relative h-full primary-button flex flex-col gap-4 p-4 rounded-theme-capped">
<span className="text-base text-background/75 leading-tight truncate">
{blog.category}
</span>
<div className={cls("relative z-1 w-full aspect-square md:aspect-[3/4] overflow-hidden rounded-theme-capped", mediaWrapperClassName)}>
<MediaContent
imageSrc={blog.imageSrc}
videoSrc={blog.videoSrc}
imageAlt={blog.imageAlt}
videoAriaLabel={blog.videoAriaLabel}
imageClassName={cls("w-full h-full object-cover", mediaClassName)}
/>
</div>
<h3 className={cls("text-2xl font-medium leading-tight text-background line-clamp-2", cardTitleClassName)}>
{blog.title}
</h3>
</div>
<div className={cls("flex flex-wrap gap-2 mt-auto", footerClassName)}>
{blog.tags.map((tag, index) => (
<Tag key={index} text={tag} className={tagClassName} />
))}
</div>
</article>
);
});
BlogCardItem.displayName = "BlogCardItem";
const BlogCardEight = ({
blogs,
carouselMode = "buttons",
uniformGridCustomHeightClasses,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
textboxLayout,
useInvertedBackground,
ariaLabel = "Blog section",
className = "",
containerClassName = "",
cardClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
cardTitleClassName = "",
footerClassName = "",
tagClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: BlogCardEightProps) => {
return (
<CardStack
mode={carouselMode}
gridVariant="uniform-all-items-equal"
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
ariaLabel={ariaLabel}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
>
{blogs.map((blog) => (
<BlogCardItem
key={blog.id}
blog={blog}
cardClassName={cardClassName}
mediaWrapperClassName={mediaWrapperClassName}
mediaClassName={mediaClassName}
cardTitleClassName={cardTitleClassName}
footerClassName={footerClassName}
tagClassName={tagClassName}
/>
))}
</CardStack>
);
};
BlogCardEight.displayName = "BlogCardEight";
export default memo(BlogCardEight);

View File

@@ -0,0 +1,195 @@
"use client";
import React, { memo } from "react";
import CardList from "@/components/cardStack/CardList";
import Tag from "@/components/shared/Tag";
import MediaContent from "@/components/shared/MediaContent";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type MediaProps =
| {
imageSrc: string;
imageAlt?: string;
videoSrc?: never;
videoAriaLabel?: never;
}
| {
videoSrc: string;
videoAriaLabel?: string;
imageSrc?: never;
imageAlt?: never;
};
type BlogCard = MediaProps & {
id: string;
title: string;
author: string;
description: string;
tags: string[];
onBlogClick?: () => void;
};
interface BlogCardElevenProps {
blogs: BlogCard[];
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxTitleClassName?: string;
textBoxDescriptionClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
cardContentClassName?: string;
cardTitleClassName?: string;
authorClassName?: string;
cardDescriptionClassName?: string;
tagsContainerClassName?: string;
tagClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
}
const BlogCardEleven = ({
blogs,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
textboxLayout,
useInvertedBackground,
ariaLabel = "Blog section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxDescriptionClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
cardContentClassName = "",
cardTitleClassName = "",
authorClassName = "",
cardDescriptionClassName = "",
tagsContainerClassName = "",
tagClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
}: BlogCardElevenProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<CardList
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
animationType={animationType}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
cardClassName={cardClassName}
titleClassName={textBoxTitleClassName}
descriptionClassName={textBoxDescriptionClassName}
textBoxClassName={textBoxClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
ariaLabel={ariaLabel}
>
{blogs.map((blog) => (
<article
key={blog.id}
className={cls(
"relative z-1 w-full min-h-0 h-full flex flex-col md:grid md:grid-cols-10 gap-6 md:gap-10 cursor-pointer group p-6 md:p-10",
cardContentClassName
)}
onClick={blog.onBlogClick}
role="article"
aria-label={blog.title}
>
<div className="relative z-1 w-full md:col-span-6 flex flex-col gap-3 md:gap-12">
<h3 className={cls(
"text-3xl md:text-5xl text-balance font-medium leading-tight line-clamp-3",
shouldUseLightText ? "text-background" : "text-foreground",
cardTitleClassName
)}>
{blog.title}{" "}
<span className={cls(
shouldUseLightText ? "text-background/50" : "text-foreground/50",
authorClassName
)}>
by {blog.author}
</span>
</h3>
<div className="mt-auto flex flex-col gap-4">
<div className={cls("flex flex-wrap gap-2", tagsContainerClassName)}>
{blog.tags.map((tagText, index) => (
<Tag key={index} text={tagText} className={tagClassName} />
))}
</div>
<p className={cls(
"text-base md:text-2xl text-balance leading-tight line-clamp-2",
shouldUseLightText ? "text-background" : "text-foreground",
cardDescriptionClassName
)}>
{blog.description}
</p>
</div>
</div>
<div className={cls(
"relative z-1 w-full md:col-span-4 aspect-square md:aspect-auto overflow-hidden rounded-theme-capped",
mediaWrapperClassName
)}>
<MediaContent
imageSrc={blog.imageSrc}
videoSrc={blog.videoSrc}
imageAlt={blog.imageAlt}
videoAriaLabel={blog.videoAriaLabel}
imageClassName={cls("w-full h-full object-cover", mediaClassName)}
/>
</div>
</article>
))}
</CardList>
);
};
BlogCardEleven.displayName = "BlogCardEleven";
export default memo(BlogCardEleven);

View File

@@ -0,0 +1,169 @@
"use client";
import React, { memo } from "react";
import CardStackTextBox from "@/components/cardStack/CardStackTextBox";
import MediaContent from "@/components/shared/MediaContent";
import { useCardAnimation } from "@/components/cardStack/hooks/useCardAnimation";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type BlogPost = {
id: string;
title: string;
items: string[];
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
};
interface BlogCardFiveProps {
blogs: BlogPost[];
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxTitleClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
textBoxDescriptionClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
gridClassName?: string;
cardClassName?: string;
cardContentClassName?: string;
cardTitleClassName?: string;
itemsContainerClassName?: string;
itemTextClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
}
const BlogCardFive = ({
blogs,
animationType,
title,
titleSegments,
description,
textboxLayout,
useInvertedBackground,
tag,
tagIcon,
buttons,
ariaLabel = "Blog section",
className = "",
containerClassName = "",
textBoxTitleClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
textBoxDescriptionClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
gridClassName = "",
cardClassName = "",
cardContentClassName = "",
cardTitleClassName = "",
itemsContainerClassName = "",
itemTextClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
}: BlogCardFiveProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const { itemRefs } = useCardAnimation({ animationType, itemCount: blogs.length });
return (
<section
aria-label={ariaLabel}
className={cls("relative py-20", useInvertedBackground === "invertCard" ? "w-content-width-expanded mx-auto rounded-theme-capped bg-foreground" : "w-full", useInvertedBackground === "invertDefault" && "bg-foreground", className)}
>
<div className={cls("w-content-width mx-auto flex flex-col gap-8", containerClassName)}>
<CardStackTextBox
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
/>
<div className={cls("grid grid-cols-1 md:grid-cols-2 gap-8 card rounded-theme-capped p-5 md:p-8", gridClassName)}>
{blogs.map((blog, index) => (
<article
key={blog.id}
ref={(el) => { itemRefs.current[index] = el; }}
className={cls("relative h-full flex flex-col md:flex-row md:items-center gap-4 md:gap-8 cursor-pointer", cardClassName)}
>
<div className={cls("relative z-1 w-full md:h-50 md:w-auto aspect-square rounded-theme-capped overflow-hidden shrink-0", mediaWrapperClassName)}>
<MediaContent
imageSrc={blog.imageSrc}
videoSrc={blog.videoSrc}
imageAlt={blog.imageAlt || blog.title}
videoAriaLabel={blog.videoAriaLabel || blog.title}
imageClassName={cls("w-full h-full object-cover", mediaClassName)}
/>
</div>
<div className={cls("relative z-1 flex flex-col gap-2 min-w-0 flex-1", cardContentClassName)}>
<h3 className={cls(
"text-3xl font-medium leading-tight line-clamp-2",
shouldUseLightText ? "text-background" : "text-foreground",
cardTitleClassName
)}>
{blog.title}
</h3>
<div className={cls("flex flex-wrap items-center gap-2", itemsContainerClassName)}>
{blog.items.map((item, index) => (
<React.Fragment key={index}>
<span className={cls(
"text-sm",
shouldUseLightText ? "text-background/75" : "text-foreground/75",
itemTextClassName
)}>
{item}
</span>
{index < blog.items.length - 1 && (
<span className="text-sm text-accent"></span>
)}
</React.Fragment>
))}
</div>
</div>
</article>
))}
</div>
</div>
</section>
);
};
BlogCardFive.displayName = "BlogCardFive";
export default memo(BlogCardFive);

View File

@@ -0,0 +1,218 @@
"use client";
import React, { memo } from "react";
import CardList from "@/components/cardStack/CardList";
import Tag from "@/components/shared/Tag";
import MediaContent from "@/components/shared/MediaContent";
import Button from "@/components/button/Button";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type MediaProps =
| {
imageSrc: string;
imageAlt?: string;
videoSrc?: never;
videoAriaLabel?: never;
}
| {
videoSrc: string;
videoAriaLabel?: string;
imageSrc?: never;
imageAlt?: never;
};
type BlogCard = MediaProps & {
id: string;
category: string;
title: string;
tags: string[];
buttons?: ButtonConfig[];
onBlogClick?: () => void;
};
interface BlogCardNineProps {
blogs: BlogCard[];
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxTitleClassName?: string;
textBoxDescriptionClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
cardContentClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
categoryClassName?: string;
cardTitleClassName?: string;
tagsContainerClassName?: string;
tagClassName?: string;
cardButtonClassName?: string;
cardButtonTextClassName?: string;
}
const BlogCardNine = ({
blogs,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
textboxLayout,
useInvertedBackground,
ariaLabel = "Blog section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxDescriptionClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
cardContentClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
categoryClassName = "",
cardTitleClassName = "",
tagsContainerClassName = "",
tagClassName = "",
cardButtonClassName = "",
cardButtonTextClassName = "",
}: BlogCardNineProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const getMobileButtonProps = () => {
if (theme.defaultButtonVariant === "hover-bubble") {
return { bgClassName: "w-full" };
}
if (theme.defaultButtonVariant === "icon-arrow") {
return { className: "justify-between" };
}
return {};
};
return (
<CardList
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
animationType={animationType}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
cardClassName={cardClassName}
titleClassName={textBoxTitleClassName}
descriptionClassName={textBoxDescriptionClassName}
textBoxClassName={textBoxClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
ariaLabel={ariaLabel}
>
{blogs.map((blog) => (
<article
key={blog.id}
className={cls(
"relative z-1 w-full min-h-0 h-full flex flex-col md:flex-row gap-6 cursor-pointer p-6 md:p-10",
cardContentClassName
)}
onClick={blog.onBlogClick}
role="article"
aria-label={blog.title}
>
<div className={cls(
"relative z-1 w-full md:w-2/5 aspect-square md:aspect-[4/3] overflow-hidden rounded-theme-capped",
mediaWrapperClassName
)}>
<MediaContent
imageSrc={blog.imageSrc}
videoSrc={blog.videoSrc}
imageAlt={blog.imageAlt}
videoAriaLabel={blog.videoAriaLabel}
imageClassName={cls("w-full h-full object-cover", mediaClassName)}
/>
</div>
<div className="relative z-1 w-full h-px bg-foreground/20 md:hidden" />
<div className="relative z-1 w-full md:w-3/5 flex flex-col gap-2">
<span className={cls(
"text-lg leading-tight",
shouldUseLightText ? "text-background/75" : "text-foreground/75",
categoryClassName
)}>
{blog.category}
</span>
<h3 className={cls(
"text-2xl md:text-4xl font-medium text-balance leading-tight",
shouldUseLightText ? "text-background" : "text-foreground",
cardTitleClassName
)}>
{blog.title}
</h3>
<div className={cls("relative mt-1 flex flex-wrap gap-2", tagsContainerClassName)}>
{blog.tags.map((tagText, index) => (
<Tag key={index} text={tagText} className={tagClassName} />
))}
</div>
{blog.buttons && blog.buttons.length > 0 && (
<>
<div className="mt-6 flex-col gap-4 flex md:hidden">
{blog.buttons.slice(0, 2).map((button, index) => (
<Button key={`${button.text}-${index}`} {...getButtonProps(button, index, theme.defaultButtonVariant, cls("w-full", cardButtonClassName), cardButtonTextClassName)} {...getMobileButtonProps()} />
))}
</div>
<div className="mt-auto gap-4 hidden md:flex">
{blog.buttons.slice(0, 2).map((button, index) => (
<Button key={`${button.text}-${index}`} {...getButtonProps(button, index, theme.defaultButtonVariant, cardButtonClassName, cardButtonTextClassName)} />
))}
</div>
</>
)}
</div>
</article>
))}
</CardList>
);
};
BlogCardNine.displayName = "BlogCardNine";
export default memo(BlogCardNine);

View File

@@ -0,0 +1,247 @@
"use client";
import React, { memo } from "react";
import Image from "next/image";
import CardStack from "@/components/cardStack/CardStack";
import Badge from "@/components/shared/Badge";
import OverlayArrowButton from "@/components/shared/OverlayArrowButton";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type BlogCard = {
id: string;
category: string;
title: string;
excerpt: string;
imageSrc: string;
imageAlt?: string;
authorName: string;
authorAvatar: string;
date: string;
onBlogClick?: () => void;
};
interface BlogCardOneProps {
blogs: BlogCard[];
carouselMode?: "auto" | "buttons";
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
imageWrapperClassName?: string;
imageClassName?: string;
categoryClassName?: string;
cardTitleClassName?: string;
excerptClassName?: string;
authorContainerClassName?: string;
authorAvatarClassName?: string;
authorNameClassName?: string;
dateClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
}
interface BlogCardItemProps {
blog: BlogCard;
shouldUseLightText: boolean;
cardClassName?: string;
imageWrapperClassName?: string;
imageClassName?: string;
categoryClassName?: string;
cardTitleClassName?: string;
excerptClassName?: string;
authorContainerClassName?: string;
authorAvatarClassName?: string;
authorNameClassName?: string;
dateClassName?: string;
}
const BlogCardItem = memo(({
blog,
shouldUseLightText,
cardClassName = "",
imageWrapperClassName = "",
imageClassName = "",
categoryClassName = "",
cardTitleClassName = "",
excerptClassName = "",
authorContainerClassName = "",
authorAvatarClassName = "",
authorNameClassName = "",
dateClassName = "",
}: BlogCardItemProps) => {
return (
<article
className={cls("relative h-full card group flex flex-col gap-4 cursor-pointer p-4 rounded-theme-capped", cardClassName)}
onClick={blog.onBlogClick}
role="article"
aria-label={`${blog.title} by ${blog.authorName}`}
>
<div className={cls("relative z-1 w-full aspect-[4/3] overflow-hidden rounded-theme-capped", imageWrapperClassName)}>
<Image
src={blog.imageSrc}
alt={blog.imageAlt || blog.title}
fill
className={cls("w-full h-full object-cover transition-transform duration-500 ease-in-out group-hover:scale-105", imageClassName)}
unoptimized={blog.imageSrc.startsWith('http') || blog.imageSrc.startsWith('//')}
/>
<OverlayArrowButton ariaLabel={`Read ${blog.title}`} />
</div>
<div className="relative z-1 flex flex-col justify-between gap-6 flex-1">
<div className="flex flex-col gap-2">
<Badge text={blog.category} variant="primary" className={categoryClassName} />
<h3 className={cls("text-2xl font-medium leading-[1.25] mt-1", shouldUseLightText ? "text-background" : "text-foreground", cardTitleClassName)}>
{blog.title}
</h3>
<p className={cls("text-base leading-[1.25]", shouldUseLightText ? "text-background" : "text-foreground", excerptClassName)}>
{blog.excerpt}
</p>
</div>
<div className={cls("flex items-center gap-3", authorContainerClassName)}>
<Image
src={blog.authorAvatar}
alt={blog.authorName}
width={40}
height={40}
className={cls("h-9 w-auto aspect-square rounded-theme object-cover", authorAvatarClassName)}
unoptimized={blog.authorAvatar.startsWith('http') || blog.authorAvatar.startsWith('//')}
/>
<div className="flex flex-col">
<p className={cls("text-sm font-medium", shouldUseLightText ? "text-background" : "text-foreground", authorNameClassName)}>
{blog.authorName}
</p>
<p className={cls("text-xs", shouldUseLightText ? "text-background/75" : "text-foreground/75", dateClassName)}>
{blog.date}
</p>
</div>
</div>
</div>
</article>
);
});
BlogCardItem.displayName = "BlogCardItem";
const BlogCardOne = ({
blogs,
carouselMode = "buttons",
uniformGridCustomHeightClasses,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
textboxLayout,
useInvertedBackground,
ariaLabel = "Blog section",
className = "",
containerClassName = "",
cardClassName = "",
imageWrapperClassName = "",
imageClassName = "",
categoryClassName = "",
cardTitleClassName = "",
excerptClassName = "",
authorContainerClassName = "",
authorAvatarClassName = "",
authorNameClassName = "",
dateClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: BlogCardOneProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<CardStack
mode={carouselMode}
gridVariant="uniform-all-items-equal"
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
ariaLabel={ariaLabel}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
>
{blogs.map((blog) => (
<BlogCardItem
key={blog.id}
blog={blog}
shouldUseLightText={shouldUseLightText}
cardClassName={cardClassName}
imageWrapperClassName={imageWrapperClassName}
imageClassName={imageClassName}
categoryClassName={categoryClassName}
cardTitleClassName={cardTitleClassName}
excerptClassName={excerptClassName}
authorContainerClassName={authorContainerClassName}
authorAvatarClassName={authorAvatarClassName}
authorNameClassName={authorNameClassName}
dateClassName={dateClassName}
/>
))}
</CardStack>
);
};
BlogCardOne.displayName = "BlogCardOne";
export default memo(BlogCardOne);

View File

@@ -0,0 +1,205 @@
"use client";
import React, { memo } from "react";
import Image from "next/image";
import CardStack from "@/components/cardStack/CardStack";
import { ArrowRight } from "lucide-react";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationType, GridVariant, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type BlogCard = {
id: string;
title: string;
date: string;
imageSrc: string;
imageAlt?: string;
onBlogClick?: () => void;
};
interface BlogCardSevenProps {
blogs: BlogCard[];
carouselMode?: "auto" | "buttons";
gridVariant: GridVariant;
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
cardTitleClassName?: string;
cardDateClassName?: string;
cardLinkClassName?: string;
imageWrapperClassName?: string;
imageClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
}
interface BlogCardItemProps {
blog: BlogCard;
shouldUseLightText: boolean;
cardClassName?: string;
titleClassName?: string;
dateClassName?: string;
linkClassName?: string;
imageWrapperClassName?: string;
imageClassName?: string;
}
const BlogCardItem = memo(({
blog,
shouldUseLightText,
cardClassName = "",
titleClassName = "",
dateClassName = "",
linkClassName = "",
imageWrapperClassName = "",
imageClassName = "",
}: BlogCardItemProps) => {
return (
<article
className={cls("relative h-full card group grid grid-cols-2 gap-6 cursor-pointer p-6 rounded-theme-capped", cardClassName)}
onClick={blog.onBlogClick}
role="article"
aria-label={blog.title}
>
<div className="col-span-1 flex flex-col gap-6 justify-between">
<div className="flex flex-col gap-2">
<p className={cls("text-sm", shouldUseLightText ? "text-background/75" : "text-foreground/75", dateClassName)}>
{blog.date}
</p>
<h3 className={cls("text-2xl font-medium leading-tight line-clamp-3", shouldUseLightText ? "text-background" : "text-foreground", titleClassName)}>
{blog.title}
</h3>
</div>
<div className={cls("flex items-center gap-2 text-sm group-hover:gap-3 transition-all duration-300", shouldUseLightText ? "text-background" : "text-foreground", linkClassName)}>
<span>Read blog</span>
<ArrowRight className="h-[1em] w-auto" />
</div>
</div>
<div className={cls("col-span-1 relative overflow-hidden rounded-theme-capped", imageWrapperClassName)}>
<Image
src={blog.imageSrc}
alt={blog.imageAlt || blog.title}
fill
className={cls("w-full h-full object-cover transition-transform duration-500 ease-in-out group-hover:scale-105", imageClassName)}
unoptimized={blog.imageSrc.startsWith('http') || blog.imageSrc.startsWith('//')}
/>
</div>
</article>
);
});
BlogCardItem.displayName = "BlogCardItem";
const BlogCardSeven = ({
blogs,
carouselMode = "buttons",
gridVariant,
uniformGridCustomHeightClasses,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
textboxLayout,
useInvertedBackground,
ariaLabel = "Blog section",
className = "",
containerClassName = "",
cardClassName = "",
cardTitleClassName = "",
cardDateClassName = "",
cardLinkClassName = "",
imageWrapperClassName = "",
imageClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: BlogCardSevenProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<CardStack
mode={carouselMode}
gridVariant={gridVariant}
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
ariaLabel={ariaLabel}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
carouselItemClassName="!w-carousel-item-3"
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
>
{blogs.map((blog) => (
<BlogCardItem
key={blog.id}
blog={blog}
shouldUseLightText={shouldUseLightText}
cardClassName={cardClassName}
titleClassName={cardTitleClassName}
dateClassName={cardDateClassName}
linkClassName={cardLinkClassName}
imageWrapperClassName={imageWrapperClassName}
imageClassName={imageClassName}
/>
))}
</CardStack>
);
};
BlogCardSeven.displayName = "BlogCardSeven";
export default memo(BlogCardSeven);

View File

@@ -0,0 +1,235 @@
"use client";
import React, { memo } from "react";
import { ArrowRight } from "lucide-react";
import CardStack from "@/components/cardStack/CardStack";
import MediaContent from "@/components/shared/MediaContent";
import Tag from "@/components/shared/Tag";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type BlogCard = {
id: string;
title: string;
tags: string[];
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
onBlogClick?: () => void;
};
interface BlogCardSixProps {
blogs: BlogCard[];
carouselMode?: "auto" | "buttons";
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
itemClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
cardClassName?: string;
cardTitleClassName?: string;
tagsContainerClassName?: string;
tagClassName?: string;
arrowClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
}
interface BlogCardItemProps {
blog: BlogCard;
shouldUseLightText: boolean;
useInvertedBackground: InvertedBackground;
itemClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
cardClassName?: string;
cardTitleClassName?: string;
tagsContainerClassName?: string;
tagClassName?: string;
arrowClassName?: string;
}
const BlogCardItem = memo(({
blog,
shouldUseLightText,
useInvertedBackground,
itemClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
cardClassName = "",
cardTitleClassName = "",
tagsContainerClassName = "",
tagClassName = "",
arrowClassName = "",
}: BlogCardItemProps) => {
return (
<article
className={cls("relative h-full flex flex-col gap-6 cursor-pointer group", itemClassName)}
onClick={blog.onBlogClick}
role="article"
aria-label={blog.title}
>
<div className={cls("relative w-full aspect-square overflow-hidden rounded-theme-capped", mediaWrapperClassName)}>
<MediaContent
imageSrc={blog.imageSrc}
videoSrc={blog.videoSrc}
imageAlt={blog.imageAlt || blog.title}
videoAriaLabel={blog.videoAriaLabel || blog.title}
imageClassName={cls("w-full h-full object-cover transition-transform duration-500 ease-in-out group-hover:scale-105", mediaClassName)}
/>
</div>
<div className={cls("relative z-1 card rounded-theme-capped p-5 flex flex-col gap-4", cardClassName)}>
<h3 className={cls(
"text-xl md:text-2xl font-medium leading-tight",
shouldUseLightText ? "text-background" : "text-foreground",
cardTitleClassName
)}>
{blog.title}
</h3>
<div className="flex items-center justify-between gap-4">
<div className={cls("flex items-center gap-2 flex-wrap", tagsContainerClassName)}>
{blog.tags.map((tag, index) => (
<Tag
key={index}
text={tag}
useInvertedBackground={useInvertedBackground}
className={tagClassName}
/>
))}
</div>
<ArrowRight
className={cls(
"h-[var(--text-base)] w-auto shrink-0 transition-transform duration-300 group-hover:-rotate-45",
shouldUseLightText ? "text-background" : "text-foreground",
arrowClassName
)}
strokeWidth={1.5}
/>
</div>
</div>
</article>
);
});
BlogCardItem.displayName = "BlogCardItem";
const BlogCardSix = ({
blogs,
carouselMode = "buttons",
uniformGridCustomHeightClasses,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
textboxLayout,
useInvertedBackground,
ariaLabel = "Blog section",
className = "",
containerClassName = "",
itemClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
cardClassName = "",
cardTitleClassName = "",
tagsContainerClassName = "",
tagClassName = "",
arrowClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: BlogCardSixProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<CardStack
mode={carouselMode}
gridVariant="uniform-all-items-equal"
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
ariaLabel={ariaLabel}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
>
{blogs.map((blog) => (
<BlogCardItem
key={blog.id}
blog={blog}
shouldUseLightText={shouldUseLightText}
useInvertedBackground={useInvertedBackground}
itemClassName={itemClassName}
mediaWrapperClassName={mediaWrapperClassName}
mediaClassName={mediaClassName}
cardClassName={cardClassName}
cardTitleClassName={cardTitleClassName}
tagsContainerClassName={tagsContainerClassName}
tagClassName={tagClassName}
arrowClassName={arrowClassName}
/>
))}
</CardStack>
);
};
BlogCardSix.displayName = "BlogCardSix";
export default memo(BlogCardSix);

View File

@@ -0,0 +1,166 @@
"use client";
import React, { memo } from "react";
import CardList from "@/components/cardStack/CardList";
import Tag from "@/components/shared/Tag";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import { ArrowRight } from "lucide-react";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type BlogCard = {
id: string;
title: string;
subtitle: string;
tags: string[];
onBlogClick?: () => void;
};
interface BlogCardTenProps {
blogs: BlogCard[];
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxTitleClassName?: string;
textBoxDescriptionClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
cardContentClassName?: string;
cardTitleClassName?: string;
subtitleClassName?: string;
tagsContainerClassName?: string;
tagClassName?: string;
arrowClassName?: string;
}
const BlogCardTen = ({
blogs,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
textboxLayout,
useInvertedBackground,
ariaLabel = "Blog section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxDescriptionClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
cardContentClassName = "",
cardTitleClassName = "",
subtitleClassName = "",
tagsContainerClassName = "",
tagClassName = "",
arrowClassName = "",
}: BlogCardTenProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<CardList
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
animationType={animationType}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
cardClassName={cardClassName}
titleClassName={textBoxTitleClassName}
descriptionClassName={textBoxDescriptionClassName}
textBoxClassName={textBoxClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
ariaLabel={ariaLabel}
>
{blogs.map((blog) => (
<article
key={blog.id}
className={cls(
"relative z-1 w-full min-h-0 h-full flex flex-col gap-6 cursor-pointer group p-6 md:p-10",
cardContentClassName
)}
onClick={blog.onBlogClick}
role="article"
aria-label={blog.title}
>
<div className="flex flex-col">
<h3 className={cls(
"text-3xl md:text-5xl font-medium text-balance leading-tight line-clamp-2",
shouldUseLightText ? "text-background" : "text-foreground",
cardTitleClassName
)}>
{blog.title}
</h3>
<span className={cls(
"text-base md:text-lg leading-tight",
shouldUseLightText ? "text-background/75" : "text-foreground/75",
subtitleClassName
)}>
{blog.subtitle}
</span>
</div>
<div className="flex items-end justify-between mt-auto">
<div className={cls("flex flex-wrap gap-2", tagsContainerClassName)}>
{blog.tags.map((tagText, index) => (
<Tag key={index} text={tagText} className={tagClassName} />
))}
</div>
<ArrowRight
className={cls(
"h-[var(--text-base)] w-auto transition-transform duration-300 group-hover:-rotate-45",
shouldUseLightText ? "text-background" : "text-foreground",
arrowClassName
)}
strokeWidth={1.5}
/>
</div>
</article>
))}
</CardList>
);
};
BlogCardTen.displayName = "BlogCardTen";
export default memo(BlogCardTen);

View File

@@ -0,0 +1,228 @@
"use client";
import React, { memo } from "react";
import CardStack from "@/components/cardStack/CardStack";
import Tag from "@/components/shared/Tag";
import MediaContent from "@/components/shared/MediaContent";
import OverlayArrowButton from "@/components/shared/OverlayArrowButton";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type BlogCard = {
id: string;
category: string;
categoryIcon?: LucideIcon;
title: string;
description: string;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
onBlogClick?: () => void;
};
interface BlogCardThreeProps {
blogs: BlogCard[];
carouselMode?: "auto" | "buttons";
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
cardContentClassName?: string;
categoryTagClassName?: string;
cardTitleClassName?: string;
cardDescriptionClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
}
interface BlogCardItemProps {
blog: BlogCard;
useInvertedBackground: "noInvert" | "invertDefault" | "invertCard";
cardClassName?: string;
cardContentClassName?: string;
categoryTagClassName?: string;
cardTitleClassName?: string;
cardDescriptionClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
}
const BlogCardItem = memo(({
blog,
useInvertedBackground,
cardClassName = "",
cardContentClassName = "",
categoryTagClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
}: BlogCardItemProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<article
className={cls(
"relative h-full card group flex flex-col justify-between gap-6 p-6 cursor-pointer rounded-theme-capped overflow-hidden",
cardClassName
)}
onClick={blog.onBlogClick}
role="article"
aria-label={blog.title}
>
<div className={cls("relative z-1 flex flex-col gap-3", cardContentClassName)}>
<Tag
text={blog.category}
icon={blog.categoryIcon}
useInvertedBackground={useInvertedBackground}
className={categoryTagClassName}
/>
<h3 className={cls(
"text-3xl md:text-4xl font-medium leading-tight line-clamp-2",
shouldUseLightText ? "text-background" : "text-foreground",
cardTitleClassName
)}>
{blog.title}
</h3>
<p className={cls(
"text-base leading-tight line-clamp-2",
shouldUseLightText ? "text-background/75" : "text-foreground/75",
cardDescriptionClassName
)}>
{blog.description}
</p>
</div>
<div className={cls("relative z-1 w-full aspect-square", mediaWrapperClassName)}>
<MediaContent
imageSrc={blog.imageSrc}
videoSrc={blog.videoSrc}
imageAlt={blog.imageAlt || blog.title}
videoAriaLabel={blog.videoAriaLabel}
imageClassName={cls("absolute inset-0 w-full h-full object-cover", mediaClassName)}
/>
<OverlayArrowButton ariaLabel={`Read ${blog.title}`} />
</div>
</article>
);
});
BlogCardItem.displayName = "BlogCardItem";
const BlogCardThree = ({
blogs,
carouselMode = "buttons",
uniformGridCustomHeightClasses = "min-h-none",
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
textboxLayout,
useInvertedBackground,
ariaLabel = "Blog section",
className = "",
containerClassName = "",
cardClassName = "",
cardContentClassName = "",
categoryTagClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: BlogCardThreeProps) => {
return (
<CardStack
mode={carouselMode}
gridVariant="uniform-all-items-equal"
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
ariaLabel={ariaLabel}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
>
{blogs.map((blog) => (
<BlogCardItem
key={blog.id}
blog={blog}
useInvertedBackground={useInvertedBackground}
cardClassName={cardClassName}
cardContentClassName={cardContentClassName}
categoryTagClassName={categoryTagClassName}
cardTitleClassName={cardTitleClassName}
cardDescriptionClassName={cardDescriptionClassName}
mediaWrapperClassName={mediaWrapperClassName}
mediaClassName={mediaClassName}
/>
))}
</CardStack>
);
};
BlogCardThree.displayName = "BlogCardThree";
export default memo(BlogCardThree);

View File

@@ -0,0 +1,225 @@
"use client";
import React, { memo } from "react";
import Image from "next/image";
import CardStack from "@/components/cardStack/CardStack";
import Badge from "@/components/shared/Badge";
import OverlayArrowButton from "@/components/shared/OverlayArrowButton";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type BlogCard = {
id: string;
tags: string[];
title: string;
excerpt: string;
imageSrc: string;
imageAlt?: string;
authorName: string;
date: string;
onBlogClick?: () => void;
};
interface BlogCardTwoProps {
blogs: BlogCard[];
carouselMode?: "auto" | "buttons";
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
imageWrapperClassName?: string;
imageClassName?: string;
authorDateClassName?: string;
cardTitleClassName?: string;
excerptClassName?: string;
tagsContainerClassName?: string;
tagClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
}
interface BlogCardItemProps {
blog: BlogCard;
shouldUseLightText: boolean;
cardClassName?: string;
imageWrapperClassName?: string;
imageClassName?: string;
authorDateClassName?: string;
cardTitleClassName?: string;
excerptClassName?: string;
tagsContainerClassName?: string;
tagClassName?: string;
}
const BlogCardItem = memo(({
blog,
shouldUseLightText,
cardClassName = "",
imageWrapperClassName = "",
imageClassName = "",
authorDateClassName = "",
cardTitleClassName = "",
excerptClassName = "",
tagsContainerClassName = "",
tagClassName = "",
}: BlogCardItemProps) => {
return (
<article
className={cls("relative h-full card group flex flex-col gap-4 cursor-pointer p-4 rounded-theme-capped", cardClassName)}
onClick={blog.onBlogClick}
role="article"
aria-label={`${blog.title} by ${blog.authorName}`}
>
<div className={cls("relative z-1 w-full aspect-[4/3] overflow-hidden rounded-theme-capped", imageWrapperClassName)}>
<Image
src={blog.imageSrc}
alt={blog.imageAlt || blog.title}
fill
className={cls("w-full h-full object-cover transition-transform duration-500 ease-in-out group-hover:scale-105", imageClassName)}
unoptimized={blog.imageSrc.startsWith('http') || blog.imageSrc.startsWith('//')}
/>
<OverlayArrowButton ariaLabel={`Read ${blog.title}`} />
</div>
<div className="relative z-1 flex flex-col justify-between gap-6 flex-1">
<div className="flex flex-col gap-2">
<p className={cls("text-xs", shouldUseLightText ? "text-background" : "text-foreground", authorDateClassName)}>
{blog.authorName} {blog.date}
</p>
<h3 className={cls("text-2xl font-medium leading-[1.25]", shouldUseLightText ? "text-background" : "text-foreground", cardTitleClassName)}>
{blog.title}
</h3>
<p className={cls("text-base leading-[1.25]", shouldUseLightText ? "text-background" : "text-foreground", excerptClassName)}>
{blog.excerpt}
</p>
</div>
<div className={cls("flex flex-wrap gap-2", tagsContainerClassName)}>
{blog.tags.map((tag, index) => (
<Badge key={`${tag}-${index}`} text={tag} variant="primary" className={tagClassName} />
))}
</div>
</div>
</article>
);
});
BlogCardItem.displayName = "BlogCardItem";
const BlogCardTwo = ({
blogs,
carouselMode = "buttons",
uniformGridCustomHeightClasses,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
textboxLayout,
useInvertedBackground,
ariaLabel = "Blog section",
className = "",
containerClassName = "",
cardClassName = "",
imageWrapperClassName = "",
imageClassName = "",
authorDateClassName = "",
cardTitleClassName = "",
excerptClassName = "",
tagsContainerClassName = "",
tagClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: BlogCardTwoProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<CardStack
mode={carouselMode}
gridVariant="uniform-all-items-equal"
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
ariaLabel={ariaLabel}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
>
{blogs.map((blog) => (
<BlogCardItem
key={blog.id}
blog={blog}
shouldUseLightText={shouldUseLightText}
cardClassName={cardClassName}
imageWrapperClassName={imageWrapperClassName}
imageClassName={imageClassName}
authorDateClassName={authorDateClassName}
cardTitleClassName={cardTitleClassName}
excerptClassName={excerptClassName}
tagsContainerClassName={tagsContainerClassName}
tagClassName={tagClassName}
/>
))}
</CardStack>
);
};
BlogCardTwo.displayName = "BlogCardTwo";
export default memo(BlogCardTwo);

View File

@@ -0,0 +1,92 @@
"use client";
import React, { memo } from "react";
import ContactForm from "@/components/form/ContactForm";
import { cls } from "@/lib/utils";
import { LucideIcon } from "lucide-react";
interface ContactCenterProps {
title: string;
description: string;
tag: string;
tagIcon?: LucideIcon;
useInvertedBackground: "noInvert" | "invertDefault" | "invertCard";
tagClassName?: string;
inputPlaceholder?: string;
buttonText?: string;
termsText?: string;
onSubmit?: (email: string) => void;
ariaLabel?: string;
className?: string;
containerClassName?: string;
contentClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
formWrapperClassName?: string;
formClassName?: string;
inputClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
termsClassName?: string;
}
const ContactCenter = ({
title,
description,
tag,
tagIcon,
useInvertedBackground,
tagClassName = "",
inputPlaceholder = "Enter your email",
buttonText = "Sign Up",
termsText = "By clicking Sign Up you're confirming that you agree with our Terms and Conditions.",
onSubmit,
ariaLabel = "Contact section",
className = "",
containerClassName = "",
contentClassName = "",
titleClassName = "",
descriptionClassName = "",
formWrapperClassName = "",
formClassName = "",
inputClassName = "",
buttonClassName = "",
buttonTextClassName = "",
termsClassName = "",
}: ContactCenterProps) => {
return (
<section aria-label={ariaLabel} className={cls("relative py-20", useInvertedBackground === "invertCard" ? "w-content-width-expanded mx-auto rounded-theme-capped bg-foreground" : "w-full", useInvertedBackground === "invertDefault" && "bg-foreground", className)}>
<div className={cls("w-content-width mx-auto", containerClassName)}>
<div className={cls("w-full card p-6 md:p-0 py-20 md:py-20 rounded-theme-capped flex items-center justify-center", contentClassName)}>
<div className="relative z-1 w-full md:w-1/2">
<ContactForm
tag={tag}
tagIcon={tagIcon}
title={title}
description={description}
useInvertedBackground={useInvertedBackground}
inputPlaceholder={inputPlaceholder}
buttonText={buttonText}
termsText={termsText}
onSubmit={onSubmit}
centered={true}
tagClassName={tagClassName}
titleClassName={titleClassName}
descriptionClassName={descriptionClassName}
formWrapperClassName={cls("md:w-8/10 2xl:w-6/10", formWrapperClassName)}
formClassName={formClassName}
inputClassName={inputClassName}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
termsClassName={termsClassName}
/>
</div>
</div>
</div>
</section>
);
};
ContactCenter.displayName = "ContactCenter";
export default memo(ContactCenter);

View File

@@ -0,0 +1,158 @@
"use client";
import React, { memo, useState } from "react";
import TextAnimation from "@/components/text/TextAnimation";
import Button from "@/components/button/Button";
import Input from "@/components/form/Input";
import Textarea from "@/components/form/Textarea";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import { getButtonProps } from "@/lib/buttonUtils";
import type { AnimationType } from "@/components/text/types";
export interface InputField {
name: string;
type: string;
placeholder: string;
required?: boolean;
className?: string;
}
export interface TextareaField {
name: string;
placeholder: string;
rows?: number;
required?: boolean;
className?: string;
}
interface ContactCenterFormProps {
title: string;
description: string;
inputs: InputField[];
textarea?: TextareaField;
useInvertedBackground: "noInvert" | "invertDefault" | "invertCard";
buttonText?: string;
onSubmit?: (data: Record<string, string>) => void;
ariaLabel?: string;
className?: string;
containerClassName?: string;
contentClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
}
const ContactCenterForm = ({
title,
description,
inputs,
textarea,
useInvertedBackground,
buttonText = "Submit",
onSubmit,
ariaLabel = "Contact section",
className = "",
containerClassName = "",
contentClassName = "",
titleClassName = "",
descriptionClassName = "",
buttonClassName = "",
buttonTextClassName = "",
}: ContactCenterFormProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
// Validate minimum inputs requirement
if (inputs.length < 2) {
throw new Error("ContactCenterForm requires at least 2 inputs");
}
// Initialize form data dynamically
const initialFormData: Record<string, string> = {};
inputs.forEach(input => {
initialFormData[input.name] = "";
});
if (textarea) {
initialFormData[textarea.name] = "";
}
const [formData, setFormData] = useState(initialFormData);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (onSubmit) {
onSubmit(formData);
}
};
return (
<section aria-label={ariaLabel} className={cls("relative py-20", useInvertedBackground === "invertCard" ? "w-content-width-expanded mx-auto rounded-theme-capped bg-foreground" : "w-full", useInvertedBackground === "invertDefault" && "bg-foreground", className)}>
<div className={cls("w-content-width mx-auto", containerClassName)}>
<div className={cls("w-full card p-6 md:p-0 py-20 md:py-20 rounded-theme-capped flex items-center justify-center", contentClassName)}>
<div className="relative z-1 w-full md:w-1/2">
<form onSubmit={handleSubmit} className="w-full flex flex-col gap-6">
<div className="w-full flex flex-col gap-0 text-center">
<TextAnimation
type={theme.defaultTextAnimation as AnimationType}
text={title}
variant="trigger"
className={cls("text-4xl font-medium leading-[1.175] text-balance", shouldUseLightText ? "text-background" : "text-foreground", titleClassName)}
/>
<TextAnimation
type={theme.defaultTextAnimation as AnimationType}
text={description}
variant="words-trigger"
className={cls("text-base leading-[1.15] text-balance", shouldUseLightText ? "text-background" : "text-foreground", descriptionClassName)}
/>
</div>
<div className="w-full flex flex-col gap-5">
{inputs.map((input) => (
<Input
key={input.name}
type={input.type}
placeholder={input.placeholder}
value={formData[input.name] || ""}
onChange={(value) => setFormData({ ...formData, [input.name]: value })}
required={input.required}
ariaLabel={input.placeholder}
className={input.className}
/>
))}
{textarea && (
<Textarea
placeholder={textarea.placeholder}
value={formData[textarea.name] || ""}
onChange={(value) => setFormData({ ...formData, [textarea.name]: value })}
required={textarea.required}
rows={textarea.rows || 5}
ariaLabel={textarea.placeholder}
className={textarea.className}
/>
)}
<Button
{...getButtonProps(
{ text: buttonText },
0,
theme.defaultButtonVariant,
cls("w-full", buttonClassName),
cls("text-base", buttonTextClassName)
)}
/>
</div>
</form>
</div>
</div>
</div>
</section>
);
};
ContactCenterForm.displayName = "ContactCenterForm";
export default memo(ContactCenterForm);

View File

@@ -0,0 +1,195 @@
"use client";
import React, { memo, useState, Fragment } from "react";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import Accordion from "@/components/Accordion";
import Button from "@/components/button/Button";
import { useCardAnimation } from "@/components/cardStack/hooks/useCardAnimation";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
import type { CardAnimationType } from "@/components/cardStack/types";
import type { ButtonConfig } from "@/types/button";
interface FaqItem {
id: string;
title: string;
content: string;
}
interface ContactFaqProps {
faqs: FaqItem[];
ctaTitle: string;
ctaDescription: string;
ctaButton: ButtonConfig;
ctaIcon: LucideIcon;
useInvertedBackground: InvertedBackground;
animationType: CardAnimationType;
accordionAnimationType?: "smooth" | "instant";
showCard?: boolean;
ariaLabel?: string;
className?: string;
containerClassName?: string;
ctaPanelClassName?: string;
ctaIconClassName?: string;
ctaTitleClassName?: string;
ctaDescriptionClassName?: string;
ctaButtonClassName?: string;
ctaButtonTextClassName?: string;
faqsPanelClassName?: string;
faqsContainerClassName?: string;
accordionClassName?: string;
accordionTitleClassName?: string;
accordionIconContainerClassName?: string;
accordionIconClassName?: string;
accordionContentClassName?: string;
separatorClassName?: string;
}
const ContactFaq = ({
faqs,
ctaTitle,
ctaDescription,
ctaButton,
ctaIcon: CtaIcon,
useInvertedBackground,
animationType,
accordionAnimationType = "smooth",
showCard = true,
ariaLabel = "Contact and FAQ section",
className = "",
containerClassName = "",
ctaPanelClassName = "",
ctaIconClassName = "",
ctaTitleClassName = "",
ctaDescriptionClassName = "",
ctaButtonClassName = "",
ctaButtonTextClassName = "",
faqsPanelClassName = "",
faqsContainerClassName = "",
accordionClassName = "",
accordionTitleClassName = "",
accordionIconContainerClassName = "",
accordionIconClassName = "",
accordionContentClassName = "",
separatorClassName = "",
}: ContactFaqProps) => {
const [activeIndex, setActiveIndex] = useState<number | null>(null);
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const { itemRefs } = useCardAnimation({ animationType, itemCount: 2 });
const handleToggle = (index: number) => {
setActiveIndex(activeIndex === index ? null : index);
};
const getButtonConfigProps = () => {
if (theme.defaultButtonVariant === "hover-bubble") {
return { bgClassName: "w-full" };
}
if (theme.defaultButtonVariant === "icon-arrow") {
return { className: "justify-between" };
}
return {};
};
return (
<section
aria-label={ariaLabel}
className={cls(
"relative py-20",
useInvertedBackground === "invertCard"
? "w-content-width-expanded mx-auto rounded-theme-capped bg-foreground"
: "w-full",
useInvertedBackground === "invertDefault" && "bg-foreground",
className
)}
>
<div className={cls("w-content-width mx-auto", containerClassName)}>
<div className="grid grid-cols-1 md:grid-cols-12 gap-6 md:gap-8">
<div
ref={(el) => { itemRefs.current[0] = el; }}
className={cls(
"md:col-span-4 card rounded-theme-capped p-6 md:p-8 flex flex-col items-center justify-center gap-6 text-center",
ctaPanelClassName
)}
>
<div className={cls("h-16 w-auto aspect-square rounded-full primary-button flex items-center justify-center", ctaIconClassName)}>
<CtaIcon className="h-4/10 w-4/10 text-background" strokeWidth={1.5} />
</div>
<div className="flex flex-col" >
<h2 className={cls(
"text-2xl md:text-3xl font-medium",
shouldUseLightText ? "text-background" : "text-foreground",
ctaTitleClassName
)}>
{ctaTitle}
</h2>
<p className={cls(
"text-base",
shouldUseLightText ? "text-background/70" : "text-foreground/70",
ctaDescriptionClassName
)}>
{ctaDescription}
</p>
</div>
<Button
{...getButtonProps(
{ ...ctaButton, props: { ...ctaButton.props, ...getButtonConfigProps() } },
0,
theme.defaultButtonVariant,
cls("w-full", ctaButtonClassName),
ctaButtonTextClassName
)}
/>
</div>
<div
ref={(el) => { itemRefs.current[1] = el; }}
className={cls(
"md:col-span-8 flex flex-col gap-4",
faqsPanelClassName
)}
>
<div className={cls("flex flex-col gap-4", faqsContainerClassName)}>
{faqs.map((faq, index) => (
<Fragment key={faq.id}>
<Accordion
index={index}
isActive={activeIndex === index}
onToggle={handleToggle}
title={faq.title}
content={faq.content}
animationType={accordionAnimationType}
showCard={showCard}
useInvertedBackground={useInvertedBackground}
className={accordionClassName}
titleClassName={accordionTitleClassName}
iconContainerClassName={accordionIconContainerClassName}
iconClassName={accordionIconClassName}
contentClassName={accordionContentClassName}
/>
{!showCard && index < faqs.length - 1 && (
<div className={cls(
"w-full border-b",
shouldUseLightText ? "border-background/10" : "border-foreground/10",
separatorClassName
)} />
)}
</Fragment>
))}
</div>
</div>
</div>
</div>
</section>
);
};
ContactFaq.displayName = "ContactFaq";
export default memo(ContactFaq);

View File

@@ -0,0 +1,97 @@
"use client";
import React, { memo } from "react";
import TextAnimation from "@/components/text/TextAnimation";
import EmailSignupForm from "@/components/form/EmailSignupForm";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
type AnimationType = "entrance-slide" | "reveal-blur" | "background-highlight";
interface ContactInlineProps {
text: string;
animationType?: AnimationType;
inputPlaceholder?: string;
buttonText?: string;
onSubmit?: (email: string) => void;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
textClassName?: string;
formClassName?: string;
inputClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
}
const ContactInline = ({
text,
animationType = "entrance-slide",
inputPlaceholder = "Enter your message",
buttonText = "Submit",
onSubmit,
useInvertedBackground,
ariaLabel = "Contact section",
className = "",
containerClassName = "",
textClassName = "",
formClassName = "",
inputClassName = "",
buttonClassName = "",
buttonTextClassName = "",
}: ContactInlineProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<section
aria-label={ariaLabel}
className={cls(
"relative py-20",
useInvertedBackground === "invertCard"
? "w-content-width-expanded mx-auto rounded-theme-capped bg-foreground"
: "w-full",
useInvertedBackground === "invertDefault" && "bg-foreground",
className
)}
>
<div className={cls("w-content-width mx-auto", containerClassName)}>
<div className="card rounded-theme-capped p-6 md:p-15">
<div className="relative z-1 w-full flex flex-col md:flex-row gap-6 md:gap-15 md:items-center">
<div className="w-full md:w-1/2">
<TextAnimation
type={animationType}
text={text}
variant="words-trigger"
as="h2"
className={cls(
"text-3xl md:text-5xl font-medium leading-[1.15]",
shouldUseLightText && "text-background",
textClassName
)}
/>
</div>
<div className="w-full md:w-1/2">
<EmailSignupForm
inputPlaceholder={inputPlaceholder}
buttonText={buttonText}
onSubmit={onSubmit}
className={formClassName}
inputClassName={inputClassName}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
/>
</div>
</div>
</div>
</div>
</section>
);
};
ContactInline.displayName = "ContactInline";
export default memo(ContactInline);

View File

@@ -0,0 +1,128 @@
"use client";
import React, { memo } from "react";
import ContactForm from "@/components/form/ContactForm";
import MediaContent from "@/components/shared/MediaContent";
import { cls } from "@/lib/utils";
import { LucideIcon } from "lucide-react";
interface ContactSplitProps {
title: string;
description: string;
tag: string;
tagIcon?: LucideIcon;
useInvertedBackground: "noInvert" | "invertDefault" | "invertCard";
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
mediaPosition?: "left" | "right";
inputPlaceholder?: string;
buttonText?: string;
termsText?: string;
onSubmit?: (email: string) => void;
ariaLabel?: string;
className?: string;
containerClassName?: string;
contentClassName?: string;
contactFormClassName?: string;
tagClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
formWrapperClassName?: string;
formClassName?: string;
inputClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
termsClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
}
const ContactSplit = ({
title,
description,
tag,
tagIcon,
useInvertedBackground,
imageSrc,
videoSrc,
imageAlt = "",
videoAriaLabel = "Contact section video",
mediaPosition = "right",
inputPlaceholder = "Enter your email",
buttonText = "Sign Up",
termsText = "By clicking Sign Up you're confirming that you agree with our Terms and Conditions.",
onSubmit,
ariaLabel = "Contact section",
className = "",
containerClassName = "",
contentClassName = "",
contactFormClassName = "",
tagClassName = "",
titleClassName = "",
descriptionClassName = "",
formWrapperClassName = "",
formClassName = "",
inputClassName = "",
buttonClassName = "",
buttonTextClassName = "",
termsClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
}: ContactSplitProps) => {
const contactContent = (
<div className="card rounded-theme-capped p-6 py-15 md:py-6 flex items-center justify-center">
<ContactForm
tag={tag}
tagIcon={tagIcon}
title={title}
description={description}
useInvertedBackground={useInvertedBackground}
inputPlaceholder={inputPlaceholder}
buttonText={buttonText}
termsText={termsText}
onSubmit={onSubmit}
centered={true}
className={cls("w-full", contactFormClassName)}
tagClassName={tagClassName}
titleClassName={titleClassName}
descriptionClassName={descriptionClassName}
formWrapperClassName={cls("w-full md:w-8/10 2xl:w-7/10", formWrapperClassName)}
formClassName={formClassName}
inputClassName={inputClassName}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
termsClassName={termsClassName}
/>
</div>
);
const mediaContent = (
<div className={cls("overflow-hidden rounded-theme-capped card h-130", mediaWrapperClassName)}>
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("relative z-1 w-full h-full object-cover", mediaClassName)}
/>
</div>
);
return (
<section aria-label={ariaLabel} className={cls("relative py-20", useInvertedBackground === "invertCard" ? "w-content-width-expanded mx-auto rounded-theme-capped bg-foreground" : "w-full", useInvertedBackground === "invertDefault" && "bg-foreground", className)}>
<div className={cls("w-content-width mx-auto", containerClassName)}>
<div className={cls("grid grid-cols-1 md:grid-cols-2 gap-6 md:auto-rows-fr", contentClassName)}>
{mediaPosition === "left" && mediaContent}
{contactContent}
{mediaPosition === "right" && mediaContent}
</div>
</div>
</section>
);
};
ContactSplit.displayName = "ContactSplit";
export default memo(ContactSplit);

View File

@@ -0,0 +1,203 @@
"use client";
import React, { memo, useState } from "react";
import TextAnimation from "@/components/text/TextAnimation";
import Button from "@/components/button/Button";
import Input from "@/components/form/Input";
import Textarea from "@/components/form/Textarea";
import MediaContent from "@/components/shared/MediaContent";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import { getButtonProps } from "@/lib/buttonUtils";
import type { AnimationType } from "@/components/text/types";
export interface InputField {
name: string;
type: string;
placeholder: string;
required?: boolean;
className?: string;
}
export interface TextareaField {
name: string;
placeholder: string;
rows?: number;
required?: boolean;
className?: string;
}
interface ContactSplitFormProps {
title: string;
description: string;
inputs: InputField[];
textarea?: TextareaField;
useInvertedBackground: "noInvert" | "invertDefault" | "invertCard";
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
mediaPosition?: "left" | "right";
buttonText?: string;
onSubmit?: (data: Record<string, string>) => void;
ariaLabel?: string;
className?: string;
containerClassName?: string;
contentClassName?: string;
formCardClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
}
const ContactSplitForm = ({
title,
description,
inputs,
textarea,
useInvertedBackground,
imageSrc,
videoSrc,
imageAlt = "",
videoAriaLabel = "Contact section video",
mediaPosition = "right",
buttonText = "Submit",
onSubmit,
ariaLabel = "Contact section",
className = "",
containerClassName = "",
contentClassName = "",
formCardClassName = "",
titleClassName = "",
descriptionClassName = "",
buttonClassName = "",
buttonTextClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
}: ContactSplitFormProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
// Validate minimum inputs requirement
if (inputs.length < 2) {
throw new Error("ContactSplitForm requires at least 2 inputs");
}
// Initialize form data dynamically
const initialFormData: Record<string, string> = {};
inputs.forEach(input => {
initialFormData[input.name] = "";
});
if (textarea) {
initialFormData[textarea.name] = "";
}
const [formData, setFormData] = useState(initialFormData);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (onSubmit) {
onSubmit(formData);
}
};
const getButtonConfigProps = () => {
if (theme.defaultButtonVariant === "hover-bubble") {
return { bgClassName: "w-full" };
}
if (theme.defaultButtonVariant === "icon-arrow") {
return { className: "justify-between" };
}
return {};
};
const formContent = (
<div className={cls("card rounded-theme-capped p-6 md:p-10 flex items-center justify-center", formCardClassName)}>
<form onSubmit={handleSubmit} className="relative z-1 w-full flex flex-col gap-6">
<div className="w-full flex flex-col gap-0 text-center">
<TextAnimation
type={theme.defaultTextAnimation as AnimationType}
text={title}
variant="trigger"
className={cls("text-4xl font-medium leading-[1.175] text-balance", shouldUseLightText ? "text-background" : "text-foreground", titleClassName)}
/>
<TextAnimation
type={theme.defaultTextAnimation as AnimationType}
text={description}
variant="words-trigger"
className={cls("text-base leading-[1.15] text-balance", shouldUseLightText ? "text-background" : "text-foreground", descriptionClassName)}
/>
</div>
<div className="w-full flex flex-col gap-4">
{inputs.map((input) => (
<Input
key={input.name}
type={input.type}
placeholder={input.placeholder}
value={formData[input.name] || ""}
onChange={(value) => setFormData({ ...formData, [input.name]: value })}
required={input.required}
ariaLabel={input.placeholder}
className={input.className}
/>
))}
{textarea && (
<Textarea
placeholder={textarea.placeholder}
value={formData[textarea.name] || ""}
onChange={(value) => setFormData({ ...formData, [textarea.name]: value })}
required={textarea.required}
rows={textarea.rows || 5}
ariaLabel={textarea.placeholder}
className={textarea.className}
/>
)}
<Button
{...getButtonProps(
{ text: buttonText, props: getButtonConfigProps() },
0,
theme.defaultButtonVariant,
cls("w-full", buttonClassName),
cls("text-base", buttonTextClassName)
)}
/>
</div>
</form>
</div>
);
const mediaContent = (
<div className={cls("overflow-hidden rounded-theme-capped card md:relative md:h-full", mediaWrapperClassName)}>
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("w-full md:absolute md:inset-0 md:h-full object-cover", mediaClassName)}
/>
</div>
);
return (
<section aria-label={ariaLabel} className={cls("relative py-20", useInvertedBackground === "invertCard" ? "w-content-width-expanded mx-auto rounded-theme-capped bg-foreground" : "w-full", useInvertedBackground === "invertDefault" && "bg-foreground", className)}>
<div className={cls("w-content-width mx-auto", containerClassName)}>
<div className={cls("grid grid-cols-1 md:grid-cols-2 gap-6 md:auto-rows-fr", contentClassName)}>
{mediaPosition === "left" && mediaContent}
{formContent}
{mediaPosition === "right" && mediaContent}
</div>
</div>
</section>
);
};
ContactSplitForm.displayName = "ContactSplitForm";
export default memo(ContactSplitForm);

View File

@@ -0,0 +1,94 @@
"use client";
import React, { memo } from "react";
import TextAnimation from "@/components/text/TextAnimation";
import Button from "@/components/button/Button";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { ButtonConfig } from "@/types/button";
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
type AnimationType = "entrance-slide" | "reveal-blur" | "background-highlight";
interface ContactTextProps {
text: string;
animationType?: AnimationType;
buttons?: ButtonConfig[];
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
textClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
}
const ContactText = ({
text,
animationType = "entrance-slide",
buttons,
useInvertedBackground,
ariaLabel = "Contact section",
className = "",
containerClassName = "",
textClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
}: ContactTextProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<section
aria-label={ariaLabel}
className={cls(
"relative py-20",
useInvertedBackground === "invertCard"
? "w-content-width-expanded mx-auto rounded-theme-capped bg-foreground"
: "w-full",
useInvertedBackground === "invertDefault" && "bg-foreground",
className
)}
>
<div className={cls("relative w-content-width mx-auto card rounded-theme-capped py-20 px-10", containerClassName)}>
<div className="relative z-1 w-full md:w-3/4 mx-auto flex flex-col items-center justify-center gap-8" >
<TextAnimation
type={animationType}
text={text}
variant="words-trigger"
as="h2"
className={cls(
"text-4xl md:text-5xl font-medium text-center leading-[1.15]",
shouldUseLightText && "text-background",
textClassName
)}
/>
{buttons && buttons.length > 0 && (
<div className={cls("flex gap-4", buttonContainerClassName)}>
{buttons.slice(0, 2).map((button, index) => (
<Button
key={index}
{...getButtonProps(
button,
index,
theme.defaultButtonVariant,
cls("px-8", buttonClassName),
cls("text-base", buttonTextClassName)
)}
/>
))}
</div>
)}
</div>
</div>
</section>
);
};
ContactText.displayName = "ContactText";
export default memo(ContactText);

View File

@@ -0,0 +1,144 @@
"use client";
import { memo, useState, Fragment } from "react";
import CardStackTextBox from "@/components/cardStack/CardStackTextBox";
import Accordion from "@/components/Accordion";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
interface FaqItem {
id: string;
title: string;
content: string;
}
interface FaqBaseProps {
faqs: FaqItem[];
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
animationType?: "smooth" | "instant";
showCard?: boolean;
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxTitleClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
textBoxDescriptionClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
faqsContainerClassName?: string;
accordionClassName?: string;
accordionTitleClassName?: string;
accordionIconContainerClassName?: string;
accordionIconClassName?: string;
accordionContentClassName?: string;
separatorClassName?: string;
}
const FaqBase = ({
faqs,
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
textboxLayout,
useInvertedBackground,
animationType = "smooth",
showCard = true,
ariaLabel = "FAQ section",
className = "",
containerClassName = "",
textBoxTitleClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
textBoxDescriptionClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
faqsContainerClassName = "",
accordionClassName = "",
accordionTitleClassName = "",
accordionIconContainerClassName = "",
accordionIconClassName = "",
accordionContentClassName = "",
separatorClassName = "",
}: FaqBaseProps) => {
const [activeIndex, setActiveIndex] = useState<number | null>(null);
const handleToggle = (index: number) => {
setActiveIndex(activeIndex === index ? null : index);
};
return (
<section aria-label={ariaLabel} className={cls("relative py-20", useInvertedBackground === "invertCard" ? "w-content-width-expanded mx-auto rounded-theme-capped bg-foreground" : "w-full", useInvertedBackground === "invertDefault" && "bg-foreground", className)}>
<div className={cls("w-content-width mx-auto flex flex-col gap-8", containerClassName)}>
{(title || description) && (
<CardStackTextBox
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
/>
)}
<div className={cls("flex flex-col gap-4", faqsContainerClassName)}>
{faqs.map((faq, index) => (
<Fragment key={faq.id}>
<Accordion
index={index}
isActive={activeIndex === index}
onToggle={handleToggle}
title={faq.title}
content={faq.content}
animationType={animationType}
showCard={showCard}
useInvertedBackground={useInvertedBackground}
className={accordionClassName}
titleClassName={accordionTitleClassName}
iconContainerClassName={accordionIconContainerClassName}
iconClassName={accordionIconClassName}
contentClassName={accordionContentClassName}
/>
{!showCard && index < faqs.length - 1 && (
<div className={cls("w-full border-b border-foreground/10", separatorClassName)} />
)}
</Fragment>
))}
</div>
</div>
</section>
);
};
FaqBase.displayName = "FaqBase";
export default memo(FaqBase);

View File

@@ -0,0 +1,169 @@
"use client";
import React, { memo, useState, useCallback } from "react";
import CardStackTextBox from "@/components/cardStack/CardStackTextBox";
import Accordion from "@/components/Accordion";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
interface FaqItem {
id: string;
title: string;
content: string;
}
interface FaqDoubleProps {
faqs: FaqItem[];
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
animationType?: "smooth" | "instant";
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxTitleClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
textBoxDescriptionClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
faqsContainerClassName?: string;
columnClassName?: string;
accordionClassName?: string;
accordionTitleClassName?: string;
accordionIconContainerClassName?: string;
accordionIconClassName?: string;
accordionContentClassName?: string;
}
const FaqDouble = ({
faqs,
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
textboxLayout,
useInvertedBackground,
animationType = "smooth",
ariaLabel = "FAQ section",
className = "",
containerClassName = "",
textBoxTitleClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
textBoxDescriptionClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
faqsContainerClassName = "",
columnClassName = "",
accordionClassName = "",
accordionTitleClassName = "",
accordionIconContainerClassName = "",
accordionIconClassName = "",
accordionContentClassName = "",
}: FaqDoubleProps) => {
const [activeIndex, setActiveIndex] = useState<number | null>(null);
const handleToggle = useCallback((index: number) => {
setActiveIndex((prevActiveIndex) =>
prevActiveIndex === index ? null : index
);
}, []);
const halfLength = Math.ceil(faqs.length / 2);
const firstHalf = faqs.slice(0, halfLength);
const secondHalf = faqs.slice(halfLength);
return (
<section aria-label={ariaLabel} className={cls("relative py-20", useInvertedBackground === "invertCard" ? "w-content-width-expanded mx-auto rounded-theme-capped bg-foreground" : "w-full", useInvertedBackground === "invertDefault" && "bg-foreground", className)}>
<div className={cls("w-content-width mx-auto flex flex-col gap-8", containerClassName)}>
{(title || description) && (
<CardStackTextBox
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
/>
)}
<div className={cls("card p-4 rounded-theme-capped flex flex-col md:flex-row gap-4", faqsContainerClassName)}>
<div className={cls("relative z-1 flex-1 flex flex-col gap-4", columnClassName)}>
{firstHalf.map((faq, index) => (
<Accordion
key={faq.id}
index={index}
isActive={activeIndex === index}
onToggle={handleToggle}
title={faq.title}
content={faq.content}
animationType={animationType}
useInvertedBackground={useInvertedBackground}
className={accordionClassName}
titleClassName={accordionTitleClassName}
iconContainerClassName={accordionIconContainerClassName}
iconClassName={accordionIconClassName}
contentClassName={accordionContentClassName}
/>
))}
</div>
{secondHalf.length > 0 && (
<div className={cls("relative z-1 flex-1 flex flex-col gap-4", columnClassName)}>
{secondHalf.map((faq, index) => {
const actualIndex = index + halfLength;
return (
<Accordion
key={faq.id}
index={actualIndex}
isActive={activeIndex === actualIndex}
onToggle={handleToggle}
title={faq.title}
content={faq.content}
animationType={animationType}
useInvertedBackground={useInvertedBackground}
className={accordionClassName}
titleClassName={accordionTitleClassName}
iconContainerClassName={accordionIconContainerClassName}
iconClassName={accordionIconClassName}
contentClassName={accordionContentClassName}
/>
);
})}
</div>
)}
</div>
</div>
</section>
);
};
FaqDouble.displayName = "FaqDouble";
export default memo(FaqDouble);

View File

@@ -0,0 +1,187 @@
"use client";
import { memo, useState, useCallback, Fragment } from "react";
import CardStackTextBox from "@/components/cardStack/CardStackTextBox";
import Accordion from "@/components/Accordion";
import MediaContent from "@/components/shared/MediaContent";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
interface FaqItem {
id: string;
title: string;
content: string;
}
interface FaqSplitMediaProps {
faqs: FaqItem[];
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
mediaPosition?: "left" | "right";
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
animationType?: "smooth" | "instant";
showCard?: boolean;
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxTitleClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
textBoxDescriptionClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
contentClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
faqsContainerClassName?: string;
accordionClassName?: string;
accordionTitleClassName?: string;
accordionIconContainerClassName?: string;
accordionIconClassName?: string;
accordionContentClassName?: string;
separatorClassName?: string;
}
const FaqSplitMedia = ({
faqs,
imageSrc,
videoSrc,
imageAlt = "",
videoAriaLabel = "FAQ section video",
mediaPosition = "left",
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
textboxLayout,
useInvertedBackground,
animationType = "smooth",
showCard = true,
ariaLabel = "FAQ section",
className = "",
containerClassName = "",
textBoxTitleClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
textBoxDescriptionClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
contentClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
faqsContainerClassName = "",
accordionClassName = "",
accordionTitleClassName = "",
accordionIconContainerClassName = "",
accordionIconClassName = "",
accordionContentClassName = "",
separatorClassName = "",
}: FaqSplitMediaProps) => {
const [activeIndex, setActiveIndex] = useState<number | null>(null);
const handleToggle = useCallback((index: number) => {
setActiveIndex((prevActiveIndex) =>
prevActiveIndex === index ? null : index
);
}, []);
return (
<section aria-label={ariaLabel} className={cls("relative py-20", useInvertedBackground === "invertCard" ? "w-content-width-expanded mx-auto rounded-theme-capped bg-foreground" : "w-full", useInvertedBackground === "invertDefault" && "bg-foreground", className)}>
<div className={cls("w-content-width mx-auto flex flex-col gap-8", containerClassName)}>
{(title || description) && (
<CardStackTextBox
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
/>
)}
<div className={cls("grid grid-cols-1 md:grid-cols-5 gap-4 md:auto-rows-fr", contentClassName)}>
{mediaPosition === "left" && (
<div className={cls("overflow-hidden rounded-theme-capped card relative h-80 md:h-auto col-span-1 md:col-span-2", mediaWrapperClassName)}>
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("absolute z-1 inset-0 w-full h-full object-cover", mediaClassName)}
/>
</div>
)}
<div className={cls("relative z-1 col-span-1 md:col-span-3 flex flex-col gap-4", faqsContainerClassName)}>
{faqs.map((faq, index) => (
<Fragment key={faq.id}>
<Accordion
index={index}
isActive={activeIndex === index}
onToggle={handleToggle}
title={faq.title}
content={faq.content}
animationType={animationType}
showCard={showCard}
useInvertedBackground={useInvertedBackground}
className={accordionClassName}
titleClassName={accordionTitleClassName}
iconContainerClassName={accordionIconContainerClassName}
iconClassName={accordionIconClassName}
contentClassName={accordionContentClassName}
/>
{!showCard && index < faqs.length - 1 && (
<div className={cls("w-full border-b border-foreground/10", separatorClassName)} />
)}
</Fragment>
))}
</div>
{mediaPosition === "right" && (
<div className={cls("overflow-hidden rounded-theme card relative h-80 md:h-auto col-span-1 md:col-span-2", mediaWrapperClassName)}>
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("absolute z-1 inset-0 w-full h-full object-cover", mediaClassName)}
/>
</div>
)}
</div>
</div>
</section>
);
};
FaqSplitMedia.displayName = "FaqSplitMedia";
export default memo(FaqSplitMedia);

View File

@@ -0,0 +1,152 @@
"use client";
import { memo, useState, useCallback, Fragment } from "react";
import TextAnimation from "@/components/text/TextAnimation";
import Accordion from "@/components/Accordion";
import Button from "@/components/button/Button";
import { cls } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { AnimationType } from "@/components/text/types";
import type { ButtonConfig } from "@/components/cardStack/types";
interface FaqItem {
id: string;
title: string;
content: string;
}
interface FaqSplitTextProps {
faqs: FaqItem[];
sideTitle: string;
sideDescription?: string;
buttons?: ButtonConfig[];
textPosition?: "left" | "right";
useInvertedBackground: "noInvert" | "invertDefault" | "invertCard";
animationType?: "smooth" | "instant";
showCard?: boolean;
ariaLabel?: string;
className?: string;
containerClassName?: string;
contentClassName?: string;
textContainerClassName?: string;
sideTitleClassName?: string;
sideDescriptionClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
faqsContainerClassName?: string;
accordionClassName?: string;
accordionTitleClassName?: string;
accordionIconContainerClassName?: string;
accordionIconClassName?: string;
accordionContentClassName?: string;
separatorClassName?: string;
}
const FaqSplitText = ({
faqs,
sideTitle,
sideDescription,
buttons,
textPosition = "left",
useInvertedBackground,
animationType = "smooth",
showCard = true,
ariaLabel = "FAQ section",
className = "",
containerClassName = "",
contentClassName = "",
textContainerClassName = "",
sideTitleClassName = "",
sideDescriptionClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
faqsContainerClassName = "",
accordionClassName = "",
accordionTitleClassName = "",
accordionIconContainerClassName = "",
accordionIconClassName = "",
accordionContentClassName = "",
separatorClassName = "",
}: FaqSplitTextProps) => {
const [activeIndex, setActiveIndex] = useState<number | null>(null);
const theme = useTheme();
const shouldUseLightText = useInvertedBackground === "invertDefault" || useInvertedBackground === "invertCard";
const handleToggle = useCallback((index: number) => {
setActiveIndex((prevActiveIndex) =>
prevActiveIndex === index ? null : index
);
}, []);
const textContent = (
<div className={cls("w-full md:w-2/5 flex flex-col gap-3", textContainerClassName)}>
<TextAnimation
type={theme.defaultTextAnimation as AnimationType}
text={sideTitle}
variant="trigger"
className={cls("text-6xl font-medium", shouldUseLightText ? "text-background" : "text-foreground", sideTitleClassName)}
/>
{sideDescription && (
<TextAnimation
type={theme.defaultTextAnimation as AnimationType}
text={sideDescription}
variant="words-trigger"
className={cls("text-lg leading-[1.2]", shouldUseLightText ? "text-background" : "text-foreground", sideDescriptionClassName)}
/>
)}
{buttons && buttons.length > 0 && (
<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>
);
const faqsContent = (
<div className={cls("w-full md:w-3/5 flex flex-col gap-4", faqsContainerClassName)}>
{faqs.map((faq, index) => (
<Fragment key={faq.id}>
<Accordion
index={index}
isActive={activeIndex === index}
onToggle={handleToggle}
title={faq.title}
content={faq.content}
animationType={animationType}
showCard={showCard}
useInvertedBackground={useInvertedBackground}
className={accordionClassName}
titleClassName={accordionTitleClassName}
iconContainerClassName={accordionIconContainerClassName}
iconClassName={accordionIconClassName}
contentClassName={accordionContentClassName}
/>
{!showCard && index < faqs.length - 1 && (
<div className={cls("w-full border-b border-foreground/10", separatorClassName)} />
)}
</Fragment>
))}
</div>
);
return (
<section aria-label={ariaLabel} className={cls("relative py-20", useInvertedBackground === "invertCard" ? "w-content-width-expanded mx-auto rounded-theme-capped bg-foreground" : "w-full", useInvertedBackground === "invertDefault" && "bg-foreground", className)}>
<div className={cls("w-content-width mx-auto", containerClassName)}>
<div className={cls("flex flex-col md:flex-row gap-6 md:gap-10", contentClassName)}>
{textPosition === "left" && textContent}
{faqsContent}
{textPosition === "right" && textContent}
</div>
</div>
</section>
);
};
FaqSplitText.displayName = "FaqSplitText";
export default memo(FaqSplitText);

View File

@@ -0,0 +1,152 @@
"use client";
import React, { memo } from "react";
import TimelineHorizontalCardStack from "@/components/cardStack/layouts/timelines/TimelineHorizontalCardStack";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type FeatureCard = {
id: number;
title: string;
description: string;
imageAlt?: string;
videoAriaLabel?: string;
} & (
| { imageSrc: string; videoSrc?: never }
| { videoSrc: string; imageSrc?: never }
);
interface FeatureCardEightProps {
features: FeatureCard[];
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxTitleClassName?: string;
textBoxDescriptionClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
cardClassName?: string;
progressBarClassName?: string;
cardContentClassName?: string;
stepNumberClassName?: string;
cardTitleClassName?: string;
cardDescriptionClassName?: string;
mediaContainerClassName?: string;
mediaClassName?: string;
}
const FeatureCardEight = ({
features,
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
textboxLayout,
useInvertedBackground,
ariaLabel = "Feature section",
className = "",
containerClassName = "",
textBoxTitleClassName = "",
textBoxDescriptionClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
cardClassName = "",
progressBarClassName = "",
cardContentClassName = "",
stepNumberClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
mediaContainerClassName = "",
mediaClassName = "",
}: FeatureCardEightProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const mediaItems = features.map((feature) => ({
imageSrc: feature.imageSrc,
videoSrc: feature.videoSrc,
imageAlt: feature.imageAlt || feature.title,
videoAriaLabel: feature.videoAriaLabel || feature.title,
}));
return (
<TimelineHorizontalCardStack
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
mediaItems={mediaItems}
className={className}
containerClassName={containerClassName}
titleClassName={textBoxTitleClassName}
descriptionClassName={textBoxDescriptionClassName}
textBoxClassName={textBoxClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
cardClassName={cardClassName}
progressBarClassName={progressBarClassName}
mediaContainerClassName={mediaContainerClassName}
mediaClassName={mediaClassName}
ariaLabel={ariaLabel}
>
{features.map((feature) => (
<div
key={feature.id}
className={cls("relative z-1 w-full min-h-0 h-fit flex flex-col gap-3", cardContentClassName)}
>
<div
className={cls(
"h-8 w-[var(--height-8)] primary-button text-background rounded-full flex items-center justify-center",
stepNumberClassName
)}
>
<p className="text-sm truncate">
{feature.id}
</p>
</div>
<h2 className={cls("mt-1 text-3xl font-medium leading-[1.15] text-balance", shouldUseLightText && "text-background", cardTitleClassName)}>
{feature.title}
</h2>
<p className={cls("text-base leading-[1.15] text-balance", shouldUseLightText ? "text-background" : "text-foreground", cardDescriptionClassName)}>
{feature.description}
</p>
</div>
))}
</TimelineHorizontalCardStack>
);
};
FeatureCardEight.displayName = "FeatureCardEight";
export default memo(FeatureCardEight);

View File

@@ -0,0 +1,187 @@
"use client";
import React, { memo } from "react";
import CardStackTextBox from "@/components/cardStack/CardStackTextBox";
import PricingFeatureList from "@/components/shared/PricingFeatureList";
import MediaContent from "@/components/shared/MediaContent";
import { useCardAnimation } from "@/components/cardStack/hooks/useCardAnimation";
import { Check, X } from "lucide-react";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type ComparisonItem = {
title: string;
items: string[];
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
};
interface FeatureCardEighteenProps {
negativeCard: ComparisonItem;
positiveCard: ComparisonItem;
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxTitleClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
textBoxDescriptionClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
gridClassName?: string;
cardClassName?: string;
cardMediaWrapperClassName?: string;
cardMediaClassName?: string;
cardTitleClassName?: string;
itemsListClassName?: string;
itemClassName?: string;
itemIconClassName?: string;
itemTextClassName?: string;
}
const FeatureCardEighteen = ({
negativeCard,
positiveCard,
animationType,
title,
titleSegments,
description,
textboxLayout,
useInvertedBackground,
tag,
tagIcon,
buttons,
ariaLabel = "Feature comparison section",
className = "",
containerClassName = "",
textBoxTitleClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
textBoxDescriptionClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
gridClassName = "",
cardClassName = "",
cardMediaWrapperClassName = "",
cardMediaClassName = "",
cardTitleClassName = "",
itemsListClassName = "",
itemClassName = "",
itemIconClassName = "",
itemTextClassName = "",
}: FeatureCardEighteenProps) => {
const theme = useTheme();
const shouldCardUseLightText = shouldUseInvertedText("invertDefault", theme.cardStyle) || shouldUseInvertedText("invertCard", theme.cardStyle);
const { itemRefs } = useCardAnimation({ animationType, itemCount: 2 });
const cards = [
{ ...negativeCard, variant: "negative" as const },
{ ...positiveCard, variant: "positive" as const },
];
return (
<section
aria-label={ariaLabel}
className={cls(
"relative py-20",
useInvertedBackground === "invertCard"
? "w-content-width-expanded mx-auto rounded-theme-capped bg-foreground"
: "w-full",
useInvertedBackground === "invertDefault" && "bg-foreground",
className
)}
>
<div className={cls("w-content-width mx-auto flex flex-col gap-8", containerClassName)}>
<CardStackTextBox
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
/>
<div className={cls(
"relative mx-auto w-full md:w-60 grid grid-cols-1 gap-6",
cards.length >= 2 ? "md:grid-cols-2" : "md:grid-cols-1",
gridClassName
)}>
{cards.map((card, index) => (
<div
key={card.variant}
ref={(el) => { itemRefs.current[index] = el; }}
className={cls(
"relative h-full card rounded-theme-capped p-6",
cardClassName
)}
>
<div className={cls("flex flex-col gap-6", card.variant === "negative" && "opacity-50")}>
<div className={cls("relative w-full aspect-[4/3] rounded-theme-capped overflow-hidden", cardMediaWrapperClassName)}>
<MediaContent
imageSrc={card.imageSrc}
videoSrc={card.videoSrc}
imageAlt={card.imageAlt || card.title}
videoAriaLabel={card.videoAriaLabel || card.title}
imageClassName={cls("w-full h-full object-cover", cardMediaClassName)}
/>
</div>
<h3 className={cls(
"text-3xl font-medium",
shouldCardUseLightText ? "text-background" : "text-foreground",
cardTitleClassName
)}>
{card.title}
</h3>
<PricingFeatureList
features={card.items}
icon={card.variant === "positive" ? Check : X}
shouldUseLightText={shouldCardUseLightText}
className={itemsListClassName}
featureItemClassName={itemClassName}
featureIconWrapperClassName=""
featureIconClassName={itemIconClassName}
featureTextClassName={cls("truncate", itemTextClassName)}
/>
</div>
</div>
))}
</div>
</div>
</section>
);
};
FeatureCardEighteen.displayName = "FeatureCardEighteen";
export default memo(FeatureCardEighteen);

View File

@@ -0,0 +1,166 @@
"use client";
import React, { memo } from "react";
import CardList from "@/components/cardStack/CardList";
import MediaContent from "@/components/shared/MediaContent";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type FeatureCard = {
id: number;
title: string;
description: string;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
};
interface FeatureCardElevenProps {
features: FeatureCard[];
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
usePrimaryButtonImage: boolean;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxTitleClassName?: string;
textBoxDescriptionClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
cardContentClassName?: string;
textCardClassName?: string;
imageCardClassName?: string;
stepNumberClassName?: string;
cardTitleClassName?: string;
cardDescriptionClassName?: string;
imageClassName?: string;
}
const FeatureCardEleven = ({
features,
animationType,
title,
titleSegments,
description,
textboxLayout,
useInvertedBackground,
usePrimaryButtonImage,
tag,
tagIcon,
buttons,
ariaLabel = "Feature section",
className = "",
containerClassName = "",
textBoxTitleClassName = "",
textBoxDescriptionClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
cardContentClassName = "",
textCardClassName = "",
imageCardClassName = "",
stepNumberClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
imageClassName = "",
}: FeatureCardElevenProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<CardList
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
animationType={animationType}
useInvertedBackground={useInvertedBackground}
disableCardWrapper={true}
className={className}
containerClassName={containerClassName}
cardClassName=""
titleClassName={textBoxTitleClassName}
descriptionClassName={textBoxDescriptionClassName}
textBoxClassName={textBoxClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
ariaLabel={ariaLabel}
>
{features.map((feature, index) => (
<div
key={feature.id}
className={cls("w-full min-h-0 h-full flex flex-col justify-between items-center gap-6", index % 2 === 0 ? "md:flex-row" : "md:flex-row-reverse", cardContentClassName)}
>
<div className={cls("w-full md:w-1/2 min-w-0 h-fit md:h-full md:aspect-square card rounded-theme-capped p-6 md:p-15 flex flex-col justify-center", textCardClassName)}>
<div className="relative z-1 w-full min-w-0 flex flex-col gap-3 md:gap-5">
<div
className={cls(
"h-8 w-[var(--height-8)] primary-button text-background rounded-full flex items-center justify-center",
stepNumberClassName
)}
>
<p className="text-sm truncate">
{feature.id}
</p>
</div>
<h2 className={cls("mt-1 text-4xl md:text-5xl font-medium leading-[1.15] text-balance", shouldUseLightText && "text-background", cardTitleClassName)}>
{feature.title}
</h2>
<p className={cls("text-base leading-[1.15] text-balance", shouldUseLightText ? "text-background" : "text-foreground", cardDescriptionClassName)}>
{feature.description}
</p>
</div>
</div>
<div
className={cls(
"relative w-full md:w-1/2 aspect-square overflow-hidden rounded-theme-capped",
usePrimaryButtonImage ? "primary-button p-10 md:p-20" : "card",
imageCardClassName
)}
>
<div className="absolute inset-10 md:inset-20 z-1" >
<MediaContent
imageSrc={feature.imageSrc}
videoSrc={feature.videoSrc}
imageAlt={feature.imageAlt || feature.title}
videoAriaLabel={feature.videoAriaLabel || feature.title}
imageClassName={cls("relative z-1 w-full h-full object-cover", imageClassName)}
/>
</div>
</div>
</div>
))}
</CardList>
);
};
FeatureCardEleven.displayName = "FeatureCardEleven";
export default memo(FeatureCardEleven);

View File

@@ -0,0 +1,179 @@
"use client";
import React, { memo } from "react";
import AutoCarousel from "@/components/cardStack/layouts/carousels/AutoCarousel";
import MediaContent from "@/components/shared/MediaContent";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { CardAnimationType, ButtonConfig, TitleSegment, TextboxLayout, InvertedBackground } from "@/components/cardStack/types";
type Feature = {
id: string;
title: string;
description: string;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
};
interface FeatureCardFifteenProps {
features: Feature[];
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
speed?: number;
topMarqueeDirection?: "left" | "right";
ariaLabel?: string;
className?: string;
containerClassName?: string;
carouselClassName?: string;
bottomCarouselClassName?: string;
cardClassName?: string;
mediaClassName?: string;
featureTitleClassName?: string;
featureDescriptionClassName?: string;
textBoxClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
}
interface FeatureCardProps {
feature: Feature;
useInvertedBackground: "noInvert" | "invertDefault" | "invertCard";
cardClassName?: string;
mediaClassName?: string;
featureTitleClassName?: string;
featureDescriptionClassName?: string;
}
const FeatureCard = memo(({
feature,
useInvertedBackground,
cardClassName = "",
mediaClassName = "",
featureTitleClassName = "",
featureDescriptionClassName = "",
}: FeatureCardProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<div className={cls("relative h-full min-h-0 flex flex-col gap-4", cardClassName)}>
<MediaContent
imageSrc={feature.imageSrc}
videoSrc={feature.videoSrc}
imageAlt={feature.imageAlt || feature.title}
videoAriaLabel={feature.videoAriaLabel || feature.title}
imageClassName={cls("w-full aspect-square object-cover rounded-theme", mediaClassName)}
/>
<div className="flex flex-col gap-1">
<h3 className={cls("relative z-1 text-lg font-medium leading-tight", shouldUseLightText ? "text-background" : "text-foreground", featureTitleClassName)}>
{feature.title}
</h3>
<p className={cls("relative z-1 text-base leading-tight truncate", shouldUseLightText ? "text-background/75" : "text-foreground/75", featureDescriptionClassName)}>
{feature.description}
</p>
</div>
</div>
);
});
FeatureCard.displayName = "FeatureCard";
const FeatureCardFifteen = ({
features,
animationType,
title,
titleSegments,
description,
textboxLayout,
useInvertedBackground,
tag,
tagIcon,
buttons,
speed = 40,
topMarqueeDirection = "left",
ariaLabel = "Features section",
className = "",
containerClassName = "",
carouselClassName = "",
bottomCarouselClassName = "",
cardClassName = "",
mediaClassName = "",
featureTitleClassName = "",
featureDescriptionClassName = "",
textBoxClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: FeatureCardFifteenProps) => {
return (
<AutoCarousel
speed={speed}
uniformGridCustomHeightClasses="min-h-none"
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
showTextBox={true}
dualMarquee={true}
topMarqueeDirection={topMarqueeDirection}
carouselClassName={carouselClassName}
bottomCarouselClassName={bottomCarouselClassName}
marqueeGapClassName="gap-8"
containerClassName={containerClassName}
className={className}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
itemClassName="w-60! md:w-carousel-item-3! xl:w-carousel-item-4!"
>
{features.map((feature, index) => (
<FeatureCard
key={`${feature.id}-${index}`}
feature={feature}
useInvertedBackground={useInvertedBackground}
cardClassName={cardClassName}
mediaClassName={mediaClassName}
featureTitleClassName={featureTitleClassName}
featureDescriptionClassName={featureDescriptionClassName}
/>
))}
</AutoCarousel>
);
};
FeatureCardFifteen.displayName = "FeatureCardFifteen";
export default memo(FeatureCardFifteen);

View File

@@ -0,0 +1,172 @@
"use client";
import { memo } from "react";
import FullWidthCarousel from "@/components/cardStack/layouts/carousels/FullWidthCarousel";
import MediaContent from "@/components/shared/MediaContent";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, TitleSegment, TextboxLayout, InvertedBackground } from "@/components/cardStack/types";
type Feature = {
id: string;
title: string;
description: string;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
};
interface FeatureCardFourteenProps {
features: Feature[];
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
cardImageClassName?: string;
cardTitleClassName?: string;
cardDescriptionClassName?: string;
cardContentClassName?: string;
carouselClassName?: string;
dotsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
}
interface FeatureCardProps {
feature: Feature;
shouldUseLightText: boolean;
cardClassName?: string;
imageClassName?: string;
cardTitleClassName?: string;
cardDescriptionClassName?: string;
cardContentClassName?: string;
}
const FeatureCard = memo(({
feature,
shouldUseLightText,
cardClassName = "",
imageClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
cardContentClassName = "",
}: FeatureCardProps) => {
return (
<div className={cls("relative h-full w-full rounded-theme-capped overflow-hidden", cardClassName)}>
<MediaContent
imageSrc={feature.imageSrc}
videoSrc={feature.videoSrc}
imageAlt={feature.imageAlt}
videoAriaLabel={feature.videoAriaLabel}
imageClassName={cls("relative w-full h-full aspect-[10/16] md:aspect-video object-cover !rounded-none", imageClassName)}
/>
<div className={cls("absolute! md:max-w-[var(--width-30)] card backdrop-blur-xs rounded-theme-capped bottom-8 left-8 right-8 md:right-auto p-6", cardContentClassName)}>
<div className="w-full min-w-0 flex flex-col gap-1">
<h2 className={cls("text-3xl md:text-4xl font-medium leading-[1.15] text-balance truncate", shouldUseLightText && "text-background", cardTitleClassName)}>
{feature.title}
</h2>
<p className={cls("text-base leading-[1.15] text-balance truncate", shouldUseLightText ? "text-background" : "text-foreground", cardDescriptionClassName)}>
{feature.description}
</p>
</div>
</div>
</div>
);
});
FeatureCard.displayName = "FeatureCard";
const FeatureCardFourteen = ({
features,
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
textboxLayout,
useInvertedBackground,
ariaLabel = "Features section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
cardImageClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
cardContentClassName = "",
carouselClassName = "",
dotsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: FeatureCardFourteenProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<FullWidthCarousel
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
carouselClassName={carouselClassName}
dotsClassName={dotsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{features.map((feature, index) => (
<FeatureCard
key={`${feature.id}-${index}`}
feature={feature}
shouldUseLightText={shouldUseLightText}
cardClassName={cardClassName}
imageClassName={cardImageClassName}
cardTitleClassName={cardTitleClassName}
cardDescriptionClassName={cardDescriptionClassName}
cardContentClassName={cardContentClassName}
/>
))}
</FullWidthCarousel>
);
};
FeatureCardFourteen.displayName = "FeatureCardFourteen";
export default memo(FeatureCardFourteen);

View File

@@ -0,0 +1,200 @@
"use client";
import React, { memo, useMemo } from "react";
import TimelinePhoneView from "@/components/cardStack/layouts/timelines/TimelinePhoneView";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, TitleSegment } from "@/components/cardStack/types";
import type { TimelinePhoneViewItem } from "@/components/cardStack/hooks/usePhoneAnimations";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type FeaturePhone = {
imageAlt?: string;
videoAriaLabel?: string;
} & (
| { imageSrc: string; videoSrc?: never }
| { videoSrc: string; imageSrc?: never }
);
type FeatureCard = {
id: number;
title: string;
description: string;
phoneOne: FeaturePhone;
phoneTwo: FeaturePhone;
};
interface FeatureCardNineProps {
features: FeatureCard[];
showStepNumbers: boolean;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxTitleClassName?: string;
textBoxDescriptionClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
desktopContainerClassName?: string;
mobileContainerClassName?: string;
desktopContentClassName?: string;
desktopWrapperClassName?: string;
mobileWrapperClassName?: string;
phoneFrameClassName?: string;
mobilePhoneFrameClassName?: string;
featureContentClassName?: string;
stepNumberClassName?: string;
featureTitleClassName?: string;
featureDescriptionClassName?: string;
}
interface FeatureContentProps {
feature: FeatureCard;
showStepNumbers: boolean;
useInvertedBackground: InvertedBackground;
featureContentClassName: string;
stepNumberClassName: string;
featureTitleClassName: string;
featureDescriptionClassName: string;
}
const FeatureContent = ({
feature,
showStepNumbers,
useInvertedBackground,
featureContentClassName,
stepNumberClassName,
featureTitleClassName,
featureDescriptionClassName,
}: FeatureContentProps) => (
<div className={cls("relative z-1 h-full w-content-width mx-auto md:w-full flex flex-col items-center text-center gap-3 md:px-5", featureContentClassName)}>
{showStepNumbers && (
<div
className={cls(
"h-8 w-[var(--height-8)] primary-button text-background rounded-full flex items-center justify-center",
stepNumberClassName
)}
>
<p className="text-sm truncate">
{feature.id}
</p>
</div>
)}
<h2 className={cls("text-5xl font-medium leading-[1.15] text-balance", (useInvertedBackground === "invertDefault" || useInvertedBackground === "invertCard") && "text-background", featureTitleClassName)}>
{feature.title}
</h2>
<p className={cls("text-base leading-[1.2] text-balance", (useInvertedBackground === "invertDefault" || useInvertedBackground === "invertCard") ? "text-background/75" : "text-foreground/75", featureDescriptionClassName)}>
{feature.description}
</p>
</div>
);
const FeatureCardNine = ({
features,
showStepNumbers,
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
textboxLayout,
useInvertedBackground,
ariaLabel = "Feature section",
className = "",
containerClassName = "",
textBoxTitleClassName = "",
textBoxDescriptionClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
desktopContainerClassName = "",
mobileContainerClassName = "",
desktopContentClassName = "",
desktopWrapperClassName = "",
mobileWrapperClassName = "",
phoneFrameClassName = "",
mobilePhoneFrameClassName = "",
featureContentClassName = "",
stepNumberClassName = "",
featureTitleClassName = "",
featureDescriptionClassName = "",
}: FeatureCardNineProps) => {
const items: TimelinePhoneViewItem[] = useMemo(() => features.map((feature, index) => ({
trigger: `trigger-${index}`,
content: (
<FeatureContent
feature={feature}
showStepNumbers={showStepNumbers}
useInvertedBackground={useInvertedBackground}
featureContentClassName={featureContentClassName}
stepNumberClassName={stepNumberClassName}
featureTitleClassName={featureTitleClassName}
featureDescriptionClassName={featureDescriptionClassName}
/>
),
imageOne: feature.phoneOne.imageSrc,
videoOne: feature.phoneOne.videoSrc,
imageAltOne: feature.phoneOne.imageAlt || `${feature.title} - Phone 1`,
videoAriaLabelOne: feature.phoneOne.videoAriaLabel || `${feature.title} - Phone 1 video`,
imageTwo: feature.phoneTwo.imageSrc,
videoTwo: feature.phoneTwo.videoSrc,
imageAltTwo: feature.phoneTwo.imageAlt || `${feature.title} - Phone 2`,
videoAriaLabelTwo: feature.phoneTwo.videoAriaLabel || `${feature.title} - Phone 2 video`,
})), [features, showStepNumbers, useInvertedBackground, featureContentClassName, stepNumberClassName, featureTitleClassName, featureDescriptionClassName]);
return (
<TimelinePhoneView
items={items}
showTextBox={true}
showDivider={true}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
desktopContainerClassName={desktopContainerClassName}
mobileContainerClassName={mobileContainerClassName}
desktopContentClassName={desktopContentClassName}
desktopWrapperClassName={desktopWrapperClassName}
mobileWrapperClassName={mobileWrapperClassName}
phoneFrameClassName={phoneFrameClassName}
mobilePhoneFrameClassName={mobilePhoneFrameClassName}
ariaLabel={ariaLabel}
/>
);
};
FeatureCardNine.displayName = "FeatureCardNine";
export default memo(FeatureCardNine);

View File

@@ -0,0 +1,194 @@
"use client";
import React, { memo } from "react";
import TimelineCardStack from "@/components/cardStack/layouts/timelines/TimelineCardStack";
import MediaContent from "@/components/shared/MediaContent";
import Tag from "@/components/shared/Tag";
import Button from "@/components/button/Button";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type FeatureCard = {
id: number;
tag: string;
title: string;
subtitle: string;
description: string;
buttons?: ButtonConfig[];
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
};
interface FeatureCardNineteenProps {
features: FeatureCard[];
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxTitleClassName?: string;
textBoxDescriptionClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
cardContentClassName?: string;
cardTagClassName?: string;
cardTitleClassName?: string;
cardDescriptionClassName?: string;
cardButtonClassName?: string;
cardButtonTextClassName?: string;
imageContainerClassName?: string;
imageClassName?: string;
}
const FeatureCardNineteen = ({
features,
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
textboxLayout,
useInvertedBackground,
ariaLabel = "Feature section",
className = "",
containerClassName = "",
textBoxTitleClassName = "",
textBoxDescriptionClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
cardContentClassName = "",
cardTagClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
cardButtonClassName = "",
cardButtonTextClassName = "",
imageContainerClassName = "",
imageClassName = "",
}: FeatureCardNineteenProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<TimelineCardStack
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
titleClassName={textBoxTitleClassName}
descriptionClassName={textBoxDescriptionClassName}
textBoxClassName={textBoxClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
ariaLabel={ariaLabel}
>
{features.map((feature) => {
const stepNumber = String(feature.id).padStart(2, "0");
return (
<div
key={feature.id}
className={cls("relative z-1 w-full min-h-0 h-full flex flex-col md:flex-row justify-between overflow-hidden", cardContentClassName)}
>
<div className="relative w-full md:w-1/2 md:h-full flex flex-col justify-between p-8 md:p-12">
<div className="flex flex-col gap-4 md:gap-6">
<Tag
text={feature.tag}
useInvertedBackground={useInvertedBackground}
className={cardTagClassName}
/>
<h2 className={cls(
"text-5xl md:text-7xl font-medium leading-none",
shouldUseLightText ? "text-background" : "text-foreground",
cardTitleClassName
)}>
{feature.title}
</h2>
</div>
<div className="block md:hidden w-full h-px my-6 bg-accent/20" />
<div className="flex flex-col gap-2 md:gap-4">
<h3 className={cls(
"text-xl md:text-2xl font-medium",
shouldUseLightText ? "text-background" : "text-foreground"
)}>
{feature.subtitle}
</h3>
<p className={cls(
"text-base md:text-lg leading-tight",
shouldUseLightText ? "text-background/80" : "text-foreground/80",
cardDescriptionClassName
)}>
{feature.description}
</p>
{feature.buttons && feature.buttons.length > 0 && (
<div className="flex gap-4 mt-2">
{feature.buttons.slice(0, 2).map((button, buttonIndex) => (
<Button
key={`${button.text}-${buttonIndex}`}
{...getButtonProps(button, buttonIndex, theme.defaultButtonVariant, cardButtonClassName, cardButtonTextClassName)}
/>
))}
</div>
)}
</div>
</div>
<div className="relative w-full md:w-4/10 min-h-0 h-full flex flex-col gap-10 p-8 md:p-12">
<h3 className="hidden md:block text-8xl font-medium text-accent/20 self-end">
{stepNumber}
</h3>
<div
className={cls(
"relative max-h-full min-h-0 w-full h-full min-w-0 max-w-full md:aspect-[4/5] rounded-theme-capped overflow-hidden rotate-3",
imageContainerClassName
)}
>
<MediaContent
imageSrc={feature.imageSrc}
videoSrc={feature.videoSrc}
imageAlt={feature.imageAlt || feature.title}
videoAriaLabel={feature.videoAriaLabel || feature.title}
imageClassName={cls("w-full h-full object-cover", imageClassName)}
/>
</div>
</div>
</div>
);
})}
</TimelineCardStack>
);
};
FeatureCardNineteen.displayName = "FeatureCardNineteen";
export default memo(FeatureCardNineteen);

View File

@@ -0,0 +1,173 @@
"use client";
import React, { memo } from "react";
import CardStack from "@/components/cardStack/CardStack";
import MediaContent from "@/components/shared/MediaContent";
import Button from "@/components/button/Button";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, GridVariant, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
type FeatureCardOneGridVariant = Exclude<GridVariant, "uniform-alternating-heights" | "uniform-alternating-heights-inverted">;
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type FeatureCard = {
title: string;
description: string;
button?: ButtonConfig;
} & (
| {
imageSrc: string;
imageAlt?: string;
videoSrc?: never;
videoAriaLabel?: never;
}
| {
videoSrc: string;
videoAriaLabel?: string;
imageSrc?: never;
imageAlt?: never;
}
);
interface FeatureCardOneProps {
features: FeatureCard[];
carouselMode?: "auto" | "buttons";
gridVariant: FeatureCardOneGridVariant;
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
mediaClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
cardTitleClassName?: string;
cardDescriptionClassName?: string;
cardButtonClassName?: string;
cardButtonTextClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
}
const FeatureCardOne = ({
features,
carouselMode = "buttons",
gridVariant,
uniformGridCustomHeightClasses,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
textboxLayout,
useInvertedBackground,
ariaLabel = "Feature section",
className = "",
containerClassName = "",
cardClassName = "",
mediaClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
cardButtonClassName = "",
cardButtonTextClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: FeatureCardOneProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<CardStack
mode={carouselMode}
gridVariant={gridVariant}
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{features.map((feature, index) => (
<div
key={`${feature.title}-${index}`}
className={cls("card flex flex-col gap-4 p-4 rounded-theme-capped min-h-0 h-full", cardClassName)}
>
<MediaContent
imageSrc={feature.imageSrc}
videoSrc={feature.videoSrc}
imageAlt={feature.imageAlt || "Feature image"}
videoAriaLabel={feature.videoAriaLabel || "Feature video"}
imageClassName={cls("relative z-1 min-h-0 h-full", mediaClassName)}
/>
<div className="relative z-1 flex flex-col gap-1">
<h3 className={cls("text-2xl font-medium", shouldUseLightText && "text-background", cardTitleClassName)}>
{feature.title}
</h3>
<p className={cls("text-sm leading-[1.1]", shouldUseLightText ? "text-background" : "text-foreground", cardDescriptionClassName)}>
{feature.description}
</p>
</div>
{feature.button && (
<Button {...getButtonProps(feature.button, 0, theme.defaultButtonVariant, cls("w-full", cardButtonClassName), cardButtonTextClassName)} />
)}
</div>
))}
</CardStack>
);
};
FeatureCardOne.displayName = "FeatureCardOne";
export default memo(FeatureCardOne);

View File

@@ -0,0 +1,160 @@
"use client";
import React, { memo } from "react";
import CardList from "@/components/cardStack/CardList";
import MediaContent from "@/components/shared/MediaContent";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type FeatureCard = {
id: number;
title: string;
description: string;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
};
interface FeatureCardSevenProps {
features: FeatureCard[];
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxTitleClassName?: string;
textBoxDescriptionClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
cardContentClassName?: string;
stepNumberClassName?: string;
cardTitleClassName?: string;
cardDescriptionClassName?: string;
imageContainerClassName?: string;
imageClassName?: string;
}
const FeatureCardSeven = ({
features,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
textboxLayout,
useInvertedBackground,
ariaLabel = "Feature section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxDescriptionClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
cardContentClassName = "",
stepNumberClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
imageContainerClassName = "",
imageClassName = "",
}: FeatureCardSevenProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<CardList
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
animationType={animationType}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
cardClassName={cardClassName}
titleClassName={textBoxTitleClassName}
descriptionClassName={textBoxDescriptionClassName}
textBoxClassName={textBoxClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
ariaLabel={ariaLabel}
>
{features.map((feature, index) => (
<div
key={feature.id}
className={cls("relative z-1 w-full min-h-0 h-full flex flex-col justify-between items-center p-6 gap-6 md:p-15 md:gap-15", index % 2 === 0 ? "md:flex-row" : "md:flex-row-reverse", cardContentClassName)}
>
<div className="w-full md:w-1/2 min-w-0 h-fit md:h-full flex flex-col justify-center">
<div className="w-full min-w-0 flex flex-col gap-3 md:gap-5">
<div
className={cls(
"h-8 w-[var(--height-8)] primary-button text-background rounded-full flex items-center justify-center",
stepNumberClassName
)}
>
<p className="text-sm truncate">
{feature.id}
</p>
</div>
<h2 className={cls("mt-1 text-4xl md:text-5xl font-medium leading-[1.15] text-balance", shouldUseLightText && "text-background", cardTitleClassName)}>
{feature.title}
</h2>
<p className={cls("text-base leading-[1.15] text-balance", shouldUseLightText ? "text-background" : "text-foreground", cardDescriptionClassName)}>
{feature.description}
</p>
</div>
</div>
<div
className={cls(
"relative w-full md:w-1/2 aspect-square overflow-hidden rounded-theme-capped",
imageContainerClassName
)}
>
<MediaContent
imageSrc={feature.imageSrc}
videoSrc={feature.videoSrc}
imageAlt={feature.imageAlt || feature.title}
videoAriaLabel={feature.videoAriaLabel || feature.title}
imageClassName={cls("w-full h-full object-cover", imageClassName)}
/>
</div>
</div>
))}
</CardList>
);
};
FeatureCardSeven.displayName = "FeatureCardSeven";
export default memo(FeatureCardSeven);

View File

@@ -0,0 +1,154 @@
"use client";
import React, { memo } from "react";
import TimelineCardStack from "@/components/cardStack/layouts/timelines/TimelineCardStack";
import MediaContent from "@/components/shared/MediaContent";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type FeatureCard = {
id: number;
title: string;
description: string;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
};
interface FeatureCardSixProps {
features: FeatureCard[];
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxTitleClassName?: string;
textBoxDescriptionClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
cardContentClassName?: string;
stepNumberClassName?: string;
cardTitleClassName?: string;
cardDescriptionClassName?: string;
imageContainerClassName?: string;
imageClassName?: string;
}
const FeatureCardSix = ({
features,
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
textboxLayout,
useInvertedBackground,
ariaLabel = "Feature section",
className = "",
containerClassName = "",
textBoxTitleClassName = "",
textBoxDescriptionClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
cardContentClassName = "",
stepNumberClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
imageContainerClassName = "",
imageClassName = "",
}: FeatureCardSixProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<TimelineCardStack
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
titleClassName={textBoxTitleClassName}
descriptionClassName={textBoxDescriptionClassName}
textBoxClassName={textBoxClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
ariaLabel={ariaLabel}
>
{features.map((feature) => (
<div
key={feature.id}
className={cls("relative z-1 w-full min-h-0 h-full flex flex-col md:flex-row justify-between items-center p-10 gap-10 md:p-15 md:gap-15", cardContentClassName)}
>
<div className="w-full md:w-1/2 min-w-0 h-fit md:h-full flex flex-col justify-center">
<div className="w-full min-w-0 flex flex-col gap-3 md:gap-5">
<div
className={cls(
"h-8 w-[var(--height-8)] primary-button text-background rounded-full flex items-center justify-center",
stepNumberClassName
)}
>
<p className="text-sm truncate">
{feature.id}
</p>
</div>
<h2 className={cls("mt-1 text-4xl md:text-5xl font-medium leading-[1.15] text-balance truncate", shouldUseLightText && "text-background", cardTitleClassName)}>
{feature.title}
</h2>
<p className={cls("text-base leading-[1.15] text-balance truncate", shouldUseLightText ? "text-background" : "text-foreground", cardDescriptionClassName)}>
{feature.description}
</p>
</div>
</div>
<div
className={cls(
"relative z-1 w-full md:w-1/2 min-h-0 h-full overflow-hidden rounded-theme-capped",
imageContainerClassName
)}
>
<MediaContent
imageSrc={feature.imageSrc}
videoSrc={feature.videoSrc}
imageAlt={feature.imageAlt || feature.title}
videoAriaLabel={feature.videoAriaLabel || feature.title}
imageClassName={cls("w-full min-h-0 h-full object-cover", imageClassName)}
/>
</div>
</div>
))}
</TimelineCardStack>
);
};
FeatureCardSix.displayName = "FeatureCardSix";
export default memo(FeatureCardSix);

View File

@@ -0,0 +1,169 @@
"use client";
import React, { memo } from "react";
import CardStackTextBox from "@/components/cardStack/CardStackTextBox";
import PricingFeatureList from "@/components/shared/PricingFeatureList";
import { useCardAnimation } from "@/components/cardStack/hooks/useCardAnimation";
import { Check, X } from "lucide-react";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type ComparisonItem = {
title: string;
items: string[];
};
interface FeatureCardSixteenProps {
negativeCard: ComparisonItem;
positiveCard: ComparisonItem;
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxTitleClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
textBoxDescriptionClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
gridClassName?: string;
cardClassName?: string;
cardTitleClassName?: string;
itemsListClassName?: string;
itemClassName?: string;
itemIconClassName?: string;
itemTextClassName?: string;
}
const FeatureCardSixteen = ({
negativeCard,
positiveCard,
animationType,
title,
titleSegments,
description,
textboxLayout,
useInvertedBackground,
tag,
tagIcon,
buttons,
ariaLabel = "Feature comparison section",
className = "",
containerClassName = "",
textBoxTitleClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
textBoxDescriptionClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
gridClassName = "",
cardClassName = "",
cardTitleClassName = "",
itemsListClassName = "",
itemClassName = "",
itemIconClassName = "",
itemTextClassName = "",
}: FeatureCardSixteenProps) => {
const theme = useTheme();
const shouldCardUseLightText = shouldUseInvertedText("invertDefault", theme.cardStyle) || shouldUseInvertedText("invertCard", theme.cardStyle);
const { itemRefs } = useCardAnimation({ animationType, itemCount: 2 });
const cards = [
{ ...negativeCard, variant: "negative" as const },
{ ...positiveCard, variant: "positive" as const },
];
return (
<section
aria-label={ariaLabel}
className={cls(
"relative py-20",
useInvertedBackground === "invertCard"
? "w-content-width-expanded mx-auto rounded-theme-capped bg-foreground"
: "w-full",
useInvertedBackground === "invertDefault" && "bg-foreground",
className
)}
>
<div className={cls("w-content-width mx-auto flex flex-col gap-8", containerClassName)}>
<CardStackTextBox
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
/>
<div className={cls(
"relative mx-auto w-full md:w-60 grid grid-cols-1 gap-6",
cards.length >= 2 ? "md:grid-cols-2" : "md:grid-cols-1",
gridClassName
)}>
{cards.map((card, index) => (
<div
key={card.variant}
ref={(el) => { itemRefs.current[index] = el; }}
className={cls(
"relative h-full card rounded-theme-capped p-6",
cardClassName
)}
>
<div className={cls("flex flex-col gap-6", card.variant === "negative" && "opacity-50")}>
<h3 className={cls(
"text-3xl text-center font-medium",
shouldCardUseLightText ? "text-background" : "text-foreground",
cardTitleClassName
)}>
{card.title}
</h3>
<PricingFeatureList
features={card.items}
icon={card.variant === "positive" ? Check : X}
shouldUseLightText={shouldCardUseLightText}
className={itemsListClassName}
featureItemClassName={itemClassName}
featureIconWrapperClassName=""
featureIconClassName={itemIconClassName}
featureTextClassName={cls("truncate", itemTextClassName)}
/>
</div>
</div>
))}
</div>
</div>
</section>
);
};
FeatureCardSixteen.displayName = "FeatureCardSixteen";
export default memo(FeatureCardSixteen);

View File

@@ -0,0 +1,257 @@
"use client";
import React, { memo, useMemo } from "react";
import TimelineProcessFlow from "@/components/cardStack/layouts/timelines/TimelineProcessFlow";
import MediaContent from "@/components/shared/MediaContent";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type FeatureMedia = {
imageAlt?: string;
videoAriaLabel?: string;
} & (
| { imageSrc: string; videoSrc?: never }
| { videoSrc: string; imageSrc?: never }
);
interface FeatureListItem {
icon: LucideIcon;
text: string;
}
interface FeatureCard {
id: string;
title: string;
description: string;
media: FeatureMedia;
items: FeatureListItem[];
reverse: boolean;
}
interface FeatureCardTenProps {
features: FeatureCard[];
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
textboxLayout: TextboxLayout;
animationType: CardAnimationType;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxClassName?: string;
textBoxTitleClassName?: string;
textBoxDescriptionClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
itemClassName?: string;
mediaWrapperClassName?: string;
mediaCardClassName?: string;
numberClassName?: string;
contentWrapperClassName?: string;
featureTitleClassName?: string;
featureDescriptionClassName?: string;
listItemClassName?: string;
iconContainerClassName?: string;
iconClassName?: string;
gapClassName?: string;
}
interface FeatureMediaProps {
media: FeatureMedia;
title: string;
mediaCardClassName: string;
}
const FeatureMedia = ({
media,
title,
mediaCardClassName,
}: FeatureMediaProps) => (
<div className={cls("card rounded-theme-capped p-4", mediaCardClassName)}>
<MediaContent
imageSrc={media.imageSrc}
videoSrc={media.videoSrc}
imageAlt={media.imageAlt || title}
videoAriaLabel={media.videoAriaLabel || `${title} video`}
imageClassName="relative z-1 w-full h-full object-cover rounded-theme-capped"
/>
</div>
);
interface FeatureContentProps {
feature: FeatureCard;
useInvertedBackground: InvertedBackground;
shouldUseLightText: boolean;
featureTitleClassName: string;
featureDescriptionClassName: string;
listItemClassName: string;
iconContainerClassName: string;
iconClassName: string;
}
const FeatureContent = ({
feature,
useInvertedBackground,
shouldUseLightText,
featureTitleClassName,
featureDescriptionClassName,
listItemClassName,
iconContainerClassName,
iconClassName,
}: FeatureContentProps) => (
<div className="flex flex-col gap-3" >
<h3 className={cls("text-xl md:text-4xl font-medium leading-[1.15]", (useInvertedBackground === "invertDefault" || useInvertedBackground === "invertCard") && "text-background", featureTitleClassName)}>
{feature.title}
</h3>
<p className={cls("text-base leading-[1.2]", (useInvertedBackground === "invertDefault" || useInvertedBackground === "invertCard") ? "text-background/75" : "text-foreground/75", featureDescriptionClassName)}>
{feature.description}
</p>
<ul className="flex flex-col m-0 mt-1 p-0 list-none gap-3">
{feature.items.map((listItem, listIndex) => {
const Icon = listItem.icon;
return (
<li key={listIndex} className="flex items-center gap-3">
<div
className={cls(
"shrink-0 h-9 aspect-square flex items-center justify-center rounded bg-background card",
iconContainerClassName
)}
>
<Icon
className={cls("h-4/10 w-4/10", shouldUseLightText ? "text-background" : "text-foreground", iconClassName)}
strokeWidth={1.25}
/>
</div>
<p className={cls("text-base", (useInvertedBackground === "invertDefault" || useInvertedBackground === "invertCard") ? "text-background/75" : "text-foreground/75", listItemClassName)}>
{listItem.text}
</p>
</li>
);
})}
</ul>
</div>
);
const FeatureCardTen = ({
features,
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
textboxLayout,
animationType,
useInvertedBackground,
ariaLabel = "Feature section",
className = "",
containerClassName = "",
textBoxClassName = "",
textBoxTitleClassName = "",
textBoxDescriptionClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
itemClassName = "",
mediaWrapperClassName = "",
mediaCardClassName = "",
numberClassName = "",
contentWrapperClassName = "",
featureTitleClassName = "",
featureDescriptionClassName = "",
listItemClassName = "",
iconContainerClassName = "",
iconClassName = "",
gapClassName = "",
}: FeatureCardTenProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const timelineItems = useMemo(
() =>
features.map((feature) => ({
id: feature.id,
reverse: feature.reverse,
media: (
<FeatureMedia
media={feature.media}
title={feature.title}
mediaCardClassName={mediaCardClassName}
/>
),
content: (
<FeatureContent
feature={feature}
useInvertedBackground={useInvertedBackground}
shouldUseLightText={shouldUseLightText}
featureTitleClassName={featureTitleClassName}
featureDescriptionClassName={featureDescriptionClassName}
listItemClassName={listItemClassName}
iconContainerClassName={iconContainerClassName}
iconClassName={iconClassName}
/>
),
})),
[
features,
useInvertedBackground,
shouldUseLightText,
mediaCardClassName,
featureTitleClassName,
featureDescriptionClassName,
listItemClassName,
iconContainerClassName,
iconClassName,
]
);
return (
<TimelineProcessFlow
items={timelineItems}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
animationType={animationType}
useInvertedBackground={useInvertedBackground}
ariaLabel={ariaLabel}
className={className}
containerClassName={containerClassName}
textBoxClassName={textBoxClassName}
textBoxTitleClassName={textBoxTitleClassName}
textBoxDescriptionClassName={textBoxDescriptionClassName}
textBoxTagClassName={textBoxTagClassName}
textBoxButtonContainerClassName={textBoxButtonContainerClassName}
textBoxButtonClassName={textBoxButtonClassName}
textBoxButtonTextClassName={textBoxButtonTextClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
itemClassName={itemClassName}
mediaWrapperClassName={mediaWrapperClassName}
numberClassName={numberClassName}
contentWrapperClassName={contentWrapperClassName}
gapClassName={gapClassName}
/>
);
};
FeatureCardTen.displayName = "FeatureCardTen";
export default memo(FeatureCardTen);

View File

@@ -0,0 +1,232 @@
"use client";
import React, { memo } from "react";
import CardStack from "@/components/cardStack/CardStack";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, GridVariant, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type FeatureCardThirteenGridVariant = Exclude<GridVariant, "one-large-right-three-stacked-left" | "one-large-left-three-stacked-right" | "timeline" | "timeline-three-columns">;
type FeatureCard = {
id: string;
title: string;
description: string;
};
interface FeatureCardThirteenProps {
features: FeatureCard[];
carouselMode?: "auto" | "buttons";
gridVariant: FeatureCardThirteenGridVariant;
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
cardIdClassName?: string;
cardTitleClassName?: string;
cardDescriptionClassName?: string;
cardDotsClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
}
const FeatureCardThirteen = ({
features,
carouselMode = "buttons",
gridVariant,
uniformGridCustomHeightClasses,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
textboxLayout,
useInvertedBackground,
ariaLabel = "Feature section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
cardIdClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
cardDotsClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: FeatureCardThirteenProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
// Override gridRows for certain variants to use smaller heights
const getGridRowsOverride = () => {
if (gridVariant === "two-columns-alternating-heights") {
return "md:grid-rows-[9rem_9rem_0.5rem_0.5rem_9rem_9rem] 2xl:grid-rows-[12rem_12rem_0.5rem_0.5rem_12rem_12rem]";
}
if (gridVariant === "asymmetric-60-wide-40-narrow") {
return "md:grid-rows-[18rem_18rem] 2xl:grid-rows-[21rem_21rem]";
}
if (gridVariant === "three-columns-all-equal-width") {
return "md:grid-rows-[15rem_15rem] 2xl:grid-rows-[18rem_18rem]";
}
if (gridVariant === "four-items-2x2-equal-grid") {
return "md:grid-rows-[20rem_20rem] 2xl:grid-rows-[26rem_26rem]";
}
if (gridVariant === "items-top-row-full-width-bottom" || gridVariant === "full-width-top-items-bottom-row") {
return "md:grid-rows-[18rem_18rem] 2xl:grid-rows-[21rem_21rem]";
}
return undefined;
};
const gridRowsOverride = getGridRowsOverride();
// Override heightClasses for certain variants to use smaller heights
const getHeightClassesOverride = () => {
if (gridVariant === "four-items-2x2-staggered-grid" || gridVariant === "four-items-2x2-staggered-grid-inverted") {
return "min-h-60 2xl:min-h-70";
}
return uniformGridCustomHeightClasses;
};
const heightClassesOverride = getHeightClassesOverride();
// Override itemHeightClasses for alternating height variants to use smaller heights
const getItemHeightClassesOverride = () => {
if (gridVariant === "four-items-2x2-alternating-heights") {
return [
"min-h-80 md:min-h-80 2xl:min-h-100",
"min-h-80 md:min-h-50 2xl:min-h-60",
"min-h-80 md:min-h-80 2xl:min-h-100",
"min-h-80 md:min-h-50 2xl:min-h-60",
];
}
if (gridVariant === "four-items-2x2-alternating-heights-inverted") {
return [
"min-h-80 md:min-h-50 2xl:min-h-60",
"min-h-80 md:min-h-80 2xl:min-h-100",
"min-h-80 md:min-h-50 2xl:min-h-60",
"min-h-80 md:min-h-80 2xl:min-h-100",
];
}
return undefined;
};
const itemHeightClassesOverride = getItemHeightClassesOverride();
return (
<CardStack
mode={carouselMode}
gridVariant={gridVariant}
uniformGridCustomHeightClasses={heightClassesOverride}
gridRowsClassName={gridRowsOverride}
itemHeightClassesOverride={itemHeightClassesOverride}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{features.map((feature, index) => (
<div
key={`${feature.id}-${index}`}
className={cls(
"card relative flex flex-col justify-between p-6 rounded-theme-capped min-h-0 h-full overflow-hidden",
cardClassName
)}
>
<div className="relative z-10 flex flex-col gap-2" >
<div className="relative z-10 flex justify-between items-center">
<span
className={cls(
"text-sm font-medium text-foreground",
cardIdClassName
)}
>
/ {feature.id}
</span>
<div className={cls("flex gap-1", cardDotsClassName)}>
<span className="h-2 w-auto aspect-square rounded-theme bg-accent" />
<span className="h-2 w-auto aspect-square rounded-theme bg-accent" />
<span className="h-2 w-auto aspect-square rounded-theme bg-accent" />
</div>
</div>
<h3
className={cls(
"text-4xl font-medium leading-tight",
shouldUseLightText ? "text-background" : "text-foreground",
cardTitleClassName
)}
>
{feature.title}
</h3>
</div>
<div className="relative z-10 flex flex-col gap-1">
<p
className={cls(
"text-sm leading-tight text-balance line-clamp-3",
shouldUseLightText ? "text-background" : "text-foreground",
cardDescriptionClassName
)}
>
{feature.description}
</p>
</div>
</div>
))}
</CardStack>
);
};
FeatureCardThirteen.displayName = "FeatureCardThirteen";
export default memo(FeatureCardThirteen);

View File

@@ -0,0 +1,177 @@
"use client";
import React, { memo } from "react";
import CardList from "@/components/cardStack/CardList";
import Button from "@/components/button/Button";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
interface FeatureCard {
id: string;
label: string;
title: string;
items: string[];
buttons?: ButtonConfig[];
}
interface FeatureCardTwelveProps {
features: FeatureCard[];
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxTitleClassName?: string;
textBoxDescriptionClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
cardContentClassName?: string;
labelClassName?: string;
cardTitleClassName?: string;
itemsContainerClassName?: string;
itemTextClassName?: string;
cardButtonClassName?: string;
cardButtonTextClassName?: string;
}
const FeatureCardTwelve = ({
features,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
textboxLayout,
useInvertedBackground,
ariaLabel = "Feature section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxDescriptionClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
cardContentClassName = "",
labelClassName = "",
cardTitleClassName = "",
itemsContainerClassName = "",
itemTextClassName = "",
cardButtonClassName = "",
cardButtonTextClassName = "",
}: FeatureCardTwelveProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<CardList
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
animationType={animationType}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
cardClassName={cardClassName}
titleClassName={textBoxTitleClassName}
descriptionClassName={textBoxDescriptionClassName}
textBoxClassName={textBoxClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
ariaLabel={ariaLabel}
>
{features.map((feature) => (
<div
key={feature.id}
className={cls(
"relative z-1 w-full min-h-0 h-full flex flex-col md:flex-row gap-6 p-6 md:p-15",
cardContentClassName
)}
>
<div className="relative z-1 w-full md:w-1/2 flex md:justify-start">
<h2 className={cls(
"text-5xl md:text-6xl font-medium leading-[1.1]",
shouldUseLightText && "text-background",
labelClassName
)}>
{feature.label}
</h2>
</div>
<div className="relative z-1 w-full h-px bg-foreground/20 md:hidden" />
<div className="relative z-1 w-full md:w-1/2 flex flex-col gap-4">
<h3 className={cls(
"text-xl md:text-3xl font-medium leading-tight",
shouldUseLightText ? "text-background" : "text-foreground",
cardTitleClassName
)}>
{feature.title}
</h3>
<div className={cls("flex flex-wrap items-center gap-2", itemsContainerClassName)}>
{feature.items.map((item, index) => (
<React.Fragment key={index}>
<span className={cls(
"text-base",
shouldUseLightText ? "text-background" : "text-foreground",
itemTextClassName
)}>
{item}
</span>
{index < feature.items.length - 1 && (
<span className="text-base text-accent"></span>
)}
</React.Fragment>
))}
</div>
{feature.buttons && feature.buttons.length > 0 && (
<div className="mt-3 flex gap-4">
{feature.buttons.slice(0, 2).map((button, index) => (
<Button key={`${button.text}-${index}`} {...getButtonProps(button, index, theme.defaultButtonVariant, cardButtonClassName, cardButtonTextClassName)} />
))}
</div>
)}
</div>
</div>
))}
</CardList>
);
};
FeatureCardTwelve.displayName = "FeatureCardTwelve";
export default memo(FeatureCardTwelve);

View File

@@ -0,0 +1,178 @@
"use client";
import React, { memo, useState, useEffect } from "react";
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, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type FeatureImage = {
id: number;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
};
interface FeatureCardTwentyProps {
images: FeatureImage[];
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
imagesContainerClassName?: string;
imageWrapperClassName?: string;
imageClassName?: string;
}
const FeatureCardTwenty = ({
images,
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
textboxLayout,
useInvertedBackground,
ariaLabel = "Feature section",
className = "",
containerClassName = "",
textBoxClassName = "",
titleClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
imagesContainerClassName = "",
imageWrapperClassName = "",
imageClassName = "",
}: FeatureCardTwentyProps) => {
const [isCentered, setIsCentered] = useState(false);
useEffect(() => {
const handleResize = () => {
setIsCentered(window.innerWidth < 768);
};
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
const renderImageCard = (image: FeatureImage, heightClass: string) => (
<div
key={image.id}
className={cls(
"w-full overflow-hidden rounded-theme-capped",
heightClass,
imageWrapperClassName
)}
>
<MediaContent
imageSrc={image.imageSrc}
videoSrc={image.videoSrc}
imageAlt={image.imageAlt || `Feature image ${image.id}`}
videoAriaLabel={image.videoAriaLabel || `Feature video ${image.id}`}
imageClassName={cls("w-full h-full object-cover", imageClassName)}
/>
</div>
);
const textBoxElement = (
<TextBox
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
center={isCentered}
className={cls("flex flex-col gap-3 md:gap-3", textBoxClassName)}
titleClassName={cls("text-7xl 2xl:text-8xl font-medium text-center md:text-left text-balance", titleClassName)}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
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)}
/>
);
return (
<section
aria-label={ariaLabel}
className={cls(
"relative py-20",
useInvertedBackground === "invertCard"
? "w-content-width-expanded mx-auto rounded-theme-capped bg-foreground"
: "w-full",
useInvertedBackground === "invertDefault" && "bg-foreground",
className
)}
>
<div className={cls("hidden md:flex w-content-width mx-auto gap-10", containerClassName)}>
<div className={cls("w-1/2 flex flex-col gap-10", imagesContainerClassName)}>
{images.map((image) => renderImageCard(image, "h-[90svh]"))}
</div>
<div className="w-1/2">
<div className={cls("sticky top-0 flex flex-col justify-center gap-6 h-svh", textBoxClassName)}>
{textBoxElement}
</div>
</div>
</div>
<div className="md:hidden">
<AutoCarousel
title=""
description=""
textboxLayout="default"
animationType="none"
className="py-0"
carouselClassName="py-0"
containerClassName="!w-full"
itemClassName="!w-55"
ariaLabel="Feature images carousel"
showTextBox={false}
>
{images.map((image) => renderImageCard(image, "aspect-[4/5]"))}
</AutoCarousel>
</div>
<div className="md:hidden w-content-width mx-auto">
<div className={cls("flex flex-col gap-6 mt-8", textBoxClassName)}>
{textBoxElement}
</div>
</div>
</section>
);
};
FeatureCardTwenty.displayName = "FeatureCardTwenty";
export default memo(FeatureCardTwenty);

View File

@@ -0,0 +1,215 @@
"use client";
import React, { memo, useState, useEffect } from "react";
import TextBox from "@/components/Textbox";
import Accordion from "@/components/Accordion";
import MediaContent from "@/components/shared/MediaContent";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, TitleSegment } from "@/components/cardStack/types";
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
type MediaProps =
| {
imageSrc: string;
imageAlt?: string;
videoSrc?: never;
videoAriaLabel?: never;
}
| {
videoSrc: string;
videoAriaLabel?: string;
imageSrc?: never;
imageAlt?: never;
};
type AccordionItem = {
id: string;
title: string;
content: string;
};
type FeatureCardTwentyOneProps = MediaProps & {
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
accordionItems: AccordionItem[];
useInvertedBackground: InvertedBackground;
mediaPosition?: "left" | "right";
ariaLabel?: string;
className?: string;
containerClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
contentClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
accordionContainerClassName?: string;
accordionClassName?: string;
accordionTitleClassName?: string;
accordionContentClassName?: string;
accordionIconContainerClassName?: string;
accordionIconClassName?: string;
};
const FeatureCardTwentyOne = ({
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
accordionItems,
imageSrc,
imageAlt,
videoSrc,
videoAriaLabel,
useInvertedBackground,
mediaPosition = "left",
ariaLabel = "Feature section",
className = "",
containerClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
contentClassName = "",
textBoxClassName = "",
titleClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
accordionContainerClassName = "",
accordionClassName = "",
accordionTitleClassName = "",
accordionContentClassName = "",
accordionIconContainerClassName = "",
accordionIconClassName = "",
}: FeatureCardTwentyOneProps) => {
const [isCentered, setIsCentered] = useState(false);
const [activeAccordion, setActiveAccordion] = useState<number>(0);
useEffect(() => {
const handleResize = () => {
setIsCentered(window.innerWidth < 768);
};
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
const handleAccordionToggle = (index: number) => {
setActiveAccordion(activeAccordion === index ? -1 : index);
};
const mediaElement = (
<div className={cls(
"w-full md:w-1/2 h-[50svh] md:h-auto rounded-theme-capped overflow-hidden",
mediaWrapperClassName
)}>
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("w-full h-full object-cover", mediaClassName)}
/>
</div>
);
const contentElement = (
<div className={cls(
"w-full md:w-1/2 flex flex-col",
contentClassName
)}>
<TextBox
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
useInvertedBackground={useInvertedBackground}
className={cls("flex flex-col gap-1", textBoxClassName)}
titleClassName={cls("text-4xl md:text-5xl font-medium text-center md:text-left text-balance", titleClassName)}
descriptionClassName={cls("text-base md:text-lg 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-1 md:mb-2", tagClassName)}
buttonContainerClassName={cls("flex gap-4 mt-4", buttonContainerClassName)}
buttonClassName={buttonClassName}
buttonTextClassName={cls("text-base", buttonTextClassName)}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
center={isCentered}
/>
<div className={cls(
"flex flex-col mt-8 divide-y divide-accent/20 border-y border-accent/20",
accordionContainerClassName
)}>
{accordionItems.map((item, index) => (
<Accordion
key={item.id}
index={index}
isActive={activeAccordion === index}
onToggle={handleAccordionToggle}
title={item.title}
content={item.content}
showCard={false}
useInvertedBackground={useInvertedBackground === "noInvert" ? undefined : useInvertedBackground}
className={cls("py-4 md:py-6", accordionClassName)}
titleClassName={cls("text-xl md:text-2xl", accordionTitleClassName)}
contentClassName={accordionContentClassName}
iconContainerClassName={accordionIconContainerClassName}
iconClassName={accordionIconClassName}
/>
))}
</div>
</div>
);
return (
<section
aria-label={ariaLabel}
className={cls(
"relative w-full py-20",
useInvertedBackground === "invertCard" && "w-content-width-expanded mx-auto rounded-theme-capped bg-foreground",
useInvertedBackground === "invertDefault" && "bg-foreground",
className
)}
>
<div className={cls(
"w-content-width mx-auto flex flex-col md:flex-row gap-8 md:gap-15",
containerClassName
)}>
{mediaPosition === "left" ? (
<>
{mediaElement}
{contentElement}
</>
) : (
<>
{contentElement}
{mediaElement}
</>
)}
</div>
</section>
);
};
FeatureCardTwentyOne.displayName = "FeatureCardTwentyOne";
export default memo(FeatureCardTwentyOne);

View File

@@ -0,0 +1,32 @@
.feature-card-three-title {
font-size: var(--text-2xl);
}
.feature-card-three-description {
font-size: var(--text-sm);
}
/* Mobile touch support - duplicate hover states for is-active class */
.feature-card-three-item.is-active .feature-card-three-item-box > div {
transform: rotateY(180deg);
}
.feature-card-three-item.is-active .feature-card-three-content-wrapper {
transform: translateY(var(--hover-translate-y));
}
.feature-card-three-item.is-active .feature-card-three-title {
color: var(--color-foreground);
}
.feature-card-three-item.is-active .feature-card-three-description-wrapper {
opacity: 1;
}
.feature-card-three-item.is-active .feature-card-three-reveal-bg {
--tw-translate-y: 0px !important;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)) !important;
left: calc(var(--vw-1_5) * 0.75);
bottom: calc(var(--vw-1_5) * 0.75);
right: calc(var(--vw-1_5) * 0.75);
}

View File

@@ -0,0 +1,181 @@
"use client";
import "./FeatureCardThree.css";
import React, { memo, useRef, useCallback, useState } from "react";
import CardStack from "@/components/cardStack/CardStack";
import FeatureCardThreeItem from "./FeatureCardThreeItem";
import { useDynamicDimensions } from "./useDynamicDimensions";
import { useClickOutside } from "@/hooks/useClickOutside";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, GridVariant, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type FeatureCard = {
id: string;
title: string;
description: string;
imageSrc: string;
imageAlt?: string;
};
interface FeatureCardThreeProps {
features: FeatureCard[];
carouselMode?: "auto" | "buttons";
gridVariant: GridVariant;
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
cardTitleClassName?: string;
cardDescriptionClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
itemContentClassName?: string;
}
const FeatureCardThree = ({
features,
carouselMode = "buttons",
gridVariant,
uniformGridCustomHeightClasses,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
textboxLayout,
useInvertedBackground,
ariaLabel = "Feature section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
itemContentClassName = "",
}: FeatureCardThreeProps) => {
const featureCardThreeRefs = useRef<(HTMLDivElement | null)[]>([]);
const containerRef = useRef<HTMLDivElement>(null);
const [activeIndex, setActiveIndex] = useState<number | null>(null);
// Override heightClasses for staggered variants to use smaller heights
const heightClassesOverride =
gridVariant === "four-items-2x2-staggered-grid" || gridVariant === "four-items-2x2-staggered-grid-inverted"
? "h-110 2xl:h-120"
: uniformGridCustomHeightClasses;
const setRef = useCallback(
(index: number) => (el: HTMLDivElement | null) => {
if (featureCardThreeRefs.current) {
featureCardThreeRefs.current[index] = el;
}
},
[]
);
// Check if device supports hover (desktop) or not (mobile/touch)
const isTouchDevice = typeof window !== "undefined" && window.matchMedia("(hover: none)").matches;
// Handle click outside to deactivate on mobile
useClickOutside(
containerRef,
() => setActiveIndex(null),
activeIndex !== null && isTouchDevice
);
const handleItemClick = useCallback((index: number) => {
if (typeof window !== "undefined" && !window.matchMedia("(hover: none)").matches) return;
setActiveIndex((prev) => (prev === index ? null : index));
}, []);
useDynamicDimensions([featureCardThreeRefs], {
titleSelector: ".feature-card-three-title-row .feature-card-three-title",
descriptionSelector: ".feature-card-three-description-wrapper .feature-card-three-description",
});
return (
<div ref={containerRef}>
<CardStack
mode={carouselMode}
gridVariant={gridVariant}
uniformGridCustomHeightClasses={heightClassesOverride}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{features.map((feature, index) => (
<FeatureCardThreeItem
key={`${feature.id}-${index}`}
ref={setRef(index)}
item={feature}
isActive={activeIndex === index}
onItemClick={() => handleItemClick(index)}
className={cardClassName}
itemContentClassName={itemContentClassName}
itemTitleClassName={cardTitleClassName}
itemDescriptionClassName={cardDescriptionClassName}
/>
))}
</CardStack>
</div>
);
};
FeatureCardThree.displayName = "FeatureCardThree";
export default memo(FeatureCardThree);

View File

@@ -0,0 +1,144 @@
"use client";
import { forwardRef, memo } from "react";
import Image from "next/image";
import { Info } from "lucide-react";
import { cls } from "@/lib/utils";
interface FeatureCardThreeItemData {
id: string;
title: string;
description: string;
imageSrc: string;
imageAlt?: string;
}
interface FeatureCardThreeItemProps {
item: FeatureCardThreeItemData;
isActive?: boolean;
onItemClick?: () => void;
className?: string;
itemContentClassName?: string;
itemTitleClassName?: string;
itemDescriptionClassName?: string;
}
const MASK_GRADIENT = "linear-gradient(to bottom, transparent, black 60%)";
const FeatureCardThreeItem = memo(
forwardRef<HTMLDivElement, FeatureCardThreeItemProps>(
(
{
item,
isActive = false,
onItemClick,
className = "",
itemContentClassName = "",
itemTitleClassName = "",
itemDescriptionClassName = "",
},
ref
) => {
return (
<div
ref={ref}
className={cls(
"feature-card-three-item relative overflow-hidden h-full rounded-theme-capped group",
isActive && "is-active",
className
)}
role="article"
aria-label={`${item.title} - Feature ${item.id}`}
tabIndex={0}
onClick={onItemClick}
>
<div
className="feature-card-three-item-box absolute top-6 left-6 z-10 flex items-center justify-center [perspective:1000px] [transform-style:preserve-3d]"
>
<div
className="relative h-8 aspect-square rounded-theme bg-background transition-transform duration-400 ease-[cubic-bezier(0.4,0,0.2,1)] [transform-style:preserve-3d] group-hover:[transform:rotateY(180deg)]"
>
<div
className="feature-card-three-item-box-front absolute w-full h-full rounded-theme bg-background flex items-center justify-center [backface-visibility:hidden]"
>
<p
className="feature-card-three-description text-foreground truncate"
>
{item.id}
</p>
</div>
<div
className="feature-card-three-item-box-back absolute w-full h-full rounded-theme bg-background flex items-center justify-center [backface-visibility:hidden] [transform:rotateY(180deg)]"
>
<Info
className="w-1/2 h-1/2 text-foreground"
strokeWidth={1.5}
aria-hidden="true"
/>
</div>
</div>
</div>
<Image
src={item.imageSrc}
alt={item.imageAlt || item.title}
width={1920}
height={1080}
className="relative z-1 object-cover rounded-theme-capped h-full w-full "
unoptimized={item.imageSrc.startsWith('http') || item.imageSrc.startsWith('//')}
aria-hidden={item.imageAlt === ""}
/>
<div
className="absolute z-10 bottom-0 left-0 right-0 h-30 backdrop-blur-xl opacity-100"
style={{ maskImage: MASK_GRADIENT }}
aria-hidden="true"
/>
<div
className="feature-card-three-content-wrapper absolute z-20 transition-all duration-400 ease-out flex flex-col gap-2 group-hover:[transform:translateY(var(--hover-translate-y))]"
style={{
top: "var(--content-top-position)",
left: "calc((var(--vw-1_5) * 1.5))",
width: "calc(100% - (var(--vw-1_5) * 3))",
}}
>
<div className="feature-card-three-title-row">
<h2
className={cls(
"feature-card-three-title font-semibold leading-[110%] transition-colors text-background group-hover:text-foreground",
itemTitleClassName
)}
>
{item.title}
</h2>
</div>
<div
className="feature-card-three-description-wrapper transition-all duration-400 ease-out opacity-0 group-hover:opacity-100"
>
<p
className={cls("feature-card-three-description leading-[120%] w-full text-foreground", itemDescriptionClassName)}
>
{item.description}
</p>
</div>
</div>
<div
className={cls(
"feature-card-three-reveal-bg !absolute left-0 bottom-0 card z-10 rounded-theme-capped transition-all duration-400 ease-out translate-y-full right-0 group-hover:translate-y-0 group-hover:left-[calc(var(--vw-1_5)*0.75)] group-hover:bottom-[calc(var(--vw-1_5)*0.75)] group-hover:right-[calc(var(--vw-1_5)*0.75)]",
itemContentClassName
)}
style={{
height: "var(--reveal-bg-height)",
}}
></div>
</div>
);
}
)
);
FeatureCardThreeItem.displayName = "FeatureCardThreeItem";
export default FeatureCardThreeItem;

View File

@@ -0,0 +1,122 @@
import { useEffect, useCallback, useMemo } from 'react'
let cachedVw15: number | null = null
let lastWindowWidth = 0
const getVw15InPixels = (): number => {
const currentWidth = window.innerWidth
if (cachedVw15 !== null && lastWindowWidth === currentWidth) {
return cachedVw15
}
const temp = document.createElement('div')
temp.style.position = 'absolute'
temp.style.width = 'var(--vw-1_5)'
document.body.appendChild(temp)
const width = temp.getBoundingClientRect().width
document.body.removeChild(temp)
cachedVw15 = width || 0
lastWindowWidth = currentWidth
return cachedVw15
}
const debounce = <T extends (...args: unknown[]) => void>(func: T, wait: number): ((...args: Parameters<T>) => void) & { cancel: () => void } => {
let timeout: NodeJS.Timeout | null = null
const debouncedFunction = function executedFunction(...args: Parameters<T>) {
const later = () => {
timeout = null
func(...args)
}
if (timeout !== null) {
clearTimeout(timeout)
}
timeout = setTimeout(later, wait)
}
debouncedFunction.cancel = () => {
if (timeout !== null) {
clearTimeout(timeout)
timeout = null
}
}
return debouncedFunction
}
interface DynamicDimensionsOptions {
titleSelector?: string
descriptionSelector?: string
containerSelector?: string | null
}
type RefArray = React.RefObject<(HTMLDivElement | null)[]> | React.RefObject<(HTMLDivElement | null)[]>[]
export const useDynamicDimensions = (refs: RefArray, options: DynamicDimensionsOptions = {}) => {
const {
titleSelector = '.feature-card-three-title',
descriptionSelector = '.feature-card-three-description',
containerSelector = null
} = options
const calculateDimensions = useCallback(() => {
const processRef = (ref: HTMLElement | null) => {
if (!ref) return
const container = containerSelector ? ref.querySelector(containerSelector) as HTMLElement : ref
if (!container) return
const titleElement = container.querySelector(titleSelector) as HTMLElement
const descriptionElement = container.querySelector(descriptionSelector) as HTMLElement
if (titleElement && descriptionElement) {
const titleHeight = titleElement.offsetHeight
const descriptionHeight = descriptionElement.offsetHeight
const contentTop = `calc(100% - ${titleHeight}px - calc(var(--vw-1_5) * 1.5))`
const vw15 = getVw15InPixels()
const contentWrapperHeight = titleHeight + descriptionHeight
const revealBgHeight = contentWrapperHeight + (vw15 * 2)
const moveUp = descriptionHeight + (vw15 * 0.55)
ref.style.setProperty('--reveal-bg-height', `${revealBgHeight}px`)
ref.style.setProperty('--content-top-position', contentTop)
ref.style.setProperty('--hover-translate-y', `-${moveUp}px`)
}
}
if (Array.isArray(refs)) {
refs.forEach((refArray) => {
if (refArray?.current && Array.isArray(refArray.current)) {
refArray.current.forEach(processRef)
}
})
} else if ('current' in refs && refs.current) {
if (Array.isArray(refs.current)) {
refs.current.forEach(processRef)
} else {
processRef(refs.current)
}
}
}, [titleSelector, descriptionSelector, containerSelector, refs])
const debouncedCalculate = useMemo(
() => debounce(calculateDimensions, 250),
[calculateDimensions]
)
useEffect(() => {
calculateDimensions()
window.addEventListener('resize', debouncedCalculate)
return () => {
window.removeEventListener('resize', debouncedCalculate)
debouncedCalculate.cancel()
}
}, [calculateDimensions, debouncedCalculate])
}

View File

@@ -0,0 +1,109 @@
"use client";
import { memo } from "react";
import Image from "next/image";
import ButtonTextUnderline from "@/components/button/ButtonTextUnderline";
import FooterColumns from "@/components/shared/FooterColumns";
import { cls } from "@/lib/utils";
import type { FooterColumn } from "@/components/shared/FooterColumns";
interface FooterBaseProps {
logoSrc?: string;
logoText?: string;
logoWidth?: number;
logoHeight?: number;
columns: FooterColumn[];
copyrightText?: string;
onPrivacyClick?: () => void;
ariaLabel?: string;
className?: string;
containerClassName?: string;
logoClassName?: string;
logoTextClassName?: string;
columnsClassName?: string;
columnClassName?: string;
columnTitleClassName?: string;
columnItemClassName?: string;
copyrightContainerClassName?: string;
copyrightTextClassName?: string;
privacyButtonClassName?: string;
}
const FooterBase = memo<FooterBaseProps>(function FooterBase({
logoSrc = "/brand/logowhite.svg",
logoText = "Webild",
logoWidth = 120,
logoHeight = 40,
columns,
copyrightText = `© 2025 | Webild`,
onPrivacyClick,
ariaLabel = "Site footer",
className = "",
containerClassName = "",
logoClassName = "",
logoTextClassName = "",
columnsClassName = "",
columnClassName = "",
columnTitleClassName = "",
columnItemClassName = "",
copyrightContainerClassName = "",
copyrightTextClassName = "",
privacyButtonClassName = "",
}) {
return (
<footer
role="contentinfo"
aria-label={ariaLabel}
className={cls("relative overflow-hidden w-full primary-button text-background py-15", className)}
>
<div
className={cls("relative w-content-width mx-auto z-10", containerClassName)}
>
<div className="flex flex-col md:flex-row gap-10 md:gap-0 justify-between items-start mb-10">
{logoSrc ? (
<div className="flex-shrink-0">
<Image
src={logoSrc}
alt="Logo"
width={logoWidth}
height={logoHeight}
className={cls("object-contain", logoClassName)}
unoptimized={logoSrc.startsWith('http') || logoSrc.startsWith('//')}
aria-hidden={true}
/>
</div>
) : (
<h2 className={cls("text-4xl font-medium text-background", logoTextClassName)}>
{logoText}
</h2>
)}
<FooterColumns
columns={columns}
className={columnsClassName}
columnClassName={columnClassName}
columnTitleClassName={cls("text-background/50", columnTitleClassName)}
columnItemClassName={cls("text-background", columnItemClassName)}
/>
</div>
<div
className={cls("w-full flex items-center justify-between pt-9 border-t border-background/20", copyrightContainerClassName)}
>
<span className={cls("text-background/50 text-sm", copyrightTextClassName)}>
{copyrightText}
</span>
<ButtonTextUnderline
text="Privacy Policy"
onClick={onPrivacyClick}
className={cls("text-background/50", privacyButtonClassName)}
/>
</div>
</div>
</footer>
);
});
FooterBase.displayName = "FooterBase";
export default FooterBase;

View File

@@ -0,0 +1,109 @@
"use client";
import { memo } from "react";
import Image from "next/image";
import ButtonTextUnderline from "@/components/button/ButtonTextUnderline";
import FooterColumns from "@/components/shared/FooterColumns";
import { cls } from "@/lib/utils";
import type { FooterColumn } from "@/components/shared/FooterColumns";
interface FooterBaseCardProps {
logoSrc?: string;
logoText?: string;
logoWidth?: number;
logoHeight?: number;
columns: FooterColumn[];
copyrightText?: string;
onPrivacyClick?: () => void;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
logoClassName?: string;
logoTextClassName?: string;
columnsClassName?: string;
columnClassName?: string;
columnTitleClassName?: string;
columnItemClassName?: string;
copyrightContainerClassName?: string;
copyrightTextClassName?: string;
privacyButtonClassName?: string;
}
const FooterBaseCard = memo<FooterBaseCardProps>(function FooterBaseCard({
logoSrc = "/brand/logowhite.svg",
logoText = "Webild",
logoWidth = 120,
logoHeight = 40,
columns,
copyrightText = `© 2025 | Webild`,
onPrivacyClick,
ariaLabel = "Site footer",
className = "",
containerClassName = "",
cardClassName = "",
logoClassName = "",
logoTextClassName = "",
columnsClassName = "",
columnClassName = "",
columnTitleClassName = "",
columnItemClassName = "",
copyrightContainerClassName = "",
copyrightTextClassName = "",
privacyButtonClassName = "",
}) {
return (
<footer
role="contentinfo"
aria-label={ariaLabel}
className={cls("relative w-full py-20", className)}
>
<div className={cls("relative w-content-width mx-auto card rounded-theme-capped p-10", containerClassName, cardClassName)}>
<div className="relative z-1 flex flex-col md:flex-row gap-10 md:gap-0 justify-between items-start mb-10">
{logoSrc ? (
<div className="flex-shrink-0">
<Image
src={logoSrc}
alt="Logo"
width={logoWidth}
height={logoHeight}
className={cls("object-contain", logoClassName)}
unoptimized={logoSrc.startsWith('http') || logoSrc.startsWith('//')}
aria-hidden={true}
/>
</div>
) : (
<h2 className={cls("text-4xl font-medium", logoTextClassName)}>
{logoText}
</h2>
)}
<FooterColumns
columns={columns}
className={columnsClassName}
columnClassName={columnClassName}
columnTitleClassName={columnTitleClassName}
columnItemClassName={columnItemClassName}
/>
</div>
<div
className={cls("relative z-1 w-full flex items-center justify-between pt-9 border-t border-foreground/20", copyrightContainerClassName)}
>
<span className={cls("text-foreground/50 text-sm", copyrightTextClassName)}>
{copyrightText}
</span>
<ButtonTextUnderline
text="Privacy Policy"
onClick={onPrivacyClick}
className={cls("text-foreground/50", privacyButtonClassName)}
/>
</div>
</div>
</footer>
);
});
FooterBaseCard.displayName = "FooterBaseCard";
export default FooterBaseCard;

View File

@@ -0,0 +1,126 @@
"use client";
import { memo, useRef, useEffect, useState } from "react";
import FooterBase from "./FooterBase";
import { cls } from "@/lib/utils";
interface FooterColumn {
title: string;
items: Array<{
label: string;
href?: string;
onClick?: () => void;
}>;
}
interface FooterBaseRevealProps {
logoSrc?: string;
logoWidth?: number;
logoHeight?: number;
columns: FooterColumn[];
copyrightText?: string;
onPrivacyClick?: () => void;
ariaLabel?: string;
className?: string;
wrapperClassName?: string;
containerClassName?: string;
footerClassName?: string;
footerContainerClassName?: string;
logoClassName?: string;
columnsClassName?: string;
columnClassName?: string;
columnTitleClassName?: string;
columnItemClassName?: string;
copyrightContainerClassName?: string;
copyrightTextClassName?: string;
privacyButtonClassName?: string;
}
const FooterBaseReveal = memo<FooterBaseRevealProps>(function FooterBaseReveal({
logoSrc,
logoWidth,
logoHeight,
columns,
copyrightText,
onPrivacyClick,
ariaLabel,
className = "",
wrapperClassName = "",
containerClassName = "",
footerClassName,
footerContainerClassName,
logoClassName,
columnsClassName,
columnClassName,
columnTitleClassName,
columnItemClassName,
copyrightContainerClassName,
copyrightTextClassName,
privacyButtonClassName,
}) {
const footerRef = useRef<HTMLDivElement>(null);
const [footerHeight, setFooterHeight] = useState<number>(0);
useEffect(() => {
const updateHeight = () => {
if (footerRef.current) {
const height = footerRef.current.offsetHeight;
setFooterHeight(height);
}
};
updateHeight();
const resizeObserver = new ResizeObserver(updateHeight);
const currentFooter = footerRef.current;
if (currentFooter) {
resizeObserver.observe(currentFooter);
}
return () => {
resizeObserver.disconnect();
};
}, []);
return (
<section
className={cls("relative z-0 w-full", className)}
style={{
height: footerHeight ? `${footerHeight}px` : "auto",
clipPath: "polygon(0% 0, 100% 0%, 100% 100%, 0 100%)",
}}
>
<div
className={cls("fixed bottom-0 w-full flex items-center justify-center overflow-hidden", wrapperClassName)}
style={{ height: footerHeight ? `${footerHeight}px` : "auto" }}
>
<div ref={footerRef} className={cls("w-full", containerClassName)}>
<FooterBase
logoSrc={logoSrc}
logoWidth={logoWidth}
logoHeight={logoHeight}
columns={columns}
copyrightText={copyrightText}
onPrivacyClick={onPrivacyClick}
ariaLabel={ariaLabel}
className={footerClassName}
containerClassName={footerContainerClassName}
logoClassName={logoClassName}
columnsClassName={columnsClassName}
columnClassName={columnClassName}
columnTitleClassName={columnTitleClassName}
columnItemClassName={columnItemClassName}
copyrightContainerClassName={copyrightContainerClassName}
copyrightTextClassName={copyrightTextClassName}
privacyButtonClassName={privacyButtonClassName}
/>
</div>
</div>
</section>
);
});
FooterBaseReveal.displayName = "FooterBaseReveal";
export default FooterBaseReveal;

View File

@@ -0,0 +1,133 @@
"use client";
import { memo } from "react";
import Image from "next/image";
import ButtonTextUnderline from "@/components/button/ButtonTextUnderline";
import FooterColumns from "@/components/shared/FooterColumns";
import SocialLinks from "@/components/shared/SocialLinks";
import { cls } from "@/lib/utils";
import type { FooterColumn } from "@/components/shared/FooterColumns";
import type { SocialLink } from "@/components/shared/SocialLinks";
interface FooterBaseSocialProps {
logoSrc?: string;
logoText?: string;
logoWidth?: number;
logoHeight?: number;
description: string;
columns: FooterColumn[];
socialLinks: SocialLink[];
copyrightText?: string;
onPrivacyClick?: () => void;
ariaLabel?: string;
className?: string;
containerClassName?: string;
logoClassName?: string;
logoTextClassName?: string;
descriptionClassName?: string;
columnsClassName?: string;
columnClassName?: string;
columnTitleClassName?: string;
columnItemClassName?: string;
socialLinksClassName?: string;
socialIconClassName?: string;
copyrightContainerClassName?: string;
copyrightTextClassName?: string;
privacyButtonClassName?: string;
}
const FooterBaseSocial = memo<FooterBaseSocialProps>(function FooterBaseSocial({
logoSrc = "/brand/logowhite.svg",
logoText = "Webild",
logoWidth = 120,
logoHeight = 40,
description,
columns,
socialLinks,
copyrightText = `© 2025 | Webild`,
onPrivacyClick,
ariaLabel = "Site footer",
className = "",
containerClassName = "",
logoClassName = "",
logoTextClassName = "",
descriptionClassName = "",
columnsClassName = "",
columnClassName = "",
columnTitleClassName = "",
columnItemClassName = "",
socialLinksClassName = "",
socialIconClassName = "",
copyrightContainerClassName = "",
copyrightTextClassName = "",
privacyButtonClassName = "",
}) {
return (
<footer
role="contentinfo"
aria-label={ariaLabel}
className={cls("relative overflow-hidden w-full primary-button text-background py-15", className)}
>
<div
className={cls("relative w-content-width mx-auto z-10", containerClassName)}
>
<div className="flex flex-col md:flex-row gap-10 md:gap-0 justify-between mb-10">
<div className="relative flex flex-col justify-between gap-4">
{logoSrc ? (
<div className="flex-shrink-0">
<Image
src={logoSrc}
alt="Logo"
width={logoWidth}
height={logoHeight}
className={cls("object-contain", logoClassName)}
unoptimized={logoSrc.startsWith('http') || logoSrc.startsWith('//')}
aria-hidden={true}
/>
</div>
) : (
<h2 className={cls("text-4xl font-medium text-background", logoTextClassName)}>
{logoText}
</h2>
)}
<p className={cls("text-background/50 text-base leading-tight text-balance md:max-w-[var(--width-20)]", descriptionClassName)}>
{description}
</p>
<SocialLinks
socialLinks={socialLinks}
className={cls("mt-auto", socialLinksClassName)}
iconClassName={socialIconClassName}
/>
</div>
<FooterColumns
columns={columns}
className={columnsClassName}
columnClassName={columnClassName}
columnTitleClassName={cls("text-background/50", columnTitleClassName)}
columnItemClassName={cls("text-background", columnItemClassName)}
/>
</div>
<div
className={cls("w-full flex items-center justify-between pt-9 border-t border-background/20", copyrightContainerClassName)}
>
<span className={cls("text-background/50 text-sm", copyrightTextClassName)}>
{copyrightText}
</span>
<ButtonTextUnderline
text="Privacy Policy"
onClick={onPrivacyClick}
className={cls("text-background/50", privacyButtonClassName)}
/>
</div>
</div>
</footer>
);
});
FooterBaseSocial.displayName = "FooterBaseSocial";
export default FooterBaseSocial;

View File

@@ -0,0 +1,84 @@
"use client";
import { memo } from "react";
import FooterLogo from "@/components/sections/footer/FooterLogo";
import SocialLinks from "@/components/shared/SocialLinks";
import { cls } from "@/lib/utils";
import type { SocialLink } from "@/components/shared/SocialLinks";
interface FooterCardProps {
// logoSrc?: string;
// logoAlt?: string;
logoText?: string;
copyrightText?: string;
socialLinks?: SocialLink[];
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
logoClassName?: string;
svgClassName?: string;
dividerClassName?: string;
copyrightContainerClassName?: string;
copyrightTextClassName?: string;
socialContainerClassName?: string;
socialIconClassName?: string;
}
const FooterCard = memo<FooterCardProps>(function FooterCard({
// logoSrc,
// logoAlt = "Logo",
logoText = "Webild",
copyrightText = `© 2025 | Webild`,
socialLinks,
ariaLabel = "Site footer",
className = "",
containerClassName = "",
cardClassName = "",
logoClassName = "",
svgClassName = "",
dividerClassName = "",
copyrightContainerClassName = "",
copyrightTextClassName = "",
socialContainerClassName = "",
socialIconClassName = "",
}) {
return (
<footer
role="contentinfo"
aria-label={ariaLabel}
className={cls("relative w-full py-20", className)}
>
<div className={cls("relative w-content-width mx-auto card rounded-theme-capped px-10", containerClassName, cardClassName)}>
<FooterLogo
// logoSrc={logoSrc}
// logoAlt={logoAlt}
logoText={logoText}
className={logoClassName}
svgClassName={svgClassName}
/>
<div className={cls("relative z-1 w-full h-px bg-accent/20 mb-6", dividerClassName)} />
<div
className={cls("relative z-1 w-full flex flex-col md:flex-row items-center justify-between gap-4 mb-6", copyrightContainerClassName)}
>
<span className={cls("text-accent/75 text-sm", copyrightTextClassName)}>
{copyrightText}
</span>
{socialLinks && socialLinks.length > 0 && (
<SocialLinks
socialLinks={socialLinks}
className={socialContainerClassName}
iconClassName={socialIconClassName}
/>
)}
</div>
</div>
</footer>
);
});
FooterCard.displayName = "FooterCard";
export default FooterCard;

View File

@@ -0,0 +1,44 @@
"use client";
import { memo } from "react";
// import Image from "next/image";
import SvgTextLogo from "@/components/shared/SvgTextLogo/SvgTextLogo";
import { cls } from "@/lib/utils";
interface FooterLogoProps {
// logoSrc?: string;
// logoAlt?: string;
logoText?: string;
className?: string;
svgClassName?: string;
}
const FooterLogo = memo<FooterLogoProps>(function FooterLogo({
// logoSrc,
// logoAlt = "Logo",
logoText = "Webild",
className = "",
svgClassName = ""
}) {
return (
<div className={cls("relative z-1 w-full", className)}>
{/* {logoSrc ? (
<Image
src={logoSrc}
alt={logoAlt}
width={1000}
height={1000}
className="w-full h-auto object-contain"
unoptimized={logoSrc.startsWith('http') || logoSrc.startsWith('//')}
aria-hidden={logoAlt === ""}
/>
) : ( */}
<SvgTextLogo logoText={logoText} className={svgClassName} />
{/* )} */}
</div>
);
});
FooterLogo.displayName = "FooterLogo";
export default FooterLogo;

View File

@@ -0,0 +1,125 @@
"use client";
import { memo } from "react";
import ButtonTextUnderline from "@/components/button/ButtonTextUnderline";
import { ChevronRight } from "lucide-react";
import FooterLogo from "./FooterLogo";
import { cls } from "@/lib/utils";
interface FooterColumn {
items: Array<{
label: string;
href?: string;
onClick?: () => void;
}>;
}
interface FooterLogoEmphasisProps {
// logoSrc?: string;
// logoAlt?: string;
columns: FooterColumn[];
logoText?: string;
ariaLabel?: string;
className?: string;
containerClassName?: string;
logoClassName?: string;
columnsClassName?: string;
columnClassName?: string;
itemClassName?: string;
iconClassName?: string;
buttonClassName?: string;
}
const FooterLogoEmphasis = memo<FooterLogoEmphasisProps>(
function FooterLogoEmphasis({
// logoSrc,
// logoAlt = "Logo",
columns,
logoText = "Webild",
ariaLabel = "Site footer",
className = "",
containerClassName = "",
logoClassName = "",
columnsClassName = "",
columnClassName = "",
itemClassName = "",
iconClassName = "",
buttonClassName = "",
}) {
const columnCount = columns.length;
const useFlex = columnCount <= 3;
const gridColsClass = columnCount === 4
? "grid-cols-2 md:grid-cols-4"
: "grid-cols-2 md:grid-cols-5";
return (
<footer
className={cls(
"w-full flex justify-center relative z-1 overflow-hidden primary-button text-background rounded-t-theme-capped",
"py-15",
className
)}
role="contentinfo"
aria-label={ariaLabel}
>
<div
className={cls(
"w-content-width mx-auto flex flex-col relative z-10",
"gap-10 md:gap-20",
containerClassName
)}
>
<FooterLogo
// logoSrc={logoSrc}
// logoAlt={logoAlt}
logoText={logoText}
className={logoClassName}
/>
<div
className={cls(
"w-full mb-10",
useFlex
? cls(
"flex flex-col md:flex-row gap-8 md:gap-[var(--width-10)]",
columnCount === 1 ? "md:justify-center" : "md:justify-between"
)
: cls("grid gap-[var(--width-10)] md:gap-[calc(var(--width-10)/2)]", gridColsClass),
columnsClassName
)}
>
{columns.map((column, index) => (
<div
key={`column-${index}`}
className={cls("flex items-start flex-col gap-4", columnClassName)}
>
{column.items.map((item) => (
<div
key={`${item.label}-${index}`}
className={cls("flex items-center gap-2 text-base", itemClassName)}
>
<ChevronRight
className={cls("h-[1em] w-auto", iconClassName)}
strokeWidth={3}
aria-hidden="true"
/>
<ButtonTextUnderline
text={item.label}
href={item.href}
onClick={item.onClick}
className={cls("font-medium text-base", buttonClassName)}
/>
</div>
))}
</div>
))}
</div>
</div>
</footer>
);
}
);
FooterLogoEmphasis.displayName = "FooterLogoEmphasis";
export default FooterLogoEmphasis;

View File

@@ -0,0 +1,91 @@
"use client";
import { memo, useRef, useEffect, useState } from "react";
import FooterLogo from "./FooterLogo";
import { cls } from "@/lib/utils";
interface FooterLogoRevealProps {
// logoSrc?: string;
// logoAlt?: string;
logoText?: string;
ariaLabel?: string;
className?: string;
wrapperClassName?: string;
containerClassName?: string;
logoClassName?: string;
svgClassName?: string;
}
const FooterLogoReveal = memo<FooterLogoRevealProps>(function FooterLogoReveal({
// logoSrc,
// logoAlt = "Logo",
logoText = "Webild",
ariaLabel = "Site footer",
className = "",
wrapperClassName = "",
containerClassName = "",
logoClassName = "",
svgClassName = "",
}) {
const footerRef = useRef<HTMLDivElement>(null);
const [footerHeight, setFooterHeight] = useState<number>(0);
useEffect(() => {
const updateHeight = () => {
if (footerRef.current) {
const height = footerRef.current.offsetHeight;
setFooterHeight(height);
}
};
updateHeight();
const resizeObserver = new ResizeObserver(updateHeight);
const currentFooter = footerRef.current;
if (currentFooter) {
resizeObserver.observe(currentFooter);
}
return () => {
resizeObserver.disconnect();
};
}, []);
return (
<section
aria-label={ariaLabel}
className={cls("relative z-0 w-full", className)}
style={{
height: footerHeight ? `${footerHeight}px` : "auto",
clipPath: "polygon(0% 0, 100% 0%, 100% 100%, 0 100%)",
}}
>
<div
className={cls("fixed bottom-0 w-full flex items-center justify-center overflow-hidden", wrapperClassName)}
style={{ height: footerHeight ? `${footerHeight}px` : "auto" }}
>
<div ref={footerRef} className={cls("w-full", containerClassName)}>
<footer
role="contentinfo"
className="relative w-full py-20 card"
>
<div className="w-content-width mx-auto flex flex-col relative z-10">
<FooterLogo
// logoSrc={logoSrc}
// logoAlt={logoAlt}
logoText={logoText}
className={logoClassName}
svgClassName={svgClassName}
/>
</div>
</footer>
</div>
</div>
</section>
);
});
FooterLogoReveal.displayName = "FooterLogoReveal";
export default FooterLogoReveal;

View File

@@ -0,0 +1,144 @@
"use client";
import { memo } from "react";
import Image from "next/image";
import ButtonTextUnderline from "@/components/button/ButtonTextUnderline";
import FooterColumns from "@/components/shared/FooterColumns";
import MediaContent from "@/components/shared/MediaContent";
import { cls } from "@/lib/utils";
import type { FooterColumn } from "@/components/shared/FooterColumns";
type MediaProps =
| {
imageSrc: string;
imageAlt?: string;
videoSrc?: never;
videoAriaLabel?: never;
}
| {
videoSrc: string;
videoAriaLabel?: string;
imageSrc?: never;
imageAlt?: never;
};
type FooterMediaProps = MediaProps & {
logoSrc?: string;
logoText?: string;
logoWidth?: number;
logoHeight?: number;
columns: FooterColumn[];
copyrightText?: string;
onPrivacyClick?: () => void;
ariaLabel?: string;
className?: string;
containerClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
logoClassName?: string;
logoTextClassName?: string;
columnsClassName?: string;
columnClassName?: string;
columnTitleClassName?: string;
columnItemClassName?: string;
copyrightContainerClassName?: string;
copyrightTextClassName?: string;
privacyButtonClassName?: string;
};
const FooterMedia = memo<FooterMediaProps>(function FooterMedia({
imageSrc,
videoSrc,
imageAlt = "",
videoAriaLabel = "Footer video",
logoSrc = "/brand/logowhite.svg",
logoText = "Webild",
logoWidth = 120,
logoHeight = 40,
columns,
copyrightText = `© 2025 | Webild`,
onPrivacyClick,
ariaLabel = "Site footer",
className = "",
containerClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
logoClassName = "",
logoTextClassName = "",
columnsClassName = "",
columnClassName = "",
columnTitleClassName = "",
columnItemClassName = "",
copyrightContainerClassName = "",
copyrightTextClassName = "",
privacyButtonClassName = "",
}) {
return (
<footer
role="contentinfo"
aria-label={ariaLabel}
className={cls("relative overflow-hidden w-full", className)}
>
<div className={cls("w-full aspect-square md:aspect-[16/6] mask-fade-top-long", mediaWrapperClassName)}>
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("w-full h-full object-cover rounded-none!", mediaClassName)}
/>
</div>
<div className="primary-button text-background py-15">
<div
className={cls("relative w-content-width mx-auto z-10", containerClassName)}
>
<div className="flex flex-col md:flex-row gap-10 md:gap-0 justify-between items-start mb-10">
{logoSrc ? (
<div className="flex-shrink-0">
<Image
src={logoSrc}
alt="Logo"
width={logoWidth}
height={logoHeight}
className={cls("object-contain", logoClassName)}
unoptimized={logoSrc.startsWith('http') || logoSrc.startsWith('//')}
aria-hidden={true}
/>
</div>
) : (
<h2 className={cls("text-4xl font-medium text-background", logoTextClassName)}>
{logoText}
</h2>
)}
<FooterColumns
columns={columns}
className={columnsClassName}
columnClassName={columnClassName}
columnTitleClassName={cls("text-background/50", columnTitleClassName)}
columnItemClassName={cls("text-background", columnItemClassName)}
/>
</div>
<div
className={cls("w-full flex items-center justify-between pt-9 border-t border-background/20", copyrightContainerClassName)}
>
<span className={cls("text-background/50 text-sm", copyrightTextClassName)}>
{copyrightText}
</span>
<ButtonTextUnderline
text="Privacy Policy"
onClick={onPrivacyClick}
className={cls("text-background/50", privacyButtonClassName)}
/>
</div>
</div>
</div>
</footer>
);
});
FooterMedia.displayName = "FooterMedia";
export default FooterMedia;

View File

@@ -0,0 +1,114 @@
"use client";
import { memo } from "react";
import Image from "next/image";
import SocialLinks from "@/components/shared/SocialLinks";
import FooterColumns from "@/components/shared/FooterColumns";
import { cls } from "@/lib/utils";
import type { SocialLink } from "@/components/shared/SocialLinks";
import type { FooterColumn } from "@/components/shared/FooterColumns";
interface FooterSocialProps {
logoSrc?: string;
logoText?: string;
logoWidth?: number;
logoHeight?: number;
columns: FooterColumn[];
socialLinks?: SocialLink[];
copyrightText?: string;
ariaLabel?: string;
className?: string;
containerClassName?: string;
logoContainerClassName?: string;
logoClassName?: string;
logoTextClassName?: string;
copyrightTextClassName?: string;
socialContainerClassName?: string;
socialIconClassName?: string;
columnsClassName?: string;
columnClassName?: string;
columnTitleClassName?: string;
columnItemClassName?: string;
}
const FooterSocial = memo<FooterSocialProps>(function FooterSocial({
logoSrc,
logoText = "Webild",
logoWidth = 120,
logoHeight = 40,
columns,
socialLinks,
copyrightText = "© Finerpoint, Inc. 2025",
ariaLabel = "Site footer",
className = "",
containerClassName = "",
logoContainerClassName = "",
logoClassName = "",
logoTextClassName = "",
copyrightTextClassName = "",
socialContainerClassName = "",
socialIconClassName = "",
columnsClassName = "",
columnClassName = "",
columnTitleClassName = "",
columnItemClassName = "",
}) {
return (
<footer
role="contentinfo"
aria-label={ariaLabel}
className={cls("relative overflow-hidden w-full text-foreground py-15", className)}
>
<div
className={cls("relative w-content-width mx-auto", containerClassName)}
>
<div className="flex flex-col md:flex-row gap-14 md:gap-20 justify-between">
<div className={cls("flex flex-col gap-15", logoContainerClassName)}>
<div className="flex flex-col gap-0">
{logoSrc ? (
<div className="flex-shrink-0">
<Image
src={logoSrc}
alt="Logo"
width={logoWidth}
height={logoHeight}
className={cls("object-contain", logoClassName)}
unoptimized={logoSrc.startsWith('http') || logoSrc.startsWith('//')}
aria-hidden={true}
/>
</div>
) : (
<h2 className={cls("text-4xl font-medium", logoTextClassName)}>
{logoText}
</h2>
)}
<p className={cls("text-sm text-accent/75", copyrightTextClassName)}>
{copyrightText}
</p>
</div>
{socialLinks && socialLinks.length > 0 && (
<SocialLinks
socialLinks={socialLinks}
className={socialContainerClassName}
iconClassName={socialIconClassName}
/>
)}
</div>
<FooterColumns
columns={columns}
className={columnsClassName}
columnClassName={columnClassName}
columnTitleClassName={columnTitleClassName}
columnItemClassName={columnItemClassName}
/>
</div>
</div>
</footer>
);
});
FooterSocial.displayName = "FooterSocial";
export default FooterSocial;

View File

@@ -0,0 +1,139 @@
"use client";
import { memo } from "react";
import Image from "next/image";
import FooterColumns from "@/components/shared/FooterColumns";
import AvatarGroup from "@/components/shared/AvatarGroup";
import { cls } from "@/lib/utils";
import type { FooterColumn } from "@/components/shared/FooterColumns";
import type { Avatar } from "@/components/shared/AvatarGroup";
import type { LucideIcon } from "lucide-react";
interface ContactItem {
icon: LucideIcon;
text: string;
}
interface FooterSplitProps {
logoSrc?: string;
logoAlt?: string;
logoText?: string;
logoWidth?: number;
logoHeight?: number;
columns: FooterColumn[];
title: string;
avatars?: Avatar[];
contactItems: ContactItem[];
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
contentClassName?: string;
columnsClassName?: string;
columnClassName?: string;
columnTitleClassName?: string;
columnItemClassName?: string;
titleClassName?: string;
avatarGroupClassName?: string;
logoClassName?: string;
logoTextClassName?: string;
contactClassName?: string;
contactItemClassName?: string;
contactIconClassName?: string;
}
const FooterSplit = memo<FooterSplitProps>(function FooterSplit({
logoSrc,
logoAlt = "Logo",
logoText = "Webild",
logoWidth = 120,
logoHeight = 40,
columns,
title,
avatars,
contactItems,
ariaLabel = "Site footer",
className = "",
containerClassName = "",
cardClassName = "",
contentClassName = "",
columnsClassName = "",
columnClassName = "",
columnTitleClassName = "",
columnItemClassName = "",
titleClassName = "",
avatarGroupClassName = "",
logoClassName = "",
logoTextClassName = "",
contactClassName = "",
contactItemClassName = "",
contactIconClassName = "",
}) {
return (
<footer
role="contentinfo"
aria-label={ariaLabel}
className={cls("relative w-full py-20", className)}
>
<div className={cls("relative w-content-width mx-auto card rounded-theme-capped flex flex-col gap-40 p-10", containerClassName, cardClassName)}>
<div className={cls("relative z-1 flex flex-col md:flex-row gap-10", contentClassName)}>
<FooterColumns
columns={columns}
className={columnsClassName}
columnClassName={columnClassName}
columnTitleClassName={columnTitleClassName}
columnItemClassName={columnItemClassName}
/>
<div className="flex-1 flex flex-col items-start md:items-end text-left md:text-right gap-4">
<h2 className={cls("text-4xl font-medium w-full md:w-1/2 text-balance", titleClassName)}>
{title}
</h2>
{avatars && avatars.length > 0 && (
<AvatarGroup
avatars={avatars}
className={avatarGroupClassName}
/>
)}
</div>
</div>
<div className="relative z-1 flex flex-col-reverse md:flex-row items-start md:items-end justify-between gap-6">
{logoSrc ? (
<div className="w-full flex-shrink-0">
<Image
src={logoSrc}
alt={logoAlt}
width={logoWidth}
height={logoHeight}
className={cls("object-contain w-full md:w-20", logoClassName)}
unoptimized={logoSrc.startsWith('http') || logoSrc.startsWith('//')}
aria-hidden={true}
/>
</div>
) : (
<h2 className={cls("w-full text-9xl font-medium truncate", logoTextClassName)}>
{logoText}
</h2>
)}
<div className={cls("w-full md:w-auto flex flex-col gap-2 text-accent text-base", contactClassName)}>
{contactItems.map((item, index) => {
const Icon = item.icon;
return (
<div key={index} className={cls("w-full md:w-auto flex items-center gap-2", contactItemClassName)}>
<Icon className={cls("h-[1em] w-auto", contactIconClassName)} />
<span className="whitespace-nowrap truncate" >{item.text}</span>
</div>
);
})}
</div>
</div>
</div>
</footer>
);
});
FooterSplit.displayName = "FooterSplit";
export default FooterSplit;

View File

@@ -0,0 +1,128 @@
"use client";
import React, { memo } from "react";
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 memo(HeroBillboard);

View File

@@ -0,0 +1,127 @@
"use client";
import React, { memo } from "react";
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 memo(HeroBillboardCarousel);

View File

@@ -0,0 +1,113 @@
"use client";
import React, { memo } from "react";
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 memo(HeroBillboardCarouselSplit);

View File

@@ -0,0 +1,167 @@
"use client";
import React, { memo } from "react";
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 memo(HeroBillboardGallery);

View File

@@ -0,0 +1,197 @@
"use client";
import React, { memo } from "react";
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 memo(HeroBillboardMetrics);

View File

@@ -0,0 +1,98 @@
"use client";
import { memo } from "react";
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 memo(HeroBillboardRotatedCarousel);

View File

@@ -0,0 +1,139 @@
"use client";
import React, { memo, 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 memo(HeroBillboardScroll);

View File

@@ -0,0 +1,137 @@
"use client";
import React, { memo } from "react";
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 memo(HeroBillboardSplit);

View File

@@ -0,0 +1,124 @@
"use client";
import React, { memo } from "react";
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 memo(HeroChatPrompt);

View File

@@ -0,0 +1,186 @@
"use client";
import React, { memo } from "react";
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 memo(HeroChatPromptFeatures);

View File

@@ -0,0 +1,92 @@
"use client";
import React, { memo } from "react";
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 memo(HeroGlobeOverlay);

View File

@@ -0,0 +1,132 @@
"use client";
import React, { memo } from "react";
import MediaContent from "@/components/shared/MediaContent";
import SvgTextLogo from "@/components/shared/SvgTextLogo/SvgTextLogo";
import TextAnimation from "@/components/text/TextAnimation";
import Button from "@/components/button/Button";
import { cls } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { ButtonConfig } from "@/types/button";
const MASK_GRADIENT = "linear-gradient(to bottom, transparent, black 60%)";
interface HeroLogoProps {
logoText: string;
description: string;
buttons: ButtonConfig[];
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
showDimOverlay?: boolean;
ariaLabel?: string;
className?: string;
containerClassName?: string;
contentContainerClassName?: string;
descriptionClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
logoContainerClassName?: string;
logoClassName?: string;
mediaWrapperClassName?: string;
imageClassName?: string;
blurClassName?: string;
dimOverlayClassName?: string;
}
const HeroLogo = ({
logoText,
description,
buttons,
imageSrc,
videoSrc,
imageAlt = "",
videoAriaLabel = "Hero video",
showDimOverlay = false,
ariaLabel = "Hero section",
className = "",
containerClassName = "",
contentContainerClassName = "",
descriptionClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
logoContainerClassName = "",
logoClassName = "",
mediaWrapperClassName = "",
imageClassName = "",
blurClassName = "",
dimOverlayClassName = "",
}: HeroLogoProps) => {
const theme = useTheme();
const [isMobile, setIsMobile] = React.useState(false);
React.useEffect(() => {
const checkMobile = () => setIsMobile(window.innerWidth < 768);
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, []);
const adjustHeightFactor = isMobile ? 1.1 : 0.8;
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"
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" >
<SvgTextLogo logoText={logoText} adjustHeightFactor={adjustHeightFactor} className={cls("text-background", logoClassName)} />
</div>
</div>
</div>
</section>
);
};
HeroLogo.displayName = "HeroLogo";
export default memo(HeroLogo);

View File

@@ -0,0 +1,129 @@
"use client";
import React, { memo } from "react";
import MediaContent from "@/components/shared/MediaContent";
import SvgTextLogo from "@/components/shared/SvgTextLogo/SvgTextLogo";
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";
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",
ariaLabel = "Hero section",
className = "",
containerClassName = "",
logoContainerClassName = "",
logoClassName = "",
descriptionClassName = "",
mediaWrapperClassName = "",
imageClassName = "",
browserBarClassName = "",
addressBarClassName = "",
}: HeroLogoBillboardProps) => {
const theme = useTheme();
const [isMobile, setIsMobile] = React.useState(false);
React.useEffect(() => {
const checkMobile = () => setIsMobile(window.innerWidth < 768);
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, []);
const adjustHeightFactor = isMobile ? 1.1 : 0.8;
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" >
<SvgTextLogo
logoText={logoText}
adjustHeightFactor={adjustHeightFactor}
className={cls("text-foreground", logoClassName)}
/>
</div>
<div className="relative w-full md:w-1/2" >
<TextAnimation
type={theme.defaultTextAnimation}
text={description}
variant="words-trigger"
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 memo(HeroLogoBillboard);

View File

@@ -0,0 +1,153 @@
"use client";
import React, { memo } from "react";
import MediaContent from "@/components/shared/MediaContent";
import SvgTextLogo from "@/components/shared/SvgTextLogo/SvgTextLogo";
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";
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",
ariaLabel = "Hero section",
className = "",
containerClassName = "",
logoContainerClassName = "",
descriptionClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
logoClassName = "",
mediaWrapperClassName = "",
imageClassName = "",
browserBarClassName = "",
addressBarClassName = "",
}: HeroLogoBillboardSplitProps) => {
const theme = useTheme();
const [isMobile, setIsMobile] = React.useState(false);
React.useEffect(() => {
const checkMobile = () => setIsMobile(window.innerWidth < 768);
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, []);
const adjustHeightFactor = isMobile ? 1.0 : 0.8;
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"
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" >
<SvgTextLogo
logoText={logoText}
adjustHeightFactor={adjustHeightFactor}
className={cls("text-foreground", logoClassName)}
/>
</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 memo(HeroLogoBillboardSplit);

View File

@@ -0,0 +1,174 @@
"use client";
import React, { memo } from "react";
import MediaContent from "@/components/shared/MediaContent";
import SvgTextLogo from "@/components/shared/SvgTextLogo/SvgTextLogo";
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 HeroLogoBillboardSplitImageProps {
logoText: string;
description: string;
buttons: ButtonConfig[];
layoutOrder: "default" | "reverse";
splitImageSrc?: string;
splitVideoSrc?: string;
splitImageAlt?: string;
splitVideoAriaLabel?: string;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
frameStyle?: "card" | "browser";
ariaLabel?: string;
className?: string;
containerClassName?: string;
logoContainerClassName?: string;
descriptionClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
logoClassName?: string;
splitMediaWrapperClassName?: string;
splitImageClassName?: string;
mediaWrapperClassName?: string;
imageClassName?: string;
browserBarClassName?: string;
addressBarClassName?: string;
}
const HeroLogoBillboardSplitImage = ({
logoText,
description,
buttons,
layoutOrder,
splitImageSrc,
splitVideoSrc,
splitImageAlt = "",
splitVideoAriaLabel = "Split section video",
imageSrc,
videoSrc,
imageAlt = "",
videoAriaLabel = "Hero video",
frameStyle = "card",
ariaLabel = "Hero section",
className = "",
containerClassName = "",
logoContainerClassName = "",
descriptionClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
logoClassName = "",
splitMediaWrapperClassName = "",
splitImageClassName = "",
mediaWrapperClassName = "",
imageClassName = "",
browserBarClassName = "",
addressBarClassName = "",
}: HeroLogoBillboardSplitImageProps) => {
const theme = useTheme();
const [isMobile, setIsMobile] = React.useState(false);
React.useEffect(() => {
const checkMobile = () => setIsMobile(window.innerWidth < 768);
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, []);
const adjustHeightFactor = isMobile ? 1.0 : 0.8;
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-12", containerClassName)}>
<div className={cls(
"w-full flex gap-6 md:gap-12",
layoutOrder === "default" ? "flex-col" : "flex-col-reverse",
logoContainerClassName
)}>
<div className="relative grid grid-cols-[1fr_auto] items-end gap-6 md:gap-10 w-full md:flex md:flex-row md:items-end md:justify-between">
<div className="relative flex flex-col gap-6 md:w-2/5">
<TextAnimation
type={theme.defaultTextAnimation}
text={description}
variant="words-trigger"
className={cls("text-lg md:text-3xl text-foreground/75 text-balance text-start leading-[1.2]", descriptionClassName)}
/>
<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={cls("relative h-full md:h-auto md:w-1/3 md:aspect-video overflow-hidden rounded-theme-capped card p-1 md:p-4", splitMediaWrapperClassName)}>
<MediaContent
imageSrc={splitImageSrc}
videoSrc={splitVideoSrc}
imageAlt={splitImageAlt}
videoAriaLabel={splitVideoAriaLabel}
imageClassName={cls("h-full w-full object-cover z-1", splitImageClassName)}
/>
</div>
</div>
<div className="relative w-full flex">
<SvgTextLogo
logoText={logoText}
adjustHeightFactor={adjustHeightFactor}
className={cls("text-foreground", logoClassName)}
/>
</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>
);
};
HeroLogoBillboardSplitImage.displayName = "HeroLogoBillboardSplitImage";
export default memo(HeroLogoBillboardSplitImage);

View File

@@ -0,0 +1,117 @@
"use client";
import React, { memo } from "react";
import MediaContent from "@/components/shared/MediaContent";
import SvgTextLogo from "@/components/shared/SvgTextLogo/SvgTextLogo";
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[];
ariaLabel?: string;
className?: string;
containerClassName?: string;
logoContainerClassName?: string;
logoClassName?: string;
descriptionClassName?: string;
carouselWrapperClassName?: string;
}
const HeroLogoCarousel = ({
logoText,
description,
mediaItems,
ariaLabel = "Hero section",
className = "",
containerClassName = "",
logoContainerClassName = "",
logoClassName = "",
descriptionClassName = "",
carouselWrapperClassName = "",
}: HeroLogoCarouselProps) => {
const theme = useTheme();
const [isMobile, setIsMobile] = React.useState(false);
React.useEffect(() => {
const checkMobile = () => setIsMobile(window.innerWidth < 768);
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, []);
const adjustHeightFactor = isMobile ? 1.1 : 0.8;
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" >
<SvgTextLogo
logoText={logoText}
adjustHeightFactor={adjustHeightFactor}
className={cls("text-foreground", logoClassName)}
/>
</div>
<div className="relative w-full md:w-1/2" >
<TextAnimation
type={theme.defaultTextAnimation}
text={description}
variant="words-trigger"
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 memo(HeroLogoCarousel);

View File

@@ -0,0 +1,143 @@
"use client";
import React, { memo } from "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";
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 memo(HeroOverlay);

View File

@@ -0,0 +1,214 @@
"use client";
import React, { memo } 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) => (
<React.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>
)}
</React.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 memo(HeroShowcaseSplitOverlay);

View File

@@ -0,0 +1,123 @@
"use client";
import React, { memo, useState, useEffect } from "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 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 [isCentered, setIsCentered] = useState(false);
useEffect(() => {
const handleResize = () => {
setIsCentered(window.innerWidth < 768);
};
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
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")}>
<TextBox
title={title}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
className={cls("flex flex-col gap-3 md: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={isCentered}
/>
</div>
{imagePosition === "right" && mediaContent}
</div>
</section>
);
};
HeroSplit.displayName = "HeroSplit";
export default memo(HeroSplit);

View File

@@ -0,0 +1,133 @@
"use client";
import React, { memo, useState, useEffect } from "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";
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 [isCentered, setIsCentered] = useState(false);
useEffect(() => {
const handleResize = () => {
setIsCentered(window.innerWidth < 768);
};
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
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")}>
<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: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={isCentered}
/>
</div>
{imagePosition === "right" && mediaContent}
</div>
</section>
);
};
HeroSplitAvatars.displayName = "HeroSplitAvatars";
export default memo(HeroSplitAvatars);

View File

@@ -0,0 +1,111 @@
"use client";
import React, { memo, useState, useEffect } from "react";
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 [isCentered, setIsCentered] = useState(false);
useEffect(() => {
const handleResize = () => {
setIsCentered(window.innerWidth < 768);
};
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
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")}>
<TextBox
title={title}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
className={cls("flex flex-col gap-3 md: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={isCentered}
/>
</div>
{imagePosition === "right" && globeContent}
</div>
</section>
);
};
HeroSplitGlobe.displayName = "HeroSplitGlobe";
export default memo(HeroSplitGlobe);

View File

@@ -0,0 +1,144 @@
"use client";
import React, { memo, useState, useEffect } from "react";
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 [isCentered, setIsCentered] = useState(false);
useEffect(() => {
const handleResize = () => {
setIsCentered(window.innerWidth < 768);
};
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
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")}>
<TextBox
title={title}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
className={cls("flex flex-col gap-3 md: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={isCentered}
/>
</div>
{globePosition === "right" && globeContent}
</div>
</section>
);
};
HeroSplitGlobeKpi.displayName = "HeroSplitGlobeKpi";
export default memo(HeroSplitGlobeKpi);

View File

@@ -0,0 +1,155 @@
"use client";
import React, { memo, useState, useEffect } from "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 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 [isCentered, setIsCentered] = useState(false);
useEffect(() => {
const handleResize = () => {
setIsCentered(window.innerWidth < 768);
};
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
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")}>
<TextBox
title={title}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
className={cls("flex flex-col gap-3 md: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={isCentered}
/>
</div>
{imagePosition === "right" && mediaContent}
</div>
</section>
);
};
HeroSplitKpi.displayName = "HeroSplitKpi";
export default memo(HeroSplitKpi);

View File

@@ -0,0 +1,112 @@
"use client";
import React, { memo, useState, useEffect } from "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 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) => {
const [isCentered, setIsCentered] = useState(false);
useEffect(() => {
const handleResize = () => {
setIsCentered(window.innerWidth < 768);
};
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
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">
<TextBox
title={title}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
className={cls("flex flex-col gap-3 md: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={isCentered}
/>
</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 memo(HeroSplitLarge);

View File

@@ -0,0 +1,179 @@
"use client";
import React, { memo, useCallback } from "react";
import MediaContent from "@/components/shared/MediaContent";
import SvgTextLogo from "@/components/shared/SvgTextLogo/SvgTextLogo";
import TextAnimation from "@/components/text/TextAnimation";
import Button from "@/components/button/Button";
import { cls } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import { useCarouselFullscreen } from "./useCarouselFullscreen";
import type { ButtonConfig } from "@/types/button";
const MASK_GRADIENT = "linear-gradient(to bottom, transparent, black 60%)";
interface CarouselSlide {
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
}
interface HeroCarouselLogoProps {
logoText: string;
description: string;
buttons: ButtonConfig[];
slides: CarouselSlide[];
autoplayDelay?: number;
showDimOverlay?: boolean;
ariaLabel?: string;
className?: string;
containerClassName?: string;
contentContainerClassName?: string;
descriptionClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
logoContainerClassName?: string;
logoClassName?: string;
mediaWrapperClassName?: string;
imageClassName?: string;
blurClassName?: string;
dimOverlayClassName?: string;
progressBarClassName?: string;
}
const HeroCarouselLogo = ({
logoText,
description,
buttons,
slides,
autoplayDelay = 3000,
showDimOverlay = false,
ariaLabel = "Hero section",
className = "",
containerClassName = "",
contentContainerClassName = "",
descriptionClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
logoContainerClassName = "",
logoClassName = "",
mediaWrapperClassName = "",
imageClassName = "",
blurClassName = "",
dimOverlayClassName = "",
progressBarClassName = "",
}: HeroCarouselLogoProps) => {
const theme = useTheme();
const [isMobile, setIsMobile] = React.useState(false);
const { currentSlide, progressRefs, goToSlide } = useCarouselFullscreen({
totalSlides: slides.length,
autoplayDelay,
});
const setProgressRef = useCallback(
(el: HTMLDivElement | null, index: number) => {
progressRefs.current[index] = el;
},
[progressRefs]
);
React.useEffect(() => {
const checkMobile = () => setIsMobile(window.innerWidth < 768);
checkMobile();
window.addEventListener("resize", checkMobile);
return () => window.removeEventListener("resize", checkMobile);
}, []);
const adjustHeightFactor = isMobile ? 1.1 : 0.8;
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">
<SvgTextLogo logoText={logoText} adjustHeightFactor={adjustHeightFactor} className={cls("text-background", logoClassName)} />
</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-full 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-full"
style={{ transform: "translateX(-100%)" }}
/>
</button>
))}
</div>
</div>
</div>
</section>
);
};
HeroCarouselLogo.displayName = "HeroCarouselLogo";
export default memo(HeroCarouselLogo);

View File

@@ -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,
};
};

View File

@@ -0,0 +1,141 @@
"use client";
import React, { memo } from "react";
import TextAnimation from "@/components/text/TextAnimation";
import MediaContent from "@/components/shared/MediaContent";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
type Metric = {
id: string;
value: string;
title: string;
};
interface MetricCardEightProps {
title: string;
imageSrc?: string;
imageAlt?: string;
videoSrc?: string;
videoAriaLabel?: string;
metrics: Metric[];
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
topSectionClassName?: string;
textWrapperClassName?: string;
titleClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
metricsContainerClassName?: string;
metricClassName?: string;
valueClassName?: string;
metricTitleClassName?: string;
}
const MetricCardEight = ({
title,
imageSrc,
imageAlt,
videoSrc,
videoAriaLabel,
metrics,
useInvertedBackground,
ariaLabel = "Metrics section",
className = "",
containerClassName = "",
topSectionClassName = "",
textWrapperClassName = "",
titleClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
metricsContainerClassName = "",
metricClassName = "",
valueClassName = "",
metricTitleClassName = "",
}: MetricCardEightProps) => {
const theme = useTheme();
const shouldUseLightText = useInvertedBackground === "invertDefault" || useInvertedBackground === "invertCard";
const shouldMetricUseLightText = shouldUseInvertedText("invertDefault", theme.cardStyle) || shouldUseInvertedText("invertCard", theme.cardStyle);
return (
<section
aria-label={ariaLabel}
className={cls(
"relative py-20",
useInvertedBackground === "invertCard"
? "w-content-width-expanded mx-auto rounded-theme-capped bg-foreground"
: "w-full",
useInvertedBackground === "invertDefault" && "bg-foreground",
className
)}
>
<div className={cls("w-content-width mx-auto flex flex-col gap-10", containerClassName)}>
<div className={cls("flex flex-col md:flex-row md:items-center md:justify-between gap-8", topSectionClassName)}>
<div className={cls("flex-1", textWrapperClassName)}>
<TextAnimation
type={theme.defaultTextAnimation}
text={title}
variant="words-trigger"
className={cls(
"text-3xl md:text-5xl font-medium leading-tight",
shouldUseLightText ? "text-background" : "text-foreground",
titleClassName
)}
/>
</div>
<div className={cls("relative w-full md:w-20 aspect-square rounded-theme-capped overflow-hidden flex-shrink-0", mediaWrapperClassName)}>
<MediaContent
imageSrc={imageSrc}
imageAlt={imageAlt}
videoSrc={videoSrc}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("w-full h-full object-cover", mediaClassName)}
/>
</div>
</div>
<div className={cls(
"grid gap-8",
metrics.length === 2 && "grid-cols-2",
metrics.length === 3 && "grid-cols-2 md:grid-cols-3",
metrics.length >= 4 && "grid-cols-2 md:grid-cols-4",
metricsContainerClassName
)}>
{metrics.map((metric) => (
<div
key={metric.id}
className={cls(
"flex flex-col gap-6 card rounded-theme-capped p-6",
metricClassName
)}
>
<div className="flex flex-col">
<h3 className={cls(
"text-5xl md:text-6xl font-medium leading-tight truncate",
shouldMetricUseLightText ? "text-background" : "text-foreground",
valueClassName
)}>
{metric.value}
</h3>
<p className={cls(
"text-base leading-tight truncate",
shouldMetricUseLightText ? "text-background/70" : "text-foreground/70",
metricTitleClassName
)}>
{metric.title}
</p>
</div>
</div>
))}
</div>
</div>
</section>
);
};
MetricCardEight.displayName = "MetricCardEight";
export default memo(MetricCardEight);

View File

@@ -0,0 +1,273 @@
"use client";
import React, { memo } from "react";
import CardStackTextBox from "@/components/cardStack/CardStackTextBox";
import MediaContent from "@/components/shared/MediaContent";
import { useCardAnimation } from "@/components/cardStack/hooks/useCardAnimation";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type MediaProps =
| {
imageSrc: string;
imageAlt?: string;
videoSrc?: never;
videoAriaLabel?: never;
}
| {
videoSrc: string;
videoAriaLabel?: string;
imageSrc?: never;
imageAlt?: never;
};
type Metric = MediaProps & {
id: string;
value: string;
title: string;
description: string;
};
interface MetricCardElevenProps {
metrics: Metric[];
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
gridClassName?: string;
cardClassName?: string;
valueClassName?: string;
cardTitleClassName?: string;
cardDescriptionClassName?: string;
mediaCardClassName?: string;
mediaClassName?: string;
}
interface MetricTextCardProps {
metric: Metric;
shouldUseLightText: boolean;
cardClassName?: string;
valueClassName?: string;
cardTitleClassName?: string;
cardDescriptionClassName?: string;
}
interface MetricMediaCardProps {
metric: Metric;
mediaCardClassName?: string;
mediaClassName?: string;
}
const MetricTextCard = memo(({
metric,
shouldUseLightText,
cardClassName = "",
valueClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
}: MetricTextCardProps) => {
return (
<div className={cls(
"relative w-full min-w-0 max-w-full h-full card text-foreground rounded-theme-capped flex flex-col justify-between p-6 md:p-8",
cardClassName
)}>
<h3 className={cls(
"text-5xl md:text-6xl font-medium leading-tight",
shouldUseLightText ? "text-background" : "text-foreground",
valueClassName
)}>
{metric.value}
</h3>
<div className="w-full min-w-0 flex flex-col gap-2 mt-auto">
<p className={cls(
"text-xl md:text-2xl font-medium leading-tight truncate",
shouldUseLightText ? "text-background" : "text-foreground",
cardTitleClassName
)}>
{metric.title}
</p>
<div className="w-full h-px bg-accent" />
<p className={cls(
"text-base truncate leading-tight",
shouldUseLightText ? "text-background/75" : "text-foreground/75",
cardDescriptionClassName
)}>
{metric.description}
</p>
</div>
</div>
);
});
MetricTextCard.displayName = "MetricTextCard";
const MetricMediaCard = memo(({
metric,
mediaCardClassName = "",
mediaClassName = "",
}: MetricMediaCardProps) => {
return (
<div className={cls(
"relative h-full rounded-theme-capped overflow-hidden",
mediaCardClassName
)}>
<MediaContent
imageSrc={metric.imageSrc}
videoSrc={metric.videoSrc}
imageAlt={metric.imageAlt}
videoAriaLabel={metric.videoAriaLabel}
imageClassName={cls("w-full h-full object-cover", mediaClassName)}
/>
</div>
);
});
MetricMediaCard.displayName = "MetricMediaCard";
const MetricCardEleven = ({
metrics,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
textboxLayout,
useInvertedBackground,
ariaLabel = "Metrics section",
className = "",
containerClassName = "",
textBoxClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
gridClassName = "",
cardClassName = "",
valueClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
mediaCardClassName = "",
mediaClassName = "",
}: MetricCardElevenProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
// Inner grid for each metric item (text + media side by side)
const innerGridCols = "grid-cols-2";
const { itemRefs } = useCardAnimation({ animationType, itemCount: metrics.length });
return (
<section
aria-label={ariaLabel}
className={cls(
"relative w-full py-20",
useInvertedBackground === "invertCard" && "w-content-width-expanded mx-auto rounded-theme-capped bg-foreground",
useInvertedBackground === "invertDefault" && "bg-foreground",
className
)}
>
<div className={cls("w-content-width mx-auto", containerClassName)}>
<CardStackTextBox
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
/>
<div className={cls(
"grid gap-4 mt-8 md:mt-12",
metrics.length === 1 ? "grid-cols-1" : "grid-cols-1 md:grid-cols-2",
gridClassName
)}>
{metrics.map((metric, index) => {
const isLastItem = index === metrics.length - 1;
const isOddTotal = metrics.length % 2 !== 0;
const isSingleItem = metrics.length === 1;
const shouldSpanFull = isSingleItem || (isLastItem && isOddTotal);
// On mobile, even items (2nd, 4th, 6th - index 1, 3, 5) have media first
const isEvenItem = (index + 1) % 2 === 0;
return (
<div
key={`${metric.id}-${index}`}
ref={(el) => { itemRefs.current[index] = el; }}
className={cls(
"grid gap-4",
innerGridCols,
shouldSpanFull && "md:col-span-2"
)}
>
<MetricTextCard
metric={metric}
shouldUseLightText={shouldUseLightText}
cardClassName={cls(
shouldSpanFull ? "aspect-square md:aspect-video" : "aspect-square",
isEvenItem && "order-2 md:order-1",
cardClassName
)}
valueClassName={valueClassName}
cardTitleClassName={cardTitleClassName}
cardDescriptionClassName={cardDescriptionClassName}
/>
<MetricMediaCard
metric={metric}
mediaCardClassName={cls(
shouldSpanFull ? "aspect-square md:aspect-video" : "aspect-square",
isEvenItem && "order-1 md:order-2",
mediaCardClassName
)}
mediaClassName={mediaClassName}
/>
</div>
);
})}
</div>
</div>
</section>
);
};
MetricCardEleven.displayName = "MetricCardEleven";
export default memo(MetricCardEleven);

View File

@@ -0,0 +1,140 @@
"use client";
import React, { memo } from "react";
import CardList from "@/components/cardStack/CardList";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
interface Metric {
id: string;
value: string;
description: string;
}
interface MetricCardFiveProps {
metrics: Metric[];
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxTitleClassName?: string;
textBoxDescriptionClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
cardContentClassName?: string;
metricValueClassName?: string;
metricDescriptionClassName?: string;
}
const MetricCardFive = ({
metrics,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
textboxLayout,
useInvertedBackground,
ariaLabel = "Metrics section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxDescriptionClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
cardContentClassName = "",
metricValueClassName = "",
metricDescriptionClassName = "",
}: MetricCardFiveProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<CardList
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
animationType={animationType}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
cardClassName={cardClassName}
titleClassName={textBoxTitleClassName}
descriptionClassName={textBoxDescriptionClassName}
textBoxClassName={textBoxClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
ariaLabel={ariaLabel}
>
{metrics.map((metric) => (
<div
key={metric.id}
className={cls(
"relative z-1 w-full min-h-0 h-full flex flex-col md:flex-row gap-3 md:gap-6 p-6 md:p-15",
cardContentClassName
)}
>
<div className="relative z-1 w-full md:w-1/2 flex md:justify-start">
<h2 className={cls(
"text-5xl md:text-9xl font-medium leading-[1.1]",
shouldUseLightText ? "text-background" : "text-foreground",
metricValueClassName
)}>
{metric.value}
</h2>
</div>
<div className="relative z-1 w-full h-px bg-foreground/20 md:hidden" />
<div className="relative z-1 w-full md:w-1/2 flex flex-col">
<p className={cls(
"text-xl md:text-2xl leading-tight text-balance",
shouldUseLightText ? "text-background" : "text-foreground",
metricDescriptionClassName
)}>
{metric.description}
</p>
</div>
</div>
))}
</CardList>
);
};
MetricCardFive.displayName = "MetricCardFive";
export default memo(MetricCardFive);

View File

@@ -0,0 +1,151 @@
"use client";
import React, { memo } from "react";
import TimelineHorizontalCardStack from "@/components/cardStack/layouts/timelines/TimelineHorizontalCardStack";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type Metric = {
id: string;
icon: LucideIcon;
title: string;
value: string;
imageAlt?: string;
videoAriaLabel?: string;
} & (
| { imageSrc: string; videoSrc?: never }
| { videoSrc: string; imageSrc?: never }
);
interface MetricCardFourProps {
metrics: Metric[];
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxTitleClassName?: string;
textBoxDescriptionClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
cardClassName?: string;
progressBarClassName?: string;
cardContentClassName?: string;
iconContainerClassName?: string;
iconClassName?: string;
metricTitleClassName?: string;
valueClassName?: string;
mediaContainerClassName?: string;
mediaClassName?: string;
}
const MetricCardFour = ({
metrics,
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
textboxLayout,
useInvertedBackground,
ariaLabel = "Metrics section",
className = "",
containerClassName = "",
textBoxTitleClassName = "",
textBoxDescriptionClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
cardClassName = "",
progressBarClassName = "",
cardContentClassName = "",
iconContainerClassName = "",
iconClassName = "",
metricTitleClassName = "",
valueClassName = "",
mediaContainerClassName = "",
mediaClassName = "",
}: MetricCardFourProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const mediaItems = metrics.map((metric) => ({
imageSrc: metric.imageSrc,
videoSrc: metric.videoSrc,
imageAlt: metric.imageAlt || metric.title,
videoAriaLabel: metric.videoAriaLabel || metric.title,
}));
return (
<TimelineHorizontalCardStack
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
mediaItems={mediaItems}
className={className}
containerClassName={containerClassName}
titleClassName={textBoxTitleClassName}
descriptionClassName={textBoxDescriptionClassName}
textBoxClassName={textBoxClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
cardClassName={cardClassName}
progressBarClassName={progressBarClassName}
mediaContainerClassName={mediaContainerClassName}
mediaClassName={mediaClassName}
ariaLabel={ariaLabel}
>
{metrics.map((metric, index) => (
<div
key={`${metric.id}-${index}`}
className={cls("w-full min-h-0 h-fit flex flex-col items-center justify-center gap-3 py-10", cardContentClassName)}
>
<div className="w-full flex items-center justify-center gap-2">
<div className={cls("h-8 primary-button aspect-square rounded-theme flex items-center justify-center", iconContainerClassName)}>
<metric.icon className={cls("h-4/10 text-background", iconClassName)} strokeWidth={1.5} />
</div>
<h3 className={cls("text-xl truncate", shouldUseLightText ? "text-background" : "text-foreground", metricTitleClassName)}>
{metric.title}
</h3>
</div>
<div className="w-full flex items-center justify-center">
<h4 className={cls("text-7xl font-medium truncate", shouldUseLightText ? "text-background" : "text-foreground", valueClassName)}>
{metric.value}
</h4>
</div>
</div>
))}
</TimelineHorizontalCardStack>
);
};
MetricCardFour.displayName = "MetricCardFour";
export default memo(MetricCardFour);

View File

@@ -0,0 +1,129 @@
"use client";
import React, { memo } from "react";
import TextAnimation from "@/components/text/TextAnimation";
import Tag from "@/components/shared/Tag";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
type MetricItem = {
id: string;
value: string;
description: string;
};
interface MetricCardFourteenProps {
title: string;
tag: string;
metrics: MetricItem[];
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
titleClassName?: string;
tagClassName?: string;
metricsContainerClassName?: string;
metricClassName?: string;
valueClassName?: string;
descriptionClassName?: string;
}
const MetricCardFourteen = ({
title,
tag,
metrics,
useInvertedBackground,
ariaLabel = "Metrics section",
className = "",
containerClassName = "",
titleClassName = "",
tagClassName = "",
metricsContainerClassName = "",
metricClassName = "",
valueClassName = "",
descriptionClassName = "",
}: MetricCardFourteenProps) => {
const theme = useTheme();
const shouldUseLightText = useInvertedBackground === "invertDefault" || useInvertedBackground === "invertCard";
const shouldMetricUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<section
aria-label={ariaLabel}
className={cls(
"relative py-20",
useInvertedBackground === "invertCard"
? "w-content-width-expanded mx-auto rounded-theme-capped bg-foreground"
: "w-full",
useInvertedBackground === "invertDefault" && "bg-foreground",
className
)}
>
<div className={cls("w-content-width mx-auto flex flex-col gap-10", containerClassName)}>
<TextAnimation
type={theme.defaultTextAnimation}
text={title}
variant="words-trigger"
className={cls(
"text-3xl md:text-5xl font-medium leading-tight",
shouldUseLightText ? "text-background" : "text-foreground",
titleClassName
)}
/>
<div className="relative w-full h-px bg-accent/20" />
<div className="flex flex-col md:flex-row md:items-start gap-10 md:gap-8">
<div className="w-fit md:w-15">
<Tag
text={tag}
useInvertedBackground={useInvertedBackground}
className={cls("min-w-0 max-w-full text-xl px-6", tagClassName)}
textClassName="truncate"
/>
</div>
<div className={cls(
"w-full grid gap-4 flex-1",
metrics.length === 1 && "grid-cols-1",
metrics.length >= 2 && "grid-cols-2",
metricsContainerClassName
)}>
{metrics.map((metric) => (
<div
key={metric.id}
className={cls(
"card rounded-theme-capped p-6 md:p-8 flex flex-col justify-between aspect-video",
metricClassName
)}
>
<p className={cls(
"text-6xl md:text-8xl font-medium",
shouldMetricUseLightText ? "text-background" : "text-foreground",
valueClassName
)}>
{metric.value}
</p>
<div className="flex flex-col gap-4">
<div className="w-full h-px bg-accent/20" />
<p className={cls(
"text-base md:text-lg leading-tight text-balance",
shouldMetricUseLightText ? "text-background" : "text-foreground",
descriptionClassName
)}>
{metric.description}
</p>
</div>
</div>
))}
</div>
</div>
</div>
</section>
);
};
MetricCardFourteen.displayName = "MetricCardFourteen";
export default memo(MetricCardFourteen);

View File

@@ -0,0 +1,197 @@
"use client";
import React, { memo } from "react";
import CardStack from "@/components/cardStack/CardStack";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, GridVariant, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type Metric = {
id: string;
value: string;
title: string;
description: string;
icon: LucideIcon;
};
interface MetricCardOneProps {
metrics: Metric[];
carouselMode?: "auto" | "buttons";
gridVariant: GridVariant;
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
valueClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
iconContainerClassName?: string;
iconClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
}
interface MetricCardItemProps {
metric: Metric;
shouldUseLightText: boolean;
cardClassName?: string;
valueClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
iconContainerClassName?: string;
iconClassName?: string;
}
const MetricCardItem = memo(({
metric,
shouldUseLightText,
cardClassName = "",
valueClassName = "",
titleClassName = "",
descriptionClassName = "",
iconContainerClassName = "",
iconClassName = "",
}: MetricCardItemProps) => {
return (
<div className={cls("relative w-full min-w-0 h-full card text-foreground rounded-theme-capped p-6 flex flex-col items-center justify-center gap-0", cardClassName)}>
<h2
className={cls("relative z-1 w-full text-9xl font-foreground font-medium leading-[1.1] truncate text-center", valueClassName)}
style={{
backgroundImage: shouldUseLightText
? `linear-gradient(to bottom, var(--color-background) 0%, var(--color-background) 20%, transparent 72%, transparent 80%, transparent 100%)`
: `linear-gradient(to bottom, var(--color-foreground) 0%, var(--color-foreground) 20%, transparent 72%, transparent 80%, transparent 100%)`,
WebkitBackgroundClip: "text",
backgroundClip: "text",
WebkitTextFillColor: "transparent",
color: "transparent",
}}
>
{metric.value} dakdakdmakdmakedmae
</h2>
<p className={cls("relative w-full z-1 mt-[calc(var(--text-4xl)*-0.75)] md:mt-[calc(var(--text-4xl)*-1.15)] text-4xl font-medium text-center truncate", shouldUseLightText ? "text-background" : "text-foreground", titleClassName)}>
{metric.title}
</p>
<p className={cls("relative line-clamp-2 z-1 max-w-9/10 md:max-w-7/10 text-base text-center leading-[1.1] mt-2", shouldUseLightText ? "text-background" : "text-foreground", descriptionClassName)}>
{metric.description}
</p>
<div className={cls("absolute z-1 left-6 bottom-6 h-10 aspect-square primary-button rounded-theme flex items-center justify-center", iconContainerClassName)}>
<metric.icon className={cls("h-4/10 text-background", iconClassName)} />
</div>
</div>
);
});
MetricCardItem.displayName = "MetricCardItem";
const MetricCardOne = ({
metrics,
carouselMode = "buttons",
gridVariant,
uniformGridCustomHeightClasses,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
textboxLayout,
useInvertedBackground,
ariaLabel = "Metrics section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
valueClassName = "",
titleClassName = "",
descriptionClassName = "",
iconContainerClassName = "",
iconClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: MetricCardOneProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<CardStack
useInvertedBackground={useInvertedBackground}
mode={carouselMode}
gridVariant={gridVariant}
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{metrics.map((metric, index) => (
<MetricCardItem
key={`${metric.id}-${index}`}
metric={metric}
shouldUseLightText={shouldUseLightText}
cardClassName={cardClassName}
valueClassName={valueClassName}
titleClassName={titleClassName}
descriptionClassName={descriptionClassName}
iconContainerClassName={iconContainerClassName}
iconClassName={iconClassName}
/>
))}
</CardStack>
);
};
MetricCardOne.displayName = "MetricCardOne";
export default memo(MetricCardOne);

Some files were not shown because too many files have changed in this diff Show More