100 Commits

Author SHA1 Message Date
fb34fc3aec Update src/app/layout.tsx 2026-03-09 08:37:23 +00:00
b42e766676 Add src/app/projects/page.tsx 2026-03-09 08:35:05 +00:00
a382f26c5b Add src/app/projects/[id]/page.tsx 2026-03-09 08:35:05 +00:00
d0239a2bdf Update src/app/page.tsx 2026-03-09 08:35:04 +00:00
fecad6ca40 Update src/app/layout.tsx 2026-03-09 08:35:04 +00:00
1770b3b158 Switch to version 1: remove src/app/projects/[id]/page.tsx 2026-03-09 08:32:33 +00:00
110f9e59ed Switch to version 1: remove src/app/project/[id]/page.tsx 2026-03-09 08:32:33 +00:00
4fbbc8ac54 Switch to version 1: remove src/app/portfolio/[projectId]/page.tsx 2026-03-09 08:32:33 +00:00
7a2e95a3b9 Switch to version 1: modified src/app/page.tsx 2026-03-09 08:32:32 +00:00
82212aa874 Switch to version 1: modified src/app/layout.tsx 2026-03-09 08:32:32 +00:00
3fbd6c5631 Merge version_3 into main
Merge version_3 into main
2026-03-09 08:32:25 +00:00
12d76cdee5 Update src/app/projects/[id]/page.tsx 2026-03-09 08:32:21 +00:00
954a7e26e7 Add src/app/project/[id]/page.tsx 2026-03-09 08:32:21 +00:00
ab54fcc5cd Update src/app/page.tsx 2026-03-09 08:32:20 +00:00
70933c2e9a Update src/app/layout.tsx 2026-03-09 08:32:20 +00:00
e926d14730 Add src/app/projects/[id]/page.tsx 2026-03-09 08:29:36 +00:00
1edc8132ba Add src/app/portfolio/[projectId]/page.tsx 2026-03-09 08:29:35 +00:00
6378b4744a Update src/app/layout.tsx 2026-03-09 08:29:35 +00:00
ae674306f4 Switch to version 1: remove src/app/projects/saas-product-interface-design/page.tsx 2026-03-09 08:27:00 +00:00
77ba4a86af Switch to version 1: remove src/app/projects/ecommerce-platform-redesign/page.tsx 2026-03-09 08:26:59 +00:00
0a127211e9 Switch to version 1: remove src/app/projects/digital-brand-identity/page.tsx 2026-03-09 08:26:59 +00:00
f0daf18d4d Switch to version 1: remove src/app/projects/[id]/page.tsx 2026-03-09 08:26:59 +00:00
27d27c3187 Switch to version 1: remove src/app/project/[id]/page.tsx 2026-03-09 08:26:58 +00:00
8232ca1054 Switch to version 1: modified src/lib/api/product.ts 2026-03-09 08:26:58 +00:00
816bc0d1fe Switch to version 1: modified src/hooks/useProducts.ts 2026-03-09 08:26:57 +00:00
5f23ec9d4d Switch to version 1: modified src/hooks/useProductDetail.ts 2026-03-09 08:26:57 +00:00
aec1080d7f Switch to version 1: modified src/hooks/useProductCatalog.ts 2026-03-09 08:26:56 +00:00
5dd7b6b565 Switch to version 1: modified src/hooks/useProduct.ts 2026-03-09 08:26:56 +00:00
01a02e43b5 Switch to version 1: modified src/hooks/useCheckout.ts 2026-03-09 08:26:55 +00:00
22f1f53e9d Switch to version 1: modified src/components/shared/Dashboard.tsx 2026-03-09 08:26:55 +00:00
5f0a6d17a6 Switch to version 1: modified src/components/sections/testimonial/TestimonialCardTwo.tsx 2026-03-09 08:26:55 +00:00
917116a825 Switch to version 1: modified src/components/sections/testimonial/TestimonialCardThirteen.tsx 2026-03-09 08:26:54 +00:00
94080973e6 Switch to version 1: modified src/components/sections/testimonial/TestimonialCardSixteen.tsx 2026-03-09 08:26:54 +00:00
e85362908f Switch to version 1: modified src/components/sections/testimonial/TestimonialCardSix.tsx 2026-03-09 08:26:53 +00:00
59579a146c Switch to version 1: modified src/components/sections/testimonial/TestimonialCardOne.tsx 2026-03-09 08:26:52 +00:00
7b96ce57d8 Switch to version 1: modified src/components/sections/team/TeamCardTwo.tsx 2026-03-09 08:26:51 +00:00
308f65c4f7 Switch to version 1: modified src/components/sections/team/TeamCardSix.tsx 2026-03-09 08:26:51 +00:00
b2de0368d1 Switch to version 1: modified src/components/sections/team/TeamCardOne.tsx 2026-03-09 08:26:50 +00:00
c2182b265c Switch to version 1: modified src/components/sections/team/TeamCardFive.tsx 2026-03-09 08:26:50 +00:00
a7d75f7248 Switch to version 1: modified src/components/sections/team/TeamCardEleven.tsx 2026-03-09 08:26:49 +00:00
d02871af73 Switch to version 1: modified src/components/sections/product/ProductCardTwo.tsx 2026-03-09 08:26:49 +00:00
cff4d64d73 Switch to version 1: modified src/components/sections/product/ProductCardThree.tsx 2026-03-09 08:26:48 +00:00
ba6253cd37 Switch to version 1: modified src/components/sections/product/ProductCardOne.tsx 2026-03-09 08:26:48 +00:00
833d268e05 Switch to version 1: modified src/components/sections/product/ProductCardFour.tsx 2026-03-09 08:26:47 +00:00
cd29ed60bc Switch to version 1: modified src/components/sections/pricing/PricingCardTwo.tsx 2026-03-09 08:26:47 +00:00
4878a2024d Switch to version 1: modified src/components/sections/pricing/PricingCardThree.tsx 2026-03-09 08:26:46 +00:00
ea2c4606bd Switch to version 1: modified src/components/sections/pricing/PricingCardOne.tsx 2026-03-09 08:26:46 +00:00
187a225db3 Switch to version 1: modified src/components/sections/pricing/PricingCardNine.tsx 2026-03-09 08:26:45 +00:00
f0fc407cbc Switch to version 1: modified src/components/sections/pricing/PricingCardFive.tsx 2026-03-09 08:26:45 +00:00
533c37df04 Switch to version 1: modified src/components/sections/pricing/PricingCardEight.tsx 2026-03-09 08:26:44 +00:00
9f798332c9 Switch to version 1: modified src/components/sections/metrics/MetricCardTwo.tsx 2026-03-09 08:26:44 +00:00
3f9f8049be Switch to version 1: modified src/components/sections/metrics/MetricCardThree.tsx 2026-03-09 08:26:44 +00:00
0d95793876 Switch to version 1: modified src/components/sections/metrics/MetricCardTen.tsx 2026-03-09 08:26:43 +00:00
784682d810 Switch to version 1: modified src/components/sections/metrics/MetricCardSeven.tsx 2026-03-09 08:26:43 +00:00
a93bc8ab1b Switch to version 1: modified src/components/sections/metrics/MetricCardOne.tsx 2026-03-09 08:26:42 +00:00
7e15e8a83b Switch to version 1: modified src/components/sections/metrics/MetricCardEleven.tsx 2026-03-09 08:26:42 +00:00
a09643d4c2 Switch to version 1: modified src/components/sections/hero/HeroBillboardGallery.tsx 2026-03-09 08:26:41 +00:00
61c4e1fc00 Switch to version 1: modified src/components/sections/hero/HeroBillboardDashboard.tsx 2026-03-09 08:26:41 +00:00
a3958438d5 Switch to version 1: modified src/components/sections/hero/HeroBillboardCarousel.tsx 2026-03-09 08:26:40 +00:00
39049a4257 Switch to version 1: modified src/components/sections/feature/featureHoverPattern/FeatureHoverPattern.tsx 2026-03-09 08:26:40 +00:00
7a1ead0ad9 Switch to version 1: modified src/components/sections/feature/featureCardThree/FeatureCardThree.tsx 2026-03-09 08:26:39 +00:00
b47046ca12 Switch to version 1: modified src/components/sections/feature/featureBorderGlow/FeatureBorderGlow.tsx 2026-03-09 08:26:39 +00:00
c208bbaccc Switch to version 1: modified src/components/sections/feature/FeatureCardTwentyThree.tsx 2026-03-09 08:26:38 +00:00
db7f7fb177 Switch to version 1: modified src/components/sections/feature/FeatureCardTwentySeven.tsx 2026-03-09 08:26:38 +00:00
2792346d33 Switch to version 1: modified src/components/sections/feature/FeatureCardTwentyFour.tsx 2026-03-09 08:26:37 +00:00
cb580e89ad Switch to version 1: modified src/components/sections/feature/FeatureCardTwentyFive.tsx 2026-03-09 08:26:37 +00:00
ff49c73a40 Switch to version 1: modified src/components/sections/feature/FeatureCardTwelve.tsx 2026-03-09 08:26:37 +00:00
0c38c85fd1 Switch to version 1: modified src/components/sections/feature/FeatureCardTen.tsx 2026-03-09 08:26:36 +00:00
ab2e3400b8 Switch to version 1: modified src/components/sections/feature/FeatureCardSixteen.tsx 2026-03-09 08:26:36 +00:00
4254042c27 Switch to version 1: modified src/components/sections/feature/FeatureCardSeven.tsx 2026-03-09 08:26:35 +00:00
a8e68f5dcf Switch to version 1: modified src/components/sections/feature/FeatureCardOne.tsx 2026-03-09 08:26:35 +00:00
3c579bee24 Switch to version 1: modified src/components/sections/feature/FeatureCardNine.tsx 2026-03-09 08:26:34 +00:00
18785c35d6 Switch to version 1: modified src/components/sections/feature/FeatureCardMedia.tsx 2026-03-09 08:26:34 +00:00
764c5bb5f5 Switch to version 1: modified src/components/sections/feature/FeatureBento.tsx 2026-03-09 08:26:33 +00:00
49f25baee1 Switch to version 1: modified src/components/sections/contact/ContactSplitForm.tsx 2026-03-09 08:26:33 +00:00
cd7df75d42 Switch to version 1: modified src/components/sections/contact/ContactSplit.tsx 2026-03-09 08:26:32 +00:00
1a65c035ed Switch to version 1: modified src/components/sections/contact/ContactFaq.tsx 2026-03-09 08:26:32 +00:00
66151a6460 Switch to version 1: modified src/components/sections/contact/ContactCenter.tsx 2026-03-09 08:26:32 +00:00
36375d9810 Switch to version 1: modified src/components/sections/blog/BlogCardTwo.tsx 2026-03-09 08:26:31 +00:00
c2fe08e7dc Switch to version 1: modified src/components/sections/blog/BlogCardThree.tsx 2026-03-09 08:26:31 +00:00
9f7085ecdd Switch to version 1: modified src/components/sections/blog/BlogCardOne.tsx 2026-03-09 08:26:30 +00:00
5383737478 Switch to version 1: modified src/components/ecommerce/productCatalog/ProductCatalog.tsx 2026-03-09 08:26:30 +00:00
2936b85b31 Switch to version 1: modified src/components/cardStack/layouts/timelines/TimelineProcessFlow.tsx 2026-03-09 08:26:29 +00:00
6d045b36c8 Switch to version 1: modified src/components/cardStack/layouts/timelines/TimelinePhoneView.tsx 2026-03-09 08:26:29 +00:00
d87f029086 Switch to version 1: modified src/components/cardStack/layouts/timelines/TimelineBase.tsx 2026-03-09 08:26:28 +00:00
2ae3e74440 Switch to version 1: modified src/components/cardStack/layouts/grid/GridLayout.tsx 2026-03-09 08:26:28 +00:00
cc413c8c53 Switch to version 1: modified src/components/cardStack/layouts/carousels/ButtonCarousel.tsx 2026-03-09 08:26:27 +00:00
d715408be3 Switch to version 1: modified src/components/cardStack/layouts/carousels/AutoCarousel.tsx 2026-03-09 08:26:27 +00:00
3c687a6a36 Switch to version 1: modified src/components/cardStack/hooks/useDepth3DAnimation.ts 2026-03-09 08:26:27 +00:00
53d25be5c8 Switch to version 1: modified src/components/cardStack/hooks/useCardAnimation.ts 2026-03-09 08:26:26 +00:00
c65a0c5ec4 Switch to version 1: modified src/components/cardStack/CardStack.tsx 2026-03-09 08:26:26 +00:00
7cf9afc3dd Switch to version 1: modified src/components/cardStack/CardList.tsx 2026-03-09 08:26:25 +00:00
f3735dbf0e Switch to version 1: modified src/app/page.tsx 2026-03-09 08:26:25 +00:00
1c003d1673 Switch to version 1: modified src/app/layout.tsx 2026-03-09 08:26:24 +00:00
c706ba2726 Merge version_2 into main
Merge version_2 into main
2026-03-09 08:25:25 +00:00
d2bd7fe3e7 Merge version_2 into main
Merge version_2 into main
2026-03-09 08:23:47 +00:00
8c585ad0bf Merge version_2 into main
Merge version_2 into main
2026-03-09 08:22:40 +00:00
3798951e6e Merge version_2 into main
Merge version_2 into main
2026-03-09 08:19:48 +00:00
a07b8cf2b9 Merge version_2 into main
Merge version_2 into main
2026-03-09 08:18:52 +00:00
54db82b9d1 Merge version_2 into main
Merge version_2 into main
2026-03-09 08:15:59 +00:00
77 changed files with 13717 additions and 2512 deletions

View File

