97 Commits

Author SHA1 Message Date
cdea021774 Update src/components/cardStack/types.ts 2026-03-12 04:06:45 +00:00
476e01f044 Update src/components/cardStack/types.ts 2026-03-12 04:05:54 +00:00
e38eeb5350 Update src/app/page.tsx 2026-03-12 04:05:54 +00:00
8eb2f7877d Update src/components/shared/Dashboard.tsx 2026-03-12 04:05:04 +00:00
e350d1c202 Update src/components/cardStack/types.ts 2026-03-12 04:05:03 +00:00
816d81405e Update src/components/cardStack/layouts/timelines/TimelineProcessFlow.tsx 2026-03-12 04:05:03 +00:00
9feccde53c Update src/components/cardStack/layouts/timelines/TimelinePhoneView.tsx 2026-03-12 04:05:02 +00:00
d69851d675 Update src/components/cardStack/layouts/timelines/TimelineHorizontalCardStack.tsx 2026-03-12 04:05:02 +00:00
2051632d3f Update src/components/cardStack/layouts/timelines/TimelineCardStack.tsx 2026-03-12 04:05:02 +00:00
df12cd8ec3 Update src/components/cardStack/layouts/carousels/FullWidthCarousel.tsx 2026-03-12 04:05:01 +00:00
37e24091b7 Update src/components/cardStack/layouts/carousels/AutoCarousel.tsx 2026-03-12 04:05:01 +00:00
fa003f67da Update src/components/cardStack/layouts/carousels/ArrowCarousel.tsx 2026-03-12 04:05:00 +00:00
ba1140d4f4 Update src/components/cardStack/CardStackTextBox.tsx 2026-03-12 04:05:00 +00:00
36f1ed5a75 Update src/components/cardStack/CardStack.tsx 2026-03-12 04:04:59 +00:00
248ef9b704 Update src/components/cardStack/CardList.tsx 2026-03-12 04:04:59 +00:00
07ff76d019 Update src/app/page.tsx 2026-03-12 04:04:59 +00:00
1ebff371d5 Update src/components/shared/Dashboard.tsx 2026-03-12 04:04:01 +00:00
f79c8e5a3f Update src/components/sections/testimonial/TestimonialCardTwo.tsx 2026-03-12 04:04:00 +00:00
453d60a6c3 Update src/components/sections/testimonial/TestimonialCardThirteen.tsx 2026-03-12 04:04:00 +00:00
0f85122742 Update src/components/sections/testimonial/TestimonialCardSixteen.tsx 2026-03-12 04:03:59 +00:00
da979e188f Update src/components/sections/testimonial/TestimonialCardOne.tsx 2026-03-12 04:03:59 +00:00
656992626f Update src/components/sections/team/TeamCardTwo.tsx 2026-03-12 04:03:58 +00:00
0705a26704 Update src/components/sections/team/TeamCardSix.tsx 2026-03-12 04:03:58 +00:00
0fa3ab966c Update src/components/sections/team/TeamCardOne.tsx 2026-03-12 04:03:57 +00:00
a078df7036 Update src/components/sections/team/TeamCardFive.tsx 2026-03-12 04:03:57 +00:00
814622f615 Update src/components/sections/pricing/PricingCardTwo.tsx 2026-03-12 04:03:57 +00:00
d6db1b676e Update src/components/sections/pricing/PricingCardThree.tsx 2026-03-12 04:03:56 +00:00
e56e5de91d Update src/components/sections/pricing/PricingCardOne.tsx 2026-03-12 04:03:56 +00:00
3902776001 Update src/components/sections/metrics/MetricCardTwo.tsx 2026-03-12 04:03:55 +00:00
7188bfd07e Update src/components/sections/metrics/MetricCardThree.tsx 2026-03-12 04:03:55 +00:00
4bf7b32444 Update src/components/sections/metrics/MetricCardTen.tsx 2026-03-12 04:03:54 +00:00
76bb9e9227 Update src/components/sections/metrics/MetricCardSeven.tsx 2026-03-12 04:03:54 +00:00
7e38f342d1 Update src/components/sections/metrics/MetricCardOne.tsx 2026-03-12 04:03:54 +00:00
2e074c7f81 Update src/components/sections/metrics/MetricCardEleven.tsx 2026-03-12 04:03:53 +00:00
edaf9736c6 Update src/components/sections/feature/featureHoverPattern/FeatureHoverPattern.tsx 2026-03-12 04:03:53 +00:00
37f4f07b46 Update src/components/sections/feature/featureCardThree/FeatureCardThree.tsx 2026-03-12 04:03:52 +00:00
e92c3b82ce Update src/components/sections/feature/featureBorderGlow/FeatureBorderGlow.tsx 2026-03-12 04:03:52 +00:00
e74b335ceb Update src/components/sections/feature/FeatureCardTwentyThree.tsx 2026-03-12 04:03:51 +00:00
ca03a2e1c5 Update src/components/sections/feature/FeatureCardTwentySeven.tsx 2026-03-12 04:03:51 +00:00
d73eb9a263 Update src/components/sections/feature/FeatureCardTwentyFive.tsx 2026-03-12 04:03:51 +00:00
4560f438d6 Update src/components/sections/feature/FeatureCardSixteen.tsx 2026-03-12 04:03:50 +00:00
c41ced08b5 Update src/components/sections/feature/FeatureCardOne.tsx 2026-03-12 04:03:50 +00:00
845a659346 Update src/components/sections/feature/FeatureCardMedia.tsx 2026-03-12 04:03:49 +00:00
3406752e5f Update src/components/sections/feature/FeatureBento.tsx 2026-03-12 04:03:49 +00:00
c9f96b07b3 Update src/components/sections/contact/ContactFaq.tsx 2026-03-12 04:03:48 +00:00
22738c93c8 Update src/components/sections/blog/BlogCardTwo.tsx 2026-03-12 04:03:48 +00:00
8fa09be198 Update src/components/sections/blog/BlogCardThree.tsx 2026-03-12 04:03:48 +00:00
9c8e073ca0 Update src/components/sections/blog/BlogCardOne.tsx 2026-03-12 04:03:47 +00:00
c777571888 Update src/components/cardStack/types.ts 2026-03-12 04:03:47 +00:00
8190cc8b86 Update src/components/cardStack/layouts/timelines/TimelineProcessFlow.tsx 2026-03-12 04:03:46 +00:00
46f507759e Update src/components/cardStack/layouts/timelines/TimelinePhoneView.tsx 2026-03-12 04:03:46 +00:00
f804265dac Update src/components/cardStack/layouts/grid/GridLayout.tsx 2026-03-12 04:03:46 +00:00
0e051a7588 Update src/components/cardStack/layouts/carousels/ButtonCarousel.tsx 2026-03-12 04:03:45 +00:00
d5305382f6 Update src/components/cardStack/layouts/carousels/AutoCarousel.tsx 2026-03-12 04:03:45 +00:00
5940544878 Update src/components/cardStack/hooks/useCardAnimation.ts 2026-03-12 04:03:44 +00:00
7f2e42c708 Update src/components/cardStack/CardList.tsx 2026-03-12 04:03:44 +00:00
48c7b2d32b Update src/app/page.tsx 2026-03-12 04:01:23 +00:00
508825c797 Update src/app/page.tsx 2026-03-12 04:00:23 +00:00
3a71cd0188 Update src/app/home/page.tsx 2026-03-12 04:00:23 +00:00
b4cf132a21 Update src/app/gym/page.tsx 2026-03-12 04:00:22 +00:00
05b4368c30 Update src/app/fashion/page.tsx 2026-03-12 04:00:22 +00:00
f97603234a Update src/app/electronics/page.tsx 2026-03-12 04:00:22 +00:00
d8c1ac202a Update src/components/cardStack/hooks/useCardAnimation.ts 2026-03-12 03:58:50 +00:00
3ca082eef2 Update src/hooks/useProducts.ts 2026-03-12 03:57:10 +00:00
1d629a470b Update src/components/ecommerce/productCatalog/ProductCatalog.tsx 2026-03-12 03:57:09 +00:00
c106c1384b Update src/components/cardStack/hooks/useCardAnimation.ts 2026-03-12 03:57:09 +00:00
89d9670e27 Update src/lib/api/product.ts 2026-03-12 03:56:34 +00:00
d934b71aa8 Update src/hooks/useProducts.ts 2026-03-12 03:56:34 +00:00
00a0ffe979 Update src/hooks/useProduct.ts 2026-03-12 03:56:34 +00:00
56f56965ee Update src/components/sections/product/ProductCardTwo.tsx 2026-03-12 03:56:33 +00:00
553b656a13 Update src/components/sections/product/ProductCardThree.tsx 2026-03-12 03:56:33 +00:00
8b08e8efea Update src/components/sections/product/ProductCardOne.tsx 2026-03-12 03:56:32 +00:00
61ab655771 Update src/components/sections/product/ProductCardFour.tsx 2026-03-12 03:56:32 +00:00
c6343baaf3 Update src/components/cardStack/hooks/useDepth3DAnimation.ts 2026-03-12 03:56:31 +00:00
3150e9055e Update src/components/cardStack/hooks/useCardAnimation.ts 2026-03-12 03:56:31 +00:00
c3f30e8105 Update src/components/cardStack/CardStack.tsx 2026-03-12 03:56:31 +00:00
d9d8db3367 Update src/lib/api/product.ts 2026-03-12 03:55:23 +00:00
636e81ccdf Update src/hooks/useProductDetail.ts 2026-03-12 03:55:22 +00:00
f9e8bdc033 Update src/hooks/useProductCatalog.ts 2026-03-12 03:55:21 +00:00
416dc41e78 Update src/hooks/useCheckout.ts 2026-03-12 03:55:21 +00:00
1d0f8c52b2 Update src/components/sections/pricing/PricingCardEight.tsx 2026-03-12 03:55:20 +00:00
5a69c61933 Update src/components/sections/contact/ContactSplitForm.tsx 2026-03-12 03:55:20 +00:00
91cfcf4774 Update src/components/sections/contact/ContactSplit.tsx 2026-03-12 03:55:19 +00:00
eba0846f2f Update src/components/sections/contact/ContactCenter.tsx 2026-03-12 03:55:18 +00:00
8bd7af31c3 Update src/components/cardStack/layouts/timelines/TimelineBase.tsx 2026-03-12 03:55:17 +00:00
42261d217e Update src/components/cardStack/hooks/useDepth3DAnimation.ts 2026-03-12 03:55:17 +00:00
b94aa9a265 Update src/components/shared/SvgTextLogo/SvgTextLogo.tsx 2026-03-12 03:51:28 +00:00
98cca80272 Update src/app/page.tsx 2026-03-12 03:51:27 +00:00
a953797a77 Update src/app/page.tsx 2026-03-12 03:48:55 +00:00
c295859a1c Update src/app/home/page.tsx 2026-03-12 03:48:54 +00:00
f9779ffa50 Update src/app/gym/page.tsx 2026-03-12 03:48:54 +00:00
a032a21dfe Update src/app/fashion/page.tsx 2026-03-12 03:48:53 +00:00
9e53130ffd Update src/app/electronics/page.tsx 2026-03-12 03:48:52 +00:00
a11da917be Update src/app/page.tsx 2026-03-12 03:46:07 +00:00
8d45f1f9b0 Update src/app/gym/page.tsx 2026-03-12 03:46:06 +00:00
8b34530963 Update src/app/fashion/page.tsx 2026-03-12 03:46:06 +00:00
7d7db123c2 Merge version_1 into main
Merge version_1 into main
2026-03-12 03:39:54 +00:00
69 changed files with 2687 additions and 13308 deletions

View File

@@ -1,520 +1,37 @@
"use client";
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
import NavbarStyleFullscreen from "@/components/navbar/NavbarStyleFullscreen/NavbarStyleFullscreen";
import HeroBillboardTestimonial from "@/components/sections/hero/HeroBillboardTestimonial";
import ProductCardTwo from "@/components/sections/product/ProductCardTwo";
import FeatureCardTen from "@/components/sections/feature/FeatureCardTen";
import MetricSplitMediaAbout from "@/components/sections/about/MetricSplitMediaAbout";
import MetricCardSeven from "@/components/sections/metrics/MetricCardSeven";
import SocialProofOne from "@/components/sections/socialProof/SocialProofOne";
import TestimonialCardFifteen from "@/components/sections/testimonial/TestimonialCardFifteen";
import FaqSplitMedia from "@/components/sections/faq/FaqSplitMedia";
import ContactCTA from "@/components/sections/contact/ContactCTA";
import FooterBase from "@/components/sections/footer/FooterBase";
import Link from "next/link";
import {
Sparkles,
Star,
Grid,
Award,
TrendingUp,
Briefcase,
Mail,
HelpCircle,
Shirt,
Home,
Sofa,
Layout,
Dumbbell,
Activity,
Zap,
Smartphone,
Cpu,
} from "lucide-react";
import { ThemeProvider } from '@/providers/themeProvider/ThemeProvider';
import NavbarStyleFullscreen from '@/components/navbar/NavbarStyleFullscreen/NavbarStyleFullscreen';
export default function ElectronicsPage() {
return (
<ThemeProvider
defaultButtonVariant="hover-magnetic"
defaultTextAnimation="reveal-blur"
defaultButtonVariant="text-stagger"
defaultTextAnimation="entrance-slide"
borderRadius="rounded"
contentWidth="smallMedium"
sizing="mediumLargeSizeLargeTitles"
background="floatingGradient"
cardStyle="glass-depth"
primaryButtonStyle="double-inset"
secondaryButtonStyle="radial-glow"
headingFontWeight="extrabold"
contentWidth="medium"
sizing="medium"
background="circleGradient"
cardStyle="glass-elevated"
primaryButtonStyle="gradient"
secondaryButtonStyle="glass"
headingFontWeight="normal"
>
<div id="nav" data-section="nav">
<NavbarStyleFullscreen
brandName="ZSMX Store"
navItems={[
{ name: "Home", id: "/" },
{ name: "Fashion", id: "fashion" },
{ name: "Home", id: "home-category" },
{ name: "Home & Decor", id: "home-category" },
{ name: "Gym", id: "gym" },
{ name: "Electronics", id: "electronics" },
{ name: "Contact", id: "contact" },
]}
brandName="ZSMX Store"
bottomLeftText="Premium Multi-Category Store"
bottomRightText="hello@zsmxstore.com"
/>
</div>
<div id="hero" data-section="hero">
<HeroBillboardTestimonial
title="Discover Your Perfect Style"
description="Explore our curated collection of fashion, home decor, fitness equipment, and premium electronics. Where quality meets elegance."
tag="Welcome to ZSMX Store"
tagIcon={Sparkles}
tagAnimation="slide-up"
background={{ variant: "floatingGradient" }}
imageSrc="http://img.b2bpic.net/free-photo/internationals-people-standing-cafe_1157-32402.jpg?_wi=3"
imageAlt="Premium multi-category product showcase"
mediaAnimation="slide-up"
testimonials={[
{
name: "Sarah Mitchell",
handle: "Fashion Enthusiast",
testimonial:
"Exceptional quality and stunning designs. ZSMX Store has become my go-to for everything.",
rating: 5,
imageSrc:
"http://img.b2bpic.net/free-photo/portrait-confident-young-businessman-with-his-arms-crossed_23-2148176206.jpg?_wi=3",
imageAlt: "Sarah Mitchell",
},
{
name: "James Chen",
handle: "Interior Designer",
testimonial:
"The home collection is absolutely exquisite. Premium pieces that transform any space.",
rating: 5,
imageSrc:
"http://img.b2bpic.net/free-photo/positive-confident-businessman-posing-outside_74855-1183.jpg?_wi=3",
imageAlt: "James Chen",
},
{
name: "Emma Rodriguez",
handle: "Fitness Coach",
testimonial:
"Top-tier gym equipment. My clients and I love the durability and design.",
rating: 5,
imageSrc:
"http://img.b2bpic.net/free-photo/modern-businesswoman_23-2148012909.jpg?_wi=3",
imageAlt: "Emma Rodriguez",
},
]}
testimonialRotationInterval={5000}
buttons={[
{ text: "Shop Now", href: "https://example.com" },
{ text: "Explore Categories", href: "#" },
]}
buttonAnimation="slide-up"
useInvertedBackground={false}
/>
</div>
<div id="products" data-section="products">
<ProductCardTwo
title="Featured Collection"
description="Hand-picked premium products across all categories. New arrivals updated daily."
tag="Best Sellers"
tagIcon={Star}
tagAnimation="slide-up"
textboxLayout="default"
animationType="slide-up"
gridVariant="bento-grid"
useInvertedBackground={false}
products={[
{
id: "fashion-1",
brand: "LuxeStyle",
name: "Premium Wool Overcoat",
price: "$450.00",
rating: 5,
reviewCount: "342",
imageSrc:
"http://img.b2bpic.net/free-photo/bag-hanging-from-furniture-item-indoors_23-2151073505.jpg?_wi=9",
imageAlt: "Premium Wool Overcoat",
},
{
id: "fashion-2",
brand: "ElegantWear",
name: "Designer Evening Gown",
price: "$680.00",
rating: 5,
reviewCount: "289",
imageSrc:
"http://img.b2bpic.net/free-photo/store-customer-holding-shirt-body_482257-85803.jpg?_wi=7",
imageAlt: "Designer Evening Gown",
},
{
id: "fashion-3",
brand: "ClassicThreads",
name: "Italian Leather Shoes",
price: "$395.00",
rating: 4,
reviewCount: "156",
imageSrc:
"http://img.b2bpic.net/free-photo/still-life-with-classic-shirts_23-2150828626.jpg?_wi=7",
imageAlt: "Italian Leather Shoes",
},
{
id: "home-1",
brand: "Luxehome",
name: "Modern Sectional Sofa",
price: "$1,299.00",
rating: 5,
reviewCount: "201",
imageSrc:
"http://img.b2bpic.net/free-photo/beautiful-dried-flowers-table_23-2149591635.jpg?_wi=5",
imageAlt: "Modern Sectional Sofa",
},
{
id: "home-2",
brand: "DecorPremium",
name: "Crystal Chandelier",
price: "$850.00",
rating: 5,
reviewCount: "178",
imageSrc:
"http://img.b2bpic.net/free-photo/couch-with-cushions-glass-table_1203-764.jpg?_wi=3",
imageAlt: "Crystal Chandelier",
},
{
id: "home-3",
brand: "InteriorLux",
name: "Turkish Area Rug",
price: "$625.00",
rating: 4,
reviewCount: "124",
imageSrc:
"http://img.b2bpic.net/free-photo/cafe-with-coffee-tables-cosy-sofas-plants-shelves_140725-7785.jpg?_wi=3",
imageAlt: "Turkish Area Rug",
},
]}
buttons={[{ text: "View All Products", href: "#" }]}
/>
</div>
<div id="categories" data-section="categories">
<FeatureCardTen
title="Our Category Showcase"
description="Explore our diverse range of premium products across four expertly curated categories. Each collection represents the finest in quality and design."
tag="Categories"
tagIcon={Grid}
tagAnimation="slide-up"
textboxLayout="default"
animationType="slide-up"
useInvertedBackground={false}
features={[
{
id: "1",
title: "Fashion Excellence",
description:
"Premium apparel and accessories designed for those who appreciate style. From casual elegance to formal sophistication.",
media: {
imageSrc:
"http://img.b2bpic.net/free-photo/bag-hanging-from-furniture-item-indoors_23-2151073505.jpg?_wi=10",
},
items: [
{ icon: Shirt, text: "Designer Collections" },
{ icon: Sparkles, text: "Premium Fabrics" },
{ icon: Star, text: "Timeless Styles" },
],
reverse: false,
},
{
id: "2",
title: "Home Furnishings",
description:
"Transform your living space with luxury home decor. Curated pieces that combine functionality with aesthetic elegance.",
media: {
imageSrc:
"http://img.b2bpic.net/free-photo/beautiful-dried-flowers-table_23-2149591635.jpg?_wi=6",
},
items: [
{ icon: Home, text: "Modern Design" },
{ icon: Sofa, text: "Premium Materials" },
{ icon: Layout, text: "Expert Curation" },
],
reverse: true,
},
{
id: "3",
title: "Fitness & Gym",
description:
"Professional-grade fitness equipment and apparel. Engineered for performance and durability in every workout.",
media: {
imageSrc:
"http://img.b2bpic.net/free-photo/still-life-perfectly-ordered-fitness-gym-accessories_52683-100705.jpg?_wi=6",
},
items: [
{ icon: Dumbbell, text: "Professional Equipment" },
{ icon: Activity, text: "Performance Gear" },
{ icon: Zap, text: "High Durability" },
],
reverse: false,
},
{
id: "4",
title: "Premium Electronics",
description:
"Latest technology and innovative gadgets. Cutting-edge devices that enhance your digital lifestyle.",
media: {
imageSrc:
"http://img.b2bpic.net/free-photo/view-robotic-vacuum-cleaner-flat-surface_23-2151736769.jpg?_wi=3",
},
items: [
{ icon: Smartphone, text: "Latest Technology" },
{ icon: Cpu, text: "Advanced Features" },
{ icon: Zap, text: "Top Performance" },
],
reverse: true,
},
]}
/>
</div>
<div id="about" data-section="about">
<MetricSplitMediaAbout
title="Your Trusted Multi-Category Destination"
description="ZSMX Store is your premier destination for premium products across fashion, home, fitness, and electronics. We believe in delivering excellence through carefully curated collections, exceptional quality, and outstanding customer service. Our mission is to make luxury and quality accessible to everyone."
tag="About ZSMX"
tagIcon={Award}
tagAnimation="slide-up"
metrics={[
{ value: "50k+", title: "Satisfied Customers" },
{ value: "10k+", title: "Premium Products" },
]}
imageSrc="http://img.b2bpic.net/free-photo/modern-sauna-with-panoramic-windows-wooden-design_169016-70021.jpg?_wi=3"
imageAlt="ZSMX Store - Premium retail environment"
useInvertedBackground={true}
mediaAnimation="slide-up"
/>
</div>
<div id="metrics" data-section="metrics">
<MetricCardSeven
title="By The Numbers"
description="Trusted by thousands of customers worldwide. Our commitment to quality and service speaks for itself."
tag="Our Growth"
tagIcon={TrendingUp}
tagAnimation="slide-up"
textboxLayout="default"
animationType="slide-up"
useInvertedBackground={false}
metrics={[
{
id: "1",
value: "98%",
title: "Customer Satisfaction Rate",
items: [
"Premium quality guaranteed",
"Expert curation",
"Dedicated support team",
],
},
{
id: "2",
value: "24/7",
title: "Customer Support Available",
items: [
"Real-time assistance",
"Expert consultations",
"Fast responses",
],
},
{
id: "3",
value: "100%",
title: "Authentic Products",
items: [
"Verified sources",
"Quality assurance",
"Brand authenticity",
],
},
{
id: "4",
value: "Free",
title: "Shipping On Orders Over $100",
items: ["Fast delivery", "Tracking included", "Safe packaging"],
},
]}
/>
</div>
<div id="social-proof" data-section="social-proof">
<SocialProofOne
title="Trusted by Leading Brands & Retailers"
description="Partnered with premium brands worldwide to bring you authentic luxury products."
tag="Our Partners"
tagIcon={Briefcase}
tagAnimation="slide-up"
textboxLayout="default"
useInvertedBackground={false}
names={[
"LuxeStyle",
"ElegantWear",
"ClassicThreads",
"Luxehome",
"DecorPremium",
"InteriorLux",
"FitnessPro",
"SportsTech",
]}
speed={40}
showCard={true}
/>
</div>
<div id="testimonials" data-section="testimonials">
<TestimonialCardFifteen
testimonial="ZSMX Store has completely revolutionized how I shop online. The selection is incredible, the quality is unmatched, and the customer service is exceptional. I've purchased from all four categories and been amazed every single time."
rating={5}
author="Victoria Thompson, Premium Lifestyle Enthusiast"
avatars={[
{
src: "http://img.b2bpic.net/free-photo/portrait-confident-young-businessman-with-his-arms-crossed_23-2148176206.jpg",
alt: "Customer 1",
},
{
src: "http://img.b2bpic.net/free-photo/positive-confident-businessman-posing-outside_74855-1183.jpg",
alt: "Customer 2",
},
{
src: "http://img.b2bpic.net/free-photo/modern-businesswoman_23-2148012909.jpg",
alt: "Customer 3",
},
{
src: "http://img.b2bpic.net/free-photo/businessman-formal-wear-professional-corporate-concept_53876-71166.jpg",
alt: "Customer 4",
},
{
src: "http://img.b2bpic.net/free-photo/beautiful-business-woman-portrait_23-2149280717.jpg",
alt: "Customer 5",
},
{
src: "http://img.b2bpic.net/free-photo/portrait-outdoors-business-man-smiles_23-2148763856.jpg",
alt: "Customer 6",
},
]}
ratingAnimation="slide-up"
avatarsAnimation="slide-up"
useInvertedBackground={false}
/>
</div>
<div id="faq" data-section="faq">
<FaqSplitMedia
title="Frequently Asked Questions"
description="Find answers to common questions about our products, ordering, shipping, and customer service."
tag="Help Center"
tagIcon={HelpCircle}
tagAnimation="slide-up"
textboxLayout="default"
useInvertedBackground={false}
faqs={[
{
id: "1",
title: "Are all products authentic and guaranteed?",
content:
"Yes, absolutely. We source directly from authorized distributors and verify authenticity of all products. Every item comes with our quality guarantee and certification of authenticity.",
},
{
id: "2",
title: "What is your return and exchange policy?",
content:
"We offer hassle-free returns and exchanges within 30 days of purchase. Items must be unused and in original packaging. Simply contact our customer service team to initiate the process.",
},
{
id: "3",
title: "How long does shipping typically take?",
content:
"Standard shipping takes 5-7 business days. Express shipping options (2-3 days) are available for most orders. Orders over $100 qualify for free standard shipping.",
},
{
id: "4",
title: "Do you offer international shipping?",
content:
"Yes, we ship to most countries worldwide. International shipping costs vary by location and are calculated at checkout. Customs duties may apply depending on your country.",
},
{
id: "5",
title: "Is my personal information secure?",
content:
"We use industry-standard SSL encryption to protect all personal and payment information. Your data is never shared with third parties. We comply with all privacy regulations.",
},
{
id: "6",
title: "What payment methods do you accept?",
content:
"We accept all major credit cards, debit cards, PayPal, Apple Pay, and Google Pay. All transactions are secure and encrypted.",
},
]}
imageSrc="http://img.b2bpic.net/free-photo/woman-sitting-wheelchair-modern-concept_23-2148497283.jpg?_wi=3"
imageAlt="Customer service support team"
mediaAnimation="slide-up"
faqsAnimation="slide-up"
mediaPosition="left"
animationType="smooth"
/>
</div>
<div id="contact" data-section="contact">
<ContactCTA
tag="Get In Touch"
tagIcon={Mail}
tagAnimation="slide-up"
title="Ready to Discover Premium Products?"
description="Have questions about our products or services? Our expert team is here to help. Contact us today and experience the ZSMX Store difference."
buttons={[
{ text: "Contact Our Team", href: "#" },
{ text: "Shop Now", href: "#" },
]}
buttonAnimation="slide-up"
background={{ variant: "plain" }}
useInvertedBackground={false}
/>
</div>
<div id="footer" data-section="footer">
<FooterBase
logoText="ZSMX Store"
copyrightText="© 2025 ZSMX Store. All rights reserved."
columns={[
{
title: "Shop",
items: [
{ label: "Fashion", href: "#" },
{ label: "Home", href: "#" },
{ label: "Gym", href: "#" },
{ label: "Electronics", href: "#" },
],
},
{
title: "Support",
items: [
{ label: "Contact Us", href: "#" },
{ label: "FAQ", href: "#faq" },
{ label: "Shipping Info", href: "#" },
{ label: "Returns", href: "#" },
],
},
{
title: "Company",
items: [
{ label: "About Us", href: "#about" },
{ label: "Blog", href: "#" },
{ label: "Careers", href: "#" },
{ label: "Privacy Policy", href: "#" },
],
},
]}
/>
</div>
<div>Electronics Page</div>
</ThemeProvider>
);
}
}

View File

@@ -1,248 +1,37 @@
"use client";
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
import NavbarStyleFullscreen from "@/components/navbar/NavbarStyleFullscreen/NavbarStyleFullscreen";
import ProductCardTwo from "@/components/sections/product/ProductCardTwo";
import FooterBase from "@/components/sections/footer/FooterBase";
import Link from "next/link";
import { Star } from "lucide-react";
import { ThemeProvider } from '@/providers/themeProvider/ThemeProvider';
import NavbarStyleFullscreen from '@/components/navbar/NavbarStyleFullscreen/NavbarStyleFullscreen';
export default function FashionPage() {
return (
<ThemeProvider
defaultButtonVariant="hover-magnetic"
defaultTextAnimation="reveal-blur"
defaultButtonVariant="text-stagger"
defaultTextAnimation="entrance-slide"
borderRadius="rounded"
contentWidth="smallMedium"
sizing="mediumLargeSizeLargeTitles"
background="floatingGradient"
cardStyle="glass-depth"
primaryButtonStyle="double-inset"
secondaryButtonStyle="radial-glow"
headingFontWeight="extrabold"
contentWidth="medium"
sizing="medium"
background="circleGradient"
cardStyle="glass-elevated"
primaryButtonStyle="gradient"
secondaryButtonStyle="glass"
headingFontWeight="normal"
>
<div id="nav" data-section="nav">
<NavbarStyleFullscreen
brandName="ZSMX Store"
navItems={[
{ name: "Home", id: "/" },
{ name: "Fashion", id: "fashion" },
{ name: "Home", id: "home-category" },
{ name: "Home & Decor", id: "home-category" },
{ name: "Gym", id: "gym" },
{ name: "Electronics", id: "electronics" },
{ name: "Contact", id: "contact" },
]}
brandName="ZSMX Store"
bottomLeftText="Premium Multi-Category Store"
bottomRightText="hello@zsmxstore.com"
/>
</div>
<div id="fashion-collection" data-section="fashion-collection">
<ProductCardTwo
title="Fashion Collection"
description="Premium apparel and accessories carefully selected for style, quality, and timeless elegance. Discover designer pieces and contemporary fashion."
tag="New Collection"
tagIcon={Star}
tagAnimation="slide-up"
textboxLayout="default"
animationType="slide-up"
gridVariant="bento-grid"
useInvertedBackground={false}
products={[
{
id: "fashion-1",
brand: "LuxeStyle",
name: "Premium Wool Overcoat",
price: "$450.00",
rating: 5,
reviewCount: "342",
imageSrc: "http://img.b2bpic.net/free-photo/bag-hanging-from-furniture-item-indoors_23-2151073505.jpg?_wi=3",
imageAlt: "Premium Wool Overcoat",
},
{
id: "fashion-2",
brand: "ElegantWear",
name: "Designer Evening Gown",
price: "$680.00",
rating: 5,
reviewCount: "289",
imageSrc: "http://img.b2bpic.net/free-photo/store-customer-holding-shirt-body_482257-85803.jpg?_wi=2",
imageAlt: "Designer Evening Gown",
},
{
id: "fashion-3",
brand: "ClassicThreads",
name: "Italian Leather Shoes",
price: "$395.00",
rating: 4,
reviewCount: "156",
imageSrc: "http://img.b2bpic.net/free-photo/still-life-with-classic-shirts_23-2150828626.jpg?_wi=2",
imageAlt: "Italian Leather Shoes",
},
{
id: "fashion-4",
brand: "LuxeStyle",
name: "Cashmere Sweater Collection",
price: "$320.00",
rating: 5,
reviewCount: "267",
imageSrc: "http://img.b2bpic.net/free-photo/bag-hanging-from-furniture-item-indoors_23-2151073505.jpg?_wi=4",
imageAlt: "Cashmere Sweater",
},
{
id: "fashion-5",
brand: "ElegantWear",
name: "Silk Blouse Set",
price: "$275.00",
rating: 5,
reviewCount: "198",
imageSrc: "http://img.b2bpic.net/free-photo/store-customer-holding-shirt-body_482257-85803.jpg?_wi=3",
imageAlt: "Silk Blouse",
},
{
id: "fashion-6",
brand: "ClassicThreads",
name: "Designer Jean Collection",
price: "$199.00",
rating: 4,
reviewCount: "445",
imageSrc: "http://img.b2bpic.net/free-photo/still-life-with-classic-shirts_23-2150828626.jpg?_wi=3",
imageAlt: "Designer Jeans",
},
]}
buttons={[
{ text: "View All", href: "#" },
{ text: "Back to Home", href: "/" },
]}
/>
</div>
<div id="fashion-details" data-section="fashion-details">
<ProductCardTwo
title="Trending Now"
description="Explore the latest fashion trends and seasonal favorites. Premium quality pieces for every occasion and style preference."
tag="Trending"
tagIcon={Star}
tagAnimation="slide-up"
textboxLayout="default"
animationType="slide-up"
gridVariant="three-columns-all-equal-width"
useInvertedBackground={true}
products={[
{
id: "fashion-trend-1",
brand: "LuxeStyle",
name: "Minimalist White Shirt",
price: "$155.00",
rating: 5,
reviewCount: "523",
imageSrc: "http://img.b2bpic.net/free-photo/store-customer-holding-shirt-body_482257-85803.jpg?_wi=4",
imageAlt: "Minimalist White Shirt",
},
{
id: "fashion-trend-2",
brand: "ElegantWear",
name: "Black Leather Jacket",
price: "$495.00",
rating: 5,
reviewCount: "389",
imageSrc: "http://img.b2bpic.net/free-photo/bag-hanging-from-furniture-item-indoors_23-2151073505.jpg?_wi=5",
imageAlt: "Black Leather Jacket",
},
{
id: "fashion-trend-3",
brand: "ClassicThreads",
name: "Neutral Tone Blazer",
price: "$425.00",
rating: 4,
reviewCount: "267",
imageSrc: "http://img.b2bpic.net/free-photo/still-life-with-classic-shirts_23-2150828626.jpg?_wi=4",
imageAlt: "Neutral Blazer",
},
]}
/>
</div>
<div id="fashion-cta" data-section="fashion-cta">
<ProductCardTwo
title="Featured Designers"
description="Exclusive collections from world-renowned fashion designers. Limited edition pieces that showcase luxury and artistry."
tag="Exclusive"
tagIcon={Star}
tagAnimation="slide-up"
textboxLayout="default"
animationType="slide-up"
gridVariant="uniform-all-items-equal"
useInvertedBackground={false}
products={[
{
id: "designer-1",
brand: "ElegantWear",
name: "Haute Couture Gown",
price: "$1,200.00",
rating: 5,
reviewCount: "89",
imageSrc: "http://img.b2bpic.net/free-photo/store-customer-holding-shirt-body_482257-85803.jpg?_wi=5",
imageAlt: "Haute Couture Gown",
},
{
id: "designer-2",
brand: "LuxeStyle",
name: "Signature Collection Dress",
price: "$895.00",
rating: 5,
reviewCount: "134",
imageSrc: "http://img.b2bpic.net/free-photo/bag-hanging-from-furniture-item-indoors_23-2151073505.jpg?_wi=6",
imageAlt: "Signature Dress",
},
{
id: "designer-3",
brand: "ClassicThreads",
name: "Artisan Wool Coat",
price: "$750.00",
rating: 5,
reviewCount: "167",
imageSrc: "http://img.b2bpic.net/free-photo/still-life-with-classic-shirts_23-2150828626.jpg?_wi=5",
imageAlt: "Artisan Wool Coat",
},
]}
/>
</div>
<div id="footer" data-section="footer">
<FooterBase
logoText="ZSMX Store"
copyrightText="© 2025 ZSMX Store. All rights reserved."
columns={[
{
title: "Shop",
items: [
{ label: "Fashion", href: "/fashion" },
{ label: "Home", href: "#home-category" },
{ label: "Gym", href: "#gym" },
{ label: "Electronics", href: "#electronics" },
],
},
{
title: "Support",
items: [
{ label: "Contact Us", href: "#contact" },
{ label: "FAQ", href: "#faq" },
{ label: "Shipping Info", href: "#" },
{ label: "Returns", href: "#" },
],
},
{
title: "Company",
items: [
{ label: "About Us", href: "#about" },
{ label: "Blog", href: "#" },
{ label: "Careers", href: "#" },
{ label: "Privacy Policy", href: "#" },
],
},
]}
/>
</div>
<div>Fashion Page</div>
</ThemeProvider>
);
}
}

View File

