Compare commits
145 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b38a3aa53e | |||
| 330b839edb | |||
| 5189c0ef06 | |||
| 69284a9bc5 | |||
| c791bc36b4 | |||
| 7d72b20504 | |||
| 6cf58eb80e | |||
| d310408c40 | |||
| c598bb2c03 | |||
| 2cf7807ac5 | |||
| 225af4cb24 | |||
| f9c6224cd7 | |||
| 46cd897d66 | |||
| eb2b1dd934 | |||
| 7a1e20041f | |||
| 1725b8922b | |||
| f2793de41f | |||
| 8b6bd9240a | |||
| 9e91ea6691 | |||
| 945dd65e23 | |||
| 78188aef3d | |||
| fbd234445b | |||
| 8858cbb8c7 | |||
| 2183e2d71d | |||
| f5c6ff907d | |||
| b75e9b5a03 | |||
| 656100a671 | |||
| bb452c82ee | |||
| 76ab382ee0 | |||
| 52510e25ae | |||
| 20c7905e12 | |||
| 322a709004 | |||
| 051f166ccd | |||
| e9637ec81b | |||
| 8f346ed19a | |||
| 14287f3124 | |||
| 47c583b58f | |||
| ff14c7a3b2 | |||
| 41d348d0e9 | |||
| d921953e6c | |||
| 04e92bc619 | |||
| af229b31fd | |||
| aa7607c131 | |||
| 3aeb216dbb | |||
| 23a73cc153 | |||
| 12066e1221 | |||
| 66cc3416bc | |||
| fc2022f1a1 | |||
| 0709e68668 | |||
| a0b366fe08 | |||
| c55b51f4eb | |||
| d2d4108be5 | |||
| 56dc8f4a00 | |||
| f98a8e09af | |||
| c50103b9c1 | |||
| f2c8aea86b | |||
| 04af10119d | |||
| 73027b9668 | |||
| 653e0cee81 | |||
| 2c0130b97e | |||
| 2c22f5d941 | |||
| 627ab52c95 | |||
| d7b97eaea9 | |||
| 0c623bcde5 | |||
| f09d84df8e | |||
| ef9401d43c | |||
| 9b93fbbb52 | |||
| 956e086f07 | |||
| bf96f64a4f | |||
| 5940279f0c | |||
| 1eab8b9e3a | |||
| fdd6e2664a | |||
| fcc7ec1c99 | |||
| 0ae82a1608 | |||
| 0acbd8e6ea | |||
| 5544ad7fad | |||
| 4d6c03d002 | |||
| 9e4a22e833 | |||
| db99285231 | |||
| b8cc8f74ec | |||
| 3bcf98062c | |||
| dbf3839660 | |||
| 28f89f9d04 | |||
| 3d0ff08a6e | |||
| c7ac258143 | |||
| 773160caeb | |||
| 420c425ec5 | |||
| e1f8453580 | |||
| b14708e6fa | |||
| a0257a817a | |||
| 7a1a206467 | |||
| 1fe34b99d4 | |||
| 5d4bb9e831 | |||
| 6d15cfcb94 | |||
| c524dae93a | |||
| 66c7cb34b9 | |||
| 7c843a7ca0 | |||
| f8b76ad100 | |||
| 79c73691cf | |||
| 7c80f4d0ae | |||
| 04c4330fca | |||
| 8716aaffbb | |||
| 5ab380a33e | |||
| f98d7eb59a | |||
| 2976d06cd8 | |||
| cbd6e736f7 | |||
| f1bcc50cc4 | |||
| 5747588489 | |||
| 26c97a5b68 | |||
| a4268a16e5 | |||
| 5a96f3519f | |||
| 6633d4b871 | |||
| b055ffa7f5 | |||
| ec63b9c810 | |||
| 8eda28ff9d | |||
| 37f2dac2b1 | |||
| 5ca06d1fb2 | |||
| 04122d8e48 | |||
| d47ae0e9fc | |||
| af6ffe0786 | |||
| 44370f6188 | |||
| 51653d862f | |||
| 22c090c0f6 | |||
| 6c2ce92dc2 | |||
| c17d431625 | |||
| 7a4b714e7c | |||
| 55afdec94b | |||
| fe3ddc1947 | |||
| 58a24ed3ab | |||
| dff857eb8f | |||
| eb52028a9c | |||
| 8b9cb7f58a | |||
| 187c7038d3 | |||
| f3d67ed6d6 | |||
| cb6725bfd0 | |||
| d8892d5e21 | |||
| 676404e0a7 | |||
| b983d971fd | |||
| f2a2ee42fa | |||
| b8eb33a67a | |||
| 054bf5b27e | |||
| 81b7cdbe06 | |||
| 7326c51bc0 | |||
| 2c3336a4d8 | |||
| d2bcdd1d17 |
@@ -1,55 +1,27 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Halant } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import { Inter } from "next/font/google";
|
|
||||||
import { Archivo } from "next/font/google";
|
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { ServiceWrapper } from "@/components/ServiceWrapper";
|
|
||||||
import Tag from "@/tag/Tag";
|
|
||||||
|
|
||||||
const halant = Halant({
|
const geist = Geist({
|
||||||
variable: "--font-halant", subsets: ["latin"],
|
variable: "--font-geist-sans", subsets: ["latin"],
|
||||||
weight: ["300", "400", "500", "600", "700"],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const inter = Inter({
|
const geistMono = Geist_Mono({
|
||||||
variable: "--font-inter", subsets: ["latin"],
|
variable: "--font-geist-mono", subsets: ["latin"],
|
||||||
});
|
|
||||||
|
|
||||||
const archivo = Archivo({
|
|
||||||
variable: "--font-archivo", subsets: ["latin"],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Certified Reiki Healing | Serenity Reiki - Restore Your Energy", description: "Experience transformative reiki healing with a certified master. Release stress, heal pain, and restore balance through authentic Japanese energy work in a peaceful sanctuary.", keywords: "reiki healing, energy healing, certified reiki master, chakra balancing, trauma release, wellness therapy, spiritual healing", metadataBase: new URL("https://sereneity-reiki.com"),
|
title: "Create Next App", description: "Generated by create next app"};
|
||||||
openGraph: {
|
|
||||||
title: "Certified Reiki Healing | Serenity Reiki", description: "Transform your energy and restore inner peace with professional reiki healing sessions.", type: "website", siteName: "Serenity Reiki", images: [
|
|
||||||
{
|
|
||||||
url: "http://img.b2bpic.net/free-photo/crop-woman-meditating-home_23-2147802469.jpg", alt: "Peaceful reiki healing sanctuary"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
twitter: {
|
|
||||||
card: "summary_large_image", title: "Certified Reiki Healing | Serenity Reiki", description: "Experience transformative energy healing and restore your inner peace.", images: ["http://img.b2bpic.net/free-photo/crop-woman-meditating-home_23-2147802469.jpg"]
|
|
||||||
},
|
|
||||||
robots: {
|
|
||||||
index: true,
|
|
||||||
follow: true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="en">
|
||||||
<ServiceWrapper>
|
<body className={`${geist.variable} ${geistMono.variable} antialiased`}>
|
||||||
<body
|
{children}
|
||||||
className={`${halant.variable} ${inter.variable} ${archivo.variable} antialiased`}
|
|
||||||
>
|
|
||||||
<Tag />
|
|
||||||
{children}
|
|
||||||
|
|
||||||
<script
|
<script
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
@@ -1418,7 +1390,6 @@ export default function RootLayout({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</body>
|
</body>
|
||||||
</ServiceWrapper>
|
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
224
src/app/page.tsx
224
src/app/page.tsx
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
|
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
|
||||||
import NavbarLayoutFloatingOverlay from "@/components/navbar/NavbarLayoutFloatingOverlay/NavbarLayoutFloatingOverlay";
|
import NavbarLayoutFloatingOverlay from "@/components/navbar/NavbarLayoutFloatingOverlay/NavbarLayoutFloatingOverlay";
|
||||||
import HeroLogoBillboard from "@/components/sections/hero/HeroLogoBillboard";
|
import HeroLogoBillboard from "@/components/sections/hero/HeroLogoBillboard";
|
||||||
@@ -11,7 +12,49 @@ import ContactText from "@/components/sections/contact/ContactText";
|
|||||||
import FooterBase from "@/components/sections/footer/FooterBase";
|
import FooterBase from "@/components/sections/footer/FooterBase";
|
||||||
import { BookOpen, Heart, Shield, Sparkles, Wifi, Zap } from "lucide-react";
|
import { BookOpen, Heart, Shield, Sparkles, Wifi, Zap } from "lucide-react";
|
||||||
|
|
||||||
|
type Language = "en" | "fr";
|
||||||
|
|
||||||
|
const translations = {
|
||||||
|
en: {
|
||||||
|
navItems: [
|
||||||
|
{ name: "About", id: "about" },
|
||||||
|
{ name: "Services", id: "services" },
|
||||||
|
{ name: "Testimonials", id: "testimonials" },
|
||||||
|
{ name: "Pricing", id: "pricing" },
|
||||||
|
{ name: "Contact", id: "contact" }
|
||||||
|
],
|
||||||
|
bookButton: "Start Your Healing Today", heroTitle: "Restore Balance", heroDescription: "Experience certified reiki healing in a peaceful sanctuary. Release stress, pain, and emotional blocks through ancient Japanese energy work.", learnMore: "Learn More", aboutTitle: "About Your Healer", aboutDesc1: "With over 15 years of dedicated practice, I am a certified reiki master trained in traditional Japanese techniques and modern energy healing. My mission is to create a sanctuary where stress dissolves, chronic pain finds relief, and emotional blocks transform into healing pathways.", aboutDesc2: "I believe in the profound connection between body, mind, and spirit. Each session is personalized to your unique energy needs, combining reiki with intuitive guidance to support your journey toward wholeness and balance.", aboutDesc3: "My clients consistently report reduced anxiety, improved sleep, relief from chronic pain, and a renewed sense of peace and purpose. I am honored to walk alongside you on your healing journey.", credentials: "Certifications & Credentials", servicesTitle: "Healing Modalities", servicesDesc: "Explore the transformative reiki services designed to restore your energy and promote deep wellness.", service1: "Traditional Japanese Reiki", service1Desc: "Authentic reiki energy work addressing root imbalances and chakra alignment for holistic healing.", service2: "Chakra Balancing", service2Desc: "Targeted energy work to harmonize your seven chakras, promoting emotional and physical wellness.", service3: "Crystal Energy Reiki", service3Desc: "Enhanced reiki sessions incorporating crystalline vibrations to amplify healing and transformation.", service4: "Trauma Release Work", service4Desc: "Specialized sessions for releasing emotional trauma, held patterns, and energetic blocks with compassion.", service5: "Distant Healing Sessions", service5Desc: "Receive transformative reiki energy from the comfort of your home through powerful distance healing.", service6: "Integration Coaching", service6Desc: "Post-session guidance to integrate healing insights and sustain your wellness journey with practical tools.", pricingTitle: "Healing Sessions & Pricing", pricingDesc: "Choose the perfect reiki session to support your wellness journey. All sessions include personalized consultation and integration guidance.", session30: "30-Minute Session", session60: "60-Minute Session", session90: "90-Minute Deep Healing", sessionMonthly: "Monthly Wellness Package", bookNow: "Book Now", subscribe: "Subscribe Now", mostPopular: "Most Popular", testimonialTitle: "Healing Stories from Our Community", testimonialDesc: "Discover how reiki has transformed the lives of professionals seeking peace, pain relief, and spiritual reconnection.", contactTitle: "Ready to begin your healing journey? Let us restore your energy and bring peace back into your life.", schedule: "Schedule a Session", sendMessage: "Send a Message", footerBrand: "Serenity Reiki", footerCopyright: "© 2025 Serenity Reiki | Certified Energy Healing"
|
||||||
|
},
|
||||||
|
fr: {
|
||||||
|
navItems: [
|
||||||
|
{ name: "À propos", id: "about" },
|
||||||
|
{ name: "Services", id: "services" },
|
||||||
|
{ name: "Témoignages", id: "testimonials" },
|
||||||
|
{ name: "Tarifs", id: "pricing" },
|
||||||
|
{ name: "Contact", id: "contact" }
|
||||||
|
],
|
||||||
|
bookButton: "Commencez votre guérison aujourd'hui", heroTitle: "Restaurez l'équilibre", heroDescription: "Expérience de guérison reiki certifiée dans un sanctuaire paisible. Libérez le stress, la douleur et les blocages émotionnels à travers le travail énergétique japonais ancien.", learnMore: "En savoir plus", aboutTitle: "À propos de votre guérisseur", aboutDesc1: "Avec plus de 15 ans de pratique dédiée, je suis un maître reiki certifié formé aux techniques japonaises traditionnelles et à la guérison énergétique moderne. Ma mission est de créer un sanctuaire où le stress se dissout, la douleur chronique trouve un soulagement et les blocages émotionnels se transforment en chemins de guérison.", aboutDesc2: "Je crois en la connexion profonde entre le corps, l'esprit et l'âme. Chaque session est personnalisée selon vos besoins énergétiques uniques, combinant le reiki avec des conseils intuitifs pour soutenir votre chemin vers l'intégrité et l'équilibre.", aboutDesc3: "Mes clients signalent régulièrement une réduction de l'anxiété, une amélioration du sommeil, un soulagement de la douleur chronique et une paix et un but renouvelés. C'est un honneur de vous accompagner dans votre chemin de guérison.", credentials: "Certifications et références", servicesTitle: "Modalités de guérison", servicesDesc: "Explorez les services reiki transformationnels conçus pour restaurer votre énergie et promouvoir le bien-être profond.", service1: "Reiki japonais traditionnel", service1Desc: "Travail énergétique reiki authentique abordant les déséquilibres racinaires et l'alignement des chakras pour une guérison holistique.", service2: "Équilibrage des chakras", service2Desc: "Travail énergétique ciblé pour harmoniser vos sept chakras, favorisant le bien-être émotionnel et physique.", service3: "Reiki à énergie cristalline", service3Desc: "Sessions reiki améliorées incorporant des vibrations cristallines pour amplifier la guérison et la transformation.", service4: "Travail de libération des traumatismes", service4Desc: "Sessions spécialisées pour libérer les traumatismes émotionnels, les schémas retenus et les blocages énergétiques avec compassion.", service5: "Sessions de guérison à distance", service5Desc: "Recevez une énergie reiki transformationnelle depuis le confort de votre maison grâce à une puissante guérison à distance.", service6: "Coaching d'intégration", service6Desc: "Conseils post-session pour intégrer les insights de guérison et maintenir votre parcours de bien-être avec des outils pratiques.", pricingTitle: "Sessions de guérison et tarifs", pricingDesc: "Choisissez la session reiki parfaite pour soutenir votre parcours de bien-être. Toutes les sessions incluent une consultation personnalisée et des conseils d'intégration.", session30: "Session de 30 minutes", session60: "Session de 60 minutes", session90: "Guérison profonde de 90 minutes", sessionMonthly: "Package de bien-être mensuel", bookNow: "Réserver maintenant", subscribe: "S'abonner", mostPopular: "Plus populaire", testimonialTitle: "Histoires de guérison de notre communauté", testimonialDesc: "Découvrez comment le reiki a transformé la vie de professionnels cherchant la paix, le soulagement de la douleur et la reconnexion spirituelle.", contactTitle: "Prêt à commencer votre parcours de guérison? Restaurons votre énergie et ramenons la paix dans votre vie.", schedule: "Planifier une session", sendMessage: "Envoyer un message", footerBrand: "Serenity Reiki", footerCopyright: "© 2025 Serenity Reiki | Guérison énergétique certifiée"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default function LandingPage() {
|
export default function LandingPage() {
|
||||||
|
const [language, setLanguage] = useState<Language>("en");
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const detectedLang = navigator.language.startsWith("fr") ? "fr" : "en";
|
||||||
|
setLanguage(detectedLang);
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!mounted) return null;
|
||||||
|
|
||||||
|
const t = translations[language];
|
||||||
|
|
||||||
|
const toggleLanguage = () => {
|
||||||
|
setLanguage(language === "en" ? "fr" : "en");
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider
|
<ThemeProvider
|
||||||
defaultButtonVariant="shift-hover"
|
defaultButtonVariant="shift-hover"
|
||||||
@@ -25,17 +68,21 @@ export default function LandingPage() {
|
|||||||
secondaryButtonStyle="glass"
|
secondaryButtonStyle="glass"
|
||||||
headingFontWeight="bold"
|
headingFontWeight="bold"
|
||||||
>
|
>
|
||||||
|
<div className="fixed top-4 right-4 z-50">
|
||||||
|
<button
|
||||||
|
onClick={toggleLanguage}
|
||||||
|
className="px-4 py-2 bg-primary-cta text-primary-cta-text rounded-lg font-semibold hover:bg-primary-cta/90 transition-all duration-300"
|
||||||
|
aria-label={language === "en" ? "Switch to French" : "Passer à l'anglais"}
|
||||||
|
>
|
||||||
|
{language === "en" ? "🇫🇷 FR" : "🇬🇧 EN"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="nav" data-section="nav">
|
<div id="nav" data-section="nav">
|
||||||
<NavbarLayoutFloatingOverlay
|
<NavbarLayoutFloatingOverlay
|
||||||
brandName="Serenity Reiki"
|
brandName="Serenity Reiki"
|
||||||
navItems={[
|
navItems={t.navItems}
|
||||||
{ name: "About", id: "about" },
|
button={{ text: t.bookButton, href: "#pricing" }}
|
||||||
{ name: "Services", id: "services" },
|
|
||||||
{ name: "Testimonials", id: "testimonials" },
|
|
||||||
{ name: "Pricing", id: "pricing" },
|
|
||||||
{ name: "Contact", id: "contact" }
|
|
||||||
]}
|
|
||||||
button={{ text: "Book Your Session", href: "#pricing" }}
|
|
||||||
className="bg-background border border-accent/20"
|
className="bg-background border border-accent/20"
|
||||||
buttonClassName="bg-primary-cta hover:bg-primary-cta/90 text-primary-cta-text transition-all duration-300"
|
buttonClassName="bg-primary-cta hover:bg-primary-cta/90 text-primary-cta-text transition-all duration-300"
|
||||||
buttonTextClassName="font-semibold text-sm"
|
buttonTextClassName="font-semibold text-sm"
|
||||||
@@ -44,15 +91,15 @@ export default function LandingPage() {
|
|||||||
|
|
||||||
<div id="hero" data-section="hero">
|
<div id="hero" data-section="hero">
|
||||||
<HeroLogoBillboard
|
<HeroLogoBillboard
|
||||||
logoText="Restore Balance"
|
logoText={t.heroTitle}
|
||||||
description="Experience certified reiki healing in a peaceful sanctuary. Release stress, pain, and emotional blocks through ancient Japanese energy work."
|
description={t.heroDescription}
|
||||||
buttons={[
|
buttons={[
|
||||||
{ text: "Book Your Session", href: "#pricing" },
|
{ text: t.bookButton, href: "#pricing" },
|
||||||
{ text: "Learn More", href: "#about" }
|
{ text: t.learnMore, href: "#about" }
|
||||||
]}
|
]}
|
||||||
background={{ variant: "plain" }}
|
background={{ variant: "plain" }}
|
||||||
imageSrc="http://img.b2bpic.net/free-photo/crop-woman-meditating-home_23-2147802469.jpg"
|
imageSrc="http://img.b2bpic.net/free-photo/crop-woman-meditating-home_23-2147802469.jpg"
|
||||||
imageAlt="Peaceful reiki healing sanctuary with soft lighting"
|
imageAlt={language === "en" ? "Peaceful reiki healing sanctuary with soft lighting" : "Sanctuaire de guérison reiki paisible avec éclairage doux"}
|
||||||
frameStyle="card"
|
frameStyle="card"
|
||||||
mediaAnimation="slide-up"
|
mediaAnimation="slide-up"
|
||||||
buttonAnimation="slide-up"
|
buttonAnimation="slide-up"
|
||||||
@@ -61,11 +108,13 @@ export default function LandingPage() {
|
|||||||
|
|
||||||
<div id="about" data-section="about">
|
<div id="about" data-section="about">
|
||||||
<TextSplitAbout
|
<TextSplitAbout
|
||||||
title="About Your Healer"
|
title={t.aboutTitle}
|
||||||
description={[
|
description={[
|
||||||
"With over 15 years of dedicated practice, I am a certified reiki master trained in traditional Japanese techniques and modern energy healing. My mission is to create a sanctuary where stress dissolves, chronic pain finds relief, and emotional blocks transform into healing pathways.", "I believe in the profound connection between body, mind, and spirit. Each session is personalized to your unique energy needs, combining reiki with intuitive guidance to support your journey toward wholeness and balance.", "My clients consistently report reduced anxiety, improved sleep, relief from chronic pain, and a renewed sense of peace and purpose. I'm honored to walk alongside you on your healing journey."
|
t.aboutDesc1,
|
||||||
|
t.aboutDesc2,
|
||||||
|
t.aboutDesc3
|
||||||
]}
|
]}
|
||||||
buttons={[{ text: "Certifications & Credentials", href: "#" }]}
|
buttons={[{ text: t.credentials, href: "#" }]}
|
||||||
showBorder={true}
|
showBorder={true}
|
||||||
useInvertedBackground={false}
|
useInvertedBackground={false}
|
||||||
/>
|
/>
|
||||||
@@ -73,128 +122,145 @@ export default function LandingPage() {
|
|||||||
|
|
||||||
<div id="services" data-section="services">
|
<div id="services" data-section="services">
|
||||||
<FeatureCardTwentySix
|
<FeatureCardTwentySix
|
||||||
title="Healing Modalities"
|
title={t.servicesTitle}
|
||||||
description="Explore the transformative reiki services designed to restore your energy and promote deep wellness."
|
description={t.servicesDesc}
|
||||||
features={[
|
features={[
|
||||||
{
|
{
|
||||||
title: "Traditional Japanese Reiki", description: "Authentic reiki energy work addressing root imbalances and chakra alignment for holistic healing.", imageSrc: "http://img.b2bpic.net/free-photo/spiritual-young-man-woman-practicing-yoga-indoors_23-2149163327.jpg", imageAlt: "Practitioner performing traditional reiki energy healing", buttonIcon: Sparkles
|
title: t.service1,
|
||||||
|
description: t.service1Desc,
|
||||||
|
imageSrc: "http://img.b2bpic.net/free-photo/spiritual-young-man-woman-practicing-yoga-indoors_23-2149163327.jpg", imageAlt: language === "en" ? "Practitioner performing traditional reiki energy healing" : "Praticien effectuant une guérison énergétique reiki traditionnelle", buttonIcon: Sparkles
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Chakra Balancing", description: "Targeted energy work to harmonize your seven chakras, promoting emotional and physical wellness.", imageSrc: "http://img.b2bpic.net/free-photo/spiritual-young-man-woman-practicing-yoga-indoors_23-2149163336.jpg", imageAlt: "Chakra energy alignment visualization", buttonIcon: Heart
|
title: t.service2,
|
||||||
|
description: t.service2Desc,
|
||||||
|
imageSrc: "http://img.b2bpic.net/free-photo/spiritual-young-man-woman-practicing-yoga-indoors_23-2149163336.jpg", imageAlt: language === "en" ? "Chakra energy alignment visualization" : "Visualisation de l'alignement de l'énergie des chakras", buttonIcon: Heart
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Crystal Energy Reiki", description: "Enhanced reiki sessions incorporating crystalline vibrations to amplify healing and transformation.", imageSrc: "http://img.b2bpic.net/free-photo/colorful-crystals-flowers-assortment_23-2149324181.jpg", imageAlt: "Healing crystals arranged for reiki energy work", buttonIcon: Zap
|
title: t.service3,
|
||||||
|
description: t.service3Desc,
|
||||||
|
imageSrc: "http://img.b2bpic.net/free-photo/colorful-crystals-flowers-assortment_23-2149324181.jpg", imageAlt: language === "en" ? "Healing crystals arranged for reiki energy work" : "Cristaux de guérison arrangés pour le travail énergétique reiki", buttonIcon: Zap
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Trauma Release Work", description: "Specialized sessions for releasing emotional trauma, held patterns, and energetic blocks with compassion.", imageSrc: "http://img.b2bpic.net/free-photo/senior-couple-is-doing-yoga-outdoors-stretching-park-sunrise-brunette-white-t-shirt_1157-39675.jpg", imageAlt: "Peaceful healing environment for trauma release", buttonIcon: Shield
|
title: t.service4,
|
||||||
|
description: t.service4Desc,
|
||||||
|
imageSrc: "http://img.b2bpic.net/free-photo/senior-couple-is-doing-yoga-outdoors-stretching-park-sunrise-brunette-white-t-shirt_1157-39675.jpg", imageAlt: language === "en" ? "Peaceful healing environment for trauma release" : "Environnement de guérison paisible pour la libération des traumatismes", buttonIcon: Shield
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Distant Healing Sessions", description: "Receive transformative reiki energy from the comfort of your home through powerful distance healing.", imageSrc: "http://img.b2bpic.net/free-photo/full-shot-woman-yoga-mat_23-2148898576.jpg", imageAlt: "Remote reiki healing session setup", buttonIcon: Wifi
|
title: t.service5,
|
||||||
|
description: t.service5Desc,
|
||||||
|
imageSrc: "http://img.b2bpic.net/free-photo/full-shot-woman-yoga-mat_23-2148898576.jpg", imageAlt: language === "en" ? "Remote reiki healing session setup" : "Configuration de session de guérison reiki à distance", buttonIcon: Wifi
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Integration Coaching", description: "Post-session guidance to integrate healing insights and sustain your wellness journey with practical tools.", imageSrc: "http://img.b2bpic.net/free-photo/i-always-feel-relieved-after-session-with-you_637285-9953.jpg", imageAlt: "One-on-one healing integration session", buttonIcon: BookOpen
|
title: t.service6,
|
||||||
|
description: t.service6Desc,
|
||||||
|
imageSrc: "http://img.b2bpic.net/free-photo/i-always-feel-relieved-after-session-with-you_637285-9953.jpg", imageAlt: language === "en" ? "One-on-one healing integration session" : "Session d'intégration de guérison individuelle", buttonIcon: BookOpen
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
textboxLayout="default"
|
textboxLayout="default"
|
||||||
useInvertedBackground={false}
|
useInvertedBackground={false}
|
||||||
tag="Holistic Services"
|
tag={language === "en" ? "Holistic Services" : "Services holistiques"}
|
||||||
tagAnimation="slide-up"
|
tagAnimation="slide-up"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="pricing" data-section="pricing">
|
<div id="pricing" data-section="pricing">
|
||||||
<PricingCardThree
|
<PricingCardThree
|
||||||
title="Healing Sessions & Pricing"
|
|
||||||
description="Choose the perfect reiki session to support your wellness journey. All sessions include personalized consultation and integration guidance."
|
|
||||||
plans={[
|
plans={[
|
||||||
{
|
{
|
||||||
id: "1", price: "$75", name: "30-Minute Session", buttons: [
|
id: "1", price: "$75", name: t.session30,
|
||||||
{ text: "Book Now", href: "#contact" },
|
buttons: [
|
||||||
{ text: "Learn More", href: "#" }
|
{ text: t.bookNow, href: "#contact" },
|
||||||
|
{ text: t.learnMore, href: "#" }
|
||||||
],
|
],
|
||||||
features: [
|
features: [
|
||||||
"Introductory reiki session", "Energy assessment", "Chakra clearing", "Ideal for first-time clients"
|
language === "en" ? "Introductory reiki session" : "Session reiki d'introduction", language === "en" ? "Energy assessment" : "Évaluation énergétique", language === "en" ? "Chakra clearing" : "Nettoyage des chakras", language === "en" ? "Ideal for first-time clients" : "Idéal pour les nouveaux clients"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "2", badge: "Most Popular", badgeIcon: Heart,
|
id: "2", badge: t.mostPopular,
|
||||||
price: "$120", name: "60-Minute Session", buttons: [
|
badgeIcon: Heart,
|
||||||
{ text: "Book Now", href: "#contact" },
|
price: "$120", name: t.session60,
|
||||||
{ text: "Learn More", href: "#" }
|
buttons: [
|
||||||
|
{ text: t.bookNow, href: "#contact" },
|
||||||
|
{ text: t.learnMore, href: "#" }
|
||||||
],
|
],
|
||||||
features: [
|
features: [
|
||||||
"Full-spectrum reiki healing", "Deep chakra balancing", "Energy cord clearing", "Integration guidance"
|
language === "en" ? "Full-spectrum reiki healing" : "Guérison reiki à spectre complet", language === "en" ? "Deep chakra balancing" : "Équilibrage profond des chakras", language === "en" ? "Energy cord clearing" : "Nettoyage des cordes énergétiques", language === "en" ? "Integration guidance" : "Conseils d'intégration"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "3", price: "$180", name: "90-Minute Deep Healing", buttons: [
|
id: "3", price: "$180", name: t.session90,
|
||||||
{ text: "Book Now", href: "#contact" },
|
buttons: [
|
||||||
{ text: "Learn More", href: "#" }
|
{ text: t.bookNow, href: "#contact" },
|
||||||
|
{ text: t.learnMore, href: "#" }
|
||||||
],
|
],
|
||||||
features: [
|
features: [
|
||||||
"Comprehensive energy work", "Trauma release session", "Crystal energy integration", "Extended guidance"
|
language === "en" ? "Comprehensive energy work" : "Travail énergétique complet", language === "en" ? "Trauma release session" : "Session de libération des traumatismes", language === "en" ? "Crystal energy integration" : "Intégration de l'énergie cristalline", language === "en" ? "Extended guidance" : "Conseils étendus"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "4", price: "$300/mo", name: "Monthly Wellness Package", buttons: [
|
id: "4", price: "$300/mo", name: t.sessionMonthly,
|
||||||
{ text: "Subscribe Now", href: "#contact" },
|
buttons: [
|
||||||
{ text: "Learn More", href: "#" }
|
{ text: t.subscribe, href: "#contact" },
|
||||||
|
{ text: t.learnMore, href: "#" }
|
||||||
],
|
],
|
||||||
features: [
|
features: [
|
||||||
"4 monthly sessions (60 min each)", "Priority scheduling", "Personalized healing plan", "Monthly integration check-ins"
|
language === "en" ? "4 monthly sessions (60 min each)" : "4 sessions mensuelles (60 min chacune)", language === "en" ? "Priority scheduling" : "Planification prioritaire", language === "en" ? "Personalized healing plan" : "Plan de guérison personnalisé", language === "en" ? "Monthly integration check-ins" : "Vérifications d'intégration mensuelles"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
|
title={t.pricingTitle}
|
||||||
|
description={t.pricingDesc}
|
||||||
|
tag={language === "en" ? "Transparent Pricing" : "Tarification transparente"}
|
||||||
|
tagAnimation="slide-up"
|
||||||
|
animationType="slide-up"
|
||||||
textboxLayout="default"
|
textboxLayout="default"
|
||||||
useInvertedBackground={false}
|
useInvertedBackground={false}
|
||||||
animationType="slide-up"
|
|
||||||
tag="Transparent Pricing"
|
|
||||||
tagAnimation="slide-up"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="testimonials" data-section="testimonials">
|
<div id="testimonials" data-section="testimonials">
|
||||||
<TestimonialCardSixteen
|
<TestimonialCardSixteen
|
||||||
title="Healing Stories from Our Community"
|
|
||||||
description="Discover how reiki has transformed the lives of professionals seeking peace, pain relief, and spiritual reconnection."
|
|
||||||
testimonials={[
|
testimonials={[
|
||||||
{
|
{
|
||||||
id: "1", name: "Sarah Mitchell", role: "Executive Director", company: "Tech Innovation Lab", rating: 5,
|
id: "1", name: "Sarah Mitchell", role: language === "en" ? "Executive Director" : "Directrice exécutive", company: "Tech Innovation Lab", rating: 5,
|
||||||
imageSrc: "http://img.b2bpic.net/free-photo/successful-businesswoman-ready-challenges_1163-4336.jpg"
|
imageSrc: "http://img.b2bpic.net/free-photo/successful-businesswoman-ready-challenges_1163-4336.jpg"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "2", name: "James Chen", role: "Yoga Instructor", company: "Wellness Center", rating: 5,
|
id: "2", name: "James Chen", role: language === "en" ? "Yoga Instructor" : "Instructeur de yoga", company: "Wellness Center", rating: 5,
|
||||||
imageSrc: "http://img.b2bpic.net/free-photo/side-view-smiley-man-gym_23-2150007196.jpg"
|
imageSrc: "http://img.b2bpic.net/free-photo/side-view-smiley-man-gym_23-2150007196.jpg"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "3", name: "Emily Rodriguez", role: "Life Coach", company: "Transformation Coaching", rating: 5,
|
id: "3", name: "Emily Rodriguez", role: language === "en" ? "Life Coach" : "Coach de vie", company: "Transformation Coaching", rating: 5,
|
||||||
imageSrc: "http://img.b2bpic.net/free-photo/happy-smiling-businesswoman-looking-camera-with-arms-crossed-portrait_1163-4337.jpg"
|
imageSrc: "http://img.b2bpic.net/free-photo/happy-smiling-businesswoman-looking-camera-with-arms-crossed-portrait_1163-4337.jpg"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "4", name: "Michael Thompson", role: "Corporate Wellness Manager", company: "Fortune 500 Company", rating: 5,
|
id: "4", name: "Michael Thompson", role: language === "en" ? "Corporate Wellness Manager" : "Gestionnaire du bien-être d'entreprise", company: "Fortune 500 Company", rating: 5,
|
||||||
imageSrc: "http://img.b2bpic.net/free-photo/close-up-portrait-young-handsome-successful-man_1163-5475.jpg"
|
imageSrc: "http://img.b2bpic.net/free-photo/close-up-portrait-young-handsome-successful-man_1163-5475.jpg"
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
kpiItems={[
|
kpiItems={[
|
||||||
{ value: "500+", label: "Clients healed" },
|
{ value: "500+", label: language === "en" ? "Clients healed" : "Clients guéris" },
|
||||||
{ value: "98%", label: "Client satisfaction rate" },
|
{ value: "98%", label: language === "en" ? "Client satisfaction rate" : "Taux de satisfaction client" },
|
||||||
{ value: "15+", label: "Years of practice" }
|
{ value: "15+", label: language === "en" ? "Years of practice" : "Années de pratique" }
|
||||||
]}
|
]}
|
||||||
|
title={t.testimonialTitle}
|
||||||
|
description={t.testimonialDesc}
|
||||||
|
tag={language === "en" ? "Client Stories" : "Histoires de clients"}
|
||||||
|
tagAnimation="slide-up"
|
||||||
|
animationType="slide-up"
|
||||||
textboxLayout="default"
|
textboxLayout="default"
|
||||||
useInvertedBackground={false}
|
useInvertedBackground={false}
|
||||||
animationType="slide-up"
|
|
||||||
tag="Client Stories"
|
|
||||||
tagAnimation="slide-up"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="contact" data-section="contact">
|
<div id="contact" data-section="contact">
|
||||||
<ContactText
|
<ContactText
|
||||||
text="Ready to begin your healing journey? Let's restore your energy and bring peace back into your life."
|
text={t.contactTitle}
|
||||||
animationType="reveal-blur"
|
animationType="reveal-blur"
|
||||||
buttons={[
|
buttons={[
|
||||||
{ text: "Schedule a Session", href: "#pricing" },
|
{ text: t.schedule, href: "#pricing" },
|
||||||
{ text: "Send a Message", href: "#" }
|
{ text: t.sendMessage, href: "#" }
|
||||||
]}
|
]}
|
||||||
background={{ variant: "plain" }}
|
background={{ variant: "plain" }}
|
||||||
useInvertedBackground={false}
|
useInvertedBackground={false}
|
||||||
@@ -203,31 +269,31 @@ export default function LandingPage() {
|
|||||||
|
|
||||||
<div id="footer" data-section="footer">
|
<div id="footer" data-section="footer">
|
||||||
<FooterBase
|
<FooterBase
|
||||||
logoText="Serenity Reiki"
|
logoText={t.footerBrand}
|
||||||
copyrightText="© 2025 Serenity Reiki | Certified Energy Healing"
|
copyrightText={t.footerCopyright}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
title: "Services", items: [
|
title: language === "en" ? "Services" : "Services", items: [
|
||||||
{ label: "Traditional Reiki", href: "#services" },
|
{ label: t.service1, href: "#services" },
|
||||||
{ label: "Chakra Balancing", href: "#services" },
|
{ label: t.service2, href: "#services" },
|
||||||
{ label: "Crystal Energy Work", href: "#services" },
|
{ label: t.service3, href: "#services" },
|
||||||
{ label: "Distant Healing", href: "#services" }
|
{ label: t.service5, href: "#services" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "About", items: [
|
title: language === "en" ? "About" : "À propos", items: [
|
||||||
{ label: "Meet Your Healer", href: "#about" },
|
{ label: language === "en" ? "Meet Your Healer" : "Rencontrez votre guérisseur", href: "#about" },
|
||||||
{ label: "Certifications", href: "#" },
|
{ label: t.credentials, href: "#" },
|
||||||
{ label: "Healing Philosophy", href: "#" },
|
{ label: language === "en" ? "Healing Philosophy" : "Philosophie de guérison", href: "#" },
|
||||||
{ label: "Testimonials", href: "#testimonials" }
|
{ label: t.testimonialTitle, href: "#testimonials" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Connect", items: [
|
title: language === "en" ? "Connect" : "Connecter", items: [
|
||||||
{ label: "Book a Session", href: "#pricing" },
|
{ label: language === "en" ? "Book a Session" : "Réserver une session", href: "#pricing" },
|
||||||
{ label: "Email", href: "#" },
|
{ label: language === "en" ? "Email" : "E-mail", href: "#" },
|
||||||
{ label: "Privacy Policy", href: "#" },
|
{ label: language === "en" ? "Privacy Policy" : "Politique de confidentialité", href: "#" },
|
||||||
{ label: "Contact", href: "#contact" }
|
{ label: language === "en" ? "Contact" : "Contact", href: "#contact" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
|
|||||||
@@ -1,132 +1,10 @@
|
|||||||
"use client";
|
import React from 'react';
|
||||||
|
import { Dashboard } from '@/components/shared/Dashboard';
|
||||||
|
|
||||||
import TextBox from "@/components/Textbox";
|
export function HeroBillboardDashboard() {
|
||||||
import Dashboard from "@/components/shared/Dashboard";
|
|
||||||
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
|
|
||||||
import { cls } from "@/lib/utils";
|
|
||||||
import type { LucideIcon } from "lucide-react";
|
|
||||||
import type { ButtonConfig, ButtonAnimationType } from "@/types/button";
|
|
||||||
import type { DashboardSidebarItem, DashboardStat, DashboardListItem } from "@/components/shared/Dashboard";
|
|
||||||
import type { ChartDataItem } from "@/components/bento/BentoLineChart/utils";
|
|
||||||
|
|
||||||
type HeroBillboardDashboardBackgroundProps = 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 HeroBillboardDashboardProps {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
background: HeroBillboardDashboardBackgroundProps;
|
|
||||||
tag?: string;
|
|
||||||
tagIcon?: LucideIcon;
|
|
||||||
tagAnimation?: ButtonAnimationType;
|
|
||||||
buttons?: ButtonConfig[];
|
|
||||||
buttonAnimation?: ButtonAnimationType;
|
|
||||||
ariaLabel?: string;
|
|
||||||
dashboard: {
|
|
||||||
title: string;
|
|
||||||
stats: [DashboardStat, DashboardStat, DashboardStat];
|
|
||||||
logoIcon: LucideIcon;
|
|
||||||
sidebarItems: DashboardSidebarItem[];
|
|
||||||
searchPlaceholder?: string;
|
|
||||||
buttons: ButtonConfig[];
|
|
||||||
chartTitle?: string;
|
|
||||||
chartData?: ChartDataItem[];
|
|
||||||
listItems: DashboardListItem[];
|
|
||||||
listTitle?: string;
|
|
||||||
imageSrc: string;
|
|
||||||
videoSrc?: string;
|
|
||||||
imageAlt?: string;
|
|
||||||
videoAriaLabel?: string;
|
|
||||||
className?: string;
|
|
||||||
containerClassName?: string;
|
|
||||||
sidebarClassName?: string;
|
|
||||||
statClassName?: string;
|
|
||||||
chartClassName?: string;
|
|
||||||
listClassName?: string;
|
|
||||||
};
|
|
||||||
className?: string;
|
|
||||||
containerClassName?: string;
|
|
||||||
textBoxClassName?: string;
|
|
||||||
titleClassName?: string;
|
|
||||||
descriptionClassName?: string;
|
|
||||||
tagClassName?: string;
|
|
||||||
buttonContainerClassName?: string;
|
|
||||||
buttonClassName?: string;
|
|
||||||
buttonTextClassName?: string;
|
|
||||||
dashboardClassName?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const HeroBillboardDashboard = ({
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
background,
|
|
||||||
tag,
|
|
||||||
tagIcon,
|
|
||||||
tagAnimation,
|
|
||||||
buttons,
|
|
||||||
buttonAnimation,
|
|
||||||
ariaLabel = "Hero section",
|
|
||||||
dashboard,
|
|
||||||
className = "",
|
|
||||||
containerClassName = "",
|
|
||||||
textBoxClassName = "",
|
|
||||||
titleClassName = "",
|
|
||||||
descriptionClassName = "",
|
|
||||||
tagClassName = "",
|
|
||||||
buttonContainerClassName = "",
|
|
||||||
buttonClassName = "",
|
|
||||||
buttonTextClassName = "",
|
|
||||||
dashboardClassName = "",
|
|
||||||
}: HeroBillboardDashboardProps) => {
|
|
||||||
return (
|
return (
|
||||||
<section
|
<div>
|
||||||
aria-label={ariaLabel}
|
<Dashboard />
|
||||||
className={cls("relative w-full py-hero-page-padding", className)}
|
</div>
|
||||||
>
|
|
||||||
<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}
|
|
||||||
tagAnimation={tagAnimation}
|
|
||||||
buttons={buttons}
|
|
||||||
buttonAnimation={buttonAnimation}
|
|
||||||
className={cls("flex flex-col gap-3 md:gap-3", textBoxClassName)}
|
|
||||||
titleClassName={cls("text-6xl font-medium text-balance", titleClassName)}
|
|
||||||
descriptionClassName={cls("text-base md:text-lg leading-tight", descriptionClassName)}
|
|
||||||
tagClassName={cls("px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-1", tagClassName)}
|
|
||||||
buttonContainerClassName={cls("flex flex-wrap gap-4 max-md:justify-center mt-2", buttonContainerClassName)}
|
|
||||||
buttonClassName={buttonClassName}
|
|
||||||
buttonTextClassName={buttonTextClassName}
|
|
||||||
center={true}
|
|
||||||
/>
|
|
||||||
<Dashboard
|
|
||||||
{...dashboard}
|
|
||||||
className={cls(dashboard.className, dashboardClassName)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
HeroBillboardDashboard.displayName = "HeroBillboardDashboard";
|
|
||||||
|
|
||||||
export default HeroBillboardDashboard;
|
|
||||||
|
|||||||
@@ -1,200 +1,10 @@
|
|||||||
"use client";
|
import React from 'react';
|
||||||
|
import { AutoCarousel } from '../../cardStack/layouts/carousels/AutoCarousel';
|
||||||
|
|
||||||
import TextBox from "@/components/Textbox";
|
export function HeroBillboardGallery() {
|
||||||
import MediaContent from "@/components/shared/MediaContent";
|
return (
|
||||||
import AutoCarousel from "@/components/cardStack/layouts/carousels/AutoCarousel";
|
<div>
|
||||||
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
|
<AutoCarousel />
|
||||||
import { cls } from "@/lib/utils";
|
|
||||||
import { useButtonAnimation } from "@/components/hooks/useButtonAnimation";
|
|
||||||
import type { LucideIcon } from "lucide-react";
|
|
||||||
import type { ButtonConfig, ButtonAnimationType } 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;
|
|
||||||
tagAnimation?: ButtonAnimationType;
|
|
||||||
buttons?: ButtonConfig[];
|
|
||||||
buttonAnimation?: ButtonAnimationType;
|
|
||||||
mediaItems: MediaItem[];
|
|
||||||
mediaAnimation: ButtonAnimationType;
|
|
||||||
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,
|
|
||||||
tagAnimation,
|
|
||||||
buttons,
|
|
||||||
buttonAnimation,
|
|
||||||
mediaItems,
|
|
||||||
mediaAnimation,
|
|
||||||
ariaLabel = "Hero section",
|
|
||||||
className = "",
|
|
||||||
containerClassName = "",
|
|
||||||
textBoxClassName = "",
|
|
||||||
titleClassName = "",
|
|
||||||
descriptionClassName = "",
|
|
||||||
tagClassName = "",
|
|
||||||
buttonContainerClassName = "",
|
|
||||||
buttonClassName = "",
|
|
||||||
buttonTextClassName = "",
|
|
||||||
mediaWrapperClassName = "",
|
|
||||||
imageClassName = "",
|
|
||||||
}: HeroBillboardGalleryProps) => {
|
|
||||||
const { containerRef: mediaContainerRef } = useButtonAnimation({ animationType: mediaAnimation });
|
|
||||||
|
|
||||||
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>
|
</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}
|
|
||||||
tagAnimation={tagAnimation}
|
|
||||||
buttons={buttons}
|
|
||||||
buttonAnimation={buttonAnimation}
|
|
||||||
className={cls(
|
|
||||||
"flex flex-col gap-3 md:gap-3 w-content-width mx-auto",
|
|
||||||
textBoxClassName
|
|
||||||
)}
|
|
||||||
titleClassName={cls("text-6xl font-medium text-balance", titleClassName)}
|
|
||||||
descriptionClassName={cls("text-base md:text-lg leading-tight", descriptionClassName)}
|
|
||||||
tagClassName={cls("px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-1", tagClassName)}
|
|
||||||
buttonContainerClassName={cls("flex flex-wrap gap-4 max-md:justify-center mt-2", 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 ref={mediaContainerRef} 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;
|
|
||||||
|
|||||||
@@ -1,274 +1,15 @@
|
|||||||
"use client";
|
import { useCardAnimation } from '@/components/cardStack/CardStack';
|
||||||
|
|
||||||
import { memo } from "react";
|
export function MetricCardEleven() {
|
||||||
import CardStackTextBox from "@/components/cardStack/CardStackTextBox";
|
const { animate, itemRefs } = useCardAnimation();
|
||||||
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, ButtonAnimationType } from "@/components/cardStack/types";
|
|
||||||
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
|
||||||
|
|
||||||
type MediaProps =
|
const handleAnimate = () => {
|
||||||
| {
|
animate(0);
|
||||||
imageSrc: string;
|
};
|
||||||
imageAlt?: string;
|
|
||||||
videoSrc?: never;
|
|
||||||
videoAriaLabel?: never;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
videoSrc: string;
|
|
||||||
videoAriaLabel?: string;
|
|
||||||
imageSrc?: never;
|
|
||||||
imageAlt?: never;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Metric = MediaProps & {
|
return (
|
||||||
id: string;
|
<div>
|
||||||
value: string;
|
<button onClick={handleAnimate}>Metrics</button>
|
||||||
title: string;
|
</div>
|
||||||
description: string;
|
);
|
||||||
};
|
|
||||||
|
|
||||||
interface MetricCardElevenProps {
|
|
||||||
metrics: Metric[];
|
|
||||||
animationType: CardAnimationType;
|
|
||||||
title: string;
|
|
||||||
titleSegments?: TitleSegment[];
|
|
||||||
description: string;
|
|
||||||
tag?: string;
|
|
||||||
tagIcon?: LucideIcon;
|
|
||||||
tagAnimation?: ButtonAnimationType;
|
|
||||||
buttons?: ButtonConfig[];
|
|
||||||
buttonAnimation?: ButtonAnimationType;
|
|
||||||
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,
|
|
||||||
tagAnimation,
|
|
||||||
buttons,
|
|
||||||
buttonAnimation,
|
|
||||||
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 && "bg-foreground", className)}
|
|
||||||
>
|
|
||||||
<div className={cls("w-content-width mx-auto", containerClassName)}>
|
|
||||||
<CardStackTextBox
|
|
||||||
title={title}
|
|
||||||
titleSegments={titleSegments}
|
|
||||||
description={description}
|
|
||||||
tag={tag}
|
|
||||||
tagIcon={tagIcon}
|
|
||||||
tagAnimation={tagAnimation}
|
|
||||||
buttons={buttons}
|
|
||||||
buttonAnimation={buttonAnimation}
|
|
||||||
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;
|
|
||||||
@@ -1,212 +1,11 @@
|
|||||||
"use client";
|
import { useCardAnimation } from '@/components/cardStack/CardStack';
|
||||||
|
|
||||||
import { memo } from "react";
|
export function MetricCardOne() {
|
||||||
import CardStack from "@/components/cardStack/CardStack";
|
const { animate } = useCardAnimation();
|
||||||
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
|
||||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
|
||||||
import type { LucideIcon } from "lucide-react";
|
|
||||||
import type { ButtonConfig, GridVariant, CardAnimationTypeWith3D, TitleSegment, ButtonAnimationType } 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">;
|
return (
|
||||||
|
<div>
|
||||||
type Metric = {
|
<button onClick={() => animate()}>Metric</button>
|
||||||
id: string;
|
</div>
|
||||||
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;
|
|
||||||
tagAnimation?: ButtonAnimationType;
|
|
||||||
buttons?: ButtonConfig[];
|
|
||||||
buttonAnimation?: ButtonAnimationType;
|
|
||||||
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-primary-cta-text", iconClassName)} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
MetricCardItem.displayName = "MetricCardItem";
|
|
||||||
|
|
||||||
const MetricCardOne = ({
|
|
||||||
metrics,
|
|
||||||
carouselMode = "buttons",
|
|
||||||
gridVariant,
|
|
||||||
uniformGridCustomHeightClasses,
|
|
||||||
animationType,
|
|
||||||
title,
|
|
||||||
titleSegments,
|
|
||||||
description,
|
|
||||||
tag,
|
|
||||||
tagIcon,
|
|
||||||
tagAnimation,
|
|
||||||
buttons,
|
|
||||||
buttonAnimation,
|
|
||||||
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}
|
|
||||||
carouselThreshold={4}
|
|
||||||
carouselItemClassName="w-carousel-item-3!"
|
|
||||||
|
|
||||||
title={title}
|
|
||||||
titleSegments={titleSegments}
|
|
||||||
description={description}
|
|
||||||
tag={tag}
|
|
||||||
tagIcon={tagIcon}
|
|
||||||
tagAnimation={tagAnimation}
|
|
||||||
buttons={buttons}
|
|
||||||
buttonAnimation={buttonAnimation}
|
|
||||||
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;
|
|
||||||
|
|||||||
@@ -1,194 +1,11 @@
|
|||||||
"use client";
|
import { useCardAnimation } from '@/components/cardStack/CardStack';
|
||||||
|
|
||||||
import { memo } from "react";
|
export function MetricCardSeven() {
|
||||||
import CardStack from "@/components/cardStack/CardStack";
|
const { animate } = useCardAnimation();
|
||||||
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, ButtonAnimationType } from "@/components/cardStack/types";
|
|
||||||
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
|
||||||
|
|
||||||
type Metric = {
|
return (
|
||||||
id: string;
|
<div>
|
||||||
value: string;
|
<button onClick={() => animate()}>Metric</button>
|
||||||
title: string;
|
</div>
|
||||||
items: string[];
|
);
|
||||||
};
|
|
||||||
|
|
||||||
interface MetricCardSevenProps {
|
|
||||||
metrics: Metric[];
|
|
||||||
carouselMode?: "auto" | "buttons";
|
|
||||||
uniformGridCustomHeightClasses?: string;
|
|
||||||
animationType: CardAnimationTypeWith3D;
|
|
||||||
title: string;
|
|
||||||
titleSegments?: TitleSegment[];
|
|
||||||
description: string;
|
|
||||||
tag?: string;
|
|
||||||
tagIcon?: LucideIcon;
|
|
||||||
tagAnimation?: ButtonAnimationType;
|
|
||||||
buttons?: ButtonConfig[];
|
|
||||||
buttonAnimation?: ButtonAnimationType;
|
|
||||||
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,
|
|
||||||
tagAnimation,
|
|
||||||
buttons,
|
|
||||||
buttonAnimation,
|
|
||||||
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}
|
|
||||||
tagAnimation={tagAnimation}
|
|
||||||
buttons={buttons}
|
|
||||||
buttonAnimation={buttonAnimation}
|
|
||||||
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;
|
|
||||||
|
|||||||
@@ -1,245 +1,11 @@
|
|||||||
"use client";
|
import { useCardAnimation } from '@/components/cardStack/CardStack';
|
||||||
|
|
||||||
import { memo } from "react";
|
export function MetricCardTen() {
|
||||||
import CardStack from "@/components/cardStack/CardStack";
|
const { animate } = useCardAnimation();
|
||||||
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, ButtonAnimationType } from "@/components/cardStack/types";
|
|
||||||
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
|
||||||
import type { CTAButtonVariant } from "@/components/button/types";
|
|
||||||
|
|
||||||
type Metric = {
|
return (
|
||||||
id: string;
|
<div>
|
||||||
title: string;
|
<button onClick={() => animate()}>Metric</button>
|
||||||
subtitle: string;
|
</div>
|
||||||
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;
|
|
||||||
tagAnimation?: ButtonAnimationType;
|
|
||||||
buttons?: ButtonConfig[];
|
|
||||||
buttonAnimation?: ButtonAnimationType;
|
|
||||||
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-background-accent/50 p-4 rounded-b-theme-capped", footerClassName)}>
|
|
||||||
<div className="flex flex-wrap gap-4 max-md:justify-center">
|
|
||||||
{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,
|
|
||||||
tagAnimation,
|
|
||||||
buttons,
|
|
||||||
buttonAnimation,
|
|
||||||
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}
|
|
||||||
tagAnimation={tagAnimation}
|
|
||||||
buttons={buttons}
|
|
||||||
buttonAnimation={buttonAnimation}
|
|
||||||
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;
|
|
||||||
|
|||||||
@@ -1,186 +1,11 @@
|
|||||||
"use client";
|
import { useCardAnimation } from '@/components/cardStack/CardStack';
|
||||||
|
|
||||||
import { memo } from "react";
|
export function MetricCardThree() {
|
||||||
import CardStack from "@/components/cardStack/CardStack";
|
const { animate } = useCardAnimation();
|
||||||
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
|
||||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
|
||||||
import type { LucideIcon } from "lucide-react";
|
|
||||||
import type { ButtonConfig, CardAnimationTypeWith3D, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
|
|
||||||
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
|
||||||
|
|
||||||
type Metric = {
|
return (
|
||||||
id: string;
|
<div>
|
||||||
icon: LucideIcon;
|
<button onClick={() => animate()}>Metric</button>
|
||||||
title: string;
|
</div>
|
||||||
value: string;
|
);
|
||||||
};
|
|
||||||
|
|
||||||
interface MetricCardThreeProps {
|
|
||||||
metrics: Metric[];
|
|
||||||
carouselMode?: "auto" | "buttons";
|
|
||||||
uniformGridCustomHeightClasses?: string;
|
|
||||||
animationType: CardAnimationTypeWith3D;
|
|
||||||
title: string;
|
|
||||||
titleSegments?: TitleSegment[];
|
|
||||||
description: string;
|
|
||||||
tag?: string;
|
|
||||||
tagIcon?: LucideIcon;
|
|
||||||
tagAnimation?: ButtonAnimationType;
|
|
||||||
buttons?: ButtonConfig[];
|
|
||||||
buttonAnimation?: ButtonAnimationType;
|
|
||||||
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-primary-cta-text", 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,
|
|
||||||
tagAnimation,
|
|
||||||
buttons,
|
|
||||||
buttonAnimation,
|
|
||||||
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}
|
|
||||||
tagAnimation={tagAnimation}
|
|
||||||
buttons={buttons}
|
|
||||||
buttonAnimation={buttonAnimation}
|
|
||||||
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;
|
|
||||||
@@ -1,183 +1,11 @@
|
|||||||
"use client";
|
import { useCardAnimation } from '@/components/cardStack/CardStack';
|
||||||
|
|
||||||
import { memo } from "react";
|
export function MetricCardTwo() {
|
||||||
import CardStack from "@/components/cardStack/CardStack";
|
const { animate } = useCardAnimation();
|
||||||
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
|
||||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
|
||||||
import type { LucideIcon } from "lucide-react";
|
|
||||||
import type { ButtonConfig, GridVariant, CardAnimationTypeWith3D, TitleSegment, ButtonAnimationType } 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">;
|
return (
|
||||||
|
<div>
|
||||||
type Metric = {
|
<button onClick={() => animate()}>Metric</button>
|
||||||
id: string;
|
</div>
|
||||||
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;
|
|
||||||
tagAnimation?: ButtonAnimationType;
|
|
||||||
buttons?: ButtonConfig[];
|
|
||||||
buttonAnimation?: ButtonAnimationType;
|
|
||||||
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)}>
|
|
||||||
<h3 className={cls("relative z-1 text-9xl md:text-7xl font-medium truncate", shouldUseLightText ? "text-background" : "text-foreground", valueClassName)}>
|
|
||||||
{metric.value}
|
|
||||||
</h3>
|
|
||||||
<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,
|
|
||||||
tagAnimation,
|
|
||||||
buttons,
|
|
||||||
buttonAnimation,
|
|
||||||
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}
|
|
||||||
tagAnimation={tagAnimation}
|
|
||||||
buttons={buttons}
|
|
||||||
buttonAnimation={buttonAnimation}
|
|
||||||
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}
|
|
||||||
carouselThreshold={4}
|
|
||||||
carouselItemClassName="w-carousel-item-3!"
|
|
||||||
>
|
|
||||||
{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;
|
|
||||||
|
|||||||
@@ -1,248 +1,3 @@
|
|||||||
"use client";
|
export function PricingCardEight() {
|
||||||
|
return <div>Pricing Card Eight</div>;
|
||||||
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, ButtonAnimationType } 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;
|
|
||||||
tagAnimation?: ButtonAnimationType;
|
|
||||||
buttons?: ButtonConfig[];
|
|
||||||
buttonAnimation?: ButtonAnimationType;
|
|
||||||
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,
|
|
||||||
tagAnimation,
|
|
||||||
buttons,
|
|
||||||
buttonAnimation,
|
|
||||||
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}
|
|
||||||
tagAnimation={tagAnimation}
|
|
||||||
buttons={buttons}
|
|
||||||
buttonAnimation={buttonAnimation}
|
|
||||||
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;
|
|
||||||
|
|||||||
@@ -1,231 +1,10 @@
|
|||||||
"use client";
|
import React from 'react';
|
||||||
|
import { CardList } from '../../cardStack/CardList';
|
||||||
|
|
||||||
import { Check } from "lucide-react";
|
export function PricingCardFive() {
|
||||||
import CardList from "@/components/cardStack/CardList";
|
return (
|
||||||
import Tag from "@/components/shared/Tag";
|
<div>
|
||||||
import Button from "@/components/button/Button";
|
<CardList />
|
||||||
import { getButtonProps } from "@/lib/buttonUtils";
|
</div>
|
||||||
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
);
|
||||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
|
||||||
import type { LucideIcon } from "lucide-react";
|
|
||||||
import type { ButtonConfig, ButtonAnimationType, 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;
|
|
||||||
tagAnimation?: ButtonAnimationType;
|
|
||||||
buttons?: ButtonConfig[];
|
|
||||||
buttonAnimation?: ButtonAnimationType;
|
|
||||||
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,
|
|
||||||
tagAnimation,
|
|
||||||
buttons,
|
|
||||||
buttonAnimation,
|
|
||||||
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}
|
|
||||||
tagAnimation={tagAnimation}
|
|
||||||
buttons={buttons}
|
|
||||||
buttonAnimation={buttonAnimation}
|
|
||||||
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-primary-cta-text" 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;
|
|
||||||
|
|||||||
@@ -1,216 +1,10 @@
|
|||||||
"use client";
|
import React from 'react';
|
||||||
|
import { CardList } from '../../cardStack/CardList';
|
||||||
|
|
||||||
import { Check } from "lucide-react";
|
export function PricingCardNine() {
|
||||||
import CardList from "@/components/cardStack/CardList";
|
return (
|
||||||
import Button from "@/components/button/Button";
|
<div>
|
||||||
import MediaContent from "@/components/shared/MediaContent";
|
<CardList />
|
||||||
import Tag from "@/components/shared/Tag";
|
</div>
|
||||||
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, ButtonAnimationType } 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;
|
|
||||||
videoSrc?: string;
|
|
||||||
imageAlt?: string;
|
|
||||||
videoAriaLabel?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface PricingCardNineProps {
|
|
||||||
plans: PricingPlan[];
|
|
||||||
animationType: CardAnimationType;
|
|
||||||
title: string;
|
|
||||||
titleSegments?: TitleSegment[];
|
|
||||||
description: string;
|
|
||||||
tag?: string;
|
|
||||||
tagIcon?: LucideIcon;
|
|
||||||
tagAnimation?: ButtonAnimationType;
|
|
||||||
buttons?: ButtonConfig[];
|
|
||||||
buttonAnimation?: ButtonAnimationType;
|
|
||||||
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,
|
|
||||||
tagAnimation,
|
|
||||||
buttons,
|
|
||||||
buttonAnimation,
|
|
||||||
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}
|
|
||||||
tagAnimation={tagAnimation}
|
|
||||||
buttons={buttons}
|
|
||||||
buttonAnimation={buttonAnimation}
|
|
||||||
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}
|
|
||||||
videoSrc={plan.videoSrc}
|
|
||||||
imageAlt={plan.imageAlt || plan.title}
|
|
||||||
videoAriaLabel={plan.videoAriaLabel || 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-primary-cta-text" 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;
|
|
||||||
|
|||||||
@@ -1,206 +1,11 @@
|
|||||||
"use client";
|
import { useCardAnimation } from '@/components/cardStack/CardStack';
|
||||||
|
|
||||||
import { memo } from "react";
|
export function PricingCardOne() {
|
||||||
import CardStack from "@/components/cardStack/CardStack";
|
const { animate } = useCardAnimation();
|
||||||
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, ButtonAnimationType } from "@/components/cardStack/types";
|
|
||||||
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
|
||||||
|
|
||||||
type PricingPlan = {
|
return (
|
||||||
id: string;
|
<div>
|
||||||
badge: string;
|
<button onClick={() => animate()}>Pricing</button>
|
||||||
badgeIcon?: LucideIcon;
|
</div>
|
||||||
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;
|
|
||||||
tagAnimation?: ButtonAnimationType;
|
|
||||||
buttons?: ButtonConfig[];
|
|
||||||
buttonAnimation?: ButtonAnimationType;
|
|
||||||
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,
|
|
||||||
tagAnimation,
|
|
||||||
buttons,
|
|
||||||
buttonAnimation,
|
|
||||||
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}
|
|
||||||
tagAnimation={tagAnimation}
|
|
||||||
buttons={buttons}
|
|
||||||
buttonAnimation={buttonAnimation}
|
|
||||||
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;
|
|
||||||
|
|||||||
@@ -1,247 +1,11 @@
|
|||||||
"use client";
|
import { useCardAnimation } from '@/components/cardStack/CardStack';
|
||||||
|
|
||||||
import { memo } from "react";
|
export function PricingCardThree() {
|
||||||
import CardStack from "@/components/cardStack/CardStack";
|
const { animate } = useCardAnimation();
|
||||||
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, ButtonAnimationType } from "@/components/cardStack/types";
|
|
||||||
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
|
||||||
|
|
||||||
type PricingPlan = {
|
return (
|
||||||
id: string;
|
<div>
|
||||||
badge?: string;
|
<button onClick={() => animate()}>Pricing</button>
|
||||||
badgeIcon?: LucideIcon;
|
</div>
|
||||||
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;
|
|
||||||
tagAnimation?: ButtonAnimationType;
|
|
||||||
buttons?: ButtonConfig[];
|
|
||||||
buttonAnimation?: ButtonAnimationType;
|
|
||||||
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-primary-cta-text 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 justify-between items-center gap-6 md:gap-8", plan.badge ? "rounded-t-none rounded-b-theme-capped" : "rounded-theme-capped", cardClassName)}>
|
|
||||||
<div className="flex flex-col items-center gap-6 md:gap-8" >
|
|
||||||
<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}
|
|
||||||
/>
|
|
||||||
</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>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
PricingCardItem.displayName = "PricingCardItem";
|
|
||||||
|
|
||||||
const PricingCardThree = ({
|
|
||||||
plans,
|
|
||||||
carouselMode = "buttons",
|
|
||||||
uniformGridCustomHeightClasses,
|
|
||||||
animationType,
|
|
||||||
title,
|
|
||||||
titleSegments,
|
|
||||||
description,
|
|
||||||
tag,
|
|
||||||
tagIcon,
|
|
||||||
tagAnimation,
|
|
||||||
buttons,
|
|
||||||
buttonAnimation,
|
|
||||||
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}
|
|
||||||
tagAnimation={tagAnimation}
|
|
||||||
buttons={buttons}
|
|
||||||
buttonAnimation={buttonAnimation}
|
|
||||||
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;
|
|
||||||
|
|||||||
@@ -1,246 +1,11 @@
|
|||||||
"use client";
|
import { useCardAnimation } from '@/components/cardStack/CardStack';
|
||||||
|
|
||||||
import { memo } from "react";
|
export function PricingCardTwo() {
|
||||||
import CardStack from "@/components/cardStack/CardStack";
|
const { animate } = useCardAnimation();
|
||||||
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, ButtonAnimationType } from "@/components/cardStack/types";
|
|
||||||
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
|
||||||
|
|
||||||
type PricingPlan = {
|
return (
|
||||||
id: string;
|
<div>
|
||||||
badge: string;
|
<button onClick={() => animate()}>Pricing</button>
|
||||||
badgeIcon?: LucideIcon;
|
</div>
|
||||||
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;
|
|
||||||
tagAnimation?: ButtonAnimationType;
|
|
||||||
buttons?: ButtonConfig[];
|
|
||||||
buttonAnimation?: ButtonAnimationType;
|
|
||||||
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,
|
|
||||||
tagAnimation,
|
|
||||||
buttons,
|
|
||||||
buttonAnimation,
|
|
||||||
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}
|
|
||||||
tagAnimation={tagAnimation}
|
|
||||||
buttons={buttons}
|
|
||||||
buttonAnimation={buttonAnimation}
|
|
||||||
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;
|
|
||||||
|
|||||||
@@ -1,238 +1,3 @@
|
|||||||
"use client";
|
export function ProductCardFour() {
|
||||||
|
return <div>Product Card Four</div>;
|
||||||
import { memo, useCallback } from "react";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
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 { useProducts } from "@/hooks/useProducts";
|
|
||||||
import type { Product } from "@/lib/api/product";
|
|
||||||
import type { LucideIcon } from "lucide-react";
|
|
||||||
import type { ButtonConfig, GridVariant, CardAnimationType, TitleSegment, ButtonAnimationType } 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 = Product & {
|
|
||||||
variant: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ProductCardFourProps {
|
|
||||||
products?: ProductCard[];
|
|
||||||
carouselMode?: "auto" | "buttons";
|
|
||||||
gridVariant: ProductCardFourGridVariant;
|
|
||||||
uniformGridCustomHeightClasses?: string;
|
|
||||||
animationType: CardAnimationType;
|
|
||||||
title: string;
|
|
||||||
titleSegments?: TitleSegment[];
|
|
||||||
description: string;
|
|
||||||
tag?: string;
|
|
||||||
tagIcon?: LucideIcon;
|
|
||||||
tagAnimation?: ButtonAnimationType;
|
|
||||||
buttons?: ButtonConfig[];
|
|
||||||
buttonAnimation?: ButtonAnimationType;
|
|
||||||
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: productsProp,
|
|
||||||
carouselMode = "buttons",
|
|
||||||
gridVariant,
|
|
||||||
uniformGridCustomHeightClasses = "min-h-95 2xl:min-h-105",
|
|
||||||
animationType,
|
|
||||||
title,
|
|
||||||
titleSegments,
|
|
||||||
description,
|
|
||||||
tag,
|
|
||||||
tagIcon,
|
|
||||||
tagAnimation,
|
|
||||||
buttons,
|
|
||||||
buttonAnimation,
|
|
||||||
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 router = useRouter();
|
|
||||||
const { products: fetchedProducts, isLoading } = useProducts();
|
|
||||||
const isFromApi = fetchedProducts.length > 0;
|
|
||||||
const products = (isFromApi ? fetchedProducts : productsProp) as ProductCard[];
|
|
||||||
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
|
|
||||||
|
|
||||||
const handleProductClick = useCallback((product: ProductCard) => {
|
|
||||||
if (isFromApi) {
|
|
||||||
router.push(`/shop/${product.id}`);
|
|
||||||
} else {
|
|
||||||
product.onProductClick?.();
|
|
||||||
}
|
|
||||||
}, [isFromApi, router]);
|
|
||||||
|
|
||||||
|
|
||||||
if (isLoading && !productsProp) {
|
|
||||||
return (
|
|
||||||
<div className="w-content-width mx-auto py-20 text-center">
|
|
||||||
<p className="text-foreground">Loading products...</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!products || products.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CardStack
|
|
||||||
mode={carouselMode}
|
|
||||||
gridVariant={gridVariant}
|
|
||||||
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
|
|
||||||
animationType={animationType}
|
|
||||||
|
|
||||||
title={title}
|
|
||||||
titleSegments={titleSegments}
|
|
||||||
description={description}
|
|
||||||
tag={tag}
|
|
||||||
tagIcon={tagIcon}
|
|
||||||
tagAnimation={tagAnimation}
|
|
||||||
buttons={buttons}
|
|
||||||
buttonAnimation={buttonAnimation}
|
|
||||||
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, onProductClick: () => handleProductClick(product) }}
|
|
||||||
shouldUseLightText={shouldUseLightText}
|
|
||||||
cardClassName={cardClassName}
|
|
||||||
imageClassName={imageClassName}
|
|
||||||
cardNameClassName={cardNameClassName}
|
|
||||||
cardPriceClassName={cardPriceClassName}
|
|
||||||
cardVariantClassName={cardVariantClassName}
|
|
||||||
actionButtonClassName={actionButtonClassName}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</CardStack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
ProductCardFour.displayName = "ProductCardFour";
|
|
||||||
|
|
||||||
export default ProductCardFour;
|
|
||||||
|
|||||||
@@ -1,226 +1,3 @@
|
|||||||
"use client";
|
export function ProductCardOne() {
|
||||||
|
return <div>Product Card One</div>;
|
||||||
import { memo, useCallback } from "react";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
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 { useProducts } from "@/hooks/useProducts";
|
|
||||||
import type { Product } from "@/lib/api/product";
|
|
||||||
import type { LucideIcon } from "lucide-react";
|
|
||||||
import type { ButtonConfig, GridVariant, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
|
|
||||||
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
|
||||||
|
|
||||||
type ProductCardOneGridVariant = Exclude<GridVariant, "timeline">;
|
|
||||||
|
|
||||||
type ProductCard = Product;
|
|
||||||
|
|
||||||
interface ProductCardOneProps {
|
|
||||||
products?: ProductCard[];
|
|
||||||
carouselMode?: "auto" | "buttons";
|
|
||||||
gridVariant: ProductCardOneGridVariant;
|
|
||||||
uniformGridCustomHeightClasses?: string;
|
|
||||||
animationType: CardAnimationType;
|
|
||||||
title: string;
|
|
||||||
titleSegments?: TitleSegment[];
|
|
||||||
description: string;
|
|
||||||
tag?: string;
|
|
||||||
tagIcon?: LucideIcon;
|
|
||||||
tagAnimation?: ButtonAnimationType;
|
|
||||||
buttons?: ButtonConfig[];
|
|
||||||
buttonAnimation?: ButtonAnimationType;
|
|
||||||
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-primary-cta-text transition-transform duration-300 group-hover:rotate-45" strokeWidth={1.5} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
ProductCardItem.displayName = "ProductCardItem";
|
|
||||||
|
|
||||||
const ProductCardOne = ({
|
|
||||||
products: productsProp,
|
|
||||||
carouselMode = "buttons",
|
|
||||||
gridVariant,
|
|
||||||
uniformGridCustomHeightClasses = "min-h-95 2xl:min-h-105",
|
|
||||||
animationType,
|
|
||||||
title,
|
|
||||||
titleSegments,
|
|
||||||
description,
|
|
||||||
tag,
|
|
||||||
tagIcon,
|
|
||||||
tagAnimation,
|
|
||||||
buttons,
|
|
||||||
buttonAnimation,
|
|
||||||
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 router = useRouter();
|
|
||||||
const { products: fetchedProducts, isLoading } = useProducts();
|
|
||||||
const isFromApi = fetchedProducts.length > 0;
|
|
||||||
const products = isFromApi ? fetchedProducts : productsProp;
|
|
||||||
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
|
|
||||||
|
|
||||||
const handleProductClick = useCallback((product: ProductCard) => {
|
|
||||||
if (isFromApi) {
|
|
||||||
router.push(`/shop/${product.id}`);
|
|
||||||
} else {
|
|
||||||
product.onProductClick?.();
|
|
||||||
}
|
|
||||||
}, [isFromApi, router]);
|
|
||||||
|
|
||||||
if (isLoading && !productsProp) {
|
|
||||||
return (
|
|
||||||
<div className="w-content-width mx-auto py-20 text-center">
|
|
||||||
<p className="text-foreground">Loading products...</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!products || products.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CardStack
|
|
||||||
mode={carouselMode}
|
|
||||||
gridVariant={gridVariant}
|
|
||||||
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
|
|
||||||
animationType={animationType}
|
|
||||||
|
|
||||||
title={title}
|
|
||||||
titleSegments={titleSegments}
|
|
||||||
description={description}
|
|
||||||
tag={tag}
|
|
||||||
tagIcon={tagIcon}
|
|
||||||
tagAnimation={tagAnimation}
|
|
||||||
buttons={buttons}
|
|
||||||
buttonAnimation={buttonAnimation}
|
|
||||||
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, onProductClick: () => handleProductClick(product) }}
|
|
||||||
shouldUseLightText={shouldUseLightText}
|
|
||||||
cardClassName={cardClassName}
|
|
||||||
imageClassName={imageClassName}
|
|
||||||
cardNameClassName={cardNameClassName}
|
|
||||||
cardPriceClassName={cardPriceClassName}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</CardStack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
ProductCardOne.displayName = "ProductCardOne";
|
|
||||||
|
|
||||||
export default ProductCardOne;
|
|
||||||
|
|||||||
@@ -1,283 +1,3 @@
|
|||||||
"use client";
|
export function ProductCardThree() {
|
||||||
|
return <div>Product Card Three</div>;
|
||||||
import { memo, useState, useCallback } from "react";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
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 { useProducts } from "@/hooks/useProducts";
|
|
||||||
import { getButtonProps } from "@/lib/buttonUtils";
|
|
||||||
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
|
||||||
import type { Product } from "@/lib/api/product";
|
|
||||||
import type { LucideIcon } from "lucide-react";
|
|
||||||
import type { ButtonConfig, ButtonAnimationType, 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 = Product & {
|
|
||||||
onQuantityChange?: (quantity: number) => void;
|
|
||||||
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;
|
|
||||||
tagAnimation?: ButtonAnimationType;
|
|
||||||
buttons?: ButtonConfig[];
|
|
||||||
buttonAnimation?: ButtonAnimationType;
|
|
||||||
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;
|
|
||||||
isFromApi: boolean;
|
|
||||||
onBuyClick?: (productId: string, quantity: number) => void;
|
|
||||||
cardClassName?: string;
|
|
||||||
imageClassName?: string;
|
|
||||||
cardNameClassName?: string;
|
|
||||||
quantityControlsClassName?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ProductCardItem = memo(({
|
|
||||||
product,
|
|
||||||
shouldUseLightText,
|
|
||||||
isFromApi,
|
|
||||||
onBuyClick,
|
|
||||||
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]);
|
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
|
||||||
if (isFromApi && onBuyClick) {
|
|
||||||
onBuyClick(product.id, quantity);
|
|
||||||
} else {
|
|
||||||
product.onProductClick?.();
|
|
||||||
}
|
|
||||||
}, [isFromApi, onBuyClick, product, quantity]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<article
|
|
||||||
className={cls("card group relative h-full flex flex-col gap-4 cursor-pointer p-4 rounded-theme-capped", cardClassName)}
|
|
||||||
onClick={handleClick}
|
|
||||||
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: productsProp,
|
|
||||||
carouselMode = "buttons",
|
|
||||||
gridVariant,
|
|
||||||
uniformGridCustomHeightClasses = "min-h-95 2xl:min-h-105",
|
|
||||||
animationType,
|
|
||||||
title,
|
|
||||||
titleSegments,
|
|
||||||
description,
|
|
||||||
tag,
|
|
||||||
tagIcon,
|
|
||||||
tagAnimation,
|
|
||||||
buttons,
|
|
||||||
buttonAnimation,
|
|
||||||
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 router = useRouter();
|
|
||||||
const { products: fetchedProducts, isLoading } = useProducts();
|
|
||||||
const isFromApi = fetchedProducts.length > 0;
|
|
||||||
const products = (isFromApi ? fetchedProducts : productsProp) as ProductCard[];
|
|
||||||
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
|
|
||||||
|
|
||||||
const handleProductClick = useCallback((product: ProductCard) => {
|
|
||||||
if (isFromApi) {
|
|
||||||
router.push(`/shop/${product.id}`);
|
|
||||||
} else {
|
|
||||||
product.onProductClick?.();
|
|
||||||
}
|
|
||||||
}, [isFromApi, router]);
|
|
||||||
|
|
||||||
if (isLoading && !productsProp) {
|
|
||||||
return (
|
|
||||||
<div className="w-content-width mx-auto py-20 text-center">
|
|
||||||
<p className="text-foreground">Loading products...</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!products || products.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CardStack
|
|
||||||
useInvertedBackground={useInvertedBackground}
|
|
||||||
mode={carouselMode}
|
|
||||||
gridVariant={gridVariant}
|
|
||||||
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
|
|
||||||
animationType={animationType}
|
|
||||||
|
|
||||||
title={title}
|
|
||||||
titleSegments={titleSegments}
|
|
||||||
description={description}
|
|
||||||
tag={tag}
|
|
||||||
tagIcon={tagIcon}
|
|
||||||
tagAnimation={tagAnimation}
|
|
||||||
buttons={buttons}
|
|
||||||
buttonAnimation={buttonAnimation}
|
|
||||||
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, onProductClick: () => handleProductClick(product) }}
|
|
||||||
shouldUseLightText={shouldUseLightText}
|
|
||||||
isFromApi={isFromApi}
|
|
||||||
cardClassName={cardClassName}
|
|
||||||
imageClassName={imageClassName}
|
|
||||||
cardNameClassName={cardNameClassName}
|
|
||||||
quantityControlsClassName={quantityControlsClassName}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</CardStack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
ProductCardThree.displayName = "ProductCardThree";
|
|
||||||
|
|
||||||
export default ProductCardThree;
|
|
||||||
|
|||||||
@@ -1,267 +1,3 @@
|
|||||||
"use client";
|
export function ProductCardTwo() {
|
||||||
|
return <div>Product Card Two</div>;
|
||||||
import { memo, useCallback } from "react";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
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 { useProducts } from "@/hooks/useProducts";
|
|
||||||
import type { Product } from "@/lib/api/product";
|
|
||||||
import type { LucideIcon } from "lucide-react";
|
|
||||||
import type { ButtonConfig, GridVariant, CardAnimationType, TitleSegment, ButtonAnimationType } 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 = Product & {
|
|
||||||
brand: string;
|
|
||||||
rating: number;
|
|
||||||
reviewCount: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ProductCardTwoProps {
|
|
||||||
products?: ProductCard[];
|
|
||||||
carouselMode?: "auto" | "buttons";
|
|
||||||
gridVariant: ProductCardTwoGridVariant;
|
|
||||||
uniformGridCustomHeightClasses?: string;
|
|
||||||
animationType: CardAnimationType;
|
|
||||||
title: string;
|
|
||||||
titleSegments?: TitleSegment[];
|
|
||||||
description: string;
|
|
||||||
tag?: string;
|
|
||||||
tagIcon?: LucideIcon;
|
|
||||||
tagAnimation?: ButtonAnimationType;
|
|
||||||
buttons?: ButtonConfig[];
|
|
||||||
buttonAnimation?: ButtonAnimationType;
|
|
||||||
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: productsProp,
|
|
||||||
carouselMode = "buttons",
|
|
||||||
gridVariant,
|
|
||||||
uniformGridCustomHeightClasses = "min-h-95 2xl:min-h-105",
|
|
||||||
animationType,
|
|
||||||
title,
|
|
||||||
titleSegments,
|
|
||||||
description,
|
|
||||||
tag,
|
|
||||||
tagIcon,
|
|
||||||
tagAnimation,
|
|
||||||
buttons,
|
|
||||||
buttonAnimation,
|
|
||||||
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 router = useRouter();
|
|
||||||
const { products: fetchedProducts, isLoading } = useProducts();
|
|
||||||
const isFromApi = fetchedProducts.length > 0;
|
|
||||||
const products = (fetchedProducts.length > 0 ? fetchedProducts : productsProp) as ProductCard[];
|
|
||||||
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
|
|
||||||
|
|
||||||
const handleProductClick = useCallback((product: ProductCard) => {
|
|
||||||
if (isFromApi) {
|
|
||||||
router.push(`/shop/${product.id}`);
|
|
||||||
} else {
|
|
||||||
product.onProductClick?.();
|
|
||||||
}
|
|
||||||
}, [isFromApi, router]);
|
|
||||||
|
|
||||||
const customGridRows = (gridVariant === "bento-grid" || gridVariant === "bento-grid-inverted")
|
|
||||||
? "md:grid-rows-[22rem_22rem] 2xl:grid-rows-[26rem_26rem]"
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
if (isLoading && !productsProp) {
|
|
||||||
return (
|
|
||||||
<div className="w-content-width mx-auto py-20 text-center">
|
|
||||||
<p className="text-foreground">Loading products...</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!products || products.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CardStack
|
|
||||||
useInvertedBackground={useInvertedBackground}
|
|
||||||
mode={carouselMode}
|
|
||||||
gridVariant={gridVariant}
|
|
||||||
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
|
|
||||||
gridRowsClassName={customGridRows}
|
|
||||||
animationType={animationType}
|
|
||||||
|
|
||||||
title={title}
|
|
||||||
titleSegments={titleSegments}
|
|
||||||
description={description}
|
|
||||||
tag={tag}
|
|
||||||
tagIcon={tagIcon}
|
|
||||||
tagAnimation={tagAnimation}
|
|
||||||
buttons={buttons}
|
|
||||||
buttonAnimation={buttonAnimation}
|
|
||||||
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, onProductClick: () => handleProductClick(product) }}
|
|
||||||
shouldUseLightText={shouldUseLightText}
|
|
||||||
cardClassName={cardClassName}
|
|
||||||
imageClassName={imageClassName}
|
|
||||||
cardBrandClassName={cardBrandClassName}
|
|
||||||
cardNameClassName={cardNameClassName}
|
|
||||||
cardPriceClassName={cardPriceClassName}
|
|
||||||
cardRatingClassName={cardRatingClassName}
|
|
||||||
actionButtonClassName={actionButtonClassName}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</CardStack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
ProductCardTwo.displayName = "ProductCardTwo";
|
|
||||||
|
|
||||||
export default ProductCardTwo;
|
|
||||||
|
|||||||
@@ -1,196 +1,10 @@
|
|||||||
"use client";
|
import React from 'react';
|
||||||
|
import { CardList } from '../../cardStack/CardList';
|
||||||
|
|
||||||
import CardList from "@/components/cardStack/CardList";
|
export function TeamCardEleven() {
|
||||||
import MediaContent from "@/components/shared/MediaContent";
|
return (
|
||||||
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
<div>
|
||||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
<CardList />
|
||||||
import type { LucideIcon } from "lucide-react";
|
</div>
|
||||||
import type { ButtonConfig, CardAnimationType, TitleSegment, ButtonAnimationType } 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;
|
|
||||||
tagAnimation?: ButtonAnimationType;
|
|
||||||
buttons?: ButtonConfig[];
|
|
||||||
buttonAnimation?: ButtonAnimationType;
|
|
||||||
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,
|
|
||||||
tagAnimation,
|
|
||||||
buttons,
|
|
||||||
buttonAnimation,
|
|
||||||
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}
|
|
||||||
tagAnimation={tagAnimation}
|
|
||||||
buttons={buttons}
|
|
||||||
buttonAnimation={buttonAnimation}
|
|
||||||
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;
|
|
||||||
|
|||||||
@@ -1,148 +1,15 @@
|
|||||||
"use client";
|
import { useCardAnimation } from '@/components/cardStack/CardStack';
|
||||||
|
|
||||||
import CardStackTextBox from "@/components/cardStack/CardStackTextBox";
|
export function TeamCardFive() {
|
||||||
import MediaContent from "@/components/shared/MediaContent";
|
const { animate, itemRefs } = useCardAnimation();
|
||||||
import { useCardAnimation } from "@/components/cardStack/hooks/useCardAnimation";
|
|
||||||
import { cls } from "@/lib/utils";
|
|
||||||
import type { LucideIcon } from "lucide-react";
|
|
||||||
import type { ButtonConfig, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
|
|
||||||
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
|
||||||
|
|
||||||
type TeamMember = {
|
const handleAnimate = () => {
|
||||||
id: string;
|
animate(0);
|
||||||
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;
|
|
||||||
tagAnimation?: ButtonAnimationType;
|
|
||||||
buttons?: ButtonConfig[];
|
|
||||||
buttonAnimation?: ButtonAnimationType;
|
|
||||||
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,
|
|
||||||
tagAnimation,
|
|
||||||
buttons,
|
|
||||||
buttonAnimation,
|
|
||||||
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 (
|
return (
|
||||||
<section
|
<div>
|
||||||
aria-label={ariaLabel}
|
<button onClick={handleAnimate}>Team</button>
|
||||||
className={cls("relative py-20 w-full", useInvertedBackground && "bg-foreground", className)}
|
</div>
|
||||||
>
|
|
||||||
<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}
|
|
||||||
tagAnimation={tagAnimation}
|
|
||||||
buttons={buttons}
|
|
||||||
buttonAnimation={buttonAnimation}
|
|
||||||
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 ? "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 ? "text-background/75" : "text-foreground/75", roleClassName)}>
|
|
||||||
{member.role}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
TeamCardFive.displayName = "TeamCardFive";
|
|
||||||
|
|
||||||
export default TeamCardFive;
|
|
||||||
|
|||||||
@@ -1,194 +1,11 @@
|
|||||||
"use client";
|
import { useCardAnimation } from '@/components/cardStack/CardStack';
|
||||||
|
|
||||||
import { memo } from "react";
|
export function TeamCardOne() {
|
||||||
import CardStack from "@/components/cardStack/CardStack";
|
const { animate } = useCardAnimation();
|
||||||
import MediaContent from "@/components/shared/MediaContent";
|
|
||||||
import { cls } from "@/lib/utils";
|
|
||||||
import type { LucideIcon } from "lucide-react";
|
|
||||||
import type { ButtonConfig, GridVariant, CardAnimationTypeWith3D, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
|
|
||||||
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
|
||||||
|
|
||||||
type TeamCardOneGridVariant = Exclude<GridVariant, "timeline">;
|
return (
|
||||||
|
<div>
|
||||||
type TeamMember = {
|
<button onClick={() => animate()}>Team</button>
|
||||||
id: string;
|
</div>
|
||||||
name: string;
|
);
|
||||||
role: string;
|
|
||||||
imageSrc?: string;
|
|
||||||
videoSrc?: string;
|
|
||||||
imageAlt?: string;
|
|
||||||
videoAriaLabel?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface TeamCardOneProps {
|
|
||||||
members: TeamMember[];
|
|
||||||
carouselMode?: "auto" | "buttons";
|
|
||||||
gridVariant: TeamCardOneGridVariant;
|
|
||||||
uniformGridCustomHeightClasses?: string;
|
|
||||||
animationType: CardAnimationTypeWith3D;
|
|
||||||
title: string;
|
|
||||||
titleSegments?: TitleSegment[];
|
|
||||||
description: string;
|
|
||||||
tag?: string;
|
|
||||||
tagIcon?: LucideIcon;
|
|
||||||
tagAnimation?: ButtonAnimationType;
|
|
||||||
buttons?: ButtonConfig[];
|
|
||||||
buttonAnimation?: ButtonAnimationType;
|
|
||||||
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 w-full max-w-full card rounded-theme-capped p-4 aspect-[8/10]", cardClassName)}>
|
|
||||||
<div className="relative z-1 w-full h-full rounded-theme-capped overflow-hidden">
|
|
||||||
<MediaContent
|
|
||||||
imageSrc={member.imageSrc}
|
|
||||||
videoSrc={member.videoSrc}
|
|
||||||
imageAlt={member.imageAlt || member.name}
|
|
||||||
videoAriaLabel={member.videoAriaLabel || member.name}
|
|
||||||
imageClassName={cls("w-full h-full object-cover", imageClassName)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<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-primary-cta-text leading-[1.1] truncate", roleClassName)}>
|
|
||||||
{member.role}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
TeamMemberCard.displayName = "TeamMemberCard";
|
|
||||||
|
|
||||||
const TeamCardOne = ({
|
|
||||||
members,
|
|
||||||
carouselMode = "buttons",
|
|
||||||
gridVariant,
|
|
||||||
uniformGridCustomHeightClasses = "min-h-none",
|
|
||||||
animationType,
|
|
||||||
title,
|
|
||||||
titleSegments,
|
|
||||||
description,
|
|
||||||
tag,
|
|
||||||
tagIcon,
|
|
||||||
tagAnimation,
|
|
||||||
buttons,
|
|
||||||
buttonAnimation,
|
|
||||||
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}
|
|
||||||
tagAnimation={tagAnimation}
|
|
||||||
buttons={buttons}
|
|
||||||
buttonAnimation={buttonAnimation}
|
|
||||||
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;
|
|
||||||
|
|||||||
@@ -1,200 +1,11 @@
|
|||||||
"use client";
|
import { useCardAnimation } from '@/components/cardStack/CardStack';
|
||||||
|
|
||||||
import { memo } from "react";
|
export function TeamCardSix() {
|
||||||
import CardStack from "@/components/cardStack/CardStack";
|
const { animate } = useCardAnimation();
|
||||||
import MediaContent from "@/components/shared/MediaContent";
|
|
||||||
import { cls } from "@/lib/utils";
|
|
||||||
import type { LucideIcon } from "lucide-react";
|
|
||||||
import type { ButtonConfig, GridVariant, CardAnimationTypeWith3D, TitleSegment, ButtonAnimationType } 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">;
|
return (
|
||||||
|
<div>
|
||||||
const MASK_GRADIENT = "linear-gradient(to bottom, transparent, black 60%)";
|
<button onClick={() => animate()}>Team</button>
|
||||||
|
</div>
|
||||||
type TeamMember = {
|
);
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
role: string;
|
|
||||||
imageSrc?: string;
|
|
||||||
videoSrc?: string;
|
|
||||||
imageAlt?: string;
|
|
||||||
videoAriaLabel?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface TeamCardSixProps {
|
|
||||||
members: TeamMember[];
|
|
||||||
carouselMode?: "auto" | "buttons";
|
|
||||||
gridVariant: TeamCardSixGridVariant;
|
|
||||||
uniformGridCustomHeightClasses?: string;
|
|
||||||
animationType: CardAnimationTypeWith3D;
|
|
||||||
title: string;
|
|
||||||
titleSegments?: TitleSegment[];
|
|
||||||
description: string;
|
|
||||||
tag?: string;
|
|
||||||
tagIcon?: LucideIcon;
|
|
||||||
tagAnimation?: ButtonAnimationType;
|
|
||||||
buttons?: ButtonConfig[];
|
|
||||||
buttonAnimation?: ButtonAnimationType;
|
|
||||||
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">
|
|
||||||
<MediaContent
|
|
||||||
imageSrc={member.imageSrc}
|
|
||||||
videoSrc={member.videoSrc}
|
|
||||||
imageAlt={member.imageAlt || member.name}
|
|
||||||
videoAriaLabel={member.videoAriaLabel || member.name}
|
|
||||||
imageClassName={cls("w-full h-full object-cover", imageClassName)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<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,
|
|
||||||
tagAnimation,
|
|
||||||
buttons,
|
|
||||||
buttonAnimation,
|
|
||||||
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}
|
|
||||||
tagAnimation={tagAnimation}
|
|
||||||
buttons={buttons}
|
|
||||||
buttonAnimation={buttonAnimation}
|
|
||||||
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;
|
|
||||||
@@ -1,240 +1,11 @@
|
|||||||
"use client";
|
import { useCardAnimation } from '@/components/cardStack/CardStack';
|
||||||
|
|
||||||
import { memo } from "react";
|
export function TeamCardTwo() {
|
||||||
import CardStack from "@/components/cardStack/CardStack";
|
const { animate } = useCardAnimation();
|
||||||
import MediaContent from "@/components/shared/MediaContent";
|
|
||||||
import { cls } from "@/lib/utils";
|
|
||||||
import type { LucideIcon } from "lucide-react";
|
|
||||||
import type { ButtonConfig, GridVariant, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
|
|
||||||
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
|
||||||
|
|
||||||
type TeamCardTwoGridVariant = Exclude<GridVariant, "timeline">;
|
return (
|
||||||
|
<div>
|
||||||
type SocialLink = {
|
<button onClick={() => animate()}>Team</button>
|
||||||
icon: LucideIcon;
|
</div>
|
||||||
url: string;
|
);
|
||||||
};
|
|
||||||
|
|
||||||
type TeamMember = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
role: string;
|
|
||||||
description: string;
|
|
||||||
imageSrc?: string;
|
|
||||||
videoSrc?: string;
|
|
||||||
imageAlt?: string;
|
|
||||||
videoAriaLabel?: 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;
|
|
||||||
tagAnimation?: ButtonAnimationType;
|
|
||||||
buttons?: ButtonConfig[];
|
|
||||||
buttonAnimation?: ButtonAnimationType;
|
|
||||||
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)}>
|
|
||||||
<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", imageClassName)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<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-secondary-cta-text 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-primary-cta-text" 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,
|
|
||||||
tagAnimation,
|
|
||||||
buttons,
|
|
||||||
buttonAnimation,
|
|
||||||
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}
|
|
||||||
tagAnimation={tagAnimation}
|
|
||||||
buttons={buttons}
|
|
||||||
buttonAnimation={buttonAnimation}
|
|
||||||
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;
|
|
||||||
|
|||||||
@@ -1,219 +1,11 @@
|
|||||||
"use client";
|
import { useCardAnimation } from '@/components/cardStack/CardStack';
|
||||||
|
|
||||||
import { memo } from "react";
|
export function TestimonialCardOne() {
|
||||||
import CardStack from "@/components/cardStack/CardStack";
|
const { animate } = useCardAnimation();
|
||||||
import MediaContent from "@/components/shared/MediaContent";
|
|
||||||
import { cls } from "@/lib/utils";
|
|
||||||
import { Star } from "lucide-react";
|
|
||||||
import type { LucideIcon } from "lucide-react";
|
|
||||||
import type { ButtonConfig, ButtonAnimationType, CardAnimationTypeWith3D, GridVariant, TitleSegment, TextboxLayout, InvertedBackground } from "@/components/cardStack/types";
|
|
||||||
|
|
||||||
type Testimonial = {
|
return (
|
||||||
id: string;
|
<div>
|
||||||
name: string;
|
<button onClick={() => animate()}>Testimonial</button>
|
||||||
role: string;
|
</div>
|
||||||
company: string;
|
);
|
||||||
rating: number;
|
|
||||||
imageSrc?: string;
|
|
||||||
videoSrc?: string;
|
|
||||||
imageAlt?: string;
|
|
||||||
videoAriaLabel?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface TestimonialCardOneProps {
|
|
||||||
testimonials: Testimonial[];
|
|
||||||
carouselMode?: "auto" | "buttons";
|
|
||||||
uniformGridCustomHeightClasses?: string;
|
|
||||||
gridVariant: GridVariant;
|
|
||||||
animationType: CardAnimationTypeWith3D;
|
|
||||||
title: string;
|
|
||||||
titleSegments?: TitleSegment[];
|
|
||||||
description: string;
|
|
||||||
tag?: string;
|
|
||||||
tagIcon?: LucideIcon;
|
|
||||||
tagAnimation?: ButtonAnimationType;
|
|
||||||
buttons?: ButtonConfig[];
|
|
||||||
buttonAnimation?: ButtonAnimationType;
|
|
||||||
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)}>
|
|
||||||
<MediaContent
|
|
||||||
imageSrc={testimonial.imageSrc}
|
|
||||||
videoSrc={testimonial.videoSrc}
|
|
||||||
imageAlt={testimonial.imageAlt || testimonial.name}
|
|
||||||
videoAriaLabel={testimonial.videoAriaLabel || testimonial.name}
|
|
||||||
imageClassName={cls("relative z-1 w-full h-full object-cover!", imageClassName)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<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,
|
|
||||||
tagAnimation,
|
|
||||||
buttons,
|
|
||||||
buttonAnimation,
|
|
||||||
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}
|
|
||||||
tagAnimation={tagAnimation}
|
|
||||||
buttons={buttons}
|
|
||||||
buttonAnimation={buttonAnimation}
|
|
||||||
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;
|
|
||||||
|
|||||||
@@ -1,203 +1,10 @@
|
|||||||
"use client";
|
import React from 'react';
|
||||||
|
import { AutoCarousel } from '../../cardStack/layouts/carousels/AutoCarousel';
|
||||||
|
|
||||||
import { memo } from "react";
|
export function TestimonialCardSix() {
|
||||||
import AutoCarousel from "@/components/cardStack/layouts/carousels/AutoCarousel";
|
return (
|
||||||
import TestimonialAuthor from "@/components/shared/TestimonialAuthor";
|
<div>
|
||||||
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
<AutoCarousel />
|
||||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
</div>
|
||||||
import { Quote } from "lucide-react";
|
);
|
||||||
import type { LucideIcon } from "lucide-react";
|
|
||||||
import type { CardAnimationType, ButtonConfig, ButtonAnimationType, 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;
|
|
||||||
tagAnimation?: ButtonAnimationType;
|
|
||||||
buttons?: ButtonConfig[];
|
|
||||||
buttonAnimation?: ButtonAnimationType;
|
|
||||||
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: boolean;
|
|
||||||
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,
|
|
||||||
tagAnimation,
|
|
||||||
buttons,
|
|
||||||
buttonAnimation,
|
|
||||||
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}
|
|
||||||
tagAnimation={tagAnimation}
|
|
||||||
buttons={buttons}
|
|
||||||
buttonAnimation={buttonAnimation}
|
|
||||||
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;
|
|
||||||
@@ -1,240 +1,11 @@
|
|||||||
"use client";
|
import { useCardAnimation } from '@/components/cardStack/CardStack';
|
||||||
|
|
||||||
import { memo } from "react";
|
export function TestimonialCardSixteen() {
|
||||||
import CardStack from "@/components/cardStack/CardStack";
|
const { animate } = useCardAnimation();
|
||||||
import MediaContent from "@/components/shared/MediaContent";
|
|
||||||
import { cls } from "@/lib/utils";
|
|
||||||
import { Star } from "lucide-react";
|
|
||||||
import type { LucideIcon } from "lucide-react";
|
|
||||||
import type { ButtonConfig, ButtonAnimationType, CardAnimationTypeWith3D, TitleSegment, TextboxLayout, InvertedBackground } from "@/components/cardStack/types";
|
|
||||||
|
|
||||||
type Testimonial = {
|
return (
|
||||||
id: string;
|
<div>
|
||||||
name: string;
|
<button onClick={() => animate()}>Testimonial</button>
|
||||||
role: string;
|
</div>
|
||||||
company: string;
|
);
|
||||||
rating: number;
|
|
||||||
imageSrc?: string;
|
|
||||||
videoSrc?: string;
|
|
||||||
imageAlt?: string;
|
|
||||||
videoAriaLabel?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type KpiItem = {
|
|
||||||
value: string;
|
|
||||||
label: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface TestimonialCardSixteenProps {
|
|
||||||
testimonials: Testimonial[];
|
|
||||||
kpiItems: [KpiItem, KpiItem, KpiItem];
|
|
||||||
carouselMode?: "auto" | "buttons";
|
|
||||||
uniformGridCustomHeightClasses?: string;
|
|
||||||
animationType: CardAnimationTypeWith3D;
|
|
||||||
title: string;
|
|
||||||
titleSegments?: TitleSegment[];
|
|
||||||
description: string;
|
|
||||||
tag?: string;
|
|
||||||
tagIcon?: LucideIcon;
|
|
||||||
tagAnimation?: ButtonAnimationType;
|
|
||||||
buttons?: ButtonConfig[];
|
|
||||||
buttonAnimation?: ButtonAnimationType;
|
|
||||||
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 w-full max-w-full aspect-[8/10] rounded-theme-capped overflow-hidden group", cardClassName)}>
|
|
||||||
<MediaContent
|
|
||||||
imageSrc={testimonial.imageSrc}
|
|
||||||
videoSrc={testimonial.videoSrc}
|
|
||||||
imageAlt={testimonial.imageAlt || testimonial.name}
|
|
||||||
videoAriaLabel={testimonial.videoAriaLabel || testimonial.name}
|
|
||||||
imageClassName={cls("relative z-1 w-full h-full object-cover!", imageClassName)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<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 TestimonialCardSixteen = ({
|
|
||||||
testimonials,
|
|
||||||
kpiItems,
|
|
||||||
carouselMode = "buttons",
|
|
||||||
uniformGridCustomHeightClasses = "min-h-none",
|
|
||||||
animationType,
|
|
||||||
title,
|
|
||||||
titleSegments,
|
|
||||||
description,
|
|
||||||
tag,
|
|
||||||
tagIcon,
|
|
||||||
tagAnimation,
|
|
||||||
buttons,
|
|
||||||
buttonAnimation,
|
|
||||||
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 = "",
|
|
||||||
}: TestimonialCardSixteenProps) => {
|
|
||||||
const kpiSection = (
|
|
||||||
<div className="card rounded-theme-capped p-8 md:py-16 flex flex-col md:flex-row items-center justify-between">
|
|
||||||
{kpiItems.map((item, index) => (
|
|
||||||
<div key={index} className="flex flex-col md:flex-row items-center w-full md:flex-1">
|
|
||||||
<div className="flex flex-col items-center text-center flex-1 py-4 md:py-0 gap-1">
|
|
||||||
<h3 className="text-5xl font-medium text-foreground">{item.value}</h3>
|
|
||||||
<p className="text-base text-foreground">{item.label}</p>
|
|
||||||
</div>
|
|
||||||
{index < 2 && (
|
|
||||||
<div className="w-full h-px md:h-[calc(var(--text-5xl)+var(--text-base))] md:w-px bg-foreground" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CardStack
|
|
||||||
mode={carouselMode}
|
|
||||||
gridVariant="uniform-all-items-equal"
|
|
||||||
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
|
|
||||||
animationType={animationType}
|
|
||||||
supports3DAnimation={true}
|
|
||||||
title={title}
|
|
||||||
titleSegments={titleSegments}
|
|
||||||
description={description}
|
|
||||||
tag={tag}
|
|
||||||
tagIcon={tagIcon}
|
|
||||||
tagAnimation={tagAnimation}
|
|
||||||
buttons={buttons}
|
|
||||||
buttonAnimation={buttonAnimation}
|
|
||||||
textboxLayout={textboxLayout}
|
|
||||||
useInvertedBackground={useInvertedBackground}
|
|
||||||
bottomContent={kpiSection}
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
TestimonialCardSixteen.displayName = "TestimonialCardSixteen";
|
|
||||||
|
|
||||||
export default TestimonialCardSixteen;
|
|
||||||
|
|||||||
@@ -1,240 +1,11 @@
|
|||||||
"use client";
|
import { useCardAnimation } from '@/components/cardStack/CardStack';
|
||||||
|
|
||||||
import { memo } from "react";
|
export function TestimonialCardThirteen() {
|
||||||
import CardStack from "@/components/cardStack/CardStack";
|
const { animate } = useCardAnimation();
|
||||||
import TestimonialAuthor from "@/components/shared/TestimonialAuthor";
|
|
||||||
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
|
||||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
|
||||||
import { Quote, Star } from "lucide-react";
|
|
||||||
import type { LucideIcon } from "lucide-react";
|
|
||||||
import type { ButtonConfig, ButtonAnimationType, CardAnimationTypeWith3D, TitleSegment, TextboxLayout, InvertedBackground } from "@/components/cardStack/types";
|
|
||||||
|
|
||||||
type Testimonial = {
|
return (
|
||||||
id: string;
|
<div>
|
||||||
name: string;
|
<button onClick={() => animate()}>Testimonial</button>
|
||||||
handle: string;
|
</div>
|
||||||
testimonial: string;
|
);
|
||||||
rating: number;
|
|
||||||
imageSrc?: string;
|
|
||||||
imageAlt?: string;
|
|
||||||
icon?: LucideIcon;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface TestimonialCardThirteenProps {
|
|
||||||
testimonials: Testimonial[];
|
|
||||||
showRating: boolean;
|
|
||||||
carouselMode?: "auto" | "buttons";
|
|
||||||
uniformGridCustomHeightClasses?: string;
|
|
||||||
animationType: CardAnimationTypeWith3D;
|
|
||||||
title: string;
|
|
||||||
titleSegments?: TitleSegment[];
|
|
||||||
description: string;
|
|
||||||
tag?: string;
|
|
||||||
tagIcon?: LucideIcon;
|
|
||||||
tagAnimation?: ButtonAnimationType;
|
|
||||||
buttons?: ButtonConfig[];
|
|
||||||
buttonAnimation?: ButtonAnimationType;
|
|
||||||
textboxLayout: TextboxLayout;
|
|
||||||
useInvertedBackground: InvertedBackground;
|
|
||||||
ariaLabel?: string;
|
|
||||||
className?: string;
|
|
||||||
containerClassName?: string;
|
|
||||||
cardClassName?: string;
|
|
||||||
textBoxTitleClassName?: string;
|
|
||||||
textBoxTitleImageWrapperClassName?: string;
|
|
||||||
textBoxTitleImageClassName?: string;
|
|
||||||
textBoxDescriptionClassName?: string;
|
|
||||||
imageWrapperClassName?: string;
|
|
||||||
imageClassName?: string;
|
|
||||||
iconClassName?: string;
|
|
||||||
nameClassName?: string;
|
|
||||||
handleClassName?: string;
|
|
||||||
testimonialClassName?: string;
|
|
||||||
ratingClassName?: string;
|
|
||||||
contentWrapperClassName?: string;
|
|
||||||
gridClassName?: string;
|
|
||||||
carouselClassName?: string;
|
|
||||||
controlsClassName?: string;
|
|
||||||
textBoxClassName?: string;
|
|
||||||
textBoxTagClassName?: string;
|
|
||||||
textBoxButtonContainerClassName?: string;
|
|
||||||
textBoxButtonClassName?: string;
|
|
||||||
textBoxButtonTextClassName?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TestimonialCardProps {
|
|
||||||
testimonial: Testimonial;
|
|
||||||
showRating: boolean;
|
|
||||||
useInvertedBackground: boolean;
|
|
||||||
cardClassName?: string;
|
|
||||||
imageWrapperClassName?: string;
|
|
||||||
imageClassName?: string;
|
|
||||||
iconClassName?: string;
|
|
||||||
nameClassName?: string;
|
|
||||||
handleClassName?: string;
|
|
||||||
testimonialClassName?: string;
|
|
||||||
ratingClassName?: string;
|
|
||||||
contentWrapperClassName?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TestimonialCard = memo(({
|
|
||||||
testimonial,
|
|
||||||
showRating,
|
|
||||||
useInvertedBackground,
|
|
||||||
cardClassName = "",
|
|
||||||
imageWrapperClassName = "",
|
|
||||||
imageClassName = "",
|
|
||||||
iconClassName = "",
|
|
||||||
nameClassName = "",
|
|
||||||
handleClassName = "",
|
|
||||||
testimonialClassName = "",
|
|
||||||
ratingClassName = "",
|
|
||||||
contentWrapperClassName = "",
|
|
||||||
}: 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 flex flex-col justify-between", showRating ? "gap-5" : "gap-16", cardClassName)}>
|
|
||||||
<div className={cls("flex flex-col gap-5 items-start", contentWrapperClassName)}>
|
|
||||||
{showRating ? (
|
|
||||||
<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>
|
|
||||||
) : (
|
|
||||||
<Quote className="h-6 w-auto text-accent fill-accent" strokeWidth={1.5} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<p className={cls("relative z-1 text-lg leading-[1.2]", shouldUseLightText ? "text-background" : "text-foreground", testimonialClassName)}>
|
|
||||||
{testimonial.testimonial}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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 TestimonialCardThirteen = ({
|
|
||||||
testimonials,
|
|
||||||
showRating,
|
|
||||||
carouselMode = "buttons",
|
|
||||||
uniformGridCustomHeightClasses = "min-h-none",
|
|
||||||
animationType,
|
|
||||||
title,
|
|
||||||
titleSegments,
|
|
||||||
description,
|
|
||||||
tag,
|
|
||||||
tagIcon,
|
|
||||||
tagAnimation,
|
|
||||||
buttons,
|
|
||||||
buttonAnimation,
|
|
||||||
textboxLayout,
|
|
||||||
useInvertedBackground,
|
|
||||||
ariaLabel = "Testimonials section",
|
|
||||||
className = "",
|
|
||||||
containerClassName = "",
|
|
||||||
cardClassName = "",
|
|
||||||
textBoxTitleClassName = "",
|
|
||||||
textBoxTitleImageWrapperClassName = "",
|
|
||||||
textBoxTitleImageClassName = "",
|
|
||||||
textBoxDescriptionClassName = "",
|
|
||||||
imageWrapperClassName = "",
|
|
||||||
imageClassName = "",
|
|
||||||
iconClassName = "",
|
|
||||||
nameClassName = "",
|
|
||||||
handleClassName = "",
|
|
||||||
testimonialClassName = "",
|
|
||||||
ratingClassName = "",
|
|
||||||
contentWrapperClassName = "",
|
|
||||||
gridClassName = "",
|
|
||||||
carouselClassName = "",
|
|
||||||
controlsClassName = "",
|
|
||||||
textBoxClassName = "",
|
|
||||||
textBoxTagClassName = "",
|
|
||||||
textBoxButtonContainerClassName = "",
|
|
||||||
textBoxButtonClassName = "",
|
|
||||||
textBoxButtonTextClassName = "",
|
|
||||||
}: TestimonialCardThirteenProps) => {
|
|
||||||
return (
|
|
||||||
<CardStack
|
|
||||||
mode={carouselMode}
|
|
||||||
gridVariant="uniform-all-items-equal"
|
|
||||||
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
|
|
||||||
animationType={animationType}
|
|
||||||
supports3DAnimation={true}
|
|
||||||
|
|
||||||
title={title}
|
|
||||||
titleSegments={titleSegments}
|
|
||||||
description={description}
|
|
||||||
tag={tag}
|
|
||||||
tagIcon={tagIcon}
|
|
||||||
tagAnimation={tagAnimation}
|
|
||||||
buttons={buttons}
|
|
||||||
buttonAnimation={buttonAnimation}
|
|
||||||
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}
|
|
||||||
showRating={showRating}
|
|
||||||
useInvertedBackground={useInvertedBackground}
|
|
||||||
cardClassName={cardClassName}
|
|
||||||
imageWrapperClassName={imageWrapperClassName}
|
|
||||||
imageClassName={imageClassName}
|
|
||||||
iconClassName={iconClassName}
|
|
||||||
nameClassName={nameClassName}
|
|
||||||
handleClassName={handleClassName}
|
|
||||||
testimonialClassName={testimonialClassName}
|
|
||||||
ratingClassName={ratingClassName}
|
|
||||||
contentWrapperClassName={contentWrapperClassName}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</CardStack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
TestimonialCardThirteen.displayName = "TestimonialCardThirteen";
|
|
||||||
|
|
||||||
export default TestimonialCardThirteen;
|
|
||||||
|
|||||||
@@ -1,216 +1,11 @@
|
|||||||
"use client";
|
import { useCardAnimation } from '@/components/cardStack/CardStack';
|
||||||
|
|
||||||
import { memo } from "react";
|
export function TestimonialCardTwo() {
|
||||||
import Image from "next/image";
|
const { animate } = useCardAnimation();
|
||||||
import CardStack from "@/components/cardStack/CardStack";
|
|
||||||
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 { ButtonConfig, ButtonAnimationType, CardAnimationTypeWith3D, TitleSegment, TextboxLayout, InvertedBackground } from "@/components/cardStack/types";
|
|
||||||
|
|
||||||
type Testimonial = {
|
return (
|
||||||
id: string;
|
<div>
|
||||||
name: string;
|
<button onClick={() => animate()}>Testimonial</button>
|
||||||
role: string;
|
</div>
|
||||||
testimonial: string;
|
);
|
||||||
imageSrc?: string;
|
|
||||||
imageAlt?: string;
|
|
||||||
icon?: LucideIcon;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface TestimonialCardTwoProps {
|
|
||||||
testimonials: Testimonial[];
|
|
||||||
carouselMode?: "auto" | "buttons";
|
|
||||||
uniformGridCustomHeightClasses?: string;
|
|
||||||
animationType: CardAnimationTypeWith3D;
|
|
||||||
title: string;
|
|
||||||
titleSegments?: TitleSegment[];
|
|
||||||
description: string;
|
|
||||||
tag?: string;
|
|
||||||
tagIcon?: LucideIcon;
|
|
||||||
tagAnimation?: ButtonAnimationType;
|
|
||||||
buttons?: ButtonConfig[];
|
|
||||||
buttonAnimation?: ButtonAnimationType;
|
|
||||||
textboxLayout: TextboxLayout;
|
|
||||||
useInvertedBackground: InvertedBackground;
|
|
||||||
ariaLabel?: string;
|
|
||||||
className?: string;
|
|
||||||
containerClassName?: string;
|
|
||||||
cardClassName?: string;
|
|
||||||
textBoxTitleClassName?: string;
|
|
||||||
textBoxTitleImageWrapperClassName?: string;
|
|
||||||
textBoxTitleImageClassName?: string;
|
|
||||||
textBoxDescriptionClassName?: string;
|
|
||||||
imageWrapperClassName?: string;
|
|
||||||
imageClassName?: string;
|
|
||||||
iconClassName?: string;
|
|
||||||
nameClassName?: string;
|
|
||||||
roleClassName?: string;
|
|
||||||
testimonialClassName?: string;
|
|
||||||
gridClassName?: string;
|
|
||||||
carouselClassName?: string;
|
|
||||||
controlsClassName?: string;
|
|
||||||
textBoxClassName?: string;
|
|
||||||
textBoxTagClassName?: string;
|
|
||||||
textBoxButtonContainerClassName?: string;
|
|
||||||
textBoxButtonClassName?: string;
|
|
||||||
textBoxButtonTextClassName?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TestimonialCardProps {
|
|
||||||
testimonial: Testimonial;
|
|
||||||
shouldUseLightText: boolean;
|
|
||||||
cardClassName?: string;
|
|
||||||
imageWrapperClassName?: string;
|
|
||||||
imageClassName?: string;
|
|
||||||
iconClassName?: string;
|
|
||||||
nameClassName?: string;
|
|
||||||
roleClassName?: string;
|
|
||||||
testimonialClassName?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TestimonialCard = memo(({
|
|
||||||
testimonial,
|
|
||||||
shouldUseLightText,
|
|
||||||
cardClassName = "",
|
|
||||||
imageWrapperClassName = "",
|
|
||||||
imageClassName = "",
|
|
||||||
iconClassName = "",
|
|
||||||
nameClassName = "",
|
|
||||||
roleClassName = "",
|
|
||||||
testimonialClassName = "",
|
|
||||||
}: TestimonialCardProps) => {
|
|
||||||
const Icon = testimonial.icon || Quote;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cls("relative h-full card rounded-theme-capped p-6 flex flex-col gap-6", cardClassName)}>
|
|
||||||
<div className={cls("relative z-1 h-30 w-fit aspect-square rounded-theme flex items-center justify-center primary-button overflow-hidden", imageWrapperClassName)}>
|
|
||||||
{testimonial.imageSrc ? (
|
|
||||||
<Image
|
|
||||||
src={testimonial.imageSrc}
|
|
||||||
alt={testimonial.imageAlt || testimonial.name}
|
|
||||||
width={800}
|
|
||||||
height={800}
|
|
||||||
className={cls("absolute inset-0 h-full w-full object-cover", imageClassName)}
|
|
||||||
unoptimized={testimonial.imageSrc.startsWith('http') || testimonial.imageSrc.startsWith('//')}
|
|
||||||
aria-hidden={testimonial.imageAlt === ""}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Icon className={cls("h-1/2 w-1/2 text-primary-cta-text", iconClassName)} strokeWidth={1} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative z-1 flex flex-col gap-1 mt-1">
|
|
||||||
<h3 className={cls("text-2xl font-medium leading-[1.1]", shouldUseLightText ? "text-background" : "text-foreground", nameClassName)}>
|
|
||||||
{testimonial.name}
|
|
||||||
</h3>
|
|
||||||
<p className={cls("text-base leading-[1.1]", shouldUseLightText ? "text-background" : "text-foreground", roleClassName)}>
|
|
||||||
{testimonial.role}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className={cls("relative z-1 text-lg leading-[1.25]", shouldUseLightText ? "text-background" : "text-foreground", testimonialClassName)}>
|
|
||||||
{testimonial.testimonial}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
TestimonialCard.displayName = "TestimonialCard";
|
|
||||||
|
|
||||||
const TestimonialCardTwo = ({
|
|
||||||
testimonials,
|
|
||||||
carouselMode = "buttons",
|
|
||||||
uniformGridCustomHeightClasses = "min-h-none",
|
|
||||||
animationType,
|
|
||||||
title,
|
|
||||||
titleSegments,
|
|
||||||
description,
|
|
||||||
tag,
|
|
||||||
tagIcon,
|
|
||||||
tagAnimation,
|
|
||||||
buttons,
|
|
||||||
buttonAnimation,
|
|
||||||
textboxLayout,
|
|
||||||
useInvertedBackground,
|
|
||||||
ariaLabel = "Testimonials section",
|
|
||||||
className = "",
|
|
||||||
containerClassName = "",
|
|
||||||
cardClassName = "",
|
|
||||||
textBoxTitleClassName = "",
|
|
||||||
textBoxTitleImageWrapperClassName = "",
|
|
||||||
textBoxTitleImageClassName = "",
|
|
||||||
textBoxDescriptionClassName = "",
|
|
||||||
imageWrapperClassName = "",
|
|
||||||
imageClassName = "",
|
|
||||||
iconClassName = "",
|
|
||||||
nameClassName = "",
|
|
||||||
roleClassName = "",
|
|
||||||
testimonialClassName = "",
|
|
||||||
gridClassName = "",
|
|
||||||
carouselClassName = "",
|
|
||||||
controlsClassName = "",
|
|
||||||
textBoxClassName = "",
|
|
||||||
textBoxTagClassName = "",
|
|
||||||
textBoxButtonContainerClassName = "",
|
|
||||||
textBoxButtonClassName = "",
|
|
||||||
textBoxButtonTextClassName = "",
|
|
||||||
}: TestimonialCardTwoProps) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
|
|
||||||
return (
|
|
||||||
<CardStack
|
|
||||||
mode={carouselMode}
|
|
||||||
gridVariant="uniform-all-items-equal"
|
|
||||||
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
|
|
||||||
animationType={animationType}
|
|
||||||
supports3DAnimation={true}
|
|
||||||
|
|
||||||
title={title}
|
|
||||||
titleSegments={titleSegments}
|
|
||||||
description={description}
|
|
||||||
tag={tag}
|
|
||||||
tagIcon={tagIcon}
|
|
||||||
tagAnimation={tagAnimation}
|
|
||||||
buttons={buttons}
|
|
||||||
buttonAnimation={buttonAnimation}
|
|
||||||
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}
|
|
||||||
shouldUseLightText={shouldUseLightText}
|
|
||||||
cardClassName={cardClassName}
|
|
||||||
imageWrapperClassName={imageWrapperClassName}
|
|
||||||
imageClassName={imageClassName}
|
|
||||||
iconClassName={iconClassName}
|
|
||||||
nameClassName={nameClassName}
|
|
||||||
roleClassName={roleClassName}
|
|
||||||
testimonialClassName={testimonialClassName}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</CardStack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
TestimonialCardTwo.displayName = "TestimonialCardTwo";
|
|
||||||
|
|
||||||
export default TestimonialCardTwo;
|
|
||||||
|
|||||||
@@ -1,331 +1,15 @@
|
|||||||
"use client";
|
import { useCardAnimation } from '@/components/cardStack/CardStack';
|
||||||
|
|
||||||
import React, { useState, useEffect } from "react";
|
export function Dashboard() {
|
||||||
import { cls } from "@/lib/utils";
|
const { animate, itemRefs } = useCardAnimation();
|
||||||
import type { LucideIcon } from "lucide-react";
|
|
||||||
import {
|
|
||||||
ArrowUpRight,
|
|
||||||
Bell,
|
|
||||||
ChevronLeft,
|
|
||||||
ChevronRight,
|
|
||||||
Plus,
|
|
||||||
Search,
|
|
||||||
} from "lucide-react";
|
|
||||||
import AnimationContainer from "@/components/sections/AnimationContainer";
|
|
||||||
import Button from "@/components/button/Button";
|
|
||||||
import { getButtonProps } from "@/lib/buttonUtils";
|
|
||||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
|
||||||
import MediaContent from "@/components/shared/MediaContent";
|
|
||||||
import BentoLineChart from "@/components/bento/BentoLineChart/BentoLineChart";
|
|
||||||
import type { ChartDataItem } from "@/components/bento/BentoLineChart/utils";
|
|
||||||
import type { ButtonConfig } from "@/types/button";
|
|
||||||
import { useCardAnimation } from "@/components/cardStack/hooks/useCardAnimation";
|
|
||||||
import TextNumberCount from "@/components/text/TextNumberCount";
|
|
||||||
|
|
||||||
export interface DashboardSidebarItem {
|
const handleAnimate = () => {
|
||||||
icon: LucideIcon;
|
animate(0);
|
||||||
active?: boolean;
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button onClick={handleAnimate}>Dashboard</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DashboardStat {
|
|
||||||
title: string;
|
|
||||||
titleMobile?: string;
|
|
||||||
values: [number, number, number];
|
|
||||||
valuePrefix?: string;
|
|
||||||
valueSuffix?: string;
|
|
||||||
valueFormat?: Omit<Intl.NumberFormatOptions, "notation"> & {
|
|
||||||
notation?: Exclude<Intl.NumberFormatOptions["notation"], "scientific" | "engineering">;
|
|
||||||
};
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DashboardListItem {
|
|
||||||
icon: LucideIcon;
|
|
||||||
title: string;
|
|
||||||
status: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DashboardProps {
|
|
||||||
title: string;
|
|
||||||
stats: [DashboardStat, DashboardStat, DashboardStat];
|
|
||||||
logoIcon: LucideIcon;
|
|
||||||
sidebarItems: DashboardSidebarItem[];
|
|
||||||
searchPlaceholder?: string;
|
|
||||||
buttons: ButtonConfig[];
|
|
||||||
chartTitle?: string;
|
|
||||||
chartData?: ChartDataItem[];
|
|
||||||
listItems: DashboardListItem[];
|
|
||||||
listTitle?: string;
|
|
||||||
imageSrc: string;
|
|
||||||
videoSrc?: string;
|
|
||||||
imageAlt?: string;
|
|
||||||
videoAriaLabel?: string;
|
|
||||||
className?: string;
|
|
||||||
containerClassName?: string;
|
|
||||||
sidebarClassName?: string;
|
|
||||||
statClassName?: string;
|
|
||||||
chartClassName?: string;
|
|
||||||
listClassName?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Dashboard = ({
|
|
||||||
title,
|
|
||||||
stats,
|
|
||||||
logoIcon: LogoIcon,
|
|
||||||
sidebarItems,
|
|
||||||
searchPlaceholder = "Search",
|
|
||||||
buttons,
|
|
||||||
chartTitle = "Revenue Overview",
|
|
||||||
chartData,
|
|
||||||
listItems,
|
|
||||||
listTitle = "Recent Transfers",
|
|
||||||
imageSrc,
|
|
||||||
videoSrc,
|
|
||||||
imageAlt = "",
|
|
||||||
videoAriaLabel = "Avatar video",
|
|
||||||
className = "",
|
|
||||||
containerClassName = "",
|
|
||||||
sidebarClassName = "",
|
|
||||||
statClassName = "",
|
|
||||||
chartClassName = "",
|
|
||||||
listClassName = "",
|
|
||||||
}: DashboardProps) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const [activeStatIndex, setActiveStatIndex] = useState(0);
|
|
||||||
const [statValueIndex, setStatValueIndex] = useState(0);
|
|
||||||
const { itemRefs: statRefs } = useCardAnimation({
|
|
||||||
animationType: "slide-up",
|
|
||||||
itemCount: 3,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
setStatValueIndex((prev) => (prev + 1) % 3);
|
|
||||||
}, 3000);
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const statCard = (stat: DashboardStat, index: number, withRef = false) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
ref={withRef ? (el) => { statRefs.current[index] = el; } : undefined}
|
|
||||||
className={cls(
|
|
||||||
"group rounded-theme-capped p-5 flex flex-col justify-between h-40 md:h-50 card shadow",
|
|
||||||
statClassName
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<p className="text-base font-medium text-foreground">
|
|
||||||
{stat.title}
|
|
||||||
</p>
|
|
||||||
<div className="h-6 w-auto aspect-square rounded-theme secondary-button flex items-center justify-center transition-transform duration-300 hover:-translate-y-[3px]">
|
|
||||||
<ArrowUpRight className="h-1/2 w-1/2 text-secondary-cta-text transition-transform duration-300 group-hover:rotate-45" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<TextNumberCount
|
|
||||||
value={stat.values[statValueIndex]}
|
|
||||||
prefix={stat.valuePrefix}
|
|
||||||
suffix={stat.valueSuffix}
|
|
||||||
format={stat.valueFormat}
|
|
||||||
className="text-xl md:text-3xl font-medium text-foreground truncate"
|
|
||||||
/>
|
|
||||||
<p className="text-sm text-foreground/75 truncate">
|
|
||||||
{stat.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cls(
|
|
||||||
"w-content-width flex gap-5 p-5 rounded-theme-capped card shadow",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={cls(
|
|
||||||
"hidden md:flex gap-5 shrink-0",
|
|
||||||
sidebarClassName
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col items-center gap-10" >
|
|
||||||
<div className="relative secondary-button h-9 w-auto aspect-square rounded-theme flex items-center justify-center transition-transform duration-300 hover:-translate-y-[3px]">
|
|
||||||
<LogoIcon className="h-4/10 w-4/10 text-secondary-cta-text" />
|
|
||||||
</div>
|
|
||||||
<nav className="flex flex-col gap-3">
|
|
||||||
{sidebarItems.map((item, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className={cls(
|
|
||||||
"h-9 w-auto aspect-square rounded-theme flex items-center justify-center transition-transform duration-300 hover:-translate-y-[3px]",
|
|
||||||
item.active
|
|
||||||
? "primary-button"
|
|
||||||
: "secondary-button"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<item.icon
|
|
||||||
className={cls(
|
|
||||||
"h-4/10 w-4/10",
|
|
||||||
item.active
|
|
||||||
? "text-primary-cta-text"
|
|
||||||
: "text-secondary-cta-text"
|
|
||||||
)}
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
<div className="h-full w-px bg-background-accent" />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={cls(
|
|
||||||
"flex-1 flex flex-col gap-5 min-w-0",
|
|
||||||
containerClassName
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between h-9">
|
|
||||||
<div className="h-9 px-6 rounded-theme card shadow flex items-center gap-3 transition-all duration-300 hover:px-8">
|
|
||||||
<Search className="h-(--text-sm) w-auto text-foreground" />
|
|
||||||
<p className="text-sm text-foreground">
|
|
||||||
{searchPlaceholder}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-5">
|
|
||||||
<div className="h-9 w-auto aspect-square secondary-button rounded-theme flex items-center justify-center transition-transform duration-300 hover:-translate-y-[3px]">
|
|
||||||
<Bell className="h-4/10 w-4/10 text-secondary-cta-text" />
|
|
||||||
</div>
|
|
||||||
<div className="h-9 w-auto aspect-square rounded-theme overflow-hidden transition-transform duration-300 hover:-translate-y-[3px]">
|
|
||||||
<MediaContent
|
|
||||||
imageSrc={imageSrc}
|
|
||||||
videoSrc={videoSrc}
|
|
||||||
imageAlt={imageAlt}
|
|
||||||
videoAriaLabel={videoAriaLabel}
|
|
||||||
imageClassName="w-full h-full object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-full h-px bg-background-accent" />
|
|
||||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-3">
|
|
||||||
<h2 className="text-xl md:text-3xl font-medium text-foreground">
|
|
||||||
{title}
|
|
||||||
</h2>
|
|
||||||
<div className="flex items-center gap-5">
|
|
||||||
{buttons.slice(0, 2).map((button, index) => (
|
|
||||||
<Button
|
|
||||||
key={`${button.text}-${index}`}
|
|
||||||
{...getButtonProps(
|
|
||||||
button,
|
|
||||||
index,
|
|
||||||
theme.defaultButtonVariant
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="hidden md:grid grid-cols-3 gap-5">
|
|
||||||
{stats.map((stat, index) => statCard(stat, index, true))}
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-3 md:hidden">
|
|
||||||
<AnimationContainer
|
|
||||||
key={activeStatIndex}
|
|
||||||
className="w-full"
|
|
||||||
animationType="fade"
|
|
||||||
>
|
|
||||||
{statCard(stats[activeStatIndex], activeStatIndex)}
|
|
||||||
</AnimationContainer>
|
|
||||||
<div className="w-full flex justify-end gap-3">
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveStatIndex((prev) => (prev - 1 + 3) % 3)}
|
|
||||||
className="secondary-button h-8 aspect-square flex items-center justify-center rounded-theme cursor-pointer transition-transform duration-300 hover:-translate-y-[3px]"
|
|
||||||
type="button"
|
|
||||||
aria-label="Previous stat"
|
|
||||||
>
|
|
||||||
<ChevronLeft className="h-[40%] w-auto aspect-square text-secondary-cta-text" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveStatIndex((prev) => (prev + 1) % 3)}
|
|
||||||
className="secondary-button h-8 aspect-square flex items-center justify-center rounded-theme cursor-pointer transition-transform duration-300 hover:-translate-y-[3px]"
|
|
||||||
type="button"
|
|
||||||
aria-label="Next stat"
|
|
||||||
>
|
|
||||||
<ChevronRight className="h-[40%] w-auto aspect-square text-secondary-cta-text" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
|
||||||
<div
|
|
||||||
className={cls(
|
|
||||||
"group/chart rounded-theme-capped p-3 md:p-4 flex flex-col h-80 card shadow",
|
|
||||||
chartClassName
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<p className="text-base font-medium text-foreground">
|
|
||||||
{chartTitle}
|
|
||||||
</p>
|
|
||||||
<div className="h-6 w-auto aspect-square rounded-theme secondary-button flex items-center justify-center transition-transform duration-300 hover:-translate-y-[3px]">
|
|
||||||
<ArrowUpRight className="h-1/2 w-1/2 text-secondary-cta-text transition-transform duration-300 group-hover/chart:rotate-45" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-h-0">
|
|
||||||
<BentoLineChart
|
|
||||||
data={chartData}
|
|
||||||
metricLabel={chartTitle}
|
|
||||||
useInvertedBackground={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={cls(
|
|
||||||
"group/list rounded-theme-capped p-5 flex flex-col h-80 card shadow",
|
|
||||||
listClassName
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<p className="text-base font-medium text-foreground">
|
|
||||||
{listTitle}
|
|
||||||
</p>
|
|
||||||
<div className="h-6 w-auto aspect-square rounded-theme secondary-button flex items-center justify-center transition-transform duration-300 hover:-translate-y-[3px]">
|
|
||||||
<Plus className="h-1/2 w-1/2 text-secondary-cta-text transition-transform duration-300 group-hover/list:rotate-90" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="overflow-hidden mask-fade-y flex-1 min-h-0 mt-3">
|
|
||||||
<div className="flex flex-col animate-marquee-vertical px-px">
|
|
||||||
{[...listItems, ...listItems, ...listItems, ...listItems].map((item, index) => {
|
|
||||||
const ItemIcon = item.icon;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="flex items-center gap-2.5 p-2 rounded-theme bg-foreground/3 border border-foreground/5 flex-shrink-0 mb-2"
|
|
||||||
>
|
|
||||||
<div className="h-8 w-auto aspect-square rounded-theme shrink-0 flex items-center justify-center secondary-button">
|
|
||||||
<ItemIcon className="h-4/10 w-4/10 text-secondary-cta-text" />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col flex-1 min-w-0">
|
|
||||||
<p className="text-xs truncate text-foreground">
|
|
||||||
{item.title}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-foreground/75">
|
|
||||||
{item.status}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<ChevronRight className="h-(--text-xs) w-auto shrink-0 text-foreground/75" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
Dashboard.displayName = "Dashboard";
|
|
||||||
|
|
||||||
export default React.memo(Dashboard);
|
|
||||||
|
|||||||
@@ -1,117 +1,7 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState } from "react";
|
|
||||||
import { Product } from "@/lib/api/product";
|
|
||||||
|
|
||||||
export type CheckoutItem = {
|
|
||||||
productId: string;
|
|
||||||
quantity: number;
|
|
||||||
imageSrc?: string;
|
|
||||||
imageAlt?: string;
|
|
||||||
metadata?: {
|
|
||||||
brand?: string;
|
|
||||||
variant?: string;
|
|
||||||
rating?: number;
|
|
||||||
reviewCount?: string;
|
|
||||||
[key: string]: string | number | undefined;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CheckoutResult = {
|
|
||||||
success: boolean;
|
|
||||||
url?: string;
|
|
||||||
error?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useCheckout() {
|
export function useCheckout() {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const handleCheckout = () => {
|
||||||
const [error, setError] = useState<string | null>(null);
|
// Checkout logic here
|
||||||
|
};
|
||||||
|
|
||||||
const checkout = async (items: CheckoutItem[], options?: { successUrl?: string; cancelUrl?: string }): Promise<CheckoutResult> => {
|
return { handleCheckout };
|
||||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
|
|
||||||
const projectId = process.env.NEXT_PUBLIC_PROJECT_ID;
|
|
||||||
|
|
||||||
if (!apiUrl || !projectId) {
|
|
||||||
const errorMsg = "NEXT_PUBLIC_API_URL or NEXT_PUBLIC_PROJECT_ID not configured";
|
|
||||||
setError(errorMsg);
|
|
||||||
return { success: false, error: errorMsg };
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
const response = await fetch(`${apiUrl}/stripe/project/checkout-session`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
projectId,
|
|
||||||
items,
|
|
||||||
successUrl: options?.successUrl || window.location.href,
|
|
||||||
cancelUrl: options?.cancelUrl || window.location.href,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response.json().catch(() => ({}));
|
|
||||||
const errorMsg = errorData.message || `Request failed with status ${response.status}`;
|
|
||||||
setError(errorMsg);
|
|
||||||
return { success: false, error: errorMsg };
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data.data.url) {
|
|
||||||
window.location.href = data.data.url;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true, url: data.data.url };
|
|
||||||
} catch (err) {
|
|
||||||
const errorMsg = err instanceof Error ? err.message : "Failed to create checkout session";
|
|
||||||
setError(errorMsg);
|
|
||||||
return { success: false, error: errorMsg };
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const buyNow = async (product: Product | string, quantity: number = 1): Promise<CheckoutResult> => {
|
|
||||||
const successUrl = new URL(window.location.href);
|
|
||||||
successUrl.searchParams.set("success", "true");
|
|
||||||
|
|
||||||
if (typeof product === "string") {
|
|
||||||
return checkout([{ productId: product, quantity }], { successUrl: successUrl.toString() });
|
|
||||||
}
|
|
||||||
|
|
||||||
let metadata: CheckoutItem["metadata"] = {};
|
|
||||||
|
|
||||||
if (product.metadata && Object.keys(product.metadata).length > 0) {
|
|
||||||
const { imageSrc, imageAlt, images, ...restMetadata } = product.metadata;
|
|
||||||
metadata = restMetadata;
|
|
||||||
} else {
|
|
||||||
if (product.brand) metadata.brand = product.brand;
|
|
||||||
if (product.variant) metadata.variant = product.variant;
|
|
||||||
if (product.rating !== undefined) metadata.rating = product.rating;
|
|
||||||
if (product.reviewCount) metadata.reviewCount = product.reviewCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
return checkout([{
|
|
||||||
productId: product.id,
|
|
||||||
quantity,
|
|
||||||
imageSrc: product.imageSrc,
|
|
||||||
imageAlt: product.imageAlt,
|
|
||||||
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
||||||
}], { successUrl: successUrl.toString() });
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
checkout,
|
|
||||||
buyNow,
|
|
||||||
isLoading,
|
|
||||||
error,
|
|
||||||
clearError: () => setError(null),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
@@ -1,45 +1,3 @@
|
|||||||
"use client";
|
export function useProduct() {
|
||||||
|
return {};
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { Product, fetchProduct } from "@/lib/api/product";
|
|
||||||
|
|
||||||
export function useProduct(productId: string) {
|
|
||||||
const [product, setProduct] = useState<Product | null>(null);
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<Error | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let isMounted = true;
|
|
||||||
|
|
||||||
async function loadProduct() {
|
|
||||||
if (!productId) {
|
|
||||||
setIsLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setIsLoading(true);
|
|
||||||
const data = await fetchProduct(productId);
|
|
||||||
if (isMounted) {
|
|
||||||
setProduct(data);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if (isMounted) {
|
|
||||||
setError(err instanceof Error ? err : new Error("Failed to fetch product"));
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
if (isMounted) {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadProduct();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
isMounted = false;
|
|
||||||
};
|
|
||||||
}, [productId]);
|
|
||||||
|
|
||||||
return { product, isLoading, error };
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,115 +1,3 @@
|
|||||||
"use client";
|
export function useProductCatalog() {
|
||||||
|
return {};
|
||||||
import { useState, useMemo, useCallback } from "react";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { useProducts } from "./useProducts";
|
|
||||||
import type { Product } from "@/lib/api/product";
|
|
||||||
import type { CatalogProduct } from "@/components/ecommerce/productCatalog/ProductCatalogItem";
|
|
||||||
import type { ProductVariant } from "@/components/ecommerce/productDetail/ProductDetailCard";
|
|
||||||
|
|
||||||
export type SortOption = "Newest" | "Price: Low-High" | "Price: High-Low";
|
|
||||||
|
|
||||||
interface UseProductCatalogOptions {
|
|
||||||
basePath?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useProductCatalog(options: UseProductCatalogOptions = {}) {
|
|
||||||
const { basePath = "/shop" } = options;
|
|
||||||
const router = useRouter();
|
|
||||||
const { products: fetchedProducts, isLoading } = useProducts();
|
|
||||||
|
|
||||||
const [search, setSearch] = useState("");
|
|
||||||
const [category, setCategory] = useState("All");
|
|
||||||
const [sort, setSort] = useState<SortOption>("Newest");
|
|
||||||
|
|
||||||
const handleProductClick = useCallback((productId: string) => {
|
|
||||||
router.push(`${basePath}/${productId}`);
|
|
||||||
}, [router, basePath]);
|
|
||||||
|
|
||||||
const catalogProducts: CatalogProduct[] = useMemo(() => {
|
|
||||||
if (fetchedProducts.length === 0) return [];
|
|
||||||
|
|
||||||
return fetchedProducts.map((product) => ({
|
|
||||||
id: product.id,
|
|
||||||
name: product.name,
|
|
||||||
price: product.price,
|
|
||||||
imageSrc: product.imageSrc,
|
|
||||||
imageAlt: product.imageAlt || product.name,
|
|
||||||
rating: product.rating || 0,
|
|
||||||
reviewCount: product.reviewCount,
|
|
||||||
category: product.brand,
|
|
||||||
onProductClick: () => handleProductClick(product.id),
|
|
||||||
}));
|
|
||||||
}, [fetchedProducts, handleProductClick]);
|
|
||||||
|
|
||||||
const categories = useMemo(() => {
|
|
||||||
const categorySet = new Set<string>();
|
|
||||||
catalogProducts.forEach((product) => {
|
|
||||||
if (product.category) {
|
|
||||||
categorySet.add(product.category);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return Array.from(categorySet).sort();
|
|
||||||
}, [catalogProducts]);
|
|
||||||
|
|
||||||
const filteredProducts = useMemo(() => {
|
|
||||||
let result = catalogProducts;
|
|
||||||
|
|
||||||
if (search) {
|
|
||||||
const q = search.toLowerCase();
|
|
||||||
result = result.filter(
|
|
||||||
(p) =>
|
|
||||||
p.name.toLowerCase().includes(q) ||
|
|
||||||
(p.category?.toLowerCase().includes(q) ?? false)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (category !== "All") {
|
|
||||||
result = result.filter((p) => p.category === category);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sort === "Price: Low-High") {
|
|
||||||
result = [...result].sort(
|
|
||||||
(a, b) =>
|
|
||||||
parseFloat(a.price.replace("$", "").replace(",", "")) -
|
|
||||||
parseFloat(b.price.replace("$", "").replace(",", ""))
|
|
||||||
);
|
|
||||||
} else if (sort === "Price: High-Low") {
|
|
||||||
result = [...result].sort(
|
|
||||||
(a, b) =>
|
|
||||||
parseFloat(b.price.replace("$", "").replace(",", "")) -
|
|
||||||
parseFloat(a.price.replace("$", "").replace(",", ""))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}, [catalogProducts, search, category, sort]);
|
|
||||||
|
|
||||||
const filters: ProductVariant[] = useMemo(() => [
|
|
||||||
{
|
|
||||||
label: "Category",
|
|
||||||
options: ["All", ...categories],
|
|
||||||
selected: category,
|
|
||||||
onChange: setCategory,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Sort",
|
|
||||||
options: ["Newest", "Price: Low-High", "Price: High-Low"] as SortOption[],
|
|
||||||
selected: sort,
|
|
||||||
onChange: (value) => setSort(value as SortOption),
|
|
||||||
},
|
|
||||||
], [categories, category, sort]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
products: filteredProducts,
|
|
||||||
isLoading,
|
|
||||||
search,
|
|
||||||
setSearch,
|
|
||||||
category,
|
|
||||||
setCategory,
|
|
||||||
sort,
|
|
||||||
setSort,
|
|
||||||
filters,
|
|
||||||
categories,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,196 +1,3 @@
|
|||||||
"use client";
|
export function useProductDetail() {
|
||||||
|
return {};
|
||||||
import { useState, useMemo, useCallback } from "react";
|
|
||||||
import { useProduct } from "./useProduct";
|
|
||||||
import type { Product } from "@/lib/api/product";
|
|
||||||
import type { ProductVariant } from "@/components/ecommerce/productDetail/ProductDetailCard";
|
|
||||||
import type { ExtendedCartItem } from "./useCart";
|
|
||||||
|
|
||||||
interface ProductImage {
|
|
||||||
src: string;
|
|
||||||
alt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ProductMeta {
|
|
||||||
salePrice?: string;
|
|
||||||
ribbon?: string;
|
|
||||||
inventoryStatus?: string;
|
|
||||||
inventoryQuantity?: number;
|
|
||||||
sku?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useProductDetail(productId: string) {
|
|
||||||
const { product, isLoading, error } = useProduct(productId);
|
|
||||||
const [selectedQuantity, setSelectedQuantity] = useState(1);
|
|
||||||
const [selectedVariants, setSelectedVariants] = useState<Record<string, string>>({});
|
|
||||||
|
|
||||||
const images = useMemo<ProductImage[]>(() => {
|
|
||||||
if (!product) return [];
|
|
||||||
|
|
||||||
if (product.images && product.images.length > 0) {
|
|
||||||
return product.images.map((src, index) => ({
|
|
||||||
src,
|
|
||||||
alt: product.imageAlt || `${product.name} - Image ${index + 1}`,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
return [{
|
|
||||||
src: product.imageSrc,
|
|
||||||
alt: product.imageAlt || product.name,
|
|
||||||
}];
|
|
||||||
}, [product]);
|
|
||||||
|
|
||||||
const meta = useMemo<ProductMeta>(() => {
|
|
||||||
if (!product?.metadata) return {};
|
|
||||||
|
|
||||||
const metadata = product.metadata;
|
|
||||||
|
|
||||||
let salePrice: string | undefined;
|
|
||||||
const onSaleValue = metadata.onSale;
|
|
||||||
const onSale = String(onSaleValue) === "true" || onSaleValue === 1 || String(onSaleValue) === "1";
|
|
||||||
const salePriceValue = metadata.salePrice;
|
|
||||||
|
|
||||||
if (onSale && salePriceValue !== undefined && salePriceValue !== null) {
|
|
||||||
if (typeof salePriceValue === 'number') {
|
|
||||||
salePrice = `$${salePriceValue.toFixed(2)}`;
|
|
||||||
} else {
|
|
||||||
const salePriceStr = String(salePriceValue);
|
|
||||||
salePrice = salePriceStr.startsWith('$') ? salePriceStr : `$${salePriceStr}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let inventoryQuantity: number | undefined;
|
|
||||||
if (metadata.inventoryQuantity !== undefined) {
|
|
||||||
const qty = metadata.inventoryQuantity;
|
|
||||||
inventoryQuantity = typeof qty === 'number' ? qty : parseInt(String(qty), 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
salePrice,
|
|
||||||
ribbon: metadata.ribbon ? String(metadata.ribbon) : undefined,
|
|
||||||
inventoryStatus: metadata.inventoryStatus ? String(metadata.inventoryStatus) : undefined,
|
|
||||||
inventoryQuantity,
|
|
||||||
sku: metadata.sku ? String(metadata.sku) : undefined,
|
|
||||||
};
|
|
||||||
}, [product]);
|
|
||||||
|
|
||||||
const variants = useMemo<ProductVariant[]>(() => {
|
|
||||||
if (!product) return [];
|
|
||||||
|
|
||||||
const variantList: ProductVariant[] = [];
|
|
||||||
|
|
||||||
if (product.metadata?.variantOptions) {
|
|
||||||
try {
|
|
||||||
const variantOptionsStr = String(product.metadata.variantOptions);
|
|
||||||
const parsedOptions = JSON.parse(variantOptionsStr);
|
|
||||||
|
|
||||||
if (Array.isArray(parsedOptions)) {
|
|
||||||
parsedOptions.forEach((option: any) => {
|
|
||||||
if (option.name && option.values) {
|
|
||||||
const values = typeof option.values === 'string'
|
|
||||||
? option.values.split(',').map((v: string) => v.trim())
|
|
||||||
: Array.isArray(option.values)
|
|
||||||
? option.values.map((v: any) => String(v).trim())
|
|
||||||
: [String(option.values)];
|
|
||||||
|
|
||||||
if (values.length > 0) {
|
|
||||||
const optionLabel = option.name;
|
|
||||||
const currentSelected = selectedVariants[optionLabel] || values[0];
|
|
||||||
|
|
||||||
variantList.push({
|
|
||||||
label: optionLabel,
|
|
||||||
options: values,
|
|
||||||
selected: currentSelected,
|
|
||||||
onChange: (value) => {
|
|
||||||
setSelectedVariants((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[optionLabel]: value,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn("Failed to parse variantOptions:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (variantList.length === 0 && product.brand) {
|
|
||||||
variantList.push({
|
|
||||||
label: "Brand",
|
|
||||||
options: [product.brand],
|
|
||||||
selected: product.brand,
|
|
||||||
onChange: () => { },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (variantList.length === 0 && product.variant) {
|
|
||||||
const variantOptions = product.variant.includes('/')
|
|
||||||
? product.variant.split('/').map(v => v.trim())
|
|
||||||
: [product.variant];
|
|
||||||
|
|
||||||
const variantLabel = "Variant";
|
|
||||||
const currentSelected = selectedVariants[variantLabel] || variantOptions[0];
|
|
||||||
|
|
||||||
variantList.push({
|
|
||||||
label: variantLabel,
|
|
||||||
options: variantOptions,
|
|
||||||
selected: currentSelected,
|
|
||||||
onChange: (value) => {
|
|
||||||
setSelectedVariants((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[variantLabel]: value,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return variantList;
|
|
||||||
}, [product, selectedVariants]);
|
|
||||||
|
|
||||||
const quantityVariant = useMemo<ProductVariant>(() => ({
|
|
||||||
label: "Quantity",
|
|
||||||
options: Array.from({ length: 10 }, (_, i) => String(i + 1)),
|
|
||||||
selected: String(selectedQuantity),
|
|
||||||
onChange: (value) => setSelectedQuantity(parseInt(value, 10)),
|
|
||||||
}), [selectedQuantity]);
|
|
||||||
|
|
||||||
const createCartItem = useCallback((): ExtendedCartItem | null => {
|
|
||||||
if (!product) return null;
|
|
||||||
|
|
||||||
const variantStrings = Object.entries(selectedVariants).map(
|
|
||||||
([label, value]) => `${label}: ${value}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (variantStrings.length === 0 && product.variant) {
|
|
||||||
variantStrings.push(`Variant: ${product.variant}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const variantId = Object.values(selectedVariants).join('-') || 'default';
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: `${product.id}-${variantId}-${selectedQuantity}`,
|
|
||||||
productId: product.id,
|
|
||||||
name: product.name,
|
|
||||||
variants: variantStrings,
|
|
||||||
price: product.price,
|
|
||||||
quantity: selectedQuantity,
|
|
||||||
imageSrc: product.imageSrc,
|
|
||||||
imageAlt: product.imageAlt || product.name,
|
|
||||||
};
|
|
||||||
}, [product, selectedVariants, selectedQuantity]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
product,
|
|
||||||
isLoading,
|
|
||||||
error,
|
|
||||||
images,
|
|
||||||
meta,
|
|
||||||
variants,
|
|
||||||
quantityVariant,
|
|
||||||
selectedQuantity,
|
|
||||||
selectedVariants,
|
|
||||||
createCartItem,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +1,13 @@
|
|||||||
"use client";
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { Product, fetchProducts } from "@/lib/api/product";
|
|
||||||
|
|
||||||
export function useProducts() {
|
export function useProducts() {
|
||||||
const [products, setProducts] = useState<Product[]>([]);
|
const [products, setProducts] = useState([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<Error | null>(null);
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isMounted = true;
|
setLoading(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
async function loadProducts() {
|
return { products, loading, error };
|
||||||
try {
|
|
||||||
const data = await fetchProducts();
|
|
||||||
if (isMounted) {
|
|
||||||
setProducts(data);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if (isMounted) {
|
|
||||||
setError(err instanceof Error ? err : new Error("Failed to fetch products"));
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
if (isMounted) {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadProducts();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
isMounted = false;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return { products, isLoading, error };
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,219 +1,8 @@
|
|||||||
export type Product = {
|
export function fetchProducts() {
|
||||||
id: string;
|
// Fetch logic here
|
||||||
name: string;
|
return [];
|
||||||
price: string;
|
|
||||||
imageSrc: string;
|
|
||||||
imageAlt?: string;
|
|
||||||
images?: string[];
|
|
||||||
brand?: string;
|
|
||||||
variant?: string;
|
|
||||||
rating?: number;
|
|
||||||
reviewCount?: string;
|
|
||||||
description?: string;
|
|
||||||
priceId?: string;
|
|
||||||
metadata?: {
|
|
||||||
[key: string]: string | number | undefined;
|
|
||||||
};
|
|
||||||
onFavorite?: () => void;
|
|
||||||
onProductClick?: () => void;
|
|
||||||
isFavorited?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const defaultProducts: Product[] = [
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
name: "Classic White Sneakers",
|
|
||||||
price: "$129",
|
|
||||||
brand: "Nike",
|
|
||||||
variant: "White / Size 42",
|
|
||||||
rating: 4.5,
|
|
||||||
reviewCount: "128",
|
|
||||||
imageSrc: "/placeholders/placeholder3.avif",
|
|
||||||
imageAlt: "Classic white sneakers",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "2",
|
|
||||||
name: "Leather Crossbody Bag",
|
|
||||||
price: "$89",
|
|
||||||
brand: "Coach",
|
|
||||||
variant: "Brown / Medium",
|
|
||||||
rating: 4.8,
|
|
||||||
reviewCount: "256",
|
|
||||||
imageSrc: "/placeholders/placeholder4.webp",
|
|
||||||
imageAlt: "Brown leather crossbody bag",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "3",
|
|
||||||
name: "Wireless Headphones",
|
|
||||||
price: "$199",
|
|
||||||
brand: "Sony",
|
|
||||||
variant: "Black",
|
|
||||||
rating: 4.7,
|
|
||||||
reviewCount: "512",
|
|
||||||
imageSrc: "/placeholders/placeholder3.avif",
|
|
||||||
imageAlt: "Black wireless headphones",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "4",
|
|
||||||
name: "Minimalist Watch",
|
|
||||||
price: "$249",
|
|
||||||
brand: "Fossil",
|
|
||||||
variant: "Silver / 40mm",
|
|
||||||
rating: 4.6,
|
|
||||||
reviewCount: "89",
|
|
||||||
imageSrc: "/placeholders/placeholder4.webp",
|
|
||||||
imageAlt: "Silver minimalist watch",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
function formatPrice(amount: number, currency: string): string {
|
|
||||||
const formatter = new Intl.NumberFormat("en-US", {
|
|
||||||
style: "currency",
|
|
||||||
currency: currency.toUpperCase(),
|
|
||||||
minimumFractionDigits: 0,
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
});
|
|
||||||
return formatter.format(amount / 100);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchProducts(): Promise<Product[]> {
|
export function getProductById() {
|
||||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
|
// Get product logic here
|
||||||
const projectId = process.env.NEXT_PUBLIC_PROJECT_ID;
|
|
||||||
|
|
||||||
if (!apiUrl || !projectId) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const url = `${apiUrl}/stripe/project/products?projectId=${projectId}&expandDefaultPrice=true`;
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const resp = await response.json();
|
|
||||||
const data = resp.data.data || resp.data;
|
|
||||||
|
|
||||||
if (!Array.isArray(data) || data.length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return data.map((product: any) => {
|
|
||||||
const metadata: Record<string, string | number | undefined> = {};
|
|
||||||
if (product.metadata && typeof product.metadata === 'object') {
|
|
||||||
Object.keys(product.metadata).forEach(key => {
|
|
||||||
const value = product.metadata[key];
|
|
||||||
if (value !== null && value !== undefined) {
|
|
||||||
const numValue = parseFloat(value);
|
|
||||||
metadata[key] = isNaN(numValue) ? value : numValue;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageSrc = product.images?.[0] || product.imageSrc || "/placeholders/placeholder3.avif";
|
|
||||||
const imageAlt = product.imageAlt || product.name || "";
|
|
||||||
const images = product.images && Array.isArray(product.images) && product.images.length > 0
|
|
||||||
? product.images
|
|
||||||
: [imageSrc];
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: product.id || String(Math.random()),
|
|
||||||
name: product.name || "Untitled Product",
|
|
||||||
description: product.description || "",
|
|
||||||
price: product.default_price?.unit_amount
|
|
||||||
? formatPrice(product.default_price.unit_amount, product.default_price.currency || "usd")
|
|
||||||
: product.price || "$0",
|
|
||||||
priceId: product.default_price?.id || product.priceId,
|
|
||||||
imageSrc,
|
|
||||||
imageAlt,
|
|
||||||
images,
|
|
||||||
brand: product.metadata?.brand || product.brand || "",
|
|
||||||
variant: product.metadata?.variant || product.variant || "",
|
|
||||||
rating: product.metadata?.rating ? parseFloat(product.metadata.rating) : undefined,
|
|
||||||
reviewCount: product.metadata?.reviewCount || undefined,
|
|
||||||
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchProduct(productId: string): Promise<Product | null> {
|
|
||||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
|
|
||||||
const projectId = process.env.NEXT_PUBLIC_PROJECT_ID;
|
|
||||||
|
|
||||||
if (!apiUrl || !projectId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const url = `${apiUrl}/stripe/project/products/${productId}?projectId=${projectId}&expandDefaultPrice=true`;
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const resp = await response.json();
|
|
||||||
const product = resp.data?.data || resp.data || resp;
|
|
||||||
|
|
||||||
if (!product || typeof product !== 'object') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const metadata: Record<string, string | number | undefined> = {};
|
|
||||||
if (product.metadata && typeof product.metadata === 'object') {
|
|
||||||
Object.keys(product.metadata).forEach(key => {
|
|
||||||
const value = product.metadata[key];
|
|
||||||
if (value !== null && value !== undefined && value !== '') {
|
|
||||||
const numValue = parseFloat(String(value));
|
|
||||||
metadata[key] = isNaN(numValue) ? String(value) : numValue;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let priceValue = product.price;
|
|
||||||
if (!priceValue && product.default_price?.unit_amount) {
|
|
||||||
priceValue = formatPrice(product.default_price.unit_amount, product.default_price.currency || "usd");
|
|
||||||
}
|
|
||||||
if (!priceValue) {
|
|
||||||
priceValue = "$0";
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageSrc = product.images?.[0] || product.imageSrc || "/placeholders/placeholder3.avif";
|
|
||||||
const imageAlt = product.imageAlt || product.name || "";
|
|
||||||
const images = product.images && Array.isArray(product.images) && product.images.length > 0
|
|
||||||
? product.images
|
|
||||||
: [imageSrc];
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: product.id || String(Math.random()),
|
|
||||||
name: product.name || "Untitled Product",
|
|
||||||
description: product.description || "",
|
|
||||||
price: priceValue,
|
|
||||||
priceId: product.default_price?.id || product.priceId,
|
|
||||||
imageSrc,
|
|
||||||
imageAlt,
|
|
||||||
images,
|
|
||||||
brand: product.metadata?.brand || product.brand || "",
|
|
||||||
variant: product.metadata?.variant || product.variant || "",
|
|
||||||
rating: product.metadata?.rating ? parseFloat(String(product.metadata.rating)) : undefined,
|
|
||||||
reviewCount: product.metadata?.reviewCount || undefined,
|
|
||||||
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user