Compare commits
97 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cdea021774 | |||
| 476e01f044 | |||
| e38eeb5350 | |||
| 8eb2f7877d | |||
| e350d1c202 | |||
| 816d81405e | |||
| 9feccde53c | |||
| d69851d675 | |||
| 2051632d3f | |||
| df12cd8ec3 | |||
| 37e24091b7 | |||
| fa003f67da | |||
| ba1140d4f4 | |||
| 36f1ed5a75 | |||
| 248ef9b704 | |||
| 07ff76d019 | |||
| 1ebff371d5 | |||
| f79c8e5a3f | |||
| 453d60a6c3 | |||
| 0f85122742 | |||
| da979e188f | |||
| 656992626f | |||
| 0705a26704 | |||
| 0fa3ab966c | |||
| a078df7036 | |||
| 814622f615 | |||
| d6db1b676e | |||
| e56e5de91d | |||
| 3902776001 | |||
| 7188bfd07e | |||
| 4bf7b32444 | |||
| 76bb9e9227 | |||
| 7e38f342d1 | |||
| 2e074c7f81 | |||
| edaf9736c6 | |||
| 37f4f07b46 | |||
| e92c3b82ce | |||
| e74b335ceb | |||
| ca03a2e1c5 | |||
| d73eb9a263 | |||
| 4560f438d6 | |||
| c41ced08b5 | |||
| 845a659346 | |||
| 3406752e5f | |||
| c9f96b07b3 | |||
| 22738c93c8 | |||
| 8fa09be198 | |||
| 9c8e073ca0 | |||
| c777571888 | |||
| 8190cc8b86 | |||
| 46f507759e | |||
| f804265dac | |||
| 0e051a7588 | |||
| d5305382f6 | |||
| 5940544878 | |||
| 7f2e42c708 | |||
| 48c7b2d32b | |||
| 508825c797 | |||
| 3a71cd0188 | |||
| b4cf132a21 | |||
| 05b4368c30 | |||
| f97603234a | |||
| d8c1ac202a | |||
| 3ca082eef2 | |||
| 1d629a470b | |||
| c106c1384b | |||
| 89d9670e27 | |||
| d934b71aa8 | |||
| 00a0ffe979 | |||
| 56f56965ee | |||
| 553b656a13 | |||
| 8b08e8efea | |||
| 61ab655771 | |||
| c6343baaf3 | |||
| 3150e9055e | |||
| c3f30e8105 | |||
| d9d8db3367 | |||
| 636e81ccdf | |||
| f9e8bdc033 | |||
| 416dc41e78 | |||
| 1d0f8c52b2 | |||
| 5a69c61933 | |||
| 91cfcf4774 | |||
| eba0846f2f | |||
| 8bd7af31c3 | |||
| 42261d217e | |||
| b94aa9a265 | |||
| 98cca80272 | |||
| a953797a77 | |||
| c295859a1c | |||
| f9779ffa50 | |||
| a032a21dfe | |||
| 9e53130ffd | |||
| a11da917be | |||
| 8d45f1f9b0 | |||
| 8b34530963 | |||
| 7d7db123c2 |
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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: "#" },
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
487
src/app/page.tsx
487
src/app/page.tsx
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -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 };
|
||||||
@@ -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);
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
@@ -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);
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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 };
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -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 };
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user