Initial commit
This commit is contained in:
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;
|
||||
Reference in New Issue
Block a user