Initial commit
This commit is contained in:
45
src/components/sections/AnimationContainer.tsx
Normal file
45
src/components/sections/AnimationContainer.tsx
Normal 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;
|
||||
102
src/components/sections/about/AboutMetric.tsx
Normal file
102
src/components/sections/about/AboutMetric.tsx
Normal 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);
|
||||
214
src/components/sections/about/AboutPhoneTimeline.tsx
Normal file
214
src/components/sections/about/AboutPhoneTimeline.tsx
Normal 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);
|
||||
130
src/components/sections/about/InlineImageSplitTextAbout.tsx
Normal file
130
src/components/sections/about/InlineImageSplitTextAbout.tsx
Normal 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);
|
||||
98
src/components/sections/about/MediaAbout.tsx
Normal file
98
src/components/sections/about/MediaAbout.tsx
Normal 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);
|
||||
125
src/components/sections/about/MediaSplitAbout.tsx
Normal file
125
src/components/sections/about/MediaSplitAbout.tsx
Normal 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);
|
||||
163
src/components/sections/about/MediaSplitTabsAbout.tsx
Normal file
163
src/components/sections/about/MediaSplitTabsAbout.tsx
Normal 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);
|
||||
95
src/components/sections/about/ParallaxAbout.tsx
Normal file
95
src/components/sections/about/ParallaxAbout.tsx
Normal 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);
|
||||
178
src/components/sections/about/SplitAbout.tsx
Normal file
178
src/components/sections/about/SplitAbout.tsx
Normal 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);
|
||||
129
src/components/sections/about/SplitAboutCards.tsx
Normal file
129
src/components/sections/about/SplitAboutCards.tsx
Normal 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);
|
||||
102
src/components/sections/about/SplitAboutMetric.tsx
Normal file
102
src/components/sections/about/SplitAboutMetric.tsx
Normal 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);
|
||||
54
src/components/sections/about/TagAbout.tsx
Normal file
54
src/components/sections/about/TagAbout.tsx
Normal 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);
|
||||
211
src/components/sections/about/TagMediaSplitAbout.tsx
Normal file
211
src/components/sections/about/TagMediaSplitAbout.tsx
Normal 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);
|
||||
135
src/components/sections/about/TestimonialAboutCard.tsx
Normal file
135
src/components/sections/about/TestimonialAboutCard.tsx
Normal 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);
|
||||
65
src/components/sections/about/TextAbout.tsx
Normal file
65
src/components/sections/about/TextAbout.tsx
Normal 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);
|
||||
88
src/components/sections/about/TextSplitAbout.tsx
Normal file
88
src/components/sections/about/TextSplitAbout.tsx
Normal 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);
|
||||
211
src/components/sections/blog/BlogCardEight.tsx
Normal file
211
src/components/sections/blog/BlogCardEight.tsx
Normal 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);
|
||||
195
src/components/sections/blog/BlogCardEleven.tsx
Normal file
195
src/components/sections/blog/BlogCardEleven.tsx
Normal 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);
|
||||
169
src/components/sections/blog/BlogCardFive.tsx
Normal file
169
src/components/sections/blog/BlogCardFive.tsx
Normal 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);
|
||||
218
src/components/sections/blog/BlogCardNine.tsx
Normal file
218
src/components/sections/blog/BlogCardNine.tsx
Normal 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);
|
||||
247
src/components/sections/blog/BlogCardOne.tsx
Normal file
247
src/components/sections/blog/BlogCardOne.tsx
Normal 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);
|
||||
205
src/components/sections/blog/BlogCardSeven.tsx
Normal file
205
src/components/sections/blog/BlogCardSeven.tsx
Normal 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);
|
||||
235
src/components/sections/blog/BlogCardSix.tsx
Normal file
235
src/components/sections/blog/BlogCardSix.tsx
Normal 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);
|
||||
166
src/components/sections/blog/BlogCardTen.tsx
Normal file
166
src/components/sections/blog/BlogCardTen.tsx
Normal 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);
|
||||
228
src/components/sections/blog/BlogCardThree.tsx
Normal file
228
src/components/sections/blog/BlogCardThree.tsx
Normal 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);
|
||||
225
src/components/sections/blog/BlogCardTwo.tsx
Normal file
225
src/components/sections/blog/BlogCardTwo.tsx
Normal 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);
|
||||
92
src/components/sections/contact/ContactCenter.tsx
Normal file
92
src/components/sections/contact/ContactCenter.tsx
Normal 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);
|
||||
158
src/components/sections/contact/ContactCenterForm.tsx
Normal file
158
src/components/sections/contact/ContactCenterForm.tsx
Normal 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);
|
||||
195
src/components/sections/contact/ContactFaq.tsx
Normal file
195
src/components/sections/contact/ContactFaq.tsx
Normal 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);
|
||||
97
src/components/sections/contact/ContactInline.tsx
Normal file
97
src/components/sections/contact/ContactInline.tsx
Normal 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);
|
||||
128
src/components/sections/contact/ContactSplit.tsx
Normal file
128
src/components/sections/contact/ContactSplit.tsx
Normal 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);
|
||||
203
src/components/sections/contact/ContactSplitForm.tsx
Normal file
203
src/components/sections/contact/ContactSplitForm.tsx
Normal 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);
|
||||
94
src/components/sections/contact/ContactText.tsx
Normal file
94
src/components/sections/contact/ContactText.tsx
Normal 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);
|
||||
144
src/components/sections/faq/FaqBase.tsx
Normal file
144
src/components/sections/faq/FaqBase.tsx
Normal 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);
|
||||
169
src/components/sections/faq/FaqDouble.tsx
Normal file
169
src/components/sections/faq/FaqDouble.tsx
Normal 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);
|
||||
187
src/components/sections/faq/FaqSplitMedia.tsx
Normal file
187
src/components/sections/faq/FaqSplitMedia.tsx
Normal 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);
|
||||
152
src/components/sections/faq/FaqSplitText.tsx
Normal file
152
src/components/sections/faq/FaqSplitText.tsx
Normal 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);
|
||||
152
src/components/sections/feature/FeatureCardEight.tsx
Normal file
152
src/components/sections/feature/FeatureCardEight.tsx
Normal 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);
|
||||
187
src/components/sections/feature/FeatureCardEighteen.tsx
Normal file
187
src/components/sections/feature/FeatureCardEighteen.tsx
Normal 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);
|
||||
166
src/components/sections/feature/FeatureCardEleven.tsx
Normal file
166
src/components/sections/feature/FeatureCardEleven.tsx
Normal 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);
|
||||
179
src/components/sections/feature/FeatureCardFifteen.tsx
Normal file
179
src/components/sections/feature/FeatureCardFifteen.tsx
Normal 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);
|
||||
172
src/components/sections/feature/FeatureCardFourteen.tsx
Normal file
172
src/components/sections/feature/FeatureCardFourteen.tsx
Normal 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);
|
||||
200
src/components/sections/feature/FeatureCardNine.tsx
Normal file
200
src/components/sections/feature/FeatureCardNine.tsx
Normal 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);
|
||||
194
src/components/sections/feature/FeatureCardNineteen.tsx
Normal file
194
src/components/sections/feature/FeatureCardNineteen.tsx
Normal 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);
|
||||
173
src/components/sections/feature/FeatureCardOne.tsx
Normal file
173
src/components/sections/feature/FeatureCardOne.tsx
Normal 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);
|
||||
160
src/components/sections/feature/FeatureCardSeven.tsx
Normal file
160
src/components/sections/feature/FeatureCardSeven.tsx
Normal 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);
|
||||
154
src/components/sections/feature/FeatureCardSix.tsx
Normal file
154
src/components/sections/feature/FeatureCardSix.tsx
Normal 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);
|
||||
169
src/components/sections/feature/FeatureCardSixteen.tsx
Normal file
169
src/components/sections/feature/FeatureCardSixteen.tsx
Normal 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);
|
||||
257
src/components/sections/feature/FeatureCardTen.tsx
Normal file
257
src/components/sections/feature/FeatureCardTen.tsx
Normal 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);
|
||||
232
src/components/sections/feature/FeatureCardThirteen.tsx
Normal file
232
src/components/sections/feature/FeatureCardThirteen.tsx
Normal 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);
|
||||
177
src/components/sections/feature/FeatureCardTwelve.tsx
Normal file
177
src/components/sections/feature/FeatureCardTwelve.tsx
Normal 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);
|
||||
178
src/components/sections/feature/FeatureCardTwenty.tsx
Normal file
178
src/components/sections/feature/FeatureCardTwenty.tsx
Normal 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);
|
||||
215
src/components/sections/feature/FeatureCardTwentyOne.tsx
Normal file
215
src/components/sections/feature/FeatureCardTwentyOne.tsx
Normal 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);
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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])
|
||||
}
|
||||
109
src/components/sections/footer/FooterBase.tsx
Normal file
109
src/components/sections/footer/FooterBase.tsx
Normal 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;
|
||||
109
src/components/sections/footer/FooterBaseCard.tsx
Normal file
109
src/components/sections/footer/FooterBaseCard.tsx
Normal 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;
|
||||
126
src/components/sections/footer/FooterBaseReveal.tsx
Normal file
126
src/components/sections/footer/FooterBaseReveal.tsx
Normal 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;
|
||||
133
src/components/sections/footer/FooterBaseSocial.tsx
Normal file
133
src/components/sections/footer/FooterBaseSocial.tsx
Normal 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;
|
||||
84
src/components/sections/footer/FooterCard.tsx
Normal file
84
src/components/sections/footer/FooterCard.tsx
Normal 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;
|
||||
44
src/components/sections/footer/FooterLogo.tsx
Normal file
44
src/components/sections/footer/FooterLogo.tsx
Normal 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;
|
||||
125
src/components/sections/footer/FooterLogoEmphasis.tsx
Normal file
125
src/components/sections/footer/FooterLogoEmphasis.tsx
Normal 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;
|
||||
91
src/components/sections/footer/FooterLogoReveal.tsx
Normal file
91
src/components/sections/footer/FooterLogoReveal.tsx
Normal 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;
|
||||
144
src/components/sections/footer/FooterMedia.tsx
Normal file
144
src/components/sections/footer/FooterMedia.tsx
Normal 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;
|
||||
114
src/components/sections/footer/FooterSocial.tsx
Normal file
114
src/components/sections/footer/FooterSocial.tsx
Normal 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;
|
||||
139
src/components/sections/footer/FooterSplit.tsx
Normal file
139
src/components/sections/footer/FooterSplit.tsx
Normal 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;
|
||||
128
src/components/sections/hero/HeroBillboard.tsx
Normal file
128
src/components/sections/hero/HeroBillboard.tsx
Normal 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);
|
||||
127
src/components/sections/hero/HeroBillboardCarousel.tsx
Normal file
127
src/components/sections/hero/HeroBillboardCarousel.tsx
Normal 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);
|
||||
113
src/components/sections/hero/HeroBillboardCarouselSplit.tsx
Normal file
113
src/components/sections/hero/HeroBillboardCarouselSplit.tsx
Normal 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);
|
||||
167
src/components/sections/hero/HeroBillboardGallery.tsx
Normal file
167
src/components/sections/hero/HeroBillboardGallery.tsx
Normal 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);
|
||||
197
src/components/sections/hero/HeroBillboardMetrics.tsx
Normal file
197
src/components/sections/hero/HeroBillboardMetrics.tsx
Normal 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);
|
||||
@@ -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);
|
||||
139
src/components/sections/hero/HeroBillboardScroll.tsx
Normal file
139
src/components/sections/hero/HeroBillboardScroll.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
"use client";
|
||||
|
||||
import 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);
|
||||
137
src/components/sections/hero/HeroBillboardSplit.tsx
Normal file
137
src/components/sections/hero/HeroBillboardSplit.tsx
Normal 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);
|
||||
124
src/components/sections/hero/HeroChatPrompt.tsx
Normal file
124
src/components/sections/hero/HeroChatPrompt.tsx
Normal 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);
|
||||
186
src/components/sections/hero/HeroChatPromptFeatures.tsx
Normal file
186
src/components/sections/hero/HeroChatPromptFeatures.tsx
Normal 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);
|
||||
92
src/components/sections/hero/HeroGlobeOverlay.tsx
Normal file
92
src/components/sections/hero/HeroGlobeOverlay.tsx
Normal 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);
|
||||
132
src/components/sections/hero/HeroLogo.tsx
Normal file
132
src/components/sections/hero/HeroLogo.tsx
Normal 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);
|
||||
129
src/components/sections/hero/HeroLogoBillboard.tsx
Normal file
129
src/components/sections/hero/HeroLogoBillboard.tsx
Normal 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);
|
||||
153
src/components/sections/hero/HeroLogoBillboardSplit.tsx
Normal file
153
src/components/sections/hero/HeroLogoBillboardSplit.tsx
Normal 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);
|
||||
174
src/components/sections/hero/HeroLogoBillboardSplitImage.tsx
Normal file
174
src/components/sections/hero/HeroLogoBillboardSplitImage.tsx
Normal 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);
|
||||
117
src/components/sections/hero/HeroLogoCarousel.tsx
Normal file
117
src/components/sections/hero/HeroLogoCarousel.tsx
Normal 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);
|
||||
143
src/components/sections/hero/HeroOverlay.tsx
Normal file
143
src/components/sections/hero/HeroOverlay.tsx
Normal 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);
|
||||
214
src/components/sections/hero/HeroShowcaseSplitOverlay.tsx
Normal file
214
src/components/sections/hero/HeroShowcaseSplitOverlay.tsx
Normal file
@@ -0,0 +1,214 @@
|
||||
"use client";
|
||||
|
||||
import 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);
|
||||
123
src/components/sections/hero/HeroSplit.tsx
Normal file
123
src/components/sections/hero/HeroSplit.tsx
Normal 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);
|
||||
133
src/components/sections/hero/HeroSplitAvatars.tsx
Normal file
133
src/components/sections/hero/HeroSplitAvatars.tsx
Normal 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);
|
||||
111
src/components/sections/hero/HeroSplitGlobe.tsx
Normal file
111
src/components/sections/hero/HeroSplitGlobe.tsx
Normal 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);
|
||||
144
src/components/sections/hero/HeroSplitGlobeKpi.tsx
Normal file
144
src/components/sections/hero/HeroSplitGlobeKpi.tsx
Normal 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);
|
||||
155
src/components/sections/hero/HeroSplitKpi.tsx
Normal file
155
src/components/sections/hero/HeroSplitKpi.tsx
Normal 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);
|
||||
112
src/components/sections/hero/HeroSplitLarge.tsx
Normal file
112
src/components/sections/hero/HeroSplitLarge.tsx
Normal 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);
|
||||
@@ -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);
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
141
src/components/sections/metrics/MetricCardEight.tsx
Normal file
141
src/components/sections/metrics/MetricCardEight.tsx
Normal 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);
|
||||
273
src/components/sections/metrics/MetricCardEleven.tsx
Normal file
273
src/components/sections/metrics/MetricCardEleven.tsx
Normal 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);
|
||||
140
src/components/sections/metrics/MetricCardFive.tsx
Normal file
140
src/components/sections/metrics/MetricCardFive.tsx
Normal 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);
|
||||
151
src/components/sections/metrics/MetricCardFour.tsx
Normal file
151
src/components/sections/metrics/MetricCardFour.tsx
Normal 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);
|
||||
129
src/components/sections/metrics/MetricCardFourteen.tsx
Normal file
129
src/components/sections/metrics/MetricCardFourteen.tsx
Normal 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);
|
||||
197
src/components/sections/metrics/MetricCardOne.tsx
Normal file
197
src/components/sections/metrics/MetricCardOne.tsx
Normal 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
Reference in New Issue
Block a user