Compare commits
91 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2195117139 | |||
| 5f5e8b835e | |||
| 12d76cdee5 | |||
| 954a7e26e7 | |||
| ab54fcc5cd | |||
| 70933c2e9a | |||
| e926d14730 | |||
| 1edc8132ba | |||
| 6378b4744a | |||
| ae674306f4 | |||
| 77ba4a86af | |||
| 0a127211e9 | |||
| f0daf18d4d | |||
| 27d27c3187 | |||
| 8232ca1054 | |||
| 816bc0d1fe | |||
| 5f23ec9d4d | |||
| aec1080d7f | |||
| 5dd7b6b565 | |||
| 01a02e43b5 | |||
| 22f1f53e9d | |||
| 5f0a6d17a6 | |||
| 917116a825 | |||
| 94080973e6 | |||
| e85362908f | |||
| 59579a146c | |||
| 7b96ce57d8 | |||
| 308f65c4f7 | |||
| b2de0368d1 | |||
| c2182b265c | |||
| a7d75f7248 | |||
| d02871af73 | |||
| cff4d64d73 | |||
| ba6253cd37 | |||
| 833d268e05 | |||
| cd29ed60bc | |||
| 4878a2024d | |||
| ea2c4606bd | |||
| 187a225db3 | |||
| f0fc407cbc | |||
| 533c37df04 | |||
| 9f798332c9 | |||
| 3f9f8049be | |||
| 0d95793876 | |||
| 784682d810 | |||
| a93bc8ab1b | |||
| 7e15e8a83b | |||
| a09643d4c2 | |||
| 61c4e1fc00 | |||
| a3958438d5 | |||
| 39049a4257 | |||
| 7a1ead0ad9 | |||
| b47046ca12 | |||
| c208bbaccc | |||
| db7f7fb177 | |||
| 2792346d33 | |||
| cb580e89ad | |||
| ff49c73a40 | |||
| 0c38c85fd1 | |||
| ab2e3400b8 | |||
| 4254042c27 | |||
| a8e68f5dcf | |||
| 3c579bee24 | |||
| 18785c35d6 | |||
| 764c5bb5f5 | |||
| 49f25baee1 | |||
| cd7df75d42 | |||
| 1a65c035ed | |||
| 66151a6460 | |||
| 36375d9810 | |||
| c2fe08e7dc | |||
| 9f7085ecdd | |||
| 5383737478 | |||
| 2936b85b31 | |||
| 6d045b36c8 | |||
| d87f029086 | |||
| 2ae3e74440 | |||
| cc413c8c53 | |||
| d715408be3 | |||
| 3c687a6a36 | |||
| 53d25be5c8 | |||
| c65a0c5ec4 | |||
| 7cf9afc3dd | |||
| f3735dbf0e | |||
| 1c003d1673 | |||
| c706ba2726 | |||
| d2bd7fe3e7 | |||
| 8c585ad0bf | |||
| 3798951e6e | |||
| a07b8cf2b9 | |||
| 54db82b9d1 |
@@ -1,19 +1,9 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from 'next';
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
|
||||||
import "./globals.css";
|
|
||||||
import { ServiceWrapper } from "@/providers/service/ServiceWrapper";
|
|
||||||
import { Tag } from "@/components/utils/Tag";
|
|
||||||
|
|
||||||
const geist = Geist({
|
|
||||||
variable: "--font-geist-sans", subsets: ["latin"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const geistMono = Geist_Mono({
|
|
||||||
variable: "--font-geist-mono", subsets: ["latin"],
|
|
||||||
});
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Portfolio", description: "Creative portfolio showcasing design excellence"};
|
title: 'Portfolio',
|
||||||
|
description: 'Portfolio website',
|
||||||
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
@@ -21,29 +11,8 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="en">
|
||||||
<body
|
<body>{children}
|
||||||
className={`${geist.variable} ${geistMono.variable} antialiased`}
|
|
||||||
suppressHydrationWarning
|
|
||||||
>
|
|
||||||
<ServiceWrapper>
|
|
||||||
<Tag />
|
|
||||||
{children}
|
|
||||||
</ServiceWrapper>
|
|
||||||
<script
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: `
|
|
||||||
try {
|
|
||||||
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
|
||||||
document.documentElement.classList.add('dark')
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.remove('dark')
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<script
|
<script
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: `
|
__html: `
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
|
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
|
||||||
import NavbarLayoutFloatingOverlay from "@/components/navbar/NavbarLayoutFloatingOverlay/NavbarLayoutFloatingOverlay";
|
import NavbarLayoutFloatingOverlay from "@/components/navbar/NavbarLayoutFloatingOverlay/NavbarLayoutFloatingOverlay";
|
||||||
import HeroBillboardCarousel from "@/components/sections/hero/HeroBillboardCarousel";
|
import HeroBillboardCarousel from "@/components/sections/hero/HeroBillboardCarousel";
|
||||||
@@ -10,8 +11,10 @@ import ContactCenter from "@/components/sections/contact/ContactCenter";
|
|||||||
import FooterLogoReveal from "@/components/sections/footer/FooterLogoReveal";
|
import FooterLogoReveal from "@/components/sections/footer/FooterLogoReveal";
|
||||||
|
|
||||||
export default function LandingPage() {
|
export default function LandingPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const handleProjectClick = (projectId: string) => {
|
const handleProjectClick = (projectId: string) => {
|
||||||
window.location.href = `/project/${projectId}`;
|
router.push(`/project/${projectId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -37,8 +40,7 @@ export default function LandingPage() {
|
|||||||
{ name: "Contact", id: "contact" },
|
{ name: "Contact", id: "contact" },
|
||||||
]}
|
]}
|
||||||
button={{
|
button={{
|
||||||
text: "Get in Touch", href: "contact"
|
text: "Get in Touch", href: "contact"}}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -50,23 +52,17 @@ export default function LandingPage() {
|
|||||||
background={{ variant: "sparkles-gradient" }}
|
background={{ variant: "sparkles-gradient" }}
|
||||||
mediaItems={[
|
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 project one"
|
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: "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/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/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/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/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"
|
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={[
|
buttons={[
|
||||||
{ text: "View Work", href: "work" },
|
{ text: "View Work", href: "work" },
|
||||||
@@ -83,15 +79,15 @@ export default function LandingPage() {
|
|||||||
features={[
|
features={[
|
||||||
{
|
{
|
||||||
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"],
|
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")
|
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 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"],
|
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")
|
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: "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"],
|
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")
|
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"
|
animationType="slide-up"
|
||||||
@@ -104,8 +100,7 @@ export default function LandingPage() {
|
|||||||
<TextSplitAbout
|
<TextSplitAbout
|
||||||
title="About My Practice"
|
title="About My Practice"
|
||||||
description={[
|
description={[
|
||||||
"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."
|
"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={[
|
buttons={[
|
||||||
{ text: "Download Resume", href: "#" },
|
{ text: "Download Resume", href: "#" },
|
||||||
{ text: "Connect", href: "contact" },
|
{ text: "Connect", href: "contact" },
|
||||||
@@ -145,23 +140,17 @@ export default function LandingPage() {
|
|||||||
useInvertedBackground={false}
|
useInvertedBackground={false}
|
||||||
testimonials={[
|
testimonials={[
|
||||||
{
|
{
|
||||||
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: "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: "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: "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: "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: "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: "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: "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"
|
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>
|
||||||
|
|||||||
@@ -4,52 +4,37 @@ import { useParams, useRouter } from "next/navigation";
|
|||||||
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
|
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
|
||||||
import NavbarLayoutFloatingOverlay from "@/components/navbar/NavbarLayoutFloatingOverlay/NavbarLayoutFloatingOverlay";
|
import NavbarLayoutFloatingOverlay from "@/components/navbar/NavbarLayoutFloatingOverlay/NavbarLayoutFloatingOverlay";
|
||||||
import FooterLogoReveal from "@/components/sections/footer/FooterLogoReveal";
|
import FooterLogoReveal from "@/components/sections/footer/FooterLogoReveal";
|
||||||
|
import { ArrowLeft } from "lucide-react";
|
||||||
|
|
||||||
// Project data
|
const projectData: Record<string, any> = {
|
||||||
const projectsData: Record<string, any> = {
|
|
||||||
"1": {
|
"1": {
|
||||||
id: "1", title: "Digital Brand Identity System", author: "Brand Strategy", description:
|
title: "Digital Brand Identity System", category: "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"],
|
||||||
"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:
|
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"],
|
"This comprehensive brand identity project transformed a tech startup's visual presence. We developed a complete visual language that aligned with their innovative mission while maintaining professional credibility. The project included logo design, color palette development, typography system, brand guidelines, and a complete digital asset library. The new identity increased brand recognition by 40% within the first six months.", 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", challenge:
|
||||||
imageSrc:
|
"The startup needed a distinct visual identity that could compete in a crowded tech market while conveying trust and innovation.", solution:
|
||||||
"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: [
|
"We created a modern, scalable brand system that balanced cutting-edge design with timeless principles, ensuring consistency across all touchpoints.", results: ["40% increase in brand recognition", "Unified visual language", "Comprehensive style guide", "Digital asset library"],
|
||||||
"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": {
|
"2": {
|
||||||
id: "2", title: "E-commerce Platform Redesign", author: "UX Design", description:
|
title: "E-commerce Platform Redesign", category: "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"],
|
||||||
"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:
|
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"],
|
"This e-commerce platform redesign focused on improving user experience across all devices. Through extensive user research and testing, we identified pain points in the shopping journey and implemented solutions that significantly improved conversion rates. The redesign emphasized accessibility, mobile-first design, and simplified navigation.", 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", challenge:
|
||||||
imageSrc:
|
"The platform had a high cart abandonment rate and users complained about complex navigation and confusing checkout process.", solution:
|
||||||
"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: [
|
"We streamlined the user journey, implemented progressive disclosure, and optimized the checkout flow to reduce friction and improve conversion.", results: ["35% improvement in conversion rate", "50% reduction in cart abandonment", "WCAG 2.1 AA compliance", "Mobile conversion rate increased by 45%"],
|
||||||
"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": {
|
"3": {
|
||||||
id: "3", title: "SaaS Product Interface Design", author: "Product Design", description:
|
title: "SaaS Product Interface Design", category: "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"],
|
||||||
"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:
|
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"],
|
"For this data analytics SaaS platform, we designed an intuitive interface that made complex data visualization accessible to non-technical users. The project included creating an extensive design system with reusable components, improving the onboarding experience, and implementing best practices for data presentation.", 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", challenge:
|
||||||
imageSrc:
|
"Users struggled with the complexity of data analytics tools and required extensive training. Support tickets were overwhelming.", solution:
|
||||||
"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: [
|
"We redesigned the interface with a focus on progressive disclosure, clear data visualization, and comprehensive onboarding that guided users through key features.", results: ["50% reduction in support tickets", "Design system with 15+ components", "Improved user onboarding time by 60%", "User satisfaction score increased from 6.2 to 8.7 out of 10"],
|
||||||
"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() {
|
export default function ProjectDetailPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const projectId = params.id as string;
|
const projectId = params.id as string;
|
||||||
const project = projectsData[projectId];
|
const project = projectData[projectId];
|
||||||
|
|
||||||
if (!project) {
|
if (!project) {
|
||||||
return (
|
return (
|
||||||
@@ -65,16 +50,15 @@ export default function ProjectPage() {
|
|||||||
secondaryButtonStyle="solid"
|
secondaryButtonStyle="solid"
|
||||||
headingFontWeight="normal"
|
headingFontWeight="normal"
|
||||||
>
|
>
|
||||||
<div className="min-h-screen flex items-center justify-center">
|
<div className="min-h-screen flex flex-col items-center justify-center">
|
||||||
<div className="text-center">
|
<h1 className="text-3xl font-bold mb-4">Project not found</h1>
|
||||||
<h1 className="text-4xl font-bold mb-4">Project Not Found</h1>
|
<button
|
||||||
<button
|
onClick={() => router.push("/#work")}
|
||||||
onClick={() => router.push("/")}
|
className="flex items-center gap-2 text-blue-500 hover:text-blue-600"
|
||||||
className="mt-8 px-8 py-3 bg-primary-cta text-white rounded-lg hover:opacity-90"
|
>
|
||||||
>
|
<ArrowLeft size={20} />
|
||||||
Back to Portfolio
|
Back to Work
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
@@ -93,127 +77,123 @@ export default function ProjectPage() {
|
|||||||
secondaryButtonStyle="solid"
|
secondaryButtonStyle="solid"
|
||||||
headingFontWeight="normal"
|
headingFontWeight="normal"
|
||||||
>
|
>
|
||||||
<div id="nav" data-section="nav">
|
<div className="min-h-screen flex flex-col">
|
||||||
<NavbarLayoutFloatingOverlay
|
{/* Navigation */}
|
||||||
brandName="Portfolio"
|
<div className="sticky top-0 z-50">
|
||||||
navItems={[
|
<NavbarLayoutFloatingOverlay
|
||||||
{ name: "Work", id: "/" },
|
brandName="Portfolio"
|
||||||
{ name: "About", id: "/" },
|
navItems={[
|
||||||
{ name: "Services", id: "/" },
|
{ name: "Work", id: "work" },
|
||||||
{ name: "Contact", id: "/" },
|
{ name: "About", id: "about" },
|
||||||
]}
|
{ name: "Services", id: "services" },
|
||||||
button={{
|
{ name: "Contact", id: "contact" },
|
||||||
text: "Get in Touch", href: "/#contact"}}
|
]}
|
||||||
/>
|
button={{
|
||||||
</div>
|
text: "Get in Touch", href: "contact"}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<main className="py-20">
|
{/* Main Content */}
|
||||||
<div className="container mx-auto px-4">
|
<main className="flex-1 w-full max-w-6xl mx-auto px-4 py-16 md:py-24">
|
||||||
{/* Back button */}
|
{/* Back Button */}
|
||||||
<button
|
<button
|
||||||
onClick={() => router.push("/#work")}
|
onClick={() => router.push("/#work")}
|
||||||
className="text-secondary-cta hover:text-primary-cta transition-colors mb-8"
|
className="flex items-center gap-2 text-foreground hover:text-primary-cta mb-8 transition-colors"
|
||||||
>
|
>
|
||||||
← Back to Work
|
<ArrowLeft size={20} />
|
||||||
|
<span>Back to Work</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Project Hero */}
|
{/* Hero Section */}
|
||||||
<section className="mb-20">
|
<div className="mb-12 md:mb-16">
|
||||||
<div className="space-y-8">
|
<div className="mb-6">
|
||||||
<div>
|
<span className="inline-block px-3 py-1 rounded-full bg-accent/10 text-accent text-sm font-medium mb-4">
|
||||||
<div className="text-sm text-secondary-cta mb-4">{project.author}</div>
|
{project.category}
|
||||||
<h1 className="text-5xl md:text-6xl font-bold mb-6">{project.title}</h1>
|
</span>
|
||||||
<p className="text-xl text-foreground/80 max-w-3xl">
|
<h1 className="text-4xl md:text-5xl font-bold mb-4">{project.title}</h1>
|
||||||
{project.fullDescription}
|
<p className="text-lg text-foreground/70 mb-6">{project.description}</p>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tags */}
|
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{project.tags.map((tag: string, index: number) => (
|
{project.tags.map((tag: string) => (
|
||||||
<span
|
<span key={tag} className="px-3 py-1 rounded-full bg-card border border-foreground/10 text-sm">
|
||||||
key={index}
|
|
||||||
className="px-4 py-2 bg-card text-foreground rounded-full text-sm border border-accent"
|
|
||||||
>
|
|
||||||
{tag}
|
{tag}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</section>
|
|
||||||
|
{/* Project Image */}
|
||||||
|
<div className="rounded-lg overflow-hidden bg-card aspect-video">
|
||||||
|
<img
|
||||||
|
src={project.imageSrc}
|
||||||
|
alt={project.imageAlt}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Project Details */}
|
{/* Project Details */}
|
||||||
<section className="grid md:grid-cols-2 gap-12 mb-20">
|
<div className="grid md:grid-cols-2 gap-12 mb-16">
|
||||||
{/* Challenges */}
|
{/* Challenge */}
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold mb-6">Challenges</h2>
|
<h2 className="text-2xl font-bold mb-4">Challenge</h2>
|
||||||
<ul className="space-y-4">
|
<p className="text-foreground/70 leading-relaxed">{project.challenge}</p>
|
||||||
{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>
|
</div>
|
||||||
|
|
||||||
{/* Solutions */}
|
{/* Solution */}
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold mb-6">Solutions</h2>
|
<h2 className="text-2xl font-bold mb-4">Solution</h2>
|
||||||
<ul className="space-y-4">
|
<p className="text-foreground/70 leading-relaxed">{project.solution}</p>
|
||||||
{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>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
|
|
||||||
|
{/* Full Description */}
|
||||||
|
<div className="mb-16 p-8 rounded-lg bg-card border border-foreground/10">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Project Overview</h2>
|
||||||
|
<p className="text-foreground/70 leading-relaxed">{project.fullDescription}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Results */}
|
{/* Results */}
|
||||||
<section className="mb-20">
|
<div>
|
||||||
<h2 className="text-2xl font-bold mb-6">Results & Impact</h2>
|
<h2 className="text-2xl font-bold mb-6">Results</h2>
|
||||||
<div className="grid md:grid-cols-3 gap-8">
|
<ul className="grid md:grid-cols-2 gap-4">
|
||||||
{project.results.map((result: string, index: number) => (
|
{project.results.map((result: string, index: number) => (
|
||||||
<div
|
<li key={index} className="flex items-start gap-3 p-4 rounded-lg bg-card border border-foreground/10">
|
||||||
key={index}
|
<div className="flex-shrink-0 mt-1">
|
||||||
className="p-6 bg-card rounded-lg border border-accent text-center"
|
<div className="flex items-center justify-center h-6 w-6 rounded-full bg-accent text-background">
|
||||||
>
|
<span className="text-sm font-bold">✓</span>
|
||||||
<p className="text-foreground/80">{result}</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<span className="text-foreground/70">{result}</span>
|
||||||
|
</li>
|
||||||
))}
|
))}
|
||||||
</div>
|
</ul>
|
||||||
</section>
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
{/* CTA */}
|
{/* CTA Section */}
|
||||||
<section className="text-center py-12">
|
<section className="w-full py-16 md:py-24 border-t border-foreground/10">
|
||||||
<h2 className="text-3xl font-bold mb-6">Ready to Work Together?</h2>
|
<div className="max-w-6xl mx-auto px-4 text-center">
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold mb-4">Interested in working together?</h2>
|
||||||
|
<p className="text-lg text-foreground/70 mb-8 max-w-2xl mx-auto">
|
||||||
|
Let's discuss your project vision and how I can help bring your ideas to life.
|
||||||
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => router.push("/#contact")}
|
onClick={() => router.push("/#contact")}
|
||||||
className="px-8 py-3 bg-primary-cta text-white rounded-lg hover:opacity-90 transition-opacity"
|
className="inline-block px-8 py-3 rounded-lg bg-primary-cta text-background font-medium hover:bg-primary-cta/90 transition-colors"
|
||||||
>
|
>
|
||||||
Start Your Project
|
Start a Project
|
||||||
</button>
|
</button>
|
||||||
</section>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</main>
|
|
||||||
|
|
||||||
<div id="footer" data-section="footer">
|
{/* Footer */}
|
||||||
<FooterLogoReveal
|
<div className="border-t border-foreground/10">
|
||||||
logoText="Portfolio"
|
<FooterLogoReveal
|
||||||
leftLink={{ text: "Privacy Policy", href: "#" }}
|
logoText="Portfolio"
|
||||||
rightLink={{ text: "Terms of Service", href: "#" }}
|
leftLink={{ text: "Privacy Policy", href: "#" }}
|
||||||
/>
|
rightLink={{ text: "Terms of Service", href: "#" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,32 +1,95 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
|
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
|
||||||
import NavbarLayoutFloatingOverlay from "@/components/navbar/NavbarLayoutFloatingOverlay/NavbarLayoutFloatingOverlay";
|
import NavbarLayoutFloatingOverlay from "@/components/navbar/NavbarLayoutFloatingOverlay/NavbarLayoutFloatingOverlay";
|
||||||
import HeroBillboardCarousel from "@/components/sections/hero/HeroBillboardCarousel";
|
import HeroBillboardCarousel from "@/components/sections/hero/HeroBillboardCarousel";
|
||||||
|
import FeatureCardTwentyFour from "@/components/sections/feature/FeatureCardTwentyFour";
|
||||||
import TextSplitAbout from "@/components/sections/about/TextSplitAbout";
|
import TextSplitAbout from "@/components/sections/about/TextSplitAbout";
|
||||||
|
import TestimonialCardTwelve from "@/components/sections/testimonial/TestimonialCardTwelve";
|
||||||
import ContactCenter from "@/components/sections/contact/ContactCenter";
|
import ContactCenter from "@/components/sections/contact/ContactCenter";
|
||||||
import FooterLogoReveal from "@/components/sections/footer/FooterLogoReveal";
|
import FooterLogoReveal from "@/components/sections/footer/FooterLogoReveal";
|
||||||
import { useParams } from "next/navigation";
|
|
||||||
|
|
||||||
|
// Mock project data structure
|
||||||
const projectsData: Record<string, any> = {
|
const projectsData: Record<string, any> = {
|
||||||
"1": {
|
"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: [
|
id: "1", title: "Digital Brand Identity System", subtitle: "Complete visual identity redesign for tech startup", 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.", heroImages: [
|
||||||
"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."],
|
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/a-beautiful-case-study-image-showing-a-c-1773043067456-ca425d2a.png", "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/a-stunning-creative-portfolio-showcasing-1773043068077-12221410.png"],
|
||||||
|
challenge: "The client needed a fresh brand identity to reflect their innovative approach to technology solutions. Their existing branding felt dated and didn't resonate with their target market of tech-savvy startups.", solution: "I conducted extensive market research and competitor analysis to identify market gaps. Working closely with the client, I developed a comprehensive brand strategy that included logo design, color palette, typography system, and brand guidelines. The new identity was modern, scalable, and distinctly positioned in the market.", results: [
|
||||||
|
"40% increase in brand recognition", "Enhanced market positioning", "Improved customer engagement", "Scalable design system for future growth"],
|
||||||
|
testimonial: {
|
||||||
|
name: "Sarah Chen", role: "CEO", quote: "The new brand identity transformed how customers perceive our company. Within months, we saw significant improvement in lead generation and customer retention.", image: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/professional-headshot-portrait-of-a-crea-1773043067225-0aed98b9.png"},
|
||||||
|
relatedProjects: [
|
||||||
|
{
|
||||||
|
id: "2", title: "E-commerce Platform Redesign", author: "UX Design", description: "User-centered redesign of enterprise e-commerce platform", tags: ["UX Design", "E-commerce"],
|
||||||
|
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/professional-portfolio-project-image-fea-1773043068039-3c07e3ca.png", imageAlt: "E-commerce redesign"},
|
||||||
|
{
|
||||||
|
id: "3", title: "SaaS Product Interface Design", author: "Product Design", description: "Designed intuitive interface for data analytics platform", tags: ["SaaS", "Product Design"],
|
||||||
|
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/contemporary-design-project-featured-in--1773043068896-89ed9073.png", imageAlt: "SaaS design"},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
"2": {
|
"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: [
|
id: "2", title: "E-commerce Platform Redesign", subtitle: "User-centered redesign of enterprise e-commerce platform", 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.", heroImages: [
|
||||||
"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."],
|
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/professional-portfolio-project-image-fea-1773043068039-3c07e3ca.png", "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/bold-creative-design-project-displayed-i-1773043067366-f2dd6201.png"],
|
||||||
|
challenge: "The existing e-commerce platform had a complex checkout flow resulting in a 60% cart abandonment rate. The interface was cluttered, and mobile users faced significant usability issues.", solution: "I conducted user research and usability testing to identify pain points. The redesign focused on streamlining the checkout process, implementing clear call-to-action buttons, and ensuring mobile-first responsiveness. I also implemented accessibility features following WCAG guidelines.", results: [
|
||||||
|
"35% increase in conversion rate", "60% reduction in cart abandonment", "40% increase in mobile transactions", "Enhanced accessibility compliance"],
|
||||||
|
testimonial: {
|
||||||
|
name: "Marcus Johnson", role: "Product Director", quote: "The redesigned platform has been instrumental in our growth. The improved user experience directly translated to increased sales and customer satisfaction.", image: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/professional-portrait-photograph-of-busi-1773043067191-64c1ffe8.png"},
|
||||||
|
relatedProjects: [
|
||||||
|
{
|
||||||
|
id: "1", title: "Digital Brand Identity System", author: "Brand Strategy", description: "Complete visual identity redesign for tech startup", tags: ["Branding", "Identity"],
|
||||||
|
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"},
|
||||||
|
{
|
||||||
|
id: "3", title: "SaaS Product Interface Design", author: "Product Design", description: "Designed intuitive interface for data analytics platform", tags: ["SaaS", "Product Design"],
|
||||||
|
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/contemporary-design-project-featured-in--1773043068896-89ed9073.png", imageAlt: "SaaS design"},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
"3": {
|
"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: [
|
id: "3", title: "SaaS Product Interface Design", subtitle: "Intuitive interface design for data analytics platform", 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%.", heroImages: [
|
||||||
"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."],
|
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/contemporary-design-project-featured-in--1773043068896-89ed9073.png", "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/professional-portfolio-piece-featuring-s-1773043067962-bd19ff43.png"],
|
||||||
|
challenge: "Users found the data analytics platform overwhelming due to complex interfaces and poor information hierarchy. The steep learning curve resulted in high support costs and low user adoption rates.", solution: "I created a comprehensive design system with reusable components and clear information architecture. The new interface simplified complex data visualization, implemented progressive disclosure, and included an interactive onboarding flow to guide users through key features.", results: [
|
||||||
|
"50% reduction in support tickets", "75% faster user onboarding", "Improved user satisfaction scores", "Scalable design system for product growth"],
|
||||||
|
testimonial: {
|
||||||
|
name: "Elena Rodriguez", role: "Head of Product", quote: "The new design has revolutionized how our users interact with our platform. The reduction in support tickets alone has freed up resources for innovation.", image: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/professional-headshot-of-creative-indust-1773043067885-58b8d4c1.png"},
|
||||||
|
relatedProjects: [
|
||||||
|
{
|
||||||
|
id: "1", title: "Digital Brand Identity System", author: "Brand Strategy", description: "Complete visual identity redesign for tech startup", tags: ["Branding", "Identity"],
|
||||||
|
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"},
|
||||||
|
{
|
||||||
|
id: "2", title: "E-commerce Platform Redesign", author: "UX Design", description: "User-centered redesign of enterprise e-commerce platform", tags: ["UX Design", "E-commerce"],
|
||||||
|
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/professional-portfolio-project-image-fea-1773043068039-3c07e3ca.png", imageAlt: "E-commerce redesign"},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ProjectDetailPage() {
|
export default function ProjectDetailPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const projectId = params.id as string;
|
const projectId = params.id as string;
|
||||||
const project = projectsData[projectId] || projectsData["1"];
|
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 flex-col items-center justify-center">
|
||||||
|
<h1 className="text-4xl font-bold mb-4">Project Not Found</h1>
|
||||||
|
<a href="/" className="text-primary-cta underline">
|
||||||
|
Return to Portfolio
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider
|
<ThemeProvider
|
||||||
@@ -45,10 +108,10 @@ export default function ProjectDetailPage() {
|
|||||||
<NavbarLayoutFloatingOverlay
|
<NavbarLayoutFloatingOverlay
|
||||||
brandName="Portfolio"
|
brandName="Portfolio"
|
||||||
navItems={[
|
navItems={[
|
||||||
{ name: "Work", id: "/" },
|
{ name: "Work", id: "work" },
|
||||||
{ name: "About", id: "/" },
|
{ name: "About", id: "about" },
|
||||||
{ name: "Services", id: "/" },
|
{ name: "Services", id: "services" },
|
||||||
{ name: "Contact", id: "/" },
|
{ name: "Contact", id: "contact" },
|
||||||
]}
|
]}
|
||||||
button={{
|
button={{
|
||||||
text: "Get in Touch", href: "/"}}
|
text: "Get in Touch", href: "/"}}
|
||||||
@@ -57,16 +120,14 @@ export default function ProjectDetailPage() {
|
|||||||
|
|
||||||
<div id="hero" data-section="hero">
|
<div id="hero" data-section="hero">
|
||||||
<HeroBillboardCarousel
|
<HeroBillboardCarousel
|
||||||
title={project.heroTitle}
|
title={project.title}
|
||||||
description={project.heroDescription}
|
description={project.subtitle}
|
||||||
tag="Project"
|
tag="Case Study"
|
||||||
background={{ variant: "sparkles-gradient" }}
|
background={{ variant: "sparkles-gradient" }}
|
||||||
mediaItems={[
|
mediaItems={project.heroImages.map((src: string) => ({
|
||||||
{
|
imageSrc: src,
|
||||||
imageSrc: project.heroImage,
|
imageAlt: project.title,
|
||||||
imageAlt: project.title,
|
}))}
|
||||||
},
|
|
||||||
]}
|
|
||||||
buttons={[
|
buttons={[
|
||||||
{ text: "Back to Portfolio", href: "/" },
|
{ text: "Back to Portfolio", href: "/" },
|
||||||
{ text: "Start a Project", href: "/" },
|
{ text: "Start a Project", href: "/" },
|
||||||
@@ -76,21 +137,70 @@ export default function ProjectDetailPage() {
|
|||||||
|
|
||||||
<div id="about" data-section="about">
|
<div id="about" data-section="about">
|
||||||
<TextSplitAbout
|
<TextSplitAbout
|
||||||
title={project.aboutTitle}
|
title="Project Overview"
|
||||||
description={project.aboutDescription}
|
description={[
|
||||||
|
`Challenge: ${project.challenge}`,
|
||||||
|
`Solution: ${project.solution}`,
|
||||||
|
`Results: ${project.results.join(", ")}`,
|
||||||
|
]}
|
||||||
buttons={[
|
buttons={[
|
||||||
{ text: "View Other Projects", href: "/" },
|
{ text: "Back to Portfolio", href: "/" },
|
||||||
{ text: "Connect", href: "/" },
|
{ text: "Next Project", href: "/" },
|
||||||
]}
|
]}
|
||||||
showBorder={true}
|
showBorder={true}
|
||||||
useInvertedBackground={true}
|
useInvertedBackground={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="services" data-section="services">
|
||||||
|
<FeatureCardTwentyFour
|
||||||
|
title="Key Results"
|
||||||
|
description="Measurable impact achieved through this project"
|
||||||
|
tag="Outcomes"
|
||||||
|
features={project.results.map((result: string, index: number) => ({
|
||||||
|
id: String(index + 1),
|
||||||
|
title: result,
|
||||||
|
author: "Achievement", description: "Successfully delivered this key result through strategic design and implementation.", tags: ["Success", "Impact"],
|
||||||
|
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AhRowzw9k0ZSJ87n7KX34EwoE1/abstract-illustration-representing-creat-1773043067648-d1f91d56.png", imageAlt: result,
|
||||||
|
}))}
|
||||||
|
animationType="slide-up"
|
||||||
|
textboxLayout="default"
|
||||||
|
useInvertedBackground={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="testimonials" data-section="testimonials">
|
||||||
|
<TestimonialCardTwelve
|
||||||
|
cardTitle={`${project.testimonial.name}, ${project.testimonial.role}`}
|
||||||
|
cardTag="Client Feedback"
|
||||||
|
cardAnimation="slide-up"
|
||||||
|
useInvertedBackground={false}
|
||||||
|
testimonials={[
|
||||||
|
{
|
||||||
|
id: "1", name: project.testimonial.name,
|
||||||
|
imageSrc: project.testimonial.image,
|
||||||
|
imageAlt: project.testimonial.name,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="work" data-section="work">
|
||||||
|
<FeatureCardTwentyFour
|
||||||
|
title="Related Projects"
|
||||||
|
description="Explore other successful case studies"
|
||||||
|
tag="More Work"
|
||||||
|
features={project.relatedProjects}
|
||||||
|
animationType="slide-up"
|
||||||
|
textboxLayout="default"
|
||||||
|
useInvertedBackground={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="contact" data-section="contact">
|
<div id="contact" data-section="contact">
|
||||||
<ContactCenter
|
<ContactCenter
|
||||||
tag="Let's Talk"
|
tag="Let's Talk"
|
||||||
title="Ready to Start Your Next Project?"
|
title="Ready for Your Next Project?"
|
||||||
description="Get in touch to discuss your design needs, project vision, and how we can collaborate to create exceptional digital experiences."
|
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" }}
|
background={{ variant: "sparkles-gradient" }}
|
||||||
useInvertedBackground={false}
|
useInvertedBackground={false}
|
||||||
|
|||||||
@@ -1,62 +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 FooterLogoReveal from "@/components/sections/footer/FooterLogoReveal";
|
|
||||||
|
|
||||||
export default function DigitalBrandIdentityPage() {
|
|
||||||
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="Digital Brand Identity System"
|
|
||||||
description="Complete visual identity redesign for tech startup. Developed comprehensive brand guidelines, typography system, and digital asset library."
|
|
||||||
background={{ variant: "sparkles-gradient" }}
|
|
||||||
mediaItems={[
|
|
||||||
{
|
|
||||||
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"
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
buttons={[
|
|
||||||
{ text: "Back to Work", href: "/" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="footer" data-section="footer">
|
|
||||||
<FooterLogoReveal
|
|
||||||
logoText="Portfolio"
|
|
||||||
leftLink={{ text: "Privacy Policy", href: "#" }}
|
|
||||||
rightLink={{ text: "Terms of Service", href: "#" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</ThemeProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,62 +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 FooterLogoReveal from "@/components/sections/footer/FooterLogoReveal";
|
|
||||||
|
|
||||||
export default function EcommercePlatformPage() {
|
|
||||||
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="E-commerce Platform Redesign"
|
|
||||||
description="User-centered redesign of enterprise e-commerce platform. Improved conversion rate by 35% through streamlined navigation and optimized checkout flow."
|
|
||||||
background={{ variant: "sparkles-gradient" }}
|
|
||||||
mediaItems={[
|
|
||||||
{
|
|
||||||
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"
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
buttons={[
|
|
||||||
{ text: "Back to Work", href: "/" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="footer" data-section="footer">
|
|
||||||
<FooterLogoReveal
|
|
||||||
logoText="Portfolio"
|
|
||||||
leftLink={{ text: "Privacy Policy", href: "#" }}
|
|
||||||
rightLink={{ text: "Terms of Service", href: "#" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</ThemeProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,102 +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";
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,62 +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 FooterLogoReveal from "@/components/sections/footer/FooterLogoReveal";
|
|
||||||
|
|
||||||
export default function SaasProductPage() {
|
|
||||||
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="SaaS Product Interface Design"
|
|
||||||
description="Designed intuitive interface for data analytics SaaS platform. Created interactive prototypes and design system for 15+ component variations."
|
|
||||||
background={{ variant: "sparkles-gradient" }}
|
|
||||||
mediaItems={[
|
|
||||||
{
|
|
||||||
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"
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
buttons={[
|
|
||||||
{ text: "Back to Work", href: "/" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="footer" data-section="footer">
|
|
||||||
<FooterLogoReveal
|
|
||||||
logoText="Portfolio"
|
|
||||||
leftLink={{ text: "Privacy Policy", href: "#" }}
|
|
||||||
rightLink={{ text: "Terms of Service", href: "#" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</ThemeProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,27 +1,123 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { useCardAnimation } from "./hooks/useCardAnimation";
|
|
||||||
|
import { memo, Children } from "react";
|
||||||
|
import CardStackTextBox from "@/components/cardStack/CardStackTextBox";
|
||||||
|
import { useCardAnimation } from "@/components/cardStack/hooks/useCardAnimation";
|
||||||
|
import { cls } from "@/lib/utils";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import type { ButtonConfig, ButtonAnimationType, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
|
||||||
|
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||||
|
|
||||||
interface CardListProps {
|
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;
|
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 CardList = ({
|
||||||
const state = useCardAnimation({
|
children,
|
||||||
rotationX: 0,
|
animationType,
|
||||||
rotationY: 0,
|
useUncappedRounding = false,
|
||||||
rotationZ: 0,
|
title,
|
||||||
perspective: 1000,
|
titleSegments,
|
||||||
duration: 0.3,
|
description,
|
||||||
});
|
tag,
|
||||||
|
tagIcon,
|
||||||
|
tagAnimation,
|
||||||
|
buttons,
|
||||||
|
buttonAnimation,
|
||||||
|
textboxLayout,
|
||||||
|
useInvertedBackground,
|
||||||
|
disableCardWrapper = false,
|
||||||
|
ariaLabel = "Card list",
|
||||||
|
className = "",
|
||||||
|
containerClassName = "",
|
||||||
|
cardClassName = "",
|
||||||
|
textBoxClassName = "",
|
||||||
|
titleClassName = "",
|
||||||
|
titleImageWrapperClassName = "",
|
||||||
|
titleImageClassName = "",
|
||||||
|
descriptionClassName = "",
|
||||||
|
tagClassName = "",
|
||||||
|
buttonContainerClassName = "",
|
||||||
|
buttonClassName = "",
|
||||||
|
buttonTextClassName = "",
|
||||||
|
}: CardListProps) => {
|
||||||
|
const childrenArray = Children.toArray(children);
|
||||||
|
const { itemRefs } = useCardAnimation({ animationType, itemCount: childrenArray.length, useIndividualTriggers: true });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`card-list-container ${className}`}>
|
<section
|
||||||
{items.map((item, index) => (
|
aria-label={ariaLabel}
|
||||||
<div key={index} className="card-item">
|
className={cls(
|
||||||
{item.label}
|
"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>
|
||||||
</div>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
CardList.displayName = "CardList";
|
||||||
|
|
||||||
|
export default memo(CardList);
|
||||||
|
|||||||
@@ -1,23 +1,229 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { TimelineBase } from "./layouts/timelines/TimelineBase";
|
|
||||||
|
|
||||||
interface CardStackProps {
|
import { memo, Children } from "react";
|
||||||
items: any[];
|
import { CardStackProps } from "./types";
|
||||||
className?: string;
|
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 = "" }) => {
|
// Check if the current grid config has gridRows defined
|
||||||
return (
|
const gridConfig = gridConfigs[gridVariant]?.[itemCount];
|
||||||
<div className={`card-stack-container ${className}`}>
|
const hasFixedGridRows = gridConfig && 'gridRows' in gridConfig && gridConfig.gridRows;
|
||||||
<TimelineBase
|
|
||||||
items={items.map((item) => ({
|
// If grid has fixed row heights and we have uniformGridCustomHeightClasses,
|
||||||
id: item.id,
|
// we need to use min-h-0 on md+ to prevent conflicts
|
||||||
label: item.label,
|
let adjustedHeightClasses = uniformGridCustomHeightClasses;
|
||||||
detail: item.detail,
|
if (hasFixedGridRows && uniformGridCustomHeightClasses) {
|
||||||
}))}
|
// Extract the mobile min-height and add md:min-h-0
|
||||||
/>
|
const mobileMinHeight = uniformGridCustomHeightClasses.split(' ')[0];
|
||||||
</div>
|
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);
|
||||||
|
|||||||
@@ -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 {
|
gsap.registerPlugin(ScrollTrigger);
|
||||||
rotationX?: number;
|
|
||||||
rotationY?: number;
|
interface UseCardAnimationProps {
|
||||||
rotationZ?: number;
|
animationType: CardAnimationType | "depth-3d";
|
||||||
perspective?: number;
|
itemCount: number;
|
||||||
duration?: number;
|
isGrid?: boolean;
|
||||||
|
supports3DAnimation?: boolean;
|
||||||
|
gridVariant?: GridVariant;
|
||||||
|
useIndividualTriggers?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AnimationState {
|
export const useCardAnimation = ({
|
||||||
transform: string;
|
animationType,
|
||||||
transition: string;
|
itemCount,
|
||||||
itemRefs?: React.MutableRefObject<(HTMLElement | null)[]>;
|
isGrid = true,
|
||||||
containerRef?: React.MutableRefObject<HTMLDivElement | null>;
|
supports3DAnimation = false,
|
||||||
perspectiveRef?: React.MutableRefObject<HTMLDivElement | null>;
|
gridVariant,
|
||||||
bottomContentRef?: React.MutableRefObject<HTMLDivElement | null>;
|
useIndividualTriggers = false
|
||||||
}
|
}: UseCardAnimationProps) => {
|
||||||
|
|
||||||
export const useCardAnimation = (
|
|
||||||
options: Depth3DAnimationOptions = {}
|
|
||||||
): AnimationState => {
|
|
||||||
const [state, setState] = useState<AnimationState>({
|
|
||||||
transform: "", transition: ""});
|
|
||||||
|
|
||||||
const itemRefs = useRef<(HTMLElement | null)[]>([]);
|
const itemRefs = useRef<(HTMLElement | null)[]>([]);
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const perspectiveRef = useRef<HTMLDivElement | null>(null);
|
const perspectiveRef = useRef<HTMLDivElement | null>(null);
|
||||||
const bottomContentRef = useRef<HTMLDivElement | null>(null);
|
const bottomContentRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const {
|
// Enable 3D effect only when explicitly supported and conditions are met
|
||||||
rotationX = 0,
|
const { isMobile } = useDepth3DAnimation({
|
||||||
rotationY = 0,
|
itemRefs,
|
||||||
rotationZ = 0,
|
containerRef,
|
||||||
perspective = 1000,
|
perspectiveRef,
|
||||||
duration = 0.3,
|
isEnabled: animationType === "depth-3d" && isGrid && supports3DAnimation && gridVariant === "uniform-all-items-equal",
|
||||||
} = options;
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
// Use scale-rotate as fallback when depth-3d conditions aren't met
|
||||||
const transform = `perspective(${perspective}px) rotateX(${rotationX}deg) rotateY(${rotationY}deg) rotateZ(${rotationZ}deg)`;
|
const effectiveAnimationType =
|
||||||
setState({
|
animationType === "depth-3d" && (isMobile || !isGrid || gridVariant !== "uniform-all-items-equal")
|
||||||
transform,
|
? "scale-rotate"
|
||||||
transition: `transform ${duration}s ease-out`,
|
: animationType;
|
||||||
itemRefs,
|
|
||||||
containerRef,
|
|
||||||
perspectiveRef,
|
|
||||||
bottomContentRef,
|
|
||||||
});
|
|
||||||
}, [rotationX, rotationY, rotationZ, perspective, duration]);
|
|
||||||
|
|
||||||
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 };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,33 +1,118 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useRef, RefObject } from "react";
|
||||||
|
|
||||||
interface Depth3DAnimationOptions {
|
const MOBILE_BREAKPOINT = 768;
|
||||||
rotationX?: number;
|
const ANIMATION_SPEED = 0.05;
|
||||||
rotationY?: number;
|
const ROTATION_SPEED = 0.1;
|
||||||
rotationZ?: number;
|
const MOUSE_MULTIPLIER = 0.5;
|
||||||
perspective?: number;
|
const ROTATION_MULTIPLIER = 0.25;
|
||||||
duration?: number;
|
|
||||||
|
interface UseDepth3DAnimationProps {
|
||||||
|
itemRefs: RefObject<(HTMLElement | null)[]>;
|
||||||
|
containerRef: RefObject<HTMLDivElement | null>;
|
||||||
|
perspectiveRef?: RefObject<HTMLDivElement | null>;
|
||||||
|
isEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useDepth3DAnimation = (
|
export const useDepth3DAnimation = ({
|
||||||
options: Depth3DAnimationOptions = {}
|
itemRefs,
|
||||||
) => {
|
containerRef,
|
||||||
const [transform, setTransform] = useState("");
|
perspectiveRef,
|
||||||
|
isEnabled,
|
||||||
const {
|
}: UseDepth3DAnimationProps) => {
|
||||||
rotationX = 0,
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
rotationY = 0,
|
|
||||||
rotationZ = 0,
|
|
||||||
perspective = 1000,
|
|
||||||
duration = 0.3,
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
|
// Detect mobile viewport
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const transform = `perspective(${perspective}px) rotateX(${rotationX}deg) rotateY(${rotationY}deg) rotateZ(${rotationZ}deg)`;
|
const checkMobile = () => {
|
||||||
setTransform(transform);
|
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||||
}, [rotationX, rotationY, rotationZ, perspective]);
|
};
|
||||||
|
|
||||||
return {
|
checkMobile();
|
||||||
transform,
|
window.addEventListener("resize", checkMobile);
|
||||||
transition: `transform ${duration}s ease-out`,
|
|
||||||
};
|
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 };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,26 +1,148 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { useCardAnimation } from "@/components/cardStack/hooks/useCardAnimation";
|
|
||||||
|
|
||||||
interface AutoCarouselProps {
|
import { memo, Children } from "react";
|
||||||
items?: any[];
|
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 AutoCarousel = ({
|
||||||
const state = useCardAnimation({
|
children,
|
||||||
rotationX: 0,
|
uniformGridCustomHeightClasses,
|
||||||
rotationY: 0,
|
animationType,
|
||||||
rotationZ: 0,
|
speed = 50,
|
||||||
perspective: 1000,
|
title,
|
||||||
duration: 0.3,
|
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 (
|
// Bottom marquee direction is opposite of top
|
||||||
<div className="auto-carousel">
|
const bottomMarqueeDirection = topMarqueeDirection === "left" ? "right" : "left";
|
||||||
{items.map((item, index) => (
|
|
||||||
<div key={index} className="carousel-item">
|
// Reverse order for bottom marquee to avoid alignment with top
|
||||||
{item.label}
|
const bottomChildren = dualMarquee ? [...childrenArray].reverse() : [];
|
||||||
</div>
|
|
||||||
))}
|
return (
|
||||||
</div>
|
<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);
|
||||||
|
|||||||
@@ -1,26 +1,182 @@
|
|||||||
import React, { useRef } from "react";
|
"use client";
|
||||||
import { useCardAnimation } from "@/components/cardStack/hooks/useCardAnimation";
|
|
||||||
|
|
||||||
interface ButtonCarouselProps {
|
import { memo, Children } from "react";
|
||||||
items?: any[];
|
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 ButtonCarousel = ({
|
||||||
const state = useCardAnimation({
|
children,
|
||||||
rotationX: 0,
|
uniformGridCustomHeightClasses,
|
||||||
rotationY: 0,
|
animationType,
|
||||||
rotationZ: 0,
|
title,
|
||||||
perspective: 1000,
|
titleSegments,
|
||||||
duration: 0.3,
|
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 (
|
const {
|
||||||
<div className="button-carousel">
|
prevBtnDisabled,
|
||||||
{items.map((item, index) => (
|
nextBtnDisabled,
|
||||||
<div key={index} className="carousel-item">
|
onPrevButtonClick,
|
||||||
{item.label}
|
onNextButtonClick,
|
||||||
</div>
|
} = usePrevNextButtons(emblaApi);
|
||||||
))}
|
|
||||||
</div>
|
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);
|
||||||
|
|||||||
@@ -1,26 +1,150 @@
|
|||||||
import React, { useRef } from "react";
|
"use client";
|
||||||
import { useCardAnimation } from "@/components/cardStack/hooks/useCardAnimation";
|
|
||||||
|
|
||||||
interface GridLayoutProps {
|
import { memo, Children } from "react";
|
||||||
items?: any[];
|
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 GridLayout = ({
|
||||||
const state = useCardAnimation({
|
children,
|
||||||
rotationX: 0,
|
itemCount,
|
||||||
rotationY: 0,
|
gridVariant = "uniform-all-items-equal",
|
||||||
rotationZ: 0,
|
uniformGridCustomHeightClasses,
|
||||||
perspective: 1000,
|
gridRowsClassName,
|
||||||
duration: 0.3,
|
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 (
|
// Fallback to default uniform grid if no config
|
||||||
<div className="grid-layout">
|
const gridColsMap = {
|
||||||
{items.map((item, index) => (
|
1: "md:grid-cols-1",
|
||||||
<div key={index} className="grid-item">
|
2: "md:grid-cols-2",
|
||||||
{item.label}
|
3: "md:grid-cols-3",
|
||||||
</div>
|
4: "md:grid-cols-4",
|
||||||
))}
|
};
|
||||||
</div>
|
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);
|
||||||
|
|||||||
@@ -1,30 +1,149 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
|
|
||||||
interface TimelineItem {
|
import React, { Children, useCallback } from "react";
|
||||||
id: string;
|
import { cls } from "@/lib/utils";
|
||||||
label: string;
|
import CardStackTextBox from "../../CardStackTextBox";
|
||||||
detail: string;
|
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 {
|
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;
|
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> = ({
|
const TimelineBase = ({
|
||||||
items,
|
children,
|
||||||
className = ""}) => {
|
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 (
|
return (
|
||||||
<div className={`timeline-container ${className}`}>
|
<section
|
||||||
{items.map((item, index) => (
|
className={cls(
|
||||||
<div key={item.id} className="timeline-item">
|
"relative py-20 w-full",
|
||||||
<div className="timeline-marker" />
|
useInvertedBackground && "bg-foreground",
|
||||||
<div className="timeline-content">
|
className
|
||||||
<h3 className="timeline-label">{item.label}</h3>
|
)}
|
||||||
<p className="timeline-detail">{item.detail}</p>
|
aria-label={ariaLabel}
|
||||||
</div>
|
>
|
||||||
|
<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>
|
||||||
</div>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
TimelineBase.displayName = "TimelineBase";
|
||||||
|
|
||||||
|
export default React.memo(TimelineBase);
|
||||||
|
|||||||
@@ -1,32 +1,275 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { useCardAnimation } from "@/components/cardStack/hooks/useCardAnimation";
|
|
||||||
|
|
||||||
interface TimelinePhoneViewItem {
|
import React, { memo } from "react";
|
||||||
id: string;
|
import MediaContent from "@/components/shared/MediaContent";
|
||||||
label: string;
|
import CardStackTextBox from "../../CardStackTextBox";
|
||||||
detail: string;
|
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 {
|
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 TimelinePhoneView = ({
|
||||||
const state = useCardAnimation({
|
items,
|
||||||
rotationX: 0,
|
showTextBox = true,
|
||||||
rotationY: 0,
|
showDivider = false,
|
||||||
rotationZ: 0,
|
title,
|
||||||
perspective: 1000,
|
titleSegments,
|
||||||
duration: 0.3,
|
description,
|
||||||
|
tag,
|
||||||
|
tagIcon,
|
||||||
|
tagAnimation,
|
||||||
|
buttons,
|
||||||
|
buttonAnimation,
|
||||||
|
animationType,
|
||||||
|
textboxLayout,
|
||||||
|
useInvertedBackground,
|
||||||
|
className = "",
|
||||||
|
containerClassName = "",
|
||||||
|
textBoxClassName = "",
|
||||||
|
titleClassName = "",
|
||||||
|
descriptionClassName = "",
|
||||||
|
tagClassName = "",
|
||||||
|
buttonContainerClassName = "",
|
||||||
|
buttonClassName = "",
|
||||||
|
buttonTextClassName = "",
|
||||||
|
desktopContainerClassName = "",
|
||||||
|
mobileContainerClassName = "",
|
||||||
|
desktopContentClassName = "",
|
||||||
|
desktopWrapperClassName = "",
|
||||||
|
mobileWrapperClassName = "",
|
||||||
|
phoneFrameClassName = "",
|
||||||
|
mobilePhoneFrameClassName = "",
|
||||||
|
titleImageWrapperClassName = "",
|
||||||
|
titleImageClassName = "",
|
||||||
|
ariaLabel = "Timeline phone view section",
|
||||||
|
}: TimelinePhoneViewProps) => {
|
||||||
|
const { imageRefs, mobileImageRefs } = usePhoneAnimations(items);
|
||||||
|
const { itemRefs: contentRefs } = useCardAnimation({
|
||||||
|
animationType,
|
||||||
|
itemCount: items.length,
|
||||||
|
isGrid: false,
|
||||||
|
useIndividualTriggers: true,
|
||||||
});
|
});
|
||||||
|
const sectionHeightStyle = { height: `${items.length * 100}vh` };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="timeline-phone-view">
|
<section
|
||||||
{items.map((item) => (
|
className={cls(
|
||||||
<div key={item.id} className="timeline-item">
|
"relative py-20 overflow-hidden md:overflow-visible w-full",
|
||||||
{item.label}
|
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)}>
|
||||||
</div>
|
{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);
|
||||||
|
|||||||
@@ -1,33 +1,202 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { useCardAnimation } from "@/components/cardStack/hooks/useCardAnimation";
|
|
||||||
|
import React, { useEffect, useRef, memo, useState } from "react";
|
||||||
|
import { gsap } from "gsap";
|
||||||
|
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
||||||
|
import CardStackTextBox from "../../CardStackTextBox";
|
||||||
|
import { useCardAnimation } from "../../hooks/useCardAnimation";
|
||||||
|
import { cls } from "@/lib/utils";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import type { ButtonConfig, ButtonAnimationType, CardAnimationType, TitleSegment } from "../../types";
|
||||||
|
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||||
|
|
||||||
|
gsap.registerPlugin(ScrollTrigger);
|
||||||
|
|
||||||
interface TimelineProcessFlowItem {
|
interface TimelineProcessFlowItem {
|
||||||
id: string;
|
id: string;
|
||||||
|
content: React.ReactNode;
|
||||||
|
media: React.ReactNode;
|
||||||
reverse: boolean;
|
reverse: boolean;
|
||||||
media: React.ReactElement;
|
|
||||||
content: React.ReactElement;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TimelineProcessFlowProps {
|
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 TimelineProcessFlow = ({
|
||||||
const state = useCardAnimation({
|
items,
|
||||||
rotationX: 0,
|
title,
|
||||||
rotationY: 0,
|
titleSegments,
|
||||||
rotationZ: 0,
|
description,
|
||||||
perspective: 1000,
|
tag,
|
||||||
duration: 0.3,
|
tagIcon,
|
||||||
});
|
tagAnimation,
|
||||||
|
buttons,
|
||||||
|
buttonAnimation,
|
||||||
|
textboxLayout,
|
||||||
|
animationType,
|
||||||
|
useInvertedBackground,
|
||||||
|
ariaLabel = "Timeline process flow section",
|
||||||
|
className = "",
|
||||||
|
containerClassName = "",
|
||||||
|
textBoxClassName = "",
|
||||||
|
textBoxTitleClassName = "",
|
||||||
|
textBoxDescriptionClassName = "",
|
||||||
|
textBoxTagClassName = "",
|
||||||
|
textBoxButtonContainerClassName = "",
|
||||||
|
textBoxButtonClassName = "",
|
||||||
|
textBoxButtonTextClassName = "",
|
||||||
|
itemClassName = "",
|
||||||
|
mediaWrapperClassName = "",
|
||||||
|
numberClassName = "",
|
||||||
|
contentWrapperClassName = "",
|
||||||
|
gapClassName = "",
|
||||||
|
titleImageWrapperClassName = "",
|
||||||
|
titleImageClassName = "",
|
||||||
|
}: TimelineProcessFlowProps) => {
|
||||||
|
const processLineRef = useRef<HTMLDivElement>(null);
|
||||||
|
const { itemRefs } = useCardAnimation({ animationType, itemCount: items.length, useIndividualTriggers: true });
|
||||||
|
const [isMdScreen, setIsMdScreen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkScreenSize = () => {
|
||||||
|
setIsMdScreen(window.innerWidth >= 768);
|
||||||
|
};
|
||||||
|
|
||||||
|
checkScreenSize();
|
||||||
|
window.addEventListener('resize', checkScreenSize);
|
||||||
|
|
||||||
|
return () => window.removeEventListener('resize', checkScreenSize);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!processLineRef.current) return;
|
||||||
|
|
||||||
|
gsap.fromTo(
|
||||||
|
processLineRef.current,
|
||||||
|
{ yPercent: -100 },
|
||||||
|
{
|
||||||
|
yPercent: 0,
|
||||||
|
ease: "none",
|
||||||
|
scrollTrigger: {
|
||||||
|
trigger: ".timeline-line",
|
||||||
|
start: "top center",
|
||||||
|
end: "bottom center",
|
||||||
|
scrub: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
ScrollTrigger.getAll().forEach((trigger) => trigger.kill());
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="timeline-process-flow">
|
<section
|
||||||
{items.map((item) => (
|
className={cls(
|
||||||
<div key={item.id} className="timeline-item">
|
"relative py-20 w-full",
|
||||||
{item.content}
|
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>
|
<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);
|
||||||
|
|||||||
@@ -1,68 +1,156 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
"use client";
|
||||||
import { Product } from "@/lib/api/product";
|
|
||||||
|
|
||||||
interface CatalogProduct {
|
import { memo, useMemo, useCallback } from "react";
|
||||||
id: string;
|
import { useRouter } from "next/navigation";
|
||||||
name: string;
|
import Input from "@/components/form/Input";
|
||||||
price: string;
|
import ProductDetailVariantSelect from "@/components/ecommerce/productDetail/ProductDetailVariantSelect";
|
||||||
imageSrc: string;
|
import type { ProductVariant } from "@/components/ecommerce/productDetail/ProductDetailCard";
|
||||||
imageAlt?: string;
|
import { cls } from "@/lib/utils";
|
||||||
rating?: number;
|
import { useProducts } from "@/hooks/useProducts";
|
||||||
reviewCount?: string;
|
import ProductCatalogItem from "./ProductCatalogItem";
|
||||||
category?: string;
|
import type { CatalogProduct } from "./ProductCatalogItem";
|
||||||
onProductClick?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ProductCatalogProps {
|
interface ProductCatalogProps {
|
||||||
products?: Product[];
|
layout: "page" | "section";
|
||||||
loading?: boolean;
|
products?: CatalogProduct[];
|
||||||
error?: string;
|
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> = ({
|
const ProductCatalog = ({
|
||||||
products = [],
|
layout,
|
||||||
loading = false,
|
products: productsProp,
|
||||||
error = ""}) => {
|
searchValue = "",
|
||||||
const [catalogProducts, setCatalogProducts] = useState<CatalogProduct[]>([]);
|
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(() => {
|
const handleProductClick = useCallback((productId: string) => {
|
||||||
if (!loading) {
|
router.push(`/shop/${productId}`);
|
||||||
const transformed = products.map((product) => ({
|
}, [router]);
|
||||||
id: product.id,
|
|
||||||
name: product.name,
|
const products: CatalogProduct[] = useMemo(() => {
|
||||||
price: String(product.price),
|
if (productsProp && productsProp.length > 0) {
|
||||||
imageSrc: product.imageSrc || "/placeholder.jpg", imageAlt: product.imageAlt || product.name,
|
return productsProp;
|
||||||
rating: product.rating,
|
}
|
||||||
reviewCount: product.reviewCount,
|
|
||||||
category: product.category,
|
if (fetchedProducts.length === 0) {
|
||||||
brand: product.brand,
|
return [];
|
||||||
onProductClick: () => {},
|
}
|
||||||
}));
|
|
||||||
setCatalogProducts(transformed);
|
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 (
|
||||||
return <div className="error">Error: {error}</div>;
|
<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) {
|
{products.length === 0 ? (
|
||||||
return <div className="loading">Loading...</div>;
|
<p className="text-sm text-foreground/50 text-center py-20">
|
||||||
}
|
{emptyMessage}
|
||||||
|
</p>
|
||||||
return (
|
) : (
|
||||||
<div className="product-catalog">
|
<div
|
||||||
<div className="product-grid">
|
className={cls(
|
||||||
{catalogProducts.map((product) => (
|
"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6",
|
||||||
<div key={product.id} className="product-item">
|
gridClassName
|
||||||
<img src={product.imageSrc} alt={product.imageAlt} />
|
)}
|
||||||
<h3>{product.name}</h3>
|
>
|
||||||
<p className="price">${product.price}</p>
|
{products.map((product) => (
|
||||||
{product.rating && <div className="rating">{product.rating} stars</div>}
|
<ProductCatalogItem
|
||||||
{product.reviewCount && <div className="reviews">{product.reviewCount} reviews</div>}
|
key={product.id}
|
||||||
</div>
|
product={product}
|
||||||
))}
|
className={cardClassName}
|
||||||
</div>
|
imageClassName={imageClassName}
|
||||||
</div>
|
/>
|
||||||
);
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ProductCatalog.displayName = "ProductCatalog";
|
||||||
|
|
||||||
|
export default memo(ProductCatalog);
|
||||||
@@ -1,28 +1,244 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { CardStack } from "@/components/cardStack/CardStack";
|
|
||||||
|
import { memo } from "react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import CardStack from "@/components/cardStack/CardStack";
|
||||||
|
import Badge from "@/components/shared/Badge";
|
||||||
|
import OverlayArrowButton from "@/components/shared/OverlayArrowButton";
|
||||||
|
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
||||||
|
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||||
|
import type { BlogPost } from "@/lib/api/blog";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import type { ButtonConfig, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
|
||||||
|
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||||
|
|
||||||
|
type BlogCard = BlogPost;
|
||||||
|
|
||||||
interface BlogCardOneProps {
|
interface BlogCardOneProps {
|
||||||
blogs?: any[];
|
blogs: BlogCard[];
|
||||||
title?: string;
|
carouselMode?: "auto" | "buttons";
|
||||||
description?: string;
|
uniformGridCustomHeightClasses?: string;
|
||||||
animationType?: string;
|
animationType: CardAnimationType;
|
||||||
textboxLayout?: string;
|
title: string;
|
||||||
useInvertedBackground?: boolean;
|
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({
|
interface BlogCardItemProps {
|
||||||
blogs = [],
|
blog: BlogCard;
|
||||||
title = "Blog", description = "Latest articles", animationType = "slide-up", textboxLayout = "default", useInvertedBackground = false,
|
shouldUseLightText: boolean;
|
||||||
}: BlogCardOneProps) {
|
cardClassName?: string;
|
||||||
const items = blogs.map((blog) => ({
|
imageWrapperClassName?: string;
|
||||||
id: blog.id,
|
imageClassName?: string;
|
||||||
label: blog.title,
|
categoryClassName?: string;
|
||||||
detail: blog.excerpt,
|
cardTitleClassName?: string;
|
||||||
}));
|
excerptClassName?: string;
|
||||||
|
authorContainerClassName?: string;
|
||||||
return (
|
authorAvatarClassName?: string;
|
||||||
<div className="blog-card-one">
|
authorNameClassName?: string;
|
||||||
<CardStack items={items} />
|
dateClassName?: string;
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|||||||
@@ -1,28 +1,288 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { CardStack } from "@/components/cardStack/CardStack";
|
|
||||||
|
import { memo } from "react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import CardStack from "@/components/cardStack/CardStack";
|
||||||
|
import Tag from "@/components/shared/Tag";
|
||||||
|
import MediaContent from "@/components/shared/MediaContent";
|
||||||
|
import OverlayArrowButton from "@/components/shared/OverlayArrowButton";
|
||||||
|
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
||||||
|
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||||
|
import type { BlogPost } from "@/lib/api/blog";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import type { ButtonConfig, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
|
||||||
|
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||||
|
|
||||||
|
type BlogCard = BlogPost;
|
||||||
|
|
||||||
interface BlogCardThreeProps {
|
interface BlogCardThreeProps {
|
||||||
blogs?: any[];
|
blogs: BlogCard[];
|
||||||
title?: string;
|
carouselMode?: "auto" | "buttons";
|
||||||
description?: string;
|
uniformGridCustomHeightClasses?: string;
|
||||||
animationType?: string;
|
animationType: CardAnimationType;
|
||||||
textboxLayout?: string;
|
title: string;
|
||||||
useInvertedBackground?: boolean;
|
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({
|
interface BlogCardItemProps {
|
||||||
blogs = [],
|
blog: BlogCard;
|
||||||
title = "Blog", description = "Latest articles", animationType = "slide-up", textboxLayout = "default", useInvertedBackground = false,
|
useInvertedBackground: boolean;
|
||||||
}: BlogCardThreeProps) {
|
cardClassName?: string;
|
||||||
const items = blogs.map((blog) => ({
|
cardContentClassName?: string;
|
||||||
id: blog.id,
|
categoryTagClassName?: string;
|
||||||
label: blog.title,
|
cardTitleClassName?: string;
|
||||||
detail: blog.excerpt,
|
excerptClassName?: string;
|
||||||
}));
|
authorContainerClassName?: string;
|
||||||
|
authorAvatarClassName?: string;
|
||||||
return (
|
authorNameClassName?: string;
|
||||||
<div className="blog-card-three">
|
dateClassName?: string;
|
||||||
<CardStack items={items} />
|
mediaWrapperClassName?: string;
|
||||||
</div>
|
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;
|
||||||
|
|||||||
@@ -1,28 +1,241 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { CardStack } from "@/components/cardStack/CardStack";
|
|
||||||
|
import { memo } from "react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import CardStack from "@/components/cardStack/CardStack";
|
||||||
|
import Badge from "@/components/shared/Badge";
|
||||||
|
import OverlayArrowButton from "@/components/shared/OverlayArrowButton";
|
||||||
|
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
||||||
|
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||||
|
import type { BlogPost } from "@/lib/api/blog";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import type { ButtonConfig, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
|
||||||
|
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||||
|
|
||||||
|
type BlogCard = Omit<BlogPost, 'category'> & {
|
||||||
|
category: string | string[];
|
||||||
|
};
|
||||||
|
|
||||||
interface BlogCardTwoProps {
|
interface BlogCardTwoProps {
|
||||||
blogs?: any[];
|
blogs: BlogCard[];
|
||||||
title?: string;
|
carouselMode?: "auto" | "buttons";
|
||||||
description?: string;
|
uniformGridCustomHeightClasses?: string;
|
||||||
animationType?: string;
|
animationType: CardAnimationType;
|
||||||
textboxLayout?: string;
|
title: string;
|
||||||
useInvertedBackground?: boolean;
|
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({
|
interface BlogCardItemProps {
|
||||||
blogs = [],
|
blog: BlogCard;
|
||||||
title = "Blog", description = "Latest articles", animationType = "slide-up", textboxLayout = "default", useInvertedBackground = false,
|
shouldUseLightText: boolean;
|
||||||
}: BlogCardTwoProps) {
|
cardClassName?: string;
|
||||||
const items = blogs.map((blog) => ({
|
imageWrapperClassName?: string;
|
||||||
id: blog.id,
|
imageClassName?: string;
|
||||||
label: blog.title,
|
authorAvatarClassName?: string;
|
||||||
detail: blog.excerpt,
|
authorDateClassName?: string;
|
||||||
}));
|
cardTitleClassName?: string;
|
||||||
|
excerptClassName?: string;
|
||||||
return (
|
categoryClassName?: string;
|
||||||
<div className="blog-card-two">
|
|
||||||
<CardStack items={items} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|||||||
@@ -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 {
|
interface ContactCenterProps {
|
||||||
tag: string;
|
title: string;
|
||||||
title: string;
|
description: string;
|
||||||
description: string;
|
tag: string;
|
||||||
background?: { variant: string };
|
tagIcon?: LucideIcon;
|
||||||
useInvertedBackground?: boolean;
|
tagAnimation?: ButtonAnimationType;
|
||||||
inputPlaceholder?: string;
|
background: ContactCenterBackgroundProps;
|
||||||
buttonText?: string;
|
useInvertedBackground: boolean;
|
||||||
termsText?: string;
|
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({
|
const ContactCenter = ({
|
||||||
tag,
|
title,
|
||||||
title,
|
description,
|
||||||
description,
|
tag,
|
||||||
background = { variant: "sparkles-gradient" },
|
tagIcon,
|
||||||
useInvertedBackground = false,
|
tagAnimation,
|
||||||
inputPlaceholder = "Enter your email", buttonText = "Sign Up", termsText = "By clicking Sign Up you're confirming that you agree with our Terms and Conditions."}: ContactCenterProps) {
|
background,
|
||||||
const [email, setEmail] = useState("");
|
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) => {
|
const handleSubmit = async (email: string) => {
|
||||||
e.preventDefault();
|
try {
|
||||||
setEmail("");
|
await sendContactEmail({ email });
|
||||||
};
|
console.log("Email send successfully");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to send email:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="contact-center-container">
|
<section aria-label={ariaLabel} className={cls("relative py-20 w-full", useInvertedBackground && "bg-foreground", className)}>
|
||||||
<div className="contact-content">
|
<div className={cls("w-content-width mx-auto relative z-10", containerClassName)}>
|
||||||
<div className="contact-tag">{tag}</div>
|
<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)}>
|
||||||
<h2 className="contact-title">{title}</h2>
|
<div className="relative z-10 w-full md:w-1/2">
|
||||||
<p className="contact-description">{description}</p>
|
<ContactForm
|
||||||
<form onSubmit={handleSubmit} className="contact-form">
|
tag={tag}
|
||||||
<input
|
tagIcon={tagIcon}
|
||||||
type="email"
|
tagAnimation={tagAnimation}
|
||||||
placeholder={inputPlaceholder}
|
title={title}
|
||||||
value={email}
|
description={description}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
useInvertedBackground={useInvertedBackground}
|
||||||
required
|
inputPlaceholder={inputPlaceholder}
|
||||||
className="contact-input"
|
buttonText={buttonText}
|
||||||
/>
|
termsText={termsText}
|
||||||
<button type="submit" className="contact-button">
|
onSubmit={handleSubmit}
|
||||||
{buttonText}
|
centered={true}
|
||||||
</button>
|
tagClassName={tagClassName}
|
||||||
</form>
|
titleClassName={titleClassName}
|
||||||
<p className="contact-terms">{termsText}</p>
|
descriptionClassName={descriptionClassName}
|
||||||
</div>
|
formWrapperClassName={cls("md:w-8/10 2xl:w-6/10", formWrapperClassName)}
|
||||||
</div>
|
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;
|
||||||
|
|||||||
@@ -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 { 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 {
|
interface ContactFaqProps {
|
||||||
faqs?: any[];
|
faqs: FaqItem[];
|
||||||
title?: string;
|
ctaTitle: string;
|
||||||
description?: 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({
|
const ContactFaq = ({
|
||||||
faqs = [],
|
faqs,
|
||||||
title = "FAQ", description = "Frequently asked questions"}: ContactFaqProps) {
|
ctaTitle,
|
||||||
const state = useCardAnimation({
|
ctaDescription,
|
||||||
rotationX: 0,
|
ctaButton,
|
||||||
rotationY: 0,
|
ctaIcon: CtaIcon,
|
||||||
rotationZ: 0,
|
useInvertedBackground,
|
||||||
perspective: 1000,
|
animationType,
|
||||||
duration: 0.3,
|
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 (
|
return (
|
||||||
<div className="contact-faq">
|
<section
|
||||||
<h2>{title}</h2>
|
aria-label={ariaLabel}
|
||||||
<p>{description}</p>
|
className={cls("relative py-20 w-full", useInvertedBackground && "bg-foreground", className)}
|
||||||
<div className="faqs-container">
|
>
|
||||||
{faqs.map((faq) => (
|
<div className={cls("w-content-width mx-auto", containerClassName)}>
|
||||||
<div key={faq.id} className="faq-item">
|
<div className="grid grid-cols-1 md:grid-cols-12 gap-6 md:gap-8">
|
||||||
<h3>{faq.title}</h3>
|
<div
|
||||||
<p>{faq.content}</p>
|
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>
|
||||||
))}
|
|
||||||
|
<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>
|
||||||
</div>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
ContactFaq.displayName = "ContactFaq";
|
||||||
|
|
||||||
|
export default ContactFaq;
|
||||||
|
|||||||
@@ -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 {
|
interface ContactSplitProps {
|
||||||
tag: string;
|
title: string;
|
||||||
title: string;
|
description: string;
|
||||||
description: string;
|
tag: string;
|
||||||
background?: { variant: string };
|
tagIcon?: LucideIcon;
|
||||||
useInvertedBackground?: boolean;
|
tagAnimation?: ButtonAnimationType;
|
||||||
imageSrc?: string;
|
background: ContactSplitBackgroundProps;
|
||||||
inputPlaceholder?: string;
|
useInvertedBackground: boolean;
|
||||||
buttonText?: string;
|
imageSrc?: string;
|
||||||
termsText?: 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({
|
const ContactSplit = ({
|
||||||
tag,
|
title,
|
||||||
title,
|
description,
|
||||||
description,
|
tag,
|
||||||
background = { variant: "sparkles-gradient" },
|
tagIcon,
|
||||||
useInvertedBackground = false,
|
tagAnimation,
|
||||||
imageSrc,
|
background,
|
||||||
inputPlaceholder = "Enter your email", buttonText = "Sign Up", termsText = "By clicking Sign Up you're confirming that you agree with our Terms and Conditions."}: ContactSplitProps) {
|
useInvertedBackground,
|
||||||
const [email, setEmail] = useState("");
|
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) => {
|
const handleSubmit = async (email: string) => {
|
||||||
e.preventDefault();
|
try {
|
||||||
setEmail("");
|
await sendContactEmail({ email });
|
||||||
};
|
console.log("Email send successfully");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to send email:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
const contactContent = (
|
||||||
<div className="contact-split-container">
|
<div className="relative card rounded-theme-capped p-6 py-15 md:py-6 flex items-center justify-center">
|
||||||
<div className="contact-split-form">
|
<ContactForm
|
||||||
<div className="contact-tag">{tag}</div>
|
tag={tag}
|
||||||
<h2 className="contact-title">{title}</h2>
|
tagIcon={tagIcon}
|
||||||
<p className="contact-description">{description}</p>
|
tagAnimation={tagAnimation}
|
||||||
<form onSubmit={handleSubmit} className="contact-form">
|
title={title}
|
||||||
<input
|
description={description}
|
||||||
type="email"
|
useInvertedBackground={useInvertedBackground}
|
||||||
placeholder={inputPlaceholder}
|
inputPlaceholder={inputPlaceholder}
|
||||||
value={email}
|
buttonText={buttonText}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
termsText={termsText}
|
||||||
required
|
onSubmit={handleSubmit}
|
||||||
className="contact-input"
|
centered={true}
|
||||||
/>
|
className={cls("w-full", contactFormClassName)}
|
||||||
<button type="submit" className="contact-button">
|
tagClassName={tagClassName}
|
||||||
{buttonText}
|
titleClassName={titleClassName}
|
||||||
</button>
|
descriptionClassName={descriptionClassName}
|
||||||
</form>
|
formWrapperClassName={cls("w-full md:w-8/10 2xl:w-7/10", formWrapperClassName)}
|
||||||
<p className="contact-terms">{termsText}</p>
|
formClassName={formClassName}
|
||||||
</div>
|
inputClassName={inputClassName}
|
||||||
{imageSrc && (
|
buttonClassName={buttonClassName}
|
||||||
<div className="contact-split-image">
|
buttonTextClassName={buttonTextClassName}
|
||||||
<img src={imageSrc} alt="Contact" />
|
termsClassName={termsClassName}
|
||||||
|
/>
|
||||||
|
<div className="absolute inset w-full h-full z-0 rounded-theme-capped overflow-hidden" >
|
||||||
|
<HeroBackgrounds {...background} />
|
||||||
|
</div>
|
||||||
</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;
|
||||||
|
|||||||
@@ -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 {
|
interface ContactSplitFormProps {
|
||||||
tag: string;
|
title: string;
|
||||||
title: string;
|
description: string;
|
||||||
description: string;
|
inputs: InputField[];
|
||||||
background?: { variant: string };
|
textarea?: TextareaField;
|
||||||
useInvertedBackground?: boolean;
|
useInvertedBackground: boolean;
|
||||||
inputPlaceholder?: string;
|
imageSrc?: string;
|
||||||
buttonText?: string;
|
videoSrc?: string;
|
||||||
termsText?: 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({
|
const ContactSplitForm = ({
|
||||||
tag,
|
title,
|
||||||
title,
|
description,
|
||||||
description,
|
inputs,
|
||||||
background = { variant: "sparkles-gradient" },
|
textarea,
|
||||||
useInvertedBackground = false,
|
useInvertedBackground,
|
||||||
inputPlaceholder = "Enter your email", buttonText = "Sign Up", termsText = "By clicking Sign Up you're confirming that you agree with our Terms and Conditions."}: ContactSplitFormProps) {
|
imageSrc,
|
||||||
const [email, setEmail] = useState("");
|
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) => {
|
// Validate minimum inputs requirement
|
||||||
e.preventDefault();
|
if (inputs.length < 2) {
|
||||||
setEmail("");
|
throw new Error("ContactSplitForm requires at least 2 inputs");
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
// Initialize form data dynamically
|
||||||
<div className="contact-split-form-container">
|
const initialFormData: Record<string, string> = {};
|
||||||
<div className="contact-tag">{tag}</div>
|
inputs.forEach(input => {
|
||||||
<h2 className="contact-title">{title}</h2>
|
initialFormData[input.name] = "";
|
||||||
<p className="contact-description">{description}</p>
|
});
|
||||||
<form onSubmit={handleSubmit} className="contact-form">
|
if (textarea) {
|
||||||
<input
|
initialFormData[textarea.name] = "";
|
||||||
type="email"
|
}
|
||||||
placeholder={inputPlaceholder}
|
|
||||||
value={email}
|
const [formData, setFormData] = useState(initialFormData);
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
required
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
className="contact-input"
|
e.preventDefault();
|
||||||
/>
|
try {
|
||||||
<button type="submit" className="contact-button">
|
await sendContactEmail({ formData });
|
||||||
{buttonText}
|
console.log("Email send successfully");
|
||||||
</button>
|
setFormData(initialFormData);
|
||||||
</form>
|
} catch (error) {
|
||||||
<p className="contact-terms">{termsText}</p>
|
console.error("Failed to send email:", error);
|
||||||
</div>
|
}
|
||||||
);
|
};
|
||||||
}
|
|
||||||
|
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;
|
||||||
|
|||||||
@@ -1,28 +1,300 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { CardStack } from "@/components/cardStack/CardStack";
|
|
||||||
|
import CardStack from "@/components/cardStack/CardStack";
|
||||||
|
import Button from "@/components/button/Button";
|
||||||
|
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
||||||
|
import { getButtonProps } from "@/lib/buttonUtils";
|
||||||
|
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||||
|
import { BentoGlobe } from "@/components/bento/BentoGlobe";
|
||||||
|
import BentoIconInfoCards from "@/components/bento/BentoIconInfoCards";
|
||||||
|
import BentoAnimatedBarChart from "@/components/bento/BentoAnimatedBarChart";
|
||||||
|
import Bento3DStackCards from "@/components/bento/Bento3DStackCards";
|
||||||
|
import Bento3DTaskList, { type TaskItem } from "@/components/bento/Bento3DTaskList";
|
||||||
|
import BentoOrbitingIcons, { type OrbitingItem } from "@/components/bento/BentoOrbitingIcons";
|
||||||
|
import BentoMap from "@/components/bento/BentoMap";
|
||||||
|
import BentoMarquee from "@/components/bento/BentoMarquee";
|
||||||
|
import BentoLineChart from "@/components/bento/BentoLineChart/BentoLineChart";
|
||||||
|
import BentoPhoneAnimation, { type PhoneApp, type PhoneApps8 } from "@/components/bento/BentoPhoneAnimation";
|
||||||
|
import BentoChatAnimation, { type ChatExchange } from "@/components/bento/BentoChatAnimation";
|
||||||
|
import Bento3DCardGrid from "@/components/bento/Bento3DCardGrid";
|
||||||
|
import BentoRevealIcon from "@/components/bento/BentoRevealIcon";
|
||||||
|
import BentoTimeline, { type TimelineItem } from "@/components/bento/BentoTimeline";
|
||||||
|
import BentoMediaStack, { type MediaStackItem } from "@/components/bento/BentoMediaStack";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
|
||||||
|
export type { PhoneApp, PhoneApps8, ChatExchange, TimelineItem, MediaStackItem };
|
||||||
|
import type { ButtonConfig, CardAnimationTypeWith3D, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
|
||||||
|
|
||||||
|
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||||
|
|
||||||
|
type BentoAnimationType = Exclude<CardAnimationTypeWith3D, "depth-3d" | "scale-rotate">;
|
||||||
|
|
||||||
|
export type BentoInfoItem = {
|
||||||
|
icon: LucideIcon;
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Bento3DItem = {
|
||||||
|
icon: LucideIcon;
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
detail: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type BaseFeatureCard = {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
button?: ButtonConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FeatureCard = BaseFeatureCard & (
|
||||||
|
| {
|
||||||
|
bentoComponent: "icon-info-cards";
|
||||||
|
items: BentoInfoItem[];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
bentoComponent: "3d-stack-cards";
|
||||||
|
items: [Bento3DItem, Bento3DItem, Bento3DItem];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
bentoComponent: "3d-task-list";
|
||||||
|
title: string;
|
||||||
|
items: TaskItem[];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
bentoComponent: "orbiting-icons";
|
||||||
|
centerIcon: LucideIcon;
|
||||||
|
items: OrbitingItem[];
|
||||||
|
}
|
||||||
|
| ({
|
||||||
|
bentoComponent: "marquee";
|
||||||
|
centerIcon: LucideIcon;
|
||||||
|
} & (
|
||||||
|
| { variant: "text"; texts: string[] }
|
||||||
|
| { variant: "icon"; icons: LucideIcon[] }
|
||||||
|
))
|
||||||
|
| {
|
||||||
|
bentoComponent: "globe" | "animated-bar-chart" | "map" | "line-chart";
|
||||||
|
items?: never;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
bentoComponent: "3d-card-grid";
|
||||||
|
items: [{ name: string; icon: LucideIcon }, { name: string; icon: LucideIcon }, { name: string; icon: LucideIcon }, { name: string; icon: LucideIcon }];
|
||||||
|
centerIcon: LucideIcon;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
bentoComponent: "phone";
|
||||||
|
statusIcon: LucideIcon;
|
||||||
|
alertIcon: LucideIcon;
|
||||||
|
alertTitle: string;
|
||||||
|
alertMessage: string;
|
||||||
|
apps: PhoneApps8;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
bentoComponent: "chat";
|
||||||
|
aiIcon: LucideIcon;
|
||||||
|
userIcon: LucideIcon;
|
||||||
|
exchanges: ChatExchange[];
|
||||||
|
placeholder: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
bentoComponent: "reveal-icon";
|
||||||
|
icon: LucideIcon;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
bentoComponent: "timeline";
|
||||||
|
heading: string;
|
||||||
|
subheading: string;
|
||||||
|
items: [TimelineItem, TimelineItem, TimelineItem];
|
||||||
|
completedLabel: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
bentoComponent: "media-stack";
|
||||||
|
items: [MediaStackItem, MediaStackItem, MediaStackItem];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
interface FeatureBentoProps {
|
interface FeatureBentoProps {
|
||||||
features?: any[];
|
features: FeatureCard[];
|
||||||
title?: string;
|
carouselMode?: "auto" | "buttons";
|
||||||
description?: string;
|
animationType: BentoAnimationType;
|
||||||
animationType?: string;
|
title: string;
|
||||||
textboxLayout?: string;
|
titleSegments?: TitleSegment[];
|
||||||
useInvertedBackground?: boolean;
|
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({
|
const FeatureBento = ({
|
||||||
features = [],
|
features,
|
||||||
title = "Features", description = "Our features", animationType = "slide-up", textboxLayout = "default", useInvertedBackground = false,
|
carouselMode = "buttons",
|
||||||
}: FeatureBentoProps) {
|
animationType,
|
||||||
const items = features.map((feature) => ({
|
title,
|
||||||
id: feature.id,
|
titleSegments,
|
||||||
label: feature.title,
|
description,
|
||||||
detail: feature.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 (
|
return (
|
||||||
<div className="feature-bento">
|
<CardStack
|
||||||
<CardStack items={items} />
|
mode={carouselMode}
|
||||||
</div>
|
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;
|
||||||
|
|||||||
@@ -1,28 +1,261 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { CardStack } from "@/components/cardStack/CardStack";
|
|
||||||
|
import { memo } from "react";
|
||||||
|
import CardStack from "@/components/cardStack/CardStack";
|
||||||
|
import MediaContent from "@/components/shared/MediaContent";
|
||||||
|
import Tag from "@/components/shared/Tag";
|
||||||
|
import Button from "@/components/button/Button";
|
||||||
|
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
||||||
|
import { getButtonProps } from "@/lib/buttonUtils";
|
||||||
|
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import type { ButtonConfig, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
|
||||||
|
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||||
|
|
||||||
|
type FeatureCard = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
tag: string;
|
||||||
|
imageSrc?: string;
|
||||||
|
videoSrc?: string;
|
||||||
|
imageAlt?: string;
|
||||||
|
videoAriaLabel?: string;
|
||||||
|
buttons?: ButtonConfig[];
|
||||||
|
onCardClick?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
interface FeatureCardMediaProps {
|
interface FeatureCardMediaProps {
|
||||||
features?: any[];
|
features: FeatureCard[];
|
||||||
title?: string;
|
carouselMode?: "auto" | "buttons";
|
||||||
description?: string;
|
uniformGridCustomHeightClasses?: string;
|
||||||
animationType?: string;
|
animationType: CardAnimationType;
|
||||||
textboxLayout?: string;
|
title: string;
|
||||||
useInvertedBackground?: boolean;
|
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({
|
interface FeatureCardItemProps {
|
||||||
features = [],
|
feature: FeatureCard;
|
||||||
title = "Features", description = "Our features", animationType = "slide-up", textboxLayout = "default", useInvertedBackground = false,
|
shouldUseLightText: boolean;
|
||||||
}: FeatureCardMediaProps) {
|
useInvertedBackground: InvertedBackground;
|
||||||
const items = features.map((feature) => ({
|
itemClassName?: string;
|
||||||
id: feature.id,
|
mediaWrapperClassName?: string;
|
||||||
label: feature.title,
|
mediaClassName?: string;
|
||||||
detail: feature.description,
|
tagClassName?: string;
|
||||||
}));
|
contentClassName?: string;
|
||||||
|
cardTitleClassName?: string;
|
||||||
return (
|
cardDescriptionClassName?: string;
|
||||||
<div className="feature-card-media">
|
cardButtonContainerClassName?: string;
|
||||||
<CardStack items={items} />
|
cardButtonClassName?: string;
|
||||||
</div>
|
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;
|
||||||
|
|||||||
@@ -1,29 +1,233 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
|
|
||||||
import TimelinePhoneView from "@/components/cardStack/layouts/timelines/TimelinePhoneView";
|
import TimelinePhoneView from "@/components/cardStack/layouts/timelines/TimelinePhoneView";
|
||||||
|
import Button from "@/components/button/Button";
|
||||||
|
import { cls } from "@/lib/utils";
|
||||||
|
import { getButtonProps } from "@/lib/buttonUtils";
|
||||||
|
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import type { ButtonConfig, ButtonAnimationType, TitleSegment, CardAnimationType } from "@/components/cardStack/types";
|
||||||
|
import type { TimelinePhoneViewItem } from "@/components/cardStack/hooks/usePhoneAnimations";
|
||||||
|
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||||
|
|
||||||
|
type FeaturePhone = {
|
||||||
|
imageAlt?: string;
|
||||||
|
videoAriaLabel?: string;
|
||||||
|
} & (
|
||||||
|
| { imageSrc: string; videoSrc?: never }
|
||||||
|
| { videoSrc: string; imageSrc?: never }
|
||||||
|
);
|
||||||
|
|
||||||
|
type FeatureCard = {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
buttons?: ButtonConfig[];
|
||||||
|
phoneOne: FeaturePhone;
|
||||||
|
phoneTwo: FeaturePhone;
|
||||||
|
};
|
||||||
|
|
||||||
interface FeatureCardNineProps {
|
interface FeatureCardNineProps {
|
||||||
features?: any[];
|
features: FeatureCard[];
|
||||||
title?: string;
|
showStepNumbers: boolean;
|
||||||
description?: string;
|
title: string;
|
||||||
animationType?: string;
|
titleSegments?: TitleSegment[];
|
||||||
useInvertedBackground?: boolean;
|
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({
|
interface FeatureContentProps {
|
||||||
features = [],
|
feature: FeatureCard;
|
||||||
title = "Features", description = "Our features", animationType = "slide-up", useInvertedBackground = false,
|
showStepNumbers: boolean;
|
||||||
}: FeatureCardNineProps) {
|
useInvertedBackground: InvertedBackground;
|
||||||
const items = features.map((feature) => ({
|
featureContentClassName: string;
|
||||||
id: feature.id,
|
stepNumberClassName: string;
|
||||||
label: feature.title,
|
featureTitleClassName: string;
|
||||||
detail: feature.description,
|
featureDescriptionClassName: string;
|
||||||
}));
|
cardButtonClassName: string;
|
||||||
|
cardButtonTextClassName: string;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
const FeatureContent = ({
|
||||||
<div className="feature-card-nine">
|
feature,
|
||||||
<h2>{title}</h2>
|
showStepNumbers,
|
||||||
<p>{description}</p>
|
useInvertedBackground,
|
||||||
<TimelinePhoneView items={items} />
|
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>
|
</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;
|
||||||
|
|||||||
@@ -1,28 +1,196 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { CardStack } from "@/components/cardStack/CardStack";
|
|
||||||
|
import CardStack from "@/components/cardStack/CardStack";
|
||||||
|
import MediaContent from "@/components/shared/MediaContent";
|
||||||
|
import Button from "@/components/button/Button";
|
||||||
|
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
||||||
|
import { getButtonProps } from "@/lib/buttonUtils";
|
||||||
|
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import type { ButtonConfig, GridVariant, CardAnimationTypeWith3D, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
|
||||||
|
|
||||||
|
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||||
|
|
||||||
|
type FeatureCard = {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
button?: ButtonConfig;
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
imageSrc: string;
|
||||||
|
imageAlt?: string;
|
||||||
|
videoSrc?: never;
|
||||||
|
videoAriaLabel?: never;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
videoSrc: string;
|
||||||
|
videoAriaLabel?: string;
|
||||||
|
imageSrc?: never;
|
||||||
|
imageAlt?: never;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
interface FeatureCardOneProps {
|
interface FeatureCardOneProps {
|
||||||
features?: any[];
|
features: FeatureCard[];
|
||||||
title?: string;
|
carouselMode?: "auto" | "buttons";
|
||||||
description?: string;
|
gridVariant: GridVariant;
|
||||||
animationType?: string;
|
uniformGridCustomHeightClasses?: string;
|
||||||
textboxLayout?: string;
|
animationType: CardAnimationTypeWith3D;
|
||||||
useInvertedBackground?: boolean;
|
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({
|
const FeatureCardOne = ({
|
||||||
features = [],
|
features,
|
||||||
title = "Features", description = "Our features", animationType = "slide-up", textboxLayout = "default", useInvertedBackground = false,
|
carouselMode = "buttons",
|
||||||
}: FeatureCardOneProps) {
|
gridVariant,
|
||||||
const items = features.map((feature) => ({
|
uniformGridCustomHeightClasses,
|
||||||
id: feature.id,
|
animationType,
|
||||||
label: feature.title,
|
title,
|
||||||
detail: feature.description,
|
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 (
|
return (
|
||||||
<div className="feature-card-one">
|
<CardStack
|
||||||
<CardStack items={items} />
|
mode={carouselMode}
|
||||||
</div>
|
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;
|
||||||
|
|||||||
@@ -1,31 +1,179 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import CardList from "@/components/cardStack/CardList";
|
import CardList from "@/components/cardStack/CardList";
|
||||||
|
import MediaContent from "@/components/shared/MediaContent";
|
||||||
|
import Button from "@/components/button/Button";
|
||||||
|
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
||||||
|
import { getButtonProps } from "@/lib/buttonUtils";
|
||||||
|
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import type { ButtonConfig, ButtonAnimationType, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
|
||||||
|
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||||
|
|
||||||
|
type FeatureCard = {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
buttons?: ButtonConfig[];
|
||||||
|
imageSrc?: string;
|
||||||
|
videoSrc?: string;
|
||||||
|
imageAlt?: string;
|
||||||
|
videoAriaLabel?: string;
|
||||||
|
};
|
||||||
|
|
||||||
interface FeatureCardSevenProps {
|
interface FeatureCardSevenProps {
|
||||||
features?: any[];
|
features: FeatureCard[];
|
||||||
title?: string;
|
animationType: CardAnimationType;
|
||||||
description?: string;
|
title: string;
|
||||||
animationType?: string;
|
titleSegments?: TitleSegment[];
|
||||||
useInvertedBackground?: boolean;
|
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({
|
const FeatureCardSeven = ({
|
||||||
features = [],
|
features,
|
||||||
title = "Features", description = "Our features", animationType = "slide-up", useInvertedBackground = false,
|
animationType,
|
||||||
}: FeatureCardSevenProps) {
|
title,
|
||||||
const items = features.map((feature) => ({
|
titleSegments,
|
||||||
id: feature.id,
|
description,
|
||||||
label: feature.title,
|
tag,
|
||||||
detail: feature.description,
|
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 (
|
return (
|
||||||
<div className="feature-card-seven">
|
<CardList
|
||||||
<h2>{title}</h2>
|
title={title}
|
||||||
<p>{description}</p>
|
titleSegments={titleSegments}
|
||||||
<CardList items={items} />
|
description={description}
|
||||||
</div>
|
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;
|
||||||
@@ -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 { 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 {
|
interface FeatureCardSixteenProps {
|
||||||
features?: any[];
|
negativeCard: ComparisonItem;
|
||||||
title?: string;
|
positiveCard: ComparisonItem;
|
||||||
description?: string;
|
animationType: CardAnimationTypeWith3D;
|
||||||
animationType?: string;
|
title: string;
|
||||||
useInvertedBackground?: boolean;
|
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({
|
const FeatureCardSixteen = ({
|
||||||
features = [],
|
negativeCard,
|
||||||
title = "Features", description = "Our features", animationType = "slide-up", useInvertedBackground = false,
|
positiveCard,
|
||||||
}: FeatureCardSixteenProps) {
|
animationType,
|
||||||
const state = useCardAnimation({
|
title,
|
||||||
rotationX: 0,
|
titleSegments,
|
||||||
rotationY: 0,
|
description,
|
||||||
rotationZ: 0,
|
textboxLayout,
|
||||||
perspective: 1000,
|
useInvertedBackground,
|
||||||
duration: 0.3,
|
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 (
|
const cards = [
|
||||||
<div className="feature-card-sixteen">
|
{ ...negativeCard, variant: "negative" as const },
|
||||||
<h2>{title}</h2>
|
{ ...positiveCard, variant: "positive" as const },
|
||||||
<p>{description}</p>
|
];
|
||||||
<div className="features-container">
|
|
||||||
{features.map((feature) => (
|
return (
|
||||||
<div key={feature.id} className="feature-item">
|
<section
|
||||||
<h3>{feature.title}</h3>
|
ref={containerRef}
|
||||||
<p>{feature.description}</p>
|
aria-label={ariaLabel}
|
||||||
</div>
|
className={cls("relative py-20 w-full", useInvertedBackground && "bg-foreground", className)}
|
||||||
))}
|
>
|
||||||
</div>
|
<div className={cls("w-content-width mx-auto flex flex-col gap-8", containerClassName)}>
|
||||||
</div>
|
<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;
|
||||||
@@ -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 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 {
|
interface FeatureCardTenProps {
|
||||||
features?: any[];
|
features: FeatureCard[];
|
||||||
title?: string;
|
title: string;
|
||||||
description?: string;
|
titleSegments?: TitleSegment[];
|
||||||
animationType?: string;
|
description: string;
|
||||||
useInvertedBackground?: boolean;
|
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({
|
interface FeatureMediaProps {
|
||||||
features = [],
|
media: FeatureMedia;
|
||||||
title = "Features", description = "Our features", animationType = "slide-up", useInvertedBackground = false,
|
title: string;
|
||||||
}: FeatureCardTenProps) {
|
mediaCardClassName: string;
|
||||||
const items = features.map((feature) => ({
|
}
|
||||||
id: feature.id,
|
|
||||||
reverse: false,
|
|
||||||
media: <div>{feature.title}</div>,
|
|
||||||
content: <div>{feature.description}</div>,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
const FeatureMedia = ({
|
||||||
<div className="feature-card-ten">
|
media,
|
||||||
<h2>{title}</h2>
|
title,
|
||||||
<p>{description}</p>
|
mediaCardClassName,
|
||||||
<TimelineProcessFlow items={items} />
|
}: 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>
|
</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);
|
||||||
@@ -1,31 +1,182 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import { Fragment } from "react";
|
||||||
import CardList from "@/components/cardStack/CardList";
|
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 {
|
interface FeatureCardTwelveProps {
|
||||||
features?: any[];
|
features: FeatureCard[];
|
||||||
title?: string;
|
animationType: CardAnimationType;
|
||||||
description?: string;
|
title: string;
|
||||||
animationType?: string;
|
titleSegments?: TitleSegment[];
|
||||||
useInvertedBackground?: boolean;
|
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({
|
const FeatureCardTwelve = ({
|
||||||
features = [],
|
features,
|
||||||
title = "Features", description = "Our features", animationType = "slide-up", useInvertedBackground = false,
|
animationType,
|
||||||
}: FeatureCardTwelveProps) {
|
title,
|
||||||
const items = features.map((feature) => ({
|
titleSegments,
|
||||||
id: feature.id,
|
description,
|
||||||
label: feature.title,
|
tag,
|
||||||
detail: feature.description,
|
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 (
|
return (
|
||||||
<div className="feature-card-twelve">
|
<CardList
|
||||||
<h2>{title}</h2>
|
title={title}
|
||||||
<p>{description}</p>
|
titleSegments={titleSegments}
|
||||||
<CardList items={items} />
|
description={description}
|
||||||
</div>
|
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;
|
||||||
|
|||||||
@@ -1,28 +1,178 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { CardStack } from "@/components/cardStack/CardStack";
|
|
||||||
|
import CardStack from "@/components/cardStack/CardStack";
|
||||||
|
import MediaContent from "@/components/shared/MediaContent";
|
||||||
|
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
||||||
|
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import type { CardAnimationTypeWith3D, TitleSegment, ButtonConfig, ButtonAnimationType } from "@/components/cardStack/types";
|
||||||
|
|
||||||
|
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||||
|
|
||||||
|
interface MediaItem {
|
||||||
|
imageSrc?: string;
|
||||||
|
videoSrc?: string;
|
||||||
|
imageAlt?: string;
|
||||||
|
videoAriaLabel?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeatureCard = {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
icon: LucideIcon;
|
||||||
|
mediaItems: [MediaItem, MediaItem];
|
||||||
|
};
|
||||||
|
|
||||||
interface FeatureCardTwentyFiveProps {
|
interface FeatureCardTwentyFiveProps {
|
||||||
features?: any[];
|
features: FeatureCard[];
|
||||||
title?: string;
|
carouselMode?: "auto" | "buttons";
|
||||||
description?: string;
|
uniformGridCustomHeightClasses?: string;
|
||||||
animationType?: string;
|
animationType: CardAnimationTypeWith3D;
|
||||||
textboxLayout?: string;
|
title: string;
|
||||||
useInvertedBackground?: boolean;
|
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({
|
const FeatureCardTwentyFive = ({
|
||||||
features = [],
|
features,
|
||||||
title = "Features", description = "Our features", animationType = "slide-up", textboxLayout = "default", useInvertedBackground = false,
|
carouselMode = "buttons",
|
||||||
}: FeatureCardTwentyFiveProps) {
|
uniformGridCustomHeightClasses,
|
||||||
const items = features.map((feature) => ({
|
animationType,
|
||||||
id: feature.id,
|
title,
|
||||||
label: feature.title,
|
titleSegments,
|
||||||
detail: feature.description,
|
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 (
|
return (
|
||||||
<div className="feature-card-twenty-five">
|
<CardStack
|
||||||
<CardStack items={items} />
|
mode={carouselMode}
|
||||||
</div>
|
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;
|
||||||
|
|||||||
@@ -1,59 +1,199 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import CardList from "@/components/cardStack/CardList";
|
import CardList from "@/components/cardStack/CardList";
|
||||||
|
import Tag from "@/components/shared/Tag";
|
||||||
|
import MediaContent from "@/components/shared/MediaContent";
|
||||||
|
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
||||||
|
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import type { ButtonConfig, ButtonAnimationType, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
|
||||||
|
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||||
|
|
||||||
|
type MediaProps =
|
||||||
|
| {
|
||||||
|
imageSrc: string;
|
||||||
|
imageAlt?: string;
|
||||||
|
videoSrc?: never;
|
||||||
|
videoAriaLabel?: never;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
videoSrc: string;
|
||||||
|
videoAriaLabel?: string;
|
||||||
|
imageSrc?: never;
|
||||||
|
imageAlt?: never;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FeatureItem = MediaProps & {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
author: string;
|
||||||
|
description: string;
|
||||||
|
tags: string[];
|
||||||
|
onFeatureClick?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
interface FeatureCardTwentyFourProps {
|
interface FeatureCardTwentyFourProps {
|
||||||
features?: any[];
|
features: FeatureItem[];
|
||||||
title?: string;
|
animationType: CardAnimationType;
|
||||||
description?: string;
|
title: string;
|
||||||
animationType?: string;
|
titleSegments?: TitleSegment[];
|
||||||
useInvertedBackground?: boolean;
|
description: string;
|
||||||
textboxLayout?: string;
|
tag?: string;
|
||||||
tag?: string;
|
tagIcon?: LucideIcon;
|
||||||
tagIcon?: any;
|
tagAnimation?: ButtonAnimationType;
|
||||||
tagAnimation?: string;
|
buttons?: ButtonConfig[];
|
||||||
buttons?: any[];
|
buttonAnimation?: ButtonAnimationType;
|
||||||
buttonAnimation?: string;
|
textboxLayout: TextboxLayout;
|
||||||
titleSegments?: any[];
|
useInvertedBackground: InvertedBackground;
|
||||||
ariaLabel?: string;
|
ariaLabel?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
containerClassName?: string;
|
containerClassName?: string;
|
||||||
cardClassName?: string;
|
cardClassName?: string;
|
||||||
textBoxTitleClassName?: string;
|
textBoxTitleClassName?: string;
|
||||||
textBoxDescriptionClassName?: string;
|
textBoxDescriptionClassName?: string;
|
||||||
textBoxClassName?: string;
|
textBoxClassName?: string;
|
||||||
textBoxTagClassName?: string;
|
textBoxTagClassName?: string;
|
||||||
textBoxButtonContainerClassName?: string;
|
textBoxButtonContainerClassName?: string;
|
||||||
textBoxButtonClassName?: string;
|
textBoxButtonClassName?: string;
|
||||||
textBoxButtonTextClassName?: string;
|
textBoxButtonTextClassName?: string;
|
||||||
titleImageWrapperClassName?: string;
|
titleImageWrapperClassName?: string;
|
||||||
titleImageClassName?: string;
|
titleImageClassName?: string;
|
||||||
cardContentClassName?: string;
|
cardContentClassName?: string;
|
||||||
cardTitleClassName?: string;
|
cardTitleClassName?: string;
|
||||||
authorClassName?: string;
|
authorClassName?: string;
|
||||||
cardDescriptionClassName?: string;
|
cardDescriptionClassName?: string;
|
||||||
tagsContainerClassName?: string;
|
tagsContainerClassName?: string;
|
||||||
tagClassName?: string;
|
tagClassName?: string;
|
||||||
mediaWrapperClassName?: string;
|
mediaWrapperClassName?: string;
|
||||||
mediaClassName?: string;
|
mediaClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FeatureCardTwentyFour({
|
const FeatureCardTwentyFour = ({
|
||||||
features = [],
|
features,
|
||||||
title = "Features", description = "Our features", animationType = "slide-up", useInvertedBackground = false,
|
animationType,
|
||||||
textboxLayout = "default"}: FeatureCardTwentyFourProps) {
|
title,
|
||||||
const items = features.map((feature) => ({
|
titleSegments,
|
||||||
id: feature.id,
|
description,
|
||||||
label: feature.title,
|
tag,
|
||||||
detail: feature.description,
|
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 (
|
return (
|
||||||
<div className="feature-card-twenty-four">
|
<CardList
|
||||||
<h2>{title}</h2>
|
title={title}
|
||||||
<p>{description}</p>
|
titleSegments={titleSegments}
|
||||||
<CardList items={items} />
|
description={description}
|
||||||
</div>
|
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;
|
||||||
|
|||||||
@@ -1,28 +1,219 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { CardStack } from "@/components/cardStack/CardStack";
|
|
||||||
|
|
||||||
interface FeatureCardTwentySevenProps {
|
import { useState } from "react";
|
||||||
features?: any[];
|
import { Plus } from "lucide-react";
|
||||||
title?: string;
|
import CardStack from "@/components/cardStack/CardStack";
|
||||||
description?: string;
|
import MediaContent from "@/components/shared/MediaContent";
|
||||||
animationType?: string;
|
import { cls } from "@/lib/utils";
|
||||||
textboxLayout?: string;
|
import type { LucideIcon } from "lucide-react";
|
||||||
useInvertedBackground?: boolean;
|
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({
|
const FeatureCardTwentySevenItem = ({
|
||||||
features = [],
|
title,
|
||||||
title = "Features", description = "Our features", animationType = "slide-up", textboxLayout = "default", useInvertedBackground = false,
|
description,
|
||||||
}: FeatureCardTwentySevenProps) {
|
imageSrc,
|
||||||
const items = features.map((feature) => ({
|
videoSrc,
|
||||||
id: feature.id,
|
imageAlt = "",
|
||||||
label: feature.title,
|
className = "",
|
||||||
detail: feature.description,
|
titleClassName = "",
|
||||||
}));
|
descriptionClassName = "",
|
||||||
|
}: FeatureCardTwentySevenItemProps) => {
|
||||||
|
const [isFlipped, setIsFlipped] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="feature-card-twenty-seven">
|
<div
|
||||||
<CardStack items={items} />
|
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>
|
</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;
|
||||||
|
|||||||
@@ -1,28 +1,241 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { CardStack } from "@/components/cardStack/CardStack";
|
|
||||||
|
import { memo } from "react";
|
||||||
|
import { ArrowRight } from "lucide-react";
|
||||||
|
import CardStack from "@/components/cardStack/CardStack";
|
||||||
|
import MediaContent from "@/components/shared/MediaContent";
|
||||||
|
import Tag from "@/components/shared/Tag";
|
||||||
|
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
||||||
|
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import type { ButtonConfig, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
|
||||||
|
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||||
|
|
||||||
|
type FeatureItem = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
tags: string[];
|
||||||
|
imageSrc?: string;
|
||||||
|
videoSrc?: string;
|
||||||
|
imageAlt?: string;
|
||||||
|
videoAriaLabel?: string;
|
||||||
|
onFeatureClick?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
interface FeatureCardTwentyThreeProps {
|
interface FeatureCardTwentyThreeProps {
|
||||||
features?: any[];
|
features: FeatureItem[];
|
||||||
title?: string;
|
carouselMode?: "auto" | "buttons";
|
||||||
description?: string;
|
uniformGridCustomHeightClasses?: string;
|
||||||
animationType?: string;
|
animationType: CardAnimationType;
|
||||||
textboxLayout?: string;
|
title: string;
|
||||||
useInvertedBackground?: boolean;
|
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({
|
interface FeatureCardItemProps {
|
||||||
features = [],
|
feature: FeatureItem;
|
||||||
title = "Features", description = "Our features", animationType = "slide-up", textboxLayout = "default", useInvertedBackground = false,
|
shouldUseLightText: boolean;
|
||||||
}: FeatureCardTwentyThreeProps) {
|
useInvertedBackground: InvertedBackground;
|
||||||
const items = features.map((feature) => ({
|
itemClassName?: string;
|
||||||
id: feature.id,
|
mediaWrapperClassName?: string;
|
||||||
label: feature.title,
|
mediaClassName?: string;
|
||||||
detail: feature.description,
|
cardClassName?: string;
|
||||||
}));
|
cardTitleClassName?: string;
|
||||||
|
tagsContainerClassName?: string;
|
||||||
return (
|
tagClassName?: string;
|
||||||
<div className="feature-card-twenty-three">
|
arrowClassName?: string;
|
||||||
<CardStack items={items} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|||||||
@@ -1,28 +1,155 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { CardStack } from "@/components/cardStack/CardStack";
|
|
||||||
|
import CardStack from "@/components/cardStack/CardStack";
|
||||||
|
import FeatureBorderGlowItem from "./FeatureBorderGlowItem";
|
||||||
|
import { shouldUseInvertedText } from "@/lib/utils";
|
||||||
|
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import type {
|
||||||
|
ButtonConfig,
|
||||||
|
CardAnimationType,
|
||||||
|
TitleSegment,
|
||||||
|
ButtonAnimationType,
|
||||||
|
} from "@/components/cardStack/types";
|
||||||
|
import type {
|
||||||
|
TextboxLayout,
|
||||||
|
InvertedBackground,
|
||||||
|
} from "@/providers/themeProvider/config/constants";
|
||||||
|
|
||||||
|
interface FeatureCard {
|
||||||
|
icon: LucideIcon;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface FeatureBorderGlowProps {
|
interface FeatureBorderGlowProps {
|
||||||
features?: any[];
|
features: FeatureCard[];
|
||||||
title?: string;
|
carouselMode?: "auto" | "buttons";
|
||||||
description?: string;
|
uniformGridCustomHeightClasses?: string;
|
||||||
animationType?: string;
|
animationType: CardAnimationType;
|
||||||
textboxLayout?: string;
|
title: string;
|
||||||
useInvertedBackground?: boolean;
|
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({
|
const FeatureBorderGlow = ({
|
||||||
features = [],
|
features,
|
||||||
title = "Features", description = "Our features", animationType = "slide-up", textboxLayout = "default", useInvertedBackground = false,
|
carouselMode = "buttons",
|
||||||
}: FeatureBorderGlowProps) {
|
uniformGridCustomHeightClasses = "min-h-75 2xl:min-h-85",
|
||||||
const items = features.map((feature) => ({
|
animationType,
|
||||||
id: feature.id,
|
title,
|
||||||
label: feature.title,
|
titleSegments,
|
||||||
detail: feature.description,
|
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 (
|
return (
|
||||||
<div className="feature-border-glow">
|
<CardStack
|
||||||
<CardStack items={items} />
|
mode={carouselMode}
|
||||||
</div>
|
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;
|
||||||
|
|||||||
@@ -1,28 +1,182 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { CardStack } from "@/components/cardStack/CardStack";
|
|
||||||
|
import "./FeatureCardThree.css";
|
||||||
|
import { useRef, useCallback, useState } from "react";
|
||||||
|
import CardStack from "@/components/cardStack/CardStack";
|
||||||
|
import FeatureCardThreeItem from "./FeatureCardThreeItem";
|
||||||
|
import { useDynamicDimensions } from "./useDynamicDimensions";
|
||||||
|
import { useClickOutside } from "@/hooks/useClickOutside";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import type { ButtonConfig, GridVariant, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
|
||||||
|
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||||
|
|
||||||
|
type FeatureCard = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
imageSrc: string;
|
||||||
|
imageAlt?: string;
|
||||||
|
};
|
||||||
|
|
||||||
interface FeatureCardThreeProps {
|
interface FeatureCardThreeProps {
|
||||||
features?: any[];
|
features: FeatureCard[];
|
||||||
title?: string;
|
carouselMode?: "auto" | "buttons";
|
||||||
description?: string;
|
gridVariant: GridVariant;
|
||||||
animationType?: string;
|
uniformGridCustomHeightClasses?: string;
|
||||||
textboxLayout?: string;
|
animationType: CardAnimationType;
|
||||||
useInvertedBackground?: boolean;
|
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({
|
const FeatureCardThree = ({
|
||||||
features = [],
|
features,
|
||||||
title = "Features", description = "Our features", animationType = "slide-up", textboxLayout = "default", useInvertedBackground = false,
|
carouselMode = "buttons",
|
||||||
}: FeatureCardThreeProps) {
|
gridVariant,
|
||||||
const items = features.map((feature) => ({
|
uniformGridCustomHeightClasses,
|
||||||
id: feature.id,
|
animationType,
|
||||||
label: feature.title,
|
title,
|
||||||
detail: feature.description,
|
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 (
|
return (
|
||||||
<div className="feature-card-three">
|
<div ref={containerRef}>
|
||||||
<CardStack items={items} />
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
FeatureCardThree.displayName = "FeatureCardThree";
|
||||||
|
|
||||||
|
export default FeatureCardThree;
|
||||||
|
|||||||
@@ -1,28 +1,165 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { CardStack } from "@/components/cardStack/CardStack";
|
|
||||||
|
import CardStack from "@/components/cardStack/CardStack";
|
||||||
|
import FeatureHoverPatternItem from "./FeatureHoverPatternItem";
|
||||||
|
import { shouldUseInvertedText } from "@/lib/utils";
|
||||||
|
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import type {
|
||||||
|
ButtonConfig,
|
||||||
|
CardAnimationType,
|
||||||
|
TitleSegment,
|
||||||
|
ButtonAnimationType,
|
||||||
|
} from "@/components/cardStack/types";
|
||||||
|
import type {
|
||||||
|
TextboxLayout,
|
||||||
|
InvertedBackground,
|
||||||
|
} from "@/providers/themeProvider/config/constants";
|
||||||
|
|
||||||
|
interface FeatureCard {
|
||||||
|
icon: LucideIcon;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
button?: ButtonConfig;
|
||||||
|
}
|
||||||
|
|
||||||
interface FeatureHoverPatternProps {
|
interface FeatureHoverPatternProps {
|
||||||
features?: any[];
|
features: FeatureCard[];
|
||||||
title?: string;
|
carouselMode?: "auto" | "buttons";
|
||||||
description?: string;
|
uniformGridCustomHeightClasses?: string;
|
||||||
animationType?: string;
|
animationType: CardAnimationType;
|
||||||
textboxLayout?: string;
|
title: string;
|
||||||
useInvertedBackground?: boolean;
|
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({
|
const FeatureHoverPattern = ({
|
||||||
features = [],
|
features,
|
||||||
title = "Features", description = "Our features", animationType = "slide-up", textboxLayout = "default", useInvertedBackground = false,
|
carouselMode = "buttons",
|
||||||
}: FeatureHoverPatternProps) {
|
uniformGridCustomHeightClasses = "min-h-85 2xl:min-h-95",
|
||||||
const items = features.map((feature) => ({
|
animationType,
|
||||||
id: feature.id,
|
title,
|
||||||
label: feature.title,
|
titleSegments,
|
||||||
detail: feature.description,
|
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 (
|
return (
|
||||||
<div className="feature-hover-pattern">
|
<CardStack
|
||||||
<CardStack items={items} />
|
mode={carouselMode}
|
||||||
</div>
|
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;
|
||||||
|
|||||||
@@ -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 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 {
|
interface HeroBillboardCarouselProps {
|
||||||
title?: string;
|
title: string;
|
||||||
description?: string;
|
description: string;
|
||||||
textboxLayout?: string;
|
background: HeroBillboardCarouselBackgroundProps;
|
||||||
animationType?: string;
|
tag?: string;
|
||||||
className?: string;
|
tagIcon?: LucideIcon;
|
||||||
carouselClassName?: string;
|
tagAnimation?: ButtonAnimationType;
|
||||||
containerClassName?: string;
|
buttons?: ButtonConfig[];
|
||||||
itemClassName?: string;
|
buttonAnimation?: ButtonAnimationType;
|
||||||
ariaLabel?: string;
|
mediaItems: MediaItem[];
|
||||||
mediaItems?: Array<{ imageSrc?: string; videoSrc?: string; imageAlt?: string }>;
|
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({
|
const HeroBillboardCarousel = ({
|
||||||
title = "Hero", description = "Welcome", textboxLayout = "default", animationType = "slide-up", className = "", carouselClassName = "", containerClassName = "", itemClassName = "", ariaLabel = "Hero section", mediaItems = [],
|
title,
|
||||||
}: HeroBillboardCarouselProps) {
|
description,
|
||||||
const items = mediaItems.map((item) => ({
|
background,
|
||||||
imageSrc: item.imageSrc,
|
tag,
|
||||||
videoSrc: item.videoSrc,
|
tagIcon,
|
||||||
imageAlt: item.imageAlt,
|
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 (
|
return (
|
||||||
<div className={`hero-billboard-carousel ${className}`} aria-label={ariaLabel}>
|
<section
|
||||||
<h1>{title}</h1>
|
aria-label={ariaLabel}
|
||||||
<p>{description}</p>
|
className={cls(
|
||||||
<AutoCarousel items={items} />
|
"relative w-full py-hero-page-padding md:h-svh md:py-0",
|
||||||
</div>
|
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;
|
||||||
@@ -1,18 +1,132 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
|
|
||||||
|
import TextBox from "@/components/Textbox";
|
||||||
import Dashboard from "@/components/shared/Dashboard";
|
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 {
|
interface HeroBillboardDashboardProps {
|
||||||
title?: string;
|
title: string;
|
||||||
description?: 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({
|
const HeroBillboardDashboard = ({
|
||||||
title = "Dashboard", description = "Welcome"}: HeroBillboardDashboardProps) {
|
title,
|
||||||
|
description,
|
||||||
|
background,
|
||||||
|
tag,
|
||||||
|
tagIcon,
|
||||||
|
tagAnimation,
|
||||||
|
buttons,
|
||||||
|
buttonAnimation,
|
||||||
|
ariaLabel = "Hero section",
|
||||||
|
dashboard,
|
||||||
|
className = "",
|
||||||
|
containerClassName = "",
|
||||||
|
textBoxClassName = "",
|
||||||
|
titleClassName = "",
|
||||||
|
descriptionClassName = "",
|
||||||
|
tagClassName = "",
|
||||||
|
buttonContainerClassName = "",
|
||||||
|
buttonClassName = "",
|
||||||
|
buttonTextClassName = "",
|
||||||
|
dashboardClassName = "",
|
||||||
|
}: HeroBillboardDashboardProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="hero-billboard-dashboard">
|
<section
|
||||||
<h1>{title}</h1>
|
aria-label={ariaLabel}
|
||||||
<p>{description}</p>
|
className={cls("relative w-full py-hero-page-padding", className)}
|
||||||
<Dashboard data={[]} />
|
>
|
||||||
</div>
|
<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;
|
||||||
|
|||||||
@@ -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 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 {
|
interface HeroBillboardGalleryProps {
|
||||||
title?: string;
|
title: string;
|
||||||
description?: string;
|
description: string;
|
||||||
textboxLayout?: string;
|
background: HeroBillboardGalleryBackgroundProps;
|
||||||
animationType?: string;
|
tag?: string;
|
||||||
className?: string;
|
tagIcon?: LucideIcon;
|
||||||
carouselClassName?: string;
|
tagAnimation?: ButtonAnimationType;
|
||||||
containerClassName?: string;
|
buttons?: ButtonConfig[];
|
||||||
itemClassName?: string;
|
buttonAnimation?: ButtonAnimationType;
|
||||||
|
mediaItems: MediaItem[];
|
||||||
|
mediaAnimation: ButtonAnimationType;
|
||||||
ariaLabel?: string;
|
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({
|
const HeroBillboardGallery = ({
|
||||||
title = "Gallery", description = "Welcome", textboxLayout = "default", animationType = "slide-up", className = "", carouselClassName = "", containerClassName = "", itemClassName = "", ariaLabel = "Gallery section", mediaItems = [],
|
title,
|
||||||
}: HeroBillboardGalleryProps) {
|
description,
|
||||||
const items = mediaItems.map((item) => ({
|
background,
|
||||||
imageSrc: item.imageSrc,
|
tag,
|
||||||
videoSrc: item.videoSrc,
|
tagIcon,
|
||||||
imageAlt: item.imageAlt,
|
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 (
|
const renderCarouselItem = (item: MediaItem, index: number) => (
|
||||||
<div className={`hero-billboard-gallery ${className}`} aria-label={ariaLabel}>
|
<div
|
||||||
<h1>{title}</h1>
|
key={index}
|
||||||
<p>{description}</p>
|
className="w-full aspect-[4/5] overflow-hidden rounded-theme-capped card p-2 shadow-lg"
|
||||||
<AutoCarousel items={items} />
|
>
|
||||||
|
<MediaContent
|
||||||
|
imageSrc={item.imageSrc}
|
||||||
|
videoSrc={item.videoSrc}
|
||||||
|
imageAlt={item.imageAlt || ""}
|
||||||
|
videoAriaLabel={item.videoAriaLabel || "Gallery media"}
|
||||||
|
imageClassName="h-full object-cover"
|
||||||
|
/>
|
||||||
</div>
|
</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;
|
||||||
|
|||||||
@@ -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 { 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 {
|
interface MetricCardElevenProps {
|
||||||
metrics?: any[];
|
metrics: Metric[];
|
||||||
title?: string;
|
animationType: CardAnimationType;
|
||||||
description?: string;
|
title: string;
|
||||||
animationType?: string;
|
titleSegments?: TitleSegment[];
|
||||||
useInvertedBackground?: boolean;
|
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({
|
interface MetricTextCardProps {
|
||||||
metrics = [],
|
metric: Metric;
|
||||||
title = "Metrics", description = "Key metrics", animationType = "slide-up", useInvertedBackground = false,
|
shouldUseLightText: boolean;
|
||||||
}: MetricCardElevenProps) {
|
cardClassName?: string;
|
||||||
const state = useCardAnimation({
|
valueClassName?: string;
|
||||||
rotationX: 0,
|
cardTitleClassName?: string;
|
||||||
rotationY: 0,
|
cardDescriptionClassName?: string;
|
||||||
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 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;
|
||||||
@@ -1,28 +1,212 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { CardStack } from "@/components/cardStack/CardStack";
|
|
||||||
|
import { memo } from "react";
|
||||||
|
import CardStack from "@/components/cardStack/CardStack";
|
||||||
|
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
||||||
|
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import type { ButtonConfig, GridVariant, CardAnimationTypeWith3D, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
|
||||||
|
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||||
|
|
||||||
|
type MetricCardOneGridVariant = Extract<GridVariant, "uniform-all-items-equal" | "bento-grid" | "bento-grid-inverted">;
|
||||||
|
|
||||||
|
type Metric = {
|
||||||
|
id: string;
|
||||||
|
value: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
icon: LucideIcon;
|
||||||
|
};
|
||||||
|
|
||||||
interface MetricCardOneProps {
|
interface MetricCardOneProps {
|
||||||
metrics?: any[];
|
metrics: Metric[];
|
||||||
title?: string;
|
carouselMode?: "auto" | "buttons";
|
||||||
description?: string;
|
gridVariant: MetricCardOneGridVariant;
|
||||||
animationType?: string;
|
uniformGridCustomHeightClasses?: string;
|
||||||
textboxLayout?: string;
|
animationType: CardAnimationTypeWith3D;
|
||||||
useInvertedBackground?: boolean;
|
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({
|
interface MetricCardItemProps {
|
||||||
metrics = [],
|
metric: Metric;
|
||||||
title = "Metrics", description = "Key metrics", animationType = "slide-up", textboxLayout = "default", useInvertedBackground = false,
|
shouldUseLightText: boolean;
|
||||||
}: MetricCardOneProps) {
|
cardClassName?: string;
|
||||||
const items = metrics.map((metric) => ({
|
valueClassName?: string;
|
||||||
id: metric.id,
|
titleClassName?: string;
|
||||||
label: metric.label,
|
descriptionClassName?: string;
|
||||||
detail: metric.value,
|
iconContainerClassName?: string;
|
||||||
}));
|
iconClassName?: string;
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="metric-card-one">
|
|
||||||
<CardStack items={items} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|||||||
@@ -1,28 +1,194 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { CardStack } from "@/components/cardStack/CardStack";
|
|
||||||
|
import { memo } from "react";
|
||||||
|
import CardStack from "@/components/cardStack/CardStack";
|
||||||
|
import PricingFeatureList from "@/components/shared/PricingFeatureList";
|
||||||
|
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
||||||
|
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import type { ButtonConfig, CardAnimationTypeWith3D, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
|
||||||
|
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||||
|
|
||||||
|
type Metric = {
|
||||||
|
id: string;
|
||||||
|
value: string;
|
||||||
|
title: string;
|
||||||
|
items: string[];
|
||||||
|
};
|
||||||
|
|
||||||
interface MetricCardSevenProps {
|
interface MetricCardSevenProps {
|
||||||
metrics?: any[];
|
metrics: Metric[];
|
||||||
title?: string;
|
carouselMode?: "auto" | "buttons";
|
||||||
description?: string;
|
uniformGridCustomHeightClasses?: string;
|
||||||
animationType?: string;
|
animationType: CardAnimationTypeWith3D;
|
||||||
textboxLayout?: string;
|
title: string;
|
||||||
useInvertedBackground?: boolean;
|
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({
|
interface MetricCardItemProps {
|
||||||
metrics = [],
|
metric: Metric;
|
||||||
title = "Metrics", description = "Key metrics", animationType = "slide-up", textboxLayout = "default", useInvertedBackground = false,
|
shouldUseLightText: boolean;
|
||||||
}: MetricCardSevenProps) {
|
cardClassName?: string;
|
||||||
const items = metrics.map((metric) => ({
|
valueClassName?: string;
|
||||||
id: metric.id,
|
metricTitleClassName?: string;
|
||||||
label: metric.label,
|
featuresClassName?: string;
|
||||||
detail: metric.value,
|
featureItemClassName?: string;
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="metric-card-seven">
|
|
||||||
<CardStack items={items} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|||||||
@@ -1,28 +1,245 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { CardStack } from "@/components/cardStack/CardStack";
|
|
||||||
|
import { memo } from "react";
|
||||||
|
import CardStack from "@/components/cardStack/CardStack";
|
||||||
|
import Button from "@/components/button/Button";
|
||||||
|
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
||||||
|
import { getButtonProps } from "@/lib/buttonUtils";
|
||||||
|
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import type { ButtonConfig, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
|
||||||
|
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||||
|
import type { CTAButtonVariant } from "@/components/button/types";
|
||||||
|
|
||||||
|
type Metric = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
category: string;
|
||||||
|
value: string;
|
||||||
|
buttons?: ButtonConfig[];
|
||||||
|
};
|
||||||
|
|
||||||
interface MetricCardTenProps {
|
interface MetricCardTenProps {
|
||||||
metrics?: any[];
|
metrics: Metric[];
|
||||||
title?: string;
|
carouselMode?: "auto" | "buttons";
|
||||||
description?: string;
|
uniformGridCustomHeightClasses?: string;
|
||||||
animationType?: string;
|
animationType: CardAnimationType;
|
||||||
textboxLayout?: string;
|
title: string;
|
||||||
useInvertedBackground?: boolean;
|
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({
|
interface MetricCardItemProps {
|
||||||
metrics = [],
|
metric: Metric;
|
||||||
title = "Metrics", description = "Key metrics", animationType = "slide-up", textboxLayout = "default", useInvertedBackground = false,
|
shouldUseLightText: boolean;
|
||||||
}: MetricCardTenProps) {
|
defaultButtonVariant: CTAButtonVariant;
|
||||||
const items = metrics.map((metric) => ({
|
cardClassName?: string;
|
||||||
id: metric.id,
|
cardTitleClassName?: string;
|
||||||
label: metric.label,
|
subtitleClassName?: string;
|
||||||
detail: metric.value,
|
categoryClassName?: string;
|
||||||
}));
|
valueClassName?: string;
|
||||||
|
footerClassName?: string;
|
||||||
return (
|
cardButtonClassName?: string;
|
||||||
<div className="metric-card-ten">
|
cardButtonTextClassName?: string;
|
||||||
<CardStack items={items} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|||||||
@@ -1,28 +1,186 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { CardStack } from "@/components/cardStack/CardStack";
|
|
||||||
|
import { memo } from "react";
|
||||||
|
import CardStack from "@/components/cardStack/CardStack";
|
||||||
|
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
||||||
|
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import type { ButtonConfig, CardAnimationTypeWith3D, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
|
||||||
|
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||||
|
|
||||||
|
type Metric = {
|
||||||
|
id: string;
|
||||||
|
icon: LucideIcon;
|
||||||
|
title: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
interface MetricCardThreeProps {
|
interface MetricCardThreeProps {
|
||||||
metrics?: any[];
|
metrics: Metric[];
|
||||||
title?: string;
|
carouselMode?: "auto" | "buttons";
|
||||||
description?: string;
|
uniformGridCustomHeightClasses?: string;
|
||||||
animationType?: string;
|
animationType: CardAnimationTypeWith3D;
|
||||||
textboxLayout?: string;
|
title: string;
|
||||||
useInvertedBackground?: boolean;
|
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({
|
interface MetricCardItemProps {
|
||||||
metrics = [],
|
metric: Metric;
|
||||||
title = "Metrics", description = "Key metrics", animationType = "slide-up", textboxLayout = "default", useInvertedBackground = false,
|
shouldUseLightText: boolean;
|
||||||
}: MetricCardThreeProps) {
|
cardClassName?: string;
|
||||||
const items = metrics.map((metric) => ({
|
iconContainerClassName?: string;
|
||||||
id: metric.id,
|
iconClassName?: string;
|
||||||
label: metric.label,
|
metricTitleClassName?: string;
|
||||||
detail: metric.value,
|
valueClassName?: string;
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="metric-card-three">
|
|
||||||
<CardStack items={items} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
@@ -1,28 +1,183 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { CardStack } from "@/components/cardStack/CardStack";
|
|
||||||
|
import { memo } from "react";
|
||||||
|
import CardStack from "@/components/cardStack/CardStack";
|
||||||
|
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
||||||
|
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import type { ButtonConfig, GridVariant, CardAnimationTypeWith3D, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
|
||||||
|
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||||
|
|
||||||
|
type MetricCardTwoGridVariant = Extract<GridVariant, "uniform-all-items-equal" | "bento-grid" | "bento-grid-inverted">;
|
||||||
|
|
||||||
|
type Metric = {
|
||||||
|
id: string;
|
||||||
|
value: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
interface MetricCardTwoProps {
|
interface MetricCardTwoProps {
|
||||||
metrics?: any[];
|
metrics: Metric[];
|
||||||
title?: string;
|
carouselMode?: "auto" | "buttons";
|
||||||
description?: string;
|
gridVariant: MetricCardTwoGridVariant;
|
||||||
animationType?: string;
|
uniformGridCustomHeightClasses?: string;
|
||||||
textboxLayout?: string;
|
animationType: CardAnimationTypeWith3D;
|
||||||
useInvertedBackground?: boolean;
|
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({
|
interface MetricCardItemProps {
|
||||||
metrics = [],
|
metric: Metric;
|
||||||
title = "Metrics", description = "Key metrics", animationType = "slide-up", textboxLayout = "default", useInvertedBackground = false,
|
shouldUseLightText: boolean;
|
||||||
}: MetricCardTwoProps) {
|
cardClassName?: string;
|
||||||
const items = metrics.map((metric) => ({
|
valueClassName?: string;
|
||||||
id: metric.id,
|
metricDescriptionClassName?: string;
|
||||||
label: metric.label,
|
|
||||||
detail: metric.value,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="metric-card-two">
|
|
||||||
<CardStack items={items} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|||||||
@@ -1,60 +1,248 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
|
|
||||||
interface PricingPlan {
|
import { memo } from "react";
|
||||||
id: string;
|
import CardStack from "@/components/cardStack/CardStack";
|
||||||
badge: string;
|
import Button from "@/components/button/Button";
|
||||||
price: string;
|
import PricingBadge from "@/components/shared/PricingBadge";
|
||||||
subtitle: string;
|
import PricingFeatureList from "@/components/shared/PricingFeatureList";
|
||||||
features: string[];
|
import { getButtonProps } from "@/lib/buttonUtils";
|
||||||
buttons?: Array<{ text: string; href?: string; onClick?: () => void }>;
|
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 {
|
interface PricingCardEightProps {
|
||||||
plans: PricingPlan[];
|
plans: PricingPlan[];
|
||||||
title: string;
|
carouselMode?: "auto" | "buttons";
|
||||||
description: string;
|
uniformGridCustomHeightClasses?: string;
|
||||||
animationType?: string;
|
animationType: CardAnimationType;
|
||||||
textboxLayout?: string;
|
title: string;
|
||||||
useInvertedBackground?: boolean;
|
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({
|
interface PricingCardItemProps {
|
||||||
plans,
|
plan: PricingPlan;
|
||||||
title,
|
shouldUseLightText: boolean;
|
||||||
description,
|
cardClassName?: string;
|
||||||
animationType = "slide-up", textboxLayout = "default", useInvertedBackground = false,
|
badgeClassName?: string;
|
||||||
}: PricingCardEightProps) {
|
priceClassName?: string;
|
||||||
return (
|
subtitleClassName?: string;
|
||||||
<div className="pricing-card-eight-container">
|
planButtonContainerClassName?: string;
|
||||||
<div className="pricing-header">
|
planButtonClassName?: string;
|
||||||
<h2 className="pricing-title">{title}</h2>
|
featuresClassName?: string;
|
||||||
<p className="pricing-description">{description}</p>
|
featureItemClassName?: string;
|
||||||
</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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|||||||
@@ -1,51 +1,231 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import { Check } from "lucide-react";
|
||||||
import CardList from "@/components/cardStack/CardList";
|
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 {
|
interface PricingCardFiveProps {
|
||||||
plans?: any[];
|
plans: PricingPlan[];
|
||||||
title?: string;
|
animationType: CardAnimationType;
|
||||||
description?: string;
|
title: string;
|
||||||
animationType?: string;
|
titleSegments?: TitleSegment[];
|
||||||
useInvertedBackground?: boolean;
|
description: string;
|
||||||
textboxLayout?: string;
|
tag?: string;
|
||||||
tag?: string;
|
tagIcon?: LucideIcon;
|
||||||
tagIcon?: any;
|
tagAnimation?: ButtonAnimationType;
|
||||||
tagAnimation?: string;
|
buttons?: ButtonConfig[];
|
||||||
buttons?: any[];
|
buttonAnimation?: ButtonAnimationType;
|
||||||
buttonAnimation?: string;
|
textboxLayout: TextboxLayout;
|
||||||
titleSegments?: any[];
|
useInvertedBackground: InvertedBackground;
|
||||||
ariaLabel?: string;
|
ariaLabel?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
containerClassName?: string;
|
containerClassName?: string;
|
||||||
cardClassName?: string;
|
cardClassName?: string;
|
||||||
textBoxTitleClassName?: string;
|
textBoxTitleClassName?: string;
|
||||||
textBoxDescriptionClassName?: string;
|
textBoxDescriptionClassName?: string;
|
||||||
textBoxClassName?: string;
|
textBoxClassName?: string;
|
||||||
textBoxTagClassName?: string;
|
textBoxTagClassName?: string;
|
||||||
textBoxButtonContainerClassName?: string;
|
textBoxButtonContainerClassName?: string;
|
||||||
textBoxButtonClassName?: string;
|
textBoxButtonClassName?: string;
|
||||||
textBoxButtonTextClassName?: string;
|
textBoxButtonTextClassName?: string;
|
||||||
titleImageWrapperClassName?: string;
|
titleImageWrapperClassName?: string;
|
||||||
titleImageClassName?: 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({
|
const PricingCardFive = ({
|
||||||
plans = [],
|
plans,
|
||||||
title = "Pricing", description = "Our pricing plans", animationType = "slide-up", useInvertedBackground = false,
|
animationType,
|
||||||
textboxLayout = "default"}: PricingCardFiveProps) {
|
title,
|
||||||
const items = plans.map((plan) => ({
|
titleSegments,
|
||||||
id: plan.id,
|
description,
|
||||||
label: plan.badge,
|
tag,
|
||||||
detail: plan.price,
|
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 (
|
const getButtonConfigProps = () => {
|
||||||
<div className="pricing-card-five">
|
if (theme.defaultButtonVariant === "hover-bubble") {
|
||||||
<h2>{title}</h2>
|
return { bgClassName: "w-full" };
|
||||||
<p>{description}</p>
|
}
|
||||||
<CardList items={items} />
|
if (theme.defaultButtonVariant === "icon-arrow") {
|
||||||
</div>
|
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;
|
||||||
|
|||||||
@@ -1,51 +1,216 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import { Check } from "lucide-react";
|
||||||
import CardList from "@/components/cardStack/CardList";
|
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 {
|
interface PricingCardNineProps {
|
||||||
plans?: any[];
|
plans: PricingPlan[];
|
||||||
title?: string;
|
animationType: CardAnimationType;
|
||||||
description?: string;
|
title: string;
|
||||||
animationType?: string;
|
titleSegments?: TitleSegment[];
|
||||||
useInvertedBackground?: boolean;
|
description: string;
|
||||||
textboxLayout?: string;
|
tag?: string;
|
||||||
tag?: string;
|
tagIcon?: LucideIcon;
|
||||||
tagIcon?: any;
|
tagAnimation?: ButtonAnimationType;
|
||||||
tagAnimation?: string;
|
buttons?: ButtonConfig[];
|
||||||
buttons?: any[];
|
buttonAnimation?: ButtonAnimationType;
|
||||||
buttonAnimation?: string;
|
textboxLayout: TextboxLayout;
|
||||||
titleSegments?: any[];
|
useInvertedBackground: InvertedBackground;
|
||||||
ariaLabel?: string;
|
ariaLabel?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
containerClassName?: string;
|
containerClassName?: string;
|
||||||
cardClassName?: string;
|
cardClassName?: string;
|
||||||
textBoxTitleClassName?: string;
|
textBoxTitleClassName?: string;
|
||||||
textBoxDescriptionClassName?: string;
|
textBoxDescriptionClassName?: string;
|
||||||
textBoxClassName?: string;
|
textBoxClassName?: string;
|
||||||
textBoxTagClassName?: string;
|
textBoxTagClassName?: string;
|
||||||
textBoxButtonContainerClassName?: string;
|
textBoxButtonContainerClassName?: string;
|
||||||
textBoxButtonClassName?: string;
|
textBoxButtonClassName?: string;
|
||||||
textBoxButtonTextClassName?: string;
|
textBoxButtonTextClassName?: string;
|
||||||
titleImageWrapperClassName?: string;
|
titleImageWrapperClassName?: string;
|
||||||
titleImageClassName?: 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({
|
const PricingCardNine = ({
|
||||||
plans = [],
|
plans,
|
||||||
title = "Pricing", description = "Our pricing plans", animationType = "slide-up", useInvertedBackground = false,
|
animationType,
|
||||||
textboxLayout = "default"}: PricingCardNineProps) {
|
title,
|
||||||
const items = plans.map((plan) => ({
|
titleSegments,
|
||||||
id: plan.id,
|
description,
|
||||||
label: plan.badge,
|
tag,
|
||||||
detail: plan.price,
|
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 (
|
const getButtonConfigProps = () => {
|
||||||
<div className="pricing-card-nine">
|
if (theme.defaultButtonVariant === "hover-bubble") {
|
||||||
<h2>{title}</h2>
|
return { bgClassName: "w-full" };
|
||||||
<p>{description}</p>
|
}
|
||||||
<CardList items={items} />
|
if (theme.defaultButtonVariant === "icon-arrow") {
|
||||||
</div>
|
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;
|
||||||
|
|||||||
@@ -1,28 +1,206 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { CardStack } from "@/components/cardStack/CardStack";
|
|
||||||
|
import { memo } from "react";
|
||||||
|
import CardStack from "@/components/cardStack/CardStack";
|
||||||
|
import PricingBadge from "@/components/shared/PricingBadge";
|
||||||
|
import PricingFeatureList from "@/components/shared/PricingFeatureList";
|
||||||
|
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
||||||
|
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import type { ButtonConfig, CardAnimationTypeWith3D, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
|
||||||
|
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||||
|
|
||||||
|
type PricingPlan = {
|
||||||
|
id: string;
|
||||||
|
badge: string;
|
||||||
|
badgeIcon?: LucideIcon;
|
||||||
|
price: string;
|
||||||
|
subtitle: string;
|
||||||
|
features: string[];
|
||||||
|
};
|
||||||
|
|
||||||
interface PricingCardOneProps {
|
interface PricingCardOneProps {
|
||||||
plans?: any[];
|
plans: PricingPlan[];
|
||||||
title?: string;
|
carouselMode?: "auto" | "buttons";
|
||||||
description?: string;
|
uniformGridCustomHeightClasses?: string;
|
||||||
animationType?: string;
|
animationType: CardAnimationTypeWith3D;
|
||||||
textboxLayout?: string;
|
title: string;
|
||||||
useInvertedBackground?: boolean;
|
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({
|
interface PricingCardItemProps {
|
||||||
plans = [],
|
plan: PricingPlan;
|
||||||
title = "Pricing", description = "Our plans", animationType = "slide-up", textboxLayout = "default", useInvertedBackground = false,
|
shouldUseLightText: boolean;
|
||||||
}: PricingCardOneProps) {
|
cardClassName?: string;
|
||||||
const items = plans.map((plan) => ({
|
badgeClassName?: string;
|
||||||
id: plan.id,
|
priceClassName?: string;
|
||||||
label: plan.price,
|
subtitleClassName?: string;
|
||||||
detail: plan.subtitle,
|
featuresClassName?: string;
|
||||||
}));
|
featureItemClassName?: string;
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="pricing-card-one">
|
|
||||||
<CardStack items={items} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|||||||
@@ -1,28 +1,247 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { CardStack } from "@/components/cardStack/CardStack";
|
|
||||||
|
import { memo } from "react";
|
||||||
|
import CardStack from "@/components/cardStack/CardStack";
|
||||||
|
import PricingFeatureList from "@/components/shared/PricingFeatureList";
|
||||||
|
import Button from "@/components/button/Button";
|
||||||
|
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||||
|
import { getButtonProps } from "@/lib/buttonUtils";
|
||||||
|
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import type { ButtonConfig, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
|
||||||
|
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||||
|
|
||||||
|
type PricingPlan = {
|
||||||
|
id: string;
|
||||||
|
badge?: string;
|
||||||
|
badgeIcon?: LucideIcon;
|
||||||
|
price: string;
|
||||||
|
name: string;
|
||||||
|
buttons: ButtonConfig[];
|
||||||
|
features: string[];
|
||||||
|
};
|
||||||
|
|
||||||
interface PricingCardThreeProps {
|
interface PricingCardThreeProps {
|
||||||
plans?: any[];
|
plans: PricingPlan[];
|
||||||
title?: string;
|
carouselMode?: "auto" | "buttons";
|
||||||
description?: string;
|
uniformGridCustomHeightClasses?: string;
|
||||||
animationType?: string;
|
animationType: CardAnimationType;
|
||||||
textboxLayout?: string;
|
title: string;
|
||||||
useInvertedBackground?: boolean;
|
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({
|
interface PricingCardItemProps {
|
||||||
plans = [],
|
plan: PricingPlan;
|
||||||
title = "Pricing", description = "Our plans", animationType = "slide-up", textboxLayout = "default", useInvertedBackground = false,
|
shouldUseLightText: boolean;
|
||||||
}: PricingCardThreeProps) {
|
cardClassName?: string;
|
||||||
const items = plans.map((plan) => ({
|
badgeClassName?: string;
|
||||||
id: plan.id,
|
priceClassName?: string;
|
||||||
label: plan.price,
|
nameClassName?: string;
|
||||||
detail: plan.subtitle,
|
planButtonContainerClassName?: string;
|
||||||
}));
|
planButtonClassName?: string;
|
||||||
|
featuresClassName?: string;
|
||||||
return (
|
featureItemClassName?: string;
|
||||||
<div className="pricing-card-three">
|
|
||||||
<CardStack items={items} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|||||||
@@ -1,28 +1,246 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { CardStack } from "@/components/cardStack/CardStack";
|
|
||||||
|
import { memo } from "react";
|
||||||
|
import CardStack from "@/components/cardStack/CardStack";
|
||||||
|
import PricingBadge from "@/components/shared/PricingBadge";
|
||||||
|
import PricingFeatureList from "@/components/shared/PricingFeatureList";
|
||||||
|
import Button from "@/components/button/Button";
|
||||||
|
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||||
|
import { getButtonProps } from "@/lib/buttonUtils";
|
||||||
|
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import type { ButtonConfig, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
|
||||||
|
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||||
|
|
||||||
|
type PricingPlan = {
|
||||||
|
id: string;
|
||||||
|
badge: string;
|
||||||
|
badgeIcon?: LucideIcon;
|
||||||
|
price: string;
|
||||||
|
subtitle: string;
|
||||||
|
buttons: ButtonConfig[];
|
||||||
|
features: string[];
|
||||||
|
};
|
||||||
|
|
||||||
interface PricingCardTwoProps {
|
interface PricingCardTwoProps {
|
||||||
plans?: any[];
|
plans: PricingPlan[];
|
||||||
title?: string;
|
carouselMode?: "auto" | "buttons";
|
||||||
description?: string;
|
uniformGridCustomHeightClasses?: string;
|
||||||
animationType?: string;
|
animationType: CardAnimationType;
|
||||||
textboxLayout?: string;
|
title: string;
|
||||||
useInvertedBackground?: boolean;
|
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({
|
interface PricingCardItemProps {
|
||||||
plans = [],
|
plan: PricingPlan;
|
||||||
title = "Pricing", description = "Our plans", animationType = "slide-up", textboxLayout = "default", useInvertedBackground = false,
|
shouldUseLightText: boolean;
|
||||||
}: PricingCardTwoProps) {
|
cardClassName?: string;
|
||||||
const items = plans.map((plan) => ({
|
badgeClassName?: string;
|
||||||
id: plan.id,
|
priceClassName?: string;
|
||||||
label: plan.price,
|
subtitleClassName?: string;
|
||||||
detail: plan.subtitle,
|
planButtonContainerClassName?: string;
|
||||||
}));
|
planButtonClassName?: string;
|
||||||
|
featuresClassName?: string;
|
||||||
return (
|
featureItemClassName?: string;
|
||||||
<div className="pricing-card-two">
|
|
||||||
<CardStack items={items} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|||||||
@@ -1,35 +1,238 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { Product } from "@/lib/api/product";
|
|
||||||
|
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 {
|
interface ProductCardFourProps {
|
||||||
products?: Product[];
|
products?: ProductCard[];
|
||||||
title?: string;
|
carouselMode?: "auto" | "buttons";
|
||||||
description?: string;
|
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({
|
interface ProductCardItemProps {
|
||||||
products = [],
|
product: ProductCard;
|
||||||
title = "Products", description = "Our premium product collection"}: ProductCardFourProps) {
|
shouldUseLightText: boolean;
|
||||||
return (
|
cardClassName?: string;
|
||||||
<div className="product-card-four-container">
|
imageClassName?: string;
|
||||||
<div className="product-header">
|
cardNameClassName?: string;
|
||||||
<h2 className="product-title">{title}</h2>
|
cardPriceClassName?: string;
|
||||||
<p className="product-description">{description}</p>
|
cardVariantClassName?: string;
|
||||||
</div>
|
actionButtonClassName?: string;
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|||||||
@@ -1,35 +1,226 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { Product } from "@/lib/api/product";
|
|
||||||
|
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 {
|
interface ProductCardOneProps {
|
||||||
products?: Product[];
|
products?: ProductCard[];
|
||||||
title?: string;
|
carouselMode?: "auto" | "buttons";
|
||||||
description?: string;
|
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({
|
interface ProductCardItemProps {
|
||||||
products = [],
|
product: ProductCard;
|
||||||
title = "Products", description = "Our premium product collection"}: ProductCardOneProps) {
|
shouldUseLightText: boolean;
|
||||||
return (
|
cardClassName?: string;
|
||||||
<div className="product-card-one-container">
|
imageClassName?: string;
|
||||||
<div className="product-header">
|
cardNameClassName?: string;
|
||||||
<h2 className="product-title">{title}</h2>
|
cardPriceClassName?: string;
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|||||||
@@ -1,35 +1,283 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { Product } from "@/lib/api/product";
|
|
||||||
|
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 {
|
interface ProductCardThreeProps {
|
||||||
products?: Product[];
|
products?: ProductCard[];
|
||||||
title?: string;
|
carouselMode?: "auto" | "buttons";
|
||||||
description?: string;
|
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 = [],
|
interface ProductCardItemProps {
|
||||||
title = "Products", description = "Our premium product collection"}: ProductCardThreeProps) {
|
product: ProductCard;
|
||||||
return (
|
shouldUseLightText: boolean;
|
||||||
<div className="product-card-three-container">
|
isFromApi: boolean;
|
||||||
<div className="product-header">
|
onBuyClick?: (productId: string, quantity: number) => void;
|
||||||
<h2 className="product-title">{title}</h2>
|
cardClassName?: string;
|
||||||
<p className="product-description">{description}</p>
|
imageClassName?: string;
|
||||||
</div>
|
cardNameClassName?: string;
|
||||||
<div className="product-grid">
|
quantityControlsClassName?: string;
|
||||||
{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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|||||||
@@ -1,35 +1,267 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { Product } from "@/lib/api/product";
|
|
||||||
|
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 {
|
interface ProductCardTwoProps {
|
||||||
products?: Product[];
|
products?: ProductCard[];
|
||||||
title?: string;
|
carouselMode?: "auto" | "buttons";
|
||||||
description?: string;
|
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({
|
interface ProductCardItemProps {
|
||||||
products = [],
|
product: ProductCard;
|
||||||
title = "Products", description = "Our premium product collection"}: ProductCardTwoProps) {
|
shouldUseLightText: boolean;
|
||||||
return (
|
cardClassName?: string;
|
||||||
<div className="product-card-two-container">
|
imageClassName?: string;
|
||||||
<div className="product-header">
|
cardBrandClassName?: string;
|
||||||
<h2 className="product-title">{title}</h2>
|
cardNameClassName?: string;
|
||||||
<p className="product-description">{description}</p>
|
cardPriceClassName?: string;
|
||||||
</div>
|
cardRatingClassName?: string;
|
||||||
<div className="product-grid">
|
actionButtonClassName?: string;
|
||||||
{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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|||||||
@@ -1,51 +1,196 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import CardList from "@/components/cardStack/CardList";
|
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 {
|
interface TeamCardElevenProps {
|
||||||
members?: any[];
|
groups: TeamGroup[];
|
||||||
title?: string;
|
animationType: CardAnimationType;
|
||||||
description?: string;
|
title: string;
|
||||||
animationType?: string;
|
titleSegments?: TitleSegment[];
|
||||||
useInvertedBackground?: boolean;
|
description: string;
|
||||||
textboxLayout?: string;
|
tag?: string;
|
||||||
tag?: string;
|
tagIcon?: LucideIcon;
|
||||||
tagIcon?: any;
|
tagAnimation?: ButtonAnimationType;
|
||||||
tagAnimation?: string;
|
buttons?: ButtonConfig[];
|
||||||
buttons?: any[];
|
buttonAnimation?: ButtonAnimationType;
|
||||||
buttonAnimation?: string;
|
textboxLayout: TextboxLayout;
|
||||||
titleSegments?: any[];
|
useInvertedBackground: InvertedBackground;
|
||||||
ariaLabel?: string;
|
ariaLabel?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
containerClassName?: string;
|
containerClassName?: string;
|
||||||
cardClassName?: string;
|
cardClassName?: string;
|
||||||
textBoxTitleClassName?: string;
|
textBoxClassName?: string;
|
||||||
textBoxDescriptionClassName?: string;
|
textBoxTitleClassName?: string;
|
||||||
textBoxClassName?: string;
|
textBoxDescriptionClassName?: string;
|
||||||
textBoxTagClassName?: string;
|
textBoxTagClassName?: string;
|
||||||
textBoxButtonContainerClassName?: string;
|
textBoxButtonContainerClassName?: string;
|
||||||
textBoxButtonClassName?: string;
|
textBoxButtonClassName?: string;
|
||||||
textBoxButtonTextClassName?: string;
|
textBoxButtonTextClassName?: string;
|
||||||
titleImageWrapperClassName?: string;
|
titleImageWrapperClassName?: string;
|
||||||
titleImageClassName?: string;
|
titleImageClassName?: string;
|
||||||
|
groupTitleClassName?: string;
|
||||||
|
memberClassName?: string;
|
||||||
|
memberImageClassName?: string;
|
||||||
|
memberTitleClassName?: string;
|
||||||
|
memberSubtitleClassName?: string;
|
||||||
|
memberDetailClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TeamCardEleven({
|
const TeamCardEleven = ({
|
||||||
members = [],
|
groups,
|
||||||
title = "Team", description = "Our team members", animationType = "slide-up", useInvertedBackground = false,
|
animationType,
|
||||||
textboxLayout = "default"}: TeamCardElevenProps) {
|
title,
|
||||||
const items = members.map((member) => ({
|
titleSegments,
|
||||||
id: member.id,
|
description,
|
||||||
label: member.name,
|
tag,
|
||||||
detail: member.role,
|
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 (
|
const renderMemberRow = (member: TeamMember) => (
|
||||||
<div className="team-card-eleven">
|
<div
|
||||||
<h2>{title}</h2>
|
key={member.id}
|
||||||
<p>{description}</p>
|
className={cls(
|
||||||
<CardList items={items} />
|
"flex flex-col md:flex-row md:items-center gap-4 py-6",
|
||||||
</div>
|
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;
|
||||||
|
|||||||
@@ -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 { 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 {
|
interface TeamCardFiveProps {
|
||||||
members?: any[];
|
team: TeamMember[];
|
||||||
title?: string;
|
animationType: CardAnimationType;
|
||||||
description?: string;
|
title: string;
|
||||||
animationType?: string;
|
titleSegments?: TitleSegment[];
|
||||||
useInvertedBackground?: boolean;
|
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({
|
const TeamCardFive = ({
|
||||||
members = [],
|
team,
|
||||||
title = "Team", description = "Our team", animationType = "slide-up", useInvertedBackground = false,
|
animationType,
|
||||||
}: TeamCardFiveProps) {
|
title,
|
||||||
const state = useCardAnimation({
|
titleSegments,
|
||||||
rotationX: 0,
|
description,
|
||||||
rotationY: 0,
|
textboxLayout,
|
||||||
rotationZ: 0,
|
useInvertedBackground,
|
||||||
perspective: 1000,
|
tag,
|
||||||
duration: 0.3,
|
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 (
|
return (
|
||||||
<div className="team-card-five">
|
<section
|
||||||
<h2>{title}</h2>
|
aria-label={ariaLabel}
|
||||||
<p>{description}</p>
|
className={cls("relative py-20 w-full", useInvertedBackground && "bg-foreground", className)}
|
||||||
<div className="members-container">
|
>
|
||||||
{members.map((member) => (
|
<div className={cls("w-content-width mx-auto flex flex-col gap-8", containerClassName)}>
|
||||||
<div key={member.id} className="member-item">
|
<CardStackTextBox
|
||||||
<h3>{member.name}</h3>
|
title={title}
|
||||||
<p>{member.role}</p>
|
titleSegments={titleSegments}
|
||||||
</div>
|
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>
|
||||||
</div>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
TeamCardFive.displayName = "TeamCardFive";
|
||||||
|
|
||||||
|
export default TeamCardFive;
|
||||||
|
|||||||
@@ -1,28 +1,194 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { CardStack } from "@/components/cardStack/CardStack";
|
|
||||||
|
import { memo } from "react";
|
||||||
|
import CardStack from "@/components/cardStack/CardStack";
|
||||||
|
import MediaContent from "@/components/shared/MediaContent";
|
||||||
|
import { cls } from "@/lib/utils";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import type { ButtonConfig, GridVariant, CardAnimationTypeWith3D, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
|
||||||
|
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||||
|
|
||||||
|
type TeamCardOneGridVariant = Exclude<GridVariant, "timeline">;
|
||||||
|
|
||||||
|
type TeamMember = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
imageSrc?: string;
|
||||||
|
videoSrc?: string;
|
||||||
|
imageAlt?: string;
|
||||||
|
videoAriaLabel?: string;
|
||||||
|
};
|
||||||
|
|
||||||
interface TeamCardOneProps {
|
interface TeamCardOneProps {
|
||||||
members?: any[];
|
members: TeamMember[];
|
||||||
title?: string;
|
carouselMode?: "auto" | "buttons";
|
||||||
description?: string;
|
gridVariant: TeamCardOneGridVariant;
|
||||||
animationType?: string;
|
uniformGridCustomHeightClasses?: string;
|
||||||
textboxLayout?: string;
|
animationType: CardAnimationTypeWith3D;
|
||||||
useInvertedBackground?: boolean;
|
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({
|
interface TeamMemberCardProps {
|
||||||
members = [],
|
member: TeamMember;
|
||||||
title = "Team", description = "Our team", animationType = "slide-up", textboxLayout = "default", useInvertedBackground = false,
|
cardClassName?: string;
|
||||||
}: TeamCardOneProps) {
|
imageClassName?: string;
|
||||||
const items = members.map((member) => ({
|
overlayClassName?: string;
|
||||||
id: member.id,
|
nameClassName?: string;
|
||||||
label: member.name,
|
roleClassName?: string;
|
||||||
detail: member.role,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="team-card-one">
|
|
||||||
<CardStack items={items} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|||||||
@@ -1,28 +1,200 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { CardStack } from "@/components/cardStack/CardStack";
|
|
||||||
|
import { memo } from "react";
|
||||||
|
import CardStack from "@/components/cardStack/CardStack";
|
||||||
|
import MediaContent from "@/components/shared/MediaContent";
|
||||||
|
import { cls } from "@/lib/utils";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import type { ButtonConfig, GridVariant, CardAnimationTypeWith3D, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
|
||||||
|
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||||
|
|
||||||
|
type TeamCardSixGridVariant = Exclude<GridVariant, "timeline" | "two-columns-alternating-heights" | "four-items-2x2-equal-grid">;
|
||||||
|
|
||||||
|
const MASK_GRADIENT = "linear-gradient(to bottom, transparent, black 60%)";
|
||||||
|
|
||||||
|
type TeamMember = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
imageSrc?: string;
|
||||||
|
videoSrc?: string;
|
||||||
|
imageAlt?: string;
|
||||||
|
videoAriaLabel?: string;
|
||||||
|
};
|
||||||
|
|
||||||
interface TeamCardSixProps {
|
interface TeamCardSixProps {
|
||||||
members?: any[];
|
members: TeamMember[];
|
||||||
title?: string;
|
carouselMode?: "auto" | "buttons";
|
||||||
description?: string;
|
gridVariant: TeamCardSixGridVariant;
|
||||||
animationType?: string;
|
uniformGridCustomHeightClasses?: string;
|
||||||
textboxLayout?: string;
|
animationType: CardAnimationTypeWith3D;
|
||||||
useInvertedBackground?: boolean;
|
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({
|
interface TeamMemberCardProps {
|
||||||
members = [],
|
member: TeamMember;
|
||||||
title = "Team", description = "Our team", animationType = "slide-up", textboxLayout = "default", useInvertedBackground = false,
|
cardClassName?: string;
|
||||||
}: TeamCardSixProps) {
|
imageClassName?: string;
|
||||||
const items = members.map((member) => ({
|
overlayClassName?: string;
|
||||||
id: member.id,
|
nameClassName?: string;
|
||||||
label: member.name,
|
roleClassName?: string;
|
||||||
detail: member.role,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="team-card-six">
|
|
||||||
<CardStack items={items} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
@@ -1,28 +1,240 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { CardStack } from "@/components/cardStack/CardStack";
|
|
||||||
|
import { memo } from "react";
|
||||||
|
import CardStack from "@/components/cardStack/CardStack";
|
||||||
|
import MediaContent from "@/components/shared/MediaContent";
|
||||||
|
import { cls } from "@/lib/utils";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import type { ButtonConfig, GridVariant, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
|
||||||
|
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||||
|
|
||||||
|
type TeamCardTwoGridVariant = Exclude<GridVariant, "timeline">;
|
||||||
|
|
||||||
|
type SocialLink = {
|
||||||
|
icon: LucideIcon;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TeamMember = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
description: string;
|
||||||
|
imageSrc?: string;
|
||||||
|
videoSrc?: string;
|
||||||
|
imageAlt?: string;
|
||||||
|
videoAriaLabel?: string;
|
||||||
|
socialLinks?: SocialLink[];
|
||||||
|
};
|
||||||
|
|
||||||
interface TeamCardTwoProps {
|
interface TeamCardTwoProps {
|
||||||
members?: any[];
|
members: TeamMember[];
|
||||||
title?: string;
|
carouselMode?: "auto" | "buttons";
|
||||||
description?: string;
|
gridVariant: TeamCardTwoGridVariant;
|
||||||
animationType?: string;
|
uniformGridCustomHeightClasses?: string;
|
||||||
textboxLayout?: string;
|
animationType: CardAnimationType;
|
||||||
useInvertedBackground?: boolean;
|
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({
|
interface TeamMemberCardProps {
|
||||||
members = [],
|
member: TeamMember;
|
||||||
title = "Team", description = "Our team", animationType = "slide-up", textboxLayout = "default", useInvertedBackground = false,
|
cardClassName?: string;
|
||||||
}: TeamCardTwoProps) {
|
imageClassName?: string;
|
||||||
const items = members.map((member) => ({
|
overlayClassName?: string;
|
||||||
id: member.id,
|
nameClassName?: string;
|
||||||
label: member.name,
|
roleClassName?: string;
|
||||||
detail: member.role,
|
memberDescriptionClassName?: string;
|
||||||
}));
|
socialLinksClassName?: string;
|
||||||
|
socialIconClassName?: string;
|
||||||
return (
|
|
||||||
<div className="team-card-two">
|
|
||||||
<CardStack items={items} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|||||||
@@ -1,28 +1,219 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { CardStack } from "@/components/cardStack/CardStack";
|
|
||||||
|
import { memo } from "react";
|
||||||
|
import CardStack from "@/components/cardStack/CardStack";
|
||||||
|
import MediaContent from "@/components/shared/MediaContent";
|
||||||
|
import { cls } from "@/lib/utils";
|
||||||
|
import { Star } from "lucide-react";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import type { ButtonConfig, ButtonAnimationType, CardAnimationTypeWith3D, GridVariant, TitleSegment, TextboxLayout, InvertedBackground } from "@/components/cardStack/types";
|
||||||
|
|
||||||
|
type Testimonial = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
company: string;
|
||||||
|
rating: number;
|
||||||
|
imageSrc?: string;
|
||||||
|
videoSrc?: string;
|
||||||
|
imageAlt?: string;
|
||||||
|
videoAriaLabel?: string;
|
||||||
|
};
|
||||||
|
|
||||||
interface TestimonialCardOneProps {
|
interface TestimonialCardOneProps {
|
||||||
testimonials?: any[];
|
testimonials: Testimonial[];
|
||||||
title?: string;
|
carouselMode?: "auto" | "buttons";
|
||||||
description?: string;
|
uniformGridCustomHeightClasses?: string;
|
||||||
animationType?: string;
|
gridVariant: GridVariant;
|
||||||
textboxLayout?: string;
|
animationType: CardAnimationTypeWith3D;
|
||||||
useInvertedBackground?: boolean;
|
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({
|
interface TestimonialCardProps {
|
||||||
testimonials = [],
|
testimonial: Testimonial;
|
||||||
title = "Testimonials", description = "What customers say", animationType = "slide-up", textboxLayout = "default", useInvertedBackground = false,
|
cardClassName?: string;
|
||||||
}: TestimonialCardOneProps) {
|
imageClassName?: string;
|
||||||
const items = testimonials.map((testimonial) => ({
|
overlayClassName?: string;
|
||||||
id: testimonial.id,
|
ratingClassName?: string;
|
||||||
label: testimonial.name,
|
nameClassName?: string;
|
||||||
detail: testimonial.company,
|
roleClassName?: string;
|
||||||
}));
|
companyClassName?: string;
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="testimonial-card-one">
|
|
||||||
<CardStack items={items} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|||||||
@@ -1,29 +1,203 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
|
|
||||||
|
import { memo } from "react";
|
||||||
import AutoCarousel from "@/components/cardStack/layouts/carousels/AutoCarousel";
|
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 {
|
interface TestimonialCardSixProps {
|
||||||
testimonials?: any[];
|
testimonials: Testimonial[];
|
||||||
title?: string;
|
animationType: CardAnimationType;
|
||||||
description?: string;
|
title: string;
|
||||||
animationType?: string;
|
titleSegments?: TitleSegment[];
|
||||||
useInvertedBackground?: boolean;
|
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({
|
interface TestimonialCardProps {
|
||||||
testimonials = [],
|
testimonial: Testimonial;
|
||||||
title = "Testimonials", description = "What customers say", animationType = "slide-up", useInvertedBackground = false,
|
useInvertedBackground: boolean;
|
||||||
}: TestimonialCardSixProps) {
|
cardClassName?: string;
|
||||||
const items = testimonials.map((testimonial) => ({
|
testimonialClassName?: string;
|
||||||
id: testimonial.id,
|
imageWrapperClassName?: string;
|
||||||
label: testimonial.name,
|
imageClassName?: string;
|
||||||
detail: testimonial.company,
|
iconClassName?: string;
|
||||||
}));
|
nameClassName?: string;
|
||||||
|
handleClassName?: string;
|
||||||
return (
|
|
||||||
<div className="testimonial-card-six">
|
|
||||||
<h2>{title}</h2>
|
|
||||||
<p>{description}</p>
|
|
||||||
<AutoCarousel items={items} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
@@ -1,28 +1,240 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { CardStack } from "@/components/cardStack/CardStack";
|
|
||||||
|
import { memo } from "react";
|
||||||
|
import CardStack from "@/components/cardStack/CardStack";
|
||||||
|
import MediaContent from "@/components/shared/MediaContent";
|
||||||
|
import { cls } from "@/lib/utils";
|
||||||
|
import { Star } from "lucide-react";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import type { ButtonConfig, ButtonAnimationType, CardAnimationTypeWith3D, TitleSegment, TextboxLayout, InvertedBackground } from "@/components/cardStack/types";
|
||||||
|
|
||||||
|
type Testimonial = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
company: string;
|
||||||
|
rating: number;
|
||||||
|
imageSrc?: string;
|
||||||
|
videoSrc?: string;
|
||||||
|
imageAlt?: string;
|
||||||
|
videoAriaLabel?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type KpiItem = {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
interface TestimonialCardSixteenProps {
|
interface TestimonialCardSixteenProps {
|
||||||
testimonials?: any[];
|
testimonials: Testimonial[];
|
||||||
title?: string;
|
kpiItems: [KpiItem, KpiItem, KpiItem];
|
||||||
description?: string;
|
carouselMode?: "auto" | "buttons";
|
||||||
animationType?: string;
|
uniformGridCustomHeightClasses?: string;
|
||||||
textboxLayout?: string;
|
animationType: CardAnimationTypeWith3D;
|
||||||
useInvertedBackground?: boolean;
|
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({
|
interface TestimonialCardProps {
|
||||||
testimonials = [],
|
testimonial: Testimonial;
|
||||||
title = "Testimonials", description = "What customers say", animationType = "slide-up", textboxLayout = "default", useInvertedBackground = false,
|
cardClassName?: string;
|
||||||
}: TestimonialCardSixteenProps) {
|
imageClassName?: string;
|
||||||
const items = testimonials.map((testimonial) => ({
|
overlayClassName?: string;
|
||||||
id: testimonial.id,
|
ratingClassName?: string;
|
||||||
label: testimonial.name,
|
nameClassName?: string;
|
||||||
detail: testimonial.company,
|
roleClassName?: string;
|
||||||
}));
|
companyClassName?: string;
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="testimonial-card-sixteen">
|
|
||||||
<CardStack items={items} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|||||||
@@ -1,28 +1,240 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { CardStack } from "@/components/cardStack/CardStack";
|
|
||||||
|
import { memo } from "react";
|
||||||
|
import CardStack from "@/components/cardStack/CardStack";
|
||||||
|
import TestimonialAuthor from "@/components/shared/TestimonialAuthor";
|
||||||
|
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
||||||
|
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||||
|
import { Quote, Star } from "lucide-react";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import type { ButtonConfig, ButtonAnimationType, CardAnimationTypeWith3D, TitleSegment, TextboxLayout, InvertedBackground } from "@/components/cardStack/types";
|
||||||
|
|
||||||
|
type Testimonial = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
handle: string;
|
||||||
|
testimonial: string;
|
||||||
|
rating: number;
|
||||||
|
imageSrc?: string;
|
||||||
|
imageAlt?: string;
|
||||||
|
icon?: LucideIcon;
|
||||||
|
};
|
||||||
|
|
||||||
interface TestimonialCardThirteenProps {
|
interface TestimonialCardThirteenProps {
|
||||||
testimonials?: any[];
|
testimonials: Testimonial[];
|
||||||
title?: string;
|
showRating: boolean;
|
||||||
description?: string;
|
carouselMode?: "auto" | "buttons";
|
||||||
animationType?: string;
|
uniformGridCustomHeightClasses?: string;
|
||||||
textboxLayout?: string;
|
animationType: CardAnimationTypeWith3D;
|
||||||
useInvertedBackground?: boolean;
|
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({
|
interface TestimonialCardProps {
|
||||||
testimonials = [],
|
testimonial: Testimonial;
|
||||||
title = "Testimonials", description = "What customers say", animationType = "slide-up", textboxLayout = "default", useInvertedBackground = false,
|
showRating: boolean;
|
||||||
}: TestimonialCardThirteenProps) {
|
useInvertedBackground: boolean;
|
||||||
const items = testimonials.map((testimonial) => ({
|
cardClassName?: string;
|
||||||
id: testimonial.id,
|
imageWrapperClassName?: string;
|
||||||
label: testimonial.name,
|
imageClassName?: string;
|
||||||
detail: testimonial.company,
|
iconClassName?: string;
|
||||||
}));
|
nameClassName?: string;
|
||||||
|
handleClassName?: string;
|
||||||
return (
|
testimonialClassName?: string;
|
||||||
<div className="testimonial-card-thirteen">
|
ratingClassName?: string;
|
||||||
<CardStack items={items} />
|
contentWrapperClassName?: string;
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|||||||
@@ -1,28 +1,216 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { CardStack } from "@/components/cardStack/CardStack";
|
|
||||||
|
import { memo } from "react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import CardStack from "@/components/cardStack/CardStack";
|
||||||
|
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
||||||
|
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||||
|
import { Quote } from "lucide-react";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import type { ButtonConfig, ButtonAnimationType, CardAnimationTypeWith3D, TitleSegment, TextboxLayout, InvertedBackground } from "@/components/cardStack/types";
|
||||||
|
|
||||||
|
type Testimonial = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
testimonial: string;
|
||||||
|
imageSrc?: string;
|
||||||
|
imageAlt?: string;
|
||||||
|
icon?: LucideIcon;
|
||||||
|
};
|
||||||
|
|
||||||
interface TestimonialCardTwoProps {
|
interface TestimonialCardTwoProps {
|
||||||
testimonials?: any[];
|
testimonials: Testimonial[];
|
||||||
title?: string;
|
carouselMode?: "auto" | "buttons";
|
||||||
description?: string;
|
uniformGridCustomHeightClasses?: string;
|
||||||
animationType?: string;
|
animationType: CardAnimationTypeWith3D;
|
||||||
textboxLayout?: string;
|
title: string;
|
||||||
useInvertedBackground?: boolean;
|
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({
|
interface TestimonialCardProps {
|
||||||
testimonials = [],
|
testimonial: Testimonial;
|
||||||
title = "Testimonials", description = "What customers say", animationType = "slide-up", textboxLayout = "default", useInvertedBackground = false,
|
shouldUseLightText: boolean;
|
||||||
}: TestimonialCardTwoProps) {
|
cardClassName?: string;
|
||||||
const items = testimonials.map((testimonial) => ({
|
imageWrapperClassName?: string;
|
||||||
id: testimonial.id,
|
imageClassName?: string;
|
||||||
label: testimonial.name,
|
iconClassName?: string;
|
||||||
detail: testimonial.company,
|
nameClassName?: string;
|
||||||
}));
|
roleClassName?: string;
|
||||||
|
testimonialClassName?: string;
|
||||||
return (
|
|
||||||
<div className="testimonial-card-two">
|
|
||||||
<CardStack items={items} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|||||||
@@ -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 { 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 {
|
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 Dashboard = ({
|
||||||
const state = useCardAnimation({
|
title,
|
||||||
rotationX: 0,
|
stats,
|
||||||
rotationY: 0,
|
logoIcon: LogoIcon,
|
||||||
rotationZ: 0,
|
sidebarItems,
|
||||||
perspective: 1000,
|
searchPlaceholder = "Search",
|
||||||
duration: 0.3,
|
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 (
|
useEffect(() => {
|
||||||
<div className="dashboard">
|
const interval = setInterval(() => {
|
||||||
{data.map((item, index) => (
|
setStatValueIndex((prev) => (prev + 1) % 3);
|
||||||
<div key={index} className="dashboard-item">
|
}, 3000);
|
||||||
{item.label}
|
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>
|
||||||
))}
|
);
|
||||||
</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);
|
||||||
|
|||||||
@@ -1,45 +1,117 @@
|
|||||||
import { useState, useCallback } from "react";
|
"use client";
|
||||||
|
|
||||||
interface CheckoutItem {
|
import { useState } from "react";
|
||||||
id: string;
|
import { Product } from "@/lib/api/product";
|
||||||
name: string;
|
|
||||||
price: number;
|
|
||||||
quantity: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useCheckout = () => {
|
export type CheckoutItem = {
|
||||||
const [items, setItems] = useState<CheckoutItem[]>([]);
|
productId: string;
|
||||||
const [total, setTotal] = useState(0);
|
quantity: number;
|
||||||
|
imageSrc?: string;
|
||||||
const addItem = useCallback(
|
imageAlt?: string;
|
||||||
(item: CheckoutItem) => {
|
metadata?: {
|
||||||
setItems((prev) => [...prev, item]);
|
brand?: string;
|
||||||
setTotal((prev) => prev + item.price * item.quantity);
|
variant?: string;
|
||||||
},
|
rating?: number;
|
||||||
[]
|
reviewCount?: string;
|
||||||
);
|
[key: string]: string | number | undefined;
|
||||||
|
};
|
||||||
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 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),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,28 +1,45 @@
|
|||||||
import { useState, useEffect } from "react";
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import { Product, fetchProduct } from "@/lib/api/product";
|
import { Product, fetchProduct } from "@/lib/api/product";
|
||||||
|
|
||||||
export const useProduct = (productId: string) => {
|
export function useProduct(productId: string) {
|
||||||
const [product, setProduct] = useState<Product | null>(null);
|
const [product, setProduct] = useState<Product | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetch = async () => {
|
let isMounted = true;
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const data = await fetchProduct(productId);
|
|
||||||
setProduct(data);
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : "Unknown error");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (productId) {
|
async function loadProduct() {
|
||||||
fetch();
|
if (!productId) {
|
||||||
}
|
setIsLoading(false);
|
||||||
}, [productId]);
|
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 };
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,33 +1,115 @@
|
|||||||
import { useState, useEffect } from "react";
|
"use client";
|
||||||
|
|
||||||
interface CatalogItem {
|
import { useState, useMemo, useCallback } from "react";
|
||||||
id: string;
|
import { useRouter } from "next/navigation";
|
||||||
name: string;
|
import { useProducts } from "./useProducts";
|
||||||
price: number;
|
import type { Product } from "@/lib/api/product";
|
||||||
category: string;
|
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 = () => {
|
export function useProductCatalog(options: UseProductCatalogOptions = {}) {
|
||||||
const [items, setItems] = useState<CatalogItem[]>([]);
|
const { basePath = "/shop" } = options;
|
||||||
const [loading, setLoading] = useState(true);
|
const router = useRouter();
|
||||||
const [error, setError] = useState<string | null>(null);
|
const { products: fetchedProducts, isLoading } = useProducts();
|
||||||
|
|
||||||
useEffect(() => {
|
const [search, setSearch] = useState("");
|
||||||
// Fetch catalog items
|
const [category, setCategory] = useState("All");
|
||||||
const fetchCatalog = async () => {
|
const [sort, setSort] = useState<SortOption>("Newest");
|
||||||
try {
|
|
||||||
setLoading(true);
|
const handleProductClick = useCallback((productId: string) => {
|
||||||
// Simulated fetch
|
router.push(`${basePath}/${productId}`);
|
||||||
setItems([]);
|
}, [router, basePath]);
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : "Unknown error");
|
const catalogProducts: CatalogProduct[] = useMemo(() => {
|
||||||
} finally {
|
if (fetchedProducts.length === 0) return [];
|
||||||
setLoading(false);
|
|
||||||
}
|
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 };
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,35 +1,196 @@
|
|||||||
import { useState, useEffect } from "react";
|
"use client";
|
||||||
|
|
||||||
interface ProductDetail {
|
import { useState, useMemo, useCallback } from "react";
|
||||||
id: string;
|
import { useProduct } from "./useProduct";
|
||||||
name: string;
|
import type { Product } from "@/lib/api/product";
|
||||||
price: number;
|
import type { ProductVariant } from "@/components/ecommerce/productDetail/ProductDetailCard";
|
||||||
description: string;
|
import type { ExtendedCartItem } from "./useCart";
|
||||||
|
|
||||||
|
interface ProductImage {
|
||||||
|
src: string;
|
||||||
|
alt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useProductDetail = (productId: string) => {
|
interface ProductMeta {
|
||||||
const [product, setProduct] = useState<ProductDetail | null>(null);
|
salePrice?: string;
|
||||||
const [loading, setLoading] = useState(true);
|
ribbon?: string;
|
||||||
const [error, setError] = useState<string | null>(null);
|
inventoryStatus?: string;
|
||||||
|
inventoryQuantity?: number;
|
||||||
|
sku?: string;
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
export function useProductDetail(productId: string) {
|
||||||
// Fetch product details
|
const { product, isLoading, error } = useProduct(productId);
|
||||||
const fetchProduct = async () => {
|
const [selectedQuantity, setSelectedQuantity] = useState(1);
|
||||||
try {
|
const [selectedVariants, setSelectedVariants] = useState<Record<string, string>>({});
|
||||||
setLoading(true);
|
|
||||||
// Simulated fetch
|
const images = useMemo<ProductImage[]>(() => {
|
||||||
setProduct(null);
|
if (!product) return [];
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : "Unknown error");
|
if (product.images && product.images.length > 0) {
|
||||||
} finally {
|
return product.images.map((src, index) => ({
|
||||||
setLoading(false);
|
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 };
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,26 +1,39 @@
|
|||||||
import { useState, useEffect } from "react";
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import { Product, fetchProducts } from "@/lib/api/product";
|
import { Product, fetchProducts } from "@/lib/api/product";
|
||||||
|
|
||||||
export const useProducts = () => {
|
export function useProducts() {
|
||||||
const [products, setProducts] = useState<Product[]>([]);
|
const [products, setProducts] = useState<Product[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetch = async () => {
|
let isMounted = true;
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const data = await fetchProducts();
|
|
||||||
setProducts(data);
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : "Unknown error");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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 };
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,38 +1,219 @@
|
|||||||
export interface Product {
|
export type Product = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
price: number;
|
price: string;
|
||||||
description?: string;
|
imageSrc: string;
|
||||||
imageSrc?: string;
|
imageAlt?: string;
|
||||||
imageAlt?: string;
|
images?: string[];
|
||||||
rating?: number;
|
brand?: string;
|
||||||
reviewCount?: string;
|
variant?: string;
|
||||||
brand?: string;
|
rating?: number;
|
||||||
category?: string;
|
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[]> => {
|
export async function fetchProducts(): Promise<Product[]> {
|
||||||
try {
|
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
|
||||||
const response = await fetch("/api/products");
|
const projectId = process.env.NEXT_PUBLIC_PROJECT_ID;
|
||||||
const data = await response.json();
|
|
||||||
return data;
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to fetch products");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchProduct = async (id: string): Promise<Product | null> => {
|
if (!apiUrl || !projectId) {
|
||||||
try {
|
return [];
|
||||||
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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchProductDetail = async (id: string): Promise<Product | null> => {
|
try {
|
||||||
return fetchProduct(id);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user