@@ -26,11 +26,11 @@ export default function GymPage() {
<div id="nav" data-section="nav">
<NavbarStyleFullscreen
navItems={[
{ name: "Home", id: "/" },
{ name: "Fashion", id: "fashion" },
{ name: "Home", id: "home-category" },
{ name: "Home & Decor", id: "home-category" },
{ name: "Gym", id: "gym" },
{ name: "Electronics", id: "electronics" },
{ name: "Contact", id: "contact" },
]}
brandName="ZSMX Store"
bottomLeftText="Premium Multi-Category Store"
@@ -52,65 +52,23 @@ export default function GymPage() {
useInvertedBackground={false}
products={[
{
id: "gym-1",
brand: "FitnessPro",
name: "Professional Dumbbell Set",
price: "$599.00",
rating: 5,
reviewCount: "287",
imageSrc: "http://img.b2bpic.net/free-photo/still-life-perfectly-ordered-fitness-gym-accessories_52683-100705.jpg?_wi=3",
imageAlt: "Professional Dumbbell Set",
},
id: "gym-1", brand: "FitnessPro", name: "Professional Dumbbell Set", price: "$599.00", rating: 5,
reviewCount: "287", imageSrc: "http://img.b2bpic.net/free-photo/still-life-perfectly-ordered-fitness-gym-accessories_52683-100705.jpg?_wi=3", imageAlt: "Professional Dumbbell Set"},
{
id: "gym-2",
brand: "SportsTech",
name: "High-Performance Treadmill",
price: "$1,199.00",
rating: 5,
reviewCount: "342",
imageSrc: "http://img.b2bpic.net/free-vector/sport-landing-page-template-with-photo_23-2148217108.jpg?_wi=1",
imageAlt: "High-Performance Treadmill",
},
id: "gym-2", brand: "SportsTech", name: "High-Performance Treadmill", price: "$1,199.00", rating: 5,
reviewCount: "342", imageSrc: "http://img.b2bpic.net/free-vector/sport-landing-page-template-with-photo_23-2148217108.jpg?_wi=1", imageAlt: "High-Performance Treadmill"},
{
id: "gym-3",
brand: "EliteGym",
name: "Commercial Weight Bench",
price: "$450.00",
rating: 4,
reviewCount: "198",
imageSrc: "http://img.b2bpic.net/free-photo/perfectly-ordered-compositions-view_23-2149872090.jpg?_wi=1",
imageAlt: "Commercial Weight Bench",
},
id: "gym-3", brand: "EliteGym", name: "Commercial Weight Bench", price: "$450.00", rating: 4,
reviewCount: "198", imageSrc: "http://img.b2bpic.net/free-photo/perfectly-ordered-compositions-view_23-2149872090.jpg?_wi=1", imageAlt: "Commercial Weight Bench"},
{
id: "gym-4",
brand: "PowerTech",
name: "Adjustable Kettlebell Set",
price: "$349.00",
rating: 5,
reviewCount: "265",
imageSrc: "http://img.b2bpic.net/free-photo/still-life-perfectly-ordered-fitness-gym-accessories_52683-100705.jpg?_wi=4",
imageAlt: "Adjustable Kettlebell Set",
},
id: "gym-4", brand: "PowerTech", name: "Adjustable Kettlebell Set", price: "$349.00", rating: 5,
reviewCount: "265", imageSrc: "http://img.b2bpic.net/free-photo/still-life-perfectly-ordered-fitness-gym-accessories_52683-100705.jpg?_wi=4", imageAlt: "Adjustable Kettlebell Set"},
{
id: "gym-5",
brand: "FitGear",
name: "Premium Yoga Mat Collection",
price: "$89.00",
rating: 5,
reviewCount: "521",
imageSrc: "http://img.b2bpic.net/free-vector/sport-landing-page-template-with-photo_23-2148217108.jpg?_wi=2",
imageAlt: "Premium Yoga Mat Collection",
},
id: "gym-5", brand: "FitGear", name: "Premium Yoga Mat Collection", price: "$89.00", rating: 5,
reviewCount: "521", imageSrc: "http://img.b2bpic.net/free-vector/sport-landing-page-template-with-photo_23-2148217108.jpg?_wi=2", imageAlt: "Premium Yoga Mat Collection"},
{
id: "gym-6",
brand: "ResistancePro",
name: "Resistance Band Set",
price: "$79.00",
rating: 4,
reviewCount: "403",
imageSrc: "http://img.b2bpic.net/free-photo/perfectly-ordered-compositions-view_23-2149872090.jpg?_wi=2",
imageAlt: "Resistance Band Set",
},
id: "gym-6", brand: "ResistancePro", name: "Resistance Band Set", price: "$79.00", rating: 4,
reviewCount: "403", imageSrc: "http://img.b2bpic.net/free-photo/perfectly-ordered-compositions-view_23-2149872090.jpg?_wi=2", imageAlt: "Resistance Band Set"},
]}
buttons={[{ text: "View All Gym Gear", href: "/gym" }]}
/>
@@ -121,12 +79,8 @@ export default function GymPage() {
<FeatureCardTen
features={[
{
id: "1",
title: "Strength Training",
description: "Premium dumbbells, barbells, and weight plates. Built for durability and precision in every lift.",
media: {
imageSrc: "http://img.b2bpic.net/free-photo/still-life-perfectly-ordered-fitness-gym-accessories_52683-100705.jpg?_wi=5",
},
id: "1", title: "Strength Training", description: "Premium dumbbells, barbells, and weight plates. Built for durability and precision in every lift.", media: {
imageSrc: "http://img.b2bpic.net/free-photo/still-life-perfectly-ordered-fitness-gym-accessories_52683-100705.jpg?_wi=5"},
items: [
{ icon: Dumbbell, text: "Professional Quality" },
{ icon: Zap, text: "High Performance" },
@@ -135,12 +89,8 @@ export default function GymPage() {
reverse: false,
},
{
id: "2",
title: "Cardio Equipment",
description: "State-of-the-art treadmills, bikes, and rowing machines. Engineered for smooth, efficient workouts.",
media: {
imageSrc: "http://img.b2bpic.net/free-vector/sport-landing-page-template-with-photo_23-2148217108.jpg?_wi=3",
},
id: "2", title: "Cardio Equipment", description: "State-of-the-art treadmills, bikes, and rowing machines. Engineered for smooth, efficient workouts.", media: {
imageSrc: "http://img.b2bpic.net/free-vector/sport-landing-page-template-with-photo_23-2148217108.jpg?_wi=3"},
items: [
{ icon: Activity, text: "Advanced Technology" },
{ icon: Zap, text: "High Durability" },
@@ -149,12 +99,8 @@ export default function GymPage() {
reverse: true,
},
{
id: "3",
title: "Functional Training",
description: "Kettlebells, resistance bands, and functional rigs. Perfect for dynamic, full-body workouts.",
media: {
imageSrc: "http://img.b2bpic.net/free-photo/perfectly-ordered-compositions-view_23-2149872090.jpg?_wi=3",
},
id: "3", title: "Functional Training", description: "Kettlebells, resistance bands, and functional rigs. Perfect for dynamic, full-body workouts.", media: {
imageSrc: "http://img.b2bpic.net/free-photo/perfectly-ordered-compositions-view_23-2149872090.jpg?_wi=3"},
items: [
{ icon: Zap, text: "Versatile Equipment" },
{ icon: Activity, text: "Space-Saving Design" },
@@ -186,35 +132,17 @@ export default function GymPage() {
useInvertedBackground={false}
faqs={[
{
id: "1",
title: "What warranty do gym equipment come with?",
content: "All equipment comes with a comprehensive 2-year warranty covering manufacturing defects. Professional equipment includes extended support options.",
},
id: "1", title: "What warranty do gym equipment come with?", content: "All equipment comes with a comprehensive 2-year warranty covering manufacturing defects. Professional equipment includes extended support options."},
{
id: "2",
title: "Do you offer assembly and installation services?",
content: "Yes, we offer professional assembly and installation for all equipment. White-glove delivery is available in select areas.",
},
id: "2", title: "Do you offer assembly and installation services?", content: "Yes, we offer professional assembly and installation for all equipment. White-glove delivery is available in select areas."},
{
id: "3",
title: "Can I rent equipment before purchasing?",
content: "We offer a 30-day trial rental program for major equipment. Rental fees can be applied toward purchase if you decide to buy.",
},
id: "3", title: "Can I rent equipment before purchasing?", content: "We offer a 30-day trial rental program for major equipment. Rental fees can be applied toward purchase if you decide to buy."},
{
id: "4",
title: "Are there financing options available?",
content: "Yes, we offer flexible financing plans with 0% APR for 12 months on purchases over $500.",
},
id: "4", title: "Are there financing options available?", content: "Yes, we offer flexible financing plans with 0% APR for 12 months on purchases over $500."},
{
id: "5",
title: "What is your return policy for gym equipment?",
content: "Equipment can be returned within 30 days if unused and in original condition. We handle return shipping for most items.",
},
id: "5", title: "What is your return policy for gym equipment?", content: "Equipment can be returned within 30 days if unused and in original condition. We handle return shipping for most items."},
{
id: "6",
title: "Do you provide maintenance and repair services?",
content: "We offer comprehensive maintenance plans and professional repair services. Contact our team for custom support options.",
},
id: "6", title: "Do you provide maintenance and repair services?", content: "We offer comprehensive maintenance plans and professional repair services. Contact our team for custom support options."},
]}
imageSrc="http://img.b2bpic.net/free-photo/woman-sitting-wheelchair-modern-concept_23-2148497283.jpg?_wi=2"
imageAlt="Customer service support team"
@@ -232,8 +160,7 @@ export default function GymPage() {
copyrightText="© 2025 ZSMX Store. All rights reserved."
columns={[
{
title: "Shop",
items: [
title: "Shop", items: [
{ label: "Fashion", href: "fashion" },
{ label: "Home", href: "home-category" },
{ label: "Gym", href: "gym" },
@@ -241,8 +168,7 @@ export default function GymPage() {
],
},
{
title: "Support",
items: [
title: "Support", items: [
{ label: "Contact Us", href: "#contact" },
{ label: "FAQ", href: "#faq" },
{ label: "Shipping Info", href: "#" },
@@ -250,8 +176,7 @@ export default function GymPage() {
],
},
{
title: "Company",
items: [
title: "Company", items: [
{ label: "About Us", href: "#about" },
{ label: "Blog", href: "#" },
{ label: "Careers", href: "#" },
@@ -263,4 +188,4 @@ export default function GymPage() {
</div>
</ThemeProvider>
);
}
}

View File

@@ -1,381 +1,37 @@
"use client";
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
import NavbarStyleFullscreen from "@/components/navbar/NavbarStyleFullscreen/NavbarStyleFullscreen";
import HeroBillboardTestimonial from "@/components/sections/hero/HeroBillboardTestimonial";
import ProductCardTwo from "@/components/sections/product/ProductCardTwo";
import FeatureCardTen from "@/components/sections/feature/FeatureCardTen";
import MetricSplitMediaAbout from "@/components/sections/about/MetricSplitMediaAbout";
import MetricCardSeven from "@/components/sections/metrics/MetricCardSeven";
import SocialProofOne from "@/components/sections/socialProof/SocialProofOne";
import FooterBase from "@/components/sections/footer/FooterBase";
import Link from "next/link";
import { Sparkles, Star, Grid, Award, TrendingUp, Briefcase, Home, Shirt, Sofa, Layout, Dumbbell, Activity, Zap, Smartphone, Cpu } from "lucide-react";
import { ThemeProvider } from '@/providers/themeProvider/ThemeProvider';
import NavbarStyleFullscreen from '@/components/navbar/NavbarStyleFullscreen/NavbarStyleFullscreen';
export default function HomeCategoryPage() {
export default function HomePage() {
return (
<ThemeProvider
defaultButtonVariant="hover-magnetic"
defaultTextAnimation="reveal-blur"
defaultButtonVariant="text-stagger"
defaultTextAnimation="entrance-slide"
borderRadius="rounded"
contentWidth="smallMedium"
sizing="mediumLargeSizeLargeTitles"
background="floatingGradient"
cardStyle="glass-depth"
primaryButtonStyle="double-inset"
secondaryButtonStyle="radial-glow"
headingFontWeight="extrabold"
contentWidth="medium"
sizing="medium"
background="circleGradient"
cardStyle="glass-elevated"
primaryButtonStyle="gradient"
secondaryButtonStyle="glass"
headingFontWeight="normal"
>
{/* Navbar */}
<div id="nav" data-section="nav">
<NavbarStyleFullscreen
navItems={[
{ name: "Home", id: "/" },
{ name: "Fashion", id: "fashion" },
{ name: "Home", id: "home-category" },
{ name: "Home & Decor", id: "home-category" },
{ name: "Gym", id: "gym" },
{ name: "Electronics", id: "electronics" },
{ name: "Contact", id: "contact" },
]}
brandName="ZSMX Store"
bottomLeftText="Premium Multi-Category Store"
bottomRightText="hello@zsmxstore.com"
/>
</div>
{/* Hero Section */}
<div id="hero" data-section="hero">
<HeroBillboardTestimonial
title="Discover Your Perfect Style"
description="Explore our curated collection of fashion, home decor, fitness equipment, and premium electronics. Where quality meets elegance."
tag="Welcome to ZSMX Store"
tagIcon={Sparkles}
tagAnimation="slide-up"
background={{ variant: "floatingGradient" }}
imageSrc="http://img.b2bpic.net/free-photo/internationals-people-standing-cafe_1157-32402.jpg?_wi=2"
imageAlt="Premium multi-category product showcase"
mediaAnimation="slide-up"
testimonials={[
{
name: "Sarah Mitchell",
handle: "Fashion Enthusiast",
testimonial: "Exceptional quality and stunning designs. ZSMX Store has become my go-to for everything.",
rating: 5,
imageSrc: "http://img.b2bpic.net/free-photo/portrait-confident-young-businessman-with-his-arms-crossed_23-2148176206.jpg?_wi=2",
imageAlt: "Sarah Mitchell",
},
{
name: "James Chen",
handle: "Interior Designer",
testimonial: "The home collection is absolutely exquisite. Premium pieces that transform any space.",
rating: 5,
imageSrc: "http://img.b2bpic.net/free-photo/positive-confident-businessman-posing-outside_74855-1183.jpg?_wi=2",
imageAlt: "James Chen",
},
{
name: "Emma Rodriguez",
handle: "Fitness Coach",
testimonial: "Top-tier gym equipment. My clients and I love the durability and design.",
rating: 5,
imageSrc: "http://img.b2bpic.net/free-photo/modern-businesswoman_23-2148012909.jpg?_wi=2",
imageAlt: "Emma Rodriguez",
},
]}
testimonialRotationInterval={5000}
buttons={[
{ text: "Shop Now", href: "/home" },
{ text: "Explore Categories", href: "/home" },
]}
buttonAnimation="slide-up"
useInvertedBackground={false}
/>
</div>
{/* Featured Products Section */}
<div id="products" data-section="products">
<ProductCardTwo
title="Featured Collection"
description="Hand-picked premium products across all categories. New arrivals updated daily."
tag="Best Sellers"
tagIcon={Star}
tagAnimation="slide-up"
textboxLayout="default"
animationType="slide-up"
gridVariant="bento-grid"
useInvertedBackground={false}
products={[
{
id: "fashion-1",
brand: "LuxeStyle",
name: "Premium Wool Overcoat",
price: "$450.00",
rating: 5,
reviewCount: "342",
imageSrc: "http://img.b2bpic.net/free-photo/bag-hanging-from-furniture-item-indoors_23-2151073505.jpg?_wi=7",
imageAlt: "Premium Wool Overcoat",
},
{
id: "fashion-2",
brand: "ElegantWear",
name: "Designer Evening Gown",
price: "$680.00",
rating: 5,
reviewCount: "289",
imageSrc: "http://img.b2bpic.net/free-photo/store-customer-holding-shirt-body_482257-85803.jpg?_wi=6",
imageAlt: "Designer Evening Gown",
},
{
id: "fashion-3",
brand: "ClassicThreads",
name: "Italian Leather Shoes",
price: "$395.00",
rating: 4,
reviewCount: "156",
imageSrc: "http://img.b2bpic.net/free-photo/still-life-with-classic-shirts_23-2150828626.jpg?_wi=6",
imageAlt: "Italian Leather Shoes",
},
{
id: "home-1",
brand: "Luxehome",
name: "Modern Sectional Sofa",
price: "$1,299.00",
rating: 5,
reviewCount: "201",
imageSrc: "http://img.b2bpic.net/free-photo/beautiful-dried-flowers-table_23-2149591635.jpg?_wi=3",
imageAlt: "Modern Sectional Sofa",
},
{
id: "home-2",
brand: "DecorPremium",
name: "Crystal Chandelier",
price: "$850.00",
rating: 5,
reviewCount: "178",
imageSrc: "http://img.b2bpic.net/free-photo/couch-with-cushions-glass-table_1203-764.jpg?_wi=2",
imageAlt: "Crystal Chandelier",
},
{
id: "home-3",
brand: "InteriorLux",
name: "Turkish Area Rug",
price: "$625.00",
rating: 4,
reviewCount: "124",
imageSrc: "http://img.b2bpic.net/free-photo/cafe-with-coffee-tables-cosy-sofas-plants-shelves_140725-7785.jpg?_wi=2",
imageAlt: "Turkish Area Rug",
},
]}
buttons={[{ text: "View All Products", href: "/home" }]}
/>
</div>
{/* Category Showcase Section */}
<div id="categories" data-section="categories">
<FeatureCardTen
features={[
{
id: "1",
title: "Fashion Excellence",
description: "Premium apparel and accessories designed for those who appreciate style. From casual elegance to formal sophistication.",
media: {
imageSrc: "http://img.b2bpic.net/free-photo/bag-hanging-from-furniture-item-indoors_23-2151073505.jpg?_wi=8",
},
items: [
{ icon: Shirt, text: "Designer Collections" },
{ icon: Sparkles, text: "Premium Fabrics" },
{ icon: Sparkles, text: "Timeless Styles" },
],
reverse: false,
},
{
id: "2",
title: "Home Furnishings",
description: "Transform your living space with luxury home decor. Curated pieces that combine functionality with aesthetic elegance.",
media: {
imageSrc: "http://img.b2bpic.net/free-photo/beautiful-dried-flowers-table_23-2149591635.jpg?_wi=4",
},
items: [
{ icon: Home, text: "Modern Design" },
{ icon: Sofa, text: "Premium Materials" },
{ icon: Layout, text: "Expert Curation" },
],
reverse: true,
},
{
id: "3",
title: "Fitness & Gym",
description: "Professional-grade fitness equipment and apparel. Engineered for performance and durability in every workout.",
media: {
imageSrc: "http://img.b2bpic.net/free-photo/still-life-perfectly-ordered-fitness-gym-accessories_52683-100705.jpg?_wi=2",
},
items: [
{ icon: Dumbbell, text: "Professional Equipment" },
{ icon: Activity, text: "Performance Gear" },
{ icon: Zap, text: "High Durability" },
],
reverse: false,
},
{
id: "4",
title: "Premium Electronics",
description: "Latest technology and innovative gadgets. Cutting-edge devices that enhance your digital lifestyle.",
media: {
imageSrc: "http://img.b2bpic.net/free-photo/view-robotic-vacuum-cleaner-flat-surface_23-2151736769.jpg?_wi=2",
},
items: [
{ icon: Smartphone, text: "Latest Technology" },
{ icon: Cpu, text: "Advanced Features" },
{ icon: Zap, text: "Top Performance" },
],
reverse: true,
},
]}
title="Our Category Showcase"
description="Explore our diverse range of premium products across four expertly curated categories. Each collection represents the finest in quality and design."
tag="Categories"
tagIcon={Grid}
tagAnimation="slide-up"
textboxLayout="default"
animationType="slide-up"
useInvertedBackground={false}
/>
</div>
{/* About Section */}
<div id="about" data-section="about">
<MetricSplitMediaAbout
title="Your Trusted Multi-Category Destination"
description="ZSMX Store is your premier destination for premium products across fashion, home, fitness, and electronics. We believe in delivering excellence through carefully curated collections, exceptional quality, and outstanding customer service. Our mission is to make luxury and quality accessible to everyone."
tag="About ZSMX"
tagIcon={Award}
tagAnimation="slide-up"
metrics={[
{ value: "50k+", title: "Satisfied Customers" },
{ value: "10k+", title: "Premium Products" },
]}
imageSrc="http://img.b2bpic.net/free-photo/modern-sauna-with-panoramic-windows-wooden-design_169016-70021.jpg?_wi=2"
imageAlt="ZSMX Store - Premium retail environment"
useInvertedBackground={true}
mediaAnimation="slide-up"
/>
</div>
{/* Metrics Section */}
<div id="metrics" data-section="metrics">
<MetricCardSeven
metrics={[
{
id: "1",
value: "98%",
title: "Customer Satisfaction Rate",
items: [
"Premium quality guaranteed",
"Expert curation",
"Dedicated support team",
],
},
{
id: "2",
value: "24/7",
title: "Customer Support Available",
items: [
"Real-time assistance",
"Expert consultations",
"Fast responses",
],
},
{
id: "3",
value: "100%",
title: "Authentic Products",
items: [
"Verified sources",
"Quality assurance",
"Brand authenticity",
],
},
{
id: "4",
value: "Free",
title: "Shipping On Orders Over $100",
items: [
"Fast delivery",
"Tracking included",
"Safe packaging",
],
},
]}
title="By The Numbers"
description="Trusted by thousands of customers worldwide. Our commitment to quality and service speaks for itself."
tag="Our Growth"
tagIcon={TrendingUp}
tagAnimation="slide-up"
textboxLayout="default"
animationType="slide-up"
useInvertedBackground={false}
/>
</div>
{/* Social Proof Section */}
<div id="social-proof" data-section="social-proof">
<SocialProofOne
title="Trusted by Leading Brands & Retailers"
description="Partnered with premium brands worldwide to bring you authentic luxury products."
tag="Our Partners"
tagIcon={Briefcase}
tagAnimation="slide-up"
textboxLayout="default"
useInvertedBackground={false}
names={[
"LuxeStyle",
"ElegantWear",
"ClassicThreads",
"Luxehome",
"DecorPremium",
"InteriorLux",
"FitnessPro",
"SportsTech",
]}
speed={40}
showCard={true}
/>
</div>
{/* Footer */}
<div id="footer" data-section="footer">
<FooterBase
logoText="ZSMX Store"
copyrightText="© 2025 ZSMX Store. All rights reserved."
columns={[
{
title: "Shop",
items: [
{ label: "Fashion", href: "fashion" },
{ label: "Home", href: "home-category" },
{ label: "Gym", href: "gym" },
{ label: "Electronics", href: "electronics" },
],
},
{
title: "Support",
items: [
{ label: "Contact Us", href: "#contact" },
{ label: "FAQ", href: "#faq" },
{ label: "Shipping Info", href: "#" },
{ label: "Returns", href: "#" },
],
},
{
title: "Company",
items: [
{ label: "About Us", href: "#about" },
{ label: "Blog", href: "#" },
{ label: "Careers", href: "#" },
{ label: "Privacy Policy", href: "#" },
],
},
]}
/>
</div>
<div>Home Page</div>
</ThemeProvider>
);
}
}

View File

@@ -1,359 +1,195 @@
"use client";
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
import NavbarStyleFullscreen from "@/components/navbar/NavbarStyleFullscreen/NavbarStyleFullscreen";
import HeroBillboardTestimonial from "@/components/sections/hero/HeroBillboardTestimonial";
import ProductCardTwo from "@/components/sections/product/ProductCardTwo";
import FeatureCardTen from "@/components/sections/feature/FeatureCardTen";
import MetricSplitMediaAbout from "@/components/sections/about/MetricSplitMediaAbout";
import MetricCardSeven from "@/components/sections/metrics/MetricCardSeven";
import SocialProofOne from "@/components/sections/socialProof/SocialProofOne";
import TestimonialCardFifteen from "@/components/sections/testimonial/TestimonialCardFifteen";
import FaqSplitMedia from "@/components/sections/faq/FaqSplitMedia";
import ContactCTA from "@/components/sections/contact/ContactCTA";
import FooterBase from "@/components/sections/footer/FooterBase";
import Link from "next/link";
import { Sparkles, Star, Grid, Award, TrendingUp, Briefcase, Mail, HelpCircle, Shirt, Heart, Home, Sofa, Layout, Dumbbell, Activity, Zap, Smartphone, Cpu } from "lucide-react";
import React from 'react';
import { ThemeProvider } from '@/providers/themeProvider/ThemeProvider';
import NavbarStyleFullscreen from '@/components/navbar/NavbarStyleFullscreen/NavbarStyleFullscreen';
import HeroBillboardTestimonial from '@/components/sections/hero/HeroBillboardTestimonial';
import ProductCardTwo from '@/components/sections/product/ProductCardTwo';
import FeatureCardTen from '@/components/sections/feature/FeatureCardTen';
import MetricSplitMediaAbout from '@/components/sections/about/MetricSplitMediaAbout';
import MetricCardSeven from '@/components/sections/metrics/MetricCardSeven';
import SocialProofOne from '@/components/sections/socialProof/SocialProofOne';
import TestimonialCardFifteen from '@/components/sections/testimonial/TestimonialCardFifteen';
import FaqSplitMedia from '@/components/sections/faq/FaqSplitMedia';
import ContactCTA from '@/components/sections/contact/ContactCTA';
import FooterBase from '@/components/sections/footer/FooterBase';
import Link from 'next/link';
import { Sparkles, TrendingUp, Users, ArrowRight } from 'lucide-react';
export default function HomePage() {
const plan = {
theme: {
defaultButtonVariant: "text-stagger" as const,
defaultTextAnimation: "entrance-slide" as const,
borderRadius: "rounded" as const,
contentWidth: "medium" as const,
sizing: "medium" as const,
background: "circleGradient" as const,
cardStyle: "glass-elevated" as const,
primaryButtonStyle: "gradient" as const,
secondaryButtonStyle: "glass" as const,
headingFontWeight: "normal" as const,
},
};
const navItems = [
{ name: "Home", id: "/" },
{ name: "About", id: "about" },
{ name: "Services", id: "categories" },
{ name: "Contact", id: "contact" },
];
export default function Home() {
return (
<ThemeProvider
defaultButtonVariant="hover-magnetic"
defaultTextAnimation="reveal-blur"
borderRadius="rounded"
contentWidth="smallMedium"
sizing="mediumLargeSizeLargeTitles"
background="floatingGradient"
cardStyle="glass-depth"
primaryButtonStyle="double-inset"
secondaryButtonStyle="radial-glow"
headingFontWeight="extrabold"
defaultButtonVariant={plan.theme.defaultButtonVariant}
defaultTextAnimation={plan.theme.defaultTextAnimation}
borderRadius={plan.theme.borderRadius}
contentWidth={plan.theme.contentWidth}
sizing={plan.theme.sizing}
background={plan.theme.background}
cardStyle={plan.theme.cardStyle}
primaryButtonStyle={plan.theme.primaryButtonStyle}
secondaryButtonStyle={plan.theme.secondaryButtonStyle}
headingFontWeight={plan.theme.headingFontWeight}
>
<div id="nav" data-section="nav">
<NavbarStyleFullscreen
brandName="ZSMX Store"
navItems={[
{ name: "Fashion", id: "fashion" },
{ name: "Home", id: "home-category" },
{ name: "Gym", id: "gym" },
{ name: "Electronics", id: "electronics" },
{ name: "Contact", id: "contact" },
]}
bottomLeftText="Premium Multi-Category Store"
bottomRightText="hello@zsmxstore.com"
/>
<NavbarStyleFullscreen navItems={navItems} />
</div>
<div id="hero" data-section="hero">
<HeroBillboardTestimonial
title="Discover Your Perfect Style"
description="Explore our curated collection of fashion, home decor, fitness equipment, and premium electronics. Where quality meets elegance."
tag="Welcome to ZSMX Store"
background={{ variant: "radial-gradient" }}
tag="Testimonials"
tagIcon={Sparkles}
tagAnimation="slide-up"
background={{ variant: "floatingGradient" }}
imageSrc="http://img.b2bpic.net/free-photo/internationals-people-standing-cafe_1157-32402.jpg?_wi=1"
imageAlt="Premium multi-category product showcase"
mediaAnimation="slide-up"
title="What Our Customers Say"
description="Hear from our satisfied clients about their experience with our products and services."
testimonials={[
{
name: "Sarah Mitchell",
handle: "Fashion Enthusiast",
testimonial: "Exceptional quality and stunning designs. ZSMX Store has become my go-to for everything.",
rating: 5,
imageSrc: "http://img.b2bpic.net/free-photo/portrait-confident-young-businessman-with-his-arms-crossed_23-2148176206.jpg?_wi=1",
imageAlt: "Sarah Mitchell",
},
name: "Sarah Johnson", handle: "@sarahj", testimonial: "Amazing product that transformed our workflow!", rating: 5,
imageSrc: "/placeholders/placeholder1.webp"},
{
name: "James Chen",
handle: "Interior Designer",
testimonial: "The home collection is absolutely exquisite. Premium pieces that transform any space.",
rating: 5,
imageSrc: "http://img.b2bpic.net/free-photo/positive-confident-businessman-posing-outside_74855-1183.jpg?_wi=1",
imageAlt: "James Chen",
},
{
name: "Emma Rodriguez",
handle: "Fitness Coach",
testimonial: "Top-tier gym equipment. My clients and I love the durability and design.",
rating: 5,
imageSrc: "http://img.b2bpic.net/free-photo/modern-businesswoman_23-2148012909.jpg?_wi=1",
imageAlt: "Emma Rodriguez",
},
name: "John Doe", handle: "@johndoe", testimonial: "Great support and excellent service. Highly recommended!", rating: 5,
imageSrc: "/placeholders/placeholder2.webp"},
]}
testimonialRotationInterval={5000}
buttons={[
{ text: "Shop Now", href: "/fashion" },
{ text: "Explore Categories", href: "#categories" },
{ text: "Get Started", href: "/" },
{ text: "Learn More", href: "categories" },
]}
buttonAnimation="slide-up"
useInvertedBackground={false}
/>
</div>
<div id="products" data-section="products">
<ProductCardTwo
title="Featured Collection"
description="Hand-picked premium products across all categories. New arrivals updated daily."
tag="Best Sellers"
tagIcon={Star}
tagAnimation="slide-up"
textboxLayout="default"
animationType="slide-up"
gridVariant="bento-grid"
useInvertedBackground={false}
products={[
{
id: "fashion-1",
brand: "LuxeStyle",
name: "Premium Wool Overcoat",
price: "$450.00",
rating: 5,
reviewCount: "342",
imageSrc: "http://img.b2bpic.net/free-photo/bag-hanging-from-furniture-item-indoors_23-2151073505.jpg?_wi=1",
imageAlt: "Premium Wool Overcoat",
},
id: "1", brand: "Premium", name: "Eclipse Motion Pro", price: "$150", rating: 5,
reviewCount: "128", imageSrc: "/placeholders/placeholder1.webp"},
{
id: "fashion-2",
brand: "ElegantWear",
name: "Designer Evening Gown",
price: "$680.00",
rating: 5,
reviewCount: "289",
imageSrc: "http://img.b2bpic.net/free-photo/store-customer-holding-shirt-body_482257-85803.jpg?_wi=1",
imageAlt: "Designer Evening Gown",
},
id: "2", brand: "Standard", name: "Wave Dynamics", price: "$99", rating: 4,
reviewCount: "95", imageSrc: "/placeholders/placeholder2.webp"},
{
id: "fashion-3",
brand: "ClassicThreads",
name: "Italian Leather Shoes",
price: "$395.00",
rating: 4,
reviewCount: "156",
imageSrc: "http://img.b2bpic.net/free-photo/still-life-with-classic-shirts_23-2150828626.jpg?_wi=1",
imageAlt: "Italian Leather Shoes",
},
{
id: "home-1",
brand: "Luxehome",
name: "Modern Sectional Sofa",
price: "$1,299.00",
rating: 5,
reviewCount: "201",
imageSrc: "http://img.b2bpic.net/free-photo/beautiful-dried-flowers-table_23-2149591635.jpg?_wi=1",
imageAlt: "Modern Sectional Sofa",
},
{
id: "home-2",
brand: "DecorPremium",
name: "Crystal Chandelier",
price: "$850.00",
rating: 5,
reviewCount: "178",
imageSrc: "http://img.b2bpic.net/free-photo/couch-with-cushions-glass-table_1203-764.jpg?_wi=1",
imageAlt: "Crystal Chandelier",
},
{
id: "home-3",
brand: "InteriorLux",
name: "Turkish Area Rug",
price: "$625.00",
rating: 4,
reviewCount: "124",
imageSrc: "http://img.b2bpic.net/free-photo/cafe-with-coffee-tables-cosy-sofas-plants-shelves_140725-7785.jpg?_wi=1",
imageAlt: "Turkish Area Rug",
},
id: "3", brand: "Elite", name: "Aurora Series", price: "$199", rating: 5,
reviewCount: "156", imageSrc: "/placeholders/placeholder3.webp"},
]}
buttons={[{ text: "View All Products", href: "/fashion" }]}
gridVariant="three-columns-all-equal-width"
animationType="slide-up"
title="Featured Products"
description="Discover our latest collection of premium products"
textboxLayout="default"
useInvertedBackground={false}
/>
</div>
<div id="categories" data-section="categories">
<FeatureCardTen
title="Our Category Showcase"
description="Explore our diverse range of premium products across four expertly curated categories. Each collection represents the finest in quality and design."
tag="Categories"
tagIcon={Grid}
tagAnimation="slide-up"
textboxLayout="default"
animationType="slide-up"
useInvertedBackground={false}
features={[
{
id: "1",
title: "Fashion Excellence",
description: "Premium apparel and accessories designed for those who appreciate style. From casual elegance to formal sophistication.",
media: {
imageSrc: "http://img.b2bpic.net/free-photo/bag-hanging-from-furniture-item-indoors_23-2151073505.jpg?_wi=2",
},
id: "1", title: "Fast Performance", description: "Lightning-fast speeds optimized for your workflow", media: { imageSrc: "/placeholders/placeholder1.webp", imageAlt: "Performance" },
items: [
{ icon: Shirt, text: "Designer Collections" },
{ icon: Sparkles, text: "Premium Fabrics" },
{ icon: Heart, text: "Timeless Styles" },
{ icon: TrendingUp, text: "10x faster processing" },
{ icon: Users, text: "Real-time collaboration" },
],
reverse: false,
},
{
id: "2",
title: "Home Furnishings",
description: "Transform your living space with luxury home decor. Curated pieces that combine functionality with aesthetic elegance.",
media: {
imageSrc: "http://img.b2bpic.net/free-photo/beautiful-dried-flowers-table_23-2149591635.jpg?_wi=2",
},
id: "2", title: "Scalable Solutions", description: "Grow your business without limitations", media: { imageSrc: "/placeholders/placeholder2.webp", imageAlt: "Scalability" },
items: [
{ icon: Home, text: "Modern Design" },
{ icon: Sofa, text: "Premium Materials" },
{ icon: Layout, text: "Expert Curation" },
],
reverse: true,
},
{
id: "3",
title: "Fitness & Gym",
description: "Professional-grade fitness equipment and apparel. Engineered for performance and durability in every workout.",
media: {
imageSrc: "http://img.b2bpic.net/free-photo/still-life-perfectly-ordered-fitness-gym-accessories_52683-100705.jpg?_wi=1",
},
items: [
{ icon: Dumbbell, text: "Professional Equipment" },
{ icon: Activity, text: "Performance Gear" },
{ icon: Zap, text: "High Durability" },
],
reverse: false,
},
{
id: "4",
title: "Premium Electronics",
description: "Latest technology and innovative gadgets. Cutting-edge devices that enhance your digital lifestyle.",
media: {
imageSrc: "http://img.b2bpic.net/free-photo/view-robotic-vacuum-cleaner-flat-surface_23-2151736769.jpg?_wi=1",
},
items: [
{ icon: Smartphone, text: "Latest Technology" },
{ icon: Cpu, text: "Advanced Features" },
{ icon: Zap, text: "Top Performance" },
{ icon: ArrowRight, text: "Unlimited growth" },
{ icon: Sparkles, text: "Enterprise ready" },
],
reverse: true,
},
]}
title="Why Choose Us"
description="Powerful features designed for success"
textboxLayout="default"
animationType="slide-up"
useInvertedBackground={false}
/>
</div>
<div id="about" data-section="about">
<MetricSplitMediaAbout
title="Your Trusted Multi-Category Destination"
description="ZSMX Store is your premier destination for premium products across fashion, home, fitness, and electronics. We believe in delivering excellence through carefully curated collections, exceptional quality, and outstanding customer service. Our mission is to make luxury and quality accessible to everyone."
tag="About ZSMX"
tagIcon={Award}
tagAnimation="slide-up"
title="About Our Company"
description="We're dedicated to delivering excellence and innovation in everything we do."
metrics={[
{ value: "50k+", title: "Satisfied Customers" },
{ value: "10k+", title: "Premium Products" },
{ value: "10+", title: "Years Experience" },
{ value: "500+", title: "Happy Clients" },
{ value: "50M+", title: "Users Worldwide" },
{ value: "99.9%", title: "Uptime" },
]}
imageSrc="http://img.b2bpic.net/free-photo/modern-sauna-with-panoramic-windows-wooden-design_169016-70021.jpg?_wi=1"
imageAlt="ZSMX Store - Premium retail environment"
useInvertedBackground={true}
imageSrc="/placeholders/placeholder1.webp"
imageAlt="About us"
mediaAnimation="slide-up"
metricsAnimation="slide-up"
useInvertedBackground={false}
/>
</div>
<div id="metrics" data-section="metrics">
<MetricCardSeven
title="By The Numbers"
description="Trusted by thousands of customers worldwide. Our commitment to quality and service speaks for itself."
tag="Our Growth"
tagIcon={TrendingUp}
tagAnimation="slide-up"
textboxLayout="default"
animationType="slide-up"
useInvertedBackground={false}
metrics={[
{
id: "1",
value: "98%",
title: "Customer Satisfaction Rate",
items: [
"Premium quality guaranteed",
"Expert curation",
"Dedicated support team",
],
id: "1", value: "7,000+", title: "Conversions", items: ["Increased by 45%", "Monthly growth"],
},
{
id: "2",
value: "24/7",
title: "Customer Support Available",
items: ["Real-time assistance", "Expert consultations", "Fast responses"],
id: "2", value: "50,000+", title: "Active Users", items: ["Growing daily", "Engaged community"],
},
{
id: "3",
value: "100%",
title: "Authentic Products",
items: ["Verified sources", "Quality assurance", "Brand authenticity"],
id: "3", value: "$2.5M", title: "Revenue", items: ["Year-over-year", "Consistent growth"],
},
{
id: "4",
value: "Free",
title: "Shipping On Orders Over $100",
items: ["Fast delivery", "Tracking included", "Safe packaging"],
id: "4", value: "99.9%", title: "Uptime", items: ["24/7 monitoring", "Reliable service"],
},
]}
animationType="slide-up"
title="Performance Metrics"
description="See how we're making a difference"
textboxLayout="default"
useInvertedBackground={false}
/>
</div>
<div id="social-proof" data-section="social-proof">
<SocialProofOne
title="Trusted by Leading Brands & Retailers"
description="Partnered with premium brands worldwide to bring you authentic luxury products."
tag="Our Partners"
tagIcon={Briefcase}
tagAnimation="slide-up"
names={["Company A", "Company B", "Company C", "Company D", "Company E"]}
title="Trusted by Leading Companies"
description="Join thousands of businesses using our platform"
textboxLayout="default"
useInvertedBackground={false}
names={[
"LuxeStyle",
"ElegantWear",
"ClassicThreads",
"Luxehome",
"DecorPremium",
"InteriorLux",
"FitnessPro",
"SportsTech",
]}
speed={40}
showCard={true}
/>
</div>
<div id="testimonials" data-section="testimonials">
<TestimonialCardFifteen
testimonial="ZSMX Store has completely revolutionized how I shop online. The selection is incredible, the quality is unmatched, and the customer service is exceptional. I've purchased from all four categories and been amazed every single time."
testimonial="This platform has completely transformed how we manage our business. The support team is exceptional!"
rating={5}
author="Victoria Thompson, Premium Lifestyle Enthusiast"
author="Jane Smith"
avatars={[
{
src: "http://img.b2bpic.net/free-photo/portrait-confident-young-businessman-with-his-arms-crossed_23-2148176206.jpg",
alt: "Customer 1",
},
{
src: "http://img.b2bpic.net/free-photo/positive-confident-businessman-posing-outside_74855-1183.jpg",
alt: "Customer 2",
},
{
src: "http://img.b2bpic.net/free-photo/modern-businesswoman_23-2148012909.jpg",
alt: "Customer 3",
},
{
src: "http://img.b2bpic.net/free-photo/businessman-formal-wear-professional-corporate-concept_53876-71166.jpg",
alt: "Customer 4",
},
{
src: "http://img.b2bpic.net/free-photo/beautiful-business-woman-portrait_23-2149280717.jpg",
alt: "Customer 5",
},
{
src: "http://img.b2bpic.net/free-photo/portrait-outdoors-business-man-smiles_23-2148763856.jpg",
alt: "Customer 6",
},
{ src: "/placeholders/placeholder1.webp", alt: "Avatar 1" },
{ src: "/placeholders/placeholder2.webp", alt: "Avatar 2" },
{ src: "/placeholders/placeholder3.webp", alt: "Avatar 3" },
]}
ratingAnimation="slide-up"
avatarsAnimation="slide-up"
@@ -363,112 +199,69 @@ export default function HomePage() {
<div id="faq" data-section="faq">
<FaqSplitMedia
title="Frequently Asked Questions"
description="Find answers to common questions about our products, ordering, shipping, and customer service."
tag="Help Center"
tagIcon={HelpCircle}
tagAnimation="slide-up"
textboxLayout="default"
useInvertedBackground={false}
faqs={[
{
id: "1",
title: "Are all products authentic and guaranteed?",
content:
"Yes, absolutely. We source directly from authorized distributors and verify authenticity of all products. Every item comes with our quality guarantee and certification of authenticity.",
},
id: "1", title: "How do I get started?", content: "Getting started is easy. Sign up for an account, choose your plan, and start using our platform immediately."},
{
id: "2",
title: "What is your return and exchange policy?",
content:
"We offer hassle-free returns and exchanges within 30 days of purchase. Items must be unused and in original packaging. Simply contact our customer service team to initiate the process.",
},
id: "2", title: "What is your support policy?", content: "We offer 24/7 customer support via email, chat, and phone. Our average response time is under 2 hours."},
{
id: "3",
title: "How long does shipping typically take?",
content:
"Standard shipping takes 5-7 business days. Express shipping options (2-3 days) are available for most orders. Orders over $100 qualify for free standard shipping.",
},
{
id: "4",
title: "Do you offer international shipping?",
content:
"Yes, we ship to most countries worldwide. International shipping costs vary by location and are calculated at checkout. Customs duties may apply depending on your country.",
},
{
id: "5",
title: "Is my personal information secure?",
content:
"We use industry-standard SSL encryption to protect all personal and payment information. Your data is never shared with third parties. We comply with all privacy regulations.",
},
{
id: "6",
title: "What payment methods do you accept?",
content:
"We accept all major credit cards, debit cards, PayPal, Apple Pay, and Google Pay. All transactions are secure and encrypted.",
},
id: "3", title: "Can I cancel anytime?", content: "Yes, you can cancel your subscription at any time. No long-term contracts or hidden fees."},
]}
imageSrc="http://img.b2bpic.net/free-photo/woman-sitting-wheelchair-modern-concept_23-2148497283.jpg?_wi=1"
imageAlt="Customer service support team"
imageSrc="/placeholders/placeholder1.webp"
imageAlt="FAQ"
mediaAnimation="slide-up"
mediaPosition="right"
title="Frequently Asked Questions"
description="Find answers to common questions"
textboxLayout="default"
faqsAnimation="slide-up"
mediaPosition="left"
animationType="smooth"
useInvertedBackground={false}
/>
</div>
<div id="contact" data-section="contact">
<ContactCTA
tag="Get In Touch"
tagIcon={Mail}
tagAnimation="slide-up"
title="Ready to Discover Premium Products?"
description="Have questions about our products or services? Our expert team is here to help. Contact us today and experience the ZSMX Store difference."
tag="Get in Touch"
title="Ready to Get Started?"
description="Contact us today to learn how we can help you achieve your goals."
buttons={[
{ text: "Contact Our Team", href: "mailto:hello@zsmxstore.com" },
{ text: "Shop Now", href: "/fashion" },
{ text: "Contact Us", href: "#" },
{ text: "Schedule Demo", href: "#" },
]}
buttonAnimation="slide-up"
background={{ variant: "plain" }}
background={{ variant: "radial-gradient" }}
useInvertedBackground={false}
/>
</div>
<div id="footer" data-section="footer">
<FooterBase
logoText="ZSMX Store"
copyrightText="© 2025 ZSMX Store. All rights reserved."
columns={[
{
title: "Shop",
items: [
{ label: "Fashion", href: "/fashion" },
{ label: "Home", href: "#home-category" },
{ label: "Gym", href: "#gym" },
{ label: "Electronics", href: "#electronics" },
title: "Product", items: [
{ label: "Features", href: "categories" },
{ label: "Pricing", href: "#" },
{ label: "Security", href: "#" },
],
},
{
title: "Support",
items: [
{ label: "Contact Us", href: "#contact" },
{ label: "FAQ", href: "#faq" },
{ label: "Shipping Info", href: "#" },
{ label: "Returns", href: "#" },
],
},
{
title: "Company",
items: [
{ label: "About Us", href: "#about" },
title: "Company", items: [
{ label: "About", href: "about" },
{ label: "Blog", href: "#" },
{ label: "Careers", href: "#" },
{ label: "Privacy Policy", href: "#" },
],
},
{
title: "Legal", items: [
{ label: "Privacy", href: "#" },
{ label: "Terms", href: "#" },
{ label: "Contact", href: "contact" },
],
},
]}
logoText="Webild"
copyrightText="© 2025 | Webild"
/>
</div>
</ThemeProvider>
);
}
}

View File

@@ -1,123 +1,16 @@
"use client";
import React from 'react';
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";
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;
export interface CardListProps {
children?: React.ReactNode;
[key: string]: any;
}
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 });
export const CardList: React.FC<CardListProps> = ({ children, ...props }) => {
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 {...props}>
{children}
</div>
);
};
CardList.displayName = "CardList";
export default memo(CardList);
export default CardList;

View File

@@ -1,229 +1,20 @@
"use client";
import React 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 interface CardStackProps {
children?: React.ReactNode;
items?: any[];
[key: string]: any;
}
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;
// Check if the current grid config has gridRows defined
const gridConfig = gridConfigs[gridVariant]?.[itemCount];
const hasFixedGridRows = gridConfig && 'gridRows' in gridConfig && gridConfig.gridRows;
// 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>
);
export const CardStack: React.FC<CardStackProps> = ({ children, items, ...props }) => {
return (
<div {...props}>
{children}
{items && items.map((item: any, idx: number) => (
<div key={idx}>{JSON.stringify(item)}</div>
))}
</div>
);
};
CardStack.displayName = "CardStack";
export default memo(CardStack);
export default CardStack;

View File

@@ -1,92 +1,16 @@
"use client";
import React from 'react';
import { memo, useMemo } from "react";
import TextBox from "@/components/Textbox";
import { cls } from "@/lib/utils";
import type { TextBoxProps } from "./types";
export interface CardStackTextBoxProps {
children?: React.ReactNode;
[key: string]: any;
}
const CardStackTextBox = ({
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
textBoxClassName = "",
titleClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
}: TextBoxProps) => {
const styles = useMemo(() => {
if (textboxLayout === "default") {
return {
className: cls("flex flex-col gap-3 md:gap-2", textBoxClassName),
titleClassName: cls("text-6xl font-medium text-center", titleClassName),
descriptionClassName: cls("text-lg leading-tight text-center md:max-w-6/10", descriptionClassName),
tagClassName: cls("w-fit px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-0 mx-auto", tagClassName),
buttonContainerClassName: cls("flex flex-wrap gap-4 max-md:justify-center mt-1 md:mt-3 justify-center", buttonContainerClassName),
center: true,
};
}
if (textboxLayout === "inline-image") {
return {
className: cls("flex flex-col gap-3 md:gap-2", textBoxClassName),
titleClassName: cls("text-4xl md:text-5xl font-medium text-center", titleClassName),
descriptionClassName: cls("text-lg leading-tight text-center", descriptionClassName),
tagClassName: cls("w-fit px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-0 mx-auto", tagClassName),
buttonContainerClassName: cls("flex flex-wrap gap-4 max-md:justify-center mt-1 md:mt-3 justify-center", buttonContainerClassName),
center: true,
};
}
return {
className: textBoxClassName,
titleClassName: cls("text-6xl font-medium", titleClassName),
descriptionClassName: cls("text-lg leading-tight", descriptionClassName),
tagClassName: cls("px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2", tagClassName),
buttonContainerClassName: cls("flex flex-wrap gap-4 max-md:justify-center", buttonContainerClassName),
center: false,
};
}, [textboxLayout, textBoxClassName, titleClassName, descriptionClassName, tagClassName, buttonContainerClassName]);
if (!title && !titleSegments && !description) return null;
return (
<TextBox
title={title!}
titleSegments={titleSegments}
description={description!}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
className={styles.className}
titleClassName={styles.titleClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
descriptionClassName={styles.descriptionClassName}
tagClassName={styles.tagClassName}
buttonContainerClassName={styles.buttonContainerClassName}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
center={styles.center}
/>
);
export const CardStackTextBox: React.FC<CardStackTextBoxProps> = ({ children, ...props }) => {
return (
<div {...props}>
{children}
</div>
);
};
CardStackTextBox.displayName = "CardStackTextBox";
export default memo(CardStackTextBox);
export default CardStackTextBox;

View File

@@ -1,187 +1,48 @@
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 { useEffect, useRef } from 'react';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import type { CardAnimationConfig } from '../types';
gsap.registerPlugin(ScrollTrigger);
interface UseCardAnimationProps {
animationType: CardAnimationType | "depth-3d";
itemCount: number;
isGrid?: boolean;
supports3DAnimation?: boolean;
gridVariant?: GridVariant;
useIndividualTriggers?: boolean;
export function useCardAnimation(
cardsRef: React.RefObject<HTMLDivElement[]>,
config: CardAnimationConfig
) {
const animationsRef = useRef<gsap.core.Animation[]>([]);
useEffect(() => {
if (!cardsRef.current || cardsRef.current.length === 0) return;
animationsRef.current.forEach(anim => anim.kill());
animationsRef.current = [];
cardsRef.current.forEach((card, index) => {
if (!card) return;
const animation = gsap.fromTo(
card,
{ opacity: 0, y: 20 },
{
opacity: 1,
y: 0,
duration: config.duration || 0.6,
delay: (config.stagger || 0.1) * index,
scrollTrigger: {
trigger: card,
start: 'top 80%',
end: 'top 20%',
scrub: config.scrub || false,
markers: false,
},
}
);
animationsRef.current.push(animation);
});
return () => {
animationsRef.current.forEach(anim => anim.kill());
};
}, [cardsRef, config]);
}
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,23 @@
import { useEffect, useState, useRef, RefObject } from "react";
import { useEffect, useState } 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;
interface Depth3DConfig {
rotateX?: number;
rotateY?: number;
scale?: number;
perspective?: number;
}
export const useDepth3DAnimation = ({
itemRefs,
containerRef,
perspectiveRef,
isEnabled,
}: UseDepth3DAnimationProps) => {
const [isMobile, setIsMobile] = useState(false);
const useDepth3DAnimation = (config: Depth3DConfig = {}) => {
const [transform, setTransform] = useState<string>('');
const { rotateX = 0, rotateY = 0, scale = 1, perspective = 1000 } = config;
// Detect mobile viewport
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
const transformValue = `perspective(${perspective}px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) scale(${scale})`;
setTransform(transformValue);
}, [rotateX, rotateY, scale, perspective]);
checkMobile();
window.addEventListener("resize", checkMobile);
return () => {
window.removeEventListener("resize", checkMobile);
};
}, []);
// 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 { transform };
};
export { useDepth3DAnimation };

View File

@@ -1,144 +1,16 @@
"use client";
import React from 'react';
import { memo, Children, useCallback, useEffect, useState } from "react";
import useEmblaCarousel from "embla-carousel-react";
import { EmblaCarouselType } from "embla-carousel";
import CardStackTextBox from "../../CardStackTextBox";
import { cls } from "@/lib/utils";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { ArrowCarouselProps } from "../../types";
export interface ArrowCarouselProps {
children?: React.ReactNode;
[key: string]: any;
}
const ArrowCarousel = ({
children,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout = "default",
useInvertedBackground,
className = "",
containerClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
titleClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
ariaLabel = "Carousel section",
}: ArrowCarouselProps) => {
const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true, align: "center" });
const [selectedIndex, setSelectedIndex] = useState(0);
const childrenArray = Children.toArray(children);
const onSelect = useCallback((emblaApi: EmblaCarouselType) => {
setSelectedIndex(emblaApi.selectedScrollSnap());
}, []);
const scrollPrev = useCallback(() => emblaApi?.scrollPrev(), [emblaApi]);
const scrollNext = useCallback(() => emblaApi?.scrollNext(), [emblaApi]);
useEffect(() => {
if (!emblaApi) return;
onSelect(emblaApi);
emblaApi.on("select", onSelect).on("reInit", onSelect);
return () => {
emblaApi.off("select", onSelect).off("reInit", onSelect);
};
}, [emblaApi, onSelect]);
return (
<section
className={cls(
"relative py-20 w-full",
useInvertedBackground && "bg-foreground",
className
)}
aria-label={ariaLabel}
>
<div className={cls("w-full mx-auto flex flex-col gap-6", containerClassName)}>
<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="relative w-full">
<div
className={cls(
"overflow-hidden w-full relative z-10 mask-fade-x",
carouselClassName
)}
ref={emblaRef}
>
<div className="flex w-full">
{childrenArray.map((child, index) => (
<div
key={index}
className="flex-none w-60 md:w-40 mr-6"
>
<div className={cls(
"transition-all duration-500 ease-out",
selectedIndex === index ? "opacity-100 scale-100" : "opacity-70 scale-90"
)}>
{child}
</div>
</div>
))}
</div>
</div>
<div className={cls("absolute inset-y-0 w-content-width mx-auto left-0 right-0 flex items-center justify-between pointer-events-none z-10", controlsClassName)}>
<button
onClick={scrollPrev}
className="pointer-events-auto primary-button h-8 w-auto aspect-square rounded-theme flex items-center justify-center cursor-pointer"
aria-label="Previous slide"
>
<ChevronLeft className="w-4/10 h-4/10 text-primary-cta-text" />
</button>
<button
onClick={scrollNext}
className="pointer-events-auto primary-button h-8 w-auto aspect-square rounded-theme flex items-center justify-center cursor-pointer"
aria-label="Next slide"
>
<ChevronRight className="w-4/10 h-4/10 text-primary-cta-text" />
</button>
</div>
</div>
</div>
</section>
);
export const ArrowCarousel: React.FC<ArrowCarouselProps> = ({ children, ...props }) => {
return (
<div {...props}>
{children}
</div>
);
};
ArrowCarousel.displayName = "ArrowCarousel";
export default memo(ArrowCarousel);
export default ArrowCarousel;

View File

@@ -1,148 +1,16 @@
"use client";
import React from 'react';
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 interface AutoCarouselProps {
children?: React.ReactNode;
[key: string]: any;
}
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
});
// 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>
);
export const AutoCarousel: React.FC<AutoCarouselProps> = ({ children, ...props }) => {
return (
<div {...props}>
{children}
</div>
);
};
AutoCarousel.displayName = "AutoCarousel";
export default memo(AutoCarousel);
export default AutoCarousel;

