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"; "use client";
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider"; import { ThemeProvider } from '@/providers/themeProvider/ThemeProvider';
import NavbarStyleFullscreen from "@/components/navbar/NavbarStyleFullscreen/NavbarStyleFullscreen"; 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";
export default function ElectronicsPage() { export default function ElectronicsPage() {
return ( return (
<ThemeProvider <ThemeProvider
defaultButtonVariant="hover-magnetic" defaultButtonVariant="text-stagger"
defaultTextAnimation="reveal-blur" defaultTextAnimation="entrance-slide"
borderRadius="rounded" borderRadius="rounded"
contentWidth="smallMedium" contentWidth="medium"
sizing="mediumLargeSizeLargeTitles" sizing="medium"
background="floatingGradient" background="circleGradient"
cardStyle="glass-depth" cardStyle="glass-elevated"
primaryButtonStyle="double-inset" primaryButtonStyle="gradient"
secondaryButtonStyle="radial-glow" secondaryButtonStyle="glass"
headingFontWeight="extrabold" headingFontWeight="normal"
> >
<div id="nav" data-section="nav"> <div id="nav" data-section="nav">
<NavbarStyleFullscreen <NavbarStyleFullscreen
brandName="ZSMX Store"
navItems={[ navItems={[
{ name: "Home", id: "/" },
{ name: "Fashion", id: "fashion" }, { name: "Fashion", id: "fashion" },
{ name: "Home", id: "home-category" }, { name: "Home & Decor", id: "home-category" },
{ name: "Gym", id: "gym" }, { name: "Gym", id: "gym" },
{ name: "Electronics", id: "electronics" }, { name: "Electronics", id: "electronics" },
{ name: "Contact", id: "contact" },
]} ]}
brandName="ZSMX Store"
bottomLeftText="Premium Multi-Category Store" bottomLeftText="Premium Multi-Category Store"
bottomRightText="hello@zsmxstore.com" bottomRightText="hello@zsmxstore.com"
/> />
</div> </div>
<div>Electronics Page</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>
</ThemeProvider> </ThemeProvider>
); );
} }

View File

@@ -1,248 +1,37 @@
"use client"; "use client";
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider"; import { ThemeProvider } from '@/providers/themeProvider/ThemeProvider';
import NavbarStyleFullscreen from "@/components/navbar/NavbarStyleFullscreen/NavbarStyleFullscreen"; 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";
export default function FashionPage() { export default function FashionPage() {
return ( return (
<ThemeProvider <ThemeProvider
defaultButtonVariant="hover-magnetic" defaultButtonVariant="text-stagger"
defaultTextAnimation="reveal-blur" defaultTextAnimation="entrance-slide"
borderRadius="rounded" borderRadius="rounded"
contentWidth="smallMedium" contentWidth="medium"
sizing="mediumLargeSizeLargeTitles" sizing="medium"
background="floatingGradient" background="circleGradient"
cardStyle="glass-depth" cardStyle="glass-elevated"
primaryButtonStyle="double-inset" primaryButtonStyle="gradient"
secondaryButtonStyle="radial-glow" secondaryButtonStyle="glass"
headingFontWeight="extrabold" headingFontWeight="normal"
> >
<div id="nav" data-section="nav"> <div id="nav" data-section="nav">
<NavbarStyleFullscreen <NavbarStyleFullscreen
brandName="ZSMX Store"
navItems={[ navItems={[
{ name: "Home", id: "/" },
{ name: "Fashion", id: "fashion" }, { name: "Fashion", id: "fashion" },
{ name: "Home", id: "home-category" }, { name: "Home & Decor", id: "home-category" },
{ name: "Gym", id: "gym" }, { name: "Gym", id: "gym" },
{ name: "Electronics", id: "electronics" }, { name: "Electronics", id: "electronics" },
{ name: "Contact", id: "contact" },
]} ]}
brandName="ZSMX Store"
bottomLeftText="Premium Multi-Category Store" bottomLeftText="Premium Multi-Category Store"
bottomRightText="hello@zsmxstore.com" bottomRightText="hello@zsmxstore.com"
/> />
</div> </div>
<div>Fashion Page</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>
</ThemeProvider> </ThemeProvider>
); );
} }

View File

