112 Commits

Author SHA1 Message Date
912cedbf1b Switch to version 2: modified src/lib/api/product.ts 2026-03-04 00:43:15 +00:00
878892dafb Switch to version 2: modified src/hooks/useProducts.ts 2026-03-04 00:43:14 +00:00
79ba54ae2d Switch to version 2: modified src/hooks/useProductDetail.ts 2026-03-04 00:43:14 +00:00
bc2e96c4c4 Switch to version 2: modified src/hooks/useProductCatalog.ts 2026-03-04 00:43:13 +00:00
f8a32980d5 Switch to version 2: modified src/hooks/useProduct.ts 2026-03-04 00:43:13 +00:00
06217594e0 Switch to version 2: modified src/hooks/useCheckout.ts 2026-03-04 00:43:12 +00:00
793305d6c6 Switch to version 2: modified src/components/shared/Dashboard.tsx 2026-03-04 00:43:12 +00:00
e02cf3cd3c Switch to version 2: modified src/components/sections/testimonial/TestimonialCardTwo.tsx 2026-03-04 00:43:11 +00:00
982cffae82 Switch to version 2: modified src/components/sections/testimonial/TestimonialCardThirteen.tsx 2026-03-04 00:43:10 +00:00
31363ba75f Switch to version 2: modified src/components/sections/testimonial/TestimonialCardSixteen.tsx 2026-03-04 00:43:10 +00:00
72a32f5777 Switch to version 2: modified src/components/sections/testimonial/TestimonialCardSix.tsx 2026-03-04 00:43:09 +00:00
5939b2d897 Switch to version 2: modified src/components/sections/testimonial/TestimonialCardOne.tsx 2026-03-04 00:43:09 +00:00
8e35234aaa Switch to version 2: modified src/components/sections/team/TeamCardTwo.tsx 2026-03-04 00:43:08 +00:00
89b1173d55 Switch to version 2: modified src/components/sections/team/TeamCardSix.tsx 2026-03-04 00:43:08 +00:00
5586852e72 Switch to version 2: modified src/components/sections/team/TeamCardOne.tsx 2026-03-04 00:43:07 +00:00
04fc908452 Switch to version 2: modified src/components/sections/team/TeamCardFive.tsx 2026-03-04 00:43:06 +00:00
d198440917 Switch to version 2: modified src/components/sections/team/TeamCardEleven.tsx 2026-03-04 00:43:06 +00:00
3654946521 Switch to version 2: modified src/components/sections/product/ProductCardTwo.tsx 2026-03-04 00:43:05 +00:00
3959aa70e2 Switch to version 2: modified src/components/sections/product/ProductCardThree.tsx 2026-03-04 00:43:05 +00:00
e58d0d44ac Switch to version 2: modified src/components/sections/product/ProductCardOne.tsx 2026-03-04 00:43:04 +00:00
f0959b6198 Switch to version 2: modified src/components/sections/product/ProductCardFour.tsx 2026-03-04 00:43:03 +00:00
f937ab68cb Switch to version 2: modified src/components/sections/pricing/PricingCardTwo.tsx 2026-03-04 00:43:02 +00:00
5ff4b33bcf Switch to version 2: modified src/components/sections/pricing/PricingCardThree.tsx 2026-03-04 00:43:02 +00:00
d06f00d655 Switch to version 2: modified src/components/sections/pricing/PricingCardOne.tsx 2026-03-04 00:43:01 +00:00
59fa03ba4a Switch to version 2: modified src/components/sections/pricing/PricingCardNine.tsx 2026-03-04 00:43:00 +00:00
6350b08448 Switch to version 2: modified src/components/sections/pricing/PricingCardFive.tsx 2026-03-04 00:42:59 +00:00
9c830a7dbf Switch to version 2: modified src/components/sections/pricing/PricingCardEight.tsx 2026-03-04 00:42:59 +00:00
663adfc69d Switch to version 2: modified src/components/sections/metrics/MetricCardTwo.tsx 2026-03-04 00:42:58 +00:00
2366543ac4 Switch to version 2: modified src/components/sections/metrics/MetricCardThree.tsx 2026-03-04 00:42:57 +00:00
c8e61e8057 Switch to version 2: modified src/components/sections/metrics/MetricCardTen.tsx 2026-03-04 00:42:57 +00:00
7355b02263 Switch to version 2: modified src/components/sections/metrics/MetricCardSeven.tsx 2026-03-04 00:42:56 +00:00
6caec15e0c Switch to version 2: modified src/components/sections/metrics/MetricCardOne.tsx 2026-03-04 00:42:55 +00:00
65faf434bd Switch to version 2: modified src/components/sections/metrics/MetricCardEleven.tsx 2026-03-04 00:42:55 +00:00
77b5fa4369 Switch to version 2: modified src/components/sections/hero/HeroBillboardGallery.tsx 2026-03-04 00:42:54 +00:00
eddc31cfaa Switch to version 2: modified src/components/sections/hero/HeroBillboardDashboard.tsx 2026-03-04 00:42:54 +00:00
9a44615c9d Switch to version 2: modified src/components/sections/hero/HeroBillboardCarousel.tsx 2026-03-04 00:42:53 +00:00
1d46d0403d Switch to version 2: modified src/components/sections/feature/featureHoverPattern/FeatureHoverPattern.tsx 2026-03-04 00:42:52 +00:00
860f26ce98 Switch to version 2: modified src/components/sections/feature/featureCardThree/FeatureCardThree.tsx 2026-03-04 00:42:52 +00:00
0fffa9a709 Switch to version 2: modified src/components/sections/feature/featureBorderGlow/FeatureBorderGlow.tsx 2026-03-04 00:42:51 +00:00
6960d007a5 Switch to version 2: modified src/components/sections/feature/FeatureCardTwentyThree.tsx 2026-03-04 00:42:51 +00:00
f8d60ab76b Switch to version 2: modified src/components/sections/feature/FeatureCardTwentySeven.tsx 2026-03-04 00:42:50 +00:00
f59bdc1a4d Switch to version 2: modified src/components/sections/feature/FeatureCardTwentyFour.tsx 2026-03-04 00:42:49 +00:00
e32bd412ce Switch to version 2: modified src/components/sections/feature/FeatureCardTwentyFive.tsx 2026-03-04 00:42:49 +00:00
4406f3cb4e Switch to version 2: modified src/components/sections/feature/FeatureCardTwelve.tsx 2026-03-04 00:42:48 +00:00
d7b8b9d1a1 Switch to version 2: modified src/components/sections/feature/FeatureCardTen.tsx 2026-03-04 00:42:47 +00:00
2754142975 Switch to version 2: modified src/components/sections/feature/FeatureCardSixteen.tsx 2026-03-04 00:42:47 +00:00
2bcfc3ec8e Switch to version 2: modified src/components/sections/feature/FeatureCardSeven.tsx 2026-03-04 00:42:46 +00:00
d4de952f1d Switch to version 2: modified src/components/sections/feature/FeatureCardOne.tsx 2026-03-04 00:42:46 +00:00
05237d9024 Switch to version 2: modified src/components/sections/feature/FeatureCardNine.tsx 2026-03-04 00:42:45 +00:00
be4a04b90f Switch to version 2: modified src/components/sections/feature/FeatureCardMedia.tsx 2026-03-04 00:42:45 +00:00
987b8d6543 Switch to version 2: modified src/components/sections/feature/FeatureBento.tsx 2026-03-04 00:42:44 +00:00
0f296d00a0 Switch to version 2: modified src/components/sections/contact/ContactSplitForm.tsx 2026-03-04 00:42:43 +00:00
dc4a81b6dd Switch to version 2: modified src/components/sections/contact/ContactSplit.tsx 2026-03-04 00:42:42 +00:00
c696dbec3e Switch to version 2: modified src/components/sections/contact/ContactFaq.tsx 2026-03-04 00:42:42 +00:00
514df9afe7 Switch to version 2: modified src/components/sections/contact/ContactCenter.tsx 2026-03-04 00:42:41 +00:00
a128c8cd81 Switch to version 2: modified src/components/sections/blog/BlogCardTwo.tsx 2026-03-04 00:42:41 +00:00
de2d5f3964 Switch to version 2: modified src/components/sections/blog/BlogCardThree.tsx 2026-03-04 00:42:40 +00:00
9c2e5d611d Switch to version 2: modified src/components/sections/blog/BlogCardOne.tsx 2026-03-04 00:42:39 +00:00
fa80321b11 Switch to version 2: modified src/components/ecommerce/productCatalog/ProductCatalog.tsx 2026-03-04 00:42:39 +00:00
c0b88e5f1a Switch to version 2: modified src/components/cardStack/layouts/timelines/TimelineProcessFlow.tsx 2026-03-04 00:42:38 +00:00
12ff6bc1b7 Switch to version 2: modified src/components/cardStack/layouts/timelines/TimelinePhoneView.tsx 2026-03-04 00:42:38 +00:00
b183845cd8 Switch to version 2: modified src/components/cardStack/layouts/timelines/TimelineBase.tsx 2026-03-04 00:42:37 +00:00
42f41b8801 Switch to version 2: modified src/components/cardStack/layouts/grid/GridLayout.tsx 2026-03-04 00:42:37 +00:00
64937a1f20 Switch to version 2: modified src/components/cardStack/layouts/carousels/ButtonCarousel.tsx 2026-03-04 00:42:36 +00:00
2630f77ddf Switch to version 2: modified src/components/cardStack/layouts/carousels/AutoCarousel.tsx 2026-03-04 00:42:36 +00:00
2adff55f6e Switch to version 2: modified src/components/cardStack/hooks/useDepth3DAnimation.ts 2026-03-04 00:42:35 +00:00
0b31583cfc Switch to version 2: modified src/components/cardStack/hooks/useCardAnimation.ts 2026-03-04 00:42:34 +00:00
60600590dd Switch to version 2: modified src/components/cardStack/CardStack.tsx 2026-03-04 00:42:33 +00:00
ddc8a52df8 Switch to version 2: modified src/components/cardStack/CardList.tsx 2026-03-04 00:42:33 +00:00
db95df0061 Switch to version 2: modified src/app/page.tsx 2026-03-04 00:42:32 +00:00
37c44c77a8 Switch to version 2: modified src/app/layout.tsx 2026-03-04 00:42:32 +00:00
dafbe23828 Merge version_3 into main
Merge version_3 into main
2026-03-04 00:41:37 +00:00
58b9371284 Merge version_3 into main
Merge version_3 into main
2026-03-04 00:37:10 +00:00
03b89ec4c2 Merge version_3 into main
Merge version_3 into main
2026-03-04 00:35:35 +00:00
2ad370c1ed Merge version_3 into main
Merge version_3 into main
2026-03-04 00:33:15 +00:00
ee3648dd87 Merge version_3 into main
Merge version_3 into main
2026-03-04 00:31:36 +00:00
23026fe930 Merge version_3 into main
Merge version_3 into main
2026-03-04 00:30:04 +00:00
9ce2d53e5d Switch to version 1: modified src/lib/api/product.ts 2026-03-04 00:29:38 +00:00
c958a4ffb2 Switch to version 1: modified src/hooks/useProducts.ts 2026-03-04 00:29:38 +00:00
3fb2f210fd Switch to version 1: modified src/hooks/useProductDetail.ts 2026-03-04 00:29:37 +00:00
a0dac333bb Switch to version 1: modified src/hooks/useProductCatalog.ts 2026-03-04 00:29:36 +00:00
e3aaf00583 Switch to version 1: modified src/hooks/useProduct.ts 2026-03-04 00:29:36 +00:00
dd70529a93 Switch to version 1: modified src/hooks/useCheckout.ts 2026-03-04 00:29:35 +00:00
f9a67ea921 Switch to version 1: modified src/components/shared/Dashboard.tsx 2026-03-04 00:29:34 +00:00
431b506870 Switch to version 1: modified src/components/sections/testimonial/TestimonialCardTwo.tsx 2026-03-04 00:29:34 +00:00
d8f3f96a2c Switch to version 1: modified src/components/sections/testimonial/TestimonialCardThirteen.tsx 2026-03-04 00:29:33 +00:00
e5a9bf7e52 Switch to version 1: modified src/components/sections/testimonial/TestimonialCardSixteen.tsx 2026-03-04 00:29:33 +00:00
898618b761 Switch to version 1: modified src/components/sections/testimonial/TestimonialCardSix.tsx 2026-03-04 00:29:32 +00:00
997454271a Switch to version 1: modified src/components/sections/testimonial/TestimonialCardOne.tsx 2026-03-04 00:29:31 +00:00
0168228dc7 Switch to version 1: modified src/components/sections/team/TeamCardTwo.tsx 2026-03-04 00:29:30 +00:00
6bef0bcea6 Switch to version 1: modified src/components/sections/team/TeamCardSix.tsx 2026-03-04 00:29:30 +00:00
5010eecad0 Switch to version 1: modified src/components/sections/team/TeamCardOne.tsx 2026-03-04 00:29:29 +00:00
2ab7cdf63e Switch to version 1: modified src/components/sections/team/TeamCardFive.tsx 2026-03-04 00:29:29 +00:00
95c9b6e0e2 Switch to version 1: modified src/components/sections/team/TeamCardEleven.tsx 2026-03-04 00:29:28 +00:00
4dce485679 Switch to version 1: modified src/components/sections/product/ProductCardTwo.tsx 2026-03-04 00:29:27 +00:00
7911612a3f Switch to version 1: modified src/components/sections/product/ProductCardThree.tsx 2026-03-04 00:29:27 +00:00
571bef20af Switch to version 1: modified src/components/sections/product/ProductCardOne.tsx 2026-03-04 00:29:26 +00:00
bcf1350d7a Switch to version 1: modified src/components/sections/product/ProductCardFour.tsx 2026-03-04 00:29:26 +00:00
d5ab7a40bd Switch to version 1: modified src/components/sections/pricing/PricingCardTwo.tsx 2026-03-04 00:29:25 +00:00
671fa3fe28 Switch to version 1: modified src/components/sections/pricing/PricingCardThree.tsx 2026-03-04 00:29:24 +00:00
b1424545ee Switch to version 1: modified src/components/sections/pricing/PricingCardOne.tsx 2026-03-04 00:29:24 +00:00
51f888ebcb Switch to version 1: modified src/components/sections/pricing/PricingCardNine.tsx 2026-03-04 00:29:23 +00:00
2d6866bb15 Switch to version 1: modified src/components/sections/pricing/PricingCardFive.tsx 2026-03-04 00:29:23 +00:00
f90d447aae Switch to version 1: modified src/components/sections/pricing/PricingCardEight.tsx 2026-03-04 00:29:22 +00:00
2d5da38b38 Switch to version 1: modified src/components/sections/metrics/MetricCardTwo.tsx 2026-03-04 00:29:21 +00:00
a3136c07e5 Switch to version 1: modified src/components/sections/metrics/MetricCardThree.tsx 2026-03-04 00:29:21 +00:00
da2b84ddba Switch to version 1: modified src/components/sections/metrics/MetricCardTen.tsx 2026-03-04 00:29:20 +00:00
6138abece5 Switch to version 1: modified src/components/sections/metrics/MetricCardSeven.tsx 2026-03-04 00:29:20 +00:00
f74f12f57b Switch to version 1: modified src/components/sections/metrics/MetricCardOne.tsx 2026-03-04 00:29:19 +00:00
f50aa4a4b8 Switch to version 1: modified src/components/sections/metrics/MetricCardEleven.tsx 2026-03-04 00:29:19 +00:00
a6b78d2c8c Switch to version 1: modified src/components/sections/hero/HeroBillboardGallery.tsx 2026-03-04 00:29:18 +00:00
1524b55f9d Switch to version 1: modified src/components/sections/hero/HeroBillboardDashboard.tsx 2026-03-04 00:29:17 +00:00
36 changed files with 286 additions and 6588 deletions

View File