View File

@@ -1,182 +1,39 @@
"use client";
import React, { useRef, useCallback } from 'react';
import { useCardAnimation } from '../../hooks/useCardAnimation';
import type { CardAnimationConfig } from '../../types';
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";
interface ButtonCarouselProps {
items: React.ReactNode[];
animationConfig: CardAnimationConfig;
className?: string;
}
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 });
export const ButtonCarousel: React.FC<ButtonCarouselProps> = ({
items,
animationConfig,
className = '',
}) => {
const cardsRef = useRef<HTMLDivElement[]>([]);
const {
prevBtnDisabled,
nextBtnDisabled,
onPrevButtonClick,
onNextButtonClick,
} = usePrevNextButtons(emblaApi);
useCardAnimation(cardsRef, animationConfig);
const scrollProgress = useScrollProgress(emblaApi);
const setCardRef = useCallback((index: number, el: HTMLDivElement | null) => {
if (el) {
cardsRef.current[index] = el;
}
}, []);
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}
return (
<div className={`button-carousel ${className}`}>
{items.map((item, index) => (
<div
key={index}
ref={el => setCardRef(index, el)}
className="carousel-item"
>
<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>
);
{item}
</div>
))}
</div>
);
};
ButtonCarousel.displayName = "ButtonCarousel";
export default memo(ButtonCarousel);

View File

@@ -1,155 +1,16 @@
"use client";
import React from 'react';
import { memo, Children, cloneElement, isValidElement, useCallback, useEffect, useState } from "react";
import useEmblaCarousel from "embla-carousel-react";
import { EmblaCarouselType } from "embla-carousel";
import CardStackTextBox from "../../CardStackTextBox";
import { cls } from "@/lib/utils";
import { FullWidthCarouselProps } from "../../types";
export interface FullWidthCarouselProps {
children?: React.ReactNode;
[key: string]: any;
}
const FullWidthCarousel = ({
children,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout = "default",
useInvertedBackground,
className = "",
containerClassName = "",
carouselClassName = "",
dotsClassName = "",
textBoxClassName = "",
titleClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
ariaLabel = "Carousel section",
}: FullWidthCarouselProps) => {
const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true, align: "center" });
const [selectedIndex, setSelectedIndex] = useState(0);
const childrenArray = Children.toArray(children);
const onSelect = useCallback((emblaApi: EmblaCarouselType) => {
setSelectedIndex(emblaApi.selectedScrollSnap());
}, []);
const scrollTo = useCallback(
(index: number) => {
if (!emblaApi) return;
emblaApi.scrollTo(index);
},
[emblaApi]
);
useEffect(() => {
if (!emblaApi) return;
onSelect(emblaApi);
emblaApi.on("select", onSelect).on("reInit", onSelect);
return () => {
emblaApi.off("select", onSelect).off("reInit", onSelect);
};
}, [emblaApi, onSelect]);
useEffect(() => {
if (!emblaApi) return;
const autoplay = setInterval(() => {
emblaApi.scrollNext();
}, 5000);
return () => clearInterval(autoplay);
}, [emblaApi]);
return (
<section
className={cls(
"relative py-20 w-full",
useInvertedBackground && "bg-foreground",
className
)}
aria-label={ariaLabel}
>
<div className={cls("w-full mx-auto flex flex-col gap-6", containerClassName)}>
<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="w-full">
<div
className={cls(
"overflow-hidden w-full relative z-10",
carouselClassName
)}
ref={emblaRef}
>
<div className="flex w-full">
{Children.map(childrenArray, (child, index) => (
<div
key={index}
className="flex-none w-70 mr-6"
>
{isValidElement(child)
? cloneElement(child, { isActive: selectedIndex === index } as Record<string, unknown>)
: child}
</div>
))}
</div>
</div>
</div>
<div className={cls("flex items-center justify-center gap-2", dotsClassName)}>
{childrenArray.map((_, index) => (
<button
key={index}
type="button"
onClick={() => scrollTo(index)}
className={cls(
"relative cursor-pointer h-2 rounded-theme bg-accent transition-all duration-300",
selectedIndex === index
? "w-8 opacity-100"
: "w-2 opacity-20"
)}
aria-label={`Go to slide ${index + 1}`}
aria-current={selectedIndex === index}
/>
))}
</div>
</div>
</section>
);
export const FullWidthCarousel: React.FC<FullWidthCarouselProps> = ({ children, ...props }) => {
return (
<div {...props}>
{children}
</div>
);
};
FullWidthCarousel.displayName = "FullWidthCarousel";
export default memo(FullWidthCarousel);
export default FullWidthCarousel;

View File

@@ -1,150 +1,39 @@
"use client";
import React, { useRef, useCallback } from 'react';
import { useCardAnimation } from '../../hooks/useCardAnimation';
import type { CardAnimationConfig } from '../../types';
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";
interface GridLayoutProps {
items: React.ReactNode[];
animationConfig: CardAnimationConfig;
className?: string;
}
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];
export const GridLayout: React.FC<GridLayoutProps> = ({
items,
animationConfig,
className = '',
}) => {
const cardsRef = useRef<HTMLDivElement[]>([]);
// 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";
useCardAnimation(cardsRef, animationConfig);
// 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 setCardRef = useCallback((index: number, el: HTMLDivElement | null) => {
if (el) {
cardsRef.current[index] = el;
}
}, []);
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}
return (
<div className={`grid-layout ${className}`}>
{items.map((item, index) => (
<div
key={index}
ref={el => setCardRef(index, el)}
className="grid-item"
>
<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>
);
{item}
</div>
))}
</div>
);
};
GridLayout.displayName = "GridLayout";
export default memo(GridLayout);

View File

@@ -1,149 +1,50 @@
"use client";
'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";
import React from 'react';
import { ChevronDown } from 'lucide-react';
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;
interface TimelineItem {
id: string;
title: string;
description: string;
icon?: React.ReactNode;
}
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);
}, []);
interface TimelineBaseProps {
items: TimelineItem[];
className?: string;
itemClassName?: string;
connectorClassName?: string;
contentClassName?: string;
}
const TimelineBase: React.FC<TimelineBaseProps> = ({
items,
className = '',
itemClassName = '',
connectorClassName = '',
contentClassName = '',
}) => {
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 className={`space-y-8 ${className}`}>
{items.map((item, index) => (
<div key={item.id} className={`flex gap-4 ${itemClassName}`}>
<div className="flex flex-col items-center">
<div className="w-10 h-10 rounded-full bg-primary-cta flex items-center justify-center text-white text-sm font-semibold">
{item.icon || index + 1}
</div>
))}
{index < items.length - 1 && (
<div className={`w-1 h-12 bg-gray-200 my-2 ${connectorClassName}`} />
)}
</div>
<div className={`pt-1 ${contentClassName}`}>
<h3 className="text-lg font-semibold">{item.title}</h3>
<p className="text-gray-600 mt-1">{item.description}</p>
</div>
</div>
</div>
</section>
))}
</div>
);
};
TimelineBase.displayName = "TimelineBase";
export default React.memo(TimelineBase);
export default TimelineBase;

View File

