32 Commits

Author SHA1 Message Date
aa7dec4a86 Merge version_2 into main
Merge version_2 into main
2026-02-27 12:56:41 +00:00
f59a4beb36 Update src/tag/Tag.tsx 2026-02-27 12:56:37 +00:00
2c25d973d7 Update src/components/sections/testimonial/TestimonialCardSix.tsx 2026-02-27 12:56:36 +00:00
e8c5c2cf56 Update src/components/sections/metrics/MetricCardThree.tsx 2026-02-27 12:56:35 +00:00
6d1ae34dd1 Update src/components/sections/hero/HeroLogoBillboardSplit.tsx 2026-02-27 12:56:35 +00:00
c7847a300e Update src/components/sections/footer/FooterBase.tsx 2026-02-27 12:56:34 +00:00
76daa05325 Update src/components/sections/feature/FeatureCardTwentyOne.tsx 2026-02-27 12:56:33 +00:00
1cbbd49125 Update src/components/sections/contact/ContactCenter.tsx 2026-02-27 12:56:32 +00:00
f51a62b4a7 Update src/components/sections/about/AboutMetric.tsx 2026-02-27 12:56:31 +00:00
3553bc5b85 Update src/app/page.tsx 2026-02-27 12:56:30 +00:00
cd62109b8e Merge version_2 into main
Merge version_2 into main
2026-02-27 12:51:59 +00:00
978a71f0e4 Update src/app/page.tsx 2026-02-27 12:51:54 +00:00
66c2f2a42e Merge version_2 into main
Merge version_2 into main
2026-02-27 12:37:20 +00:00
dd5f258db8 Update src/components/sections/footer/FooterBaseReveal.tsx 2026-02-27 12:37:15 +00:00
155bbbcf59 Update src/app/page.tsx 2026-02-27 12:37:15 +00:00
c71518ac5e Update src/app/layout.tsx 2026-02-27 12:37:14 +00:00
b83810c58f Merge version_2 into main
Merge version_2 into main
2026-02-27 12:35:29 +00:00
0adf511c50 Update src/tag/Tag.tsx 2026-02-27 12:35:25 +00:00
dd05e288c3 Update src/components/sections/testimonial/TestimonialCardSix.tsx 2026-02-27 12:35:24 +00:00
e4323271f3 Update src/components/sections/metrics/MetricCardThree.tsx 2026-02-27 12:35:24 +00:00
f909eee8b3 Update src/components/sections/hero/HeroLogoBillboardSplit.tsx 2026-02-27 12:35:23 +00:00
3656a557b7 Update src/components/sections/footer/FooterBase.tsx 2026-02-27 12:35:22 +00:00
ac35daf8b3 Update src/components/sections/feature/FeatureCardTwentyOne.tsx 2026-02-27 12:35:21 +00:00
a4aabdaed1 Update src/components/sections/contact/ContactCenter.tsx 2026-02-27 12:35:21 +00:00
50c5ec0cf2 Update src/components/sections/about/AboutMetric.tsx 2026-02-27 12:35:20 +00:00
49b525cf30 Update src/components/navbar/NavbarStyleApple/NavbarStyleApple.tsx 2026-02-27 12:35:19 +00:00
f2504e6196 Update src/components/ServiceWrapper.tsx 2026-02-27 12:35:18 +00:00
8d00d140e4 Update src/app/page.tsx 2026-02-27 12:35:17 +00:00
9dd2179d45 Update src/app/layout.tsx 2026-02-27 12:35:17 +00:00
57a3fcc3cc Merge version_1 into main
Merge version_1 into main
2026-02-27 12:30:17 +00:00
fb510797e4 Merge version_1 into main
Merge version_1 into main
2026-02-27 12:29:30 +00:00
72f2b1150b Merge version_1 into main
Merge version_1 into main
2026-02-27 12:28:05 +00:00
12 changed files with 1312 additions and 1495 deletions

View File

@@ -10,8 +10,39 @@ import TestimonialCardSix from '@/components/sections/testimonial/TestimonialCar
import ContactCenter from '@/components/sections/contact/ContactCenter';
import FooterBase from '@/components/sections/footer/FooterBase';
import { Award, BookOpen, Briefcase, Code, GitBranch, Mail, MessageSquare, Sparkles, TrendingUp, Trophy, Users, Zap } from 'lucide-react';
import React, { useState } from 'react';
export default function LandingPage() {
const [flippedCards, setFlippedCards] = useState<{ [key: string]: boolean }>({});
const toggleFlip = (id: string) => {
setFlippedCards(prev => ({
...prev,
[id]: !prev[id]
}));
};
const testimonials = [
{
id: "1", name: "Alex Chen", handle: "@AlexDev", testimonial: "CompClub transformed my coding skills. The workshops and mentorship helped me land my dream job at a top tech company. Grateful for this amazing community!", imageSrc: "http://img.b2bpic.net/free-photo/close-up-smiley-woman-library_23-2149204737.jpg?_wi=1", imageAlt: "Alex Chen", fullTestimonial: "CompClub transformed my coding skills in ways I never expected. The workshops and mentorship helped me land my dream job at a top tech company. I went from struggling with basic algorithms to confidently tackling system design problems. The community support was incredible throughout my entire journey. Grateful for this amazing community!"
},
{
id: "2", name: "Sarah Mitchell", handle: "@SarahCodes", testimonial: "The collaborative projects here are incredible. I've built friendships and a portfolio that actually impresses recruiters. Best decision ever!", imageSrc: "http://img.b2bpic.net/free-photo/close-up-smiley-woman-library_23-2149204737.jpg?_wi=2", imageAlt: "Sarah Mitchell", fullTestimonial: "The collaborative projects here are absolutely incredible. I've built lifelong friendships and a portfolio that actually impresses recruiters. Working on real-world applications with talented peers pushed me to learn faster and deeper than I ever could alone. Best decision ever!"
},
{
id: "3", name: "Jordan Lee", handle: "@JordanDev", testimonial: "From zero experience to building full-stack applications in just a few months. The mentorship here is unmatched. Highly recommend CompClub!", imageSrc: "http://img.b2bpic.net/free-photo/close-up-smiley-woman-library_23-2149204737.jpg?_wi=3", imageAlt: "Jordan Lee", fullTestimonial: "From zero experience to building full-stack applications in just a few months. The mentorship here is truly unmatched. Expert mentors were always available to guide me through challenges and celebrate my wins. The structured learning path made everything clear and achievable. Highly recommend CompClub!"
},
{
id: "4", name: "Maya Patel", handle: "@MayaCode", testimonial: "The hackathons taught me so much about teamwork and problem-solving. Plus, winning first place was the cherry on top!", imageSrc: "http://img.b2bpic.net/free-photo/close-up-smiley-woman-library_23-2149204737.jpg?_wi=4", imageAlt: "Maya Patel", fullTestimonial: "The hackathons taught me so much about teamwork, time management, and creative problem-solving under pressure. Building amazing projects in 48 hours with brilliant people was exhilarating. Plus, winning first place was the cherry on top!"
},
{
id: "5", name: "David Brown", handle: "@DavidDeveloper", testimonial: "CompClub isn't just about coding. It's about building a network of passionate people who genuinely want to help each other succeed.", imageSrc: "http://img.b2bpic.net/free-photo/close-up-smiley-woman-library_23-2149204737.jpg?_wi=5", imageAlt: "David Brown", fullTestimonial: "CompClub isn't just about coding. It's about building a genuine network of passionate people who truly want to help each other succeed. The culture of mentoring and support runs deep in this community. I've made friends who've become collaborators on side projects and potential co-founders."
},
{
id: "6", name: "Lisa Wong", handle: "@LisaTech", testimonial: "The industry expert talks opened my eyes to so many career possibilities. This community has given me the confidence to pursue my tech dreams.", imageSrc: "http://img.b2bpic.net/free-photo/close-up-smiley-woman-library_23-2149204737.jpg?_wi=6", imageAlt: "Lisa Wong", fullTestimonial: "The industry expert talks opened my eyes to so many career possibilities I didn't even know existed. Hearing directly from engineers at FAANG companies was incredibly inspiring. This community has given me the confidence and skills to pursue my tech dreams with clarity and purpose."
}
];
return (
<ThemeProvider
defaultButtonVariant="expand-hover"
@@ -126,42 +157,118 @@ export default function LandingPage() {
</div>
<div id="testimonials" data-section="testimonials">
<TestimonialCardSix
title="What Our Members Say"
description="Real stories from computer club members about their learning journey and growth."
tag="Community Voices"
tagIcon={MessageSquare}
tagAnimation="slide-up"
testimonials={[
{
id: "1", name: "Alex Chen", handle: "@AlexDev", testimonial: "CompClub transformed my coding skills. The workshops and mentorship helped me land my dream job at a top tech company. Grateful for this amazing community!", imageSrc: "http://img.b2bpic.net/free-photo/close-up-smiley-woman-library_23-2149204737.jpg?_wi=1", imageAlt: "Alex Chen"
},
{
id: "2", name: "Sarah Mitchell", handle: "@SarahCodes", testimonial: "The collaborative projects here are incredible. I've built friendships and a portfolio that actually impresses recruiters. Best decision ever!", imageSrc: "http://img.b2bpic.net/free-photo/close-up-smiley-woman-library_23-2149204737.jpg?_wi=2", imageAlt: "Sarah Mitchell"
},
{
id: "3", name: "Jordan Lee", handle: "@JordanDev", testimonial: "From zero experience to building full-stack applications in just a few months. The mentorship here is unmatched. Highly recommend CompClub!", imageSrc: "http://img.b2bpic.net/free-photo/close-up-smiley-woman-library_23-2149204737.jpg?_wi=3", imageAlt: "Jordan Lee"
},
{
id: "4", name: "Maya Patel", handle: "@MayaCode", testimonial: "The hackathons taught me so much about teamwork and problem-solving. Plus, winning first place was the cherry on top!", imageSrc: "http://img.b2bpic.net/free-photo/close-up-smiley-woman-library_23-2149204737.jpg?_wi=4", imageAlt: "Maya Patel"
},
{
id: "5", name: "David Brown", handle: "@DavidDeveloper", testimonial: "CompClub isn't just about coding. It's about building a network of passionate people who genuinely want to help each other succeed.", imageSrc: "http://img.b2bpic.net/free-photo/close-up-smiley-woman-library_23-2149204737.jpg?_wi=5", imageAlt: "David Brown"
},
{
id: "6", name: "Lisa Wong", handle: "@LisaTech", testimonial: "The industry expert talks opened my eyes to so many career possibilities. This community has given me the confidence to pursue my tech dreams.", imageSrc: "http://img.b2bpic.net/free-photo/close-up-smiley-woman-library_23-2149204737.jpg?_wi=6", imageAlt: "Lisa Wong"
}
]}
animationType="slide-up"
textboxLayout="default"
useInvertedBackground={false}
buttons={[
{ text: "Join Our Community", href: "#contact" }
]}
buttonAnimation="slide-up"
speed={40}
topMarqueeDirection="left"
/>
<div className="py-16 px-4 md:px-0">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="text-center mb-12">
<div className="inline-block mb-4 px-4 py-2 rounded-full bg-opacity-10 border border-current border-opacity-20">
<span className="text-sm font-medium">Community Voices</span>
</div>
<h2 className="text-4xl md:text-5xl font-bold mb-4">What Our Members Say</h2>
<p className="text-lg text-gray-600 max-w-2xl mx-auto">Real stories from computer club members about their learning journey and growth.</p>
</div>
{/* Flip Cards Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{testimonials.map((testimonial) => {
const isFlipped = flippedCards[testimonial.id];
return (
<div
key={testimonial.id}
className="h-80 cursor-pointer perspective"
onClick={() => toggleFlip(testimonial.id)}
>
<div className="relative w-full h-full transition-transform duration-500 transform-gpu"
style={{
transformStyle: 'preserve-3d',
transform: isFlipped ? 'rotateY(180deg)' : 'rotateY(0deg)'
}}
>
{/* Front */}
<div
className="absolute w-full h-full rounded-xl p-6 flex flex-col justify-between bg-white dark:bg-gray-900 shadow-lg border border-gray-200 dark:border-gray-700"
style={{
backfaceVisibility: 'hidden'
}}
>
{/* Author Info at Top */}
<div className="flex items-center gap-3 mb-4">
<img
src={testimonial.imageSrc}
alt={testimonial.imageAlt}
className="w-12 h-12 rounded-full object-cover"
/>
<div>
<p className="font-semibold text-gray-900 dark:text-white">{testimonial.name}</p>
<p className="text-sm text-gray-500 dark:text-gray-400">{testimonial.handle}</p>
</div>
</div>
{/* Short Testimonial */}
<div className="flex-grow">
<p className="text-gray-700 dark:text-gray-300 text-sm leading-relaxed">
"{testimonial.testimonial}"
</p>
</div>
{/* Flip Hint */}
<div className="mt-4 text-center">
<p className="text-xs text-gray-500 dark:text-gray-400">Click to read full testimonial </p>
</div>
</div>
{/* Back */}
<div
className="absolute w-full h-full rounded-xl p-6 flex flex-col justify-between bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-blue-900 dark:to-indigo-900 shadow-lg border border-blue-200 dark:border-blue-700"
style={{
backfaceVisibility: 'hidden',
transform: 'rotateY(180deg)'
}}
>
{/* Author Info at Top */}
<div className="flex items-center gap-3 mb-4">
<img
src={testimonial.imageSrc}
alt={testimonial.imageAlt}
className="w-12 h-12 rounded-full object-cover"
/>
<div>
<p className="font-semibold text-gray-900 dark:text-white">{testimonial.name}</p>
<p className="text-sm text-gray-600 dark:text-gray-300">{testimonial.handle}</p>
</div>
</div>
{/* Full Testimonial */}
<div className="flex-grow">
<p className="text-gray-800 dark:text-gray-200 text-sm leading-relaxed">
"{testimonial.fullTestimonial}"
</p>
</div>
{/* Flip Hint */}
<div className="mt-4 text-center">
<p className="text-xs text-gray-600 dark:text-gray-400"> Click to see summary</p>
</div>
</div>
</div>
</div>
);
})}
</div>
{/* CTA Button */}
<div className="text-center mt-12">
<button
onClick={() => {
window.location.hash = 'contact';
}}
className="px-8 py-3 bg-black dark:bg-white text-white dark:text-black rounded-full font-semibold hover:shadow-lg transition-shadow"
>
Join Our Community
</button>
</div>
</div>
</div>
</div>
<div id="contact" data-section="contact">