@@ -1,17 +1,14 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
import './styles/variables.css';
const geist = Geist({
variable: "--font-geist-sans", subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono", subsets: ["latin"],
});
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: "Create Next App", description: "Generated by create next app"};
title: 'Serenity Reiki - English & French',
description: 'Experience certified reiki healing in a peaceful sanctuary. Découvrez la guérison reiki certifiée dans un sanctuaire paisible.',
};
export default function RootLayout({
children,
@@ -20,7 +17,7 @@ export default function RootLayout({
}) {
return (
<html lang="en">
<body className={`${geist.variable} ${geistMono.variable} antialiased`}>
<body className={inter.className}>
{children}
<script

View File

@@ -6,8 +6,8 @@ import NavbarLayoutFloatingOverlay from "@/components/navbar/NavbarLayoutFloatin
import HeroLogoBillboard from "@/components/sections/hero/HeroLogoBillboard";
import TextSplitAbout from "@/components/sections/about/TextSplitAbout";
import FeatureCardTwentySix from "@/components/sections/feature/FeatureCardTwentySix";
import PricingCardThree from "@/components/sections/pricing/PricingCardThree";
import TestimonialCardSixteen from "@/components/sections/testimonial/TestimonialCardSixteen";
import { PricingCardThree } from "@/components/sections/pricing/PricingCardThree";
import { TestimonialCardSixteen } from "@/components/sections/testimonial/TestimonialCardSixteen";
import ContactText from "@/components/sections/contact/ContactText";
import FooterBase from "@/components/sections/footer/FooterBase";
import { BookOpen, Heart, Shield, Sparkles, Wifi, Zap } from "lucide-react";
@@ -23,8 +23,7 @@ const translations = {
{ 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"
},
bookButton: "Book Your Session", 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" },
@@ -33,8 +32,7 @@ const translations = {
{ 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"
}
bookButton: "Réserver une session", 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() {
@@ -42,6 +40,7 @@ export default function LandingPage() {
const [mounted, setMounted] = useState(false);
useEffect(() => {
// Detect OS language on mount
const detectedLang = navigator.language.startsWith("fr") ? "fr" : "en";
setLanguage(detectedLang);
setMounted(true);
@@ -209,13 +208,13 @@ export default function LandingPage() {
]
}
]}
title={t.pricingTitle}
description={t.pricingDesc}
tag={language === "en" ? "Transparent Pricing" : "Tarification transparente"}
tagAnimation="slide-up"
animationType="slide-up"
textboxLayout="default"
useInvertedBackground={false}
animationType="slide-up"
tag={language === "en" ? "Transparent Pricing" : "Tarification transparente"}
tagAnimation="slide-up"
title={t.pricingTitle}
description={t.pricingDesc}
/>
</div>
@@ -244,13 +243,13 @@ export default function LandingPage() {
{ value: "98%", label: language === "en" ? "Client satisfaction rate" : "Taux de satisfaction client" },
{ 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"
useInvertedBackground={false}
animationType="slide-up"
tag={language === "en" ? "Client Stories" : "Histoires de clients"}
tagAnimation="slide-up"
title={t.testimonialTitle}
description={t.testimonialDesc}
/>
</div>

View File

@@ -1,123 +1,15 @@
"use client";
import { useCardAnimation } from './hooks/useCardAnimation';
import { memo, Children } from "react";
import CardStackTextBox from "@/components/cardStack/CardStackTextBox";
import { useCardAnimation } from "@/components/cardStack/hooks/useCardAnimation";
import { cls } from "@/lib/utils";
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";
export function CardList() {
const { animate } = useCardAnimation();
interface CardListProps {
children: React.ReactNode;
animationType: CardAnimationType;
useUncappedRounding?: boolean;
title?: string;
titleSegments?: TitleSegment[];
description?: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground?: InvertedBackground;
disableCardWrapper?: boolean;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
}
const CardList = ({
children,
animationType,
useUncappedRounding = false,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
disableCardWrapper = false,
ariaLabel = "Card list",
className = "",
containerClassName = "",
cardClassName = "",
textBoxClassName = "",
titleClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
}: CardListProps) => {
const childrenArray = Children.toArray(children);
const { itemRefs } = useCardAnimation({ animationType, itemCount: childrenArray.length, useIndividualTriggers: true });
const handleAnimate = () => {
animate();
};
return (
<section
aria-label={ariaLabel}
className={cls(
"relative py-20 w-full",
useInvertedBackground && "bg-foreground",
className
)}
>
<div className={cls("w-content-width mx-auto flex flex-col gap-8", containerClassName)}>
<CardStackTextBox
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
textBoxClassName={textBoxClassName}
titleClassName={titleClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
descriptionClassName={descriptionClassName}
tagClassName={tagClassName}
buttonContainerClassName={buttonContainerClassName}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
/>
<div className="flex flex-col gap-6">
{childrenArray.map((child, index) => (
<div
key={index}
ref={(el) => { itemRefs.current[index] = el; }}
className={cls(!disableCardWrapper && "card", !disableCardWrapper && (useUncappedRounding ? "rounded-theme" : "rounded-theme-capped"), cardClassName)}
>
{child}
</div>
))}
</div>
</div>
</section>
<div>
<button onClick={handleAnimate}>Animate</button>
</div>
);
};
CardList.displayName = "CardList";
export default memo(CardList);
}

View File

@@ -1,229 +1,16 @@
"use client";
import { useRef, useCallback } from 'react';
import { memo, Children } from "react";
import { CardStackProps } from "./types";
import GridLayout from "./layouts/grid/GridLayout";
import AutoCarousel from "./layouts/carousels/AutoCarousel";
import ButtonCarousel from "./layouts/carousels/ButtonCarousel";
import TimelineBase from "./layouts/timelines/TimelineBase";
import { gridConfigs } from "./layouts/grid/gridConfigs";
export function useCardAnimation() {
const itemRefs = useRef<Array<HTMLDivElement | null>>([]);
const containerRef = useRef<HTMLDivElement | null>(null);
const perspectiveRef = useRef<HTMLDivElement | null>(null);
const bottomContentRef = useRef<HTMLDivElement | null>(null);
const CardStack = ({
children,
mode = "buttons",
gridVariant = "uniform-all-items-equal",
uniformGridCustomHeightClasses,
gridRowsClassName,
itemHeightClassesOverride,
animationType,
supports3DAnimation = false,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout = "default",
useInvertedBackground,
carouselThreshold = 5,
bottomContent,
className = "",
containerClassName = "",
gridClassName = "",
carouselClassName = "",
carouselItemClassName = "",
controlsClassName = "",
textBoxClassName = "",
titleClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
ariaLabel = "Card stack",
}: CardStackProps) => {
const childrenArray = Children.toArray(children);
const itemCount = childrenArray.length;
const animate = useCallback((index?: number) => {
// Animation logic here
}, []);
// Check if the current grid config has gridRows defined
const gridConfig = gridConfigs[gridVariant]?.[itemCount];
const hasFixedGridRows = gridConfig && 'gridRows' in gridConfig && gridConfig.gridRows;
return { animate, itemRefs, containerRef, perspectiveRef, bottomContentRef };
}
// If grid has fixed row heights and we have uniformGridCustomHeightClasses,
// we need to use min-h-0 on md+ to prevent conflicts
let adjustedHeightClasses = uniformGridCustomHeightClasses;
if (hasFixedGridRows && uniformGridCustomHeightClasses) {
// Extract the mobile min-height and add md:min-h-0
const mobileMinHeight = uniformGridCustomHeightClasses.split(' ')[0];
adjustedHeightClasses = `${mobileMinHeight} md:min-h-0`;
}
// Timeline layout for zigzag pattern (works best with 3-6 items)
if (gridVariant === "timeline" && itemCount >= 3 && itemCount <= 6) {
// Convert depth-3d to scale-rotate for timeline (doesn't support 3D)
const timelineAnimationType = animationType === "depth-3d" ? "scale-rotate" : animationType;
return (
<TimelineBase
variant={gridVariant}
uniformGridCustomHeightClasses={adjustedHeightClasses}
animationType={timelineAnimationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
textBoxClassName={textBoxClassName}
titleClassName={titleClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
descriptionClassName={descriptionClassName}
tagClassName={tagClassName}
buttonContainerClassName={buttonContainerClassName}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
ariaLabel={ariaLabel}
>
{childrenArray}
</TimelineBase>
);
}
// Use grid for items below threshold, carousel for items at or above threshold
// Timeline with 7+ items will also use carousel
const useCarousel = itemCount >= carouselThreshold || (gridVariant === "timeline" && itemCount > 6);
// Grid layout for 1-4 items
if (!useCarousel) {
return (
<GridLayout
itemCount={itemCount}
gridVariant={gridVariant}
uniformGridCustomHeightClasses={adjustedHeightClasses}
gridRowsClassName={gridRowsClassName}
itemHeightClassesOverride={itemHeightClassesOverride}
animationType={animationType}
supports3DAnimation={supports3DAnimation}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
bottomContent={bottomContent}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
textBoxClassName={textBoxClassName}
titleClassName={titleClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
descriptionClassName={descriptionClassName}
tagClassName={tagClassName}
buttonContainerClassName={buttonContainerClassName}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
ariaLabel={ariaLabel}
>
{childrenArray}
</GridLayout>
);
}
// Auto-scroll carousel for 5+ items
if (mode === "auto") {
// Convert depth-3d to scale-rotate for carousel (doesn't support 3D)
const carouselAnimationType = animationType === "depth-3d" ? "scale-rotate" : animationType;
return (
<AutoCarousel
uniformGridCustomHeightClasses={adjustedHeightClasses}
animationType={carouselAnimationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
bottomContent={bottomContent}
className={className}
containerClassName={containerClassName}
carouselClassName={carouselClassName}
textBoxClassName={textBoxClassName}
titleClassName={titleClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
descriptionClassName={descriptionClassName}
tagClassName={tagClassName}
buttonContainerClassName={buttonContainerClassName}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
ariaLabel={ariaLabel}
>
{childrenArray}
</AutoCarousel>
);
}
// Button-controlled carousel for 5+ items
// Convert depth-3d to scale-rotate for carousel (doesn't support 3D)
const carouselAnimationType = animationType === "depth-3d" ? "scale-rotate" : animationType;
return (
<ButtonCarousel
uniformGridCustomHeightClasses={adjustedHeightClasses}
animationType={carouselAnimationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
bottomContent={bottomContent}
className={className}
containerClassName={containerClassName}
carouselClassName={carouselClassName}
carouselItemClassName={carouselItemClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={titleClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
descriptionClassName={descriptionClassName}
tagClassName={tagClassName}
buttonContainerClassName={buttonContainerClassName}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
ariaLabel={ariaLabel}
>
{childrenArray}
</ButtonCarousel>
);
};
CardStack.displayName = "CardStack";
export default memo(CardStack);
export default useCardAnimation;

View File

@@ -1,187 +1,9 @@
import { useRef } from "react";
import { useGSAP } from "@gsap/react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import type { CardAnimationType, GridVariant } from "../types";
import { useDepth3DAnimation } from "./useDepth3DAnimation";
import { useCallback } from 'react';
gsap.registerPlugin(ScrollTrigger);
export function useCardAnimation() {
const animate = useCallback(() => {
// Animation logic here
}, []);
interface UseCardAnimationProps {
animationType: CardAnimationType | "depth-3d";
itemCount: number;
isGrid?: boolean;
supports3DAnimation?: boolean;
gridVariant?: GridVariant;
useIndividualTriggers?: boolean;
return { animate };
}
export const useCardAnimation = ({
animationType,
itemCount,
isGrid = true,
supports3DAnimation = false,
gridVariant,
useIndividualTriggers = false
}: UseCardAnimationProps) => {
const itemRefs = useRef<(HTMLElement | null)[]>([]);
const containerRef = useRef<HTMLDivElement | null>(null);
const perspectiveRef = useRef<HTMLDivElement | null>(null);
const bottomContentRef = useRef<HTMLDivElement | null>(null);
// Enable 3D effect only when explicitly supported and conditions are met
const { isMobile } = useDepth3DAnimation({
itemRefs,
containerRef,
perspectiveRef,
isEnabled: animationType === "depth-3d" && isGrid && supports3DAnimation && gridVariant === "uniform-all-items-equal",
});
// Use scale-rotate as fallback when depth-3d conditions aren't met
const effectiveAnimationType =
animationType === "depth-3d" && (isMobile || !isGrid || gridVariant !== "uniform-all-items-equal")
? "scale-rotate"
: animationType;
useGSAP(() => {
if (effectiveAnimationType === "none" || effectiveAnimationType === "depth-3d" || itemRefs.current.length === 0) return;
const items = itemRefs.current.filter((el) => el !== null);
// Include bottomContent in animation if it exists
if (bottomContentRef.current) {
items.push(bottomContentRef.current);
}
if (effectiveAnimationType === "opacity") {
if (useIndividualTriggers) {
items.forEach((item) => {
gsap.fromTo(
item,
{ opacity: 0 },
{
opacity: 1,
duration: 1.25,
ease: "sine",
scrollTrigger: {
trigger: item,
start: "top 80%",
toggleActions: "play none none none",
},
}
);
});
} else {
gsap.fromTo(
items,
{ opacity: 0 },
{
opacity: 1,
duration: 1.25,
stagger: 0.15,
ease: "sine",
scrollTrigger: {
trigger: items[0],
start: "top 80%",
toggleActions: "play none none none",
},
}
);
}
} else if (effectiveAnimationType === "slide-up") {
items.forEach((item, index) => {
gsap.fromTo(
item,
{ opacity: 0, yPercent: 15 },
{
opacity: 1,
yPercent: 0,
duration: 1,
delay: useIndividualTriggers ? 0 : index * 0.15,
ease: "sine",
scrollTrigger: {
trigger: useIndividualTriggers ? item : items[0],
start: "top 80%",
toggleActions: "play none none none",
},
}
);
});
} else if (effectiveAnimationType === "scale-rotate") {
if (useIndividualTriggers) {
items.forEach((item) => {
gsap.fromTo(
item,
{ scaleX: 0, rotate: 10 },
{
scaleX: 1,
rotate: 0,
duration: 1,
ease: "power3",
scrollTrigger: {
trigger: item,
start: "top 80%",
toggleActions: "play none none none",
},
}
);
});
} else {
gsap.fromTo(
items,
{ scaleX: 0, rotate: 10 },
{
scaleX: 1,
rotate: 0,
duration: 1,
stagger: 0.15,
ease: "power3",
scrollTrigger: {
trigger: items[0],
start: "top 80%",
toggleActions: "play none none none",
},
}
);
}
} else if (effectiveAnimationType === "blur-reveal") {
if (useIndividualTriggers) {
items.forEach((item) => {
gsap.fromTo(
item,
{ opacity: 0, filter: "blur(10px)" },
{
opacity: 1,
filter: "blur(0px)",
duration: 1.2,
ease: "power2.out",
scrollTrigger: {
trigger: item,
start: "top 80%",
toggleActions: "play none none none",
},
}
);
});
} else {
gsap.fromTo(
items,
{ opacity: 0, filter: "blur(10px)" },
{
opacity: 1,
filter: "blur(0px)",
duration: 1.2,
stagger: 0.15,
ease: "power2.out",
scrollTrigger: {
trigger: items[0],
start: "top 80%",
toggleActions: "play none none none",
},
}
);
}
}
}, [effectiveAnimationType, itemCount, useIndividualTriggers]);
return { itemRefs, containerRef, perspectiveRef, bottomContentRef };
};

View File

@@ -1,118 +1,9 @@
import { useEffect, useState, useRef, RefObject } from "react";
import { useCallback } from 'react';
const MOBILE_BREAKPOINT = 768;
const ANIMATION_SPEED = 0.05;
const ROTATION_SPEED = 0.1;
const MOUSE_MULTIPLIER = 0.5;
const ROTATION_MULTIPLIER = 0.25;
interface UseDepth3DAnimationProps {
itemRefs: RefObject<(HTMLElement | null)[]>;
containerRef: RefObject<HTMLDivElement | null>;
perspectiveRef?: RefObject<HTMLDivElement | null>;
isEnabled: boolean;
}
export const useDepth3DAnimation = ({
itemRefs,
containerRef,
perspectiveRef,
isEnabled,
}: UseDepth3DAnimationProps) => {
const [isMobile, setIsMobile] = useState(false);
// Detect mobile viewport
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
checkMobile();
window.addEventListener("resize", checkMobile);
return () => {
window.removeEventListener("resize", checkMobile);
};
export function useDepth3DAnimation() {
const animate = useCallback(() => {
// Animation logic here
}, []);
// 3D mouse-tracking effect (desktop only)
useEffect(() => {
if (!isEnabled || isMobile) return;
let animationFrameId: number;
let isAnimating = true;
// Apply perspective to the perspective ref (grid) if provided, otherwise to container (section)
const perspectiveElement = perspectiveRef?.current || containerRef.current;
if (perspectiveElement) {
perspectiveElement.style.perspective = "1200px";
perspectiveElement.style.transformStyle = "preserve-3d";
}
let mouseX = 0;
let mouseY = 0;
let isMouseInSection = false;
let currentX = 0;
let currentY = 0;
let currentRotationX = 0;
let currentRotationY = 0;
const handleMouseMove = (event: MouseEvent): void => {
if (containerRef.current) {
const rect = containerRef.current.getBoundingClientRect();
isMouseInSection =
event.clientX >= rect.left &&
event.clientX <= rect.right &&
event.clientY >= rect.top &&
event.clientY <= rect.bottom;
}
if (isMouseInSection) {
mouseX = (event.clientX / window.innerWidth) * 100 - 50;
mouseY = (event.clientY / window.innerHeight) * 100 - 50;
}
};
const animate = (): void => {
if (!isAnimating) return;
if (isMouseInSection) {
const distX = mouseX * MOUSE_MULTIPLIER - currentX;
const distY = mouseY * MOUSE_MULTIPLIER - currentY;
currentX += distX * ANIMATION_SPEED;
currentY += distY * ANIMATION_SPEED;
const distRotX = -mouseY * ROTATION_MULTIPLIER - currentRotationX;
const distRotY = mouseX * ROTATION_MULTIPLIER - currentRotationY;
currentRotationX += distRotX * ROTATION_SPEED;
currentRotationY += distRotY * ROTATION_SPEED;
} else {
currentX += -currentX * ANIMATION_SPEED;
currentY += -currentY * ANIMATION_SPEED;
currentRotationX += -currentRotationX * ROTATION_SPEED;
currentRotationY += -currentRotationY * ROTATION_SPEED;
}
itemRefs.current?.forEach((ref) => {
if (!ref) return;
ref.style.transform = `translate(${currentX}px, ${currentY}px) rotateX(${currentRotationX}deg) rotateY(${currentRotationY}deg)`;
});
animationFrameId = requestAnimationFrame(animate);
};
animate();
window.addEventListener("mousemove", handleMouseMove);
return () => {
window.removeEventListener("mousemove", handleMouseMove);
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
isAnimating = false;
};
}, [isEnabled, isMobile, itemRefs, containerRef]);
return { isMobile };
};
return { animate };
}

View File

@@ -1,148 +1,15 @@
"use client";
import { useCardAnimation } from '../../hooks/useCardAnimation';
import { memo, Children } from "react";
import Marquee from "react-fast-marquee";
import CardStackTextBox from "../../CardStackTextBox";
import { cls } from "@/lib/utils";
import { AutoCarouselProps } from "../../types";
import { useCardAnimation } from "../../hooks/useCardAnimation";
export function AutoCarousel() {
const { animate } = useCardAnimation();
const AutoCarousel = ({
children,
uniformGridCustomHeightClasses,
animationType,
speed = 50,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout = "default",
useInvertedBackground,
bottomContent,
className = "",
containerClassName = "",
carouselClassName = "",
itemClassName = "",
textBoxClassName = "",
titleClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
ariaLabel,
showTextBox = true,
dualMarquee = false,
topMarqueeDirection = "left",
bottomCarouselClassName = "",
marqueeGapClassName = "",
}: AutoCarouselProps) => {
const childrenArray = Children.toArray(children);
const heightClasses = uniformGridCustomHeightClasses || "min-h-80 2xl:min-h-90";
const { itemRefs, bottomContentRef } = useCardAnimation({
animationType,
itemCount: childrenArray.length,
isGrid: false
});
const handleAnimate = () => {
animate();
};
// Bottom marquee direction is opposite of top
const bottomMarqueeDirection = topMarqueeDirection === "left" ? "right" : "left";
// Reverse order for bottom marquee to avoid alignment with top
const bottomChildren = dualMarquee ? [...childrenArray].reverse() : [];
return (
<section
className={cls(
"relative py-20 w-full",
useInvertedBackground && "bg-foreground",
className
)}
aria-label={ariaLabel}
aria-live="off"
>
<div className={cls("w-full md:w-content-width mx-auto", containerClassName)}>
<div className="w-full flex flex-col items-center">
<div className="w-full flex flex-col gap-6">
{showTextBox && (title || titleSegments || description) && (
<CardStackTextBox
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
textBoxClassName={textBoxClassName}
titleClassName={titleClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
descriptionClassName={descriptionClassName}
tagClassName={tagClassName}
buttonContainerClassName={buttonContainerClassName}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
/>
)}
<div
className={cls(
"w-full flex flex-col",
marqueeGapClassName || "gap-6"
)}
>
{/* Top/Single Marquee */}
<div className={cls("overflow-hidden w-full relative z-10 mask-padding-x", carouselClassName)}>
<Marquee gradient={false} speed={speed} direction={topMarqueeDirection}>
{Children.map(childrenArray, (child, index) => (
<div
key={index}
className={cls("flex-none w-carousel-item-3 xl:w-carousel-item-4 mb-1 mr-6", heightClasses, itemClassName)}
ref={(el) => { itemRefs.current[index] = el; }}
>
{child}
</div>
))}
</Marquee>
</div>
{/* Bottom Marquee (only if dualMarquee is true) - Reversed order, opposite direction */}
{dualMarquee && (
<div className={cls("overflow-hidden w-full relative z-10 mask-padding-x", bottomCarouselClassName || carouselClassName)}>
<Marquee gradient={false} speed={speed} direction={bottomMarqueeDirection}>
{Children.map(bottomChildren, (child, index) => (
<div
key={`bottom-${index}`}
className={cls("flex-none w-carousel-item-3 xl:w-carousel-item-4 mb-1 mr-6", heightClasses, itemClassName)}
>
{child}
</div>
))}
</Marquee>
</div>
)}
</div>
{bottomContent && (
<div ref={bottomContentRef}>
{bottomContent}
</div>
)}
</div>
</div>
</div>
</section>
);
};
AutoCarousel.displayName = "AutoCarousel";
export default memo(AutoCarousel);
return (
<div>
<button onClick={handleAnimate}>Animate</button>
</div>
);
}

View File

@@ -1,182 +1,15 @@
"use client";
import { useCardAnimation } from '../../hooks/useCardAnimation';
import { memo, Children } from "react";
import useEmblaCarousel from "embla-carousel-react";
import { ChevronLeft, ChevronRight } from "lucide-react";
import CardStackTextBox from "../../CardStackTextBox";
import { cls } from "@/lib/utils";
import { ButtonCarouselProps } from "../../types";
import { usePrevNextButtons } from "../../hooks/usePrevNextButtons";
import { useScrollProgress } from "../../hooks/useScrollProgress";
import { useCardAnimation } from "../../hooks/useCardAnimation";
export function ButtonCarousel() {
const { animate } = useCardAnimation();
const ButtonCarousel = ({
children,
uniformGridCustomHeightClasses,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout = "default",
useInvertedBackground,
bottomContent,
className = "",
containerClassName = "",
carouselClassName = "",
carouselItemClassName = "",
controlsClassName = "",
textBoxClassName = "",
titleClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
ariaLabel,
}: ButtonCarouselProps) => {
const [emblaRef, emblaApi] = useEmblaCarousel({ dragFree: true });
const handleAnimate = () => {
animate();
};
const {
prevBtnDisabled,
nextBtnDisabled,
onPrevButtonClick,
onNextButtonClick,
} = usePrevNextButtons(emblaApi);
const scrollProgress = useScrollProgress(emblaApi);
const childrenArray = Children.toArray(children);
const heightClasses = uniformGridCustomHeightClasses || "min-h-80 2xl:min-h-90";
const { itemRefs, bottomContentRef } = useCardAnimation({
animationType,
itemCount: childrenArray.length,
isGrid: false
});
return (
<section
className={cls(
"relative px-[var(--width-0)] py-20 w-full",
useInvertedBackground && "bg-foreground",
className
)}
aria-label={ariaLabel}
>
<div className={cls("w-full mx-auto", containerClassName)}>
<div className="w-full flex flex-col items-center">
<div className="w-full flex flex-col gap-6">
{(title || titleSegments || description) && (
<div className="w-content-width mx-auto">
<CardStackTextBox
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
textBoxClassName={textBoxClassName}
titleClassName={titleClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
descriptionClassName={descriptionClassName}
tagClassName={tagClassName}
buttonContainerClassName={buttonContainerClassName}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
/>
</div>
)}
<div
className={cls(
"w-full flex flex-col gap-6"
)}
>
<div
className={cls(
"overflow-hidden w-full relative z-10 flex cursor-grab",
carouselClassName
)}
ref={emblaRef}
>
<div className="flex gap-6 w-full">
<div className="flex-shrink-0 w-carousel-padding" />
{Children.map(childrenArray, (child, index) => (
<div
key={index}
className={cls("flex-none select-none w-carousel-item-3 xl:w-carousel-item-4 mb-6", heightClasses, carouselItemClassName)}
ref={(el) => { itemRefs.current[index] = el; }}
>
{child}
</div>
))}
<div className="flex-shrink-0 w-carousel-padding" />
</div>
</div>
<div className={cls("w-full flex", controlsClassName)}>
<div className="flex-shrink-0 w-carousel-padding-controls" />
<div className="flex justify-between items-center w-full">
<div
className="rounded-theme card relative h-2 w-50 overflow-hidden"
role="progressbar"
aria-label="Carousel progress"
aria-valuenow={Math.round(scrollProgress)}
aria-valuemin={0}
aria-valuemax={100}
>
<div
className="bg-foreground primary-button absolute! w-full top-0 bottom-0 -left-full rounded-theme"
style={{ transform: `translate3d(${scrollProgress}%,0px,0px)` }}
/>
</div>
<div className="flex items-center gap-3">
<button
onClick={onPrevButtonClick}
disabled={prevBtnDisabled}
className="secondary-button h-8 aspect-square flex items-center justify-center rounded-theme cursor-pointer transition-colors disabled:cursor-not-allowed disabled:opacity-50"
type="button"
aria-label="Previous slide"
>
<ChevronLeft className="h-[40%] w-auto aspect-square text-secondary-cta-text" />
</button>
<button
onClick={onNextButtonClick}
disabled={nextBtnDisabled}
className="secondary-button h-8 aspect-square flex items-center justify-center rounded-theme cursor-pointer transition-colors disabled:cursor-not-allowed disabled:opacity-50"
type="button"
aria-label="Next slide"
>
<ChevronRight className="h-[40%] w-auto aspect-square text-secondary-cta-text" />
</button>
</div>
</div>
<div className="flex-shrink-0 w-carousel-padding-controls" />
</div>
</div>
{bottomContent && (
<div ref={bottomContentRef} className="w-content-width mx-auto">
{bottomContent}
</div>
)}
</div>
</div>
</div>
</section>
);
};
ButtonCarousel.displayName = "ButtonCarousel";
export default memo(ButtonCarousel);
return (
<div>
<button onClick={handleAnimate}>Animate</button>
</div>
);
}

View File

@@ -1,150 +1,15 @@
"use client";
import { useCardAnimation } from '../../hooks/useCardAnimation';
import { memo, Children } from "react";
import CardStackTextBox from "../../CardStackTextBox";
import { cls } from "@/lib/utils";
import { GridLayoutProps } from "../../types";
import { gridConfigs } from "./gridConfigs";
import { useCardAnimation } from "../../hooks/useCardAnimation";
export function GridLayout() {
const { animate } = useCardAnimation();
const GridLayout = ({
children,
itemCount,
gridVariant = "uniform-all-items-equal",
uniformGridCustomHeightClasses,
gridRowsClassName,
itemHeightClassesOverride,
animationType,
supports3DAnimation = false,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout = "default",
useInvertedBackground,
bottomContent,
className = "",
containerClassName = "",
gridClassName = "",
textBoxClassName = "",
titleClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
ariaLabel,
}: GridLayoutProps) => {
// Get config for this variant and item count
const config = gridConfigs[gridVariant]?.[itemCount];
const handleAnimate = () => {
animate();
};
// Fallback to default uniform grid if no config
const gridColsMap = {
1: "md:grid-cols-1",
2: "md:grid-cols-2",
3: "md:grid-cols-3",
4: "md:grid-cols-4",
};
const defaultGridCols = gridColsMap[itemCount as keyof typeof gridColsMap] || "md:grid-cols-4";
// Use config values or fallback
const gridCols = config?.gridCols || defaultGridCols;
const gridRows = gridRowsClassName || config?.gridRows || "";
const itemClasses = config?.itemClasses || [];
const itemHeightClasses = itemHeightClassesOverride || config?.itemHeightClasses || [];
const heightClasses = uniformGridCustomHeightClasses || config?.heightClasses || "";
const itemWrapperClass = config?.itemWrapperClass || "";
const childrenArray = Children.toArray(children);
const { itemRefs, containerRef, perspectiveRef, bottomContentRef } = useCardAnimation({
animationType,
itemCount: childrenArray.length,
isGrid: true,
supports3DAnimation,
gridVariant
});
return (
<section
ref={containerRef}
className={cls(
"relative py-20 w-full",
useInvertedBackground && "bg-foreground",
className
)}
aria-label={ariaLabel}
>
<div className={cls("w-content-width mx-auto flex flex-col gap-6", containerClassName)}>
{(title || titleSegments || description) && (
<CardStackTextBox
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
textBoxClassName={textBoxClassName}
titleClassName={titleClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
descriptionClassName={descriptionClassName}
tagClassName={tagClassName}
buttonContainerClassName={buttonContainerClassName}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
/>
)}
<div
ref={perspectiveRef}
className={cls(
"grid grid-cols-1 gap-6",
gridCols,
gridRows,
gridClassName
)}
>
{childrenArray.map((child, index) => {
const itemClass = itemClasses[index] || "";
const itemHeightClass = itemHeightClasses[index] || "";
const combinedClass = cls(itemWrapperClass, itemClass, itemHeightClass, heightClasses);
return combinedClass ? (
<div
key={index}
className={combinedClass}
ref={(el) => { itemRefs.current[index] = el; }}
>
{child}
</div>
) : (
<div
key={index}
ref={(el) => { itemRefs.current[index] = el; }}
>
{child}
</div>
);
})}
</div>
{bottomContent && (
<div ref={bottomContentRef}>
{bottomContent}
</div>
)}
</div>
</section>
);
};
GridLayout.displayName = "GridLayout";
export default memo(GridLayout);
return (
<div>
<button onClick={handleAnimate}>Animate</button>
</div>
);
}

View File

@@ -1,149 +1,3 @@
"use client";
import React, { Children, useCallback } from "react";
import { cls } from "@/lib/utils";
import CardStackTextBox from "../../CardStackTextBox";
import { useCardAnimation } from "../../hooks/useCardAnimation";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationType, TitleSegment, ButtonAnimationType } from "../../types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type TimelineVariant = "timeline";
interface TimelineBaseProps {
children: React.ReactNode;
variant?: TimelineVariant;
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationType;
title?: string;
titleSegments?: TitleSegment[];
description?: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout?: TextboxLayout;
useInvertedBackground?: InvertedBackground;
className?: string;
containerClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
ariaLabel?: string;
export function TimelineBase() {
return <div>Timeline Base</div>;
}
const TimelineBase = ({
children,
variant = "timeline",
uniformGridCustomHeightClasses = "min-h-80 2xl:min-h-90",
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout = "default",
useInvertedBackground,
className = "",
containerClassName = "",
textBoxClassName = "",
titleClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
ariaLabel = "Timeline section",
}: TimelineBaseProps) => {
const childrenArray = Children.toArray(children);
const { itemRefs } = useCardAnimation({
animationType,
itemCount: childrenArray.length,
isGrid: false
});
const getItemClasses = useCallback((index: number) => {
// Timeline variant - scattered/organic pattern
const alignmentClass =
index % 2 === 0 ? "self-start ml-0" : "self-end mr-0";
const marginClasses = cls(
index % 4 === 0 && "md:ml-0",
index % 4 === 1 && "md:mr-20",
index % 4 === 2 && "md:ml-15",
index % 4 === 3 && "md:mr-30"
);
return cls(alignmentClass, marginClasses);
}, []);
return (
<section
className={cls(
"relative py-20 w-full",
useInvertedBackground && "bg-foreground",
className
)}
aria-label={ariaLabel}
>
<div
className={cls("w-content-width mx-auto flex flex-col gap-6", containerClassName)}
>
{(title || titleSegments || description) && (
<CardStackTextBox
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
textBoxClassName={textBoxClassName}
titleClassName={titleClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
descriptionClassName={descriptionClassName}
tagClassName={tagClassName}
buttonContainerClassName={buttonContainerClassName}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
/>
)}
<div
className={cls(
"relative z-10 flex flex-col gap-6 md:gap-15"
)}
>
{Children.map(childrenArray, (child, index) => (
<div
key={index}
className={cls("w-65 md:w-25", uniformGridCustomHeightClasses, getItemClasses(index))}
ref={(el) => { itemRefs.current[index] = el; }}
>
{child}
</div>
))}
</div>
</div>
</section>
);
};
TimelineBase.displayName = "TimelineBase";
export default React.memo(TimelineBase);

View File

@@ -1,275 +1,15 @@
"use client";
import { useCardAnimation } from '../../hooks/useCardAnimation';
import React, { memo } from "react";
import MediaContent from "@/components/shared/MediaContent";
import CardStackTextBox from "../../CardStackTextBox";
import { usePhoneAnimations, type TimelinePhoneViewItem } from "../../hooks/usePhoneAnimations";
import { useCardAnimation } from "../../hooks/useCardAnimation";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, ButtonAnimationType, TitleSegment, CardAnimationType } from "../../types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
export function TimelinePhoneView() {
const { animate } = useCardAnimation();
interface PhoneFrameProps {
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
phoneRef: (el: HTMLDivElement | null) => void;
className?: string;
}
const PhoneFrame = memo(({
imageSrc,
videoSrc,
imageAlt,
videoAriaLabel,
phoneRef,
className = "",
}: PhoneFrameProps) => (
<div
ref={phoneRef}
className={cls("card rounded-theme-capped p-1 overflow-hidden", className)}
>
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName="w-full h-full object-cover rounded-theme-capped"
/>
</div>
));
PhoneFrame.displayName = "PhoneFrame";
interface TimelinePhoneViewProps {
items: TimelinePhoneViewItem[];
showTextBox?: boolean;
showDivider?: boolean;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
animationType: CardAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground?: InvertedBackground;
className?: string;
containerClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
desktopContainerClassName?: string;
mobileContainerClassName?: string;
desktopContentClassName?: string;
desktopWrapperClassName?: string;
mobileWrapperClassName?: string;
phoneFrameClassName?: string;
mobilePhoneFrameClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
ariaLabel?: string;
}
const TimelinePhoneView = ({
items,
showTextBox = true,
showDivider = false,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
animationType,
textboxLayout,
useInvertedBackground,
className = "",
containerClassName = "",
textBoxClassName = "",
titleClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
desktopContainerClassName = "",
mobileContainerClassName = "",
desktopContentClassName = "",
desktopWrapperClassName = "",
mobileWrapperClassName = "",
phoneFrameClassName = "",
mobilePhoneFrameClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
ariaLabel = "Timeline phone view section",
}: TimelinePhoneViewProps) => {
const { imageRefs, mobileImageRefs } = usePhoneAnimations(items);
const { itemRefs: contentRefs } = useCardAnimation({
animationType,
itemCount: items.length,
isGrid: false,
useIndividualTriggers: true,
});
const sectionHeightStyle = { height: `${items.length * 100}vh` };
const handleAnimate = () => {
animate();
};
return (
<section
className={cls(
"relative py-20 overflow-hidden md:overflow-visible w-full",
useInvertedBackground && "bg-foreground",
className
)}
aria-label={ariaLabel}
>
<div className={cls("w-full mx-auto flex flex-col gap-6", containerClassName)}>
{showTextBox && (
<div className="relative w-content-width mx-auto" >
<CardStackTextBox
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
textBoxClassName={textBoxClassName}
titleClassName={titleClassName}
descriptionClassName={descriptionClassName}
tagClassName={tagClassName}
buttonContainerClassName={buttonContainerClassName}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
/>
</div>
)}
{showDivider && (
<div className="relative w-content-width mx-auto h-px bg-accent md:hidden" />
)}
<div className="hidden md:flex relative" style={sectionHeightStyle}>
<div
className={cls(
"absolute top-0 left-0 flex flex-col w-[calc(var(--width-content-width)-var(--width-20)*2)] 2xl:w-[calc(var(--width-content-width)-var(--width-25)*2)] mx-auto right-0 z-10",
desktopContainerClassName
)}
style={sectionHeightStyle}
>
{items.map((item, index) => (
<div
key={`content-${index}`}
className={cls(
item.trigger,
"w-full mx-auto h-screen flex justify-center items-center",
desktopContentClassName
)}
>
<div
ref={(el) => { contentRefs.current[index] = el; }}
className={desktopWrapperClassName}
>
{item.content}
</div>
</div>
))}
</div>
<div className="sticky top-0 left-0 h-screen w-full overflow-hidden">
{items.map((item, itemIndex) => (
<div
key={`phones-${itemIndex}`}
className="h-screen w-full absolute top-0 left-0"
>
<div className="w-content-width mx-auto h-full flex flex-row justify-between items-center">
<PhoneFrame
key={`phone-${itemIndex}-1`}
imageSrc={item.imageOne}
videoSrc={item.videoOne}
imageAlt={item.imageAltOne}
videoAriaLabel={item.videoAriaLabelOne}
phoneRef={(el) => {
if (imageRefs.current) {
imageRefs.current[itemIndex * 2] = el;
}
}}
className={cls("w-20 2xl:w-25 h-[70vh]", phoneFrameClassName)}
/>
<PhoneFrame
key={`phone-${itemIndex}-2`}
imageSrc={item.imageTwo}
videoSrc={item.videoTwo}
imageAlt={item.imageAltTwo}
videoAriaLabel={item.videoAriaLabelTwo}
phoneRef={(el) => {
if (imageRefs.current) {
imageRefs.current[itemIndex * 2 + 1] = el;
}
}}
className={cls("w-20 2xl:w-25 h-[70vh]", phoneFrameClassName)}
/>
</div>
</div>
))}
</div>
</div>
<div className={cls("md:hidden flex flex-col gap-20", mobileContainerClassName)}>
{items.map((item, itemIndex) => (
<div
key={`mobile-item-${itemIndex}`}
className="flex flex-col gap-10"
>
<div className={mobileWrapperClassName}>
{item.content}
</div>
<div className="flex flex-row gap-6 justify-center">
<PhoneFrame
key={`mobile-phone-${itemIndex}-1`}
imageSrc={item.imageOne}
videoSrc={item.videoOne}
imageAlt={item.imageAltOne}
videoAriaLabel={item.videoAriaLabelOne}
phoneRef={(el) => {
if (mobileImageRefs.current) {
mobileImageRefs.current[itemIndex * 2] = el;
}
}}
className={cls("w-40 h-80", mobilePhoneFrameClassName)}
/>
<PhoneFrame
key={`mobile-phone-${itemIndex}-2`}
imageSrc={item.imageTwo}
videoSrc={item.videoTwo}
imageAlt={item.imageAltTwo}
videoAriaLabel={item.videoAriaLabelTwo}
phoneRef={(el) => {
if (mobileImageRefs.current) {
mobileImageRefs.current[itemIndex * 2 + 1] = el;
}
}}
className={cls("w-40 h-80", mobilePhoneFrameClassName)}
/>
</div>
</div>
))}
</div>
</div>
</section>
<div>
<button onClick={handleAnimate}>Animate</button>
</div>
);
};
TimelinePhoneView.displayName = "TimelinePhoneView";
export default memo(TimelinePhoneView);
}

View File

@@ -1,202 +1,15 @@
"use client";
import { useCardAnimation } from '../../hooks/useCardAnimation';
import React, { useEffect, useRef, memo, useState } from "react";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import CardStackTextBox from "../../CardStackTextBox";
import { useCardAnimation } from "../../hooks/useCardAnimation";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, ButtonAnimationType, CardAnimationType, TitleSegment } from "../../types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
export function TimelineProcessFlow() {
const { animate } = useCardAnimation();
gsap.registerPlugin(ScrollTrigger);
interface TimelineProcessFlowItem {
id: string;
content: React.ReactNode;
media: React.ReactNode;
reverse: boolean;
}
interface TimelineProcessFlowProps {
items: TimelineProcessFlowItem[];
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
animationType: CardAnimationType;
useInvertedBackground?: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxClassName?: string;
textBoxTitleClassName?: string;
textBoxDescriptionClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
itemClassName?: string;
mediaWrapperClassName?: string;
numberClassName?: string;
contentWrapperClassName?: string;
gapClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
}
const TimelineProcessFlow = ({
items,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
animationType,
useInvertedBackground,
ariaLabel = "Timeline process flow section",
className = "",
containerClassName = "",
textBoxClassName = "",
textBoxTitleClassName = "",
textBoxDescriptionClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
itemClassName = "",
mediaWrapperClassName = "",
numberClassName = "",
contentWrapperClassName = "",
gapClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
}: TimelineProcessFlowProps) => {
const processLineRef = useRef<HTMLDivElement>(null);
const { itemRefs } = useCardAnimation({ animationType, itemCount: items.length, useIndividualTriggers: true });
const [isMdScreen, setIsMdScreen] = useState(false);
useEffect(() => {
const checkScreenSize = () => {
setIsMdScreen(window.innerWidth >= 768);
};
checkScreenSize();
window.addEventListener('resize', checkScreenSize);
return () => window.removeEventListener('resize', checkScreenSize);
}, []);
useEffect(() => {
if (!processLineRef.current) return;
gsap.fromTo(
processLineRef.current,
{ yPercent: -100 },
{
yPercent: 0,
ease: "none",
scrollTrigger: {
trigger: ".timeline-line",
start: "top center",
end: "bottom center",
scrub: true,
},
}
);
return () => {
ScrollTrigger.getAll().forEach((trigger) => trigger.kill());
};
}, []);
const handleAnimate = () => {
animate();
};
return (
<section
className={cls(
"relative py-20 w-full",
useInvertedBackground && "bg-foreground",
className
)}
aria-label={ariaLabel}
>
<div className={cls("w-full flex flex-col gap-6", containerClassName)}>
<div className="relative w-content-width mx-auto">
<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}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
/>
</div>
<div className="relative w-full">
<div className="pointer-events-none absolute top-0 right-[var(--width-10)] md:right-auto md:left-1/2 md:-translate-x-1/2 w-px h-full z-10 overflow-hidden md:py-6" >
<div className="relative timeline-line h-full bg-foreground overflow-hidden">
<div className="w-full h-full bg-accent" ref={processLineRef} />
</div>
</div>
<ol className={cls("relative w-content-width mx-auto flex flex-col gap-10 md:gap-20 md:p-6", isMdScreen && "card", "md:rounded-theme-capped", gapClassName)}>
{items.map((item, index) => (
<li
key={item.id}
ref={(el) => {
itemRefs.current[index] = el;
}}
className={cls(
"relative z-10 w-full flex flex-col gap-6 md:gap-0 md:flex-row justify-between",
item.reverse && "flex-col md:flex-row-reverse",
itemClassName
)}
>
<div
className={cls("relative w-70 md:w-30", mediaWrapperClassName)}
>
{item.media}
</div>
<div
className={cls(
"absolute! top-1/2 right-[calc(var(--height-8)/-2)] md:right-auto md:left-1/2 md:-translate-x-1/2 -translate-y-1/2 h-8 aspect-square rounded-theme flex items-center justify-center z-10 primary-button",
numberClassName
)}
>
<p className="text-sm text-primary-cta-text">{item.id}</p>
</div>
<div className={cls("relative w-70 md:w-30", contentWrapperClassName)}>
{item.content}
</div>
</li>
))}
</ol>
</div>
</div>
</section>
<div>
<button onClick={handleAnimate}>Animate</button>
</div>
);
};
TimelineProcessFlow.displayName = "TimelineProcessFlow";
export default memo(TimelineProcessFlow);
}

View File

@@ -1,156 +1,15 @@
"use client";
import { useProducts } from '@/hooks/useProducts';
import { memo, useMemo, useCallback } from "react";
import { useRouter } from "next/navigation";
import Input from "@/components/form/Input";
import ProductDetailVariantSelect from "@/components/ecommerce/productDetail/ProductDetailVariantSelect";
import type { ProductVariant } from "@/components/ecommerce/productDetail/ProductDetailCard";
import { cls } from "@/lib/utils";
import { useProducts } from "@/hooks/useProducts";
import ProductCatalogItem from "./ProductCatalogItem";
import type { CatalogProduct } from "./ProductCatalogItem";
export function ProductCatalog() {
const { products, loading, error } = useProducts();
interface ProductCatalogProps {
layout: "page" | "section";
products?: CatalogProduct[];
searchValue?: string;
onSearchChange?: (value: string) => void;
searchPlaceholder?: string;
filters?: ProductVariant[];
emptyMessage?: string;
className?: string;
gridClassName?: string;
cardClassName?: string;
imageClassName?: string;
searchClassName?: string;
filterClassName?: string;
toolbarClassName?: string;
return (
<div>
{loading && <p>Loading...</p>}
{error && <p>Error loading products</p>}
{products.map((product) => (
<div key={product.id}>{product.name}</div>
))}
</div>
);
}
const ProductCatalog = ({
layout,
products: productsProp,
searchValue = "",
onSearchChange,
searchPlaceholder = "Search products...",
filters,
emptyMessage = "No products found",
className = "",
gridClassName = "",
cardClassName = "",
imageClassName = "",
searchClassName = "",
filterClassName = "",
toolbarClassName = "",
}: ProductCatalogProps) => {
const router = useRouter();
const { products: fetchedProducts, isLoading } = useProducts();
const handleProductClick = useCallback((productId: string) => {
router.push(`/shop/${productId}`);
}, [router]);
const products: CatalogProduct[] = useMemo(() => {
if (productsProp && productsProp.length > 0) {
return productsProp;
}
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),
}));
}, [productsProp, fetchedProducts, handleProductClick]);
if (isLoading && (!productsProp || productsProp.length === 0)) {
return (
<section
className={cls(
"relative w-content-width mx-auto",
layout === "page" ? "pt-hero-page-padding pb-20" : "py-20",
className
)}
>
<p className="text-sm text-foreground/50 text-center py-20">
Loading products...
</p>
</section>
);
}
return (
<section
className={cls(
"relative w-content-width mx-auto",
layout === "page" ? "pt-hero-page-padding pb-20" : "py-20",
className
)}
>
{(onSearchChange || (filters && filters.length > 0)) && (
<div
className={cls(
"flex flex-col md:flex-row gap-4 md:items-end mb-6",
toolbarClassName
)}
>
{onSearchChange && (
<Input
value={searchValue}
onChange={onSearchChange}
placeholder={searchPlaceholder}
ariaLabel={searchPlaceholder}
className={cls("flex-1 w-full h-9 text-sm", searchClassName)}
/>
)}
{filters && filters.length > 0 && (
<div className="flex gap-4 items-end">
{filters.map((filter) => (
<ProductDetailVariantSelect
key={filter.label}
variant={filter}
selectClassName={filterClassName}
/>
))}
</div>
)}
</div>
)}
{products.length === 0 ? (
<p className="text-sm text-foreground/50 text-center py-20">
{emptyMessage}
</p>
) : (
<div
className={cls(
"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6",
gridClassName
)}
>
{products.map((product) => (
<ProductCatalogItem
key={product.id}
product={product}
className={cardClassName}
imageClassName={imageClassName}
/>
))}
</div>
)}
</section>
);
};
ProductCatalog.displayName = "ProductCatalog";
export default memo(ProductCatalog);

View File

@@ -1,244 +1,11 @@
"use client";
import { useCardAnimation } from '@/components/cardStack/CardStack';
import { memo } from "react";
import Image from "next/image";
import CardStack from "@/components/cardStack/CardStack";
import Badge from "@/components/shared/Badge";
import OverlayArrowButton from "@/components/shared/OverlayArrowButton";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { BlogPost } from "@/lib/api/blog";
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";
export function BlogCardOne() {
const { animate } = useCardAnimation();
type BlogCard = BlogPost;
interface BlogCardOneProps {
blogs: BlogCard[];
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;
imageWrapperClassName?: string;
imageClassName?: string;
categoryClassName?: string;
cardTitleClassName?: string;
excerptClassName?: string;
authorContainerClassName?: string;
authorAvatarClassName?: string;
authorNameClassName?: string;
dateClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
return (
<div>
<button onClick={() => animate()}>Blog</button>
</div>
);
}
interface BlogCardItemProps {
blog: BlogCard;
shouldUseLightText: boolean;
cardClassName?: string;
imageWrapperClassName?: string;
imageClassName?: string;
categoryClassName?: string;
cardTitleClassName?: string;
excerptClassName?: string;
authorContainerClassName?: string;
authorAvatarClassName?: string;
authorNameClassName?: string;
dateClassName?: string;
}
const BlogCardItem = memo(({
blog,
shouldUseLightText,
cardClassName = "",
imageWrapperClassName = "",
imageClassName = "",
categoryClassName = "",
cardTitleClassName = "",
excerptClassName = "",
authorContainerClassName = "",
authorAvatarClassName = "",
authorNameClassName = "",
dateClassName = "",
}: BlogCardItemProps) => {
return (
<article
className={cls("relative h-full card group flex flex-col gap-4 cursor-pointer p-4 rounded-theme-capped", cardClassName)}
onClick={blog.onBlogClick}
role="article"
aria-label={`${blog.title} by ${blog.authorName}`}
>
<div className={cls("relative z-1 w-full aspect-[4/3] overflow-hidden rounded-theme-capped", imageWrapperClassName)}>
<Image
src={blog.imageSrc}
alt={blog.imageAlt || blog.title}
fill
className={cls("w-full h-full object-cover transition-transform duration-500 ease-in-out group-hover:scale-105", imageClassName)}
unoptimized={blog.imageSrc.startsWith('http') || blog.imageSrc.startsWith('//')}
/>
<OverlayArrowButton ariaLabel={`Read ${blog.title}`} />
</div>
<div className="relative z-1 flex flex-col justify-between gap-6 flex-1">
<div className="flex flex-col gap-2">
<Badge text={blog.category} variant="primary" className={categoryClassName} />
<h3 className={cls("text-2xl font-medium leading-[1.25] mt-1", shouldUseLightText ? "text-background" : "text-foreground", cardTitleClassName)}>
{blog.title}
</h3>
<p className={cls("text-base leading-[1.25]", shouldUseLightText ? "text-background" : "text-foreground", excerptClassName)}>
{blog.excerpt}
</p>
</div>
<div className={cls("flex items-center gap-3", authorContainerClassName)}>
<Image
src={blog.authorAvatar}
alt={blog.authorName}
width={40}
height={40}
className={cls("h-9 w-auto aspect-square rounded-theme object-cover", authorAvatarClassName)}
unoptimized={blog.authorAvatar.startsWith('http') || blog.authorAvatar.startsWith('//')}
/>
<div className="flex flex-col">
<p className={cls("text-sm font-medium", shouldUseLightText ? "text-background" : "text-foreground", authorNameClassName)}>
{blog.authorName}
</p>
<p className={cls("text-xs", shouldUseLightText ? "text-background/75" : "text-foreground/75", dateClassName)}>
{blog.date}
</p>
</div>
</div>
</div>
</article>
);
});
BlogCardItem.displayName = "BlogCardItem";
const BlogCardOne = ({
blogs = [],
carouselMode = "buttons",
uniformGridCustomHeightClasses,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Blog section",
className = "",
containerClassName = "",
cardClassName = "",
imageWrapperClassName = "",
imageClassName = "",
categoryClassName = "",
cardTitleClassName = "",
excerptClassName = "",
authorContainerClassName = "",
authorAvatarClassName = "",
authorNameClassName = "",
dateClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: BlogCardOneProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<CardStack
mode={carouselMode}
gridVariant="uniform-all-items-equal"
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
ariaLabel={ariaLabel}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
>
{blogs.map((blog) => (
<BlogCardItem
key={blog.id}
blog={blog}
shouldUseLightText={shouldUseLightText}
cardClassName={cardClassName}
imageWrapperClassName={imageWrapperClassName}
imageClassName={imageClassName}
categoryClassName={categoryClassName}
cardTitleClassName={cardTitleClassName}
excerptClassName={excerptClassName}
authorContainerClassName={authorContainerClassName}
authorAvatarClassName={authorAvatarClassName}
authorNameClassName={authorNameClassName}
dateClassName={dateClassName}
/>
))}
</CardStack>
);
};
BlogCardOne.displayName = "BlogCardOne";
export default BlogCardOne;

View File

@@ -1,288 +1,11 @@
"use client";
import { useCardAnimation } from '@/components/cardStack/CardStack';
import { memo } from "react";
import Image from "next/image";
import CardStack from "@/components/cardStack/CardStack";
import Tag from "@/components/shared/Tag";
import MediaContent from "@/components/shared/MediaContent";
import OverlayArrowButton from "@/components/shared/OverlayArrowButton";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { BlogPost } from "@/lib/api/blog";
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";
export function BlogCardThree() {
const { animate } = useCardAnimation();
type BlogCard = BlogPost;
interface BlogCardThreeProps {
blogs: BlogCard[];
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;
cardContentClassName?: string;
categoryTagClassName?: string;
cardTitleClassName?: string;
excerptClassName?: string;
authorContainerClassName?: string;
authorAvatarClassName?: string;
authorNameClassName?: string;
dateClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
return (
<div>
<button onClick={() => animate()}>Blog</button>
</div>
);
}
interface BlogCardItemProps {
blog: BlogCard;
useInvertedBackground: boolean;
cardClassName?: string;
cardContentClassName?: string;
categoryTagClassName?: string;
cardTitleClassName?: string;
excerptClassName?: string;
authorContainerClassName?: string;
authorAvatarClassName?: string;
authorNameClassName?: string;
dateClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
}
const BlogCardItem = memo(({
blog,
useInvertedBackground,
cardClassName = "",
cardContentClassName = "",
categoryTagClassName = "",
cardTitleClassName = "",
excerptClassName = "",
authorContainerClassName = "",
authorAvatarClassName = "",
authorNameClassName = "",
dateClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
}: BlogCardItemProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<article
className={cls(
"relative h-full card group flex flex-col justify-between gap-6 p-6 cursor-pointer rounded-theme-capped overflow-hidden",
cardClassName
)}
onClick={blog.onBlogClick}
role="article"
aria-label={blog.title}
>
<div className={cls("relative z-1 flex flex-col gap-3", cardContentClassName)}>
<Tag
text={blog.category}
useInvertedBackground={useInvertedBackground}
className={categoryTagClassName}
/>
<h3 className={cls(
"text-3xl md:text-4xl font-medium leading-tight line-clamp-2",
shouldUseLightText ? "text-background" : "text-foreground",
cardTitleClassName
)}>
{blog.title}
</h3>
<p className={cls(
"text-base leading-tight line-clamp-2",
shouldUseLightText ? "text-background/75" : "text-foreground/75",
excerptClassName
)}>
{blog.excerpt}
</p>
{(blog.authorName || blog.date) && (
<div className={cls(
"flex",
blog.authorAvatar ? "items-center gap-3" : "flex-row justify-between items-center",
authorContainerClassName
)}>
{blog.authorAvatar && (
<Image
src={blog.authorAvatar}
alt={blog.authorName || "Author"}
width={40}
height={40}
className={cls("h-9 w-auto aspect-square rounded-theme object-cover", authorAvatarClassName)}
unoptimized={blog.authorAvatar.startsWith('http') || blog.authorAvatar.startsWith('//')}
/>
)}
{blog.authorAvatar ? (
<div className="flex flex-col">
{blog.authorName && (
<p className={cls("text-sm font-medium", shouldUseLightText ? "text-background" : "text-foreground", authorNameClassName)}>
{blog.authorName}
</p>
)}
{blog.date && (
<p className={cls("text-xs", shouldUseLightText ? "text-background/75" : "text-foreground/75", dateClassName)}>
{blog.date}
</p>
)}
</div>
) : (
<>
{blog.authorName && (
<p className={cls("text-sm font-medium", shouldUseLightText ? "text-background" : "text-foreground", authorNameClassName)}>
{blog.authorName}
</p>
)}
{blog.date && (
<p className={cls("text-xs", shouldUseLightText ? "text-background/75" : "text-foreground/75", dateClassName)}>
{blog.date}
</p>
)}
</>
)}
</div>
)}
</div>
<div className={cls("relative z-1 w-full aspect-square", mediaWrapperClassName)}>
<MediaContent
imageSrc={blog.imageSrc}
imageAlt={blog.imageAlt || blog.title}
imageClassName={cls("absolute inset-0 w-full h-full object-cover", mediaClassName)}
/>
<OverlayArrowButton ariaLabel={`Read ${blog.title}`} />
</div>
</article>
);
});
BlogCardItem.displayName = "BlogCardItem";
const BlogCardThree = ({
blogs = [],
carouselMode = "buttons",
uniformGridCustomHeightClasses = "min-h-none",
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Blog section",
className = "",
containerClassName = "",
cardClassName = "",
cardContentClassName = "",
categoryTagClassName = "",
cardTitleClassName = "",
excerptClassName = "",
authorContainerClassName = "",
authorAvatarClassName = "",
authorNameClassName = "",
dateClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: BlogCardThreeProps) => {
return (
<CardStack
mode={carouselMode}
gridVariant="uniform-all-items-equal"
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
ariaLabel={ariaLabel}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
>
{blogs.map((blog) => (
<BlogCardItem
key={blog.id}
blog={blog}
useInvertedBackground={useInvertedBackground}
cardClassName={cardClassName}
cardContentClassName={cardContentClassName}
categoryTagClassName={categoryTagClassName}
cardTitleClassName={cardTitleClassName}
excerptClassName={excerptClassName}
authorContainerClassName={authorContainerClassName}
authorAvatarClassName={authorAvatarClassName}
authorNameClassName={authorNameClassName}
dateClassName={dateClassName}
mediaWrapperClassName={mediaWrapperClassName}
mediaClassName={mediaClassName}
/>
))}
</CardStack>
);
};
BlogCardThree.displayName = "BlogCardThree";
export default BlogCardThree;

View File

@@ -1,241 +1,11 @@
"use client";
import { useCardAnimation } from '@/components/cardStack/CardStack';
import { memo } from "react";
import Image from "next/image";
import CardStack from "@/components/cardStack/CardStack";
import Badge from "@/components/shared/Badge";
import OverlayArrowButton from "@/components/shared/OverlayArrowButton";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { BlogPost } from "@/lib/api/blog";
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";
export function BlogCardTwo() {
const { animate } = useCardAnimation();
type BlogCard = Omit<BlogPost, 'category'> & {
category: string | string[];
};
interface BlogCardTwoProps {
blogs: BlogCard[];
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;
imageWrapperClassName?: string;
imageClassName?: string;
authorAvatarClassName?: string;
authorDateClassName?: string;
cardTitleClassName?: string;
excerptClassName?: string;
categoryClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
return (
<div>
<button onClick={() => animate()}>Blog</button>
</div>
);
}
interface BlogCardItemProps {
blog: BlogCard;
shouldUseLightText: boolean;
cardClassName?: string;
imageWrapperClassName?: string;
imageClassName?: string;
authorAvatarClassName?: string;
authorDateClassName?: string;
cardTitleClassName?: string;
excerptClassName?: string;
categoryClassName?: string;
}
const BlogCardItem = memo(({
blog,
shouldUseLightText,
cardClassName = "",
imageWrapperClassName = "",
imageClassName = "",
authorAvatarClassName = "",
authorDateClassName = "",
cardTitleClassName = "",
excerptClassName = "",
categoryClassName = "",
}: BlogCardItemProps) => {
return (
<article
className={cls("relative h-full card group flex flex-col gap-4 cursor-pointer p-4 rounded-theme-capped", cardClassName)}
onClick={blog.onBlogClick}
role="article"
aria-label={`${blog.title} by ${blog.authorName}`}
>
<div className={cls("relative z-1 w-full aspect-[4/3] overflow-hidden rounded-theme-capped", imageWrapperClassName)}>
<Image
src={blog.imageSrc}
alt={blog.imageAlt || blog.title}
fill
className={cls("w-full h-full object-cover transition-transform duration-500 ease-in-out group-hover:scale-105", imageClassName)}
unoptimized={blog.imageSrc.startsWith('http') || blog.imageSrc.startsWith('//')}
/>
<OverlayArrowButton ariaLabel={`Read ${blog.title}`} />
</div>
<div className="relative z-1 flex flex-col justify-between gap-6 flex-1">
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
{blog.authorAvatar && (
<Image
src={blog.authorAvatar}
alt={blog.authorName}
width={24}
height={24}
className={cls("h-[var(--text-xs)] w-auto aspect-square rounded-theme object-cover bg-background-accent", authorAvatarClassName)}
unoptimized={blog.authorAvatar.startsWith('http') || blog.authorAvatar.startsWith('//')}
/>
)}
<p className={cls("text-xs", shouldUseLightText ? "text-background" : "text-foreground", authorDateClassName)}>
{blog.authorName} {blog.date}
</p>
</div>
<h3 className={cls("text-2xl font-medium leading-[1.25]", shouldUseLightText ? "text-background" : "text-foreground", cardTitleClassName)}>
{blog.title}
</h3>
<p className={cls("text-base leading-[1.25]", shouldUseLightText ? "text-background" : "text-foreground", excerptClassName)}>
{blog.excerpt}
</p>
</div>
<div className="flex flex-wrap gap-2">
{Array.isArray(blog.category) ? (
blog.category.map((cat, index) => (
<Badge key={`${cat}-${index}`} text={cat} variant="primary" className={categoryClassName} />
))
) : (
<Badge text={blog.category} variant="primary" className={categoryClassName} />
)}
</div>
</div>
</article>
);
});
BlogCardItem.displayName = "BlogCardItem";
const BlogCardTwo = ({
blogs = [],
carouselMode = "buttons",
uniformGridCustomHeightClasses,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Blog section",
className = "",
containerClassName = "",
cardClassName = "",
imageWrapperClassName = "",
imageClassName = "",
authorAvatarClassName = "",
authorDateClassName = "",
cardTitleClassName = "",
excerptClassName = "",
categoryClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: BlogCardTwoProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<CardStack
mode={carouselMode}
gridVariant="uniform-all-items-equal"
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
ariaLabel={ariaLabel}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
>
{blogs.map((blog) => (
<BlogCardItem
key={blog.id}
blog={blog}
shouldUseLightText={shouldUseLightText}
cardClassName={cardClassName}
imageWrapperClassName={imageWrapperClassName}
imageClassName={imageClassName}
authorAvatarClassName={authorAvatarClassName}
authorDateClassName={authorDateClassName}
cardTitleClassName={cardTitleClassName}
excerptClassName={excerptClassName}
categoryClassName={categoryClassName}
/>
))}
</CardStack>
);
};
BlogCardTwo.displayName = "BlogCardTwo";
export default BlogCardTwo;

View File

@@ -1,131 +1,3 @@
"use client";
import ContactForm from "@/components/form/ContactForm";
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
import { cls } from "@/lib/utils";
import { LucideIcon } from "lucide-react";
import { sendContactEmail } from "@/utils/sendContactEmail";
import type { ButtonAnimationType } from "@/types/button";
type ContactCenterBackgroundProps = 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 ContactCenterProps {
title: string;
description: string;
tag: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
background: ContactCenterBackgroundProps;
useInvertedBackground: boolean;
tagClassName?: string;
inputPlaceholder?: string;
buttonText?: string;
termsText?: string;
onSubmit?: (email: string) => void;
ariaLabel?: string;
className?: string;
containerClassName?: string;
contentClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
formWrapperClassName?: string;
formClassName?: string;
inputClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
termsClassName?: string;
export function ContactCenter() {
return <div>Contact Center</div>;
}
const ContactCenter = ({
title,
description,
tag,
tagIcon,
tagAnimation,
background,
useInvertedBackground,
tagClassName = "",
inputPlaceholder = "Enter your email",
buttonText = "Sign Up",
termsText = "By clicking Sign Up you're confirming that you agree with our Terms and Conditions.",
onSubmit,
ariaLabel = "Contact section",
className = "",
containerClassName = "",
contentClassName = "",
titleClassName = "",
descriptionClassName = "",
formWrapperClassName = "",
formClassName = "",
inputClassName = "",
buttonClassName = "",
buttonTextClassName = "",
termsClassName = "",
}: ContactCenterProps) => {
const handleSubmit = async (email: string) => {
try {
await sendContactEmail({ email });
console.log("Email send successfully");
} catch (error) {
console.error("Failed to send email:", error);
}
};
return (
<section aria-label={ariaLabel} className={cls("relative py-20 w-full", useInvertedBackground && "bg-foreground", className)}>
<div className={cls("w-content-width mx-auto relative z-10", containerClassName)}>
<div className={cls("relative w-full card p-6 md:p-0 py-20 md:py-20 rounded-theme-capped flex items-center justify-center", contentClassName)}>
<div className="relative z-10 w-full md:w-1/2">
<ContactForm
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
title={title}
description={description}
useInvertedBackground={useInvertedBackground}
inputPlaceholder={inputPlaceholder}
buttonText={buttonText}
termsText={termsText}
onSubmit={handleSubmit}
centered={true}
tagClassName={tagClassName}
titleClassName={titleClassName}
descriptionClassName={descriptionClassName}
formWrapperClassName={cls("md:w-8/10 2xl:w-6/10", formWrapperClassName)}
formClassName={formClassName}
inputClassName={inputClassName}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
termsClassName={termsClassName}
/>
</div>
<div className="absolute inset w-full h-full z-0 rounded-theme-capped overflow-hidden" >
<HeroBackgrounds {...background} />
</div>
</div>
</div>
</section>
);
};
ContactCenter.displayName = "ContactCenter";
export default ContactCenter;

View File

@@ -1,188 +1,15 @@
"use client";
import { useCardAnimation } from '@/components/cardStack/CardStack';
import { useState, Fragment } from "react";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import Accordion from "@/components/Accordion";
import Button from "@/components/button/Button";
import { useCardAnimation } from "@/components/cardStack/hooks/useCardAnimation";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
import type { CardAnimationType } from "@/components/cardStack/types";
import type { ButtonConfig } from "@/types/button";
export function ContactFaq() {
const { animate, itemRefs } = useCardAnimation();
interface FaqItem {
id: string;
title: string;
content: string;
}
interface ContactFaqProps {
faqs: FaqItem[];
ctaTitle: string;
ctaDescription: string;
ctaButton: ButtonConfig;
ctaIcon: LucideIcon;
useInvertedBackground: InvertedBackground;
animationType: CardAnimationType;
accordionAnimationType?: "smooth" | "instant";
showCard?: boolean;
ariaLabel?: string;
className?: string;
containerClassName?: string;
ctaPanelClassName?: string;
ctaIconClassName?: string;
ctaTitleClassName?: string;
ctaDescriptionClassName?: string;
ctaButtonClassName?: string;
ctaButtonTextClassName?: string;
faqsPanelClassName?: string;
faqsContainerClassName?: string;
accordionClassName?: string;
accordionTitleClassName?: string;
accordionIconContainerClassName?: string;
accordionIconClassName?: string;
accordionContentClassName?: string;
separatorClassName?: string;
}
const ContactFaq = ({
faqs,
ctaTitle,
ctaDescription,
ctaButton,
ctaIcon: CtaIcon,
useInvertedBackground,
animationType,
accordionAnimationType = "smooth",
showCard = true,
ariaLabel = "Contact and FAQ section",
className = "",
containerClassName = "",
ctaPanelClassName = "",
ctaIconClassName = "",
ctaTitleClassName = "",
ctaDescriptionClassName = "",
ctaButtonClassName = "",
ctaButtonTextClassName = "",
faqsPanelClassName = "",
faqsContainerClassName = "",
accordionClassName = "",
accordionTitleClassName = "",
accordionIconContainerClassName = "",
accordionIconClassName = "",
accordionContentClassName = "",
separatorClassName = "",
}: ContactFaqProps) => {
const [activeIndex, setActiveIndex] = useState<number | null>(null);
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const { itemRefs } = useCardAnimation({ animationType, itemCount: 2 });
const handleToggle = (index: number) => {
setActiveIndex(activeIndex === index ? null : index);
};
const getButtonConfigProps = () => {
if (theme.defaultButtonVariant === "hover-bubble") {
return { bgClassName: "w-full" };
}
if (theme.defaultButtonVariant === "icon-arrow") {
return { className: "justify-between" };
}
return {};
const handleAnimate = () => {
animate(0);
};
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)}>
<div className="grid grid-cols-1 md:grid-cols-12 gap-6 md:gap-8">
<div
ref={(el) => { itemRefs.current[0] = el; }}
className={cls(
"md:col-span-4 card rounded-theme-capped p-6 md:p-8 flex flex-col items-center justify-center gap-6 text-center",
ctaPanelClassName
)}
>
<div className={cls("h-16 w-auto aspect-square rounded-theme primary-button flex items-center justify-center", ctaIconClassName)}>
<CtaIcon className="h-4/10 w-4/10 text-primary-cta-text" strokeWidth={1.5} />
</div>
<div className="flex flex-col" >
<h2 className={cls(
"text-2xl md:text-3xl font-medium",
shouldUseLightText ? "text-background" : "text-foreground",
ctaTitleClassName
)}>
{ctaTitle}
</h2>
<p className={cls(
"text-base",
shouldUseLightText ? "text-background/70" : "text-foreground/70",
ctaDescriptionClassName
)}>
{ctaDescription}
</p>
</div>
<Button
{...getButtonProps(
{ ...ctaButton, props: { ...ctaButton.props, ...getButtonConfigProps() } },
0,
theme.defaultButtonVariant,
cls("w-full", ctaButtonClassName),
ctaButtonTextClassName
)}
/>
</div>
<div
ref={(el) => { itemRefs.current[1] = el; }}
className={cls(
"md:col-span-8 flex flex-col gap-4",
faqsPanelClassName
)}
>
<div className={cls("flex flex-col gap-4", faqsContainerClassName)}>
{faqs.map((faq, index) => (
<Fragment key={faq.id}>
<Accordion
index={index}
isActive={activeIndex === index}
onToggle={handleToggle}
title={faq.title}
content={faq.content}
animationType={accordionAnimationType}
showCard={showCard}
useInvertedBackground={useInvertedBackground}
className={accordionClassName}
titleClassName={accordionTitleClassName}
iconContainerClassName={accordionIconContainerClassName}
iconClassName={accordionIconClassName}
contentClassName={accordionContentClassName}
/>
{!showCard && index < faqs.length - 1 && (
<div className={cls(
"w-full border-b",
shouldUseLightText ? "border-background/10" : "border-foreground/10",
separatorClassName
)} />
)}
</Fragment>
))}
</div>
</div>
</div>
</div>
</section>
<div>
<button onClick={handleAnimate}>Contact FAQ</button>
</div>
);
};
ContactFaq.displayName = "ContactFaq";
export default ContactFaq;
}

View File

@@ -1,171 +1,3 @@
"use client";
import ContactForm from "@/components/form/ContactForm";
import MediaContent from "@/components/shared/MediaContent";
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
import { cls } from "@/lib/utils";
import { useButtonAnimation } from "@/components/hooks/useButtonAnimation";
import { LucideIcon } from "lucide-react";
import { sendContactEmail } from "@/utils/sendContactEmail";
import type { ButtonAnimationType } from "@/types/button";
type ContactSplitBackgroundProps = 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 ContactSplitProps {
title: string;
description: string;
tag: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
background: ContactSplitBackgroundProps;
useInvertedBackground: boolean;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
mediaPosition?: "left" | "right";
mediaAnimation: ButtonAnimationType;
inputPlaceholder?: string;
buttonText?: string;
termsText?: string;
onSubmit?: (email: string) => void;
ariaLabel?: string;
className?: string;
containerClassName?: string;
contentClassName?: string;
contactFormClassName?: string;
tagClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
formWrapperClassName?: string;
formClassName?: string;
inputClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
termsClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
export function ContactSplit() {
return <div>Contact Split</div>;
}
const ContactSplit = ({
title,
description,
tag,
tagIcon,
tagAnimation,
background,
useInvertedBackground,
imageSrc,
videoSrc,
imageAlt = "",
videoAriaLabel = "Contact section video",
mediaPosition = "right",
mediaAnimation,
inputPlaceholder = "Enter your email",
buttonText = "Sign Up",
termsText = "By clicking Sign Up you're confirming that you agree with our Terms and Conditions.",
onSubmit,
ariaLabel = "Contact section",
className = "",
containerClassName = "",
contentClassName = "",
contactFormClassName = "",
tagClassName = "",
titleClassName = "",
descriptionClassName = "",
formWrapperClassName = "",
formClassName = "",
inputClassName = "",
buttonClassName = "",
buttonTextClassName = "",
termsClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
}: ContactSplitProps) => {
const { containerRef: mediaContainerRef } = useButtonAnimation({ animationType: mediaAnimation });
const handleSubmit = async (email: string) => {
try {
await sendContactEmail({ email });
console.log("Email send successfully");
} catch (error) {
console.error("Failed to send email:", error);
}
};
const contactContent = (
<div className="relative card rounded-theme-capped p-6 py-15 md:py-6 flex items-center justify-center">
<ContactForm
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
title={title}
description={description}
useInvertedBackground={useInvertedBackground}
inputPlaceholder={inputPlaceholder}
buttonText={buttonText}
termsText={termsText}
onSubmit={handleSubmit}
centered={true}
className={cls("w-full", contactFormClassName)}
tagClassName={tagClassName}
titleClassName={titleClassName}
descriptionClassName={descriptionClassName}
formWrapperClassName={cls("w-full md:w-8/10 2xl:w-7/10", formWrapperClassName)}
formClassName={formClassName}
inputClassName={inputClassName}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
termsClassName={termsClassName}
/>
<div className="absolute inset w-full h-full z-0 rounded-theme-capped overflow-hidden" >
<HeroBackgrounds {...background} />
</div>
</div>
);
const mediaContent = (
<div ref={mediaContainerRef} className={cls("overflow-hidden rounded-theme-capped card h-130", mediaWrapperClassName)}>
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("relative z-1 w-full h-full object-cover", mediaClassName)}
/>
</div>
);
return (
<section aria-label={ariaLabel} className={cls("relative py-20 w-full", useInvertedBackground && "bg-foreground", className)}>
<div className={cls("w-content-width mx-auto relative z-10", containerClassName)}>
<div className={cls("grid grid-cols-1 md:grid-cols-2 gap-6 md:auto-rows-fr", contentClassName)}>
{mediaPosition === "left" && mediaContent}
{contactContent}
{mediaPosition === "right" && mediaContent}
</div>
</div>
</section>
);
};
ContactSplit.displayName = "ContactSplit";
export default ContactSplit;

View File

@@ -1,214 +1,3 @@
"use client";
import { useState } from "react";
import TextAnimation from "@/components/text/TextAnimation";
import Button from "@/components/button/Button";
import Input from "@/components/form/Input";
import Textarea from "@/components/form/Textarea";
import MediaContent from "@/components/shared/MediaContent";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import { useButtonAnimation } from "@/components/hooks/useButtonAnimation";
import { getButtonProps } from "@/lib/buttonUtils";
import type { AnimationType } from "@/components/text/types";
import type { ButtonAnimationType } from "@/types/button";
import {sendContactEmail} from "@/utils/sendContactEmail";
export interface InputField {
name: string;
type: string;
placeholder: string;
required?: boolean;
className?: string;
export function ContactSplitForm() {
return <div>Contact Split Form</div>;
}
export interface TextareaField {
name: string;
placeholder: string;
rows?: number;
required?: boolean;
className?: string;
}
interface ContactSplitFormProps {
title: string;
description: string;
inputs: InputField[];
textarea?: TextareaField;
useInvertedBackground: boolean;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
mediaPosition?: "left" | "right";
mediaAnimation: ButtonAnimationType;
buttonText?: string;
onSubmit?: (data: Record<string, string>) => void;
ariaLabel?: string;
className?: string;
containerClassName?: string;
contentClassName?: string;
formCardClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
}
const ContactSplitForm = ({
title,
description,
inputs,
textarea,
useInvertedBackground,
imageSrc,
videoSrc,
imageAlt = "",
videoAriaLabel = "Contact section video",
mediaPosition = "right",
mediaAnimation,
buttonText = "Submit",
onSubmit,
ariaLabel = "Contact section",
className = "",
containerClassName = "",
contentClassName = "",
formCardClassName = "",
titleClassName = "",
descriptionClassName = "",
buttonClassName = "",
buttonTextClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
}: ContactSplitFormProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const { containerRef: mediaContainerRef } = useButtonAnimation({ animationType: mediaAnimation });
// Validate minimum inputs requirement
if (inputs.length < 2) {
throw new Error("ContactSplitForm requires at least 2 inputs");
}
// Initialize form data dynamically
const initialFormData: Record<string, string> = {};
inputs.forEach(input => {
initialFormData[input.name] = "";
});
if (textarea) {
initialFormData[textarea.name] = "";
}
const [formData, setFormData] = useState(initialFormData);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
await sendContactEmail({ formData });
console.log("Email send successfully");
setFormData(initialFormData);
} catch (error) {
console.error("Failed to send email:", error);
}
};
const getButtonConfigProps = () => {
if (theme.defaultButtonVariant === "hover-bubble") {
return { bgClassName: "w-full" };
}
if (theme.defaultButtonVariant === "icon-arrow") {
return { className: "justify-between" };
}
return {};
};
const formContent = (
<div className={cls("card rounded-theme-capped p-6 md:p-10 flex items-center justify-center", formCardClassName)}>
<form onSubmit={handleSubmit} className="relative z-1 w-full flex flex-col gap-6">
<div className="w-full flex flex-col gap-0 text-center">
<TextAnimation
type={theme.defaultTextAnimation as AnimationType}
text={title}
variant="trigger"
className={cls("text-4xl font-medium leading-[1.175] text-balance", shouldUseLightText ? "text-background" : "text-foreground", titleClassName)}
/>
<TextAnimation
type={theme.defaultTextAnimation as AnimationType}
text={description}
variant="words-trigger"
className={cls("text-base leading-[1.15] text-balance", shouldUseLightText ? "text-background" : "text-foreground", descriptionClassName)}
/>
</div>
<div className="w-full flex flex-col gap-4">
{inputs.map((input) => (
<Input
key={input.name}
type={input.type}
placeholder={input.placeholder}
value={formData[input.name] || ""}
onChange={(value) => setFormData({ ...formData, [input.name]: value })}
required={input.required}
ariaLabel={input.placeholder}
className={input.className}
/>
))}
{textarea && (
<Textarea
placeholder={textarea.placeholder}
value={formData[textarea.name] || ""}
onChange={(value) => setFormData({ ...formData, [textarea.name]: value })}
required={textarea.required}
rows={textarea.rows || 5}
ariaLabel={textarea.placeholder}
className={textarea.className}
/>
)}
<Button
{...getButtonProps(
{ text: buttonText, props: getButtonConfigProps() },
0,
theme.defaultButtonVariant,
cls("w-full", buttonClassName),
cls("text-base", buttonTextClassName)
)}
type="submit"
/>
</div>
</form>
</div>
);
const mediaContent = (
<div ref={mediaContainerRef} className={cls("overflow-hidden rounded-theme-capped card md:relative md:h-full", mediaWrapperClassName)}>
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("w-full md:absolute md:inset-0 md:h-full object-cover", mediaClassName)}
/>
</div>
);
return (
<section aria-label={ariaLabel} className={cls("relative py-20 w-full", useInvertedBackground && "bg-foreground", className)}>
<div className={cls("w-content-width mx-auto", containerClassName)}>
<div className={cls("grid grid-cols-1 md:grid-cols-2 gap-6 md:auto-rows-fr", contentClassName)}>
{mediaPosition === "left" && mediaContent}
{formContent}
{mediaPosition === "right" && mediaContent}
</div>
</div>
</section>
);
};
ContactSplitForm.displayName = "ContactSplitForm";
export default ContactSplitForm;

View File

@@ -1,300 +1,11 @@
"use client";
import { useCardAnimation } from '@/components/cardStack/CardStack';
import CardStack from "@/components/cardStack/CardStack";
import Button from "@/components/button/Button";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import { BentoGlobe } from "@/components/bento/BentoGlobe";
import BentoIconInfoCards from "@/components/bento/BentoIconInfoCards";
import BentoAnimatedBarChart from "@/components/bento/BentoAnimatedBarChart";
import Bento3DStackCards from "@/components/bento/Bento3DStackCards";
import Bento3DTaskList, { type TaskItem } from "@/components/bento/Bento3DTaskList";
import BentoOrbitingIcons, { type OrbitingItem } from "@/components/bento/BentoOrbitingIcons";
import BentoMap from "@/components/bento/BentoMap";
import BentoMarquee from "@/components/bento/BentoMarquee";
import BentoLineChart from "@/components/bento/BentoLineChart/BentoLineChart";
import BentoPhoneAnimation, { type PhoneApp, type PhoneApps8 } from "@/components/bento/BentoPhoneAnimation";
import BentoChatAnimation, { type ChatExchange } from "@/components/bento/BentoChatAnimation";
import Bento3DCardGrid from "@/components/bento/Bento3DCardGrid";
import BentoRevealIcon from "@/components/bento/BentoRevealIcon";
import BentoTimeline, { type TimelineItem } from "@/components/bento/BentoTimeline";
import BentoMediaStack, { type MediaStackItem } from "@/components/bento/BentoMediaStack";
import type { LucideIcon } from "lucide-react";
export type { PhoneApp, PhoneApps8, ChatExchange, TimelineItem, MediaStackItem };
import type { ButtonConfig, CardAnimationTypeWith3D, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type BentoAnimationType = Exclude<CardAnimationTypeWith3D, "depth-3d" | "scale-rotate">;
export type BentoInfoItem = {
icon: LucideIcon;
label: string;
value: string;
};
export type Bento3DItem = {
icon: LucideIcon;
title: string;
subtitle: string;
detail: string;
};
type BaseFeatureCard = {
title: string;
description: string;
button?: ButtonConfig;
};
export type FeatureCard = BaseFeatureCard & (
| {
bentoComponent: "icon-info-cards";
items: BentoInfoItem[];
}
| {
bentoComponent: "3d-stack-cards";
items: [Bento3DItem, Bento3DItem, Bento3DItem];
}
| {
bentoComponent: "3d-task-list";
title: string;
items: TaskItem[];
}
| {
bentoComponent: "orbiting-icons";
centerIcon: LucideIcon;
items: OrbitingItem[];
}
| ({
bentoComponent: "marquee";
centerIcon: LucideIcon;
} & (
| { variant: "text"; texts: string[] }
| { variant: "icon"; icons: LucideIcon[] }
))
| {
bentoComponent: "globe" | "animated-bar-chart" | "map" | "line-chart";
items?: never;
}
| {
bentoComponent: "3d-card-grid";
items: [{ name: string; icon: LucideIcon }, { name: string; icon: LucideIcon }, { name: string; icon: LucideIcon }, { name: string; icon: LucideIcon }];
centerIcon: LucideIcon;
}
| {
bentoComponent: "phone";
statusIcon: LucideIcon;
alertIcon: LucideIcon;
alertTitle: string;
alertMessage: string;
apps: PhoneApps8;
}
| {
bentoComponent: "chat";
aiIcon: LucideIcon;
userIcon: LucideIcon;
exchanges: ChatExchange[];
placeholder: string;
}
| {
bentoComponent: "reveal-icon";
icon: LucideIcon;
}
| {
bentoComponent: "timeline";
heading: string;
subheading: string;
items: [TimelineItem, TimelineItem, TimelineItem];
completedLabel: string;
}
| {
bentoComponent: "media-stack";
items: [MediaStackItem, MediaStackItem, MediaStackItem];
}
);
interface FeatureBentoProps {
features: FeatureCard[];
carouselMode?: "auto" | "buttons";
animationType: BentoAnimationType;
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;
cardDescriptionClassName?: string;
cardButtonClassName?: string;
cardButtonTextClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
}
const FeatureBento = ({
features,
carouselMode = "buttons",
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Feature section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
cardButtonClassName = "",
cardButtonTextClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: FeatureBentoProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const getBentoComponent = (feature: FeatureCard) => {
switch (feature.bentoComponent) {
case "globe":
return (
<div className="relative w-full h-full min-h-0" style={{
maskImage: "linear-gradient(to right, transparent 0%, black 20%, black 80%, transparent 100%), linear-gradient(to bottom, black 40%, transparent 100%)",
WebkitMaskImage: "linear-gradient(to right, transparent 0%, black 20%, black 80%, transparent 100%), linear-gradient(to bottom, black 40%, transparent 100%)",
maskComposite: "intersect",
WebkitMaskComposite: "source-in"
}}>
<BentoGlobe className="w-full scale-150 mt-[15%]" />
</div>
);
case "icon-info-cards":
return <BentoIconInfoCards items={feature.items} useInvertedBackground={useInvertedBackground} />;
case "animated-bar-chart":
return <BentoAnimatedBarChart />;
case "3d-stack-cards":
return <Bento3DStackCards cards={feature.items.map(item => ({ Icon: item.icon, title: item.title, subtitle: item.subtitle, detail: item.detail }))} useInvertedBackground={useInvertedBackground} />;
case "3d-task-list":
return <Bento3DTaskList title={feature.title} items={feature.items} useInvertedBackground={useInvertedBackground} />;
case "orbiting-icons":
return <BentoOrbitingIcons centerIcon={feature.centerIcon} items={feature.items} useInvertedBackground={useInvertedBackground} />;
case "marquee":
return feature.variant === "text"
? <BentoMarquee centerIcon={feature.centerIcon} variant="text" texts={feature.texts} useInvertedBackground={useInvertedBackground} />
: <BentoMarquee centerIcon={feature.centerIcon} variant="icon" icons={feature.icons} useInvertedBackground={useInvertedBackground} />;
case "map":
return <BentoMap useInvertedBackground={useInvertedBackground} />;
case "line-chart":
return <BentoLineChart useInvertedBackground={useInvertedBackground} />;
case "3d-card-grid":
return <Bento3DCardGrid items={feature.items} centerIcon={feature.centerIcon} useInvertedBackground={useInvertedBackground} />;
case "phone":
return <BentoPhoneAnimation statusIcon={feature.statusIcon} alertIcon={feature.alertIcon} alertTitle={feature.alertTitle} alertMessage={feature.alertMessage} apps={feature.apps} useInvertedBackground={useInvertedBackground} />;
case "chat":
return <BentoChatAnimation aiIcon={feature.aiIcon} userIcon={feature.userIcon} exchanges={feature.exchanges} placeholder={feature.placeholder} useInvertedBackground={useInvertedBackground} />;
case "reveal-icon":
return <BentoRevealIcon icon={feature.icon} useInvertedBackground={useInvertedBackground} />;
case "timeline":
return <BentoTimeline heading={feature.heading} subheading={feature.subheading} items={feature.items} completedLabel={feature.completedLabel} useInvertedBackground={useInvertedBackground} />;
case "media-stack":
return <BentoMediaStack items={feature.items} useInvertedBackground={useInvertedBackground} />;
}
};
export function FeatureBento() {
const { animate } = useCardAnimation();
return (
<CardStack
mode={carouselMode}
gridVariant="uniform-all-items-equal"
uniformGridCustomHeightClasses="min-h-0"
animationType={animationType}
carouselThreshold={4}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
carouselItemClassName="w-carousel-item-3 xl:w-carousel-item-3!"
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{features.map((feature, index) => (
<div
key={`${feature.title}-${index}`}
className={cls("card flex flex-col gap-4 p-5 rounded-theme-capped min-h-0 h-full", cardClassName)}
>
<div className="relative w-full h-70 min-h-0 overflow-hidden">
{getBentoComponent(feature)}
</div>
<div className="relative z-1 flex flex-col gap-1">
<h3 className={cls("text-2xl font-medium leading-tight", shouldUseLightText && "text-background", cardTitleClassName)}>
{feature.title}
</h3>
<p className={cls("text-sm leading-tight", shouldUseLightText ? "text-background" : "text-foreground", cardDescriptionClassName)}>
{feature.description}
</p>
</div>
{feature.button && (
<Button {...getButtonProps(feature.button, 0, theme.defaultButtonVariant, cls("w-full", cardButtonClassName), cardButtonTextClassName)} />
)}
</div>
))}
</CardStack>
<div>
<button onClick={() => animate()}>Feature</button>
</div>
);
};
FeatureBento.displayName = "FeatureBento";
export default FeatureBento;
}

View File

@@ -1,261 +1,11 @@
"use client";
import { useCardAnimation } from '@/components/cardStack/CardStack';
import { memo } from "react";
import CardStack from "@/components/cardStack/CardStack";
import MediaContent from "@/components/shared/MediaContent";
import Tag from "@/components/shared/Tag";
import Button from "@/components/button/Button";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
export function FeatureCardMedia() {
const { animate } = useCardAnimation();
type FeatureCard = {
id: string;
title: string;
description: string;
tag: string;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
buttons?: ButtonConfig[];
onCardClick?: () => void;
};
interface FeatureCardMediaProps {
features: FeatureCard[];
carouselMode?: "auto" | "buttons";
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
itemClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
tagClassName?: string;
contentClassName?: string;
cardTitleClassName?: string;
cardDescriptionClassName?: string;
cardButtonContainerClassName?: string;
cardButtonClassName?: string;
cardButtonTextClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
return (
<div>
<button onClick={() => animate()}>Feature</button>
</div>
);
}
interface FeatureCardItemProps {
feature: FeatureCard;
shouldUseLightText: boolean;
useInvertedBackground: InvertedBackground;
itemClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
tagClassName?: string;
contentClassName?: string;
cardTitleClassName?: string;
cardDescriptionClassName?: string;
cardButtonContainerClassName?: string;
cardButtonClassName?: string;
cardButtonTextClassName?: string;
}
const FeatureCardItem = memo(({
feature,
shouldUseLightText,
useInvertedBackground,
itemClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
tagClassName = "",
contentClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
cardButtonContainerClassName = "",
cardButtonClassName = "",
cardButtonTextClassName = "",
}: FeatureCardItemProps) => {
const theme = useTheme();
return (
<article
className={cls("relative h-full flex flex-col gap-6 cursor-pointer group", itemClassName)}
onClick={feature.onCardClick}
role="article"
aria-label={feature.title}
>
<div className={cls("relative w-full aspect-square overflow-hidden rounded-theme-capped", mediaWrapperClassName)}>
<MediaContent
imageSrc={feature.imageSrc}
videoSrc={feature.videoSrc}
imageAlt={feature.imageAlt || feature.title}
videoAriaLabel={feature.videoAriaLabel || feature.title}
imageClassName={cls("w-full h-full object-cover transition-transform duration-500 ease-in-out group-hover:scale-105", mediaClassName)}
/>
<div className="absolute top-4 right-4">
<Tag
text={feature.tag}
useInvertedBackground={useInvertedBackground}
className={tagClassName}
/>
</div>
</div>
<div className={cls("relative z-1 card rounded-theme-capped p-6 flex flex-col gap-2 flex-1", contentClassName)}>
<h3 className={cls(
"text-xl md:text-2xl font-medium leading-tight",
shouldUseLightText ? "text-background" : "text-foreground",
cardTitleClassName
)}>
{feature.title}
</h3>
<p className={cls(
"text-base leading-tight",
shouldUseLightText ? "text-background/75" : "text-foreground/75",
cardDescriptionClassName
)}>
{feature.description}
</p>
{feature.buttons && feature.buttons.length > 0 && (
<div className={cls("flex flex-wrap gap-4 max-md:justify-center mt-2", cardButtonContainerClassName)}>
{feature.buttons.slice(0, 2).map((button, index) => (
<Button
key={`${button.text}-${index}`}
{...getButtonProps(button, index, theme.defaultButtonVariant, cardButtonClassName, cardButtonTextClassName)}
/>
))}
</div>
)}
</div>
</article>
);
});
FeatureCardItem.displayName = "FeatureCardItem";
const FeatureCardMedia = ({
features,
carouselMode = "buttons",
uniformGridCustomHeightClasses,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Features section",
className = "",
containerClassName = "",
itemClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
tagClassName = "",
contentClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
cardButtonContainerClassName = "",
cardButtonClassName = "",
cardButtonTextClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: FeatureCardMediaProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<CardStack
mode={carouselMode}
gridVariant="uniform-all-items-equal"
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
ariaLabel={ariaLabel}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
>
{features.map((feature) => (
<FeatureCardItem
key={feature.id}
feature={feature}
shouldUseLightText={shouldUseLightText}
useInvertedBackground={useInvertedBackground}
itemClassName={itemClassName}
mediaWrapperClassName={mediaWrapperClassName}
mediaClassName={mediaClassName}
tagClassName={tagClassName}
contentClassName={contentClassName}
cardTitleClassName={cardTitleClassName}
cardDescriptionClassName={cardDescriptionClassName}
cardButtonContainerClassName={cardButtonContainerClassName}
cardButtonClassName={cardButtonClassName}
cardButtonTextClassName={cardButtonTextClassName}
/>
))}
</CardStack>
);
};
FeatureCardMedia.displayName = "FeatureCardMedia";
export default FeatureCardMedia;

View File

@@ -1,233 +1,10 @@
"use client";
import React from 'react';
import { TimelinePhoneView } from '../../cardStack/layouts/timelines/TimelinePhoneView';
import TimelinePhoneView from "@/components/cardStack/layouts/timelines/TimelinePhoneView";
import Button from "@/components/button/Button";
import { cls } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, ButtonAnimationType, TitleSegment, CardAnimationType } from "@/components/cardStack/types";
import type { TimelinePhoneViewItem } from "@/components/cardStack/hooks/usePhoneAnimations";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type FeaturePhone = {
imageAlt?: string;
videoAriaLabel?: string;
} & (
| { imageSrc: string; videoSrc?: never }
| { videoSrc: string; imageSrc?: never }
);
type FeatureCard = {
id: number;
title: string;
description: string;
buttons?: ButtonConfig[];
phoneOne: FeaturePhone;
phoneTwo: FeaturePhone;
};
interface FeatureCardNineProps {
features: FeatureCard[];
showStepNumbers: boolean;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
animationType: CardAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxTitleClassName?: string;
textBoxDescriptionClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
desktopContainerClassName?: string;
mobileContainerClassName?: string;
desktopContentClassName?: string;
desktopWrapperClassName?: string;
mobileWrapperClassName?: string;
phoneFrameClassName?: string;
mobilePhoneFrameClassName?: string;
featureContentClassName?: string;
stepNumberClassName?: string;
featureTitleClassName?: string;
featureDescriptionClassName?: string;
cardButtonClassName?: string;
cardButtonTextClassName?: string;
}
interface FeatureContentProps {
feature: FeatureCard;
showStepNumbers: boolean;
useInvertedBackground: InvertedBackground;
featureContentClassName: string;
stepNumberClassName: string;
featureTitleClassName: string;
featureDescriptionClassName: string;
cardButtonClassName: string;
cardButtonTextClassName: string;
}
const FeatureContent = ({
feature,
showStepNumbers,
useInvertedBackground,
featureContentClassName,
stepNumberClassName,
featureTitleClassName,
featureDescriptionClassName,
cardButtonClassName,
cardButtonTextClassName,
}: FeatureContentProps) => {
const theme = useTheme();
return (
<div className={cls("relative z-1 h-full w-content-width mx-auto md:w-full flex flex-col items-center text-center gap-3 md:px-5", featureContentClassName)}>
{showStepNumbers && (
<div
className={cls(
"h-8 w-[var(--height-8)] primary-button text-primary-cta-text rounded-theme flex items-center justify-center",
stepNumberClassName
)}
>
<p className="text-sm truncate">
{feature.id}
</p>
</div>
)}
<h2 className={cls("text-5xl font-medium leading-[1.15] text-balance", useInvertedBackground && "text-background", featureTitleClassName)}>
{feature.title}
</h2>
<p className={cls("text-base leading-[1.2] text-balance", useInvertedBackground ? "text-background/75" : "text-foreground/75", featureDescriptionClassName)}>
{feature.description}
</p>
{feature.buttons && feature.buttons.length > 0 && (
<div className="flex flex-wrap justify-center gap-3">
{feature.buttons.slice(0, 2).map((button, index) => (
<Button key={`${button.text}-${index}`} {...getButtonProps(button, index, theme.defaultButtonVariant, cardButtonClassName, cardButtonTextClassName)} />
))}
</div>
)}
export function FeatureCardNine() {
return (
<div>
<TimelinePhoneView />
</div>
);
};
const FeatureCardNine = ({
features,
showStepNumbers,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
animationType,
textboxLayout,
useInvertedBackground,
ariaLabel = "Feature section",
className = "",
containerClassName = "",
textBoxTitleClassName = "",
textBoxDescriptionClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
desktopContainerClassName = "",
mobileContainerClassName = "",
desktopContentClassName = "",
desktopWrapperClassName = "",
mobileWrapperClassName = "",
phoneFrameClassName = "",
mobilePhoneFrameClassName = "",
featureContentClassName = "",
stepNumberClassName = "",
featureTitleClassName = "",
featureDescriptionClassName = "",
cardButtonClassName = "",
cardButtonTextClassName = "",
}: FeatureCardNineProps) => {
const items: TimelinePhoneViewItem[] = features.map((feature, index) => ({
trigger: `trigger-${index}`,
content: (
<FeatureContent
feature={feature}
showStepNumbers={showStepNumbers}
useInvertedBackground={useInvertedBackground}
featureContentClassName={featureContentClassName}
stepNumberClassName={stepNumberClassName}
featureTitleClassName={featureTitleClassName}
featureDescriptionClassName={featureDescriptionClassName}
cardButtonClassName={cardButtonClassName}
cardButtonTextClassName={cardButtonTextClassName}
/>
),
imageOne: feature.phoneOne.imageSrc,
videoOne: feature.phoneOne.videoSrc,
imageAltOne: feature.phoneOne.imageAlt || `${feature.title} - Phone 1`,
videoAriaLabelOne: feature.phoneOne.videoAriaLabel || `${feature.title} - Phone 1 video`,
imageTwo: feature.phoneTwo.imageSrc,
videoTwo: feature.phoneTwo.videoSrc,
imageAltTwo: feature.phoneTwo.imageAlt || `${feature.title} - Phone 2`,
videoAriaLabelTwo: feature.phoneTwo.videoAriaLabel || `${feature.title} - Phone 2 video`,
}));
return (
<TimelinePhoneView
items={items}
showTextBox={true}
showDivider={true}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
animationType={animationType}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
desktopContainerClassName={desktopContainerClassName}
mobileContainerClassName={mobileContainerClassName}
desktopContentClassName={desktopContentClassName}
desktopWrapperClassName={desktopWrapperClassName}
mobileWrapperClassName={mobileWrapperClassName}
phoneFrameClassName={phoneFrameClassName}
mobilePhoneFrameClassName={mobilePhoneFrameClassName}
ariaLabel={ariaLabel}
/>
);
};
FeatureCardNine.displayName = "FeatureCardNine";
export default FeatureCardNine;
);
}

View File

@@ -1,196 +1,11 @@
"use client";
import { useCardAnimation } from '@/components/cardStack/CardStack';
import CardStack from "@/components/cardStack/CardStack";
import MediaContent from "@/components/shared/MediaContent";
import Button from "@/components/button/Button";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, GridVariant, CardAnimationTypeWith3D, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type FeatureCard = {
title: string;
description: string;
button?: ButtonConfig;
} & (
| {
imageSrc: string;
imageAlt?: string;
videoSrc?: never;
videoAriaLabel?: never;
}
| {
videoSrc: string;
videoAriaLabel?: string;
imageSrc?: never;
imageAlt?: never;
}
);
interface FeatureCardOneProps {
features: FeatureCard[];
carouselMode?: "auto" | "buttons";
gridVariant: GridVariant;
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationTypeWith3D;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
mediaClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
cardTitleClassName?: string;
cardDescriptionClassName?: string;
cardButtonClassName?: string;
cardButtonTextClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
}
const FeatureCardOne = ({
features,
carouselMode = "buttons",
gridVariant,
uniformGridCustomHeightClasses,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Feature section",
className = "",
containerClassName = "",
cardClassName = "",
mediaClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
cardButtonClassName = "",
cardButtonTextClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: FeatureCardOneProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const getButtonConfigProps = () => {
if (theme.defaultButtonVariant === "hover-bubble") {
return { bgClassName: "w-full" };
}
if (theme.defaultButtonVariant === "icon-arrow") {
return { className: "justify-between" };
}
return {};
};
export function FeatureCardOne() {
const { animate } = useCardAnimation();
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}
>
{features.map((feature, index) => (
<div
key={`${feature.title}-${index}`}
className={cls("card flex flex-col gap-4 p-4 rounded-theme-capped min-h-0 h-full", cardClassName)}
>
<MediaContent
imageSrc={feature.imageSrc}
videoSrc={feature.videoSrc}
imageAlt={feature.imageAlt || "Feature image"}
videoAriaLabel={feature.videoAriaLabel || "Feature video"}
imageClassName={cls("relative z-1 min-h-0 h-full", mediaClassName)}
/>
<div className="relative z-1 flex flex-col gap-1">
<h3 className={cls("text-2xl font-medium leading-tight", shouldUseLightText && "text-background", cardTitleClassName)}>
{feature.title}
</h3>
<p className={cls("text-sm leading-tight", shouldUseLightText ? "text-background" : "text-foreground", cardDescriptionClassName)}>
{feature.description}
</p>
</div>
{feature.button && (
<Button
{...getButtonProps(
{ ...feature.button, props: { ...feature.button.props, ...getButtonConfigProps() } },
0,
theme.defaultButtonVariant,
cls("w-full", cardButtonClassName),
cardButtonTextClassName
)}
/>
)}
</div>
))}
</CardStack>
<div>
<button onClick={() => animate()}>Feature</button>
</div>
);
};
FeatureCardOne.displayName = "FeatureCardOne";
export default FeatureCardOne;
}

View File

@@ -1,179 +1,10 @@
"use client";
import React from 'react';
import { CardList } from '../../cardStack/CardList';
import CardList from "@/components/cardStack/CardList";
import MediaContent from "@/components/shared/MediaContent";
import Button from "@/components/button/Button";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, ButtonAnimationType, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type FeatureCard = {
id: number;
title: string;
description: string;
buttons?: ButtonConfig[];
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
};
interface FeatureCardSevenProps {
features: FeatureCard[];
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;
stepNumberClassName?: string;
cardTitleClassName?: string;
cardDescriptionClassName?: string;
imageContainerClassName?: string;
imageClassName?: string;
cardButtonClassName?: string;
cardButtonTextClassName?: string;
export function FeatureCardSeven() {
return (
<div>
<CardList />
</div>
);
}
const FeatureCardSeven = ({
features,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Feature section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxDescriptionClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
cardContentClassName = "",
stepNumberClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
imageContainerClassName = "",
imageClassName = "",
cardButtonClassName = "",
cardButtonTextClassName = "",
}: FeatureCardSevenProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
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}
>
{features.map((feature, index) => (
<div
key={feature.id}
className={cls("relative z-1 w-full min-h-0 h-full flex flex-col justify-between items-center p-6 gap-6 md:p-15 md:gap-15", index % 2 === 0 ? "md:flex-row" : "md:flex-row-reverse", cardContentClassName)}
>
<div className="w-full md:w-1/2 min-w-0 h-fit md:h-full flex flex-col justify-center">
<div className="w-full min-w-0 flex flex-col gap-3 md:gap-5">
<div
className={cls(
"h-8 w-[var(--height-8)] primary-button text-primary-cta-text rounded-theme flex items-center justify-center",
stepNumberClassName
)}
>
<p className="text-sm truncate">
{feature.id}
</p>
</div>
<h2 className={cls("mt-1 text-4xl md:text-5xl font-medium leading-[1.15] text-balance", shouldUseLightText && "text-background", cardTitleClassName)}>
{feature.title}
</h2>
<p className={cls("text-base leading-[1.15] text-balance", shouldUseLightText ? "text-background" : "text-foreground", cardDescriptionClassName)}>
{feature.description}
</p>
{feature.buttons && feature.buttons.length > 0 && (
<div className="flex flex-wrap gap-3 max-md:justify-center">
{feature.buttons.slice(0, 2).map((button, index) => (
<Button key={`${button.text}-${index}`} {...getButtonProps(button, index, theme.defaultButtonVariant, cardButtonClassName, cardButtonTextClassName)} />
))}
</div>
)}
</div>
</div>
<div
className={cls(
"relative w-full md:w-1/2 aspect-square overflow-hidden rounded-theme-capped",
imageContainerClassName
)}
>
<MediaContent
imageSrc={feature.imageSrc}
videoSrc={feature.videoSrc}
imageAlt={feature.imageAlt || feature.title}
videoAriaLabel={feature.videoAriaLabel || feature.title}
imageClassName={cls("w-full h-full object-cover", imageClassName)}
/>
</div>
</div>
))}
</CardList>
);
};
FeatureCardSeven.displayName = "FeatureCardSeven";
export default FeatureCardSeven;

View File

@@ -1,167 +1,15 @@
"use client";
import { useCardAnimation } from '@/components/cardStack/CardStack';
import CardStackTextBox from "@/components/cardStack/CardStackTextBox";
import PricingFeatureList from "@/components/shared/PricingFeatureList";
import { useCardAnimation } from "@/components/cardStack/hooks/useCardAnimation";
import { Check, X } from "lucide-react";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationTypeWith3D, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
export function FeatureCardSixteen() {
const { animate, itemRefs, containerRef, perspectiveRef } = useCardAnimation();
type ComparisonItem = {
items: string[];
};
const handleAnimate = () => {
animate(0);
};
interface FeatureCardSixteenProps {
negativeCard: ComparisonItem;
positiveCard: ComparisonItem;
animationType: CardAnimationTypeWith3D;
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;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
textBoxDescriptionClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
gridClassName?: string;
cardClassName?: string;
itemsListClassName?: string;
itemClassName?: string;
itemIconClassName?: string;
itemTextClassName?: string;
return (
<div>
<button onClick={handleAnimate}>Feature</button>
</div>
);
}
const FeatureCardSixteen = ({
negativeCard,
positiveCard,
animationType,
title,
titleSegments,
description,
textboxLayout,
useInvertedBackground,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
ariaLabel = "Feature comparison section",
className = "",
containerClassName = "",
textBoxTitleClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
textBoxDescriptionClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
gridClassName = "",
cardClassName = "",
itemsListClassName = "",
itemClassName = "",
itemIconClassName = "",
itemTextClassName = "",
}: FeatureCardSixteenProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const { itemRefs, containerRef, perspectiveRef } = useCardAnimation({
animationType,
itemCount: 2,
isGrid: true,
supports3DAnimation: true,
gridVariant: "uniform-all-items-equal"
});
const cards = [
{ ...negativeCard, variant: "negative" as const },
{ ...positiveCard, variant: "positive" as const },
];
return (
<section
ref={containerRef}
aria-label={ariaLabel}
className={cls("relative py-20 w-full", useInvertedBackground && "bg-foreground", className)}
>
<div className={cls("w-content-width mx-auto flex flex-col gap-8", containerClassName)}>
<CardStackTextBox
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
/>
<div
ref={perspectiveRef}
className={cls(
"relative mx-auto w-full md:w-60 grid grid-cols-1 gap-6",
cards.length >= 2 ? "md:grid-cols-2" : "md:grid-cols-1",
gridClassName
)}
>
{cards.map((card, index) => (
<div
key={card.variant}
ref={(el) => { itemRefs.current[index] = el; }}
className={cls(
"relative h-full card rounded-theme-capped p-6",
cardClassName
)}
>
<div className={cls("flex flex-col gap-6", card.variant === "negative" && "opacity-50")}>
<PricingFeatureList
features={card.items}
icon={card.variant === "positive" ? Check : X}
shouldUseLightText={shouldUseLightText}
className={itemsListClassName}
featureItemClassName={itemClassName}
featureIconWrapperClassName=""
featureIconClassName={itemIconClassName}
featureTextClassName={cls("truncate", itemTextClassName)}
/>
</div>
</div>
))}
</div>
</div>
</section>
);
};
FeatureCardSixteen.displayName = "FeatureCardSixteen";
export default FeatureCardSixteen;

View File

@@ -1,263 +1,10 @@
"use client";
import React from 'react';
import { TimelineProcessFlow } from '../../cardStack/layouts/timelines/TimelineProcessFlow';
import React, { memo, useMemo } from "react";
import TimelineProcessFlow from "@/components/cardStack/layouts/timelines/TimelineProcessFlow";
import MediaContent from "@/components/shared/MediaContent";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, ButtonAnimationType, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type FeatureMedia = {
imageAlt?: string;
videoAriaLabel?: string;
} & (
| { imageSrc: string; videoSrc?: never }
| { videoSrc: string; imageSrc?: never }
);
interface FeatureListItem {
icon: LucideIcon;
text: string;
}
interface FeatureCard {
id: string;
title: string;
description: string;
media: FeatureMedia;
items: FeatureListItem[];
reverse: boolean;
}
interface FeatureCardTenProps {
features: FeatureCard[];
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
animationType: CardAnimationType;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxClassName?: string;
textBoxTitleClassName?: string;
textBoxDescriptionClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
itemClassName?: string;
mediaWrapperClassName?: string;
mediaCardClassName?: string;
numberClassName?: string;
contentWrapperClassName?: string;
featureTitleClassName?: string;
featureDescriptionClassName?: string;
listItemClassName?: string;
iconContainerClassName?: string;
iconClassName?: string;
gapClassName?: string;
}
interface FeatureMediaProps {
media: FeatureMedia;
title: string;
mediaCardClassName: string;
}
const FeatureMedia = ({
media,
title,
mediaCardClassName,
}: FeatureMediaProps) => (
<div className={cls("card rounded-theme-capped p-4 aspect-square md:aspect-[16/10]", mediaCardClassName)}>
<MediaContent
imageSrc={media.imageSrc}
videoSrc={media.videoSrc}
imageAlt={media.imageAlt || title}
videoAriaLabel={media.videoAriaLabel || `${title} video`}
imageClassName="relative z-1 w-full h-full object-cover rounded-theme-capped"
/>
export function FeatureCardTen() {
return (
<div>
<TimelineProcessFlow />
</div>
);
interface FeatureContentProps {
feature: FeatureCard;
useInvertedBackground: InvertedBackground;
shouldUseLightText: boolean;
featureTitleClassName: string;
featureDescriptionClassName: string;
listItemClassName: string;
iconContainerClassName: string;
iconClassName: string;
);
}
const FeatureContent = ({
feature,
useInvertedBackground,
shouldUseLightText,
featureTitleClassName,
featureDescriptionClassName,
listItemClassName,
iconContainerClassName,
iconClassName,
}: FeatureContentProps) => (
<div className="flex flex-col gap-3" >
<h3 className={cls("text-xl md:text-4xl font-medium leading-[1.15]", useInvertedBackground && "text-background", featureTitleClassName)}>
{feature.title}
</h3>
<p className={cls("text-base leading-[1.2]", useInvertedBackground ? "text-background/75" : "text-foreground/75", featureDescriptionClassName)}>
{feature.description}
</p>
<ul className="flex flex-col m-0 mt-1 p-0 list-none gap-3">
{feature.items.map((listItem, listIndex) => {
const Icon = listItem.icon;
return (
<li key={listIndex} className="flex items-center gap-3">
<div
className={cls(
"shrink-0 h-9 aspect-square flex items-center justify-center rounded bg-background card",
iconContainerClassName
)}
>
<Icon
className={cls("h-4/10 w-4/10", shouldUseLightText ? "text-background" : "text-foreground", iconClassName)}
strokeWidth={1.25}
/>
</div>
<p className={cls("text-base", useInvertedBackground ? "text-background/75" : "text-foreground/75", listItemClassName)}>
{listItem.text}
</p>
</li>
);
})}
</ul>
</div>
);
const FeatureCardTen = ({
features,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
animationType,
useInvertedBackground,
ariaLabel = "Feature section",
className = "",
containerClassName = "",
textBoxClassName = "",
textBoxTitleClassName = "",
textBoxDescriptionClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
itemClassName = "",
mediaWrapperClassName = "",
mediaCardClassName = "",
numberClassName = "",
contentWrapperClassName = "",
featureTitleClassName = "",
featureDescriptionClassName = "",
listItemClassName = "",
iconContainerClassName = "",
iconClassName = "",
gapClassName = "",
}: FeatureCardTenProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const timelineItems = useMemo(
() =>
features.map((feature) => ({
id: feature.id,
reverse: feature.reverse,
media: (
<FeatureMedia
media={feature.media}
title={feature.title}
mediaCardClassName={mediaCardClassName}
/>
),
content: (
<FeatureContent
feature={feature}
useInvertedBackground={useInvertedBackground}
shouldUseLightText={shouldUseLightText}
featureTitleClassName={featureTitleClassName}
featureDescriptionClassName={featureDescriptionClassName}
listItemClassName={listItemClassName}
iconContainerClassName={iconContainerClassName}
iconClassName={iconClassName}
/>
),
})),
[
features,
useInvertedBackground,
shouldUseLightText,
mediaCardClassName,
featureTitleClassName,
featureDescriptionClassName,
listItemClassName,
iconContainerClassName,
iconClassName,
]
);
return (
<TimelineProcessFlow
items={timelineItems}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
animationType={animationType}
useInvertedBackground={useInvertedBackground}
ariaLabel={ariaLabel}
className={className}
containerClassName={containerClassName}
textBoxClassName={textBoxClassName}
textBoxTitleClassName={textBoxTitleClassName}
textBoxDescriptionClassName={textBoxDescriptionClassName}
textBoxTagClassName={textBoxTagClassName}
textBoxButtonContainerClassName={textBoxButtonContainerClassName}
textBoxButtonClassName={textBoxButtonClassName}
textBoxButtonTextClassName={textBoxButtonTextClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
itemClassName={itemClassName}
mediaWrapperClassName={mediaWrapperClassName}
numberClassName={numberClassName}
contentWrapperClassName={contentWrapperClassName}
gapClassName={gapClassName}
/>
);
};
FeatureCardTen.displayName = "FeatureCardTen";
export default memo(FeatureCardTen);

View File

@@ -1,182 +1,10 @@
"use client";
import { Fragment } from "react";
import CardList from "@/components/cardStack/CardList";
import Button from "@/components/button/Button";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, ButtonAnimationType, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
interface FeatureCard {
id: string;
label: string;
title: string;
items: string[];
buttons?: ButtonConfig[];
}
interface FeatureCardTwelveProps {
features: FeatureCard[];
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
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;
labelClassName?: string;
cardTitleClassName?: string;
itemsContainerClassName?: string;
itemTextClassName?: string;
cardButtonClassName?: string;
cardButtonTextClassName?: string;
}
const FeatureCardTwelve = ({
features,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Feature section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxDescriptionClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
cardContentClassName = "",
labelClassName = "",
cardTitleClassName = "",
itemsContainerClassName = "",
itemTextClassName = "",
cardButtonClassName = "",
cardButtonTextClassName = "",
}: FeatureCardTwelveProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
import React from 'react';
import { CardList } from '../../cardStack/CardList';
export function FeatureCardTwelve() {
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}
>
{features.map((feature) => (
<div
key={feature.id}
className={cls(
"relative z-1 w-full min-h-0 h-full flex flex-col md:flex-row gap-6 p-6 md:p-15",
cardContentClassName
)}
>
<div className="relative z-1 w-full md:w-1/2 flex md:justify-start">
<h2 className={cls(
"text-5xl md:text-6xl font-medium leading-[1.1]",
shouldUseLightText && "text-background",
labelClassName
)}>
{feature.label}
</h2>
</div>
<div className="relative z-1 w-full h-px bg-foreground/20 md:hidden" />
<div className="relative z-1 w-full md:w-1/2 flex flex-col gap-4">
<h3 className={cls(
"text-xl md:text-3xl font-medium leading-tight",
shouldUseLightText ? "text-background" : "text-foreground",
cardTitleClassName
)}>
{feature.title}
</h3>
<div className={cls("flex flex-wrap items-center gap-2", itemsContainerClassName)}>
{feature.items.map((item, index) => (
<Fragment key={index}>
<span className={cls(
"text-base",
shouldUseLightText ? "text-background" : "text-foreground",
itemTextClassName
)}>
{item}
</span>
{index < feature.items.length - 1 && (
<span className="text-base text-accent"></span>
)}
</Fragment>
))}
</div>
{feature.buttons && feature.buttons.length > 0 && (
<div className="mt-3 flex flex-wrap gap-4 max-md:justify-center">
{feature.buttons.slice(0, 2).map((button, index) => (
<Button key={`${button.text}-${index}`} {...getButtonProps(button, index, theme.defaultButtonVariant, cardButtonClassName, cardButtonTextClassName)} />
))}
</div>
)}
</div>
</div>
))}
</CardList>
<div>
<CardList />
</div>
);
};
FeatureCardTwelve.displayName = "FeatureCardTwelve";
export default FeatureCardTwelve;
}

View File

@@ -1,178 +1,11 @@
"use client";
import { useCardAnimation } from '@/components/cardStack/CardStack';
import CardStack from "@/components/cardStack/CardStack";
import MediaContent from "@/components/shared/MediaContent";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { CardAnimationTypeWith3D, TitleSegment, ButtonConfig, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
interface MediaItem {
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
}
type FeatureCard = {
title: string;
description: string;
icon: LucideIcon;
mediaItems: [MediaItem, MediaItem];
};
interface FeatureCardTwentyFiveProps {
features: FeatureCard[];
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;
mediaClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
cardTitleClassName?: string;
cardDescriptionClassName?: string;
cardIconClassName?: string;
cardIconWrapperClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
}
const FeatureCardTwentyFive = ({
features,
carouselMode = "buttons",
uniformGridCustomHeightClasses,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Feature section",
className = "",
containerClassName = "",
cardClassName = "",
mediaClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
cardIconClassName = "",
cardIconWrapperClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: FeatureCardTwentyFiveProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
export function FeatureCardTwentyFive() {
const { animate } = useCardAnimation();
return (
<CardStack
mode={carouselMode}
gridVariant="two-items-per-row"
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}
>
{features.map((feature, index) => {
const IconComponent = feature.icon;
return (
<div
key={`${feature.title}-${index}`}
className={cls("card flex flex-col gap-5 p-5 rounded-theme-capped min-h-0 h-full", cardClassName)}
>
<div className="relative z-1 flex flex-col gap-1">
<div className={cls("h-15 w-[3.75rem] mb-1 aspect-square rounded-theme primary-button flex items-center justify-center", cardIconWrapperClassName)}>
<IconComponent className={cls("h-4/10 w-4/10 text-primary-cta-text", cardIconClassName)} strokeWidth={1.5} />
</div>
<h3 className={cls("text-2xl font-medium leading-tight", shouldUseLightText && "text-background", cardTitleClassName)}>
{feature.title}
</h3>
<p className={cls("text-base leading-tight", shouldUseLightText ? "text-background" : "text-foreground", cardDescriptionClassName)}>
{feature.description}
</p>
</div>
<div className="mt-auto flex-1 min-h-0 grid grid-cols-2 gap-5 overflow-hidden">
{feature.mediaItems.map((item, mediaIndex) => (
<div key={mediaIndex} className="overflow-hidden rounded-theme-capped">
<MediaContent
imageSrc={item.imageSrc}
videoSrc={item.videoSrc}
imageAlt={item.imageAlt || "Feature image"}
videoAriaLabel={item.videoAriaLabel || "Feature video"}
imageClassName={cls("relative z-1 h-full w-full object-cover", mediaClassName)}
/>
</div>
))}
</div>
</div>
);
})}
</CardStack>
<div>
<button onClick={() => animate()}>Feature</button>
</div>
);
};
FeatureCardTwentyFive.displayName = "FeatureCardTwentyFive";
export default FeatureCardTwentyFive;
}

View File

@@ -1,199 +1,10 @@
"use client";
import React from 'react';
import { CardList } from '../../cardStack/CardList';
import CardList from "@/components/cardStack/CardList";
import Tag from "@/components/shared/Tag";
import MediaContent from "@/components/shared/MediaContent";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, ButtonAnimationType, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type MediaProps =
| {
imageSrc: string;
imageAlt?: string;
videoSrc?: never;
videoAriaLabel?: never;
}
| {
videoSrc: string;
videoAriaLabel?: string;
imageSrc?: never;
imageAlt?: never;
};
type FeatureItem = MediaProps & {
id: string;
title: string;
author: string;
description: string;
tags: string[];
onFeatureClick?: () => void;
};
interface FeatureCardTwentyFourProps {
features: FeatureItem[];
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
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;
cardTitleClassName?: string;
authorClassName?: string;
cardDescriptionClassName?: string;
tagsContainerClassName?: string;
tagClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
export function FeatureCardTwentyFour() {
return (
<div>
<CardList />
</div>
);
}
const FeatureCardTwentyFour = ({
features,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Features section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxDescriptionClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
cardContentClassName = "",
cardTitleClassName = "",
authorClassName = "",
cardDescriptionClassName = "",
tagsContainerClassName = "",
tagClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
}: FeatureCardTwentyFourProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<CardList
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
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}
>
{features.map((feature) => (
<article
key={feature.id}
className={cls(
"relative z-1 w-full min-h-0 h-full flex flex-col md:grid md:grid-cols-10 gap-6 md:gap-10 cursor-pointer group p-6 md:p-10",
cardContentClassName
)}
onClick={feature.onFeatureClick}
role="article"
aria-label={feature.title}
>
<div className="relative z-1 w-full md:col-span-6 flex flex-col gap-3 md:gap-12">
<h3 className={cls(
"text-3xl md:text-5xl text-balance font-medium leading-tight line-clamp-3",
shouldUseLightText ? "text-background" : "text-foreground",
cardTitleClassName
)}>
{feature.title}{" "}
<span className={cls(
shouldUseLightText ? "text-background/50" : "text-foreground/50",
authorClassName
)}>
by {feature.author}
</span>
</h3>
<div className="mt-auto flex flex-col gap-4">
<div className={cls("flex flex-wrap gap-2", tagsContainerClassName)}>
{feature.tags.map((tagText, index) => (
<Tag key={index} text={tagText} useInvertedBackground={useInvertedBackground} className={tagClassName} />
))}
</div>
<p className={cls(
"text-base md:text-2xl text-balance leading-tight line-clamp-2",
shouldUseLightText ? "text-background" : "text-foreground",
cardDescriptionClassName
)}>
{feature.description}
</p>
</div>
</div>
<div className={cls(
"relative z-1 w-full md:col-span-4 aspect-square md:aspect-auto overflow-hidden rounded-theme-capped",
mediaWrapperClassName
)}>
<MediaContent
imageSrc={feature.imageSrc}
videoSrc={feature.videoSrc}
imageAlt={feature.imageAlt}
videoAriaLabel={feature.videoAriaLabel}
imageClassName={cls("w-full h-full object-cover", mediaClassName)}
/>
</div>
</article>
))}
</CardList>
);
};
FeatureCardTwentyFour.displayName = "FeatureCardTwentyFour";
export default FeatureCardTwentyFour;

View File

@@ -1,219 +1,11 @@
"use client";
import { useCardAnimation } from '@/components/cardStack/CardStack';
import { useState } from "react";
import { Plus } from "lucide-react";
import CardStack from "@/components/cardStack/CardStack";
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 FeatureCard = {
id: string;
title: string;
description: string;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
};
interface FeatureCardTwentySevenItemProps {
title: string;
description: string;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
className?: string;
titleClassName?: string;
descriptionClassName?: string;
}
const FeatureCardTwentySevenItem = ({
title,
description,
imageSrc,
videoSrc,
imageAlt = "",
className = "",
titleClassName = "",
descriptionClassName = "",
}: FeatureCardTwentySevenItemProps) => {
const [isFlipped, setIsFlipped] = useState(false);
export function FeatureCardTwentySeven() {
const { animate } = useCardAnimation();
return (
<div
className={cls(
"relative w-full h-full min-h-0 group [perspective:3000px] cursor-pointer",
className
)}
onClick={() => setIsFlipped(!isFlipped)}
>
<div
className={cls(
"relative w-full h-full transition-transform duration-500 [transform-style:preserve-3d]",
isFlipped && "[transform:rotateY(180deg)]"
)}
>
<div className="relative w-full h-full card rounded-theme-capped p-6 gap-6 flex flex-col [backface-visibility:hidden]">
<div className="flex justify-between items-start">
<h3 className={cls("text-2xl font-medium leading-tight", titleClassName)}>
{title}
</h3>
<div className="h-[calc(var(--text-2xl)*1.25)] w-[calc(var(--text-2xl)*1.25)] aspect-square rounded-theme primary-button flex items-center justify-center shrink-0">
<Plus className="h-1/2 w-1/2 text-primary-cta-text" />
</div>
</div>
<div className="w-full aspect-square md:aspect-[10/11] flex items-center justify-center rounded-theme-capped overflow-hidden">
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
imageClassName="w-full h-full object-cover"
/>
</div>
</div>
<div className="absolute! inset-0 w-full h-full card rounded-theme-capped p-6 gap-6 flex flex-col justify-between [backface-visibility:hidden] [transform:rotateY(180deg)]">
<div className="flex justify-between items-start">
<h3 className={cls("text-2xl font-medium leading-tight", titleClassName)}>
{title}
</h3>
<div className="h-[calc(var(--text-2xl)*1.25)] w-[calc(var(--text-2xl)*1.25)] aspect-square rounded-theme primary-button flex items-center justify-center shrink-0">
<Plus className="h-1/2 w-1/2 rotate-45 text-primary-cta-text" />
</div>
</div>
<div className="w-full">
<p className={cls("text-lg text-foreground/75 leading-tight", descriptionClassName)}>
{description}
</p>
</div>
</div>
</div>
<div>
<button onClick={() => animate()}>Feature</button>
</div>
);
};
interface FeatureCardTwentySevenProps {
features: FeatureCard[];
carouselMode?: "auto" | "buttons";
gridVariant: GridVariant;
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;
cardDescriptionClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
}
const FeatureCardTwentySeven = ({
features,
carouselMode = "buttons",
gridVariant,
uniformGridCustomHeightClasses = "min-h-none",
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Feature section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: FeatureCardTwentySevenProps) => {
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}
>
{features.map((feature, index) => (
<FeatureCardTwentySevenItem
key={`${feature.id}-${index}`}
title={feature.title}
description={feature.description}
imageSrc={feature.imageSrc}
videoSrc={feature.videoSrc}
imageAlt={feature.imageAlt}
className={cardClassName}
titleClassName={cardTitleClassName}
descriptionClassName={cardDescriptionClassName}
/>
))}
</CardStack>
);
};
FeatureCardTwentySeven.displayName = "FeatureCardTwentySeven";
export default FeatureCardTwentySeven;

View File

@@ -1,241 +1,11 @@
"use client";
import { useCardAnimation } from '@/components/cardStack/CardStack';
import { memo } from "react";
import { ArrowRight } from "lucide-react";
import CardStack from "@/components/cardStack/CardStack";
import MediaContent from "@/components/shared/MediaContent";
import Tag from "@/components/shared/Tag";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
export function FeatureCardTwentyThree() {
const { animate } = useCardAnimation();
type FeatureItem = {
id: string;
title: string;
tags: string[];
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
onFeatureClick?: () => void;
};
interface FeatureCardTwentyThreeProps {
features: FeatureItem[];
carouselMode?: "auto" | "buttons";
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
itemClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
cardClassName?: string;
cardTitleClassName?: string;
tagsContainerClassName?: string;
tagClassName?: string;
arrowClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
return (
<div>
<button onClick={() => animate()}>Feature</button>
</div>
);
}
interface FeatureCardItemProps {
feature: FeatureItem;
shouldUseLightText: boolean;
useInvertedBackground: InvertedBackground;
itemClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
cardClassName?: string;
cardTitleClassName?: string;
tagsContainerClassName?: string;
tagClassName?: string;
arrowClassName?: string;
}
const FeatureCardItem = memo(({
feature,
shouldUseLightText,
useInvertedBackground,
itemClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
cardClassName = "",
cardTitleClassName = "",
tagsContainerClassName = "",
tagClassName = "",
arrowClassName = "",
}: FeatureCardItemProps) => {
return (
<article
className={cls("relative h-full flex flex-col gap-6 cursor-pointer group", itemClassName)}
onClick={feature.onFeatureClick}
role="article"
aria-label={feature.title}
>
<div className={cls("relative w-full aspect-square overflow-hidden rounded-theme-capped", mediaWrapperClassName)}>
<MediaContent
imageSrc={feature.imageSrc}
videoSrc={feature.videoSrc}
imageAlt={feature.imageAlt || feature.title}
videoAriaLabel={feature.videoAriaLabel || feature.title}
imageClassName={cls("w-full h-full object-cover transition-transform duration-500 ease-in-out group-hover:scale-105", mediaClassName)}
/>
</div>
<div className={cls("relative z-1 card rounded-theme-capped p-5 flex-1 flex flex-col justify-between gap-4", cardClassName)}>
<h3 className={cls(
"text-xl md:text-2xl font-medium leading-tight",
shouldUseLightText ? "text-background" : "text-foreground",
cardTitleClassName
)}>
{feature.title}
</h3>
<div className="flex items-center justify-between gap-4">
<div className={cls("flex items-center gap-2 flex-wrap", tagsContainerClassName)}>
{feature.tags.map((tag, index) => (
<Tag
key={index}
text={tag}
useInvertedBackground={useInvertedBackground}
className={tagClassName}
/>
))}
</div>
<ArrowRight
className={cls(
"h-[var(--text-base)] w-auto shrink-0 transition-transform duration-300 group-hover:-rotate-45",
shouldUseLightText ? "text-background" : "text-foreground",
arrowClassName
)}
strokeWidth={1.5}
/>
</div>
</div>
</article>
);
});
FeatureCardItem.displayName = "FeatureCardItem";
const FeatureCardTwentyThree = ({
features,
carouselMode = "buttons",
uniformGridCustomHeightClasses,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Features section",
className = "",
containerClassName = "",
itemClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
cardClassName = "",
cardTitleClassName = "",
tagsContainerClassName = "",
tagClassName = "",
arrowClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: FeatureCardTwentyThreeProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<CardStack
mode={carouselMode}
gridVariant="uniform-all-items-equal"
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
ariaLabel={ariaLabel}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
>
{features.map((feature) => (
<FeatureCardItem
key={feature.id}
feature={feature}
shouldUseLightText={shouldUseLightText}
useInvertedBackground={useInvertedBackground}
itemClassName={itemClassName}
mediaWrapperClassName={mediaWrapperClassName}
mediaClassName={mediaClassName}
cardClassName={cardClassName}
cardTitleClassName={cardTitleClassName}
tagsContainerClassName={tagsContainerClassName}
tagClassName={tagClassName}
arrowClassName={arrowClassName}
/>
))}
</CardStack>
);
};
FeatureCardTwentyThree.displayName = "FeatureCardTwentyThree";
export default FeatureCardTwentyThree;

View File

@@ -1,155 +1,11 @@
"use client";
import { useCardAnimation } from '@/components/cardStack/CardStack';
import CardStack from "@/components/cardStack/CardStack";
import FeatureBorderGlowItem from "./FeatureBorderGlowItem";
import { shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type {
ButtonConfig,
CardAnimationType,
TitleSegment,
ButtonAnimationType,
} from "@/components/cardStack/types";
import type {
TextboxLayout,
InvertedBackground,
} from "@/providers/themeProvider/config/constants";
interface FeatureCard {
icon: LucideIcon;
title: string;
description: string;
}
interface FeatureBorderGlowProps {
features: FeatureCard[];
carouselMode?: "auto" | "buttons";
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
iconContainerClassName?: string;
iconClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
cardTitleClassName?: string;
cardDescriptionClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
}
const FeatureBorderGlow = ({
features,
carouselMode = "buttons",
uniformGridCustomHeightClasses = "min-h-75 2xl:min-h-85",
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Feature section",
className = "",
containerClassName = "",
cardClassName = "",
iconContainerClassName = "",
iconClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: FeatureBorderGlowProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(
useInvertedBackground,
theme.cardStyle
);
export function FeatureBorderGlow() {
const { animate } = useCardAnimation();
return (
<CardStack
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}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{features.map((feature, index) => (
<FeatureBorderGlowItem
key={`${feature.title}-${index}`}
item={feature}
index={index}
className={cardClassName}
iconContainerClassName={iconContainerClassName}
iconClassName={iconClassName}
titleClassName={cardTitleClassName}
descriptionClassName={cardDescriptionClassName}
shouldUseLightText={shouldUseLightText}
/>
))}
</CardStack>
<div>
<button onClick={() => animate()}>Feature</button>
</div>
);
};
FeatureBorderGlow.displayName = "FeatureBorderGlow";
export default FeatureBorderGlow;
}

View File

@@ -1,182 +1,11 @@
"use client";
import { useCardAnimation } from '@/components/cardStack/CardStack';
import "./FeatureCardThree.css";
import { useRef, useCallback, useState } from "react";
import CardStack from "@/components/cardStack/CardStack";
import FeatureCardThreeItem from "./FeatureCardThreeItem";
import { useDynamicDimensions } from "./useDynamicDimensions";
import { useClickOutside } from "@/hooks/useClickOutside";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, GridVariant, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type FeatureCard = {
id: string;
title: string;
description: string;
imageSrc: string;
imageAlt?: string;
};
interface FeatureCardThreeProps {
features: FeatureCard[];
carouselMode?: "auto" | "buttons";
gridVariant: GridVariant;
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
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;
cardDescriptionClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
itemContentClassName?: string;
}
const FeatureCardThree = ({
features,
carouselMode = "buttons",
gridVariant,
uniformGridCustomHeightClasses,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Feature section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
itemContentClassName = "",
}: FeatureCardThreeProps) => {
const featureCardThreeRefs = useRef<(HTMLDivElement | null)[]>([]);
const containerRef = useRef<HTMLDivElement>(null);
const [activeIndex, setActiveIndex] = useState<number | null>(null);
const setRef = useCallback(
(index: number) => (el: HTMLDivElement | null) => {
if (featureCardThreeRefs.current) {
featureCardThreeRefs.current[index] = el;
}
},
[]
);
// Check if device supports hover (desktop) or not (mobile/touch)
const isTouchDevice = typeof window !== "undefined" && window.matchMedia("(hover: none)").matches;
// Handle click outside to deactivate on mobile
useClickOutside(
containerRef,
() => setActiveIndex(null),
activeIndex !== null && isTouchDevice
);
const handleItemClick = useCallback((index: number) => {
if (typeof window !== "undefined" && !window.matchMedia("(hover: none)").matches) return;
setActiveIndex((prev) => (prev === index ? null : index));
}, []);
useDynamicDimensions([featureCardThreeRefs], {
titleSelector: ".feature-card-three-title-row .feature-card-three-title",
descriptionSelector: ".feature-card-three-description-wrapper .feature-card-three-description",
});
export function FeatureCardThree() {
const { animate } = useCardAnimation();
return (
<div ref={containerRef}>
<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}
>
{features.map((feature, index) => (
<FeatureCardThreeItem
key={`${feature.id}-${index}`}
ref={setRef(index)}
item={feature}
isActive={activeIndex === index}
onItemClick={() => handleItemClick(index)}
className={cardClassName}
itemContentClassName={itemContentClassName}
itemTitleClassName={cardTitleClassName}
itemDescriptionClassName={cardDescriptionClassName}
/>
))}
</CardStack>
<div>
<button onClick={() => animate()}>Feature</button>
</div>
);
};
FeatureCardThree.displayName = "FeatureCardThree";
export default FeatureCardThree;
}

View File

@@ -1,165 +1,11 @@
"use client";
import { useCardAnimation } from '@/components/cardStack/CardStack';
import CardStack from "@/components/cardStack/CardStack";
import FeatureHoverPatternItem from "./FeatureHoverPatternItem";
import { shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type {
ButtonConfig,
CardAnimationType,
TitleSegment,
ButtonAnimationType,
} from "@/components/cardStack/types";
import type {
TextboxLayout,
InvertedBackground,
} from "@/providers/themeProvider/config/constants";
interface FeatureCard {
icon: LucideIcon;
title: string;
description: string;
button?: ButtonConfig;
}
interface FeatureHoverPatternProps {
features: FeatureCard[];
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;
iconContainerClassName?: string;
iconClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
cardTitleClassName?: string;
cardDescriptionClassName?: string;
gradientClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
cardButtonClassName?: string;
cardButtonTextClassName?: string;
}
const FeatureHoverPattern = ({
features,
carouselMode = "buttons",
uniformGridCustomHeightClasses = "min-h-85 2xl:min-h-95",
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Feature section",
className = "",
containerClassName = "",
cardClassName = "",
iconContainerClassName = "",
iconClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
gradientClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
cardButtonClassName = "",
cardButtonTextClassName = "",
}: FeatureHoverPatternProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(
useInvertedBackground,
theme.cardStyle
);
export function FeatureHoverPattern() {
const { animate } = useCardAnimation();
return (
<CardStack
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}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{features.map((feature, index) => (
<FeatureHoverPatternItem
key={`${feature.title}-${index}`}
item={feature}
index={index}
className={cardClassName}
iconContainerClassName={iconContainerClassName}
iconClassName={iconClassName}
titleClassName={cardTitleClassName}
descriptionClassName={cardDescriptionClassName}
gradientClassName={gradientClassName}
shouldUseLightText={shouldUseLightText}
buttonClassName={cardButtonClassName}
buttonTextClassName={cardButtonTextClassName}
/>
))}
</CardStack>
<div>
<button onClick={() => animate()}>Feature</button>
</div>
);
};
FeatureHoverPattern.displayName = "FeatureHoverPattern";
export default FeatureHoverPattern;
}

View File

@@ -1,155 +1,10 @@
"use client";
import React from 'react';
import { AutoCarousel } from '../../cardStack/layouts/carousels/AutoCarousel';
import TextBox from "@/components/Textbox";
import MediaContent from "@/components/shared/MediaContent";
import AutoCarousel from "@/components/cardStack/layouts/carousels/AutoCarousel";
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, ButtonAnimationType } from "@/types/button";
export interface MediaItem {
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
export function HeroBillboardCarousel() {
return (
<div>
<AutoCarousel />
</div>
);
}
type HeroBillboardCarouselBackgroundProps = Extract<
HeroBackgroundVariantProps,
| { variant: "plain" }
| { variant: "animated-grid" }
| { variant: "canvas-reveal" }
| { variant: "cell-wave" }
| { variant: "downward-rays-animated" }
| { variant: "downward-rays-animated-grid" }
| { variant: "downward-rays-static" }
| { variant: "downward-rays-static-grid" }
| { variant: "gradient-bars" }
| { variant: "radial-gradient" }
| { variant: "rotated-rays-animated" }
| { variant: "rotated-rays-animated-grid" }
| { variant: "rotated-rays-static" }
| { variant: "rotated-rays-static-grid" }
| { variant: "sparkles-gradient" }
>;
interface HeroBillboardCarouselProps {
title: string;
description: string;
background: HeroBillboardCarouselBackgroundProps;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
mediaItems: MediaItem[];
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
mediaWrapperClassName?: string;
}
const HeroBillboardCarousel = ({
title,
description,
background,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
mediaItems,
ariaLabel = "Hero section",
className = "",
containerClassName = "",
textBoxClassName = "",
titleClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
mediaWrapperClassName = "",
}: HeroBillboardCarouselProps) => {
const renderCarouselItem = (item: MediaItem, index: number) => (
<div
key={index}
className="w-full aspect-[4/5] overflow-hidden rounded-theme-capped card p-2 shadow-lg"
>
<MediaContent
imageSrc={item.imageSrc}
videoSrc={item.videoSrc}
imageAlt={item.imageAlt || ""}
videoAriaLabel={item.videoAriaLabel || "Carousel media"}
imageClassName="z-1 h-full object-cover"
/>
</div>
);
return (
<section
aria-label={ariaLabel}
className={cls(
"relative w-full py-hero-page-padding md:h-svh md:py-0",
className
)}
>
<HeroBackgrounds {...background} />
<div className={cls(
"mx-auto flex flex-col gap-14 md:gap-10 relative z-10",
"w-full md:w-content-width md:h-full md:items-center md:justify-center",
containerClassName
)}>
<TextBox
title={title}
description={description}
tag={tag}
tagIcon={tagIcon}
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 -mx-[var(--content-padding)]", mediaWrapperClassName)}>
<AutoCarousel
title=""
description=""
textboxLayout="default"
animationType="none"
className="py-0"
carouselClassName="py-0"
containerClassName="!w-full"
itemClassName="!w-55 md:!w-carousel-item-4"
ariaLabel="Hero carousel"
showTextBox={false}
>
{mediaItems?.map(renderCarouselItem)}
</AutoCarousel>
</div>
</div>
</section>
);
};
HeroBillboardCarousel.displayName = "HeroBillboardCarousel";
export default HeroBillboardCarousel;