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;
|
||||
101
src/components/sections/about/AboutMetric.tsx
Normal file
101
src/components/sections/about/AboutMetric.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
"use client";
|
||||
|
||||
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";
|
||||
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 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" && "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 AboutMetric;
|
||||
121
src/components/sections/about/InlineImageSplitTextAbout.tsx
Normal file
121
src/components/sections/about/InlineImageSplitTextAbout.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
"use client";
|
||||
|
||||
import { Fragment } 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 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" && "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 (
|
||||
<Fragment key={index}>
|
||||
{index > 0 && " "}
|
||||
{element}
|
||||
</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 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 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";
|
||||
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 w-full", useInvertedBackground === "invertDefault" && "bg-foreground", className)}
|
||||
>
|
||||
<div className={cls("relative w-content-width mx-auto aspect-auto min-h-70 md:aspect-video md:min-h-0 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 inset-0 z-0 bg-background/40 backdrop-blur-xs pointer-events-none select-none rounded-theme-capped" />
|
||||
<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 p-5 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 MediaAbout;
|
||||
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 { 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";
|
||||
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 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 MediaSplitTabsAbout;
|
||||
158
src/components/sections/about/MetricSplitMediaAbout.tsx
Normal file
158
src/components/sections/about/MetricSplitMediaAbout.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import TextBox from "@/components/Textbox";
|
||||
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 { InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||
|
||||
interface Metric {
|
||||
value: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface MetricSplitMediaAboutProps {
|
||||
title: string;
|
||||
description: string;
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
metrics: Metric[];
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
useInvertedBackground: InvertedBackground;
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
gridClassName?: string;
|
||||
leftColumnClassName?: string;
|
||||
rightColumnClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
titleClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
tagClassName?: string;
|
||||
metricsContainerClassName?: string;
|
||||
metricCardClassName?: string;
|
||||
metricValueClassName?: string;
|
||||
metricTitleClassName?: string;
|
||||
mediaWrapperClassName?: string;
|
||||
imageClassName?: string;
|
||||
}
|
||||
|
||||
const MetricSplitMediaAbout = ({
|
||||
title,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
metrics,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
imageAlt = "",
|
||||
videoAriaLabel = "About section video",
|
||||
useInvertedBackground,
|
||||
ariaLabel = "About section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
gridClassName = "",
|
||||
leftColumnClassName = "",
|
||||
rightColumnClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
metricsContainerClassName = "",
|
||||
metricCardClassName = "",
|
||||
metricValueClassName = "",
|
||||
metricTitleClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
imageClassName = "",
|
||||
}: MetricSplitMediaAboutProps) => {
|
||||
const theme = useTheme();
|
||||
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls(
|
||||
"relative py-20 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:gap-10", gridClassName)}>
|
||||
<div className={cls("flex flex-col gap-6 md:gap-10", leftColumnClassName)}>
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
className={cls("gap-3 md:gap-3", textBoxClassName)}
|
||||
titleClassName={cls("text-6xl font-medium text-balance", titleClassName)}
|
||||
descriptionClassName={cls("text-lg leading-tight text-balance", descriptionClassName)}
|
||||
tagClassName={cls("mb-1", tagClassName)}
|
||||
/>
|
||||
{metrics && metrics.length > 0 && (
|
||||
<div className={cls(
|
||||
"grid gap-6 md:gap-4",
|
||||
metrics.length === 1 ? "grid-cols-1" : "grid-cols-1 md:grid-cols-2",
|
||||
metricsContainerClassName
|
||||
)}>
|
||||
{metrics.slice(0, 2).map((metric, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cls(
|
||||
"card rounded-theme-capped p-6 flex flex-col gap-8 md:gap-16",
|
||||
metricCardClassName
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cls(
|
||||
"text-6xl font-medium truncate",
|
||||
shouldUseLightText ? "text-background" : "text-foreground",
|
||||
metricValueClassName
|
||||
)}
|
||||
>
|
||||
{metric.value}
|
||||
</span>
|
||||
<h3
|
||||
className={cls(
|
||||
"text-lg",
|
||||
shouldUseLightText ? "text-background" : "text-foreground",
|
||||
metricTitleClassName
|
||||
)}
|
||||
>
|
||||
{metric.title}
|
||||
</h3>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={cls("", rightColumnClassName)}>
|
||||
<div className={cls(
|
||||
"w-full h-full overflow-hidden rounded-theme-capped",
|
||||
mediaWrapperClassName
|
||||
)}>
|
||||
<MediaContent
|
||||
imageSrc={imageSrc}
|
||||
videoSrc={videoSrc}
|
||||
imageAlt={imageAlt}
|
||||
videoAriaLabel={videoAriaLabel}
|
||||
imageClassName={cls("w-full h-full object-cover", imageClassName)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
MetricSplitMediaAbout.displayName = "MetricSplitMediaAbout";
|
||||
|
||||
export default memo(MetricSplitMediaAbout);
|
||||
171
src/components/sections/about/SplitAbout.tsx
Normal file
171
src/components/sections/about/SplitAbout.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
"use client";
|
||||
|
||||
import { 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 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/40" />
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{imagePosition === "right" && mediaContent}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
SplitAbout.displayName = "SplitAbout";
|
||||
|
||||
export default SplitAbout;
|
||||
127
src/components/sections/about/TestimonialAboutCard.tsx
Normal file
127
src/components/sections/about/TestimonialAboutCard.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
"use client";
|
||||
|
||||
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 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 TestimonialAboutCard;
|
||||
64
src/components/sections/about/TextAbout.tsx
Normal file
64
src/components/sections/about/TextAbout.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
"use client";
|
||||
|
||||
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";
|
||||
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 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" && "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 TextAbout;
|
||||
87
src/components/sections/about/TextSplitAbout.tsx
Normal file
87
src/components/sections/about/TextSplitAbout.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
"use client";
|
||||
|
||||
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";
|
||||
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 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" && "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" ? "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 TextSplitAbout;
|
||||
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 { 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 BlogCardOne;
|
||||
292
src/components/sections/blog/BlogCardThree.tsx
Normal file
292
src/components/sections/blog/BlogCardThree.tsx
Normal file
@@ -0,0 +1,292 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import Image from "next/image";
|
||||
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;
|
||||
title: string;
|
||||
excerpt: string;
|
||||
imageSrc: string;
|
||||
imageAlt?: string;
|
||||
authorName?: string;
|
||||
authorAvatar?: string;
|
||||
date?: 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;
|
||||
excerptClassName?: string;
|
||||
authorContainerClassName?: string;
|
||||
authorAvatarClassName?: string;
|
||||
authorNameClassName?: string;
|
||||
dateClassName?: 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";
|
||||
cardClassName?: string;
|
||||
cardContentClassName?: string;
|
||||
categoryTagClassName?: string;
|
||||
cardTitleClassName?: string;
|
||||
excerptClassName?: string;
|
||||
authorContainerClassName?: string;
|
||||
authorAvatarClassName?: string;
|
||||
authorNameClassName?: string;
|
||||
dateClassName?: string;
|
||||
mediaWrapperClassName?: string;
|
||||
mediaClassName?: string;
|
||||
}
|
||||
|
||||
const BlogCardItem = memo(({
|
||||
blog,
|
||||
useInvertedBackground,
|
||||
cardClassName = "",
|
||||
cardContentClassName = "",
|
||||
categoryTagClassName = "",
|
||||
cardTitleClassName = "",
|
||||
excerptClassName = "",
|
||||
authorContainerClassName = "",
|
||||
authorAvatarClassName = "",
|
||||
authorNameClassName = "",
|
||||
dateClassName = "",
|
||||
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}
|
||||
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",
|
||||
excerptClassName
|
||||
)}>
|
||||
{blog.excerpt}
|
||||
</p>
|
||||
|
||||
{(blog.authorName || blog.date) && (
|
||||
<div className={cls(
|
||||
"flex",
|
||||
blog.authorAvatar ? "items-center gap-3" : "flex-row justify-between items-center",
|
||||
authorContainerClassName
|
||||
)}>
|
||||
{blog.authorAvatar && (
|
||||
<Image
|
||||
src={blog.authorAvatar}
|
||||
alt={blog.authorName || "Author"}
|
||||
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('//')}
|
||||
/>
|
||||
)}
|
||||
{blog.authorAvatar ? (
|
||||
<div className="flex flex-col">
|
||||
{blog.authorName && (
|
||||
<p className={cls("text-sm font-medium", shouldUseLightText ? "text-background" : "text-foreground", authorNameClassName)}>
|
||||
{blog.authorName}
|
||||
</p>
|
||||
)}
|
||||
{blog.date && (
|
||||
<p className={cls("text-xs", shouldUseLightText ? "text-background/75" : "text-foreground/75", dateClassName)}>
|
||||
{blog.date}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{blog.authorName && (
|
||||
<p className={cls("text-sm font-medium", shouldUseLightText ? "text-background" : "text-foreground", authorNameClassName)}>
|
||||
{blog.authorName}
|
||||
</p>
|
||||
)}
|
||||
{blog.date && (
|
||||
<p className={cls("text-xs", shouldUseLightText ? "text-background/75" : "text-foreground/75", dateClassName)}>
|
||||
{blog.date}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={cls("relative z-1 w-full aspect-square", mediaWrapperClassName)}>
|
||||
<MediaContent
|
||||
imageSrc={blog.imageSrc}
|
||||
imageAlt={blog.imageAlt || blog.title}
|
||||
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 = "",
|
||||
excerptClassName = "",
|
||||
authorContainerClassName = "",
|
||||
authorAvatarClassName = "",
|
||||
authorNameClassName = "",
|
||||
dateClassName = "",
|
||||
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}
|
||||
excerptClassName={excerptClassName}
|
||||
authorContainerClassName={authorContainerClassName}
|
||||
authorAvatarClassName={authorAvatarClassName}
|
||||
authorNameClassName={authorNameClassName}
|
||||
dateClassName={dateClassName}
|
||||
mediaWrapperClassName={mediaWrapperClassName}
|
||||
mediaClassName={mediaClassName}
|
||||
/>
|
||||
))}
|
||||
</CardStack>
|
||||
);
|
||||
};
|
||||
|
||||
BlogCardThree.displayName = "BlogCardThree";
|
||||
|
||||
export default BlogCardThree;
|
||||
242
src/components/sections/blog/BlogCardTwo.tsx
Normal file
242
src/components/sections/blog/BlogCardTwo.tsx
Normal file
@@ -0,0 +1,242 @@
|
||||
"use client";
|
||||
|
||||
import { 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 | string[];
|
||||
title: string;
|
||||
excerpt: string;
|
||||
imageSrc: string;
|
||||
imageAlt?: string;
|
||||
authorName: string;
|
||||
authorAvatar?: 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;
|
||||
authorAvatarClassName?: string;
|
||||
authorDateClassName?: string;
|
||||
cardTitleClassName?: string;
|
||||
excerptClassName?: string;
|
||||
categoryClassName?: 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;
|
||||
authorAvatarClassName?: string;
|
||||
authorDateClassName?: string;
|
||||
cardTitleClassName?: string;
|
||||
excerptClassName?: string;
|
||||
categoryClassName?: string;
|
||||
}
|
||||
|
||||
const BlogCardItem = memo(({
|
||||
blog,
|
||||
shouldUseLightText,
|
||||
cardClassName = "",
|
||||
imageWrapperClassName = "",
|
||||
imageClassName = "",
|
||||
authorAvatarClassName = "",
|
||||
authorDateClassName = "",
|
||||
cardTitleClassName = "",
|
||||
excerptClassName = "",
|
||||
categoryClassName = "",
|
||||
}: 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">
|
||||
<div className="flex items-center gap-2">
|
||||
{blog.authorAvatar && (
|
||||
<Image
|
||||
src={blog.authorAvatar}
|
||||
alt={blog.authorName}
|
||||
width={24}
|
||||
height={24}
|
||||
className={cls("h-[var(--text-xs)] w-auto aspect-square rounded-theme object-cover bg-background-accent", authorAvatarClassName)}
|
||||
unoptimized={blog.authorAvatar.startsWith('http') || blog.authorAvatar.startsWith('//')}
|
||||
/>
|
||||
)}
|
||||
<p className={cls("text-xs", shouldUseLightText ? "text-background" : "text-foreground", authorDateClassName)}>
|
||||
{blog.authorName} • {blog.date}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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="flex flex-wrap gap-2">
|
||||
{Array.isArray(blog.category) ? (
|
||||
blog.category.map((cat, index) => (
|
||||
<Badge key={`${cat}-${index}`} text={cat} variant="primary" className={categoryClassName} />
|
||||
))
|
||||
) : (
|
||||
<Badge text={blog.category} variant="primary" className={categoryClassName} />
|
||||
)}
|
||||
</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 = "",
|
||||
authorAvatarClassName = "",
|
||||
authorDateClassName = "",
|
||||
cardTitleClassName = "",
|
||||
excerptClassName = "",
|
||||
categoryClassName = "",
|
||||
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}
|
||||
authorAvatarClassName={authorAvatarClassName}
|
||||
authorDateClassName={authorDateClassName}
|
||||
cardTitleClassName={cardTitleClassName}
|
||||
excerptClassName={excerptClassName}
|
||||
categoryClassName={categoryClassName}
|
||||
/>
|
||||
))}
|
||||
</CardStack>
|
||||
);
|
||||
};
|
||||
|
||||
BlogCardTwo.displayName = "BlogCardTwo";
|
||||
|
||||
export default BlogCardTwo;
|
||||
91
src/components/sections/contact/ContactCenter.tsx
Normal file
91
src/components/sections/contact/ContactCenter.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
"use client";
|
||||
|
||||
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";
|
||||
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 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 ContactCenter;
|
||||
188
src/components/sections/contact/ContactFaq.tsx
Normal file
188
src/components/sections/contact/ContactFaq.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
"use client";
|
||||
|
||||
import { 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 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-theme 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 ContactFaq;
|
||||
127
src/components/sections/contact/ContactSplit.tsx
Normal file
127
src/components/sections/contact/ContactSplit.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
"use client";
|
||||
|
||||
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";
|
||||
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 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 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 { 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";
|
||||
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 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 ContactSplitForm;
|
||||
86
src/components/sections/contact/ContactText.tsx
Normal file
86
src/components/sections/contact/ContactText.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
"use client";
|
||||
|
||||
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 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 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 { 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 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 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 { 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 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 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 { 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 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 FaqSplitMedia;
|
||||
151
src/components/sections/faq/FaqSplitText.tsx
Normal file
151
src/components/sections/faq/FaqSplitText.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
"use client";
|
||||
|
||||
import { 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";
|
||||
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 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", useInvertedBackground === "invertDefault" && "text-background", sideTitleClassName)}
|
||||
/>
|
||||
{sideDescription && (
|
||||
<TextAnimation
|
||||
type={theme.defaultTextAnimation as AnimationType}
|
||||
text={sideDescription}
|
||||
variant="words-trigger"
|
||||
className={cls("text-lg leading-[1.2]", useInvertedBackground === "invertDefault" && "text-background", 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 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 FaqSplitText;
|
||||
287
src/components/sections/feature/FeatureBento.tsx
Normal file
287
src/components/sections/feature/FeatureBento.tsx
Normal file
@@ -0,0 +1,287 @@
|
||||
"use client";
|
||||
|
||||
import CardStack from "@/components/cardStack/CardStack";
|
||||
import Button from "@/components/button/Button";
|
||||
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
||||
import { getButtonProps } from "@/lib/buttonUtils";
|
||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||
import { BentoGlobe } from "@/components/bento/BentoGlobe";
|
||||
import BentoIconInfoCards from "@/components/bento/BentoIconInfoCards";
|
||||
import BentoAnimatedBarChart from "@/components/bento/BentoAnimatedBarChart";
|
||||
import Bento3DStackCards from "@/components/bento/Bento3DStackCards";
|
||||
import Bento3DTaskList, { type TaskItem } from "@/components/bento/Bento3DTaskList";
|
||||
import BentoOrbitingIcons, { type OrbitingItem } from "@/components/bento/BentoOrbitingIcons";
|
||||
import BentoMap from "@/components/bento/BentoMap";
|
||||
import BentoMarquee from "@/components/bento/BentoMarquee";
|
||||
import BentoLineChart from "@/components/bento/BentoLineChart/BentoLineChart";
|
||||
import BentoPhoneAnimation, { type PhoneApp, type PhoneApps8 } from "@/components/bento/BentoPhoneAnimation";
|
||||
import BentoChatAnimation, { type ChatExchange } from "@/components/bento/BentoChatAnimation";
|
||||
import Bento3DCardGrid from "@/components/bento/Bento3DCardGrid";
|
||||
import BentoRevealIcon from "@/components/bento/BentoRevealIcon";
|
||||
import BentoTimeline, { type TimelineItem } from "@/components/bento/BentoTimeline";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
|
||||
export type { PhoneApp, PhoneApps8, ChatExchange, TimelineItem };
|
||||
import type { ButtonConfig, CardAnimationTypeWith3D, TitleSegment } from "@/components/cardStack/types";
|
||||
|
||||
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||
|
||||
type BentoAnimationType = Exclude<CardAnimationTypeWith3D, "depth-3d" | "scale-rotate">;
|
||||
|
||||
export type BentoInfoItem = {
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type Bento3DItem = {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
detail: string;
|
||||
};
|
||||
|
||||
type BaseFeatureCard = {
|
||||
title: string;
|
||||
description: string;
|
||||
button?: ButtonConfig;
|
||||
};
|
||||
|
||||
export type FeatureCard = BaseFeatureCard & (
|
||||
| {
|
||||
bentoComponent: "icon-info-cards";
|
||||
items: BentoInfoItem[];
|
||||
}
|
||||
| {
|
||||
bentoComponent: "3d-stack-cards";
|
||||
items: [Bento3DItem, Bento3DItem, Bento3DItem];
|
||||
}
|
||||
| {
|
||||
bentoComponent: "3d-task-list";
|
||||
title: string;
|
||||
items: TaskItem[];
|
||||
}
|
||||
| {
|
||||
bentoComponent: "orbiting-icons";
|
||||
centerIcon: LucideIcon;
|
||||
items: OrbitingItem[];
|
||||
}
|
||||
| ({
|
||||
bentoComponent: "marquee";
|
||||
centerIcon: LucideIcon;
|
||||
} & (
|
||||
| { variant: "text"; texts: string[] }
|
||||
| { variant: "icon"; icons: LucideIcon[] }
|
||||
))
|
||||
| {
|
||||
bentoComponent: "globe" | "animated-bar-chart" | "map" | "line-chart";
|
||||
items?: never;
|
||||
}
|
||||
| {
|
||||
bentoComponent: "3d-card-grid";
|
||||
items: [{ name: string; icon: LucideIcon }, { name: string; icon: LucideIcon }, { name: string; icon: LucideIcon }, { name: string; icon: LucideIcon }];
|
||||
centerIcon: LucideIcon;
|
||||
}
|
||||
| {
|
||||
bentoComponent: "phone";
|
||||
statusIcon: LucideIcon;
|
||||
alertIcon: LucideIcon;
|
||||
alertTitle: string;
|
||||
alertMessage: string;
|
||||
apps: PhoneApps8;
|
||||
}
|
||||
| {
|
||||
bentoComponent: "chat";
|
||||
aiIcon: LucideIcon;
|
||||
userIcon: LucideIcon;
|
||||
exchanges: ChatExchange[];
|
||||
placeholder: string;
|
||||
}
|
||||
| {
|
||||
bentoComponent: "reveal-icon";
|
||||
icon: LucideIcon;
|
||||
}
|
||||
| {
|
||||
bentoComponent: "timeline";
|
||||
heading: string;
|
||||
subheading: string;
|
||||
items: [TimelineItem, TimelineItem, TimelineItem];
|
||||
completedLabel: string;
|
||||
}
|
||||
);
|
||||
|
||||
interface FeatureBentoProps {
|
||||
features: FeatureCard[];
|
||||
carouselMode?: "auto" | "buttons";
|
||||
animationType: BentoAnimationType;
|
||||
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;
|
||||
cardButtonClassName?: string;
|
||||
cardButtonTextClassName?: string;
|
||||
gridClassName?: string;
|
||||
carouselClassName?: string;
|
||||
controlsClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
textBoxTagClassName?: string;
|
||||
textBoxButtonContainerClassName?: string;
|
||||
textBoxButtonClassName?: string;
|
||||
textBoxButtonTextClassName?: string;
|
||||
}
|
||||
|
||||
const FeatureBento = ({
|
||||
features,
|
||||
carouselMode = "buttons",
|
||||
animationType,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
textboxLayout,
|
||||
useInvertedBackground,
|
||||
ariaLabel = "Feature section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
cardClassName = "",
|
||||
textBoxTitleClassName = "",
|
||||
textBoxTitleImageWrapperClassName = "",
|
||||
textBoxTitleImageClassName = "",
|
||||
textBoxDescriptionClassName = "",
|
||||
cardTitleClassName = "",
|
||||
cardDescriptionClassName = "",
|
||||
cardButtonClassName = "",
|
||||
cardButtonTextClassName = "",
|
||||
gridClassName = "",
|
||||
carouselClassName = "",
|
||||
controlsClassName = "",
|
||||
textBoxClassName = "",
|
||||
textBoxTagClassName = "",
|
||||
textBoxButtonContainerClassName = "",
|
||||
textBoxButtonClassName = "",
|
||||
textBoxButtonTextClassName = "",
|
||||
}: FeatureBentoProps) => {
|
||||
const theme = useTheme();
|
||||
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
|
||||
|
||||
const getBentoComponent = (feature: FeatureCard) => {
|
||||
switch (feature.bentoComponent) {
|
||||
case "globe":
|
||||
return (
|
||||
<div className="relative w-full h-full min-h-0" style={{
|
||||
maskImage: "linear-gradient(to right, transparent 0%, black 20%, black 80%, transparent 100%), linear-gradient(to bottom, black 40%, transparent 100%)",
|
||||
WebkitMaskImage: "linear-gradient(to right, transparent 0%, black 20%, black 80%, transparent 100%), linear-gradient(to bottom, black 40%, transparent 100%)",
|
||||
maskComposite: "intersect",
|
||||
WebkitMaskComposite: "source-in"
|
||||
}}>
|
||||
<BentoGlobe className="w-full scale-150 mt-[15%]" />
|
||||
</div>
|
||||
);
|
||||
case "icon-info-cards":
|
||||
return <BentoIconInfoCards items={feature.items} useInvertedBackground={useInvertedBackground} />;
|
||||
case "animated-bar-chart":
|
||||
return <BentoAnimatedBarChart />;
|
||||
case "3d-stack-cards":
|
||||
return <Bento3DStackCards cards={feature.items.map(item => ({ Icon: item.icon, title: item.title, subtitle: item.subtitle, detail: item.detail }))} useInvertedBackground={useInvertedBackground} />;
|
||||
case "3d-task-list":
|
||||
return <Bento3DTaskList title={feature.title} items={feature.items} useInvertedBackground={useInvertedBackground} />;
|
||||
case "orbiting-icons":
|
||||
return <BentoOrbitingIcons centerIcon={feature.centerIcon} items={feature.items} useInvertedBackground={useInvertedBackground} />;
|
||||
case "marquee":
|
||||
return feature.variant === "text"
|
||||
? <BentoMarquee centerIcon={feature.centerIcon} variant="text" texts={feature.texts} useInvertedBackground={useInvertedBackground} />
|
||||
: <BentoMarquee centerIcon={feature.centerIcon} variant="icon" icons={feature.icons} useInvertedBackground={useInvertedBackground} />;
|
||||
case "map":
|
||||
return <BentoMap useInvertedBackground={useInvertedBackground} />;
|
||||
case "line-chart":
|
||||
return <BentoLineChart useInvertedBackground={useInvertedBackground} />;
|
||||
case "3d-card-grid":
|
||||
return <Bento3DCardGrid items={feature.items} centerIcon={feature.centerIcon} useInvertedBackground={useInvertedBackground} />;
|
||||
case "phone":
|
||||
return <BentoPhoneAnimation statusIcon={feature.statusIcon} alertIcon={feature.alertIcon} alertTitle={feature.alertTitle} alertMessage={feature.alertMessage} apps={feature.apps} useInvertedBackground={useInvertedBackground} />;
|
||||
case "chat":
|
||||
return <BentoChatAnimation aiIcon={feature.aiIcon} userIcon={feature.userIcon} exchanges={feature.exchanges} placeholder={feature.placeholder} useInvertedBackground={useInvertedBackground} />;
|
||||
case "reveal-icon":
|
||||
return <BentoRevealIcon icon={feature.icon} useInvertedBackground={useInvertedBackground} />;
|
||||
case "timeline":
|
||||
return <BentoTimeline heading={feature.heading} subheading={feature.subheading} items={feature.items} completedLabel={feature.completedLabel} useInvertedBackground={useInvertedBackground} />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<CardStack
|
||||
mode={carouselMode}
|
||||
gridVariant="uniform-all-items-equal"
|
||||
uniformGridCustomHeightClasses="min-h-0"
|
||||
animationType={animationType}
|
||||
carouselThreshold={4}
|
||||
|
||||
title={title}
|
||||
titleSegments={titleSegments}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
buttons={buttons}
|
||||
textboxLayout={textboxLayout}
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
className={className}
|
||||
containerClassName={containerClassName}
|
||||
gridClassName={gridClassName}
|
||||
carouselClassName={carouselClassName}
|
||||
carouselItemClassName="w-carousel-item-3 xl:w-carousel-item-3!"
|
||||
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-5 rounded-theme-capped min-h-0 h-full", cardClassName)}
|
||||
>
|
||||
<div className="relative w-full h-70 min-h-0 overflow-hidden">
|
||||
{getBentoComponent(feature)}
|
||||
</div>
|
||||
<div className="relative z-1 flex flex-col gap-1">
|
||||
<h3 className={cls("text-2xl font-medium leading-tight", shouldUseLightText && "text-background", cardTitleClassName)}>
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p className={cls("text-sm leading-tight", 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>
|
||||
);
|
||||
};
|
||||
|
||||
FeatureBento.displayName = "FeatureBento";
|
||||
|
||||
export default FeatureBento;
|
||||
151
src/components/sections/feature/FeatureCardEight.tsx
Normal file
151
src/components/sections/feature/FeatureCardEight.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
"use client";
|
||||
|
||||
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-theme 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 FeatureCardEight;
|
||||
255
src/components/sections/feature/FeatureCardMedia.tsx
Normal file
255
src/components/sections/feature/FeatureCardMedia.tsx
Normal file
@@ -0,0 +1,255 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import CardStack from "@/components/cardStack/CardStack";
|
||||
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, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
|
||||
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||
|
||||
type FeatureCard = {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
tag: string;
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
buttons?: ButtonConfig[];
|
||||
onCardClick?: () => void;
|
||||
};
|
||||
|
||||
interface FeatureCardMediaProps {
|
||||
features: FeatureCard[];
|
||||
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;
|
||||
tagClassName?: string;
|
||||
contentClassName?: string;
|
||||
cardTitleClassName?: string;
|
||||
cardDescriptionClassName?: string;
|
||||
cardButtonContainerClassName?: string;
|
||||
cardButtonClassName?: string;
|
||||
cardButtonTextClassName?: 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 FeatureCardItemProps {
|
||||
feature: FeatureCard;
|
||||
shouldUseLightText: boolean;
|
||||
useInvertedBackground: InvertedBackground;
|
||||
itemClassName?: string;
|
||||
mediaWrapperClassName?: string;
|
||||
mediaClassName?: string;
|
||||
tagClassName?: string;
|
||||
contentClassName?: string;
|
||||
cardTitleClassName?: string;
|
||||
cardDescriptionClassName?: string;
|
||||
cardButtonContainerClassName?: string;
|
||||
cardButtonClassName?: string;
|
||||
cardButtonTextClassName?: string;
|
||||
}
|
||||
|
||||
const FeatureCardItem = memo(({
|
||||
feature,
|
||||
shouldUseLightText,
|
||||
useInvertedBackground,
|
||||
itemClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
mediaClassName = "",
|
||||
tagClassName = "",
|
||||
contentClassName = "",
|
||||
cardTitleClassName = "",
|
||||
cardDescriptionClassName = "",
|
||||
cardButtonContainerClassName = "",
|
||||
cardButtonClassName = "",
|
||||
cardButtonTextClassName = "",
|
||||
}: FeatureCardItemProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<article
|
||||
className={cls("relative h-full flex flex-col gap-6 cursor-pointer group", itemClassName)}
|
||||
onClick={feature.onCardClick}
|
||||
role="article"
|
||||
aria-label={feature.title}
|
||||
>
|
||||
<div className={cls("relative w-full aspect-square overflow-hidden rounded-theme-capped", mediaWrapperClassName)}>
|
||||
<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 transition-transform duration-500 ease-in-out group-hover:scale-105", mediaClassName)}
|
||||
/>
|
||||
<div className="absolute top-4 right-4">
|
||||
<Tag
|
||||
text={feature.tag}
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
className={tagClassName}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={cls("relative z-1 card rounded-theme-capped p-6 flex flex-col gap-2", contentClassName)}>
|
||||
<h3 className={cls(
|
||||
"text-xl md:text-2xl font-medium leading-tight",
|
||||
shouldUseLightText ? "text-background" : "text-foreground",
|
||||
cardTitleClassName
|
||||
)}>
|
||||
{feature.title}
|
||||
</h3>
|
||||
|
||||
<p className={cls(
|
||||
"text-base leading-tight",
|
||||
shouldUseLightText ? "text-background/75" : "text-foreground/75",
|
||||
cardDescriptionClassName
|
||||
)}>
|
||||
{feature.description}
|
||||
</p>
|
||||
|
||||
{feature.buttons && feature.buttons.length > 0 && (
|
||||
<div className={cls("flex flex-wrap gap-4 mt-2", cardButtonContainerClassName)}>
|
||||
{feature.buttons.slice(0, 2).map((button, index) => (
|
||||
<Button
|
||||
key={`${button.text}-${index}`}
|
||||
{...getButtonProps(button, index, theme.defaultButtonVariant, cardButtonClassName, cardButtonTextClassName)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
});
|
||||
|
||||
FeatureCardItem.displayName = "FeatureCardItem";
|
||||
|
||||
const FeatureCardMedia = ({
|
||||
features,
|
||||
carouselMode = "buttons",
|
||||
uniformGridCustomHeightClasses,
|
||||
animationType,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
textboxLayout,
|
||||
useInvertedBackground,
|
||||
ariaLabel = "Features section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
itemClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
mediaClassName = "",
|
||||
tagClassName = "",
|
||||
contentClassName = "",
|
||||
cardTitleClassName = "",
|
||||
cardDescriptionClassName = "",
|
||||
cardButtonContainerClassName = "",
|
||||
cardButtonClassName = "",
|
||||
cardButtonTextClassName = "",
|
||||
textBoxTitleClassName = "",
|
||||
textBoxTitleImageWrapperClassName = "",
|
||||
textBoxTitleImageClassName = "",
|
||||
textBoxDescriptionClassName = "",
|
||||
gridClassName = "",
|
||||
carouselClassName = "",
|
||||
controlsClassName = "",
|
||||
textBoxClassName = "",
|
||||
textBoxTagClassName = "",
|
||||
textBoxButtonContainerClassName = "",
|
||||
textBoxButtonClassName = "",
|
||||
textBoxButtonTextClassName = "",
|
||||
}: FeatureCardMediaProps) => {
|
||||
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}
|
||||
>
|
||||
{features.map((feature) => (
|
||||
<FeatureCardItem
|
||||
key={feature.id}
|
||||
feature={feature}
|
||||
shouldUseLightText={shouldUseLightText}
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
itemClassName={itemClassName}
|
||||
mediaWrapperClassName={mediaWrapperClassName}
|
||||
mediaClassName={mediaClassName}
|
||||
tagClassName={tagClassName}
|
||||
contentClassName={contentClassName}
|
||||
cardTitleClassName={cardTitleClassName}
|
||||
cardDescriptionClassName={cardDescriptionClassName}
|
||||
cardButtonContainerClassName={cardButtonContainerClassName}
|
||||
cardButtonClassName={cardButtonClassName}
|
||||
cardButtonTextClassName={cardButtonTextClassName}
|
||||
/>
|
||||
))}
|
||||
</CardStack>
|
||||
);
|
||||
};
|
||||
|
||||
FeatureCardMedia.displayName = "FeatureCardMedia";
|
||||
|
||||
export default FeatureCardMedia;
|
||||
199
src/components/sections/feature/FeatureCardNine.tsx
Normal file
199
src/components/sections/feature/FeatureCardNine.tsx
Normal file
@@ -0,0 +1,199 @@
|
||||
"use client";
|
||||
|
||||
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-theme 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" && "text-background", featureTitleClassName)}>
|
||||
{feature.title}
|
||||
</h2>
|
||||
<p className={cls("text-base leading-[1.2] text-balance", useInvertedBackground === "invertDefault" ? "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[] = 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`,
|
||||
}));
|
||||
|
||||
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 FeatureCardNine;
|
||||
193
src/components/sections/feature/FeatureCardNineteen.tsx
Normal file
193
src/components/sections/feature/FeatureCardNineteen.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
"use client";
|
||||
|
||||
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 FeatureCardNineteen;
|
||||
172
src/components/sections/feature/FeatureCardOne.tsx
Normal file
172
src/components/sections/feature/FeatureCardOne.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
"use client";
|
||||
|
||||
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, CardAnimationTypeWith3D, TitleSegment } from "@/components/cardStack/types";
|
||||
|
||||
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: GridVariant;
|
||||
uniformGridCustomHeightClasses?: string;
|
||||
animationType: CardAnimationTypeWith3D;
|
||||
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}
|
||||
supports3DAnimation={true}
|
||||
|
||||
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 leading-tight", shouldUseLightText && "text-background", cardTitleClassName)}>
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p className={cls("text-sm leading-tight", 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 FeatureCardOne;
|
||||
159
src/components/sections/feature/FeatureCardSeven.tsx
Normal file
159
src/components/sections/feature/FeatureCardSeven.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
"use client";
|
||||
|
||||
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-theme 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 FeatureCardSeven;
|
||||
153
src/components/sections/feature/FeatureCardSix.tsx
Normal file
153
src/components/sections/feature/FeatureCardSix.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
"use client";
|
||||
|
||||
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-theme 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 FeatureCardSix;
|
||||
161
src/components/sections/feature/FeatureCardSixteen.tsx
Normal file
161
src/components/sections/feature/FeatureCardSixteen.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
"use client";
|
||||
|
||||
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, CardAnimationTypeWith3D, TitleSegment } from "@/components/cardStack/types";
|
||||
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||
|
||||
type ComparisonItem = {
|
||||
items: string[];
|
||||
};
|
||||
|
||||
interface FeatureCardSixteenProps {
|
||||
negativeCard: ComparisonItem;
|
||||
positiveCard: ComparisonItem;
|
||||
animationType: CardAnimationTypeWith3D;
|
||||
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;
|
||||
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 = "",
|
||||
itemsListClassName = "",
|
||||
itemClassName = "",
|
||||
itemIconClassName = "",
|
||||
itemTextClassName = "",
|
||||
}: FeatureCardSixteenProps) => {
|
||||
const theme = useTheme();
|
||||
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
|
||||
const { itemRefs, containerRef, perspectiveRef } = useCardAnimation({
|
||||
animationType,
|
||||
itemCount: 2,
|
||||
isGrid: true,
|
||||
supports3DAnimation: true,
|
||||
gridVariant: "uniform-all-items-equal"
|
||||
});
|
||||
|
||||
const cards = [
|
||||
{ ...negativeCard, variant: "negative" as const },
|
||||
{ ...positiveCard, variant: "positive" as const },
|
||||
];
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={containerRef}
|
||||
aria-label={ariaLabel}
|
||||
className={cls("relative py-20 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
|
||||
ref={perspectiveRef}
|
||||
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")}>
|
||||
<PricingFeatureList
|
||||
features={card.items}
|
||||
icon={card.variant === "positive" ? Check : X}
|
||||
shouldUseLightText={shouldUseLightText}
|
||||
className={itemsListClassName}
|
||||
featureItemClassName={itemClassName}
|
||||
featureIconWrapperClassName=""
|
||||
featureIconClassName={itemIconClassName}
|
||||
featureTextClassName={cls("truncate", itemTextClassName)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
FeatureCardSixteen.displayName = "FeatureCardSixteen";
|
||||
|
||||
export default 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 aspect-square md:aspect-[16/10]", 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" && "text-background", featureTitleClassName)}>
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p className={cls("text-base leading-[1.2]", useInvertedBackground === "invertDefault" ? "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" ? "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);
|
||||
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 { Fragment } 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) => (
|
||||
<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>
|
||||
)}
|
||||
</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 FeatureCardTwelve;
|
||||
194
src/components/sections/feature/FeatureCardTwentyFour.tsx
Normal file
194
src/components/sections/feature/FeatureCardTwentyFour.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
"use client";
|
||||
|
||||
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 FeatureItem = MediaProps & {
|
||||
id: string;
|
||||
title: string;
|
||||
author: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
onFeatureClick?: () => void;
|
||||
};
|
||||
|
||||
interface FeatureCardTwentyFourProps {
|
||||
features: FeatureItem[];
|
||||
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 FeatureCardTwentyFour = ({
|
||||
features,
|
||||
animationType,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
textboxLayout,
|
||||
useInvertedBackground,
|
||||
ariaLabel = "Features section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
cardClassName = "",
|
||||
textBoxTitleClassName = "",
|
||||
textBoxDescriptionClassName = "",
|
||||
textBoxClassName = "",
|
||||
textBoxTagClassName = "",
|
||||
textBoxButtonContainerClassName = "",
|
||||
textBoxButtonClassName = "",
|
||||
textBoxButtonTextClassName = "",
|
||||
titleImageWrapperClassName = "",
|
||||
titleImageClassName = "",
|
||||
cardContentClassName = "",
|
||||
cardTitleClassName = "",
|
||||
authorClassName = "",
|
||||
cardDescriptionClassName = "",
|
||||
tagsContainerClassName = "",
|
||||
tagClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
mediaClassName = "",
|
||||
}: FeatureCardTwentyFourProps) => {
|
||||
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) => (
|
||||
<article
|
||||
key={feature.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={feature.onFeatureClick}
|
||||
role="article"
|
||||
aria-label={feature.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
|
||||
)}>
|
||||
{feature.title}{" "}
|
||||
<span className={cls(
|
||||
shouldUseLightText ? "text-background/50" : "text-foreground/50",
|
||||
authorClassName
|
||||
)}>
|
||||
by {feature.author}
|
||||
</span>
|
||||
</h3>
|
||||
|
||||
<div className="mt-auto flex flex-col gap-4">
|
||||
<div className={cls("flex flex-wrap gap-2", tagsContainerClassName)}>
|
||||
{feature.tags.map((tagText, index) => (
|
||||
<Tag key={index} text={tagText} useInvertedBackground={useInvertedBackground} className={tagClassName} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className={cls(
|
||||
"text-base md:text-2xl text-balance leading-tight line-clamp-2",
|
||||
shouldUseLightText ? "text-background" : "text-foreground",
|
||||
cardDescriptionClassName
|
||||
)}>
|
||||
{feature.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={feature.imageSrc}
|
||||
videoSrc={feature.videoSrc}
|
||||
imageAlt={feature.imageAlt}
|
||||
videoAriaLabel={feature.videoAriaLabel}
|
||||
imageClassName={cls("w-full h-full object-cover", mediaClassName)}
|
||||
/>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</CardList>
|
||||
);
|
||||
};
|
||||
|
||||
FeatureCardTwentyFour.displayName = "FeatureCardTwentyFour";
|
||||
|
||||
export default FeatureCardTwentyFour;
|
||||
220
src/components/sections/feature/FeatureCardTwentyOne.tsx
Normal file
220
src/components/sections/feature/FeatureCardTwentyOne.tsx
Normal file
@@ -0,0 +1,220 @@
|
||||
"use client";
|
||||
|
||||
import { useState } 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 [activeAccordion, setActiveAccordion] = useState<number>(0);
|
||||
|
||||
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
|
||||
)}>
|
||||
{/* Mobile */}
|
||||
<TextBox
|
||||
title={title}
|
||||
titleSegments={titleSegments}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
buttons={buttons}
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
className={cls("flex flex-col gap-1 md:hidden", 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={true}
|
||||
/>
|
||||
{/* Desktop */}
|
||||
<TextBox
|
||||
title={title}
|
||||
titleSegments={titleSegments}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
buttons={buttons}
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
className={cls("hidden md: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={false}
|
||||
/>
|
||||
|
||||
<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 === "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 FeatureCardTwentyOne;
|
||||
235
src/components/sections/feature/FeatureCardTwentyThree.tsx
Normal file
235
src/components/sections/feature/FeatureCardTwentyThree.tsx
Normal file
@@ -0,0 +1,235 @@
|
||||
"use client";
|
||||
|
||||
import { 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 FeatureItem = {
|
||||
id: string;
|
||||
title: string;
|
||||
tags: string[];
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
onFeatureClick?: () => void;
|
||||
};
|
||||
|
||||
interface FeatureCardTwentyThreeProps {
|
||||
features: FeatureItem[];
|
||||
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 FeatureCardItemProps {
|
||||
feature: FeatureItem;
|
||||
shouldUseLightText: boolean;
|
||||
useInvertedBackground: InvertedBackground;
|
||||
itemClassName?: string;
|
||||
mediaWrapperClassName?: string;
|
||||
mediaClassName?: string;
|
||||
cardClassName?: string;
|
||||
cardTitleClassName?: string;
|
||||
tagsContainerClassName?: string;
|
||||
tagClassName?: string;
|
||||
arrowClassName?: string;
|
||||
}
|
||||
|
||||
const FeatureCardItem = memo(({
|
||||
feature,
|
||||
shouldUseLightText,
|
||||
useInvertedBackground,
|
||||
itemClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
mediaClassName = "",
|
||||
cardClassName = "",
|
||||
cardTitleClassName = "",
|
||||
tagsContainerClassName = "",
|
||||
tagClassName = "",
|
||||
arrowClassName = "",
|
||||
}: FeatureCardItemProps) => {
|
||||
return (
|
||||
<article
|
||||
className={cls("relative h-full flex flex-col gap-6 cursor-pointer group", itemClassName)}
|
||||
onClick={feature.onFeatureClick}
|
||||
role="article"
|
||||
aria-label={feature.title}
|
||||
>
|
||||
<div className={cls("relative w-full aspect-square overflow-hidden rounded-theme-capped", mediaWrapperClassName)}>
|
||||
<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 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
|
||||
)}>
|
||||
{feature.title}
|
||||
</h3>
|
||||
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className={cls("flex items-center gap-2 flex-wrap", tagsContainerClassName)}>
|
||||
{feature.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>
|
||||
);
|
||||
});
|
||||
|
||||
FeatureCardItem.displayName = "FeatureCardItem";
|
||||
|
||||
const FeatureCardTwentyThree = ({
|
||||
features,
|
||||
carouselMode = "buttons",
|
||||
uniformGridCustomHeightClasses,
|
||||
animationType,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
textboxLayout,
|
||||
useInvertedBackground,
|
||||
ariaLabel = "Features section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
itemClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
mediaClassName = "",
|
||||
cardClassName = "",
|
||||
cardTitleClassName = "",
|
||||
tagsContainerClassName = "",
|
||||
tagClassName = "",
|
||||
arrowClassName = "",
|
||||
textBoxTitleClassName = "",
|
||||
textBoxTitleImageWrapperClassName = "",
|
||||
textBoxTitleImageClassName = "",
|
||||
textBoxDescriptionClassName = "",
|
||||
gridClassName = "",
|
||||
carouselClassName = "",
|
||||
controlsClassName = "",
|
||||
textBoxClassName = "",
|
||||
textBoxTagClassName = "",
|
||||
textBoxButtonContainerClassName = "",
|
||||
textBoxButtonClassName = "",
|
||||
textBoxButtonTextClassName = "",
|
||||
}: FeatureCardTwentyThreeProps) => {
|
||||
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}
|
||||
>
|
||||
{features.map((feature) => (
|
||||
<FeatureCardItem
|
||||
key={feature.id}
|
||||
feature={feature}
|
||||
shouldUseLightText={shouldUseLightText}
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
itemClassName={itemClassName}
|
||||
mediaWrapperClassName={mediaWrapperClassName}
|
||||
mediaClassName={mediaClassName}
|
||||
cardClassName={cardClassName}
|
||||
cardTitleClassName={cardTitleClassName}
|
||||
tagsContainerClassName={tagsContainerClassName}
|
||||
tagClassName={tagClassName}
|
||||
arrowClassName={arrowClassName}
|
||||
/>
|
||||
))}
|
||||
</CardStack>
|
||||
);
|
||||
};
|
||||
|
||||
FeatureCardTwentyThree.displayName = "FeatureCardTwentyThree";
|
||||
|
||||
export default FeatureCardTwentyThree;
|
||||
169
src/components/sections/feature/FeatureCardTwentyTwo.tsx
Normal file
169
src/components/sections/feature/FeatureCardTwentyTwo.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
"use client";
|
||||
|
||||
import { Fragment } 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 FeatureItem = {
|
||||
id: string;
|
||||
category: string[];
|
||||
title: string;
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
};
|
||||
|
||||
interface FeatureCardTwentyTwoProps {
|
||||
features: FeatureItem[];
|
||||
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;
|
||||
categoryContainerClassName?: string;
|
||||
categoryClassName?: string;
|
||||
mediaWrapperClassName?: string;
|
||||
mediaClassName?: string;
|
||||
}
|
||||
|
||||
const FeatureCardTwentyTwo = ({
|
||||
features,
|
||||
animationType,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
textboxLayout,
|
||||
useInvertedBackground,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
ariaLabel = "Features section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxTitleClassName = "",
|
||||
titleImageWrapperClassName = "",
|
||||
titleImageClassName = "",
|
||||
textBoxDescriptionClassName = "",
|
||||
textBoxClassName = "",
|
||||
textBoxTagClassName = "",
|
||||
textBoxButtonContainerClassName = "",
|
||||
textBoxButtonClassName = "",
|
||||
textBoxButtonTextClassName = "",
|
||||
gridClassName = "",
|
||||
cardClassName = "",
|
||||
cardContentClassName = "",
|
||||
cardTitleClassName = "",
|
||||
categoryContainerClassName = "",
|
||||
categoryClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
mediaClassName = "",
|
||||
}: FeatureCardTwentyTwoProps) => {
|
||||
const theme = useTheme();
|
||||
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
|
||||
const { itemRefs } = useCardAnimation({ animationType, itemCount: features.length });
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("relative py-20 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)}>
|
||||
{features.map((feature, index) => (
|
||||
<article
|
||||
key={feature.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={feature.imageSrc}
|
||||
videoSrc={feature.videoSrc}
|
||||
imageAlt={feature.imageAlt || feature.title}
|
||||
videoAriaLabel={feature.videoAriaLabel || feature.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
|
||||
)}>
|
||||
{feature.title}
|
||||
</h3>
|
||||
<div className={cls("flex flex-wrap items-center gap-2", categoryContainerClassName)}>
|
||||
{feature.category.map((cat, idx) => (
|
||||
<Fragment key={idx}>
|
||||
<span className={cls(
|
||||
"text-sm",
|
||||
shouldUseLightText ? "text-background/75" : "text-foreground/75",
|
||||
categoryClassName
|
||||
)}>
|
||||
{cat}
|
||||
</span>
|
||||
{idx < feature.category.length - 1 && (
|
||||
<span className="text-sm text-accent">•</span>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
FeatureCardTwentyTwo.displayName = "FeatureCardTwentyTwo";
|
||||
|
||||
export default FeatureCardTwentyTwo;
|
||||
189
src/components/sections/feature/FeatureProcessSteps.tsx
Normal file
189
src/components/sections/feature/FeatureProcessSteps.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import TextBox from "@/components/Textbox";
|
||||
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 { InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
|
||||
interface ProcessStep {
|
||||
number: string;
|
||||
title: string;
|
||||
description: string;
|
||||
tag?: string;
|
||||
}
|
||||
|
||||
interface FeatureProcessStepsProps {
|
||||
title: string;
|
||||
description: string;
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
buttons?: ButtonConfig[];
|
||||
steps: ProcessStep[];
|
||||
useInvertedBackground: InvertedBackground;
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
gridClassName?: string;
|
||||
leftColumnClassName?: string;
|
||||
rightColumnClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
titleClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
tagClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
stepsContainerClassName?: string;
|
||||
stepClassName?: string;
|
||||
stepNumberClassName?: string;
|
||||
stepContentClassName?: string;
|
||||
stepTitleClassName?: string;
|
||||
stepTagClassName?: string;
|
||||
stepDescriptionClassName?: string;
|
||||
}
|
||||
|
||||
const FeatureProcessSteps = ({
|
||||
title,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
steps,
|
||||
useInvertedBackground,
|
||||
ariaLabel = "Process steps section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
gridClassName = "",
|
||||
leftColumnClassName = "",
|
||||
rightColumnClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
stepsContainerClassName = "",
|
||||
stepClassName = "",
|
||||
stepNumberClassName = "",
|
||||
stepContentClassName = "",
|
||||
stepTitleClassName = "",
|
||||
stepTagClassName = "",
|
||||
stepDescriptionClassName = "",
|
||||
}: FeatureProcessStepsProps) => {
|
||||
const theme = useTheme();
|
||||
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls(
|
||||
"relative py-20 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-10 md:gap-10 items-center", gridClassName)}>
|
||||
<div className={cls("flex flex-col", leftColumnClassName)}>
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
buttons={buttons}
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
className={cls("gap-3 md:gap-3", textBoxClassName)}
|
||||
titleClassName={cls("text-6xl font-medium text-balance", titleClassName)}
|
||||
descriptionClassName={cls("text-lg leading-tight text-balance", descriptionClassName)}
|
||||
tagClassName={cls("mb-1", tagClassName)}
|
||||
buttonContainerClassName={cls("mt-2", buttonContainerClassName)}
|
||||
buttonClassName={buttonClassName}
|
||||
buttonTextClassName={buttonTextClassName}
|
||||
/>
|
||||
</div>
|
||||
<div className={cls("flex flex-col gap-6", rightColumnClassName)}>
|
||||
{steps && steps.length > 0 && (
|
||||
<div className={cls("flex flex-col", stepsContainerClassName)}>
|
||||
{steps.map((step, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cls(
|
||||
"flex gap-6",
|
||||
stepClassName
|
||||
)}
|
||||
>
|
||||
{/* Number box with line below */}
|
||||
<div className="flex flex-col items-center">
|
||||
<div
|
||||
className={cls(
|
||||
"card h-12 w-fit aspect-square rounded-theme flex items-center justify-center shrink-0",
|
||||
stepNumberClassName
|
||||
)}
|
||||
>
|
||||
<p
|
||||
className={cls(
|
||||
"text-lg font-medium",
|
||||
shouldUseLightText ? "text-background" : "text-foreground"
|
||||
)}
|
||||
>
|
||||
{step.number}
|
||||
</p>
|
||||
</div>
|
||||
{/* Line segment */}
|
||||
{index < steps.length - 1 && (
|
||||
<div
|
||||
className={cls(
|
||||
"h-full w-px",
|
||||
useInvertedBackground === "invertDefault" ? "bg-background/20" : "bg-foreground/20"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={cls("w-full min-w-0 flex flex-col gap-2", stepContentClassName)}>
|
||||
<div className="w-full min-w-0 flex items-center gap-3">
|
||||
<h3
|
||||
className={cls(
|
||||
"text-2xl font-medium truncate",
|
||||
useInvertedBackground === "invertDefault" ? "text-background" : "text-foreground",
|
||||
stepTitleClassName
|
||||
)}
|
||||
>
|
||||
{step.title}
|
||||
</h3>
|
||||
{step.tag && (
|
||||
<Tag
|
||||
text={step.tag}
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
className={cls("text-xs text-nowrap", stepTagClassName)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p
|
||||
className={cls(
|
||||
"text-base leading-tight mb-12",
|
||||
useInvertedBackground === "invertDefault" ? "text-background/75" : "text-foreground/75",
|
||||
stepDescriptionClassName
|
||||
)}
|
||||
>
|
||||
{step.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
FeatureProcessSteps.displayName = "FeatureProcessSteps";
|
||||
|
||||
export default memo(FeatureProcessSteps);
|
||||
@@ -0,0 +1,148 @@
|
||||
"use client";
|
||||
|
||||
import CardStack from "@/components/cardStack/CardStack";
|
||||
import FeatureBorderGlowItem from "./FeatureBorderGlowItem";
|
||||
import { 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 FeatureCard {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface FeatureBorderGlowProps {
|
||||
features: FeatureCard[];
|
||||
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;
|
||||
iconContainerClassName?: string;
|
||||
iconClassName?: 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;
|
||||
}
|
||||
|
||||
const FeatureBorderGlow = ({
|
||||
features,
|
||||
carouselMode = "buttons",
|
||||
uniformGridCustomHeightClasses = "min-h-75 2xl:min-h-85",
|
||||
animationType,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
textboxLayout,
|
||||
useInvertedBackground,
|
||||
ariaLabel = "Feature section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
cardClassName = "",
|
||||
iconContainerClassName = "",
|
||||
iconClassName = "",
|
||||
textBoxTitleClassName = "",
|
||||
textBoxTitleImageWrapperClassName = "",
|
||||
textBoxTitleImageClassName = "",
|
||||
textBoxDescriptionClassName = "",
|
||||
cardTitleClassName = "",
|
||||
cardDescriptionClassName = "",
|
||||
gridClassName = "",
|
||||
carouselClassName = "",
|
||||
controlsClassName = "",
|
||||
textBoxClassName = "",
|
||||
textBoxTagClassName = "",
|
||||
textBoxButtonContainerClassName = "",
|
||||
textBoxButtonClassName = "",
|
||||
textBoxButtonTextClassName = "",
|
||||
}: FeatureBorderGlowProps) => {
|
||||
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}
|
||||
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) => (
|
||||
<FeatureBorderGlowItem
|
||||
key={`${feature.title}-${index}`}
|
||||
item={feature}
|
||||
index={index}
|
||||
className={cardClassName}
|
||||
iconContainerClassName={iconContainerClassName}
|
||||
iconClassName={iconClassName}
|
||||
titleClassName={cardTitleClassName}
|
||||
descriptionClassName={cardDescriptionClassName}
|
||||
shouldUseLightText={shouldUseLightText}
|
||||
/>
|
||||
))}
|
||||
</CardStack>
|
||||
);
|
||||
};
|
||||
|
||||
FeatureBorderGlow.displayName = "FeatureBorderGlow";
|
||||
|
||||
export default FeatureBorderGlow;
|
||||
@@ -0,0 +1,88 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { LucideIcon } from "lucide-react";
|
||||
import { GlowingEffect } from "@/components/background/GlowingEffect";
|
||||
import { GLOWING_EFFECT_PROPS } from "./constants";
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
export interface FeatureBorderGlowItemData {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface FeatureBorderGlowItemProps {
|
||||
item: FeatureBorderGlowItemData;
|
||||
index: number;
|
||||
className?: string;
|
||||
iconContainerClassName?: string;
|
||||
iconClassName?: string;
|
||||
titleClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
shouldUseLightText?: boolean;
|
||||
}
|
||||
|
||||
const FeatureBorderGlowItem = memo(function FeatureBorderGlowItem({
|
||||
item,
|
||||
index,
|
||||
className = "",
|
||||
iconContainerClassName = "",
|
||||
iconClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
shouldUseLightText = false,
|
||||
}: FeatureBorderGlowItemProps) {
|
||||
const Icon = item.icon;
|
||||
|
||||
return (
|
||||
<article
|
||||
key={`feature-${index}`}
|
||||
className={cls("card relative rounded-theme-capped min-h-0 h-full", className)}
|
||||
aria-label={item.title}
|
||||
>
|
||||
<div className="relative z-10 w-full h-full p-5 flex flex-col justify-between gap-5">
|
||||
<div
|
||||
className={cls(
|
||||
"h-15 w-[3.75rem] aspect-square primary-button rounded-theme flex items-center justify-center",
|
||||
iconContainerClassName
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
className={cls(
|
||||
"w-[35%] aspect-square text-background",
|
||||
iconClassName
|
||||
)}
|
||||
strokeWidth={1}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3
|
||||
className={cls(
|
||||
"text-2xl font-medium leading-tight",
|
||||
shouldUseLightText && "text-background",
|
||||
titleClassName
|
||||
)}
|
||||
>
|
||||
{item.title}
|
||||
</h3>
|
||||
<p
|
||||
className={cls(
|
||||
"text-sm leading-tight",
|
||||
shouldUseLightText ? "text-background" : "text-foreground",
|
||||
descriptionClassName
|
||||
)}
|
||||
>
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<GlowingEffect {...GLOWING_EFFECT_PROPS} />
|
||||
</article>
|
||||
);
|
||||
});
|
||||
|
||||
FeatureBorderGlowItem.displayName = "FeatureBorderGlowItem";
|
||||
|
||||
export default FeatureBorderGlowItem;
|
||||
@@ -0,0 +1,8 @@
|
||||
export const GLOWING_EFFECT_PROPS = {
|
||||
spread: 40,
|
||||
glow: true,
|
||||
disabled: false,
|
||||
proximity: 64,
|
||||
inactiveZone: 0.01,
|
||||
borderWidth: 1.5,
|
||||
} as const;
|
||||
@@ -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,176 @@
|
||||
"use client";
|
||||
|
||||
import "./FeatureCardThree.css";
|
||||
import { 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);
|
||||
|
||||
|
||||
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={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) => (
|
||||
<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 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 backdrop-blur-xs 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])
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
"use client";
|
||||
|
||||
import CardStack from "@/components/cardStack/CardStack";
|
||||
import FeatureHoverPatternItem from "./FeatureHoverPatternItem";
|
||||
import { 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 FeatureCard {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface FeatureHoverPatternProps {
|
||||
features: FeatureCard[];
|
||||
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;
|
||||
iconContainerClassName?: string;
|
||||
iconClassName?: string;
|
||||
textBoxTitleClassName?: string;
|
||||
textBoxTitleImageWrapperClassName?: string;
|
||||
textBoxTitleImageClassName?: string;
|
||||
textBoxDescriptionClassName?: string;
|
||||
cardTitleClassName?: string;
|
||||
cardDescriptionClassName?: string;
|
||||
gradientClassName?: string;
|
||||
gridClassName?: string;
|
||||
carouselClassName?: string;
|
||||
controlsClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
textBoxTagClassName?: string;
|
||||
textBoxButtonContainerClassName?: string;
|
||||
textBoxButtonClassName?: string;
|
||||
textBoxButtonTextClassName?: string;
|
||||
}
|
||||
|
||||
const FeatureHoverPattern = ({
|
||||
features,
|
||||
carouselMode = "buttons",
|
||||
uniformGridCustomHeightClasses = "min-h-85 2xl:min-h-95",
|
||||
animationType,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
textboxLayout,
|
||||
useInvertedBackground,
|
||||
ariaLabel = "Feature section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
cardClassName = "",
|
||||
iconContainerClassName = "",
|
||||
iconClassName = "",
|
||||
textBoxTitleClassName = "",
|
||||
textBoxTitleImageWrapperClassName = "",
|
||||
textBoxTitleImageClassName = "",
|
||||
textBoxDescriptionClassName = "",
|
||||
cardTitleClassName = "",
|
||||
cardDescriptionClassName = "",
|
||||
gradientClassName = "",
|
||||
gridClassName = "",
|
||||
carouselClassName = "",
|
||||
controlsClassName = "",
|
||||
textBoxClassName = "",
|
||||
textBoxTagClassName = "",
|
||||
textBoxButtonContainerClassName = "",
|
||||
textBoxButtonClassName = "",
|
||||
textBoxButtonTextClassName = "",
|
||||
}: FeatureHoverPatternProps) => {
|
||||
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}
|
||||
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) => (
|
||||
<FeatureHoverPatternItem
|
||||
key={`${feature.title}-${index}`}
|
||||
item={feature}
|
||||
index={index}
|
||||
className={cardClassName}
|
||||
iconContainerClassName={iconContainerClassName}
|
||||
iconClassName={iconClassName}
|
||||
titleClassName={cardTitleClassName}
|
||||
descriptionClassName={cardDescriptionClassName}
|
||||
gradientClassName={gradientClassName}
|
||||
shouldUseLightText={shouldUseLightText}
|
||||
/>
|
||||
))}
|
||||
</CardStack>
|
||||
);
|
||||
};
|
||||
|
||||
FeatureHoverPattern.displayName = "FeatureHoverPattern";
|
||||
|
||||
export default FeatureHoverPattern;
|
||||
@@ -0,0 +1,122 @@
|
||||
"use client";
|
||||
|
||||
import { memo, useRef } from "react";
|
||||
import { useMotionValue } from "framer-motion";
|
||||
import { LucideIcon } from "lucide-react";
|
||||
import { CardPattern } from "@/components/background/CardPattern";
|
||||
import { usePatternInteraction } from "./usePatternInteraction";
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
export interface FeatureHoverPatternItemData {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface FeatureHoverPatternItemProps {
|
||||
item: FeatureHoverPatternItemData;
|
||||
index: number;
|
||||
className?: string;
|
||||
iconContainerClassName?: string;
|
||||
iconClassName?: string;
|
||||
titleClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
gradientClassName?: string;
|
||||
shouldUseLightText?: boolean;
|
||||
}
|
||||
|
||||
const FeatureHoverPatternItem = memo(function FeatureHoverPatternItem({
|
||||
item,
|
||||
index,
|
||||
className = "",
|
||||
iconContainerClassName = "",
|
||||
iconClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
gradientClassName = "",
|
||||
shouldUseLightText = false,
|
||||
}: FeatureHoverPatternItemProps) {
|
||||
const Icon = item.icon;
|
||||
const mouseX = useMotionValue(0);
|
||||
const mouseY = useMotionValue(0);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { state, onMouseMove } = usePatternInteraction(
|
||||
mouseX,
|
||||
mouseY,
|
||||
containerRef
|
||||
);
|
||||
|
||||
return (
|
||||
<article
|
||||
key={`feature-${index}`}
|
||||
className={cls(
|
||||
"card rounded-theme-capped min-h-0 h-full",
|
||||
className
|
||||
)}
|
||||
aria-label={item.title}
|
||||
>
|
||||
<div className="relative z-10 w-full h-full p-5 flex flex-col gap-5 justify-between">
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cls(
|
||||
"group/primary-button relative w-full h-full flex items-center justify-center",
|
||||
state.isMobile && state.isInView ? "group/primary-button-active" : ""
|
||||
)}
|
||||
onMouseMove={onMouseMove}
|
||||
>
|
||||
<div
|
||||
className={cls(
|
||||
"relative z-20 h-15 w-auto aspect-square primary-button rounded-theme transition-all duration-300 flex items-center justify-center shadow",
|
||||
iconContainerClassName
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
className={cls(
|
||||
"w-[35%] aspect-square text-background",
|
||||
iconClassName
|
||||
)}
|
||||
strokeWidth={1}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div className="opacity-25">
|
||||
<CardPattern
|
||||
mouseX={mouseX}
|
||||
mouseY={mouseY}
|
||||
randomString={state.randomString}
|
||||
isActive={state.isMobile && state.isInView}
|
||||
gradientClassName={
|
||||
gradientClassName || "bg-gradient-to-r from-accent to-background-accent"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3
|
||||
className={cls(
|
||||
"text-2xl font-medium leading-tight",
|
||||
shouldUseLightText && "text-background",
|
||||
titleClassName
|
||||
)}
|
||||
>
|
||||
{item.title}
|
||||
</h3>
|
||||
<p
|
||||
className={cls(
|
||||
"text-sm leading-tight",
|
||||
shouldUseLightText ? "text-background" : "text-foreground",
|
||||
descriptionClassName
|
||||
)}
|
||||
>
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
});
|
||||
|
||||
FeatureHoverPatternItem.displayName = "FeatureHoverPatternItem";
|
||||
|
||||
export default FeatureHoverPatternItem;
|
||||
@@ -0,0 +1,17 @@
|
||||
export const MOBILE_BREAKPOINT = 768;
|
||||
export const RANDOM_STRING_LENGTH = 1500;
|
||||
export const VIEW_CHECK_INTERVAL = 100;
|
||||
export const PATTERN_VISIBILITY_THRESHOLD = 0.2;
|
||||
export const ICON_VISIBILITY_THRESHOLD = 0.4;
|
||||
export const THROTTLE_DELAY = 16;
|
||||
export const CHARACTERS =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
export const CHARACTERS_LENGTH = CHARACTERS.length;
|
||||
|
||||
export const generateRandomString = (length: number): string => {
|
||||
let result = "";
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += CHARACTERS[Math.floor(Math.random() * CHARACTERS_LENGTH)];
|
||||
}
|
||||
return result;
|
||||
};
|
||||
@@ -0,0 +1,137 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
useRef,
|
||||
useCallback,
|
||||
MouseEvent,
|
||||
RefObject,
|
||||
} from "react";
|
||||
import { MotionValue } from "framer-motion";
|
||||
import {
|
||||
MOBILE_BREAKPOINT,
|
||||
VIEW_CHECK_INTERVAL,
|
||||
PATTERN_VISIBILITY_THRESHOLD,
|
||||
ICON_VISIBILITY_THRESHOLD,
|
||||
THROTTLE_DELAY,
|
||||
RANDOM_STRING_LENGTH,
|
||||
generateRandomString,
|
||||
} from "./constants";
|
||||
|
||||
interface InteractionState {
|
||||
randomString: string;
|
||||
isMobile: boolean;
|
||||
isInView: boolean;
|
||||
isIconActive: boolean;
|
||||
}
|
||||
|
||||
export function usePatternInteraction(
|
||||
mouseX: MotionValue<number>,
|
||||
mouseY: MotionValue<number>,
|
||||
containerRef: RefObject<HTMLDivElement | null>
|
||||
) {
|
||||
const lastRandomUpdateRef = useRef<number>(0);
|
||||
|
||||
const [state, setState] = useState<InteractionState>({
|
||||
randomString: "",
|
||||
isMobile: false,
|
||||
isInView: false,
|
||||
isIconActive: false,
|
||||
});
|
||||
|
||||
const checkMobile = useCallback(() => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
isMobile: window.innerWidth < MOBILE_BREAKPOINT,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
randomString: generateRandomString(RANDOM_STRING_LENGTH),
|
||||
isMobile: window.innerWidth < MOBILE_BREAKPOINT,
|
||||
}));
|
||||
|
||||
window.addEventListener("resize", checkMobile);
|
||||
return () => window.removeEventListener("resize", checkMobile);
|
||||
}, [checkMobile]);
|
||||
|
||||
const updateRandomString = useCallback(() => {
|
||||
const now = Date.now();
|
||||
if (now - lastRandomUpdateRef.current > THROTTLE_DELAY) {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
randomString: generateRandomString(RANDOM_STRING_LENGTH),
|
||||
}));
|
||||
lastRandomUpdateRef.current = now;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const updateMobilePosition = useCallback(() => {
|
||||
if (!containerRef.current) return;
|
||||
const { left, top } = containerRef.current.getBoundingClientRect();
|
||||
const viewportCenterX = window.innerWidth / 2;
|
||||
const viewportCenterY = window.innerHeight / 2;
|
||||
|
||||
mouseX.set(viewportCenterX - left);
|
||||
mouseY.set(viewportCenterY - top);
|
||||
updateRandomString();
|
||||
}, [mouseX, mouseY, updateRandomString, containerRef]);
|
||||
|
||||
const checkInView = useCallback(() => {
|
||||
if (!containerRef.current) return;
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
const viewportHeight = window.innerHeight;
|
||||
const threshold = viewportHeight * PATTERN_VISIBILITY_THRESHOLD;
|
||||
const iconThreshold = viewportHeight * ICON_VISIBILITY_THRESHOLD;
|
||||
|
||||
const inView =
|
||||
rect.top < viewportHeight - threshold && rect.bottom > threshold;
|
||||
const iconActive =
|
||||
rect.top < viewportHeight - iconThreshold &&
|
||||
rect.bottom > iconThreshold;
|
||||
|
||||
setState((prev) => ({ ...prev, isInView: inView, isIconActive: iconActive }));
|
||||
|
||||
if (inView) {
|
||||
updateMobilePosition();
|
||||
}
|
||||
}, [updateMobilePosition, containerRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!state.isMobile) {
|
||||
setState((prev) => ({ ...prev, isInView: false, isIconActive: false }));
|
||||
return;
|
||||
}
|
||||
|
||||
checkInView();
|
||||
const interval = setInterval(checkInView, VIEW_CHECK_INTERVAL);
|
||||
window.addEventListener("scroll", checkInView, { passive: true });
|
||||
window.addEventListener("resize", checkInView);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
window.removeEventListener("scroll", checkInView);
|
||||
window.removeEventListener("resize", checkInView);
|
||||
};
|
||||
}, [state.isMobile, checkInView]);
|
||||
|
||||
const onMouseMove = useCallback(
|
||||
(event: MouseEvent<HTMLDivElement>) => {
|
||||
if (state.isMobile) return;
|
||||
|
||||
const { left, top } = event.currentTarget.getBoundingClientRect();
|
||||
mouseX.set(event.clientX - left);
|
||||
mouseY.set(event.clientY - top);
|
||||
updateRandomString();
|
||||
},
|
||||
[state.isMobile, mouseX, mouseY, updateRandomString]
|
||||
);
|
||||
|
||||
return {
|
||||
state,
|
||||
onMouseMove,
|
||||
};
|
||||
}
|
||||
108
src/components/sections/footer/FooterBase.tsx
Normal file
108
src/components/sections/footer/FooterBase.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
"use client";
|
||||
|
||||
// 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 = ({
|
||||
// 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 = "",
|
||||
}: FooterBaseProps) => {
|
||||
return (
|
||||
<footer
|
||||
role="contentinfo"
|
||||
aria-label={ariaLabel}
|
||||
className={cls("relative overflow-hidden w-full primary-button text-background py-15 mt-20", 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;
|
||||
108
src/components/sections/footer/FooterBaseCard.tsx
Normal file
108
src/components/sections/footer/FooterBaseCard.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
"use client";
|
||||
|
||||
// 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 = ({
|
||||
// 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 = "",
|
||||
}: FooterBaseCardProps) => {
|
||||
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 { 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 = ({
|
||||
// logoSrc,
|
||||
// logoWidth,
|
||||
// logoHeight,
|
||||
columns,
|
||||
copyrightText,
|
||||
onPrivacyClick,
|
||||
ariaLabel,
|
||||
className = "",
|
||||
wrapperClassName = "",
|
||||
containerClassName = "",
|
||||
footerClassName,
|
||||
footerContainerClassName,
|
||||
// logoClassName,
|
||||
columnsClassName,
|
||||
columnClassName,
|
||||
columnTitleClassName,
|
||||
columnItemClassName,
|
||||
copyrightContainerClassName,
|
||||
copyrightTextClassName,
|
||||
privacyButtonClassName,
|
||||
}: FooterBaseRevealProps) => {
|
||||
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 mt-20", 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={cls("mt-0", 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;
|
||||
81
src/components/sections/footer/FooterCard.tsx
Normal file
81
src/components/sections/footer/FooterCard.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
"use client";
|
||||
|
||||
import FillWidthText from "@/components/shared/FillWidthText/FillWidthText";
|
||||
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;
|
||||
logoLineHeight?: number;
|
||||
copyrightText?: string;
|
||||
socialLinks?: SocialLink[];
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
cardClassName?: string;
|
||||
logoClassName?: string;
|
||||
dividerClassName?: string;
|
||||
copyrightContainerClassName?: string;
|
||||
copyrightTextClassName?: string;
|
||||
socialContainerClassName?: string;
|
||||
socialIconClassName?: string;
|
||||
}
|
||||
|
||||
const FooterCard = ({
|
||||
// logoSrc,
|
||||
// logoAlt = "Logo",
|
||||
logoText = "Webild",
|
||||
logoLineHeight = 1.1,
|
||||
copyrightText = `© 2025 | Webild`,
|
||||
socialLinks,
|
||||
ariaLabel = "Site footer",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
cardClassName = "",
|
||||
logoClassName = "",
|
||||
dividerClassName = "",
|
||||
copyrightContainerClassName = "",
|
||||
copyrightTextClassName = "",
|
||||
socialContainerClassName = "",
|
||||
socialIconClassName = "",
|
||||
}: FooterCardProps) => {
|
||||
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)}>
|
||||
<div className={cls("relative z-1 w-full", logoClassName)}>
|
||||
<FillWidthText lineHeight={logoLineHeight}>
|
||||
{logoText}
|
||||
</FillWidthText>
|
||||
</div>
|
||||
|
||||
<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;
|
||||
120
src/components/sections/footer/FooterLogoEmphasis.tsx
Normal file
120
src/components/sections/footer/FooterLogoEmphasis.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
"use client";
|
||||
|
||||
import ButtonTextUnderline from "@/components/button/ButtonTextUnderline";
|
||||
import FillWidthText from "@/components/shared/FillWidthText/FillWidthText";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
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 = ({
|
||||
// logoSrc,
|
||||
// logoAlt = "Logo",
|
||||
columns,
|
||||
logoText = "Webild",
|
||||
ariaLabel = "Site footer",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
logoClassName = "",
|
||||
columnsClassName = "",
|
||||
columnClassName = "",
|
||||
itemClassName = "",
|
||||
iconClassName = "",
|
||||
buttonClassName = "",
|
||||
}: FooterLogoEmphasisProps) => {
|
||||
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 py-15 mt-20 flex justify-center relative z-1 overflow-hidden primary-button text-background rounded-t-theme-capped",
|
||||
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
|
||||
)}
|
||||
>
|
||||
<div className={cls("relative z-1 w-full", logoClassName)}>
|
||||
<FillWidthText lineHeight={1.1}>
|
||||
{logoText}
|
||||
</FillWidthText>
|
||||
</div>
|
||||
|
||||
<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;
|
||||
89
src/components/sections/footer/FooterLogoReveal.tsx
Normal file
89
src/components/sections/footer/FooterLogoReveal.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
"use client";
|
||||
|
||||
import { useRef, useEffect, useState } from "react";
|
||||
import FillWidthText from "@/components/shared/FillWidthText/FillWidthText";
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
interface FooterLogoRevealProps {
|
||||
// logoSrc?: string;
|
||||
// logoAlt?: string;
|
||||
logoText?: string;
|
||||
logoLineHeight?: number;
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
wrapperClassName?: string;
|
||||
containerClassName?: string;
|
||||
logoClassName?: string;
|
||||
}
|
||||
|
||||
const FooterLogoReveal = ({
|
||||
// logoSrc,
|
||||
// logoAlt = "Logo",
|
||||
logoText = "Webild",
|
||||
logoLineHeight = 1.1,
|
||||
ariaLabel = "Site footer",
|
||||
className = "",
|
||||
wrapperClassName = "",
|
||||
containerClassName = "",
|
||||
logoClassName = "",
|
||||
}: FooterLogoRevealProps) => {
|
||||
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 mt-20", 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">
|
||||
<div className={cls("relative z-1 w-full", logoClassName)}>
|
||||
<FillWidthText lineHeight={logoLineHeight}>
|
||||
{logoText}
|
||||
</FillWidthText>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
FooterLogoReveal.displayName = "FooterLogoReveal";
|
||||
|
||||
export default FooterLogoReveal;
|
||||
143
src/components/sections/footer/FooterMedia.tsx
Normal file
143
src/components/sections/footer/FooterMedia.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
"use client";
|
||||
|
||||
// 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 = ({
|
||||
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 = "",
|
||||
}: FooterMediaProps) => {
|
||||
return (
|
||||
<footer
|
||||
role="contentinfo"
|
||||
aria-label={ariaLabel}
|
||||
className={cls("relative overflow-hidden w-full mt-20", 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;
|
||||
75
src/components/sections/footer/FooterSimple.tsx
Normal file
75
src/components/sections/footer/FooterSimple.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import FooterColumns from "@/components/shared/FooterColumns";
|
||||
import { cls } from "@/lib/utils";
|
||||
import type { FooterColumn } from "@/components/shared/FooterColumns";
|
||||
|
||||
interface FooterSimpleProps {
|
||||
columns: FooterColumn[];
|
||||
bottomLeftText: string;
|
||||
bottomRightText: string;
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
columnsClassName?: string;
|
||||
columnClassName?: string;
|
||||
columnTitleClassName?: string;
|
||||
columnItemClassName?: string;
|
||||
dividerClassName?: string;
|
||||
bottomContainerClassName?: string;
|
||||
bottomLeftTextClassName?: string;
|
||||
bottomRightTextClassName?: string;
|
||||
}
|
||||
|
||||
const FooterSimple = ({
|
||||
columns,
|
||||
bottomLeftText,
|
||||
bottomRightText,
|
||||
ariaLabel = "Site footer",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
columnsClassName = "",
|
||||
columnClassName = "",
|
||||
columnTitleClassName = "",
|
||||
columnItemClassName = "",
|
||||
dividerClassName = "",
|
||||
bottomContainerClassName = "",
|
||||
bottomLeftTextClassName = "",
|
||||
bottomRightTextClassName = "",
|
||||
}: FooterSimpleProps) => {
|
||||
return (
|
||||
<footer
|
||||
role="contentinfo"
|
||||
aria-label={ariaLabel}
|
||||
className={cls("relative w-full pt-20 pb-10", className)}
|
||||
>
|
||||
<div className={cls("w-content-width md:w-60 mx-auto", containerClassName)}>
|
||||
<FooterColumns
|
||||
columns={columns}
|
||||
className={cls("w-full! justify-between mb-10", columnsClassName)}
|
||||
columnClassName={columnClassName}
|
||||
columnTitleClassName={columnTitleClassName}
|
||||
columnItemClassName={columnItemClassName}
|
||||
/>
|
||||
<div
|
||||
className={cls("w-full h-px bg-foreground/20", dividerClassName)}
|
||||
/>
|
||||
<div
|
||||
className={cls("w-full flex items-center justify-between pt-6", bottomContainerClassName)}
|
||||
>
|
||||
<p className={cls("text-foreground/50 text-sm", bottomLeftTextClassName)}>
|
||||
{bottomLeftText}
|
||||
</p>
|
||||
<p className={cls("text-foreground/50 text-sm", bottomRightTextClassName)}>
|
||||
{bottomRightText}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
FooterSimple.displayName = "FooterSimple";
|
||||
|
||||
export default memo(FooterSimple);
|
||||
116
src/components/sections/hero/HeroBillboard.tsx
Normal file
116
src/components/sections/hero/HeroBillboard.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
"use client";
|
||||
|
||||
import TextBox from "@/components/Textbox";
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
|
||||
import { cls } from "@/lib/utils";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
|
||||
type HeroBillboardBackgroundProps = Extract<
|
||||
HeroBackgroundVariantProps,
|
||||
| { variant: "plain" }
|
||||
| { variant: "animated-grid" }
|
||||
| { variant: "canvas-reveal" }
|
||||
| { variant: "cell-wave" }
|
||||
| { variant: "downward-rays-animated" }
|
||||
| { variant: "downward-rays-animated-grid" }
|
||||
| { variant: "downward-rays-static" }
|
||||
| { variant: "downward-rays-static-grid" }
|
||||
| { variant: "gradient-bars" }
|
||||
| { variant: "radial-gradient" }
|
||||
| { variant: "rotated-rays-animated" }
|
||||
| { variant: "rotated-rays-animated-grid" }
|
||||
| { variant: "rotated-rays-static" }
|
||||
| { variant: "rotated-rays-static-grid" }
|
||||
| { variant: "sparkles-gradient" }
|
||||
>;
|
||||
|
||||
interface HeroBillboardProps {
|
||||
title: string;
|
||||
description: string;
|
||||
background: HeroBillboardBackgroundProps;
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
buttons?: ButtonConfig[];
|
||||
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 HeroBillboard = ({
|
||||
title,
|
||||
description,
|
||||
background,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
imageAlt = "",
|
||||
videoAriaLabel = "Hero video",
|
||||
ariaLabel = "Hero section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
imageClassName = "",
|
||||
}: HeroBillboardProps) => {
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("relative w-full py-hero-page-padding", className)}
|
||||
>
|
||||
<HeroBackgrounds {...background} />
|
||||
<div className={cls("w-content-width mx-auto flex flex-col gap-14 md:gap-15 relative z-10", containerClassName)}>
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
buttons={buttons}
|
||||
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 className={cls("w-full overflow-hidden rounded-theme-capped card p-4", mediaWrapperClassName)}>
|
||||
<MediaContent
|
||||
imageSrc={imageSrc}
|
||||
videoSrc={videoSrc}
|
||||
imageAlt={imageAlt}
|
||||
videoAriaLabel={videoAriaLabel}
|
||||
imageClassName={cls("z-1", imageClassName)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroBillboard.displayName = "HeroBillboard";
|
||||
|
||||
export default HeroBillboard;
|
||||
149
src/components/sections/hero/HeroBillboardCarousel.tsx
Normal file
149
src/components/sections/hero/HeroBillboardCarousel.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
"use client";
|
||||
|
||||
import TextBox from "@/components/Textbox";
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
import AutoCarousel from "@/components/cardStack/layouts/carousels/AutoCarousel";
|
||||
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
|
||||
import { cls } from "@/lib/utils";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
|
||||
export interface MediaItem {
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
}
|
||||
|
||||
type HeroBillboardCarouselBackgroundProps = Extract<
|
||||
HeroBackgroundVariantProps,
|
||||
| { variant: "plain" }
|
||||
| { variant: "animated-grid" }
|
||||
| { variant: "canvas-reveal" }
|
||||
| { variant: "cell-wave" }
|
||||
| { variant: "downward-rays-animated" }
|
||||
| { variant: "downward-rays-animated-grid" }
|
||||
| { variant: "downward-rays-static" }
|
||||
| { variant: "downward-rays-static-grid" }
|
||||
| { variant: "gradient-bars" }
|
||||
| { variant: "radial-gradient" }
|
||||
| { variant: "rotated-rays-animated" }
|
||||
| { variant: "rotated-rays-animated-grid" }
|
||||
| { variant: "rotated-rays-static" }
|
||||
| { variant: "rotated-rays-static-grid" }
|
||||
| { variant: "sparkles-gradient" }
|
||||
>;
|
||||
|
||||
interface HeroBillboardCarouselProps {
|
||||
title: string;
|
||||
description: string;
|
||||
background: HeroBillboardCarouselBackgroundProps;
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
buttons?: ButtonConfig[];
|
||||
mediaItems: MediaItem[];
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
titleClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
tagClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
mediaWrapperClassName?: string;
|
||||
}
|
||||
|
||||
const HeroBillboardCarousel = ({
|
||||
title,
|
||||
description,
|
||||
background,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
mediaItems,
|
||||
ariaLabel = "Hero section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
}: HeroBillboardCarouselProps) => {
|
||||
const renderCarouselItem = (item: MediaItem, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className="w-full aspect-[4/5] overflow-hidden rounded-theme-capped card p-2 shadow-lg"
|
||||
>
|
||||
<MediaContent
|
||||
imageSrc={item.imageSrc}
|
||||
videoSrc={item.videoSrc}
|
||||
imageAlt={item.imageAlt || ""}
|
||||
videoAriaLabel={item.videoAriaLabel || "Carousel media"}
|
||||
imageClassName="z-1 h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls(
|
||||
"relative w-full py-hero-page-padding md:h-svh md:py-0",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<HeroBackgrounds {...background} />
|
||||
<div className={cls(
|
||||
"mx-auto flex flex-col gap-14 md:gap-10 relative z-10",
|
||||
"w-full md:w-content-width md:h-full md:items-center md:justify-center",
|
||||
containerClassName
|
||||
)}>
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
buttons={buttons}
|
||||
className={cls(
|
||||
"flex flex-col gap-3 md:gap-1 w-content-width mx-auto",
|
||||
textBoxClassName
|
||||
)}
|
||||
titleClassName={cls("text-6xl font-medium text-balance", titleClassName)}
|
||||
descriptionClassName={cls("text-base md:text-lg leading-[1.2]", descriptionClassName)}
|
||||
tagClassName={cls("px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-3", tagClassName)}
|
||||
buttonContainerClassName={cls("flex gap-4 mt-3", buttonContainerClassName)}
|
||||
buttonClassName={buttonClassName}
|
||||
buttonTextClassName={buttonTextClassName}
|
||||
center={true}
|
||||
/>
|
||||
|
||||
<div className={cls("w-full -mx-[var(--content-padding)]", mediaWrapperClassName)}>
|
||||
<AutoCarousel
|
||||
title=""
|
||||
description=""
|
||||
textboxLayout="default"
|
||||
animationType="none"
|
||||
className="py-0"
|
||||
carouselClassName="py-0"
|
||||
containerClassName="!w-full"
|
||||
itemClassName="!w-55 md:!w-carousel-item-4"
|
||||
ariaLabel="Hero carousel"
|
||||
showTextBox={false}
|
||||
>
|
||||
{mediaItems?.map(renderCarouselItem)}
|
||||
</AutoCarousel>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroBillboardCarousel.displayName = "HeroBillboardCarousel";
|
||||
|
||||
export default HeroBillboardCarousel;
|
||||
189
src/components/sections/hero/HeroBillboardGallery.tsx
Normal file
189
src/components/sections/hero/HeroBillboardGallery.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
"use client";
|
||||
|
||||
import TextBox from "@/components/Textbox";
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
import AutoCarousel from "@/components/cardStack/layouts/carousels/AutoCarousel";
|
||||
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
|
||||
import { cls } from "@/lib/utils";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
|
||||
export interface MediaItem {
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
}
|
||||
|
||||
type HeroBillboardGalleryBackgroundProps = Extract<
|
||||
HeroBackgroundVariantProps,
|
||||
| { variant: "plain" }
|
||||
| { variant: "animated-grid" }
|
||||
| { variant: "canvas-reveal" }
|
||||
| { variant: "cell-wave" }
|
||||
| { variant: "downward-rays-animated" }
|
||||
| { variant: "downward-rays-animated-grid" }
|
||||
| { variant: "downward-rays-static" }
|
||||
| { variant: "downward-rays-static-grid" }
|
||||
| { variant: "gradient-bars" }
|
||||
| { variant: "radial-gradient" }
|
||||
| { variant: "rotated-rays-animated" }
|
||||
| { variant: "rotated-rays-animated-grid" }
|
||||
| { variant: "rotated-rays-static" }
|
||||
| { variant: "rotated-rays-static-grid" }
|
||||
| { variant: "sparkles-gradient" }
|
||||
>;
|
||||
|
||||
interface HeroBillboardGalleryProps {
|
||||
title: string;
|
||||
description: string;
|
||||
background: HeroBillboardGalleryBackgroundProps;
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
buttons?: ButtonConfig[];
|
||||
mediaItems: MediaItem[];
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
titleClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
tagClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
mediaWrapperClassName?: string;
|
||||
imageClassName?: string;
|
||||
}
|
||||
|
||||
const HeroBillboardGallery = ({
|
||||
title,
|
||||
description,
|
||||
background,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
mediaItems,
|
||||
ariaLabel = "Hero section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
imageClassName = "",
|
||||
}: HeroBillboardGalleryProps) => {
|
||||
const renderCarouselItem = (item: MediaItem, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className="w-full aspect-[4/5] overflow-hidden rounded-theme-capped card p-2 shadow-lg"
|
||||
>
|
||||
<MediaContent
|
||||
imageSrc={item.imageSrc}
|
||||
videoSrc={item.videoSrc}
|
||||
imageAlt={item.imageAlt || ""}
|
||||
videoAriaLabel={item.videoAriaLabel || "Gallery media"}
|
||||
imageClassName="h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const itemCount = mediaItems?.length || 0;
|
||||
const desktopWidthClass = itemCount === 3 ? "md:w-[24%]" : itemCount === 4 ? "md:w-[24%]" : "md:w-[23%]";
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls(
|
||||
"relative w-full py-hero-page-padding md:h-svh md:py-0",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<HeroBackgrounds {...background} />
|
||||
<div className={cls(
|
||||
"mx-auto flex flex-col gap-14 relative z-10",
|
||||
"w-full md:w-content-width md:h-full md:items-center md:justify-center",
|
||||
containerClassName
|
||||
)}>
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
buttons={buttons}
|
||||
className={cls(
|
||||
"flex flex-col gap-3 md:gap-1 w-content-width mx-auto",
|
||||
textBoxClassName
|
||||
)}
|
||||
titleClassName={cls("text-6xl font-medium text-balance", titleClassName)}
|
||||
descriptionClassName={cls("text-base md:text-lg leading-[1.2]", descriptionClassName)}
|
||||
tagClassName={cls("px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-3", tagClassName)}
|
||||
buttonContainerClassName={cls("flex gap-4 mt-3", buttonContainerClassName)}
|
||||
buttonClassName={buttonClassName}
|
||||
buttonTextClassName={buttonTextClassName}
|
||||
center={true}
|
||||
/>
|
||||
|
||||
<div className={cls("w-full", mediaWrapperClassName)}>
|
||||
<div className="block md:hidden -mx-[var(--content-padding)]">
|
||||
<AutoCarousel
|
||||
title=""
|
||||
description=""
|
||||
textboxLayout="default"
|
||||
animationType="none"
|
||||
className="py-0"
|
||||
carouselClassName="py-0"
|
||||
containerClassName="!w-full"
|
||||
itemClassName="!w-55"
|
||||
ariaLabel="Hero gallery carousel"
|
||||
showTextBox={false}
|
||||
>
|
||||
{mediaItems?.slice(0, 5).map(renderCarouselItem)}
|
||||
</AutoCarousel>
|
||||
</div>
|
||||
|
||||
<div className="hidden md:flex justify-center items-center pt-2">
|
||||
<div className="relative flex items-center justify-center w-full">
|
||||
{mediaItems?.slice(0, 5).map((item, index) => {
|
||||
const rotations = ["-rotate-6", "rotate-6", "-rotate-6", "rotate-6", "-rotate-6"];
|
||||
const zIndexes = ["z-10", "z-20", "z-30", "z-40", "z-50"];
|
||||
const translates = ["-translate-y-5", "translate-y-5", "-translate-y-5", "translate-y-5", "-translate-y-5"];
|
||||
const marginClass = index > 0 ? "-ml-12 md:-ml-15" : "";
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={cls(
|
||||
"relative aspect-[4/5] overflow-hidden rounded-theme-capped card p-2 shadow-lg transition-transform duration-500 ease-out hover:scale-110",
|
||||
desktopWidthClass,
|
||||
rotations[index],
|
||||
zIndexes[index],
|
||||
translates[index],
|
||||
marginClass
|
||||
)}
|
||||
>
|
||||
<MediaContent
|
||||
imageSrc={item.imageSrc}
|
||||
videoSrc={item.videoSrc}
|
||||
imageAlt={item.imageAlt || ""}
|
||||
videoAriaLabel={item.videoAriaLabel || "Gallery media"}
|
||||
imageClassName={cls("z-1 h-full object-cover", imageClassName)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroBillboardGallery.displayName = "HeroBillboardGallery";
|
||||
|
||||
export default HeroBillboardGallery;
|
||||
121
src/components/sections/hero/HeroBillboardRotatedCarousel.tsx
Normal file
121
src/components/sections/hero/HeroBillboardRotatedCarousel.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
"use client";
|
||||
|
||||
|
||||
import TextBox from "@/components/Textbox";
|
||||
import AngledCarousel from "@/components/cardStack/layouts/carousels/AngledCarousel";
|
||||
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
|
||||
import { cls } from "@/lib/utils";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
|
||||
type HeroBillboardRotatedCarouselBackgroundProps = Extract<
|
||||
HeroBackgroundVariantProps,
|
||||
| { variant: "plain" }
|
||||
| { variant: "animated-grid" }
|
||||
| { variant: "canvas-reveal" }
|
||||
| { variant: "cell-wave" }
|
||||
| { variant: "downward-rays-animated" }
|
||||
| { variant: "downward-rays-animated-grid" }
|
||||
| { variant: "downward-rays-static" }
|
||||
| { variant: "downward-rays-static-grid" }
|
||||
| { variant: "gradient-bars" }
|
||||
| { variant: "radial-gradient" }
|
||||
| { variant: "rotated-rays-animated" }
|
||||
| { variant: "rotated-rays-animated-grid" }
|
||||
| { variant: "rotated-rays-static" }
|
||||
| { variant: "rotated-rays-static-grid" }
|
||||
| { variant: "sparkles-gradient" }
|
||||
>;
|
||||
|
||||
interface CarouselItem {
|
||||
id: string;
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
}
|
||||
|
||||
interface HeroBillboardRotatedCarouselProps {
|
||||
title: string;
|
||||
description: string;
|
||||
background: HeroBillboardRotatedCarouselBackgroundProps;
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
buttons?: ButtonConfig[];
|
||||
carouselItems: CarouselItem[];
|
||||
autoPlay?: boolean;
|
||||
autoPlayInterval?: number;
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
titleClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
tagClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
carouselClassName?: string;
|
||||
}
|
||||
|
||||
const HeroBillboardRotatedCarousel = ({
|
||||
title,
|
||||
description,
|
||||
background,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
carouselItems,
|
||||
autoPlay = true,
|
||||
autoPlayInterval = 4000,
|
||||
ariaLabel = "Hero section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
carouselClassName = "",
|
||||
}: HeroBillboardRotatedCarouselProps) => {
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("relative w-full h-fit md:min-h-svh flex items-center justify-center py-hero-page-padding", className)}
|
||||
>
|
||||
<HeroBackgrounds {...background} />
|
||||
<div className={cls("w-full flex flex-col gap-14 md:gap-15 relative z-10", containerClassName)}>
|
||||
<div className="w-content-width mx-auto" >
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
buttons={buttons}
|
||||
className={cls("flex flex-col gap-3 md:gap-1", textBoxClassName)}
|
||||
titleClassName={cls("text-6xl font-medium text-balance", titleClassName)}
|
||||
descriptionClassName={cls("text-base md:text-lg leading-[1.2]", descriptionClassName)}
|
||||
tagClassName={cls("px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-3", tagClassName)}
|
||||
buttonContainerClassName={cls("flex gap-4 mt-3", buttonContainerClassName)}
|
||||
buttonClassName={buttonClassName}
|
||||
buttonTextClassName={buttonTextClassName}
|
||||
center={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AngledCarousel
|
||||
items={carouselItems}
|
||||
autoPlay={autoPlay}
|
||||
autoPlayInterval={autoPlayInterval}
|
||||
className={carouselClassName}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroBillboardRotatedCarousel.displayName = "HeroBillboardRotatedCarousel";
|
||||
|
||||
export default HeroBillboardRotatedCarousel;
|
||||
162
src/components/sections/hero/HeroBillboardScroll.tsx
Normal file
162
src/components/sections/hero/HeroBillboardScroll.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
"use client";
|
||||
|
||||
import { useRef } from "react";
|
||||
import { useScroll, useTransform, motion } from "motion/react";
|
||||
import TextBox from "@/components/Textbox";
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
|
||||
import { cls } from "@/lib/utils";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
|
||||
type HeroBillboardScrollBackgroundProps = Extract<
|
||||
HeroBackgroundVariantProps,
|
||||
| { variant: "plain" }
|
||||
| { variant: "animated-grid" }
|
||||
| { variant: "canvas-reveal" }
|
||||
| { variant: "cell-wave" }
|
||||
| { variant: "downward-rays-animated" }
|
||||
| { variant: "downward-rays-animated-grid" }
|
||||
| { variant: "downward-rays-static" }
|
||||
| { variant: "downward-rays-static-grid" }
|
||||
| { variant: "gradient-bars" }
|
||||
| { variant: "radial-gradient" }
|
||||
| { variant: "rotated-rays-animated" }
|
||||
| { variant: "rotated-rays-animated-grid" }
|
||||
| { variant: "rotated-rays-static" }
|
||||
| { variant: "rotated-rays-static-grid" }
|
||||
| { variant: "sparkles-gradient" }
|
||||
>;
|
||||
|
||||
interface HeroBillboardScrollProps {
|
||||
title: string;
|
||||
description: string;
|
||||
background: HeroBillboardScrollBackgroundProps;
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
buttons?: ButtonConfig[];
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
titleClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
tagClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
cardWrapperClassName?: string;
|
||||
cardInnerClassName?: string;
|
||||
imageClassName?: string;
|
||||
}
|
||||
|
||||
const HeroBillboardScroll = ({
|
||||
title,
|
||||
description,
|
||||
background,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
imageAlt = "",
|
||||
videoAriaLabel = "Hero video",
|
||||
ariaLabel = "Hero section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
cardWrapperClassName = "",
|
||||
cardInnerClassName = "",
|
||||
imageClassName = "",
|
||||
}: HeroBillboardScrollProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const { scrollYProgress } = useScroll({
|
||||
target: containerRef,
|
||||
});
|
||||
|
||||
const rotate = useTransform(scrollYProgress, [0, 1], [20, 0]);
|
||||
const scale = useTransform(scrollYProgress, [0, 1], [1.05, 1]);
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
ref={containerRef}
|
||||
className={cls("relative h-fit flex items-center justify-center", className)}
|
||||
>
|
||||
<HeroBackgrounds {...background} />
|
||||
<div
|
||||
className={cls("py-hero-page-padding w-full relative z-10", containerClassName)}
|
||||
style={{
|
||||
perspective: "1000px",
|
||||
}}
|
||||
>
|
||||
<div className="w-content-width mx-auto">
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
buttons={buttons}
|
||||
className={cls("flex flex-col gap-3 md:gap-1", textBoxClassName)}
|
||||
titleClassName={cls("text-6xl font-medium text-balance", titleClassName)}
|
||||
descriptionClassName={cls("text-base md:text-lg leading-[1.2]", descriptionClassName)}
|
||||
tagClassName={cls("px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-3", tagClassName)}
|
||||
buttonContainerClassName={cls("flex gap-4 mt-3", buttonContainerClassName)}
|
||||
buttonClassName={buttonClassName}
|
||||
buttonTextClassName={buttonTextClassName}
|
||||
center={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cls("relative w-content-width h-[50svh] mt-8 mx-auto md:hidden", cardWrapperClassName)}
|
||||
style={{
|
||||
transform: "rotateX(20deg)",
|
||||
}}
|
||||
>
|
||||
<div className={cls("h-full w-full overflow-hidden rounded-theme-capped card p-4", cardInnerClassName)}>
|
||||
<MediaContent
|
||||
imageSrc={imageSrc}
|
||||
videoSrc={videoSrc}
|
||||
imageAlt={imageAlt}
|
||||
videoAriaLabel={videoAriaLabel}
|
||||
imageClassName={cls("z-1 h-full w-full object-cover object-left-top", imageClassName)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
style={{
|
||||
rotateX: rotate,
|
||||
scale,
|
||||
}}
|
||||
className={cls("hidden md:block relative w-content-width mt-8 h-[75svh] mx-auto", cardWrapperClassName)}
|
||||
>
|
||||
<div className={cls("h-full w-full overflow-hidden rounded-theme-capped card p-4", cardInnerClassName)}>
|
||||
<MediaContent
|
||||
imageSrc={imageSrc}
|
||||
videoSrc={videoSrc}
|
||||
imageAlt={imageAlt}
|
||||
videoAriaLabel={videoAriaLabel}
|
||||
imageClassName={cls("z-1 h-full w-full object-cover object-left-top", imageClassName)}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroBillboardScroll.displayName = "HeroBillboardScroll";
|
||||
|
||||
export default HeroBillboardScroll;
|
||||
127
src/components/sections/hero/HeroLogo.tsx
Normal file
127
src/components/sections/hero/HeroLogo.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
"use client";
|
||||
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
import FillWidthText from "@/components/shared/FillWidthText/FillWidthText";
|
||||
import TextAnimation from "@/components/text/TextAnimation";
|
||||
import Button from "@/components/button/Button";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { getButtonProps } from "@/lib/buttonUtils";
|
||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
|
||||
const MASK_GRADIENT = "linear-gradient(to bottom, transparent, black 60%)";
|
||||
|
||||
interface HeroLogoProps {
|
||||
logoText: string;
|
||||
description: string;
|
||||
buttons: ButtonConfig[];
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
showDimOverlay?: boolean;
|
||||
logoLineHeight?: number;
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
contentContainerClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
logoContainerClassName?: string;
|
||||
logoClassName?: string;
|
||||
mediaWrapperClassName?: string;
|
||||
imageClassName?: string;
|
||||
blurClassName?: string;
|
||||
dimOverlayClassName?: string;
|
||||
}
|
||||
|
||||
const HeroLogo = ({
|
||||
logoText,
|
||||
description,
|
||||
buttons,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
imageAlt = "",
|
||||
videoAriaLabel = "Hero video",
|
||||
showDimOverlay = false,
|
||||
logoLineHeight = 1.1,
|
||||
ariaLabel = "Hero section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
contentContainerClassName = "",
|
||||
descriptionClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
logoContainerClassName = "",
|
||||
logoClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
imageClassName = "",
|
||||
blurClassName = "",
|
||||
dimOverlayClassName = "",
|
||||
}: HeroLogoProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("relative w-full h-svh overflow-hidden flex flex-col justify-end", className)}
|
||||
>
|
||||
<div className={cls("absolute inset-0 w-full h-full", mediaWrapperClassName)}>
|
||||
{showDimOverlay && (
|
||||
<div className={cls("absolute top-0 left-0 w-full h-full bg-background/20 pointer-events-none select-none", dimOverlayClassName)} />
|
||||
)}
|
||||
<MediaContent
|
||||
imageSrc={imageSrc}
|
||||
videoSrc={videoSrc}
|
||||
imageAlt={imageAlt}
|
||||
videoAriaLabel={videoAriaLabel}
|
||||
imageClassName={cls("w-full h-full object-cover !rounded-none", imageClassName)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cls(
|
||||
"absolute z-10 backdrop-blur-xl opacity-100 w-full h-[50svh] md:h-[75svh] left-0 bottom-0 pointer-events-none select-none",
|
||||
blurClassName
|
||||
)}
|
||||
style={{ maskImage: MASK_GRADIENT }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<div className={cls("relative z-20 w-content-width mx-auto h-fit flex items-end", containerClassName)}>
|
||||
<div className={cls("w-full flex flex-col", logoContainerClassName)}>
|
||||
<div className={cls("w-full flex flex-col md:flex-row md:justify-between items-start md:items-end gap-3 md:gap-6", contentContainerClassName)}>
|
||||
<div className="w-full md:w-1/2" >
|
||||
<TextAnimation
|
||||
type={theme.defaultTextAnimation}
|
||||
text={description}
|
||||
variant="words-trigger"
|
||||
start="top 100%"
|
||||
className={cls("text-lg md:text-2xl text-background text-balance font-medium leading-[1.2] md:max-w-1/2", descriptionClassName)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full md:w-1/2 flex justify-start md:justify-end" >
|
||||
<div className={cls("flex gap-4", buttonContainerClassName)}>
|
||||
{buttons.slice(0, 2).map((button, index) => (
|
||||
<Button key={index} {...getButtonProps(button, index, theme.defaultButtonVariant, cls("", buttonClassName), cls("text-base", buttonTextClassName))} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex">
|
||||
<FillWidthText lineHeight={logoLineHeight} className={cls("font-bold text-background", logoClassName)}>
|
||||
{logoText}
|
||||
</FillWidthText>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroLogo.displayName = "HeroLogo";
|
||||
|
||||
export default HeroLogo;
|
||||
143
src/components/sections/hero/HeroLogoBillboard.tsx
Normal file
143
src/components/sections/hero/HeroLogoBillboard.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
"use client";
|
||||
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
import FillWidthText from "@/components/shared/FillWidthText/FillWidthText";
|
||||
import TextAnimation from "@/components/text/TextAnimation";
|
||||
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { Plus } from "lucide-react";
|
||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||
|
||||
type HeroLogoBillboardBackgroundProps = Extract<
|
||||
HeroBackgroundVariantProps,
|
||||
| { variant: "plain" }
|
||||
| { variant: "animated-grid" }
|
||||
| { variant: "canvas-reveal" }
|
||||
| { variant: "cell-wave" }
|
||||
| { variant: "downward-rays-animated" }
|
||||
| { variant: "downward-rays-animated-grid" }
|
||||
| { variant: "downward-rays-static" }
|
||||
| { variant: "downward-rays-static-grid" }
|
||||
| { variant: "glowing-orb" }
|
||||
| { variant: "gradient-bars" }
|
||||
| { variant: "radial-gradient" }
|
||||
| { variant: "rotated-rays-animated" }
|
||||
| { variant: "rotated-rays-animated-grid" }
|
||||
| { variant: "rotated-rays-static" }
|
||||
| { variant: "rotated-rays-static-grid" }
|
||||
| { variant: "sparkles-gradient" }
|
||||
>;
|
||||
|
||||
interface HeroLogoBillboardProps {
|
||||
logoText: string;
|
||||
description: string;
|
||||
background: HeroLogoBillboardBackgroundProps;
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
frameStyle?: "card" | "browser";
|
||||
logoLineHeight?: number;
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
logoContainerClassName?: string;
|
||||
logoClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
mediaWrapperClassName?: string;
|
||||
imageClassName?: string;
|
||||
browserBarClassName?: string;
|
||||
addressBarClassName?: string;
|
||||
}
|
||||
|
||||
const HeroLogoBillboard = ({
|
||||
logoText,
|
||||
description,
|
||||
background,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
imageAlt = "",
|
||||
videoAriaLabel = "Hero video",
|
||||
frameStyle = "card",
|
||||
logoLineHeight = 1.1,
|
||||
ariaLabel = "Hero section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
logoContainerClassName = "",
|
||||
logoClassName = "",
|
||||
descriptionClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
imageClassName = "",
|
||||
browserBarClassName = "",
|
||||
addressBarClassName = "",
|
||||
}: HeroLogoBillboardProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("relative w-full py-hero-page-padding", className)}
|
||||
>
|
||||
<HeroBackgrounds {...background} />
|
||||
<div className={cls("w-content-width mx-auto flex flex-col gap-14 md:gap-15 relative z-10", containerClassName)}>
|
||||
<div className={cls("w-full flex flex-col items-end gap-6 md:gap-8", logoContainerClassName)}>
|
||||
<div className="relative w-full flex">
|
||||
<FillWidthText lineHeight={logoLineHeight} className={cls("text-foreground", logoClassName)}>
|
||||
{logoText}
|
||||
</FillWidthText>
|
||||
</div>
|
||||
<div className="relative w-full md:w-1/2" >
|
||||
<TextAnimation
|
||||
type={theme.defaultTextAnimation}
|
||||
text={description}
|
||||
variant="words-trigger"
|
||||
start="top 100%"
|
||||
className={cls("text-lg md:text-3xl text-foreground/75 text-balance text-end leading-[1.2]", descriptionClassName)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{frameStyle === "browser" ? (
|
||||
<div className={cls("w-full overflow-hidden rounded-theme-capped card", mediaWrapperClassName)}>
|
||||
<div className={cls("relative z-1 bg-background border-b border-foreground/10 px-4 py-3 flex items-center gap-4", browserBarClassName)}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-3 w-auto aspect-square rounded-theme bg-accent" />
|
||||
<div className="h-3 w-auto aspect-square rounded-theme bg-accent" />
|
||||
<div className="h-3 w-auto aspect-square rounded-theme bg-accent" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<div className={cls("w-15 md:w-10 h-8 rounded-theme bg-accent/10", addressBarClassName)} />
|
||||
<div className="w-15 md:w-10 h-8 rounded-theme bg-accent/10" />
|
||||
<div className="hidden md:block w-10 h-8 rounded-theme bg-accent/10" />
|
||||
</div>
|
||||
<Plus className="h-[var(--text-sm)] w-auto text-foreground" />
|
||||
</div>
|
||||
<div className="relative z-1 p-0">
|
||||
<MediaContent
|
||||
imageSrc={imageSrc}
|
||||
videoSrc={videoSrc}
|
||||
imageAlt={imageAlt}
|
||||
videoAriaLabel={videoAriaLabel}
|
||||
imageClassName={cls("z-1 rounded-none! aspect-square md:aspect-video", imageClassName)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={cls("w-full overflow-hidden rounded-theme-capped card p-4", mediaWrapperClassName)}>
|
||||
<MediaContent
|
||||
imageSrc={imageSrc}
|
||||
videoSrc={videoSrc}
|
||||
imageAlt={imageAlt}
|
||||
videoAriaLabel={videoAriaLabel}
|
||||
imageClassName={cls("z-1 aspect-square md:aspect-video", imageClassName)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroLogoBillboard.displayName = "HeroLogoBillboard";
|
||||
|
||||
export default HeroLogoBillboard;
|
||||
167
src/components/sections/hero/HeroLogoBillboardSplit.tsx
Normal file
167
src/components/sections/hero/HeroLogoBillboardSplit.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
"use client";
|
||||
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
import FillWidthText from "@/components/shared/FillWidthText/FillWidthText";
|
||||
import TextAnimation from "@/components/text/TextAnimation";
|
||||
import Button from "@/components/button/Button";
|
||||
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { getButtonProps } from "@/lib/buttonUtils";
|
||||
import { Plus } from "lucide-react";
|
||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
|
||||
type HeroLogoBillboardSplitBackgroundProps = Extract<
|
||||
HeroBackgroundVariantProps,
|
||||
| { variant: "plain" }
|
||||
| { variant: "animated-grid" }
|
||||
| { variant: "canvas-reveal" }
|
||||
| { variant: "cell-wave" }
|
||||
| { variant: "downward-rays-animated" }
|
||||
| { variant: "downward-rays-animated-grid" }
|
||||
| { variant: "downward-rays-static" }
|
||||
| { variant: "downward-rays-static-grid" }
|
||||
| { variant: "glowing-orb" }
|
||||
| { variant: "gradient-bars" }
|
||||
| { variant: "radial-gradient" }
|
||||
| { variant: "rotated-rays-animated" }
|
||||
| { variant: "rotated-rays-animated-grid" }
|
||||
| { variant: "rotated-rays-static" }
|
||||
| { variant: "rotated-rays-static-grid" }
|
||||
| { variant: "sparkles-gradient" }
|
||||
>;
|
||||
|
||||
interface HeroLogoBillboardSplitProps {
|
||||
logoText: string;
|
||||
description: string;
|
||||
background: HeroLogoBillboardSplitBackgroundProps;
|
||||
buttons: ButtonConfig[];
|
||||
layoutOrder: "default" | "reverse";
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
frameStyle?: "card" | "browser";
|
||||
logoLineHeight?: number;
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
logoContainerClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
logoClassName?: string;
|
||||
mediaWrapperClassName?: string;
|
||||
imageClassName?: string;
|
||||
browserBarClassName?: string;
|
||||
addressBarClassName?: string;
|
||||
}
|
||||
|
||||
const HeroLogoBillboardSplit = ({
|
||||
logoText,
|
||||
description,
|
||||
background,
|
||||
buttons,
|
||||
layoutOrder,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
imageAlt = "",
|
||||
videoAriaLabel = "Hero video",
|
||||
frameStyle = "card",
|
||||
logoLineHeight = 1.1,
|
||||
ariaLabel = "Hero section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
logoContainerClassName = "",
|
||||
descriptionClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
logoClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
imageClassName = "",
|
||||
browserBarClassName = "",
|
||||
addressBarClassName = "",
|
||||
}: HeroLogoBillboardSplitProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("relative w-full py-hero-page-padding", className)}
|
||||
>
|
||||
<HeroBackgrounds {...background} />
|
||||
<div className={cls("w-content-width mx-auto flex flex-col gap-6 md:gap-15 relative z-10", containerClassName)}>
|
||||
<div className={cls(
|
||||
"w-full flex gap-6 md:gap-8",
|
||||
layoutOrder === "default" ? "flex-col" : "flex-col-reverse",
|
||||
logoContainerClassName
|
||||
)}>
|
||||
<div className="relative flex flex-col gap-3 md:flex-row justify-between md:items-end w-full" >
|
||||
<div className="relative flex flex-col gap-4 w-full md:w-1/2" >
|
||||
<TextAnimation
|
||||
type={theme.defaultTextAnimation}
|
||||
text={description}
|
||||
variant="words-trigger"
|
||||
start="top 100%"
|
||||
className={cls("text-lg md:text-3xl text-foreground/75 text-balance text-start leading-[1.2]", descriptionClassName)}
|
||||
/>
|
||||
</div>
|
||||
<div className={cls("flex gap-4", buttonContainerClassName)}>
|
||||
{buttons.slice(0, 2).map((button, index) => (
|
||||
<Button key={`${button.text}-${index}`} {...getButtonProps(button, index, theme.defaultButtonVariant, buttonClassName, buttonTextClassName)} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative w-full flex">
|
||||
<FillWidthText lineHeight={logoLineHeight} className={cls("text-foreground", logoClassName)}>
|
||||
{logoText}
|
||||
</FillWidthText>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{frameStyle === "browser" ? (
|
||||
<div className={cls("w-full overflow-hidden rounded-theme-capped card", mediaWrapperClassName)}>
|
||||
<div className={cls("relative z-1 bg-background border-b border-foreground/10 px-4 py-3 flex items-center gap-4", browserBarClassName)}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-3 w-auto aspect-square rounded-theme bg-accent" />
|
||||
<div className="h-3 w-auto aspect-square rounded-theme bg-accent" />
|
||||
<div className="h-3 w-auto aspect-square rounded-theme bg-accent" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<div className={cls("w-15 md:w-10 h-8 rounded-theme bg-accent/10", addressBarClassName)} />
|
||||
<div className="w-15 md:w-10 h-8 rounded-theme bg-accent/10" />
|
||||
<div className="hidden md:block w-10 h-8 rounded-theme bg-accent/10" />
|
||||
</div>
|
||||
<Plus className="h-[var(--text-sm)] w-auto text-foreground" />
|
||||
</div>
|
||||
<div className="relative z-1 p-0">
|
||||
<MediaContent
|
||||
imageSrc={imageSrc}
|
||||
videoSrc={videoSrc}
|
||||
imageAlt={imageAlt}
|
||||
videoAriaLabel={videoAriaLabel}
|
||||
imageClassName={cls("z-1 rounded-none! aspect-square md:aspect-video!", imageClassName)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={cls("w-full overflow-hidden rounded-theme-capped card p-4", mediaWrapperClassName)}>
|
||||
<MediaContent
|
||||
imageSrc={imageSrc}
|
||||
videoSrc={videoSrc}
|
||||
imageAlt={imageAlt}
|
||||
videoAriaLabel={videoAriaLabel}
|
||||
imageClassName={cls("z-1 aspect-square md:aspect-video", imageClassName)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroLogoBillboardSplit.displayName = "HeroLogoBillboardSplit";
|
||||
|
||||
export default HeroLogoBillboardSplit;
|
||||
142
src/components/sections/hero/HeroOverlay.tsx
Normal file
142
src/components/sections/hero/HeroOverlay.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
"use client";
|
||||
|
||||
import TextBox from "@/components/Textbox";
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
import { cls } from "@/lib/utils";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
|
||||
const RADIAL_MASK_GRADIENT = "radial-gradient(circle, black 20%, transparent 70%)";
|
||||
|
||||
interface HeroOverlayProps {
|
||||
title: string;
|
||||
description: string;
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
buttons?: ButtonConfig[];
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
textPosition?: "center" | "bottom-left";
|
||||
showDimOverlay?: boolean;
|
||||
showBlur?: boolean;
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
titleClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
tagClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
mediaWrapperClassName?: string;
|
||||
imageClassName?: string;
|
||||
blurClassName?: string;
|
||||
dimOverlayClassName?: string;
|
||||
}
|
||||
|
||||
const HeroOverlay = ({
|
||||
title,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
imageAlt = "",
|
||||
videoAriaLabel = "Hero video",
|
||||
textPosition = "bottom-left",
|
||||
showDimOverlay = false,
|
||||
showBlur = true,
|
||||
ariaLabel = "Hero section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
imageClassName = "",
|
||||
blurClassName = "",
|
||||
dimOverlayClassName = "",
|
||||
}: HeroOverlayProps) => {
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("relative w-full h-svh overflow-hidden flex flex-col justify-end", className)}
|
||||
>
|
||||
<div className={cls("absolute inset-0 w-full h-full", mediaWrapperClassName)}>
|
||||
{showDimOverlay && (
|
||||
<div className={cls("absolute top-0 left-0 w-full h-full bg-background/20 pointer-events-none select-none", dimOverlayClassName)} />
|
||||
)}
|
||||
<MediaContent
|
||||
imageSrc={imageSrc}
|
||||
videoSrc={videoSrc}
|
||||
imageAlt={imageAlt}
|
||||
videoAriaLabel={videoAriaLabel}
|
||||
imageClassName={cls("w-full h-full object-cover !rounded-none", imageClassName)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showBlur && (
|
||||
<div
|
||||
className={cls(
|
||||
"absolute z-10 backdrop-blur-sm opacity-100 pointer-events-auto select-none",
|
||||
textPosition === "center"
|
||||
? "w-[100vw] h-[80vw] left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2"
|
||||
: "w-[150vw] h-[150vw] left-0 bottom-0 -translate-x-1/2 translate-y-1/2",
|
||||
blurClassName
|
||||
)}
|
||||
style={{ maskImage: RADIAL_MASK_GRADIENT }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className={cls(
|
||||
"relative z-10 w-content-width mx-auto h-fit flex",
|
||||
textPosition === "center" ? "items-center justify-center" : "items-end pb-[var(--width-10)] md:pb-hero-page-padding",
|
||||
containerClassName
|
||||
)}>
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
buttons={buttons}
|
||||
className={cls(
|
||||
"flex flex-col gap-3 md:gap-3 text-background",
|
||||
textPosition === "center" ? "w-full" : "w-full md:w-1/2",
|
||||
textBoxClassName
|
||||
)}
|
||||
titleClassName={cls(
|
||||
"text-7xl 2xl:text-8xl font-medium text-balance",
|
||||
textPosition === "center" ? "text-center" : "text-left",
|
||||
titleClassName
|
||||
)}
|
||||
descriptionClassName={cls(
|
||||
"text-lg md:text-xl leading-[1.2]",
|
||||
textPosition === "center" ? "text-center" : "text-left",
|
||||
descriptionClassName
|
||||
)}
|
||||
tagClassName={cls(
|
||||
"w-fit px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-3",
|
||||
tagClassName
|
||||
)}
|
||||
buttonContainerClassName={cls("flex gap-4 mt-4", buttonContainerClassName)}
|
||||
buttonClassName={cls("", buttonClassName)}
|
||||
buttonTextClassName={cls("text-base", buttonTextClassName)}
|
||||
center={textPosition === "center"}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroOverlay.displayName = "HeroOverlay";
|
||||
|
||||
export default HeroOverlay;
|
||||
164
src/components/sections/hero/HeroSplit.tsx
Normal file
164
src/components/sections/hero/HeroSplit.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
"use client";
|
||||
|
||||
import TextBox from "@/components/Textbox";
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
|
||||
import { cls } from "@/lib/utils";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
import type { Avatar } from "@/components/shared/AvatarGroup";
|
||||
|
||||
type HeroSplitBackgroundProps = Extract<
|
||||
HeroBackgroundVariantProps,
|
||||
| { variant: "plain" }
|
||||
| { variant: "animated-grid" }
|
||||
| { variant: "canvas-reveal" }
|
||||
| { variant: "cell-wave" }
|
||||
| { variant: "downward-rays-animated" }
|
||||
| { variant: "downward-rays-animated-grid" }
|
||||
| { variant: "downward-rays-static" }
|
||||
| { variant: "downward-rays-static-grid" }
|
||||
| { variant: "glowing-orb" }
|
||||
| { variant: "gradient-bars" }
|
||||
| { variant: "radial-gradient" }
|
||||
| { variant: "rotated-rays-animated" }
|
||||
| { variant: "rotated-rays-animated-grid" }
|
||||
| { variant: "rotated-rays-static" }
|
||||
| { variant: "rotated-rays-static-grid" }
|
||||
| { variant: "sparkles-gradient" }
|
||||
>;
|
||||
|
||||
interface HeroSplitProps {
|
||||
title: string;
|
||||
description: string;
|
||||
background: HeroSplitBackgroundProps;
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
buttons?: ButtonConfig[];
|
||||
avatars?: Avatar[];
|
||||
avatarText?: string;
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
ariaLabel?: string;
|
||||
imagePosition?: "left" | "right";
|
||||
fixedMediaHeight?: boolean;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
titleClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
tagClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
mediaWrapperClassName?: string;
|
||||
imageClassName?: string;
|
||||
avatarGroupClassName?: string;
|
||||
}
|
||||
|
||||
const HeroSplit = ({
|
||||
title,
|
||||
description,
|
||||
background,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
avatars,
|
||||
avatarText,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
imageAlt = "",
|
||||
videoAriaLabel = "Hero video",
|
||||
ariaLabel = "Hero section",
|
||||
imagePosition = "right",
|
||||
fixedMediaHeight = true,
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
imageClassName = "",
|
||||
avatarGroupClassName = "",
|
||||
}: HeroSplitProps) => {
|
||||
const mediaContent = (
|
||||
<div className={cls(
|
||||
"w-full h-fit md:w-1/2 overflow-hidden rounded-theme-capped card p-4 md:max-h-[75svh]",
|
||||
fixedMediaHeight && "h-100 md:h-[65vh]",
|
||||
mediaWrapperClassName
|
||||
)}>
|
||||
<MediaContent
|
||||
imageSrc={imageSrc}
|
||||
videoSrc={videoSrc}
|
||||
imageAlt={imageAlt}
|
||||
videoAriaLabel={videoAriaLabel}
|
||||
imageClassName={cls("h-full min-h-0", imageClassName)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("relative w-full h-fit py-hero-page-padding md:py-0 md:h-svh flex items-center", className)}
|
||||
>
|
||||
<HeroBackgrounds {...background} />
|
||||
<div className={cls("w-content-width mx-auto flex flex-col md:flex-row gap-13 md:gap-15 items-center relative z-10", containerClassName)}>
|
||||
{imagePosition === "left" && mediaContent}
|
||||
|
||||
<div className={cls("w-full md:w-1/2")}>
|
||||
{/* Mobile */}
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
buttons={buttons}
|
||||
avatars={avatars}
|
||||
avatarText={avatarText}
|
||||
avatarGroupClassName={cls("!mt-5", avatarGroupClassName)}
|
||||
className={cls("flex flex-col gap-3 md:hidden", textBoxClassName)}
|
||||
titleClassName={cls("text-7xl 2xl:text-8xl font-medium text-center md:text-left text-balance", titleClassName)}
|
||||
descriptionClassName={cls("max-w-8/10 text-lg md:text-xl leading-[1.2] text-center md:text-left", descriptionClassName)}
|
||||
tagClassName={cls("w-fit px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-3", tagClassName)}
|
||||
buttonContainerClassName={cls("flex gap-4 mt-2", buttonContainerClassName)}
|
||||
buttonClassName={cls("", buttonClassName)}
|
||||
buttonTextClassName={cls("text-base", buttonTextClassName)}
|
||||
center={true}
|
||||
/>
|
||||
{/* Desktop */}
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
buttons={buttons}
|
||||
avatars={avatars}
|
||||
avatarText={avatarText}
|
||||
avatarGroupClassName={cls("", avatarGroupClassName)}
|
||||
className={cls("hidden md:flex flex-col gap-3 md:gap-4", textBoxClassName)}
|
||||
titleClassName={cls("text-7xl 2xl:text-8xl font-medium text-center md:text-left text-balance", titleClassName)}
|
||||
descriptionClassName={cls("max-w-8/10 text-lg md:text-xl leading-[1.2] text-center md:text-left", descriptionClassName)}
|
||||
tagClassName={cls("w-fit px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-3", tagClassName)}
|
||||
buttonContainerClassName={cls("flex gap-4 mt-2", buttonContainerClassName)}
|
||||
buttonClassName={cls("", buttonClassName)}
|
||||
buttonTextClassName={cls("text-base", buttonTextClassName)}
|
||||
center={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{imagePosition === "right" && mediaContent}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroSplit.displayName = "HeroSplit";
|
||||
|
||||
export default HeroSplit;
|
||||
277
src/components/sections/hero/HeroSplitKpi.tsx
Normal file
277
src/components/sections/hero/HeroSplitKpi.tsx
Normal file
@@ -0,0 +1,277 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import TextBox from "@/components/Textbox";
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
|
||||
import { cls } from "@/lib/utils";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
import type { Avatar } from "@/components/shared/AvatarGroup";
|
||||
|
||||
const MOBILE_BREAKPOINT = 768;
|
||||
|
||||
const useKpiAnimation = (enableAnimation: boolean = true) => {
|
||||
const sectionRef = useRef<HTMLElement>(null);
|
||||
const boxRef1 = useRef<HTMLDivElement>(null);
|
||||
const boxRef2 = useRef<HTMLDivElement>(null);
|
||||
const boxRef3 = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enableAnimation) return;
|
||||
|
||||
const isMobile = window.innerWidth <= MOBILE_BREAKPOINT;
|
||||
if (isMobile) return;
|
||||
|
||||
let mouseX = 0;
|
||||
let mouseY = 0;
|
||||
|
||||
let box1X = 0, box1Y = 0;
|
||||
let box2X = 0, box2Y = 0;
|
||||
let box3X = 0, box3Y = 0;
|
||||
|
||||
const speed = 0.025;
|
||||
|
||||
const handleMouseMove = (event: MouseEvent): void => {
|
||||
mouseX = (event.clientX / window.innerWidth) * 100 - 50;
|
||||
mouseY = (event.clientY / window.innerHeight) * 100 - 50;
|
||||
};
|
||||
|
||||
const animate = (): void => {
|
||||
// Box 1 movement
|
||||
const distX1 = (mouseX * -0.25) - box1X;
|
||||
const distY1 = (mouseY * -0.25) - box1Y;
|
||||
box1X += distX1 * speed;
|
||||
box1Y += distY1 * speed;
|
||||
|
||||
// Box 2 movement
|
||||
const distX2 = (mouseX * -0.5) - box2X;
|
||||
const distY2 = (mouseY * -0.5) - box2Y;
|
||||
box2X += distX2 * speed;
|
||||
box2Y += distY2 * speed;
|
||||
|
||||
// Box 3 movement
|
||||
const distX3 = (mouseX * 0.25) - box3X;
|
||||
const distY3 = (mouseY * 0.25) - box3Y;
|
||||
box3X += distX3 * speed;
|
||||
box3Y += distY3 * speed;
|
||||
|
||||
// Apply transforms
|
||||
if (boxRef1.current) {
|
||||
boxRef1.current.style.transform = `translate(${box1X}px, ${box1Y}px)`;
|
||||
}
|
||||
if (boxRef2.current) {
|
||||
boxRef2.current.style.transform = `translate(${box2X}px, ${box2Y}px)`;
|
||||
}
|
||||
if (boxRef3.current) {
|
||||
boxRef3.current.style.transform = `translate(${box3X}px, ${box3Y}px)`;
|
||||
}
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
animate();
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
};
|
||||
}, [enableAnimation]);
|
||||
|
||||
return { sectionRef, boxRef1, boxRef2, boxRef3 };
|
||||
};
|
||||
|
||||
type HeroSplitKpiBackgroundProps = Extract<
|
||||
HeroBackgroundVariantProps,
|
||||
| { variant: "plain" }
|
||||
| { variant: "animated-grid" }
|
||||
| { variant: "canvas-reveal" }
|
||||
| { variant: "cell-wave" }
|
||||
| { variant: "downward-rays-animated" }
|
||||
| { variant: "downward-rays-animated-grid" }
|
||||
| { variant: "downward-rays-static" }
|
||||
| { variant: "downward-rays-static-grid" }
|
||||
| { variant: "glowing-orb" }
|
||||
| { variant: "gradient-bars" }
|
||||
| { variant: "radial-gradient" }
|
||||
| { variant: "rotated-rays-animated" }
|
||||
| { variant: "rotated-rays-animated-grid" }
|
||||
| { variant: "rotated-rays-static" }
|
||||
| { variant: "rotated-rays-static-grid" }
|
||||
| { variant: "sparkles-gradient" }
|
||||
>;
|
||||
|
||||
interface KpiItem {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface HeroSplitKpiProps {
|
||||
title: string;
|
||||
description: string;
|
||||
background: HeroSplitKpiBackgroundProps;
|
||||
kpis: [KpiItem, KpiItem, KpiItem];
|
||||
enableKpiAnimation: boolean;
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
buttons?: ButtonConfig[];
|
||||
avatars?: Avatar[];
|
||||
avatarText?: string;
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
ariaLabel?: string;
|
||||
imagePosition?: "left" | "right";
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
titleClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
tagClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
mediaWrapperClassName?: string;
|
||||
imageClassName?: string;
|
||||
avatarGroupClassName?: string;
|
||||
kpiClassName?: string;
|
||||
kpiValueClassName?: string;
|
||||
kpiLabelClassName?: string;
|
||||
}
|
||||
|
||||
const HeroSplitKpi = ({
|
||||
title,
|
||||
description,
|
||||
background,
|
||||
kpis,
|
||||
enableKpiAnimation,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
avatars,
|
||||
avatarText,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
imageAlt = "",
|
||||
videoAriaLabel = "Hero video",
|
||||
ariaLabel = "Hero section",
|
||||
imagePosition = "right",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
imageClassName = "",
|
||||
avatarGroupClassName = "",
|
||||
kpiClassName = "",
|
||||
kpiValueClassName = "",
|
||||
kpiLabelClassName = "",
|
||||
}: HeroSplitKpiProps) => {
|
||||
const { sectionRef, boxRef1, boxRef2, boxRef3 } = useKpiAnimation(enableKpiAnimation);
|
||||
const boxRefs = [boxRef1, boxRef2, boxRef3];
|
||||
const mediaContent = (
|
||||
<div
|
||||
className={cls(
|
||||
"relative w-full h-fit md:w-1/2 aspect-square md:aspect-auto md:h-[65vh]",
|
||||
mediaWrapperClassName
|
||||
)}
|
||||
>
|
||||
<div className="relative h-full scale-75 w-full overflow-hidden rounded-theme-capped card p-4">
|
||||
<MediaContent
|
||||
imageSrc={imageSrc}
|
||||
videoSrc={videoSrc}
|
||||
imageAlt={imageAlt}
|
||||
videoAriaLabel={videoAriaLabel}
|
||||
imageClassName={cls("h-full min-h-0", imageClassName)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{kpis.map((kpi, index) => (
|
||||
<div
|
||||
key={index}
|
||||
ref={boxRefs[index]}
|
||||
className={cls(
|
||||
"absolute! card backdrop-blur-xs rounded-theme-capped px-4 py-3 md:px-6 md:py-4 flex flex-col items-center",
|
||||
index === 0 && "top-[5%] left-[5%] md:top-[0%] md:left-[0%]",
|
||||
index === 1 && "top-[35%] right-[2.5%] md:top-[35%]",
|
||||
index === 2 && "bottom-[7.5%] left-[10%] md:left-[7.5%] md:bottom-[0%]",
|
||||
kpiClassName
|
||||
)}
|
||||
>
|
||||
<p className={cls("text-2xl md:text-4xl font-medium text-foreground", kpiValueClassName)}>
|
||||
{kpi.value}
|
||||
</p>
|
||||
<p className={cls("text-sm md:text-base text-foreground/70", kpiLabelClassName)}>
|
||||
{kpi.label}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={sectionRef}
|
||||
aria-label={ariaLabel}
|
||||
className={cls("relative w-full h-fit py-hero-page-padding md:py-0 md:h-svh flex items-center", className)}
|
||||
>
|
||||
<HeroBackgrounds {...background} />
|
||||
<div className={cls("w-content-width mx-auto flex flex-col md:flex-row gap-13 md:gap-15 items-center relative z-10", containerClassName)}>
|
||||
{imagePosition === "left" && mediaContent}
|
||||
|
||||
<div className={cls("w-full md:w-1/2")}>
|
||||
{/* Mobile */}
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
buttons={buttons}
|
||||
avatars={avatars}
|
||||
avatarText={avatarText}
|
||||
avatarGroupClassName={cls("!mt-5", avatarGroupClassName)}
|
||||
className={cls("flex flex-col gap-3 md:hidden", textBoxClassName)}
|
||||
titleClassName={cls("text-7xl 2xl:text-8xl font-medium text-center md:text-left text-balance", titleClassName)}
|
||||
descriptionClassName={cls("max-w-8/10 text-lg md:text-xl leading-[1.2] text-center md:text-left", descriptionClassName)}
|
||||
tagClassName={cls("w-fit px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-3", tagClassName)}
|
||||
buttonContainerClassName={cls("flex gap-4 mt-2", buttonContainerClassName)}
|
||||
buttonClassName={cls("", buttonClassName)}
|
||||
buttonTextClassName={cls("text-base", buttonTextClassName)}
|
||||
center={true}
|
||||
/>
|
||||
{/* Desktop */}
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
buttons={buttons}
|
||||
avatars={avatars}
|
||||
avatarText={avatarText}
|
||||
avatarGroupClassName={cls("", avatarGroupClassName)}
|
||||
className={cls("hidden md:flex flex-col gap-3 md:gap-4", textBoxClassName)}
|
||||
titleClassName={cls("text-7xl 2xl:text-8xl font-medium text-center md:text-left text-balance", titleClassName)}
|
||||
descriptionClassName={cls("max-w-8/10 text-lg md:text-xl leading-[1.2] text-center md:text-left", descriptionClassName)}
|
||||
tagClassName={cls("w-fit px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-3", tagClassName)}
|
||||
buttonContainerClassName={cls("flex gap-4 mt-2", buttonContainerClassName)}
|
||||
buttonClassName={cls("", buttonClassName)}
|
||||
buttonTextClassName={cls("text-base", buttonTextClassName)}
|
||||
center={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{imagePosition === "right" && mediaContent}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroSplitKpi.displayName = "HeroSplitKpi";
|
||||
|
||||
export default HeroSplitKpi;
|
||||
@@ -0,0 +1,173 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback } from "react";
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
import FillWidthText from "@/components/shared/FillWidthText/FillWidthText";
|
||||
import TextAnimation from "@/components/text/TextAnimation";
|
||||
import Button from "@/components/button/Button";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { getButtonProps } from "@/lib/buttonUtils";
|
||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||
import { useCarouselFullscreen } from "./useCarouselFullscreen";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
|
||||
const MASK_GRADIENT = "linear-gradient(to bottom, transparent, black 60%)";
|
||||
|
||||
interface CarouselSlide {
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
}
|
||||
|
||||
interface HeroCarouselLogoProps {
|
||||
logoText: string;
|
||||
description: string;
|
||||
buttons: ButtonConfig[];
|
||||
slides: CarouselSlide[];
|
||||
autoplayDelay?: number;
|
||||
showDimOverlay?: boolean;
|
||||
logoLineHeight?: number;
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
contentContainerClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
logoContainerClassName?: string;
|
||||
logoClassName?: string;
|
||||
mediaWrapperClassName?: string;
|
||||
imageClassName?: string;
|
||||
blurClassName?: string;
|
||||
dimOverlayClassName?: string;
|
||||
progressBarClassName?: string;
|
||||
}
|
||||
|
||||
const HeroCarouselLogo = ({
|
||||
logoText,
|
||||
description,
|
||||
buttons,
|
||||
slides,
|
||||
autoplayDelay = 3000,
|
||||
showDimOverlay = false,
|
||||
logoLineHeight = 1.1,
|
||||
ariaLabel = "Hero section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
contentContainerClassName = "",
|
||||
descriptionClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
logoContainerClassName = "",
|
||||
logoClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
imageClassName = "",
|
||||
blurClassName = "",
|
||||
dimOverlayClassName = "",
|
||||
progressBarClassName = "",
|
||||
}: HeroCarouselLogoProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const { currentSlide, progressRefs, goToSlide } = useCarouselFullscreen({
|
||||
totalSlides: slides.length,
|
||||
autoplayDelay,
|
||||
});
|
||||
|
||||
const setProgressRef = useCallback(
|
||||
(el: HTMLDivElement | null, index: number) => {
|
||||
progressRefs.current[index] = el;
|
||||
},
|
||||
[progressRefs]
|
||||
);
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("relative w-full h-svh overflow-hidden flex flex-col justify-end", className)}
|
||||
>
|
||||
<div className={cls("absolute inset-0 w-full h-full", mediaWrapperClassName)}>
|
||||
{showDimOverlay && (
|
||||
<div className={cls("absolute top-0 left-0 w-full h-full bg-background/20 z-10 pointer-events-none select-none", dimOverlayClassName)} />
|
||||
)}
|
||||
{slides.map((slide, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cls(
|
||||
"absolute inset-0 transition-opacity duration-600",
|
||||
currentSlide === index ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
aria-hidden={currentSlide !== index}
|
||||
>
|
||||
<MediaContent
|
||||
imageSrc={slide.imageSrc}
|
||||
videoSrc={slide.videoSrc}
|
||||
imageAlt={slide.imageAlt || ""}
|
||||
videoAriaLabel={slide.videoAriaLabel || "Hero video"}
|
||||
imageClassName={cls("w-full h-full object-cover !rounded-none", imageClassName)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cls(
|
||||
"absolute z-10 backdrop-blur-xl opacity-100 w-full h-[50svh] md:h-[75svh] left-0 bottom-0 pointer-events-none select-none",
|
||||
blurClassName
|
||||
)}
|
||||
style={{ maskImage: MASK_GRADIENT }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<div className={cls("relative z-20 w-content-width mx-auto h-fit flex items-end", containerClassName)}>
|
||||
<div className={cls("w-full flex flex-col", logoContainerClassName)}>
|
||||
<div className={cls("w-full flex flex-col md:flex-row md:justify-between items-start md:items-end gap-3 md:gap-6", contentContainerClassName)}>
|
||||
<div className="w-full md:w-1/2">
|
||||
<TextAnimation
|
||||
type={theme.defaultTextAnimation}
|
||||
text={description}
|
||||
variant="words-trigger"
|
||||
className={cls("text-lg md:text-2xl text-background text-balance font-medium leading-[1.2] md:max-w-1/2", descriptionClassName)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full md:w-1/2 flex justify-start md:justify-end">
|
||||
<div className={cls("flex gap-4", buttonContainerClassName)}>
|
||||
{buttons.slice(0, 2).map((button, index) => (
|
||||
<Button key={index} {...getButtonProps(button, index, theme.defaultButtonVariant, cls("", buttonClassName), cls("text-base", buttonTextClassName))} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex">
|
||||
<FillWidthText lineHeight={logoLineHeight} className={cls("text-background", logoClassName)}>
|
||||
{logoText}
|
||||
</FillWidthText>
|
||||
</div>
|
||||
<div className="w-full flex gap-3 pb-12 pt-6">
|
||||
{Array.from({ length: slides.length }, (_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className={cls("relative cursor-pointer h-1 w-full rounded-theme overflow-hidden bg-white/10 backdrop-blur-sm", progressBarClassName)}
|
||||
onClick={() => goToSlide(index)}
|
||||
aria-label={`Go to slide ${index + 1}`}
|
||||
aria-current={currentSlide === index ? "true" : "false"}
|
||||
>
|
||||
<div
|
||||
ref={(el) => setProgressRef(el, index)}
|
||||
className="absolute inset-0 bg-white rounded-theme"
|
||||
style={{ transform: "translateX(-100%)" }}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroCarouselLogo.displayName = "HeroCarouselLogo";
|
||||
|
||||
export default HeroCarouselLogo;
|
||||
@@ -0,0 +1,103 @@
|
||||
import { useEffect, useRef, useState, useCallback } from "react";
|
||||
|
||||
interface UseCarouselFullscreenProps {
|
||||
totalSlides: number;
|
||||
autoplayDelay?: number;
|
||||
}
|
||||
|
||||
interface UseCarouselFullscreenReturn {
|
||||
currentSlide: number;
|
||||
progressRefs: React.MutableRefObject<(HTMLDivElement | null)[]>;
|
||||
goToSlide: (index: number) => void;
|
||||
}
|
||||
|
||||
export const useCarouselFullscreen = ({
|
||||
totalSlides,
|
||||
autoplayDelay = 3000,
|
||||
}: UseCarouselFullscreenProps): UseCarouselFullscreenReturn => {
|
||||
const [currentSlide, setCurrentSlide] = useState(0);
|
||||
const progressRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
const animationFrameRef = useRef<number | null>(null);
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const previousSlideRef = useRef<number>(0);
|
||||
|
||||
const resetProgressBars = useCallback(
|
||||
(fromIndex: number, isLooping: boolean = false) => {
|
||||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
}
|
||||
|
||||
progressRefs.current.forEach((bar, index) => {
|
||||
if (bar) {
|
||||
if (isLooping || index >= fromIndex) {
|
||||
bar.style.transform = "translateX(-100%)";
|
||||
} else {
|
||||
bar.style.transform = "translateX(0%)";
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const animateProgress = useCallback(
|
||||
(index: number, prevIndex: number) => {
|
||||
const isLooping = prevIndex === totalSlides - 1 && index === 0;
|
||||
resetProgressBars(index, isLooping);
|
||||
|
||||
if (!progressRefs.current[index]) return;
|
||||
|
||||
let startTime: number | null = null;
|
||||
|
||||
const animate = (timestamp: number) => {
|
||||
if (!startTime) startTime = timestamp;
|
||||
const elapsed = timestamp - startTime;
|
||||
const progress = Math.min(elapsed / autoplayDelay, 1);
|
||||
|
||||
const translateValue = -100 + progress * 100;
|
||||
if (progressRefs.current[index]) {
|
||||
progressRefs.current[index]!.style.transform = `translateX(${translateValue}%)`;
|
||||
}
|
||||
|
||||
if (progress < 1) {
|
||||
animationFrameRef.current = requestAnimationFrame(animate);
|
||||
}
|
||||
};
|
||||
|
||||
animationFrameRef.current = requestAnimationFrame(animate);
|
||||
},
|
||||
[autoplayDelay, resetProgressBars, totalSlides]
|
||||
);
|
||||
|
||||
const goToSlide = useCallback((index: number) => {
|
||||
setCurrentSlide(index);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
animateProgress(currentSlide, previousSlideRef.current);
|
||||
previousSlideRef.current = currentSlide;
|
||||
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
}
|
||||
|
||||
intervalRef.current = setInterval(() => {
|
||||
setCurrentSlide((prev) => (prev + 1) % totalSlides);
|
||||
}, autoplayDelay);
|
||||
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
}
|
||||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
}
|
||||
};
|
||||
}, [currentSlide, totalSlides, autoplayDelay, animateProgress]);
|
||||
|
||||
return {
|
||||
currentSlide,
|
||||
progressRefs,
|
||||
goToSlide,
|
||||
};
|
||||
};
|
||||
268
src/components/sections/metrics/MetricCardEleven.tsx
Normal file
268
src/components/sections/metrics/MetricCardEleven.tsx
Normal file
@@ -0,0 +1,268 @@
|
||||
"use client";
|
||||
|
||||
import { 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 truncate",
|
||||
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 py-20 w-full", 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 MetricCardEleven;
|
||||
120
src/components/sections/metrics/MetricCardFourteen.tsx
Normal file
120
src/components/sections/metrics/MetricCardFourteen.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
"use client";
|
||||
|
||||
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 = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("relative py-20 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",
|
||||
useInvertedBackground === "invertDefault" && "text-background",
|
||||
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",
|
||||
shouldUseLightText ? "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",
|
||||
shouldUseLightText ? "text-background" : "text-foreground",
|
||||
descriptionClassName
|
||||
)}>
|
||||
{metric.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
MetricCardFourteen.displayName = "MetricCardFourteen";
|
||||
|
||||
export default MetricCardFourteen;
|
||||
204
src/components/sections/metrics/MetricCardOne.tsx
Normal file
204
src/components/sections/metrics/MetricCardOne.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
"use client";
|
||||
|
||||
import { 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, CardAnimationTypeWith3D, TitleSegment } from "@/components/cardStack/types";
|
||||
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||
|
||||
type MetricCardOneGridVariant = Extract<GridVariant, "uniform-all-items-equal" | "bento-grid" | "bento-grid-inverted">;
|
||||
|
||||
type Metric = {
|
||||
id: string;
|
||||
value: string;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: LucideIcon;
|
||||
};
|
||||
|
||||
interface MetricCardOneProps {
|
||||
metrics: Metric[];
|
||||
carouselMode?: "auto" | "buttons";
|
||||
gridVariant: MetricCardOneGridVariant;
|
||||
uniformGridCustomHeightClasses?: string;
|
||||
animationType: CardAnimationTypeWith3D;
|
||||
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}
|
||||
</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);
|
||||
|
||||
const customUniformHeight = gridVariant === "uniform-all-items-equal"
|
||||
? "min-h-70 2xl:min-h-80"
|
||||
: uniformGridCustomHeightClasses;
|
||||
|
||||
return (
|
||||
<CardStack
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
mode={carouselMode}
|
||||
gridVariant={gridVariant}
|
||||
uniformGridCustomHeightClasses={customUniformHeight}
|
||||
animationType={animationType}
|
||||
supports3DAnimation={true}
|
||||
|
||||
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 MetricCardOne;
|
||||
188
src/components/sections/metrics/MetricCardSeven.tsx
Normal file
188
src/components/sections/metrics/MetricCardSeven.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import CardStack from "@/components/cardStack/CardStack";
|
||||
import PricingFeatureList from "@/components/shared/PricingFeatureList";
|
||||
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig, CardAnimationTypeWith3D, TitleSegment } from "@/components/cardStack/types";
|
||||
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||
|
||||
type Metric = {
|
||||
id: string;
|
||||
value: string;
|
||||
title: string;
|
||||
items: string[];
|
||||
};
|
||||
|
||||
interface MetricCardSevenProps {
|
||||
metrics: Metric[];
|
||||
carouselMode?: "auto" | "buttons";
|
||||
uniformGridCustomHeightClasses?: string;
|
||||
animationType: CardAnimationTypeWith3D;
|
||||
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;
|
||||
metricTitleClassName?: string;
|
||||
featuresClassName?: string;
|
||||
featureItemClassName?: 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;
|
||||
metricTitleClassName?: string;
|
||||
featuresClassName?: string;
|
||||
featureItemClassName?: string;
|
||||
}
|
||||
|
||||
const MetricCardItem = memo(({
|
||||
metric,
|
||||
shouldUseLightText,
|
||||
cardClassName = "",
|
||||
valueClassName = "",
|
||||
metricTitleClassName = "",
|
||||
featuresClassName = "",
|
||||
featureItemClassName = "",
|
||||
}: MetricCardItemProps) => {
|
||||
return (
|
||||
<div className={cls("relative h-full card text-foreground rounded-theme-capped p-6 flex flex-col justify-between gap-4", cardClassName)}>
|
||||
<div className="flex flex-col gap-0" >
|
||||
<h3 className={cls("relative z-1 text-9xl md:text-8xl font-medium truncate", shouldUseLightText ? "text-background" : "text-foreground", valueClassName)}>
|
||||
{metric.value}
|
||||
</h3>
|
||||
<p className={cls("relative z-1 text-2xl md:text-xl truncate", shouldUseLightText ? "text-background" : "text-foreground", metricTitleClassName)}>
|
||||
{metric.title}
|
||||
</p>
|
||||
</div>
|
||||
<div className="pt-4 border-t border-t-accent" >
|
||||
{metric.items.length > 0 && (
|
||||
<PricingFeatureList
|
||||
features={metric.items}
|
||||
shouldUseLightText={shouldUseLightText}
|
||||
className={cls("mt-1", featuresClassName)}
|
||||
featureItemClassName={featureItemClassName}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
MetricCardItem.displayName = "MetricCardItem";
|
||||
|
||||
const MetricCardSeven = ({
|
||||
metrics,
|
||||
carouselMode = "buttons",
|
||||
uniformGridCustomHeightClasses,
|
||||
animationType,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
textboxLayout,
|
||||
useInvertedBackground,
|
||||
ariaLabel = "Metrics section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
cardClassName = "",
|
||||
textBoxTitleClassName = "",
|
||||
textBoxTitleImageWrapperClassName = "",
|
||||
textBoxTitleImageClassName = "",
|
||||
textBoxDescriptionClassName = "",
|
||||
valueClassName = "",
|
||||
metricTitleClassName = "",
|
||||
featuresClassName = "",
|
||||
featureItemClassName = "",
|
||||
gridClassName = "",
|
||||
carouselClassName = "",
|
||||
controlsClassName = "",
|
||||
textBoxClassName = "",
|
||||
textBoxTagClassName = "",
|
||||
textBoxButtonContainerClassName = "",
|
||||
textBoxButtonClassName = "",
|
||||
textBoxButtonTextClassName = "",
|
||||
}: MetricCardSevenProps) => {
|
||||
const theme = useTheme();
|
||||
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
|
||||
|
||||
const customUniformHeight = uniformGridCustomHeightClasses || "min-h-70 2xl:min-h-80";
|
||||
|
||||
return (
|
||||
<CardStack
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
mode={carouselMode}
|
||||
gridVariant="uniform-all-items-equal"
|
||||
uniformGridCustomHeightClasses={customUniformHeight}
|
||||
animationType={animationType}
|
||||
supports3DAnimation={true}
|
||||
|
||||
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}
|
||||
metricTitleClassName={metricTitleClassName}
|
||||
featuresClassName={featuresClassName}
|
||||
featureItemClassName={featureItemClassName}
|
||||
/>
|
||||
))}
|
||||
</CardStack>
|
||||
);
|
||||
};
|
||||
|
||||
MetricCardSeven.displayName = "MetricCardSeven";
|
||||
|
||||
export default MetricCardSeven;
|
||||
239
src/components/sections/metrics/MetricCardTen.tsx
Normal file
239
src/components/sections/metrics/MetricCardTen.tsx
Normal file
@@ -0,0 +1,239 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import CardStack from "@/components/cardStack/CardStack";
|
||||
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";
|
||||
import type { CTAButtonVariant } from "@/components/button/types";
|
||||
|
||||
type Metric = {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
category: string;
|
||||
value: string;
|
||||
buttons?: ButtonConfig[];
|
||||
};
|
||||
|
||||
interface MetricCardTenProps {
|
||||
metrics: Metric[];
|
||||
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;
|
||||
textBoxTitleClassName?: string;
|
||||
textBoxTitleImageWrapperClassName?: string;
|
||||
textBoxTitleImageClassName?: string;
|
||||
textBoxDescriptionClassName?: string;
|
||||
cardTitleClassName?: string;
|
||||
subtitleClassName?: string;
|
||||
categoryClassName?: string;
|
||||
valueClassName?: string;
|
||||
footerClassName?: string;
|
||||
cardButtonClassName?: string;
|
||||
cardButtonTextClassName?: string;
|
||||
gridClassName?: string;
|
||||
carouselClassName?: string;
|
||||
controlsClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
textBoxTagClassName?: string;
|
||||
textBoxButtonContainerClassName?: string;
|
||||
textBoxButtonClassName?: string;
|
||||
textBoxButtonTextClassName?: string;
|
||||
}
|
||||
|
||||
interface MetricCardItemProps {
|
||||
metric: Metric;
|
||||
shouldUseLightText: boolean;
|
||||
defaultButtonVariant: CTAButtonVariant;
|
||||
cardClassName?: string;
|
||||
cardTitleClassName?: string;
|
||||
subtitleClassName?: string;
|
||||
categoryClassName?: string;
|
||||
valueClassName?: string;
|
||||
footerClassName?: string;
|
||||
cardButtonClassName?: string;
|
||||
cardButtonTextClassName?: string;
|
||||
}
|
||||
|
||||
const MetricCardItem = memo(({
|
||||
metric,
|
||||
shouldUseLightText,
|
||||
defaultButtonVariant,
|
||||
cardClassName = "",
|
||||
cardTitleClassName = "",
|
||||
subtitleClassName = "",
|
||||
categoryClassName = "",
|
||||
valueClassName = "",
|
||||
footerClassName = "",
|
||||
cardButtonClassName = "",
|
||||
cardButtonTextClassName = "",
|
||||
}: MetricCardItemProps) => {
|
||||
return (
|
||||
<div className={cls("relative h-full card text-foreground rounded-theme-capped flex flex-col", cardClassName)}>
|
||||
<div className="flex flex-col gap-6 p-6 flex-1">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className={cls(
|
||||
"text-2xl md:text-3xl font-medium leading-tight truncate",
|
||||
shouldUseLightText ? "text-background" : "text-foreground",
|
||||
cardTitleClassName
|
||||
)}>
|
||||
{metric.title}
|
||||
</h3>
|
||||
<p className={cls(
|
||||
"text-base md:text-lg",
|
||||
shouldUseLightText ? "text-background/75" : "text-foreground/75",
|
||||
subtitleClassName
|
||||
)}>
|
||||
{metric.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-2 mt-auto">
|
||||
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||
<span className="h-[var(--text-base)] w-auto aspect-square rounded-theme shrink-0 bg-accent" />
|
||||
<span className={cls(
|
||||
"text-base truncate",
|
||||
shouldUseLightText ? "text-background" : "text-foreground",
|
||||
categoryClassName
|
||||
)}>
|
||||
{metric.category}
|
||||
</span>
|
||||
</div>
|
||||
<span className={cls(
|
||||
"text-xl md:text-2xl font-medium",
|
||||
shouldUseLightText ? "text-background" : "text-foreground",
|
||||
valueClassName
|
||||
)}>
|
||||
{metric.value}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{metric.buttons && metric.buttons.length > 0 && (
|
||||
<div className={cls("bg-accent/40 p-4 rounded-b-theme-capped", footerClassName)}>
|
||||
<div className="flex gap-4">
|
||||
{metric.buttons.slice(0, 2).map((button, index) => (
|
||||
<Button key={`${button.text}-${index}`} {...getButtonProps(button, index, defaultButtonVariant, cardButtonClassName, cardButtonTextClassName)} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
MetricCardItem.displayName = "MetricCardItem";
|
||||
|
||||
const MetricCardTen = ({
|
||||
metrics,
|
||||
carouselMode = "buttons",
|
||||
uniformGridCustomHeightClasses,
|
||||
animationType,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
textboxLayout,
|
||||
useInvertedBackground,
|
||||
ariaLabel = "Metrics section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
cardClassName = "",
|
||||
textBoxTitleClassName = "",
|
||||
textBoxTitleImageWrapperClassName = "",
|
||||
textBoxTitleImageClassName = "",
|
||||
textBoxDescriptionClassName = "",
|
||||
cardTitleClassName = "",
|
||||
subtitleClassName = "",
|
||||
categoryClassName = "",
|
||||
valueClassName = "",
|
||||
footerClassName = "",
|
||||
cardButtonClassName = "",
|
||||
cardButtonTextClassName = "",
|
||||
gridClassName = "",
|
||||
carouselClassName = "",
|
||||
controlsClassName = "",
|
||||
textBoxClassName = "",
|
||||
textBoxTagClassName = "",
|
||||
textBoxButtonContainerClassName = "",
|
||||
textBoxButtonClassName = "",
|
||||
textBoxButtonTextClassName = "",
|
||||
}: MetricCardTenProps) => {
|
||||
const theme = useTheme();
|
||||
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
|
||||
|
||||
return (
|
||||
<CardStack
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
mode={carouselMode}
|
||||
gridVariant="uniform-all-items-equal"
|
||||
carouselThreshold={4}
|
||||
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}
|
||||
carouselItemClassName="!w-carousel-item-3"
|
||||
>
|
||||
{metrics.map((metric, index) => (
|
||||
<MetricCardItem
|
||||
key={`${metric.id}-${index}`}
|
||||
metric={metric}
|
||||
shouldUseLightText={shouldUseLightText}
|
||||
defaultButtonVariant={theme.defaultButtonVariant}
|
||||
cardClassName={cardClassName}
|
||||
cardTitleClassName={cardTitleClassName}
|
||||
subtitleClassName={subtitleClassName}
|
||||
categoryClassName={categoryClassName}
|
||||
valueClassName={valueClassName}
|
||||
footerClassName={footerClassName}
|
||||
cardButtonClassName={cardButtonClassName}
|
||||
cardButtonTextClassName={cardButtonTextClassName}
|
||||
/>
|
||||
))}
|
||||
</CardStack>
|
||||
);
|
||||
};
|
||||
|
||||
MetricCardTen.displayName = "MetricCardTen";
|
||||
|
||||
export default MetricCardTen;
|
||||
180
src/components/sections/metrics/MetricCardThree.tsx
Normal file
180
src/components/sections/metrics/MetricCardThree.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
"use client";
|
||||
|
||||
import { 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, CardAnimationTypeWith3D, TitleSegment } from "@/components/cardStack/types";
|
||||
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||
|
||||
type Metric = {
|
||||
id: string;
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
interface MetricCardThreeProps {
|
||||
metrics: Metric[];
|
||||
carouselMode?: "auto" | "buttons";
|
||||
uniformGridCustomHeightClasses?: string;
|
||||
animationType: CardAnimationTypeWith3D;
|
||||
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;
|
||||
iconContainerClassName?: string;
|
||||
iconClassName?: string;
|
||||
metricTitleClassName?: string;
|
||||
valueClassName?: 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;
|
||||
iconContainerClassName?: string;
|
||||
iconClassName?: string;
|
||||
metricTitleClassName?: string;
|
||||
valueClassName?: string;
|
||||
}
|
||||
|
||||
const MetricCardItem = memo(({
|
||||
metric,
|
||||
shouldUseLightText,
|
||||
cardClassName = "",
|
||||
iconContainerClassName = "",
|
||||
iconClassName = "",
|
||||
metricTitleClassName = "",
|
||||
valueClassName = "",
|
||||
}: MetricCardItemProps) => {
|
||||
return (
|
||||
<div className={cls("relative h-full card text-foreground rounded-theme-capped p-6 flex flex-col items-center justify-center gap-3", cardClassName)}>
|
||||
<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", 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="relative z-1 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>
|
||||
);
|
||||
});
|
||||
|
||||
MetricCardItem.displayName = "MetricCardItem";
|
||||
|
||||
const MetricCardThree = ({
|
||||
metrics,
|
||||
carouselMode = "buttons",
|
||||
uniformGridCustomHeightClasses = "min-h-70 2xl:min-h-80",
|
||||
animationType,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
textboxLayout,
|
||||
useInvertedBackground,
|
||||
ariaLabel = "Metrics section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
cardClassName = "",
|
||||
textBoxTitleClassName = "",
|
||||
textBoxTitleImageWrapperClassName = "",
|
||||
textBoxTitleImageClassName = "",
|
||||
textBoxDescriptionClassName = "",
|
||||
iconContainerClassName = "",
|
||||
iconClassName = "",
|
||||
metricTitleClassName = "",
|
||||
valueClassName = "",
|
||||
gridClassName = "",
|
||||
carouselClassName = "",
|
||||
controlsClassName = "",
|
||||
textBoxClassName = "",
|
||||
textBoxTagClassName = "",
|
||||
textBoxButtonContainerClassName = "",
|
||||
textBoxButtonClassName = "",
|
||||
textBoxButtonTextClassName = "",
|
||||
}: MetricCardThreeProps) => {
|
||||
const theme = useTheme();
|
||||
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
|
||||
|
||||
return (
|
||||
<CardStack
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
mode={carouselMode}
|
||||
gridVariant="uniform-all-items-equal"
|
||||
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
|
||||
animationType={animationType}
|
||||
supports3DAnimation={true}
|
||||
|
||||
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}
|
||||
iconContainerClassName={iconContainerClassName}
|
||||
iconClassName={iconClassName}
|
||||
metricTitleClassName={metricTitleClassName}
|
||||
valueClassName={valueClassName}
|
||||
/>
|
||||
))}
|
||||
</CardStack>
|
||||
);
|
||||
};
|
||||
|
||||
MetricCardThree.displayName = "MetricCardThree";
|
||||
|
||||
export default MetricCardThree;
|
||||
175
src/components/sections/metrics/MetricCardTwo.tsx
Normal file
175
src/components/sections/metrics/MetricCardTwo.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
"use client";
|
||||
|
||||
import { 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, CardAnimationTypeWith3D, TitleSegment } from "@/components/cardStack/types";
|
||||
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||
|
||||
type MetricCardTwoGridVariant = Extract<GridVariant, "uniform-all-items-equal" | "bento-grid" | "bento-grid-inverted">;
|
||||
|
||||
type Metric = {
|
||||
id: string;
|
||||
value: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
interface MetricCardTwoProps {
|
||||
metrics: Metric[];
|
||||
carouselMode?: "auto" | "buttons";
|
||||
gridVariant: MetricCardTwoGridVariant;
|
||||
uniformGridCustomHeightClasses?: string;
|
||||
animationType: CardAnimationTypeWith3D;
|
||||
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;
|
||||
metricDescriptionClassName?: 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;
|
||||
metricDescriptionClassName?: string;
|
||||
}
|
||||
|
||||
const MetricCardItem = memo(({
|
||||
metric,
|
||||
shouldUseLightText,
|
||||
cardClassName = "",
|
||||
valueClassName = "",
|
||||
metricDescriptionClassName = "",
|
||||
}: MetricCardItemProps) => {
|
||||
return (
|
||||
<div className={cls("relative h-full card text-foreground rounded-theme-capped p-6 flex flex-col justify-between", cardClassName)}>
|
||||
<div className={cls("relative z-1 text-9xl md:text-7xl font-medium", shouldUseLightText ? "text-background" : "text-foreground", valueClassName)}>
|
||||
{metric.value}
|
||||
</div>
|
||||
<p className={cls("relative z-1 text-xl", shouldUseLightText ? "text-background" : "text-foreground", metricDescriptionClassName)}>
|
||||
{metric.description}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
MetricCardItem.displayName = "MetricCardItem";
|
||||
|
||||
const MetricCardTwo = ({
|
||||
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 = "",
|
||||
metricDescriptionClassName = "",
|
||||
gridClassName = "",
|
||||
carouselClassName = "",
|
||||
controlsClassName = "",
|
||||
textBoxClassName = "",
|
||||
textBoxTagClassName = "",
|
||||
textBoxButtonContainerClassName = "",
|
||||
textBoxButtonClassName = "",
|
||||
textBoxButtonTextClassName = "",
|
||||
}: MetricCardTwoProps) => {
|
||||
const theme = useTheme();
|
||||
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
|
||||
|
||||
const customUniformHeight = gridVariant === "uniform-all-items-equal"
|
||||
? "min-h-70 2xl:min-h-80"
|
||||
: uniformGridCustomHeightClasses;
|
||||
|
||||
const customGridRows = (gridVariant === "bento-grid" || gridVariant === "bento-grid-inverted")
|
||||
? "md:grid-rows-[14rem_14rem] 2xl:grid-rows-[17rem_17rem]"
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<CardStack
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
mode={carouselMode}
|
||||
gridVariant={gridVariant}
|
||||
uniformGridCustomHeightClasses={customUniformHeight}
|
||||
gridRowsClassName={customGridRows}
|
||||
animationType={animationType}
|
||||
supports3DAnimation={true}
|
||||
|
||||
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}
|
||||
metricDescriptionClassName={metricDescriptionClassName}
|
||||
/>
|
||||
))}
|
||||
</CardStack>
|
||||
);
|
||||
};
|
||||
|
||||
MetricCardTwo.displayName = "MetricCardTwo";
|
||||
|
||||
export default MetricCardTwo;
|
||||
242
src/components/sections/pricing/PricingCardEight.tsx
Normal file
242
src/components/sections/pricing/PricingCardEight.tsx
Normal file
@@ -0,0 +1,242 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import CardStack from "@/components/cardStack/CardStack";
|
||||
import Button from "@/components/button/Button";
|
||||
import PricingBadge from "@/components/shared/PricingBadge";
|
||||
import PricingFeatureList from "@/components/shared/PricingFeatureList";
|
||||
import { getButtonProps } from "@/lib/buttonUtils";
|
||||
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 PricingPlan = {
|
||||
id: string;
|
||||
badge: string;
|
||||
badgeIcon?: LucideIcon;
|
||||
price: string;
|
||||
subtitle: string;
|
||||
buttons: ButtonConfig[];
|
||||
features: string[];
|
||||
};
|
||||
|
||||
interface PricingCardEightProps {
|
||||
plans: PricingPlan[];
|
||||
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;
|
||||
textBoxTitleClassName?: string;
|
||||
textBoxTitleImageWrapperClassName?: string;
|
||||
textBoxTitleImageClassName?: string;
|
||||
textBoxDescriptionClassName?: string;
|
||||
badgeClassName?: string;
|
||||
priceClassName?: string;
|
||||
subtitleClassName?: string;
|
||||
planButtonContainerClassName?: string;
|
||||
planButtonClassName?: string;
|
||||
featuresClassName?: string;
|
||||
featureItemClassName?: string;
|
||||
gridClassName?: string;
|
||||
carouselClassName?: string;
|
||||
controlsClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
textBoxTagClassName?: string;
|
||||
textBoxButtonContainerClassName?: string;
|
||||
textBoxButtonClassName?: string;
|
||||
textBoxButtonTextClassName?: string;
|
||||
}
|
||||
|
||||
interface PricingCardItemProps {
|
||||
plan: PricingPlan;
|
||||
shouldUseLightText: boolean;
|
||||
cardClassName?: string;
|
||||
badgeClassName?: string;
|
||||
priceClassName?: string;
|
||||
subtitleClassName?: string;
|
||||
planButtonContainerClassName?: string;
|
||||
planButtonClassName?: string;
|
||||
featuresClassName?: string;
|
||||
featureItemClassName?: string;
|
||||
}
|
||||
|
||||
const PricingCardItem = memo(({
|
||||
plan,
|
||||
shouldUseLightText,
|
||||
cardClassName = "",
|
||||
badgeClassName = "",
|
||||
priceClassName = "",
|
||||
subtitleClassName = "",
|
||||
planButtonContainerClassName = "",
|
||||
planButtonClassName = "",
|
||||
featuresClassName = "",
|
||||
featureItemClassName = "",
|
||||
}: PricingCardItemProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const getButtonConfigProps = () => {
|
||||
if (theme.defaultButtonVariant === "hover-bubble") {
|
||||
return { bgClassName: "w-full" };
|
||||
}
|
||||
if (theme.defaultButtonVariant === "icon-arrow") {
|
||||
return { className: "justify-between" };
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cls("relative h-full card text-foreground rounded-theme-capped p-3 flex flex-col gap-3", cardClassName)}>
|
||||
<div className="relative secondary-button p-3 flex flex-col gap-3 rounded-theme-capped" >
|
||||
<PricingBadge
|
||||
badge={plan.badge}
|
||||
badgeIcon={plan.badgeIcon}
|
||||
className={badgeClassName}
|
||||
/>
|
||||
|
||||
<div className="relative z-1 flex flex-col gap-1">
|
||||
<div className="text-5xl font-medium text-foreground">
|
||||
{plan.price}
|
||||
</div>
|
||||
|
||||
<p className="text-base text-foreground">
|
||||
{plan.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{plan.buttons && plan.buttons.length > 0 && (
|
||||
<div className={cls("relative z-1 w-full flex flex-col gap-3", planButtonContainerClassName)}>
|
||||
{plan.buttons.slice(0, 2).map((button, index) => (
|
||||
<Button
|
||||
key={`${button.text}-${index}`}
|
||||
{...getButtonProps(
|
||||
{ ...button, props: { ...button.props, ...getButtonConfigProps() } },
|
||||
index,
|
||||
theme.defaultButtonVariant,
|
||||
cls("w-full", planButtonClassName)
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-3 pt-0" >
|
||||
<PricingFeatureList
|
||||
features={plan.features}
|
||||
shouldUseLightText={shouldUseLightText}
|
||||
className={cls("mt-1", featuresClassName)}
|
||||
featureItemClassName={featureItemClassName}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
PricingCardItem.displayName = "PricingCardItem";
|
||||
|
||||
const PricingCardEight = ({
|
||||
plans,
|
||||
carouselMode = "buttons",
|
||||
uniformGridCustomHeightClasses,
|
||||
animationType,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
textboxLayout,
|
||||
useInvertedBackground,
|
||||
ariaLabel = "Pricing section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
cardClassName = "",
|
||||
textBoxTitleClassName = "",
|
||||
textBoxTitleImageWrapperClassName = "",
|
||||
textBoxTitleImageClassName = "",
|
||||
textBoxDescriptionClassName = "",
|
||||
badgeClassName = "",
|
||||
priceClassName = "",
|
||||
subtitleClassName = "",
|
||||
planButtonContainerClassName = "",
|
||||
planButtonClassName = "",
|
||||
featuresClassName = "",
|
||||
featureItemClassName = "",
|
||||
gridClassName = "",
|
||||
carouselClassName = "",
|
||||
controlsClassName = "",
|
||||
textBoxClassName = "",
|
||||
textBoxTagClassName = "",
|
||||
textBoxButtonContainerClassName = "",
|
||||
textBoxButtonClassName = "",
|
||||
textBoxButtonTextClassName = "",
|
||||
}: PricingCardEightProps) => {
|
||||
const theme = useTheme();
|
||||
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
|
||||
|
||||
return (
|
||||
<CardStack
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
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}
|
||||
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}
|
||||
>
|
||||
{plans.map((plan, index) => (
|
||||
<PricingCardItem
|
||||
key={`${plan.id}-${index}`}
|
||||
plan={plan}
|
||||
shouldUseLightText={shouldUseLightText}
|
||||
cardClassName={cardClassName}
|
||||
badgeClassName={badgeClassName}
|
||||
priceClassName={priceClassName}
|
||||
subtitleClassName={subtitleClassName}
|
||||
planButtonContainerClassName={planButtonContainerClassName}
|
||||
planButtonClassName={planButtonClassName}
|
||||
featuresClassName={featuresClassName}
|
||||
featureItemClassName={featureItemClassName}
|
||||
/>
|
||||
))}
|
||||
</CardStack>
|
||||
);
|
||||
};
|
||||
|
||||
PricingCardEight.displayName = "PricingCardEight";
|
||||
|
||||
export default PricingCardEight;
|
||||
225
src/components/sections/pricing/PricingCardFive.tsx
Normal file
225
src/components/sections/pricing/PricingCardFive.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
"use client";
|
||||
|
||||
import { Check } from "lucide-react";
|
||||
import CardList from "@/components/cardStack/CardList";
|
||||
import Tag from "@/components/shared/Tag";
|
||||
import Button from "@/components/button/Button";
|
||||
import { getButtonProps } from "@/lib/buttonUtils";
|
||||
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 PricingPlan = {
|
||||
id: string;
|
||||
tag: string;
|
||||
tagIcon?: LucideIcon;
|
||||
price: string;
|
||||
period: string;
|
||||
description: string;
|
||||
button: ButtonConfig;
|
||||
featuresTitle: string;
|
||||
features: string[];
|
||||
};
|
||||
|
||||
interface PricingCardFiveProps {
|
||||
plans: PricingPlan[];
|
||||
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;
|
||||
planTagClassName?: string;
|
||||
planPriceClassName?: string;
|
||||
planPeriodClassName?: string;
|
||||
planDescriptionClassName?: string;
|
||||
planButtonClassName?: string;
|
||||
planButtonTextClassName?: string;
|
||||
featuresTitleClassName?: string;
|
||||
featuresListClassName?: string;
|
||||
featureItemClassName?: string;
|
||||
featureIconClassName?: string;
|
||||
featureTextClassName?: string;
|
||||
}
|
||||
|
||||
const PricingCardFive = ({
|
||||
plans,
|
||||
animationType,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
textboxLayout,
|
||||
useInvertedBackground,
|
||||
ariaLabel = "Pricing section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
cardClassName = "",
|
||||
textBoxTitleClassName = "",
|
||||
textBoxDescriptionClassName = "",
|
||||
textBoxClassName = "",
|
||||
textBoxTagClassName = "",
|
||||
textBoxButtonContainerClassName = "",
|
||||
textBoxButtonClassName = "",
|
||||
textBoxButtonTextClassName = "",
|
||||
titleImageWrapperClassName = "",
|
||||
titleImageClassName = "",
|
||||
cardContentClassName = "",
|
||||
planTagClassName = "",
|
||||
planPriceClassName = "",
|
||||
planPeriodClassName = "",
|
||||
planDescriptionClassName = "",
|
||||
planButtonClassName = "",
|
||||
planButtonTextClassName = "",
|
||||
featuresTitleClassName = "",
|
||||
featuresListClassName = "",
|
||||
featureItemClassName = "",
|
||||
featureIconClassName = "",
|
||||
featureTextClassName = "",
|
||||
}: PricingCardFiveProps) => {
|
||||
const theme = useTheme();
|
||||
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
|
||||
|
||||
const getButtonConfigProps = () => {
|
||||
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}
|
||||
>
|
||||
{plans.map((plan) => (
|
||||
<div
|
||||
key={plan.id}
|
||||
className={cls(
|
||||
"relative z-1 w-full min-h-0 h-full flex flex-col md:flex-row justify-between items-stretch gap-8 md:gap-15 p-6 md:p-15",
|
||||
cardContentClassName
|
||||
)}
|
||||
>
|
||||
<div className="w-full md:w-1/2 min-w-0 flex flex-col justify-between gap-6">
|
||||
<div className="flex flex-col gap-4">
|
||||
<Tag
|
||||
text={plan.tag}
|
||||
icon={plan.tagIcon}
|
||||
className={planTagClassName}
|
||||
/>
|
||||
<div className="flex items-baseline gap-1 mt-1">
|
||||
<span className={cls(
|
||||
"text-5xl md:text-6xl font-medium",
|
||||
shouldUseLightText ? "text-background" : "text-foreground",
|
||||
planPriceClassName
|
||||
)}>
|
||||
{plan.price}
|
||||
</span>
|
||||
<span className={cls(
|
||||
"text-xl",
|
||||
shouldUseLightText ? "text-background" : "text-foreground",
|
||||
planPeriodClassName
|
||||
)}>
|
||||
{plan.period}
|
||||
</span>
|
||||
</div>
|
||||
<p className={cls(
|
||||
"text-2xl leading-tight text-balance",
|
||||
shouldUseLightText ? "text-background" : "text-foreground",
|
||||
planDescriptionClassName
|
||||
)}>
|
||||
{plan.description}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
{...getButtonProps(
|
||||
{ ...plan.button, props: { ...plan.button.props, ...getButtonConfigProps() } },
|
||||
0,
|
||||
theme.defaultButtonVariant,
|
||||
cls("w-full h-12", planButtonClassName),
|
||||
planButtonTextClassName
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative z-1 w-full h-px bg-foreground/20 md:hidden" />
|
||||
|
||||
<div className="w-full md:w-1/2 min-w-0 flex flex-col gap-4">
|
||||
<h3 className={cls(
|
||||
"text-xl",
|
||||
shouldUseLightText ? "text-background" : "text-foreground",
|
||||
featuresTitleClassName
|
||||
)}>
|
||||
{plan.featuresTitle}
|
||||
</h3>
|
||||
<ul className={cls("flex flex-col gap-3", featuresListClassName)}>
|
||||
{plan.features.map((feature, index) => (
|
||||
<li key={index} className={cls("flex items-start gap-3", featureItemClassName)}>
|
||||
<div className={cls("flex-shrink-0 h-6 w-auto aspect-square rounded-theme primary-button flex items-center justify-center", featureIconClassName)}>
|
||||
<Check className="h-4/10 w-4/10 text-background" strokeWidth={2.5} />
|
||||
</div>
|
||||
<span className={cls(
|
||||
"text-sm leading-[1.4]",
|
||||
shouldUseLightText ? "text-background/80" : "text-foreground/80",
|
||||
featureTextClassName
|
||||
)}>
|
||||
{feature}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardList>
|
||||
);
|
||||
};
|
||||
|
||||
PricingCardFive.displayName = "PricingCardFive";
|
||||
|
||||
export default PricingCardFive;
|
||||
206
src/components/sections/pricing/PricingCardNine.tsx
Normal file
206
src/components/sections/pricing/PricingCardNine.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
"use client";
|
||||
|
||||
import { Check } from "lucide-react";
|
||||
import CardList from "@/components/cardStack/CardList";
|
||||
import Button from "@/components/button/Button";
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
import Tag from "@/components/shared/Tag";
|
||||
import { getButtonProps } from "@/lib/buttonUtils";
|
||||
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 PricingPlan = {
|
||||
id: string;
|
||||
title: string;
|
||||
price: string;
|
||||
period: string;
|
||||
features: string[];
|
||||
button: ButtonConfig;
|
||||
imageSrc: string;
|
||||
imageAlt?: string;
|
||||
};
|
||||
|
||||
interface PricingCardNineProps {
|
||||
plans: PricingPlan[];
|
||||
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;
|
||||
planImageWrapperClassName?: string;
|
||||
planImageClassName?: string;
|
||||
planTitleClassName?: string;
|
||||
planPriceClassName?: string;
|
||||
planButtonClassName?: string;
|
||||
planButtonTextClassName?: string;
|
||||
featuresListClassName?: string;
|
||||
featureItemClassName?: string;
|
||||
featureIconClassName?: string;
|
||||
featureTextClassName?: string;
|
||||
}
|
||||
|
||||
const PricingCardNine = ({
|
||||
plans,
|
||||
animationType,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
textboxLayout,
|
||||
useInvertedBackground,
|
||||
ariaLabel = "Pricing section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
cardClassName = "",
|
||||
textBoxTitleClassName = "",
|
||||
textBoxDescriptionClassName = "",
|
||||
textBoxClassName = "",
|
||||
textBoxTagClassName = "",
|
||||
textBoxButtonContainerClassName = "",
|
||||
textBoxButtonClassName = "",
|
||||
textBoxButtonTextClassName = "",
|
||||
titleImageWrapperClassName = "",
|
||||
titleImageClassName = "",
|
||||
cardContentClassName = "",
|
||||
planImageWrapperClassName = "",
|
||||
planImageClassName = "",
|
||||
planTitleClassName = "",
|
||||
planPriceClassName = "",
|
||||
planButtonClassName = "",
|
||||
planButtonTextClassName = "",
|
||||
featuresListClassName = "",
|
||||
featureItemClassName = "",
|
||||
featureIconClassName = "",
|
||||
featureTextClassName = "",
|
||||
}: PricingCardNineProps) => {
|
||||
const theme = useTheme();
|
||||
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
|
||||
|
||||
const getButtonConfigProps = () => {
|
||||
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}
|
||||
>
|
||||
{plans.map((plan) => (
|
||||
<div
|
||||
key={plan.id}
|
||||
className={cls(
|
||||
"relative z-1 w-full min-h-0 h-full flex flex-col md:flex-row items-stretch gap-6 md:gap-10 p-4 md:p-6",
|
||||
cardContentClassName
|
||||
)}
|
||||
>
|
||||
<div className={cls("w-full md:w-1/2 min-w-0 aspect-square md:aspect-[4/3]", planImageWrapperClassName)}>
|
||||
<MediaContent
|
||||
imageSrc={plan.imageSrc}
|
||||
imageAlt={plan.imageAlt || plan.title}
|
||||
imageClassName={cls("w-full h-full object-cover rounded-theme", planImageClassName)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-full md:w-1/2 min-w-0 flex flex-col justify-center gap-6 py-2">
|
||||
<div className="flex flex-col gap-4">
|
||||
<Tag
|
||||
text={`${plan.price}${plan.period}`}
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
className={planPriceClassName}
|
||||
/>
|
||||
|
||||
<h3 className={cls(
|
||||
"text-4xl md:text-5xl font-medium mb-1 truncate",
|
||||
shouldUseLightText ? "text-background" : "text-foreground",
|
||||
planTitleClassName
|
||||
)}>
|
||||
{plan.title}
|
||||
</h3>
|
||||
|
||||
<ul className={cls("flex flex-col gap-3", featuresListClassName)}>
|
||||
{plan.features.map((feature, index) => (
|
||||
<li key={index} className={cls("flex items-start gap-3", featureItemClassName)}>
|
||||
<div className={cls("flex-shrink-0 h-6 w-auto aspect-square rounded-theme primary-button flex items-center justify-center", featureIconClassName)}>
|
||||
<Check className="h-4/10 w-4/10 text-background" strokeWidth={2.5} />
|
||||
</div>
|
||||
<span className={cls(
|
||||
"text-sm leading-[1.4]",
|
||||
shouldUseLightText ? "text-background/80" : "text-foreground/80",
|
||||
featureTextClassName
|
||||
)}>
|
||||
{feature}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
{...getButtonProps(
|
||||
{ ...plan.button, props: { ...plan.button.props, ...getButtonConfigProps() } },
|
||||
0,
|
||||
theme.defaultButtonVariant,
|
||||
planButtonClassName,
|
||||
planButtonTextClassName
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardList>
|
||||
);
|
||||
};
|
||||
|
||||
PricingCardNine.displayName = "PricingCardNine";
|
||||
|
||||
export default PricingCardNine;
|
||||
200
src/components/sections/pricing/PricingCardOne.tsx
Normal file
200
src/components/sections/pricing/PricingCardOne.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import CardStack from "@/components/cardStack/CardStack";
|
||||
import PricingBadge from "@/components/shared/PricingBadge";
|
||||
import PricingFeatureList from "@/components/shared/PricingFeatureList";
|
||||
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig, CardAnimationTypeWith3D, TitleSegment } from "@/components/cardStack/types";
|
||||
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||
|
||||
type PricingPlan = {
|
||||
id: string;
|
||||
badge: string;
|
||||
badgeIcon?: LucideIcon;
|
||||
price: string;
|
||||
subtitle: string;
|
||||
features: string[];
|
||||
};
|
||||
|
||||
interface PricingCardOneProps {
|
||||
plans: PricingPlan[];
|
||||
carouselMode?: "auto" | "buttons";
|
||||
uniformGridCustomHeightClasses?: string;
|
||||
animationType: CardAnimationTypeWith3D;
|
||||
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;
|
||||
badgeClassName?: string;
|
||||
priceClassName?: string;
|
||||
subtitleClassName?: string;
|
||||
featuresClassName?: string;
|
||||
featureItemClassName?: string;
|
||||
gridClassName?: string;
|
||||
carouselClassName?: string;
|
||||
controlsClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
textBoxTagClassName?: string;
|
||||
textBoxButtonContainerClassName?: string;
|
||||
textBoxButtonClassName?: string;
|
||||
textBoxButtonTextClassName?: string;
|
||||
}
|
||||
|
||||
interface PricingCardItemProps {
|
||||
plan: PricingPlan;
|
||||
shouldUseLightText: boolean;
|
||||
cardClassName?: string;
|
||||
badgeClassName?: string;
|
||||
priceClassName?: string;
|
||||
subtitleClassName?: string;
|
||||
featuresClassName?: string;
|
||||
featureItemClassName?: string;
|
||||
}
|
||||
|
||||
const PricingCardItem = memo(({
|
||||
plan,
|
||||
shouldUseLightText,
|
||||
cardClassName = "",
|
||||
badgeClassName = "",
|
||||
priceClassName = "",
|
||||
subtitleClassName = "",
|
||||
featuresClassName = "",
|
||||
featureItemClassName = "",
|
||||
}: PricingCardItemProps) => {
|
||||
return (
|
||||
<div className={cls("relative h-full card text-foreground rounded-theme-capped p-6 flex flex-col gap-6 md:gap-8", cardClassName)}>
|
||||
<PricingBadge
|
||||
badge={plan.badge}
|
||||
badgeIcon={plan.badgeIcon}
|
||||
className={badgeClassName}
|
||||
/>
|
||||
|
||||
<div className="relative z-1 flex flex-col gap-1">
|
||||
<div className={cls("text-5xl font-medium", shouldUseLightText ? "text-background" : "text-foreground", priceClassName)}>
|
||||
{plan.price}
|
||||
</div>
|
||||
|
||||
<p className={cls("text-base", shouldUseLightText ? "text-background" : "text-foreground", subtitleClassName)}>
|
||||
{plan.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="relative z-1 w-full h-px bg-foreground/20" />
|
||||
|
||||
<PricingFeatureList
|
||||
features={plan.features}
|
||||
shouldUseLightText={shouldUseLightText}
|
||||
className={cls("mt-1", featuresClassName)}
|
||||
featureItemClassName={featureItemClassName}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
PricingCardItem.displayName = "PricingCardItem";
|
||||
|
||||
const PricingCardOne = ({
|
||||
plans,
|
||||
carouselMode = "buttons",
|
||||
uniformGridCustomHeightClasses,
|
||||
animationType,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
textboxLayout,
|
||||
useInvertedBackground,
|
||||
ariaLabel = "Pricing section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
cardClassName = "",
|
||||
textBoxTitleClassName = "",
|
||||
textBoxTitleImageWrapperClassName = "",
|
||||
textBoxTitleImageClassName = "",
|
||||
textBoxDescriptionClassName = "",
|
||||
badgeClassName = "",
|
||||
priceClassName = "",
|
||||
subtitleClassName = "",
|
||||
featuresClassName = "",
|
||||
featureItemClassName = "",
|
||||
gridClassName = "",
|
||||
carouselClassName = "",
|
||||
controlsClassName = "",
|
||||
textBoxClassName = "",
|
||||
textBoxTagClassName = "",
|
||||
textBoxButtonContainerClassName = "",
|
||||
textBoxButtonClassName = "",
|
||||
textBoxButtonTextClassName = "",
|
||||
}: PricingCardOneProps) => {
|
||||
const theme = useTheme();
|
||||
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
|
||||
|
||||
return (
|
||||
<CardStack
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
mode={carouselMode}
|
||||
gridVariant="uniform-all-items-equal"
|
||||
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
|
||||
animationType={animationType}
|
||||
supports3DAnimation={true}
|
||||
|
||||
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}
|
||||
>
|
||||
{plans.map((plan, index) => (
|
||||
<PricingCardItem
|
||||
key={`${plan.id}-${index}`}
|
||||
plan={plan}
|
||||
shouldUseLightText={shouldUseLightText}
|
||||
cardClassName={cardClassName}
|
||||
badgeClassName={badgeClassName}
|
||||
priceClassName={priceClassName}
|
||||
subtitleClassName={subtitleClassName}
|
||||
featuresClassName={featuresClassName}
|
||||
featureItemClassName={featureItemClassName}
|
||||
/>
|
||||
))}
|
||||
</CardStack>
|
||||
);
|
||||
};
|
||||
|
||||
PricingCardOne.displayName = "PricingCardOne";
|
||||
|
||||
export default PricingCardOne;
|
||||
240
src/components/sections/pricing/PricingCardThree.tsx
Normal file
240
src/components/sections/pricing/PricingCardThree.tsx
Normal file
@@ -0,0 +1,240 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import CardStack from "@/components/cardStack/CardStack";
|
||||
import PricingFeatureList from "@/components/shared/PricingFeatureList";
|
||||
import Button from "@/components/button/Button";
|
||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||
import { getButtonProps } from "@/lib/buttonUtils";
|
||||
import { cls, shouldUseInvertedText } 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 PricingPlan = {
|
||||
id: string;
|
||||
badge?: string;
|
||||
badgeIcon?: LucideIcon;
|
||||
price: string;
|
||||
name: string;
|
||||
buttons: ButtonConfig[];
|
||||
features: string[];
|
||||
};
|
||||
|
||||
interface PricingCardThreeProps {
|
||||
plans: PricingPlan[];
|
||||
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;
|
||||
textBoxTitleClassName?: string;
|
||||
textBoxTitleImageWrapperClassName?: string;
|
||||
textBoxTitleImageClassName?: string;
|
||||
textBoxDescriptionClassName?: string;
|
||||
badgeClassName?: string;
|
||||
priceClassName?: string;
|
||||
nameClassName?: string;
|
||||
planButtonContainerClassName?: string;
|
||||
planButtonClassName?: string;
|
||||
featuresClassName?: string;
|
||||
featureItemClassName?: string;
|
||||
gridClassName?: string;
|
||||
carouselClassName?: string;
|
||||
controlsClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
textBoxTagClassName?: string;
|
||||
textBoxButtonContainerClassName?: string;
|
||||
textBoxButtonClassName?: string;
|
||||
textBoxButtonTextClassName?: string;
|
||||
}
|
||||
|
||||
interface PricingCardItemProps {
|
||||
plan: PricingPlan;
|
||||
shouldUseLightText: boolean;
|
||||
cardClassName?: string;
|
||||
badgeClassName?: string;
|
||||
priceClassName?: string;
|
||||
nameClassName?: string;
|
||||
planButtonContainerClassName?: string;
|
||||
planButtonClassName?: string;
|
||||
featuresClassName?: string;
|
||||
featureItemClassName?: string;
|
||||
}
|
||||
|
||||
const PricingCardItem = memo(({
|
||||
plan,
|
||||
shouldUseLightText,
|
||||
cardClassName = "",
|
||||
badgeClassName = "",
|
||||
priceClassName = "",
|
||||
nameClassName = "",
|
||||
planButtonContainerClassName = "",
|
||||
planButtonClassName = "",
|
||||
featuresClassName = "",
|
||||
featureItemClassName = "",
|
||||
}: PricingCardItemProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const getButtonConfigProps = () => {
|
||||
if (theme.defaultButtonVariant === "hover-bubble") {
|
||||
return { bgClassName: "w-full" };
|
||||
}
|
||||
if (theme.defaultButtonVariant === "icon-arrow") {
|
||||
return { className: "justify-between" };
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative h-full flex flex-col">
|
||||
<div className={cls("px-4 py-3 primary-button rounded-t-theme-capped rounded-b-none text-base text-background whitespace-nowrap z-10 flex items-center justify-center gap-2", plan.badge ? "visible" : "invisible", badgeClassName)}>
|
||||
{plan.badgeIcon && <plan.badgeIcon className="inline h-[1em] w-auto" />}
|
||||
{plan.badge || "placeholder"}
|
||||
</div>
|
||||
<div className={cls("relative min-h-0 h-full card text-foreground p-6 flex flex-col items-center gap-6 md:gap-8", plan.badge ? "rounded-t-none rounded-b-theme-capped" : "rounded-theme-capped", cardClassName)}>
|
||||
|
||||
<div className="relative z-1 flex flex-col gap-2 text-center">
|
||||
<div className={cls("text-5xl font-medium", shouldUseLightText ? "text-background" : "text-foreground", priceClassName)}>
|
||||
{plan.price}
|
||||
</div>
|
||||
|
||||
<h3 className={cls("text-xl font-medium leading-[1.1]", shouldUseLightText ? "text-background" : "text-foreground", nameClassName)}>
|
||||
{plan.name}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="relative z-1 w-full h-px bg-foreground/10" />
|
||||
|
||||
<PricingFeatureList
|
||||
features={plan.features}
|
||||
shouldUseLightText={shouldUseLightText}
|
||||
className={featuresClassName}
|
||||
featureItemClassName={featureItemClassName}
|
||||
/>
|
||||
|
||||
{plan.buttons && plan.buttons.length > 0 && (
|
||||
<div className={cls("relative z-1 w-full flex flex-col gap-3", planButtonContainerClassName)}>
|
||||
{plan.buttons.slice(0, 2).map((button, index) => (
|
||||
<Button
|
||||
key={`${button.text}-${index}`}
|
||||
{...getButtonProps(
|
||||
{ ...button, props: { ...button.props, ...getButtonConfigProps() } },
|
||||
index,
|
||||
theme.defaultButtonVariant,
|
||||
cls("w-full", planButtonClassName)
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
PricingCardItem.displayName = "PricingCardItem";
|
||||
|
||||
const PricingCardThree = ({
|
||||
plans,
|
||||
carouselMode = "buttons",
|
||||
uniformGridCustomHeightClasses,
|
||||
animationType,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
textboxLayout,
|
||||
useInvertedBackground,
|
||||
ariaLabel = "Pricing section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
cardClassName = "",
|
||||
textBoxTitleClassName = "",
|
||||
textBoxTitleImageWrapperClassName = "",
|
||||
textBoxTitleImageClassName = "",
|
||||
textBoxDescriptionClassName = "",
|
||||
badgeClassName = "",
|
||||
priceClassName = "",
|
||||
nameClassName = "",
|
||||
planButtonContainerClassName = "",
|
||||
planButtonClassName = "",
|
||||
featuresClassName = "",
|
||||
featureItemClassName = "",
|
||||
gridClassName = "",
|
||||
carouselClassName = "",
|
||||
controlsClassName = "",
|
||||
textBoxClassName = "",
|
||||
textBoxTagClassName = "",
|
||||
textBoxButtonContainerClassName = "",
|
||||
textBoxButtonClassName = "",
|
||||
textBoxButtonTextClassName = "",
|
||||
}: PricingCardThreeProps) => {
|
||||
const theme = useTheme();
|
||||
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
|
||||
|
||||
return (
|
||||
<CardStack
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
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}
|
||||
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}
|
||||
>
|
||||
{plans.map((plan, index) => (
|
||||
<PricingCardItem
|
||||
key={`${plan.id}-${index}`}
|
||||
plan={plan}
|
||||
shouldUseLightText={shouldUseLightText}
|
||||
cardClassName={cardClassName}
|
||||
badgeClassName={badgeClassName}
|
||||
priceClassName={priceClassName}
|
||||
nameClassName={nameClassName}
|
||||
planButtonContainerClassName={planButtonContainerClassName}
|
||||
planButtonClassName={planButtonClassName}
|
||||
featuresClassName={featuresClassName}
|
||||
featureItemClassName={featureItemClassName}
|
||||
/>
|
||||
))}
|
||||
</CardStack>
|
||||
);
|
||||
};
|
||||
|
||||
PricingCardThree.displayName = "PricingCardThree";
|
||||
|
||||
export default PricingCardThree;
|
||||
240
src/components/sections/pricing/PricingCardTwo.tsx
Normal file
240
src/components/sections/pricing/PricingCardTwo.tsx
Normal file
@@ -0,0 +1,240 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import CardStack from "@/components/cardStack/CardStack";
|
||||
import PricingBadge from "@/components/shared/PricingBadge";
|
||||
import PricingFeatureList from "@/components/shared/PricingFeatureList";
|
||||
import Button from "@/components/button/Button";
|
||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||
import { getButtonProps } from "@/lib/buttonUtils";
|
||||
import { cls, shouldUseInvertedText } 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 PricingPlan = {
|
||||
id: string;
|
||||
badge: string;
|
||||
badgeIcon?: LucideIcon;
|
||||
price: string;
|
||||
subtitle: string;
|
||||
buttons: ButtonConfig[];
|
||||
features: string[];
|
||||
};
|
||||
|
||||
interface PricingCardTwoProps {
|
||||
plans: PricingPlan[];
|
||||
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;
|
||||
textBoxTitleClassName?: string;
|
||||
textBoxTitleImageWrapperClassName?: string;
|
||||
textBoxTitleImageClassName?: string;
|
||||
textBoxDescriptionClassName?: string;
|
||||
badgeClassName?: string;
|
||||
priceClassName?: string;
|
||||
subtitleClassName?: string;
|
||||
planButtonContainerClassName?: string;
|
||||
planButtonClassName?: string;
|
||||
featuresClassName?: string;
|
||||
featureItemClassName?: string;
|
||||
gridClassName?: string;
|
||||
carouselClassName?: string;
|
||||
controlsClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
textBoxTagClassName?: string;
|
||||
textBoxButtonContainerClassName?: string;
|
||||
textBoxButtonClassName?: string;
|
||||
textBoxButtonTextClassName?: string;
|
||||
}
|
||||
|
||||
interface PricingCardItemProps {
|
||||
plan: PricingPlan;
|
||||
shouldUseLightText: boolean;
|
||||
cardClassName?: string;
|
||||
badgeClassName?: string;
|
||||
priceClassName?: string;
|
||||
subtitleClassName?: string;
|
||||
planButtonContainerClassName?: string;
|
||||
planButtonClassName?: string;
|
||||
featuresClassName?: string;
|
||||
featureItemClassName?: string;
|
||||
}
|
||||
|
||||
const PricingCardItem = memo(({
|
||||
plan,
|
||||
shouldUseLightText,
|
||||
cardClassName = "",
|
||||
badgeClassName = "",
|
||||
priceClassName = "",
|
||||
subtitleClassName = "",
|
||||
planButtonContainerClassName = "",
|
||||
planButtonClassName = "",
|
||||
featuresClassName = "",
|
||||
featureItemClassName = "",
|
||||
}: PricingCardItemProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const getButtonConfigProps = () => {
|
||||
if (theme.defaultButtonVariant === "hover-bubble") {
|
||||
return { bgClassName: "w-full" };
|
||||
}
|
||||
if (theme.defaultButtonVariant === "icon-arrow") {
|
||||
return { className: "justify-between" };
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cls("relative h-full card text-foreground rounded-theme-capped p-6 flex flex-col items-center gap-6 md:gap-8", cardClassName)}>
|
||||
<PricingBadge
|
||||
badge={plan.badge}
|
||||
badgeIcon={plan.badgeIcon}
|
||||
className={badgeClassName}
|
||||
/>
|
||||
|
||||
<div className="relative z-1 flex flex-col gap-1 text-center">
|
||||
<div className={cls("text-5xl font-medium", shouldUseLightText ? "text-background" : "text-foreground", priceClassName)}>
|
||||
{plan.price}
|
||||
</div>
|
||||
|
||||
<p className={cls("text-base", shouldUseLightText ? "text-background" : "text-foreground", subtitleClassName)}>
|
||||
{plan.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{plan.buttons && plan.buttons.length > 0 && (
|
||||
<div className={cls("relative z-1 w-full flex flex-col gap-3", planButtonContainerClassName)}>
|
||||
{plan.buttons.slice(0, 2).map((button, index) => (
|
||||
<Button
|
||||
key={`${button.text}-${index}`}
|
||||
{...getButtonProps(
|
||||
{ ...button, props: { ...button.props, ...getButtonConfigProps() } },
|
||||
index,
|
||||
theme.defaultButtonVariant,
|
||||
cls("w-full", planButtonClassName)
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="relative z-1 w-full h-px bg-foreground/10 my-3" />
|
||||
|
||||
<PricingFeatureList
|
||||
features={plan.features}
|
||||
shouldUseLightText={shouldUseLightText}
|
||||
className={featuresClassName}
|
||||
featureItemClassName={featureItemClassName}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
PricingCardItem.displayName = "PricingCardItem";
|
||||
|
||||
const PricingCardTwo = ({
|
||||
plans,
|
||||
carouselMode = "buttons",
|
||||
uniformGridCustomHeightClasses,
|
||||
animationType,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
textboxLayout,
|
||||
useInvertedBackground,
|
||||
ariaLabel = "Pricing section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
cardClassName = "",
|
||||
textBoxTitleClassName = "",
|
||||
textBoxTitleImageWrapperClassName = "",
|
||||
textBoxTitleImageClassName = "",
|
||||
textBoxDescriptionClassName = "",
|
||||
badgeClassName = "",
|
||||
priceClassName = "",
|
||||
subtitleClassName = "",
|
||||
planButtonContainerClassName = "",
|
||||
planButtonClassName = "",
|
||||
featuresClassName = "",
|
||||
featureItemClassName = "",
|
||||
gridClassName = "",
|
||||
carouselClassName = "",
|
||||
controlsClassName = "",
|
||||
textBoxClassName = "",
|
||||
textBoxTagClassName = "",
|
||||
textBoxButtonContainerClassName = "",
|
||||
textBoxButtonClassName = "",
|
||||
textBoxButtonTextClassName = "",
|
||||
}: PricingCardTwoProps) => {
|
||||
const theme = useTheme();
|
||||
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
|
||||
|
||||
return (
|
||||
<CardStack
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
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}
|
||||
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}
|
||||
>
|
||||
{plans.map((plan, index) => (
|
||||
<PricingCardItem
|
||||
key={`${plan.id}-${index}`}
|
||||
plan={plan}
|
||||
shouldUseLightText={shouldUseLightText}
|
||||
cardClassName={cardClassName}
|
||||
badgeClassName={badgeClassName}
|
||||
priceClassName={priceClassName}
|
||||
subtitleClassName={subtitleClassName}
|
||||
planButtonContainerClassName={planButtonContainerClassName}
|
||||
planButtonClassName={planButtonClassName}
|
||||
featuresClassName={featuresClassName}
|
||||
featureItemClassName={featureItemClassName}
|
||||
/>
|
||||
))}
|
||||
</CardStack>
|
||||
);
|
||||
};
|
||||
|
||||
PricingCardTwo.displayName = "PricingCardTwo";
|
||||
|
||||
export default PricingCardTwo;
|
||||
214
src/components/sections/product/ProductCardFour.tsx
Normal file
214
src/components/sections/product/ProductCardFour.tsx
Normal file
@@ -0,0 +1,214 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
|
||||
|
||||
import CardStack from "@/components/cardStack/CardStack";
|
||||
import ProductImage from "@/components/shared/ProductImage";
|
||||
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 ProductCardFourGridVariant = Exclude<GridVariant, "timeline" | "items-top-row-full-width-bottom" | "full-width-top-items-bottom-row">;
|
||||
|
||||
type ProductCard = {
|
||||
id: string;
|
||||
name: string;
|
||||
price: string;
|
||||
variant: string;
|
||||
imageSrc: string;
|
||||
imageAlt?: string;
|
||||
onFavorite?: () => void;
|
||||
onProductClick?: () => void;
|
||||
isFavorited?: boolean;
|
||||
};
|
||||
|
||||
interface ProductCardFourProps {
|
||||
products: ProductCard[];
|
||||
carouselMode?: "auto" | "buttons";
|
||||
gridVariant: ProductCardFourGridVariant;
|
||||
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;
|
||||
imageClassName?: string;
|
||||
textBoxTitleClassName?: string;
|
||||
textBoxTitleImageWrapperClassName?: string;
|
||||
textBoxTitleImageClassName?: string;
|
||||
textBoxDescriptionClassName?: string;
|
||||
cardNameClassName?: string;
|
||||
cardPriceClassName?: string;
|
||||
cardVariantClassName?: string;
|
||||
actionButtonClassName?: string;
|
||||
gridClassName?: string;
|
||||
carouselClassName?: string;
|
||||
controlsClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
textBoxTagClassName?: string;
|
||||
textBoxButtonContainerClassName?: string;
|
||||
textBoxButtonClassName?: string;
|
||||
textBoxButtonTextClassName?: string;
|
||||
}
|
||||
|
||||
interface ProductCardItemProps {
|
||||
product: ProductCard;
|
||||
shouldUseLightText: boolean;
|
||||
cardClassName?: string;
|
||||
imageClassName?: string;
|
||||
cardNameClassName?: string;
|
||||
cardPriceClassName?: string;
|
||||
cardVariantClassName?: string;
|
||||
actionButtonClassName?: string;
|
||||
}
|
||||
|
||||
const ProductCardItem = memo(({
|
||||
product,
|
||||
shouldUseLightText,
|
||||
cardClassName = "",
|
||||
imageClassName = "",
|
||||
cardNameClassName = "",
|
||||
cardPriceClassName = "",
|
||||
cardVariantClassName = "",
|
||||
actionButtonClassName = "",
|
||||
}: ProductCardItemProps) => {
|
||||
return (
|
||||
<article
|
||||
className={cls("card group relative h-full flex flex-col gap-4 cursor-pointer p-4 rounded-theme-capped", cardClassName)}
|
||||
onClick={product.onProductClick}
|
||||
role="article"
|
||||
aria-label={`${product.name} - ${product.price}`}
|
||||
>
|
||||
<ProductImage
|
||||
imageSrc={product.imageSrc}
|
||||
imageAlt={product.imageAlt || product.name}
|
||||
isFavorited={product.isFavorited}
|
||||
onFavoriteToggle={product.onFavorite}
|
||||
showActionButton={true}
|
||||
actionButtonAriaLabel={`View ${product.name} details`}
|
||||
imageClassName={imageClassName}
|
||||
actionButtonClassName={actionButtonClassName}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex flex-col gap-0 flex-1 min-w-0">
|
||||
<h3 className={cls("text-base font-medium leading-[1.3]", shouldUseLightText ? "text-background" : "text-foreground", cardNameClassName)}>
|
||||
{product.name}
|
||||
</h3>
|
||||
<p className={cls("text-sm leading-[1.3]", shouldUseLightText ? "text-background/60" : "text-foreground/60", cardVariantClassName)}>
|
||||
{product.variant}
|
||||
</p>
|
||||
</div>
|
||||
<p className={cls("text-base font-medium leading-[1.3] flex-shrink-0", shouldUseLightText ? "text-background" : "text-foreground", cardPriceClassName)}>
|
||||
{product.price}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
});
|
||||
|
||||
ProductCardItem.displayName = "ProductCardItem";
|
||||
|
||||
const ProductCardFour = ({
|
||||
products,
|
||||
carouselMode = "buttons",
|
||||
gridVariant,
|
||||
uniformGridCustomHeightClasses = "min-h-95 2xl:min-h-105",
|
||||
animationType,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
textboxLayout,
|
||||
useInvertedBackground,
|
||||
ariaLabel = "Product section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
cardClassName = "",
|
||||
imageClassName = "",
|
||||
textBoxTitleClassName = "",
|
||||
textBoxTitleImageWrapperClassName = "",
|
||||
textBoxTitleImageClassName = "",
|
||||
textBoxDescriptionClassName = "",
|
||||
cardNameClassName = "",
|
||||
cardPriceClassName = "",
|
||||
cardVariantClassName = "",
|
||||
actionButtonClassName = "",
|
||||
gridClassName = "",
|
||||
carouselClassName = "",
|
||||
controlsClassName = "",
|
||||
textBoxClassName = "",
|
||||
textBoxTagClassName = "",
|
||||
textBoxButtonContainerClassName = "",
|
||||
textBoxButtonClassName = "",
|
||||
textBoxButtonTextClassName = "",
|
||||
}: ProductCardFourProps) => {
|
||||
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}
|
||||
>
|
||||
{products.map((product, index) => (
|
||||
<ProductCardItem
|
||||
key={`${product.id}-${index}`}
|
||||
product={product}
|
||||
shouldUseLightText={shouldUseLightText}
|
||||
cardClassName={cardClassName}
|
||||
imageClassName={imageClassName}
|
||||
cardNameClassName={cardNameClassName}
|
||||
cardPriceClassName={cardPriceClassName}
|
||||
cardVariantClassName={cardVariantClassName}
|
||||
actionButtonClassName={actionButtonClassName}
|
||||
/>
|
||||
))}
|
||||
</CardStack>
|
||||
);
|
||||
};
|
||||
|
||||
ProductCardFour.displayName = "ProductCardFour";
|
||||
|
||||
export default ProductCardFour;
|
||||
202
src/components/sections/product/ProductCardOne.tsx
Normal file
202
src/components/sections/product/ProductCardOne.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { ArrowUpRight } from "lucide-react";
|
||||
import CardStack from "@/components/cardStack/CardStack";
|
||||
import ProductImage from "@/components/shared/ProductImage";
|
||||
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 ProductCardOneGridVariant = Exclude<GridVariant, "timeline">;
|
||||
|
||||
type ProductCard = {
|
||||
id: string;
|
||||
name: string;
|
||||
price: string;
|
||||
imageSrc: string;
|
||||
imageAlt?: string;
|
||||
onFavorite?: () => void;
|
||||
onProductClick?: () => void;
|
||||
isFavorited?: boolean;
|
||||
};
|
||||
|
||||
interface ProductCardOneProps {
|
||||
products: ProductCard[];
|
||||
carouselMode?: "auto" | "buttons";
|
||||
gridVariant: ProductCardOneGridVariant;
|
||||
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;
|
||||
imageClassName?: string;
|
||||
textBoxTitleClassName?: string;
|
||||
textBoxTitleImageWrapperClassName?: string;
|
||||
textBoxTitleImageClassName?: string;
|
||||
textBoxDescriptionClassName?: string;
|
||||
cardNameClassName?: string;
|
||||
cardPriceClassName?: string;
|
||||
gridClassName?: string;
|
||||
carouselClassName?: string;
|
||||
controlsClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
textBoxTagClassName?: string;
|
||||
textBoxButtonContainerClassName?: string;
|
||||
textBoxButtonClassName?: string;
|
||||
textBoxButtonTextClassName?: string;
|
||||
}
|
||||
|
||||
interface ProductCardItemProps {
|
||||
product: ProductCard;
|
||||
shouldUseLightText: boolean;
|
||||
cardClassName?: string;
|
||||
imageClassName?: string;
|
||||
cardNameClassName?: string;
|
||||
cardPriceClassName?: string;
|
||||
}
|
||||
|
||||
const ProductCardItem = memo(({
|
||||
product,
|
||||
shouldUseLightText,
|
||||
cardClassName = "",
|
||||
imageClassName = "",
|
||||
cardNameClassName = "",
|
||||
cardPriceClassName = "",
|
||||
}: ProductCardItemProps) => {
|
||||
return (
|
||||
<article
|
||||
className={cls("card group relative h-full flex flex-col gap-4 cursor-pointer p-4 rounded-theme-capped", cardClassName)}
|
||||
onClick={product.onProductClick}
|
||||
role="article"
|
||||
aria-label={`${product.name} - ${product.price}`}
|
||||
>
|
||||
<ProductImage
|
||||
imageSrc={product.imageSrc}
|
||||
imageAlt={product.imageAlt || product.name}
|
||||
isFavorited={product.isFavorited}
|
||||
onFavoriteToggle={product.onFavorite}
|
||||
imageClassName={imageClassName}
|
||||
/>
|
||||
|
||||
<div className="relative z-1 flex items-center justify-between gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className={cls("text-base font-medium truncate leading-[1.3]", shouldUseLightText ? "text-background" : "text-foreground", cardNameClassName)}>
|
||||
{product.name}
|
||||
</h3>
|
||||
<p className={cls("text-2xl font-medium leading-[1.3]", shouldUseLightText ? "text-background" : "text-foreground", cardPriceClassName)}>
|
||||
{product.price}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="relative cursor-pointer primary-button h-10 w-auto aspect-square rounded-theme flex items-center justify-center flex-shrink-0"
|
||||
aria-label={`View ${product.name} details`}
|
||||
type="button"
|
||||
>
|
||||
<ArrowUpRight className="h-4/10 text-background transition-transform duration-300 group-hover:rotate-45" strokeWidth={1.5} />
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
});
|
||||
|
||||
ProductCardItem.displayName = "ProductCardItem";
|
||||
|
||||
const ProductCardOne = ({
|
||||
products,
|
||||
carouselMode = "buttons",
|
||||
gridVariant,
|
||||
uniformGridCustomHeightClasses = "min-h-95 2xl:min-h-105",
|
||||
animationType,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
textboxLayout,
|
||||
useInvertedBackground,
|
||||
ariaLabel = "Product section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
cardClassName = "",
|
||||
imageClassName = "",
|
||||
textBoxTitleClassName = "",
|
||||
textBoxTitleImageWrapperClassName = "",
|
||||
textBoxTitleImageClassName = "",
|
||||
textBoxDescriptionClassName = "",
|
||||
cardNameClassName = "",
|
||||
cardPriceClassName = "",
|
||||
gridClassName = "",
|
||||
carouselClassName = "",
|
||||
controlsClassName = "",
|
||||
textBoxClassName = "",
|
||||
textBoxTagClassName = "",
|
||||
textBoxButtonContainerClassName = "",
|
||||
textBoxButtonClassName = "",
|
||||
textBoxButtonTextClassName = "",
|
||||
}: ProductCardOneProps) => {
|
||||
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}
|
||||
>
|
||||
{products.map((product, index) => (
|
||||
<ProductCardItem
|
||||
key={`${product.id}-${index}`}
|
||||
product={product}
|
||||
shouldUseLightText={shouldUseLightText}
|
||||
cardClassName={cardClassName}
|
||||
imageClassName={imageClassName}
|
||||
cardNameClassName={cardNameClassName}
|
||||
cardPriceClassName={cardPriceClassName}
|
||||
/>
|
||||
))}
|
||||
</CardStack>
|
||||
);
|
||||
};
|
||||
|
||||
ProductCardOne.displayName = "ProductCardOne";
|
||||
|
||||
export default ProductCardOne;
|
||||
245
src/components/sections/product/ProductCardThree.tsx
Normal file
245
src/components/sections/product/ProductCardThree.tsx
Normal file
@@ -0,0 +1,245 @@
|
||||
"use client";
|
||||
|
||||
import { memo, useState, useCallback } from "react";
|
||||
import { Plus, Minus } from "lucide-react";
|
||||
import CardStack from "@/components/cardStack/CardStack";
|
||||
import ProductImage from "@/components/shared/ProductImage";
|
||||
import QuantityButton from "@/components/shared/QuantityButton";
|
||||
import Button from "@/components/button/Button";
|
||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||
import { getButtonProps } from "@/lib/buttonUtils";
|
||||
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig, GridVariant, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
|
||||
import type { CTAButtonVariant, ButtonPropsForVariant } from "@/components/button/types";
|
||||
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||
|
||||
type ProductCardThreeGridVariant = Exclude<GridVariant, "timeline" | "items-top-row-full-width-bottom" | "full-width-top-items-bottom-row">;
|
||||
|
||||
type ProductCard = {
|
||||
id: string;
|
||||
name: string;
|
||||
price: string;
|
||||
imageSrc: string;
|
||||
imageAlt?: string;
|
||||
onFavorite?: () => void;
|
||||
onProductClick?: () => void;
|
||||
onQuantityChange?: (quantity: number) => void;
|
||||
isFavorited?: boolean;
|
||||
initialQuantity?: number;
|
||||
priceButtonProps?: Partial<ButtonPropsForVariant<CTAButtonVariant>>;
|
||||
};
|
||||
|
||||
interface ProductCardThreeProps {
|
||||
products: ProductCard[];
|
||||
carouselMode?: "auto" | "buttons";
|
||||
gridVariant: ProductCardThreeGridVariant;
|
||||
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;
|
||||
imageClassName?: string;
|
||||
textBoxTitleClassName?: string;
|
||||
textBoxTitleImageWrapperClassName?: string;
|
||||
textBoxTitleImageClassName?: string;
|
||||
textBoxDescriptionClassName?: string;
|
||||
cardNameClassName?: string;
|
||||
quantityControlsClassName?: string;
|
||||
gridClassName?: string;
|
||||
carouselClassName?: string;
|
||||
controlsClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
textBoxTagClassName?: string;
|
||||
textBoxButtonContainerClassName?: string;
|
||||
textBoxButtonClassName?: string;
|
||||
textBoxButtonTextClassName?: string;
|
||||
}
|
||||
|
||||
|
||||
interface ProductCardItemProps {
|
||||
product: ProductCard;
|
||||
shouldUseLightText: boolean;
|
||||
cardClassName?: string;
|
||||
imageClassName?: string;
|
||||
cardNameClassName?: string;
|
||||
quantityControlsClassName?: string;
|
||||
}
|
||||
|
||||
const ProductCardItem = memo(({
|
||||
product,
|
||||
shouldUseLightText,
|
||||
cardClassName = "",
|
||||
imageClassName = "",
|
||||
cardNameClassName = "",
|
||||
quantityControlsClassName = "",
|
||||
}: ProductCardItemProps) => {
|
||||
const theme = useTheme();
|
||||
const [quantity, setQuantity] = useState(product.initialQuantity || 1);
|
||||
|
||||
const handleIncrement = useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
const newQuantity = quantity + 1;
|
||||
setQuantity(newQuantity);
|
||||
product.onQuantityChange?.(newQuantity);
|
||||
}, [quantity, product]);
|
||||
|
||||
const handleDecrement = useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (quantity > 1) {
|
||||
const newQuantity = quantity - 1;
|
||||
setQuantity(newQuantity);
|
||||
product.onQuantityChange?.(newQuantity);
|
||||
}
|
||||
}, [quantity, product]);
|
||||
|
||||
return (
|
||||
<article
|
||||
className={cls("card group relative h-full flex flex-col gap-4 cursor-pointer p-4 rounded-theme-capped", cardClassName)}
|
||||
onClick={product.onProductClick}
|
||||
role="article"
|
||||
aria-label={`${product.name} - ${product.price}`}
|
||||
>
|
||||
<ProductImage
|
||||
imageSrc={product.imageSrc}
|
||||
imageAlt={product.imageAlt || product.name}
|
||||
isFavorited={product.isFavorited}
|
||||
onFavoriteToggle={product.onFavorite}
|
||||
imageClassName={imageClassName}
|
||||
/>
|
||||
|
||||
<div className="relative z-1 flex flex-col gap-3">
|
||||
<h3 className={cls("text-xl font-medium leading-[1.15] truncate", shouldUseLightText ? "text-background" : "text-foreground", cardNameClassName)}>
|
||||
{product.name}
|
||||
</h3>
|
||||
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className={cls("flex items-center gap-2", quantityControlsClassName)}>
|
||||
<QuantityButton
|
||||
onClick={handleDecrement}
|
||||
ariaLabel="Decrease quantity"
|
||||
Icon={Minus}
|
||||
/>
|
||||
<span className={cls("text-base font-medium min-w-[2ch] text-center leading-[1]", shouldUseLightText ? "text-background" : "text-foreground")}>
|
||||
{quantity}
|
||||
</span>
|
||||
<QuantityButton
|
||||
onClick={handleIncrement}
|
||||
ariaLabel="Increase quantity"
|
||||
Icon={Plus}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
{...getButtonProps(
|
||||
{
|
||||
text: product.price,
|
||||
props: product.priceButtonProps,
|
||||
},
|
||||
0,
|
||||
theme.defaultButtonVariant
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
});
|
||||
|
||||
ProductCardItem.displayName = "ProductCardItem";
|
||||
|
||||
const ProductCardThree = ({
|
||||
products,
|
||||
carouselMode = "buttons",
|
||||
gridVariant,
|
||||
uniformGridCustomHeightClasses = "min-h-95 2xl:min-h-105",
|
||||
animationType,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
textboxLayout,
|
||||
useInvertedBackground,
|
||||
ariaLabel = "Product section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
cardClassName = "",
|
||||
imageClassName = "",
|
||||
textBoxTitleClassName = "",
|
||||
textBoxTitleImageWrapperClassName = "",
|
||||
textBoxTitleImageClassName = "",
|
||||
textBoxDescriptionClassName = "",
|
||||
cardNameClassName = "",
|
||||
quantityControlsClassName = "",
|
||||
gridClassName = "",
|
||||
carouselClassName = "",
|
||||
controlsClassName = "",
|
||||
textBoxClassName = "",
|
||||
textBoxTagClassName = "",
|
||||
textBoxButtonContainerClassName = "",
|
||||
textBoxButtonClassName = "",
|
||||
textBoxButtonTextClassName = "",
|
||||
}: ProductCardThreeProps) => {
|
||||
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}
|
||||
>
|
||||
{products.map((product, index) => (
|
||||
<ProductCardItem
|
||||
key={`${product.id}-${index}`}
|
||||
product={product}
|
||||
shouldUseLightText={shouldUseLightText}
|
||||
cardClassName={cardClassName}
|
||||
imageClassName={imageClassName}
|
||||
cardNameClassName={cardNameClassName}
|
||||
quantityControlsClassName={quantityControlsClassName}
|
||||
/>
|
||||
))}
|
||||
</CardStack>
|
||||
);
|
||||
};
|
||||
|
||||
ProductCardThree.displayName = "ProductCardThree";
|
||||
|
||||
export default ProductCardThree;
|
||||
242
src/components/sections/product/ProductCardTwo.tsx
Normal file
242
src/components/sections/product/ProductCardTwo.tsx
Normal file
@@ -0,0 +1,242 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { Star } from "lucide-react";
|
||||
import CardStack from "@/components/cardStack/CardStack";
|
||||
import ProductImage from "@/components/shared/ProductImage";
|
||||
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 ProductCardTwoGridVariant = Exclude<GridVariant, "timeline" | "one-large-right-three-stacked-left" | "items-top-row-full-width-bottom" | "full-width-top-items-bottom-row" | "one-large-left-three-stacked-right">;
|
||||
|
||||
type ProductCard = {
|
||||
id: string;
|
||||
brand: string;
|
||||
name: string;
|
||||
price: string;
|
||||
rating: number;
|
||||
reviewCount: string;
|
||||
imageSrc: string;
|
||||
imageAlt?: string;
|
||||
onFavorite?: () => void;
|
||||
onProductClick?: () => void;
|
||||
isFavorited?: boolean;
|
||||
};
|
||||
|
||||
interface ProductCardTwoProps {
|
||||
products: ProductCard[];
|
||||
carouselMode?: "auto" | "buttons";
|
||||
gridVariant: ProductCardTwoGridVariant;
|
||||
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;
|
||||
imageClassName?: string;
|
||||
textBoxTitleClassName?: string;
|
||||
textBoxTitleImageWrapperClassName?: string;
|
||||
textBoxTitleImageClassName?: string;
|
||||
textBoxDescriptionClassName?: string;
|
||||
cardBrandClassName?: string;
|
||||
cardNameClassName?: string;
|
||||
cardPriceClassName?: string;
|
||||
cardRatingClassName?: string;
|
||||
actionButtonClassName?: string;
|
||||
gridClassName?: string;
|
||||
carouselClassName?: string;
|
||||
controlsClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
textBoxTagClassName?: string;
|
||||
textBoxButtonContainerClassName?: string;
|
||||
textBoxButtonClassName?: string;
|
||||
textBoxButtonTextClassName?: string;
|
||||
}
|
||||
|
||||
interface ProductCardItemProps {
|
||||
product: ProductCard;
|
||||
shouldUseLightText: boolean;
|
||||
cardClassName?: string;
|
||||
imageClassName?: string;
|
||||
cardBrandClassName?: string;
|
||||
cardNameClassName?: string;
|
||||
cardPriceClassName?: string;
|
||||
cardRatingClassName?: string;
|
||||
actionButtonClassName?: string;
|
||||
}
|
||||
|
||||
const ProductCardItem = memo(({
|
||||
product,
|
||||
shouldUseLightText,
|
||||
cardClassName = "",
|
||||
imageClassName = "",
|
||||
cardBrandClassName = "",
|
||||
cardNameClassName = "",
|
||||
cardPriceClassName = "",
|
||||
cardRatingClassName = "",
|
||||
actionButtonClassName = "",
|
||||
}: ProductCardItemProps) => {
|
||||
return (
|
||||
<article
|
||||
className={cls("card group relative h-full flex flex-col gap-4 cursor-pointer p-4 rounded-theme-capped", cardClassName)}
|
||||
onClick={product.onProductClick}
|
||||
role="article"
|
||||
aria-label={`${product.brand} ${product.name} - ${product.price}`}
|
||||
>
|
||||
<ProductImage
|
||||
imageSrc={product.imageSrc}
|
||||
imageAlt={product.imageAlt || `${product.brand} ${product.name}`}
|
||||
isFavorited={product.isFavorited}
|
||||
onFavoriteToggle={product.onFavorite}
|
||||
showActionButton={true}
|
||||
actionButtonAriaLabel={`View ${product.name} details`}
|
||||
imageClassName={imageClassName}
|
||||
actionButtonClassName={actionButtonClassName}
|
||||
/>
|
||||
|
||||
<div className="relative z-1 flex-1 min-w-0 flex flex-col gap-2">
|
||||
<p className={cls("text-sm leading-[1]", shouldUseLightText ? "text-background" : "text-foreground", cardBrandClassName)}>
|
||||
{product.brand}
|
||||
</p>
|
||||
<div className="flex flex-col gap-1" >
|
||||
<h3 className={cls("text-xl font-medium truncate leading-[1.15]", shouldUseLightText ? "text-background" : "text-foreground", cardNameClassName)}>
|
||||
{product.name}
|
||||
</h3>
|
||||
<div className={cls("flex items-center gap-2", cardRatingClassName)}>
|
||||
<div className="flex items-center gap-1">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<Star
|
||||
key={i}
|
||||
className={cls(
|
||||
"h-4 w-auto",
|
||||
i < Math.floor(product.rating)
|
||||
? "text-accent fill-accent"
|
||||
: "text-accent opacity-20"
|
||||
)}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<span className={cls("text-sm leading-[1.3]", shouldUseLightText ? "text-background" : "text-foreground")}>
|
||||
({product.reviewCount})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className={cls("text-2xl font-medium leading-[1.3]", shouldUseLightText ? "text-background" : "text-foreground", cardPriceClassName)}>
|
||||
{product.price}
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
});
|
||||
|
||||
ProductCardItem.displayName = "ProductCardItem";
|
||||
|
||||
const ProductCardTwo = ({
|
||||
products,
|
||||
carouselMode = "buttons",
|
||||
gridVariant,
|
||||
uniformGridCustomHeightClasses = "min-h-95 2xl:min-h-105",
|
||||
animationType,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
textboxLayout,
|
||||
useInvertedBackground,
|
||||
ariaLabel = "Product section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
cardClassName = "",
|
||||
imageClassName = "",
|
||||
textBoxTitleClassName = "",
|
||||
textBoxTitleImageWrapperClassName = "",
|
||||
textBoxTitleImageClassName = "",
|
||||
textBoxDescriptionClassName = "",
|
||||
cardBrandClassName = "",
|
||||
cardNameClassName = "",
|
||||
cardPriceClassName = "",
|
||||
cardRatingClassName = "",
|
||||
actionButtonClassName = "",
|
||||
gridClassName = "",
|
||||
carouselClassName = "",
|
||||
controlsClassName = "",
|
||||
textBoxClassName = "",
|
||||
textBoxTagClassName = "",
|
||||
textBoxButtonContainerClassName = "",
|
||||
textBoxButtonClassName = "",
|
||||
textBoxButtonTextClassName = "",
|
||||
}: ProductCardTwoProps) => {
|
||||
const theme = useTheme();
|
||||
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
|
||||
|
||||
const customGridRows = (gridVariant === "bento-grid" || gridVariant === "bento-grid-inverted")
|
||||
? "md:grid-rows-[22rem_22rem] 2xl:grid-rows-[26rem_26rem]"
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<CardStack
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
mode={carouselMode}
|
||||
gridVariant={gridVariant}
|
||||
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
|
||||
gridRowsClassName={customGridRows}
|
||||
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}
|
||||
>
|
||||
{products.map((product, index) => (
|
||||
<ProductCardItem
|
||||
key={`${product.id}-${index}`}
|
||||
product={product}
|
||||
shouldUseLightText={shouldUseLightText}
|
||||
cardClassName={cardClassName}
|
||||
imageClassName={imageClassName}
|
||||
cardBrandClassName={cardBrandClassName}
|
||||
cardNameClassName={cardNameClassName}
|
||||
cardPriceClassName={cardPriceClassName}
|
||||
cardRatingClassName={cardRatingClassName}
|
||||
actionButtonClassName={actionButtonClassName}
|
||||
/>
|
||||
))}
|
||||
</CardStack>
|
||||
);
|
||||
};
|
||||
|
||||
ProductCardTwo.displayName = "ProductCardTwo";
|
||||
|
||||
export default ProductCardTwo;
|
||||
122
src/components/sections/socialProof/SocialProofOne.tsx
Normal file
122
src/components/sections/socialProof/SocialProofOne.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import Marquee from "react-fast-marquee";
|
||||
import CardStackTextBox from "@/components/cardStack/CardStackTextBox";
|
||||
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 SocialProofOneProps {
|
||||
logos: string[];
|
||||
title: string;
|
||||
titleSegments?: TitleSegment[];
|
||||
description: string;
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
buttons?: ButtonConfig[];
|
||||
textboxLayout: TextboxLayout;
|
||||
useInvertedBackground: InvertedBackground;
|
||||
speed?: number;
|
||||
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;
|
||||
logoItemClassName?: string;
|
||||
logoCardClassName?: string;
|
||||
logoImageClassName?: string;
|
||||
}
|
||||
|
||||
const SocialProofOne = ({
|
||||
logos,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
textboxLayout,
|
||||
useInvertedBackground,
|
||||
speed = 40,
|
||||
showCard = true,
|
||||
ariaLabel = "Social proof section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxTitleClassName = "",
|
||||
titleImageWrapperClassName = "",
|
||||
titleImageClassName = "",
|
||||
textBoxDescriptionClassName = "",
|
||||
textBoxClassName = "",
|
||||
textBoxTagClassName = "",
|
||||
textBoxButtonContainerClassName = "",
|
||||
textBoxButtonClassName = "",
|
||||
textBoxButtonTextClassName = "",
|
||||
contentClassName = "",
|
||||
logoItemClassName = "",
|
||||
logoCardClassName = "",
|
||||
logoImageClassName = "",
|
||||
}: SocialProofOneProps) => {
|
||||
const repeatedLogos = [...logos, ...logos, ...logos];
|
||||
|
||||
return (
|
||||
<section aria-label={ariaLabel} className={cls("relative py-20 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("mask-padding-x", contentClassName)}>
|
||||
<Marquee gradient={false} speed={speed}>
|
||||
{repeatedLogos.map((src, i) => (
|
||||
<div className={cls(showCard ? "mx-4" : "mx-8", logoItemClassName)} key={i}>
|
||||
<div className={cls(showCard ? "card px-4 py-3 mb-1 rounded-theme" : "", logoCardClassName)}>
|
||||
<Image
|
||||
width={500}
|
||||
height={500}
|
||||
src={src}
|
||||
alt={`Partner ${i + 1}`}
|
||||
className={cls("relative z-1", showCard ? "h-7 w-auto" : "h-8 w-auto", logoImageClassName)}
|
||||
unoptimized={src.startsWith('http') || src.startsWith('//')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Marquee>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
SocialProofOne.displayName = "SocialProofOne";
|
||||
|
||||
export default SocialProofOne;
|
||||
148
src/components/sections/socialProof/SocialProofThree.tsx
Normal file
148
src/components/sections/socialProof/SocialProofThree.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import Image from "next/image";
|
||||
import AutoCarousel from "@/components/cardStack/layouts/carousels/AutoCarousel";
|
||||
import { cls } from "@/lib/utils";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { CardAnimationType, ButtonConfig, TitleSegment } from "@/components/cardStack/types";
|
||||
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||
|
||||
interface SocialProofThreeProps {
|
||||
logos: string[];
|
||||
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;
|
||||
logoCardClassName?: string;
|
||||
logoImageClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
textBoxTitleClassName?: string;
|
||||
textBoxDescriptionClassName?: string;
|
||||
textBoxTagClassName?: string;
|
||||
textBoxButtonContainerClassName?: string;
|
||||
textBoxButtonClassName?: string;
|
||||
textBoxButtonTextClassName?: string;
|
||||
titleImageWrapperClassName?: string;
|
||||
titleImageClassName?: string;
|
||||
}
|
||||
|
||||
interface LogoCardProps {
|
||||
src: string;
|
||||
index: number;
|
||||
logoCardClassName?: string;
|
||||
logoImageClassName?: string;
|
||||
}
|
||||
|
||||
const LogoCard = memo(({
|
||||
src,
|
||||
index,
|
||||
logoCardClassName = "",
|
||||
logoImageClassName = "",
|
||||
}: LogoCardProps) => {
|
||||
return (
|
||||
<div className={cls("h-full card rounded-theme flex items-center justify-center", logoCardClassName)}>
|
||||
<Image
|
||||
width={500}
|
||||
height={500}
|
||||
src={src}
|
||||
alt={`Partner ${index + 1}`}
|
||||
className={cls("relative z-1 h-1/2 w-1/2", logoImageClassName)}
|
||||
unoptimized={src.startsWith('http') || src.startsWith('//')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
LogoCard.displayName = "LogoCard";
|
||||
|
||||
const SocialProofThree = ({
|
||||
logos,
|
||||
animationType,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
textboxLayout,
|
||||
useInvertedBackground,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
speed = 40,
|
||||
topMarqueeDirection = "left",
|
||||
ariaLabel = "Social proof section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
carouselClassName = "",
|
||||
bottomCarouselClassName = "",
|
||||
logoCardClassName = "",
|
||||
logoImageClassName = "",
|
||||
textBoxClassName = "",
|
||||
textBoxTitleClassName = "",
|
||||
textBoxDescriptionClassName = "",
|
||||
textBoxTagClassName = "",
|
||||
textBoxButtonContainerClassName = "",
|
||||
textBoxButtonClassName = "",
|
||||
textBoxButtonTextClassName = "",
|
||||
titleImageWrapperClassName = "",
|
||||
titleImageClassName = "",
|
||||
}: SocialProofThreeProps) => {
|
||||
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}
|
||||
containerClassName={containerClassName}
|
||||
className={className}
|
||||
textBoxClassName={textBoxClassName}
|
||||
titleClassName={textBoxTitleClassName}
|
||||
descriptionClassName={textBoxDescriptionClassName}
|
||||
tagClassName={textBoxTagClassName}
|
||||
buttonContainerClassName={textBoxButtonContainerClassName}
|
||||
buttonClassName={textBoxButtonClassName}
|
||||
buttonTextClassName={textBoxButtonTextClassName}
|
||||
titleImageWrapperClassName={titleImageWrapperClassName}
|
||||
titleImageClassName={titleImageClassName}
|
||||
ariaLabel={ariaLabel}
|
||||
itemClassName="w-30! md:w-carousel-item-3! xl:w-carousel-item-4! aspect-square"
|
||||
>
|
||||
{logos.map((src, index) => (
|
||||
<LogoCard
|
||||
key={`${src}-${index}`}
|
||||
src={src}
|
||||
index={index}
|
||||
logoCardClassName={logoCardClassName}
|
||||
logoImageClassName={logoImageClassName}
|
||||
/>
|
||||
))}
|
||||
</AutoCarousel>
|
||||
);
|
||||
};
|
||||
|
||||
SocialProofThree.displayName = "SocialProofThree";
|
||||
|
||||
export default SocialProofThree;
|
||||
134
src/components/sections/socialProof/SocialProofTwo.tsx
Normal file
134
src/components/sections/socialProof/SocialProofTwo.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
"use client";
|
||||
|
||||
|
||||
import Image from "next/image";
|
||||
import CardStackTextBox from "@/components/cardStack/CardStackTextBox";
|
||||
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 SocialProofTwoProps {
|
||||
logos: string[];
|
||||
title: string;
|
||||
titleSegments?: TitleSegment[];
|
||||
description: string;
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
buttons?: ButtonConfig[];
|
||||
textboxLayout: TextboxLayout;
|
||||
useInvertedBackground: InvertedBackground;
|
||||
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;
|
||||
logoRowClassName?: string;
|
||||
logoItemClassName?: string;
|
||||
logoCardClassName?: string;
|
||||
logoImageClassName?: string;
|
||||
}
|
||||
|
||||
const SocialProofTwo = ({
|
||||
logos,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
textboxLayout,
|
||||
useInvertedBackground,
|
||||
ariaLabel = "Social proof section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxTitleClassName = "",
|
||||
titleImageWrapperClassName = "",
|
||||
titleImageClassName = "",
|
||||
textBoxDescriptionClassName = "",
|
||||
textBoxClassName = "",
|
||||
textBoxTagClassName = "",
|
||||
textBoxButtonContainerClassName = "",
|
||||
textBoxButtonClassName = "",
|
||||
textBoxButtonTextClassName = "",
|
||||
contentClassName = "",
|
||||
logoRowClassName = "",
|
||||
logoItemClassName = "",
|
||||
logoCardClassName = "",
|
||||
logoImageClassName = "",
|
||||
}: SocialProofTwoProps) => {
|
||||
// Calculate flex basis based on number of logos
|
||||
const getFlexBasis = () => {
|
||||
const count = logos.length;
|
||||
if (count === 2) return "md:basis-1/2";
|
||||
if (count === 3) return "md:basis-1/3";
|
||||
if (count === 4) return "md:basis-1/4";
|
||||
if (count === 5) return "md:basis-1/5";
|
||||
return "md:flex-1";
|
||||
};
|
||||
return (
|
||||
<section aria-label={ariaLabel} className={cls("relative py-20 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("", contentClassName)}>
|
||||
<div className={cls("flex flex-col md:flex-row items-stretch gap-4", logoRowClassName)}>
|
||||
{logos.map((src, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={cls(
|
||||
"flex items-center justify-center w-full",
|
||||
getFlexBasis(),
|
||||
logoItemClassName
|
||||
)}
|
||||
>
|
||||
<div className={cls("card p-6 rounded-theme w-full h-full flex items-center justify-center", logoCardClassName)}>
|
||||
<Image
|
||||
width={500}
|
||||
height={500}
|
||||
src={src}
|
||||
alt={`Partner ${i + 1}`}
|
||||
className={cls("relative z-1 w-1/2 md:w-auto md:h-8 2xl:h-10 h-auto object-contain", logoImageClassName)}
|
||||
unoptimized={src.startsWith('http') || src.startsWith('//')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
SocialProofTwo.displayName = "SocialProofTwo";
|
||||
|
||||
export default SocialProofTwo;
|
||||
190
src/components/sections/team/TeamCardEleven.tsx
Normal file
190
src/components/sections/team/TeamCardEleven.tsx
Normal file
@@ -0,0 +1,190 @@
|
||||
"use client";
|
||||
|
||||
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 TeamMember = {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
detail: string;
|
||||
imageSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoSrc?: string;
|
||||
videoAriaLabel?: string;
|
||||
};
|
||||
|
||||
type TeamGroup = {
|
||||
id: string;
|
||||
groupTitle: string;
|
||||
members: TeamMember[];
|
||||
};
|
||||
|
||||
interface TeamCardElevenProps {
|
||||
groups: TeamGroup[];
|
||||
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;
|
||||
textBoxClassName?: string;
|
||||
textBoxTitleClassName?: string;
|
||||
textBoxDescriptionClassName?: string;
|
||||
textBoxTagClassName?: string;
|
||||
textBoxButtonContainerClassName?: string;
|
||||
textBoxButtonClassName?: string;
|
||||
textBoxButtonTextClassName?: string;
|
||||
titleImageWrapperClassName?: string;
|
||||
titleImageClassName?: string;
|
||||
groupTitleClassName?: string;
|
||||
memberClassName?: string;
|
||||
memberImageClassName?: string;
|
||||
memberTitleClassName?: string;
|
||||
memberSubtitleClassName?: string;
|
||||
memberDetailClassName?: string;
|
||||
}
|
||||
|
||||
const TeamCardEleven = ({
|
||||
groups,
|
||||
animationType,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
textboxLayout,
|
||||
useInvertedBackground,
|
||||
ariaLabel = "Team section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
cardClassName = "",
|
||||
textBoxClassName = "",
|
||||
textBoxTitleClassName = "",
|
||||
textBoxDescriptionClassName = "",
|
||||
textBoxTagClassName = "",
|
||||
textBoxButtonContainerClassName = "",
|
||||
textBoxButtonClassName = "",
|
||||
textBoxButtonTextClassName = "",
|
||||
titleImageWrapperClassName = "",
|
||||
titleImageClassName = "",
|
||||
groupTitleClassName = "",
|
||||
memberClassName = "",
|
||||
memberImageClassName = "",
|
||||
memberTitleClassName = "",
|
||||
memberSubtitleClassName = "",
|
||||
memberDetailClassName = "",
|
||||
}: TeamCardElevenProps) => {
|
||||
const theme = useTheme();
|
||||
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
|
||||
|
||||
const renderMemberRow = (member: TeamMember) => (
|
||||
<div
|
||||
key={member.id}
|
||||
className={cls(
|
||||
"flex flex-col md:flex-row md:items-center gap-4 py-6",
|
||||
memberClassName
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-4 flex-1">
|
||||
<div className={cls(
|
||||
"relative h-14 w-auto md:h-16 aspect-square rounded-theme overflow-hidden shrink-0",
|
||||
memberImageClassName
|
||||
)}>
|
||||
<MediaContent
|
||||
imageSrc={member.imageSrc}
|
||||
imageAlt={member.imageAlt || member.title}
|
||||
videoSrc={member.videoSrc}
|
||||
videoAriaLabel={member.videoAriaLabel}
|
||||
imageClassName="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<p className={cls(
|
||||
"text-lg md:text-xl font-medium",
|
||||
shouldUseLightText ? "text-background" : "text-foreground",
|
||||
memberTitleClassName
|
||||
)}>
|
||||
{member.title}
|
||||
</p>
|
||||
<p className={cls(
|
||||
"text-base",
|
||||
shouldUseLightText ? "text-background/60" : "text-foreground/60",
|
||||
memberSubtitleClassName
|
||||
)}>
|
||||
{member.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className={cls(
|
||||
"text-base md:text-lg font-medium",
|
||||
shouldUseLightText ? "text-background" : "text-foreground",
|
||||
memberDetailClassName
|
||||
)}>
|
||||
{member.detail}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
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}
|
||||
>
|
||||
{groups.map((group) => (
|
||||
<div key={group.id} className="p-6 md:p-8">
|
||||
<h3 className={cls(
|
||||
"text-2xl md:text-3xl font-medium mb-2",
|
||||
shouldUseLightText ? "text-background" : "text-foreground",
|
||||
groupTitleClassName
|
||||
)}>
|
||||
{group.groupTitle}
|
||||
</h3>
|
||||
|
||||
<div className="flex flex-col divide-y divide-accent/40 border-y border-accent/40">
|
||||
{group.members.map(renderMemberRow)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardList>
|
||||
);
|
||||
};
|
||||
|
||||
TeamCardEleven.displayName = "TeamCardEleven";
|
||||
|
||||
export default TeamCardEleven;
|
||||
142
src/components/sections/team/TeamCardFive.tsx
Normal file
142
src/components/sections/team/TeamCardFive.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
"use client";
|
||||
|
||||
import CardStackTextBox from "@/components/cardStack/CardStackTextBox";
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
import { useCardAnimation } from "@/components/cardStack/hooks/useCardAnimation";
|
||||
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 TeamMember = {
|
||||
id: string;
|
||||
name: string;
|
||||
role: string;
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
};
|
||||
|
||||
interface TeamCardFiveProps {
|
||||
team: TeamMember[];
|
||||
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;
|
||||
textBoxTitleImageWrapperClassName?: string;
|
||||
textBoxTitleImageClassName?: string;
|
||||
textBoxDescriptionClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
textBoxTagClassName?: string;
|
||||
textBoxButtonContainerClassName?: string;
|
||||
textBoxButtonClassName?: string;
|
||||
textBoxButtonTextClassName?: string;
|
||||
gridClassName?: string;
|
||||
cardClassName?: string;
|
||||
mediaWrapperClassName?: string;
|
||||
mediaClassName?: string;
|
||||
nameClassName?: string;
|
||||
roleClassName?: string;
|
||||
}
|
||||
|
||||
const TeamCardFive = ({
|
||||
team,
|
||||
animationType,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
textboxLayout,
|
||||
useInvertedBackground,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
ariaLabel = "Team section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxTitleClassName = "",
|
||||
textBoxTitleImageWrapperClassName = "",
|
||||
textBoxTitleImageClassName = "",
|
||||
textBoxDescriptionClassName = "",
|
||||
textBoxClassName = "",
|
||||
textBoxTagClassName = "",
|
||||
textBoxButtonContainerClassName = "",
|
||||
textBoxButtonClassName = "",
|
||||
textBoxButtonTextClassName = "",
|
||||
gridClassName = "",
|
||||
cardClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
mediaClassName = "",
|
||||
nameClassName = "",
|
||||
roleClassName = "",
|
||||
}: TeamCardFiveProps) => {
|
||||
const { itemRefs } = useCardAnimation({ animationType, itemCount: team.length });
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("relative py-20 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={textBoxTitleImageWrapperClassName}
|
||||
titleImageClassName={textBoxTitleImageClassName}
|
||||
descriptionClassName={textBoxDescriptionClassName}
|
||||
tagClassName={textBoxTagClassName}
|
||||
buttonContainerClassName={textBoxButtonContainerClassName}
|
||||
buttonClassName={textBoxButtonClassName}
|
||||
buttonTextClassName={textBoxButtonTextClassName}
|
||||
/>
|
||||
|
||||
<div className={cls("flex flex-row flex-wrap gap-y-6 md:gap-x-0 justify-center", gridClassName)}>
|
||||
{team.map((member, index) => (
|
||||
<div
|
||||
key={member.id}
|
||||
ref={(el) => { itemRefs.current[index] = el; }}
|
||||
className={cls("relative flex flex-col items-center text-center w-[55%] md:w-[28%] -mx-[4%] md:-mx-[2%]", cardClassName)}
|
||||
>
|
||||
<div className={cls("relative card w-full aspect-square rounded-theme overflow-hidden p-2 mb-4", mediaWrapperClassName)}>
|
||||
<MediaContent
|
||||
imageSrc={member.imageSrc}
|
||||
videoSrc={member.videoSrc}
|
||||
imageAlt={member.imageAlt || member.name}
|
||||
videoAriaLabel={member.videoAriaLabel || member.name}
|
||||
imageClassName={cls("relative z-1 w-full h-full object-cover rounded-theme!", mediaClassName)}
|
||||
/>
|
||||
</div>
|
||||
<h3 className={cls("relative z-1 w-8/10 text-2xl font-medium leading-tight truncate", useInvertedBackground === "invertDefault" ? "text-background" : "text-foreground", nameClassName)}>
|
||||
{member.name}
|
||||
</h3>
|
||||
<p className={cls("relative z-1 w-8/10 text-base leading-tight mt-1 truncate", useInvertedBackground === "invertDefault" ? "text-background/75" : "text-foreground/75", roleClassName)}>
|
||||
{member.role}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
TeamCardFive.displayName = "TeamCardFive";
|
||||
|
||||
export default TeamCardFive;
|
||||
188
src/components/sections/team/TeamCardOne.tsx
Normal file
188
src/components/sections/team/TeamCardOne.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import Image from "next/image";
|
||||
import CardStack from "@/components/cardStack/CardStack";
|
||||
import { cls } from "@/lib/utils";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig, GridVariant, CardAnimationTypeWith3D, TitleSegment } from "@/components/cardStack/types";
|
||||
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||
|
||||
type TeamCardOneGridVariant = Exclude<GridVariant, "timeline">;
|
||||
|
||||
type TeamMember = {
|
||||
id: string;
|
||||
name: string;
|
||||
role: string;
|
||||
imageSrc: string;
|
||||
imageAlt?: string;
|
||||
};
|
||||
|
||||
interface TeamCardOneProps {
|
||||
members: TeamMember[];
|
||||
carouselMode?: "auto" | "buttons";
|
||||
gridVariant: TeamCardOneGridVariant;
|
||||
uniformGridCustomHeightClasses?: string;
|
||||
animationType: CardAnimationTypeWith3D;
|
||||
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;
|
||||
imageClassName?: string;
|
||||
overlayClassName?: string;
|
||||
nameClassName?: string;
|
||||
roleClassName?: string;
|
||||
gridClassName?: string;
|
||||
carouselClassName?: string;
|
||||
controlsClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
textBoxTagClassName?: string;
|
||||
textBoxButtonContainerClassName?: string;
|
||||
textBoxButtonClassName?: string;
|
||||
textBoxButtonTextClassName?: string;
|
||||
}
|
||||
|
||||
interface TeamMemberCardProps {
|
||||
member: TeamMember;
|
||||
cardClassName?: string;
|
||||
imageClassName?: string;
|
||||
overlayClassName?: string;
|
||||
nameClassName?: string;
|
||||
roleClassName?: string;
|
||||
}
|
||||
|
||||
const TeamMemberCard = memo(({
|
||||
member,
|
||||
cardClassName = "",
|
||||
imageClassName = "",
|
||||
overlayClassName = "",
|
||||
nameClassName = "",
|
||||
roleClassName = "",
|
||||
}: TeamMemberCardProps) => {
|
||||
return (
|
||||
<div className={cls("relative h-full card rounded-theme-capped p-4", cardClassName)}>
|
||||
<div className="relative z-1 w-full h-full rounded-theme-capped overflow-hidden">
|
||||
<Image
|
||||
src={member.imageSrc}
|
||||
alt={member.imageAlt || member.name}
|
||||
width={800}
|
||||
height={800}
|
||||
className={cls("w-full h-full object-cover ", imageClassName)}
|
||||
unoptimized={member.imageSrc.startsWith('http') || member.imageSrc.startsWith('//')}
|
||||
aria-hidden={member.imageAlt === ""}
|
||||
/>
|
||||
|
||||
<div className={cls("!absolute z-1 bottom-4 left-4 right-4 card backdrop-blur-xs p-4 rounded-theme-capped flex items-center justify-between gap-3", overlayClassName)}>
|
||||
<h3 className={cls("relative z-1 text-xl font-medium text-foreground leading-[1.1] truncate", nameClassName)}>
|
||||
{member.name}
|
||||
</h3>
|
||||
<div className="min-w-0 max-w-full w-fit primary-button px-3 py-2 rounded-theme">
|
||||
<p className={cls("text-sm text-background leading-[1.1] truncate", roleClassName)}>
|
||||
{member.role}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
TeamMemberCard.displayName = "TeamMemberCard";
|
||||
|
||||
const TeamCardOne = ({
|
||||
members,
|
||||
carouselMode = "buttons",
|
||||
gridVariant,
|
||||
uniformGridCustomHeightClasses = "min-h-95 2xl:min-h-105",
|
||||
animationType,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
textboxLayout,
|
||||
useInvertedBackground,
|
||||
ariaLabel = "Team section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
cardClassName = "",
|
||||
textBoxTitleClassName = "",
|
||||
textBoxTitleImageWrapperClassName = "",
|
||||
textBoxTitleImageClassName = "",
|
||||
textBoxDescriptionClassName = "",
|
||||
imageClassName = "",
|
||||
overlayClassName = "",
|
||||
nameClassName = "",
|
||||
roleClassName = "",
|
||||
gridClassName = "",
|
||||
carouselClassName = "",
|
||||
controlsClassName = "",
|
||||
textBoxClassName = "",
|
||||
textBoxTagClassName = "",
|
||||
textBoxButtonContainerClassName = "",
|
||||
textBoxButtonClassName = "",
|
||||
textBoxButtonTextClassName = "",
|
||||
}: TeamCardOneProps) => {
|
||||
return (
|
||||
<CardStack
|
||||
mode={carouselMode}
|
||||
gridVariant={gridVariant}
|
||||
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
|
||||
animationType={animationType}
|
||||
supports3DAnimation={true}
|
||||
|
||||
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}
|
||||
>
|
||||
{members.map((member, index) => (
|
||||
<TeamMemberCard
|
||||
key={`${member.id}-${index}`}
|
||||
member={member}
|
||||
cardClassName={cardClassName}
|
||||
imageClassName={imageClassName}
|
||||
overlayClassName={overlayClassName}
|
||||
nameClassName={nameClassName}
|
||||
roleClassName={roleClassName}
|
||||
/>
|
||||
))}
|
||||
</CardStack>
|
||||
);
|
||||
};
|
||||
|
||||
TeamCardOne.displayName = "TeamCardOne";
|
||||
|
||||
export default TeamCardOne;
|
||||
194
src/components/sections/team/TeamCardSix.tsx
Normal file
194
src/components/sections/team/TeamCardSix.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import Image from "next/image";
|
||||
import CardStack from "@/components/cardStack/CardStack";
|
||||
import { cls } from "@/lib/utils";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig, GridVariant, CardAnimationTypeWith3D, TitleSegment } from "@/components/cardStack/types";
|
||||
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||
|
||||
type TeamCardSixGridVariant = Exclude<GridVariant, "timeline" | "two-columns-alternating-heights" | "four-items-2x2-equal-grid">;
|
||||
|
||||
const MASK_GRADIENT = "linear-gradient(to bottom, transparent, black 60%)";
|
||||
|
||||
type TeamMember = {
|
||||
id: string;
|
||||
name: string;
|
||||
role: string;
|
||||
imageSrc: string;
|
||||
imageAlt?: string;
|
||||
};
|
||||
|
||||
interface TeamCardSixProps {
|
||||
members: TeamMember[];
|
||||
carouselMode?: "auto" | "buttons";
|
||||
gridVariant: TeamCardSixGridVariant;
|
||||
uniformGridCustomHeightClasses?: string;
|
||||
animationType: CardAnimationTypeWith3D;
|
||||
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;
|
||||
imageClassName?: string;
|
||||
overlayClassName?: string;
|
||||
nameClassName?: string;
|
||||
roleClassName?: string;
|
||||
gridClassName?: string;
|
||||
carouselClassName?: string;
|
||||
controlsClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
textBoxTagClassName?: string;
|
||||
textBoxButtonContainerClassName?: string;
|
||||
textBoxButtonClassName?: string;
|
||||
textBoxButtonTextClassName?: string;
|
||||
}
|
||||
|
||||
interface TeamMemberCardProps {
|
||||
member: TeamMember;
|
||||
cardClassName?: string;
|
||||
imageClassName?: string;
|
||||
overlayClassName?: string;
|
||||
nameClassName?: string;
|
||||
roleClassName?: string;
|
||||
}
|
||||
|
||||
const TeamMemberCard = memo(({
|
||||
member,
|
||||
cardClassName = "",
|
||||
imageClassName = "",
|
||||
overlayClassName = "",
|
||||
nameClassName = "",
|
||||
roleClassName = "",
|
||||
}: TeamMemberCardProps) => {
|
||||
return (
|
||||
<div className={cls("relative h-full rounded-theme-capped", cardClassName)}>
|
||||
<div className="relative w-full h-full rounded-theme-capped overflow-hidden">
|
||||
<Image
|
||||
src={member.imageSrc}
|
||||
alt={member.imageAlt || member.name}
|
||||
width={800}
|
||||
height={800}
|
||||
className={cls("w-full h-full object-cover", imageClassName)}
|
||||
unoptimized={member.imageSrc.startsWith('http') || member.imageSrc.startsWith('//')}
|
||||
aria-hidden={member.imageAlt === ""}
|
||||
/>
|
||||
|
||||
<div className={cls("absolute z-10 bottom-4 left-4 right-4 p-4 flex flex-col gap-0 text-background", overlayClassName)}>
|
||||
<h3 className={cls("text-2xl font-medium leading-tight truncate", nameClassName)}>
|
||||
{member.name}
|
||||
</h3>
|
||||
<p className={cls("text-base leading-tight truncate", roleClassName)}>
|
||||
{member.role}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="absolute z-0 backdrop-blur-xl opacity-100 w-full h-1/3 left-0 bottom-0"
|
||||
style={{ maskImage: MASK_GRADIENT }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
TeamMemberCard.displayName = "TeamMemberCard";
|
||||
|
||||
const TeamCardSix = ({
|
||||
members,
|
||||
carouselMode = "buttons",
|
||||
gridVariant,
|
||||
uniformGridCustomHeightClasses = "min-h-95 2xl:min-h-105",
|
||||
animationType,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
textboxLayout,
|
||||
useInvertedBackground,
|
||||
ariaLabel = "Team section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
cardClassName = "",
|
||||
textBoxTitleClassName = "",
|
||||
textBoxTitleImageWrapperClassName = "",
|
||||
textBoxTitleImageClassName = "",
|
||||
textBoxDescriptionClassName = "",
|
||||
imageClassName = "",
|
||||
overlayClassName = "",
|
||||
nameClassName = "",
|
||||
roleClassName = "",
|
||||
gridClassName = "",
|
||||
carouselClassName = "",
|
||||
controlsClassName = "",
|
||||
textBoxClassName = "",
|
||||
textBoxTagClassName = "",
|
||||
textBoxButtonContainerClassName = "",
|
||||
textBoxButtonClassName = "",
|
||||
textBoxButtonTextClassName = "",
|
||||
}: TeamCardSixProps) => {
|
||||
return (
|
||||
<CardStack
|
||||
mode={carouselMode}
|
||||
gridVariant={gridVariant}
|
||||
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
|
||||
animationType={animationType}
|
||||
supports3DAnimation={true}
|
||||
|
||||
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}
|
||||
>
|
||||
{members.map((member, index) => (
|
||||
<TeamMemberCard
|
||||
key={`${member.id}-${index}`}
|
||||
member={member}
|
||||
cardClassName={cardClassName}
|
||||
imageClassName={imageClassName}
|
||||
overlayClassName={overlayClassName}
|
||||
nameClassName={nameClassName}
|
||||
roleClassName={roleClassName}
|
||||
/>
|
||||
))}
|
||||
</CardStack>
|
||||
);
|
||||
};
|
||||
|
||||
TeamCardSix.displayName = "TeamCardSix";
|
||||
|
||||
export default TeamCardSix;
|
||||
128
src/components/sections/team/TeamCardTen.tsx
Normal file
128
src/components/sections/team/TeamCardTen.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
"use client";
|
||||
|
||||
import TextAnimation from "@/components/text/TextAnimation";
|
||||
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 { InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||
|
||||
type TeamMember = {
|
||||
id: string;
|
||||
name: string;
|
||||
imageSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoSrc?: string;
|
||||
videoAriaLabel?: string;
|
||||
};
|
||||
|
||||
interface TeamCardTenProps {
|
||||
title: string;
|
||||
tag: string;
|
||||
members: TeamMember[];
|
||||
memberVariant: "default" | "card";
|
||||
useInvertedBackground: InvertedBackground;
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
titleClassName?: string;
|
||||
tagClassName?: string;
|
||||
membersContainerClassName?: string;
|
||||
memberClassName?: string;
|
||||
mediaWrapperClassName?: string;
|
||||
mediaClassName?: string;
|
||||
nameClassName?: string;
|
||||
}
|
||||
|
||||
const TeamCardTen = ({
|
||||
title,
|
||||
tag,
|
||||
members,
|
||||
memberVariant,
|
||||
useInvertedBackground,
|
||||
ariaLabel = "Team section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
titleClassName = "",
|
||||
tagClassName = "",
|
||||
membersContainerClassName = "",
|
||||
memberClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
mediaClassName = "",
|
||||
nameClassName = "",
|
||||
}: TeamCardTenProps) => {
|
||||
const theme = useTheme();
|
||||
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("relative py-20 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",
|
||||
useInvertedBackground === "invertDefault" && "text-background",
|
||||
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-8 flex-1",
|
||||
members.length === 1 && "grid-cols-1",
|
||||
members.length >= 2 && "grid-cols-2",
|
||||
membersContainerClassName
|
||||
)}>
|
||||
{members.map((member) => (
|
||||
<div
|
||||
key={member.id}
|
||||
className={cls(
|
||||
"flex flex-col gap-4",
|
||||
memberVariant === "card" && "card rounded-theme-capped p-4",
|
||||
memberClassName
|
||||
)}
|
||||
>
|
||||
<div className={cls("relative aspect-[3/3.5] rounded-theme-capped overflow-hidden", mediaWrapperClassName)}>
|
||||
<MediaContent
|
||||
imageSrc={member.imageSrc}
|
||||
imageAlt={member.imageAlt || member.name}
|
||||
videoSrc={member.videoSrc}
|
||||
videoAriaLabel={member.videoAriaLabel}
|
||||
imageClassName={cls("w-full h-full object-cover", mediaClassName)}
|
||||
/>
|
||||
</div>
|
||||
<p className={cls(
|
||||
"text-xl md:text-3xl font-medium truncate",
|
||||
shouldUseLightText ? "text-background" : "text-foreground",
|
||||
nameClassName
|
||||
)}>
|
||||
{member.name}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
TeamCardTen.displayName = "TeamCardTen";
|
||||
|
||||
export default TeamCardTen;
|
||||
234
src/components/sections/team/TeamCardTwo.tsx
Normal file
234
src/components/sections/team/TeamCardTwo.tsx
Normal file
@@ -0,0 +1,234 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import Image from "next/image";
|
||||
import CardStack from "@/components/cardStack/CardStack";
|
||||
import { cls } from "@/lib/utils";
|
||||
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 TeamCardTwoGridVariant = Exclude<GridVariant, "timeline">;
|
||||
|
||||
type SocialLink = {
|
||||
icon: LucideIcon;
|
||||
url: string;
|
||||
};
|
||||
|
||||
type TeamMember = {
|
||||
id: string;
|
||||
name: string;
|
||||
role: string;
|
||||
description: string;
|
||||
imageSrc: string;
|
||||
imageAlt?: string;
|
||||
socialLinks?: SocialLink[];
|
||||
};
|
||||
|
||||
interface TeamCardTwoProps {
|
||||
members: TeamMember[];
|
||||
carouselMode?: "auto" | "buttons";
|
||||
gridVariant: TeamCardTwoGridVariant;
|
||||
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;
|
||||
imageClassName?: string;
|
||||
overlayClassName?: string;
|
||||
nameClassName?: string;
|
||||
roleClassName?: string;
|
||||
memberDescriptionClassName?: string;
|
||||
socialLinksClassName?: string;
|
||||
socialIconClassName?: string;
|
||||
gridClassName?: string;
|
||||
carouselClassName?: string;
|
||||
controlsClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
textBoxTagClassName?: string;
|
||||
textBoxButtonContainerClassName?: string;
|
||||
textBoxButtonClassName?: string;
|
||||
textBoxButtonTextClassName?: string;
|
||||
}
|
||||
|
||||
interface TeamMemberCardProps {
|
||||
member: TeamMember;
|
||||
cardClassName?: string;
|
||||
imageClassName?: string;
|
||||
overlayClassName?: string;
|
||||
nameClassName?: string;
|
||||
roleClassName?: string;
|
||||
memberDescriptionClassName?: string;
|
||||
socialLinksClassName?: string;
|
||||
socialIconClassName?: string;
|
||||
}
|
||||
|
||||
const TeamMemberCard = memo(({
|
||||
member,
|
||||
cardClassName = "",
|
||||
imageClassName = "",
|
||||
overlayClassName = "",
|
||||
nameClassName = "",
|
||||
roleClassName = "",
|
||||
memberDescriptionClassName = "",
|
||||
socialLinksClassName = "",
|
||||
socialIconClassName = "",
|
||||
}: TeamMemberCardProps) => {
|
||||
return (
|
||||
<div className={cls("relative h-full rounded-theme-capped overflow-hidden group", cardClassName)}>
|
||||
<Image
|
||||
src={member.imageSrc}
|
||||
alt={member.imageAlt || member.name}
|
||||
width={800}
|
||||
height={800}
|
||||
className={cls("relative z-1 w-full h-full object-cover", imageClassName)}
|
||||
unoptimized={member.imageSrc.startsWith('http') || member.imageSrc.startsWith('//')}
|
||||
aria-hidden={member.imageAlt === ""}
|
||||
/>
|
||||
|
||||
<div className={cls("!absolute z-10 bottom-6 left-6 right-6 card backdrop-blur-xs p-6 flex flex-col gap-2 rounded-theme-capped", overlayClassName)}>
|
||||
<div className="relative z-1 flex items-start justify-between">
|
||||
<h3 className={cls("text-2xl font-medium text-foreground leading-[1.1] truncate", nameClassName)}>
|
||||
{member.name}
|
||||
</h3>
|
||||
<div className="relative z-1 secondary-button px-3 py-1 rounded-theme" >
|
||||
<p className={cls("text-xs text-foreground leading-[1.1] truncate", roleClassName)}>
|
||||
{member.role}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className={cls("relative z-1 text-base text-foreground leading-[1.1]", memberDescriptionClassName)}>
|
||||
{member.description}
|
||||
</p>
|
||||
|
||||
{member.socialLinks && member.socialLinks.length > 0 && (
|
||||
<div className={cls("relative z-1 flex gap-3 mt-1", socialLinksClassName)}>
|
||||
{member.socialLinks.map((link, index) => (
|
||||
<a
|
||||
key={index}
|
||||
href={link.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cls("primary-button h-9 aspect-square w-auto flex items-center justify-center rounded-theme", socialIconClassName)}
|
||||
>
|
||||
<link.icon className="h-4/10 text-background" strokeWidth={1.5} />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
TeamMemberCard.displayName = "TeamMemberCard";
|
||||
|
||||
const TeamCardTwo = ({
|
||||
members,
|
||||
carouselMode = "buttons",
|
||||
gridVariant,
|
||||
uniformGridCustomHeightClasses = "min-h-95 2xl:min-h-105",
|
||||
animationType,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
textboxLayout,
|
||||
useInvertedBackground,
|
||||
ariaLabel = "Team section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
cardClassName = "",
|
||||
textBoxTitleClassName = "",
|
||||
textBoxTitleImageWrapperClassName = "",
|
||||
textBoxTitleImageClassName = "",
|
||||
textBoxDescriptionClassName = "",
|
||||
imageClassName = "",
|
||||
overlayClassName = "",
|
||||
nameClassName = "",
|
||||
roleClassName = "",
|
||||
memberDescriptionClassName = "",
|
||||
socialLinksClassName = "",
|
||||
socialIconClassName = "",
|
||||
gridClassName = "",
|
||||
carouselClassName = "",
|
||||
controlsClassName = "",
|
||||
textBoxClassName = "",
|
||||
textBoxTagClassName = "",
|
||||
textBoxButtonContainerClassName = "",
|
||||
textBoxButtonClassName = "",
|
||||
textBoxButtonTextClassName = "",
|
||||
}: TeamCardTwoProps) => {
|
||||
const customGridRows = (gridVariant === "bento-grid" || gridVariant === "bento-grid-inverted")
|
||||
? "md:grid-rows-[22rem_22rem] 2xl:grid-rows-[26rem_26rem]"
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<CardStack
|
||||
mode={carouselMode}
|
||||
gridVariant={gridVariant}
|
||||
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
|
||||
gridRowsClassName={customGridRows}
|
||||
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}
|
||||
>
|
||||
{members.map((member, index) => (
|
||||
<TeamMemberCard
|
||||
key={`${member.id}-${index}`}
|
||||
member={member}
|
||||
cardClassName={cardClassName}
|
||||
imageClassName={imageClassName}
|
||||
overlayClassName={overlayClassName}
|
||||
nameClassName={nameClassName}
|
||||
roleClassName={roleClassName}
|
||||
memberDescriptionClassName={memberDescriptionClassName}
|
||||
socialLinksClassName={socialLinksClassName}
|
||||
socialIconClassName={socialIconClassName}
|
||||
/>
|
||||
))}
|
||||
</CardStack>
|
||||
);
|
||||
};
|
||||
|
||||
TeamCardTwo.displayName = "TeamCardTwo";
|
||||
|
||||
export default TeamCardTwo;
|
||||
109
src/components/sections/testimonial/TestimonialCardFifteen.tsx
Normal file
109
src/components/sections/testimonial/TestimonialCardFifteen.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Star } from "lucide-react";
|
||||
import TextAnimation from "@/components/text/TextAnimation";
|
||||
import AvatarGroup from "@/components/shared/AvatarGroup";
|
||||
import type { Avatar } from "@/components/shared/AvatarGroup";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||
|
||||
interface TestimonialCardFifteenProps {
|
||||
testimonial: string;
|
||||
rating: number;
|
||||
author: string;
|
||||
avatars: Avatar[];
|
||||
useInvertedBackground: InvertedBackground;
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
ratingClassName?: string;
|
||||
starClassName?: string;
|
||||
testimonialClassName?: string;
|
||||
avatarGroupClassName?: string;
|
||||
avatarClassName?: string;
|
||||
avatarImageClassName?: string;
|
||||
}
|
||||
|
||||
const TestimonialCardFifteen = ({
|
||||
testimonial,
|
||||
rating,
|
||||
author,
|
||||
avatars,
|
||||
useInvertedBackground,
|
||||
ariaLabel = "Testimonial section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
ratingClassName = "",
|
||||
starClassName = "",
|
||||
testimonialClassName = "",
|
||||
avatarGroupClassName = "",
|
||||
avatarClassName = "",
|
||||
avatarImageClassName = "",
|
||||
}: TestimonialCardFifteenProps) => {
|
||||
const theme = useTheme();
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkMobile = () => setIsMobile(window.innerWidth < 768);
|
||||
checkMobile();
|
||||
window.addEventListener("resize", checkMobile);
|
||||
return () => window.removeEventListener("resize", checkMobile);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("relative py-20 w-full", useInvertedBackground === "invertDefault" && "bg-foreground", className)}
|
||||
>
|
||||
<div className={cls("w-content-width mx-auto flex flex-col items-center gap-6", containerClassName)}>
|
||||
<div className={cls("relative z-1 flex gap-1 -mb-1", ratingClassName)}>
|
||||
{Array.from({ length: 5 }).map((_, index) => (
|
||||
<Star
|
||||
key={index}
|
||||
className={cls(
|
||||
"h-6 w-auto text-accent",
|
||||
index < rating ? "fill-accent" : "fill-transparent",
|
||||
starClassName
|
||||
)}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<TextAnimation
|
||||
type={theme.defaultTextAnimation}
|
||||
text={testimonial}
|
||||
variant="words-trigger"
|
||||
as="p"
|
||||
className={cls(
|
||||
"text-3xl md:text-5xl font-medium text-balance text-center leading-tight",
|
||||
useInvertedBackground === "invertDefault" && "text-background",
|
||||
testimonialClassName
|
||||
)}
|
||||
/>
|
||||
|
||||
<p className={cls(
|
||||
"text-xl text-center mb-1",
|
||||
useInvertedBackground === "invertDefault" && "text-background"
|
||||
)}>
|
||||
{author}
|
||||
</p>
|
||||
|
||||
<AvatarGroup
|
||||
avatars={avatars}
|
||||
maxVisible={isMobile ? 3 : 6}
|
||||
className={cls("justify-center", avatarGroupClassName)}
|
||||
avatarClassName={avatarClassName}
|
||||
avatarImageClassName={cls("h-[var(--width-17_5)] md:h-[var(--width-5)]", avatarImageClassName)}
|
||||
avatarOverlapClassName="-ml-8"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
TestimonialCardFifteen.displayName = "TestimonialCardFifteen";
|
||||
|
||||
export default TestimonialCardFifteen;
|
||||
230
src/components/sections/testimonial/TestimonialCardFive.tsx
Normal file
230
src/components/sections/testimonial/TestimonialCardFive.tsx
Normal file
@@ -0,0 +1,230 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
|
||||
|
||||
import FullWidthCarousel from "@/components/cardStack/layouts/carousels/FullWidthCarousel";
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
import Tag from "@/components/shared/Tag";
|
||||
import TestimonialAuthor from "@/components/shared/TestimonialAuthor";
|
||||
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 Testimonial = {
|
||||
id: string;
|
||||
name: string;
|
||||
date: string;
|
||||
title: string;
|
||||
quote: string;
|
||||
tag: string;
|
||||
avatarSrc: string;
|
||||
avatarAlt?: string;
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
};
|
||||
|
||||
interface TestimonialCardFiveProps {
|
||||
testimonials: Testimonial[];
|
||||
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;
|
||||
cardTagClassName?: string;
|
||||
cardTitleClassName?: string;
|
||||
cardQuoteClassName?: string;
|
||||
cardAuthorClassName?: string;
|
||||
cardAvatarWrapperClassName?: string;
|
||||
cardAvatarClassName?: string;
|
||||
cardNameClassName?: string;
|
||||
cardDateClassName?: string;
|
||||
cardImageClassName?: string;
|
||||
carouselClassName?: string;
|
||||
dotsClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
textBoxTagClassName?: string;
|
||||
textBoxButtonContainerClassName?: string;
|
||||
textBoxButtonClassName?: string;
|
||||
textBoxButtonTextClassName?: string;
|
||||
}
|
||||
|
||||
interface TestimonialCardProps {
|
||||
testimonial: Testimonial;
|
||||
useInvertedBackground: "noInvert" | "invertDefault";
|
||||
cardClassName?: string;
|
||||
tagClassName?: string;
|
||||
titleClassName?: string;
|
||||
quoteClassName?: string;
|
||||
authorClassName?: string;
|
||||
avatarWrapperClassName?: string;
|
||||
avatarClassName?: string;
|
||||
nameClassName?: string;
|
||||
dateClassName?: string;
|
||||
imageClassName?: string;
|
||||
}
|
||||
|
||||
const TestimonialCard = memo(({
|
||||
testimonial,
|
||||
useInvertedBackground,
|
||||
cardClassName = "",
|
||||
tagClassName = "",
|
||||
titleClassName = "",
|
||||
quoteClassName = "",
|
||||
authorClassName = "",
|
||||
avatarWrapperClassName = "",
|
||||
avatarClassName = "",
|
||||
nameClassName = "",
|
||||
dateClassName = "",
|
||||
imageClassName = "",
|
||||
}: TestimonialCardProps) => {
|
||||
const theme = useTheme();
|
||||
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
|
||||
|
||||
return (
|
||||
<div className={cls("card h-fit md:h-full rounded-theme-capped overflow-hidden flex flex-col-reverse md:grid md:grid-cols-2 gap-0", cardClassName)}>
|
||||
<div className="relative z-1 p-6 md:p-10 flex flex-col gap-10 justify-between">
|
||||
<div className="flex flex-col gap-4 md:gap-6">
|
||||
<Tag
|
||||
text={testimonial.tag}
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
className={tagClassName}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-4 overflow-hidden">
|
||||
<h3 className={cls("text-4xl font-medium leading-tight line-clamp-3", shouldUseLightText ? "text-background" : "text-foreground", titleClassName)}>
|
||||
{testimonial.title}
|
||||
</h3>
|
||||
<blockquote className={cls("text-base md:text-lg leading-tight line-clamp-10", shouldUseLightText ? "text-background/75" : "text-foreground/75", quoteClassName)}>
|
||||
{testimonial.quote}
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TestimonialAuthor
|
||||
name={testimonial.name}
|
||||
subtitle={testimonial.date}
|
||||
imageSrc={testimonial.avatarSrc}
|
||||
imageAlt={testimonial.avatarAlt}
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
className={authorClassName}
|
||||
imageWrapperClassName={avatarWrapperClassName}
|
||||
imageClassName={avatarClassName}
|
||||
nameClassName={nameClassName}
|
||||
subtitleClassName={dateClassName}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative z-1 min-h-0 aspect-square">
|
||||
<MediaContent
|
||||
imageSrc={testimonial.imageSrc}
|
||||
videoSrc={testimonial.videoSrc}
|
||||
imageAlt={testimonial.imageAlt}
|
||||
videoAriaLabel={testimonial.videoAriaLabel}
|
||||
imageClassName={cls("!absolute inset-0 w-full h-full object-cover !rounded-none", imageClassName)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
TestimonialCard.displayName = "TestimonialCard";
|
||||
|
||||
const TestimonialCardFive = ({
|
||||
testimonials,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
textboxLayout,
|
||||
useInvertedBackground,
|
||||
ariaLabel = "Testimonials section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
cardClassName = "",
|
||||
textBoxTitleClassName = "",
|
||||
textBoxTitleImageWrapperClassName = "",
|
||||
textBoxTitleImageClassName = "",
|
||||
textBoxDescriptionClassName = "",
|
||||
cardTagClassName = "",
|
||||
cardTitleClassName = "",
|
||||
cardQuoteClassName = "",
|
||||
cardAuthorClassName = "",
|
||||
cardAvatarWrapperClassName = "",
|
||||
cardAvatarClassName = "",
|
||||
cardNameClassName = "",
|
||||
cardDateClassName = "",
|
||||
cardImageClassName = "",
|
||||
carouselClassName = "",
|
||||
dotsClassName = "",
|
||||
textBoxClassName = "",
|
||||
textBoxTagClassName = "",
|
||||
textBoxButtonContainerClassName = "",
|
||||
textBoxButtonClassName = "",
|
||||
textBoxButtonTextClassName = "",
|
||||
}: TestimonialCardFiveProps) => {
|
||||
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}
|
||||
>
|
||||
{testimonials.map((testimonial, index) => (
|
||||
<TestimonialCard
|
||||
key={`${testimonial.id}-${index}`}
|
||||
testimonial={testimonial}
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
cardClassName={cardClassName}
|
||||
tagClassName={cardTagClassName}
|
||||
titleClassName={cardTitleClassName}
|
||||
quoteClassName={cardQuoteClassName}
|
||||
authorClassName={cardAuthorClassName}
|
||||
avatarWrapperClassName={cardAvatarWrapperClassName}
|
||||
avatarClassName={cardAvatarClassName}
|
||||
nameClassName={cardNameClassName}
|
||||
dateClassName={cardDateClassName}
|
||||
imageClassName={cardImageClassName}
|
||||
/>
|
||||
))}
|
||||
</FullWidthCarousel>
|
||||
);
|
||||
};
|
||||
|
||||
TestimonialCardFive.displayName = "TestimonialCardFive";
|
||||
|
||||
export default TestimonialCardFive;
|
||||
213
src/components/sections/testimonial/TestimonialCardOne.tsx
Normal file
213
src/components/sections/testimonial/TestimonialCardOne.tsx
Normal file
@@ -0,0 +1,213 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import Image from "next/image";
|
||||
import CardStack from "@/components/cardStack/CardStack";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { Star } from "lucide-react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig, CardAnimationTypeWith3D, GridVariant, TitleSegment, TextboxLayout, InvertedBackground } from "@/components/cardStack/types";
|
||||
|
||||
type Testimonial = {
|
||||
id: string;
|
||||
name: string;
|
||||
role: string;
|
||||
company: string;
|
||||
rating: number;
|
||||
imageSrc: string;
|
||||
imageAlt?: string;
|
||||
};
|
||||
|
||||
interface TestimonialCardOneProps {
|
||||
testimonials: Testimonial[];
|
||||
carouselMode?: "auto" | "buttons";
|
||||
uniformGridCustomHeightClasses?: string;
|
||||
gridVariant: GridVariant;
|
||||
animationType: CardAnimationTypeWith3D;
|
||||
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;
|
||||
imageClassName?: string;
|
||||
overlayClassName?: string;
|
||||
ratingClassName?: string;
|
||||
nameClassName?: string;
|
||||
roleClassName?: string;
|
||||
companyClassName?: string;
|
||||
gridClassName?: string;
|
||||
carouselClassName?: string;
|
||||
controlsClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
textBoxTagClassName?: string;
|
||||
textBoxButtonContainerClassName?: string;
|
||||
textBoxButtonClassName?: string;
|
||||
textBoxButtonTextClassName?: string;
|
||||
}
|
||||
|
||||
interface TestimonialCardProps {
|
||||
testimonial: Testimonial;
|
||||
cardClassName?: string;
|
||||
imageClassName?: string;
|
||||
overlayClassName?: string;
|
||||
ratingClassName?: string;
|
||||
nameClassName?: string;
|
||||
roleClassName?: string;
|
||||
companyClassName?: string;
|
||||
}
|
||||
|
||||
const TestimonialCard = memo(({
|
||||
testimonial,
|
||||
cardClassName = "",
|
||||
imageClassName = "",
|
||||
overlayClassName = "",
|
||||
ratingClassName = "",
|
||||
nameClassName = "",
|
||||
roleClassName = "",
|
||||
companyClassName = "",
|
||||
}: TestimonialCardProps) => {
|
||||
return (
|
||||
<div className={cls("relative h-full rounded-theme-capped overflow-hidden group", cardClassName)}>
|
||||
<Image
|
||||
src={testimonial.imageSrc}
|
||||
alt={testimonial.imageAlt || testimonial.name}
|
||||
width={800}
|
||||
height={800}
|
||||
className={cls("relative z-1 w-full h-full object-cover!", imageClassName)}
|
||||
unoptimized={testimonial.imageSrc.startsWith('http') || testimonial.imageSrc.startsWith('//')}
|
||||
aria-hidden={testimonial.imageAlt === ""}
|
||||
/>
|
||||
|
||||
<div className={cls("!absolute z-1 bottom-6 left-6 right-6 card backdrop-blur-xs p-6 flex flex-col gap-3 rounded-theme-capped", overlayClassName)}>
|
||||
<div className={cls("relative z-1 flex gap-1", ratingClassName)}>
|
||||
{Array.from({ length: 5 }).map((_, index) => (
|
||||
<Star
|
||||
key={index}
|
||||
className={cls(
|
||||
"h-5 w-auto text-accent",
|
||||
index < testimonial.rating ? "fill-accent" : "fill-transparent"
|
||||
)}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<h3 className={cls("relative z-1 text-2xl font-medium text-foreground leading-[1.1] mt-1", nameClassName)}>
|
||||
{testimonial.name}
|
||||
</h3>
|
||||
|
||||
<div className="relative z-1 flex flex-col gap-1">
|
||||
<p className={cls("text-base text-foreground leading-[1.1]", roleClassName)}>
|
||||
{testimonial.role}
|
||||
</p>
|
||||
<p className={cls("text-base text-foreground leading-[1.1]", companyClassName)}>
|
||||
{testimonial.company}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
TestimonialCard.displayName = "TestimonialCard";
|
||||
|
||||
const TestimonialCardOne = ({
|
||||
testimonials,
|
||||
carouselMode = "buttons",
|
||||
uniformGridCustomHeightClasses = "min-h-95 2xl:min-h-105",
|
||||
gridVariant,
|
||||
animationType,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
textboxLayout,
|
||||
useInvertedBackground,
|
||||
ariaLabel = "Testimonials section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
cardClassName = "",
|
||||
textBoxTitleClassName = "",
|
||||
textBoxTitleImageWrapperClassName = "",
|
||||
textBoxTitleImageClassName = "",
|
||||
textBoxDescriptionClassName = "",
|
||||
imageClassName = "",
|
||||
overlayClassName = "",
|
||||
ratingClassName = "",
|
||||
nameClassName = "",
|
||||
roleClassName = "",
|
||||
companyClassName = "",
|
||||
gridClassName = "",
|
||||
carouselClassName = "",
|
||||
controlsClassName = "",
|
||||
textBoxClassName = "",
|
||||
textBoxTagClassName = "",
|
||||
textBoxButtonContainerClassName = "",
|
||||
textBoxButtonClassName = "",
|
||||
textBoxButtonTextClassName = "",
|
||||
}: TestimonialCardOneProps) => {
|
||||
return (
|
||||
<CardStack
|
||||
mode={carouselMode}
|
||||
gridVariant={gridVariant}
|
||||
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
|
||||
animationType={animationType}
|
||||
supports3DAnimation={true}
|
||||
|
||||
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}
|
||||
>
|
||||
{testimonials.map((testimonial, index) => (
|
||||
<TestimonialCard
|
||||
key={`${testimonial.id}-${index}`}
|
||||
testimonial={testimonial}
|
||||
cardClassName={cardClassName}
|
||||
imageClassName={imageClassName}
|
||||
overlayClassName={overlayClassName}
|
||||
ratingClassName={ratingClassName}
|
||||
nameClassName={nameClassName}
|
||||
roleClassName={roleClassName}
|
||||
companyClassName={companyClassName}
|
||||
/>
|
||||
))}
|
||||
</CardStack>
|
||||
);
|
||||
};
|
||||
|
||||
TestimonialCardOne.displayName = "TestimonialCardOne";
|
||||
|
||||
export default TestimonialCardOne;
|
||||
197
src/components/sections/testimonial/TestimonialCardSix.tsx
Normal file
197
src/components/sections/testimonial/TestimonialCardSix.tsx
Normal file
@@ -0,0 +1,197 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import AutoCarousel from "@/components/cardStack/layouts/carousels/AutoCarousel";
|
||||
import TestimonialAuthor from "@/components/shared/TestimonialAuthor";
|
||||
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||
import { Quote } from "lucide-react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { CardAnimationType, ButtonConfig, TitleSegment, TextboxLayout, InvertedBackground } from "@/components/cardStack/types";
|
||||
|
||||
type Testimonial = {
|
||||
id: string;
|
||||
name: string;
|
||||
handle: string;
|
||||
testimonial: string;
|
||||
imageSrc?: string;
|
||||
imageAlt?: string;
|
||||
icon?: LucideIcon;
|
||||
};
|
||||
|
||||
interface TestimonialCardSixProps {
|
||||
testimonials: Testimonial[];
|
||||
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;
|
||||
testimonialClassName?: string;
|
||||
imageWrapperClassName?: string;
|
||||
imageClassName?: string;
|
||||
iconClassName?: string;
|
||||
nameClassName?: string;
|
||||
handleClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
textBoxTitleClassName?: string;
|
||||
textBoxTitleImageWrapperClassName?: string;
|
||||
textBoxTitleImageClassName?: string;
|
||||
textBoxDescriptionClassName?: string;
|
||||
textBoxTagClassName?: string;
|
||||
textBoxButtonContainerClassName?: string;
|
||||
textBoxButtonClassName?: string;
|
||||
textBoxButtonTextClassName?: string;
|
||||
}
|
||||
|
||||
interface TestimonialCardProps {
|
||||
testimonial: Testimonial;
|
||||
useInvertedBackground: "noInvert" | "invertDefault";
|
||||
cardClassName?: string;
|
||||
testimonialClassName?: string;
|
||||
imageWrapperClassName?: string;
|
||||
imageClassName?: string;
|
||||
iconClassName?: string;
|
||||
nameClassName?: string;
|
||||
handleClassName?: string;
|
||||
}
|
||||
|
||||
const TestimonialCard = memo(({
|
||||
testimonial,
|
||||
useInvertedBackground,
|
||||
cardClassName = "",
|
||||
testimonialClassName = "",
|
||||
imageWrapperClassName = "",
|
||||
imageClassName = "",
|
||||
iconClassName = "",
|
||||
nameClassName = "",
|
||||
handleClassName = "",
|
||||
}: TestimonialCardProps) => {
|
||||
const Icon = testimonial.icon || Quote;
|
||||
const theme = useTheme();
|
||||
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
|
||||
|
||||
return (
|
||||
<div className={cls("relative h-full card rounded-theme-capped p-6 min-h-0 flex flex-col gap-10", cardClassName)}>
|
||||
<p className={cls("relative z-1 text-lg leading-tight line-clamp-2", shouldUseLightText ? "text-background" : "text-foreground", testimonialClassName)}>
|
||||
{testimonial.testimonial}
|
||||
</p>
|
||||
|
||||
<TestimonialAuthor
|
||||
name={testimonial.name}
|
||||
subtitle={testimonial.handle}
|
||||
imageSrc={testimonial.imageSrc}
|
||||
imageAlt={testimonial.imageAlt}
|
||||
icon={Icon}
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
imageWrapperClassName={imageWrapperClassName}
|
||||
imageClassName={imageClassName}
|
||||
iconClassName={iconClassName}
|
||||
nameClassName={nameClassName}
|
||||
subtitleClassName={handleClassName}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
TestimonialCard.displayName = "TestimonialCard";
|
||||
|
||||
const TestimonialCardSix = ({
|
||||
testimonials,
|
||||
animationType,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
textboxLayout,
|
||||
useInvertedBackground,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
speed = 40,
|
||||
topMarqueeDirection = "left",
|
||||
ariaLabel = "Testimonials section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
carouselClassName = "",
|
||||
bottomCarouselClassName = "",
|
||||
cardClassName = "",
|
||||
testimonialClassName = "",
|
||||
imageWrapperClassName = "",
|
||||
imageClassName = "",
|
||||
iconClassName = "",
|
||||
nameClassName = "",
|
||||
handleClassName = "",
|
||||
textBoxClassName = "",
|
||||
textBoxTitleClassName = "",
|
||||
textBoxTitleImageWrapperClassName = "",
|
||||
textBoxTitleImageClassName = "",
|
||||
textBoxDescriptionClassName = "",
|
||||
textBoxTagClassName = "",
|
||||
textBoxButtonContainerClassName = "",
|
||||
textBoxButtonClassName = "",
|
||||
textBoxButtonTextClassName = "",
|
||||
}: TestimonialCardSixProps) => {
|
||||
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}
|
||||
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!"
|
||||
>
|
||||
{testimonials.map((testimonial, index) => (
|
||||
<TestimonialCard
|
||||
key={`${testimonial.id}-${index}`}
|
||||
testimonial={testimonial}
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
cardClassName={cardClassName}
|
||||
testimonialClassName={testimonialClassName}
|
||||
imageWrapperClassName={imageWrapperClassName}
|
||||
imageClassName={imageClassName}
|
||||
iconClassName={iconClassName}
|
||||
nameClassName={nameClassName}
|
||||
handleClassName={handleClassName}
|
||||
/>
|
||||
))}
|
||||
</AutoCarousel>
|
||||
);
|
||||
};
|
||||
|
||||
TestimonialCardSix.displayName = "TestimonialCardSix";
|
||||
|
||||
export default TestimonialCardSix;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user