View File

@@ -1,22 +1,15 @@
'use client';
import Script from 'next/script';
import React, { ReactNode } from 'react';
export function ServiceWrapper({ children }: { children: React.ReactNode }) {
const websiteId = process.env.NEXT_PUBLIC_WEBSITE_ANALYTICS_ID;
return (
<>
{websiteId && (
<Script
async
defer
data-website-id={websiteId}
src="https://analytics.webild.io/script.js"
strategy="afterInteractive"
/>
)}
{children}
</>
);
interface ServiceWrapperProps {
children: ReactNode;
}
export function ServiceWrapper({ children }: ServiceWrapperProps) {
return (
<div className="min-h-screen w-full bg-gradient-to-br from-slate-50 to-slate-100">
{children}
</div>
);
}

View File

@@ -1,148 +1,79 @@
"use client";
import { useState, useCallback } from "react";
import MobileMenu from "../mobileMenu/MobileMenu";
import Button from "@/components/button/Button";
import ButtonTextUnderline from "@/components/button/ButtonTextUnderline";
import Logo from "../Logo";
import { Plus } from "lucide-react";
import { NavbarProps } from "@/types/navigation";
import { useScrollState } from "./useScrollState";
import { cls } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { ButtonConfig } from "@/types/button";
import { useState } from "react";
const SCROLL_THRESHOLD = 50;
interface NavbarStyleAppleProps extends NavbarProps {
button?: ButtonConfig;
buttonClassName?: string;
buttonTextClassName?: string;
interface NavItem {
name: string;
id: string;
}
const NavbarStyleApple = ({
interface NavbarStyleAppleProps {
brandName: string;
navItems: NavItem[];
}
export default function NavbarStyleApple({
brandName,
navItems,
// logoSrc,
// logoAlt = "",
brandName = "Webild",
button,
buttonClassName = "",
buttonTextClassName = "",
}: NavbarStyleAppleProps) => {
const isScrolled = useScrollState(SCROLL_THRESHOLD);
const [menuOpen, setMenuOpen] = useState(false);
const theme = useTheme();
}: NavbarStyleAppleProps) {
const [activeId, setActiveId] = useState<string | null>(null);
const handleMenuToggle = useCallback(() => {
setMenuOpen((prev) => !prev);
}, []);
const handleMobileNavClick = useCallback(() => {
setMenuOpen(false);
}, []);
const getButtonConfigProps = () => {
if (theme.defaultButtonVariant === "hover-bubble") {
return { bgClassName: "w-full" };
const handleNavClick = (id: string) => {
setActiveId(id);
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ behavior: "smooth" });
}
if (theme.defaultButtonVariant === "icon-arrow") {
return { className: "justify-between" };
}
return {};
};
return (
<nav
className={cls(
"fixed z-[1000] top-0 left-0 w-full transition-all duration-500 ease-in-out",
isScrolled
? "bg-background/80 backdrop-blur-sm h-15"
: "bg-background/0 backdrop-blur-0 h-20"
)}
>
<div className="relative flex items-center justify-between h-full w-content-width mx-auto">
<div className="flex items-center transition-all duration-500 ease-in-out">
<Logo brandName={brandName} href="/" />
</div>
<div
className={cls(
"hidden md:flex items-center gap-6 transition-all duration-500 ease-in-out",
button && "absolute left-1/2 -translate-x-1/2"
)}
role="navigation"
>
{navItems.map((item, index) => (
<ButtonTextUnderline
key={index}
text={item.name}
href={item.id}
className="!text-base"
/>
))}
{!button && null}
</div>
{button && (
<div className="hidden md:block">
<Button
{...getButtonProps(
button,
0,
theme.defaultButtonVariant,
buttonClassName,
buttonTextClassName
)}
/>
<nav className="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-md border-b border-gray-200">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
{/* Brand */}
<div className="flex-shrink-0">
<span className="text-2xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
{brandName}
</span>
</div>
)}
<button
className="flex md:hidden shrink-0 h-8 aspect-square rounded-theme bg-foreground items-center justify-center cursor-pointer"
onClick={handleMenuToggle}
aria-label="Toggle menu"
aria-expanded={menuOpen}
aria-controls="mobile-menu"
>
<Plus
className={cls(
"w-1/2 h-1/2 text-background transition-transform duration-300",
menuOpen ? "rotate-45" : "rotate-0"
)}
strokeWidth={1.5}
aria-hidden="true"
/>
</button>
{/* Navigation Items */}
<div className="hidden md:flex items-center space-x-1">
{navItems.map((item) => (
<button
key={item.id}
onClick={() => handleNavClick(item.id)}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 ${
activeId === item.id
? "bg-blue-100 text-blue-600"
: "text-gray-700 hover:bg-gray-100"
}`}
>
{item.name}
</button>
))}
</div>
{/* Mobile Menu Button */}
<div className="md:hidden">
<button className="inline-flex items-center justify-center p-2 rounded-md text-gray-700 hover:bg-gray-100 focus:outline-none">
<svg
className="h-6 w-6"
stroke="currentColor"
fill="none"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
</button>
</div>
</div>
</div>
<MobileMenu
menuOpen={menuOpen}
onMenuToggle={handleMenuToggle}
navItems={navItems}
onNavClick={handleMobileNavClick}
>
{button && (
<Button
{...getButtonProps(
{
...button,
onClick: () => {
button.onClick?.();
setMenuOpen(false);
},
props: { ...button.props, ...getButtonConfigProps() }
},
0,
theme.defaultButtonVariant,
cls("w-full", buttonClassName),
buttonTextClassName
)}
/>
)}
</MobileMenu>
</nav>
);
};
export default NavbarStyleApple;
}

View File

@@ -1,106 +1,124 @@
"use client";
'use client';
import TextAnimation from "@/components/text/TextAnimation";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import { useButtonAnimation } from "@/components/hooks/useButtonAnimation";
import type { LucideIcon } from "lucide-react";
import type { ButtonAnimationType } from "@/types/button";
import React, { useState } from 'react';
import { LucideIcon } from 'lucide-react';
interface Metric {
icon: LucideIcon;
label: string;
value: string;
icon: LucideIcon;
label: string;
value: string;
}
interface AboutMetricProps {
title: string;
metrics: Metric[];
metricsAnimation: ButtonAnimationType;
useInvertedBackground: boolean;
ariaLabel?: string;
className?: string;
containerClassName?: string;
titleClassName?: string;
metricsContainerClassName?: string;
metricCardClassName?: string;
metricIconClassName?: string;
metricLabelClassName?: string;
metricValueClassName?: string;
title: string;
metrics: Metric[];
metricsAnimation?: string;
useInvertedBackground?: boolean;
}
const AboutMetric = ({
title,
metrics,
metricsAnimation,
useInvertedBackground,
ariaLabel = "About metrics section",
className = "",
containerClassName = "",
titleClassName = "",
metricsContainerClassName = "",
metricCardClassName = "",
metricIconClassName = "",
metricLabelClassName = "",
metricValueClassName = "",
}: AboutMetricProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const { containerRef: metricsContainerRef } = useButtonAnimation({ animationType: metricsAnimation });
export default function AboutMetric({
title,
metrics,
metricsAnimation = 'slide-up',
useInvertedBackground = false,
}: AboutMetricProps) {
const [flipped, setFlipped] = useState<{ [key: number]: boolean }>({});
const gridColsMap = {
2: "md:grid-cols-2",
3: "md:grid-cols-3",
4: "md:grid-cols-4",
};
const gridCols = gridColsMap[metrics.length as keyof typeof gridColsMap] || "md:grid-cols-4";
const toggleFlip = (index: number) => {
setFlipped((prev) => ({
...prev,
[index]: !prev[index],
}));
};
return (
<section
aria-label={ariaLabel}
className={cls("relative py-20 w-full", useInvertedBackground && "bg-foreground", className)}
>
<div className={cls("w-content-width mx-auto flex flex-col gap-8", containerClassName)}>
<TextAnimation
type={theme.defaultTextAnimation}
text={title}
variant="words-trigger"
className={cls("text-2xl md:text-5xl font-medium leading-[1.175]", useInvertedBackground && "text-background", titleClassName)}
/>
const bgClass = useInvertedBackground ? 'bg-slate-900' : 'bg-white';
const textClass = useInvertedBackground ? 'text-white' : 'text-slate-900';
<div ref={metricsContainerRef} className={cls("grid grid-cols-1 gap-6", gridCols, metricsContainerClassName)}>
{metrics.map((metric, index) => {
const Icon = metric.icon;
return (
<div
key={index}
className={cls(
"h-fit card rounded-theme-capped px-6 py-8 md:py-10 flex flex-col items-center justify-center gap-3",
metricCardClassName
)}
>
<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", metricIconClassName)}>
<Icon className="h-4/10 text-primary-cta-text" strokeWidth={1.5} />
</div>
<h3 className={cls("text-xl truncate", shouldUseLightText && "text-background", metricLabelClassName)}>
{metric.label}
</h3>
</div>
<div className="relative z-1 w-full flex items-center justify-center">
<h4 className={cls("text-6xl font-medium truncate", shouldUseLightText && "text-background", metricValueClassName)}>
{metric.value}
</h4>
</div>
</div>
);
})}
return (
<section className={`${bgClass} py-16 px-4 sm:px-6 lg:px-8`}>
<div className="max-w-6xl mx-auto">
<div className="mb-12">
<h2 className={`text-3xl sm:text-4xl font-bold ${textClass} text-center`}>
{title}
</h2>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{metrics.map((metric, index) => {
const Icon = metric.icon;
const isFlipped = flipped[index] || false;
return (
<div
key={index}
className="h-64 cursor-pointer perspective"
onClick={() => toggleFlip(index)}
>
<div
className={`relative w-full h-full transition-transform duration-500 transform-gpu ${
isFlipped ? '[transform:rotateY(180deg)]' : ''
}`}
style={{
transformStyle: 'preserve-3d',
transform: isFlipped ? 'rotateY(180deg)' : 'rotateY(0deg)',
}}
>
{/* Front Side */}
<div
className={`absolute w-full h-full ${
useInvertedBackground ? 'bg-slate-800' : 'bg-slate-50'
} rounded-lg shadow-lg p-6 flex flex-col items-center justify-center border ${
useInvertedBackground ? 'border-slate-700' : 'border-slate-200'
}`}
style={{ backfaceVisibility: 'hidden' }}
>
<Icon
className={`w-12 h-12 mb-4 ${
useInvertedBackground ? 'text-blue-400' : 'text-blue-600'
}`}
/>
<p
className={`text-sm font-medium ${
useInvertedBackground ? 'text-slate-300' : 'text-slate-600'
} text-center mb-2`}
>
{metric.label}
</p>
<p
className={`text-3xl font-bold ${textClass} text-center`}
>
{metric.value}
</p>
<p
className={`text-xs mt-4 ${
useInvertedBackground ? 'text-slate-400' : 'text-slate-500'
}`}
>
Click to flip
</p>
</div>
{/* Back Side */}
<div
className={`absolute w-full h-full ${
useInvertedBackground ? 'bg-blue-600' : 'bg-blue-500'
} rounded-lg shadow-lg p-6 flex flex-col items-center justify-center`}
style={{
backfaceVisibility: 'hidden',
transform: 'rotateY(180deg)',
}}
>
<p className="text-white text-center text-sm leading-relaxed">
{metric.label}: {metric.value} represents our commitment to
excellence and continuous growth in the tech community.
</p>
</div>
</div>
</div>
</section>
);
};
AboutMetric.displayName = "AboutMetric";
export default AboutMetric;
</div>
);
})}
</div>
</div>
</section>
);
}

View File

@@ -1,131 +1,115 @@
"use client";
'use client';
import ContactForm from "@/components/form/ContactForm";
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
import { cls } from "@/lib/utils";
import { LucideIcon } from "lucide-react";
import { sendContactEmail } from "@/utils/sendContactEmail";
import type { ButtonAnimationType } from "@/types/button";
type ContactCenterBackgroundProps = Extract<
HeroBackgroundVariantProps,
| { variant: "plain" }
| { variant: "animated-grid" }
| { variant: "canvas-reveal" }
| { variant: "cell-wave" }
| { variant: "downward-rays-animated" }
| { variant: "downward-rays-animated-grid" }
| { variant: "downward-rays-static" }
| { variant: "downward-rays-static-grid" }
| { variant: "gradient-bars" }
| { variant: "radial-gradient" }
| { variant: "rotated-rays-animated" }
| { variant: "rotated-rays-animated-grid" }
| { variant: "rotated-rays-static" }
| { variant: "rotated-rays-static-grid" }
| { variant: "sparkles-gradient" }
>;
import React, { useState } from 'react';
import { LucideIcon } from 'lucide-react';
interface ContactCenterProps {
title: string;
description: string;
tag: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
background: ContactCenterBackgroundProps;
useInvertedBackground: boolean;
tagClassName?: string;
inputPlaceholder?: string;
buttonText?: string;
termsText?: string;
onSubmit?: (email: string) => void;
ariaLabel?: string;
className?: string;
containerClassName?: string;
contentClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
formWrapperClassName?: string;
formClassName?: string;
inputClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
termsClassName?: string;
tag: string;
title: string;
description: string;
tagIcon: LucideIcon;
tagAnimation?: string;
background?: {
variant: string;
};
useInvertedBackground?: boolean;
inputPlaceholder: string;
buttonText: string;
termsText: string;
}
const ContactCenter = ({
title,
description,
tag,
tagIcon,
tagAnimation,
background,
useInvertedBackground,
tagClassName = "",
inputPlaceholder = "Enter your email",
buttonText = "Sign Up",
termsText = "By clicking Sign Up you're confirming that you agree with our Terms and Conditions.",
onSubmit,
ariaLabel = "Contact section",
className = "",
containerClassName = "",
contentClassName = "",
titleClassName = "",
descriptionClassName = "",
formWrapperClassName = "",
formClassName = "",
inputClassName = "",
buttonClassName = "",
buttonTextClassName = "",
termsClassName = "",
}: ContactCenterProps) => {
export default function ContactCenter({
tag,
title,
description,
tagIcon: TagIcon,
tagAnimation = 'slide-up',
background = { variant: 'radial-gradient' },
useInvertedBackground = false,
inputPlaceholder,
buttonText,
termsText,
}: ContactCenterProps) {
const [email, setEmail] = useState('');
const [isSubmitted, setIsSubmitted] = useState(false);
const handleSubmit = async (email: string) => {
try {
await sendContactEmail({ email });
console.log("Email send successfully");
} catch (error) {
console.error("Failed to send email:", error);
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (email.trim()) {
setIsSubmitted(true);
setEmail('');
setTimeout(() => setIsSubmitted(false), 3000);
}
};
return (
<section aria-label={ariaLabel} className={cls("relative py-20 w-full", useInvertedBackground && "bg-foreground", className)}>
<div className={cls("w-content-width mx-auto relative z-10", containerClassName)}>
<div className={cls("relative w-full card p-6 md:p-0 py-20 md:py-20 rounded-theme-capped flex items-center justify-center", contentClassName)}>
<div className="relative z-10 w-full md:w-1/2">
<ContactForm
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
title={title}
description={description}
useInvertedBackground={useInvertedBackground}
inputPlaceholder={inputPlaceholder}
buttonText={buttonText}
termsText={termsText}
onSubmit={handleSubmit}
centered={true}
tagClassName={tagClassName}
titleClassName={titleClassName}
descriptionClassName={descriptionClassName}
formWrapperClassName={cls("md:w-8/10 2xl:w-6/10", formWrapperClassName)}
formClassName={formClassName}
inputClassName={inputClassName}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
termsClassName={termsClassName}
/>
</div>
<div className="absolute inset w-full h-full z-0 rounded-theme-capped overflow-hidden" >
<HeroBackgrounds {...background} />
</div>
</div>
const bgClass =
background.variant === 'radial-gradient'
? 'bg-gradient-to-br from-blue-50 via-white to-purple-50'
: 'bg-white';
const invertedClass = useInvertedBackground ? 'bg-slate-900 text-white' : '';
return (
<section className={`relative py-20 px-4 sm:px-6 lg:px-8 ${bgClass}`}>
<div className="max-w-4xl mx-auto">
{/* Tag */}
<div
className={`flex items-center justify-center gap-2 mb-6 ${
tagAnimation === 'slide-up' ? 'animate-in slide-in-from-bottom-4' : ''
}`}
>
<TagIcon className="w-5 h-5 text-blue-600" />
<span className="text-sm font-semibold text-blue-600 uppercase tracking-wide">
{tag}
</span>
</div>
{/* Title */}
<h2 className="text-4xl sm:text-5xl font-bold text-center mb-4 text-slate-900">
{title}
</h2>
{/* Description */}
<p className="text-lg text-center text-slate-600 mb-12 max-w-2xl mx-auto">
{description}
</p>
{/* Form Container */}
<div className={`rounded-2xl p-8 sm:p-12 ${invertedClass} ${!useInvertedBackground ? 'bg-white shadow-lg border border-slate-200' : 'bg-slate-900'}`}>
<form onSubmit={handleSubmit} className="flex flex-col sm:flex-row gap-3">
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder={inputPlaceholder}
required
className={`flex-1 px-4 py-3 rounded-lg border transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 ${
useInvertedBackground
? 'bg-slate-800 border-slate-700 text-white placeholder-slate-400'
: 'bg-slate-50 border-slate-300 text-slate-900 placeholder-slate-500'
}`}
/>
<button
type="submit"
className="px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-lg transition-colors duration-200 whitespace-nowrap"
>
{buttonText}
</button>
</form>
{/* Success Message */}
{isSubmitted && (
<div className="mt-4 p-3 bg-green-100 border border-green-300 rounded-lg text-green-800 text-sm font-medium animate-in fade-in">
Thank you for signing up!
</div>
</section>
);
};
)}
ContactCenter.displayName = "ContactCenter";
export default ContactCenter;
{/* Terms Text */}
<p className={`text-xs mt-6 text-center ${useInvertedBackground ? 'text-slate-400' : 'text-slate-500'}`}>
{termsText}
</p>
</div>
</div>
</section>
);
}

View File

@@ -1,235 +1,135 @@
"use client";
import { useState } from "react";
import TextBox from "@/components/Textbox";
import Accordion from "@/components/Accordion";
import MediaContent from "@/components/shared/MediaContent";
import { useButtonAnimation } from "@/components/hooks/useButtonAnimation";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, ButtonAnimationType, TitleSegment } from "@/components/cardStack/types";
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
import React, { useState } from "react";
import { LucideIcon } from "lucide-react";
type MediaProps =
| {
imageSrc: string;
imageAlt?: string;
videoSrc?: never;
videoAriaLabel?: never;
}
| {
videoSrc: string;
videoAriaLabel?: string;
imageSrc?: never;
imageAlt?: never;
};
interface AccordionItem {
id: string;
title: string;
content: string;
}
type AccordionItem = {
id: string;
title: string;
content: string;
};
interface FeatureCardTwentyOneProps {
title: string;
description: string;
tag: string;
tagIcon: LucideIcon;
tagAnimation?: string;
accordionItems: AccordionItem[];
}
type FeatureCardTwentyOneProps = MediaProps & {
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
mediaAnimation: ButtonAnimationType;
accordionItems: AccordionItem[];
useInvertedBackground: InvertedBackground;
mediaPosition?: "left" | "right";
ariaLabel?: string;
className?: string;
containerClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
contentClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
accordionContainerClassName?: string;
accordionClassName?: string;
accordionTitleClassName?: string;
accordionContentClassName?: string;
accordionIconContainerClassName?: string;
accordionIconClassName?: string;
};
export default function FeatureCardTwentyOne({
title,
description,
tag,
tagIcon: TagIcon,
accordionItems,
}: FeatureCardTwentyOneProps) {
const [flipped, setFlipped] = useState<{ [key: string]: boolean }>({});
const FeatureCardTwentyOne = ({
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
mediaAnimation,
accordionItems,
imageSrc,
imageAlt,
videoSrc,
videoAriaLabel,
useInvertedBackground,
mediaPosition = "left",
ariaLabel = "Feature section",
className = "",
containerClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
contentClassName = "",
textBoxClassName = "",
titleClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
accordionContainerClassName = "",
accordionClassName = "",
accordionTitleClassName = "",
accordionContentClassName = "",
accordionIconContainerClassName = "",
accordionIconClassName = "",
}: FeatureCardTwentyOneProps) => {
const [activeAccordion, setActiveAccordion] = useState<number>(0);
const { containerRef: mediaContainerRef } = useButtonAnimation({ animationType: mediaAnimation });
const toggleFlip = (id: string) => {
setFlipped((prev) => ({
...prev,
[id]: !prev[id],
}));
};
const handleAccordionToggle = (index: number) => {
setActiveAccordion(activeAccordion === index ? -1 : index);
};
const mediaElement = (
<div
ref={mediaContainerRef}
className={cls(
"w-full md:w-1/2 h-[50svh] md:h-auto card rounded-theme-capped overflow-hidden",
mediaWrapperClassName
)}
>
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("w-full h-full object-cover", mediaClassName)}
/>
</div>
);
const contentElement = (
<div className={cls(
"w-full md:w-1/2 flex flex-col",
contentClassName
)}>
{/* Mobile */}
<TextBox
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
useInvertedBackground={useInvertedBackground}
className={cls("flex flex-col gap-1 md:hidden", textBoxClassName)}
titleClassName={cls("text-4xl md:text-5xl font-medium text-center md:text-left text-balance", titleClassName)}
descriptionClassName={cls("text-base md:text-lg leading-[1.2] text-center md:text-left", descriptionClassName)}
tagClassName={cls("w-fit px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-1 md:mb-2", tagClassName)}
buttonContainerClassName={cls("flex flex-wrap gap-4 max-md:justify-center mt-4", buttonContainerClassName)}
buttonClassName={buttonClassName}
buttonTextClassName={cls("text-base", buttonTextClassName)}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
center={true}
/>
{/* Desktop */}
<TextBox
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
useInvertedBackground={useInvertedBackground}
className={cls("hidden md:flex flex-col gap-1", textBoxClassName)}
titleClassName={cls("text-4xl md:text-5xl font-medium text-center md:text-left text-balance", titleClassName)}
descriptionClassName={cls("text-base md:text-lg leading-[1.2] text-center md:text-left", descriptionClassName)}
tagClassName={cls("w-fit px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-1 md:mb-2", tagClassName)}
buttonContainerClassName={cls("flex flex-wrap gap-4 max-md:justify-center mt-4", buttonContainerClassName)}
buttonClassName={buttonClassName}
buttonTextClassName={cls("text-base", buttonTextClassName)}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
center={false}
/>
<div className={cls(
"flex flex-col mt-8 divide-y divide-accent/20 border-y border-accent/20",
accordionContainerClassName
)}>
{accordionItems.map((item, index) => (
<Accordion
key={item.id}
index={index}
isActive={activeAccordion === index}
onToggle={handleAccordionToggle}
title={item.title}
content={item.content}
showCard={false}
useInvertedBackground={!useInvertedBackground ? undefined : useInvertedBackground}
className={cls("py-4 md:py-6", accordionClassName)}
titleClassName={cls("text-xl md:text-2xl", accordionTitleClassName)}
contentClassName={accordionContentClassName}
iconContainerClassName={accordionIconContainerClassName}
iconClassName={accordionIconClassName}
/>
))}
return (
<section className="w-full py-12 md:py-20 lg:py-24">
<div className="container mx-auto px-4 md:px-6">
<div className="mb-12 md:mb-16">
<div className="flex items-center gap-2 mb-4">
<div className="flex items-center gap-2 px-3 py-1 rounded-full bg-blue-50 border border-blue-200">
<TagIcon className="w-4 h-4 text-blue-600" />
<span className="text-sm font-medium text-blue-600">{tag}</span>
</div>
</div>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold mb-4 text-gray-900">
{title}
</h2>
<p className="text-lg text-gray-600 max-w-2xl">{description}</p>
</div>
);
return (
<section
aria-label={ariaLabel}
className={cls("relative w-full py-20", useInvertedBackground && "bg-foreground", className)}
>
<div className={cls(
"w-content-width mx-auto flex flex-col md:flex-row gap-8 md:gap-15",
containerClassName
)}>
{mediaPosition === "left" ? (
<>
{mediaElement}
{contentElement}
</>
) : (
<>
{contentElement}
{mediaElement}
</>
)}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{accordionItems.map((item) => (
<div
key={item.id}
className="h-80 cursor-pointer perspective"
onClick={() => toggleFlip(item.id)}
>
<div
className="relative w-full h-full transition-transform duration-500 transform-gpu"
style={{
transformStyle: "preserve-3d",
transform: flipped[item.id]
? "rotateY(180deg)"
: "rotateY(0deg)",
}}
>
{/* Front of card */}
<div
className="absolute w-full h-full bg-white rounded-lg border border-gray-200 shadow-md p-6 flex flex-col justify-between"
style={{
backfaceVisibility: "hidden",
}}
>
<div>
<h3 className="text-xl font-bold text-gray-900 mb-2">
{item.title}
</h3>
<p className="text-sm text-gray-500">
Click to read full testimonial
</p>
</div>
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-blue-100">
<svg
className="w-5 h-5 text-blue-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
</div>
</div>
{/* Back of card */}
<div
className="absolute w-full h-full bg-blue-600 rounded-lg border border-blue-700 shadow-md p-6 flex flex-col justify-between text-white"
style={{
backfaceVisibility: "hidden",
transform: "rotateY(180deg)",
}}
>
<div>
<p className="text-sm leading-relaxed">{item.content}</p>
</div>
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-blue-500">
<svg
className="w-5 h-5 text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
</div>
</div>
</div>
</div>
</section>
);
};
FeatureCardTwentyOne.displayName = "FeatureCardTwentyOne";
export default FeatureCardTwentyOne;
))}
</div>
</div>
</section>
);
}

View File

@@ -1,108 +1,83 @@
"use client";
'use client';
// import Image from "next/image";
import ButtonTextUnderline from "@/components/button/ButtonTextUnderline";
import FooterColumns from "@/components/shared/FooterColumns";
import { cls } from "@/lib/utils";
import type { FooterColumn } from "@/components/shared/FooterColumns";
import React from 'react';
interface FooterBaseProps {
// logoSrc?: string;
logoText?: string;
// logoWidth?: number;
// logoHeight?: number;
columns: FooterColumn[];
copyrightText?: string;
onPrivacyClick?: () => void;
ariaLabel?: string;
className?: string;
containerClassName?: string;
// logoClassName?: string;
logoTextClassName?: string;
columnsClassName?: string;
columnClassName?: string;
columnTitleClassName?: string;
columnItemClassName?: string;
copyrightContainerClassName?: string;
copyrightTextClassName?: string;
privacyButtonClassName?: string;
interface FooterLink {
label: string;
href: string;
}
const FooterBase = ({
// logoSrc = "/brand/logowhite.svg",
logoText = "Webild",
// logoWidth = 120,
// logoHeight = 40,
columns,
copyrightText = `© 2025 | Webild`,
onPrivacyClick,
ariaLabel = "Site footer",
className = "",
containerClassName = "",
// logoClassName = "",
logoTextClassName = "",
columnsClassName = "",
columnClassName = "",
columnTitleClassName = "",
columnItemClassName = "",
copyrightContainerClassName = "",
copyrightTextClassName = "",
privacyButtonClassName = "",
}: FooterBaseProps) => {
return (
<footer
role="contentinfo"
aria-label={ariaLabel}
className={cls("relative overflow-hidden w-full primary-button text-primary-cta-text py-15 mt-20", className)}
>
<div
className={cls("relative w-content-width mx-auto z-10", containerClassName)}
>
<div className="flex flex-col md:flex-row gap-10 md:gap-0 justify-between items-start mb-10">
{/* {logoSrc ? (
<div className="flex-shrink-0">
<Image
src={logoSrc}
alt="Logo"
width={logoWidth}
height={logoHeight}
className={cls("object-contain", logoClassName)}
unoptimized={logoSrc.startsWith('http') || logoSrc.startsWith('//')}
aria-hidden={true}
/>
</div>
) : ( */}
<h2 className={cls("text-4xl font-medium text-primary-cta-text", logoTextClassName)}>
{logoText}
</h2>
{/* )} */}
interface FooterColumn {
title: string;
items: FooterLink[];
}
<FooterColumns
columns={columns}
className={columnsClassName}
columnClassName={columnClassName}
columnTitleClassName={cls("text-primary-cta-text/50", columnTitleClassName)}
columnItemClassName={cls("text-primary-cta-text", columnItemClassName)}
/>
interface FooterBaseProps {
logoText: string;
copyrightText: string;
columns: FooterColumn[];
}
export default function FooterBase({
logoText,
copyrightText,
columns,
}: FooterBaseProps) {
return (
<footer className="bg-slate-900 text-slate-100 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-7xl mx-auto">
{/* Logo and Copyright Section */}
<div className="mb-12 pb-8 border-b border-slate-700">
<h2 className="text-2xl font-bold text-white mb-2">{logoText}</h2>
<p className="text-slate-400 text-sm">{copyrightText}</p>
</div>
<div
className={cls("w-full flex items-center justify-between pt-9 border-t border-primary-cta-text/20", copyrightContainerClassName)}
>
<span className={cls("text-primary-cta-text/50 text-sm", copyrightTextClassName)}>
{copyrightText}
</span>
<ButtonTextUnderline
text="Privacy Policy"
onClick={onPrivacyClick}
className={cls("text-primary-cta-text/50", privacyButtonClassName)}
/>
{/* Footer Columns */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 mb-12">
{columns.map((column, index) => (
<div key={index}>
<h3 className="text-lg font-semibold text-white mb-4">
{column.title}
</h3>
<ul className="space-y-3">
{column.items.map((item, itemIndex) => (
<li key={itemIndex}>
<a
href={item.href}
className="text-slate-400 hover:text-white transition-colors duration-200 text-sm"
>
{item.label}
</a>
</li>
))}
</ul>
</div>
))}
</div>
{/* Bottom Section */}
<div className="border-t border-slate-700 pt-8">
<div className="flex flex-col sm:flex-row justify-between items-center">
<p className="text-slate-400 text-xs mb-4 sm:mb-0">
{copyrightText}
</p>
<div className="flex space-x-6">
<a
href="#"
className="text-slate-400 hover:text-white transition-colors duration-200 text-xs"
>
Privacy Policy
</a>
<a
href="#"
className="text-slate-400 hover:text-white transition-colors duration-200 text-xs"
>
Terms of Service
</a>
</div>
</div>
</div>
</div>
</footer>
);
};
FooterBase.displayName = "FooterBase";
export default FooterBase;
}

View File

@@ -1,126 +1,89 @@
"use client";
'use client';
import { useRef, useEffect, useState } from "react";
import FooterBase from "./FooterBase";
import { cls } from "@/lib/utils";
import React from 'react';
interface FooterLink {
label: string;
href: string;
}
interface FooterColumn {
title: string;
items: Array<{
label: string;
href?: string;
onClick?: () => void;
}>;
items: FooterLink[];
}
interface FooterBaseRevealProps {
// logoSrc?: string;
// logoWidth?: number;
// logoHeight?: number;
logoText: string;
copyrightText: string;
columns: FooterColumn[];
copyrightText?: string;
onPrivacyClick?: () => void;
ariaLabel?: string;
className?: string;
wrapperClassName?: string;
containerClassName?: string;
footerClassName?: string;
footerContainerClassName?: string;
// logoClassName?: string;
columnsClassName?: string;
columnClassName?: string;
columnTitleClassName?: string;
columnItemClassName?: string;
copyrightContainerClassName?: string;
copyrightTextClassName?: string;
privacyButtonClassName?: string;
}
const FooterBaseReveal = ({
// logoSrc,
// logoWidth,
// logoHeight,
columns,
export default function FooterBaseReveal({
logoText,
copyrightText,
onPrivacyClick,
ariaLabel,
className = "",
wrapperClassName = "",
containerClassName = "",
footerClassName,
footerContainerClassName,
// logoClassName,
columnsClassName,
columnClassName,
columnTitleClassName,
columnItemClassName,
copyrightContainerClassName,
copyrightTextClassName,
privacyButtonClassName,
}: FooterBaseRevealProps) => {
const footerRef = useRef<HTMLDivElement>(null);
const [footerHeight, setFooterHeight] = useState<number>(0);
useEffect(() => {
const updateHeight = () => {
if (footerRef.current) {
const height = footerRef.current.offsetHeight;
setFooterHeight(height);
}
};
updateHeight();
const resizeObserver = new ResizeObserver(updateHeight);
const currentFooter = footerRef.current;
if (currentFooter) {
resizeObserver.observe(currentFooter);
}
return () => {
resizeObserver.disconnect();
};
}, []);
columns
}: FooterBaseRevealProps) {
return (
<section
className={cls("relative z-0 w-full mt-20", className)}
style={{
height: footerHeight ? `${footerHeight}px` : "auto",
clipPath: "polygon(0% 0, 100% 0%, 100% 100%, 0 100%)",
}}
>
<div
className={cls("fixed bottom-0 w-full flex items-center justify-center overflow-hidden", wrapperClassName)}
style={{ height: footerHeight ? `${footerHeight}px` : "auto" }}
>
<div ref={footerRef} className={cls("w-full", containerClassName)}>
<FooterBase
// logoSrc={logoSrc}
// logoWidth={logoWidth}
// logoHeight={logoHeight}
columns={columns}
copyrightText={copyrightText}
onPrivacyClick={onPrivacyClick}
ariaLabel={ariaLabel}
className={cls("mt-0", footerClassName)}
containerClassName={footerContainerClassName}
// logoClassName={logoClassName}
columnsClassName={columnsClassName}
columnClassName={columnClassName}
columnTitleClassName={columnTitleClassName}
columnItemClassName={columnItemClassName}
copyrightContainerClassName={copyrightContainerClassName}
copyrightTextClassName={copyrightTextClassName}
privacyButtonClassName={privacyButtonClassName}
/>
<footer className="bg-slate-900 text-slate-100 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-7xl mx-auto">
{/* Top Section */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-8 mb-8">
{/* Logo Section */}
<div className="flex flex-col justify-start">
<h2 className="text-2xl font-bold text-white mb-2">{logoText}</h2>
<p className="text-slate-400 text-sm">
Building amazing communities together
</p>
</div>
{/* Footer Columns */}
{columns.map((column, index) => (
<div key={index}>
<h3 className="text-lg font-semibold text-white mb-4">
{column.title}
</h3>
<ul className="space-y-2">
{column.items.map((item, itemIndex) => (
<li key={itemIndex}>
<a
href={item.href}
className="text-slate-400 hover:text-white transition-colors duration-200 text-sm"
>
{item.label}
</a>
</li>
))}
</ul>
</div>
))}
</div>
{/* Divider */}
<div className="border-t border-slate-700 my-8"></div>
{/* Bottom Section */}
<div className="flex flex-col sm:flex-row justify-between items-center">
<p className="text-slate-400 text-sm mb-4 sm:mb-0">
{copyrightText}
</p>
<div className="flex gap-6">
<a
href="#"
className="text-slate-400 hover:text-white transition-colors duration-200"
aria-label="Privacy Policy"
>
<span className="text-sm">Privacy Policy</span>
</a>
<a
href="#"
className="text-slate-400 hover:text-white transition-colors duration-200"
aria-label="Terms of Service"
>
<span className="text-sm">Terms of Service</span>
</a>
</div>
</div>
</div>
</section>
</footer>
);
};
FooterBaseReveal.displayName = "FooterBaseReveal";
export default FooterBaseReveal;
}

View File

@@ -1,172 +1,193 @@
"use client";
'use client';
import MediaContent from "@/components/shared/MediaContent";
import FillWidthText from "@/components/shared/FillWidthText/FillWidthText";
import TextAnimation from "@/components/text/TextAnimation";
import Button from "@/components/button/Button";
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
import { cls } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { Plus } from "lucide-react";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import { useButtonAnimation } from "@/components/hooks/useButtonAnimation";
import type { ButtonConfig, ButtonAnimationType } from "@/types/button";
import React, { useState } from 'react';
import Image from 'next/image';
type HeroLogoBillboardSplitBackgroundProps = 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: "glowing-orb" }
| { 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 HeroLogoBillboardSplitProps {
logoText: string;
description: string;
background: HeroLogoBillboardSplitBackgroundProps;
buttons: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
layoutOrder: "default" | "reverse";
mediaAnimation: ButtonAnimationType;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
frameStyle?: "card" | "browser";
ariaLabel?: string;
className?: string;
containerClassName?: string;
logoContainerClassName?: string;
descriptionClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
logoClassName?: string;
mediaWrapperClassName?: string;
imageClassName?: string;
browserBarClassName?: string;
addressBarClassName?: string;
interface Button {
text: string;
href: string;
}
const HeroLogoBillboardSplit = ({
logoText,
description,
background,
buttons,
buttonAnimation = "none",
layoutOrder,
mediaAnimation,
imageSrc,
videoSrc,
imageAlt = "",
videoAriaLabel = "Hero video",
frameStyle = "card",
ariaLabel = "Hero section",
className = "",
containerClassName = "",
logoContainerClassName = "",
descriptionClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
logoClassName = "",
mediaWrapperClassName = "",
imageClassName = "",
browserBarClassName = "",
addressBarClassName = "",
}: HeroLogoBillboardSplitProps) => {
const theme = useTheme();
const { containerRef: buttonContainerRef } = useButtonAnimation({ animationType: buttonAnimation });
const { containerRef: mediaContainerRef } = useButtonAnimation({ animationType: mediaAnimation });
interface Background {
variant: 'radial-gradient' | 'linear-gradient' | 'solid';
}
return (
<section
aria-label={ariaLabel}
className={cls("relative w-full py-hero-page-padding", className)}
>
<HeroBackgrounds {...background} />
<div className={cls("w-content-width mx-auto flex flex-col gap-6 md:gap-15 relative z-10", containerClassName)}>
<div className={cls(
"w-full flex gap-6 md:gap-8",
layoutOrder === "default" ? "flex-col" : "flex-col-reverse",
logoContainerClassName
)}>
<div className="relative flex flex-col gap-3 md:flex-row justify-between md:items-end w-full" >
<div className="relative flex flex-col gap-4 w-full md:w-1/2" >
<TextAnimation
type={theme.defaultTextAnimation}
text={description}
variant="words-trigger"
start="top 100%"
className={cls("text-lg md:text-3xl text-foreground/75 text-balance text-start leading-[1.2]", descriptionClassName)}
/>
</div>
<div ref={buttonContainerRef} className={cls("flex flex-wrap gap-4 max-md:justify-center", buttonContainerClassName)}>
{buttons.slice(0, 2).map((button, index) => (
<Button key={`${button.text}-${index}`} {...getButtonProps(button, index, theme.defaultButtonVariant, buttonClassName, buttonTextClassName)} />
))}
</div>
</div>
<div className="relative w-full flex">
<FillWidthText className={cls("text-foreground", logoClassName)}>
{logoText}
</FillWidthText>
</div>
interface HeroLogoBillboardSplitProps {
logoText: string;
description: string;
background: Background;
buttons: Button[];
layoutOrder: 'default' | 'reverse';
imageSrc: string;
imageAlt: string;
frameStyle: 'card' | 'none';
buttonAnimation: 'slide-up' | 'fade' | 'none';
mediaAnimation: 'opacity' | 'scale' | 'none';
}
export default function HeroLogoBillboardSplit({
logoText,
description,
background,
buttons,
layoutOrder,
imageSrc,
imageAlt,
frameStyle,
buttonAnimation,
mediaAnimation,
}: HeroLogoBillboardSplitProps) {
const [isFlipped, setIsFlipped] = useState(false);
const getBackgroundClass = () => {
switch (background.variant) {
case 'radial-gradient':
return 'bg-gradient-to-br from-blue-50 via-white to-purple-50';
case 'linear-gradient':
return 'bg-gradient-to-r from-blue-50 to-purple-50';
case 'solid':
return 'bg-white';
default:
return 'bg-white';
}
};
const getButtonAnimationClass = () => {
switch (buttonAnimation) {
case 'slide-up':
return 'transform transition-all duration-500 hover:translate-y-[-4px]';
case 'fade':
return 'transition-opacity duration-300 hover:opacity-80';
case 'none':
return '';
default:
return '';
}
};
const getMediaAnimationClass = () => {
switch (mediaAnimation) {
case 'opacity':
return 'transition-opacity duration-500 hover:opacity-90';
case 'scale':
return 'transition-transform duration-500 hover:scale-105';
case 'none':
return '';
default:
return '';
}
};
const getFrameClass = () => {
switch (frameStyle) {
case 'card':
return 'rounded-2xl shadow-lg overflow-hidden';
case 'none':
return '';
default:
return '';
}
};
const isReverse = layoutOrder === 'reverse';
return (
<section className={`w-full min-h-screen flex items-center justify-center ${getBackgroundClass()} py-20 px-4 sm:px-6 lg:px-8`}>
<div className="max-w-7xl w-full mx-auto">
<div className={`grid grid-cols-1 lg:grid-cols-2 gap-12 items-center ${isReverse ? 'lg:grid-cols-2' : ''}`}>
{/* Content Section */}
<div className={`flex flex-col justify-center ${isReverse ? 'lg:order-2' : 'lg:order-1'}`}>
<div className="space-y-6">
<div>
<h1 className="text-5xl sm:text-6xl font-bold text-gray-900 mb-4">
{logoText}
</h1>
<div className="w-20 h-1 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full"></div>
</div>
<p className="text-lg sm:text-xl text-gray-600 leading-relaxed max-w-lg">
{description}
</p>
<div className="flex flex-wrap gap-4 pt-4">
{buttons.map((button, index) => (
<a
key={index}
href={button.href}
className={`px-8 py-3 rounded-lg font-semibold transition-all duration-300 ${
index === 0
? 'bg-gradient-to-r from-blue-500 to-purple-500 text-white hover:shadow-lg hover:from-blue-600 hover:to-purple-600'
: 'border-2 border-gray-300 text-gray-700 hover:border-blue-500 hover:text-blue-500'
} ${getButtonAnimationClass()}`}
>
{button.text}
</a>
))}
</div>
</div>
</div>
{/* Image/Flip Card Section */}
<div className={`flex justify-center ${isReverse ? 'lg:order-1' : 'lg:order-2'}`}>
<div
className="w-full max-w-md h-96 cursor-pointer perspective"
onClick={() => setIsFlipped(!isFlipped)}
style={{
perspective: '1000px',
}}
>
<div
className={`relative w-full h-full transition-transform duration-500 ${getFrameClass()}`}
style={{
transformStyle: 'preserve-3d',
transform: isFlipped ? 'rotateY(180deg)' : 'rotateY(0deg)',
}}
>
{/* Front Side - Image */}
<div
className={`absolute w-full h-full ${getFrameClass()} ${getMediaAnimationClass()}`}
style={{
backfaceVisibility: 'hidden',
}}
>
<Image
src={imageSrc}
alt={imageAlt}
fill
className="object-cover"
priority
/>
</div>
{frameStyle === "browser" ? (
<div ref={mediaContainerRef} className={cls("w-full overflow-hidden rounded-theme-capped card", mediaWrapperClassName)}>
<div className={cls("relative z-1 bg-background border-b border-foreground/10 px-4 py-3 flex items-center gap-4", browserBarClassName)}>
<div className="flex items-center gap-2">
<div className="h-3 w-auto aspect-square rounded-theme bg-accent" />
<div className="h-3 w-auto aspect-square rounded-theme bg-accent" />
<div className="h-3 w-auto aspect-square rounded-theme bg-accent" />
</div>
<div className="flex items-center gap-2 flex-1">
<div className={cls("w-15 md:w-10 h-8 rounded-theme bg-accent/10", addressBarClassName)} />
<div className="w-15 md:w-10 h-8 rounded-theme bg-accent/10" />
<div className="hidden md:block w-10 h-8 rounded-theme bg-accent/10" />
</div>
<Plus className="h-[var(--text-sm)] w-auto text-foreground" />
</div>
<div className="relative z-1 p-0">
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("z-1 rounded-none! aspect-square md:aspect-video!", imageClassName)}
/>
</div>
</div>
) : (
<div ref={mediaContainerRef} className={cls("w-full overflow-hidden rounded-theme-capped card p-4", mediaWrapperClassName)}>
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("z-1 aspect-square md:aspect-video", imageClassName)}
/>
</div>
)}
{/* Back Side - Testimonial */}
<div
className={`absolute w-full h-full ${getFrameClass()} bg-gradient-to-br from-blue-500 to-purple-600 p-8 flex flex-col justify-center items-center text-white`}
style={{
backfaceVisibility: 'hidden',
transform: 'rotateY(180deg)',
}}
>
<div className="text-center space-y-4">
<svg
className="w-12 h-12 mx-auto opacity-80"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M3 21c3 0 7-1 7-8V5c0-1.25-4.716-5-7-5-6 0-6.002 4-6 7v11c0 1-1 4 6 5z" />
</svg>
<p className="text-lg leading-relaxed font-medium">
{description}
</p>
<p className="text-sm opacity-90 pt-4">
Click to flip back
</p>
</div>
</div>
</div>
</div>
</section>
);
};
HeroLogoBillboardSplit.displayName = "HeroLogoBillboardSplit";
export default HeroLogoBillboardSplit;
</div>
</div>
</div>
</section>
);
}

View File

@@ -1,186 +1,147 @@
"use client";
import { memo } from "react";
import CardStack from "@/components/cardStack/CardStack";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationTypeWith3D, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
import React, { useState } from "react";
import { LucideIcon } from "lucide-react";
type Metric = {
id: string;
icon: LucideIcon;
title: string;
value: string;
};
interface MetricCardThreeProps {
metrics: Metric[];
carouselMode?: "auto" | "buttons";
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationTypeWith3D;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
iconContainerClassName?: string;
iconClassName?: string;
metricTitleClassName?: string;
valueClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
interface Metric {
id: string;
icon: LucideIcon;
title: string;
value: string;
}
interface MetricCardItemProps {
metric: Metric;
shouldUseLightText: boolean;
cardClassName?: string;
iconContainerClassName?: string;
iconClassName?: string;
metricTitleClassName?: string;
valueClassName?: string;
interface Button {
text: string;
href: string;
}
const MetricCardItem = memo(({
metric,
shouldUseLightText,
cardClassName = "",
iconContainerClassName = "",
iconClassName = "",
metricTitleClassName = "",
valueClassName = "",
}: MetricCardItemProps) => {
return (
<div className={cls("relative h-full card text-foreground rounded-theme-capped p-6 flex flex-col items-center justify-center gap-3", cardClassName)}>
<div className="relative z-1 w-full flex items-center justify-center gap-2">
<div className={cls("h-8 primary-button aspect-square rounded-theme flex items-center justify-center", iconContainerClassName)}>
<metric.icon className={cls("h-4/10 text-primary-cta-text", iconClassName)} strokeWidth={1.5} />
</div>
<h3 className={cls("text-xl truncate", shouldUseLightText ? "text-background" : "text-foreground", metricTitleClassName)}>
{metric.title}
</h3>
</div>
<div className="relative z-1 w-full flex items-center justify-center">
<h4 className={cls("text-7xl font-medium truncate", shouldUseLightText ? "text-background" : "text-foreground", valueClassName)}>
{metric.value}
</h4>
</div>
interface Props {
title: string;
description: string;
tag: string;
tagIcon: LucideIcon;
tagAnimation?: string;
metrics: Metric[];
textboxLayout?: string;
animationType?: string;
useInvertedBackground?: boolean;
buttons?: Button[];
buttonAnimation?: string;
}
export default function MetricCardThree({
title,
description,
tag,
tagIcon: TagIcon,
metrics,
useInvertedBackground = false,
buttons = [],
}: Props) {
const [flipped, setFlipped] = useState<{ [key: string]: boolean }>({});
const toggleFlip = (id: string) => {
setFlipped((prev) => ({
...prev,
[id]: !prev[id],
}));
};
const bgClass = useInvertedBackground
? "bg-slate-900 text-white"
: "bg-white text-slate-900";
return (
<section className={`py-16 px-4 sm:px-6 lg:px-8 ${bgClass}`}>
<div className="max-w-6xl mx-auto">
<div className="mb-12">
<div className="flex items-center gap-2 mb-4">
<TagIcon className="w-5 h-5 text-blue-500" />
<span className="text-sm font-semibold text-blue-500 uppercase tracking-wide">
{tag}
</span>
</div>
<h2 className="text-4xl sm:text-5xl font-bold mb-4">{title}</h2>
<p className={`text-lg ${useInvertedBackground ? "text-slate-300" : "text-slate-600"}`}>
{description}
</p>
</div>
);
});
MetricCardItem.displayName = "MetricCardItem";
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
{metrics.map((metric) => {
const Icon = metric.icon;
const isFlipped = flipped[metric.id] || false;
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 (
<div
key={metric.id}
className="h-64 cursor-pointer perspective"
onClick={() => toggleFlip(metric.id)}
>
<div
className={`relative w-full h-full transition-transform duration-500 transform-gpu ${
isFlipped ? "scale-x-[-1]" : ""
}`}
style={{
transformStyle: "preserve-3d",
transform: isFlipped ? "rotateY(180deg)" : "rotateY(0deg)",
}}
>
<div
className={`absolute w-full h-full p-6 rounded-lg border-2 flex flex-col justify-between ${
useInvertedBackground
? "bg-slate-800 border-slate-700 text-white"
: "bg-slate-50 border-slate-200 text-slate-900"
}`}
style={{ backfaceVisibility: "hidden" }}
>
<div>
<div className="mb-4 p-3 bg-blue-100 rounded-lg w-fit">
<Icon className="w-6 h-6 text-blue-600" />
</div>
<h3 className="text-sm font-semibold mb-2 opacity-75">
{metric.title}
</h3>
</div>
<div className="text-4xl font-bold">{metric.value}</div>
</div>
return (
<CardStack
useInvertedBackground={useInvertedBackground}
mode={carouselMode}
gridVariant="uniform-all-items-equal"
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
supports3DAnimation={true}
<div
className={`absolute w-full h-full p-6 rounded-lg border-2 flex items-center justify-center ${
useInvertedBackground
? "bg-slate-800 border-slate-700 text-white"
: "bg-slate-50 border-slate-200 text-slate-900"
}`}
style={{
backfaceVisibility: "hidden",
transform: "rotateY(180deg)",
}}
>
<p className="text-center text-sm leading-relaxed">
{metric.title} represents our commitment to excellence and
continuous growth in the computer club community.
</p>
</div>
</div>
</div>
);
})}
</div>
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}
/>
{buttons.length > 0 && (
<div className="flex flex-wrap gap-4">
{buttons.map((button, index) => (
<a
key={index}
href={button.href}
className="px-6 py-3 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 transition-colors duration-200"
>
{button.text}
</a>
))}
</CardStack>
);
};
MetricCardThree.displayName = "MetricCardThree";
export default MetricCardThree;
</div>
)}
</div>
</section>
);
}

View File

@@ -1,203 +1,164 @@
"use client";
'use client';
import { memo } from "react";
import AutoCarousel from "@/components/cardStack/layouts/carousels/AutoCarousel";
import TestimonialAuthor from "@/components/shared/TestimonialAuthor";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import { Quote } from "lucide-react";
import type { LucideIcon } from "lucide-react";
import type { CardAnimationType, ButtonConfig, ButtonAnimationType, TitleSegment, TextboxLayout, InvertedBackground } from "@/components/cardStack/types";
import { useState } from 'react';
type Testimonial = {
id: string;
name: string;
handle: string;
testimonial: string;
imageSrc?: string;
imageAlt?: string;
icon?: LucideIcon;
};
interface TestimonialCardSixProps {
testimonials: Testimonial[];
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
speed?: number;
topMarqueeDirection?: "left" | "right";
ariaLabel?: string;
className?: string;
containerClassName?: string;
carouselClassName?: string;
bottomCarouselClassName?: string;
cardClassName?: string;
testimonialClassName?: string;
imageWrapperClassName?: string;
imageClassName?: string;
iconClassName?: string;
nameClassName?: string;
handleClassName?: string;
textBoxClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
interface Testimonial {
id: number;
author: string;
role: string;
company: string;
shortText: string;
fullText: string;
avatar?: string;
rating?: number;
}
interface TestimonialCardProps {
testimonial: Testimonial;
useInvertedBackground: boolean;
cardClassName?: string;
testimonialClassName?: string;
imageWrapperClassName?: string;
imageClassName?: string;
iconClassName?: string;
nameClassName?: string;
handleClassName?: string;
const testimonials: Testimonial[] = [
{
id: 1,
author: 'Sarah Johnson',
role: 'Product Manager',
company: 'Tech Innovations Inc',
shortText: 'Exceptional service and support.',
fullText: 'This product has completely transformed how our team works. The exceptional service and support from the team made the transition seamless. We saw a 40% improvement in productivity within the first month.',
avatar: '👩‍💼',
rating: 5,
},
{
id: 2,
author: 'Michael Chen',
role: 'CEO',
company: 'Digital Solutions Ltd',
shortText: 'Best investment we made this year.',
fullText: 'Best investment we made this year. The platform is intuitive, powerful, and the ROI has exceeded our expectations. The team is responsive and genuinely cares about customer success. Highly recommended for any growing business.',
avatar: '👨‍💼',
rating: 5,
},
{
id: 3,
author: 'Emily Rodriguez',
role: 'Operations Director',
company: 'Global Enterprises',
shortText: 'Streamlined our entire workflow.',
fullText: 'Streamlined our entire workflow and reduced operational costs significantly. The implementation was smooth, and the training provided was comprehensive. Our team is now more efficient than ever before.',
avatar: '👩‍💻',
rating: 5,
},
{
id: 4,
author: 'David Thompson',
role: 'Founder',
company: 'StartUp Ventures',
shortText: 'Game-changer for our startup.',
fullText: 'A true game-changer for our startup. The scalability and flexibility of the solution allowed us to grow without limitations. Customer support is outstanding and always available when we need them.',
avatar: '👨‍🚀',
rating: 5,
},
{
id: 5,
author: 'Jessica Lee',
role: 'Marketing Lead',
company: 'Creative Agency Pro',
shortText: 'Transformed our client deliverables.',
fullText: 'Transformed our client deliverables and improved our team collaboration. The analytics and reporting features are incredibly detailed and actionable. We\'ve already recommended it to several clients.',
avatar: '👩‍🎨',
rating: 5,
},
{
id: 6,
author: 'Robert Martinez',
role: 'Technical Director',
company: 'Enterprise Solutions',
shortText: 'Robust, reliable, and secure.',
fullText: 'Robust, reliable, and secure platform that meets all our enterprise requirements. The API documentation is excellent, and integration with our existing systems was straightforward. Outstanding technical support team.',
avatar: '👨‍💻',
rating: 5,
},
];
interface CardProps {
testimonial: Testimonial;
}
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);
function TestimonialCard({ testimonial }: CardProps) {
const [isFlipped, setIsFlipped] = useState(false);
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!"
return (
<div
className="h-80 cursor-pointer perspective"
onMouseEnter={() => setIsFlipped(true)}
onMouseLeave={() => setIsFlipped(false)}
>
<div
className="relative w-full h-full transition-transform duration-500 transform-gpu"
style={{
transformStyle: 'preserve-3d',
transform: isFlipped ? 'rotateY(180deg)' : 'rotateY(0deg)',
}}
>
{/* Front Side */}
<div
className="absolute w-full h-full bg-white rounded-lg shadow-lg p-6 flex flex-col justify-between border border-gray-200"
style={{ backfaceVisibility: 'hidden' }}
>
{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>
);
};
<div>
<div className="flex items-center gap-4 mb-4">
<div className="text-4xl">{testimonial.avatar}</div>
<div>
<h3 className="font-semibold text-gray-900">{testimonial.author}</h3>
<p className="text-sm text-gray-600">{testimonial.role}</p>
<p className="text-xs text-gray-500">{testimonial.company}</p>
</div>
</div>
<div className="flex gap-1 mb-4">
{Array.from({ length: testimonial.rating || 5 }).map((_, i) => (
<span key={i} className="text-yellow-400">
</span>
))}
</div>
<p className="text-gray-700 text-sm leading-relaxed">{testimonial.shortText}</p>
</div>
<p className="text-xs text-gray-500 text-center mt-4">Hover to read more</p>
</div>
TestimonialCardSix.displayName = "TestimonialCardSix";
{/* Back Side */}
<div
className="absolute w-full h-full bg-gradient-to-br from-blue-600 to-blue-800 rounded-lg shadow-lg p-6 flex flex-col justify-center border border-blue-700"
style={{
backfaceVisibility: 'hidden',
transform: 'rotateY(180deg)',
}}
>
<p className="text-white text-sm leading-relaxed font-medium">
{testimonial.fullText}
</p>
<p className="text-blue-200 text-xs mt-4 text-center italic">
{testimonial.author}
</p>
</div>
</div>
</div>
);
}
export default TestimonialCardSix;
export default function TestimonialCardSix() {
return (
<section className="py-16 px-4 bg-gray-50">
<div className="max-w-7xl mx-auto">
<div className="text-center mb-12">
<h2 className="text-4xl font-bold text-gray-900 mb-4">What Our Clients Say</h2>
<p className="text-lg text-gray-600">
Hover over any card to read the full testimonial
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{testimonials.map((testimonial) => (
<TestimonialCard key={testimonial.id} testimonial={testimonial} />
))}
</div>
</div>
</section>
);
}

View File

@@ -1,140 +1,143 @@
// "use client";
'use client';
// import { memo } from "react";
// import { useTagEffects } from "./useTagEffects";
import { useState } from 'react';
// const Tag = () => {
// const { shouldShow, handleMouseEnter, handleClick, buttonClassName } = useTagEffects();
interface Testimonial {
id: string;
author: string;
role: string;
company: string;
excerpt: string;
fullText: string;
avatar?: string;
}
// const handleTagClick = () => {
// window.open('https://webild.io', '_blank');
// };
interface TagProps {
testimonials?: Testimonial[];
}
// if (!shouldShow) {
// return null;
// }
const defaultTestimonials: Testimonial[] = [
{
id: '1',
author: 'Sarah Johnson',
role: 'Product Manager',
company: 'TechCorp',
excerpt: 'Amazing experience working with this team.',
fullText: 'This has been an absolutely transformative experience. The team went above and beyond to ensure our project succeeded. Their attention to detail and commitment to excellence is unmatched in the industry.',
avatar: '👩‍💼',
},
{
id: '2',
author: 'Michael Chen',
role: 'CEO',
company: 'StartupHub',
excerpt: 'Exceeded all our expectations.',
fullText: 'From day one, they demonstrated exceptional expertise and professionalism. The results speak for themselves - we saw a 300% improvement in our metrics. Highly recommend to anyone looking for top-tier service.',
avatar: '👨‍💼',
},
{
id: '3',
author: 'Emily Rodriguez',
role: 'Design Lead',
company: 'CreativeStudio',
excerpt: 'Best decision we made this year.',
fullText: 'The collaboration was seamless and the final product exceeded our vision. They understood our brand deeply and delivered something truly special. We are already planning our next project together.',
avatar: '👩‍🎨',
},
];
// return (
// <button
// className={`fixed z-[99999] bottom-6 right-6 w-fit tag-card h-8 px-3 flex items-center justify-center gap-1 rounded-[6px] text-xs cursor-pointer ${buttonClassName}`}
// onClick={(e) => handleClick(e, handleTagClick)}
// onMouseEnter={handleMouseEnter}
// >
// <p className="text-foreground font-medium">Made With</p>
// <svg className="h-[1.25em] w-auto mt-[-2.5%]" version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 67 21" width="67" height="21">
// <defs>
// <clipPath clipPathUnits="userSpaceOnUse" id="cp1">
// <path d="m26.44 7.16c1.75 0 3.18 0.62 4.17 1.72 0.99 1.11 1.53 2.68 1.53 4.59v0.98h-8.77v0.04c0 1.06 0.32 1.93 0.88 2.53 0.56 0.6 1.36 0.95 2.35 0.95 1.42 0 2.44-0.71 2.76-1.74l0.03-0.08h2.63l-0.02 0.13c-0.37 2.33-2.58 3.97-5.46 3.97-1.85 0-3.34-0.62-4.37-1.76-1.03-1.13-1.59-2.75-1.59-4.73 0-1.97 0.57-3.62 1.59-4.78 1.02-1.16 2.49-1.82 4.27-1.82zm9.66 2.13c0.8-1.32 2.18-2.1 3.87-2.1 1.59 0 2.92 0.63 3.84 1.77 0.92 1.13 1.43 2.76 1.43 4.74 0 1.99-0.51 3.62-1.43 4.76-0.92 1.13-2.25 1.77-3.85 1.77-1.74 0-3.12-0.8-3.93-2.11v1.9h-2.7v-17.25h2.77zm30.9 10.73h-2.7v-1.89c-0.78 1.29-2.16 2.1-3.89 2.1-1.6 0-2.93-0.64-3.87-1.78-0.93-1.14-1.45-2.77-1.45-4.75 0-1.97 0.52-3.6 1.45-4.74 0.93-1.14 2.26-1.77 3.85-1.77 1.69 0 3.07 0.79 3.83 2.04v-6.45h2.78zm-64-17.16l3.09 13.09 3.54-13.09 0.02-0.09h2.62l0.02 0.09 3.55 13.09 3.09-13.09 0.02-0.09h2.98l-0.03 0.14-4.61 17.03-0.02 0.08h-2.65l-0.03-0.08-3.63-12.67-3.62 12.67-0.02 0.08h-2.67l-0.02-0.08-4.59-17.03-0.04-0.14h2.98zm50.91 17.16h-2.77v-17.24h2.77zm-4.87-0.03h-2.77v-12.63h2.77zm11.87-2.03q0.09 0 0.18 0zm-21.64-8.4c-0.93 0-1.72 0.41-2.29 1.13-0.56 0.72-0.89 1.76-0.89 3.02 0 1.27 0.33 2.31 0.89 3.03 0.57 0.71 1.36 1.12 2.29 1.12 0.97 0 1.75-0.4 2.28-1.1 0.55-0.71 0.85-1.75 0.85-3.05 0-1.29-0.3-2.33-0.85-3.04-0.53-0.71-1.31-1.11-2.28-1.11zm21.81 0c-0.97 0-1.76 0.4-2.31 1.11-0.54 0.71-0.86 1.75-0.86 3.04 0 1.29 0.32 2.33 0.86 3.04 0.55 0.71 1.34 1.11 2.31 1.11 0.95 0 1.73-0.4 2.28-1.12 0.56-0.71 0.88-1.75 0.88-3.03 0-1.27-0.32-2.31-0.88-3.03-0.55-0.71-1.33-1.12-2.28-1.12zm-19.79 7.65q-0.01 0.01-0.03 0.03 0.02-0.02 0.03-0.03zm0.13-0.13h0.01q-0.07 0.07-0.14 0.13 0.07-0.06 0.13-0.13zm16.38-3.37q0 0.45 0.05 0.87-0.02-0.19-0.03-0.39l-0.02-0.48q0.01-0.06 0.01-0.12-0.01 0.06-0.01 0.12zm-12.67 0q0 0.04 0 0.09v-0.09q0-0.23-0.01-0.45zm0 0.36v-0.15q0 0.11-0.01 0.21 0.01-0.03 0.01-0.06zm0-0.15q0-0.06 0-0.12zm-18.69-4.47c-1.68 0-2.87 1.24-3.04 3.01h5.96c-0.05-0.88-0.35-1.62-0.84-2.15-0.5-0.54-1.22-0.86-2.08-0.86zm32.25 1.15l0.11-0.13q0.08-0.09 0.16-0.17-0.14 0.14-0.27 0.3zm0.89-0.77q-0.31 0.17-0.56 0.41 0.07-0.07 0.14-0.13 0.13-0.1 0.27-0.19 0.07-0.05 0.15-0.09zm-0.56 0.41q-0.02 0.02-0.03 0.03 0.01-0.01 0.03-0.03zm-32.75-0.9q0 0 0 0 0 0 0 0zm0.17 0h0.09q-0.05 0-0.09-0.01-0.09 0.01-0.17 0.01 0.08 0 0.17 0zm34.25 8.61q0.09 0.01 0.18 0.01-0.09 0-0.18-0.01zm-0.25-0.04q0.11 0.02 0.21 0.03-0.1-0.01-0.21-0.03zm-2.58-3.28q0.02 0.16 0.05 0.31-0.03-0.15-0.05-0.31zm-12.76-0.11q0.02-0.17 0.02-0.35 0 0.18-0.02 0.35zm12.72-1.3q0 0.07 0 0.14 0-0.07 0-0.14zm0.04-0.36q-0.01 0.05-0.02 0.1 0.01-0.05 0.02-0.1zm0.07-0.49q-0.01 0.09-0.03 0.19 0.02-0.1 0.03-0.19zm0.07-0.29q-0.02 0.08-0.04 0.15 0.02-0.07 0.04-0.15zm0.14-0.44q-0.05 0.12-0.09 0.25 0.04-0.13 0.09-0.25zm0.08-0.21q-0.02 0.05-0.05 0.11 0.03-0.06 0.05-0.11zm0.14-0.31q-0.02 0.04-0.04 0.09 0.02-0.05 0.04-0.09z"/>
// </clipPath>
// <filter x="-50%" y="-50%" width="200%" height="200%" id="f1"> <feGaussianBlur stdDeviation=".7"/> </filter>
// <filter x="-50%" y="-50%" width="200%" height="200%" id="f2"> <feGaussianBlur stdDeviation="2.7"/> </filter>
// <linearGradient id="g1" x2="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(.62,1.977,-1.976,.62,47.533,3.611)">
// <stop offset="0" stopColor="#0f3da6" stopOpacity="1"/>
// <stop offset=".952" stopColor="#3a9aff" stopOpacity="0"/>
// </linearGradient>
// <linearGradient id="g2" x2="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(.495,2.092,-2.092,.495,46.707,3.06)">
// <stop offset=".01" stopColor="#0d50e8" stopOpacity="1"/>
// <stop offset=".952" stopColor="#3a9aff" stopOpacity="0"/>
// </linearGradient>
// <filter x="-50%" y="-50%" width="200%" height="200%" id="f3"> <feGaussianBlur stdDeviation="0"/> </filter>
// <linearGradient id="g3" x2="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.982,3.688,-3.688,1.982,46.267,2.069)">
// <stop offset="0" stopColor="#0f3da6" stopOpacity="0"/>
// <stop offset="1" stopColor="#59abff" stopOpacity="1"/>
// </linearGradient>
// <linearGradient id="g4" x2="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(.606,2.752,-2.752,.606,46.32,2.122)">
// <stop offset="0" stopColor="#0f3da6" stopOpacity="0"/>
// <stop offset="1" stopColor="#59abff" stopOpacity="1"/>
// </linearGradient>
// </defs>
// <style>{`.s0 { fill: var(--foreground) } .s1 { filter: url(#f1);fill: #0597ff } .s2 { filter: url(#f2);fill: #0597ff } .s3 { fill: url(#g1) } .s4 { fill: url(#g2) } .s5 { filter: url(#f3);fill: url(#g3) } .s6 { filter: url(#f1);fill: url(#g4) }`}</style>
// <path className="s0" d="m26.44 7.16c1.75 0 3.18 0.62 4.17 1.72 0.99 1.11 1.53 2.68 1.53 4.59v0.98h-8.77v0.04c0 1.06 0.32 1.93 0.88 2.53 0.56 0.6 1.36 0.95 2.35 0.95 1.42 0 2.44-0.71 2.76-1.74l0.03-0.08h2.63l-0.02 0.13c-0.37 2.33-2.58 3.97-5.46 3.97-1.85 0-3.34-0.62-4.37-1.76-1.03-1.13-1.59-2.75-1.59-4.73 0-1.97 0.57-3.62 1.59-4.78 1.02-1.16 2.49-1.82 4.27-1.82zm9.66 2.13c0.8-1.32 2.18-2.1 3.87-2.1 1.59 0 2.92 0.63 3.84 1.77 0.92 1.13 1.43 2.76 1.43 4.74 0 1.99-0.51 3.62-1.43 4.76-0.92 1.13-2.25 1.77-3.85 1.77-1.74 0-3.12-0.8-3.93-2.11v1.9h-2.7v-17.25h2.77zm30.9 10.73h-2.7v-1.89c-0.78 1.29-2.16 2.1-3.89 2.1-1.6 0-2.93-0.64-3.87-1.78-0.93-1.14-1.45-2.77-1.45-4.75 0-1.97 0.52-3.6 1.45-4.74 0.93-1.14 2.26-1.77 3.85-1.77 1.69 0 3.07 0.79 3.83 2.04v-6.45h2.78zm-64-17.16l3.09 13.09 3.54-13.09 0.02-0.09h2.62l0.02 0.09 3.55 13.09 3.09-13.09 0.02-0.09h2.98l-0.03 0.14-4.61 17.03-0.02 0.08h-2.65l-0.03-0.08-3.63-12.67-3.62 12.67-0.02 0.08h-2.67l-0.02-0.08-4.59-17.03-0.04-0.14h2.98zm50.91 17.16h-2.77v-17.24h2.77zm-4.87-0.03h-2.77v-12.63h2.77zm11.87-2.03q0.09 0 0.18 0zm-0.22-0.02q0.09 0.01 0.18 0.02-0.09-0.01-0.18-0.02zm-0.25-0.03q0.1 0.02 0.21 0.03-0.11-0.01-0.21-0.03zm-21.17-8.35c-0.93 0-1.72 0.41-2.29 1.13-0.56 0.72-0.89 1.76-0.89 3.02 0 1.27 0.33 2.31 0.89 3.03 0.57 0.71 1.36 1.12 2.29 1.12 0.97 0 1.75-0.4 2.28-1.1 0.55-0.71 0.85-1.75 0.85-3.05 0-1.29-0.3-2.33-0.85-3.04-0.53-0.71-1.31-1.11-2.28-1.11zm21.81 0c-0.97 0-1.76 0.4-2.31 1.11-0.54 0.71-0.86 1.75-0.86 3.04 0 1.29 0.32 2.33 0.86 3.04 0.55 0.71 1.34 1.11 2.31 1.11 0.95 0 1.73-0.4 2.28-1.12 0.56-0.71 0.88-1.75 0.88-3.03 0-1.27-0.32-2.31-0.88-3.03-0.55-0.71-1.33-1.12-2.28-1.12zm-19.82 7.68q0.09-0.08 0.16-0.16h0.01q-0.08 0.08-0.17 0.16zm16.6-2.61q0.02 0.16 0.05 0.3-0.03-0.14-0.05-0.3zm-0.06-0.92q0 0.45 0.05 0.87-0.02-0.19-0.03-0.39l-0.02-0.48q0.01-0.06 0.01-0.12-0.01 0.06-0.01 0.12zm-12.7 0.8q0.01-0.17 0.02-0.34-0.01 0.17-0.02 0.34zm0.03-0.8q0 0.21-0.01 0.42 0-0.03 0.01-0.06v-0.36q0-0.23-0.01-0.45zm12.69-0.49q0 0.07-0.01 0.13 0.01-0.06 0.01-0.13zm0.03-0.36q0 0.05-0.01 0.1 0.01-0.05 0.01-0.1zm0.08-0.49q-0.02 0.09-0.03 0.19 0.01-0.1 0.03-0.19zm-31.49-2.93c-1.68 0-2.87 1.25-3.04 3.02h5.96c-0.05-0.88-0.35-1.62-0.84-2.15-0.51-0.54-1.22-0.87-2.08-0.87zm31.56 2.64q-0.02 0.07-0.04 0.15 0.02-0.08 0.04-0.15zm0.14-0.44q-0.05 0.12-0.09 0.25 0.04-0.13 0.09-0.25zm0.08-0.22q-0.03 0.06-0.05 0.12 0.02-0.06 0.05-0.12zm0.14-0.3q-0.02 0.04-0.04 0.09 0.02-0.05 0.04-0.09zm0.6-0.83q-0.14 0.15-0.27 0.31l0.11-0.13q0.08-0.09 0.16-0.18zm0.61-0.46q-0.32 0.18-0.59 0.44 0.09-0.08 0.18-0.16 0.13-0.11 0.27-0.2 0.07-0.04 0.14-0.08zm-33.3-0.49q0.08-0.01 0.17-0.01l0.09 0.01q-0.05-0.01-0.09-0.01-0.09 0-0.17 0.01z"/>
// <g id="Clip-Path" clipPath="url(#cp1)">
// <g>
// <g style={{opacity: .17}}>
// <path className="s1" d="m36.14 2.62h-3.58l9.91-9.03 12.33 1.32 8.09 3.75-7.32 4.73c-1.1-0.2-3.42-0.48-3.91 0-0.6 0.61-0.6 2.26-0.77 3.52-0.13 1.02-1.08 0.94-1.54 0.77l-2.92 0.44-0.88 4.3c-0.24-0.17-0.87-0.85-1.49-2.26-0.77-1.76-1.76-2.31-3.35-2.48-1.28-0.13-3.93 0.79-4.57 1.11z"/>
// </g>
// <g>
// <path className="s2" d="m36.14 2.62h-3.58l9.91-9.03 12.33 1.32 8.09 3.75-7.32 4.73c-1.1-0.2-3.42-0.48-3.91 0-0.6 0.61-0.6 2.26-0.77 3.52-0.13 1.02-1.08 0.94-1.54 0.77l-2.92 0.44-0.88 4.3c-0.24-0.17-0.87-0.85-1.49-2.26-0.77-1.76-1.76-2.31-3.35-2.48-1.28-0.13-3.93 0.79-4.57 1.11z"/>
// </g>
// </g>
// </g>
// <g>
// <path fillRule="evenodd" className="s3" d="m46.78 5c-0.95-0.45-1.34-1.58-0.89-2.53 0.45-0.95 1.58-1.36 2.52-0.91 0.94 0.44 1.34 1.58 0.89 2.53-0.46 0.95-1.58 1.36-2.52 0.91z"/>
// </g>
// <g>
// <path fillRule="evenodd" className="s4" d="m46.99 5.1c-0.83-0.39-1.18-1.37-0.79-2.2 0.4-0.82 1.38-1.17 2.2-0.78 0.83 0.39 1.18 1.37 0.79 2.2-0.39 0.82-1.38 1.17-2.2 0.78z"/>
// </g>
// <g>
// <path fillRule="evenodd" className="s5" d="m47.99 1.98c0.9 0.17 1.5 1.03 1.33 1.93-0.16 0.9-1.02 1.49-1.92 1.32-0.9-0.16-1.49-1.02-1.33-1.92 0.17-0.9 1.03-1.49 1.92-1.33z"/>
// </g>
// <g>
// <path fillRule="evenodd" className="s6" d="m48.05 2.04c0.9 0.16 1.49 1.02 1.32 1.92-0.16 0.9-1.02 1.49-1.92 1.33-0.9-0.17-1.49-1.03-1.33-1.93 0.17-0.89 1.03-1.49 1.93-1.32z"/>
// </g>
// </svg>
// </button>
// );
// };
export default function Tag({ testimonials = defaultTestimonials }: TagProps) {
const [flipped, setFlipped] = useState<Record<string, boolean>>({});
// Tag.displayName = "Tag";
const toggleFlip = (id: string) => {
setFlipped((prev) => ({
...prev,
[id]: !prev[id],
}));
};
// export default memo(Tag);
return (
<div className="w-full py-12 px-4 bg-gradient-to-br from-slate-50 to-slate-100">
<div className="max-w-6xl mx-auto">
<div className="text-center mb-12">
<h2 className="text-4xl font-bold text-slate-900 mb-4">
What Our Clients Say
</h2>
<p className="text-lg text-slate-600">
Hover over cards to see full testimonials
</p>
</div>
"use client";
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{testimonials.map((testimonial) => (
<div
key={testimonial.id}
className="h-80 cursor-pointer perspective"
onClick={() => toggleFlip(testimonial.id)}
>
<div
className="relative w-full h-full transition-transform duration-500 transform-gpu"
style={{
transformStyle: 'preserve-3d',
transform: flipped[testimonial.id]
? 'rotateY(180deg)'
: 'rotateY(0deg)',
}}
>
{/* Front of card */}
<div
className="absolute w-full h-full bg-white rounded-lg shadow-lg p-8 flex flex-col justify-between border border-slate-200"
style={{ backfaceVisibility: 'hidden' }}
>
<div>
<div className="flex items-center gap-4 mb-6">
<div className="text-5xl">{testimonial.avatar}</div>
<div>
<h3 className="font-bold text-slate-900">
{testimonial.author}
</h3>
<p className="text-sm text-slate-600">
{testimonial.role}
</p>
<p className="text-xs text-slate-500">
{testimonial.company}
</p>
</div>
</div>
<p className="text-slate-700 italic text-lg leading-relaxed">
"{testimonial.excerpt}"
</p>
</div>
<div className="text-center text-sm text-slate-500 font-medium">
Click to read more
</div>
</div>
import { memo } from "react";
import { useRive, useStateMachineInput, Layout, Fit } from "@rive-app/react-canvas";
import { useTagEffects } from "./useTagEffects";
const STATE_MACHINE_NAME = "State Machine 1";
const HOVER_INPUT_NAME = "Hover";
const Tag = () => {
const { shouldShow, handleMouseEnter, handleClick } = useTagEffects();
const { rive, RiveComponent } = useRive({
src: "/watermark-bob2.riv",
stateMachines: STATE_MACHINE_NAME,
autoplay: true,
layout: new Layout({
fit: Fit.Contain,
}),
});
const hoverInput = useStateMachineInput(rive, STATE_MACHINE_NAME, HOVER_INPUT_NAME);
const handleTagClick = () => {
window.open('https://webild.io', '_blank');
};
const onMouseEnter = () => {
handleMouseEnter();
if (hoverInput) {
hoverInput.value = true;
}
};
const onMouseLeave = () => {
if (hoverInput) {
hoverInput.value = false;
}
};
if (!shouldShow) {
return null;
}
return (
<button
className="fixed z-[99999] bottom-6 right-6 w-[160px] h-[92px] cursor-pointer"
onClick={(e) => handleClick(e, handleTagClick)}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<RiveComponent className="w-full h-full" />
</button>
);
};
Tag.displayName = "Tag";
export default memo(Tag);
{/* Back of card */}
<div
className="absolute w-full h-full bg-gradient-to-br from-blue-600 to-blue-700 rounded-lg shadow-lg p-8 flex flex-col justify-between border border-blue-500"
style={{
backfaceVisibility: 'hidden',
transform: 'rotateY(180deg)',
}}
>
<div>
<h3 className="font-bold text-white mb-4 text-lg">
{testimonial.author}
</h3>
<p className="text-blue-50 leading-relaxed text-sm">
{testimonial.fullText}
</p>
</div>
<div className="text-center text-xs text-blue-200 font-medium">
Click to go back
</div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
);
}