@@ -1,147 +1,16 @@
"use client";
import React from 'react';
import React, { useEffect, useRef, memo, Children } from "react";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import CardStackTextBox from "../../CardStackTextBox";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, ButtonAnimationType, TitleSegment } from "../../types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
gsap.registerPlugin(ScrollTrigger);
interface TimelineCardStackProps {
children: React.ReactNode;
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 interface TimelineCardStackProps {
children?: React.ReactNode;
[key: string]: any;
}
const TimelineCardStack = ({
children,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
className = "",
containerClassName = "",
textBoxClassName = "",
titleClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
ariaLabel = "Timeline section",
}: TimelineCardStackProps) => {
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
const childrenArray = Children.toArray(children);
useEffect(() => {
const ctx = gsap.context(() => {
itemRefs.current.forEach((ref, position) => {
if (!ref) return;
const isLast = position === itemRefs.current.length - 1;
const timeline = gsap.timeline({
scrollTrigger: {
trigger: ref,
start: "center center",
end: "+=100%",
scrub: true,
},
});
timeline.set(ref, { willChange: "opacity" }).to(ref, {
ease: "none",
opacity: isLast ? 1 : 0,
});
});
});
return () => {
ctx.revert();
};
}, [childrenArray.length]);
return (
<section
className={cls(
"relative overflow-visible h-fit 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)}>
<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="w-full flex flex-col gap-[var(--width-25)] md:gap-[6.25vh]">
{Children.map(childrenArray, (child, index) => (
<div
key={index}
ref={(el) => {
itemRefs.current[index] = el;
}}
className="!sticky w-full card backdrop-blur-xs rounded-theme-capped h-[140vw] md:h-[75vh] top-[25vw] md:top-[12.5vh]"
>
{child}
</div>
))}
</div>
</div>
</section>
);
export const TimelineCardStack: React.FC<TimelineCardStackProps> = ({ children, ...props }) => {
return (
<div {...props}>
{children}
</div>
);
};
TimelineCardStack.displayName = "TimelineCardStack";
export default memo(TimelineCardStack);
export default TimelineCardStack;

View File

@@ -1,175 +1,16 @@
"use client";
import React from 'react';
import React, { Children, useCallback } from "react";
import { cls } from "@/lib/utils";
import CardStackTextBox from "../../CardStackTextBox";
import { useTimelineHorizontal, type MediaItem } from "../../hooks/useTimelineHorizontal";
import MediaContent from "@/components/shared/MediaContent";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, ButtonAnimationType, TitleSegment, TextboxLayout, InvertedBackground } from "../../types";
interface TimelineHorizontalCardStackProps {
children: React.ReactNode;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground?: InvertedBackground;
mediaItems?: MediaItem[];
className?: string;
containerClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
cardClassName?: string;
progressBarClassName?: string;
mediaContainerClassName?: string;
mediaClassName?: string;
ariaLabel?: string;
export interface TimelineHorizontalCardStackProps {
children?: React.ReactNode;
[key: string]: any;
}
const TimelineHorizontalCardStack = ({
children,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
mediaItems,
className = "",
containerClassName = "",
textBoxClassName = "",
titleClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
cardClassName = "",
progressBarClassName = "",
mediaContainerClassName = "",
mediaClassName = "",
ariaLabel = "Timeline section",
}: TimelineHorizontalCardStackProps) => {
const childrenArray = Children.toArray(children);
const itemCount = childrenArray.length;
const { activeIndex, progressRefs, handleItemClick, imageOpacity, currentMediaSrc } = useTimelineHorizontal({
itemCount,
mediaItems,
});
const getGridColumns = useCallback(() => {
if (itemCount === 2) return "md:grid-cols-2";
if (itemCount === 3) return "md:grid-cols-3";
return "md:grid-cols-4";
}, [itemCount]);
const getItemOpacity = useCallback(
(index: number) => {
return index <= activeIndex ? "opacity-100" : "opacity-50";
},
[activeIndex]
);
export const TimelineHorizontalCardStack: React.FC<TimelineHorizontalCardStackProps> = ({ children, ...props }) => {
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)}>
<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}
/>
{mediaItems && mediaItems.length > 0 && (
<div className={cls("relative card rounded-theme-capped overflow-hidden aspect-square md:aspect-[17/9]", mediaContainerClassName)}>
<div
className="absolute inset-6 z-1 transition-opacity duration-300 overflow-hidden"
style={{ opacity: imageOpacity }}
>
<MediaContent
imageSrc={currentMediaSrc.imageSrc}
videoSrc={currentMediaSrc.videoSrc}
imageAlt={mediaItems[activeIndex]?.imageAlt}
videoAriaLabel={mediaItems[activeIndex]?.videoAriaLabel}
imageClassName={cls("w-full h-full object-cover", mediaClassName)}
/>
</div>
</div>
)}
<div className={cls("relative grid grid-cols-1 gap-6 md:gap-6", getGridColumns())}>
{Children.map(childrenArray, (child, index) => (
<div
key={index}
className={cls(
"card rounded-theme-capped p-6 flex flex-col justify-between gap-6 transition-all duration-300",
index === activeIndex ? "cursor-default" : "cursor-pointer hover:shadow-lg",
getItemOpacity(index),
cardClassName
)}
onClick={() => handleItemClick(index)}
>
{child}
<div className="relative w-full h-px overflow-hidden">
<div className="absolute z-0 w-full h-full bg-foreground/20" />
<div
ref={(el) => {
if (el !== null) {
progressRefs.current[index] = el;
}
}}
className={cls("absolute z-10 h-full w-full bg-foreground origin-left", progressBarClassName)}
style={{ transform: "scaleX(0)" }}
/>
</div>
</div>
))}
</div>
</div>
</section>
<div {...props}>
{children}
</div>
);
};
TimelineHorizontalCardStack.displayName = "TimelineHorizontalCardStack";
export default React.memo(TimelineHorizontalCardStack);
export default TimelineHorizontalCardStack;

View File

@@ -1,275 +1,16 @@
"use client";
import React from 'react';
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";
interface PhoneFrameProps {
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
phoneRef: (el: HTMLDivElement | null) => void;
className?: string;
export interface TimelinePhoneViewProps {
children?: React.ReactNode;
[key: string]: any;
}
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` };
export const TimelinePhoneView: React.FC<TimelinePhoneViewProps> = ({ children, ...props }) => {
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 {...props}>
{children}
</div>
);
};
TimelinePhoneView.displayName = "TimelinePhoneView";
export default memo(TimelinePhoneView);
export default TimelinePhoneView;

View File

@@ -1,202 +1,16 @@
"use client";
import React from 'react';
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";
gsap.registerPlugin(ScrollTrigger);
interface TimelineProcessFlowItem {
id: string;
content: React.ReactNode;
media: React.ReactNode;
reverse: boolean;
export interface TimelineProcessFlowProps {
children?: React.ReactNode;
[key: string]: any;
}
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());
};
}, []);
export const TimelineProcessFlow: React.FC<TimelineProcessFlowProps> = ({ children, ...props }) => {
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-[calc(50%-var(--width-5))]", 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-[calc(50%-var(--width-5))]", contentWrapperClassName)}>
{item.content}
</div>
</li>
))}
</ol>
</div>
</div>
</section>
<div {...props}>
{children}
</div>
);
};
TimelineProcessFlow.displayName = "TimelineProcessFlow";
export default memo(TimelineProcessFlow);
export default TimelineProcessFlow;

View File

@@ -1,149 +1,26 @@
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, ButtonAnimationType } from "@/types/button";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
export type { ButtonConfig, ButtonAnimationType, TextboxLayout, InvertedBackground };
export type TitleSegment =
| { type: "text"; content: string }
| { type: "image"; src: string; alt?: string };
export interface TimelineCardStackItem {
id: number;
title: string;
description: string;
image: string;
imageAlt?: string;
export interface CardAnimationConfig {
duration?: number;
stagger?: number;
scrub?: boolean | number;
delay?: number;
}
export type GridVariant =
| "uniform-all-items-equal"
| "bento-grid"
| "bento-grid-inverted"
| "two-columns-alternating-heights"
| "asymmetric-60-wide-40-narrow"
| "three-columns-all-equal-width"
| "four-items-2x2-equal-grid"
| "one-large-right-three-stacked-left"
| "items-top-row-full-width-bottom"
| "full-width-top-items-bottom-row"
| "one-large-left-three-stacked-right"
| "two-items-per-row"
| "timeline";
export type CardAnimationType = 'none' | 'opacity' | 'slide-up' | 'scale-rotate' | 'blur-reveal' | 'depth-3d';
export type CardAnimationTypeWith3D = CardAnimationType | 'depth-3d';
export type BentoAnimationType = CardAnimationType;
export type GridVariant = 'uniform-all-items-equal' | 'bento-grid' | 'bento-grid-inverted' | 'two-columns-alternating-heights' | 'asymmetric-60-wide-40-narrow' | 'three-columns-all-equal-width' | 'four-items-2x2-equal-grid' | 'one-large-right-three-stacked-left' | 'items-top-row-full-width-bottom' | 'full-width-top-items-bottom-row' | 'one-large-left-three-stacked-right';
export type CardAnimationType =
| "none"
| "opacity"
| "slide-up"
| "scale-rotate"
| "blur-reveal";
export type TextBoxProps = any;
export type ArrowCarouselProps = any;
export type FullWidthCarouselProps = any;
export type ButtonConfig = any;
export type ButtonAnimationType = any;
export type TitleSegment = any;
export type TextboxLayout = any;
export type InvertedBackground = any;
export type Metric = { id: string; value: string; title: string; items?: string[] };
export type CardAnimationTypeWith3D = CardAnimationType | "depth-3d";
export interface TextBoxProps {
title?: string;
titleSegments?: TitleSegment[];
description?: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground?: InvertedBackground;
textBoxClassName?: string;
titleClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
}
export interface CardStackProps extends TextBoxProps {
children: React.ReactNode;
mode?: "auto" | "buttons";
gridVariant?: GridVariant;
uniformGridCustomHeightClasses?: string;
gridRowsClassName?: string;
itemHeightClassesOverride?: string[];
animationType: CardAnimationType | CardAnimationTypeWith3D;
supports3DAnimation?: boolean;
carouselThreshold?: number;
bottomContent?: React.ReactNode;
className?: string;
containerClassName?: string;
gridClassName?: string;
carouselClassName?: string;
carouselItemClassName?: string;
controlsClassName?: string;
ariaLabel?: string;
}
export interface GridLayoutProps extends TextBoxProps {
children: React.ReactNode;
itemCount: number;
gridVariant?: GridVariant;
uniformGridCustomHeightClasses?: string;
gridRowsClassName?: string;
itemHeightClassesOverride?: string[];
animationType: CardAnimationType | CardAnimationTypeWith3D;
supports3DAnimation?: boolean;
bottomContent?: React.ReactNode;
className?: string;
containerClassName?: string;
gridClassName?: string;
ariaLabel: string;
}
export interface AutoCarouselProps extends TextBoxProps {
children: React.ReactNode;
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationType;
speed?: number;
bottomContent?: React.ReactNode;
className?: string;
containerClassName?: string;
carouselClassName?: string;
itemClassName?: string;
ariaLabel: string;
showTextBox?: boolean;
dualMarquee?: boolean;
topMarqueeDirection?: "left" | "right";
bottomMarqueeDirection?: "left" | "right";
bottomCarouselClassName?: string;
marqueeGapClassName?: string;
}
export interface ButtonCarouselProps extends TextBoxProps {
children: React.ReactNode;
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationType;
bottomContent?: React.ReactNode;
className?: string;
containerClassName?: string;
carouselClassName?: string;
carouselItemClassName?: string;
controlsClassName?: string;
ariaLabel: string;
}
export interface FullWidthCarouselProps extends TextBoxProps {
children: React.ReactNode;
className?: string;
containerClassName?: string;
carouselClassName?: string;
dotsClassName?: string;
ariaLabel: string;
}
export interface ArrowCarouselProps extends TextBoxProps {
children: React.ReactNode;
className?: string;
containerClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
ariaLabel: string;
}
export interface MetricCardOneGridVariant extends GridVariant {}
export interface MetricCardTwoGridVariant extends GridVariant {}
export interface TeamCardOneGridVariant extends GridVariant {}
export interface TeamCardSixGridVariant extends GridVariant {}

View File

@@ -1,156 +1,62 @@
"use client";
'use client';
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";
import React, { useState } from 'react';
import { useProducts } from '@/hooks/useProducts';
import ProductCatalogItem from './ProductCatalogItem';
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;
className?: string;
gridClassName?: string;
itemClassName?: string;
ariaLabel?: string;
}
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 ProductCatalog: React.FC<ProductCatalogProps> = ({
className = '',
gridClassName = 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6',
itemClassName = '',
ariaLabel = 'Product catalog',
}) => {
const { products, loading, error } = useProducts();
const [favorites, setFavorites] = useState<Set<string>>(new Set());
const handleProductClick = useCallback((productId: string) => {
router.push(`/shop/${productId}`);
}, [router]);
const handleFavorite = (productId: string) => {
setFavorites((prev) => {
const updated = new Set(prev);
if (updated.has(productId)) {
updated.delete(productId);
} else {
updated.add(productId);
}
return updated;
});
};
const products: CatalogProduct[] = useMemo(() => {
if (productsProp && productsProp.length > 0) {
return productsProp;
}
if (loading) return <div className={className}>Loading products...</div>;
if (error) return <div className={className}>Error: {error}</div>;
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>
);
return (
<div className={className} aria-label={ariaLabel}>
<div className={gridClassName}>
{products.map((product) => (
<ProductCatalogItem
key={product.id}
product={{
id: product.id,
category: 'General',
name: product.name,
price: `$${product.price.toFixed(2)}`,
rating: product.rating,
imageSrc: product.imageSrc,
onFavorite: () => handleFavorite(product.id),
isFavorited: favorites.has(product.id),
}}
className={itemClassName}
/>
))}
</div>
</div>
);
};
ProductCatalog.displayName = "ProductCatalog";
export default memo(ProductCatalog);
export default ProductCatalog;

View File

@@ -1,244 +1,56 @@
"use client";
import React from 'react';
import CardStack from '@/components/cardStack/CardStack';
import type { CardStackProps } 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";
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;
interface BlogPost {
id: string;
category: string;
title: string;
excerpt: string;
imageSrc: string;
imageAlt?: string;
authorName?: string;
authorAvatar?: string;
date?: string;
}
interface BlogCardItemProps {
blog: BlogCard;
shouldUseLightText: boolean;
cardClassName?: string;
imageWrapperClassName?: string;
imageClassName?: string;
categoryClassName?: string;
cardTitleClassName?: string;
excerptClassName?: string;
authorContainerClassName?: string;
authorAvatarClassName?: string;
authorNameClassName?: string;
dateClassName?: string;
interface BlogCardOneProps extends Omit<CardStackProps, 'children'> {
blogs: BlogPost[];
}
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}`} />
export const BlogCardOne: React.FC<BlogCardOneProps> = ({
blogs,
...cardStackProps
}) => {
const blogElements = blogs.map(blog => (
<div key={blog.id} className="blog-card">
<div className="blog-image">
<img src={blog.imageSrc} alt={blog.imageAlt || blog.title} />
</div>
<div className="blog-content">
<span className="category">{blog.category}</span>
<h3>{blog.title}</h3>
<p>{blog.excerpt}</p>
{blog.authorName && (
<div className="author-info">
{blog.authorAvatar && (
<img src={blog.authorAvatar} alt={blog.authorName} className="avatar" />
)}
<div>
<p className="author-name">{blog.authorName}</p>
{blog.date && <p className="date">{blog.date}</p>}
</div>
</div>
)}
</div>
</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>
);
return (
<CardStack {...cardStackProps}>
{blogElements}
</CardStack>
);
};
BlogCardOne.displayName = "BlogCardOne";
export default BlogCardOne;

View File

@@ -1,288 +1,56 @@
"use client";
import React from 'react';
import CardStack from '@/components/cardStack/CardStack';
import type { CardStackProps } 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";
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;
interface BlogPost {
id: string;
category: string;
title: string;
excerpt: string;
imageSrc: string;
imageAlt?: string;
authorName?: string;
authorAvatar?: string;
date?: string;
}
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;
interface BlogCardThreeProps extends Omit<CardStackProps, 'children'> {
blogs: BlogPost[];
}
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
export const BlogCardThree: React.FC<BlogCardThreeProps> = ({
blogs,
...cardStackProps
}) => {
const blogElements = blogs.map(blog => (
<div key={blog.id} className="blog-card">
<div className="blog-image">
<img src={blog.imageSrc} alt={blog.imageAlt || blog.title} />
</div>
<div className="blog-content">
<span className="category">{blog.category}</span>
<h3>{blog.title}</h3>
<p>{blog.excerpt}</p>
{blog.authorName && (
<div className="author-info">
{blog.authorAvatar && (
<img src={blog.authorAvatar} alt={blog.authorName} className="avatar" />
)}
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>
<p className="author-name">{blog.authorName}</p>
{blog.date && <p className="date">{blog.date}</p>}
</div>
</div>
)}
</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>
);
return (
<CardStack {...cardStackProps}>
{blogElements}
</CardStack>
);
};
BlogCardThree.displayName = "BlogCardThree";
export default BlogCardThree;

View File

@@ -1,241 +1,56 @@
"use client";
import React from 'react';
import CardStack from '@/components/cardStack/CardStack';
import type { CardStackProps } 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";
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;
interface BlogPost {
id: string;
category: string;
title: string;
excerpt: string;
imageSrc: string;
imageAlt?: string;
authorName?: string;
authorAvatar?: string;
date?: string;
}
interface BlogCardItemProps {
blog: BlogCard;
shouldUseLightText: boolean;
cardClassName?: string;
imageWrapperClassName?: string;
imageClassName?: string;
authorAvatarClassName?: string;
authorDateClassName?: string;
cardTitleClassName?: string;
excerptClassName?: string;
categoryClassName?: string;
interface BlogCardTwoProps extends Omit<CardStackProps, 'children'> {
blogs: BlogPost[];
}
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}`} />
export const BlogCardTwo: React.FC<BlogCardTwoProps> = ({
blogs,
...cardStackProps
}) => {
const blogElements = blogs.map(blog => (
<div key={blog.id} className="blog-card">
<div className="blog-image">
<img src={blog.imageSrc} alt={blog.imageAlt || blog.title} />
</div>
<div className="blog-content">
<span className="category">{blog.category}</span>
<h3>{blog.title}</h3>
<p>{blog.excerpt}</p>
{blog.authorName && (
<div className="author-info">
{blog.authorAvatar && (
<img src={blog.authorAvatar} alt={blog.authorName} className="avatar" />
)}
<div>
<p className="author-name">{blog.authorName}</p>
{blog.date && <p className="date">{blog.date}</p>}
</div>
</div>
)}
</div>
</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>
);
return (
<CardStack {...cardStackProps}>
{blogElements}
</CardStack>
);
};
BlogCardTwo.displayName = "BlogCardTwo";
export default BlogCardTwo;

View File

@@ -1,131 +1,81 @@
"use client";
'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" }
>;
import React, { useState } from 'react';
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;
title: string;
description: string;
email?: string;
phone?: string;
className?: string;
}
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 ContactCenter: React.FC<ContactCenterProps> = ({
title,
description,
email,
phone,
className = '',
}) => {
const [formData, setFormData] = useState<Record<string, string>>({
name: '',
email: '',
message: '',
});
const handleSubmit = async (email: string) => {
try {
await sendContactEmail({ email });
console.log("Email send successfully");
} catch (error) {
console.error("Failed to send email:", error);
}
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value,
}));
};
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>
);
const handleSubmitClick = () => {
// Form submission logic would go here
console.log('Form data:', formData);
};
return (
<div className={`max-w-2xl mx-auto text-center ${className}`}>
<h2 className="text-4xl font-bold mb-4">{title}</h2>
<p className="text-gray-600 mb-8">{description}</p>
<form className="space-y-4">
<input
type="text"
name="name"
placeholder="Your Name"
value={formData.name}
onChange={handleChange}
className="w-full px-4 py-2 border rounded"
/>
<input
type="email"
name="email"
placeholder="Your Email"
value={formData.email}
onChange={handleChange}
className="w-full px-4 py-2 border rounded"
/>
<textarea
name="message"
placeholder="Your Message"
value={formData.message}
onChange={handleChange}
className="w-full px-4 py-2 border rounded min-h-32"
/>
<button
type="button"
onClick={handleSubmitClick}
className="w-full px-4 py-2 bg-primary-cta text-white rounded"
>
Send Message
</button>
</form>
{email && <p className="mt-6 text-gray-600">Email: {email}</p>}
{phone && <p className="text-gray-600">Phone: {phone}</p>}
</div>
);
};
ContactCenter.displayName = "ContactCenter";
export default ContactCenter;
export default ContactCenter;

View File

@@ -1,188 +1,48 @@
"use client";
import React, { useRef, useCallback } from 'react';
import { useCardAnimation } from '@/components/cardStack/hooks/useCardAnimation';
import type { CardAnimationConfig } from '@/components/cardStack/types';
import { useState, Fragment } from "react";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import Accordion from "@/components/Accordion";
import Button from "@/components/button/Button";
import { useCardAnimation } from "@/components/cardStack/hooks/useCardAnimation";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
import type { CardAnimationType } from "@/components/cardStack/types";
import type { ButtonConfig } from "@/types/button";
interface FaqItem {
interface FAQ {
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;
faqs: FAQ[];
animationConfig: CardAnimationConfig;
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 = ({
export const ContactFaq: React.FC<ContactFaqProps> = ({
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 });
animationConfig,
className = '',
}) => {
const cardsRef = useRef<HTMLDivElement[]>([]);
const handleToggle = (index: number) => {
setActiveIndex(activeIndex === index ? null : index);
};
useCardAnimation(cardsRef, animationConfig);
const getButtonConfigProps = () => {
if (theme.defaultButtonVariant === "hover-bubble") {
return { bgClassName: "w-full" };
const setCardRef = useCallback((index: number, el: HTMLDivElement | null) => {
if (el) {
cardsRef.current[index] = el;
}
if (theme.defaultButtonVariant === "icon-arrow") {
return { className: "justify-between" };
}
return {};
};
}, []);
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 className={`contact-faq ${className}`}>
{faqs.map((faq, index) => (
<div
key={faq.id}
ref={el => setCardRef(index, el)}
className="faq-item"
>
<h3>{faq.title}</h3>
<p>{faq.content}</p>
</div>
</div>
</section>
))}
</div>
);
};
ContactFaq.displayName = "ContactFaq";
export default ContactFaq;

View File

@@ -1,171 +1,84 @@
"use client";
'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" }
>;
import React, { useState } from 'react';
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;
title: string;
description: string;
imageSrc?: string;
className?: string;
}
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 ContactSplit: React.FC<ContactSplitProps> = ({
title,
description,
imageSrc,
className = '',
}) => {
const [formData, setFormData] = useState<Record<string, string>>({
name: '',
email: '',
message: '',
});
const handleSubmit = async (email: string) => {
try {
await sendContactEmail({ email });
console.log("Email send successfully");
} catch (error) {
console.error("Failed to send email:", error);
}
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value,
}));
};
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>
const handleSubmitClick = () => {
// Form submission logic would go here
console.log('Form data:', formData);
};
return (
<div className={`grid grid-cols-2 gap-8 ${className}`}>
<div>
<h2 className="text-4xl font-bold mb-4">{title}</h2>
<p className="text-gray-600 mb-8">{description}</p>
<form className="space-y-4">
<input
type="text"
name="name"
placeholder="Your Name"
value={formData.name}
onChange={handleChange}
className="w-full px-4 py-2 border rounded"
/>
<input
type="email"
name="email"
placeholder="Your Email"
value={formData.email}
onChange={handleChange}
className="w-full px-4 py-2 border rounded"
/>
<textarea
name="message"
placeholder="Your Message"
value={formData.message}
onChange={handleChange}
className="w-full px-4 py-2 border rounded min-h-32"
/>
<button
type="button"
onClick={handleSubmitClick}
className="w-full px-4 py-2 bg-primary-cta text-white rounded"
>
Send Message
</button>
</form>
</div>
{imageSrc && (
<div>
<img src={imageSrc} alt="Contact" className="w-full h-full object-cover rounded" />
</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>
);
)}
</div>
);
};
ContactSplit.displayName = "ContactSplit";
export default ContactSplit;
export default ContactSplit;

View File

@@ -1,214 +1,80 @@
"use client";
'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";
import React, { useState } from 'react';
export interface InputField {
name: string;
type: string;
placeholder: string;
required?: boolean;
className?: string;
}
export interface TextareaField {
name: string;
placeholder: string;
rows?: number;
required?: boolean;
className?: string;
interface FormInput {
name: string;
type: string;
placeholder: string;
required?: boolean;
}
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;
title: string;
description: string;
inputs: FormInput[];
imageSrc?: string;
className?: 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 });
const ContactSplitForm: React.FC<ContactSplitFormProps> = ({
title,
description,
inputs,
imageSrc,
className = '',
}) => {
const [formData, setFormData] = useState<Record<string, string>>({
...inputs.reduce((acc, input) => ({ ...acc, [input.name]: '' }), {}),
});
// Validate minimum inputs requirement
if (inputs.length < 2) {
throw new Error("ContactSplitForm requires at least 2 inputs");
}
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value,
}));
};
// Initialize form data dynamically
const initialFormData: Record<string, string> = {};
inputs.forEach(input => {
initialFormData[input.name] = "";
});
if (textarea) {
initialFormData[textarea.name] = "";
}
const handleSubmitClick = () => {
// Form submission logic would go here
console.log('Form data:', formData);
};
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)}
return (
<div className={`grid grid-cols-2 gap-8 ${className}`}>
<div>
<h2 className="text-4xl font-bold mb-4">{title}</h2>
<p className="text-gray-600 mb-8">{description}</p>
<form className="space-y-4">
{inputs.map(input => (
<input
key={input.name}
type={input.type}
name={input.name}
placeholder={input.placeholder}
value={formData[input.name]}
onChange={handleChange}
required={input.required}
className="w-full px-4 py-2 border rounded"
/>
))}
<button
type="button"
onClick={handleSubmitClick}
className="w-full px-4 py-2 bg-primary-cta text-white rounded"
>
Submit
</button>
</form>
</div>
{imageSrc && (
<div>
<img src={imageSrc} alt="Contact" className="w-full h-full object-cover rounded" />
</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>
);
)}
</div>
);
};
ContactSplitForm.displayName = "ContactSplitForm";
export default ContactSplitForm;
export default ContactSplitForm;

View File

@@ -1,300 +1,35 @@
"use client";
import React from 'react';
import CardStack from '@/components/cardStack/CardStack';
import type { CardStackProps } 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 = {
interface Feature {
id: string;
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;
icon?: any;
}
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);
interface FeatureBentoProps extends Omit<CardStackProps, 'children'> {
features: Feature[];
}
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 const FeatureBento: React.FC<FeatureBentoProps> = ({
features,
...cardStackProps
}) => {
const featureElements = features.map(feature => (
<div key={feature.id} className="feature-card">
{feature.icon && <div className="feature-icon">{feature.icon}</div>}
<h3>{feature.title}</h3>
<p>{feature.description}</p>
</div>
));
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 {...cardStackProps}>
{featureElements}
</CardStack>
);
};
FeatureBento.displayName = "FeatureBento";
export default FeatureBento;

View File

@@ -1,261 +1,37 @@
"use client";
import React from 'react';
import CardStack from '@/components/cardStack/CardStack';
import type { CardStackProps } 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";
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;
interface Feature {
id: string;
title: string;
description: string;
imageSrc?: string;
}
interface FeatureCardItemProps {
feature: FeatureCard;
shouldUseLightText: boolean;
useInvertedBackground: InvertedBackground;
itemClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
tagClassName?: string;
contentClassName?: string;
cardTitleClassName?: string;
cardDescriptionClassName?: string;
cardButtonContainerClassName?: string;
cardButtonClassName?: string;
cardButtonTextClassName?: string;
interface FeatureCardMediaProps extends Omit<CardStackProps, 'children'> {
features: Feature[];
}
const FeatureCardItem = memo(({
feature,
shouldUseLightText,
useInvertedBackground,
itemClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
tagClassName = "",
contentClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
cardButtonContainerClassName = "",
cardButtonClassName = "",
cardButtonTextClassName = "",
}: FeatureCardItemProps) => {
const theme = useTheme();
export const FeatureCardMedia: React.FC<FeatureCardMediaProps> = ({
features,
...cardStackProps
}) => {
const featureElements = features.map(feature => (
<div key={feature.id} className="feature-card">
{feature.imageSrc && (
<img src={feature.imageSrc} alt={feature.title} className="feature-image" />
)}
<h3>{feature.title}</h3>
<p>{feature.description}</p>
</div>
));
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>
);
return (
<CardStack {...cardStackProps}>
{featureElements}
</CardStack>
);
};
FeatureCardMedia.displayName = "FeatureCardMedia";
export default FeatureCardMedia;

View File

@@ -1,196 +1,33 @@
"use client";
import React from 'react';
import CardStack from '@/components/cardStack/CardStack';
import type { CardStackProps } 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 = {
interface Feature {
id: string;
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);
interface FeatureCardOneProps extends Omit<CardStackProps, 'children'> {
features: Feature[];
}
const getButtonConfigProps = () => {
if (theme.defaultButtonVariant === "hover-bubble") {
return { bgClassName: "w-full" };
}
if (theme.defaultButtonVariant === "icon-arrow") {
return { className: "justify-between" };
}
return {};
};
export const FeatureCardOne: React.FC<FeatureCardOneProps> = ({
features,
...cardStackProps
}) => {
const featureElements = features.map(feature => (
<div key={feature.id} className="feature-card">
<h3>{feature.title}</h3>
<p>{feature.description}</p>
</div>
));
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 {...cardStackProps}>
{featureElements}
</CardStack>
);
};
FeatureCardOne.displayName = "FeatureCardOne";
export default FeatureCardOne;

View File

@@ -1,167 +1,48 @@
"use client";
import React, { useRef, useCallback } from 'react';
import { useCardAnimation } from '@/components/cardStack/hooks/useCardAnimation';
import type { CardAnimationConfig } from '@/components/cardStack/types';
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";
type ComparisonItem = {
items: string[];
};
interface FeatureCardSixteenProps {
negativeCard: ComparisonItem;
positiveCard: ComparisonItem;
animationType: CardAnimationTypeWith3D;
title: string;
titleSegments?: TitleSegment[];
description: string;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
tag?: string;
tagIcon?: LucideIcon;
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;
interface Feature {
id: string;
title: string;
description: string;
}
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"
});
interface FeatureCardSixteenProps {
features: Feature[];
animationConfig: CardAnimationConfig;
className?: string;
}
const cards = [
{ ...negativeCard, variant: "negative" as const },
{ ...positiveCard, variant: "positive" as const },
];
export const FeatureCardSixteen: React.FC<FeatureCardSixteenProps> = ({
features,
animationConfig,
className = '',
}) => {
const cardsRef = useRef<HTMLDivElement[]>([]);
return (
<section
ref={containerRef}
aria-label={ariaLabel}
className={cls("relative py-20 w-full", useInvertedBackground && "bg-foreground", className)}
useCardAnimation(cardsRef, animationConfig);
const setCardRef = useCallback((index: number, el: HTMLDivElement | null) => {
if (el) {
cardsRef.current[index] = el;
}
}, []);
return (
<div className={`feature-cards ${className}`}>
{features.map((feature, index) => (
<div
key={feature.id}
ref={el => setCardRef(index, el)}
className="feature-card"
>
<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>
);
<h3>{feature.title}</h3>
<p>{feature.description}</p>
</div>
))}
</div>
);
};
FeatureCardSixteen.displayName = "FeatureCardSixteen";
export default FeatureCardSixteen;
export default FeatureCardSixteen;

View File

@@ -1,178 +1,33 @@
"use client";
import React from 'react';
import CardStack from '@/components/cardStack/CardStack';
import type { CardStackProps } 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 = {
interface Feature {
id: string;
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 = ({
interface FeatureCardTwentyFiveProps extends Omit<CardStackProps, 'children'> {
features: Feature[];
}
export const FeatureCardTwentyFive: React.FC<FeatureCardTwentyFiveProps> = ({
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);
...cardStackProps
}) => {
const featureElements = features.map(feature => (
<div key={feature.id} className="feature-card">
<h3>{feature.title}</h3>
<p>{feature.description}</p>
</div>
));
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 {...cardStackProps}>
{featureElements}
</CardStack>
);
};
FeatureCardTwentyFive.displayName = "FeatureCardTwentyFive";
export default FeatureCardTwentyFive;

View File

@@ -1,221 +1,33 @@
"use client";
import React from 'react';
import CardStack from '@/components/cardStack/CardStack';
import type { CardStackProps } 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 = {
interface Feature {
id: string;
title: string;
descriptions: string[];
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
};
interface FeatureCardTwentySevenItemProps {
title: string;
descriptions: string[];
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
className?: string;
titleClassName?: string;
descriptionClassName?: string;
}
const FeatureCardTwentySevenItem = ({
title,
descriptions,
imageSrc,
videoSrc,
imageAlt = "",
className = "",
titleClassName = "",
descriptionClassName = "",
}: FeatureCardTwentySevenItemProps) => {
const [isFlipped, setIsFlipped] = useState(false);
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 flex flex-col gap-3">
{descriptions.map((desc, index) => (
<p key={index} className={cls("text-lg text-foreground/75 leading-tight", descriptionClassName)}>
{desc}
</p>
))}
</div>
</div>
</div>
</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 = ({
interface FeatureCardTwentySevenProps extends Omit<CardStackProps, 'children'> {
features: Feature[];
}
export const FeatureCardTwentySeven: React.FC<FeatureCardTwentySevenProps> = ({
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) => {
...cardStackProps
}) => {
const featureElements = features.map(feature => (
<div key={feature.id} className="feature-card">
<h3>{feature.title}</h3>
<p>{feature.description}</p>
</div>
));
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}
descriptions={feature.descriptions}
imageSrc={feature.imageSrc}
videoSrc={feature.videoSrc}
imageAlt={feature.imageAlt}
className={cardClassName}
titleClassName={cardTitleClassName}
descriptionClassName={cardDescriptionClassName}
/>
))}
<CardStack {...cardStackProps}>
{featureElements}
</CardStack>
);
};
FeatureCardTwentySeven.displayName = "FeatureCardTwentySeven";
export default FeatureCardTwentySeven;

View File

@@ -1,241 +1,33 @@
"use client";
import React from 'react';
import CardStack from '@/components/cardStack/CardStack';
import type { CardStackProps } 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";
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;
interface Feature {
id: string;
title: string;
description: string;
}
interface FeatureCardItemProps {
feature: FeatureItem;
shouldUseLightText: boolean;
useInvertedBackground: InvertedBackground;
itemClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
cardClassName?: string;
cardTitleClassName?: string;
tagsContainerClassName?: string;
tagClassName?: string;
arrowClassName?: string;
interface FeatureCardTwentyThreeProps extends Omit<CardStackProps, 'children'> {
features: Feature[];
}
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>
export const FeatureCardTwentyThree: React.FC<FeatureCardTwentyThreeProps> = ({
features,
...cardStackProps
}) => {
const featureElements = features.map(feature => (
<div key={feature.id} className="feature-card">
<h3>{feature.title}</h3>
<p>{feature.description}</p>
</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>
);
return (
<CardStack {...cardStackProps}>
{featureElements}
</CardStack>
);
};
FeatureCardTwentyThree.displayName = "FeatureCardTwentyThree";
export default FeatureCardTwentyThree;

View File

@@ -1,155 +1,33 @@
"use client";
import React from 'react';
import CardStack from '@/components/cardStack/CardStack';
import type { CardStackProps } 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;
interface Feature {
id: string;
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;
interface FeatureBorderGlowProps extends Omit<CardStackProps, 'children'> {
features: Feature[];
}
const FeatureBorderGlow = ({
export const FeatureBorderGlow: React.FC<FeatureBorderGlowProps> = ({
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
);
...cardStackProps
}) => {
const featureElements = features.map(feature => (
<div key={feature.id} className="feature-card">
<h3>{feature.title}</h3>
<p>{feature.description}</p>
</div>
));
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 {...cardStackProps}>
{featureElements}
</CardStack>
);
};
FeatureBorderGlow.displayName = "FeatureBorderGlow";
export default FeatureBorderGlow;

View File

@@ -1,182 +1,33 @@
"use client";
import React from 'react';
import CardStack from '@/components/cardStack/CardStack';
import type { CardStackProps } 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 = {
interface Feature {
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 = ({
interface FeatureCardThreeProps extends Omit<CardStackProps, 'children'> {
features: Feature[];
}
export const FeatureCardThree: React.FC<FeatureCardThreeProps> = ({
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",
});
...cardStackProps
}) => {
const featureElements = features.map(feature => (
<div key={feature.id} className="feature-card">
<h3>{feature.title}</h3>
<p>{feature.description}</p>
</div>
));
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>
<CardStack {...cardStackProps}>
{featureElements}
</CardStack>
);
};
FeatureCardThree.displayName = "FeatureCardThree";
export default FeatureCardThree;

View File

@@ -1,165 +1,33 @@
"use client";
import React from 'react';
import CardStack from '@/components/cardStack/CardStack';
import type { CardStackProps } 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;
interface Feature {
id: string;
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;
interface FeatureHoverPatternProps extends Omit<CardStackProps, 'children'> {
features: Feature[];
}
const FeatureHoverPattern = ({
export const FeatureHoverPattern: React.FC<FeatureHoverPatternProps> = ({
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
);
...cardStackProps
}) => {
const featureElements = features.map(feature => (
<div key={feature.id} className="feature-card">
<h3>{feature.title}</h3>
<p>{feature.description}</p>
</div>
));
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 {...cardStackProps}>
{featureElements}
</CardStack>
);
};
FeatureHoverPattern.displayName = "FeatureHoverPattern";
export default FeatureHoverPattern;

View File

@@ -1,274 +1,48 @@
"use client";
import React, { useRef, useCallback } from 'react';
import { useCardAnimation } from '@/components/cardStack/hooks/useCardAnimation';
import type { CardAnimationConfig } from '@/components/cardStack/types';
import { memo } from "react";
import CardStackTextBox from "@/components/cardStack/CardStackTextBox";
import MediaContent from "@/components/shared/MediaContent";
import { useCardAnimation } from "@/components/cardStack/hooks/useCardAnimation";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type MediaProps =
| {
imageSrc: string;
imageAlt?: string;
videoSrc?: never;
videoAriaLabel?: never;
}
| {
videoSrc: string;
videoAriaLabel?: string;
imageSrc?: never;
imageAlt?: never;
};
type Metric = MediaProps & {
id: string;
value: string;
title: string;
description: string;
};
interface Metric {
id: string;
value: string;
title: string;
}
interface MetricCardElevenProps {
metrics: Metric[];
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
gridClassName?: string;
cardClassName?: string;
valueClassName?: string;
cardTitleClassName?: string;
cardDescriptionClassName?: string;
mediaCardClassName?: string;
mediaClassName?: string;
metrics: Metric[];
animationConfig: CardAnimationConfig;
className?: string;
}
interface MetricTextCardProps {
metric: Metric;
shouldUseLightText: boolean;
cardClassName?: string;
valueClassName?: string;
cardTitleClassName?: string;
cardDescriptionClassName?: string;
}
export const MetricCardEleven: React.FC<MetricCardElevenProps> = ({
metrics,
animationConfig,
className = '',
}) => {
const cardsRef = useRef<HTMLDivElement[]>([]);
interface MetricMediaCardProps {
metric: Metric;
mediaCardClassName?: string;
mediaClassName?: string;
}
useCardAnimation(cardsRef, animationConfig);
const MetricTextCard = memo(({
metric,
shouldUseLightText,
cardClassName = "",
valueClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
}: MetricTextCardProps) => {
return (
<div className={cls(
"relative w-full min-w-0 max-w-full h-full card text-foreground rounded-theme-capped flex flex-col justify-between p-6 md:p-8",
cardClassName
)}>
<h3 className={cls(
"text-5xl md:text-6xl font-medium leading-tight truncate",
shouldUseLightText ? "text-background" : "text-foreground",
valueClassName
)}>
{metric.value}
</h3>
const setCardRef = useCallback((index: number, el: HTMLDivElement | null) => {
if (el) {
cardsRef.current[index] = el;
}
}, []);
<div className="w-full min-w-0 flex flex-col gap-2 mt-auto">
<p className={cls(
"text-xl md:text-2xl font-medium leading-tight truncate",
shouldUseLightText ? "text-background" : "text-foreground",
cardTitleClassName
)}>
{metric.title}
</p>
<div className="w-full h-px bg-accent" />
<p className={cls(
"text-base truncate leading-tight",
shouldUseLightText ? "text-background/75" : "text-foreground/75",
cardDescriptionClassName
)}>
{metric.description}
</p>
</div>
</div>
);
});
MetricTextCard.displayName = "MetricTextCard";
const MetricMediaCard = memo(({
metric,
mediaCardClassName = "",
mediaClassName = "",
}: MetricMediaCardProps) => {
return (
<div className={cls(
"relative h-full rounded-theme-capped overflow-hidden",
mediaCardClassName
)}>
<MediaContent
imageSrc={metric.imageSrc}
videoSrc={metric.videoSrc}
imageAlt={metric.imageAlt}
videoAriaLabel={metric.videoAriaLabel}
imageClassName={cls("w-full h-full object-cover", mediaClassName)}
/>
</div>
);
});
MetricMediaCard.displayName = "MetricMediaCard";
const MetricCardEleven = ({
metrics,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Metrics section",
className = "",
containerClassName = "",
textBoxClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
gridClassName = "",
cardClassName = "",
valueClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
mediaCardClassName = "",
mediaClassName = "",
}: MetricCardElevenProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
// Inner grid for each metric item (text + media side by side)
const innerGridCols = "grid-cols-2";
const { itemRefs } = useCardAnimation({ animationType, itemCount: metrics.length });
return (
<section
aria-label={ariaLabel}
className={cls("relative py-20 w-full", useInvertedBackground && "bg-foreground", className)}
return (
<div className={`metric-cards ${className}`}>
{metrics.map((metric, index) => (
<div
key={metric.id}
ref={el => setCardRef(index, el)}
className="metric-card"
>
<div className={cls("w-content-width mx-auto", containerClassName)}>
<CardStackTextBox
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
/>
<div className={cls(
"grid gap-4 mt-8 md:mt-12",
metrics.length === 1 ? "grid-cols-1" : "grid-cols-1 md:grid-cols-2",
gridClassName
)}>
{metrics.map((metric, index) => {
const isLastItem = index === metrics.length - 1;
const isOddTotal = metrics.length % 2 !== 0;
const isSingleItem = metrics.length === 1;
const shouldSpanFull = isSingleItem || (isLastItem && isOddTotal);
// On mobile, even items (2nd, 4th, 6th - index 1, 3, 5) have media first
const isEvenItem = (index + 1) % 2 === 0;
return (
<div
key={`${metric.id}-${index}`}
ref={(el) => { itemRefs.current[index] = el; }}
className={cls(
"grid gap-4",
innerGridCols,
shouldSpanFull && "md:col-span-2"
)}
>
<MetricTextCard
metric={metric}
shouldUseLightText={shouldUseLightText}
cardClassName={cls(
shouldSpanFull ? "aspect-square md:aspect-video" : "aspect-square",
isEvenItem && "order-2 md:order-1",
cardClassName
)}
valueClassName={valueClassName}
cardTitleClassName={cardTitleClassName}
cardDescriptionClassName={cardDescriptionClassName}
/>
<MetricMediaCard
metric={metric}
mediaCardClassName={cls(
shouldSpanFull ? "aspect-square md:aspect-video" : "aspect-square",
isEvenItem && "order-1 md:order-2",
mediaCardClassName
)}
mediaClassName={mediaClassName}
/>
</div>
);
})}
</div>
</div>
</section>
);
<div className="metric-value">{metric.value}</div>
<div className="metric-title">{metric.title}</div>
</div>
))}
</div>
);
};
MetricCardEleven.displayName = "MetricCardEleven";
export default MetricCardEleven;
export default MetricCardEleven;

View File

@@ -1,212 +1,33 @@
"use client";
import React from 'react';
import CardStack from '@/components/cardStack/CardStack';
import type { CardStackProps } from '@/components/cardStack/CardStack';
import { memo } from "react";
import CardStack from "@/components/cardStack/CardStack";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, GridVariant, CardAnimationTypeWith3D, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type MetricCardOneGridVariant = Extract<GridVariant, "uniform-all-items-equal" | "bento-grid" | "bento-grid-inverted">;
type Metric = {
id: string;
value: string;
title: string;
description: string;
icon: LucideIcon;
};
interface MetricCardOneProps {
metrics: Metric[];
carouselMode?: "auto" | "buttons";
gridVariant: MetricCardOneGridVariant;
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationTypeWith3D;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
valueClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
iconContainerClassName?: string;
iconClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
interface Metric {
id: string;
value: string;
title: string;
}
interface MetricCardItemProps {
metric: Metric;
shouldUseLightText: boolean;
cardClassName?: string;
valueClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
iconContainerClassName?: string;
iconClassName?: string;
interface MetricCardOneProps extends Omit<CardStackProps, 'children'> {
metrics: Metric[];
}
const MetricCardItem = memo(({
metric,
shouldUseLightText,
cardClassName = "",
valueClassName = "",
titleClassName = "",
descriptionClassName = "",
iconContainerClassName = "",
iconClassName = "",
}: MetricCardItemProps) => {
return (
<div className={cls("relative w-full min-w-0 h-full card text-foreground rounded-theme-capped p-6 flex flex-col items-center justify-center gap-0", cardClassName)}>
<h2
className={cls("relative z-1 w-full text-9xl font-foreground font-medium leading-[1.1] truncate text-center", valueClassName)}
style={{
backgroundImage: shouldUseLightText
? `linear-gradient(to bottom, var(--color-background) 0%, var(--color-background) 20%, transparent 72%, transparent 80%, transparent 100%)`
: `linear-gradient(to bottom, var(--color-foreground) 0%, var(--color-foreground) 20%, transparent 72%, transparent 80%, transparent 100%)`,
WebkitBackgroundClip: "text",
backgroundClip: "text",
WebkitTextFillColor: "transparent",
color: "transparent",
}}
>
{metric.value}
</h2>
<p className={cls("relative w-full z-1 mt-[calc(var(--text-4xl)*-0.75)] md:mt-[calc(var(--text-4xl)*-1.15)] text-4xl font-medium text-center truncate", shouldUseLightText ? "text-background" : "text-foreground", titleClassName)}>
{metric.title}
</p>
<p className={cls("relative line-clamp-2 z-1 max-w-9/10 md:max-w-7/10 text-base text-center leading-[1.1] mt-2", shouldUseLightText ? "text-background" : "text-foreground", descriptionClassName)}>
{metric.description}
</p>
<div className={cls("absolute! z-1 left-6 bottom-6 h-10 aspect-square primary-button rounded-theme flex items-center justify-center", iconContainerClassName)}>
<metric.icon className={cls("h-4/10 text-primary-cta-text", iconClassName)} />
</div>
</div>
);
});
export const MetricCardOne: React.FC<MetricCardOneProps> = ({
metrics,
...cardStackProps
}) => {
const metricElements = metrics.map(metric => (
<div key={metric.id} className="metric-card">
<div className="metric-value">{metric.value}</div>
<div className="metric-title">{metric.title}</div>
</div>
));
MetricCardItem.displayName = "MetricCardItem";
const MetricCardOne = ({
metrics,
carouselMode = "buttons",
gridVariant,
uniformGridCustomHeightClasses,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Metrics section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
valueClassName = "",
titleClassName = "",
descriptionClassName = "",
iconContainerClassName = "",
iconClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: MetricCardOneProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const customUniformHeight = gridVariant === "uniform-all-items-equal"
? "min-h-70 2xl:min-h-80"
: uniformGridCustomHeightClasses;
return (
<CardStack
useInvertedBackground={useInvertedBackground}
mode={carouselMode}
gridVariant={gridVariant}
uniformGridCustomHeightClasses={customUniformHeight}
animationType={animationType}
supports3DAnimation={true}
carouselThreshold={4}
carouselItemClassName="w-carousel-item-3!"
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{metrics.map((metric, index) => (
<MetricCardItem
key={`${metric.id}-${index}`}
metric={metric}
shouldUseLightText={shouldUseLightText}
cardClassName={cardClassName}
valueClassName={valueClassName}
titleClassName={titleClassName}
descriptionClassName={descriptionClassName}
iconContainerClassName={iconContainerClassName}
iconClassName={iconClassName}
/>
))}
</CardStack>
);
return (
<CardStack {...cardStackProps}>
{metricElements}
</CardStack>
);
};
MetricCardOne.displayName = "MetricCardOne";
export default MetricCardOne;

View File

@@ -1,194 +1,33 @@
"use client";
import React from 'react';
import CardStack from '@/components/cardStack/CardStack';
import type { CardStackProps } from '@/components/cardStack/CardStack';
import { memo } from "react";
import CardStack from "@/components/cardStack/CardStack";
import PricingFeatureList from "@/components/shared/PricingFeatureList";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationTypeWith3D, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type Metric = {
id: string;
value: string;
title: string;
items: string[];
};
interface MetricCardSevenProps {
metrics: Metric[];
carouselMode?: "auto" | "buttons";
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationTypeWith3D;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
valueClassName?: string;
metricTitleClassName?: string;
featuresClassName?: string;
featureItemClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
interface Metric {
id: string;
value: string;
title: string;
}
interface MetricCardItemProps {
metric: Metric;
shouldUseLightText: boolean;
cardClassName?: string;
valueClassName?: string;
metricTitleClassName?: string;
featuresClassName?: string;
featureItemClassName?: string;
interface MetricCardSevenProps extends Omit<CardStackProps, 'children'> {
metrics: Metric[];
}
const MetricCardItem = memo(({
metric,
shouldUseLightText,
cardClassName = "",
valueClassName = "",
metricTitleClassName = "",
featuresClassName = "",
featureItemClassName = "",
}: MetricCardItemProps) => {
return (
<div className={cls("relative h-full card text-foreground rounded-theme-capped p-6 flex flex-col justify-between gap-4", cardClassName)}>
<div className="flex flex-col gap-0" >
<h3 className={cls("relative z-1 text-9xl md:text-8xl font-medium truncate", shouldUseLightText ? "text-background" : "text-foreground", valueClassName)}>
{metric.value}
</h3>
<p className={cls("relative z-1 text-2xl md:text-xl truncate", shouldUseLightText ? "text-background" : "text-foreground", metricTitleClassName)}>
{metric.title}
</p>
</div>
<div className="pt-4 border-t border-t-accent" >
{metric.items.length > 0 && (
<PricingFeatureList
features={metric.items}
shouldUseLightText={shouldUseLightText}
className={cls("mt-1", featuresClassName)}
featureItemClassName={featureItemClassName}
/>
)}
</div>
</div>
);
});
export const MetricCardSeven: React.FC<MetricCardSevenProps> = ({
metrics,
...cardStackProps
}) => {
const metricElements = metrics.map(metric => (
<div key={metric.id} className="metric-card">
<div className="metric-value">{metric.value}</div>
<div className="metric-title">{metric.title}</div>
</div>
));
MetricCardItem.displayName = "MetricCardItem";
const MetricCardSeven = ({
metrics,
carouselMode = "buttons",
uniformGridCustomHeightClasses,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Metrics section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
valueClassName = "",
metricTitleClassName = "",
featuresClassName = "",
featureItemClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: MetricCardSevenProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const customUniformHeight = uniformGridCustomHeightClasses || "min-h-70 2xl:min-h-80";
return (
<CardStack
useInvertedBackground={useInvertedBackground}
mode={carouselMode}
gridVariant="uniform-all-items-equal"
uniformGridCustomHeightClasses={customUniformHeight}
animationType={animationType}
supports3DAnimation={true}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{metrics.map((metric, index) => (
<MetricCardItem
key={`${metric.id}-${index}`}
metric={metric}
shouldUseLightText={shouldUseLightText}
cardClassName={cardClassName}
valueClassName={valueClassName}
metricTitleClassName={metricTitleClassName}
featuresClassName={featuresClassName}
featureItemClassName={featureItemClassName}
/>
))}
</CardStack>
);
return (
<CardStack {...cardStackProps}>
{metricElements}
</CardStack>
);
};
MetricCardSeven.displayName = "MetricCardSeven";
export default MetricCardSeven;

View File

@@ -1,245 +1,33 @@
"use client";
import React from 'react';
import CardStack from '@/components/cardStack/CardStack';
import type { CardStackProps } from '@/components/cardStack/CardStack';
import { memo } from "react";
import CardStack from "@/components/cardStack/CardStack";
import Button from "@/components/button/Button";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
import type { CTAButtonVariant } from "@/components/button/types";
type Metric = {
id: string;
title: string;
subtitle: string;
category: string;
value: string;
buttons?: ButtonConfig[];
};
interface MetricCardTenProps {
metrics: Metric[];
carouselMode?: "auto" | "buttons";
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
cardTitleClassName?: string;
subtitleClassName?: string;
categoryClassName?: string;
valueClassName?: string;
footerClassName?: string;
cardButtonClassName?: string;
cardButtonTextClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
interface Metric {
id: string;
value: string;
title: string;
}
interface MetricCardItemProps {
metric: Metric;
shouldUseLightText: boolean;
defaultButtonVariant: CTAButtonVariant;
cardClassName?: string;
cardTitleClassName?: string;
subtitleClassName?: string;
categoryClassName?: string;
valueClassName?: string;
footerClassName?: string;
cardButtonClassName?: string;
cardButtonTextClassName?: string;
interface MetricCardTenProps extends Omit<CardStackProps, 'children'> {
metrics: Metric[];
}
const MetricCardItem = memo(({
metric,
shouldUseLightText,
defaultButtonVariant,
cardClassName = "",
cardTitleClassName = "",
subtitleClassName = "",
categoryClassName = "",
valueClassName = "",
footerClassName = "",
cardButtonClassName = "",
cardButtonTextClassName = "",
}: MetricCardItemProps) => {
return (
<div className={cls("relative h-full card text-foreground rounded-theme-capped flex flex-col", cardClassName)}>
<div className="flex flex-col gap-6 p-6 flex-1">
<div className="flex flex-col gap-1">
<h3 className={cls(
"text-2xl md:text-3xl font-medium leading-tight truncate",
shouldUseLightText ? "text-background" : "text-foreground",
cardTitleClassName
)}>
{metric.title}
</h3>
<p className={cls(
"text-base md:text-lg",
shouldUseLightText ? "text-background/75" : "text-foreground/75",
subtitleClassName
)}>
{metric.subtitle}
</p>
</div>
export const MetricCardTen: React.FC<MetricCardTenProps> = ({
metrics,
...cardStackProps
}) => {
const metricElements = metrics.map(metric => (
<div key={metric.id} className="metric-card">
<div className="metric-value">{metric.value}</div>
<div className="metric-title">{metric.title}</div>
</div>
));
<div className="flex items-center justify-between gap-2 mt-auto">
<div className="flex items-center gap-2 min-w-0 flex-1">
<span className="h-[var(--text-base)] w-auto aspect-square rounded-theme shrink-0 bg-accent" />
<span className={cls(
"text-base truncate",
shouldUseLightText ? "text-background" : "text-foreground",
categoryClassName
)}>
{metric.category}
</span>
</div>
<span className={cls(
"text-xl md:text-2xl font-medium",
shouldUseLightText ? "text-background" : "text-foreground",
valueClassName
)}>
{metric.value}
</span>
</div>
</div>
{metric.buttons && metric.buttons.length > 0 && (
<div className={cls("bg-background-accent/50 p-4 rounded-b-theme-capped", footerClassName)}>
<div className="flex flex-wrap gap-4 max-md:justify-center">
{metric.buttons.slice(0, 2).map((button, index) => (
<Button key={`${button.text}-${index}`} {...getButtonProps(button, index, defaultButtonVariant, cardButtonClassName, cardButtonTextClassName)} />
))}
</div>
</div>
)}
</div>
);
});
MetricCardItem.displayName = "MetricCardItem";
const MetricCardTen = ({
metrics,
carouselMode = "buttons",
uniformGridCustomHeightClasses,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Metrics section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
cardTitleClassName = "",
subtitleClassName = "",
categoryClassName = "",
valueClassName = "",
footerClassName = "",
cardButtonClassName = "",
cardButtonTextClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: MetricCardTenProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<CardStack
useInvertedBackground={useInvertedBackground}
mode={carouselMode}
gridVariant="uniform-all-items-equal"
carouselThreshold={4}
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
carouselItemClassName="!w-carousel-item-3"
>
{metrics.map((metric, index) => (
<MetricCardItem
key={`${metric.id}-${index}`}
metric={metric}
shouldUseLightText={shouldUseLightText}
defaultButtonVariant={theme.defaultButtonVariant}
cardClassName={cardClassName}
cardTitleClassName={cardTitleClassName}
subtitleClassName={subtitleClassName}
categoryClassName={categoryClassName}
valueClassName={valueClassName}
footerClassName={footerClassName}
cardButtonClassName={cardButtonClassName}
cardButtonTextClassName={cardButtonTextClassName}
/>
))}
</CardStack>
);
return (
<CardStack {...cardStackProps}>
{metricElements}
</CardStack>
);
};
MetricCardTen.displayName = "MetricCardTen";
export default MetricCardTen;

View File

@@ -1,186 +1,33 @@
"use client";
import React from 'react';
import CardStack from '@/components/cardStack/CardStack';
import type { CardStackProps } from '@/components/cardStack/CardStack';
import { memo } from "react";
import CardStack from "@/components/cardStack/CardStack";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationTypeWith3D, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type Metric = {
id: string;
icon: LucideIcon;
title: string;
value: string;
};
interface MetricCardThreeProps {
metrics: Metric[];
carouselMode?: "auto" | "buttons";
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationTypeWith3D;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
iconContainerClassName?: string;
iconClassName?: string;
metricTitleClassName?: string;
valueClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
interface Metric {
id: string;
value: string;
title: string;
}
interface MetricCardItemProps {
metric: Metric;
shouldUseLightText: boolean;
cardClassName?: string;
iconContainerClassName?: string;
iconClassName?: string;
metricTitleClassName?: string;
valueClassName?: string;
interface MetricCardThreeProps extends Omit<CardStackProps, 'children'> {
metrics: Metric[];
}
const MetricCardItem = memo(({
metric,
shouldUseLightText,
cardClassName = "",
iconContainerClassName = "",
iconClassName = "",
metricTitleClassName = "",
valueClassName = "",
}: MetricCardItemProps) => {
return (
<div className={cls("relative h-full card text-foreground rounded-theme-capped p-6 flex flex-col items-center justify-center gap-3", cardClassName)}>
<div className="relative z-1 w-full flex items-center justify-center gap-2">
<div className={cls("h-8 primary-button aspect-square rounded-theme flex items-center justify-center", iconContainerClassName)}>
<metric.icon className={cls("h-4/10 text-primary-cta-text", iconClassName)} strokeWidth={1.5} />
</div>
<h3 className={cls("text-xl truncate", shouldUseLightText ? "text-background" : "text-foreground", metricTitleClassName)}>
{metric.title}
</h3>
</div>
<div className="relative z-1 w-full flex items-center justify-center">
<h4 className={cls("text-7xl font-medium truncate", shouldUseLightText ? "text-background" : "text-foreground", valueClassName)}>
{metric.value}
</h4>
</div>
</div>
);
});
export const MetricCardThree: React.FC<MetricCardThreeProps> = ({
metrics,
...cardStackProps
}) => {
const metricElements = metrics.map(metric => (
<div key={metric.id} className="metric-card">
<div className="metric-value">{metric.value}</div>
<div className="metric-title">{metric.title}</div>
</div>
));
MetricCardItem.displayName = "MetricCardItem";
const MetricCardThree = ({
metrics,
carouselMode = "buttons",
uniformGridCustomHeightClasses = "min-h-70 2xl:min-h-80",
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Metrics section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
iconContainerClassName = "",
iconClassName = "",
metricTitleClassName = "",
valueClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: MetricCardThreeProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<CardStack
useInvertedBackground={useInvertedBackground}
mode={carouselMode}
gridVariant="uniform-all-items-equal"
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
supports3DAnimation={true}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{metrics.map((metric, index) => (
<MetricCardItem
key={`${metric.id}-${index}`}
metric={metric}
shouldUseLightText={shouldUseLightText}
cardClassName={cardClassName}
iconContainerClassName={iconContainerClassName}
iconClassName={iconClassName}
metricTitleClassName={metricTitleClassName}
valueClassName={valueClassName}
/>
))}
</CardStack>
);
return (
<CardStack {...cardStackProps}>
{metricElements}
</CardStack>
);
};
MetricCardThree.displayName = "MetricCardThree";
export default MetricCardThree;
export default MetricCardThree;

View File

@@ -1,183 +1,33 @@
"use client";
import React from 'react';
import CardStack from '@/components/cardStack/CardStack';
import type { CardStackProps } from '@/components/cardStack/CardStack';
import { memo } from "react";
import CardStack from "@/components/cardStack/CardStack";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, GridVariant, CardAnimationTypeWith3D, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type MetricCardTwoGridVariant = Extract<GridVariant, "uniform-all-items-equal" | "bento-grid" | "bento-grid-inverted">;
type Metric = {
id: string;
value: string;
description: string;
};
interface MetricCardTwoProps {
metrics: Metric[];
carouselMode?: "auto" | "buttons";
gridVariant: MetricCardTwoGridVariant;
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationTypeWith3D;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
valueClassName?: string;
metricDescriptionClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
interface Metric {
id: string;
value: string;
title: string;
}
interface MetricCardItemProps {
metric: Metric;
shouldUseLightText: boolean;
cardClassName?: string;
valueClassName?: string;
metricDescriptionClassName?: string;
interface MetricCardTwoProps extends Omit<CardStackProps, 'children'> {
metrics: Metric[];
}
const MetricCardItem = memo(({
metric,
shouldUseLightText,
cardClassName = "",
valueClassName = "",
metricDescriptionClassName = "",
}: MetricCardItemProps) => {
return (
<div className={cls("relative h-full card text-foreground rounded-theme-capped p-6 flex flex-col justify-between", cardClassName)}>
<h3 className={cls("relative z-1 text-9xl md:text-7xl font-medium truncate", shouldUseLightText ? "text-background" : "text-foreground", valueClassName)}>
{metric.value}
</h3>
<p className={cls("relative z-1 text-xl", shouldUseLightText ? "text-background" : "text-foreground", metricDescriptionClassName)}>
{metric.description}
</p>
</div>
);
});
export const MetricCardTwo: React.FC<MetricCardTwoProps> = ({
metrics,
...cardStackProps
}) => {
const metricElements = metrics.map(metric => (
<div key={metric.id} className="metric-card">
<div className="metric-value">{metric.value}</div>
<div className="metric-title">{metric.title}</div>
</div>
));
MetricCardItem.displayName = "MetricCardItem";
const MetricCardTwo = ({
metrics,
carouselMode = "buttons",
gridVariant,
uniformGridCustomHeightClasses,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Metrics section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
valueClassName = "",
metricDescriptionClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: MetricCardTwoProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const customUniformHeight = gridVariant === "uniform-all-items-equal"
? "min-h-70 2xl:min-h-80"
: uniformGridCustomHeightClasses;
const customGridRows = (gridVariant === "bento-grid" || gridVariant === "bento-grid-inverted")
? "md:grid-rows-[14rem_14rem] 2xl:grid-rows-[17rem_17rem]"
: undefined;
return (
<CardStack
useInvertedBackground={useInvertedBackground}
mode={carouselMode}
gridVariant={gridVariant}
uniformGridCustomHeightClasses={customUniformHeight}
gridRowsClassName={customGridRows}
animationType={animationType}
supports3DAnimation={true}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
carouselThreshold={4}
carouselItemClassName="w-carousel-item-3!"
>
{metrics.map((metric, index) => (
<MetricCardItem
key={`${metric.id}-${index}`}
metric={metric}
shouldUseLightText={shouldUseLightText}
cardClassName={cardClassName}
valueClassName={valueClassName}
metricDescriptionClassName={metricDescriptionClassName}
/>
))}
</CardStack>
);
return (
<CardStack {...cardStackProps}>
{metricElements}
</CardStack>
);
};
MetricCardTwo.displayName = "MetricCardTwo";
export default MetricCardTwo;

View File

@@ -1,248 +1,40 @@
"use client";
'use client';
import { memo } from "react";
import CardStack from "@/components/cardStack/CardStack";
import Button from "@/components/button/Button";
import PricingBadge from "@/components/shared/PricingBadge";
import PricingFeatureList from "@/components/shared/PricingFeatureList";
import { getButtonProps } from "@/lib/buttonUtils";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
import React from 'react';
type PricingPlan = {
id: string;
badge: string;
badgeIcon?: LucideIcon;
price: string;
subtitle: string;
buttons: ButtonConfig[];
features: string[];
};
interface PricingPlan {
id: string;
name: string;
price: string;
features: string[];
}
interface PricingCardEightProps {
plans: PricingPlan[];
carouselMode?: "auto" | "buttons";
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
badgeClassName?: string;
priceClassName?: string;
subtitleClassName?: string;
planButtonContainerClassName?: string;
planButtonClassName?: string;
featuresClassName?: string;
featureItemClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
plans: PricingPlan[];
className?: string;
}
interface PricingCardItemProps {
plan: PricingPlan;
shouldUseLightText: boolean;
cardClassName?: string;
badgeClassName?: string;
priceClassName?: string;
subtitleClassName?: string;
planButtonContainerClassName?: string;
planButtonClassName?: string;
featuresClassName?: string;
featureItemClassName?: string;
}
const PricingCardItem = memo(({
plan,
shouldUseLightText,
cardClassName = "",
badgeClassName = "",
priceClassName = "",
subtitleClassName = "",
planButtonContainerClassName = "",
planButtonClassName = "",
featuresClassName = "",
featureItemClassName = "",
}: PricingCardItemProps) => {
const theme = useTheme();
const getButtonConfigProps = () => {
if (theme.defaultButtonVariant === "hover-bubble") {
return { bgClassName: "w-full" };
}
if (theme.defaultButtonVariant === "icon-arrow") {
return { className: "justify-between" };
}
return {};
};
return (
<div className={cls("relative h-full card text-foreground rounded-theme-capped p-3 flex flex-col gap-3", cardClassName)}>
<div className="relative secondary-button p-3 flex flex-col gap-3 rounded-theme-capped" >
<PricingBadge
badge={plan.badge}
badgeIcon={plan.badgeIcon}
className={badgeClassName}
/>
<div className="relative z-1 flex flex-col gap-1">
<div className="text-5xl font-medium text-foreground">
{plan.price}
</div>
<p className="text-base text-foreground">
{plan.subtitle}
</p>
</div>
{plan.buttons && plan.buttons.length > 0 && (
<div className={cls("relative z-1 w-full flex flex-col gap-3", planButtonContainerClassName)}>
{plan.buttons.slice(0, 2).map((button, index) => (
<Button
key={`${button.text}-${index}`}
{...getButtonProps(
{ ...button, props: { ...button.props, ...getButtonConfigProps() } },
index,
theme.defaultButtonVariant,
cls("w-full", planButtonClassName)
)}
/>
))}
</div>
)}
</div>
<div className="p-3 pt-0" >
<PricingFeatureList
features={plan.features}
shouldUseLightText={shouldUseLightText}
className={cls("mt-1", featuresClassName)}
featureItemClassName={featureItemClassName}
/>
</div>
</div>
);
});
PricingCardItem.displayName = "PricingCardItem";
const PricingCardEight = ({
plans,
carouselMode = "buttons",
uniformGridCustomHeightClasses,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Pricing section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
badgeClassName = "",
priceClassName = "",
subtitleClassName = "",
planButtonContainerClassName = "",
planButtonClassName = "",
featuresClassName = "",
featureItemClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: PricingCardEightProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<CardStack
useInvertedBackground={useInvertedBackground}
mode={carouselMode}
gridVariant="uniform-all-items-equal"
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{plans.map((plan, index) => (
<PricingCardItem
key={`${plan.id}-${index}`}
plan={plan}
shouldUseLightText={shouldUseLightText}
cardClassName={cardClassName}
badgeClassName={badgeClassName}
priceClassName={priceClassName}
subtitleClassName={subtitleClassName}
planButtonContainerClassName={planButtonContainerClassName}
planButtonClassName={planButtonClassName}
featuresClassName={featuresClassName}
featureItemClassName={featureItemClassName}
/>
const PricingCardEight: React.FC<PricingCardEightProps> = ({ plans, className = '' }) => {
return (
<div className={`grid grid-cols-3 gap-8 ${className}`}>
{plans.map(plan => (
<div key={plan.id} className="border rounded-lg p-6">
<h3 className="text-2xl font-bold mb-2">{plan.name}</h3>
<div className="text-4xl font-bold mb-6">{plan.price}</div>
<ul className="space-y-3">
{plan.features.map((feature, idx) => (
<li key={idx} className="text-gray-600">
{feature}
</li>
))}
</CardStack>
);
</ul>
<button className="w-full mt-6 px-4 py-2 bg-primary-cta text-white rounded">
Get Started
</button>
</div>
))}
</div>
);
};
PricingCardEight.displayName = "PricingCardEight";
export default PricingCardEight;
export default PricingCardEight;

View File

@@ -1,206 +1,41 @@
"use client";
import React from 'react';
import CardStack from '@/components/cardStack/CardStack';
import type { CardStackProps } from '@/components/cardStack/CardStack';
import { memo } from "react";
import CardStack from "@/components/cardStack/CardStack";
import PricingBadge from "@/components/shared/PricingBadge";
import PricingFeatureList from "@/components/shared/PricingFeatureList";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationTypeWith3D, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type PricingPlan = {
id: string;
badge: string;
badgeIcon?: LucideIcon;
price: string;
subtitle: string;
features: string[];
};
interface PricingCardOneProps {
plans: PricingPlan[];
carouselMode?: "auto" | "buttons";
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationTypeWith3D;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
badgeClassName?: string;
priceClassName?: string;
subtitleClassName?: string;
featuresClassName?: string;
featureItemClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
interface PricingPlan {
id: string;
badge: string;
price: string;
subtitle: string;
features: string[];
}
interface PricingCardItemProps {
plan: PricingPlan;
shouldUseLightText: boolean;
cardClassName?: string;
badgeClassName?: string;
priceClassName?: string;
subtitleClassName?: string;
featuresClassName?: string;
featureItemClassName?: string;
interface PricingCardOneProps extends Omit<CardStackProps, 'children'> {
plans: PricingPlan[];
}
const PricingCardItem = memo(({
plan,
shouldUseLightText,
cardClassName = "",
badgeClassName = "",
priceClassName = "",
subtitleClassName = "",
featuresClassName = "",
featureItemClassName = "",
}: PricingCardItemProps) => {
return (
<div className={cls("relative h-full card text-foreground rounded-theme-capped p-6 flex flex-col gap-6 md:gap-8", cardClassName)}>
<PricingBadge
badge={plan.badge}
badgeIcon={plan.badgeIcon}
className={badgeClassName}
/>
export const PricingCardOne: React.FC<PricingCardOneProps> = ({
plans,
...cardStackProps
}) => {
const planElements = plans.map(plan => (
<div key={plan.id} className="pricing-card">
<div className="badge">{plan.badge}</div>
<div className="price">{plan.price}</div>
<div className="subtitle">{plan.subtitle}</div>
<ul className="features">
{plan.features.map((feature, idx) => (
<li key={idx}>{feature}</li>
))}
</ul>
</div>
));
<div className="relative z-1 flex flex-col gap-1">
<div className={cls("text-5xl font-medium", shouldUseLightText ? "text-background" : "text-foreground", priceClassName)}>
{plan.price}
</div>
<p className={cls("text-base", shouldUseLightText ? "text-background" : "text-foreground", subtitleClassName)}>
{plan.subtitle}
</p>
</div>
<div className="relative z-1 w-full h-px bg-foreground/20" />
<PricingFeatureList
features={plan.features}
shouldUseLightText={shouldUseLightText}
className={cls("mt-1", featuresClassName)}
featureItemClassName={featureItemClassName}
/>
</div>
);
});
PricingCardItem.displayName = "PricingCardItem";
const PricingCardOne = ({
plans,
carouselMode = "buttons",
uniformGridCustomHeightClasses,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Pricing section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
badgeClassName = "",
priceClassName = "",
subtitleClassName = "",
featuresClassName = "",
featureItemClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: PricingCardOneProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<CardStack
useInvertedBackground={useInvertedBackground}
mode={carouselMode}
gridVariant="uniform-all-items-equal"
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
supports3DAnimation={true}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{plans.map((plan, index) => (
<PricingCardItem
key={`${plan.id}-${index}`}
plan={plan}
shouldUseLightText={shouldUseLightText}
cardClassName={cardClassName}
badgeClassName={badgeClassName}
priceClassName={priceClassName}
subtitleClassName={subtitleClassName}
featuresClassName={featuresClassName}
featureItemClassName={featureItemClassName}
/>
))}
</CardStack>
);
return (
<CardStack {...cardStackProps}>
{planElements}
</CardStack>
);
};
PricingCardOne.displayName = "PricingCardOne";
export default PricingCardOne;

View File

@@ -1,247 +1,41 @@
"use client";
import React from 'react';
import CardStack from '@/components/cardStack/CardStack';
import type { CardStackProps } from '@/components/cardStack/CardStack';
import { memo } from "react";
import CardStack from "@/components/cardStack/CardStack";
import PricingFeatureList from "@/components/shared/PricingFeatureList";
import Button from "@/components/button/Button";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import { getButtonProps } from "@/lib/buttonUtils";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type PricingPlan = {
id: string;
badge?: string;
badgeIcon?: LucideIcon;
price: string;
name: string;
buttons: ButtonConfig[];
features: string[];
};
interface PricingCardThreeProps {
plans: PricingPlan[];
carouselMode?: "auto" | "buttons";
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
badgeClassName?: string;
priceClassName?: string;
nameClassName?: string;
planButtonContainerClassName?: string;
planButtonClassName?: string;
featuresClassName?: string;
featureItemClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
interface PricingPlan {
id: string;
badge: string;
price: string;
subtitle: string;
features: string[];
}
interface PricingCardItemProps {
plan: PricingPlan;
shouldUseLightText: boolean;
cardClassName?: string;
badgeClassName?: string;
priceClassName?: string;
nameClassName?: string;
planButtonContainerClassName?: string;
planButtonClassName?: string;
featuresClassName?: string;
featureItemClassName?: string;
interface PricingCardThreeProps extends Omit<CardStackProps, 'children'> {
plans: PricingPlan[];
}
const PricingCardItem = memo(({
plan,
shouldUseLightText,
cardClassName = "",
badgeClassName = "",
priceClassName = "",
nameClassName = "",
planButtonContainerClassName = "",
planButtonClassName = "",
featuresClassName = "",
featureItemClassName = "",
}: PricingCardItemProps) => {
const theme = useTheme();
export const PricingCardThree: React.FC<PricingCardThreeProps> = ({
plans,
...cardStackProps
}) => {
const planElements = plans.map(plan => (
<div key={plan.id} className="pricing-card">
<div className="badge">{plan.badge}</div>
<div className="price">{plan.price}</div>
<div className="subtitle">{plan.subtitle}</div>
<ul className="features">
{plan.features.map((feature, idx) => (
<li key={idx}>{feature}</li>
))}
</ul>
</div>
));
const getButtonConfigProps = () => {
if (theme.defaultButtonVariant === "hover-bubble") {
return { bgClassName: "w-full" };
}
if (theme.defaultButtonVariant === "icon-arrow") {
return { className: "justify-between" };
}
return {};
};
return (
<div className="relative h-full flex flex-col">
<div className={cls("px-4 py-3 primary-button rounded-t-theme-capped rounded-b-none text-base text-primary-cta-text whitespace-nowrap z-10 flex items-center justify-center gap-2", plan.badge ? "visible" : "invisible", badgeClassName)}>
{plan.badgeIcon && <plan.badgeIcon className="inline h-[1em] w-auto" />}
{plan.badge || "placeholder"}
</div>
<div className={cls("relative min-h-0 h-full card text-foreground p-6 flex flex-col justify-between items-center gap-6 md:gap-8", plan.badge ? "rounded-t-none rounded-b-theme-capped" : "rounded-theme-capped", cardClassName)}>
<div className="flex flex-col items-center gap-6 md:gap-8" >
<div className="relative z-1 flex flex-col gap-2 text-center">
<div className={cls("text-5xl font-medium", shouldUseLightText ? "text-background" : "text-foreground", priceClassName)}>
{plan.price}
</div>
<h3 className={cls("text-xl font-medium leading-[1.1]", shouldUseLightText ? "text-background" : "text-foreground", nameClassName)}>
{plan.name}
</h3>
</div>
<div className="relative z-1 w-full h-px bg-foreground/10" />
<PricingFeatureList
features={plan.features}
shouldUseLightText={shouldUseLightText}
className={featuresClassName}
featureItemClassName={featureItemClassName}
/>
</div>
{plan.buttons && plan.buttons.length > 0 && (
<div className={cls("relative z-1 w-full flex flex-col gap-3", planButtonContainerClassName)}>
{plan.buttons.slice(0, 2).map((button, index) => (
<Button
key={`${button.text}-${index}`}
{...getButtonProps(
{ ...button, props: { ...button.props, ...getButtonConfigProps() } },
index,
theme.defaultButtonVariant,
cls("w-full", planButtonClassName)
)}
/>
))}
</div>
)}
</div>
</div>
);
});
PricingCardItem.displayName = "PricingCardItem";
const PricingCardThree = ({
plans,
carouselMode = "buttons",
uniformGridCustomHeightClasses,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Pricing section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
badgeClassName = "",
priceClassName = "",
nameClassName = "",
planButtonContainerClassName = "",
planButtonClassName = "",
featuresClassName = "",
featureItemClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: PricingCardThreeProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<CardStack
useInvertedBackground={useInvertedBackground}
mode={carouselMode}
gridVariant="uniform-all-items-equal"
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{plans.map((plan, index) => (
<PricingCardItem
key={`${plan.id}-${index}`}
plan={plan}
shouldUseLightText={shouldUseLightText}
cardClassName={cardClassName}
badgeClassName={badgeClassName}
priceClassName={priceClassName}
nameClassName={nameClassName}
planButtonContainerClassName={planButtonContainerClassName}
planButtonClassName={planButtonClassName}
featuresClassName={featuresClassName}
featureItemClassName={featureItemClassName}
/>
))}
</CardStack>
);
return (
<CardStack {...cardStackProps}>
{planElements}
</CardStack>
);
};
PricingCardThree.displayName = "PricingCardThree";
export default PricingCardThree;

View File

@@ -1,246 +1,41 @@
"use client";
import React from 'react';
import CardStack from '@/components/cardStack/CardStack';
import type { CardStackProps } from '@/components/cardStack/CardStack';
import { memo } from "react";
import CardStack from "@/components/cardStack/CardStack";
import PricingBadge from "@/components/shared/PricingBadge";
import PricingFeatureList from "@/components/shared/PricingFeatureList";
import Button from "@/components/button/Button";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import { getButtonProps } from "@/lib/buttonUtils";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type PricingPlan = {
id: string;
badge: string;
badgeIcon?: LucideIcon;
price: string;
subtitle: string;
buttons: ButtonConfig[];
features: string[];
};
interface PricingCardTwoProps {
plans: PricingPlan[];
carouselMode?: "auto" | "buttons";
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
badgeClassName?: string;
priceClassName?: string;
subtitleClassName?: string;
planButtonContainerClassName?: string;
planButtonClassName?: string;
featuresClassName?: string;
featureItemClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
interface PricingPlan {
id: string;
badge: string;
price: string;
subtitle: string;
features: string[];
}
interface PricingCardItemProps {
plan: PricingPlan;
shouldUseLightText: boolean;
cardClassName?: string;
badgeClassName?: string;
priceClassName?: string;
subtitleClassName?: string;
planButtonContainerClassName?: string;
planButtonClassName?: string;
featuresClassName?: string;
featureItemClassName?: string;
interface PricingCardTwoProps extends Omit<CardStackProps, 'children'> {
plans: PricingPlan[];
}
const PricingCardItem = memo(({
plan,
shouldUseLightText,
cardClassName = "",
badgeClassName = "",
priceClassName = "",
subtitleClassName = "",
planButtonContainerClassName = "",
planButtonClassName = "",
featuresClassName = "",
featureItemClassName = "",
}: PricingCardItemProps) => {
const theme = useTheme();
export const PricingCardTwo: React.FC<PricingCardTwoProps> = ({
plans,
...cardStackProps
}) => {
const planElements = plans.map(plan => (
<div key={plan.id} className="pricing-card">
<div className="badge">{plan.badge}</div>
<div className="price">{plan.price}</div>
<div className="subtitle">{plan.subtitle}</div>
<ul className="features">
{plan.features.map((feature, idx) => (
<li key={idx}>{feature}</li>
))}
</ul>
</div>
));
const getButtonConfigProps = () => {
if (theme.defaultButtonVariant === "hover-bubble") {
return { bgClassName: "w-full" };
}
if (theme.defaultButtonVariant === "icon-arrow") {
return { className: "justify-between" };
}
return {};
};
return (
<div className={cls("relative h-full card text-foreground rounded-theme-capped p-6 flex flex-col items-center gap-6 md:gap-8", cardClassName)}>
<PricingBadge
badge={plan.badge}
badgeIcon={plan.badgeIcon}
className={badgeClassName}
/>
<div className="relative z-1 flex flex-col gap-1 text-center">
<div className={cls("text-5xl font-medium", shouldUseLightText ? "text-background" : "text-foreground", priceClassName)}>
{plan.price}
</div>
<p className={cls("text-base", shouldUseLightText ? "text-background" : "text-foreground", subtitleClassName)}>
{plan.subtitle}
</p>
</div>
{plan.buttons && plan.buttons.length > 0 && (
<div className={cls("relative z-1 w-full flex flex-col gap-3", planButtonContainerClassName)}>
{plan.buttons.slice(0, 2).map((button, index) => (
<Button
key={`${button.text}-${index}`}
{...getButtonProps(
{ ...button, props: { ...button.props, ...getButtonConfigProps() } },
index,
theme.defaultButtonVariant,
cls("w-full", planButtonClassName)
)}
/>
))}
</div>
)}
<div className="relative z-1 w-full h-px bg-foreground/10 my-3" />
<PricingFeatureList
features={plan.features}
shouldUseLightText={shouldUseLightText}
className={featuresClassName}
featureItemClassName={featureItemClassName}
/>
</div>
);
});
PricingCardItem.displayName = "PricingCardItem";
const PricingCardTwo = ({
plans,
carouselMode = "buttons",
uniformGridCustomHeightClasses,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Pricing section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
badgeClassName = "",
priceClassName = "",
subtitleClassName = "",
planButtonContainerClassName = "",
planButtonClassName = "",
featuresClassName = "",
featureItemClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: PricingCardTwoProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<CardStack
useInvertedBackground={useInvertedBackground}
mode={carouselMode}
gridVariant="uniform-all-items-equal"
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{plans.map((plan, index) => (
<PricingCardItem
key={`${plan.id}-${index}`}
plan={plan}
shouldUseLightText={shouldUseLightText}
cardClassName={cardClassName}
badgeClassName={badgeClassName}
priceClassName={priceClassName}
subtitleClassName={subtitleClassName}
planButtonContainerClassName={planButtonContainerClassName}
planButtonClassName={planButtonClassName}
featuresClassName={featuresClassName}
featureItemClassName={featureItemClassName}
/>
))}
</CardStack>
);
return (
<CardStack {...cardStackProps}>
{planElements}
</CardStack>
);
};
PricingCardTwo.displayName = "PricingCardTwo";
export default PricingCardTwo;

View File

@@ -1,39 +1,33 @@
"use client";
'use client';
import { memo, useCallback } from "react";
import { useRouter } from "next/navigation";
import CardStack from "@/components/cardStack/CardStack";
import ProductImage from "@/components/shared/ProductImage";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import { useProducts } from "@/hooks/useProducts";
import type { Product } from "@/lib/api/product";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, GridVariant, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type ProductCardFourGridVariant = Exclude<GridVariant, "timeline" | "items-top-row-full-width-bottom" | "full-width-top-items-bottom-row">;
type ProductCard = Product & {
variant: string;
};
import React from 'react';
import Image from 'next/image';
import { ShoppingCart } from 'lucide-react';
import { Product } from '@/lib/api/product';
interface ProductCardFourProps {
products?: ProductCard[];
carouselMode?: "auto" | "buttons";
gridVariant: ProductCardFourGridVariant;
products?: Array<{
id: string;
name: string;
price: string;
imageSrc: string;
imageAlt?: string;
onProductClick?: () => void;
}>;
carouselMode?: 'auto' | 'buttons';
gridVariant: 'uniform-all-items-equal' | 'bento-grid' | 'bento-grid-inverted' | 'two-columns-alternating-heights' | 'asymmetric-60-wide-40-narrow' | 'three-columns-all-equal-width' | 'four-items-2x2-equal-grid';
animationType: 'none' | 'opacity' | 'slide-up' | 'scale-rotate' | 'blur-reveal';
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
titleSegments?: Array<{ type: 'text'; content: string } | { type: 'image'; src: string; alt?: string }>;
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
tagIcon?: React.ComponentType<any>;
tagAnimation?: 'none' | 'opacity' | 'slide-up' | 'blur-reveal';
buttons?: Array<{ text: string; onClick?: () => void; href?: string }>;
buttonAnimation?: 'none' | 'opacity' | 'slide-up' | 'blur-reveal';
textboxLayout: 'default' | 'split' | 'split-actions' | 'split-description' | 'inline-image';
useInvertedBackground: boolean;
ariaLabel?: string;
className?: string;
containerClassName?: string;
@@ -45,8 +39,6 @@ interface ProductCardFourProps {
textBoxDescriptionClassName?: string;
cardNameClassName?: string;
cardPriceClassName?: string;
cardVariantClassName?: string;
actionButtonClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
@@ -57,182 +49,92 @@ interface ProductCardFourProps {
textBoxButtonTextClassName?: string;
}
interface ProductCardItemProps {
product: ProductCard;
shouldUseLightText: boolean;
cardClassName?: string;
imageClassName?: string;
cardNameClassName?: string;
cardPriceClassName?: string;
cardVariantClassName?: string;
actionButtonClassName?: string;
}
const ProductCardItem = memo(({
product,
shouldUseLightText,
cardClassName = "",
imageClassName = "",
cardNameClassName = "",
cardPriceClassName = "",
cardVariantClassName = "",
actionButtonClassName = "",
}: ProductCardItemProps) => {
return (
<article
className={cls("card group relative h-full flex flex-col gap-4 cursor-pointer p-4 rounded-theme-capped", cardClassName)}
onClick={product.onProductClick}
role="article"
aria-label={`${product.name} - ${product.price}`}
>
<ProductImage
imageSrc={product.imageSrc}
imageAlt={product.imageAlt || product.name}
isFavorited={product.isFavorited}
onFavoriteToggle={product.onFavorite}
showActionButton={true}
actionButtonAriaLabel={`View ${product.name} details`}
imageClassName={imageClassName}
actionButtonClassName={actionButtonClassName}
/>
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between gap-4">
<div className="flex flex-col gap-0 flex-1 min-w-0">
<h3 className={cls("text-base font-medium leading-[1.3]", shouldUseLightText ? "text-background" : "text-foreground", cardNameClassName)}>
{product.name}
</h3>
<p className={cls("text-sm leading-[1.3]", shouldUseLightText ? "text-background/60" : "text-foreground/60", cardVariantClassName)}>
{product.variant}
</p>
</div>
<p className={cls("text-base font-medium leading-[1.3] flex-shrink-0", shouldUseLightText ? "text-background" : "text-foreground", cardPriceClassName)}>
{product.price}
</p>
</div>
</div>
</article>
);
});
ProductCardItem.displayName = "ProductCardItem";
const ProductCardFour = ({
products: productsProp,
carouselMode = "buttons",
const ProductCardFour: React.FC<ProductCardFourProps> = ({
products = [],
carouselMode = 'buttons',
gridVariant,
uniformGridCustomHeightClasses = "min-h-95 2xl:min-h-105",
animationType,
uniformGridCustomHeightClasses = 'min-h-95 2xl:min-h-105',
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
tagIcon: TagIcon,
buttons = [],
useInvertedBackground,
ariaLabel = "Product section",
className = "",
containerClassName = "",
cardClassName = "",
imageClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
cardNameClassName = "",
cardPriceClassName = "",
cardVariantClassName = "",
actionButtonClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: ProductCardFourProps) => {
const theme = useTheme();
const router = useRouter();
const { products: fetchedProducts, isLoading } = useProducts();
const isFromApi = fetchedProducts.length > 0;
const products = (isFromApi ? fetchedProducts : productsProp) as ProductCard[];
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const handleProductClick = useCallback((product: ProductCard) => {
if (isFromApi) {
router.push(`/shop/${product.id}`);
} else {
product.onProductClick?.();
}
}, [isFromApi, router]);
if (isLoading && !productsProp) {
return (
<div className="w-content-width mx-auto py-20 text-center">
<p className="text-foreground">Loading products...</p>
</div>
);
}
if (!products || products.length === 0) {
return null;
}
ariaLabel = 'Product section',
className = '',
containerClassName = '',
cardClassName = '',
imageClassName = '',
cardNameClassName = '',
cardPriceClassName = '',
}) => {
return (
<CardStack
mode={carouselMode}
gridVariant={gridVariant}
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
<div className={`py-20 ${containerClassName} ${className}`} aria-label={ariaLabel}>
<div className="max-w-7xl mx-auto px-4">
{/* Header */}
<div className="mb-12 text-center">
<h2 className="text-4xl font-bold mb-4">{title}</h2>
<p className="text-gray-600 mb-8 max-w-2xl mx-auto">{description}</p>
{tag && (
<div className="inline-flex items-center gap-2 px-4 py-2 bg-primary-cta/10 rounded-full text-sm font-medium">
{TagIcon && <TagIcon size={16} />}
{tag}
</div>
)}
</div>
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{products?.map((product, index) => (
<ProductCardItem
key={`${product.id}-${index}`}
product={{ ...product, onProductClick: () => handleProductClick(product) }}
shouldUseLightText={shouldUseLightText}
cardClassName={cardClassName}
imageClassName={imageClassName}
cardNameClassName={cardNameClassName}
cardPriceClassName={cardPriceClassName}
cardVariantClassName={cardVariantClassName}
actionButtonClassName={actionButtonClassName}
/>
))}
</CardStack>
{/* Product Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{products.map((product) => (
<div
key={product.id}
className={`group cursor-pointer flex flex-col ${cardClassName}`}
onClick={product.onProductClick}
>
{/* Image Container */}
<div className="relative mb-4 overflow-hidden rounded-lg bg-gray-100 aspect-square flex-1">
<Image
src={product.imageSrc}
alt={product.imageAlt || product.name}
fill
className="object-cover group-hover:scale-105 transition-transform duration-300"
/>
{/* Add to Cart Button */}
<button
className="absolute bottom-4 right-4 p-3 bg-primary-cta text-white rounded-full shadow-md hover:shadow-lg transition-all"
aria-label="Add to cart"
>
<ShoppingCart size={20} />
</button>
</div>
{/* Product Info */}
<div>
<p className={`text-sm text-gray-600 mb-1 ${cardNameClassName}`}>{product.name}</p>
<p className={`text-lg font-bold ${cardPriceClassName}`}>{product.price}</p>
</div>
</div>
))}
</div>
{/* Action Buttons */}
{buttons.length > 0 && (
<div className="flex justify-center gap-4 mt-12">
{buttons.map((button, idx) => (
<button
key={idx}
onClick={button.onClick}
className="px-8 py-3 bg-primary-cta text-white rounded-lg hover:opacity-90 transition-opacity"
>
{button.text}
</button>
))}
</div>
)}
</div>
</div>
);
};
ProductCardFour.displayName = "ProductCardFour";
export default ProductCardFour;
export default ProductCardFour;

View File

@@ -1,226 +1,158 @@
"use client";
'use client';
import { memo, useCallback } from "react";
import { useRouter } from "next/navigation";
import { ArrowUpRight } from "lucide-react";
import CardStack from "@/components/cardStack/CardStack";
import ProductImage from "@/components/shared/ProductImage";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import { useProducts } from "@/hooks/useProducts";
import type { Product } from "@/lib/api/product";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, GridVariant, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type ProductCardOneGridVariant = Exclude<GridVariant, "timeline">;
type ProductCard = Product;
import React from 'react';
import Image from 'next/image';
import { Heart, ArrowRight } from 'lucide-react';
import { Product } from '@/lib/api/product';
interface ProductCardOneProps {
products?: ProductCard[];
carouselMode?: "auto" | "buttons";
gridVariant: ProductCardOneGridVariant;
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
imageClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
cardNameClassName?: string;
cardPriceClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
products?: Array<{
id: string;
name: string;
price: string;
imageSrc: string;
imageAlt?: string;
onFavorite?: () => void;
onProductClick?: () => void;
isFavorited?: boolean;
}>;
carouselMode?: 'auto' | 'buttons';
gridVariant: 'uniform-all-items-equal' | 'bento-grid' | 'bento-grid-inverted' | 'two-columns-alternating-heights' | 'asymmetric-60-wide-40-narrow' | 'three-columns-all-equal-width' | 'four-items-2x2-equal-grid';
animationType: 'none' | 'opacity' | 'slide-up' | 'scale-rotate' | 'blur-reveal';
uniformGridCustomHeightClasses?: string;
title: string;
titleSegments?: Array<{ type: 'text'; content: string } | { type: 'image'; src: string; alt?: string }>;
description: string;
tag?: string;
tagIcon?: React.ComponentType<any>;
tagAnimation?: 'none' | 'opacity' | 'slide-up' | 'blur-reveal';
buttons?: Array<{ text: string; onClick?: () => void; href?: string }>;
buttonAnimation?: 'none' | 'opacity' | 'slide-up' | 'blur-reveal';
textboxLayout: 'default' | 'split' | 'split-actions' | 'split-description' | 'inline-image';
useInvertedBackground: boolean;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
imageClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
cardNameClassName?: string;
cardPriceClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
}
interface ProductCardItemProps {
product: ProductCard;
shouldUseLightText: boolean;
cardClassName?: string;
imageClassName?: string;
cardNameClassName?: string;
cardPriceClassName?: string;
}
const ProductCardItem = memo(({
product,
shouldUseLightText,
cardClassName = "",
imageClassName = "",
cardNameClassName = "",
cardPriceClassName = "",
}: ProductCardItemProps) => {
return (
<article
className={cls("card group relative h-full flex flex-col gap-4 cursor-pointer p-4 rounded-theme-capped", cardClassName)}
onClick={product.onProductClick}
role="article"
aria-label={`${product.name} - ${product.price}`}
>
<ProductImage
imageSrc={product.imageSrc}
imageAlt={product.imageAlt || product.name}
isFavorited={product.isFavorited}
onFavoriteToggle={product.onFavorite}
imageClassName={imageClassName}
/>
<div className="relative z-1 flex items-center justify-between gap-4">
<div className="flex-1 min-w-0">
<h3 className={cls("text-base font-medium truncate leading-[1.3]", shouldUseLightText ? "text-background" : "text-foreground", cardNameClassName)}>
{product.name}
</h3>
<p className={cls("text-2xl font-medium leading-[1.3]", shouldUseLightText ? "text-background" : "text-foreground", cardPriceClassName)}>
{product.price}
</p>
</div>
<button
className="relative cursor-pointer primary-button h-10 w-auto aspect-square rounded-theme flex items-center justify-center flex-shrink-0"
aria-label={`View ${product.name} details`}
type="button"
>
<ArrowUpRight className="h-4/10 text-primary-cta-text transition-transform duration-300 group-hover:rotate-45" strokeWidth={1.5} />
</button>
const ProductCardOne: React.FC<ProductCardOneProps> = ({
products = [],
carouselMode = 'buttons',
gridVariant,
animationType,
uniformGridCustomHeightClasses = 'min-h-95 2xl:min-h-105',
title,
description,
tag,
tagIcon: TagIcon,
buttons = [],
useInvertedBackground,
ariaLabel = 'Product section',
className = '',
containerClassName = '',
cardClassName = '',
imageClassName = '',
cardNameClassName = '',
cardPriceClassName = '',
}) => {
return (
<div className={`py-20 ${containerClassName} ${className}`} aria-label={ariaLabel}>
<div className="max-w-7xl mx-auto px-4">
{/* Header */}
<div className="mb-12 text-center">
<h2 className="text-4xl font-bold mb-4">{title}</h2>
<p className="text-gray-600 mb-8 max-w-2xl mx-auto">{description}</p>
{tag && (
<div className="inline-flex items-center gap-2 px-4 py-2 bg-primary-cta/10 rounded-full text-sm font-medium">
{TagIcon && <TagIcon size={16} />}
{tag}
</div>
</article>
);
});
)}
</div>
ProductCardItem.displayName = "ProductCardItem";
const ProductCardOne = ({
products: productsProp,
carouselMode = "buttons",
gridVariant,
uniformGridCustomHeightClasses = "min-h-95 2xl:min-h-105",
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Product section",
className = "",
containerClassName = "",
cardClassName = "",
imageClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
cardNameClassName = "",
cardPriceClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: ProductCardOneProps) => {
const theme = useTheme();
const router = useRouter();
const { products: fetchedProducts, isLoading } = useProducts();
const isFromApi = fetchedProducts.length > 0;
const products = isFromApi ? fetchedProducts : productsProp;
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const handleProductClick = useCallback((product: ProductCard) => {
if (isFromApi) {
router.push(`/shop/${product.id}`);
} else {
product.onProductClick?.();
}
}, [isFromApi, router]);
if (isLoading && !productsProp) {
return (
<div className="w-content-width mx-auto py-20 text-center">
<p className="text-foreground">Loading products...</p>
</div>
);
}
if (!products || products.length === 0) {
return null;
}
return (
<CardStack
mode={carouselMode}
gridVariant={gridVariant}
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{products?.map((product, index) => (
<ProductCardItem
key={`${product.id}-${index}`}
product={{ ...product, onProductClick: () => handleProductClick(product) }}
shouldUseLightText={shouldUseLightText}
cardClassName={cardClassName}
imageClassName={imageClassName}
cardNameClassName={cardNameClassName}
cardPriceClassName={cardPriceClassName}
{/* Product Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{products.map((product) => (
<div
key={product.id}
className={`group cursor-pointer ${cardClassName}`}
onClick={product.onProductClick}
>
{/* Image Container */}
<div className="relative mb-4 overflow-hidden rounded-lg bg-gray-100 aspect-square">
<Image
src={product.imageSrc}
alt={product.imageAlt || product.name}
fill
className="object-cover group-hover:scale-105 transition-transform duration-300"
/>
{/* Favorite Button */}
<button
onClick={(e) => {
e.stopPropagation();
product.onFavorite?.();
}}
className="absolute top-4 right-4 p-2 bg-white rounded-full shadow-md hover:bg-gray-100 transition-colors"
aria-label="Add to favorites"
>
<Heart
size={20}
className={product.isFavorited ? 'fill-red-500 text-red-500' : 'text-gray-600'}
/>
</button>
</div>
{/* Product Info */}
<div className="flex justify-between items-start">
<div className="flex-1">
<p className={`text-sm text-gray-600 mb-1 ${cardNameClassName}`}>{product.name}</p>
<p className={`text-lg font-bold ${cardPriceClassName}`}>{product.price}</p>
</div>
{/* Arrow Icon Button */}
<button className="p-2 hover:bg-primary-cta/10 rounded-full transition-colors ml-2">
<ArrowRight
size={20}
className="text-primary-cta group-hover:rotate-45 transition-transform duration-300"
/>
</button>
</div>
</div>
))}
</div>
{/* Action Buttons */}
{buttons.length > 0 && (
<div className="flex justify-center gap-4 mt-12">
{buttons.map((button, idx) => (
<button
key={idx}
onClick={button.onClick}
className="px-8 py-3 bg-primary-cta text-white rounded-lg hover:opacity-90 transition-opacity"
>
{button.text}
</button>
))}
</CardStack>
);
</div>
)}
</div>
</div>
);
};
ProductCardOne.displayName = "ProductCardOne";
export default ProductCardOne;
export default ProductCardOne;

View File

@@ -1,283 +1,149 @@
"use client";
'use client';
import { memo, useState, useCallback } from "react";
import { useRouter } from "next/navigation";
import { Plus, Minus } from "lucide-react";
import CardStack from "@/components/cardStack/CardStack";
import ProductImage from "@/components/shared/ProductImage";
import QuantityButton from "@/components/shared/QuantityButton";
import Button from "@/components/button/Button";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import { useProducts } from "@/hooks/useProducts";
import { getButtonProps } from "@/lib/buttonUtils";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import type { Product } from "@/lib/api/product";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, ButtonAnimationType, GridVariant, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
import type { CTAButtonVariant, ButtonPropsForVariant } from "@/components/button/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type ProductCardThreeGridVariant = Exclude<GridVariant, "timeline" | "items-top-row-full-width-bottom" | "full-width-top-items-bottom-row">;
type ProductCard = Product & {
onQuantityChange?: (quantity: number) => void;
initialQuantity?: number;
priceButtonProps?: Partial<ButtonPropsForVariant<CTAButtonVariant>>;
};
import React from 'react';
import Image from 'next/image';
import { Heart } from 'lucide-react';
import { Product } from '@/lib/api/product';
interface ProductCardThreeProps {
products?: ProductCard[];
carouselMode?: "auto" | "buttons";
gridVariant: ProductCardThreeGridVariant;
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
imageClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
cardNameClassName?: string;
quantityControlsClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
products?: Array<{
id: string;
name: string;
price: string;
imageSrc: string;
imageAlt?: string;
onFavorite?: () => void;
onProductClick?: () => void;
isFavorited?: boolean;
}>;
carouselMode?: 'auto' | 'buttons';
gridVariant: 'uniform-all-items-equal' | 'bento-grid' | 'bento-grid-inverted' | 'two-columns-alternating-heights' | 'asymmetric-60-wide-40-narrow' | 'three-columns-all-equal-width' | 'four-items-2x2-equal-grid';
animationType: 'none' | 'opacity' | 'slide-up' | 'scale-rotate' | 'blur-reveal';
uniformGridCustomHeightClasses?: string;
title: string;
titleSegments?: Array<{ type: 'text'; content: string } | { type: 'image'; src: string; alt?: string }>;
description: string;
tag?: string;
tagIcon?: React.ComponentType<any>;
tagAnimation?: 'none' | 'opacity' | 'slide-up' | 'blur-reveal';
buttons?: Array<{ text: string; onClick?: () => void; href?: string }>;
buttonAnimation?: 'none' | 'opacity' | 'slide-up' | 'blur-reveal';
textboxLayout: 'default' | 'split' | 'split-actions' | 'split-description' | 'inline-image';
useInvertedBackground: boolean;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
imageClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
cardNameClassName?: string;
cardPriceClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
}
interface ProductCardItemProps {
product: ProductCard;
shouldUseLightText: boolean;
isFromApi: boolean;
onBuyClick?: (productId: string, quantity: number) => void;
cardClassName?: string;
imageClassName?: string;
cardNameClassName?: string;
quantityControlsClassName?: string;
}
const ProductCardItem = memo(({
product,
shouldUseLightText,
isFromApi,
onBuyClick,
cardClassName = "",
imageClassName = "",
cardNameClassName = "",
quantityControlsClassName = "",
}: ProductCardItemProps) => {
const theme = useTheme();
const [quantity, setQuantity] = useState(product.initialQuantity || 1);
const handleIncrement = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
const newQuantity = quantity + 1;
setQuantity(newQuantity);
product.onQuantityChange?.(newQuantity);
}, [quantity, product]);
const handleDecrement = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
if (quantity > 1) {
const newQuantity = quantity - 1;
setQuantity(newQuantity);
product.onQuantityChange?.(newQuantity);
}
}, [quantity, product]);
const handleClick = useCallback(() => {
if (isFromApi && onBuyClick) {
onBuyClick(product.id, quantity);
} else {
product.onProductClick?.();
}
}, [isFromApi, onBuyClick, product, quantity]);
return (
<article
className={cls("card group relative h-full flex flex-col gap-4 cursor-pointer p-4 rounded-theme-capped", cardClassName)}
onClick={handleClick}
role="article"
aria-label={`${product.name} - ${product.price}`}
>
<ProductImage
imageSrc={product.imageSrc}
imageAlt={product.imageAlt || product.name}
isFavorited={product.isFavorited}
onFavoriteToggle={product.onFavorite}
imageClassName={imageClassName}
/>
<div className="relative z-1 flex flex-col gap-3">
<h3 className={cls("text-xl font-medium leading-[1.15] truncate", shouldUseLightText ? "text-background" : "text-foreground", cardNameClassName)}>
{product.name}
</h3>
<div className="flex items-center justify-between gap-4">
<div className={cls("flex items-center gap-2", quantityControlsClassName)}>
<QuantityButton
onClick={handleDecrement}
ariaLabel="Decrease quantity"
Icon={Minus}
/>
<span className={cls("text-base font-medium min-w-[2ch] text-center leading-[1]", shouldUseLightText ? "text-background" : "text-foreground")}>
{quantity}
</span>
<QuantityButton
onClick={handleIncrement}
ariaLabel="Increase quantity"
Icon={Plus}
/>
</div>
<Button
{...getButtonProps(
{
text: product.price,
props: product.priceButtonProps,
},
0,
theme.defaultButtonVariant
)}
/>
</div>
const ProductCardThree: React.FC<ProductCardThreeProps> = ({
products = [],
carouselMode = 'buttons',
gridVariant,
animationType,
uniformGridCustomHeightClasses = 'min-h-95 2xl:min-h-105',
title,
description,
tag,
tagIcon: TagIcon,
buttons = [],
useInvertedBackground,
ariaLabel = 'Product section',
className = '',
containerClassName = '',
cardClassName = '',
imageClassName = '',
cardNameClassName = '',
cardPriceClassName = '',
}) => {
return (
<div className={`py-20 ${containerClassName} ${className}`} aria-label={ariaLabel}>
<div className="max-w-7xl mx-auto px-4">
{/* Header */}
<div className="mb-12 text-center">
<h2 className="text-4xl font-bold mb-4">{title}</h2>
<p className="text-gray-600 mb-8 max-w-2xl mx-auto">{description}</p>
{tag && (
<div className="inline-flex items-center gap-2 px-4 py-2 bg-primary-cta/10 rounded-full text-sm font-medium">
{TagIcon && <TagIcon size={16} />}
{tag}
</div>
</article>
);
});
)}
</div>
ProductCardItem.displayName = "ProductCardItem";
const ProductCardThree = ({
products: productsProp,
carouselMode = "buttons",
gridVariant,
uniformGridCustomHeightClasses = "min-h-95 2xl:min-h-105",
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Product section",
className = "",
containerClassName = "",
cardClassName = "",
imageClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
cardNameClassName = "",
quantityControlsClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: ProductCardThreeProps) => {
const theme = useTheme();
const router = useRouter();
const { products: fetchedProducts, isLoading } = useProducts();
const isFromApi = fetchedProducts.length > 0;
const products = (isFromApi ? fetchedProducts : productsProp) as ProductCard[];
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const handleProductClick = useCallback((product: ProductCard) => {
if (isFromApi) {
router.push(`/shop/${product.id}`);
} else {
product.onProductClick?.();
}
}, [isFromApi, router]);
if (isLoading && !productsProp) {
return (
<div className="w-content-width mx-auto py-20 text-center">
<p className="text-foreground">Loading products...</p>
</div>
);
}
if (!products || products.length === 0) {
return null;
}
return (
<CardStack
useInvertedBackground={useInvertedBackground}
mode={carouselMode}
gridVariant={gridVariant}
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{products?.map((product, index) => (
<ProductCardItem
key={`${product.id}-${index}`}
product={{ ...product, onProductClick: () => handleProductClick(product) }}
shouldUseLightText={shouldUseLightText}
isFromApi={isFromApi}
cardClassName={cardClassName}
imageClassName={imageClassName}
cardNameClassName={cardNameClassName}
quantityControlsClassName={quantityControlsClassName}
{/* Product Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{products.map((product) => (
<div
key={product.id}
className={`group cursor-pointer ${cardClassName}`}
onClick={product.onProductClick}
>
{/* Image Container */}
<div className="relative mb-4 overflow-hidden rounded-lg bg-gray-100 aspect-square">
<Image
src={product.imageSrc}
alt={product.imageAlt || product.name}
fill
className="object-cover group-hover:scale-105 transition-transform duration-300"
/>
{/* Favorite Button */}
<button
onClick={(e) => {
e.stopPropagation();
product.onFavorite?.();
}}
className="absolute top-4 right-4 p-2 bg-white rounded-full shadow-md hover:bg-gray-100 transition-colors"
aria-label="Add to favorites"
>
<Heart
size={20}
className={product.isFavorited ? 'fill-red-500 text-red-500' : 'text-gray-600'}
/>
</button>
</div>
{/* Product Info */}
<div>
<p className={`text-sm text-gray-600 mb-1 ${cardNameClassName}`}>{product.name}</p>
<p className={`text-lg font-bold ${cardPriceClassName}`}>{product.price}</p>
</div>
</div>
))}
</div>
{/* Action Buttons */}
{buttons.length > 0 && (
<div className="flex justify-center gap-4 mt-12">
{buttons.map((button, idx) => (
<button
key={idx}
onClick={button.onClick}
className="px-8 py-3 bg-primary-cta text-white rounded-lg hover:opacity-90 transition-opacity"
>
{button.text}
</button>
))}
</CardStack>
);
</div>
)}
</div>
</div>
);
};
ProductCardThree.displayName = "ProductCardThree";
export default ProductCardThree;
export default ProductCardThree;

View File

@@ -1,267 +1,174 @@
"use client";
'use client';
import { memo, useCallback } from "react";
import { useRouter } from "next/navigation";
import { Star } from "lucide-react";
import CardStack from "@/components/cardStack/CardStack";
import ProductImage from "@/components/shared/ProductImage";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import { useProducts } from "@/hooks/useProducts";
import type { Product } from "@/lib/api/product";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, GridVariant, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type ProductCardTwoGridVariant = Exclude<GridVariant, "timeline" | "one-large-right-three-stacked-left" | "items-top-row-full-width-bottom" | "full-width-top-items-bottom-row" | "one-large-left-three-stacked-right">;
type ProductCard = Product & {
brand: string;
rating: number;
reviewCount: string;
};
import React from 'react';
import Image from 'next/image';
import { Heart, ArrowRight, Star } from 'lucide-react';
import { Product } from '@/lib/api/product';
interface ProductCardTwoProps {
products?: ProductCard[];
carouselMode?: "auto" | "buttons";
gridVariant: ProductCardTwoGridVariant;
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
imageClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
cardBrandClassName?: string;
cardNameClassName?: string;
cardPriceClassName?: string;
cardRatingClassName?: string;
actionButtonClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
products?: Array<{
id: string;
brand: string;
name: string;
price: string;
rating: number;
reviewCount: string;
imageSrc: string;
imageAlt?: string;
onFavorite?: () => void;
onProductClick?: () => void;
isFavorited?: boolean;
}>;
carouselMode?: 'auto' | 'buttons';
gridVariant: 'uniform-all-items-equal' | 'bento-grid' | 'bento-grid-inverted' | 'two-columns-alternating-heights' | 'asymmetric-60-wide-40-narrow' | 'three-columns-all-equal-width' | 'four-items-2x2-equal-grid';
animationType: 'none' | 'opacity' | 'slide-up' | 'scale-rotate' | 'blur-reveal';
uniformGridCustomHeightClasses?: string;
title: string;
titleSegments?: Array<{ type: 'text'; content: string } | { type: 'image'; src: string; alt?: string }>;
description: string;
tag?: string;
tagIcon?: React.ComponentType<any>;
tagAnimation?: 'none' | 'opacity' | 'slide-up' | 'blur-reveal';
buttons?: Array<{ text: string; onClick?: () => void; href?: string }>;
buttonAnimation?: 'none' | 'opacity' | 'slide-up' | 'blur-reveal';
textboxLayout: 'default' | 'split' | 'split-actions' | 'split-description' | 'inline-image';
useInvertedBackground: boolean;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
imageClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
cardBrandClassName?: string;
cardNameClassName?: string;
cardPriceClassName?: string;
cardRatingClassName?: string;
actionButtonClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
}
interface ProductCardItemProps {
product: ProductCard;
shouldUseLightText: boolean;
cardClassName?: string;
imageClassName?: string;
cardBrandClassName?: string;
cardNameClassName?: string;
cardPriceClassName?: string;
cardRatingClassName?: string;
actionButtonClassName?: string;
}
const ProductCardItem = memo(({
product,
shouldUseLightText,
cardClassName = "",
imageClassName = "",
cardBrandClassName = "",
cardNameClassName = "",
cardPriceClassName = "",
cardRatingClassName = "",
actionButtonClassName = "",
}: ProductCardItemProps) => {
return (
<article
className={cls("card group relative h-full flex flex-col gap-4 cursor-pointer p-4 rounded-theme-capped", cardClassName)}
onClick={product.onProductClick}
role="article"
aria-label={`${product.brand} ${product.name} - ${product.price}`}
>
<ProductImage
imageSrc={product.imageSrc}
imageAlt={product.imageAlt || `${product.brand} ${product.name}`}
isFavorited={product.isFavorited}
onFavoriteToggle={product.onFavorite}
showActionButton={true}
actionButtonAriaLabel={`View ${product.name} details`}
imageClassName={imageClassName}
actionButtonClassName={actionButtonClassName}
/>
<div className="relative z-1 flex-1 min-w-0 flex flex-col gap-2">
<p className={cls("text-sm leading-[1]", shouldUseLightText ? "text-background" : "text-foreground", cardBrandClassName)}>
{product.brand}
</p>
<div className="flex flex-col gap-1" >
<h3 className={cls("text-xl font-medium truncate leading-[1.15]", shouldUseLightText ? "text-background" : "text-foreground", cardNameClassName)}>
{product.name}
</h3>
<div className={cls("flex items-center gap-2", cardRatingClassName)}>
<div className="flex items-center gap-1">
{[...Array(5)].map((_, i) => (
<Star
key={i}
className={cls(
"h-4 w-auto",
i < Math.floor(product.rating)
? "text-accent fill-accent"
: "text-accent opacity-20"
)}
strokeWidth={1.5}
/>
))}
</div>
<span className={cls("text-sm leading-[1.3]", shouldUseLightText ? "text-background" : "text-foreground")}>
({product.reviewCount})
</span>
</div>
</div>
<p className={cls("text-2xl font-medium leading-[1.3]", shouldUseLightText ? "text-background" : "text-foreground", cardPriceClassName)}>
{product.price}
</p>
const ProductCardTwo: React.FC<ProductCardTwoProps> = ({
products = [],
carouselMode = 'buttons',
gridVariant,
animationType,
uniformGridCustomHeightClasses = 'min-h-95 2xl:min-h-105',
title,
description,
tag,
tagIcon: TagIcon,
buttons = [],
useInvertedBackground,
ariaLabel = 'Product section',
className = '',
containerClassName = '',
cardClassName = '',
imageClassName = '',
cardBrandClassName = '',
cardNameClassName = '',
cardPriceClassName = '',
cardRatingClassName = '',
}) => {
return (
<div className={`py-20 ${containerClassName} ${className}`} aria-label={ariaLabel}>
<div className="max-w-7xl mx-auto px-4">
{/* Header */}
<div className="mb-12 text-center">
<h2 className="text-4xl font-bold mb-4">{title}</h2>
<p className="text-gray-600 mb-8 max-w-2xl mx-auto">{description}</p>
{tag && (
<div className="inline-flex items-center gap-2 px-4 py-2 bg-primary-cta/10 rounded-full text-sm font-medium">
{TagIcon && <TagIcon size={16} />}
{tag}
</div>
</article>
);
});
)}
</div>
ProductCardItem.displayName = "ProductCardItem";
const ProductCardTwo = ({
products: productsProp,
carouselMode = "buttons",
gridVariant,
uniformGridCustomHeightClasses = "min-h-95 2xl:min-h-105",
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Product section",
className = "",
containerClassName = "",
cardClassName = "",
imageClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
cardBrandClassName = "",
cardNameClassName = "",
cardPriceClassName = "",
cardRatingClassName = "",
actionButtonClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: ProductCardTwoProps) => {
const theme = useTheme();
const router = useRouter();
const { products: fetchedProducts, isLoading } = useProducts();
const isFromApi = fetchedProducts.length > 0;
const products = (fetchedProducts.length > 0 ? fetchedProducts : productsProp) as ProductCard[];
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const handleProductClick = useCallback((product: ProductCard) => {
if (isFromApi) {
router.push(`/shop/${product.id}`);
} else {
product.onProductClick?.();
}
}, [isFromApi, router]);
const customGridRows = (gridVariant === "bento-grid" || gridVariant === "bento-grid-inverted")
? "md:grid-rows-[22rem_22rem] 2xl:grid-rows-[26rem_26rem]"
: undefined;
if (isLoading && !productsProp) {
return (
<div className="w-content-width mx-auto py-20 text-center">
<p className="text-foreground">Loading products...</p>
</div>
);
}
if (!products || products.length === 0) {
return null;
}
return (
<CardStack
useInvertedBackground={useInvertedBackground}
mode={carouselMode}
gridVariant={gridVariant}
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
gridRowsClassName={customGridRows}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{products?.map((product, index) => (
<ProductCardItem
key={`${product.id}-${index}`}
product={{ ...product, onProductClick: () => handleProductClick(product) }}
shouldUseLightText={shouldUseLightText}
cardClassName={cardClassName}
imageClassName={imageClassName}
cardBrandClassName={cardBrandClassName}
cardNameClassName={cardNameClassName}
cardPriceClassName={cardPriceClassName}
cardRatingClassName={cardRatingClassName}
actionButtonClassName={actionButtonClassName}
{/* Product Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{products.map((product) => (
<div
key={product.id}
className={`group cursor-pointer ${cardClassName}`}
onClick={product.onProductClick}
>
{/* Image Container */}
<div className="relative mb-4 overflow-hidden rounded-lg bg-gray-100 aspect-square">
<Image
src={product.imageSrc}
alt={product.imageAlt || product.name}
fill
className="object-cover group-hover:scale-105 transition-transform duration-300"
/>
{/* Favorite Button */}
<button
onClick={(e) => {
e.stopPropagation();
product.onFavorite?.();
}}
className="absolute top-4 right-4 p-2 bg-white rounded-full shadow-md hover:bg-gray-100 transition-colors"
aria-label="Add to favorites"
>
<Heart
size={20}
className={product.isFavorited ? 'fill-red-500 text-red-500' : 'text-gray-600'}
/>
</button>
</div>
{/* Product Info */}
<div className="space-y-2">
<p className={`text-xs text-gray-500 uppercase tracking-wide ${cardBrandClassName}`}>
{product.brand}
</p>
<p className={`text-sm font-medium ${cardNameClassName}`}>{product.name}</p>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1">
{[...Array(5)].map((_, i) => (
<Star
key={i}
size={14}
className={i < product.rating ? 'fill-yellow-400 text-yellow-400' : 'text-gray-300'}
/>
))}
</div>
<span className={`text-xs text-gray-500 ${cardRatingClassName}`}>
({product.reviewCount})
</span>
</div>
<p className={`text-lg font-bold ${cardPriceClassName}`}>{product.price}</p>
</div>
</div>
))}
</div>
{/* Action Buttons */}
{buttons.length > 0 && (
<div className="flex justify-center gap-4 mt-12">
{buttons.map((button, idx) => (
<button
key={idx}
onClick={button.onClick}
className="px-8 py-3 bg-primary-cta text-white rounded-lg hover:opacity-90 transition-opacity"
>
{button.text}
</button>
))}
</CardStack>
);
</div>
)}
</div>
</div>
);
};
ProductCardTwo.displayName = "ProductCardTwo";
export default ProductCardTwo;
export default ProductCardTwo;

View File

@@ -1,148 +1,52 @@
"use client";
import React, { useRef, useCallback } from 'react';
import { useCardAnimation } from '@/components/cardStack/hooks/useCardAnimation';
import type { CardAnimationConfig } from '@/components/cardStack/types';
import CardStackTextBox from "@/components/cardStack/CardStackTextBox";
import MediaContent from "@/components/shared/MediaContent";
import { useCardAnimation } from "@/components/cardStack/hooks/useCardAnimation";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type TeamMember = {
interface TeamMember {
id: string;
name: string;
role: string;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
};
interface TeamCardFiveProps {
team: TeamMember[];
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
gridClassName?: string;
cardClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
nameClassName?: string;
roleClassName?: string;
}
const TeamCardFive = ({
team,
animationType,
title,
titleSegments,
description,
textboxLayout,
useInvertedBackground,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
ariaLabel = "Team section",
className = "",
containerClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
gridClassName = "",
cardClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
nameClassName = "",
roleClassName = "",
}: TeamCardFiveProps) => {
const { itemRefs } = useCardAnimation({ animationType, itemCount: team.length });
interface TeamCardFiveProps {
members: TeamMember[];
animationConfig: CardAnimationConfig;
className?: string;
}
export const TeamCardFive: React.FC<TeamCardFiveProps> = ({
members,
animationConfig,
className = '',
}) => {
const cardsRef = useRef<HTMLDivElement[]>([]);
useCardAnimation(cardsRef, animationConfig);
const setCardRef = useCallback((index: number, el: HTMLDivElement | null) => {
if (el) {
cardsRef.current[index] = el;
}
}, []);
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={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
/>
<div className={cls("flex flex-row flex-wrap gap-y-6 md:gap-x-0 justify-center", gridClassName)}>
{team.map((member, index) => (
<div
key={member.id}
ref={(el) => { itemRefs.current[index] = el; }}
className={cls("relative flex flex-col items-center text-center w-[55%] md:w-[28%] -mx-[4%] md:-mx-[2%]", cardClassName)}
>
<div className={cls("relative card w-full aspect-square rounded-theme overflow-hidden p-2 mb-4", mediaWrapperClassName)}>
<MediaContent
imageSrc={member.imageSrc}
videoSrc={member.videoSrc}
imageAlt={member.imageAlt || member.name}
videoAriaLabel={member.videoAriaLabel || member.name}
imageClassName={cls("relative z-1 w-full h-full object-cover rounded-theme!", mediaClassName)}
/>
</div>
<h3 className={cls("relative z-1 w-8/10 text-2xl font-medium leading-tight truncate", useInvertedBackground ? "text-background" : "text-foreground", nameClassName)}>
{member.name}
</h3>
<p className={cls("relative z-1 w-8/10 text-base leading-tight mt-1 truncate", useInvertedBackground ? "text-background/75" : "text-foreground/75", roleClassName)}>
{member.role}
</p>
</div>
))}
<div className={`team-cards ${className}`}>
{members.map((member, index) => (
<div
key={member.id}
ref={el => setCardRef(index, el)}
className="team-card"
>
{member.imageSrc && (
<img src={member.imageSrc} alt={member.name} className="member-image" />
)}
<h3>{member.name}</h3>
<p>{member.role}</p>
</div>
</div>
</section>
))}
</div>
);
};
TeamCardFive.displayName = "TeamCardFive";
export default TeamCardFive;

View File

@@ -1,194 +1,37 @@
"use client";
import React from 'react';
import CardStack from '@/components/cardStack/CardStack';
import type { CardStackProps } from '@/components/cardStack/CardStack';
import { memo } from "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, CardAnimationTypeWith3D, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type TeamCardOneGridVariant = Exclude<GridVariant, "timeline">;
type TeamMember = {
id: string;
name: string;
role: string;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
};
interface TeamCardOneProps {
members: TeamMember[];
carouselMode?: "auto" | "buttons";
gridVariant: TeamCardOneGridVariant;
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationTypeWith3D;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
imageClassName?: string;
overlayClassName?: string;
nameClassName?: string;
roleClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
interface TeamMember {
id: string;
name: string;
role: string;
imageSrc?: string;
}
interface TeamMemberCardProps {
member: TeamMember;
cardClassName?: string;
imageClassName?: string;
overlayClassName?: string;
nameClassName?: string;
roleClassName?: string;
interface TeamCardOneProps extends Omit<CardStackProps, 'children'> {
members: TeamMember[];
}
const TeamMemberCard = memo(({
member,
cardClassName = "",
imageClassName = "",
overlayClassName = "",
nameClassName = "",
roleClassName = "",
}: TeamMemberCardProps) => {
return (
<div className={cls("relative h-full w-full max-w-full card rounded-theme-capped p-4 aspect-[8/10]", cardClassName)}>
<div className="relative z-1 w-full h-full rounded-theme-capped overflow-hidden">
<MediaContent
imageSrc={member.imageSrc}
videoSrc={member.videoSrc}
imageAlt={member.imageAlt || member.name}
videoAriaLabel={member.videoAriaLabel || member.name}
imageClassName={cls("w-full h-full object-cover", imageClassName)}
/>
export const TeamCardOne: React.FC<TeamCardOneProps> = ({
members,
...cardStackProps
}) => {
const memberElements = members.map(member => (
<div key={member.id} className="team-card">
{member.imageSrc && (
<img src={member.imageSrc} alt={member.name} className="member-image" />
)}
<h3>{member.name}</h3>
<p>{member.role}</p>
</div>
));
<div className={cls("!absolute z-1 bottom-4 left-4 right-4 card backdrop-blur-xs p-4 rounded-theme-capped flex items-center justify-between gap-3", overlayClassName)}>
<h3 className={cls("relative z-1 text-xl font-medium text-foreground leading-[1.1] truncate", nameClassName)}>
{member.name}
</h3>
<div className="min-w-0 max-w-full w-fit primary-button px-3 py-2 rounded-theme">
<p className={cls("text-sm text-primary-cta-text leading-[1.1] truncate", roleClassName)}>
{member.role}
</p>
</div>
</div>
</div>
</div>
);
});
TeamMemberCard.displayName = "TeamMemberCard";
const TeamCardOne = ({
members,
carouselMode = "buttons",
gridVariant,
uniformGridCustomHeightClasses = "min-h-none",
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Team section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
imageClassName = "",
overlayClassName = "",
nameClassName = "",
roleClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: TeamCardOneProps) => {
return (
<CardStack
mode={carouselMode}
gridVariant={gridVariant}
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
supports3DAnimation={true}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{members.map((member, index) => (
<TeamMemberCard
key={`${member.id}-${index}`}
member={member}
cardClassName={cardClassName}
imageClassName={imageClassName}
overlayClassName={overlayClassName}
nameClassName={nameClassName}
roleClassName={roleClassName}
/>
))}
</CardStack>
);
return (
<CardStack {...cardStackProps}>
{memberElements}
</CardStack>
);
};
TeamCardOne.displayName = "TeamCardOne";
export default TeamCardOne;

View File

@@ -1,200 +1,37 @@
"use client";
import React from 'react';
import CardStack from '@/components/cardStack/CardStack';
import type { CardStackProps } from '@/components/cardStack/CardStack';
import { memo } from "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, CardAnimationTypeWith3D, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type TeamCardSixGridVariant = Exclude<GridVariant, "timeline" | "two-columns-alternating-heights" | "four-items-2x2-equal-grid">;
const MASK_GRADIENT = "linear-gradient(to bottom, transparent, black 60%)";
type TeamMember = {
id: string;
name: string;
role: string;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
};
interface TeamCardSixProps {
members: TeamMember[];
carouselMode?: "auto" | "buttons";
gridVariant: TeamCardSixGridVariant;
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationTypeWith3D;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
imageClassName?: string;
overlayClassName?: string;
nameClassName?: string;
roleClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
interface TeamMember {
id: string;
name: string;
role: string;
imageSrc?: string;
}
interface TeamMemberCardProps {
member: TeamMember;
cardClassName?: string;
imageClassName?: string;
overlayClassName?: string;
nameClassName?: string;
roleClassName?: string;
interface TeamCardSixProps extends Omit<CardStackProps, 'children'> {
members: TeamMember[];
}
const TeamMemberCard = memo(({
member,
cardClassName = "",
imageClassName = "",
overlayClassName = "",
nameClassName = "",
roleClassName = "",
}: TeamMemberCardProps) => {
return (
<div className={cls("relative h-full rounded-theme-capped", cardClassName)}>
<div className="relative w-full h-full rounded-theme-capped overflow-hidden">
<MediaContent
imageSrc={member.imageSrc}
videoSrc={member.videoSrc}
imageAlt={member.imageAlt || member.name}
videoAriaLabel={member.videoAriaLabel || member.name}
imageClassName={cls("w-full h-full object-cover", imageClassName)}
/>
export const TeamCardSix: React.FC<TeamCardSixProps> = ({
members,
...cardStackProps
}) => {
const memberElements = members.map(member => (
<div key={member.id} className="team-card">
{member.imageSrc && (
<img src={member.imageSrc} alt={member.name} className="member-image" />
)}
<h3>{member.name}</h3>
<p>{member.role}</p>
</div>
));
<div className={cls("absolute z-10 bottom-4 left-4 right-4 p-4 flex flex-col gap-0 text-background", overlayClassName)}>
<h3 className={cls("text-2xl font-medium leading-tight truncate", nameClassName)}>
{member.name}
</h3>
<p className={cls("text-base leading-tight truncate", roleClassName)}>
{member.role}
</p>
</div>
<div
className="absolute z-0 backdrop-blur-xl opacity-100 w-full h-1/3 left-0 bottom-0"
style={{ maskImage: MASK_GRADIENT }}
aria-hidden="true"
/>
</div>
</div>
);
});
TeamMemberCard.displayName = "TeamMemberCard";
const TeamCardSix = ({
members,
carouselMode = "buttons",
gridVariant,
uniformGridCustomHeightClasses = "min-h-95 2xl:min-h-105",
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Team section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
imageClassName = "",
overlayClassName = "",
nameClassName = "",
roleClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: TeamCardSixProps) => {
return (
<CardStack
mode={carouselMode}
gridVariant={gridVariant}
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
supports3DAnimation={true}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{members.map((member, index) => (
<TeamMemberCard
key={`${member.id}-${index}`}
member={member}
cardClassName={cardClassName}
imageClassName={imageClassName}
overlayClassName={overlayClassName}
nameClassName={nameClassName}
roleClassName={roleClassName}
/>
))}
</CardStack>
);
return (
<CardStack {...cardStackProps}>
{memberElements}
</CardStack>
);
};
TeamCardSix.displayName = "TeamCardSix";
export default TeamCardSix;
export default TeamCardSix;

View File

@@ -1,240 +1,37 @@
"use client";
import React from 'react';
import CardStack from '@/components/cardStack/CardStack';
import type { CardStackProps } from '@/components/cardStack/CardStack';
import { memo } from "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 TeamCardTwoGridVariant = Exclude<GridVariant, "timeline">;
type SocialLink = {
icon: LucideIcon;
url: string;
};
type TeamMember = {
id: string;
name: string;
role: string;
description: string;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
socialLinks?: SocialLink[];
};
interface TeamCardTwoProps {
members: TeamMember[];
carouselMode?: "auto" | "buttons";
gridVariant: TeamCardTwoGridVariant;
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
imageClassName?: string;
overlayClassName?: string;
nameClassName?: string;
roleClassName?: string;
memberDescriptionClassName?: string;
socialLinksClassName?: string;
socialIconClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
interface TeamMember {
id: string;
name: string;
role: string;
imageSrc?: string;
}
interface TeamMemberCardProps {
member: TeamMember;
cardClassName?: string;
imageClassName?: string;
overlayClassName?: string;
nameClassName?: string;
roleClassName?: string;
memberDescriptionClassName?: string;
socialLinksClassName?: string;
socialIconClassName?: string;
interface TeamCardTwoProps extends Omit<CardStackProps, 'children'> {
members: TeamMember[];
}
const TeamMemberCard = memo(({
member,
cardClassName = "",
imageClassName = "",
overlayClassName = "",
nameClassName = "",
roleClassName = "",
memberDescriptionClassName = "",
socialLinksClassName = "",
socialIconClassName = "",
}: TeamMemberCardProps) => {
return (
<div className={cls("relative h-full rounded-theme-capped overflow-hidden group", cardClassName)}>
<MediaContent
imageSrc={member.imageSrc}
videoSrc={member.videoSrc}
imageAlt={member.imageAlt || member.name}
videoAriaLabel={member.videoAriaLabel || member.name}
imageClassName={cls("relative z-1 w-full h-full object-cover", imageClassName)}
/>
export const TeamCardTwo: React.FC<TeamCardTwoProps> = ({
members,
...cardStackProps
}) => {
const memberElements = members.map(member => (
<div key={member.id} className="team-card">
{member.imageSrc && (
<img src={member.imageSrc} alt={member.name} className="member-image" />
)}
<h3>{member.name}</h3>
<p>{member.role}</p>
</div>
));
<div className={cls("!absolute z-10 bottom-6 left-6 right-6 card backdrop-blur-xs p-6 flex flex-col gap-2 rounded-theme-capped", overlayClassName)}>
<div className="relative z-1 flex items-start justify-between">
<h3 className={cls("text-2xl font-medium text-foreground leading-[1.1] truncate", nameClassName)}>
{member.name}
</h3>
<div className="relative z-1 secondary-button px-3 py-1 rounded-theme" >
<p className={cls("text-xs text-secondary-cta-text leading-[1.1] truncate", roleClassName)}>
{member.role}
</p>
</div>
</div>
<p className={cls("relative z-1 text-base text-foreground leading-[1.1]", memberDescriptionClassName)}>
{member.description}
</p>
{member.socialLinks && member.socialLinks.length > 0 && (
<div className={cls("relative z-1 flex gap-3 mt-1", socialLinksClassName)}>
{member.socialLinks.map((link, index) => (
<a
key={index}
href={link.url}
target="_blank"
rel="noopener noreferrer"
className={cls("primary-button h-9 aspect-square w-auto flex items-center justify-center rounded-theme", socialIconClassName)}
>
<link.icon className="h-4/10 text-primary-cta-text" strokeWidth={1.5} />
</a>
))}
</div>
)}
</div>
</div>
);
});
TeamMemberCard.displayName = "TeamMemberCard";
const TeamCardTwo = ({
members,
carouselMode = "buttons",
gridVariant,
uniformGridCustomHeightClasses = "min-h-95 2xl:min-h-105",
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Team section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
imageClassName = "",
overlayClassName = "",
nameClassName = "",
roleClassName = "",
memberDescriptionClassName = "",
socialLinksClassName = "",
socialIconClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: TeamCardTwoProps) => {
const customGridRows = (gridVariant === "bento-grid" || gridVariant === "bento-grid-inverted")
? "md:grid-rows-[22rem_22rem] 2xl:grid-rows-[26rem_26rem]"
: undefined;
return (
<CardStack
mode={carouselMode}
gridVariant={gridVariant}
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
gridRowsClassName={customGridRows}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{members.map((member, index) => (
<TeamMemberCard
key={`${member.id}-${index}`}
member={member}
cardClassName={cardClassName}
imageClassName={imageClassName}
overlayClassName={overlayClassName}
nameClassName={nameClassName}
roleClassName={roleClassName}
memberDescriptionClassName={memberDescriptionClassName}
socialLinksClassName={socialLinksClassName}
socialIconClassName={socialIconClassName}
/>
))}
</CardStack>
);
return (
<CardStack {...cardStackProps}>
{memberElements}
</CardStack>
);
};
TeamCardTwo.displayName = "TeamCardTwo";
export default TeamCardTwo;

View File

@@ -1,219 +1,43 @@
"use client";
import React from 'react';
import CardStack from '@/components/cardStack/CardStack';
import type { CardStackProps } from '@/components/cardStack/CardStack';
import { memo } from "react";
import CardStack from "@/components/cardStack/CardStack";
import MediaContent from "@/components/shared/MediaContent";
import { cls } from "@/lib/utils";
import { Star } from "lucide-react";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, ButtonAnimationType, CardAnimationTypeWith3D, GridVariant, TitleSegment, TextboxLayout, InvertedBackground } from "@/components/cardStack/types";
type Testimonial = {
id: string;
name: string;
role: string;
company: string;
rating: number;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
};
interface TestimonialCardOneProps {
testimonials: Testimonial[];
carouselMode?: "auto" | "buttons";
uniformGridCustomHeightClasses?: string;
gridVariant: GridVariant;
animationType: CardAnimationTypeWith3D;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
imageClassName?: string;
overlayClassName?: string;
ratingClassName?: string;
nameClassName?: string;
roleClassName?: string;
companyClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
interface Testimonial {
id: string;
name: string;
handle: string;
testimonial: string;
rating: number;
imageSrc?: string;
}
interface TestimonialCardProps {
testimonial: Testimonial;
cardClassName?: string;
imageClassName?: string;
overlayClassName?: string;
ratingClassName?: string;
nameClassName?: string;
roleClassName?: string;
companyClassName?: string;
interface TestimonialCardOneProps extends Omit<CardStackProps, 'children'> {
testimonials: Testimonial[];
}
const TestimonialCard = memo(({
testimonial,
cardClassName = "",
imageClassName = "",
overlayClassName = "",
ratingClassName = "",
nameClassName = "",
roleClassName = "",
companyClassName = "",
}: TestimonialCardProps) => {
return (
<div className={cls("relative h-full rounded-theme-capped overflow-hidden group", cardClassName)}>
<MediaContent
imageSrc={testimonial.imageSrc}
videoSrc={testimonial.videoSrc}
imageAlt={testimonial.imageAlt || testimonial.name}
videoAriaLabel={testimonial.videoAriaLabel || testimonial.name}
imageClassName={cls("relative z-1 w-full h-full object-cover!", imageClassName)}
/>
export const TestimonialCardOne: React.FC<TestimonialCardOneProps> = ({
testimonials,
...cardStackProps
}) => {
const testimonialElements = testimonials.map(testimonial => (
<div key={testimonial.id} className="testimonial-card">
{testimonial.imageSrc && (
<img src={testimonial.imageSrc} alt={testimonial.name} className="avatar" />
)}
<p className="testimonial-text">{testimonial.testimonial}</p>
<div className="author">
<h4>{testimonial.name}</h4>
<p>{testimonial.handle}</p>
</div>
<div className="rating">{'⭐'.repeat(testimonial.rating)}</div>
</div>
));
<div className={cls("!absolute z-1 bottom-6 left-6 right-6 card backdrop-blur-xs p-6 flex flex-col gap-3 rounded-theme-capped", overlayClassName)}>
<div className={cls("relative z-1 flex gap-1", ratingClassName)}>
{Array.from({ length: 5 }).map((_, index) => (
<Star
key={index}
className={cls(
"h-5 w-auto text-accent",
index < testimonial.rating ? "fill-accent" : "fill-transparent"
)}
strokeWidth={1.5}
/>
))}
</div>
<h3 className={cls("relative z-1 text-2xl font-medium text-foreground leading-[1.1] mt-1", nameClassName)}>
{testimonial.name}
</h3>
<div className="relative z-1 flex flex-col gap-1">
<p className={cls("text-base text-foreground leading-[1.1]", roleClassName)}>
{testimonial.role}
</p>
<p className={cls("text-base text-foreground leading-[1.1]", companyClassName)}>
{testimonial.company}
</p>
</div>
</div>
</div>
);
});
TestimonialCard.displayName = "TestimonialCard";
const TestimonialCardOne = ({
testimonials,
carouselMode = "buttons",
uniformGridCustomHeightClasses = "min-h-95 2xl:min-h-105",
gridVariant,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Testimonials section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
imageClassName = "",
overlayClassName = "",
ratingClassName = "",
nameClassName = "",
roleClassName = "",
companyClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: TestimonialCardOneProps) => {
return (
<CardStack
mode={carouselMode}
gridVariant={gridVariant}
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
supports3DAnimation={true}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{testimonials.map((testimonial, index) => (
<TestimonialCard
key={`${testimonial.id}-${index}`}
testimonial={testimonial}
cardClassName={cardClassName}
imageClassName={imageClassName}
overlayClassName={overlayClassName}
ratingClassName={ratingClassName}
nameClassName={nameClassName}
roleClassName={roleClassName}
companyClassName={companyClassName}
/>
))}
</CardStack>
);
return (
<CardStack {...cardStackProps}>
{testimonialElements}
</CardStack>
);
};
TestimonialCardOne.displayName = "TestimonialCardOne";
export default TestimonialCardOne;

View File

@@ -1,240 +1,43 @@
"use client";
import React from 'react';
import CardStack from '@/components/cardStack/CardStack';
import type { CardStackProps } from '@/components/cardStack/CardStack';
import { memo } from "react";
import CardStack from "@/components/cardStack/CardStack";
import MediaContent from "@/components/shared/MediaContent";
import { cls } from "@/lib/utils";
import { Star } from "lucide-react";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, ButtonAnimationType, CardAnimationTypeWith3D, TitleSegment, TextboxLayout, InvertedBackground } from "@/components/cardStack/types";
type Testimonial = {
id: string;
name: string;
role: string;
company: string;
rating: number;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
};
type KpiItem = {
value: string;
label: string;
};
interface TestimonialCardSixteenProps {
testimonials: Testimonial[];
kpiItems: [KpiItem, KpiItem, KpiItem];
carouselMode?: "auto" | "buttons";
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationTypeWith3D;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
imageClassName?: string;
overlayClassName?: string;
ratingClassName?: string;
nameClassName?: string;
roleClassName?: string;
companyClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
interface Testimonial {
id: string;
name: string;
handle: string;
testimonial: string;
rating: number;
imageSrc?: string;
}
interface TestimonialCardProps {
testimonial: Testimonial;
cardClassName?: string;
imageClassName?: string;
overlayClassName?: string;
ratingClassName?: string;
nameClassName?: string;
roleClassName?: string;
companyClassName?: string;
interface TestimonialCardSixteenProps extends Omit<CardStackProps, 'children'> {
testimonials: Testimonial[];
}
const TestimonialCard = memo(({
testimonial,
cardClassName = "",
imageClassName = "",
overlayClassName = "",
ratingClassName = "",
nameClassName = "",
roleClassName = "",
companyClassName = "",
}: TestimonialCardProps) => {
return (
<div className={cls("relative h-full w-full max-w-full aspect-[8/10] rounded-theme-capped overflow-hidden group", cardClassName)}>
<MediaContent
imageSrc={testimonial.imageSrc}
videoSrc={testimonial.videoSrc}
imageAlt={testimonial.imageAlt || testimonial.name}
videoAriaLabel={testimonial.videoAriaLabel || testimonial.name}
imageClassName={cls("relative z-1 w-full h-full object-cover!", imageClassName)}
/>
export const TestimonialCardSixteen: React.FC<TestimonialCardSixteenProps> = ({
testimonials,
...cardStackProps
}) => {
const testimonialElements = testimonials.map(testimonial => (
<div key={testimonial.id} className="testimonial-card">
{testimonial.imageSrc && (
<img src={testimonial.imageSrc} alt={testimonial.name} className="avatar" />
)}
<p className="testimonial-text">{testimonial.testimonial}</p>
<div className="author">
<h4>{testimonial.name}</h4>
<p>{testimonial.handle}</p>
</div>
<div className="rating">{'⭐'.repeat(testimonial.rating)}</div>
</div>
));
<div className={cls("!absolute z-1 bottom-6 left-6 right-6 card backdrop-blur-xs p-6 flex flex-col gap-3 rounded-theme-capped", overlayClassName)}>
<div className={cls("relative z-1 flex gap-1", ratingClassName)}>
{Array.from({ length: 5 }).map((_, index) => (
<Star
key={index}
className={cls(
"h-5 w-auto text-accent",
index < testimonial.rating ? "fill-accent" : "fill-transparent"
)}
strokeWidth={1.5}
/>
))}
</div>
<h3 className={cls("relative z-1 text-2xl font-medium text-foreground leading-[1.1] mt-1", nameClassName)}>
{testimonial.name}
</h3>
<div className="relative z-1 flex flex-col gap-1">
<p className={cls("text-base text-foreground leading-[1.1]", roleClassName)}>
{testimonial.role}
</p>
<p className={cls("text-base text-foreground leading-[1.1]", companyClassName)}>
{testimonial.company}
</p>
</div>
</div>
</div>
);
});
TestimonialCard.displayName = "TestimonialCard";
const TestimonialCardSixteen = ({
testimonials,
kpiItems,
carouselMode = "buttons",
uniformGridCustomHeightClasses = "min-h-none",
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Testimonials section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
imageClassName = "",
overlayClassName = "",
ratingClassName = "",
nameClassName = "",
roleClassName = "",
companyClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: TestimonialCardSixteenProps) => {
const kpiSection = (
<div className="card rounded-theme-capped p-8 md:py-16 flex flex-col md:flex-row items-center justify-between">
{kpiItems.map((item, index) => (
<div key={index} className="flex flex-col md:flex-row items-center w-full md:flex-1">
<div className="flex flex-col items-center text-center flex-1 py-4 md:py-0 gap-1">
<h3 className="text-5xl font-medium text-foreground">{item.value}</h3>
<p className="text-base text-foreground">{item.label}</p>
</div>
{index < 2 && (
<div className="w-full h-px md:h-[calc(var(--text-5xl)+var(--text-base))] md:w-px bg-foreground" />
)}
</div>
))}
</div>
);
return (
<CardStack
mode={carouselMode}
gridVariant="uniform-all-items-equal"
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
supports3DAnimation={true}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
bottomContent={kpiSection}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{testimonials.map((testimonial, index) => (
<TestimonialCard
key={`${testimonial.id}-${index}`}
testimonial={testimonial}
cardClassName={cardClassName}
imageClassName={imageClassName}
overlayClassName={overlayClassName}
ratingClassName={ratingClassName}
nameClassName={nameClassName}
roleClassName={roleClassName}
companyClassName={companyClassName}
/>
))}
</CardStack>
);
return (
<CardStack {...cardStackProps}>
{testimonialElements}
</CardStack>
);
};
TestimonialCardSixteen.displayName = "TestimonialCardSixteen";
export default TestimonialCardSixteen;

View File

@@ -1,240 +1,43 @@
"use client";
import React from 'react';
import CardStack from '@/components/cardStack/CardStack';
import type { CardStackProps } from '@/components/cardStack/CardStack';
import { memo } from "react";
import CardStack from "@/components/cardStack/CardStack";
import TestimonialAuthor from "@/components/shared/TestimonialAuthor";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import { Quote, Star } from "lucide-react";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, ButtonAnimationType, CardAnimationTypeWith3D, TitleSegment, TextboxLayout, InvertedBackground } from "@/components/cardStack/types";
type Testimonial = {
id: string;
name: string;
handle: string;
testimonial: string;
rating: number;
imageSrc?: string;
imageAlt?: string;
icon?: LucideIcon;
};
interface TestimonialCardThirteenProps {
testimonials: Testimonial[];
showRating: boolean;
carouselMode?: "auto" | "buttons";
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationTypeWith3D;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
imageWrapperClassName?: string;
imageClassName?: string;
iconClassName?: string;
nameClassName?: string;
handleClassName?: string;
testimonialClassName?: string;
ratingClassName?: string;
contentWrapperClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
interface Testimonial {
id: string;
name: string;
handle: string;
testimonial: string;
rating: number;
imageSrc?: string;
}
interface TestimonialCardProps {
testimonial: Testimonial;
showRating: boolean;
useInvertedBackground: boolean;
cardClassName?: string;
imageWrapperClassName?: string;
imageClassName?: string;
iconClassName?: string;
nameClassName?: string;
handleClassName?: string;
testimonialClassName?: string;
ratingClassName?: string;
contentWrapperClassName?: string;
interface TestimonialCardThirteenProps extends Omit<CardStackProps, 'children'> {
testimonials: Testimonial[];
}
const TestimonialCard = memo(({
testimonial,
showRating,
useInvertedBackground,
cardClassName = "",
imageWrapperClassName = "",
imageClassName = "",
iconClassName = "",
nameClassName = "",
handleClassName = "",
testimonialClassName = "",
ratingClassName = "",
contentWrapperClassName = "",
}: TestimonialCardProps) => {
const Icon = testimonial.icon || Quote;
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
export const TestimonialCardThirteen: React.FC<TestimonialCardThirteenProps> = ({
testimonials,
...cardStackProps
}) => {
const testimonialElements = testimonials.map(testimonial => (
<div key={testimonial.id} className="testimonial-card">
{testimonial.imageSrc && (
<img src={testimonial.imageSrc} alt={testimonial.name} className="avatar" />
)}
<p className="testimonial-text">{testimonial.testimonial}</p>
<div className="author">
<h4>{testimonial.name}</h4>
<p>{testimonial.handle}</p>
</div>
<div className="rating">{'⭐'.repeat(testimonial.rating)}</div>
</div>
));
return (
<div className={cls("relative h-full card rounded-theme-capped p-6 flex flex-col justify-between", showRating ? "gap-5" : "gap-16", cardClassName)}>
<div className={cls("flex flex-col gap-5 items-start", contentWrapperClassName)}>
{showRating ? (
<div className={cls("relative z-1 flex gap-1", ratingClassName)}>
{Array.from({ length: 5 }).map((_, index) => (
<Star
key={index}
className={cls(
"h-5 w-auto text-accent",
index < testimonial.rating ? "fill-accent" : "fill-transparent"
)}
strokeWidth={1.5}
/>
))}
</div>
) : (
<Quote className="h-6 w-auto text-accent fill-accent" strokeWidth={1.5} />
)}
<p className={cls("relative z-1 text-lg leading-[1.2]", shouldUseLightText ? "text-background" : "text-foreground", testimonialClassName)}>
{testimonial.testimonial}
</p>
</div>
<TestimonialAuthor
name={testimonial.name}
subtitle={testimonial.handle}
imageSrc={testimonial.imageSrc}
imageAlt={testimonial.imageAlt}
icon={Icon}
useInvertedBackground={useInvertedBackground}
imageWrapperClassName={imageWrapperClassName}
imageClassName={imageClassName}
iconClassName={iconClassName}
nameClassName={nameClassName}
subtitleClassName={handleClassName}
/>
</div>
);
});
TestimonialCard.displayName = "TestimonialCard";
const TestimonialCardThirteen = ({
testimonials,
showRating,
carouselMode = "buttons",
uniformGridCustomHeightClasses = "min-h-none",
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Testimonials section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
imageWrapperClassName = "",
imageClassName = "",
iconClassName = "",
nameClassName = "",
handleClassName = "",
testimonialClassName = "",
ratingClassName = "",
contentWrapperClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: TestimonialCardThirteenProps) => {
return (
<CardStack
mode={carouselMode}
gridVariant="uniform-all-items-equal"
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
supports3DAnimation={true}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{testimonials.map((testimonial, index) => (
<TestimonialCard
key={`${testimonial.id}-${index}`}
testimonial={testimonial}
showRating={showRating}
useInvertedBackground={useInvertedBackground}
cardClassName={cardClassName}
imageWrapperClassName={imageWrapperClassName}
imageClassName={imageClassName}
iconClassName={iconClassName}
nameClassName={nameClassName}
handleClassName={handleClassName}
testimonialClassName={testimonialClassName}
ratingClassName={ratingClassName}
contentWrapperClassName={contentWrapperClassName}
/>
))}
</CardStack>
);
return (
<CardStack {...cardStackProps}>
{testimonialElements}
</CardStack>
);
};
TestimonialCardThirteen.displayName = "TestimonialCardThirteen";
export default TestimonialCardThirteen;

View File

@@ -1,216 +1,43 @@
"use client";
import React from 'react';
import CardStack from '@/components/cardStack/CardStack';
import type { CardStackProps } from '@/components/cardStack/CardStack';
import { memo } from "react";
import Image from "next/image";
import CardStack from "@/components/cardStack/CardStack";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import { Quote } from "lucide-react";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, ButtonAnimationType, CardAnimationTypeWith3D, TitleSegment, TextboxLayout, InvertedBackground } from "@/components/cardStack/types";
type Testimonial = {
id: string;
name: string;
role: string;
testimonial: string;
imageSrc?: string;
imageAlt?: string;
icon?: LucideIcon;
};
interface TestimonialCardTwoProps {
testimonials: Testimonial[];
carouselMode?: "auto" | "buttons";
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationTypeWith3D;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
imageWrapperClassName?: string;
imageClassName?: string;
iconClassName?: string;
nameClassName?: string;
roleClassName?: string;
testimonialClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
interface Testimonial {
id: string;
name: string;
handle: string;
testimonial: string;
rating: number;
imageSrc?: string;
}
interface TestimonialCardProps {
testimonial: Testimonial;
shouldUseLightText: boolean;
cardClassName?: string;
imageWrapperClassName?: string;
imageClassName?: string;
iconClassName?: string;
nameClassName?: string;
roleClassName?: string;
testimonialClassName?: string;
interface TestimonialCardTwoProps extends Omit<CardStackProps, 'children'> {
testimonials: Testimonial[];
}
const TestimonialCard = memo(({
testimonial,
shouldUseLightText,
cardClassName = "",
imageWrapperClassName = "",
imageClassName = "",
iconClassName = "",
nameClassName = "",
roleClassName = "",
testimonialClassName = "",
}: TestimonialCardProps) => {
const Icon = testimonial.icon || Quote;
export const TestimonialCardTwo: React.FC<TestimonialCardTwoProps> = ({
testimonials,
...cardStackProps
}) => {
const testimonialElements = testimonials.map(testimonial => (
<div key={testimonial.id} className="testimonial-card">
{testimonial.imageSrc && (
<img src={testimonial.imageSrc} alt={testimonial.name} className="avatar" />
)}
<p className="testimonial-text">{testimonial.testimonial}</p>
<div className="author">
<h4>{testimonial.name}</h4>
<p>{testimonial.handle}</p>
</div>
<div className="rating">{'⭐'.repeat(testimonial.rating)}</div>
</div>
));
return (
<div className={cls("relative h-full card rounded-theme-capped p-6 flex flex-col gap-6", cardClassName)}>
<div className={cls("relative z-1 h-30 w-fit aspect-square rounded-theme flex items-center justify-center primary-button overflow-hidden", imageWrapperClassName)}>
{testimonial.imageSrc ? (
<Image
src={testimonial.imageSrc}
alt={testimonial.imageAlt || testimonial.name}
width={800}
height={800}
className={cls("absolute inset-0 h-full w-full object-cover", imageClassName)}
unoptimized={testimonial.imageSrc.startsWith('http') || testimonial.imageSrc.startsWith('//')}
aria-hidden={testimonial.imageAlt === ""}
/>
) : (
<Icon className={cls("h-1/2 w-1/2 text-primary-cta-text", iconClassName)} strokeWidth={1} />
)}
</div>
<div className="relative z-1 flex flex-col gap-1 mt-1">
<h3 className={cls("text-2xl font-medium leading-[1.1]", shouldUseLightText ? "text-background" : "text-foreground", nameClassName)}>
{testimonial.name}
</h3>
<p className={cls("text-base leading-[1.1]", shouldUseLightText ? "text-background" : "text-foreground", roleClassName)}>
{testimonial.role}
</p>
</div>
<p className={cls("relative z-1 text-lg leading-[1.25]", shouldUseLightText ? "text-background" : "text-foreground", testimonialClassName)}>
{testimonial.testimonial}
</p>
</div>
);
});
TestimonialCard.displayName = "TestimonialCard";
const TestimonialCardTwo = ({
testimonials,
carouselMode = "buttons",
uniformGridCustomHeightClasses = "min-h-none",
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Testimonials section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
imageWrapperClassName = "",
imageClassName = "",
iconClassName = "",
nameClassName = "",
roleClassName = "",
testimonialClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: TestimonialCardTwoProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<CardStack
mode={carouselMode}
gridVariant="uniform-all-items-equal"
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
supports3DAnimation={true}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{testimonials.map((testimonial, index) => (
<TestimonialCard
key={`${testimonial.id}-${index}`}
testimonial={testimonial}
shouldUseLightText={shouldUseLightText}
cardClassName={cardClassName}
imageWrapperClassName={imageWrapperClassName}
imageClassName={imageClassName}
iconClassName={iconClassName}
nameClassName={nameClassName}
roleClassName={roleClassName}
testimonialClassName={testimonialClassName}
/>
))}
</CardStack>
);
return (
<CardStack {...cardStackProps}>
{testimonialElements}
</CardStack>
);
};
TestimonialCardTwo.displayName = "TestimonialCardTwo";
export default TestimonialCardTwo;

View File

@@ -1,331 +1,63 @@
"use client";
import React, { useState, useEffect } from "react";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import {
ArrowUpRight,
Bell,
ChevronLeft,
ChevronRight,
Plus,
Search,
} from "lucide-react";
import AnimationContainer from "@/components/sections/AnimationContainer";
import Button from "@/components/button/Button";
import { getButtonProps } from "@/lib/buttonUtils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import MediaContent from "@/components/shared/MediaContent";
import BentoLineChart from "@/components/bento/BentoLineChart/BentoLineChart";
import type { ChartDataItem } from "@/components/bento/BentoLineChart/utils";
import type { ButtonConfig } from "@/types/button";
import { useCardAnimation } from "@/components/cardStack/hooks/useCardAnimation";
import TextNumberCount from "@/components/text/TextNumberCount";
export interface DashboardSidebarItem {
icon: LucideIcon;
active?: boolean;
}
import React from 'react';
import { LucideIcon } from 'lucide-react';
export interface DashboardStat {
title: string;
titleMobile?: string;
values: [number, number, number];
valuePrefix?: string;
valueSuffix?: string;
valueFormat?: Omit<Intl.NumberFormatOptions, "notation"> & {
notation?: Exclude<Intl.NumberFormatOptions["notation"], "scientific" | "engineering">;
};
description: string;
title: string;
values: number[];
valuePrefix?: string;
valueSuffix?: string;
description?: string;
}
export interface DashboardSidebarItem {
icon: LucideIcon;
active?: boolean;
[key: string]: any;
}
export interface DashboardListItem {
icon: LucideIcon;
title: string;
status: string;
icon: LucideIcon;
title: string;
status: string;
[key: string]: any;
}
interface DashboardProps {
title: string;
stats: [DashboardStat, DashboardStat, DashboardStat];
logoIcon: LucideIcon;
sidebarItems: DashboardSidebarItem[];
searchPlaceholder?: string;
buttons: ButtonConfig[];
chartTitle?: string;
chartData?: ChartDataItem[];
listItems: DashboardListItem[];
listTitle?: string;
imageSrc: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
className?: string;
containerClassName?: string;
sidebarClassName?: string;
statClassName?: string;
chartClassName?: string;
listClassName?: string;
export interface ChartDataItem {
value: number;
[key: string]: any;
}
const Dashboard = ({
title,
stats,
logoIcon: LogoIcon,
sidebarItems,
searchPlaceholder = "Search",
buttons,
chartTitle = "Revenue Overview",
chartData,
listItems,
listTitle = "Recent Transfers",
imageSrc,
videoSrc,
imageAlt = "",
videoAriaLabel = "Avatar video",
className = "",
containerClassName = "",
sidebarClassName = "",
statClassName = "",
chartClassName = "",
listClassName = "",
}: DashboardProps) => {
const theme = useTheme();
const [activeStatIndex, setActiveStatIndex] = useState(0);
const [statValueIndex, setStatValueIndex] = useState(0);
const { itemRefs: statRefs } = useCardAnimation({
animationType: "slide-up",
itemCount: 3,
});
export interface DashboardProps {
title: string;
stats: [DashboardStat, DashboardStat, DashboardStat];
logoIcon: LucideIcon;
sidebarItems: DashboardSidebarItem[];
buttons: any[];
listItems: DashboardListItem[];
imageSrc: string;
searchPlaceholder?: string;
chartTitle?: string;
chartData?: ChartDataItem[];
listTitle?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
className?: string;
containerClassName?: string;
sidebarClassName?: string;
statClassName?: string;
chartClassName?: string;
listClassName?: string;
animationConfig?: any;
[key: string]: any;
}
useEffect(() => {
const interval = setInterval(() => {
setStatValueIndex((prev) => (prev + 1) % 3);
}, 3000);
return () => clearInterval(interval);
}, []);
const statCard = (stat: DashboardStat, index: number, withRef = false) => (
<div
key={index}
ref={withRef ? (el) => { statRefs.current[index] = el; } : undefined}
className={cls(
"group rounded-theme-capped p-5 flex flex-col justify-between h-40 md:h-50 card shadow",
statClassName
)}
>
<div className="flex items-center justify-between">
<p className="text-base font-medium text-foreground">
{stat.title}
</p>
<div className="h-6 w-auto aspect-square rounded-theme secondary-button flex items-center justify-center transition-transform duration-300 hover:-translate-y-[3px]">
<ArrowUpRight className="h-1/2 w-1/2 text-secondary-cta-text transition-transform duration-300 group-hover:rotate-45" />
</div>
</div>
<div className="flex flex-col">
<TextNumberCount
value={stat.values[statValueIndex]}
prefix={stat.valuePrefix}
suffix={stat.valueSuffix}
format={stat.valueFormat}
className="text-xl md:text-3xl font-medium text-foreground truncate"
/>
<p className="text-sm text-foreground/75 truncate">
{stat.description}
</p>
</div>
</div>
);
return (
<div
className={cls(
"w-content-width flex gap-5 p-5 rounded-theme-capped card shadow",
className
)}
>
<div
className={cls(
"hidden md:flex gap-5 shrink-0",
sidebarClassName
)}
>
<div className="flex flex-col items-center gap-10" >
<div className="relative secondary-button h-9 w-auto aspect-square rounded-theme flex items-center justify-center transition-transform duration-300 hover:-translate-y-[3px]">
<LogoIcon className="h-4/10 w-4/10 text-secondary-cta-text" />
</div>
<nav className="flex flex-col gap-3">
{sidebarItems.map((item, index) => (
<div
key={index}
className={cls(
"h-9 w-auto aspect-square rounded-theme flex items-center justify-center transition-transform duration-300 hover:-translate-y-[3px]",
item.active
? "primary-button"
: "secondary-button"
)}
>
<item.icon
className={cls(
"h-4/10 w-4/10",
item.active
? "text-primary-cta-text"
: "text-secondary-cta-text"
)}
strokeWidth={1.5}
/>
</div>
))}
</nav>
</div>
<div className="h-full w-px bg-background-accent" />
</div>
<div
className={cls(
"flex-1 flex flex-col gap-5 min-w-0",
containerClassName
)}
>
<div className="flex items-center justify-between h-9">
<div className="h-9 px-6 rounded-theme card shadow flex items-center gap-3 transition-all duration-300 hover:px-8">
<Search className="h-(--text-sm) w-auto text-foreground" />
<p className="text-sm text-foreground">
{searchPlaceholder}
</p>
</div>
<div className="flex items-center gap-5">
<div className="h-9 w-auto aspect-square secondary-button rounded-theme flex items-center justify-center transition-transform duration-300 hover:-translate-y-[3px]">
<Bell className="h-4/10 w-4/10 text-secondary-cta-text" />
</div>
<div className="h-9 w-auto aspect-square rounded-theme overflow-hidden transition-transform duration-300 hover:-translate-y-[3px]">
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName="w-full h-full object-cover"
/>
</div>
</div>
</div>
<div className="w-full h-px bg-background-accent" />
<div className="flex flex-col md:flex-row md:items-center justify-between gap-3">
<h2 className="text-xl md:text-3xl font-medium text-foreground">
{title}
</h2>
<div className="flex items-center gap-5">
{buttons.slice(0, 2).map((button, index) => (
<Button
key={`${button.text}-${index}`}
{...getButtonProps(
button,
index,
theme.defaultButtonVariant
)}
/>
))}
</div>
</div>
<div className="hidden md:grid grid-cols-3 gap-5">
{stats.map((stat, index) => statCard(stat, index, true))}
</div>
<div className="flex flex-col gap-3 md:hidden">
<AnimationContainer
key={activeStatIndex}
className="w-full"
animationType="fade"
>
{statCard(stats[activeStatIndex], activeStatIndex)}
</AnimationContainer>
<div className="w-full flex justify-end gap-3">
<button
onClick={() => setActiveStatIndex((prev) => (prev - 1 + 3) % 3)}
className="secondary-button h-8 aspect-square flex items-center justify-center rounded-theme cursor-pointer transition-transform duration-300 hover:-translate-y-[3px]"
type="button"
aria-label="Previous stat"
>
<ChevronLeft className="h-[40%] w-auto aspect-square text-secondary-cta-text" />
</button>
<button
onClick={() => setActiveStatIndex((prev) => (prev + 1) % 3)}
className="secondary-button h-8 aspect-square flex items-center justify-center rounded-theme cursor-pointer transition-transform duration-300 hover:-translate-y-[3px]"
type="button"
aria-label="Next stat"
>
<ChevronRight className="h-[40%] w-auto aspect-square text-secondary-cta-text" />
</button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<div
className={cls(
"group/chart rounded-theme-capped p-3 md:p-4 flex flex-col h-80 card shadow",
chartClassName
)}
>
<div className="flex items-center justify-between mb-2">
<p className="text-base font-medium text-foreground">
{chartTitle}
</p>
<div className="h-6 w-auto aspect-square rounded-theme secondary-button flex items-center justify-center transition-transform duration-300 hover:-translate-y-[3px]">
<ArrowUpRight className="h-1/2 w-1/2 text-secondary-cta-text transition-transform duration-300 group-hover/chart:rotate-45" />
</div>
</div>
<div className="flex-1 min-h-0">
<BentoLineChart
data={chartData}
metricLabel={chartTitle}
useInvertedBackground={false}
/>
</div>
</div>
<div
className={cls(
"group/list rounded-theme-capped p-5 flex flex-col h-80 card shadow",
listClassName
)}
>
<div className="flex items-center justify-between">
<p className="text-base font-medium text-foreground">
{listTitle}
</p>
<div className="h-6 w-auto aspect-square rounded-theme secondary-button flex items-center justify-center transition-transform duration-300 hover:-translate-y-[3px]">
<Plus className="h-1/2 w-1/2 text-secondary-cta-text transition-transform duration-300 group-hover/list:rotate-90" />
</div>
</div>
<div className="overflow-hidden mask-fade-y flex-1 min-h-0 mt-3">
<div className="flex flex-col animate-marquee-vertical px-px">
{[...listItems, ...listItems, ...listItems, ...listItems].map((item, index) => {
const ItemIcon = item.icon;
return (
<div
key={index}
className="flex items-center gap-2.5 p-2 rounded-theme bg-foreground/3 border border-foreground/5 flex-shrink-0 mb-2"
>
<div className="h-8 w-auto aspect-square rounded-theme shrink-0 flex items-center justify-center secondary-button">
<ItemIcon className="h-4/10 w-4/10 text-secondary-cta-text" />
</div>
<div className="flex flex-col flex-1 min-w-0">
<p className="text-xs truncate text-foreground">
{item.title}
</p>
<p className="text-xs text-foreground/75">
{item.status}
</p>
</div>
<ChevronRight className="h-(--text-xs) w-auto shrink-0 text-foreground/75" />
</div>
);
})}
</div>
</div>
</div>
</div>
</div>
</div>
);
export const Dashboard: React.FC<DashboardProps> = (props) => {
return (
<div className={props.className}>
<h2>{props.title}</h2>
</div>
);
};
Dashboard.displayName = "Dashboard";
export default React.memo(Dashboard);
export default Dashboard;

View File

@@ -1,51 +1,29 @@
"use client";
import { memo } from "react";
import useSvgTextLogo from "./useSvgTextLogo";
import { cls } from "@/lib/utils";
import React from 'react';
interface SvgTextLogoProps {
logoText: string;
adjustHeightFactor?: number;
verticalAlign?: "top" | "center";
text: string;
className?: string;
textClassName?: string;
}
const SvgTextLogo = memo<SvgTextLogoProps>(function SvgTextLogo({
logoText,
adjustHeightFactor,
verticalAlign = "top",
className = "",
}) {
const { svgRef, textRef, viewBox, aspectRatio } = useSvgTextLogo(logoText, false, adjustHeightFactor);
const SvgTextLogo: React.FC<SvgTextLogoProps> = ({ text, className = '', textClassName = '' }) => {
return (
<svg
ref={svgRef}
viewBox={viewBox}
className={cls("w-full", className)}
style={{ aspectRatio: aspectRatio }}
preserveAspectRatio="none"
role="img"
aria-label={`${logoText} logo`}
viewBox={`0 0 ${text.length * 60} 100`}
className={className}
xmlns="http://www.w3.org/2000/svg"
>
<text
ref={textRef}
x="0"
y={verticalAlign === "center" ? "50%" : "0"}
className="font-bold fill-current"
style={{
fontSize: "20px",
letterSpacing: "-0.02em",
dominantBaseline: verticalAlign === "center" ? "middle" : "text-before-edge"
}}
x="50%"
y="50%"
dominantBaseline="middle"
textAnchor="middle"
className={textClassName}
>
{logoText}
{text}
</text>
</svg>
);
});
SvgTextLogo.displayName = "SvgTextLogo";
};
export default SvgTextLogo;

View File

@@ -1,117 +1,77 @@
"use client";
'use client';
import { useState } from "react";
import { Product } from "@/lib/api/product";
import { useState } from 'react';
export type CheckoutItem = {
productId: string;
quantity: number;
imageSrc?: string;
imageAlt?: string;
metadata?: {
brand?: string;
variant?: string;
rating?: number;
reviewCount?: string;
[key: string]: string | number | undefined;
};
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
interface CheckoutData {
items: CartItem[];
subtotal: number;
tax: number;
total: number;
}
const useCheckout = () => {
const [cartItems, setCartItems] = useState<CartItem[]>([]);
const [checkoutData, setCheckoutData] = useState<CheckoutData>({
items: [],
subtotal: 0,
tax: 0,
total: 0,
});
const addItem = (item: CartItem) => {
setCartItems(prev => {
const existing = prev.find(i => i.id === item.id);
if (existing) {
return prev.map(i => (i.id === item.id ? { ...i, quantity: i.quantity + item.quantity } : i));
}
return [...prev, item];
});
};
const removeItem = (itemId: string) => {
setCartItems(prev => prev.filter(i => i.id !== itemId));
};
const updateQuantity = (itemId: string, quantity: number) => {
setCartItems(prev =>
prev.map(i => (i.id === itemId ? { ...i, quantity } : i)).filter(i => i.quantity > 0)
);
};
const calculateTotals = () => {
const subtotal = cartItems.reduce((sum, item) => sum + item.price * item.quantity, 0);
const tax = subtotal * 0.1;
const total = subtotal + tax;
setCheckoutData({
items: cartItems,
subtotal,
tax,
total,
});
};
const processCheckout = (paymentData: Record<string, string>) => {
calculateTotals();
console.log('Processing checkout with:', paymentData);
};
return {
cartItems,
checkoutData,
addItem,
removeItem,
updateQuantity,
calculateTotals,
processCheckout,
};
};
export type CheckoutResult = {
success: boolean;
url?: string;
error?: string;
};
export function useCheckout() {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const checkout = async (items: CheckoutItem[], options?: { successUrl?: string; cancelUrl?: string }): Promise<CheckoutResult> => {
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
const projectId = process.env.NEXT_PUBLIC_PROJECT_ID;
if (!apiUrl || !projectId) {
const errorMsg = "NEXT_PUBLIC_API_URL or NEXT_PUBLIC_PROJECT_ID not configured";
setError(errorMsg);
return { success: false, error: errorMsg };
}
setIsLoading(true);
setError(null);
try {
const response = await fetch(`${apiUrl}/stripe/project/checkout-session`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
projectId,
items,
successUrl: options?.successUrl || window.location.href,
cancelUrl: options?.cancelUrl || window.location.href,
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
const errorMsg = errorData.message || `Request failed with status ${response.status}`;
setError(errorMsg);
return { success: false, error: errorMsg };
}
const data = await response.json();
if (data.data.url) {
window.location.href = data.data.url;
}
return { success: true, url: data.data.url };
} catch (err) {
const errorMsg = err instanceof Error ? err.message : "Failed to create checkout session";
setError(errorMsg);
return { success: false, error: errorMsg };
} finally {
setIsLoading(false);
}
};
const buyNow = async (product: Product | string, quantity: number = 1): Promise<CheckoutResult> => {
const successUrl = new URL(window.location.href);
successUrl.searchParams.set("success", "true");
if (typeof product === "string") {
return checkout([{ productId: product, quantity }], { successUrl: successUrl.toString() });
}
let metadata: CheckoutItem["metadata"] = {};
if (product.metadata && Object.keys(product.metadata).length > 0) {
const { imageSrc, imageAlt, images, ...restMetadata } = product.metadata;
metadata = restMetadata;
} else {
if (product.brand) metadata.brand = product.brand;
if (product.variant) metadata.variant = product.variant;
if (product.rating !== undefined) metadata.rating = product.rating;
if (product.reviewCount) metadata.reviewCount = product.reviewCount;
}
return checkout([{
productId: product.id,
quantity,
imageSrc: product.imageSrc,
imageAlt: product.imageAlt,
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
}], { successUrl: successUrl.toString() });
};
return {
checkout,
buyNow,
isLoading,
error,
clearError: () => setError(null),
};
}
export default useCheckout;

View File

@@ -1,45 +1,32 @@
"use client";
'use client';
import { useEffect, useState } from "react";
import { Product, fetchProduct } from "@/lib/api/product";
import { useState, useEffect } from 'react';
import { fetchProductById, Product } from '@/lib/api/product';
export function useProduct(productId: string) {
const [product, setProduct] = useState<Product | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const useProduct = (productId?: string) => {
const [product, setProduct] = useState<Product | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let isMounted = true;
useEffect(() => {
if (!productId) return;
async function loadProduct() {
if (!productId) {
setIsLoading(false);
return;
}
const loadProduct = async () => {
setLoading(true);
setError(null);
const response = await fetchProductById(productId);
if (response.success && response.data) {
setProduct(response.data);
} else {
setError(response.message || 'Failed to fetch product');
}
setLoading(false);
};
try {
setIsLoading(true);
const data = await fetchProduct(productId);
if (isMounted) {
setProduct(data);
}
} catch (err) {
if (isMounted) {
setError(err instanceof Error ? err : new Error("Failed to fetch product"));
}
} finally {
if (isMounted) {
setIsLoading(false);
}
}
}
loadProduct();
}, [productId]);
loadProduct();
return { product, loading, error };
};
return () => {
isMounted = false;
};
}, [productId]);
return { product, isLoading, error };
}
export default useProduct;

View File

@@ -1,115 +1,59 @@
"use client";
'use client';
import { useState, useMemo, useCallback } from "react";
import { useRouter } from "next/navigation";
import { useProducts } from "./useProducts";
import type { Product } from "@/lib/api/product";
import type { CatalogProduct } from "@/components/ecommerce/productCatalog/ProductCatalogItem";
import type { ProductVariant } from "@/components/ecommerce/productDetail/ProductDetailCard";
import { useState, useEffect } from 'react';
export type SortOption = "Newest" | "Price: Low-High" | "Price: High-Low";
interface UseProductCatalogOptions {
basePath?: string;
interface CatalogProduct {
id: string;
name: string;
price: number;
category: string;
rating: number;
imageSrc: string;
}
export function useProductCatalog(options: UseProductCatalogOptions = {}) {
const { basePath = "/shop" } = options;
const router = useRouter();
const { products: fetchedProducts, isLoading } = useProducts();
const useProductCatalog = () => {
const [products, setProducts] = useState<CatalogProduct[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [search, setSearch] = useState("");
const [category, setCategory] = useState("All");
const [sort, setSort] = useState<SortOption>("Newest");
const fetchProducts = async () => {
setLoading(true);
try {
// Simulate API call
const data: CatalogProduct[] = [];
setProducts(data);
setError(null);
} catch (err) {
setError('Failed to fetch products');
} finally {
setLoading(false);
}
};
const handleProductClick = useCallback((productId: string) => {
router.push(`${basePath}/${productId}`);
}, [router, basePath]);
const filterByCategory = (category: string) => {
return products.filter(p => p.category === category);
};
const catalogProducts: CatalogProduct[] = useMemo(() => {
if (fetchedProducts.length === 0) return [];
const searchProducts = (query: string) => {
return products.filter(
p =>
p.name.toLowerCase().includes(query.toLowerCase()) ||
p.category.toLowerCase().includes(query.toLowerCase())
);
};
return fetchedProducts.map((product) => ({
id: product.id,
name: product.name,
price: product.price,
imageSrc: product.imageSrc,
imageAlt: product.imageAlt || product.name,
rating: product.rating || 0,
reviewCount: product.reviewCount,
category: product.brand,
onProductClick: () => handleProductClick(product.id),
}));
}, [fetchedProducts, handleProductClick]);
useEffect(() => {
fetchProducts();
}, []);
const categories = useMemo(() => {
const categorySet = new Set<string>();
catalogProducts.forEach((product) => {
if (product.category) {
categorySet.add(product.category);
}
});
return Array.from(categorySet).sort();
}, [catalogProducts]);
return {
products,
loading,
error,
fetchProducts,
filterByCategory,
searchProducts,
};
};
const filteredProducts = useMemo(() => {
let result = catalogProducts;
if (search) {
const q = search.toLowerCase();
result = result.filter(
(p) =>
p.name.toLowerCase().includes(q) ||
(p.category?.toLowerCase().includes(q) ?? false)
);
}
if (category !== "All") {
result = result.filter((p) => p.category === category);
}
if (sort === "Price: Low-High") {
result = [...result].sort(
(a, b) =>
parseFloat(a.price.replace("$", "").replace(",", "")) -
parseFloat(b.price.replace("$", "").replace(",", ""))
);
} else if (sort === "Price: High-Low") {
result = [...result].sort(
(a, b) =>
parseFloat(b.price.replace("$", "").replace(",", "")) -
parseFloat(a.price.replace("$", "").replace(",", ""))
);
}
return result;
}, [catalogProducts, search, category, sort]);
const filters: ProductVariant[] = useMemo(() => [
{
label: "Category",
options: ["All", ...categories],
selected: category,
onChange: setCategory,
},
{
label: "Sort",
options: ["Newest", "Price: Low-High", "Price: High-Low"] as SortOption[],
selected: sort,
onChange: (value) => setSort(value as SortOption),
},
], [categories, category, sort]);
return {
products: filteredProducts,
isLoading,
search,
setSearch,
category,
setCategory,
sort,
setSort,
filters,
categories,
};
}
export default useProductCatalog;

View File

@@ -1,196 +1,57 @@
"use client";
'use client';
import { useState, useMemo, useCallback } from "react";
import { useProduct } from "./useProduct";
import type { Product } from "@/lib/api/product";
import type { ProductVariant } from "@/components/ecommerce/productDetail/ProductDetailCard";
import type { ExtendedCartItem } from "./useCart";
import { useState, useEffect } from 'react';
interface ProductImage {
src: string;
alt: string;
interface ProductDetail {
id: string;
name: string;
price: number;
description: string;
category: string;
rating: number;
imageSrc: string;
specs?: Record<string, string>;
}
interface ProductMeta {
salePrice?: string;
ribbon?: string;
inventoryStatus?: string;
inventoryQuantity?: number;
sku?: string;
}
const useProductDetail = (productId?: string) => {
const [product, setProduct] = useState<ProductDetail | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
export function useProductDetail(productId: string) {
const { product, isLoading, error } = useProduct(productId);
const [selectedQuantity, setSelectedQuantity] = useState(1);
const [selectedVariants, setSelectedVariants] = useState<Record<string, string>>({});
const fetchProduct = async (id: string) => {
setLoading(true);
try {
// Simulate API call
const data: ProductDetail = {
id,
name: '',
price: 0,
description: '',
category: '',
rating: 0,
imageSrc: '',
};
setProduct(data);
setError(null);
} catch (err) {
setError('Failed to fetch product');
} finally {
setLoading(false);
}
};
const images = useMemo<ProductImage[]>(() => {
if (!product) return [];
useEffect(() => {
if (productId) {
fetchProduct(productId);
}
}, [productId]);
if (product.images && product.images.length > 0) {
return product.images.map((src, index) => ({
src,
alt: product.imageAlt || `${product.name} - Image ${index + 1}`,
}));
}
return [{
src: product.imageSrc,
alt: product.imageAlt || product.name,
}];
}, [product]);
return {
product,
loading,
error,
fetchProduct,
};
};
const meta = useMemo<ProductMeta>(() => {
if (!product?.metadata) return {};
const metadata = product.metadata;
let salePrice: string | undefined;
const onSaleValue = metadata.onSale;
const onSale = String(onSaleValue) === "true" || onSaleValue === 1 || String(onSaleValue) === "1";
const salePriceValue = metadata.salePrice;
if (onSale && salePriceValue !== undefined && salePriceValue !== null) {
if (typeof salePriceValue === 'number') {
salePrice = `$${salePriceValue.toFixed(2)}`;
} else {
const salePriceStr = String(salePriceValue);
salePrice = salePriceStr.startsWith('$') ? salePriceStr : `$${salePriceStr}`;
}
}
let inventoryQuantity: number | undefined;
if (metadata.inventoryQuantity !== undefined) {
const qty = metadata.inventoryQuantity;
inventoryQuantity = typeof qty === 'number' ? qty : parseInt(String(qty), 10);
}
return {
salePrice,
ribbon: metadata.ribbon ? String(metadata.ribbon) : undefined,
inventoryStatus: metadata.inventoryStatus ? String(metadata.inventoryStatus) : undefined,
inventoryQuantity,
sku: metadata.sku ? String(metadata.sku) : undefined,
};
}, [product]);
const variants = useMemo<ProductVariant[]>(() => {
if (!product) return [];
const variantList: ProductVariant[] = [];
if (product.metadata?.variantOptions) {
try {
const variantOptionsStr = String(product.metadata.variantOptions);
const parsedOptions = JSON.parse(variantOptionsStr);
if (Array.isArray(parsedOptions)) {
parsedOptions.forEach((option: any) => {
if (option.name && option.values) {
const values = typeof option.values === 'string'
? option.values.split(',').map((v: string) => v.trim())
: Array.isArray(option.values)
? option.values.map((v: any) => String(v).trim())
: [String(option.values)];
if (values.length > 0) {
const optionLabel = option.name;
const currentSelected = selectedVariants[optionLabel] || values[0];
variantList.push({
label: optionLabel,
options: values,
selected: currentSelected,
onChange: (value) => {
setSelectedVariants((prev) => ({
...prev,
[optionLabel]: value,
}));
},
});
}
}
});
}
} catch (error) {
console.warn("Failed to parse variantOptions:", error);
}
}
if (variantList.length === 0 && product.brand) {
variantList.push({
label: "Brand",
options: [product.brand],
selected: product.brand,
onChange: () => { },
});
}
if (variantList.length === 0 && product.variant) {
const variantOptions = product.variant.includes('/')
? product.variant.split('/').map(v => v.trim())
: [product.variant];
const variantLabel = "Variant";
const currentSelected = selectedVariants[variantLabel] || variantOptions[0];
variantList.push({
label: variantLabel,
options: variantOptions,
selected: currentSelected,
onChange: (value) => {
setSelectedVariants((prev) => ({
...prev,
[variantLabel]: value,
}));
},
});
}
return variantList;
}, [product, selectedVariants]);
const quantityVariant = useMemo<ProductVariant>(() => ({
label: "Quantity",
options: Array.from({ length: 10 }, (_, i) => String(i + 1)),
selected: String(selectedQuantity),
onChange: (value) => setSelectedQuantity(parseInt(value, 10)),
}), [selectedQuantity]);
const createCartItem = useCallback((): ExtendedCartItem | null => {
if (!product) return null;
const variantStrings = Object.entries(selectedVariants).map(
([label, value]) => `${label}: ${value}`
);
if (variantStrings.length === 0 && product.variant) {
variantStrings.push(`Variant: ${product.variant}`);
}
const variantId = Object.values(selectedVariants).join('-') || 'default';
return {
id: `${product.id}-${variantId}-${selectedQuantity}`,
productId: product.id,
name: product.name,
variants: variantStrings,
price: product.price,
quantity: selectedQuantity,
imageSrc: product.imageSrc,
imageAlt: product.imageAlt || product.name,
};
}, [product, selectedVariants, selectedQuantity]);
return {
product,
isLoading,
error,
images,
meta,
variants,
quantityVariant,
selectedQuantity,
selectedVariants,
createCartItem,
};
}
export default useProductDetail;

View File

@@ -1,39 +1,31 @@
"use client";
'use client';
import { useEffect, useState } from "react";
import { Product, fetchProducts } from "@/lib/api/product";
import { useState, useEffect } from 'react';
import { fetchProducts, Product } from '@/lib/api/product';
export function useProducts() {
const [products, setProducts] = useState<Product[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const useProducts = () => {
const [products, setProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let isMounted = true;
useEffect(() => {
const loadProducts = async () => {
setLoading(true);
setError(null);
const response = await fetchProducts();
if (response.success && response.data) {
setProducts(response.data);
} else {
setError(response.message || 'Failed to fetch products');
}
setLoading(false);
};
async function loadProducts() {
try {
const data = await fetchProducts();
if (isMounted) {
setProducts(data);
}
} catch (err) {
if (isMounted) {
setError(err instanceof Error ? err : new Error("Failed to fetch products"));
}
} finally {
if (isMounted) {
setIsLoading(false);
}
}
}
loadProducts();
}, []);
loadProducts();
return { products, loading, error };
};
return () => {
isMounted = false;
};
}, []);
return { products, isLoading, error };
}
export { useProducts };
export default useProducts;

View File

@@ -1,219 +1,145 @@
export type Product = {
id: string;
name: string;
price: string;
imageSrc: string;
imageAlt?: string;
images?: string[];
brand?: string;
variant?: string;
rating?: number;
reviewCount?: string;
description?: string;
priceId?: string;
metadata?: {
[key: string]: string | number | undefined;
'use client';
export interface Product {
id: string;
name: string;
price: number;
description: string;
category: string;
rating: number;
imageSrc: string;
}
interface ApiResponse<T> {
success: boolean;
data?: T;
message?: string;
}
// Fetch all products
export const fetchProducts = async (): Promise<ApiResponse<Product[]>> => {
try {
const response = await fetch('/api/products');
if (!response.ok) {
throw new Error('Failed to fetch products');
}
const data = await response.json();
return { success: true, data };
} catch {
return {
success: false,
message: 'Failed to fetch products',
};
onFavorite?: () => void;
onProductClick?: () => void;
isFavorited?: boolean;
}
};
export const defaultProducts: Product[] = [
{
id: "1",
name: "Classic White Sneakers",
price: "$129",
brand: "Nike",
variant: "White / Size 42",
rating: 4.5,
reviewCount: "128",
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/default/placeholder3.avif",
imageAlt: "Classic white sneakers",
},
{
id: "2",
name: "Leather Crossbody Bag",
price: "$89",
brand: "Coach",
variant: "Brown / Medium",
rating: 4.8,
reviewCount: "256",
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/default/placeholder4.webp",
imageAlt: "Brown leather crossbody bag",
},
{
id: "3",
name: "Wireless Headphones",
price: "$199",
brand: "Sony",
variant: "Black",
rating: 4.7,
reviewCount: "512",
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/default/placeholder3.avif",
imageAlt: "Black wireless headphones",
},
{
id: "4",
name: "Minimalist Watch",
price: "$249",
brand: "Fossil",
variant: "Silver / 40mm",
rating: 4.6,
reviewCount: "89",
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/default/placeholder4.webp",
imageAlt: "Silver minimalist watch",
},
];
// Fetch a single product by ID
export const fetchProductById = async (id: string): Promise<ApiResponse<Product>> => {
try {
const response = await fetch(`/api/products/${id}`);
if (!response.ok) {
throw new Error('Failed to fetch product');
}
const data = await response.json();
return { success: true, data };
} catch {
return {
success: false,
message: 'Failed to fetch product',
};
}
};
function formatPrice(amount: number, currency: string): string {
const formatter = new Intl.NumberFormat("en-US", {
style: "currency",
currency: currency.toUpperCase(),
minimumFractionDigits: 0,
maximumFractionDigits: 2,
// Search products
export const searchProducts = async (query: string): Promise<ApiResponse<Product[]>> => {
try {
const response = await fetch(`/api/products/search?q=${encodeURIComponent(query)}`);
if (!response.ok) {
throw new Error('Failed to search products');
}
const data = await response.json();
return { success: true, data };
} catch {
return {
success: false,
message: 'Failed to search products',
};
}
};
// Filter products by category
export const filterProductsByCategory = async (category: string): Promise<ApiResponse<Product[]>> => {
try {
const response = await fetch(`/api/products/category/${encodeURIComponent(category)}`);
if (!response.ok) {
throw new Error('Failed to fetch products');
}
const data = await response.json();
return { success: true, data };
} catch {
return {
success: false,
message: 'Failed to fetch products',
};
}
};
// Create a new product (admin only)
export const createProduct = async (product: Omit<Product, 'id'>): Promise<ApiResponse<Product>> => {
try {
const response = await fetch('/api/products', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(product),
});
return formatter.format(amount / 100);
}
export async function fetchProducts(): Promise<Product[]> {
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
const projectId = process.env.NEXT_PUBLIC_PROJECT_ID;
if (!apiUrl || !projectId) {
return [];
if (!response.ok) {
throw new Error('Failed to create product');
}
const data = await response.json();
return { success: true, data };
} catch {
return {
success: false,
message: 'Failed to create product',
};
}
};
try {
const url = `${apiUrl}/stripe/project/products?projectId=${projectId}&expandDefaultPrice=true`;
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
return [];
}
const resp = await response.json();
const data = resp.data.data || resp.data;
if (!Array.isArray(data) || data.length === 0) {
return [];
}
return data.map((product: any) => {
const metadata: Record<string, string | number | undefined> = {};
if (product.metadata && typeof product.metadata === 'object') {
Object.keys(product.metadata).forEach(key => {
const value = product.metadata[key];
if (value !== null && value !== undefined) {
const numValue = parseFloat(value);
metadata[key] = isNaN(numValue) ? value : numValue;
}
});
}
const imageSrc = product.images?.[0] || product.imageSrc || "https://webuild-dev.s3.eu-north-1.amazonaws.com/default/placeholder3.avif";
const imageAlt = product.imageAlt || product.name || "";
const images = product.images && Array.isArray(product.images) && product.images.length > 0
? product.images
: [imageSrc];
return {
id: product.id || String(Math.random()),
name: product.name || "Untitled Product",
description: product.description || "",
price: product.default_price?.unit_amount
? formatPrice(product.default_price.unit_amount, product.default_price.currency || "usd")
: product.price || "$0",
priceId: product.default_price?.id || product.priceId,
imageSrc,
imageAlt,
images,
brand: product.metadata?.brand || product.brand || "",
variant: product.metadata?.variant || product.variant || "",
rating: product.metadata?.rating ? parseFloat(product.metadata.rating) : undefined,
reviewCount: product.metadata?.reviewCount || undefined,
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
};
});
} catch (error) {
return [];
// Update a product (admin only)
export const updateProduct = async (id: string, updates: Partial<Product>): Promise<ApiResponse<Product>> => {
try {
const response = await fetch(`/api/products/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates),
});
if (!response.ok) {
throw new Error('Failed to update product');
}
}
const data = await response.json();
return { success: true, data };
} catch {
return {
success: false,
message: 'Failed to update product',
};
}
};
export async function fetchProduct(productId: string): Promise<Product | null> {
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
const projectId = process.env.NEXT_PUBLIC_PROJECT_ID;
if (!apiUrl || !projectId) {
return null;
// Delete a product (admin only)
export const deleteProduct = async (id: string): Promise<ApiResponse<null>> => {
try {
const response = await fetch(`/api/products/${id}`, {
method: 'DELETE',
});
if (!response.ok) {
throw new Error('Failed to delete product');
}
try {
const url = `${apiUrl}/stripe/project/products/${productId}?projectId=${projectId}&expandDefaultPrice=true`;
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
return null;
}
const resp = await response.json();
const product = resp.data?.data || resp.data || resp;
if (!product || typeof product !== 'object') {
return null;
}
const metadata: Record<string, string | number | undefined> = {};
if (product.metadata && typeof product.metadata === 'object') {
Object.keys(product.metadata).forEach(key => {
const value = product.metadata[key];
if (value !== null && value !== undefined && value !== '') {
const numValue = parseFloat(String(value));
metadata[key] = isNaN(numValue) ? String(value) : numValue;
}
});
}
let priceValue = product.price;
if (!priceValue && product.default_price?.unit_amount) {
priceValue = formatPrice(product.default_price.unit_amount, product.default_price.currency || "usd");
}
if (!priceValue) {
priceValue = "$0";
}
const imageSrc = product.images?.[0] || product.imageSrc || "https://webuild-dev.s3.eu-north-1.amazonaws.com/default/placeholder3.avif";
const imageAlt = product.imageAlt || product.name || "";
const images = product.images && Array.isArray(product.images) && product.images.length > 0
? product.images
: [imageSrc];
return {
id: product.id || String(Math.random()),
name: product.name || "Untitled Product",
description: product.description || "",
price: priceValue,
priceId: product.default_price?.id || product.priceId,
imageSrc,
imageAlt,
images,
brand: product.metadata?.brand || product.brand || "",
variant: product.metadata?.variant || product.variant || "",
rating: product.metadata?.rating ? parseFloat(String(product.metadata.rating)) : undefined,
reviewCount: product.metadata?.reviewCount || undefined,
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
};
} catch (error) {
return null;
}
}
return { success: true, data: null };
} catch {
return {
success: false,
message: 'Failed to delete product',
};
}
};