294 lines
9.4 KiB
TypeScript
294 lines
9.4 KiB
TypeScript
"use client";
|
|
|
|
import { memo, useMemo } from "react";
|
|
import Image from "next/image";
|
|
import TextAnimation from "./text/TextAnimation";
|
|
import Button from "./button/Button";
|
|
import Tag from "./shared/Tag";
|
|
import AvatarGroup from "./shared/AvatarGroup";
|
|
import { cls } from "@/lib/utils";
|
|
import { getButtonProps } from "@/lib/buttonUtils";
|
|
import { LucideIcon } from "lucide-react";
|
|
import type { AnimationType } from "./text/types";
|
|
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
|
import type { ButtonConfig } from "@/types/button";
|
|
import type { Avatar } from "./shared/AvatarGroup";
|
|
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
|
|
|
type TitleSegment =
|
|
| { type: "text"; content: string }
|
|
| { type: "image"; src: string; alt?: string };
|
|
|
|
interface TextBoxProps {
|
|
title: string;
|
|
titleSegments?: TitleSegment[];
|
|
description: string;
|
|
type?: AnimationType;
|
|
textboxLayout?: TextboxLayout;
|
|
useInvertedBackground?: InvertedBackground;
|
|
className?: string;
|
|
titleClassName?: string;
|
|
titleImageWrapperClassName?: string;
|
|
titleImageClassName?: string;
|
|
descriptionClassName?: string;
|
|
duration?: number;
|
|
start?: string;
|
|
end?: string;
|
|
gradientColors?: {
|
|
from: string;
|
|
to: string;
|
|
};
|
|
children?: React.ReactNode;
|
|
center?: boolean;
|
|
tag?: string;
|
|
tagIcon?: LucideIcon;
|
|
tagClassName?: string;
|
|
buttons?: ButtonConfig[];
|
|
buttonContainerClassName?: string;
|
|
buttonClassName?: string;
|
|
buttonTextClassName?: string;
|
|
avatars?: Avatar[];
|
|
avatarText?: string;
|
|
avatarGroupClassName?: string;
|
|
avatarsAbove?: boolean;
|
|
}
|
|
|
|
const TextBox = ({
|
|
title,
|
|
titleSegments,
|
|
description,
|
|
type,
|
|
textboxLayout = "default",
|
|
useInvertedBackground,
|
|
className = "",
|
|
titleClassName = "",
|
|
titleImageWrapperClassName = "",
|
|
titleImageClassName = "",
|
|
descriptionClassName = "",
|
|
duration = 1,
|
|
start = "top 80%",
|
|
end = "top 20%",
|
|
gradientColors,
|
|
children,
|
|
center = false,
|
|
tag,
|
|
tagIcon: TagIcon,
|
|
tagClassName = "",
|
|
buttons,
|
|
buttonContainerClassName = "",
|
|
buttonClassName = "",
|
|
buttonTextClassName = "",
|
|
avatars,
|
|
avatarText,
|
|
avatarGroupClassName = "",
|
|
avatarsAbove = false,
|
|
}: TextBoxProps) => {
|
|
const theme = useTheme();
|
|
|
|
// Shared tag component
|
|
const tagElement = useMemo(() => tag && (
|
|
<Tag
|
|
text={tag}
|
|
icon={TagIcon}
|
|
useInvertedBackground={useInvertedBackground}
|
|
className={cls(textboxLayout === "default" && "mb-3", tagClassName)}
|
|
/>
|
|
), [tag, TagIcon, useInvertedBackground, textboxLayout, tagClassName]);
|
|
|
|
// Shared title component
|
|
const titleElement = useMemo(() => (
|
|
<TextAnimation
|
|
type={type || theme.defaultTextAnimation}
|
|
text={title}
|
|
variant="trigger"
|
|
as="h2"
|
|
className={cls(
|
|
textboxLayout === "split" || textboxLayout === "split-actions" || textboxLayout === "split-description" ? "text-7xl font-medium text-balance" : "text-6xl font-medium",
|
|
center && textboxLayout === "default" && "text-center",
|
|
useInvertedBackground === "invertDefault" && "text-background",
|
|
titleClassName
|
|
)}
|
|
duration={duration}
|
|
start={start}
|
|
end={end}
|
|
gradientColors={gradientColors}
|
|
/>
|
|
), [type, theme.defaultTextAnimation, title, textboxLayout, center, useInvertedBackground, titleClassName, duration, start, end, gradientColors]);
|
|
|
|
// Inline image title component (used when textboxLayout === "inline-image")
|
|
const inlineImageTitleElement = useMemo(() => titleSegments && titleSegments.length > 0 ? (
|
|
<h2
|
|
className={cls(
|
|
"text-4xl md:text-5xl font-medium text-center leading-[1.15] text-balance",
|
|
useInvertedBackground === "invertDefault" && "text-background",
|
|
titleClassName
|
|
)}
|
|
>
|
|
{titleSegments.map((segment, index) => {
|
|
const imageIndex = titleSegments
|
|
.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",
|
|
titleImageWrapperClassName
|
|
)}
|
|
>
|
|
<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",
|
|
titleImageClassName
|
|
)}
|
|
unoptimized={segment.src.startsWith("http") || segment.src.startsWith("//")}
|
|
aria-hidden={!segment.alt || segment.alt === ""}
|
|
/>
|
|
</div>
|
|
</span>
|
|
);
|
|
|
|
return (
|
|
<span key={index}>
|
|
{index > 0 && " "}
|
|
{element}
|
|
</span>
|
|
);
|
|
})}
|
|
</h2>
|
|
) : null, [titleSegments, useInvertedBackground, titleClassName, titleImageWrapperClassName, titleImageClassName]);
|
|
|
|
// Shared description component
|
|
const descriptionElement = useMemo(() => (
|
|
<TextAnimation
|
|
type={type || theme.defaultTextAnimation}
|
|
text={description}
|
|
variant="words-trigger"
|
|
as="p"
|
|
className={cls(
|
|
"text-lg leading-[1.2]",
|
|
center && textboxLayout === "default" && "text-center",
|
|
(textboxLayout === "split" || textboxLayout === "split-description") && "text-balance",
|
|
useInvertedBackground === "invertDefault" && "text-background",
|
|
descriptionClassName
|
|
)}
|
|
duration={duration}
|
|
start={start}
|
|
end={end}
|
|
gradientColors={gradientColors}
|
|
/>
|
|
), [type, theme.defaultTextAnimation, description, center, textboxLayout, useInvertedBackground, descriptionClassName, duration, start, end, gradientColors]);
|
|
|
|
// Shared avatars component
|
|
const avatarsElement = useMemo(() => avatars && avatars.length > 0 ? (
|
|
<AvatarGroup
|
|
avatars={avatars}
|
|
text={avatarText}
|
|
className={cls(
|
|
textboxLayout === "default" && !avatarsAbove && "mt-3",
|
|
textboxLayout === "default" && avatarsAbove && "mb-3",
|
|
center && textboxLayout === "default" && "justify-center",
|
|
avatarGroupClassName
|
|
)}
|
|
/>
|
|
) : null, [avatars, avatarText, textboxLayout, center, avatarGroupClassName, avatarsAbove]);
|
|
|
|
// Shared buttons/children component
|
|
const actionsElement = useMemo(() => buttons && buttons.length > 0 ? (
|
|
<div className={cls(
|
|
"flex flex-wrap gap-4",
|
|
textboxLayout === "default" && "w-full mt-3",
|
|
(textboxLayout === "split" || textboxLayout === "split-actions") && "w-fit",
|
|
center && textboxLayout === "default" && "justify-center",
|
|
buttonContainerClassName
|
|
)}>
|
|
{/* Limit to 2 buttons for optimal layout */}
|
|
{buttons.slice(0, 2).map((button, index) => (
|
|
<Button key={`${button.text}-${index}`} {...getButtonProps(button, index, theme.defaultButtonVariant, buttonClassName, buttonTextClassName)} />
|
|
))}
|
|
</div>
|
|
) : (
|
|
children
|
|
), [buttons, textboxLayout, center, buttonContainerClassName, theme.defaultButtonVariant, buttonClassName, buttonTextClassName, children]);
|
|
|
|
// Split layout
|
|
if (textboxLayout === "split") {
|
|
return (
|
|
<div className={cls("flex flex-col md:flex-row gap-3 md:gap-15 md:items-end", className)}>
|
|
<div className="w-full md:w-6/10 flex flex-col gap-3">
|
|
{tagElement}
|
|
{titleElement}
|
|
{descriptionElement}
|
|
</div>
|
|
<div className="w-full md:w-4/10 flex flex-col gap-3 md:items-end">
|
|
{actionsElement}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Split actions layout - tag and buttons required, no description
|
|
if (textboxLayout === "split-actions") {
|
|
return (
|
|
<div className={cls("flex flex-col md:flex-row gap-3 md:gap-15 md:items-end", className)}>
|
|
<div className="w-full md:w-6/10 flex flex-col gap-3">
|
|
{tagElement}
|
|
{titleElement}
|
|
</div>
|
|
<div className="w-full md:w-4/10 flex flex-col gap-3 md:items-end">
|
|
{actionsElement}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Split description layout - tag + title left, description only right (no buttons)
|
|
if (textboxLayout === "split-description") {
|
|
return (
|
|
<div className={cls("flex flex-col md:flex-row gap-3 md:gap-15 md:items-end", className)}>
|
|
<div className="w-full md:w-6/10 flex flex-col gap-3">
|
|
{tagElement}
|
|
{titleElement}
|
|
</div>
|
|
<div className="w-full md:w-4/10 flex flex-col gap-3 md:items-end">
|
|
{descriptionElement}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Inline image layout - centered heading with inline images and optional buttons
|
|
if (textboxLayout === "inline-image") {
|
|
return (
|
|
<div className={cls("flex flex-col gap-3 md:gap-1", center && "items-center text-center", className)}>
|
|
{inlineImageTitleElement}
|
|
{actionsElement}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Default layout
|
|
return (
|
|
<div className={cls("flex flex-col gap-3 md:gap-1", center && "items-center text-center", className)}>
|
|
{avatarsAbove && avatarsElement}
|
|
{tagElement}
|
|
{titleElement}
|
|
{descriptionElement}
|
|
{actionsElement}
|
|
{!avatarsAbove && avatarsElement}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
TextBox.displayName = "TextBox";
|
|
|
|
export default memo(TextBox); |