Merge version_2 into main #4
151
src/app/page.tsx
151
src/app/page.tsx
@@ -1,4 +1,4 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
|
||||
import NavbarStyleApple from '@/components/navbar/NavbarStyleApple/NavbarStyleApple';
|
||||
@@ -10,8 +10,18 @@ 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 { useState } from 'react';
|
||||
|
||||
export default function LandingPage() {
|
||||
const [flippedCards, setFlippedCards] = useState<{ [key: string]: boolean }>({});
|
||||
|
||||
const toggleFlip = (id: string) => {
|
||||
setFlippedCards(prev => ({
|
||||
...prev,
|
||||
[id]: !prev[id]
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProvider
|
||||
defaultButtonVariant="expand-hover"
|
||||
@@ -126,42 +136,109 @@ 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 md:py-24">
|
||||
<div className="w-content-width mx-auto px-6">
|
||||
<div className="mb-12">
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-4">What Our Members Say</h2>
|
||||
<p className="text-lg text-foreground/75">Real stories from computer club members about their learning journey and growth.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{[
|
||||
{
|
||||
id: "1", name: "Alex Chen", handle: "@AlexDev", shortText: "Landed dream job at top tech company", fullText: "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"
|
||||
},
|
||||
{
|
||||
id: "2", name: "Sarah Mitchell", handle: "@SarahCodes", shortText: "Built impressive portfolio through projects", fullText: "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"
|
||||
},
|
||||
{
|
||||
id: "3", name: "Jordan Lee", handle: "@JordanDev", shortText: "Full-stack development in just months", fullText: "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"
|
||||
},
|
||||
{
|
||||
id: "4", name: "Maya Patel", handle: "@MayaCode", shortText: "Won first place in hackathon", fullText: "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"
|
||||
},
|
||||
{
|
||||
id: "5", name: "David Brown", handle: "@DavidDeveloper", shortText: "Built a supportive tech network", fullText: "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"
|
||||
},
|
||||
{
|
||||
id: "6", name: "Lisa Wong", handle: "@LisaTech", shortText: "Discovered career possibilities", fullText: "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"
|
||||
}
|
||||
].map((testimonial) => (
|
||||
<div
|
||||
key={testimonial.id}
|
||||
className="h-64 cursor-pointer perspective"
|
||||
onClick={() => toggleFlip(testimonial.id)}
|
||||
style={{
|
||||
perspective: '1000px'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="relative w-full h-full transition-transform duration-500"
|
||||
style={{
|
||||
transformStyle: 'preserve-3d',
|
||||
transform: flippedCards[testimonial.id] ? 'rotateY(180deg)' : 'rotateY(0deg)'
|
||||
}}
|
||||
>
|
||||
{/* Front side */}
|
||||
<div
|
||||
className="absolute w-full h-full bg-card rounded-lg p-6 flex flex-col justify-between border border-accent/20"
|
||||
style={{
|
||||
backfaceVisibility: 'hidden',
|
||||
WebkitBackfaceVisibility: 'hidden'
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
{testimonial.imageSrc && (
|
||||
<img
|
||||
src={testimonial.imageSrc}
|
||||
alt={testimonial.name}
|
||||
className="w-12 h-12 rounded-full object-cover"
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<p className="font-semibold text-foreground">{testimonial.name}</p>
|
||||
<p className="text-sm text-foreground/60">{testimonial.handle}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-foreground/80 line-clamp-4">{testimonial.shortText}</p>
|
||||
</div>
|
||||
<p className="text-xs text-accent text-center mt-4">Click to read full testimonial</p>
|
||||
</div>
|
||||
|
||||
{/* Back side */}
|
||||
<div
|
||||
className="absolute w-full h-full bg-primary-cta rounded-lg p-6 flex flex-col justify-between border border-accent/20"
|
||||
style={{
|
||||
backfaceVisibility: 'hidden',
|
||||
WebkitBackfaceVisibility: 'hidden',
|
||||
transform: 'rotateY(180deg)'
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<p className="text-primary-cta-text text-sm leading-relaxed">{testimonial.fullText}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-4">
|
||||
{testimonial.imageSrc && (
|
||||
<img
|
||||
src={testimonial.imageSrc}
|
||||
alt={testimonial.name}
|
||||
className="w-10 h-10 rounded-full object-cover"
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-primary-cta-text">{testimonial.name}</p>
|
||||
<p className="text-xs text-primary-cta-text/70">{testimonial.handle}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-primary-cta-text/50 text-center mt-2">Click to go back</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="contact" data-section="contact">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,106 +1,116 @@
|
||||
"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>
|
||||
);
|
||||
})}
|
||||
const getAnimationDelay = (index: number) => {
|
||||
if (metricsAnimation === 'slide-up') {
|
||||
return {
|
||||
transitionDelay: `${index * 100}ms`,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
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)}
|
||||
style={getAnimationDelay(index)}
|
||||
>
|
||||
<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 ${bgClass} rounded-lg shadow-lg p-6 flex flex-col items-center justify-center border border-slate-200`}
|
||||
style={{
|
||||
backfaceVisibility: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Icon className={`w-12 h-12 ${useInvertedBackground ? 'text-blue-400' : 'text-blue-600'} mb-4`} />
|
||||
<p className={`text-sm font-semibold ${textClass} text-center mb-2`}>
|
||||
{metric.label}
|
||||
</p>
|
||||
<p className={`text-3xl font-bold ${useInvertedBackground ? 'text-blue-400' : 'text-blue-600'}`}>
|
||||
{metric.value}
|
||||
</p>
|
||||
<p className={`text-xs ${useInvertedBackground ? 'text-slate-400' : 'text-slate-500'} mt-4 text-center`}>
|
||||
Click to learn more
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Back Side */}
|
||||
<div
|
||||
className={`absolute w-full h-full ${useInvertedBackground ? 'bg-slate-800' : 'bg-slate-100'} rounded-lg shadow-lg p-6 flex flex-col items-center justify-center border border-slate-200`}
|
||||
style={{
|
||||
backfaceVisibility: 'hidden',
|
||||
transform: 'rotateY(180deg)',
|
||||
}}
|
||||
>
|
||||
<p className={`text-sm ${textClass} text-center leading-relaxed`}>
|
||||
{metric.label}: {metric.value} represents our commitment to excellence and continuous growth in this area.
|
||||
</p>
|
||||
<p className={`text-xs ${useInvertedBackground ? 'text-slate-400' : 'text-slate-500'} mt-4 text-center`}>
|
||||
Click to flip back
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
AboutMetric.displayName = "AboutMetric";
|
||||
|
||||
export default AboutMetric;
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,131 +1,106 @@
|
||||
"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>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
const bgClass =
|
||||
background.variant === 'radial-gradient'
|
||||
? 'bg-gradient-to-br from-blue-50 via-white to-purple-50'
|
||||
: 'bg-white';
|
||||
|
||||
ContactCenter.displayName = "ContactCenter";
|
||||
const invertedBg = useInvertedBackground ? 'bg-slate-900 text-white' : '';
|
||||
|
||||
export default ContactCenter;
|
||||
return (
|
||||
<section className={`relative py-20 px-4 sm:px-6 lg:px-8 ${bgClass} ${invertedBg}`}>
|
||||
<div className="max-w-2xl 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 leading-tight">
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
{/* Description */}
|
||||
<p className={`text-center text-lg mb-10 ${useInvertedBackground ? 'text-gray-300' : 'text-gray-600'}`}>
|
||||
{description}
|
||||
</p>
|
||||
|
||||
{/* Form */}
|
||||
<form onSubmit={handleSubmit} className="flex flex-col sm:flex-row gap-3 mb-6">
|
||||
<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-all focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
||||
useInvertedBackground
|
||||
? 'bg-slate-800 border-slate-700 text-white placeholder-gray-400'
|
||||
: 'bg-white border-gray-300 text-gray-900 placeholder-gray-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"
|
||||
>
|
||||
{isSubmitted ? '✓ Subscribed' : buttonText}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* Terms Text */}
|
||||
<p className={`text-center text-sm ${useInvertedBackground ? 'text-gray-400' : 'text-gray-500'}`}>
|
||||
{termsText}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,235 +1,120 @@
|
||||
"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)}
|
||||
/>
|
||||
return (
|
||||
<section className="w-full py-12 md:py-20 lg:py-24">
|
||||
<div className="container mx-auto px-4 md:px-6">
|
||||
{/* Header Section */}
|
||||
<div className="mb-12 md:mb-16 text-center">
|
||||
<div className="inline-flex items-center gap-2 mb-4 px-4 py-2 bg-blue-50 rounded-full">
|
||||
<TagIcon className="w-4 h-4 text-blue-600" />
|
||||
<span className="text-sm font-semibold text-blue-600">{tag}</span>
|
||||
</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 mx-auto">
|
||||
{description}
|
||||
</p>
|
||||
</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}
|
||||
/>
|
||||
{/* Flip Cards Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8">
|
||||
{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 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",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<h3 className="text-xl md:text-2xl font-bold text-gray-900 mb-2">
|
||||
{item.title}
|
||||
</h3>
|
||||
<p className="text-gray-500 text-sm">
|
||||
Click to read more
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-center w-12 h-12 bg-blue-100 rounded-lg">
|
||||
<span className="text-blue-600 font-bold text-lg">
|
||||
{item.id}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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}
|
||||
/>
|
||||
))}
|
||||
{/* Back Side */}
|
||||
<div
|
||||
className="absolute w-full h-full bg-blue-600 rounded-lg shadow-lg p-6 flex flex-col justify-between border border-blue-700"
|
||||
style={{
|
||||
backfaceVisibility: "hidden",
|
||||
transform: "rotateY(180deg)",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<h3 className="text-xl md:text-2xl font-bold text-white mb-4">
|
||||
{item.title}
|
||||
</h3>
|
||||
<p className="text-blue-50 text-sm leading-relaxed">
|
||||
{item.content}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-blue-200 text-xs text-center">
|
||||
Click to flip back
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</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>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
FeatureCardTwentyOne.displayName = "FeatureCardTwentyOne";
|
||||
|
||||
export default FeatureCardTwentyOne;
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,108 +1,89 @@
|
||||
"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">
|
||||
{/* 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>
|
||||
|
||||
<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)}>
|
||||
{/* 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}
|
||||
</span>
|
||||
<ButtonTextUnderline
|
||||
text="Privacy Policy"
|
||||
onClick={onPrivacyClick}
|
||||
className={cls("text-primary-cta-text/50", privacyButtonClassName)}
|
||||
/>
|
||||
</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>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
FooterBase.displayName = "FooterBase";
|
||||
|
||||
export default FooterBase;
|
||||
}
|
||||
@@ -1,172 +1,187 @@
|
||||
"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 lg:gap-16 items-center ${isReverse ? 'lg:grid-flow-dense' : ''}`}>
|
||||
{/* Content Section */}
|
||||
<div className={`flex flex-col justify-center space-y-8 ${isReverse ? 'lg:col-start-2' : ''}`}>
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-5xl sm:text-6xl font-bold text-gray-900 leading-tight">
|
||||
{logoText}
|
||||
</h1>
|
||||
<p className="text-xl text-gray-600 leading-relaxed max-w-lg">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Buttons */}
|
||||
<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-blue-600 text-white hover:bg-blue-700 shadow-lg'
|
||||
: 'bg-gray-200 text-gray-900 hover:bg-gray-300'
|
||||
} ${getButtonAnimationClass()}`}
|
||||
>
|
||||
{button.text}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Image Section with Flip Card */}
|
||||
<div className={`flex justify-center items-center ${isReverse ? 'lg:col-start-1' : ''}`}>
|
||||
<div
|
||||
className="w-full max-w-md h-96 cursor-pointer perspective"
|
||||
onMouseEnter={() => setIsFlipped(true)}
|
||||
onMouseLeave={() => setIsFlipped(false)}
|
||||
>
|
||||
<div
|
||||
className={`relative w-full h-full transition-transform duration-500 ${getFrameClass()} ${getMediaAnimationClass()}`}
|
||||
style={{
|
||||
transformStyle: 'preserve-3d',
|
||||
transform: isFlipped ? 'rotateY(180deg)' : 'rotateY(0deg)',
|
||||
}}
|
||||
>
|
||||
{/* Front Side - Image */}
|
||||
<div
|
||||
className={`absolute w-full h-full ${getFrameClass()}`}
|
||||
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 bg-gradient-to-br from-blue-600 to-purple-600 p-8 flex flex-col justify-center items-center text-white ${getFrameClass()}`}
|
||||
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 0 7 6 7z" />
|
||||
<path d="M15 21c3 0 7-1 7-8V5c0-1.25-4.716-5-7-5-6 0-6.002 4-6 7v11c0 1 0 7 6 7z" />
|
||||
</svg>
|
||||
<p className="text-lg font-semibold leading-relaxed">
|
||||
"CompClub has transformed how I connect with fellow tech enthusiasts. The projects are innovative and the community is incredibly supportive!"
|
||||
</p>
|
||||
<p className="text-sm opacity-90">- Sarah Chen, Tech Innovator</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
HeroLogoBillboardSplit.displayName = "HeroLogoBillboardSplit";
|
||||
|
||||
export default HeroLogoBillboardSplit;
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,186 +1,151 @@
|
||||
"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">
|
||||
{/* Header Section */}
|
||||
<div className="mb-12 text-center">
|
||||
<div className="inline-flex items-center gap-2 mb-4 px-4 py-2 rounded-full bg-blue-100 text-blue-700">
|
||||
<TagIcon className="w-4 h-4" />
|
||||
<span className="text-sm font-semibold">{tag}</span>
|
||||
</div>
|
||||
|
||||
<h2 className="text-4xl sm:text-5xl font-bold mb-4">{title}</h2>
|
||||
<p className="text-lg text-slate-600 max-w-2xl mx-auto">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
MetricCardItem.displayName = "MetricCardItem";
|
||||
{/* Metrics Grid with Flip Cards */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
{metrics.map((metric) => {
|
||||
const MetricIcon = 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 ? "rotate-y-180" : ""
|
||||
}`}
|
||||
style={{
|
||||
transformStyle: "preserve-3d",
|
||||
transform: isFlipped
|
||||
? "rotateY(180deg)"
|
||||
: "rotateY(0deg)",
|
||||
}}
|
||||
>
|
||||
{/* Front of Card */}
|
||||
<div
|
||||
className="absolute w-full h-full bg-gradient-to-br from-blue-50 to-blue-100 rounded-lg p-6 flex flex-col items-center justify-center text-center border border-blue-200 shadow-lg"
|
||||
style={{
|
||||
backfaceVisibility: "hidden",
|
||||
}}
|
||||
>
|
||||
<MetricIcon className="w-12 h-12 text-blue-600 mb-4" />
|
||||
<h3 className="text-sm font-semibold text-slate-700 mb-3">
|
||||
{metric.title}
|
||||
</h3>
|
||||
<p className="text-4xl font-bold text-blue-600">
|
||||
{metric.value}
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 mt-4">
|
||||
Click to see more
|
||||
</p>
|
||||
</div>
|
||||
|
||||
return (
|
||||
<CardStack
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
mode={carouselMode}
|
||||
gridVariant="uniform-all-items-equal"
|
||||
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
|
||||
animationType={animationType}
|
||||
supports3DAnimation={true}
|
||||
{/* Back of Card */}
|
||||
<div
|
||||
className="absolute w-full h-full bg-gradient-to-br from-slate-800 to-slate-900 rounded-lg p-6 flex flex-col items-center justify-center text-center border border-slate-700 shadow-lg text-white"
|
||||
style={{
|
||||
backfaceVisibility: "hidden",
|
||||
transform: "rotateY(180deg)",
|
||||
}}
|
||||
>
|
||||
<p className="text-sm leading-relaxed mb-4">
|
||||
{metric.title} represents our commitment to excellence and
|
||||
continuous growth in the computer club community.
|
||||
</p>
|
||||
<p className="text-xs text-slate-400">
|
||||
Click to flip back
|
||||
</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 Section */}
|
||||
{buttons.length > 0 && (
|
||||
<div className="flex flex-wrap gap-4 justify-center">
|
||||
{buttons.map((button, index) => (
|
||||
<a
|
||||
key={index}
|
||||
href={button.href}
|
||||
className="px-8 py-3 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 transition-colors duration-300 shadow-md hover:shadow-lg"
|
||||
>
|
||||
{button.text}
|
||||
</a>
|
||||
))}
|
||||
</CardStack>
|
||||
);
|
||||
};
|
||||
|
||||
MetricCardThree.displayName = "MetricCardThree";
|
||||
|
||||
export default MetricCardThree;
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,203 +1,168 @@
|
||||
"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 Testimonial {
|
||||
id: number;
|
||||
author: string;
|
||||
role: string;
|
||||
company: string;
|
||||
shortText: string;
|
||||
fullText: string;
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
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;
|
||||
testimonials?: Testimonial[];
|
||||
}
|
||||
|
||||
interface TestimonialCardProps {
|
||||
testimonial: Testimonial;
|
||||
useInvertedBackground: boolean;
|
||||
cardClassName?: string;
|
||||
testimonialClassName?: string;
|
||||
imageWrapperClassName?: string;
|
||||
imageClassName?: string;
|
||||
iconClassName?: string;
|
||||
nameClassName?: string;
|
||||
handleClassName?: string;
|
||||
}
|
||||
const defaultTestimonials: Testimonial[] = [
|
||||
{
|
||||
id: 1,
|
||||
author: "Sarah Johnson",
|
||||
role: "Product Manager",
|
||||
company: "TechCorp",
|
||||
shortText: "Amazing service!",
|
||||
fullText:
|
||||
"This service has completely transformed how we manage our projects. The intuitive interface and powerful features have saved us countless hours. The support team is incredibly responsive and helpful. I would highly recommend this to any team looking to improve their workflow.",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Sarah",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
author: "Michael Chen",
|
||||
role: "CEO",
|
||||
company: "StartupXYZ",
|
||||
shortText: "Game changer!",
|
||||
fullText:
|
||||
"We've been using this platform for 6 months now and the results speak for themselves. Our team productivity has increased by 40%, and the ROI has been exceptional. The platform scales beautifully with our growing needs. Couldn't ask for a better solution.",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Michael",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
author: "Emily Rodriguez",
|
||||
role: "Design Lead",
|
||||
company: "Creative Studios",
|
||||
shortText: "Exceeded expectations!",
|
||||
fullText:
|
||||
"The design and user experience are top-notch. Every detail has been thoughtfully considered. The collaboration features make it easy for our distributed team to work together seamlessly. This is the kind of tool that makes you wonder how you ever worked without it.",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Emily",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
author: "David Park",
|
||||
role: "Operations Director",
|
||||
company: "Global Solutions",
|
||||
shortText: "Best investment ever!",
|
||||
fullText:
|
||||
"We've tried many solutions before, but this one stands out. The implementation was smooth, the training was comprehensive, and the ongoing support is exceptional. Our team adopted it immediately and we're seeing measurable improvements in efficiency and accuracy.",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=David",
|
||||
},
|
||||
];
|
||||
|
||||
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);
|
||||
export default function TestimonialCardSix({
|
||||
testimonials = defaultTestimonials,
|
||||
}: TestimonialCardSixProps) {
|
||||
const [flipped, setFlipped] = useState<{ [key: number]: boolean }>({});
|
||||
|
||||
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>
|
||||
const toggleFlip = (id: number) => {
|
||||
setFlipped((prev) => ({
|
||||
...prev,
|
||||
[id]: !prev[id],
|
||||
}));
|
||||
};
|
||||
|
||||
<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}
|
||||
/>
|
||||
return (
|
||||
<section className="py-12 px-4 sm:px-6 lg:px-8 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-3xl sm:text-4xl font-bold text-gray-900 mb-4">
|
||||
What Our Clients Say
|
||||
</h2>
|
||||
<p className="text-lg text-gray-600">
|
||||
Hover over cards to see full testimonials
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
TestimonialCard.displayName = "TestimonialCard";
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{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-6 flex flex-col justify-between"
|
||||
style={{ backfaceVisibility: "hidden" }}
|
||||
>
|
||||
<div>
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
{testimonial.avatar && (
|
||||
<img
|
||||
src={testimonial.avatar}
|
||||
alt={testimonial.author}
|
||||
className="w-12 h-12 rounded-full"
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900">
|
||||
{testimonial.author}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600">
|
||||
{testimonial.role}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gray-700 text-sm mb-4">
|
||||
"{testimonial.shortText}"
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs text-gray-500">{testimonial.company}</p>
|
||||
<div className="flex gap-1">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<span key={i} className="text-yellow-400">
|
||||
★
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
const TestimonialCardSix = ({
|
||||
testimonials,
|
||||
animationType,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
textboxLayout,
|
||||
useInvertedBackground,
|
||||
tag,
|
||||
tagIcon,
|
||||
tagAnimation,
|
||||
buttons,
|
||||
buttonAnimation,
|
||||
speed = 40,
|
||||
topMarqueeDirection = "left",
|
||||
ariaLabel = "Testimonials section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
carouselClassName = "",
|
||||
bottomCarouselClassName = "",
|
||||
cardClassName = "",
|
||||
testimonialClassName = "",
|
||||
imageWrapperClassName = "",
|
||||
imageClassName = "",
|
||||
iconClassName = "",
|
||||
nameClassName = "",
|
||||
handleClassName = "",
|
||||
textBoxClassName = "",
|
||||
textBoxTitleClassName = "",
|
||||
textBoxTitleImageWrapperClassName = "",
|
||||
textBoxTitleImageClassName = "",
|
||||
textBoxDescriptionClassName = "",
|
||||
textBoxTagClassName = "",
|
||||
textBoxButtonContainerClassName = "",
|
||||
textBoxButtonClassName = "",
|
||||
textBoxButtonTextClassName = "",
|
||||
}: TestimonialCardSixProps) => {
|
||||
return (
|
||||
<AutoCarousel
|
||||
speed={speed}
|
||||
uniformGridCustomHeightClasses="min-h-none"
|
||||
animationType={animationType}
|
||||
title={title}
|
||||
titleSegments={titleSegments}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
tagAnimation={tagAnimation}
|
||||
buttons={buttons}
|
||||
buttonAnimation={buttonAnimation}
|
||||
textboxLayout={textboxLayout}
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
showTextBox={true}
|
||||
dualMarquee={true}
|
||||
topMarqueeDirection={topMarqueeDirection}
|
||||
carouselClassName={carouselClassName}
|
||||
bottomCarouselClassName={bottomCarouselClassName}
|
||||
containerClassName={containerClassName}
|
||||
className={className}
|
||||
textBoxClassName={textBoxClassName}
|
||||
titleClassName={textBoxTitleClassName}
|
||||
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
|
||||
titleImageClassName={textBoxTitleImageClassName}
|
||||
descriptionClassName={textBoxDescriptionClassName}
|
||||
tagClassName={textBoxTagClassName}
|
||||
buttonContainerClassName={textBoxButtonContainerClassName}
|
||||
buttonClassName={textBoxButtonClassName}
|
||||
buttonTextClassName={textBoxButtonTextClassName}
|
||||
ariaLabel={ariaLabel}
|
||||
itemClassName="w-60! md:w-carousel-item-3! xl:w-carousel-item-4!"
|
||||
>
|
||||
{testimonials.map((testimonial, index) => (
|
||||
<TestimonialCard
|
||||
key={`${testimonial.id}-${index}`}
|
||||
testimonial={testimonial}
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
cardClassName={cardClassName}
|
||||
testimonialClassName={testimonialClassName}
|
||||
imageWrapperClassName={imageWrapperClassName}
|
||||
imageClassName={imageClassName}
|
||||
iconClassName={iconClassName}
|
||||
nameClassName={nameClassName}
|
||||
handleClassName={handleClassName}
|
||||
/>
|
||||
))}
|
||||
</AutoCarousel>
|
||||
);
|
||||
};
|
||||
|
||||
TestimonialCardSix.displayName = "TestimonialCardSix";
|
||||
|
||||
export default TestimonialCardSix;
|
||||
{/* Back of card */}
|
||||
<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-between text-white"
|
||||
style={{
|
||||
backfaceVisibility: "hidden",
|
||||
transform: "rotateY(180deg)",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg mb-3">
|
||||
{testimonial.author}
|
||||
</h3>
|
||||
<p className="text-sm text-blue-100 leading-relaxed">
|
||||
"{testimonial.fullText}"
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-blue-200 text-center">
|
||||
Click to flip back
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
275
src/tag/Tag.tsx
275
src/tag/Tag.tsx
@@ -1,140 +1,153 @@
|
||||
// "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. I would highly recommend them to anyone looking for top-tier service.',
|
||||
avatar: '👩💼',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
author: 'Michael Chen',
|
||||
role: 'CEO',
|
||||
company: 'StartupXYZ',
|
||||
excerpt: 'Exceeded all our expectations.',
|
||||
fullText: 'From day one, they demonstrated exceptional professionalism and expertise. They understood our vision and delivered results that surpassed our expectations. The collaboration was seamless and the outcomes were outstanding. Truly a game-changer for our business.',
|
||||
avatar: '👨💼',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
author: 'Emily Rodriguez',
|
||||
role: 'Design Lead',
|
||||
company: 'Creative Studios',
|
||||
excerpt: 'Best decision we made this year.',
|
||||
fullText: 'Working with this team was the best decision we made all year. Their innovative approach and creative solutions transformed our project completely. They were responsive, professional, and genuinely invested in our success. I cannot recommend them highly enough.',
|
||||
avatar: '👩🎨',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
author: 'David Park',
|
||||
role: 'Operations Director',
|
||||
company: 'Global Solutions Inc',
|
||||
excerpt: 'Incredible attention to detail.',
|
||||
fullText: 'The level of detail and care they put into every aspect of the project was incredible. They anticipated our needs, solved problems proactively, and delivered exceptional results on time and within budget. Their expertise and dedication made all the difference.',
|
||||
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 sm:px-6 lg:px-8">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl sm:text-4xl font-bold text-gray-900 mb-4">
|
||||
What Our Clients Say
|
||||
</h2>
|
||||
<p className="text-lg text-gray-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-4 gap-6">
|
||||
{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-6 flex flex-col justify-between border border-gray-200"
|
||||
style={{
|
||||
backfaceVisibility: 'hidden',
|
||||
}}
|
||||
>
|
||||
<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 text-sm">
|
||||
{testimonial.author}
|
||||
</h3>
|
||||
<p className="text-xs text-gray-600">
|
||||
{testimonial.role}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{testimonial.company}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gray-700 text-sm leading-relaxed">
|
||||
"{testimonial.excerpt}"
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<span key={i} className="text-yellow-400">
|
||||
★
|
||||
</span>
|
||||
))}
|
||||
</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-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-100 text-xs mt-4 font-semibold">
|
||||
— {testimonial.author}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user