@@ -26,11 +26,11 @@ export default function GymPage() {
<div id="nav" data-section="nav"> <div id="nav" data-section="nav">
<NavbarStyleFullscreen <NavbarStyleFullscreen
navItems={[ navItems={[
{ name: "Home", id: "/" },
{ name: "Fashion", id: "fashion" }, { name: "Fashion", id: "fashion" },
{ name: "Home", id: "home-category" }, { name: "Home & Decor", id: "home-category" },
{ name: "Gym", id: "gym" }, { name: "Gym", id: "gym" },
{ name: "Electronics", id: "electronics" }, { name: "Electronics", id: "electronics" },
{ name: "Contact", id: "contact" },
]} ]}
brandName="ZSMX Store" brandName="ZSMX Store"
bottomLeftText="Premium Multi-Category Store" bottomLeftText="Premium Multi-Category Store"
@@ -52,65 +52,23 @@ export default function GymPage() {
useInvertedBackground={false} useInvertedBackground={false}
products={[ products={[
{ {
id: "gym-1", id: "gym-1", brand: "FitnessPro", name: "Professional Dumbbell Set", price: "$599.00", rating: 5,
brand: "FitnessPro", 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"},
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", id: "gym-2", brand: "SportsTech", name: "High-Performance Treadmill", price: "$1,199.00", rating: 5,
brand: "SportsTech", reviewCount: "342", imageSrc: "http://img.b2bpic.net/free-vector/sport-landing-page-template-with-photo_23-2148217108.jpg?_wi=1", imageAlt: "High-Performance Treadmill"},
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", id: "gym-3", brand: "EliteGym", name: "Commercial Weight Bench", price: "$450.00", rating: 4,
brand: "EliteGym", reviewCount: "198", imageSrc: "http://img.b2bpic.net/free-photo/perfectly-ordered-compositions-view_23-2149872090.jpg?_wi=1", imageAlt: "Commercial Weight Bench"},
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", id: "gym-4", brand: "PowerTech", name: "Adjustable Kettlebell Set", price: "$349.00", rating: 5,
brand: "PowerTech", 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"},
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", id: "gym-5", brand: "FitGear", name: "Premium Yoga Mat Collection", price: "$89.00", rating: 5,
brand: "FitGear", 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"},
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", id: "gym-6", brand: "ResistancePro", name: "Resistance Band Set", price: "$79.00", rating: 4,
brand: "ResistancePro", reviewCount: "403", imageSrc: "http://img.b2bpic.net/free-photo/perfectly-ordered-compositions-view_23-2149872090.jpg?_wi=2", imageAlt: "Resistance Band Set"},
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" }]} buttons={[{ text: "View All Gym Gear", href: "/gym" }]}
/> />
@@ -121,12 +79,8 @@ export default function GymPage() {
<FeatureCardTen <FeatureCardTen
features={[ features={[
{ {
id: "1", id: "1", title: "Strength Training", description: "Premium dumbbells, barbells, and weight plates. Built for durability and precision in every lift.", media: {
title: "Strength Training", imageSrc: "http://img.b2bpic.net/free-photo/still-life-perfectly-ordered-fitness-gym-accessories_52683-100705.jpg?_wi=5"},
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: [ items: [
{ icon: Dumbbell, text: "Professional Quality" }, { icon: Dumbbell, text: "Professional Quality" },
{ icon: Zap, text: "High Performance" }, { icon: Zap, text: "High Performance" },
@@ -135,12 +89,8 @@ export default function GymPage() {
reverse: false, reverse: false,
}, },
{ {
id: "2", id: "2", title: "Cardio Equipment", description: "State-of-the-art treadmills, bikes, and rowing machines. Engineered for smooth, efficient workouts.", media: {
title: "Cardio Equipment", imageSrc: "http://img.b2bpic.net/free-vector/sport-landing-page-template-with-photo_23-2148217108.jpg?_wi=3"},
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: [ items: [
{ icon: Activity, text: "Advanced Technology" }, { icon: Activity, text: "Advanced Technology" },
{ icon: Zap, text: "High Durability" }, { icon: Zap, text: "High Durability" },
@@ -149,12 +99,8 @@ export default function GymPage() {
reverse: true, reverse: true,
}, },
{ {
id: "3", id: "3", title: "Functional Training", description: "Kettlebells, resistance bands, and functional rigs. Perfect for dynamic, full-body workouts.", media: {
title: "Functional Training", imageSrc: "http://img.b2bpic.net/free-photo/perfectly-ordered-compositions-view_23-2149872090.jpg?_wi=3"},
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: [ items: [
{ icon: Zap, text: "Versatile Equipment" }, { icon: Zap, text: "Versatile Equipment" },
{ icon: Activity, text: "Space-Saving Design" }, { icon: Activity, text: "Space-Saving Design" },
@@ -186,35 +132,17 @@ export default function GymPage() {
useInvertedBackground={false} useInvertedBackground={false}
faqs={[ faqs={[
{ {
id: "1", 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."},
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", 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."},
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", 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."},
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", 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."},
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", 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."},
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", 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."},
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" imageSrc="http://img.b2bpic.net/free-photo/woman-sitting-wheelchair-modern-concept_23-2148497283.jpg?_wi=2"
imageAlt="Customer service support team" imageAlt="Customer service support team"
@@ -232,8 +160,7 @@ export default function GymPage() {
copyrightText="© 2025 ZSMX Store. All rights reserved." copyrightText="© 2025 ZSMX Store. All rights reserved."
columns={[ columns={[
{ {
title: "Shop", title: "Shop", items: [
items: [
{ label: "Fashion", href: "fashion" }, { label: "Fashion", href: "fashion" },
{ label: "Home", href: "home-category" }, { label: "Home", href: "home-category" },
{ label: "Gym", href: "gym" }, { label: "Gym", href: "gym" },
@@ -241,8 +168,7 @@ export default function GymPage() {
], ],
}, },
{ {
title: "Support", title: "Support", items: [
items: [
{ label: "Contact Us", href: "#contact" }, { label: "Contact Us", href: "#contact" },
{ label: "FAQ", href: "#faq" }, { label: "FAQ", href: "#faq" },
{ label: "Shipping Info", href: "#" }, { label: "Shipping Info", href: "#" },
@@ -250,8 +176,7 @@ export default function GymPage() {
], ],
}, },
{ {
title: "Company", title: "Company", items: [
items: [
{ label: "About Us", href: "#about" }, { label: "About Us", href: "#about" },
{ label: "Blog", href: "#" }, { label: "Blog", href: "#" },
{ label: "Careers", href: "#" }, { label: "Careers", href: "#" },

View File

@@ -1,381 +1,37 @@
"use client"; "use client";
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider"; import { ThemeProvider } from '@/providers/themeProvider/ThemeProvider';
import NavbarStyleFullscreen from "@/components/navbar/NavbarStyleFullscreen/NavbarStyleFullscreen"; 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";
export default function HomeCategoryPage() { export default function HomePage() {
return ( return (
<ThemeProvider <ThemeProvider
defaultButtonVariant="hover-magnetic" defaultButtonVariant="text-stagger"
defaultTextAnimation="reveal-blur" defaultTextAnimation="entrance-slide"
borderRadius="rounded" borderRadius="rounded"
contentWidth="smallMedium" contentWidth="medium"
sizing="mediumLargeSizeLargeTitles" sizing="medium"
background="floatingGradient" background="circleGradient"
cardStyle="glass-depth" cardStyle="glass-elevated"
primaryButtonStyle="double-inset" primaryButtonStyle="gradient"
secondaryButtonStyle="radial-glow" secondaryButtonStyle="glass"
headingFontWeight="extrabold" headingFontWeight="normal"
> >
{/* Navbar */}
<div id="nav" data-section="nav"> <div id="nav" data-section="nav">
<NavbarStyleFullscreen <NavbarStyleFullscreen
navItems={[ navItems={[
{ name: "Home", id: "/" },
{ name: "Fashion", id: "fashion" }, { name: "Fashion", id: "fashion" },
{ name: "Home", id: "home-category" }, { name: "Home & Decor", id: "home-category" },
{ name: "Gym", id: "gym" }, { name: "Gym", id: "gym" },
{ name: "Electronics", id: "electronics" }, { name: "Electronics", id: "electronics" },
{ name: "Contact", id: "contact" },
]} ]}
brandName="ZSMX Store" brandName="ZSMX Store"
bottomLeftText="Premium Multi-Category Store" bottomLeftText="Premium Multi-Category Store"
bottomRightText="hello@zsmxstore.com" bottomRightText="hello@zsmxstore.com"
/> />
</div> </div>
<div>Home Page</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>
</ThemeProvider> </ThemeProvider>
); );
} }

View File

@@ -1,359 +1,195 @@
"use client"; "use client";
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider"; import React from 'react';
import NavbarStyleFullscreen from "@/components/navbar/NavbarStyleFullscreen/NavbarStyleFullscreen"; import { ThemeProvider } from '@/providers/themeProvider/ThemeProvider';
import HeroBillboardTestimonial from "@/components/sections/hero/HeroBillboardTestimonial"; import NavbarStyleFullscreen from '@/components/navbar/NavbarStyleFullscreen/NavbarStyleFullscreen';
import ProductCardTwo from "@/components/sections/product/ProductCardTwo"; import HeroBillboardTestimonial from '@/components/sections/hero/HeroBillboardTestimonial';
import FeatureCardTen from "@/components/sections/feature/FeatureCardTen"; import ProductCardTwo from '@/components/sections/product/ProductCardTwo';
import MetricSplitMediaAbout from "@/components/sections/about/MetricSplitMediaAbout"; import FeatureCardTen from '@/components/sections/feature/FeatureCardTen';
import MetricCardSeven from "@/components/sections/metrics/MetricCardSeven"; import MetricSplitMediaAbout from '@/components/sections/about/MetricSplitMediaAbout';
import SocialProofOne from "@/components/sections/socialProof/SocialProofOne"; import MetricCardSeven from '@/components/sections/metrics/MetricCardSeven';
import TestimonialCardFifteen from "@/components/sections/testimonial/TestimonialCardFifteen"; import SocialProofOne from '@/components/sections/socialProof/SocialProofOne';
import FaqSplitMedia from "@/components/sections/faq/FaqSplitMedia"; import TestimonialCardFifteen from '@/components/sections/testimonial/TestimonialCardFifteen';
import ContactCTA from "@/components/sections/contact/ContactCTA"; import FaqSplitMedia from '@/components/sections/faq/FaqSplitMedia';
import FooterBase from "@/components/sections/footer/FooterBase"; import ContactCTA from '@/components/sections/contact/ContactCTA';
import Link from "next/link"; import FooterBase from '@/components/sections/footer/FooterBase';
import { Sparkles, Star, Grid, Award, TrendingUp, Briefcase, Mail, HelpCircle, Shirt, Heart, Home, Sofa, Layout, Dumbbell, Activity, Zap, Smartphone, Cpu } from "lucide-react"; 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 ( return (
<ThemeProvider <ThemeProvider
defaultButtonVariant="hover-magnetic" defaultButtonVariant={plan.theme.defaultButtonVariant}
defaultTextAnimation="reveal-blur" defaultTextAnimation={plan.theme.defaultTextAnimation}
borderRadius="rounded" borderRadius={plan.theme.borderRadius}
contentWidth="smallMedium" contentWidth={plan.theme.contentWidth}
sizing="mediumLargeSizeLargeTitles" sizing={plan.theme.sizing}
background="floatingGradient" background={plan.theme.background}
cardStyle="glass-depth" cardStyle={plan.theme.cardStyle}
primaryButtonStyle="double-inset" primaryButtonStyle={plan.theme.primaryButtonStyle}
secondaryButtonStyle="radial-glow" secondaryButtonStyle={plan.theme.secondaryButtonStyle}
headingFontWeight="extrabold" headingFontWeight={plan.theme.headingFontWeight}
> >
<div id="nav" data-section="nav"> <div id="nav" data-section="nav">
<NavbarStyleFullscreen <NavbarStyleFullscreen navItems={navItems} />
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"
/>
</div> </div>
<div id="hero" data-section="hero"> <div id="hero" data-section="hero">
<HeroBillboardTestimonial <HeroBillboardTestimonial
title="Discover Your Perfect Style" background={{ variant: "radial-gradient" }}
description="Explore our curated collection of fashion, home decor, fitness equipment, and premium electronics. Where quality meets elegance." tag="Testimonials"
tag="Welcome to ZSMX Store"
tagIcon={Sparkles} tagIcon={Sparkles}
tagAnimation="slide-up" title="What Our Customers Say"
background={{ variant: "floatingGradient" }} description="Hear from our satisfied clients about their experience with our products and services."
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"
testimonials={[ testimonials={[
{ {
name: "Sarah Mitchell", name: "Sarah Johnson", handle: "@sarahj", testimonial: "Amazing product that transformed our workflow!", rating: 5,
handle: "Fashion Enthusiast", imageSrc: "/placeholders/placeholder1.webp"},
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: "James Chen", name: "John Doe", handle: "@johndoe", testimonial: "Great support and excellent service. Highly recommended!", rating: 5,
handle: "Interior Designer", imageSrc: "/placeholders/placeholder2.webp"},
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",
},
]} ]}
testimonialRotationInterval={5000}
buttons={[ buttons={[
{ text: "Shop Now", href: "/fashion" }, { text: "Get Started", href: "/" },
{ text: "Explore Categories", href: "#categories" }, { text: "Learn More", href: "categories" },
]} ]}
buttonAnimation="slide-up"
useInvertedBackground={false} useInvertedBackground={false}
/> />
</div> </div>
<div id="products" data-section="products"> <div id="products" data-section="products">
<ProductCardTwo <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={[ products={[
{ {
id: "fashion-1", id: "1", brand: "Premium", name: "Eclipse Motion Pro", price: "$150", rating: 5,
brand: "LuxeStyle", reviewCount: "128", imageSrc: "/placeholders/placeholder1.webp"},
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: "fashion-2", id: "2", brand: "Standard", name: "Wave Dynamics", price: "$99", rating: 4,
brand: "ElegantWear", reviewCount: "95", imageSrc: "/placeholders/placeholder2.webp"},
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: "fashion-3", id: "3", brand: "Elite", name: "Aurora Series", price: "$199", rating: 5,
brand: "ClassicThreads", reviewCount: "156", imageSrc: "/placeholders/placeholder3.webp"},
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",
},
]} ]}
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>
<div id="categories" data-section="categories"> <div id="categories" data-section="categories">
<FeatureCardTen <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={[ features={[
{ {
id: "1", id: "1", title: "Fast Performance", description: "Lightning-fast speeds optimized for your workflow", media: { imageSrc: "/placeholders/placeholder1.webp", imageAlt: "Performance" },
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",
},
items: [ items: [
{ icon: Shirt, text: "Designer Collections" }, { icon: TrendingUp, text: "10x faster processing" },
{ icon: Sparkles, text: "Premium Fabrics" }, { icon: Users, text: "Real-time collaboration" },
{ icon: Heart, text: "Timeless Styles" },
], ],
reverse: false, reverse: false,
}, },
{ {
id: "2", id: "2", title: "Scalable Solutions", description: "Grow your business without limitations", media: { imageSrc: "/placeholders/placeholder2.webp", imageAlt: "Scalability" },
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",
},
items: [ items: [
{ icon: Home, text: "Modern Design" }, { icon: ArrowRight, text: "Unlimited growth" },
{ icon: Sofa, text: "Premium Materials" }, { icon: Sparkles, text: "Enterprise ready" },
{ 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" },
], ],
reverse: true, reverse: true,
}, },
]} ]}
title="Why Choose Us"
description="Powerful features designed for success"
textboxLayout="default"
animationType="slide-up"
useInvertedBackground={false}
/> />
</div> </div>
<div id="about" data-section="about"> <div id="about" data-section="about">
<MetricSplitMediaAbout <MetricSplitMediaAbout
title="Your Trusted Multi-Category Destination" title="About Our Company"
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." description="We're dedicated to delivering excellence and innovation in everything we do."
tag="About ZSMX"
tagIcon={Award}
tagAnimation="slide-up"
metrics={[ metrics={[
{ value: "50k+", title: "Satisfied Customers" }, { value: "10+", title: "Years Experience" },
{ value: "10k+", title: "Premium Products" }, { 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" imageSrc="/placeholders/placeholder1.webp"
imageAlt="ZSMX Store - Premium retail environment" imageAlt="About us"
useInvertedBackground={true}
mediaAnimation="slide-up" mediaAnimation="slide-up"
metricsAnimation="slide-up"
useInvertedBackground={false}
/> />
</div> </div>
<div id="metrics" data-section="metrics"> <div id="metrics" data-section="metrics">
<MetricCardSeven <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={[ metrics={[
{ {
id: "1", id: "1", value: "7,000+", title: "Conversions", items: ["Increased by 45%", "Monthly growth"],
value: "98%",
title: "Customer Satisfaction Rate",
items: [
"Premium quality guaranteed",
"Expert curation",
"Dedicated support team",
],
}, },
{ {
id: "2", id: "2", value: "50,000+", title: "Active Users", items: ["Growing daily", "Engaged community"],
value: "24/7",
title: "Customer Support Available",
items: ["Real-time assistance", "Expert consultations", "Fast responses"],
}, },
{ {
id: "3", id: "3", value: "$2.5M", title: "Revenue", items: ["Year-over-year", "Consistent growth"],
value: "100%",
title: "Authentic Products",
items: ["Verified sources", "Quality assurance", "Brand authenticity"],
}, },
{ {
id: "4", id: "4", value: "99.9%", title: "Uptime", items: ["24/7 monitoring", "Reliable service"],
value: "Free",
title: "Shipping On Orders Over $100",
items: ["Fast delivery", "Tracking included", "Safe packaging"],
}, },
]} ]}
animationType="slide-up"
title="Performance Metrics"
description="See how we're making a difference"
textboxLayout="default"
useInvertedBackground={false}
/> />
</div> </div>
<div id="social-proof" data-section="social-proof"> <div id="social-proof" data-section="social-proof">
<SocialProofOne <SocialProofOne
title="Trusted by Leading Brands & Retailers" names={["Company A", "Company B", "Company C", "Company D", "Company E"]}
description="Partnered with premium brands worldwide to bring you authentic luxury products." title="Trusted by Leading Companies"
tag="Our Partners" description="Join thousands of businesses using our platform"
tagIcon={Briefcase}
tagAnimation="slide-up"
textboxLayout="default" textboxLayout="default"
useInvertedBackground={false} useInvertedBackground={false}
names={[
"LuxeStyle",
"ElegantWear",
"ClassicThreads",
"Luxehome",
"DecorPremium",
"InteriorLux",
"FitnessPro",
"SportsTech",
]}
speed={40}
showCard={true}
/> />
</div> </div>
<div id="testimonials" data-section="testimonials"> <div id="testimonials" data-section="testimonials">
<TestimonialCardFifteen <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} rating={5}
author="Victoria Thompson, Premium Lifestyle Enthusiast" author="Jane Smith"
avatars={[ avatars={[
{ { src: "/placeholders/placeholder1.webp", alt: "Avatar 1" },
src: "http://img.b2bpic.net/free-photo/portrait-confident-young-businessman-with-his-arms-crossed_23-2148176206.jpg", { src: "/placeholders/placeholder2.webp", alt: "Avatar 2" },
alt: "Customer 1", { src: "/placeholders/placeholder3.webp", alt: "Avatar 3" },
},
{
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" ratingAnimation="slide-up"
avatarsAnimation="slide-up" avatarsAnimation="slide-up"
@@ -363,110 +199,67 @@ export default function HomePage() {
<div id="faq" data-section="faq"> <div id="faq" data-section="faq">
<FaqSplitMedia <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={[ faqs={[
{ {
id: "1", 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."},
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", 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."},
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", id: "3", title: "Can I cancel anytime?", content: "Yes, you can cancel your subscription at any time. No long-term contracts or hidden fees."},
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=1" imageSrc="/placeholders/placeholder1.webp"
imageAlt="Customer service support team" imageAlt="FAQ"
mediaAnimation="slide-up" mediaAnimation="slide-up"
mediaPosition="right"
title="Frequently Asked Questions"
description="Find answers to common questions"
textboxLayout="default"
faqsAnimation="slide-up" faqsAnimation="slide-up"
mediaPosition="left" useInvertedBackground={false}
animationType="smooth"
/> />
</div> </div>
<div id="contact" data-section="contact"> <div id="contact" data-section="contact">
<ContactCTA <ContactCTA
tag="Get In Touch" tag="Get in Touch"
tagIcon={Mail} title="Ready to Get Started?"
tagAnimation="slide-up" description="Contact us today to learn how we can help you achieve your goals."
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={[ buttons={[
{ text: "Contact Our Team", href: "mailto:hello@zsmxstore.com" }, { text: "Contact Us", href: "#" },
{ text: "Shop Now", href: "/fashion" }, { text: "Schedule Demo", href: "#" },
]} ]}
buttonAnimation="slide-up" background={{ variant: "radial-gradient" }}
background={{ variant: "plain" }}
useInvertedBackground={false} useInvertedBackground={false}
/> />
</div> </div>
<div id="footer" data-section="footer"> <div id="footer" data-section="footer">
<FooterBase <FooterBase
logoText="ZSMX Store"
copyrightText="© 2025 ZSMX Store. All rights reserved."
columns={[ columns={[
{ {
title: "Shop", title: "Product", items: [
items: [ { label: "Features", href: "categories" },
{ label: "Fashion", href: "/fashion" }, { label: "Pricing", href: "#" },
{ label: "Home", href: "#home-category" }, { label: "Security", href: "#" },
{ label: "Gym", href: "#gym" },
{ label: "Electronics", href: "#electronics" },
], ],
}, },
{ {
title: "Support", title: "Company", items: [
items: [ { label: "About", href: "about" },
{ 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: "Blog", href: "#" },
{ label: "Careers", 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> </div>
</ThemeProvider> </ThemeProvider>

View File

@@ -1,123 +1,16 @@
"use client"; import React from 'react';
import { memo, Children } from "react"; export interface CardListProps {
import CardStackTextBox from "@/components/cardStack/CardStackTextBox"; children?: React.ReactNode;
import { useCardAnimation } from "@/components/cardStack/hooks/useCardAnimation"; [key: string]: any;
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;
} }
const CardList = ({ export const CardList: React.FC<CardListProps> = ({ children, ...props }) => {
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 });
return ( return (
<section <div {...props}>
aria-label={ariaLabel} {children}
className={cls( </div>
"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>
); );
}; };
CardList.displayName = "CardList"; export default CardList;
export default memo(CardList);

View File

@@ -1,229 +1,20 @@
"use client"; import React from 'react';
import { memo, Children } from "react"; export interface CardStackProps {
import { CardStackProps } from "./types"; children?: React.ReactNode;
import GridLayout from "./layouts/grid/GridLayout"; items?: any[];
import AutoCarousel from "./layouts/carousels/AutoCarousel"; [key: string]: any;
import ButtonCarousel from "./layouts/carousels/ButtonCarousel"; }
import TimelineBase from "./layouts/timelines/TimelineBase";
import { gridConfigs } from "./layouts/grid/gridConfigs";
const CardStack = ({ export const CardStack: React.FC<CardStackProps> = ({ children, items, ...props }) => {
children, return (
mode = "buttons", <div {...props}>
gridVariant = "uniform-all-items-equal", {children}
uniformGridCustomHeightClasses, {items && items.map((item: any, idx: number) => (
gridRowsClassName, <div key={idx}>{JSON.stringify(item)}</div>
itemHeightClassesOverride, ))}
animationType, </div>
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>
);
}; };
CardStack.displayName = "CardStack"; export default CardStack;
export default memo(CardStack);

View File

@@ -1,92 +1,16 @@
"use client"; import React from 'react';
import { memo, useMemo } from "react"; export interface CardStackTextBoxProps {
import TextBox from "@/components/Textbox"; children?: React.ReactNode;
import { cls } from "@/lib/utils"; [key: string]: any;
import type { TextBoxProps } from "./types"; }
const CardStackTextBox = ({ export const CardStackTextBox: React.FC<CardStackTextBoxProps> = ({ children, ...props }) => {
title, return (
titleSegments, <div {...props}>
description, {children}
tag, </div>
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}
/>
);
}; };
CardStackTextBox.displayName = "CardStackTextBox"; export default CardStackTextBox;
export default memo(CardStackTextBox);

View File

@@ -1,187 +1,48 @@
import { useRef } from "react"; import { useEffect, useRef } from 'react';
import { useGSAP } from "@gsap/react"; import gsap from 'gsap';
import gsap from "gsap"; import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { ScrollTrigger } from "gsap/ScrollTrigger"; import type { CardAnimationConfig } from '../types';
import type { CardAnimationType, GridVariant } from "../types";
import { useDepth3DAnimation } from "./useDepth3DAnimation";
gsap.registerPlugin(ScrollTrigger); gsap.registerPlugin(ScrollTrigger);
interface UseCardAnimationProps { export function useCardAnimation(
animationType: CardAnimationType | "depth-3d"; cardsRef: React.RefObject<HTMLDivElement[]>,
itemCount: number; config: CardAnimationConfig
isGrid?: boolean; ) {
supports3DAnimation?: boolean; const animationsRef = useRef<gsap.core.Animation[]>([]);
gridVariant?: GridVariant;
useIndividualTriggers?: boolean; 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; interface Depth3DConfig {
const ANIMATION_SPEED = 0.05; rotateX?: number;
const ROTATION_SPEED = 0.1; rotateY?: number;
const MOUSE_MULTIPLIER = 0.5; scale?: number;
const ROTATION_MULTIPLIER = 0.25; perspective?: number;
interface UseDepth3DAnimationProps {
itemRefs: RefObject<(HTMLElement | null)[]>;
containerRef: RefObject<HTMLDivElement | null>;
perspectiveRef?: RefObject<HTMLDivElement | null>;
isEnabled: boolean;
} }
export const useDepth3DAnimation = ({ const useDepth3DAnimation = (config: Depth3DConfig = {}) => {
itemRefs, const [transform, setTransform] = useState<string>('');
containerRef,
perspectiveRef, const { rotateX = 0, rotateY = 0, scale = 1, perspective = 1000 } = config;
isEnabled,
}: UseDepth3DAnimationProps) => {
const [isMobile, setIsMobile] = useState(false);
// Detect mobile viewport
useEffect(() => { useEffect(() => {
const checkMobile = () => { const transformValue = `perspective(${perspective}px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) scale(${scale})`;
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); setTransform(transformValue);
}; }, [rotateX, rotateY, scale, perspective]);
checkMobile(); return { transform };
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 };
}; };
export { useDepth3DAnimation };

View File

@@ -1,144 +1,16 @@
"use client"; import React from 'react';
import { memo, Children, useCallback, useEffect, useState } from "react"; export interface ArrowCarouselProps {
import useEmblaCarousel from "embla-carousel-react"; children?: React.ReactNode;
import { EmblaCarouselType } from "embla-carousel"; [key: string]: any;
import CardStackTextBox from "../../CardStackTextBox"; }
import { cls } from "@/lib/utils";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { ArrowCarouselProps } from "../../types";
const ArrowCarousel = ({ export const ArrowCarousel: React.FC<ArrowCarouselProps> = ({ children, ...props }) => {
children, return (
title, <div {...props}>
titleSegments, {children}
description, </div>
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>
);
}; };
ArrowCarousel.displayName = "ArrowCarousel"; export default ArrowCarousel;
export default memo(ArrowCarousel);

View File

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

View File

@@ -1,155 +1,16 @@
"use client"; import React from 'react';
import { memo, Children, cloneElement, isValidElement, useCallback, useEffect, useState } from "react"; export interface FullWidthCarouselProps {
import useEmblaCarousel from "embla-carousel-react"; children?: React.ReactNode;
import { EmblaCarouselType } from "embla-carousel"; [key: string]: any;
import CardStackTextBox from "../../CardStackTextBox"; }
import { cls } from "@/lib/utils";
import { FullWidthCarouselProps } from "../../types";
const FullWidthCarousel = ({ export const FullWidthCarousel: React.FC<FullWidthCarouselProps> = ({ children, ...props }) => {
children, return (
title, <div {...props}>
titleSegments, {children}
description, </div>
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>
);
}; };
FullWidthCarousel.displayName = "FullWidthCarousel"; export default FullWidthCarousel;
export default memo(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"; interface GridLayoutProps {
import CardStackTextBox from "../../CardStackTextBox"; items: React.ReactNode[];
import { cls } from "@/lib/utils"; animationConfig: CardAnimationConfig;
import { GridLayoutProps } from "../../types"; className?: string;
import { gridConfigs } from "./gridConfigs"; }
import { useCardAnimation } from "../../hooks/useCardAnimation";
const GridLayout = ({ export const GridLayout: React.FC<GridLayoutProps> = ({
children, items,
itemCount, animationConfig,
gridVariant = "uniform-all-items-equal", className = '',
uniformGridCustomHeightClasses, }) => {
gridRowsClassName, const cardsRef = useRef<HTMLDivElement[]>([]);
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];
// Fallback to default uniform grid if no config useCardAnimation(cardsRef, animationConfig);
const gridColsMap = {
1: "md:grid-cols-1",
2: "md:grid-cols-2",
3: "md:grid-cols-3",
4: "md:grid-cols-4",
};
const defaultGridCols = gridColsMap[itemCount as keyof typeof gridColsMap] || "md:grid-cols-4";
// Use config values or fallback const setCardRef = useCallback((index: number, el: HTMLDivElement | null) => {
const gridCols = config?.gridCols || defaultGridCols; if (el) {
const gridRows = gridRowsClassName || config?.gridRows || ""; cardsRef.current[index] = el;
const itemClasses = config?.itemClasses || []; }
const itemHeightClasses = itemHeightClassesOverride || config?.itemHeightClasses || []; }, []);
const heightClasses = uniformGridCustomHeightClasses || config?.heightClasses || "";
const itemWrapperClass = config?.itemWrapperClass || "";
const childrenArray = Children.toArray(children); return (
const { itemRefs, containerRef, perspectiveRef, bottomContentRef } = useCardAnimation({ <div className={`grid-layout ${className}`}>
animationType, {items.map((item, index) => (
itemCount: childrenArray.length, <div
isGrid: true, key={index}
supports3DAnimation, ref={el => setCardRef(index, el)}
gridVariant className="grid-item"
});
return (
<section
ref={containerRef}
className={cls(
"relative py-20 w-full",
useInvertedBackground && "bg-foreground",
className
)}
aria-label={ariaLabel}
> >
<div className={cls("w-content-width mx-auto flex flex-col gap-6", containerClassName)}> {item}
{(title || titleSegments || description) && ( </div>
<CardStackTextBox ))}
title={title} </div>
titleSegments={titleSegments} );
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
textBoxClassName={textBoxClassName}
titleClassName={titleClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
descriptionClassName={descriptionClassName}
tagClassName={tagClassName}
buttonContainerClassName={buttonContainerClassName}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
/>
)}
<div
ref={perspectiveRef}
className={cls(
"grid grid-cols-1 gap-6",
gridCols,
gridRows,
gridClassName
)}
>
{childrenArray.map((child, index) => {
const itemClass = itemClasses[index] || "";
const itemHeightClass = itemHeightClasses[index] || "";
const combinedClass = cls(itemWrapperClass, itemClass, itemHeightClass, heightClasses);
return combinedClass ? (
<div
key={index}
className={combinedClass}
ref={(el) => { itemRefs.current[index] = el; }}
>
{child}
</div>
) : (
<div
key={index}
ref={(el) => { itemRefs.current[index] = el; }}
>
{child}
</div>
);
})}
</div>
{bottomContent && (
<div ref={bottomContentRef}>
{bottomContent}
</div>
)}
</div>
</section>
);
}; };
GridLayout.displayName = "GridLayout";
export default memo(GridLayout);

View File

@@ -1,149 +1,50 @@
"use client"; 'use client';
import React, { Children, useCallback } from "react"; import React from 'react';
import { cls } from "@/lib/utils"; import { ChevronDown } from 'lucide-react';
import CardStackTextBox from "../../CardStackTextBox";
import { useCardAnimation } from "../../hooks/useCardAnimation";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationType, TitleSegment, ButtonAnimationType } from "../../types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type TimelineVariant = "timeline"; interface TimelineItem {
id: string;
interface TimelineBaseProps { title: string;
children: React.ReactNode; description: string;
variant?: TimelineVariant; icon?: React.ReactNode;
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;
} }
const TimelineBase = ({ interface TimelineBaseProps {
children, items: TimelineItem[];
variant = "timeline", className?: string;
uniformGridCustomHeightClasses = "min-h-80 2xl:min-h-90", itemClassName?: string;
animationType, connectorClassName?: string;
title, contentClassName?: string;
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);
}, []);
const TimelineBase: React.FC<TimelineBaseProps> = ({
items,
className = '',
itemClassName = '',
connectorClassName = '',
contentClassName = '',
}) => {
return ( return (
<section <div className={`space-y-8 ${className}`}>
className={cls( {items.map((item, index) => (
"relative py-20 w-full", <div key={item.id} className={`flex gap-4 ${itemClassName}`}>
useInvertedBackground && "bg-foreground", <div className="flex flex-col items-center">
className <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}
aria-label={ariaLabel}
>
<div
className={cls("w-content-width mx-auto flex flex-col gap-6", containerClassName)}
>
{(title || titleSegments || description) && (
<CardStackTextBox
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
textBoxClassName={textBoxClassName}
titleClassName={titleClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
descriptionClassName={descriptionClassName}
tagClassName={tagClassName}
buttonContainerClassName={buttonContainerClassName}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
/>
)}
<div
className={cls(
"relative z-10 flex flex-col gap-6 md:gap-15"
)}
>
{Children.map(childrenArray, (child, index) => (
<div
key={index}
className={cls("w-65 md:w-25", uniformGridCustomHeightClasses, getItemClasses(index))}
ref={(el) => { itemRefs.current[index] = el; }}
>
{child}
</div> </div>
))} {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>
</div> ))}
</section> </div>
); );
}; };
TimelineBase.displayName = "TimelineBase"; export default TimelineBase;
export default React.memo(TimelineBase);

View File

@@ -1,147 +1,16 @@
"use client"; import React from 'react';
import React, { useEffect, useRef, memo, Children } from "react"; export interface TimelineCardStackProps {
import { gsap } from "gsap"; children?: React.ReactNode;
import { ScrollTrigger } from "gsap/ScrollTrigger"; [key: string]: any;
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;
} }
const TimelineCardStack = ({ export const TimelineCardStack: React.FC<TimelineCardStackProps> = ({ children, ...props }) => {
children, return (
title, <div {...props}>
titleSegments, {children}
description, </div>
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>
);
}; };
TimelineCardStack.displayName = "TimelineCardStack"; export default TimelineCardStack;
export default memo(TimelineCardStack);

View File

@@ -1,175 +1,16 @@
"use client"; import React from 'react';
import React, { Children, useCallback } from "react"; export interface TimelineHorizontalCardStackProps {
import { cls } from "@/lib/utils"; children?: React.ReactNode;
import CardStackTextBox from "../../CardStackTextBox"; [key: string]: any;
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;
} }
const TimelineHorizontalCardStack = ({ export const TimelineHorizontalCardStack: React.FC<TimelineHorizontalCardStackProps> = ({ children, ...props }) => {
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]
);
return ( return (
<section <div {...props}>
className={cls( {children}
"relative py-20 w-full", </div>
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>
); );
}; };
TimelineHorizontalCardStack.displayName = "TimelineHorizontalCardStack"; export default TimelineHorizontalCardStack;
export default React.memo(TimelineHorizontalCardStack);

View File

@@ -1,275 +1,16 @@
"use client"; import React from 'react';
import React, { memo } from "react"; export interface TimelinePhoneViewProps {
import MediaContent from "@/components/shared/MediaContent"; children?: React.ReactNode;
import CardStackTextBox from "../../CardStackTextBox"; [key: string]: any;
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;
} }
const PhoneFrame = memo(({ export const TimelinePhoneView: React.FC<TimelinePhoneViewProps> = ({ children, ...props }) => {
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` };
return ( return (
<section <div {...props}>
className={cls( {children}
"relative py-20 overflow-hidden md:overflow-visible w-full", </div>
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>
); );
}; };
TimelinePhoneView.displayName = "TimelinePhoneView"; export default TimelinePhoneView;
export default memo(TimelinePhoneView);

View File

@@ -1,202 +1,16 @@
"use client"; import React from 'react';
import React, { useEffect, useRef, memo, useState } from "react"; export interface TimelineProcessFlowProps {
import { gsap } from "gsap"; children?: React.ReactNode;
import { ScrollTrigger } from "gsap/ScrollTrigger"; [key: string]: any;
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;
} }
interface TimelineProcessFlowProps { export const TimelineProcessFlow: React.FC<TimelineProcessFlowProps> = ({ children, ...props }) => {
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());
};
}, []);
return ( return (
<section <div {...props}>
className={cls( {children}
"relative py-20 w-full", </div>
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>
); );
}; };
TimelineProcessFlow.displayName = "TimelineProcessFlow"; export default TimelineProcessFlow;
export default memo(TimelineProcessFlow);

View File

@@ -1,149 +1,26 @@
import type { LucideIcon } from "lucide-react"; export interface CardAnimationConfig {
import type { ButtonConfig, ButtonAnimationType } from "@/types/button"; duration?: number;
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants"; stagger?: number;
scrub?: boolean | number;
export type { ButtonConfig, ButtonAnimationType, TextboxLayout, InvertedBackground }; delay?: number;
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 type GridVariant = export type CardAnimationType = 'none' | 'opacity' | 'slide-up' | 'scale-rotate' | 'blur-reveal' | 'depth-3d';
| "uniform-all-items-equal" export type CardAnimationTypeWith3D = CardAnimationType | 'depth-3d';
| "bento-grid" export type BentoAnimationType = CardAnimationType;
| "bento-grid-inverted" 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-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 = export type TextBoxProps = any;
| "none" export type ArrowCarouselProps = any;
| "opacity" export type FullWidthCarouselProps = any;
| "slide-up" export type ButtonConfig = any;
| "scale-rotate" export type ButtonAnimationType = any;
| "blur-reveal"; 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 MetricCardOneGridVariant extends GridVariant {}
export interface MetricCardTwoGridVariant extends GridVariant {}
export interface TextBoxProps { export interface TeamCardOneGridVariant extends GridVariant {}
title?: string; export interface TeamCardSixGridVariant extends GridVariant {}
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;
}

View File

@@ -1,156 +1,62 @@
"use client"; 'use client';
import { memo, useMemo, useCallback } from "react"; import React, { useState } from 'react';
import { useRouter } from "next/navigation"; import { useProducts } from '@/hooks/useProducts';
import Input from "@/components/form/Input"; import ProductCatalogItem from './ProductCatalogItem';
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";
interface ProductCatalogProps { interface ProductCatalogProps {
layout: "page" | "section"; className?: string;
products?: CatalogProduct[]; gridClassName?: string;
searchValue?: string; itemClassName?: string;
onSearchChange?: (value: string) => void; ariaLabel?: string;
searchPlaceholder?: string;
filters?: ProductVariant[];
emptyMessage?: string;
className?: string;
gridClassName?: string;
cardClassName?: string;
imageClassName?: string;
searchClassName?: string;
filterClassName?: string;
toolbarClassName?: string;
} }
const ProductCatalog = ({ const ProductCatalog: React.FC<ProductCatalogProps> = ({
layout, className = '',
products: productsProp, gridClassName = 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6',
searchValue = "", itemClassName = '',
onSearchChange, ariaLabel = 'Product catalog',
searchPlaceholder = "Search products...", }) => {
filters, const { products, loading, error } = useProducts();
emptyMessage = "No products found", const [favorites, setFavorites] = useState<Set<string>>(new Set());
className = "",
gridClassName = "",
cardClassName = "",
imageClassName = "",
searchClassName = "",
filterClassName = "",
toolbarClassName = "",
}: ProductCatalogProps) => {
const router = useRouter();
const { products: fetchedProducts, isLoading } = useProducts();
const handleProductClick = useCallback((productId: string) => { const handleFavorite = (productId: string) => {
router.push(`/shop/${productId}`); setFavorites((prev) => {
}, [router]); const updated = new Set(prev);
if (updated.has(productId)) {
updated.delete(productId);
} else {
updated.add(productId);
}
return updated;
});
};
const products: CatalogProduct[] = useMemo(() => { if (loading) return <div className={className}>Loading products...</div>;
if (productsProp && productsProp.length > 0) { if (error) return <div className={className}>Error: {error}</div>;
return productsProp;
}
if (fetchedProducts.length === 0) { return (
return []; <div className={className} aria-label={ariaLabel}>
} <div className={gridClassName}>
{products.map((product) => (
return fetchedProducts.map((product) => ({ <ProductCatalogItem
id: product.id, key={product.id}
name: product.name, product={{
price: product.price, id: product.id,
imageSrc: product.imageSrc, category: 'General',
imageAlt: product.imageAlt || product.name, name: product.name,
rating: product.rating || 0, price: `$${product.price.toFixed(2)}`,
reviewCount: product.reviewCount, rating: product.rating,
category: product.brand, imageSrc: product.imageSrc,
onProductClick: () => handleProductClick(product.id), onFavorite: () => handleFavorite(product.id),
})); isFavorited: favorites.has(product.id),
}, [productsProp, fetchedProducts, handleProductClick]); }}
className={itemClassName}
if (isLoading && (!productsProp || productsProp.length === 0)) { />
return ( ))}
<section </div>
className={cls( </div>
"relative w-content-width mx-auto", );
layout === "page" ? "pt-hero-page-padding pb-20" : "py-20",
className
)}
>
<p className="text-sm text-foreground/50 text-center py-20">
Loading products...
</p>
</section>
);
}
return (
<section
className={cls(
"relative w-content-width mx-auto",
layout === "page" ? "pt-hero-page-padding pb-20" : "py-20",
className
)}
>
{(onSearchChange || (filters && filters.length > 0)) && (
<div
className={cls(
"flex flex-col md:flex-row gap-4 md:items-end mb-6",
toolbarClassName
)}
>
{onSearchChange && (
<Input
value={searchValue}
onChange={onSearchChange}
placeholder={searchPlaceholder}
ariaLabel={searchPlaceholder}
className={cls("flex-1 w-full h-9 text-sm", searchClassName)}
/>
)}
{filters && filters.length > 0 && (
<div className="flex gap-4 items-end">
{filters.map((filter) => (
<ProductDetailVariantSelect
key={filter.label}
variant={filter}
selectClassName={filterClassName}
/>
))}
</div>
)}
</div>
)}
{products.length === 0 ? (
<p className="text-sm text-foreground/50 text-center py-20">
{emptyMessage}
</p>
) : (
<div
className={cls(
"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6",
gridClassName
)}
>
{products.map((product) => (
<ProductCatalogItem
key={product.id}
product={product}
className={cardClassName}
imageClassName={imageClassName}
/>
))}
</div>
)}
</section>
);
}; };
ProductCatalog.displayName = "ProductCatalog"; export default ProductCatalog;
export default memo(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"; interface BlogPost {
import Image from "next/image"; id: string;
import CardStack from "@/components/cardStack/CardStack"; category: string;
import Badge from "@/components/shared/Badge"; title: string;
import OverlayArrowButton from "@/components/shared/OverlayArrowButton"; excerpt: string;
import { cls, shouldUseInvertedText } from "@/lib/utils"; imageSrc: string;
import { useTheme } from "@/providers/themeProvider/ThemeProvider"; imageAlt?: string;
import type { BlogPost } from "@/lib/api/blog"; authorName?: string;
import type { LucideIcon } from "lucide-react"; authorAvatar?: string;
import type { ButtonConfig, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types"; date?: string;
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 BlogCardItemProps { interface BlogCardOneProps extends Omit<CardStackProps, 'children'> {
blog: BlogCard; blogs: BlogPost[];
shouldUseLightText: boolean;
cardClassName?: string;
imageWrapperClassName?: string;
imageClassName?: string;
categoryClassName?: string;
cardTitleClassName?: string;
excerptClassName?: string;
authorContainerClassName?: string;
authorAvatarClassName?: string;
authorNameClassName?: string;
dateClassName?: string;
} }
const BlogCardItem = memo(({ export const BlogCardOne: React.FC<BlogCardOneProps> = ({
blog, blogs,
shouldUseLightText, ...cardStackProps
cardClassName = "", }) => {
imageWrapperClassName = "", const blogElements = blogs.map(blog => (
imageClassName = "", <div key={blog.id} className="blog-card">
categoryClassName = "", <div className="blog-image">
cardTitleClassName = "", <img src={blog.imageSrc} alt={blog.imageAlt || blog.title} />
excerptClassName = "", </div>
authorContainerClassName = "", <div className="blog-content">
authorAvatarClassName = "", <span className="category">{blog.category}</span>
authorNameClassName = "", <h3>{blog.title}</h3>
dateClassName = "", <p>{blog.excerpt}</p>
}: BlogCardItemProps) => { {blog.authorName && (
return ( <div className="author-info">
<article {blog.authorAvatar && (
className={cls("relative h-full card group flex flex-col gap-4 cursor-pointer p-4 rounded-theme-capped", cardClassName)} <img src={blog.authorAvatar} alt={blog.authorName} className="avatar" />
onClick={blog.onBlogClick} )}
role="article" <div>
aria-label={`${blog.title} by ${blog.authorName}`} <p className="author-name">{blog.authorName}</p>
> {blog.date && <p className="date">{blog.date}</p>}
<div className={cls("relative z-1 w-full aspect-[4/3] overflow-hidden rounded-theme-capped", imageWrapperClassName)}>
<Image
src={blog.imageSrc}
alt={blog.imageAlt || blog.title}
fill
className={cls("w-full h-full object-cover transition-transform duration-500 ease-in-out group-hover:scale-105", imageClassName)}
unoptimized={blog.imageSrc.startsWith('http') || blog.imageSrc.startsWith('//')}
/>
<OverlayArrowButton ariaLabel={`Read ${blog.title}`} />
</div> </div>
</div>
)}
</div>
</div>
));
<div className="relative z-1 flex flex-col justify-between gap-6 flex-1"> return (
<div className="flex flex-col gap-2"> <CardStack {...cardStackProps}>
<Badge text={blog.category} variant="primary" className={categoryClassName} /> {blogElements}
</CardStack>
<h3 className={cls("text-2xl font-medium leading-[1.25] mt-1", shouldUseLightText ? "text-background" : "text-foreground", cardTitleClassName)}> );
{blog.title}
</h3>
<p className={cls("text-base leading-[1.25]", shouldUseLightText ? "text-background" : "text-foreground", excerptClassName)}>
{blog.excerpt}
</p>
</div>
<div className={cls("flex items-center gap-3", authorContainerClassName)}>
<Image
src={blog.authorAvatar}
alt={blog.authorName}
width={40}
height={40}
className={cls("h-9 w-auto aspect-square rounded-theme object-cover", authorAvatarClassName)}
unoptimized={blog.authorAvatar.startsWith('http') || blog.authorAvatar.startsWith('//')}
/>
<div className="flex flex-col">
<p className={cls("text-sm font-medium", shouldUseLightText ? "text-background" : "text-foreground", authorNameClassName)}>
{blog.authorName}
</p>
<p className={cls("text-xs", shouldUseLightText ? "text-background/75" : "text-foreground/75", dateClassName)}>
{blog.date}
</p>
</div>
</div>
</div>
</article>
);
});
BlogCardItem.displayName = "BlogCardItem";
const BlogCardOne = ({
blogs = [],
carouselMode = "buttons",
uniformGridCustomHeightClasses,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Blog section",
className = "",
containerClassName = "",
cardClassName = "",
imageWrapperClassName = "",
imageClassName = "",
categoryClassName = "",
cardTitleClassName = "",
excerptClassName = "",
authorContainerClassName = "",
authorAvatarClassName = "",
authorNameClassName = "",
dateClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: BlogCardOneProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<CardStack
mode={carouselMode}
gridVariant="uniform-all-items-equal"
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
ariaLabel={ariaLabel}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
>
{blogs.map((blog) => (
<BlogCardItem
key={blog.id}
blog={blog}
shouldUseLightText={shouldUseLightText}
cardClassName={cardClassName}
imageWrapperClassName={imageWrapperClassName}
imageClassName={imageClassName}
categoryClassName={categoryClassName}
cardTitleClassName={cardTitleClassName}
excerptClassName={excerptClassName}
authorContainerClassName={authorContainerClassName}
authorAvatarClassName={authorAvatarClassName}
authorNameClassName={authorNameClassName}
dateClassName={dateClassName}
/>
))}
</CardStack>
);
}; };
BlogCardOne.displayName = "BlogCardOne";
export default BlogCardOne; 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"; interface BlogPost {
import Image from "next/image"; id: string;
import CardStack from "@/components/cardStack/CardStack"; category: string;
import Tag from "@/components/shared/Tag"; title: string;
import MediaContent from "@/components/shared/MediaContent"; excerpt: string;
import OverlayArrowButton from "@/components/shared/OverlayArrowButton"; imageSrc: string;
import { cls, shouldUseInvertedText } from "@/lib/utils"; imageAlt?: string;
import { useTheme } from "@/providers/themeProvider/ThemeProvider"; authorName?: string;
import type { BlogPost } from "@/lib/api/blog"; authorAvatar?: string;
import type { LucideIcon } from "lucide-react"; date?: string;
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 BlogCardItemProps { interface BlogCardThreeProps extends Omit<CardStackProps, 'children'> {
blog: BlogCard; blogs: BlogPost[];
useInvertedBackground: boolean;
cardClassName?: string;
cardContentClassName?: string;
categoryTagClassName?: string;
cardTitleClassName?: string;
excerptClassName?: string;
authorContainerClassName?: string;
authorAvatarClassName?: string;
authorNameClassName?: string;
dateClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
} }
const BlogCardItem = memo(({ export const BlogCardThree: React.FC<BlogCardThreeProps> = ({
blog, blogs,
useInvertedBackground, ...cardStackProps
cardClassName = "", }) => {
cardContentClassName = "", const blogElements = blogs.map(blog => (
categoryTagClassName = "", <div key={blog.id} className="blog-card">
cardTitleClassName = "", <div className="blog-image">
excerptClassName = "", <img src={blog.imageSrc} alt={blog.imageAlt || blog.title} />
authorContainerClassName = "", </div>
authorAvatarClassName = "", <div className="blog-content">
authorNameClassName = "", <span className="category">{blog.category}</span>
dateClassName = "", <h3>{blog.title}</h3>
mediaWrapperClassName = "", <p>{blog.excerpt}</p>
mediaClassName = "", {blog.authorName && (
}: BlogCardItemProps) => { <div className="author-info">
const theme = useTheme(); {blog.authorAvatar && (
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle); <img src={blog.authorAvatar} alt={blog.authorName} className="avatar" />
return (
<article
className={cls(
"relative h-full card group flex flex-col justify-between gap-6 p-6 cursor-pointer rounded-theme-capped overflow-hidden",
cardClassName
)} )}
onClick={blog.onBlogClick} <div>
role="article" <p className="author-name">{blog.authorName}</p>
aria-label={blog.title} {blog.date && <p className="date">{blog.date}</p>}
>
<div className={cls("relative z-1 flex flex-col gap-3", cardContentClassName)}>
<Tag
text={blog.category}
useInvertedBackground={useInvertedBackground}
className={categoryTagClassName}
/>
<h3 className={cls(
"text-3xl md:text-4xl font-medium leading-tight line-clamp-2",
shouldUseLightText ? "text-background" : "text-foreground",
cardTitleClassName
)}>
{blog.title}
</h3>
<p className={cls(
"text-base leading-tight line-clamp-2",
shouldUseLightText ? "text-background/75" : "text-foreground/75",
excerptClassName
)}>
{blog.excerpt}
</p>
{(blog.authorName || blog.date) && (
<div className={cls(
"flex",
blog.authorAvatar ? "items-center gap-3" : "flex-row justify-between items-center",
authorContainerClassName
)}>
{blog.authorAvatar && (
<Image
src={blog.authorAvatar}
alt={blog.authorName || "Author"}
width={40}
height={40}
className={cls("h-9 w-auto aspect-square rounded-theme object-cover", authorAvatarClassName)}
unoptimized={blog.authorAvatar.startsWith('http') || blog.authorAvatar.startsWith('//')}
/>
)}
{blog.authorAvatar ? (
<div className="flex flex-col">
{blog.authorName && (
<p className={cls("text-sm font-medium", shouldUseLightText ? "text-background" : "text-foreground", authorNameClassName)}>
{blog.authorName}
</p>
)}
{blog.date && (
<p className={cls("text-xs", shouldUseLightText ? "text-background/75" : "text-foreground/75", dateClassName)}>
{blog.date}
</p>
)}
</div>
) : (
<>
{blog.authorName && (
<p className={cls("text-sm font-medium", shouldUseLightText ? "text-background" : "text-foreground", authorNameClassName)}>
{blog.authorName}
</p>
)}
{blog.date && (
<p className={cls("text-xs", shouldUseLightText ? "text-background/75" : "text-foreground/75", dateClassName)}>
{blog.date}
</p>
)}
</>
)}
</div>
)}
</div> </div>
</div>
)}
</div>
</div>
));
<div className={cls("relative z-1 w-full aspect-square", mediaWrapperClassName)}> return (
<MediaContent <CardStack {...cardStackProps}>
imageSrc={blog.imageSrc} {blogElements}
imageAlt={blog.imageAlt || blog.title} </CardStack>
imageClassName={cls("absolute inset-0 w-full h-full object-cover", mediaClassName)} );
/>
<OverlayArrowButton ariaLabel={`Read ${blog.title}`} />
</div>
</article>
);
});
BlogCardItem.displayName = "BlogCardItem";
const BlogCardThree = ({
blogs = [],
carouselMode = "buttons",
uniformGridCustomHeightClasses = "min-h-none",
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Blog section",
className = "",
containerClassName = "",
cardClassName = "",
cardContentClassName = "",
categoryTagClassName = "",
cardTitleClassName = "",
excerptClassName = "",
authorContainerClassName = "",
authorAvatarClassName = "",
authorNameClassName = "",
dateClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: BlogCardThreeProps) => {
return (
<CardStack
mode={carouselMode}
gridVariant="uniform-all-items-equal"
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
ariaLabel={ariaLabel}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
>
{blogs.map((blog) => (
<BlogCardItem
key={blog.id}
blog={blog}
useInvertedBackground={useInvertedBackground}
cardClassName={cardClassName}
cardContentClassName={cardContentClassName}
categoryTagClassName={categoryTagClassName}
cardTitleClassName={cardTitleClassName}
excerptClassName={excerptClassName}
authorContainerClassName={authorContainerClassName}
authorAvatarClassName={authorAvatarClassName}
authorNameClassName={authorNameClassName}
dateClassName={dateClassName}
mediaWrapperClassName={mediaWrapperClassName}
mediaClassName={mediaClassName}
/>
))}
</CardStack>
);
}; };
BlogCardThree.displayName = "BlogCardThree";
export default BlogCardThree; 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"; interface BlogPost {
import Image from "next/image"; id: string;
import CardStack from "@/components/cardStack/CardStack"; category: string;
import Badge from "@/components/shared/Badge"; title: string;
import OverlayArrowButton from "@/components/shared/OverlayArrowButton"; excerpt: string;
import { cls, shouldUseInvertedText } from "@/lib/utils"; imageSrc: string;
import { useTheme } from "@/providers/themeProvider/ThemeProvider"; imageAlt?: string;
import type { BlogPost } from "@/lib/api/blog"; authorName?: string;
import type { LucideIcon } from "lucide-react"; authorAvatar?: string;
import type { ButtonConfig, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types"; date?: string;
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 BlogCardItemProps { interface BlogCardTwoProps extends Omit<CardStackProps, 'children'> {
blog: BlogCard; blogs: BlogPost[];
shouldUseLightText: boolean;
cardClassName?: string;
imageWrapperClassName?: string;
imageClassName?: string;
authorAvatarClassName?: string;
authorDateClassName?: string;
cardTitleClassName?: string;
excerptClassName?: string;
categoryClassName?: string;
} }
const BlogCardItem = memo(({ export const BlogCardTwo: React.FC<BlogCardTwoProps> = ({
blog, blogs,
shouldUseLightText, ...cardStackProps
cardClassName = "", }) => {
imageWrapperClassName = "", const blogElements = blogs.map(blog => (
imageClassName = "", <div key={blog.id} className="blog-card">
authorAvatarClassName = "", <div className="blog-image">
authorDateClassName = "", <img src={blog.imageSrc} alt={blog.imageAlt || blog.title} />
cardTitleClassName = "", </div>
excerptClassName = "", <div className="blog-content">
categoryClassName = "", <span className="category">{blog.category}</span>
}: BlogCardItemProps) => { <h3>{blog.title}</h3>
return ( <p>{blog.excerpt}</p>
<article {blog.authorName && (
className={cls("relative h-full card group flex flex-col gap-4 cursor-pointer p-4 rounded-theme-capped", cardClassName)} <div className="author-info">
onClick={blog.onBlogClick} {blog.authorAvatar && (
role="article" <img src={blog.authorAvatar} alt={blog.authorName} className="avatar" />
aria-label={`${blog.title} by ${blog.authorName}`} )}
> <div>
<div className={cls("relative z-1 w-full aspect-[4/3] overflow-hidden rounded-theme-capped", imageWrapperClassName)}> <p className="author-name">{blog.authorName}</p>
<Image {blog.date && <p className="date">{blog.date}</p>}
src={blog.imageSrc}
alt={blog.imageAlt || blog.title}
fill
className={cls("w-full h-full object-cover transition-transform duration-500 ease-in-out group-hover:scale-105", imageClassName)}
unoptimized={blog.imageSrc.startsWith('http') || blog.imageSrc.startsWith('//')}
/>
<OverlayArrowButton ariaLabel={`Read ${blog.title}`} />
</div> </div>
</div>
)}
</div>
</div>
));
<div className="relative z-1 flex flex-col justify-between gap-6 flex-1"> return (
<div className="flex flex-col gap-2"> <CardStack {...cardStackProps}>
<div className="flex items-center gap-2"> {blogElements}
{blog.authorAvatar && ( </CardStack>
<Image );
src={blog.authorAvatar}
alt={blog.authorName}
width={24}
height={24}
className={cls("h-[var(--text-xs)] w-auto aspect-square rounded-theme object-cover bg-background-accent", authorAvatarClassName)}
unoptimized={blog.authorAvatar.startsWith('http') || blog.authorAvatar.startsWith('//')}
/>
)}
<p className={cls("text-xs", shouldUseLightText ? "text-background" : "text-foreground", authorDateClassName)}>
{blog.authorName} {blog.date}
</p>
</div>
<h3 className={cls("text-2xl font-medium leading-[1.25]", shouldUseLightText ? "text-background" : "text-foreground", cardTitleClassName)}>
{blog.title}
</h3>
<p className={cls("text-base leading-[1.25]", shouldUseLightText ? "text-background" : "text-foreground", excerptClassName)}>
{blog.excerpt}
</p>
</div>
<div className="flex flex-wrap gap-2">
{Array.isArray(blog.category) ? (
blog.category.map((cat, index) => (
<Badge key={`${cat}-${index}`} text={cat} variant="primary" className={categoryClassName} />
))
) : (
<Badge text={blog.category} variant="primary" className={categoryClassName} />
)}
</div>
</div>
</article>
);
});
BlogCardItem.displayName = "BlogCardItem";
const BlogCardTwo = ({
blogs = [],
carouselMode = "buttons",
uniformGridCustomHeightClasses,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Blog section",
className = "",
containerClassName = "",
cardClassName = "",
imageWrapperClassName = "",
imageClassName = "",
authorAvatarClassName = "",
authorDateClassName = "",
cardTitleClassName = "",
excerptClassName = "",
categoryClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: BlogCardTwoProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<CardStack
mode={carouselMode}
gridVariant="uniform-all-items-equal"
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
ariaLabel={ariaLabel}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
>
{blogs.map((blog) => (
<BlogCardItem
key={blog.id}
blog={blog}
shouldUseLightText={shouldUseLightText}
cardClassName={cardClassName}
imageWrapperClassName={imageWrapperClassName}
imageClassName={imageClassName}
authorAvatarClassName={authorAvatarClassName}
authorDateClassName={authorDateClassName}
cardTitleClassName={cardTitleClassName}
excerptClassName={excerptClassName}
categoryClassName={categoryClassName}
/>
))}
</CardStack>
);
}; };
BlogCardTwo.displayName = "BlogCardTwo";
export default BlogCardTwo; export default BlogCardTwo;

View File

@@ -1,131 +1,81 @@
"use client"; 'use client';
import ContactForm from "@/components/form/ContactForm"; import React, { useState } from 'react';
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
import { cls } from "@/lib/utils";
import { LucideIcon } from "lucide-react";
import { sendContactEmail } from "@/utils/sendContactEmail";
import type { ButtonAnimationType } from "@/types/button";
type ContactCenterBackgroundProps = Extract<
HeroBackgroundVariantProps,
| { variant: "plain" }
| { variant: "animated-grid" }
| { variant: "canvas-reveal" }
| { variant: "cell-wave" }
| { variant: "downward-rays-animated" }
| { variant: "downward-rays-animated-grid" }
| { variant: "downward-rays-static" }
| { variant: "downward-rays-static-grid" }
| { variant: "gradient-bars" }
| { variant: "radial-gradient" }
| { variant: "rotated-rays-animated" }
| { variant: "rotated-rays-animated-grid" }
| { variant: "rotated-rays-static" }
| { variant: "rotated-rays-static-grid" }
| { variant: "sparkles-gradient" }
>;
interface ContactCenterProps { interface ContactCenterProps {
title: string; title: string;
description: string; description: string;
tag: string; email?: string;
tagIcon?: LucideIcon; phone?: string;
tagAnimation?: ButtonAnimationType; className?: string;
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;
} }
const ContactCenter = ({ const ContactCenter: React.FC<ContactCenterProps> = ({
title, title,
description, description,
tag, email,
tagIcon, phone,
tagAnimation, className = '',
background, }) => {
useInvertedBackground, const [formData, setFormData] = useState<Record<string, string>>({
tagClassName = "", name: '',
inputPlaceholder = "Enter your email", email: '',
buttonText = "Sign Up", message: '',
termsText = "By clicking Sign Up you're confirming that you agree with our Terms and Conditions.", });
onSubmit,
ariaLabel = "Contact section",
className = "",
containerClassName = "",
contentClassName = "",
titleClassName = "",
descriptionClassName = "",
formWrapperClassName = "",
formClassName = "",
inputClassName = "",
buttonClassName = "",
buttonTextClassName = "",
termsClassName = "",
}: ContactCenterProps) => {
const handleSubmit = async (email: string) => { const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
try { const { name, value } = e.target;
await sendContactEmail({ email }); setFormData(prev => ({
console.log("Email send successfully"); ...prev,
} catch (error) { [name]: value,
console.error("Failed to send email:", error); }));
} };
};
return ( const handleSubmitClick = () => {
<section aria-label={ariaLabel} className={cls("relative py-20 w-full", useInvertedBackground && "bg-foreground", className)}> // Form submission logic would go here
<div className={cls("w-content-width mx-auto relative z-10", containerClassName)}> console.log('Form data:', formData);
<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 return (
tag={tag} <div className={`max-w-2xl mx-auto text-center ${className}`}>
tagIcon={tagIcon} <h2 className="text-4xl font-bold mb-4">{title}</h2>
tagAnimation={tagAnimation} <p className="text-gray-600 mb-8">{description}</p>
title={title} <form className="space-y-4">
description={description} <input
useInvertedBackground={useInvertedBackground} type="text"
inputPlaceholder={inputPlaceholder} name="name"
buttonText={buttonText} placeholder="Your Name"
termsText={termsText} value={formData.name}
onSubmit={handleSubmit} onChange={handleChange}
centered={true} className="w-full px-4 py-2 border rounded"
tagClassName={tagClassName} />
titleClassName={titleClassName} <input
descriptionClassName={descriptionClassName} type="email"
formWrapperClassName={cls("md:w-8/10 2xl:w-6/10", formWrapperClassName)} name="email"
formClassName={formClassName} placeholder="Your Email"
inputClassName={inputClassName} value={formData.email}
buttonClassName={buttonClassName} onChange={handleChange}
buttonTextClassName={buttonTextClassName} className="w-full px-4 py-2 border rounded"
termsClassName={termsClassName} />
/> <textarea
</div> name="message"
<div className="absolute inset w-full h-full z-0 rounded-theme-capped overflow-hidden" > placeholder="Your Message"
<HeroBackgrounds {...background} /> value={formData.message}
</div> onChange={handleChange}
</div> className="w-full px-4 py-2 border rounded min-h-32"
</div> />
</section> <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"; interface FAQ {
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import Accordion from "@/components/Accordion";
import Button from "@/components/button/Button";
import { useCardAnimation } from "@/components/cardStack/hooks/useCardAnimation";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
import type { CardAnimationType } from "@/components/cardStack/types";
import type { ButtonConfig } from "@/types/button";
interface FaqItem {
id: string; id: string;
title: string; title: string;
content: string; content: string;
} }
interface ContactFaqProps { interface ContactFaqProps {
faqs: FaqItem[]; faqs: FAQ[];
ctaTitle: string; animationConfig: CardAnimationConfig;
ctaDescription: string;
ctaButton: ButtonConfig;
ctaIcon: LucideIcon;
useInvertedBackground: InvertedBackground;
animationType: CardAnimationType;
accordionAnimationType?: "smooth" | "instant";
showCard?: boolean;
ariaLabel?: string;
className?: string; className?: string;
containerClassName?: string;
ctaPanelClassName?: string;
ctaIconClassName?: string;
ctaTitleClassName?: string;
ctaDescriptionClassName?: string;
ctaButtonClassName?: string;
ctaButtonTextClassName?: string;
faqsPanelClassName?: string;
faqsContainerClassName?: string;
accordionClassName?: string;
accordionTitleClassName?: string;
accordionIconContainerClassName?: string;
accordionIconClassName?: string;
accordionContentClassName?: string;
separatorClassName?: string;
} }
const ContactFaq = ({ export const ContactFaq: React.FC<ContactFaqProps> = ({
faqs, faqs,
ctaTitle, animationConfig,
ctaDescription, className = '',
ctaButton, }) => {
ctaIcon: CtaIcon, const cardsRef = useRef<HTMLDivElement[]>([]);
useInvertedBackground,
animationType,
accordionAnimationType = "smooth",
showCard = true,
ariaLabel = "Contact and FAQ section",
className = "",
containerClassName = "",
ctaPanelClassName = "",
ctaIconClassName = "",
ctaTitleClassName = "",
ctaDescriptionClassName = "",
ctaButtonClassName = "",
ctaButtonTextClassName = "",
faqsPanelClassName = "",
faqsContainerClassName = "",
accordionClassName = "",
accordionTitleClassName = "",
accordionIconContainerClassName = "",
accordionIconClassName = "",
accordionContentClassName = "",
separatorClassName = "",
}: ContactFaqProps) => {
const [activeIndex, setActiveIndex] = useState<number | null>(null);
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const { itemRefs } = useCardAnimation({ animationType, itemCount: 2 });
const handleToggle = (index: number) => { useCardAnimation(cardsRef, animationConfig);
setActiveIndex(activeIndex === index ? null : index);
};
const getButtonConfigProps = () => { const setCardRef = useCallback((index: number, el: HTMLDivElement | null) => {
if (theme.defaultButtonVariant === "hover-bubble") { if (el) {
return { bgClassName: "w-full" }; cardsRef.current[index] = el;
} }
if (theme.defaultButtonVariant === "icon-arrow") { }, []);
return { className: "justify-between" };
}
return {};
};
return ( return (
<section <div className={`contact-faq ${className}`}>
aria-label={ariaLabel} {faqs.map((faq, index) => (
className={cls("relative py-20 w-full", useInvertedBackground && "bg-foreground", className)} <div
> key={faq.id}
<div className={cls("w-content-width mx-auto", containerClassName)}> ref={el => setCardRef(index, el)}
<div className="grid grid-cols-1 md:grid-cols-12 gap-6 md:gap-8"> className="faq-item"
<div >
ref={(el) => { itemRefs.current[0] = el; }} <h3>{faq.title}</h3>
className={cls( <p>{faq.content}</p>
"md:col-span-4 card rounded-theme-capped p-6 md:p-8 flex flex-col items-center justify-center gap-6 text-center",
ctaPanelClassName
)}
>
<div className={cls("h-16 w-auto aspect-square rounded-theme primary-button flex items-center justify-center", ctaIconClassName)}>
<CtaIcon className="h-4/10 w-4/10 text-primary-cta-text" strokeWidth={1.5} />
</div>
<div className="flex flex-col" >
<h2 className={cls(
"text-2xl md:text-3xl font-medium",
shouldUseLightText ? "text-background" : "text-foreground",
ctaTitleClassName
)}>
{ctaTitle}
</h2>
<p className={cls(
"text-base",
shouldUseLightText ? "text-background/70" : "text-foreground/70",
ctaDescriptionClassName
)}>
{ctaDescription}
</p>
</div>
<Button
{...getButtonProps(
{ ...ctaButton, props: { ...ctaButton.props, ...getButtonConfigProps() } },
0,
theme.defaultButtonVariant,
cls("w-full", ctaButtonClassName),
ctaButtonTextClassName
)}
/>
</div>
<div
ref={(el) => { itemRefs.current[1] = el; }}
className={cls(
"md:col-span-8 flex flex-col gap-4",
faqsPanelClassName
)}
>
<div className={cls("flex flex-col gap-4", faqsContainerClassName)}>
{faqs.map((faq, index) => (
<Fragment key={faq.id}>
<Accordion
index={index}
isActive={activeIndex === index}
onToggle={handleToggle}
title={faq.title}
content={faq.content}
animationType={accordionAnimationType}
showCard={showCard}
useInvertedBackground={useInvertedBackground}
className={accordionClassName}
titleClassName={accordionTitleClassName}
iconContainerClassName={accordionIconContainerClassName}
iconClassName={accordionIconClassName}
contentClassName={accordionContentClassName}
/>
{!showCard && index < faqs.length - 1 && (
<div className={cls(
"w-full border-b",
shouldUseLightText ? "border-background/10" : "border-foreground/10",
separatorClassName
)} />
)}
</Fragment>
))}
</div>
</div>
</div> </div>
</div> ))}
</section> </div>
); );
}; };
ContactFaq.displayName = "ContactFaq";
export default ContactFaq; export default ContactFaq;

View File

@@ -1,171 +1,84 @@
"use client"; 'use client';
import ContactForm from "@/components/form/ContactForm"; import React, { useState } from 'react';
import MediaContent from "@/components/shared/MediaContent";
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
import { cls } from "@/lib/utils";
import { useButtonAnimation } from "@/components/hooks/useButtonAnimation";
import { LucideIcon } from "lucide-react";
import { sendContactEmail } from "@/utils/sendContactEmail";
import type { ButtonAnimationType } from "@/types/button";
type ContactSplitBackgroundProps = Extract<
HeroBackgroundVariantProps,
| { variant: "plain" }
| { variant: "animated-grid" }
| { variant: "canvas-reveal" }
| { variant: "cell-wave" }
| { variant: "downward-rays-animated" }
| { variant: "downward-rays-animated-grid" }
| { variant: "downward-rays-static" }
| { variant: "downward-rays-static-grid" }
| { variant: "gradient-bars" }
| { variant: "radial-gradient" }
| { variant: "rotated-rays-animated" }
| { variant: "rotated-rays-animated-grid" }
| { variant: "rotated-rays-static" }
| { variant: "rotated-rays-static-grid" }
| { variant: "sparkles-gradient" }
>;
interface ContactSplitProps { interface ContactSplitProps {
title: string; title: string;
description: string; description: string;
tag: string; imageSrc?: string;
tagIcon?: LucideIcon; className?: string;
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;
} }
const ContactSplit = ({ const ContactSplit: React.FC<ContactSplitProps> = ({
title, title,
description, description,
tag, imageSrc,
tagIcon, className = '',
tagAnimation, }) => {
background, const [formData, setFormData] = useState<Record<string, string>>({
useInvertedBackground, name: '',
imageSrc, email: '',
videoSrc, message: '',
imageAlt = "", });
videoAriaLabel = "Contact section video",
mediaPosition = "right",
mediaAnimation,
inputPlaceholder = "Enter your email",
buttonText = "Sign Up",
termsText = "By clicking Sign Up you're confirming that you agree with our Terms and Conditions.",
onSubmit,
ariaLabel = "Contact section",
className = "",
containerClassName = "",
contentClassName = "",
contactFormClassName = "",
tagClassName = "",
titleClassName = "",
descriptionClassName = "",
formWrapperClassName = "",
formClassName = "",
inputClassName = "",
buttonClassName = "",
buttonTextClassName = "",
termsClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
}: ContactSplitProps) => {
const { containerRef: mediaContainerRef } = useButtonAnimation({ animationType: mediaAnimation });
const handleSubmit = async (email: string) => { const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
try { const { name, value } = e.target;
await sendContactEmail({ email }); setFormData(prev => ({
console.log("Email send successfully"); ...prev,
} catch (error) { [name]: value,
console.error("Failed to send email:", error); }));
} };
};
const contactContent = ( const handleSubmitClick = () => {
<div className="relative card rounded-theme-capped p-6 py-15 md:py-6 flex items-center justify-center"> // Form submission logic would go here
<ContactForm console.log('Form data:', formData);
tag={tag} };
tagIcon={tagIcon}
tagAnimation={tagAnimation} return (
title={title} <div className={`grid grid-cols-2 gap-8 ${className}`}>
description={description} <div>
useInvertedBackground={useInvertedBackground} <h2 className="text-4xl font-bold mb-4">{title}</h2>
inputPlaceholder={inputPlaceholder} <p className="text-gray-600 mb-8">{description}</p>
buttonText={buttonText} <form className="space-y-4">
termsText={termsText} <input
onSubmit={handleSubmit} type="text"
centered={true} name="name"
className={cls("w-full", contactFormClassName)} placeholder="Your Name"
tagClassName={tagClassName} value={formData.name}
titleClassName={titleClassName} onChange={handleChange}
descriptionClassName={descriptionClassName} className="w-full px-4 py-2 border rounded"
formWrapperClassName={cls("w-full md:w-8/10 2xl:w-7/10", formWrapperClassName)} />
formClassName={formClassName} <input
inputClassName={inputClassName} type="email"
buttonClassName={buttonClassName} name="email"
buttonTextClassName={buttonTextClassName} placeholder="Your Email"
termsClassName={termsClassName} value={formData.email}
/> onChange={handleChange}
<div className="absolute inset w-full h-full z-0 rounded-theme-capped overflow-hidden" > className="w-full px-4 py-2 border rounded"
<HeroBackgrounds {...background} /> />
</div> <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> </div>
); )}
</div>
const mediaContent = ( );
<div ref={mediaContainerRef} className={cls("overflow-hidden rounded-theme-capped card h-130", mediaWrapperClassName)}>
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("relative z-1 w-full h-full object-cover", mediaClassName)}
/>
</div>
);
return (
<section aria-label={ariaLabel} className={cls("relative py-20 w-full", useInvertedBackground && "bg-foreground", className)}>
<div className={cls("w-content-width mx-auto relative z-10", containerClassName)}>
<div className={cls("grid grid-cols-1 md:grid-cols-2 gap-6 md:auto-rows-fr", contentClassName)}>
{mediaPosition === "left" && mediaContent}
{contactContent}
{mediaPosition === "right" && mediaContent}
</div>
</div>
</section>
);
}; };
ContactSplit.displayName = "ContactSplit";
export default ContactSplit; export default ContactSplit;

View File

@@ -1,214 +1,80 @@
"use client"; 'use client';
import { useState } from "react"; import React, { useState } from 'react';
import TextAnimation from "@/components/text/TextAnimation";
import Button from "@/components/button/Button";
import Input from "@/components/form/Input";
import Textarea from "@/components/form/Textarea";
import MediaContent from "@/components/shared/MediaContent";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import { useButtonAnimation } from "@/components/hooks/useButtonAnimation";
import { getButtonProps } from "@/lib/buttonUtils";
import type { AnimationType } from "@/components/text/types";
import type { ButtonAnimationType } from "@/types/button";
import {sendContactEmail} from "@/utils/sendContactEmail";
export interface InputField { interface FormInput {
name: string; name: string;
type: string; type: string;
placeholder: string; placeholder: string;
required?: boolean; required?: boolean;
className?: string;
}
export interface TextareaField {
name: string;
placeholder: string;
rows?: number;
required?: boolean;
className?: string;
} }
interface ContactSplitFormProps { interface ContactSplitFormProps {
title: string; title: string;
description: string; description: string;
inputs: InputField[]; inputs: FormInput[];
textarea?: TextareaField; imageSrc?: string;
useInvertedBackground: boolean; className?: string;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
mediaPosition?: "left" | "right";
mediaAnimation: ButtonAnimationType;
buttonText?: string;
onSubmit?: (data: Record<string, string>) => void;
ariaLabel?: string;
className?: string;
containerClassName?: string;
contentClassName?: string;
formCardClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
} }
const ContactSplitForm = ({ const ContactSplitForm: React.FC<ContactSplitFormProps> = ({
title, title,
description, description,
inputs, inputs,
textarea, imageSrc,
useInvertedBackground, className = '',
imageSrc, }) => {
videoSrc, const [formData, setFormData] = useState<Record<string, string>>({
imageAlt = "", ...inputs.reduce((acc, input) => ({ ...acc, [input.name]: '' }), {}),
videoAriaLabel = "Contact section video", });
mediaPosition = "right",
mediaAnimation,
buttonText = "Submit",
onSubmit,
ariaLabel = "Contact section",
className = "",
containerClassName = "",
contentClassName = "",
formCardClassName = "",
titleClassName = "",
descriptionClassName = "",
buttonClassName = "",
buttonTextClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
}: ContactSplitFormProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const { containerRef: mediaContainerRef } = useButtonAnimation({ animationType: mediaAnimation });
// Validate minimum inputs requirement const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
if (inputs.length < 2) { const { name, value } = e.target;
throw new Error("ContactSplitForm requires at least 2 inputs"); setFormData(prev => ({
} ...prev,
[name]: value,
}));
};
// Initialize form data dynamically const handleSubmitClick = () => {
const initialFormData: Record<string, string> = {}; // Form submission logic would go here
inputs.forEach(input => { console.log('Form data:', formData);
initialFormData[input.name] = ""; };
});
if (textarea) {
initialFormData[textarea.name] = "";
}
const [formData, setFormData] = useState(initialFormData); return (
<div className={`grid grid-cols-2 gap-8 ${className}`}>
const handleSubmit = async (e: React.FormEvent) => { <div>
e.preventDefault(); <h2 className="text-4xl font-bold mb-4">{title}</h2>
try { <p className="text-gray-600 mb-8">{description}</p>
await sendContactEmail({ formData }); <form className="space-y-4">
console.log("Email send successfully"); {inputs.map(input => (
setFormData(initialFormData); <input
} catch (error) { key={input.name}
console.error("Failed to send email:", error); type={input.type}
} name={input.name}
}; placeholder={input.placeholder}
value={formData[input.name]}
const getButtonConfigProps = () => { onChange={handleChange}
if (theme.defaultButtonVariant === "hover-bubble") { required={input.required}
return { bgClassName: "w-full" }; className="w-full px-4 py-2 border rounded"
}
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)}
/> />
))}
<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> </div>
); )}
</div>
return ( );
<section aria-label={ariaLabel} className={cls("relative py-20 w-full", useInvertedBackground && "bg-foreground", className)}>
<div className={cls("w-content-width mx-auto", containerClassName)}>
<div className={cls("grid grid-cols-1 md:grid-cols-2 gap-6 md:auto-rows-fr", contentClassName)}>
{mediaPosition === "left" && mediaContent}
{formContent}
{mediaPosition === "right" && mediaContent}
</div>
</div>
</section>
);
}; };
ContactSplitForm.displayName = "ContactSplitForm";
export default ContactSplitForm; 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"; interface Feature {
import Button from "@/components/button/Button"; id: string;
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import { BentoGlobe } from "@/components/bento/BentoGlobe";
import BentoIconInfoCards from "@/components/bento/BentoIconInfoCards";
import BentoAnimatedBarChart from "@/components/bento/BentoAnimatedBarChart";
import Bento3DStackCards from "@/components/bento/Bento3DStackCards";
import Bento3DTaskList, { type TaskItem } from "@/components/bento/Bento3DTaskList";
import BentoOrbitingIcons, { type OrbitingItem } from "@/components/bento/BentoOrbitingIcons";
import BentoMap from "@/components/bento/BentoMap";
import BentoMarquee from "@/components/bento/BentoMarquee";
import BentoLineChart from "@/components/bento/BentoLineChart/BentoLineChart";
import BentoPhoneAnimation, { type PhoneApp, type PhoneApps8 } from "@/components/bento/BentoPhoneAnimation";
import BentoChatAnimation, { type ChatExchange } from "@/components/bento/BentoChatAnimation";
import Bento3DCardGrid from "@/components/bento/Bento3DCardGrid";
import BentoRevealIcon from "@/components/bento/BentoRevealIcon";
import BentoTimeline, { type TimelineItem } from "@/components/bento/BentoTimeline";
import BentoMediaStack, { type MediaStackItem } from "@/components/bento/BentoMediaStack";
import type { LucideIcon } from "lucide-react";
export type { PhoneApp, PhoneApps8, ChatExchange, TimelineItem, MediaStackItem };
import type { ButtonConfig, CardAnimationTypeWith3D, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type BentoAnimationType = Exclude<CardAnimationTypeWith3D, "depth-3d" | "scale-rotate">;
export type BentoInfoItem = {
icon: LucideIcon;
label: string;
value: string;
};
export type Bento3DItem = {
icon: LucideIcon;
title: string;
subtitle: string;
detail: string;
};
type BaseFeatureCard = {
title: string; title: string;
description: string; description: string;
button?: ButtonConfig; icon?: any;
};
export type FeatureCard = BaseFeatureCard & (
| {
bentoComponent: "icon-info-cards";
items: BentoInfoItem[];
}
| {
bentoComponent: "3d-stack-cards";
items: [Bento3DItem, Bento3DItem, Bento3DItem];
}
| {
bentoComponent: "3d-task-list";
title: string;
items: TaskItem[];
}
| {
bentoComponent: "orbiting-icons";
centerIcon: LucideIcon;
items: OrbitingItem[];
}
| ({
bentoComponent: "marquee";
centerIcon: LucideIcon;
} & (
| { variant: "text"; texts: string[] }
| { variant: "icon"; icons: LucideIcon[] }
))
| {
bentoComponent: "globe" | "animated-bar-chart" | "map" | "line-chart";
items?: never;
}
| {
bentoComponent: "3d-card-grid";
items: [{ name: string; icon: LucideIcon }, { name: string; icon: LucideIcon }, { name: string; icon: LucideIcon }, { name: string; icon: LucideIcon }];
centerIcon: LucideIcon;
}
| {
bentoComponent: "phone";
statusIcon: LucideIcon;
alertIcon: LucideIcon;
alertTitle: string;
alertMessage: string;
apps: PhoneApps8;
}
| {
bentoComponent: "chat";
aiIcon: LucideIcon;
userIcon: LucideIcon;
exchanges: ChatExchange[];
placeholder: string;
}
| {
bentoComponent: "reveal-icon";
icon: LucideIcon;
}
| {
bentoComponent: "timeline";
heading: string;
subheading: string;
items: [TimelineItem, TimelineItem, TimelineItem];
completedLabel: string;
}
| {
bentoComponent: "media-stack";
items: [MediaStackItem, MediaStackItem, MediaStackItem];
}
);
interface FeatureBentoProps {
features: FeatureCard[];
carouselMode?: "auto" | "buttons";
animationType: BentoAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
cardTitleClassName?: string;
cardDescriptionClassName?: string;
cardButtonClassName?: string;
cardButtonTextClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
} }
const FeatureBento = ({ interface FeatureBentoProps extends Omit<CardStackProps, 'children'> {
features, features: Feature[];
carouselMode = "buttons", }
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Feature section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
cardButtonClassName = "",
cardButtonTextClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: FeatureBentoProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const getBentoComponent = (feature: FeatureCard) => { export const FeatureBento: React.FC<FeatureBentoProps> = ({
switch (feature.bentoComponent) { features,
case "globe": ...cardStackProps
return ( }) => {
<div className="relative w-full h-full min-h-0" style={{ const featureElements = features.map(feature => (
maskImage: "linear-gradient(to right, transparent 0%, black 20%, black 80%, transparent 100%), linear-gradient(to bottom, black 40%, transparent 100%)", <div key={feature.id} className="feature-card">
WebkitMaskImage: "linear-gradient(to right, transparent 0%, black 20%, black 80%, transparent 100%), linear-gradient(to bottom, black 40%, transparent 100%)", {feature.icon && <div className="feature-icon">{feature.icon}</div>}
maskComposite: "intersect", <h3>{feature.title}</h3>
WebkitMaskComposite: "source-in" <p>{feature.description}</p>
}}> </div>
<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} />;
}
};
return ( return (
<CardStack <CardStack {...cardStackProps}>
mode={carouselMode} {featureElements}
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> </CardStack>
); );
}; };
FeatureBento.displayName = "FeatureBento";
export default 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"; interface Feature {
import CardStack from "@/components/cardStack/CardStack"; id: string;
import MediaContent from "@/components/shared/MediaContent"; title: string;
import Tag from "@/components/shared/Tag"; description: string;
import Button from "@/components/button/Button"; imageSrc?: string;
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 FeatureCardItemProps { interface FeatureCardMediaProps extends Omit<CardStackProps, 'children'> {
feature: FeatureCard; features: Feature[];
shouldUseLightText: boolean;
useInvertedBackground: InvertedBackground;
itemClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
tagClassName?: string;
contentClassName?: string;
cardTitleClassName?: string;
cardDescriptionClassName?: string;
cardButtonContainerClassName?: string;
cardButtonClassName?: string;
cardButtonTextClassName?: string;
} }
const FeatureCardItem = memo(({ export const FeatureCardMedia: React.FC<FeatureCardMediaProps> = ({
feature, features,
shouldUseLightText, ...cardStackProps
useInvertedBackground, }) => {
itemClassName = "", const featureElements = features.map(feature => (
mediaWrapperClassName = "", <div key={feature.id} className="feature-card">
mediaClassName = "", {feature.imageSrc && (
tagClassName = "", <img src={feature.imageSrc} alt={feature.title} className="feature-image" />
contentClassName = "", )}
cardTitleClassName = "", <h3>{feature.title}</h3>
cardDescriptionClassName = "", <p>{feature.description}</p>
cardButtonContainerClassName = "", </div>
cardButtonClassName = "", ));
cardButtonTextClassName = "",
}: FeatureCardItemProps) => {
const theme = useTheme();
return ( return (
<article <CardStack {...cardStackProps}>
className={cls("relative h-full flex flex-col gap-6 cursor-pointer group", itemClassName)} {featureElements}
onClick={feature.onCardClick} </CardStack>
role="article" );
aria-label={feature.title}
>
<div className={cls("relative w-full aspect-square overflow-hidden rounded-theme-capped", mediaWrapperClassName)}>
<MediaContent
imageSrc={feature.imageSrc}
videoSrc={feature.videoSrc}
imageAlt={feature.imageAlt || feature.title}
videoAriaLabel={feature.videoAriaLabel || feature.title}
imageClassName={cls("w-full h-full object-cover transition-transform duration-500 ease-in-out group-hover:scale-105", mediaClassName)}
/>
<div className="absolute top-4 right-4">
<Tag
text={feature.tag}
useInvertedBackground={useInvertedBackground}
className={tagClassName}
/>
</div>
</div>
<div className={cls("relative z-1 card rounded-theme-capped p-6 flex flex-col gap-2 flex-1", contentClassName)}>
<h3 className={cls(
"text-xl md:text-2xl font-medium leading-tight",
shouldUseLightText ? "text-background" : "text-foreground",
cardTitleClassName
)}>
{feature.title}
</h3>
<p className={cls(
"text-base leading-tight",
shouldUseLightText ? "text-background/75" : "text-foreground/75",
cardDescriptionClassName
)}>
{feature.description}
</p>
{feature.buttons && feature.buttons.length > 0 && (
<div className={cls("flex flex-wrap gap-4 max-md:justify-center mt-2", cardButtonContainerClassName)}>
{feature.buttons.slice(0, 2).map((button, index) => (
<Button
key={`${button.text}-${index}`}
{...getButtonProps(button, index, theme.defaultButtonVariant, cardButtonClassName, cardButtonTextClassName)}
/>
))}
</div>
)}
</div>
</article>
);
});
FeatureCardItem.displayName = "FeatureCardItem";
const FeatureCardMedia = ({
features,
carouselMode = "buttons",
uniformGridCustomHeightClasses,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Features section",
className = "",
containerClassName = "",
itemClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
tagClassName = "",
contentClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
cardButtonContainerClassName = "",
cardButtonClassName = "",
cardButtonTextClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: FeatureCardMediaProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<CardStack
mode={carouselMode}
gridVariant="uniform-all-items-equal"
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
ariaLabel={ariaLabel}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
>
{features.map((feature) => (
<FeatureCardItem
key={feature.id}
feature={feature}
shouldUseLightText={shouldUseLightText}
useInvertedBackground={useInvertedBackground}
itemClassName={itemClassName}
mediaWrapperClassName={mediaWrapperClassName}
mediaClassName={mediaClassName}
tagClassName={tagClassName}
contentClassName={contentClassName}
cardTitleClassName={cardTitleClassName}
cardDescriptionClassName={cardDescriptionClassName}
cardButtonContainerClassName={cardButtonContainerClassName}
cardButtonClassName={cardButtonClassName}
cardButtonTextClassName={cardButtonTextClassName}
/>
))}
</CardStack>
);
}; };
FeatureCardMedia.displayName = "FeatureCardMedia";
export default FeatureCardMedia; 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"; interface Feature {
import MediaContent from "@/components/shared/MediaContent"; id: string;
import Button from "@/components/button/Button";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, GridVariant, CardAnimationTypeWith3D, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type FeatureCard = {
title: string; title: string;
description: 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 = ({ interface FeatureCardOneProps extends Omit<CardStackProps, 'children'> {
features, features: Feature[];
carouselMode = "buttons", }
gridVariant,
uniformGridCustomHeightClasses,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Feature section",
className = "",
containerClassName = "",
cardClassName = "",
mediaClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
cardButtonClassName = "",
cardButtonTextClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: FeatureCardOneProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const getButtonConfigProps = () => { export const FeatureCardOne: React.FC<FeatureCardOneProps> = ({
if (theme.defaultButtonVariant === "hover-bubble") { features,
return { bgClassName: "w-full" }; ...cardStackProps
} }) => {
if (theme.defaultButtonVariant === "icon-arrow") { const featureElements = features.map(feature => (
return { className: "justify-between" }; <div key={feature.id} className="feature-card">
} <h3>{feature.title}</h3>
return {}; <p>{feature.description}</p>
}; </div>
));
return ( return (
<CardStack <CardStack {...cardStackProps}>
mode={carouselMode} {featureElements}
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> </CardStack>
); );
}; };
FeatureCardOne.displayName = "FeatureCardOne";
export default 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"; interface Feature {
import PricingFeatureList from "@/components/shared/PricingFeatureList"; id: string;
import { useCardAnimation } from "@/components/cardStack/hooks/useCardAnimation"; title: string;
import { Check, X } from "lucide-react"; description: string;
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;
} }
const FeatureCardSixteen = ({ interface FeatureCardSixteenProps {
negativeCard, features: Feature[];
positiveCard, animationConfig: CardAnimationConfig;
animationType, className?: string;
title, }
titleSegments,
description,
textboxLayout,
useInvertedBackground,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
ariaLabel = "Feature comparison section",
className = "",
containerClassName = "",
textBoxTitleClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
textBoxDescriptionClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
gridClassName = "",
cardClassName = "",
itemsListClassName = "",
itemClassName = "",
itemIconClassName = "",
itemTextClassName = "",
}: FeatureCardSixteenProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const { itemRefs, containerRef, perspectiveRef } = useCardAnimation({
animationType,
itemCount: 2,
isGrid: true,
supports3DAnimation: true,
gridVariant: "uniform-all-items-equal"
});
const cards = [ export const FeatureCardSixteen: React.FC<FeatureCardSixteenProps> = ({
{ ...negativeCard, variant: "negative" as const }, features,
{ ...positiveCard, variant: "positive" as const }, animationConfig,
]; className = '',
}) => {
const cardsRef = useRef<HTMLDivElement[]>([]);
return ( useCardAnimation(cardsRef, animationConfig);
<section
ref={containerRef} const setCardRef = useCallback((index: number, el: HTMLDivElement | null) => {
aria-label={ariaLabel} if (el) {
className={cls("relative py-20 w-full", useInvertedBackground && "bg-foreground", className)} 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)}> <h3>{feature.title}</h3>
<CardStackTextBox <p>{feature.description}</p>
title={title} </div>
titleSegments={titleSegments} ))}
description={description} </div>
tag={tag} );
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
/>
<div
ref={perspectiveRef}
className={cls(
"relative mx-auto w-full md:w-60 grid grid-cols-1 gap-6",
cards.length >= 2 ? "md:grid-cols-2" : "md:grid-cols-1",
gridClassName
)}
>
{cards.map((card, index) => (
<div
key={card.variant}
ref={(el) => { itemRefs.current[index] = el; }}
className={cls(
"relative h-full card rounded-theme-capped p-6",
cardClassName
)}
>
<div className={cls("flex flex-col gap-6", card.variant === "negative" && "opacity-50")}>
<PricingFeatureList
features={card.items}
icon={card.variant === "positive" ? Check : X}
shouldUseLightText={shouldUseLightText}
className={itemsListClassName}
featureItemClassName={itemClassName}
featureIconWrapperClassName=""
featureIconClassName={itemIconClassName}
featureTextClassName={cls("truncate", itemTextClassName)}
/>
</div>
</div>
))}
</div>
</div>
</section>
);
}; };
FeatureCardSixteen.displayName = "FeatureCardSixteen";
export default FeatureCardSixteen; 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"; interface Feature {
import MediaContent from "@/components/shared/MediaContent"; id: string;
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { CardAnimationTypeWith3D, TitleSegment, ButtonConfig, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
interface MediaItem {
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
}
type FeatureCard = {
title: string; title: string;
description: 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, features,
carouselMode = "buttons", ...cardStackProps
uniformGridCustomHeightClasses, }) => {
animationType, const featureElements = features.map(feature => (
title, <div key={feature.id} className="feature-card">
titleSegments, <h3>{feature.title}</h3>
description, <p>{feature.description}</p>
tag, </div>
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);
return ( return (
<CardStack <CardStack {...cardStackProps}>
mode={carouselMode} {featureElements}
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> </CardStack>
); );
}; };
FeatureCardTwentyFive.displayName = "FeatureCardTwentyFive";
export default 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"; interface Feature {
import { Plus } from "lucide-react";
import CardStack from "@/components/cardStack/CardStack";
import MediaContent from "@/components/shared/MediaContent";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, GridVariant, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type FeatureCard = {
id: string; id: string;
title: 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; 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, features,
carouselMode = "buttons", ...cardStackProps
gridVariant, }) => {
uniformGridCustomHeightClasses = "min-h-none", const featureElements = features.map(feature => (
animationType, <div key={feature.id} className="feature-card">
title, <h3>{feature.title}</h3>
titleSegments, <p>{feature.description}</p>
description, </div>
tag, ));
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Feature section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: FeatureCardTwentySevenProps) => {
return ( return (
<CardStack <CardStack {...cardStackProps}>
mode={carouselMode} {featureElements}
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> </CardStack>
); );
}; };
FeatureCardTwentySeven.displayName = "FeatureCardTwentySeven";
export default 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"; interface Feature {
import { ArrowRight } from "lucide-react"; id: string;
import CardStack from "@/components/cardStack/CardStack"; title: string;
import MediaContent from "@/components/shared/MediaContent"; description: string;
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 FeatureCardItemProps { interface FeatureCardTwentyThreeProps extends Omit<CardStackProps, 'children'> {
feature: FeatureItem; features: Feature[];
shouldUseLightText: boolean;
useInvertedBackground: InvertedBackground;
itemClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
cardClassName?: string;
cardTitleClassName?: string;
tagsContainerClassName?: string;
tagClassName?: string;
arrowClassName?: string;
} }
const FeatureCardItem = memo(({ export const FeatureCardTwentyThree: React.FC<FeatureCardTwentyThreeProps> = ({
feature, features,
shouldUseLightText, ...cardStackProps
useInvertedBackground, }) => {
itemClassName = "", const featureElements = features.map(feature => (
mediaWrapperClassName = "", <div key={feature.id} className="feature-card">
mediaClassName = "", <h3>{feature.title}</h3>
cardClassName = "", <p>{feature.description}</p>
cardTitleClassName = "", </div>
tagsContainerClassName = "", ));
tagClassName = "",
arrowClassName = "",
}: FeatureCardItemProps) => {
return (
<article
className={cls("relative h-full flex flex-col gap-6 cursor-pointer group", itemClassName)}
onClick={feature.onFeatureClick}
role="article"
aria-label={feature.title}
>
<div className={cls("relative w-full aspect-square overflow-hidden rounded-theme-capped", mediaWrapperClassName)}>
<MediaContent
imageSrc={feature.imageSrc}
videoSrc={feature.videoSrc}
imageAlt={feature.imageAlt || feature.title}
videoAriaLabel={feature.videoAriaLabel || feature.title}
imageClassName={cls("w-full h-full object-cover transition-transform duration-500 ease-in-out group-hover:scale-105", mediaClassName)}
/>
</div>
<div className={cls("relative z-1 card rounded-theme-capped p-5 flex-1 flex flex-col justify-between gap-4", cardClassName)}> return (
<h3 className={cls( <CardStack {...cardStackProps}>
"text-xl md:text-2xl font-medium leading-tight", {featureElements}
shouldUseLightText ? "text-background" : "text-foreground", </CardStack>
cardTitleClassName );
)}>
{feature.title}
</h3>
<div className="flex items-center justify-between gap-4">
<div className={cls("flex items-center gap-2 flex-wrap", tagsContainerClassName)}>
{feature.tags.map((tag, index) => (
<Tag
key={index}
text={tag}
useInvertedBackground={useInvertedBackground}
className={tagClassName}
/>
))}
</div>
<ArrowRight
className={cls(
"h-[var(--text-base)] w-auto shrink-0 transition-transform duration-300 group-hover:-rotate-45",
shouldUseLightText ? "text-background" : "text-foreground",
arrowClassName
)}
strokeWidth={1.5}
/>
</div>
</div>
</article>
);
});
FeatureCardItem.displayName = "FeatureCardItem";
const FeatureCardTwentyThree = ({
features,
carouselMode = "buttons",
uniformGridCustomHeightClasses,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Features section",
className = "",
containerClassName = "",
itemClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
cardClassName = "",
cardTitleClassName = "",
tagsContainerClassName = "",
tagClassName = "",
arrowClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: FeatureCardTwentyThreeProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<CardStack
mode={carouselMode}
gridVariant="uniform-all-items-equal"
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
ariaLabel={ariaLabel}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
>
{features.map((feature) => (
<FeatureCardItem
key={feature.id}
feature={feature}
shouldUseLightText={shouldUseLightText}
useInvertedBackground={useInvertedBackground}
itemClassName={itemClassName}
mediaWrapperClassName={mediaWrapperClassName}
mediaClassName={mediaClassName}
cardClassName={cardClassName}
cardTitleClassName={cardTitleClassName}
tagsContainerClassName={tagsContainerClassName}
tagClassName={tagClassName}
arrowClassName={arrowClassName}
/>
))}
</CardStack>
);
}; };
FeatureCardTwentyThree.displayName = "FeatureCardTwentyThree";
export default FeatureCardTwentyThree; 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"; interface Feature {
import FeatureBorderGlowItem from "./FeatureBorderGlowItem"; id: string;
import { shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type {
ButtonConfig,
CardAnimationType,
TitleSegment,
ButtonAnimationType,
} from "@/components/cardStack/types";
import type {
TextboxLayout,
InvertedBackground,
} from "@/providers/themeProvider/config/constants";
interface FeatureCard {
icon: LucideIcon;
title: string; title: string;
description: string; description: string;
} }
interface FeatureBorderGlowProps { interface FeatureBorderGlowProps extends Omit<CardStackProps, 'children'> {
features: FeatureCard[]; features: Feature[];
carouselMode?: "auto" | "buttons";
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
iconContainerClassName?: string;
iconClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
cardTitleClassName?: string;
cardDescriptionClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
} }
const FeatureBorderGlow = ({ export const FeatureBorderGlow: React.FC<FeatureBorderGlowProps> = ({
features, features,
carouselMode = "buttons", ...cardStackProps
uniformGridCustomHeightClasses = "min-h-75 2xl:min-h-85", }) => {
animationType, const featureElements = features.map(feature => (
title, <div key={feature.id} className="feature-card">
titleSegments, <h3>{feature.title}</h3>
description, <p>{feature.description}</p>
tag, </div>
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
);
return ( return (
<CardStack <CardStack {...cardStackProps}>
mode={carouselMode} {featureElements}
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> </CardStack>
); );
}; };
FeatureBorderGlow.displayName = "FeatureBorderGlow";
export default 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"; interface Feature {
import { useRef, useCallback, useState } from "react";
import CardStack from "@/components/cardStack/CardStack";
import FeatureCardThreeItem from "./FeatureCardThreeItem";
import { useDynamicDimensions } from "./useDynamicDimensions";
import { useClickOutside } from "@/hooks/useClickOutside";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, GridVariant, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type FeatureCard = {
id: string; id: string;
title: string; title: string;
description: 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, features,
carouselMode = "buttons", ...cardStackProps
gridVariant, }) => {
uniformGridCustomHeightClasses, const featureElements = features.map(feature => (
animationType, <div key={feature.id} className="feature-card">
title, <h3>{feature.title}</h3>
titleSegments, <p>{feature.description}</p>
description, </div>
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",
});
return ( return (
<div ref={containerRef}> <CardStack {...cardStackProps}>
<CardStack {featureElements}
mode={carouselMode} </CardStack>
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>
); );
}; };
FeatureCardThree.displayName = "FeatureCardThree";
export default 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"; interface Feature {
import FeatureHoverPatternItem from "./FeatureHoverPatternItem"; id: string;
import { shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type {
ButtonConfig,
CardAnimationType,
TitleSegment,
ButtonAnimationType,
} from "@/components/cardStack/types";
import type {
TextboxLayout,
InvertedBackground,
} from "@/providers/themeProvider/config/constants";
interface FeatureCard {
icon: LucideIcon;
title: string; title: string;
description: string; description: string;
button?: ButtonConfig;
} }
interface FeatureHoverPatternProps { interface FeatureHoverPatternProps extends Omit<CardStackProps, 'children'> {
features: FeatureCard[]; features: Feature[];
carouselMode?: "auto" | "buttons";
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
iconContainerClassName?: string;
iconClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
cardTitleClassName?: string;
cardDescriptionClassName?: string;
gradientClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
cardButtonClassName?: string;
cardButtonTextClassName?: string;
} }
const FeatureHoverPattern = ({ export const FeatureHoverPattern: React.FC<FeatureHoverPatternProps> = ({
features, features,
carouselMode = "buttons", ...cardStackProps
uniformGridCustomHeightClasses = "min-h-85 2xl:min-h-95", }) => {
animationType, const featureElements = features.map(feature => (
title, <div key={feature.id} className="feature-card">
titleSegments, <h3>{feature.title}</h3>
description, <p>{feature.description}</p>
tag, </div>
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
);
return ( return (
<CardStack <CardStack {...cardStackProps}>
mode={carouselMode} {featureElements}
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> </CardStack>
); );
}; };
FeatureHoverPattern.displayName = "FeatureHoverPattern";
export default 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"; interface Metric {
import CardStackTextBox from "@/components/cardStack/CardStackTextBox"; id: string;
import MediaContent from "@/components/shared/MediaContent"; value: string;
import { useCardAnimation } from "@/components/cardStack/hooks/useCardAnimation"; title: string;
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 MetricCardElevenProps { interface MetricCardElevenProps {
metrics: Metric[]; metrics: Metric[];
animationType: CardAnimationType; animationConfig: CardAnimationConfig;
title: string; className?: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
gridClassName?: string;
cardClassName?: string;
valueClassName?: string;
cardTitleClassName?: string;
cardDescriptionClassName?: string;
mediaCardClassName?: string;
mediaClassName?: string;
} }
interface MetricTextCardProps { export const MetricCardEleven: React.FC<MetricCardElevenProps> = ({
metric: Metric; metrics,
shouldUseLightText: boolean; animationConfig,
cardClassName?: string; className = '',
valueClassName?: string; }) => {
cardTitleClassName?: string; const cardsRef = useRef<HTMLDivElement[]>([]);
cardDescriptionClassName?: string;
}
interface MetricMediaCardProps { useCardAnimation(cardsRef, animationConfig);
metric: Metric;
mediaCardClassName?: string;
mediaClassName?: string;
}
const MetricTextCard = memo(({ const setCardRef = useCallback((index: number, el: HTMLDivElement | null) => {
metric, if (el) {
shouldUseLightText, cardsRef.current[index] = el;
cardClassName = "", }
valueClassName = "", }, []);
cardTitleClassName = "",
cardDescriptionClassName = "",
}: MetricTextCardProps) => {
return (
<div className={cls(
"relative w-full min-w-0 max-w-full h-full card text-foreground rounded-theme-capped flex flex-col justify-between p-6 md:p-8",
cardClassName
)}>
<h3 className={cls(
"text-5xl md:text-6xl font-medium leading-tight truncate",
shouldUseLightText ? "text-background" : "text-foreground",
valueClassName
)}>
{metric.value}
</h3>
<div className="w-full min-w-0 flex flex-col gap-2 mt-auto"> return (
<p className={cls( <div className={`metric-cards ${className}`}>
"text-xl md:text-2xl font-medium leading-tight truncate", {metrics.map((metric, index) => (
shouldUseLightText ? "text-background" : "text-foreground", <div
cardTitleClassName key={metric.id}
)}> ref={el => setCardRef(index, el)}
{metric.title} className="metric-card"
</p>
<div className="w-full h-px bg-accent" />
<p className={cls(
"text-base truncate leading-tight",
shouldUseLightText ? "text-background/75" : "text-foreground/75",
cardDescriptionClassName
)}>
{metric.description}
</p>
</div>
</div>
);
});
MetricTextCard.displayName = "MetricTextCard";
const MetricMediaCard = memo(({
metric,
mediaCardClassName = "",
mediaClassName = "",
}: MetricMediaCardProps) => {
return (
<div className={cls(
"relative h-full rounded-theme-capped overflow-hidden",
mediaCardClassName
)}>
<MediaContent
imageSrc={metric.imageSrc}
videoSrc={metric.videoSrc}
imageAlt={metric.imageAlt}
videoAriaLabel={metric.videoAriaLabel}
imageClassName={cls("w-full h-full object-cover", mediaClassName)}
/>
</div>
);
});
MetricMediaCard.displayName = "MetricMediaCard";
const MetricCardEleven = ({
metrics,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Metrics section",
className = "",
containerClassName = "",
textBoxClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
gridClassName = "",
cardClassName = "",
valueClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
mediaCardClassName = "",
mediaClassName = "",
}: MetricCardElevenProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
// Inner grid for each metric item (text + media side by side)
const innerGridCols = "grid-cols-2";
const { itemRefs } = useCardAnimation({ animationType, itemCount: metrics.length });
return (
<section
aria-label={ariaLabel}
className={cls("relative py-20 w-full", useInvertedBackground && "bg-foreground", className)}
> >
<div className={cls("w-content-width mx-auto", containerClassName)}> <div className="metric-value">{metric.value}</div>
<CardStackTextBox <div className="metric-title">{metric.title}</div>
title={title} </div>
titleSegments={titleSegments} ))}
description={description} </div>
tag={tag} );
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
/>
<div className={cls(
"grid gap-4 mt-8 md:mt-12",
metrics.length === 1 ? "grid-cols-1" : "grid-cols-1 md:grid-cols-2",
gridClassName
)}>
{metrics.map((metric, index) => {
const isLastItem = index === metrics.length - 1;
const isOddTotal = metrics.length % 2 !== 0;
const isSingleItem = metrics.length === 1;
const shouldSpanFull = isSingleItem || (isLastItem && isOddTotal);
// On mobile, even items (2nd, 4th, 6th - index 1, 3, 5) have media first
const isEvenItem = (index + 1) % 2 === 0;
return (
<div
key={`${metric.id}-${index}`}
ref={(el) => { itemRefs.current[index] = el; }}
className={cls(
"grid gap-4",
innerGridCols,
shouldSpanFull && "md:col-span-2"
)}
>
<MetricTextCard
metric={metric}
shouldUseLightText={shouldUseLightText}
cardClassName={cls(
shouldSpanFull ? "aspect-square md:aspect-video" : "aspect-square",
isEvenItem && "order-2 md:order-1",
cardClassName
)}
valueClassName={valueClassName}
cardTitleClassName={cardTitleClassName}
cardDescriptionClassName={cardDescriptionClassName}
/>
<MetricMediaCard
metric={metric}
mediaCardClassName={cls(
shouldSpanFull ? "aspect-square md:aspect-video" : "aspect-square",
isEvenItem && "order-1 md:order-2",
mediaCardClassName
)}
mediaClassName={mediaClassName}
/>
</div>
);
})}
</div>
</div>
</section>
);
}; };
MetricCardEleven.displayName = "MetricCardEleven";
export default MetricCardEleven; 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"; interface Metric {
import CardStack from "@/components/cardStack/CardStack"; id: string;
import { cls, shouldUseInvertedText } from "@/lib/utils"; value: string;
import { useTheme } from "@/providers/themeProvider/ThemeProvider"; title: string;
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 MetricCardItemProps { interface MetricCardOneProps extends Omit<CardStackProps, 'children'> {
metric: Metric; metrics: Metric[];
shouldUseLightText: boolean;
cardClassName?: string;
valueClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
iconContainerClassName?: string;
iconClassName?: string;
} }
const MetricCardItem = memo(({ export const MetricCardOne: React.FC<MetricCardOneProps> = ({
metric, metrics,
shouldUseLightText, ...cardStackProps
cardClassName = "", }) => {
valueClassName = "", const metricElements = metrics.map(metric => (
titleClassName = "", <div key={metric.id} className="metric-card">
descriptionClassName = "", <div className="metric-value">{metric.value}</div>
iconContainerClassName = "", <div className="metric-title">{metric.title}</div>
iconClassName = "", </div>
}: MetricCardItemProps) => { ));
return (
<div className={cls("relative w-full min-w-0 h-full card text-foreground rounded-theme-capped p-6 flex flex-col items-center justify-center gap-0", cardClassName)}>
<h2
className={cls("relative z-1 w-full text-9xl font-foreground font-medium leading-[1.1] truncate text-center", valueClassName)}
style={{
backgroundImage: shouldUseLightText
? `linear-gradient(to bottom, var(--color-background) 0%, var(--color-background) 20%, transparent 72%, transparent 80%, transparent 100%)`
: `linear-gradient(to bottom, var(--color-foreground) 0%, var(--color-foreground) 20%, transparent 72%, transparent 80%, transparent 100%)`,
WebkitBackgroundClip: "text",
backgroundClip: "text",
WebkitTextFillColor: "transparent",
color: "transparent",
}}
>
{metric.value}
</h2>
<p className={cls("relative w-full z-1 mt-[calc(var(--text-4xl)*-0.75)] md:mt-[calc(var(--text-4xl)*-1.15)] text-4xl font-medium text-center truncate", shouldUseLightText ? "text-background" : "text-foreground", titleClassName)}>
{metric.title}
</p>
<p className={cls("relative line-clamp-2 z-1 max-w-9/10 md:max-w-7/10 text-base text-center leading-[1.1] mt-2", shouldUseLightText ? "text-background" : "text-foreground", descriptionClassName)}>
{metric.description}
</p>
<div className={cls("absolute! z-1 left-6 bottom-6 h-10 aspect-square primary-button rounded-theme flex items-center justify-center", iconContainerClassName)}>
<metric.icon className={cls("h-4/10 text-primary-cta-text", iconClassName)} />
</div>
</div>
);
});
MetricCardItem.displayName = "MetricCardItem"; return (
<CardStack {...cardStackProps}>
const MetricCardOne = ({ {metricElements}
metrics, </CardStack>
carouselMode = "buttons", );
gridVariant,
uniformGridCustomHeightClasses,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Metrics section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
valueClassName = "",
titleClassName = "",
descriptionClassName = "",
iconContainerClassName = "",
iconClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: MetricCardOneProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const customUniformHeight = gridVariant === "uniform-all-items-equal"
? "min-h-70 2xl:min-h-80"
: uniformGridCustomHeightClasses;
return (
<CardStack
useInvertedBackground={useInvertedBackground}
mode={carouselMode}
gridVariant={gridVariant}
uniformGridCustomHeightClasses={customUniformHeight}
animationType={animationType}
supports3DAnimation={true}
carouselThreshold={4}
carouselItemClassName="w-carousel-item-3!"
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{metrics.map((metric, index) => (
<MetricCardItem
key={`${metric.id}-${index}`}
metric={metric}
shouldUseLightText={shouldUseLightText}
cardClassName={cardClassName}
valueClassName={valueClassName}
titleClassName={titleClassName}
descriptionClassName={descriptionClassName}
iconContainerClassName={iconContainerClassName}
iconClassName={iconClassName}
/>
))}
</CardStack>
);
}; };
MetricCardOne.displayName = "MetricCardOne";
export default MetricCardOne; 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"; interface Metric {
import CardStack from "@/components/cardStack/CardStack"; id: string;
import PricingFeatureList from "@/components/shared/PricingFeatureList"; value: string;
import { cls, shouldUseInvertedText } from "@/lib/utils"; title: string;
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 MetricCardItemProps { interface MetricCardSevenProps extends Omit<CardStackProps, 'children'> {
metric: Metric; metrics: Metric[];
shouldUseLightText: boolean;
cardClassName?: string;
valueClassName?: string;
metricTitleClassName?: string;
featuresClassName?: string;
featureItemClassName?: string;
} }
const MetricCardItem = memo(({ export const MetricCardSeven: React.FC<MetricCardSevenProps> = ({
metric, metrics,
shouldUseLightText, ...cardStackProps
cardClassName = "", }) => {
valueClassName = "", const metricElements = metrics.map(metric => (
metricTitleClassName = "", <div key={metric.id} className="metric-card">
featuresClassName = "", <div className="metric-value">{metric.value}</div>
featureItemClassName = "", <div className="metric-title">{metric.title}</div>
}: MetricCardItemProps) => { </div>
return ( ));
<div className={cls("relative h-full card text-foreground rounded-theme-capped p-6 flex flex-col justify-between gap-4", cardClassName)}>
<div className="flex flex-col gap-0" >
<h3 className={cls("relative z-1 text-9xl md:text-8xl font-medium truncate", shouldUseLightText ? "text-background" : "text-foreground", valueClassName)}>
{metric.value}
</h3>
<p className={cls("relative z-1 text-2xl md:text-xl truncate", shouldUseLightText ? "text-background" : "text-foreground", metricTitleClassName)}>
{metric.title}
</p>
</div>
<div className="pt-4 border-t border-t-accent" >
{metric.items.length > 0 && (
<PricingFeatureList
features={metric.items}
shouldUseLightText={shouldUseLightText}
className={cls("mt-1", featuresClassName)}
featureItemClassName={featureItemClassName}
/>
)}
</div>
</div>
);
});
MetricCardItem.displayName = "MetricCardItem"; return (
<CardStack {...cardStackProps}>
const MetricCardSeven = ({ {metricElements}
metrics, </CardStack>
carouselMode = "buttons", );
uniformGridCustomHeightClasses,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Metrics section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
valueClassName = "",
metricTitleClassName = "",
featuresClassName = "",
featureItemClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: MetricCardSevenProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const customUniformHeight = uniformGridCustomHeightClasses || "min-h-70 2xl:min-h-80";
return (
<CardStack
useInvertedBackground={useInvertedBackground}
mode={carouselMode}
gridVariant="uniform-all-items-equal"
uniformGridCustomHeightClasses={customUniformHeight}
animationType={animationType}
supports3DAnimation={true}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{metrics.map((metric, index) => (
<MetricCardItem
key={`${metric.id}-${index}`}
metric={metric}
shouldUseLightText={shouldUseLightText}
cardClassName={cardClassName}
valueClassName={valueClassName}
metricTitleClassName={metricTitleClassName}
featuresClassName={featuresClassName}
featureItemClassName={featureItemClassName}
/>
))}
</CardStack>
);
}; };
MetricCardSeven.displayName = "MetricCardSeven";
export default MetricCardSeven; 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"; interface Metric {
import CardStack from "@/components/cardStack/CardStack"; id: string;
import Button from "@/components/button/Button"; value: string;
import { cls, shouldUseInvertedText } from "@/lib/utils"; title: string;
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 MetricCardItemProps { interface MetricCardTenProps extends Omit<CardStackProps, 'children'> {
metric: Metric; metrics: Metric[];
shouldUseLightText: boolean;
defaultButtonVariant: CTAButtonVariant;
cardClassName?: string;
cardTitleClassName?: string;
subtitleClassName?: string;
categoryClassName?: string;
valueClassName?: string;
footerClassName?: string;
cardButtonClassName?: string;
cardButtonTextClassName?: string;
} }
const MetricCardItem = memo(({ export const MetricCardTen: React.FC<MetricCardTenProps> = ({
metric, metrics,
shouldUseLightText, ...cardStackProps
defaultButtonVariant, }) => {
cardClassName = "", const metricElements = metrics.map(metric => (
cardTitleClassName = "", <div key={metric.id} className="metric-card">
subtitleClassName = "", <div className="metric-value">{metric.value}</div>
categoryClassName = "", <div className="metric-title">{metric.title}</div>
valueClassName = "", </div>
footerClassName = "", ));
cardButtonClassName = "",
cardButtonTextClassName = "",
}: MetricCardItemProps) => {
return (
<div className={cls("relative h-full card text-foreground rounded-theme-capped flex flex-col", cardClassName)}>
<div className="flex flex-col gap-6 p-6 flex-1">
<div className="flex flex-col gap-1">
<h3 className={cls(
"text-2xl md:text-3xl font-medium leading-tight truncate",
shouldUseLightText ? "text-background" : "text-foreground",
cardTitleClassName
)}>
{metric.title}
</h3>
<p className={cls(
"text-base md:text-lg",
shouldUseLightText ? "text-background/75" : "text-foreground/75",
subtitleClassName
)}>
{metric.subtitle}
</p>
</div>
<div className="flex items-center justify-between gap-2 mt-auto"> return (
<div className="flex items-center gap-2 min-w-0 flex-1"> <CardStack {...cardStackProps}>
<span className="h-[var(--text-base)] w-auto aspect-square rounded-theme shrink-0 bg-accent" /> {metricElements}
<span className={cls( </CardStack>
"text-base truncate", );
shouldUseLightText ? "text-background" : "text-foreground",
categoryClassName
)}>
{metric.category}
</span>
</div>
<span className={cls(
"text-xl md:text-2xl font-medium",
shouldUseLightText ? "text-background" : "text-foreground",
valueClassName
)}>
{metric.value}
</span>
</div>
</div>
{metric.buttons && metric.buttons.length > 0 && (
<div className={cls("bg-background-accent/50 p-4 rounded-b-theme-capped", footerClassName)}>
<div className="flex flex-wrap gap-4 max-md:justify-center">
{metric.buttons.slice(0, 2).map((button, index) => (
<Button key={`${button.text}-${index}`} {...getButtonProps(button, index, defaultButtonVariant, cardButtonClassName, cardButtonTextClassName)} />
))}
</div>
</div>
)}
</div>
);
});
MetricCardItem.displayName = "MetricCardItem";
const MetricCardTen = ({
metrics,
carouselMode = "buttons",
uniformGridCustomHeightClasses,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Metrics section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
cardTitleClassName = "",
subtitleClassName = "",
categoryClassName = "",
valueClassName = "",
footerClassName = "",
cardButtonClassName = "",
cardButtonTextClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: MetricCardTenProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<CardStack
useInvertedBackground={useInvertedBackground}
mode={carouselMode}
gridVariant="uniform-all-items-equal"
carouselThreshold={4}
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
carouselItemClassName="!w-carousel-item-3"
>
{metrics.map((metric, index) => (
<MetricCardItem
key={`${metric.id}-${index}`}
metric={metric}
shouldUseLightText={shouldUseLightText}
defaultButtonVariant={theme.defaultButtonVariant}
cardClassName={cardClassName}
cardTitleClassName={cardTitleClassName}
subtitleClassName={subtitleClassName}
categoryClassName={categoryClassName}
valueClassName={valueClassName}
footerClassName={footerClassName}
cardButtonClassName={cardButtonClassName}
cardButtonTextClassName={cardButtonTextClassName}
/>
))}
</CardStack>
);
}; };
MetricCardTen.displayName = "MetricCardTen";
export default MetricCardTen; 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"; interface Metric {
import CardStack from "@/components/cardStack/CardStack"; id: string;
import { cls, shouldUseInvertedText } from "@/lib/utils"; value: string;
import { useTheme } from "@/providers/themeProvider/ThemeProvider"; title: string;
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 MetricCardItemProps { interface MetricCardThreeProps extends Omit<CardStackProps, 'children'> {
metric: Metric; metrics: Metric[];
shouldUseLightText: boolean;
cardClassName?: string;
iconContainerClassName?: string;
iconClassName?: string;
metricTitleClassName?: string;
valueClassName?: string;
} }
const MetricCardItem = memo(({ export const MetricCardThree: React.FC<MetricCardThreeProps> = ({
metric, metrics,
shouldUseLightText, ...cardStackProps
cardClassName = "", }) => {
iconContainerClassName = "", const metricElements = metrics.map(metric => (
iconClassName = "", <div key={metric.id} className="metric-card">
metricTitleClassName = "", <div className="metric-value">{metric.value}</div>
valueClassName = "", <div className="metric-title">{metric.title}</div>
}: MetricCardItemProps) => { </div>
return ( ));
<div className={cls("relative h-full card text-foreground rounded-theme-capped p-6 flex flex-col items-center justify-center gap-3", cardClassName)}>
<div className="relative z-1 w-full flex items-center justify-center gap-2">
<div className={cls("h-8 primary-button aspect-square rounded-theme flex items-center justify-center", iconContainerClassName)}>
<metric.icon className={cls("h-4/10 text-primary-cta-text", iconClassName)} strokeWidth={1.5} />
</div>
<h3 className={cls("text-xl truncate", shouldUseLightText ? "text-background" : "text-foreground", metricTitleClassName)}>
{metric.title}
</h3>
</div>
<div className="relative z-1 w-full flex items-center justify-center">
<h4 className={cls("text-7xl font-medium truncate", shouldUseLightText ? "text-background" : "text-foreground", valueClassName)}>
{metric.value}
</h4>
</div>
</div>
);
});
MetricCardItem.displayName = "MetricCardItem"; return (
<CardStack {...cardStackProps}>
const MetricCardThree = ({ {metricElements}
metrics, </CardStack>
carouselMode = "buttons", );
uniformGridCustomHeightClasses = "min-h-70 2xl:min-h-80",
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Metrics section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
iconContainerClassName = "",
iconClassName = "",
metricTitleClassName = "",
valueClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: MetricCardThreeProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<CardStack
useInvertedBackground={useInvertedBackground}
mode={carouselMode}
gridVariant="uniform-all-items-equal"
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
supports3DAnimation={true}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{metrics.map((metric, index) => (
<MetricCardItem
key={`${metric.id}-${index}`}
metric={metric}
shouldUseLightText={shouldUseLightText}
cardClassName={cardClassName}
iconContainerClassName={iconContainerClassName}
iconClassName={iconClassName}
metricTitleClassName={metricTitleClassName}
valueClassName={valueClassName}
/>
))}
</CardStack>
);
}; };
MetricCardThree.displayName = "MetricCardThree";
export default MetricCardThree; 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"; interface Metric {
import CardStack from "@/components/cardStack/CardStack"; id: string;
import { cls, shouldUseInvertedText } from "@/lib/utils"; value: string;
import { useTheme } from "@/providers/themeProvider/ThemeProvider"; title: string;
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 MetricCardItemProps { interface MetricCardTwoProps extends Omit<CardStackProps, 'children'> {
metric: Metric; metrics: Metric[];
shouldUseLightText: boolean;
cardClassName?: string;
valueClassName?: string;
metricDescriptionClassName?: string;
} }
const MetricCardItem = memo(({ export const MetricCardTwo: React.FC<MetricCardTwoProps> = ({
metric, metrics,
shouldUseLightText, ...cardStackProps
cardClassName = "", }) => {
valueClassName = "", const metricElements = metrics.map(metric => (
metricDescriptionClassName = "", <div key={metric.id} className="metric-card">
}: MetricCardItemProps) => { <div className="metric-value">{metric.value}</div>
return ( <div className="metric-title">{metric.title}</div>
<div className={cls("relative h-full card text-foreground rounded-theme-capped p-6 flex flex-col justify-between", cardClassName)}> </div>
<h3 className={cls("relative z-1 text-9xl md:text-7xl font-medium truncate", shouldUseLightText ? "text-background" : "text-foreground", valueClassName)}> ));
{metric.value}
</h3>
<p className={cls("relative z-1 text-xl", shouldUseLightText ? "text-background" : "text-foreground", metricDescriptionClassName)}>
{metric.description}
</p>
</div>
);
});
MetricCardItem.displayName = "MetricCardItem"; return (
<CardStack {...cardStackProps}>
const MetricCardTwo = ({ {metricElements}
metrics, </CardStack>
carouselMode = "buttons", );
gridVariant,
uniformGridCustomHeightClasses,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Metrics section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
valueClassName = "",
metricDescriptionClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: MetricCardTwoProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const customUniformHeight = gridVariant === "uniform-all-items-equal"
? "min-h-70 2xl:min-h-80"
: uniformGridCustomHeightClasses;
const customGridRows = (gridVariant === "bento-grid" || gridVariant === "bento-grid-inverted")
? "md:grid-rows-[14rem_14rem] 2xl:grid-rows-[17rem_17rem]"
: undefined;
return (
<CardStack
useInvertedBackground={useInvertedBackground}
mode={carouselMode}
gridVariant={gridVariant}
uniformGridCustomHeightClasses={customUniformHeight}
gridRowsClassName={customGridRows}
animationType={animationType}
supports3DAnimation={true}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
carouselThreshold={4}
carouselItemClassName="w-carousel-item-3!"
>
{metrics.map((metric, index) => (
<MetricCardItem
key={`${metric.id}-${index}`}
metric={metric}
shouldUseLightText={shouldUseLightText}
cardClassName={cardClassName}
valueClassName={valueClassName}
metricDescriptionClassName={metricDescriptionClassName}
/>
))}
</CardStack>
);
}; };
MetricCardTwo.displayName = "MetricCardTwo";
export default MetricCardTwo; export default MetricCardTwo;

View File

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

View File

@@ -1,39 +1,33 @@
"use client"; 'use client';
import { memo, useCallback } from "react"; import React from 'react';
import { useRouter } from "next/navigation"; import Image from 'next/image';
import CardStack from "@/components/cardStack/CardStack"; import { ShoppingCart } from 'lucide-react';
import ProductImage from "@/components/shared/ProductImage"; import { Product } from '@/lib/api/product';
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import { useProducts } from "@/hooks/useProducts";
import type { Product } from "@/lib/api/product";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, GridVariant, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type ProductCardFourGridVariant = Exclude<GridVariant, "timeline" | "items-top-row-full-width-bottom" | "full-width-top-items-bottom-row">;
type ProductCard = Product & {
variant: string;
};
interface ProductCardFourProps { interface ProductCardFourProps {
products?: ProductCard[]; products?: Array<{
carouselMode?: "auto" | "buttons"; id: string;
gridVariant: ProductCardFourGridVariant; 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; uniformGridCustomHeightClasses?: string;
animationType: CardAnimationType;
title: string; title: string;
titleSegments?: TitleSegment[]; titleSegments?: Array<{ type: 'text'; content: string } | { type: 'image'; src: string; alt?: string }>;
description: string; description: string;
tag?: string; tag?: string;
tagIcon?: LucideIcon; tagIcon?: React.ComponentType<any>;
tagAnimation?: ButtonAnimationType; tagAnimation?: 'none' | 'opacity' | 'slide-up' | 'blur-reveal';
buttons?: ButtonConfig[]; buttons?: Array<{ text: string; onClick?: () => void; href?: string }>;
buttonAnimation?: ButtonAnimationType; buttonAnimation?: 'none' | 'opacity' | 'slide-up' | 'blur-reveal';
textboxLayout: TextboxLayout; textboxLayout: 'default' | 'split' | 'split-actions' | 'split-description' | 'inline-image';
useInvertedBackground: InvertedBackground; useInvertedBackground: boolean;
ariaLabel?: string; ariaLabel?: string;
className?: string; className?: string;
containerClassName?: string; containerClassName?: string;
@@ -45,8 +39,6 @@ interface ProductCardFourProps {
textBoxDescriptionClassName?: string; textBoxDescriptionClassName?: string;
cardNameClassName?: string; cardNameClassName?: string;
cardPriceClassName?: string; cardPriceClassName?: string;
cardVariantClassName?: string;
actionButtonClassName?: string;
gridClassName?: string; gridClassName?: string;
carouselClassName?: string; carouselClassName?: string;
controlsClassName?: string; controlsClassName?: string;
@@ -57,182 +49,92 @@ interface ProductCardFourProps {
textBoxButtonTextClassName?: string; textBoxButtonTextClassName?: string;
} }
interface ProductCardItemProps { const ProductCardFour: React.FC<ProductCardFourProps> = ({
product: ProductCard; products = [],
shouldUseLightText: boolean; carouselMode = 'buttons',
cardClassName?: string;
imageClassName?: string;
cardNameClassName?: string;
cardPriceClassName?: string;
cardVariantClassName?: string;
actionButtonClassName?: string;
}
const ProductCardItem = memo(({
product,
shouldUseLightText,
cardClassName = "",
imageClassName = "",
cardNameClassName = "",
cardPriceClassName = "",
cardVariantClassName = "",
actionButtonClassName = "",
}: ProductCardItemProps) => {
return (
<article
className={cls("card group relative h-full flex flex-col gap-4 cursor-pointer p-4 rounded-theme-capped", cardClassName)}
onClick={product.onProductClick}
role="article"
aria-label={`${product.name} - ${product.price}`}
>
<ProductImage
imageSrc={product.imageSrc}
imageAlt={product.imageAlt || product.name}
isFavorited={product.isFavorited}
onFavoriteToggle={product.onFavorite}
showActionButton={true}
actionButtonAriaLabel={`View ${product.name} details`}
imageClassName={imageClassName}
actionButtonClassName={actionButtonClassName}
/>
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between gap-4">
<div className="flex flex-col gap-0 flex-1 min-w-0">
<h3 className={cls("text-base font-medium leading-[1.3]", shouldUseLightText ? "text-background" : "text-foreground", cardNameClassName)}>
{product.name}
</h3>
<p className={cls("text-sm leading-[1.3]", shouldUseLightText ? "text-background/60" : "text-foreground/60", cardVariantClassName)}>
{product.variant}
</p>
</div>
<p className={cls("text-base font-medium leading-[1.3] flex-shrink-0", shouldUseLightText ? "text-background" : "text-foreground", cardPriceClassName)}>
{product.price}
</p>
</div>
</div>
</article>
);
});
ProductCardItem.displayName = "ProductCardItem";
const ProductCardFour = ({
products: productsProp,
carouselMode = "buttons",
gridVariant, gridVariant,
uniformGridCustomHeightClasses = "min-h-95 2xl:min-h-105",
animationType, animationType,
uniformGridCustomHeightClasses = 'min-h-95 2xl:min-h-105',
title, title,
titleSegments,
description, description,
tag, tag,
tagIcon, tagIcon: TagIcon,
tagAnimation, buttons = [],
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground, useInvertedBackground,
ariaLabel = "Product section", ariaLabel = 'Product section',
className = "", className = '',
containerClassName = "", containerClassName = '',
cardClassName = "", cardClassName = '',
imageClassName = "", imageClassName = '',
textBoxTitleClassName = "", cardNameClassName = '',
textBoxTitleImageWrapperClassName = "", cardPriceClassName = '',
textBoxTitleImageClassName = "", }) => {
textBoxDescriptionClassName = "",
cardNameClassName = "",
cardPriceClassName = "",
cardVariantClassName = "",
actionButtonClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: ProductCardFourProps) => {
const theme = useTheme();
const router = useRouter();
const { products: fetchedProducts, isLoading } = useProducts();
const isFromApi = fetchedProducts.length > 0;
const products = (isFromApi ? fetchedProducts : productsProp) as ProductCard[];
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const handleProductClick = useCallback((product: ProductCard) => {
if (isFromApi) {
router.push(`/shop/${product.id}`);
} else {
product.onProductClick?.();
}
}, [isFromApi, router]);
if (isLoading && !productsProp) {
return (
<div className="w-content-width mx-auto py-20 text-center">
<p className="text-foreground">Loading products...</p>
</div>
);
}
if (!products || products.length === 0) {
return null;
}
return ( return (
<CardStack <div className={`py-20 ${containerClassName} ${className}`} aria-label={ariaLabel}>
mode={carouselMode} <div className="max-w-7xl mx-auto px-4">
gridVariant={gridVariant} {/* Header */}
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses} <div className="mb-12 text-center">
animationType={animationType} <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} {/* Product Grid */}
titleSegments={titleSegments} <div className="grid grid-cols-1 md:grid-cols-3 gap-8">
description={description} {products.map((product) => (
tag={tag} <div
tagIcon={tagIcon} key={product.id}
tagAnimation={tagAnimation} className={`group cursor-pointer flex flex-col ${cardClassName}`}
buttons={buttons} onClick={product.onProductClick}
buttonAnimation={buttonAnimation} >
textboxLayout={textboxLayout} {/* Image Container */}
useInvertedBackground={useInvertedBackground} <div className="relative mb-4 overflow-hidden rounded-lg bg-gray-100 aspect-square flex-1">
className={className} <Image
containerClassName={containerClassName} src={product.imageSrc}
gridClassName={gridClassName} alt={product.imageAlt || product.name}
carouselClassName={carouselClassName} fill
controlsClassName={controlsClassName} className="object-cover group-hover:scale-105 transition-transform duration-300"
textBoxClassName={textBoxClassName} />
titleClassName={textBoxTitleClassName} {/* Add to Cart Button */}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName} <button
titleImageClassName={textBoxTitleImageClassName} className="absolute bottom-4 right-4 p-3 bg-primary-cta text-white rounded-full shadow-md hover:shadow-lg transition-all"
descriptionClassName={textBoxDescriptionClassName} aria-label="Add to cart"
tagClassName={textBoxTagClassName} >
buttonContainerClassName={textBoxButtonContainerClassName} <ShoppingCart size={20} />
buttonClassName={textBoxButtonClassName} </button>
buttonTextClassName={textBoxButtonTextClassName} </div>
ariaLabel={ariaLabel}
> {/* Product Info */}
{products?.map((product, index) => ( <div>
<ProductCardItem <p className={`text-sm text-gray-600 mb-1 ${cardNameClassName}`}>{product.name}</p>
key={`${product.id}-${index}`} <p className={`text-lg font-bold ${cardPriceClassName}`}>{product.price}</p>
product={{ ...product, onProductClick: () => handleProductClick(product) }} </div>
shouldUseLightText={shouldUseLightText} </div>
cardClassName={cardClassName} ))}
imageClassName={imageClassName} </div>
cardNameClassName={cardNameClassName}
cardPriceClassName={cardPriceClassName} {/* Action Buttons */}
cardVariantClassName={cardVariantClassName} {buttons.length > 0 && (
actionButtonClassName={actionButtonClassName} <div className="flex justify-center gap-4 mt-12">
/> {buttons.map((button, idx) => (
))} <button
</CardStack> 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 React from 'react';
import { useRouter } from "next/navigation"; import Image from 'next/image';
import { ArrowUpRight } from "lucide-react"; import { Heart, ArrowRight } from 'lucide-react';
import CardStack from "@/components/cardStack/CardStack"; import { Product } from '@/lib/api/product';
import ProductImage from "@/components/shared/ProductImage";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import { useProducts } from "@/hooks/useProducts";
import type { Product } from "@/lib/api/product";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, GridVariant, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type ProductCardOneGridVariant = Exclude<GridVariant, "timeline">;
type ProductCard = Product;
interface ProductCardOneProps { interface ProductCardOneProps {
products?: ProductCard[]; products?: Array<{
carouselMode?: "auto" | "buttons"; id: string;
gridVariant: ProductCardOneGridVariant; name: string;
uniformGridCustomHeightClasses?: string; price: string;
animationType: CardAnimationType; imageSrc: string;
title: string; imageAlt?: string;
titleSegments?: TitleSegment[]; onFavorite?: () => void;
description: string; onProductClick?: () => void;
tag?: string; isFavorited?: boolean;
tagIcon?: LucideIcon; }>;
tagAnimation?: ButtonAnimationType; carouselMode?: 'auto' | 'buttons';
buttons?: ButtonConfig[]; 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';
buttonAnimation?: ButtonAnimationType; animationType: 'none' | 'opacity' | 'slide-up' | 'scale-rotate' | 'blur-reveal';
textboxLayout: TextboxLayout; uniformGridCustomHeightClasses?: string;
useInvertedBackground: InvertedBackground; title: string;
ariaLabel?: string; titleSegments?: Array<{ type: 'text'; content: string } | { type: 'image'; src: string; alt?: string }>;
className?: string; description: string;
containerClassName?: string; tag?: string;
cardClassName?: string; tagIcon?: React.ComponentType<any>;
imageClassName?: string; tagAnimation?: 'none' | 'opacity' | 'slide-up' | 'blur-reveal';
textBoxTitleClassName?: string; buttons?: Array<{ text: string; onClick?: () => void; href?: string }>;
textBoxTitleImageWrapperClassName?: string; buttonAnimation?: 'none' | 'opacity' | 'slide-up' | 'blur-reveal';
textBoxTitleImageClassName?: string; textboxLayout: 'default' | 'split' | 'split-actions' | 'split-description' | 'inline-image';
textBoxDescriptionClassName?: string; useInvertedBackground: boolean;
cardNameClassName?: string; ariaLabel?: string;
cardPriceClassName?: string; className?: string;
gridClassName?: string; containerClassName?: string;
carouselClassName?: string; cardClassName?: string;
controlsClassName?: string; imageClassName?: string;
textBoxClassName?: string; textBoxTitleClassName?: string;
textBoxTagClassName?: string; textBoxTitleImageWrapperClassName?: string;
textBoxButtonContainerClassName?: string; textBoxTitleImageClassName?: string;
textBoxButtonClassName?: string; textBoxDescriptionClassName?: string;
textBoxButtonTextClassName?: string; cardNameClassName?: string;
cardPriceClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
} }
interface ProductCardItemProps { const ProductCardOne: React.FC<ProductCardOneProps> = ({
product: ProductCard; products = [],
shouldUseLightText: boolean; carouselMode = 'buttons',
cardClassName?: string; gridVariant,
imageClassName?: string; animationType,
cardNameClassName?: string; uniformGridCustomHeightClasses = 'min-h-95 2xl:min-h-105',
cardPriceClassName?: string; title,
} description,
tag,
const ProductCardItem = memo(({ tagIcon: TagIcon,
product, buttons = [],
shouldUseLightText, useInvertedBackground,
cardClassName = "", ariaLabel = 'Product section',
imageClassName = "", className = '',
cardNameClassName = "", containerClassName = '',
cardPriceClassName = "", cardClassName = '',
}: ProductCardItemProps) => { imageClassName = '',
return ( cardNameClassName = '',
<article cardPriceClassName = '',
className={cls("card group relative h-full flex flex-col gap-4 cursor-pointer p-4 rounded-theme-capped", cardClassName)} }) => {
onClick={product.onProductClick} return (
role="article" <div className={`py-20 ${containerClassName} ${className}`} aria-label={ariaLabel}>
aria-label={`${product.name} - ${product.price}`} <div className="max-w-7xl mx-auto px-4">
> {/* Header */}
<ProductImage <div className="mb-12 text-center">
imageSrc={product.imageSrc} <h2 className="text-4xl font-bold mb-4">{title}</h2>
imageAlt={product.imageAlt || product.name} <p className="text-gray-600 mb-8 max-w-2xl mx-auto">{description}</p>
isFavorited={product.isFavorited} {tag && (
onFavoriteToggle={product.onFavorite} <div className="inline-flex items-center gap-2 px-4 py-2 bg-primary-cta/10 rounded-full text-sm font-medium">
imageClassName={imageClassName} {TagIcon && <TagIcon size={16} />}
/> {tag}
<div className="relative z-1 flex items-center justify-between gap-4">
<div className="flex-1 min-w-0">
<h3 className={cls("text-base font-medium truncate leading-[1.3]", shouldUseLightText ? "text-background" : "text-foreground", cardNameClassName)}>
{product.name}
</h3>
<p className={cls("text-2xl font-medium leading-[1.3]", shouldUseLightText ? "text-background" : "text-foreground", cardPriceClassName)}>
{product.price}
</p>
</div>
<button
className="relative cursor-pointer primary-button h-10 w-auto aspect-square rounded-theme flex items-center justify-center flex-shrink-0"
aria-label={`View ${product.name} details`}
type="button"
>
<ArrowUpRight className="h-4/10 text-primary-cta-text transition-transform duration-300 group-hover:rotate-45" strokeWidth={1.5} />
</button>
</div> </div>
</article> )}
); </div>
});
ProductCardItem.displayName = "ProductCardItem"; {/* Product Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
const ProductCardOne = ({ {products.map((product) => (
products: productsProp, <div
carouselMode = "buttons", key={product.id}
gridVariant, className={`group cursor-pointer ${cardClassName}`}
uniformGridCustomHeightClasses = "min-h-95 2xl:min-h-105", onClick={product.onProductClick}
animationType, >
title, {/* Image Container */}
titleSegments, <div className="relative mb-4 overflow-hidden rounded-lg bg-gray-100 aspect-square">
description, <Image
tag, src={product.imageSrc}
tagIcon, alt={product.imageAlt || product.name}
tagAnimation, fill
buttons, className="object-cover group-hover:scale-105 transition-transform duration-300"
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}
/> />
{/* 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 React from 'react';
import { useRouter } from "next/navigation"; import Image from 'next/image';
import { Plus, Minus } from "lucide-react"; import { Heart } from 'lucide-react';
import CardStack from "@/components/cardStack/CardStack"; import { Product } from '@/lib/api/product';
import ProductImage from "@/components/shared/ProductImage";
import QuantityButton from "@/components/shared/QuantityButton";
import Button from "@/components/button/Button";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import { useProducts } from "@/hooks/useProducts";
import { getButtonProps } from "@/lib/buttonUtils";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import type { Product } from "@/lib/api/product";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, ButtonAnimationType, GridVariant, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
import type { CTAButtonVariant, ButtonPropsForVariant } from "@/components/button/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type ProductCardThreeGridVariant = Exclude<GridVariant, "timeline" | "items-top-row-full-width-bottom" | "full-width-top-items-bottom-row">;
type ProductCard = Product & {
onQuantityChange?: (quantity: number) => void;
initialQuantity?: number;
priceButtonProps?: Partial<ButtonPropsForVariant<CTAButtonVariant>>;
};
interface ProductCardThreeProps { interface ProductCardThreeProps {
products?: ProductCard[]; products?: Array<{
carouselMode?: "auto" | "buttons"; id: string;
gridVariant: ProductCardThreeGridVariant; name: string;
uniformGridCustomHeightClasses?: string; price: string;
animationType: CardAnimationType; imageSrc: string;
title: string; imageAlt?: string;
titleSegments?: TitleSegment[]; onFavorite?: () => void;
description: string; onProductClick?: () => void;
tag?: string; isFavorited?: boolean;
tagIcon?: LucideIcon; }>;
tagAnimation?: ButtonAnimationType; carouselMode?: 'auto' | 'buttons';
buttons?: ButtonConfig[]; 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';
buttonAnimation?: ButtonAnimationType; animationType: 'none' | 'opacity' | 'slide-up' | 'scale-rotate' | 'blur-reveal';
textboxLayout: TextboxLayout; uniformGridCustomHeightClasses?: string;
useInvertedBackground: InvertedBackground; title: string;
ariaLabel?: string; titleSegments?: Array<{ type: 'text'; content: string } | { type: 'image'; src: string; alt?: string }>;
className?: string; description: string;
containerClassName?: string; tag?: string;
cardClassName?: string; tagIcon?: React.ComponentType<any>;
imageClassName?: string; tagAnimation?: 'none' | 'opacity' | 'slide-up' | 'blur-reveal';
textBoxTitleClassName?: string; buttons?: Array<{ text: string; onClick?: () => void; href?: string }>;
textBoxTitleImageWrapperClassName?: string; buttonAnimation?: 'none' | 'opacity' | 'slide-up' | 'blur-reveal';
textBoxTitleImageClassName?: string; textboxLayout: 'default' | 'split' | 'split-actions' | 'split-description' | 'inline-image';
textBoxDescriptionClassName?: string; useInvertedBackground: boolean;
cardNameClassName?: string; ariaLabel?: string;
quantityControlsClassName?: string; className?: string;
gridClassName?: string; containerClassName?: string;
carouselClassName?: string; cardClassName?: string;
controlsClassName?: string; imageClassName?: string;
textBoxClassName?: string; textBoxTitleClassName?: string;
textBoxTagClassName?: string; textBoxTitleImageWrapperClassName?: string;
textBoxButtonContainerClassName?: string; textBoxTitleImageClassName?: string;
textBoxButtonClassName?: string; textBoxDescriptionClassName?: string;
textBoxButtonTextClassName?: string; cardNameClassName?: string;
cardPriceClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
} }
const ProductCardThree: React.FC<ProductCardThreeProps> = ({
interface ProductCardItemProps { products = [],
product: ProductCard; carouselMode = 'buttons',
shouldUseLightText: boolean; gridVariant,
isFromApi: boolean; animationType,
onBuyClick?: (productId: string, quantity: number) => void; uniformGridCustomHeightClasses = 'min-h-95 2xl:min-h-105',
cardClassName?: string; title,
imageClassName?: string; description,
cardNameClassName?: string; tag,
quantityControlsClassName?: string; tagIcon: TagIcon,
} buttons = [],
useInvertedBackground,
const ProductCardItem = memo(({ ariaLabel = 'Product section',
product, className = '',
shouldUseLightText, containerClassName = '',
isFromApi, cardClassName = '',
onBuyClick, imageClassName = '',
cardClassName = "", cardNameClassName = '',
imageClassName = "", cardPriceClassName = '',
cardNameClassName = "", }) => {
quantityControlsClassName = "", return (
}: ProductCardItemProps) => { <div className={`py-20 ${containerClassName} ${className}`} aria-label={ariaLabel}>
const theme = useTheme(); <div className="max-w-7xl mx-auto px-4">
const [quantity, setQuantity] = useState(product.initialQuantity || 1); {/* Header */}
<div className="mb-12 text-center">
const handleIncrement = useCallback((e: React.MouseEvent) => { <h2 className="text-4xl font-bold mb-4">{title}</h2>
e.stopPropagation(); <p className="text-gray-600 mb-8 max-w-2xl mx-auto">{description}</p>
const newQuantity = quantity + 1; {tag && (
setQuantity(newQuantity); <div className="inline-flex items-center gap-2 px-4 py-2 bg-primary-cta/10 rounded-full text-sm font-medium">
product.onQuantityChange?.(newQuantity); {TagIcon && <TagIcon size={16} />}
}, [quantity, product]); {tag}
const handleDecrement = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
if (quantity > 1) {
const newQuantity = quantity - 1;
setQuantity(newQuantity);
product.onQuantityChange?.(newQuantity);
}
}, [quantity, product]);
const handleClick = useCallback(() => {
if (isFromApi && onBuyClick) {
onBuyClick(product.id, quantity);
} else {
product.onProductClick?.();
}
}, [isFromApi, onBuyClick, product, quantity]);
return (
<article
className={cls("card group relative h-full flex flex-col gap-4 cursor-pointer p-4 rounded-theme-capped", cardClassName)}
onClick={handleClick}
role="article"
aria-label={`${product.name} - ${product.price}`}
>
<ProductImage
imageSrc={product.imageSrc}
imageAlt={product.imageAlt || product.name}
isFavorited={product.isFavorited}
onFavoriteToggle={product.onFavorite}
imageClassName={imageClassName}
/>
<div className="relative z-1 flex flex-col gap-3">
<h3 className={cls("text-xl font-medium leading-[1.15] truncate", shouldUseLightText ? "text-background" : "text-foreground", cardNameClassName)}>
{product.name}
</h3>
<div className="flex items-center justify-between gap-4">
<div className={cls("flex items-center gap-2", quantityControlsClassName)}>
<QuantityButton
onClick={handleDecrement}
ariaLabel="Decrease quantity"
Icon={Minus}
/>
<span className={cls("text-base font-medium min-w-[2ch] text-center leading-[1]", shouldUseLightText ? "text-background" : "text-foreground")}>
{quantity}
</span>
<QuantityButton
onClick={handleIncrement}
ariaLabel="Increase quantity"
Icon={Plus}
/>
</div>
<Button
{...getButtonProps(
{
text: product.price,
props: product.priceButtonProps,
},
0,
theme.defaultButtonVariant
)}
/>
</div>
</div> </div>
</article> )}
); </div>
});
ProductCardItem.displayName = "ProductCardItem"; {/* Product Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
const ProductCardThree = ({ {products.map((product) => (
products: productsProp, <div
carouselMode = "buttons", key={product.id}
gridVariant, className={`group cursor-pointer ${cardClassName}`}
uniformGridCustomHeightClasses = "min-h-95 2xl:min-h-105", onClick={product.onProductClick}
animationType, >
title, {/* Image Container */}
titleSegments, <div className="relative mb-4 overflow-hidden rounded-lg bg-gray-100 aspect-square">
description, <Image
tag, src={product.imageSrc}
tagIcon, alt={product.imageAlt || product.name}
tagAnimation, fill
buttons, className="object-cover group-hover:scale-105 transition-transform duration-300"
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}
/> />
{/* 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 React from 'react';
import { useRouter } from "next/navigation"; import Image from 'next/image';
import { Star } from "lucide-react"; import { Heart, ArrowRight, Star } from 'lucide-react';
import CardStack from "@/components/cardStack/CardStack"; import { Product } from '@/lib/api/product';
import ProductImage from "@/components/shared/ProductImage";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import { useProducts } from "@/hooks/useProducts";
import type { Product } from "@/lib/api/product";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, GridVariant, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type ProductCardTwoGridVariant = Exclude<GridVariant, "timeline" | "one-large-right-three-stacked-left" | "items-top-row-full-width-bottom" | "full-width-top-items-bottom-row" | "one-large-left-three-stacked-right">;
type ProductCard = Product & {
brand: string;
rating: number;
reviewCount: string;
};
interface ProductCardTwoProps { interface ProductCardTwoProps {
products?: ProductCard[]; products?: Array<{
carouselMode?: "auto" | "buttons"; id: string;
gridVariant: ProductCardTwoGridVariant; brand: string;
uniformGridCustomHeightClasses?: string; name: string;
animationType: CardAnimationType; price: string;
title: string; rating: number;
titleSegments?: TitleSegment[]; reviewCount: string;
description: string; imageSrc: string;
tag?: string; imageAlt?: string;
tagIcon?: LucideIcon; onFavorite?: () => void;
tagAnimation?: ButtonAnimationType; onProductClick?: () => void;
buttons?: ButtonConfig[]; isFavorited?: boolean;
buttonAnimation?: ButtonAnimationType; }>;
textboxLayout: TextboxLayout; carouselMode?: 'auto' | 'buttons';
useInvertedBackground: InvertedBackground; 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';
ariaLabel?: string; animationType: 'none' | 'opacity' | 'slide-up' | 'scale-rotate' | 'blur-reveal';
className?: string; uniformGridCustomHeightClasses?: string;
containerClassName?: string; title: string;
cardClassName?: string; titleSegments?: Array<{ type: 'text'; content: string } | { type: 'image'; src: string; alt?: string }>;
imageClassName?: string; description: string;
textBoxTitleClassName?: string; tag?: string;
textBoxTitleImageWrapperClassName?: string; tagIcon?: React.ComponentType<any>;
textBoxTitleImageClassName?: string; tagAnimation?: 'none' | 'opacity' | 'slide-up' | 'blur-reveal';
textBoxDescriptionClassName?: string; buttons?: Array<{ text: string; onClick?: () => void; href?: string }>;
cardBrandClassName?: string; buttonAnimation?: 'none' | 'opacity' | 'slide-up' | 'blur-reveal';
cardNameClassName?: string; textboxLayout: 'default' | 'split' | 'split-actions' | 'split-description' | 'inline-image';
cardPriceClassName?: string; useInvertedBackground: boolean;
cardRatingClassName?: string; ariaLabel?: string;
actionButtonClassName?: string; className?: string;
gridClassName?: string; containerClassName?: string;
carouselClassName?: string; cardClassName?: string;
controlsClassName?: string; imageClassName?: string;
textBoxClassName?: string; textBoxTitleClassName?: string;
textBoxTagClassName?: string; textBoxTitleImageWrapperClassName?: string;
textBoxButtonContainerClassName?: string; textBoxTitleImageClassName?: string;
textBoxButtonClassName?: string; textBoxDescriptionClassName?: string;
textBoxButtonTextClassName?: 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 { const ProductCardTwo: React.FC<ProductCardTwoProps> = ({
product: ProductCard; products = [],
shouldUseLightText: boolean; carouselMode = 'buttons',
cardClassName?: string; gridVariant,
imageClassName?: string; animationType,
cardBrandClassName?: string; uniformGridCustomHeightClasses = 'min-h-95 2xl:min-h-105',
cardNameClassName?: string; title,
cardPriceClassName?: string; description,
cardRatingClassName?: string; tag,
actionButtonClassName?: string; tagIcon: TagIcon,
} buttons = [],
useInvertedBackground,
const ProductCardItem = memo(({ ariaLabel = 'Product section',
product, className = '',
shouldUseLightText, containerClassName = '',
cardClassName = "", cardClassName = '',
imageClassName = "", imageClassName = '',
cardBrandClassName = "", cardBrandClassName = '',
cardNameClassName = "", cardNameClassName = '',
cardPriceClassName = "", cardPriceClassName = '',
cardRatingClassName = "", cardRatingClassName = '',
actionButtonClassName = "", }) => {
}: ProductCardItemProps) => { return (
return ( <div className={`py-20 ${containerClassName} ${className}`} aria-label={ariaLabel}>
<article <div className="max-w-7xl mx-auto px-4">
className={cls("card group relative h-full flex flex-col gap-4 cursor-pointer p-4 rounded-theme-capped", cardClassName)} {/* Header */}
onClick={product.onProductClick} <div className="mb-12 text-center">
role="article" <h2 className="text-4xl font-bold mb-4">{title}</h2>
aria-label={`${product.brand} ${product.name} - ${product.price}`} <p className="text-gray-600 mb-8 max-w-2xl mx-auto">{description}</p>
> {tag && (
<ProductImage <div className="inline-flex items-center gap-2 px-4 py-2 bg-primary-cta/10 rounded-full text-sm font-medium">
imageSrc={product.imageSrc} {TagIcon && <TagIcon size={16} />}
imageAlt={product.imageAlt || `${product.brand} ${product.name}`} {tag}
isFavorited={product.isFavorited}
onFavoriteToggle={product.onFavorite}
showActionButton={true}
actionButtonAriaLabel={`View ${product.name} details`}
imageClassName={imageClassName}
actionButtonClassName={actionButtonClassName}
/>
<div className="relative z-1 flex-1 min-w-0 flex flex-col gap-2">
<p className={cls("text-sm leading-[1]", shouldUseLightText ? "text-background" : "text-foreground", cardBrandClassName)}>
{product.brand}
</p>
<div className="flex flex-col gap-1" >
<h3 className={cls("text-xl font-medium truncate leading-[1.15]", shouldUseLightText ? "text-background" : "text-foreground", cardNameClassName)}>
{product.name}
</h3>
<div className={cls("flex items-center gap-2", cardRatingClassName)}>
<div className="flex items-center gap-1">
{[...Array(5)].map((_, i) => (
<Star
key={i}
className={cls(
"h-4 w-auto",
i < Math.floor(product.rating)
? "text-accent fill-accent"
: "text-accent opacity-20"
)}
strokeWidth={1.5}
/>
))}
</div>
<span className={cls("text-sm leading-[1.3]", shouldUseLightText ? "text-background" : "text-foreground")}>
({product.reviewCount})
</span>
</div>
</div>
<p className={cls("text-2xl font-medium leading-[1.3]", shouldUseLightText ? "text-background" : "text-foreground", cardPriceClassName)}>
{product.price}
</p>
</div> </div>
</article> )}
); </div>
});
ProductCardItem.displayName = "ProductCardItem"; {/* Product Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
const ProductCardTwo = ({ {products.map((product) => (
products: productsProp, <div
carouselMode = "buttons", key={product.id}
gridVariant, className={`group cursor-pointer ${cardClassName}`}
uniformGridCustomHeightClasses = "min-h-95 2xl:min-h-105", onClick={product.onProductClick}
animationType, >
title, {/* Image Container */}
titleSegments, <div className="relative mb-4 overflow-hidden rounded-lg bg-gray-100 aspect-square">
description, <Image
tag, src={product.imageSrc}
tagIcon, alt={product.imageAlt || product.name}
tagAnimation, fill
buttons, className="object-cover group-hover:scale-105 transition-transform duration-300"
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}
/> />
{/* 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"; interface TeamMember {
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 = {
id: string; id: string;
name: string; name: string;
role: string; role: string;
imageSrc?: 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 = ({ interface TeamCardFiveProps {
team, members: TeamMember[];
animationType, animationConfig: CardAnimationConfig;
title, className?: string;
titleSegments, }
description,
textboxLayout, export const TeamCardFive: React.FC<TeamCardFiveProps> = ({
useInvertedBackground, members,
tag, animationConfig,
tagIcon, className = '',
tagAnimation, }) => {
buttons, const cardsRef = useRef<HTMLDivElement[]>([]);
buttonAnimation,
ariaLabel = "Team section", useCardAnimation(cardsRef, animationConfig);
className = "",
containerClassName = "", const setCardRef = useCallback((index: number, el: HTMLDivElement | null) => {
textBoxTitleClassName = "", if (el) {
textBoxTitleImageWrapperClassName = "", cardsRef.current[index] = el;
textBoxTitleImageClassName = "", }
textBoxDescriptionClassName = "", }, []);
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
gridClassName = "",
cardClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
nameClassName = "",
roleClassName = "",
}: TeamCardFiveProps) => {
const { itemRefs } = useCardAnimation({ animationType, itemCount: team.length });
return ( return (
<section <div className={`team-cards ${className}`}>
aria-label={ariaLabel} {members.map((member, index) => (
className={cls("relative py-20 w-full", useInvertedBackground && "bg-foreground", className)} <div
> key={member.id}
<div className={cls("w-content-width mx-auto flex flex-col gap-8", containerClassName)}> ref={el => setCardRef(index, el)}
<CardStackTextBox className="team-card"
title={title} >
titleSegments={titleSegments} {member.imageSrc && (
description={description} <img src={member.imageSrc} alt={member.name} className="member-image" />
tag={tag} )}
tagIcon={tagIcon} <h3>{member.name}</h3>
tagAnimation={tagAnimation} <p>{member.role}</p>
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
/>
<div className={cls("flex flex-row flex-wrap gap-y-6 md:gap-x-0 justify-center", gridClassName)}>
{team.map((member, index) => (
<div
key={member.id}
ref={(el) => { itemRefs.current[index] = el; }}
className={cls("relative flex flex-col items-center text-center w-[55%] md:w-[28%] -mx-[4%] md:-mx-[2%]", cardClassName)}
>
<div className={cls("relative card w-full aspect-square rounded-theme overflow-hidden p-2 mb-4", mediaWrapperClassName)}>
<MediaContent
imageSrc={member.imageSrc}
videoSrc={member.videoSrc}
imageAlt={member.imageAlt || member.name}
videoAriaLabel={member.videoAriaLabel || member.name}
imageClassName={cls("relative z-1 w-full h-full object-cover rounded-theme!", mediaClassName)}
/>
</div>
<h3 className={cls("relative z-1 w-8/10 text-2xl font-medium leading-tight truncate", useInvertedBackground ? "text-background" : "text-foreground", nameClassName)}>
{member.name}
</h3>
<p className={cls("relative z-1 w-8/10 text-base leading-tight mt-1 truncate", useInvertedBackground ? "text-background/75" : "text-foreground/75", roleClassName)}>
{member.role}
</p>
</div>
))}
</div> </div>
</div> ))}
</section> </div>
); );
}; };
TeamCardFive.displayName = "TeamCardFive";
export default 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"; interface TeamMember {
import CardStack from "@/components/cardStack/CardStack"; id: string;
import MediaContent from "@/components/shared/MediaContent"; name: string;
import { cls } from "@/lib/utils"; role: string;
import type { LucideIcon } from "lucide-react"; imageSrc?: string;
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 TeamMemberCardProps { interface TeamCardOneProps extends Omit<CardStackProps, 'children'> {
member: TeamMember; members: TeamMember[];
cardClassName?: string;
imageClassName?: string;
overlayClassName?: string;
nameClassName?: string;
roleClassName?: string;
} }
const TeamMemberCard = memo(({ export const TeamCardOne: React.FC<TeamCardOneProps> = ({
member, members,
cardClassName = "", ...cardStackProps
imageClassName = "", }) => {
overlayClassName = "", const memberElements = members.map(member => (
nameClassName = "", <div key={member.id} className="team-card">
roleClassName = "", {member.imageSrc && (
}: TeamMemberCardProps) => { <img src={member.imageSrc} alt={member.name} className="member-image" />
return ( )}
<div className={cls("relative h-full w-full max-w-full card rounded-theme-capped p-4 aspect-[8/10]", cardClassName)}> <h3>{member.name}</h3>
<div className="relative z-1 w-full h-full rounded-theme-capped overflow-hidden"> <p>{member.role}</p>
<MediaContent </div>
imageSrc={member.imageSrc} ));
videoSrc={member.videoSrc}
imageAlt={member.imageAlt || member.name}
videoAriaLabel={member.videoAriaLabel || member.name}
imageClassName={cls("w-full h-full object-cover", imageClassName)}
/>
<div className={cls("!absolute z-1 bottom-4 left-4 right-4 card backdrop-blur-xs p-4 rounded-theme-capped flex items-center justify-between gap-3", overlayClassName)}> return (
<h3 className={cls("relative z-1 text-xl font-medium text-foreground leading-[1.1] truncate", nameClassName)}> <CardStack {...cardStackProps}>
{member.name} {memberElements}
</h3> </CardStack>
<div className="min-w-0 max-w-full w-fit primary-button px-3 py-2 rounded-theme"> );
<p className={cls("text-sm text-primary-cta-text leading-[1.1] truncate", roleClassName)}>
{member.role}
</p>
</div>
</div>
</div>
</div>
);
});
TeamMemberCard.displayName = "TeamMemberCard";
const TeamCardOne = ({
members,
carouselMode = "buttons",
gridVariant,
uniformGridCustomHeightClasses = "min-h-none",
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Team section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
imageClassName = "",
overlayClassName = "",
nameClassName = "",
roleClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: TeamCardOneProps) => {
return (
<CardStack
mode={carouselMode}
gridVariant={gridVariant}
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
supports3DAnimation={true}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{members.map((member, index) => (
<TeamMemberCard
key={`${member.id}-${index}`}
member={member}
cardClassName={cardClassName}
imageClassName={imageClassName}
overlayClassName={overlayClassName}
nameClassName={nameClassName}
roleClassName={roleClassName}
/>
))}
</CardStack>
);
}; };
TeamCardOne.displayName = "TeamCardOne";
export default TeamCardOne; 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"; interface TeamMember {
import CardStack from "@/components/cardStack/CardStack"; id: string;
import MediaContent from "@/components/shared/MediaContent"; name: string;
import { cls } from "@/lib/utils"; role: string;
import type { LucideIcon } from "lucide-react"; imageSrc?: string;
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 TeamMemberCardProps { interface TeamCardSixProps extends Omit<CardStackProps, 'children'> {
member: TeamMember; members: TeamMember[];
cardClassName?: string;
imageClassName?: string;
overlayClassName?: string;
nameClassName?: string;
roleClassName?: string;
} }
const TeamMemberCard = memo(({ export const TeamCardSix: React.FC<TeamCardSixProps> = ({
member, members,
cardClassName = "", ...cardStackProps
imageClassName = "", }) => {
overlayClassName = "", const memberElements = members.map(member => (
nameClassName = "", <div key={member.id} className="team-card">
roleClassName = "", {member.imageSrc && (
}: TeamMemberCardProps) => { <img src={member.imageSrc} alt={member.name} className="member-image" />
return ( )}
<div className={cls("relative h-full rounded-theme-capped", cardClassName)}> <h3>{member.name}</h3>
<div className="relative w-full h-full rounded-theme-capped overflow-hidden"> <p>{member.role}</p>
<MediaContent </div>
imageSrc={member.imageSrc} ));
videoSrc={member.videoSrc}
imageAlt={member.imageAlt || member.name}
videoAriaLabel={member.videoAriaLabel || member.name}
imageClassName={cls("w-full h-full object-cover", imageClassName)}
/>
<div className={cls("absolute z-10 bottom-4 left-4 right-4 p-4 flex flex-col gap-0 text-background", overlayClassName)}> return (
<h3 className={cls("text-2xl font-medium leading-tight truncate", nameClassName)}> <CardStack {...cardStackProps}>
{member.name} {memberElements}
</h3> </CardStack>
<p className={cls("text-base leading-tight truncate", roleClassName)}> );
{member.role}
</p>
</div>
<div
className="absolute z-0 backdrop-blur-xl opacity-100 w-full h-1/3 left-0 bottom-0"
style={{ maskImage: MASK_GRADIENT }}
aria-hidden="true"
/>
</div>
</div>
);
});
TeamMemberCard.displayName = "TeamMemberCard";
const TeamCardSix = ({
members,
carouselMode = "buttons",
gridVariant,
uniformGridCustomHeightClasses = "min-h-95 2xl:min-h-105",
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Team section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
imageClassName = "",
overlayClassName = "",
nameClassName = "",
roleClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: TeamCardSixProps) => {
return (
<CardStack
mode={carouselMode}
gridVariant={gridVariant}
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
supports3DAnimation={true}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{members.map((member, index) => (
<TeamMemberCard
key={`${member.id}-${index}`}
member={member}
cardClassName={cardClassName}
imageClassName={imageClassName}
overlayClassName={overlayClassName}
nameClassName={nameClassName}
roleClassName={roleClassName}
/>
))}
</CardStack>
);
}; };
TeamCardSix.displayName = "TeamCardSix";
export default TeamCardSix; 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"; interface TeamMember {
import CardStack from "@/components/cardStack/CardStack"; id: string;
import MediaContent from "@/components/shared/MediaContent"; name: string;
import { cls } from "@/lib/utils"; role: string;
import type { LucideIcon } from "lucide-react"; imageSrc?: string;
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 TeamMemberCardProps { interface TeamCardTwoProps extends Omit<CardStackProps, 'children'> {
member: TeamMember; members: TeamMember[];
cardClassName?: string;
imageClassName?: string;
overlayClassName?: string;
nameClassName?: string;
roleClassName?: string;
memberDescriptionClassName?: string;
socialLinksClassName?: string;
socialIconClassName?: string;
} }
const TeamMemberCard = memo(({ export const TeamCardTwo: React.FC<TeamCardTwoProps> = ({
member, members,
cardClassName = "", ...cardStackProps
imageClassName = "", }) => {
overlayClassName = "", const memberElements = members.map(member => (
nameClassName = "", <div key={member.id} className="team-card">
roleClassName = "", {member.imageSrc && (
memberDescriptionClassName = "", <img src={member.imageSrc} alt={member.name} className="member-image" />
socialLinksClassName = "", )}
socialIconClassName = "", <h3>{member.name}</h3>
}: TeamMemberCardProps) => { <p>{member.role}</p>
return ( </div>
<div className={cls("relative h-full rounded-theme-capped overflow-hidden group", cardClassName)}> ));
<MediaContent
imageSrc={member.imageSrc}
videoSrc={member.videoSrc}
imageAlt={member.imageAlt || member.name}
videoAriaLabel={member.videoAriaLabel || member.name}
imageClassName={cls("relative z-1 w-full h-full object-cover", imageClassName)}
/>
<div className={cls("!absolute z-10 bottom-6 left-6 right-6 card backdrop-blur-xs p-6 flex flex-col gap-2 rounded-theme-capped", overlayClassName)}> return (
<div className="relative z-1 flex items-start justify-between"> <CardStack {...cardStackProps}>
<h3 className={cls("text-2xl font-medium text-foreground leading-[1.1] truncate", nameClassName)}> {memberElements}
{member.name} </CardStack>
</h3> );
<div className="relative z-1 secondary-button px-3 py-1 rounded-theme" >
<p className={cls("text-xs text-secondary-cta-text leading-[1.1] truncate", roleClassName)}>
{member.role}
</p>
</div>
</div>
<p className={cls("relative z-1 text-base text-foreground leading-[1.1]", memberDescriptionClassName)}>
{member.description}
</p>
{member.socialLinks && member.socialLinks.length > 0 && (
<div className={cls("relative z-1 flex gap-3 mt-1", socialLinksClassName)}>
{member.socialLinks.map((link, index) => (
<a
key={index}
href={link.url}
target="_blank"
rel="noopener noreferrer"
className={cls("primary-button h-9 aspect-square w-auto flex items-center justify-center rounded-theme", socialIconClassName)}
>
<link.icon className="h-4/10 text-primary-cta-text" strokeWidth={1.5} />
</a>
))}
</div>
)}
</div>
</div>
);
});
TeamMemberCard.displayName = "TeamMemberCard";
const TeamCardTwo = ({
members,
carouselMode = "buttons",
gridVariant,
uniformGridCustomHeightClasses = "min-h-95 2xl:min-h-105",
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Team section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
imageClassName = "",
overlayClassName = "",
nameClassName = "",
roleClassName = "",
memberDescriptionClassName = "",
socialLinksClassName = "",
socialIconClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: TeamCardTwoProps) => {
const customGridRows = (gridVariant === "bento-grid" || gridVariant === "bento-grid-inverted")
? "md:grid-rows-[22rem_22rem] 2xl:grid-rows-[26rem_26rem]"
: undefined;
return (
<CardStack
mode={carouselMode}
gridVariant={gridVariant}
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
gridRowsClassName={customGridRows}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{members.map((member, index) => (
<TeamMemberCard
key={`${member.id}-${index}`}
member={member}
cardClassName={cardClassName}
imageClassName={imageClassName}
overlayClassName={overlayClassName}
nameClassName={nameClassName}
roleClassName={roleClassName}
memberDescriptionClassName={memberDescriptionClassName}
socialLinksClassName={socialLinksClassName}
socialIconClassName={socialIconClassName}
/>
))}
</CardStack>
);
}; };
TeamCardTwo.displayName = "TeamCardTwo";
export default TeamCardTwo; 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"; interface Testimonial {
import CardStack from "@/components/cardStack/CardStack"; id: string;
import MediaContent from "@/components/shared/MediaContent"; name: string;
import { cls } from "@/lib/utils"; handle: string;
import { Star } from "lucide-react"; testimonial: string;
import type { LucideIcon } from "lucide-react"; rating: number;
import type { ButtonConfig, ButtonAnimationType, CardAnimationTypeWith3D, GridVariant, TitleSegment, TextboxLayout, InvertedBackground } from "@/components/cardStack/types"; imageSrc?: string;
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 TestimonialCardProps { interface TestimonialCardOneProps extends Omit<CardStackProps, 'children'> {
testimonial: Testimonial; testimonials: Testimonial[];
cardClassName?: string;
imageClassName?: string;
overlayClassName?: string;
ratingClassName?: string;
nameClassName?: string;
roleClassName?: string;
companyClassName?: string;
} }
const TestimonialCard = memo(({ export const TestimonialCardOne: React.FC<TestimonialCardOneProps> = ({
testimonial, testimonials,
cardClassName = "", ...cardStackProps
imageClassName = "", }) => {
overlayClassName = "", const testimonialElements = testimonials.map(testimonial => (
ratingClassName = "", <div key={testimonial.id} className="testimonial-card">
nameClassName = "", {testimonial.imageSrc && (
roleClassName = "", <img src={testimonial.imageSrc} alt={testimonial.name} className="avatar" />
companyClassName = "", )}
}: TestimonialCardProps) => { <p className="testimonial-text">{testimonial.testimonial}</p>
return ( <div className="author">
<div className={cls("relative h-full rounded-theme-capped overflow-hidden group", cardClassName)}> <h4>{testimonial.name}</h4>
<MediaContent <p>{testimonial.handle}</p>
imageSrc={testimonial.imageSrc} </div>
videoSrc={testimonial.videoSrc} <div className="rating">{'⭐'.repeat(testimonial.rating)}</div>
imageAlt={testimonial.imageAlt || testimonial.name} </div>
videoAriaLabel={testimonial.videoAriaLabel || testimonial.name} ));
imageClassName={cls("relative z-1 w-full h-full object-cover!", imageClassName)}
/>
<div className={cls("!absolute z-1 bottom-6 left-6 right-6 card backdrop-blur-xs p-6 flex flex-col gap-3 rounded-theme-capped", overlayClassName)}> return (
<div className={cls("relative z-1 flex gap-1", ratingClassName)}> <CardStack {...cardStackProps}>
{Array.from({ length: 5 }).map((_, index) => ( {testimonialElements}
<Star </CardStack>
key={index} );
className={cls(
"h-5 w-auto text-accent",
index < testimonial.rating ? "fill-accent" : "fill-transparent"
)}
strokeWidth={1.5}
/>
))}
</div>
<h3 className={cls("relative z-1 text-2xl font-medium text-foreground leading-[1.1] mt-1", nameClassName)}>
{testimonial.name}
</h3>
<div className="relative z-1 flex flex-col gap-1">
<p className={cls("text-base text-foreground leading-[1.1]", roleClassName)}>
{testimonial.role}
</p>
<p className={cls("text-base text-foreground leading-[1.1]", companyClassName)}>
{testimonial.company}
</p>
</div>
</div>
</div>
);
});
TestimonialCard.displayName = "TestimonialCard";
const TestimonialCardOne = ({
testimonials,
carouselMode = "buttons",
uniformGridCustomHeightClasses = "min-h-95 2xl:min-h-105",
gridVariant,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Testimonials section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
imageClassName = "",
overlayClassName = "",
ratingClassName = "",
nameClassName = "",
roleClassName = "",
companyClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: TestimonialCardOneProps) => {
return (
<CardStack
mode={carouselMode}
gridVariant={gridVariant}
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
supports3DAnimation={true}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{testimonials.map((testimonial, index) => (
<TestimonialCard
key={`${testimonial.id}-${index}`}
testimonial={testimonial}
cardClassName={cardClassName}
imageClassName={imageClassName}
overlayClassName={overlayClassName}
ratingClassName={ratingClassName}
nameClassName={nameClassName}
roleClassName={roleClassName}
companyClassName={companyClassName}
/>
))}
</CardStack>
);
}; };
TestimonialCardOne.displayName = "TestimonialCardOne";
export default TestimonialCardOne; 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"; interface Testimonial {
import CardStack from "@/components/cardStack/CardStack"; id: string;
import MediaContent from "@/components/shared/MediaContent"; name: string;
import { cls } from "@/lib/utils"; handle: string;
import { Star } from "lucide-react"; testimonial: string;
import type { LucideIcon } from "lucide-react"; rating: number;
import type { ButtonConfig, ButtonAnimationType, CardAnimationTypeWith3D, TitleSegment, TextboxLayout, InvertedBackground } from "@/components/cardStack/types"; imageSrc?: string;
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 TestimonialCardProps { interface TestimonialCardSixteenProps extends Omit<CardStackProps, 'children'> {
testimonial: Testimonial; testimonials: Testimonial[];
cardClassName?: string;
imageClassName?: string;
overlayClassName?: string;
ratingClassName?: string;
nameClassName?: string;
roleClassName?: string;
companyClassName?: string;
} }
const TestimonialCard = memo(({ export const TestimonialCardSixteen: React.FC<TestimonialCardSixteenProps> = ({
testimonial, testimonials,
cardClassName = "", ...cardStackProps
imageClassName = "", }) => {
overlayClassName = "", const testimonialElements = testimonials.map(testimonial => (
ratingClassName = "", <div key={testimonial.id} className="testimonial-card">
nameClassName = "", {testimonial.imageSrc && (
roleClassName = "", <img src={testimonial.imageSrc} alt={testimonial.name} className="avatar" />
companyClassName = "", )}
}: TestimonialCardProps) => { <p className="testimonial-text">{testimonial.testimonial}</p>
return ( <div className="author">
<div className={cls("relative h-full w-full max-w-full aspect-[8/10] rounded-theme-capped overflow-hidden group", cardClassName)}> <h4>{testimonial.name}</h4>
<MediaContent <p>{testimonial.handle}</p>
imageSrc={testimonial.imageSrc} </div>
videoSrc={testimonial.videoSrc} <div className="rating">{'⭐'.repeat(testimonial.rating)}</div>
imageAlt={testimonial.imageAlt || testimonial.name} </div>
videoAriaLabel={testimonial.videoAriaLabel || testimonial.name} ));
imageClassName={cls("relative z-1 w-full h-full object-cover!", imageClassName)}
/>
<div className={cls("!absolute z-1 bottom-6 left-6 right-6 card backdrop-blur-xs p-6 flex flex-col gap-3 rounded-theme-capped", overlayClassName)}> return (
<div className={cls("relative z-1 flex gap-1", ratingClassName)}> <CardStack {...cardStackProps}>
{Array.from({ length: 5 }).map((_, index) => ( {testimonialElements}
<Star </CardStack>
key={index} );
className={cls(
"h-5 w-auto text-accent",
index < testimonial.rating ? "fill-accent" : "fill-transparent"
)}
strokeWidth={1.5}
/>
))}
</div>
<h3 className={cls("relative z-1 text-2xl font-medium text-foreground leading-[1.1] mt-1", nameClassName)}>
{testimonial.name}
</h3>
<div className="relative z-1 flex flex-col gap-1">
<p className={cls("text-base text-foreground leading-[1.1]", roleClassName)}>
{testimonial.role}
</p>
<p className={cls("text-base text-foreground leading-[1.1]", companyClassName)}>
{testimonial.company}
</p>
</div>
</div>
</div>
);
});
TestimonialCard.displayName = "TestimonialCard";
const TestimonialCardSixteen = ({
testimonials,
kpiItems,
carouselMode = "buttons",
uniformGridCustomHeightClasses = "min-h-none",
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Testimonials section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
imageClassName = "",
overlayClassName = "",
ratingClassName = "",
nameClassName = "",
roleClassName = "",
companyClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: TestimonialCardSixteenProps) => {
const kpiSection = (
<div className="card rounded-theme-capped p-8 md:py-16 flex flex-col md:flex-row items-center justify-between">
{kpiItems.map((item, index) => (
<div key={index} className="flex flex-col md:flex-row items-center w-full md:flex-1">
<div className="flex flex-col items-center text-center flex-1 py-4 md:py-0 gap-1">
<h3 className="text-5xl font-medium text-foreground">{item.value}</h3>
<p className="text-base text-foreground">{item.label}</p>
</div>
{index < 2 && (
<div className="w-full h-px md:h-[calc(var(--text-5xl)+var(--text-base))] md:w-px bg-foreground" />
)}
</div>
))}
</div>
);
return (
<CardStack
mode={carouselMode}
gridVariant="uniform-all-items-equal"
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
supports3DAnimation={true}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
bottomContent={kpiSection}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{testimonials.map((testimonial, index) => (
<TestimonialCard
key={`${testimonial.id}-${index}`}
testimonial={testimonial}
cardClassName={cardClassName}
imageClassName={imageClassName}
overlayClassName={overlayClassName}
ratingClassName={ratingClassName}
nameClassName={nameClassName}
roleClassName={roleClassName}
companyClassName={companyClassName}
/>
))}
</CardStack>
);
}; };
TestimonialCardSixteen.displayName = "TestimonialCardSixteen";
export default TestimonialCardSixteen; 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"; interface Testimonial {
import CardStack from "@/components/cardStack/CardStack"; id: string;
import TestimonialAuthor from "@/components/shared/TestimonialAuthor"; name: string;
import { cls, shouldUseInvertedText } from "@/lib/utils"; handle: string;
import { useTheme } from "@/providers/themeProvider/ThemeProvider"; testimonial: string;
import { Quote, Star } from "lucide-react"; rating: number;
import type { LucideIcon } from "lucide-react"; imageSrc?: string;
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 TestimonialCardProps { interface TestimonialCardThirteenProps extends Omit<CardStackProps, 'children'> {
testimonial: Testimonial; testimonials: Testimonial[];
showRating: boolean;
useInvertedBackground: boolean;
cardClassName?: string;
imageWrapperClassName?: string;
imageClassName?: string;
iconClassName?: string;
nameClassName?: string;
handleClassName?: string;
testimonialClassName?: string;
ratingClassName?: string;
contentWrapperClassName?: string;
} }
const TestimonialCard = memo(({ export const TestimonialCardThirteen: React.FC<TestimonialCardThirteenProps> = ({
testimonial, testimonials,
showRating, ...cardStackProps
useInvertedBackground, }) => {
cardClassName = "", const testimonialElements = testimonials.map(testimonial => (
imageWrapperClassName = "", <div key={testimonial.id} className="testimonial-card">
imageClassName = "", {testimonial.imageSrc && (
iconClassName = "", <img src={testimonial.imageSrc} alt={testimonial.name} className="avatar" />
nameClassName = "", )}
handleClassName = "", <p className="testimonial-text">{testimonial.testimonial}</p>
testimonialClassName = "", <div className="author">
ratingClassName = "", <h4>{testimonial.name}</h4>
contentWrapperClassName = "", <p>{testimonial.handle}</p>
}: TestimonialCardProps) => { </div>
const Icon = testimonial.icon || Quote; <div className="rating">{'⭐'.repeat(testimonial.rating)}</div>
const theme = useTheme(); </div>
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle); ));
return ( return (
<div className={cls("relative h-full card rounded-theme-capped p-6 flex flex-col justify-between", showRating ? "gap-5" : "gap-16", cardClassName)}> <CardStack {...cardStackProps}>
<div className={cls("flex flex-col gap-5 items-start", contentWrapperClassName)}> {testimonialElements}
{showRating ? ( </CardStack>
<div className={cls("relative z-1 flex gap-1", ratingClassName)}> );
{Array.from({ length: 5 }).map((_, index) => (
<Star
key={index}
className={cls(
"h-5 w-auto text-accent",
index < testimonial.rating ? "fill-accent" : "fill-transparent"
)}
strokeWidth={1.5}
/>
))}
</div>
) : (
<Quote className="h-6 w-auto text-accent fill-accent" strokeWidth={1.5} />
)}
<p className={cls("relative z-1 text-lg leading-[1.2]", shouldUseLightText ? "text-background" : "text-foreground", testimonialClassName)}>
{testimonial.testimonial}
</p>
</div>
<TestimonialAuthor
name={testimonial.name}
subtitle={testimonial.handle}
imageSrc={testimonial.imageSrc}
imageAlt={testimonial.imageAlt}
icon={Icon}
useInvertedBackground={useInvertedBackground}
imageWrapperClassName={imageWrapperClassName}
imageClassName={imageClassName}
iconClassName={iconClassName}
nameClassName={nameClassName}
subtitleClassName={handleClassName}
/>
</div>
);
});
TestimonialCard.displayName = "TestimonialCard";
const TestimonialCardThirteen = ({
testimonials,
showRating,
carouselMode = "buttons",
uniformGridCustomHeightClasses = "min-h-none",
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Testimonials section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
imageWrapperClassName = "",
imageClassName = "",
iconClassName = "",
nameClassName = "",
handleClassName = "",
testimonialClassName = "",
ratingClassName = "",
contentWrapperClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: TestimonialCardThirteenProps) => {
return (
<CardStack
mode={carouselMode}
gridVariant="uniform-all-items-equal"
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
supports3DAnimation={true}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{testimonials.map((testimonial, index) => (
<TestimonialCard
key={`${testimonial.id}-${index}`}
testimonial={testimonial}
showRating={showRating}
useInvertedBackground={useInvertedBackground}
cardClassName={cardClassName}
imageWrapperClassName={imageWrapperClassName}
imageClassName={imageClassName}
iconClassName={iconClassName}
nameClassName={nameClassName}
handleClassName={handleClassName}
testimonialClassName={testimonialClassName}
ratingClassName={ratingClassName}
contentWrapperClassName={contentWrapperClassName}
/>
))}
</CardStack>
);
}; };
TestimonialCardThirteen.displayName = "TestimonialCardThirteen";
export default TestimonialCardThirteen; 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"; interface Testimonial {
import Image from "next/image"; id: string;
import CardStack from "@/components/cardStack/CardStack"; name: string;
import { cls, shouldUseInvertedText } from "@/lib/utils"; handle: string;
import { useTheme } from "@/providers/themeProvider/ThemeProvider"; testimonial: string;
import { Quote } from "lucide-react"; rating: number;
import type { LucideIcon } from "lucide-react"; imageSrc?: string;
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 TestimonialCardProps { interface TestimonialCardTwoProps extends Omit<CardStackProps, 'children'> {
testimonial: Testimonial; testimonials: Testimonial[];
shouldUseLightText: boolean;
cardClassName?: string;
imageWrapperClassName?: string;
imageClassName?: string;
iconClassName?: string;
nameClassName?: string;
roleClassName?: string;
testimonialClassName?: string;
} }
const TestimonialCard = memo(({ export const TestimonialCardTwo: React.FC<TestimonialCardTwoProps> = ({
testimonial, testimonials,
shouldUseLightText, ...cardStackProps
cardClassName = "", }) => {
imageWrapperClassName = "", const testimonialElements = testimonials.map(testimonial => (
imageClassName = "", <div key={testimonial.id} className="testimonial-card">
iconClassName = "", {testimonial.imageSrc && (
nameClassName = "", <img src={testimonial.imageSrc} alt={testimonial.name} className="avatar" />
roleClassName = "", )}
testimonialClassName = "", <p className="testimonial-text">{testimonial.testimonial}</p>
}: TestimonialCardProps) => { <div className="author">
const Icon = testimonial.icon || Quote; <h4>{testimonial.name}</h4>
<p>{testimonial.handle}</p>
</div>
<div className="rating">{'⭐'.repeat(testimonial.rating)}</div>
</div>
));
return ( return (
<div className={cls("relative h-full card rounded-theme-capped p-6 flex flex-col gap-6", cardClassName)}> <CardStack {...cardStackProps}>
<div className={cls("relative z-1 h-30 w-fit aspect-square rounded-theme flex items-center justify-center primary-button overflow-hidden", imageWrapperClassName)}> {testimonialElements}
{testimonial.imageSrc ? ( </CardStack>
<Image );
src={testimonial.imageSrc}
alt={testimonial.imageAlt || testimonial.name}
width={800}
height={800}
className={cls("absolute inset-0 h-full w-full object-cover", imageClassName)}
unoptimized={testimonial.imageSrc.startsWith('http') || testimonial.imageSrc.startsWith('//')}
aria-hidden={testimonial.imageAlt === ""}
/>
) : (
<Icon className={cls("h-1/2 w-1/2 text-primary-cta-text", iconClassName)} strokeWidth={1} />
)}
</div>
<div className="relative z-1 flex flex-col gap-1 mt-1">
<h3 className={cls("text-2xl font-medium leading-[1.1]", shouldUseLightText ? "text-background" : "text-foreground", nameClassName)}>
{testimonial.name}
</h3>
<p className={cls("text-base leading-[1.1]", shouldUseLightText ? "text-background" : "text-foreground", roleClassName)}>
{testimonial.role}
</p>
</div>
<p className={cls("relative z-1 text-lg leading-[1.25]", shouldUseLightText ? "text-background" : "text-foreground", testimonialClassName)}>
{testimonial.testimonial}
</p>
</div>
);
});
TestimonialCard.displayName = "TestimonialCard";
const TestimonialCardTwo = ({
testimonials,
carouselMode = "buttons",
uniformGridCustomHeightClasses = "min-h-none",
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Testimonials section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
imageWrapperClassName = "",
imageClassName = "",
iconClassName = "",
nameClassName = "",
roleClassName = "",
testimonialClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: TestimonialCardTwoProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<CardStack
mode={carouselMode}
gridVariant="uniform-all-items-equal"
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
supports3DAnimation={true}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{testimonials.map((testimonial, index) => (
<TestimonialCard
key={`${testimonial.id}-${index}`}
testimonial={testimonial}
shouldUseLightText={shouldUseLightText}
cardClassName={cardClassName}
imageWrapperClassName={imageWrapperClassName}
imageClassName={imageClassName}
iconClassName={iconClassName}
nameClassName={nameClassName}
roleClassName={roleClassName}
testimonialClassName={testimonialClassName}
/>
))}
</CardStack>
);
}; };
TestimonialCardTwo.displayName = "TestimonialCardTwo";
export default TestimonialCardTwo; export default TestimonialCardTwo;

View File

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

View File

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

View File

@@ -1,117 +1,77 @@
"use client"; 'use client';
import { useState } from "react"; import { useState } from 'react';
import { Product } from "@/lib/api/product";
export type CheckoutItem = { interface CartItem {
productId: string; id: string;
quantity: number; name: string;
imageSrc?: string; price: number;
imageAlt?: string; quantity: number;
metadata?: {
brand?: string;
variant?: string;
rating?: number;
reviewCount?: string;
[key: string]: string | number | undefined;
};
};
export type CheckoutResult = {
success: boolean;
url?: string;
error?: string;
};
export function useCheckout() {
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),
};
} }
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 default useCheckout;

View File

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

View File

@@ -1,115 +1,59 @@
"use client"; 'use client';
import { useState, useMemo, useCallback } from "react"; import { useState, useEffect } from 'react';
import { useRouter } from "next/navigation";
import { useProducts } from "./useProducts";
import type { Product } from "@/lib/api/product";
import type { CatalogProduct } from "@/components/ecommerce/productCatalog/ProductCatalogItem";
import type { ProductVariant } from "@/components/ecommerce/productDetail/ProductDetailCard";
export type SortOption = "Newest" | "Price: Low-High" | "Price: High-Low"; interface CatalogProduct {
id: string;
interface UseProductCatalogOptions { name: string;
basePath?: string; price: number;
category: string;
rating: number;
imageSrc: string;
} }
export function useProductCatalog(options: UseProductCatalogOptions = {}) { const useProductCatalog = () => {
const { basePath = "/shop" } = options; const [products, setProducts] = useState<CatalogProduct[]>([]);
const router = useRouter(); const [loading, setLoading] = useState(false);
const { products: fetchedProducts, isLoading } = useProducts(); const [error, setError] = useState<string | null>(null);
const [search, setSearch] = useState(""); const fetchProducts = async () => {
const [category, setCategory] = useState("All"); setLoading(true);
const [sort, setSort] = useState<SortOption>("Newest"); 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) => { const filterByCategory = (category: string) => {
router.push(`${basePath}/${productId}`); return products.filter(p => p.category === category);
}, [router, basePath]); };
const catalogProducts: CatalogProduct[] = useMemo(() => { const searchProducts = (query: string) => {
if (fetchedProducts.length === 0) return []; return products.filter(
p =>
p.name.toLowerCase().includes(query.toLowerCase()) ||
p.category.toLowerCase().includes(query.toLowerCase())
);
};
return fetchedProducts.map((product) => ({ useEffect(() => {
id: product.id, fetchProducts();
name: product.name, }, []);
price: product.price,
imageSrc: product.imageSrc,
imageAlt: product.imageAlt || product.name,
rating: product.rating || 0,
reviewCount: product.reviewCount,
category: product.brand,
onProductClick: () => handleProductClick(product.id),
}));
}, [fetchedProducts, handleProductClick]);
const categories = useMemo(() => { return {
const categorySet = new Set<string>(); products,
catalogProducts.forEach((product) => { loading,
if (product.category) { error,
categorySet.add(product.category); fetchProducts,
} filterByCategory,
}); searchProducts,
return Array.from(categorySet).sort(); };
}, [catalogProducts]); };
const filteredProducts = useMemo(() => { export default useProductCatalog;
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,
};
}

View File

@@ -1,196 +1,57 @@
"use client"; 'use client';
import { useState, useMemo, useCallback } from "react"; import { useState, useEffect } from 'react';
import { useProduct } from "./useProduct";
import type { Product } from "@/lib/api/product";
import type { ProductVariant } from "@/components/ecommerce/productDetail/ProductDetailCard";
import type { ExtendedCartItem } from "./useCart";
interface ProductImage { interface ProductDetail {
src: string; id: string;
alt: string; name: string;
price: number;
description: string;
category: string;
rating: number;
imageSrc: string;
specs?: Record<string, string>;
} }
interface ProductMeta { const useProductDetail = (productId?: string) => {
salePrice?: string; const [product, setProduct] = useState<ProductDetail | null>(null);
ribbon?: string; const [loading, setLoading] = useState(false);
inventoryStatus?: string; const [error, setError] = useState<string | null>(null);
inventoryQuantity?: number;
sku?: string;
}
export function useProductDetail(productId: string) { const fetchProduct = async (id: string) => {
const { product, isLoading, error } = useProduct(productId); setLoading(true);
const [selectedQuantity, setSelectedQuantity] = useState(1); try {
const [selectedVariants, setSelectedVariants] = useState<Record<string, string>>({}); // 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[]>(() => { useEffect(() => {
if (!product) return []; if (productId) {
fetchProduct(productId);
}
}, [productId]);
if (product.images && product.images.length > 0) { return {
return product.images.map((src, index) => ({ product,
src, loading,
alt: product.imageAlt || `${product.name} - Image ${index + 1}`, error,
})); fetchProduct,
} };
return [{ };
src: product.imageSrc,
alt: product.imageAlt || product.name,
}];
}, [product]);
const meta = useMemo<ProductMeta>(() => { export default useProductDetail;
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,
};
}

View File

@@ -1,39 +1,31 @@
"use client"; 'use client';
import { useEffect, useState } from "react"; import { useState, useEffect } from 'react';
import { Product, fetchProducts } from "@/lib/api/product"; import { fetchProducts, Product } from '@/lib/api/product';
export function useProducts() { const useProducts = () => {
const [products, setProducts] = useState<Product[]>([]); const [products, setProducts] = useState<Product[]>([]);
const [isLoading, setIsLoading] = useState(true); const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null); const [error, setError] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
let isMounted = true; 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() { 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(); return { products, loading, error };
};
return () => { export { useProducts };
isMounted = false; export default useProducts;
};
}, []);
return { products, isLoading, error };
}

View File

@@ -1,219 +1,145 @@
export type Product = { 'use client';
id: string;
name: string; export interface Product {
price: string; id: string;
imageSrc: string; name: string;
imageAlt?: string; price: number;
images?: string[]; description: string;
brand?: string; category: string;
variant?: string; rating: number;
rating?: number; imageSrc: string;
reviewCount?: string; }
description?: string;
priceId?: string; interface ApiResponse<T> {
metadata?: { success: boolean;
[key: string]: string | number | undefined; 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[] = [ // Fetch a single product by ID
{ export const fetchProductById = async (id: string): Promise<ApiResponse<Product>> => {
id: "1", try {
name: "Classic White Sneakers", const response = await fetch(`/api/products/${id}`);
price: "$129", if (!response.ok) {
brand: "Nike", throw new Error('Failed to fetch product');
variant: "White / Size 42", }
rating: 4.5, const data = await response.json();
reviewCount: "128", return { success: true, data };
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/default/placeholder3.avif", } catch {
imageAlt: "Classic white sneakers", return {
}, success: false,
{ message: 'Failed to fetch product',
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",
},
];
function formatPrice(amount: number, currency: string): string { // Search products
const formatter = new Intl.NumberFormat("en-US", { export const searchProducts = async (query: string): Promise<ApiResponse<Product[]>> => {
style: "currency", try {
currency: currency.toUpperCase(), const response = await fetch(`/api/products/search?q=${encodeURIComponent(query)}`);
minimumFractionDigits: 0, if (!response.ok) {
maximumFractionDigits: 2, 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); if (!response.ok) {
} throw new Error('Failed to create product');
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 [];
} }
const data = await response.json();
return { success: true, data };
} catch {
return {
success: false,
message: 'Failed to create product',
};
}
};
try { // Update a product (admin only)
const url = `${apiUrl}/stripe/project/products?projectId=${projectId}&expandDefaultPrice=true`; export const updateProduct = async (id: string, updates: Partial<Product>): Promise<ApiResponse<Product>> => {
const response = await fetch(url, { try {
method: "GET", const response = await fetch(`/api/products/${id}`, {
headers: { method: 'PUT',
"Content-Type": "application/json", headers: { 'Content-Type': 'application/json' },
}, body: JSON.stringify(updates),
}); });
if (!response.ok) {
if (!response.ok) { throw new Error('Failed to update product');
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 [];
} }
} 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> { // Delete a product (admin only)
const apiUrl = process.env.NEXT_PUBLIC_API_URL; export const deleteProduct = async (id: string): Promise<ApiResponse<null>> => {
const projectId = process.env.NEXT_PUBLIC_PROJECT_ID; try {
const response = await fetch(`/api/products/${id}`, {
if (!apiUrl || !projectId) { method: 'DELETE',
return null; });
if (!response.ok) {
throw new Error('Failed to delete product');
} }
return { success: true, data: null };
try { } catch {
const url = `${apiUrl}/stripe/project/products/${productId}?projectId=${projectId}&expandDefaultPrice=true`; return {
const response = await fetch(url, { success: false,
method: "GET", message: 'Failed to delete product',
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;
}
}