@@ -1,17 +1,9 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geist = Geist({
variable: "--font-geist-sans", subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono", subsets: ["latin"],
});
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: "Portfolio", description: "Creative portfolio showcasing design excellence and strategic thinking"};
title: 'Portfolio',
description: 'Portfolio website',
};
export default function RootLayout({
children,
@@ -19,17 +11,8 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<html lang="en" suppressHydrationWarning>
<body className={`${geist.variable} ${geistMono.variable}`}>
{children}
<script
src="https://cdnjs.cloudflare.com/ajax/libs/lenis/1.1.13/lenis.min.js"
integrity="sha512-+ZrK7rJ8h09z0/VVl3c+dRkXGaSLScjqlXN8uvWGSfh+ugEGH2FCi//N0JKLaML937xsqt3g7IoGD1afS+vQ==="
crossOrigin="anonymous"
referrerPolicy="no-referrer"
async
></script>
<html lang="en">
<body>{children}
<script
dangerouslySetInnerHTML={{
__html: `

View File

@@ -2,143 +2,188 @@
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
import NavbarLayoutFloatingOverlay from "@/components/navbar/NavbarLayoutFloatingOverlay/NavbarLayoutFloatingOverlay";
import HeroBillboardCarousel from '@/components/sections/hero/HeroBillboardCarousel';
import FeatureCardTwentyFour from '@/components/sections/feature/FeatureCardTwentyFour';
import TextSplitAbout from '@/components/sections/about/TextSplitAbout';
import TestimonialCardTwelve from '@/components/sections/testimonial/TestimonialCardTwelve';
import ContactCenter from '@/components/sections/contact/ContactCenter';
import FooterLogoReveal from '@/components/sections/footer/FooterLogoReveal';
import HeroBillboardCarousel from "@/components/sections/hero/HeroBillboardCarousel";
import FeatureCardTwentyFour from "@/components/sections/feature/FeatureCardTwentyFour";
import TextSplitAbout from "@/components/sections/about/TextSplitAbout";
import TestimonialCardTwelve from "@/components/sections/testimonial/TestimonialCardTwelve";
import ContactCenter from "@/components/sections/contact/ContactCenter";
import FooterLogoReveal from "@/components/sections/footer/FooterLogoReveal";
export default function Home() {
const navItems = [
{ name: "Home", id: "/" },
{ name: "Projects", id: "/projects" },
{ name: "About", id: "about" },
{ name: "Contact", id: "contact" },
];
export default function LandingPage() {
const handleProjectClick = (projectId: string) => {
window.location.href = `/project/${projectId}`;
};
return (
<ThemeProvider
defaultButtonVariant="text-stagger"
defaultTextAnimation="entrance-slide"
borderRadius="rounded"
contentWidth="medium"
sizing="medium"
defaultButtonVariant="shift-hover"
defaultTextAnimation="reveal-blur"
borderRadius="soft"
contentWidth="smallMedium"
sizing="mediumLarge"
background="circleGradient"
cardStyle="glass-elevated"
primaryButtonStyle="gradient"
secondaryButtonStyle="glass"
cardStyle="outline"
primaryButtonStyle="shadow"
secondaryButtonStyle="solid"
headingFontWeight="normal"
>
<div id="nav" data-section="nav">
<NavbarLayoutFloatingOverlay navItems={navItems} />
<NavbarLayoutFloatingOverlay
brandName="Portfolio"
navItems={[
{ name: "Work", id: "work" },
{ name: "About", id: "about" },
{ name: "Services", id: "services" },
{ name: "Contact", id: "contact" },
]}
button={{
text: "Get in Touch", href: "contact"
}}
/>
</div>
<div id="hero" data-section="hero">
<HeroBillboardCarousel
title="Welcome to Our Portfolio"
description="Explore our latest design projects and creative work"
background={{ variant: "animated-grid" }}
title="Crafting Digital Experiences"
description="Innovative design solutions that combine aesthetics with functionality. Explore a curated collection of projects that showcase creative excellence and strategic thinking."
tag="Portfolio"
background={{ variant: "sparkles-gradient" }}
mediaItems={[
{
imageSrc: "/placeholders/placeholder1.webp", imageAlt: "Portfolio showcase 1"},
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/a-stunning-creative-portfolio-showcasing-1773043068077-12221410.png", imageAlt: "Portfolio showcase project one"
},
{
imageSrc: "/placeholders/placeholder1.webp", imageAlt: "Portfolio showcase 2"},
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/bold-creative-design-project-displayed-i-1773043067366-f2dd6201.png", imageAlt: "Portfolio showcase project two"
},
{
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/professional-portfolio-piece-featuring-s-1773043067962-bd19ff43.png", imageAlt: "Portfolio showcase project three"
},
{
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/high-end-portfolio-presentation-featurin-1773043068108-9dc5d0cb.png", imageAlt: "Portfolio showcase project four"
},
{
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/contemporary-design-portfolio-piece-with-1773043067431-feb1d48d.png", imageAlt: "Portfolio showcase project five"
},
{
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/sleek-portfolio-presentation-displaying--1773043067678-cc62c707.png", imageAlt: "Portfolio showcase project six"
},
]}
buttons={[
{ text: "View Projects", href: "/projects" },
{ text: "Get In Touch", href: "contact" },
{ text: "View Work", href: "work" },
{ text: "Start a Project", href: "contact" },
]}
/>
</div>
<div id="work" data-section="work">
<FeatureCardTwentyFour
title="Our Featured Work"
description="Discover the projects we're most proud of"
textboxLayout="default"
animationType="slide-up"
useInvertedBackground={false}
title="Featured Projects"
description="Showcase of selected works demonstrating design excellence and strategic approach"
tag="Our Work"
features={[
{
id: "1", title: "Digital Brand Identity", author: "Design Team", description: "Complete brand redesign for a tech startup", tags: ["Branding", "Design"],
imageSrc: "/placeholders/placeholder1.webp", imageAlt: "Digital brand identity project"},
id: "1", title: "Digital Brand Identity System", author: "Brand Strategy", description: "Complete visual identity redesign for tech startup. Developed comprehensive brand guidelines, typography system, and digital asset library. Increased brand recognition by 40% within six months.", tags: ["Branding", "Identity", "Design System"],
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/a-beautiful-case-study-image-showing-a-c-1773043067456-ca425d2a.png", imageAlt: "Brand identity project showcase", onFeatureClick: () => handleProjectClick("1")
},
{
id: "2", title: "E-commerce Platform Redesign", author: "UX Team", description: "Modern platform redesign with improved UX", tags: ["E-commerce", "UX"],
imageSrc: "/placeholders/placeholder1.webp", imageAlt: "E-commerce platform project"},
id: "2", title: "E-commerce Platform Redesign", author: "UX Design", description: "User-centered redesign of enterprise e-commerce platform. Improved conversion rate by 35% through streamlined navigation and optimized checkout flow. Implemented accessible design patterns and mobile-first approach.", tags: ["UX Design", "E-commerce", "Accessibility"],
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/professional-portfolio-project-image-fea-1773043068039-3c07e3ca.png", imageAlt: "E-commerce platform design", onFeatureClick: () => handleProjectClick("2")
},
{
id: "3", title: "SaaS Product Interface Design", author: "Design Team", description: "Intuitive interface for enterprise SaaS product", tags: ["SaaS", "UI Design"],
imageSrc: "/placeholders/placeholder1.webp", imageAlt: "SaaS interface project"},
id: "3", title: "SaaS Product Interface Design", author: "Product Design", description: "Designed intuitive interface for data analytics SaaS platform. Created interactive prototypes and design system for 15+ component variations. Enhanced user onboarding experience and reduced support tickets by 50%.", tags: ["SaaS", "Product Design", "UI Design"],
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/contemporary-design-project-featured-in--1773043068896-89ed9073.png", imageAlt: "SaaS product interface design", onFeatureClick: () => handleProjectClick("3")
},
]}
animationType="slide-up"
textboxLayout="default"
useInvertedBackground={false}
/>
</div>
<div id="about" data-section="about">
<TextSplitAbout
title="About Our Studio"
title="About My Practice"
description={[
"We are a creative studio specializing in digital design and brand strategy. With over a decade of experience, we help businesses transform their vision into reality.", "Our team of talented designers, strategists, and developers work collaboratively to create exceptional digital experiences that inspire and engage."]}
useInvertedBackground={false}
buttons={[
{ text: "Learn More", href: "/about" },
"With over a decade of experience in digital design, I specialize in creating meaningful visual experiences that solve real problems. My approach combines strategic thinking with meticulous attention to detail.", "I believe great design goes beyond aesthetics—it should drive business results while delighting users. Every project is an opportunity to push creative boundaries and establish new standards in digital craftsmanship.", "I collaborate closely with clients and teams to understand vision, challenge assumptions, and deliver work that exceeds expectations. Let's create something extraordinary together."
]}
buttons={[
{ text: "Download Resume", href: "#" },
{ text: "Connect", href: "contact" },
]}
showBorder={true}
useInvertedBackground={true}
/>
</div>
<div id="services" data-section="services">
<FeatureCardTwentyFour
title="Our Services"
description="Comprehensive design and development solutions"
textboxLayout="default"
animationType="slide-up"
useInvertedBackground={false}
title="Services Offered"
description="Comprehensive design solutions tailored to your business objectives"
tag="Expertise"
features={[
{
id: "1", title: "Brand Strategy", author: "Strategy Team", description: "Develop compelling brand identities and positioning", tags: ["Strategy", "Branding"],
imageSrc: "/placeholders/placeholder1.webp", imageAlt: "Brand strategy service"},
id: "1", title: "Brand Strategy & Identity", author: "Strategic Design", description: "Develop compelling brand narratives and visual identities that resonate with target audiences. From logo design to complete brand systems, I create cohesive visual languages.", tags: ["Branding", "Strategy", "Identity"],
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/abstract-illustration-representing-creat-1773043067648-d1f91d56.png?_wi=1", imageAlt: "Brand strategy service"},
{
id: "2", title: "UX/UI Design", author: "Design Team", description: "Create intuitive and beautiful digital interfaces", tags: ["Design", "UX"],
imageSrc: "/placeholders/placeholder1.webp", imageAlt: "UX/UI design service"},
id: "2", title: "Digital Product Design", author: "UX/UI", description: "User-centered design for web and mobile applications. I create intuitive interfaces, conduct user research, and build design systems that scale with your product.", tags: ["UX Design", "UI Design", "Product"],
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/modern-illustration-depicting-branding-a-1773043067558-0eccfee5.png", imageAlt: "Digital product design service"},
{
id: "3", title: "Web Development", author: "Dev Team", description: "Build responsive and performant web experiences", tags: ["Development", "Web"],
imageSrc: "/placeholders/placeholder1.webp", imageAlt: "Web development service"},
id: "3", title: "Design Systems & Component Libraries", author: "System Design", description: "Build scalable design systems and component libraries that improve workflow efficiency and maintain consistency across products. Documentation included for seamless developer handoff.", tags: ["Design System", "Components", "Scalability"],
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/abstract-illustration-representing-creat-1773043067648-d1f91d56.png?_wi=2", imageAlt: "Design systems service"},
]}
animationType="slide-up"
textboxLayout="default"
useInvertedBackground={false}
/>
</div>
<div id="testimonials" data-section="testimonials">
<TestimonialCardTwelve
cardTitle="Client Success Stories"
cardTag="Testimonials"
cardTitle="Trusted by leading brands and organizations worldwide"
cardTag="Client feedback"
cardAnimation="slide-up"
useInvertedBackground={false}
testimonials={[
{
id: "1", name: "Sarah Johnson", imageSrc: "/placeholders/placeholder1.webp", imageAlt: "Sarah Johnson"},
id: "1", name: "Sarah Chen", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/professional-headshot-portrait-of-a-crea-1773043067225-0aed98b9.png", imageAlt: "Sarah Chen"
},
{
id: "2", name: "Michael Chen", imageSrc: "/placeholders/placeholder1.webp", imageAlt: "Michael Chen"},
id: "2", name: "Marcus Johnson", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/professional-portrait-photograph-of-busi-1773043067191-64c1ffe8.png", imageAlt: "Marcus Johnson"
},
{
id: "3", name: "Emma Williams", imageSrc: "/placeholders/placeholder1.webp", imageAlt: "Emma Williams"},
id: "3", name: "Elena Rodriguez", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/professional-headshot-of-creative-indust-1773043067885-58b8d4c1.png", imageAlt: "Elena Rodriguez"
},
{
id: "4", name: "James Williams", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/professional-business-portrait-photograp-1773043066896-7b04d7eb.png", imageAlt: "James Williams"
},
{
id: "5", name: "Sophie Laurent", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/professional-headshot-portrait-of-busine-1773043067827-a52a508e.png", imageAlt: "Sophie Laurent"
},
{
id: "6", name: "David Kumar", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/professional-business-portrait-photograp-1773043067750-9681fff7.png", imageAlt: "David Kumar"
},
]}
/>
</div>
<div id="contact" data-section="contact">
<ContactCenter
tag="Get In Touch"
title="Let's Work Together"
description="Have a project in mind? We'd love to hear about it. Contact us today."
background={{ variant: "plain" }}
tag="Let's Talk"
title="Ready to Start Your Next Project?"
description="Get in touch to discuss your design needs, project vision, and how we can collaborate to create exceptional digital experiences."
background={{ variant: "sparkles-gradient" }}
useInvertedBackground={false}
inputPlaceholder="Enter your email address"
buttonText="Send Message"
termsText="I'll respond within 24 hours. We respect your privacy and will never share your information."
/>
</div>
<div id="footer" data-section="footer">
<FooterLogoReveal
logoText="Our Studio"
leftLink={{ text: "Privacy Policy", href: "/privacy" }}
rightLink={{ text: "Contact", href: "contact" }}
logoText="Portfolio"
leftLink={{ text: "Privacy Policy", href: "#" }}
rightLink={{ text: "Terms of Service", href: "#" }}
/>
</div>
</ThemeProvider>

View File

@@ -1,220 +0,0 @@
"use client";
import { useParams, useRouter } from "next/navigation";
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
import NavbarLayoutFloatingOverlay from "@/components/navbar/NavbarLayoutFloatingOverlay/NavbarLayoutFloatingOverlay";
import FooterLogoReveal from "@/components/sections/footer/FooterLogoReveal";
// Project data
const projectsData: Record<string, any> = {
"1": {
id: "1", title: "Digital Brand Identity System", author: "Brand Strategy", description:
"Complete visual identity redesign for tech startup. Developed comprehensive brand guidelines, typography system, and digital asset library. Increased brand recognition by 40% within six months.", fullDescription:
"This comprehensive project involved a complete visual identity redesign for a growing tech startup. We developed extensive brand guidelines covering typography, color systems, and application patterns. Created a modular digital asset library that scales across all platforms. The result was a 40% increase in brand recognition within six months of launch.", tags: ["Branding", "Identity", "Design System"],
imageSrc:
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/a-beautiful-case-study-image-showing-a-c-1773043067456-ca425d2a.png", imageAlt: "Brand identity project showcase", challenges: [
"Balancing innovation with brand consistency", "Creating a system scalable across digital and print", "Ensuring accessibility compliance across all assets"],
solutions: [
"Developed a modular design system with clear documentation", "Created adaptive color palettes for different contexts", "Implemented WCAG AA compliance throughout the identity"],
results: [
"40% increase in brand recognition", "30% improvement in design consistency", "50+ assets in comprehensive asset library"],
},
"2": {
id: "2", title: "E-commerce Platform Redesign", author: "UX Design", description:
"User-centered redesign of enterprise e-commerce platform. Improved conversion rate by 35% through streamlined navigation and optimized checkout flow. Implemented accessible design patterns and mobile-first approach.", fullDescription:
"This project involved a comprehensive redesign of an enterprise e-commerce platform serving millions of monthly users. We implemented a mobile-first approach, streamlined navigation, and optimized the checkout flow. The result was a 35% improvement in conversion rate and significant reduction in cart abandonment.", tags: ["UX Design", "E-commerce", "Accessibility"],
imageSrc:
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/professional-portfolio-project-image-fea-1773043068039-3c07e3ca.png", imageAlt: "E-commerce platform design", challenges: [
"Managing complex product hierarchies", "Optimizing for mobile without losing desktop functionality", "Reducing checkout friction while adding trust signals"],
solutions: [
"Implemented progressive disclosure for product filters", "Created responsive layouts that adapt to all screen sizes", "Added contextual help and security indicators"],
results: [
"35% improvement in conversion rate", "42% reduction in cart abandonment", "28% increase in average order value"],
},
"3": {
id: "3", title: "SaaS Product Interface Design", author: "Product Design", description:
"Designed intuitive interface for data analytics SaaS platform. Created interactive prototypes and design system for 15+ component variations. Enhanced user onboarding experience and reduced support tickets by 50%.", fullDescription:
"This project focused on designing a user-friendly interface for a complex data analytics SaaS platform. We created an extensive design system with 15+ component variations and built interactive prototypes. The redesigned onboarding flow resulted in a 50% reduction in support tickets.", tags: ["SaaS", "Product Design", "UI Design"],
imageSrc:
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/contemporary-design-project-featured-in--1773043068896-89ed9073.png", imageAlt: "SaaS product interface design", challenges: [
"Simplifying complex data visualization", "Creating intuitive onboarding for technical users", "Maintaining consistency across multiple product modules"],
solutions: [
"Implemented guided tours and contextual help", "Created reusable visualization components", "Built comprehensive design system documentation"],
results: [
"50% reduction in support tickets", "35% faster user adoption", "25% increase in feature adoption rate"],
},
};
export default function ProjectPage() {
const params = useParams();
const router = useRouter();
const projectId = params.id as string;
const project = projectsData[projectId];
if (!project) {
return (
<ThemeProvider
defaultButtonVariant="shift-hover"
defaultTextAnimation="reveal-blur"
borderRadius="soft"
contentWidth="smallMedium"
sizing="mediumLarge"
background="circleGradient"
cardStyle="outline"
primaryButtonStyle="shadow"
secondaryButtonStyle="solid"
headingFontWeight="normal"
>
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h1 className="text-4xl font-bold mb-4">Project Not Found</h1>
<button
onClick={() => router.push("/")}
className="mt-8 px-8 py-3 bg-primary-cta text-white rounded-lg hover:opacity-90"
>
Back to Portfolio
</button>
</div>
</div>
</ThemeProvider>
);
}
return (
<ThemeProvider
defaultButtonVariant="shift-hover"
defaultTextAnimation="reveal-blur"
borderRadius="soft"
contentWidth="smallMedium"
sizing="mediumLarge"
background="circleGradient"
cardStyle="outline"
primaryButtonStyle="shadow"
secondaryButtonStyle="solid"
headingFontWeight="normal"
>
<div id="nav" data-section="nav">
<NavbarLayoutFloatingOverlay
brandName="Portfolio"
navItems={[
{ name: "Work", id: "/" },
{ name: "About", id: "/" },
{ name: "Services", id: "/" },
{ name: "Contact", id: "/" },
]}
button={{
text: "Get in Touch", href: "/#contact"}}
/>
</div>
<main className="py-20">
<div className="container mx-auto px-4">
{/* Back button */}
<button
onClick={() => router.push("/#work")}
className="text-secondary-cta hover:text-primary-cta transition-colors mb-8"
>
Back to Work
</button>
{/* Project Hero */}
<section className="mb-20">
<div className="space-y-8">
<div>
<div className="text-sm text-secondary-cta mb-4">{project.author}</div>
<h1 className="text-5xl md:text-6xl font-bold mb-6">{project.title}</h1>
<p className="text-xl text-foreground/80 max-w-3xl">
{project.fullDescription}
</p>
</div>
{/* Tags */}
<div className="flex flex-wrap gap-2">
{project.tags.map((tag: string, index: number) => (
<span
key={index}
className="px-4 py-2 bg-card text-foreground rounded-full text-sm border border-accent"
>
{tag}
</span>
))}
</div>
{/* Project Image */}
<div className="mt-12">
<img
src={project.imageSrc}
alt={project.imageAlt}
className="w-full h-auto rounded-lg object-cover"
/>
</div>
</div>
</section>
{/* Project Details */}
<section className="grid md:grid-cols-2 gap-12 mb-20">
{/* Challenges */}
<div>
<h2 className="text-2xl font-bold mb-6">Challenges</h2>
<ul className="space-y-4">
{project.challenges.map((challenge: string, index: number) => (
<li key={index} className="flex items-start gap-4">
<div className="w-2 h-2 bg-primary-cta rounded-full mt-2 flex-shrink-0"></div>
<span className="text-foreground/80">{challenge}</span>
</li>
))}
</ul>
</div>
{/* Solutions */}
<div>
<h2 className="text-2xl font-bold mb-6">Solutions</h2>
<ul className="space-y-4">
{project.solutions.map((solution: string, index: number) => (
<li key={index} className="flex items-start gap-4">
<div className="w-2 h-2 bg-primary-cta rounded-full mt-2 flex-shrink-0"></div>
<span className="text-foreground/80">{solution}</span>
</li>
))}
</ul>
</div>
</section>
{/* Results */}
<section className="mb-20">
<h2 className="text-2xl font-bold mb-6">Results & Impact</h2>
<div className="grid md:grid-cols-3 gap-8">
{project.results.map((result: string, index: number) => (
<div
key={index}
className="p-6 bg-card rounded-lg border border-accent text-center"
>
<p className="text-foreground/80">{result}</p>
</div>
))}
</div>
</section>
{/* CTA */}
<section className="text-center py-12">
<h2 className="text-3xl font-bold mb-6">Ready to Work Together?</h2>
<button
onClick={() => router.push("/#contact")}
className="px-8 py-3 bg-primary-cta text-white rounded-lg hover:opacity-90 transition-opacity"
>
Start Your Project
</button>
</section>
</div>
</main>
<div id="footer" data-section="footer">
<FooterLogoReveal
logoText="Portfolio"
leftLink={{ text: "Privacy Policy", href: "#" }}
rightLink={{ text: "Terms of Service", href: "#" }}
/>
</div>
</ThemeProvider>
);
}

View File

@@ -2,88 +2,109 @@
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
import NavbarLayoutFloatingOverlay from "@/components/navbar/NavbarLayoutFloatingOverlay/NavbarLayoutFloatingOverlay";
import HeroBillboardCarousel from '@/components/sections/hero/HeroBillboardCarousel';
import FeatureCardTwentyFour from '@/components/sections/feature/FeatureCardTwentyFour';
import ContactCenter from '@/components/sections/contact/ContactCenter';
import FooterLogoReveal from '@/components/sections/footer/FooterLogoReveal';
import HeroBillboardCarousel from "@/components/sections/hero/HeroBillboardCarousel";
import TextSplitAbout from "@/components/sections/about/TextSplitAbout";
import ContactCenter from "@/components/sections/contact/ContactCenter";
import FooterLogoReveal from "@/components/sections/footer/FooterLogoReveal";
import { useParams } from "next/navigation";
export default function ProjectDetail() {
const navItems = [
{ name: "Home", id: "/" },
{ name: "Projects", id: "/projects" },
{ name: "About", id: "about" },
{ name: "Contact", id: "contact" },
];
const projectsData: Record<string, any> = {
"1": {
title: "Digital Brand Identity System", heroTitle: "Digital Brand Identity System", heroDescription: "Complete visual identity redesign for tech startup. Developed comprehensive brand guidelines, typography system, and digital asset library.", heroImage: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/a-beautiful-case-study-image-showing-a-c-1773043067456-ca425d2a.png", aboutTitle: "Project Overview", aboutDescription: [
"This comprehensive brand identity project involved conducting extensive market research and competitive analysis to position the tech startup effectively in their market.", "We developed a complete visual identity system including logo design, color palette, typography standards, and a comprehensive brand guidelines document spanning over 50 pages.", "The result was a cohesive and scalable brand system that increased brand recognition by 40% within six months of implementation."],
},
"2": {
title: "E-commerce Platform Redesign", heroTitle: "E-commerce Platform Redesign", heroDescription: "User-centered redesign of enterprise e-commerce platform. Improved conversion rate by 35% through streamlined navigation and optimized checkout flow.", heroImage: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/professional-portfolio-project-image-fea-1773043068039-3c07e3ca.png", aboutTitle: "Project Overview", aboutDescription: [
"This e-commerce redesign project focused on improving user experience through data-driven insights and user testing methodologies.", "We streamlined the navigation structure, optimized the checkout flow to reduce cart abandonment, and implemented accessibility standards to serve a broader audience.", "The redesigned platform resulted in a 35% improvement in conversion rate and significantly reduced support tickets through improved usability."],
},
"3": {
title: "SaaS Product Interface Design", heroTitle: "SaaS Product Interface Design", heroDescription: "Designed intuitive interface for data analytics SaaS platform. Created interactive prototypes and design system for 15+ component variations.", heroImage: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/contemporary-design-project-featured-in--1773043068896-89ed9073.png", aboutTitle: "Project Overview", aboutDescription: [
"This SaaS product design engagement involved creating a comprehensive design system for a data analytics platform serving enterprise clients.", "We conducted extensive user research with data analysts and business intelligence professionals to understand workflow patterns and pain points.", "The final design system includes 15+ core components, detailed interaction patterns, and comprehensive documentation for developer handoff."],
},
};
export default function ProjectDetailPage() {
const params = useParams();
const projectId = params.id as string;
const project = projectsData[projectId] || projectsData["1"];
return (
<ThemeProvider
defaultButtonVariant="text-stagger"
defaultTextAnimation="entrance-slide"
borderRadius="rounded"
contentWidth="medium"
sizing="medium"
defaultButtonVariant="shift-hover"
defaultTextAnimation="reveal-blur"
borderRadius="soft"
contentWidth="smallMedium"
sizing="mediumLarge"
background="circleGradient"
cardStyle="glass-elevated"
primaryButtonStyle="gradient"
secondaryButtonStyle="glass"
cardStyle="outline"
primaryButtonStyle="shadow"
secondaryButtonStyle="solid"
headingFontWeight="normal"
>
<div id="nav" data-section="nav">
<NavbarLayoutFloatingOverlay navItems={navItems} />
<NavbarLayoutFloatingOverlay
brandName="Portfolio"
navItems={[
{ name: "Work", id: "/" },
{ name: "About", id: "/" },
{ name: "Services", id: "/" },
{ name: "Contact", id: "/" },
]}
button={{
text: "Get in Touch", href: "/"}}
/>
</div>
<div id="hero" data-section="hero">
<HeroBillboardCarousel
title="Project Details"
description="Explore the complete project overview and case study"
background={{ variant: "animated-grid" }}
title={project.heroTitle}
description={project.heroDescription}
tag="Project"
background={{ variant: "sparkles-gradient" }}
mediaItems={[
{
imageSrc: "/placeholders/placeholder1.webp", imageAlt: "Project showcase"},
imageSrc: project.heroImage,
imageAlt: project.title,
},
]}
buttons={[
{ text: "Back to Projects", href: "/projects" },
{ text: "Back to Portfolio", href: "/" },
{ text: "Start a Project", href: "/" },
]}
/>
</div>
<div id="work" data-section="work">
<FeatureCardTwentyFour
title="Project Breakdown"
description="See how we approached and executed this project"
textboxLayout="default"
animationType="slide-up"
useInvertedBackground={false}
features={[
{
id: "1", title: "Discovery Phase", author: "Strategy", description: "Initial research and stakeholder interviews", tags: ["Research", "Planning"],
imageSrc: "/placeholders/placeholder1.webp", imageAlt: "Discovery phase"},
{
id: "2", title: "Design Process", author: "Design", description: "Wireframing, prototyping, and visual design", tags: ["Design", "Prototyping"],
imageSrc: "/placeholders/placeholder1.webp", imageAlt: "Design process"},
{
id: "3", title: "Implementation", author: "Development", description: "Development and quality assurance", tags: ["Development", "QA"],
imageSrc: "/placeholders/placeholder1.webp", imageAlt: "Implementation phase"},
<div id="about" data-section="about">
<TextSplitAbout
title={project.aboutTitle}
description={project.aboutDescription}
buttons={[
{ text: "View Other Projects", href: "/" },
{ text: "Connect", href: "/" },
]}
showBorder={true}
useInvertedBackground={true}
/>
</div>
<div id="contact" data-section="contact">
<ContactCenter
tag="Ready to Start?"
title="Let's Create Something Amazing"
description="Interested in working with us on your next project?"
background={{ variant: "plain" }}
tag="Let's Talk"
title="Ready to Start Your Next Project?"
description="Get in touch to discuss your design needs, project vision, and how we can collaborate to create exceptional digital experiences."
background={{ variant: "sparkles-gradient" }}
useInvertedBackground={false}
buttonText="Get Started"
inputPlaceholder="Enter your email address"
buttonText="Send Message"
termsText="I'll respond within 24 hours. We respect your privacy and will never share your information."
/>
</div>
<div id="footer" data-section="footer">
<FooterLogoReveal
logoText="Our Studio"
leftLink={{ text: "Privacy Policy", href: "/privacy" }}
rightLink={{ text: "Contact", href: "contact" }}
logoText="Portfolio"
leftLink={{ text: "Privacy Policy", href: "#" }}
rightLink={{ text: "Terms of Service", href: "#" }}
/>
</div>
</ThemeProvider>

View File

@@ -1,91 +0,0 @@
"use client";
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
import NavbarLayoutFloatingOverlay from "@/components/navbar/NavbarLayoutFloatingOverlay/NavbarLayoutFloatingOverlay";
import HeroBillboardCarousel from '@/components/sections/hero/HeroBillboardCarousel';
import FeatureCardTwentyFour from '@/components/sections/feature/FeatureCardTwentyFour';
import ContactCenter from '@/components/sections/contact/ContactCenter';
import FooterLogoReveal from '@/components/sections/footer/FooterLogoReveal';
export default function DigitalBrandIdentity() {
const navItems = [
{ name: "Home", id: "/" },
{ name: "Projects", id: "/projects" },
{ name: "About", id: "about" },
{ name: "Contact", id: "contact" },
];
return (
<ThemeProvider
defaultButtonVariant="text-stagger"
defaultTextAnimation="entrance-slide"
borderRadius="rounded"
contentWidth="medium"
sizing="medium"
background="circleGradient"
cardStyle="glass-elevated"
primaryButtonStyle="gradient"
secondaryButtonStyle="glass"
headingFontWeight="normal"
>
<div id="nav" data-section="nav">
<NavbarLayoutFloatingOverlay navItems={navItems} />
</div>
<div id="hero" data-section="hero">
<HeroBillboardCarousel
title="Digital Brand Identity"
description="A comprehensive brand redesign for a tech startup"
background={{ variant: "animated-grid" }}
mediaItems={[
{
imageSrc: "/placeholders/placeholder1.webp", imageAlt: "Brand identity showcase"},
]}
buttons={[
{ text: "Back to Projects", href: "/projects" },
]}
/>
</div>
<div id="work" data-section="work">
<FeatureCardTwentyFour
title="Project Highlights"
description="Key deliverables and outcomes"
textboxLayout="default"
animationType="slide-up"
useInvertedBackground={false}
features={[
{
id: "1", title: "Logo Design", author: "Brand Team", description: "Modern and versatile logo system", tags: ["Branding", "Logo"],
imageSrc: "/placeholders/placeholder1.webp", imageAlt: "Logo design"},
{
id: "2", title: "Color Palette", author: "Design", description: "Vibrant and cohesive color system", tags: ["Design", "Colors"],
imageSrc: "/placeholders/placeholder1.webp", imageAlt: "Color palette"},
{
id: "3", title: "Typography", author: "Design", description: "Custom typography guidelines", tags: ["Typography", "Brand"],
imageSrc: "/placeholders/placeholder1.webp", imageAlt: "Typography system"},
]}
/>
</div>
<div id="contact" data-section="contact">
<ContactCenter
tag="Interested?"
title="Let's Discuss Your Brand"
description="Ready to create a powerful brand identity?"
background={{ variant: "plain" }}
useInvertedBackground={false}
buttonText="Schedule Consultation"
/>
</div>
<div id="footer" data-section="footer">
<FooterLogoReveal
logoText="Our Studio"
leftLink={{ text: "Privacy Policy", href: "/privacy" }}
rightLink={{ text: "Contact", href: "contact" }}
/>
</div>
</ThemeProvider>
);
}

View File

@@ -1,91 +0,0 @@
"use client";
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
import NavbarLayoutFloatingOverlay from "@/components/navbar/NavbarLayoutFloatingOverlay/NavbarLayoutFloatingOverlay";
import HeroBillboardCarousel from '@/components/sections/hero/HeroBillboardCarousel';
import FeatureCardTwentyFour from '@/components/sections/feature/FeatureCardTwentyFour';
import ContactCenter from '@/components/sections/contact/ContactCenter';
import FooterLogoReveal from '@/components/sections/footer/FooterLogoReveal';
export default function EcommercePlatformRedesign() {
const navItems = [
{ name: "Home", id: "/" },
{ name: "Projects", id: "/projects" },
{ name: "About", id: "about" },
{ name: "Contact", id: "contact" },
];
return (
<ThemeProvider
defaultButtonVariant="text-stagger"
defaultTextAnimation="entrance-slide"
borderRadius="rounded"
contentWidth="medium"
sizing="medium"
background="circleGradient"
cardStyle="glass-elevated"
primaryButtonStyle="gradient"
secondaryButtonStyle="glass"
headingFontWeight="normal"
>
<div id="nav" data-section="nav">
<NavbarLayoutFloatingOverlay navItems={navItems} />
</div>
<div id="hero" data-section="hero">
<HeroBillboardCarousel
title="E-commerce Platform Redesign"
description="Modern platform redesign with improved user experience"
background={{ variant: "animated-grid" }}
mediaItems={[
{
imageSrc: "/placeholders/placeholder1.webp", imageAlt: "E-commerce platform showcase"},
]}
buttons={[
{ text: "Back to Projects", href: "/projects" },
]}
/>
</div>
<div id="work" data-section="work">
<FeatureCardTwentyFour
title="Project Goals & Results"
description="How we improved the shopping experience"
textboxLayout="default"
animationType="slide-up"
useInvertedBackground={false}
features={[
{
id: "1", title: "User Research", author: "UX Team", description: "Comprehensive user testing and interviews", tags: ["Research", "UX"],
imageSrc: "/placeholders/placeholder1.webp", imageAlt: "User research phase"},
{
id: "2", title: "Improved Navigation", author: "Design", description: "Streamlined product discovery flow", tags: ["UX", "Navigation"],
imageSrc: "/placeholders/placeholder1.webp", imageAlt: "Navigation improvements"},
{
id: "3", title: "Conversion Optimization", author: "Strategy", description: "30% increase in conversion rates", tags: ["Optimization", "Results"],
imageSrc: "/placeholders/placeholder1.webp", imageAlt: "Conversion metrics"},
]}
/>
</div>
<div id="contact" data-section="contact">
<ContactCenter
tag="Transform Your Platform"
title="Ready to Redesign?"
description="Let's create an e-commerce experience your customers will love"
background={{ variant: "plain" }}
useInvertedBackground={false}
buttonText="Start Your Project"
/>
</div>
<div id="footer" data-section="footer">
<FooterLogoReveal
logoText="Our Studio"
leftLink={{ text: "Privacy Policy", href: "/privacy" }}
rightLink={{ text: "Contact", href: "contact" }}
/>
</div>
</ThemeProvider>
);
}

102
src/app/projects/page.tsx Normal file
View File

@@ -0,0 +1,102 @@
"use client";
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
import NavbarLayoutFloatingOverlay from "@/components/navbar/NavbarLayoutFloatingOverlay/NavbarLayoutFloatingOverlay";
import HeroBillboardCarousel from "@/components/sections/hero/HeroBillboardCarousel";
import FeatureCardTwentyFour from "@/components/sections/feature/FeatureCardTwentyFour";
import ContactCenter from "@/components/sections/contact/ContactCenter";
import FooterLogoReveal from "@/components/sections/footer/FooterLogoReveal";
import Link from "next/link";
export default function ProjectsPage() {
const projects = [
{
id: "digital-brand-identity", title: "Digital Brand Identity System", author: "Brand Strategy", description: "Complete visual identity redesign for tech startup. Developed comprehensive brand guidelines, typography system, and digital asset library. Increased brand recognition by 40% within six months.", tags: ["Branding", "Identity", "Design System"],
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/a-beautiful-case-study-image-showing-a-c-1773043067456-ca425d2a.png", imageAlt: "Brand identity project showcase"},
{
id: "ecommerce-platform-redesign", title: "E-commerce Platform Redesign", author: "UX Design", description: "User-centered redesign of enterprise e-commerce platform. Improved conversion rate by 35% through streamlined navigation and optimized checkout flow. Implemented accessible design patterns and mobile-first approach.", tags: ["UX Design", "E-commerce", "Accessibility"],
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/professional-portfolio-project-image-fea-1773043068039-3c07e3ca.png", imageAlt: "E-commerce platform design"},
{
id: "saas-product-interface", title: "SaaS Product Interface Design", author: "Product Design", description: "Designed intuitive interface for data analytics SaaS platform. Created interactive prototypes and design system for 15+ component variations. Enhanced user onboarding experience and reduced support tickets by 50%.", tags: ["SaaS", "Product Design", "UI Design"],
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/contemporary-design-project-featured-in--1773043068896-89ed9073.png", imageAlt: "SaaS product interface design"},
];
return (
<ThemeProvider
defaultButtonVariant="shift-hover"
defaultTextAnimation="reveal-blur"
borderRadius="soft"
contentWidth="smallMedium"
sizing="mediumLarge"
background="circleGradient"
cardStyle="outline"
primaryButtonStyle="shadow"
secondaryButtonStyle="solid"
headingFontWeight="normal"
>
<div id="nav" data-section="nav">
<NavbarLayoutFloatingOverlay
brandName="Portfolio"
navItems={[
{ name: "Work", id: "work" },
{ name: "About", id: "about" },
{ name: "Services", id: "services" },
{ name: "Contact", id: "contact" },
]}
button={{
text: "Get in Touch", href: "#contact"}}
/>
</div>
<div id="hero" data-section="hero">
<HeroBillboardCarousel
title="All Projects"
description="A comprehensive collection of design projects showcasing expertise across branding, UX design, and product strategy. Each project demonstrates creative excellence and strategic problem-solving."
tag="Portfolio"
background={{ variant: "sparkles-gradient" }}
mediaItems={[
{
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/a-stunning-creative-portfolio-showcasing-1773043068077-12221410.png", imageAlt: "Portfolio showcase"},
]}
buttons={[
{ text: "Back Home", href: "/" },
{ text: "Start Your Project", href: "#contact" },
]}
/>
</div>
<div id="projects" data-section="projects">
<FeatureCardTwentyFour
title="Featured Projects"
description="Detailed case studies showcasing design process, outcomes, and business impact"
tag="Case Studies"
features={projects}
animationType="slide-up"
textboxLayout="default"
useInvertedBackground={false}
/>
</div>
<div id="contact" data-section="contact">
<ContactCenter
tag="Let's Collaborate"
title="Ready to Start Your Next Project?"
description="Get in touch to discuss your design needs, project vision, and how we can collaborate to create exceptional digital experiences."
background={{ variant: "sparkles-gradient" }}
useInvertedBackground={false}
inputPlaceholder="Enter your email address"
buttonText="Send Message"
termsText="I'll respond within 24 hours. We respect your privacy and will never share your information."
/>
</div>
<div id="footer" data-section="footer">
<FooterLogoReveal
logoText="Portfolio"
leftLink={{ text: "Privacy Policy", href: "#" }}
rightLink={{ text: "Terms of Service", href: "#" }}
/>
</div>
</ThemeProvider>
);
}

View File

@@ -1,91 +0,0 @@
"use client";
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
import NavbarLayoutFloatingOverlay from "@/components/navbar/NavbarLayoutFloatingOverlay/NavbarLayoutFloatingOverlay";
import HeroBillboardCarousel from '@/components/sections/hero/HeroBillboardCarousel';
import FeatureCardTwentyFour from '@/components/sections/feature/FeatureCardTwentyFour';
import ContactCenter from '@/components/sections/contact/ContactCenter';
import FooterLogoReveal from '@/components/sections/footer/FooterLogoReveal';
export default function SaasProductInterfaceDesign() {
const navItems = [
{ name: "Home", id: "/" },
{ name: "Projects", id: "/projects" },
{ name: "About", id: "about" },
{ name: "Contact", id: "contact" },
];
return (
<ThemeProvider
defaultButtonVariant="text-stagger"
defaultTextAnimation="entrance-slide"
borderRadius="rounded"
contentWidth="medium"
sizing="medium"
background="circleGradient"
cardStyle="glass-elevated"
primaryButtonStyle="gradient"
secondaryButtonStyle="glass"
headingFontWeight="normal"
>
<div id="nav" data-section="nav">
<NavbarLayoutFloatingOverlay navItems={navItems} />
</div>
<div id="hero" data-section="hero">
<HeroBillboardCarousel
title="SaaS Product Interface Design"
description="Intuitive interface design for enterprise SaaS product"
background={{ variant: "animated-grid" }}
mediaItems={[
{
imageSrc: "/placeholders/placeholder1.webp", imageAlt: "SaaS interface showcase"},
]}
buttons={[
{ text: "Back to Projects", href: "/projects" },
]}
/>
</div>
<div id="work" data-section="work">
<FeatureCardTwentyFour
title="Design Solution"
description="Creating an intuitive enterprise experience"
textboxLayout="default"
animationType="slide-up"
useInvertedBackground={false}
features={[
{
id: "1", title: "Dashboard Design", author: "Design", description: "Clean and informative dashboard layout", tags: ["UI", "Dashboard"],
imageSrc: "/placeholders/placeholder1.webp", imageAlt: "Dashboard design"},
{
id: "2", title: "Component Library", author: "Design", description: "Comprehensive design system and components", tags: ["Design System", "Components"],
imageSrc: "/placeholders/placeholder1.webp", imageAlt: "Component library"},
{
id: "3", title: "User Onboarding", author: "UX", description: "Guided onboarding flow for new users", tags: ["UX", "Onboarding"],
imageSrc: "/placeholders/placeholder1.webp", imageAlt: "Onboarding flow"},
]}
/>
</div>
<div id="contact" data-section="contact">
<ContactCenter
tag="Build Your SaaS"
title="Design for Enterprise"
description="Let's create an interface that your users will love"
background={{ variant: "plain" }}
useInvertedBackground={false}
buttonText="Discuss Your Project"
/>
</div>
<div id="footer" data-section="footer">
<FooterLogoReveal
logoText="Our Studio"
leftLink={{ text: "Privacy Policy", href: "/privacy" }}
rightLink={{ text: "Contact", href: "contact" }}
/>
</div>
</ThemeProvider>
);
}

View File

@@ -1,27 +1,123 @@
import React from "react";
import { useCardAnimation } from "./hooks/useCardAnimation";
"use client";
import { memo, Children } from "react";
import CardStackTextBox from "@/components/cardStack/CardStackTextBox";
import { useCardAnimation } from "@/components/cardStack/hooks/useCardAnimation";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, ButtonAnimationType, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
interface CardListProps {
items: any[];
children: React.ReactNode;
animationType: CardAnimationType;
useUncappedRounding?: boolean;
title?: string;
titleSegments?: TitleSegment[];
description?: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground?: InvertedBackground;
disableCardWrapper?: boolean;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
}
export default function CardList({ items, className = "" }: CardListProps) {
const state = useCardAnimation({
rotationX: 0,
rotationY: 0,
rotationZ: 0,
perspective: 1000,
duration: 0.3,
});
const CardList = ({
children,
animationType,
useUncappedRounding = false,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
disableCardWrapper = false,
ariaLabel = "Card list",
className = "",
containerClassName = "",
cardClassName = "",
textBoxClassName = "",
titleClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
}: CardListProps) => {
const childrenArray = Children.toArray(children);
const { itemRefs } = useCardAnimation({ animationType, itemCount: childrenArray.length, useIndividualTriggers: true });
return (
<div className={`card-list-container ${className}`}>
{items.map((item, index) => (
<div key={index} className="card-item">
{item.label}
<section
aria-label={ariaLabel}
className={cls(
"relative py-20 w-full",
useInvertedBackground && "bg-foreground",
className
)}
>
<div className={cls("w-content-width mx-auto flex flex-col gap-8", containerClassName)}>
<CardStackTextBox
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
textBoxClassName={textBoxClassName}
titleClassName={titleClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
descriptionClassName={descriptionClassName}
tagClassName={tagClassName}
buttonContainerClassName={buttonContainerClassName}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
/>
<div className="flex flex-col gap-6">
{childrenArray.map((child, index) => (
<div
key={index}
ref={(el) => { itemRefs.current[index] = el; }}
className={cls(!disableCardWrapper && "card", !disableCardWrapper && (useUncappedRounding ? "rounded-theme" : "rounded-theme-capped"), cardClassName)}
>
{child}
</div>
))}
</div>
))}
</div>
</div>
</section>
);
}
};
CardList.displayName = "CardList";
export default memo(CardList);

View File

@@ -1,23 +1,229 @@
import React from "react";
import { TimelineBase } from "./layouts/timelines/TimelineBase";
"use client";
interface CardStackProps {
items: any[];
className?: string;
}
import { memo, Children } from "react";
import { CardStackProps } from "./types";
import GridLayout from "./layouts/grid/GridLayout";
import AutoCarousel from "./layouts/carousels/AutoCarousel";
import ButtonCarousel from "./layouts/carousels/ButtonCarousel";
import TimelineBase from "./layouts/timelines/TimelineBase";
import { gridConfigs } from "./layouts/grid/gridConfigs";
export { CardStack };
const CardStack = ({
children,
mode = "buttons",
gridVariant = "uniform-all-items-equal",
uniformGridCustomHeightClasses,
gridRowsClassName,
itemHeightClassesOverride,
animationType,
supports3DAnimation = false,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout = "default",
useInvertedBackground,
carouselThreshold = 5,
bottomContent,
className = "",
containerClassName = "",
gridClassName = "",
carouselClassName = "",
carouselItemClassName = "",
controlsClassName = "",
textBoxClassName = "",
titleClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
ariaLabel = "Card stack",
}: CardStackProps) => {
const childrenArray = Children.toArray(children);
const itemCount = childrenArray.length;
const CardStack: React.FC<CardStackProps> = ({ items, className = "" }) => {
return (
<div className={`card-stack-container ${className}`}>
<TimelineBase
items={items.map((item) => ({
id: item.id,
label: item.label,
detail: item.detail,
}))}
/>
</div>
);
// 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 memo(CardStack);

View File

@@ -1,52 +1,187 @@
import { useEffect, useRef, useState } from "react";
import { useRef } from "react";
import { useGSAP } from "@gsap/react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import type { CardAnimationType, GridVariant } from "../types";
import { useDepth3DAnimation } from "./useDepth3DAnimation";
interface Depth3DAnimationOptions {
rotationX?: number;
rotationY?: number;
rotationZ?: number;
perspective?: number;
duration?: number;
gsap.registerPlugin(ScrollTrigger);
interface UseCardAnimationProps {
animationType: CardAnimationType | "depth-3d";
itemCount: number;
isGrid?: boolean;
supports3DAnimation?: boolean;
gridVariant?: GridVariant;
useIndividualTriggers?: boolean;
}
interface AnimationState {
transform: string;
transition: string;
itemRefs?: React.MutableRefObject<(HTMLElement | null)[]>;
containerRef?: React.MutableRefObject<HTMLDivElement | null>;
perspectiveRef?: React.MutableRefObject<HTMLDivElement | null>;
bottomContentRef?: React.MutableRefObject<HTMLDivElement | null>;
}
export const useCardAnimation = (
options: Depth3DAnimationOptions = {}
): AnimationState => {
const [state, setState] = useState<AnimationState>({
transform: "", transition: ""});
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);
const {
rotationX = 0,
rotationY = 0,
rotationZ = 0,
perspective = 1000,
duration = 0.3,
} = options;
// 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",
});
useEffect(() => {
const transform = `perspective(${perspective}px) rotateX(${rotationX}deg) rotateY(${rotationY}deg) rotateZ(${rotationZ}deg)`;
setState({
transform,
transition: `transform ${duration}s ease-out`,
itemRefs,
containerRef,
perspectiveRef,
bottomContentRef,
});
}, [rotationX, rotationY, rotationZ, perspective, duration]);
// 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;
return state;
useGSAP(() => {
if (effectiveAnimationType === "none" || effectiveAnimationType === "depth-3d" || itemRefs.current.length === 0) return;
const items = itemRefs.current.filter((el) => el !== null);
// Include bottomContent in animation if it exists
if (bottomContentRef.current) {
items.push(bottomContentRef.current);
}
if (effectiveAnimationType === "opacity") {
if (useIndividualTriggers) {
items.forEach((item) => {
gsap.fromTo(
item,
{ opacity: 0 },
{
opacity: 1,
duration: 1.25,
ease: "sine",
scrollTrigger: {
trigger: item,
start: "top 80%",
toggleActions: "play none none none",
},
}
);
});
} else {
gsap.fromTo(
items,
{ opacity: 0 },
{
opacity: 1,
duration: 1.25,
stagger: 0.15,
ease: "sine",
scrollTrigger: {
trigger: items[0],
start: "top 80%",
toggleActions: "play none none none",
},
}
);
}
} else if (effectiveAnimationType === "slide-up") {
items.forEach((item, index) => {
gsap.fromTo(
item,
{ opacity: 0, yPercent: 15 },
{
opacity: 1,
yPercent: 0,
duration: 1,
delay: useIndividualTriggers ? 0 : index * 0.15,
ease: "sine",
scrollTrigger: {
trigger: useIndividualTriggers ? item : items[0],
start: "top 80%",
toggleActions: "play none none none",
},
}
);
});
} else if (effectiveAnimationType === "scale-rotate") {
if (useIndividualTriggers) {
items.forEach((item) => {
gsap.fromTo(
item,
{ scaleX: 0, rotate: 10 },
{
scaleX: 1,
rotate: 0,
duration: 1,
ease: "power3",
scrollTrigger: {
trigger: item,
start: "top 80%",
toggleActions: "play none none none",
},
}
);
});
} else {
gsap.fromTo(
items,
{ scaleX: 0, rotate: 10 },
{
scaleX: 1,
rotate: 0,
duration: 1,
stagger: 0.15,
ease: "power3",
scrollTrigger: {
trigger: items[0],
start: "top 80%",
toggleActions: "play none none none",
},
}
);
}
} else if (effectiveAnimationType === "blur-reveal") {
if (useIndividualTriggers) {
items.forEach((item) => {
gsap.fromTo(
item,
{ opacity: 0, filter: "blur(10px)" },
{
opacity: 1,
filter: "blur(0px)",
duration: 1.2,
ease: "power2.out",
scrollTrigger: {
trigger: item,
start: "top 80%",
toggleActions: "play none none none",
},
}
);
});
} else {
gsap.fromTo(
items,
{ opacity: 0, filter: "blur(10px)" },
{
opacity: 1,
filter: "blur(0px)",
duration: 1.2,
stagger: 0.15,
ease: "power2.out",
scrollTrigger: {
trigger: items[0],
start: "top 80%",
toggleActions: "play none none none",
},
}
);
}
}
}, [effectiveAnimationType, itemCount, useIndividualTriggers]);
return { itemRefs, containerRef, perspectiveRef, bottomContentRef };
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,33 +1,202 @@
import React from "react";
import { useCardAnimation } from "@/components/cardStack/hooks/useCardAnimation";
"use client";
import React, { useEffect, useRef, memo, useState } from "react";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import CardStackTextBox from "../../CardStackTextBox";
import { useCardAnimation } from "../../hooks/useCardAnimation";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, ButtonAnimationType, CardAnimationType, TitleSegment } from "../../types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
gsap.registerPlugin(ScrollTrigger);
interface TimelineProcessFlowItem {
id: string;
content: React.ReactNode;
media: React.ReactNode;
reverse: boolean;
media: React.ReactElement;
content: React.ReactElement;
}
interface TimelineProcessFlowProps {
items?: TimelineProcessFlowItem[];
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;
}
export default function TimelineProcessFlow({ items = [] }: TimelineProcessFlowProps) {
const state = useCardAnimation({
rotationX: 0,
rotationY: 0,
rotationZ: 0,
perspective: 1000,
duration: 0.3,
});
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 (
<div className="timeline-process-flow">
{items.map((item) => (
<div key={item.id} className="timeline-item">
{item.content}
<section
className={cls(
"relative py-20 w-full",
useInvertedBackground && "bg-foreground",
className
)}
aria-label={ariaLabel}
>
<div className={cls("w-full flex flex-col gap-6", containerClassName)}>
<div className="relative w-content-width mx-auto">
<CardStackTextBox
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
/>
</div>
))}
</div>
<div className="relative w-full">
<div className="pointer-events-none absolute top-0 right-[var(--width-10)] md:right-auto md:left-1/2 md:-translate-x-1/2 w-px h-full z-10 overflow-hidden md:py-6" >
<div className="relative timeline-line h-full bg-foreground overflow-hidden">
<div className="w-full h-full bg-accent" ref={processLineRef} />
</div>
</div>
<ol className={cls("relative w-content-width mx-auto flex flex-col gap-10 md:gap-20 md:p-6", isMdScreen && "card", "md:rounded-theme-capped", gapClassName)}>
{items.map((item, index) => (
<li
key={item.id}
ref={(el) => {
itemRefs.current[index] = el;
}}
className={cls(
"relative z-10 w-full flex flex-col gap-6 md:gap-0 md:flex-row justify-between",
item.reverse && "flex-col md:flex-row-reverse",
itemClassName
)}
>
<div
className={cls("relative w-70 md:w-30", mediaWrapperClassName)}
>
{item.media}
</div>
<div
className={cls(
"absolute! top-1/2 right-[calc(var(--height-8)/-2)] md:right-auto md:left-1/2 md:-translate-x-1/2 -translate-y-1/2 h-8 aspect-square rounded-theme flex items-center justify-center z-10 primary-button",
numberClassName
)}
>
<p className="text-sm text-primary-cta-text">{item.id}</p>
</div>
<div className={cls("relative w-70 md:w-30", contentWrapperClassName)}>
{item.content}
</div>
</li>
))}
</ol>
</div>
</div>
</section>
);
}
};
TimelineProcessFlow.displayName = "TimelineProcessFlow";
export default memo(TimelineProcessFlow);

View File

@@ -1,68 +1,156 @@
import React, { useState, useEffect } from "react";
import { Product } from "@/lib/api/product";
"use client";
interface CatalogProduct {
id: string;
name: string;
price: string;
imageSrc: string;
imageAlt?: string;
rating?: number;
reviewCount?: string;
category?: string;
onProductClick?: () => void;
}
import { memo, useMemo, useCallback } from "react";
import { useRouter } from "next/navigation";
import Input from "@/components/form/Input";
import ProductDetailVariantSelect from "@/components/ecommerce/productDetail/ProductDetailVariantSelect";
import type { ProductVariant } from "@/components/ecommerce/productDetail/ProductDetailCard";
import { cls } from "@/lib/utils";
import { useProducts } from "@/hooks/useProducts";
import ProductCatalogItem from "./ProductCatalogItem";
import type { CatalogProduct } from "./ProductCatalogItem";
interface ProductCatalogProps {
products?: Product[];
loading?: boolean;
error?: string;
layout: "page" | "section";
products?: CatalogProduct[];
searchValue?: string;
onSearchChange?: (value: string) => void;
searchPlaceholder?: string;
filters?: ProductVariant[];
emptyMessage?: string;
className?: string;
gridClassName?: string;
cardClassName?: string;
imageClassName?: string;
searchClassName?: string;
filterClassName?: string;
toolbarClassName?: string;
}
export const ProductCatalog: React.FC<ProductCatalogProps> = ({
products = [],
loading = false,
error = ""}) => {
const [catalogProducts, setCatalogProducts] = useState<CatalogProduct[]>([]);
const ProductCatalog = ({
layout,
products: productsProp,
searchValue = "",
onSearchChange,
searchPlaceholder = "Search products...",
filters,
emptyMessage = "No products found",
className = "",
gridClassName = "",
cardClassName = "",
imageClassName = "",
searchClassName = "",
filterClassName = "",
toolbarClassName = "",
}: ProductCatalogProps) => {
const router = useRouter();
const { products: fetchedProducts, isLoading } = useProducts();
useEffect(() => {
if (!loading) {
const transformed = products.map((product) => ({
id: product.id,
name: product.name,
price: String(product.price),
imageSrc: product.imageSrc || "/placeholder.jpg", imageAlt: product.imageAlt || product.name,
rating: product.rating,
reviewCount: product.reviewCount,
category: product.category,
brand: product.brand,
onProductClick: () => {},
}));
setCatalogProducts(transformed);
const handleProductClick = useCallback((productId: string) => {
router.push(`/shop/${productId}`);
}, [router]);
const products: CatalogProduct[] = useMemo(() => {
if (productsProp && productsProp.length > 0) {
return productsProp;
}
if (fetchedProducts.length === 0) {
return [];
}
return fetchedProducts.map((product) => ({
id: product.id,
name: product.name,
price: product.price,
imageSrc: product.imageSrc,
imageAlt: product.imageAlt || product.name,
rating: product.rating || 0,
reviewCount: product.reviewCount,
category: product.brand,
onProductClick: () => handleProductClick(product.id),
}));
}, [productsProp, fetchedProducts, handleProductClick]);
if (isLoading && (!productsProp || productsProp.length === 0)) {
return (
<section
className={cls(
"relative w-content-width mx-auto",
layout === "page" ? "pt-hero-page-padding pb-20" : "py-20",
className
)}
>
<p className="text-sm text-foreground/50 text-center py-20">
Loading products...
</p>
</section>
);
}
}, [products, loading]);
if (error) {
return <div className="error">Error: {error}</div>;
}
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>
)}
if (loading) {
return <div className="loading">Loading...</div>;
}
return (
<div className="product-catalog">
<div className="product-grid">
{catalogProducts.map((product) => (
<div key={product.id} className="product-item">
<img src={product.imageSrc} alt={product.imageAlt} />
<h3>{product.name}</h3>
<p className="price">${product.price}</p>
{product.rating && <div className="rating">{product.rating} stars</div>}
{product.reviewCount && <div className="reviews">{product.reviewCount} reviews</div>}
</div>
))}
</div>
</div>
);
{products.length === 0 ? (
<p className="text-sm text-foreground/50 text-center py-20">
{emptyMessage}
</p>
) : (
<div
className={cls(
"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6",
gridClassName
)}
>
{products.map((product) => (
<ProductCatalogItem
key={product.id}
product={product}
className={cardClassName}
imageClassName={imageClassName}
/>
))}
</div>
)}
</section>
);
};
ProductCatalog.displayName = "ProductCatalog";
export default memo(ProductCatalog);

View File

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

View File

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

View File

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

View File

@@ -1,51 +1,131 @@
import React, { useState } from "react";
"use client";
import ContactForm from "@/components/form/ContactForm";
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
import { cls } from "@/lib/utils";
import { LucideIcon } from "lucide-react";
import { sendContactEmail } from "@/utils/sendContactEmail";
import type { ButtonAnimationType } from "@/types/button";
type ContactCenterBackgroundProps = Extract<
HeroBackgroundVariantProps,
| { variant: "plain" }
| { variant: "animated-grid" }
| { variant: "canvas-reveal" }
| { variant: "cell-wave" }
| { variant: "downward-rays-animated" }
| { variant: "downward-rays-animated-grid" }
| { variant: "downward-rays-static" }
| { variant: "downward-rays-static-grid" }
| { variant: "gradient-bars" }
| { variant: "radial-gradient" }
| { variant: "rotated-rays-animated" }
| { variant: "rotated-rays-animated-grid" }
| { variant: "rotated-rays-static" }
| { variant: "rotated-rays-static-grid" }
| { variant: "sparkles-gradient" }
>;
interface ContactCenterProps {
tag: string;
title: string;
description: string;
background?: { variant: string };
useInvertedBackground?: boolean;
inputPlaceholder?: string;
buttonText?: string;
termsText?: string;
title: string;
description: string;
tag: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
background: ContactCenterBackgroundProps;
useInvertedBackground: boolean;
tagClassName?: string;
inputPlaceholder?: string;
buttonText?: string;
termsText?: string;
onSubmit?: (email: string) => void;
ariaLabel?: string;
className?: string;
containerClassName?: string;
contentClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
formWrapperClassName?: string;
formClassName?: string;
inputClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
termsClassName?: string;
}
export default function ContactCenter({
tag,
title,
description,
background = { variant: "sparkles-gradient" },
useInvertedBackground = false,
inputPlaceholder = "Enter your email", buttonText = "Sign Up", termsText = "By clicking Sign Up you're confirming that you agree with our Terms and Conditions."}: ContactCenterProps) {
const [email, setEmail] = useState("");
const ContactCenter = ({
title,
description,
tag,
tagIcon,
tagAnimation,
background,
useInvertedBackground,
tagClassName = "",
inputPlaceholder = "Enter your email",
buttonText = "Sign Up",
termsText = "By clicking Sign Up you're confirming that you agree with our Terms and Conditions.",
onSubmit,
ariaLabel = "Contact section",
className = "",
containerClassName = "",
contentClassName = "",
titleClassName = "",
descriptionClassName = "",
formWrapperClassName = "",
formClassName = "",
inputClassName = "",
buttonClassName = "",
buttonTextClassName = "",
termsClassName = "",
}: ContactCenterProps) => {
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setEmail("");
};
const handleSubmit = async (email: string) => {
try {
await sendContactEmail({ email });
console.log("Email send successfully");
} catch (error) {
console.error("Failed to send email:", error);
}
};
return (
<div className="contact-center-container">
<div className="contact-content">
<div className="contact-tag">{tag}</div>
<h2 className="contact-title">{title}</h2>
<p className="contact-description">{description}</p>
<form onSubmit={handleSubmit} className="contact-form">
<input
type="email"
placeholder={inputPlaceholder}
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="contact-input"
/>
<button type="submit" className="contact-button">
{buttonText}
</button>
</form>
<p className="contact-terms">{termsText}</p>
</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 relative z-10", containerClassName)}>
<div className={cls("relative w-full card p-6 md:p-0 py-20 md:py-20 rounded-theme-capped flex items-center justify-center", contentClassName)}>
<div className="relative z-10 w-full md:w-1/2">
<ContactForm
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
title={title}
description={description}
useInvertedBackground={useInvertedBackground}
inputPlaceholder={inputPlaceholder}
buttonText={buttonText}
termsText={termsText}
onSubmit={handleSubmit}
centered={true}
tagClassName={tagClassName}
titleClassName={titleClassName}
descriptionClassName={descriptionClassName}
formWrapperClassName={cls("md:w-8/10 2xl:w-6/10", formWrapperClassName)}
formClassName={formClassName}
inputClassName={inputClassName}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
termsClassName={termsClassName}
/>
</div>
<div className="absolute inset w-full h-full z-0 rounded-theme-capped overflow-hidden" >
<HeroBackgrounds {...background} />
</div>
</div>
</div>
</section>
);
};
ContactCenter.displayName = "ContactCenter";
export default ContactCenter;

View File

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

View File

@@ -1,58 +1,171 @@
import React, { useState } from "react";
"use client";
import ContactForm from "@/components/form/ContactForm";
import MediaContent from "@/components/shared/MediaContent";
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
import { cls } from "@/lib/utils";
import { useButtonAnimation } from "@/components/hooks/useButtonAnimation";
import { LucideIcon } from "lucide-react";
import { sendContactEmail } from "@/utils/sendContactEmail";
import type { ButtonAnimationType } from "@/types/button";
type ContactSplitBackgroundProps = Extract<
HeroBackgroundVariantProps,
| { variant: "plain" }
| { variant: "animated-grid" }
| { variant: "canvas-reveal" }
| { variant: "cell-wave" }
| { variant: "downward-rays-animated" }
| { variant: "downward-rays-animated-grid" }
| { variant: "downward-rays-static" }
| { variant: "downward-rays-static-grid" }
| { variant: "gradient-bars" }
| { variant: "radial-gradient" }
| { variant: "rotated-rays-animated" }
| { variant: "rotated-rays-animated-grid" }
| { variant: "rotated-rays-static" }
| { variant: "rotated-rays-static-grid" }
| { variant: "sparkles-gradient" }
>;
interface ContactSplitProps {
tag: string;
title: string;
description: string;
background?: { variant: string };
useInvertedBackground?: boolean;
imageSrc?: string;
inputPlaceholder?: string;
buttonText?: string;
termsText?: string;
title: string;
description: string;
tag: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
background: ContactSplitBackgroundProps;
useInvertedBackground: boolean;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
mediaPosition?: "left" | "right";
mediaAnimation: ButtonAnimationType;
inputPlaceholder?: string;
buttonText?: string;
termsText?: string;
onSubmit?: (email: string) => void;
ariaLabel?: string;
className?: string;
containerClassName?: string;
contentClassName?: string;
contactFormClassName?: string;
tagClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
formWrapperClassName?: string;
formClassName?: string;
inputClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
termsClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
}
export default function ContactSplit({
tag,
title,
description,
background = { variant: "sparkles-gradient" },
useInvertedBackground = false,
imageSrc,
inputPlaceholder = "Enter your email", buttonText = "Sign Up", termsText = "By clicking Sign Up you're confirming that you agree with our Terms and Conditions."}: ContactSplitProps) {
const [email, setEmail] = useState("");
const ContactSplit = ({
title,
description,
tag,
tagIcon,
tagAnimation,
background,
useInvertedBackground,
imageSrc,
videoSrc,
imageAlt = "",
videoAriaLabel = "Contact section video",
mediaPosition = "right",
mediaAnimation,
inputPlaceholder = "Enter your email",
buttonText = "Sign Up",
termsText = "By clicking Sign Up you're confirming that you agree with our Terms and Conditions.",
onSubmit,
ariaLabel = "Contact section",
className = "",
containerClassName = "",
contentClassName = "",
contactFormClassName = "",
tagClassName = "",
titleClassName = "",
descriptionClassName = "",
formWrapperClassName = "",
formClassName = "",
inputClassName = "",
buttonClassName = "",
buttonTextClassName = "",
termsClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
}: ContactSplitProps) => {
const { containerRef: mediaContainerRef } = useButtonAnimation({ animationType: mediaAnimation });
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setEmail("");
};
const handleSubmit = async (email: string) => {
try {
await sendContactEmail({ email });
console.log("Email send successfully");
} catch (error) {
console.error("Failed to send email:", error);
}
};
return (
<div className="contact-split-container">
<div className="contact-split-form">
<div className="contact-tag">{tag}</div>
<h2 className="contact-title">{title}</h2>
<p className="contact-description">{description}</p>
<form onSubmit={handleSubmit} className="contact-form">
<input
type="email"
placeholder={inputPlaceholder}
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="contact-input"
/>
<button type="submit" className="contact-button">
{buttonText}
</button>
</form>
<p className="contact-terms">{termsText}</p>
</div>
{imageSrc && (
<div className="contact-split-image">
<img src={imageSrc} alt="Contact" />
const contactContent = (
<div className="relative card rounded-theme-capped p-6 py-15 md:py-6 flex items-center justify-center">
<ContactForm
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
title={title}
description={description}
useInvertedBackground={useInvertedBackground}
inputPlaceholder={inputPlaceholder}
buttonText={buttonText}
termsText={termsText}
onSubmit={handleSubmit}
centered={true}
className={cls("w-full", contactFormClassName)}
tagClassName={tagClassName}
titleClassName={titleClassName}
descriptionClassName={descriptionClassName}
formWrapperClassName={cls("w-full md:w-8/10 2xl:w-7/10", formWrapperClassName)}
formClassName={formClassName}
inputClassName={inputClassName}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
termsClassName={termsClassName}
/>
<div className="absolute inset w-full h-full z-0 rounded-theme-capped overflow-hidden" >
<HeroBackgrounds {...background} />
</div>
</div>
)}
</div>
);
}
);
const mediaContent = (
<div ref={mediaContainerRef} className={cls("overflow-hidden rounded-theme-capped card h-130", mediaWrapperClassName)}>
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("relative z-1 w-full h-full object-cover", mediaClassName)}
/>
</div>
);
return (
<section aria-label={ariaLabel} className={cls("relative py-20 w-full", useInvertedBackground && "bg-foreground", className)}>
<div className={cls("w-content-width mx-auto relative z-10", containerClassName)}>
<div className={cls("grid grid-cols-1 md:grid-cols-2 gap-6 md:auto-rows-fr", contentClassName)}>
{mediaPosition === "left" && mediaContent}
{contactContent}
{mediaPosition === "right" && mediaContent}
</div>
</div>
</section>
);
};
ContactSplit.displayName = "ContactSplit";
export default ContactSplit;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,18 +1,132 @@
import React from "react";
"use client";
import TextBox from "@/components/Textbox";
import Dashboard from "@/components/shared/Dashboard";
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, ButtonAnimationType } from "@/types/button";
import type { DashboardSidebarItem, DashboardStat, DashboardListItem } from "@/components/shared/Dashboard";
import type { ChartDataItem } from "@/components/bento/BentoLineChart/utils";
type HeroBillboardDashboardBackgroundProps = Extract<
HeroBackgroundVariantProps,
| { variant: "plain" }
| { variant: "animated-grid" }
| { variant: "canvas-reveal" }
| { variant: "cell-wave" }
| { variant: "downward-rays-animated" }
| { variant: "downward-rays-animated-grid" }
| { variant: "downward-rays-static" }
| { variant: "downward-rays-static-grid" }
| { variant: "gradient-bars" }
| { variant: "radial-gradient" }
| { variant: "rotated-rays-animated" }
| { variant: "rotated-rays-animated-grid" }
| { variant: "rotated-rays-static" }
| { variant: "rotated-rays-static-grid" }
| { variant: "sparkles-gradient" }
>;
interface HeroBillboardDashboardProps {
title?: string;
description?: string;
title: string;
description: string;
background: HeroBillboardDashboardBackgroundProps;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
ariaLabel?: string;
dashboard: {
title: string;
stats: [DashboardStat, DashboardStat, DashboardStat];
logoIcon: LucideIcon;
sidebarItems: DashboardSidebarItem[];
searchPlaceholder?: string;
buttons: ButtonConfig[];
chartTitle?: string;
chartData?: ChartDataItem[];
listItems: DashboardListItem[];
listTitle?: string;
imageSrc: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
className?: string;
containerClassName?: string;
sidebarClassName?: string;
statClassName?: string;
chartClassName?: string;
listClassName?: string;
};
className?: string;
containerClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
dashboardClassName?: string;
}
export default function HeroBillboardDashboard({
title = "Dashboard", description = "Welcome"}: HeroBillboardDashboardProps) {
const HeroBillboardDashboard = ({
title,
description,
background,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
ariaLabel = "Hero section",
dashboard,
className = "",
containerClassName = "",
textBoxClassName = "",
titleClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
dashboardClassName = "",
}: HeroBillboardDashboardProps) => {
return (
<div className="hero-billboard-dashboard">
<h1>{title}</h1>
<p>{description}</p>
<Dashboard data={[]} />
</div>
<section
aria-label={ariaLabel}
className={cls("relative w-full py-hero-page-padding", className)}
>
<HeroBackgrounds {...background} />
<div className={cls("w-content-width mx-auto flex flex-col gap-14 md:gap-15 relative z-10", containerClassName)}>
<TextBox
title={title}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
className={cls("flex flex-col gap-3 md:gap-3", textBoxClassName)}
titleClassName={cls("text-6xl font-medium text-balance", titleClassName)}
descriptionClassName={cls("text-base md:text-lg leading-tight", descriptionClassName)}
tagClassName={cls("px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-1", tagClassName)}
buttonContainerClassName={cls("flex flex-wrap gap-4 max-md:justify-center mt-2", buttonContainerClassName)}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
center={true}
/>
<Dashboard
{...dashboard}
className={cls(dashboard.className, dashboardClassName)}
/>
</div>
</section>
);
}
};
HeroBillboardDashboard.displayName = "HeroBillboardDashboard";
export default HeroBillboardDashboard;

View File

@@ -1,33 +1,200 @@
import React from "react";
"use client";
import TextBox from "@/components/Textbox";
import MediaContent from "@/components/shared/MediaContent";
import AutoCarousel from "@/components/cardStack/layouts/carousels/AutoCarousel";
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
import { cls } from "@/lib/utils";
import { useButtonAnimation } from "@/components/hooks/useButtonAnimation";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, ButtonAnimationType } from "@/types/button";
export interface MediaItem {
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
}
type HeroBillboardGalleryBackgroundProps = Extract<
HeroBackgroundVariantProps,
| { variant: "plain" }
| { variant: "animated-grid" }
| { variant: "canvas-reveal" }
| { variant: "cell-wave" }
| { variant: "downward-rays-animated" }
| { variant: "downward-rays-animated-grid" }
| { variant: "downward-rays-static" }
| { variant: "downward-rays-static-grid" }
| { variant: "gradient-bars" }
| { variant: "radial-gradient" }
| { variant: "rotated-rays-animated" }
| { variant: "rotated-rays-animated-grid" }
| { variant: "rotated-rays-static" }
| { variant: "rotated-rays-static-grid" }
| { variant: "sparkles-gradient" }
>;
interface HeroBillboardGalleryProps {
title?: string;
description?: string;
textboxLayout?: string;
animationType?: string;
className?: string;
carouselClassName?: string;
containerClassName?: string;
itemClassName?: string;
title: string;
description: string;
background: HeroBillboardGalleryBackgroundProps;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
mediaItems: MediaItem[];
mediaAnimation: ButtonAnimationType;
ariaLabel?: string;
mediaItems?: Array<{ imageSrc?: string; videoSrc?: string; imageAlt?: string }>;
className?: string;
containerClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
mediaWrapperClassName?: string;
imageClassName?: string;
}
export default function HeroBillboardGallery({
title = "Gallery", description = "Welcome", textboxLayout = "default", animationType = "slide-up", className = "", carouselClassName = "", containerClassName = "", itemClassName = "", ariaLabel = "Gallery section", mediaItems = [],
}: HeroBillboardGalleryProps) {
const items = mediaItems.map((item) => ({
imageSrc: item.imageSrc,
videoSrc: item.videoSrc,
imageAlt: item.imageAlt,
}));
const HeroBillboardGallery = ({
title,
description,
background,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
mediaItems,
mediaAnimation,
ariaLabel = "Hero section",
className = "",
containerClassName = "",
textBoxClassName = "",
titleClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
mediaWrapperClassName = "",
imageClassName = "",
}: HeroBillboardGalleryProps) => {
const { containerRef: mediaContainerRef } = useButtonAnimation({ animationType: mediaAnimation });
return (
<div className={`hero-billboard-gallery ${className}`} aria-label={ariaLabel}>
<h1>{title}</h1>
<p>{description}</p>
<AutoCarousel items={items} />
const renderCarouselItem = (item: MediaItem, index: number) => (
<div
key={index}
className="w-full aspect-[4/5] overflow-hidden rounded-theme-capped card p-2 shadow-lg"
>
<MediaContent
imageSrc={item.imageSrc}
videoSrc={item.videoSrc}
imageAlt={item.imageAlt || ""}
videoAriaLabel={item.videoAriaLabel || "Gallery media"}
imageClassName="h-full object-cover"
/>
</div>
);
}
const itemCount = mediaItems?.length || 0;
const desktopWidthClass = itemCount === 3 ? "md:w-[24%]" : itemCount === 4 ? "md:w-[24%]" : "md:w-[23%]";
return (
<section
aria-label={ariaLabel}
className={cls(
"relative w-full py-hero-page-padding md:h-svh md:py-0",
className
)}
>
<HeroBackgrounds {...background} />
<div className={cls(
"mx-auto flex flex-col gap-14 relative z-10",
"w-full md:w-content-width md:h-full md:items-center md:justify-center",
containerClassName
)}>
<TextBox
title={title}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
className={cls(
"flex flex-col gap-3 md:gap-3 w-content-width mx-auto",
textBoxClassName
)}
titleClassName={cls("text-6xl font-medium text-balance", titleClassName)}
descriptionClassName={cls("text-base md:text-lg leading-tight", descriptionClassName)}
tagClassName={cls("px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-1", tagClassName)}
buttonContainerClassName={cls("flex flex-wrap gap-4 max-md:justify-center mt-2", buttonContainerClassName)}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
center={true}
/>
<div className={cls("w-full", mediaWrapperClassName)}>
<div className="block md:hidden -mx-[var(--content-padding)]">
<AutoCarousel
title=""
description=""
textboxLayout="default"
animationType="none"
className="py-0"
carouselClassName="py-0"
containerClassName="!w-full"
itemClassName="!w-55"
ariaLabel="Hero gallery carousel"
showTextBox={false}
>
{mediaItems?.slice(0, 5).map(renderCarouselItem)}
</AutoCarousel>
</div>
<div ref={mediaContainerRef} className="hidden md:flex justify-center items-center pt-2">
<div className="relative flex items-center justify-center w-full">
{mediaItems?.slice(0, 5).map((item, index) => {
const rotations = ["-rotate-6", "rotate-6", "-rotate-6", "rotate-6", "-rotate-6"];
const zIndexes = ["z-10", "z-20", "z-30", "z-40", "z-50"];
const translates = ["-translate-y-5", "translate-y-5", "-translate-y-5", "translate-y-5", "-translate-y-5"];
const marginClass = index > 0 ? "-ml-12 md:-ml-15" : "";
return (
<div
key={index}
className={cls(
"relative aspect-[4/5] overflow-hidden rounded-theme-capped card p-2 shadow-lg transition-transform duration-500 ease-out hover:scale-110",
desktopWidthClass,
rotations[index],
zIndexes[index],
translates[index],
marginClass
)}
>
<MediaContent
imageSrc={item.imageSrc}
videoSrc={item.videoSrc}
imageAlt={item.imageAlt || ""}
videoAriaLabel={item.videoAriaLabel || "Gallery media"}
imageClassName={cls("z-1 h-full object-cover", imageClassName)}
/>
</div>
);
})}
</div>
</div>
</div>
</div>
</section>
);
};
HeroBillboardGallery.displayName = "HeroBillboardGallery";
export default HeroBillboardGallery;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,60 +1,248 @@
import React from "react";
"use client";
interface PricingPlan {
id: string;
badge: string;
price: string;
subtitle: string;
features: string[];
buttons?: Array<{ text: string; href?: string; onClick?: () => void }>;
}
import { memo } from "react";
import CardStack from "@/components/cardStack/CardStack";
import Button from "@/components/button/Button";
import PricingBadge from "@/components/shared/PricingBadge";
import PricingFeatureList from "@/components/shared/PricingFeatureList";
import { getButtonProps } from "@/lib/buttonUtils";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type PricingPlan = {
id: string;
badge: string;
badgeIcon?: LucideIcon;
price: string;
subtitle: string;
buttons: ButtonConfig[];
features: string[];
};
interface PricingCardEightProps {
plans: PricingPlan[];
title: string;
description: string;
animationType?: string;
textboxLayout?: string;
useInvertedBackground?: boolean;
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;
}
export default function PricingCardEight({
plans,
title,
description,
animationType = "slide-up", textboxLayout = "default", useInvertedBackground = false,
}: PricingCardEightProps) {
return (
<div className="pricing-card-eight-container">
<div className="pricing-header">
<h2 className="pricing-title">{title}</h2>
<p className="pricing-description">{description}</p>
</div>
<div className="pricing-plans-grid">
{plans.map((plan) => (
<div key={plan.id} className="pricing-plan-card">
<div className="plan-badge">{plan.badge}</div>
<div className="plan-price">{plan.price}</div>
<div className="plan-subtitle">{plan.subtitle}</div>
<div className="plan-features">
{plan.features.map((feature, index) => (
<div key={index} className="plan-feature">
{feature}
</div>
))}
</div>
{plan.buttons && (
<div className="plan-buttons">
{plan.buttons.map((button, index) => (
<button key={index} className="plan-button">
{button.text}
</button>
))}
</div>
)}
</div>
))}
</div>
</div>
);
interface PricingCardItemProps {
plan: PricingPlan;
shouldUseLightText: boolean;
cardClassName?: string;
badgeClassName?: string;
priceClassName?: string;
subtitleClassName?: string;
planButtonContainerClassName?: string;
planButtonClassName?: string;
featuresClassName?: string;
featureItemClassName?: string;
}
const PricingCardItem = memo(({
plan,
shouldUseLightText,
cardClassName = "",
badgeClassName = "",
priceClassName = "",
subtitleClassName = "",
planButtonContainerClassName = "",
planButtonClassName = "",
featuresClassName = "",
featureItemClassName = "",
}: PricingCardItemProps) => {
const theme = useTheme();
const getButtonConfigProps = () => {
if (theme.defaultButtonVariant === "hover-bubble") {
return { bgClassName: "w-full" };
}
if (theme.defaultButtonVariant === "icon-arrow") {
return { className: "justify-between" };
}
return {};
};
return (
<div className={cls("relative h-full card text-foreground rounded-theme-capped p-3 flex flex-col gap-3", cardClassName)}>
<div className="relative secondary-button p-3 flex flex-col gap-3 rounded-theme-capped" >
<PricingBadge
badge={plan.badge}
badgeIcon={plan.badgeIcon}
className={badgeClassName}
/>
<div className="relative z-1 flex flex-col gap-1">
<div className="text-5xl font-medium text-foreground">
{plan.price}
</div>
<p className="text-base text-foreground">
{plan.subtitle}
</p>
</div>
{plan.buttons && plan.buttons.length > 0 && (
<div className={cls("relative z-1 w-full flex flex-col gap-3", planButtonContainerClassName)}>
{plan.buttons.slice(0, 2).map((button, index) => (
<Button
key={`${button.text}-${index}`}
{...getButtonProps(
{ ...button, props: { ...button.props, ...getButtonConfigProps() } },
index,
theme.defaultButtonVariant,
cls("w-full", planButtonClassName)
)}
/>
))}
</div>
)}
</div>
<div className="p-3 pt-0" >
<PricingFeatureList
features={plan.features}
shouldUseLightText={shouldUseLightText}
className={cls("mt-1", featuresClassName)}
featureItemClassName={featureItemClassName}
/>
</div>
</div>
);
});
PricingCardItem.displayName = "PricingCardItem";
const PricingCardEight = ({
plans,
carouselMode = "buttons",
uniformGridCustomHeightClasses,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Pricing section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
badgeClassName = "",
priceClassName = "",
subtitleClassName = "",
planButtonContainerClassName = "",
planButtonClassName = "",
featuresClassName = "",
featureItemClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: PricingCardEightProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<CardStack
useInvertedBackground={useInvertedBackground}
mode={carouselMode}
gridVariant="uniform-all-items-equal"
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{plans.map((plan, index) => (
<PricingCardItem
key={`${plan.id}-${index}`}
plan={plan}
shouldUseLightText={shouldUseLightText}
cardClassName={cardClassName}
badgeClassName={badgeClassName}
priceClassName={priceClassName}
subtitleClassName={subtitleClassName}
planButtonContainerClassName={planButtonContainerClassName}
planButtonClassName={planButtonClassName}
featuresClassName={featuresClassName}
featureItemClassName={featureItemClassName}
/>
))}
</CardStack>
);
};
PricingCardEight.displayName = "PricingCardEight";
export default PricingCardEight;

View File

@@ -1,51 +1,231 @@
"use client";
import React from "react";
import { Check } from "lucide-react";
import CardList from "@/components/cardStack/CardList";
import Tag from "@/components/shared/Tag";
import Button from "@/components/button/Button";
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, ButtonAnimationType, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type PricingPlan = {
id: string;
tag: string;
tagIcon?: LucideIcon;
price: string;
period: string;
description: string;
button: ButtonConfig;
featuresTitle: string;
features: string[];
};
interface PricingCardFiveProps {
plans?: any[];
title?: string;
description?: string;
animationType?: string;
useInvertedBackground?: boolean;
textboxLayout?: string;
tag?: string;
tagIcon?: any;
tagAnimation?: string;
buttons?: any[];
buttonAnimation?: string;
titleSegments?: any[];
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxTitleClassName?: string;
textBoxDescriptionClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
plans: PricingPlan[];
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxTitleClassName?: string;
textBoxDescriptionClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
cardContentClassName?: string;
planTagClassName?: string;
planPriceClassName?: string;
planPeriodClassName?: string;
planDescriptionClassName?: string;
planButtonClassName?: string;
planButtonTextClassName?: string;
featuresTitleClassName?: string;
featuresListClassName?: string;
featureItemClassName?: string;
featureIconClassName?: string;
featureTextClassName?: string;
}
export default function PricingCardFive({
plans = [],
title = "Pricing", description = "Our pricing plans", animationType = "slide-up", useInvertedBackground = false,
textboxLayout = "default"}: PricingCardFiveProps) {
const items = plans.map((plan) => ({
id: plan.id,
label: plan.badge,
detail: plan.price,
}));
const PricingCardFive = ({
plans,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Pricing section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxDescriptionClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
cardContentClassName = "",
planTagClassName = "",
planPriceClassName = "",
planPeriodClassName = "",
planDescriptionClassName = "",
planButtonClassName = "",
planButtonTextClassName = "",
featuresTitleClassName = "",
featuresListClassName = "",
featureItemClassName = "",
featureIconClassName = "",
featureTextClassName = "",
}: PricingCardFiveProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<div className="pricing-card-five">
<h2>{title}</h2>
<p>{description}</p>
<CardList items={items} />
</div>
);
}
const getButtonConfigProps = () => {
if (theme.defaultButtonVariant === "hover-bubble") {
return { bgClassName: "w-full" };
}
if (theme.defaultButtonVariant === "icon-arrow") {
return { className: "justify-between" };
}
return {};
};
return (
<CardList
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
animationType={animationType}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
cardClassName={cardClassName}
titleClassName={textBoxTitleClassName}
descriptionClassName={textBoxDescriptionClassName}
textBoxClassName={textBoxClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
ariaLabel={ariaLabel}
>
{plans.map((plan) => (
<div
key={plan.id}
className={cls(
"relative z-1 w-full min-h-0 h-full flex flex-col md:flex-row justify-between items-stretch gap-8 md:gap-15 p-6 md:p-15",
cardContentClassName
)}
>
<div className="w-full md:w-1/2 min-w-0 flex flex-col justify-between gap-6">
<div className="flex flex-col gap-4">
<Tag
text={plan.tag}
icon={plan.tagIcon}
className={planTagClassName}
/>
<div className="flex items-baseline gap-1 mt-1">
<span className={cls(
"text-5xl md:text-6xl font-medium",
shouldUseLightText ? "text-background" : "text-foreground",
planPriceClassName
)}>
{plan.price}
</span>
<span className={cls(
"text-xl",
shouldUseLightText ? "text-background" : "text-foreground",
planPeriodClassName
)}>
{plan.period}
</span>
</div>
<p className={cls(
"text-2xl leading-tight text-balance",
shouldUseLightText ? "text-background" : "text-foreground",
planDescriptionClassName
)}>
{plan.description}
</p>
</div>
<Button
{...getButtonProps(
{ ...plan.button, props: { ...plan.button.props, ...getButtonConfigProps() } },
0,
theme.defaultButtonVariant,
cls("w-full h-12", planButtonClassName),
planButtonTextClassName
)}
/>
</div>
<div className="relative z-1 w-full h-px bg-foreground/20 md:hidden" />
<div className="w-full md:w-1/2 min-w-0 flex flex-col gap-4">
<h3 className={cls(
"text-xl",
shouldUseLightText ? "text-background" : "text-foreground",
featuresTitleClassName
)}>
{plan.featuresTitle}
</h3>
<ul className={cls("flex flex-col gap-3", featuresListClassName)}>
{plan.features.map((feature, index) => (
<li key={index} className={cls("flex items-start gap-3", featureItemClassName)}>
<div className={cls("flex-shrink-0 h-6 w-auto aspect-square rounded-theme primary-button flex items-center justify-center", featureIconClassName)}>
<Check className="h-4/10 w-4/10 text-primary-cta-text" strokeWidth={2.5} />
</div>
<span className={cls(
"text-sm leading-[1.4]",
shouldUseLightText ? "text-background/80" : "text-foreground/80",
featureTextClassName
)}>
{feature}
</span>
</li>
))}
</ul>
</div>
</div>
))}
</CardList>
);
};
PricingCardFive.displayName = "PricingCardFive";
export default PricingCardFive;

View File

@@ -1,51 +1,216 @@
"use client";
import React from "react";
import { Check } from "lucide-react";
import CardList from "@/components/cardStack/CardList";
import Button from "@/components/button/Button";
import MediaContent from "@/components/shared/MediaContent";
import Tag from "@/components/shared/Tag";
import { getButtonProps } from "@/lib/buttonUtils";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type PricingPlan = {
id: string;
title: string;
price: string;
period: string;
features: string[];
button: ButtonConfig;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
};
interface PricingCardNineProps {
plans?: any[];
title?: string;
description?: string;
animationType?: string;
useInvertedBackground?: boolean;
textboxLayout?: string;
tag?: string;
tagIcon?: any;
tagAnimation?: string;
buttons?: any[];
buttonAnimation?: string;
titleSegments?: any[];
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxTitleClassName?: string;
textBoxDescriptionClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
plans: PricingPlan[];
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxTitleClassName?: string;
textBoxDescriptionClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
cardContentClassName?: string;
planImageWrapperClassName?: string;
planImageClassName?: string;
planTitleClassName?: string;
planPriceClassName?: string;
planButtonClassName?: string;
planButtonTextClassName?: string;
featuresListClassName?: string;
featureItemClassName?: string;
featureIconClassName?: string;
featureTextClassName?: string;
}
export default function PricingCardNine({
plans = [],
title = "Pricing", description = "Our pricing plans", animationType = "slide-up", useInvertedBackground = false,
textboxLayout = "default"}: PricingCardNineProps) {
const items = plans.map((plan) => ({
id: plan.id,
label: plan.badge,
detail: plan.price,
}));
const PricingCardNine = ({
plans,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Pricing section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxDescriptionClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
cardContentClassName = "",
planImageWrapperClassName = "",
planImageClassName = "",
planTitleClassName = "",
planPriceClassName = "",
planButtonClassName = "",
planButtonTextClassName = "",
featuresListClassName = "",
featureItemClassName = "",
featureIconClassName = "",
featureTextClassName = "",
}: PricingCardNineProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<div className="pricing-card-nine">
<h2>{title}</h2>
<p>{description}</p>
<CardList items={items} />
</div>
);
}
const getButtonConfigProps = () => {
if (theme.defaultButtonVariant === "hover-bubble") {
return { bgClassName: "w-full" };
}
if (theme.defaultButtonVariant === "icon-arrow") {
return { className: "justify-between" };
}
return {};
};
return (
<CardList
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
animationType={animationType}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
cardClassName={cardClassName}
titleClassName={textBoxTitleClassName}
descriptionClassName={textBoxDescriptionClassName}
textBoxClassName={textBoxClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
ariaLabel={ariaLabel}
>
{plans.map((plan) => (
<div
key={plan.id}
className={cls(
"relative z-1 w-full min-h-0 h-full flex flex-col md:flex-row items-stretch gap-6 md:gap-10 p-4 md:p-6",
cardContentClassName
)}
>
<div className={cls("w-full md:w-1/2 min-w-0 aspect-square md:aspect-[4/3]", planImageWrapperClassName)}>
<MediaContent
imageSrc={plan.imageSrc}
videoSrc={plan.videoSrc}
imageAlt={plan.imageAlt || plan.title}
videoAriaLabel={plan.videoAriaLabel || plan.title}
imageClassName={cls("w-full h-full object-cover rounded-theme", planImageClassName)}
/>
</div>
<div className="w-full md:w-1/2 min-w-0 flex flex-col justify-center gap-6 py-2">
<div className="flex flex-col gap-4">
<Tag
text={`${plan.price}${plan.period}`}
useInvertedBackground={useInvertedBackground}
className={planPriceClassName}
/>
<h3 className={cls(
"text-4xl md:text-5xl font-medium mb-1 truncate",
shouldUseLightText ? "text-background" : "text-foreground",
planTitleClassName
)}>
{plan.title}
</h3>
<ul className={cls("flex flex-col gap-3", featuresListClassName)}>
{plan.features.map((feature, index) => (
<li key={index} className={cls("flex items-start gap-3", featureItemClassName)}>
<div className={cls("flex-shrink-0 h-6 w-auto aspect-square rounded-theme primary-button flex items-center justify-center", featureIconClassName)}>
<Check className="h-4/10 w-4/10 text-primary-cta-text" strokeWidth={2.5} />
</div>
<span className={cls(
"text-sm leading-[1.4]",
shouldUseLightText ? "text-background/80" : "text-foreground/80",
featureTextClassName
)}>
{feature}
</span>
</li>
))}
</ul>
</div>
<Button
{...getButtonProps(
{ ...plan.button, props: { ...plan.button.props, ...getButtonConfigProps() } },
0,
theme.defaultButtonVariant,
planButtonClassName,
planButtonTextClassName
)}
/>
</div>
</div>
))}
</CardList>
);
};
PricingCardNine.displayName = "PricingCardNine";
export default PricingCardNine;

View File

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

View File

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

View File

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

View File

@@ -1,35 +1,238 @@
import React from "react";
import { Product } from "@/lib/api/product";
"use client";
import { memo, useCallback } from "react";
import { useRouter } from "next/navigation";
import CardStack from "@/components/cardStack/CardStack";
import ProductImage from "@/components/shared/ProductImage";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import { useProducts } from "@/hooks/useProducts";
import type { Product } from "@/lib/api/product";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, GridVariant, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type ProductCardFourGridVariant = Exclude<GridVariant, "timeline" | "items-top-row-full-width-bottom" | "full-width-top-items-bottom-row">;
type ProductCard = Product & {
variant: string;
};
interface ProductCardFourProps {
products?: Product[];
title?: string;
description?: string;
products?: ProductCard[];
carouselMode?: "auto" | "buttons";
gridVariant: ProductCardFourGridVariant;
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
imageClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
cardNameClassName?: string;
cardPriceClassName?: string;
cardVariantClassName?: string;
actionButtonClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
}
export default function ProductCardFour({
products = [],
title = "Products", description = "Our premium product collection"}: ProductCardFourProps) {
return (
<div className="product-card-four-container">
<div className="product-header">
<h2 className="product-title">{title}</h2>
<p className="product-description">{description}</p>
</div>
<div className="product-grid">
{products.map((product) => (
<div key={product.id} className="product-card">
{product.imageSrc && (
<img src={product.imageSrc} alt={product.name} />
)}
<h3 className="product-name">{product.name}</h3>
<p className="product-price">${product.price}</p>
{product.description && (
<p className="product-desc">{product.description}</p>
)}
</div>
))}
</div>
</div>
);
interface ProductCardItemProps {
product: ProductCard;
shouldUseLightText: boolean;
cardClassName?: string;
imageClassName?: string;
cardNameClassName?: string;
cardPriceClassName?: string;
cardVariantClassName?: string;
actionButtonClassName?: string;
}
const ProductCardItem = memo(({
product,
shouldUseLightText,
cardClassName = "",
imageClassName = "",
cardNameClassName = "",
cardPriceClassName = "",
cardVariantClassName = "",
actionButtonClassName = "",
}: ProductCardItemProps) => {
return (
<article
className={cls("card group relative h-full flex flex-col gap-4 cursor-pointer p-4 rounded-theme-capped", cardClassName)}
onClick={product.onProductClick}
role="article"
aria-label={`${product.name} - ${product.price}`}
>
<ProductImage
imageSrc={product.imageSrc}
imageAlt={product.imageAlt || product.name}
isFavorited={product.isFavorited}
onFavoriteToggle={product.onFavorite}
showActionButton={true}
actionButtonAriaLabel={`View ${product.name} details`}
imageClassName={imageClassName}
actionButtonClassName={actionButtonClassName}
/>
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between gap-4">
<div className="flex flex-col gap-0 flex-1 min-w-0">
<h3 className={cls("text-base font-medium leading-[1.3]", shouldUseLightText ? "text-background" : "text-foreground", cardNameClassName)}>
{product.name}
</h3>
<p className={cls("text-sm leading-[1.3]", shouldUseLightText ? "text-background/60" : "text-foreground/60", cardVariantClassName)}>
{product.variant}
</p>
</div>
<p className={cls("text-base font-medium leading-[1.3] flex-shrink-0", shouldUseLightText ? "text-background" : "text-foreground", cardPriceClassName)}>
{product.price}
</p>
</div>
</div>
</article>
);
});
ProductCardItem.displayName = "ProductCardItem";
const ProductCardFour = ({
products: productsProp,
carouselMode = "buttons",
gridVariant,
uniformGridCustomHeightClasses = "min-h-95 2xl:min-h-105",
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Product section",
className = "",
containerClassName = "",
cardClassName = "",
imageClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
cardNameClassName = "",
cardPriceClassName = "",
cardVariantClassName = "",
actionButtonClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: ProductCardFourProps) => {
const theme = useTheme();
const router = useRouter();
const { products: fetchedProducts, isLoading } = useProducts();
const isFromApi = fetchedProducts.length > 0;
const products = (isFromApi ? fetchedProducts : productsProp) as ProductCard[];
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const handleProductClick = useCallback((product: ProductCard) => {
if (isFromApi) {
router.push(`/shop/${product.id}`);
} else {
product.onProductClick?.();
}
}, [isFromApi, router]);
if (isLoading && !productsProp) {
return (
<div className="w-content-width mx-auto py-20 text-center">
<p className="text-foreground">Loading products...</p>
</div>
);
}
if (!products || products.length === 0) {
return null;
}
return (
<CardStack
mode={carouselMode}
gridVariant={gridVariant}
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{products?.map((product, index) => (
<ProductCardItem
key={`${product.id}-${index}`}
product={{ ...product, onProductClick: () => handleProductClick(product) }}
shouldUseLightText={shouldUseLightText}
cardClassName={cardClassName}
imageClassName={imageClassName}
cardNameClassName={cardNameClassName}
cardPriceClassName={cardPriceClassName}
cardVariantClassName={cardVariantClassName}
actionButtonClassName={actionButtonClassName}
/>
))}
</CardStack>
);
};
ProductCardFour.displayName = "ProductCardFour";
export default ProductCardFour;

View File

@@ -1,35 +1,226 @@
import React from "react";
import { Product } from "@/lib/api/product";
"use client";
import { memo, useCallback } from "react";
import { useRouter } from "next/navigation";
import { ArrowUpRight } from "lucide-react";
import CardStack from "@/components/cardStack/CardStack";
import ProductImage from "@/components/shared/ProductImage";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import { useProducts } from "@/hooks/useProducts";
import type { Product } from "@/lib/api/product";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, GridVariant, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type ProductCardOneGridVariant = Exclude<GridVariant, "timeline">;
type ProductCard = Product;
interface ProductCardOneProps {
products?: Product[];
title?: string;
description?: string;
products?: ProductCard[];
carouselMode?: "auto" | "buttons";
gridVariant: ProductCardOneGridVariant;
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
imageClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
cardNameClassName?: string;
cardPriceClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
}
export default function ProductCardOne({
products = [],
title = "Products", description = "Our premium product collection"}: ProductCardOneProps) {
return (
<div className="product-card-one-container">
<div className="product-header">
<h2 className="product-title">{title}</h2>
<p className="product-description">{description}</p>
</div>
<div className="product-grid">
{products.map((product) => (
<div key={product.id} className="product-card">
{product.imageSrc && (
<img src={product.imageSrc} alt={product.name} />
)}
<h3 className="product-name">{product.name}</h3>
<p className="product-price">${product.price}</p>
{product.description && (
<p className="product-desc">{product.description}</p>
)}
</div>
))}
</div>
</div>
);
interface ProductCardItemProps {
product: ProductCard;
shouldUseLightText: boolean;
cardClassName?: string;
imageClassName?: string;
cardNameClassName?: string;
cardPriceClassName?: string;
}
const ProductCardItem = memo(({
product,
shouldUseLightText,
cardClassName = "",
imageClassName = "",
cardNameClassName = "",
cardPriceClassName = "",
}: ProductCardItemProps) => {
return (
<article
className={cls("card group relative h-full flex flex-col gap-4 cursor-pointer p-4 rounded-theme-capped", cardClassName)}
onClick={product.onProductClick}
role="article"
aria-label={`${product.name} - ${product.price}`}
>
<ProductImage
imageSrc={product.imageSrc}
imageAlt={product.imageAlt || product.name}
isFavorited={product.isFavorited}
onFavoriteToggle={product.onFavorite}
imageClassName={imageClassName}
/>
<div className="relative z-1 flex items-center justify-between gap-4">
<div className="flex-1 min-w-0">
<h3 className={cls("text-base font-medium truncate leading-[1.3]", shouldUseLightText ? "text-background" : "text-foreground", cardNameClassName)}>
{product.name}
</h3>
<p className={cls("text-2xl font-medium leading-[1.3]", shouldUseLightText ? "text-background" : "text-foreground", cardPriceClassName)}>
{product.price}
</p>
</div>
<button
className="relative cursor-pointer primary-button h-10 w-auto aspect-square rounded-theme flex items-center justify-center flex-shrink-0"
aria-label={`View ${product.name} details`}
type="button"
>
<ArrowUpRight className="h-4/10 text-primary-cta-text transition-transform duration-300 group-hover:rotate-45" strokeWidth={1.5} />
</button>
</div>
</article>
);
});
ProductCardItem.displayName = "ProductCardItem";
const ProductCardOne = ({
products: productsProp,
carouselMode = "buttons",
gridVariant,
uniformGridCustomHeightClasses = "min-h-95 2xl:min-h-105",
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Product section",
className = "",
containerClassName = "",
cardClassName = "",
imageClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
cardNameClassName = "",
cardPriceClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: ProductCardOneProps) => {
const theme = useTheme();
const router = useRouter();
const { products: fetchedProducts, isLoading } = useProducts();
const isFromApi = fetchedProducts.length > 0;
const products = isFromApi ? fetchedProducts : productsProp;
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const handleProductClick = useCallback((product: ProductCard) => {
if (isFromApi) {
router.push(`/shop/${product.id}`);
} else {
product.onProductClick?.();
}
}, [isFromApi, router]);
if (isLoading && !productsProp) {
return (
<div className="w-content-width mx-auto py-20 text-center">
<p className="text-foreground">Loading products...</p>
</div>
);
}
if (!products || products.length === 0) {
return null;
}
return (
<CardStack
mode={carouselMode}
gridVariant={gridVariant}
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{products?.map((product, index) => (
<ProductCardItem
key={`${product.id}-${index}`}
product={{ ...product, onProductClick: () => handleProductClick(product) }}
shouldUseLightText={shouldUseLightText}
cardClassName={cardClassName}
imageClassName={imageClassName}
cardNameClassName={cardNameClassName}
cardPriceClassName={cardPriceClassName}
/>
))}
</CardStack>
);
};
ProductCardOne.displayName = "ProductCardOne";
export default ProductCardOne;

View File

@@ -1,35 +1,283 @@
import React from "react";
import { Product } from "@/lib/api/product";
"use client";
import { memo, useState, useCallback } from "react";
import { useRouter } from "next/navigation";
import { Plus, Minus } from "lucide-react";
import CardStack from "@/components/cardStack/CardStack";
import ProductImage from "@/components/shared/ProductImage";
import QuantityButton from "@/components/shared/QuantityButton";
import Button from "@/components/button/Button";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import { useProducts } from "@/hooks/useProducts";
import { getButtonProps } from "@/lib/buttonUtils";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import type { Product } from "@/lib/api/product";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, ButtonAnimationType, GridVariant, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
import type { CTAButtonVariant, ButtonPropsForVariant } from "@/components/button/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type ProductCardThreeGridVariant = Exclude<GridVariant, "timeline" | "items-top-row-full-width-bottom" | "full-width-top-items-bottom-row">;
type ProductCard = Product & {
onQuantityChange?: (quantity: number) => void;
initialQuantity?: number;
priceButtonProps?: Partial<ButtonPropsForVariant<CTAButtonVariant>>;
};
interface ProductCardThreeProps {
products?: Product[];
title?: string;
description?: string;
products?: ProductCard[];
carouselMode?: "auto" | "buttons";
gridVariant: ProductCardThreeGridVariant;
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
imageClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
cardNameClassName?: string;
quantityControlsClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
}
export default function ProductCardThree({
products = [],
title = "Products", description = "Our premium product collection"}: ProductCardThreeProps) {
return (
<div className="product-card-three-container">
<div className="product-header">
<h2 className="product-title">{title}</h2>
<p className="product-description">{description}</p>
</div>
<div className="product-grid">
{products.map((product) => (
<div key={product.id} className="product-card">
{product.imageSrc && (
<img src={product.imageSrc} alt={product.name} />
)}
<h3 className="product-name">{product.name}</h3>
<p className="product-price">${product.price}</p>
{product.description && (
<p className="product-desc">{product.description}</p>
)}
</div>
))}
</div>
</div>
);
interface ProductCardItemProps {
product: ProductCard;
shouldUseLightText: boolean;
isFromApi: boolean;
onBuyClick?: (productId: string, quantity: number) => void;
cardClassName?: string;
imageClassName?: string;
cardNameClassName?: string;
quantityControlsClassName?: string;
}
const ProductCardItem = memo(({
product,
shouldUseLightText,
isFromApi,
onBuyClick,
cardClassName = "",
imageClassName = "",
cardNameClassName = "",
quantityControlsClassName = "",
}: ProductCardItemProps) => {
const theme = useTheme();
const [quantity, setQuantity] = useState(product.initialQuantity || 1);
const handleIncrement = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
const newQuantity = quantity + 1;
setQuantity(newQuantity);
product.onQuantityChange?.(newQuantity);
}, [quantity, product]);
const handleDecrement = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
if (quantity > 1) {
const newQuantity = quantity - 1;
setQuantity(newQuantity);
product.onQuantityChange?.(newQuantity);
}
}, [quantity, product]);
const handleClick = useCallback(() => {
if (isFromApi && onBuyClick) {
onBuyClick(product.id, quantity);
} else {
product.onProductClick?.();
}
}, [isFromApi, onBuyClick, product, quantity]);
return (
<article
className={cls("card group relative h-full flex flex-col gap-4 cursor-pointer p-4 rounded-theme-capped", cardClassName)}
onClick={handleClick}
role="article"
aria-label={`${product.name} - ${product.price}`}
>
<ProductImage
imageSrc={product.imageSrc}
imageAlt={product.imageAlt || product.name}
isFavorited={product.isFavorited}
onFavoriteToggle={product.onFavorite}
imageClassName={imageClassName}
/>
<div className="relative z-1 flex flex-col gap-3">
<h3 className={cls("text-xl font-medium leading-[1.15] truncate", shouldUseLightText ? "text-background" : "text-foreground", cardNameClassName)}>
{product.name}
</h3>
<div className="flex items-center justify-between gap-4">
<div className={cls("flex items-center gap-2", quantityControlsClassName)}>
<QuantityButton
onClick={handleDecrement}
ariaLabel="Decrease quantity"
Icon={Minus}
/>
<span className={cls("text-base font-medium min-w-[2ch] text-center leading-[1]", shouldUseLightText ? "text-background" : "text-foreground")}>
{quantity}
</span>
<QuantityButton
onClick={handleIncrement}
ariaLabel="Increase quantity"
Icon={Plus}
/>
</div>
<Button
{...getButtonProps(
{
text: product.price,
props: product.priceButtonProps,
},
0,
theme.defaultButtonVariant
)}
/>
</div>
</div>
</article>
);
});
ProductCardItem.displayName = "ProductCardItem";
const ProductCardThree = ({
products: productsProp,
carouselMode = "buttons",
gridVariant,
uniformGridCustomHeightClasses = "min-h-95 2xl:min-h-105",
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Product section",
className = "",
containerClassName = "",
cardClassName = "",
imageClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
cardNameClassName = "",
quantityControlsClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: ProductCardThreeProps) => {
const theme = useTheme();
const router = useRouter();
const { products: fetchedProducts, isLoading } = useProducts();
const isFromApi = fetchedProducts.length > 0;
const products = (isFromApi ? fetchedProducts : productsProp) as ProductCard[];
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const handleProductClick = useCallback((product: ProductCard) => {
if (isFromApi) {
router.push(`/shop/${product.id}`);
} else {
product.onProductClick?.();
}
}, [isFromApi, router]);
if (isLoading && !productsProp) {
return (
<div className="w-content-width mx-auto py-20 text-center">
<p className="text-foreground">Loading products...</p>
</div>
);
}
if (!products || products.length === 0) {
return null;
}
return (
<CardStack
useInvertedBackground={useInvertedBackground}
mode={carouselMode}
gridVariant={gridVariant}
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{products?.map((product, index) => (
<ProductCardItem
key={`${product.id}-${index}`}
product={{ ...product, onProductClick: () => handleProductClick(product) }}
shouldUseLightText={shouldUseLightText}
isFromApi={isFromApi}
cardClassName={cardClassName}
imageClassName={imageClassName}
cardNameClassName={cardNameClassName}
quantityControlsClassName={quantityControlsClassName}
/>
))}
</CardStack>
);
};
ProductCardThree.displayName = "ProductCardThree";
export default ProductCardThree;

View File

@@ -1,35 +1,267 @@
import React from "react";
import { Product } from "@/lib/api/product";
"use client";
import { memo, useCallback } from "react";
import { useRouter } from "next/navigation";
import { Star } from "lucide-react";
import CardStack from "@/components/cardStack/CardStack";
import ProductImage from "@/components/shared/ProductImage";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import { useProducts } from "@/hooks/useProducts";
import type { Product } from "@/lib/api/product";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, GridVariant, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type ProductCardTwoGridVariant = Exclude<GridVariant, "timeline" | "one-large-right-three-stacked-left" | "items-top-row-full-width-bottom" | "full-width-top-items-bottom-row" | "one-large-left-three-stacked-right">;
type ProductCard = Product & {
brand: string;
rating: number;
reviewCount: string;
};
interface ProductCardTwoProps {
products?: Product[];
title?: string;
description?: string;
products?: ProductCard[];
carouselMode?: "auto" | "buttons";
gridVariant: ProductCardTwoGridVariant;
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
imageClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
cardBrandClassName?: string;
cardNameClassName?: string;
cardPriceClassName?: string;
cardRatingClassName?: string;
actionButtonClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
}
export default function ProductCardTwo({
products = [],
title = "Products", description = "Our premium product collection"}: ProductCardTwoProps) {
return (
<div className="product-card-two-container">
<div className="product-header">
<h2 className="product-title">{title}</h2>
<p className="product-description">{description}</p>
</div>
<div className="product-grid">
{products.map((product) => (
<div key={product.id} className="product-card">
{product.imageSrc && (
<img src={product.imageSrc} alt={product.name} />
)}
<h3 className="product-name">{product.name}</h3>
<p className="product-price">${product.price}</p>
{product.description && (
<p className="product-desc">{product.description}</p>
)}
</div>
))}
</div>
</div>
);
interface ProductCardItemProps {
product: ProductCard;
shouldUseLightText: boolean;
cardClassName?: string;
imageClassName?: string;
cardBrandClassName?: string;
cardNameClassName?: string;
cardPriceClassName?: string;
cardRatingClassName?: string;
actionButtonClassName?: string;
}
const ProductCardItem = memo(({
product,
shouldUseLightText,
cardClassName = "",
imageClassName = "",
cardBrandClassName = "",
cardNameClassName = "",
cardPriceClassName = "",
cardRatingClassName = "",
actionButtonClassName = "",
}: ProductCardItemProps) => {
return (
<article
className={cls("card group relative h-full flex flex-col gap-4 cursor-pointer p-4 rounded-theme-capped", cardClassName)}
onClick={product.onProductClick}
role="article"
aria-label={`${product.brand} ${product.name} - ${product.price}`}
>
<ProductImage
imageSrc={product.imageSrc}
imageAlt={product.imageAlt || `${product.brand} ${product.name}`}
isFavorited={product.isFavorited}
onFavoriteToggle={product.onFavorite}
showActionButton={true}
actionButtonAriaLabel={`View ${product.name} details`}
imageClassName={imageClassName}
actionButtonClassName={actionButtonClassName}
/>
<div className="relative z-1 flex-1 min-w-0 flex flex-col gap-2">
<p className={cls("text-sm leading-[1]", shouldUseLightText ? "text-background" : "text-foreground", cardBrandClassName)}>
{product.brand}
</p>
<div className="flex flex-col gap-1" >
<h3 className={cls("text-xl font-medium truncate leading-[1.15]", shouldUseLightText ? "text-background" : "text-foreground", cardNameClassName)}>
{product.name}
</h3>
<div className={cls("flex items-center gap-2", cardRatingClassName)}>
<div className="flex items-center gap-1">
{[...Array(5)].map((_, i) => (
<Star
key={i}
className={cls(
"h-4 w-auto",
i < Math.floor(product.rating)
? "text-accent fill-accent"
: "text-accent opacity-20"
)}
strokeWidth={1.5}
/>
))}
</div>
<span className={cls("text-sm leading-[1.3]", shouldUseLightText ? "text-background" : "text-foreground")}>
({product.reviewCount})
</span>
</div>
</div>
<p className={cls("text-2xl font-medium leading-[1.3]", shouldUseLightText ? "text-background" : "text-foreground", cardPriceClassName)}>
{product.price}
</p>
</div>
</article>
);
});
ProductCardItem.displayName = "ProductCardItem";
const ProductCardTwo = ({
products: productsProp,
carouselMode = "buttons",
gridVariant,
uniformGridCustomHeightClasses = "min-h-95 2xl:min-h-105",
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Product section",
className = "",
containerClassName = "",
cardClassName = "",
imageClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
cardBrandClassName = "",
cardNameClassName = "",
cardPriceClassName = "",
cardRatingClassName = "",
actionButtonClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: ProductCardTwoProps) => {
const theme = useTheme();
const router = useRouter();
const { products: fetchedProducts, isLoading } = useProducts();
const isFromApi = fetchedProducts.length > 0;
const products = (fetchedProducts.length > 0 ? fetchedProducts : productsProp) as ProductCard[];
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const handleProductClick = useCallback((product: ProductCard) => {
if (isFromApi) {
router.push(`/shop/${product.id}`);
} else {
product.onProductClick?.();
}
}, [isFromApi, router]);
const customGridRows = (gridVariant === "bento-grid" || gridVariant === "bento-grid-inverted")
? "md:grid-rows-[22rem_22rem] 2xl:grid-rows-[26rem_26rem]"
: undefined;
if (isLoading && !productsProp) {
return (
<div className="w-content-width mx-auto py-20 text-center">
<p className="text-foreground">Loading products...</p>
</div>
);
}
if (!products || products.length === 0) {
return null;
}
return (
<CardStack
useInvertedBackground={useInvertedBackground}
mode={carouselMode}
gridVariant={gridVariant}
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
gridRowsClassName={customGridRows}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{products?.map((product, index) => (
<ProductCardItem
key={`${product.id}-${index}`}
product={{ ...product, onProductClick: () => handleProductClick(product) }}
shouldUseLightText={shouldUseLightText}
cardClassName={cardClassName}
imageClassName={imageClassName}
cardBrandClassName={cardBrandClassName}
cardNameClassName={cardNameClassName}
cardPriceClassName={cardPriceClassName}
cardRatingClassName={cardRatingClassName}
actionButtonClassName={actionButtonClassName}
/>
))}
</CardStack>
);
};
ProductCardTwo.displayName = "ProductCardTwo";
export default ProductCardTwo;

View File

@@ -1,51 +1,196 @@
"use client";
import React from "react";
import CardList from "@/components/cardStack/CardList";
import MediaContent from "@/components/shared/MediaContent";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type TeamMember = {
id: string;
title: string;
subtitle: string;
detail: string;
imageSrc?: string;
imageAlt?: string;
videoSrc?: string;
videoAriaLabel?: string;
};
type TeamGroup = {
id: string;
groupTitle: string;
members: TeamMember[];
};
interface TeamCardElevenProps {
members?: any[];
title?: string;
description?: string;
animationType?: string;
useInvertedBackground?: boolean;
textboxLayout?: string;
tag?: string;
tagIcon?: any;
tagAnimation?: string;
buttons?: any[];
buttonAnimation?: string;
titleSegments?: any[];
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxTitleClassName?: string;
textBoxDescriptionClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
groups: TeamGroup[];
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxClassName?: string;
textBoxTitleClassName?: string;
textBoxDescriptionClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
groupTitleClassName?: string;
memberClassName?: string;
memberImageClassName?: string;
memberTitleClassName?: string;
memberSubtitleClassName?: string;
memberDetailClassName?: string;
}
export default function TeamCardEleven({
members = [],
title = "Team", description = "Our team members", animationType = "slide-up", useInvertedBackground = false,
textboxLayout = "default"}: TeamCardElevenProps) {
const items = members.map((member) => ({
id: member.id,
label: member.name,
detail: member.role,
}));
const TeamCardEleven = ({
groups,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Team section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxClassName = "",
textBoxTitleClassName = "",
textBoxDescriptionClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
groupTitleClassName = "",
memberClassName = "",
memberImageClassName = "",
memberTitleClassName = "",
memberSubtitleClassName = "",
memberDetailClassName = "",
}: TeamCardElevenProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<div className="team-card-eleven">
<h2>{title}</h2>
<p>{description}</p>
<CardList items={items} />
</div>
);
}
const renderMemberRow = (member: TeamMember) => (
<div
key={member.id}
className={cls(
"flex flex-col md:flex-row md:items-center gap-4 py-6",
memberClassName
)}
>
<div className="flex items-center gap-4 flex-1">
<div className={cls(
"relative h-14 w-auto md:h-16 aspect-square rounded-theme overflow-hidden shrink-0",
memberImageClassName
)}>
<MediaContent
imageSrc={member.imageSrc}
imageAlt={member.imageAlt || member.title}
videoSrc={member.videoSrc}
videoAriaLabel={member.videoAriaLabel}
imageClassName="w-full h-full object-cover"
/>
</div>
<div className="flex flex-col">
<p className={cls(
"text-lg md:text-xl font-medium",
shouldUseLightText ? "text-background" : "text-foreground",
memberTitleClassName
)}>
{member.title}
</p>
<p className={cls(
"text-base",
shouldUseLightText ? "text-background/60" : "text-foreground/60",
memberSubtitleClassName
)}>
{member.subtitle}
</p>
</div>
</div>
<p className={cls(
"text-base md:text-lg font-medium",
shouldUseLightText ? "text-background" : "text-foreground",
memberDetailClassName
)}>
{member.detail}
</p>
</div>
);
return (
<CardList
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
animationType={animationType}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
cardClassName={cardClassName}
titleClassName={textBoxTitleClassName}
descriptionClassName={textBoxDescriptionClassName}
textBoxClassName={textBoxClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
ariaLabel={ariaLabel}
>
{groups.map((group) => (
<div key={group.id} className="p-6 md:p-8">
<h3 className={cls(
"text-2xl md:text-3xl font-medium mb-2",
shouldUseLightText ? "text-background" : "text-foreground",
groupTitleClassName
)}>
{group.groupTitle}
</h3>
<div className="flex flex-col divide-y divide-accent/40 border-y border-accent/40">
{group.members.map(renderMemberRow)}
</div>
</div>
))}
</CardList>
);
};
TeamCardEleven.displayName = "TeamCardEleven";
export default TeamCardEleven;

View File

@@ -1,38 +1,148 @@
import React, { useRef } from "react";
"use client";
import CardStackTextBox from "@/components/cardStack/CardStackTextBox";
import MediaContent from "@/components/shared/MediaContent";
import { useCardAnimation } from "@/components/cardStack/hooks/useCardAnimation";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type TeamMember = {
id: string;
name: string;
role: string;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
};
interface TeamCardFiveProps {
members?: any[];
title?: string;
description?: string;
animationType?: string;
useInvertedBackground?: boolean;
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;
}
export default function TeamCardFive({
members = [],
title = "Team", description = "Our team", animationType = "slide-up", useInvertedBackground = false,
}: TeamCardFiveProps) {
const state = useCardAnimation({
rotationX: 0,
rotationY: 0,
rotationZ: 0,
perspective: 1000,
duration: 0.3,
});
const TeamCardFive = ({
team,
animationType,
title,
titleSegments,
description,
textboxLayout,
useInvertedBackground,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
ariaLabel = "Team section",
className = "",
containerClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
gridClassName = "",
cardClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
nameClassName = "",
roleClassName = "",
}: TeamCardFiveProps) => {
const { itemRefs } = useCardAnimation({ animationType, itemCount: team.length });
return (
<div className="team-card-five">
<h2>{title}</h2>
<p>{description}</p>
<div className="members-container">
{members.map((member) => (
<div key={member.id} className="member-item">
<h3>{member.name}</h3>
<p>{member.role}</p>
</div>
))}
<section
aria-label={ariaLabel}
className={cls("relative py-20 w-full", useInvertedBackground && "bg-foreground", className)}
>
<div className={cls("w-content-width mx-auto flex flex-col gap-8", containerClassName)}>
<CardStackTextBox
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
/>
<div className={cls("flex flex-row flex-wrap gap-y-6 md:gap-x-0 justify-center", gridClassName)}>
{team.map((member, index) => (
<div
key={member.id}
ref={(el) => { itemRefs.current[index] = el; }}
className={cls("relative flex flex-col items-center text-center w-[55%] md:w-[28%] -mx-[4%] md:-mx-[2%]", cardClassName)}
>
<div className={cls("relative card w-full aspect-square rounded-theme overflow-hidden p-2 mb-4", mediaWrapperClassName)}>
<MediaContent
imageSrc={member.imageSrc}
videoSrc={member.videoSrc}
imageAlt={member.imageAlt || member.name}
videoAriaLabel={member.videoAriaLabel || member.name}
imageClassName={cls("relative z-1 w-full h-full object-cover rounded-theme!", mediaClassName)}
/>
</div>
<h3 className={cls("relative z-1 w-8/10 text-2xl font-medium leading-tight truncate", useInvertedBackground ? "text-background" : "text-foreground", nameClassName)}>
{member.name}
</h3>
<p className={cls("relative z-1 w-8/10 text-base leading-tight mt-1 truncate", useInvertedBackground ? "text-background/75" : "text-foreground/75", roleClassName)}>
{member.role}
</p>
</div>
))}
</div>
</div>
</div>
</section>
);
}
};
TeamCardFive.displayName = "TeamCardFive";
export default TeamCardFive;

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,29 +1,203 @@
import React from "react";
"use client";
import { memo } from "react";
import AutoCarousel from "@/components/cardStack/layouts/carousels/AutoCarousel";
import TestimonialAuthor from "@/components/shared/TestimonialAuthor";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import { Quote } from "lucide-react";
import type { LucideIcon } from "lucide-react";
import type { CardAnimationType, ButtonConfig, ButtonAnimationType, TitleSegment, TextboxLayout, InvertedBackground } from "@/components/cardStack/types";
type Testimonial = {
id: string;
name: string;
handle: string;
testimonial: string;
imageSrc?: string;
imageAlt?: string;
icon?: LucideIcon;
};
interface TestimonialCardSixProps {
testimonials?: any[];
title?: string;
description?: string;
animationType?: string;
useInvertedBackground?: boolean;
testimonials: Testimonial[];
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
speed?: number;
topMarqueeDirection?: "left" | "right";
ariaLabel?: string;
className?: string;
containerClassName?: string;
carouselClassName?: string;
bottomCarouselClassName?: string;
cardClassName?: string;
testimonialClassName?: string;
imageWrapperClassName?: string;
imageClassName?: string;
iconClassName?: string;
nameClassName?: string;
handleClassName?: string;
textBoxClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
}
export default function TestimonialCardSix({
testimonials = [],
title = "Testimonials", description = "What customers say", animationType = "slide-up", useInvertedBackground = false,
}: TestimonialCardSixProps) {
const items = testimonials.map((testimonial) => ({
id: testimonial.id,
label: testimonial.name,
detail: testimonial.company,
}));
return (
<div className="testimonial-card-six">
<h2>{title}</h2>
<p>{description}</p>
<AutoCarousel items={items} />
</div>
);
interface TestimonialCardProps {
testimonial: Testimonial;
useInvertedBackground: boolean;
cardClassName?: string;
testimonialClassName?: string;
imageWrapperClassName?: string;
imageClassName?: string;
iconClassName?: string;
nameClassName?: string;
handleClassName?: string;
}
const TestimonialCard = memo(({
testimonial,
useInvertedBackground,
cardClassName = "",
testimonialClassName = "",
imageWrapperClassName = "",
imageClassName = "",
iconClassName = "",
nameClassName = "",
handleClassName = "",
}: TestimonialCardProps) => {
const Icon = testimonial.icon || Quote;
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<div className={cls("relative h-full card rounded-theme-capped p-6 min-h-0 flex flex-col gap-10", cardClassName)}>
<p className={cls("relative z-1 text-lg leading-tight line-clamp-2", shouldUseLightText ? "text-background" : "text-foreground", testimonialClassName)}>
{testimonial.testimonial}
</p>
<TestimonialAuthor
name={testimonial.name}
subtitle={testimonial.handle}
imageSrc={testimonial.imageSrc}
imageAlt={testimonial.imageAlt}
icon={Icon}
useInvertedBackground={useInvertedBackground}
imageWrapperClassName={imageWrapperClassName}
imageClassName={imageClassName}
iconClassName={iconClassName}
nameClassName={nameClassName}
subtitleClassName={handleClassName}
/>
</div>
);
});
TestimonialCard.displayName = "TestimonialCard";
const TestimonialCardSix = ({
testimonials,
animationType,
title,
titleSegments,
description,
textboxLayout,
useInvertedBackground,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
speed = 40,
topMarqueeDirection = "left",
ariaLabel = "Testimonials section",
className = "",
containerClassName = "",
carouselClassName = "",
bottomCarouselClassName = "",
cardClassName = "",
testimonialClassName = "",
imageWrapperClassName = "",
imageClassName = "",
iconClassName = "",
nameClassName = "",
handleClassName = "",
textBoxClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: TestimonialCardSixProps) => {
return (
<AutoCarousel
speed={speed}
uniformGridCustomHeightClasses="min-h-none"
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
showTextBox={true}
dualMarquee={true}
topMarqueeDirection={topMarqueeDirection}
carouselClassName={carouselClassName}
bottomCarouselClassName={bottomCarouselClassName}
containerClassName={containerClassName}
className={className}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
itemClassName="w-60! md:w-carousel-item-3! xl:w-carousel-item-4!"
>
{testimonials.map((testimonial, index) => (
<TestimonialCard
key={`${testimonial.id}-${index}`}
testimonial={testimonial}
useInvertedBackground={useInvertedBackground}
cardClassName={cardClassName}
testimonialClassName={testimonialClassName}
imageWrapperClassName={imageWrapperClassName}
imageClassName={imageClassName}
iconClassName={iconClassName}
nameClassName={nameClassName}
handleClassName={handleClassName}
/>
))}
</AutoCarousel>
);
};
TestimonialCardSix.displayName = "TestimonialCardSix";
export default TestimonialCardSix;

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,45 +1,117 @@
import { useState, useCallback } from "react";
"use client";
interface CheckoutItem {
id: string;
name: string;
price: number;
quantity: number;
}
import { useState } from "react";
import { Product } from "@/lib/api/product";
export const useCheckout = () => {
const [items, setItems] = useState<CheckoutItem[]>([]);
const [total, setTotal] = useState(0);
const addItem = useCallback(
(item: CheckoutItem) => {
setItems((prev) => [...prev, item]);
setTotal((prev) => prev + item.price * item.quantity);
},
[]
);
const removeItem = useCallback(
(id: string) => {
const item = items.find((i) => i.id === id);
if (item) {
setTotal((prev) => prev - item.price * item.quantity);
setItems((prev) => prev.filter((i) => i.id !== id));
}
},
[items]
);
const clearCart = useCallback(() => {
setItems([]);
setTotal(0);
}, []);
return {
items,
total,
addItem,
removeItem,
clearCart,
};
export type CheckoutItem = {
productId: string;
quantity: number;
imageSrc?: string;
imageAlt?: string;
metadata?: {
brand?: string;
variant?: string;
rating?: number;
reviewCount?: string;
[key: string]: string | number | undefined;
};
};
export type CheckoutResult = {
success: boolean;
url?: string;
error?: string;
};
export function useCheckout() {
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),
};
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,38 +1,219 @@
export interface Product {
id: string;
name: string;
price: number;
description?: string;
imageSrc?: string;
imageAlt?: string;
rating?: number;
reviewCount?: string;
brand?: string;
category?: string;
export type Product = {
id: string;
name: string;
price: string;
imageSrc: string;
imageAlt?: string;
images?: string[];
brand?: string;
variant?: string;
rating?: number;
reviewCount?: string;
description?: string;
priceId?: string;
metadata?: {
[key: string]: string | number | undefined;
};
onFavorite?: () => void;
onProductClick?: () => void;
isFavorited?: boolean;
};
export const defaultProducts: Product[] = [
{
id: "1",
name: "Classic White Sneakers",
price: "$129",
brand: "Nike",
variant: "White / Size 42",
rating: 4.5,
reviewCount: "128",
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/default/placeholder3.avif",
imageAlt: "Classic white sneakers",
},
{
id: "2",
name: "Leather Crossbody Bag",
price: "$89",
brand: "Coach",
variant: "Brown / Medium",
rating: 4.8,
reviewCount: "256",
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/default/placeholder4.webp",
imageAlt: "Brown leather crossbody bag",
},
{
id: "3",
name: "Wireless Headphones",
price: "$199",
brand: "Sony",
variant: "Black",
rating: 4.7,
reviewCount: "512",
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/default/placeholder3.avif",
imageAlt: "Black wireless headphones",
},
{
id: "4",
name: "Minimalist Watch",
price: "$249",
brand: "Fossil",
variant: "Silver / 40mm",
rating: 4.6,
reviewCount: "89",
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/default/placeholder4.webp",
imageAlt: "Silver minimalist watch",
},
];
function formatPrice(amount: number, currency: string): string {
const formatter = new Intl.NumberFormat("en-US", {
style: "currency",
currency: currency.toUpperCase(),
minimumFractionDigits: 0,
maximumFractionDigits: 2,
});
return formatter.format(amount / 100);
}
export const fetchProducts = async (): Promise<Product[]> => {
try {
const response = await fetch("/api/products");
const data = await response.json();
return data;
} catch (err) {
console.error("Failed to fetch products");
return [];
}
};
export async function fetchProducts(): Promise<Product[]> {
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
const projectId = process.env.NEXT_PUBLIC_PROJECT_ID;
export const fetchProduct = async (id: string): Promise<Product | null> => {
try {
const response = await fetch(`/api/products/${id}`);
const data = await response.json();
return data;
} catch (err) {
console.error(`Failed to fetch product ${id}`);
return null;
}
};
if (!apiUrl || !projectId) {
return [];
}
export const fetchProductDetail = async (id: string): Promise<Product | null> => {
return fetchProduct(id);
};
try {
const url = `${apiUrl}/stripe/project/products?projectId=${projectId}&expandDefaultPrice=true`;
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
return [];
}
const resp = await response.json();
const data = resp.data.data || resp.data;
if (!Array.isArray(data) || data.length === 0) {
return [];
}
return data.map((product: any) => {
const metadata: Record<string, string | number | undefined> = {};
if (product.metadata && typeof product.metadata === 'object') {
Object.keys(product.metadata).forEach(key => {
const value = product.metadata[key];
if (value !== null && value !== undefined) {
const numValue = parseFloat(value);
metadata[key] = isNaN(numValue) ? value : numValue;
}
});
}
const imageSrc = product.images?.[0] || product.imageSrc || "https://webuild-dev.s3.eu-north-1.amazonaws.com/default/placeholder3.avif";
const imageAlt = product.imageAlt || product.name || "";
const images = product.images && Array.isArray(product.images) && product.images.length > 0
? product.images
: [imageSrc];
return {
id: product.id || String(Math.random()),
name: product.name || "Untitled Product",
description: product.description || "",
price: product.default_price?.unit_amount
? formatPrice(product.default_price.unit_amount, product.default_price.currency || "usd")
: product.price || "$0",
priceId: product.default_price?.id || product.priceId,
imageSrc,
imageAlt,
images,
brand: product.metadata?.brand || product.brand || "",
variant: product.metadata?.variant || product.variant || "",
rating: product.metadata?.rating ? parseFloat(product.metadata.rating) : undefined,
reviewCount: product.metadata?.reviewCount || undefined,
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
};
});
} catch (error) {
return [];
}
}
export async function fetchProduct(productId: string): Promise<Product | null> {
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
const projectId = process.env.NEXT_PUBLIC_PROJECT_ID;
if (!apiUrl || !projectId) {
return null;
}
try {
const url = `${apiUrl}/stripe/project/products/${productId}?projectId=${projectId}&expandDefaultPrice=true`;
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
return null;
}
const resp = await response.json();
const product = resp.data?.data || resp.data || resp;
if (!product || typeof product !== 'object') {
return null;
}
const metadata: Record<string, string | number | undefined> = {};
if (product.metadata && typeof product.metadata === 'object') {
Object.keys(product.metadata).forEach(key => {
const value = product.metadata[key];
if (value !== null && value !== undefined && value !== '') {
const numValue = parseFloat(String(value));
metadata[key] = isNaN(numValue) ? String(value) : numValue;
}
});
}
let priceValue = product.price;
if (!priceValue && product.default_price?.unit_amount) {
priceValue = formatPrice(product.default_price.unit_amount, product.default_price.currency || "usd");
}
if (!priceValue) {
priceValue = "$0";
}
const imageSrc = product.images?.[0] || product.imageSrc || "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;
}
}