Merge version_2 into main #2

Merged
bender merged 14 commits from version_2 into main 2026-02-27 12:35:14 +00:00
14 changed files with 1604 additions and 1242 deletions

View File

@@ -16,7 +16,8 @@ export const metadata: Metadata = {
openGraph: {
title: "Computer Club - Tech Community & Innovation Hub", description: "Join a vibrant community of tech enthusiasts. Access coding workshops, hackathons, networking events, and mentorship programs.", type: "website", siteName: "Computer Club", images: [
{
url: "http://img.b2bpic.net/free-photo/gradient-illuminated-neon-gaming-desk-setup-with-keyboard_23-2149529414.jpg", alt: "Computer Club Community"},
url: "http://img.b2bpic.net/free-photo/gradient-illuminated-neon-gaming-desk-setup-with-keyboard_23-2149529414.jpg", alt: "Computer Club Community"
},
],
},
twitter: {

View File

@@ -7,10 +7,10 @@ import MetricSplitMediaAbout from '@/components/sections/about/MetricSplitMediaA
import FeatureBorderGlow from '@/components/sections/feature/featureBorderGlow/FeatureBorderGlow';
import ProductCardFour from '@/components/sections/product/ProductCardFour';
import TestimonialCardTwelve from '@/components/sections/testimonial/TestimonialCardTwelve';
import TeamCardFive from '@/components/sections/team/TeamCardFive';
import TeamCardFiveFlip from '@/components/sections/team/TeamCardFiveFlip';
import FaqBase from '@/components/sections/faq/FaqBase';
import FooterLogoEmphasis from '@/components/sections/footer/FooterLogoEmphasis';
import { BookOpen, Calendar, Code, Info, Lightbulb, Rocket, Star, Trophy, Users, Zap } from "lucide-react";
import { BookOpen, Calendar, Code, Info, Lightbulb, Rocket, Star, Trophy, Users, Zap, Github, Linkedin, Twitter } from "lucide-react";
export default function ComputerClubPage() {
return (
@@ -161,7 +161,7 @@ export default function ComputerClubPage() {
</div>
<div id="team" data-section="team">
<TeamCardFive
<TeamCardFiveFlip
title="Meet Our Leadership"
description="Dedicated professionals driving innovation and community growth"
tag="Leadership"
@@ -171,16 +171,32 @@ export default function ComputerClubPage() {
useInvertedBackground={false}
team={[
{
id: "1", name: "Alex Johnson", role: "Founder & President", imageSrc: "http://img.b2bpic.net/free-photo/black-businessman-happy-expression_1194-2533.jpg", imageAlt: "Alex Johnson"
id: "1", name: "Alex Johnson", role: "Founder & President", imageSrc: "http://img.b2bpic.net/free-photo/black-businessman-happy-expression_1194-2533.jpg", imageAlt: "Alex Johnson", socialLinks: [
{ icon: Github, url: "https://github.com" },
{ icon: Linkedin, url: "https://linkedin.com" },
{ icon: Twitter, url: "https://twitter.com" }
]
},
{
id: "2", name: "Jessica Lee", role: "Vice President", imageSrc: "http://img.b2bpic.net/free-photo/smiling-beautiful-middle-aged-business-woman_1262-3085.jpg?_wi=1", imageAlt: "Jessica Lee"
id: "2", name: "Jessica Lee", role: "Vice President", imageSrc: "http://img.b2bpic.net/free-photo/smiling-beautiful-middle-aged-business-woman_1262-3085.jpg?_wi=1", imageAlt: "Jessica Lee", socialLinks: [
{ icon: Github, url: "https://github.com" },
{ icon: Linkedin, url: "https://linkedin.com" },
{ icon: Twitter, url: "https://twitter.com" }
]
},
{
id: "3", name: "David Kumar", role: "Technical Director", imageSrc: "http://img.b2bpic.net/free-photo/smiling-beautiful-middle-aged-business-woman_1262-3085.jpg?_wi=2", imageAlt: "David Kumar"
id: "3", name: "David Kumar", role: "Technical Director", imageSrc: "http://img.b2bpic.net/free-photo/smiling-beautiful-middle-aged-business-woman_1262-3085.jpg?_wi=2", imageAlt: "David Kumar", socialLinks: [
{ icon: Github, url: "https://github.com" },
{ icon: Linkedin, url: "https://linkedin.com" },
{ icon: Twitter, url: "https://twitter.com" }
]
},
{
id: "4", name: "Rachel Martinez", role: "Event Coordinator", imageSrc: "http://img.b2bpic.net/free-photo/smiling-beautiful-middle-aged-business-woman_1262-3085.jpg?_wi=3", imageAlt: "Rachel Martinez"
id: "4", name: "Rachel Martinez", role: "Event Coordinator", imageSrc: "http://img.b2bpic.net/free-photo/smiling-beautiful-middle-aged-business-woman_1262-3085.jpg?_wi=3", imageAlt: "Rachel Martinez", socialLinks: [
{ icon: Github, url: "https://github.com" },
{ icon: Linkedin, url: "https://linkedin.com" },
{ icon: Twitter, url: "https://twitter.com" }
]
}
]}
/>

View File

@@ -0,0 +1,163 @@
"use client";
import React from "react";
interface TitleSegment {
text: string;
highlight?: boolean;
}
interface Button {
label: string;
href?: string;
onClick?: () => void;
variant?: "primary" | "secondary";
}
interface CardStackTextBoxProps {
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: React.ReactNode;
tagAnimation?: string;
buttons?: Button[];
buttonAnimation?: string;
textboxLayout?: "default" | "centered" | "compact";
useInvertedBackground?: boolean;
className?: string;
titleClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
}
export function CardStackTextBox({
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout = "default",
useInvertedBackground = false,
className = "",
titleClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
}: CardStackTextBoxProps) {
const baseClasses = `
w-full h-full p-6 flex flex-col justify-between
${useInvertedBackground ? "bg-slate-900 text-white" : "bg-white text-slate-900"}
rounded-lg transition-all duration-300
`;
const layoutClasses = {
default: "items-start",
centered: "items-center text-center",
compact: "items-start gap-2",
};
const titleDefaultClasses = `
text-2xl font-bold mb-4 leading-tight
${titleClassName}
`;
const descriptionDefaultClasses = `
text-base leading-relaxed mb-4 flex-grow
${useInvertedBackground ? "text-slate-300" : "text-slate-600"}
${descriptionClassName}
`;
const tagDefaultClasses = `
inline-flex items-center gap-2 px-3 py-1 rounded-full text-sm font-medium mb-4
${useInvertedBackground ? "bg-slate-800 text-slate-200" : "bg-slate-100 text-slate-700"}
${tagClassName}
`;
const buttonContainerDefaultClasses = `
flex gap-3 w-full
${textboxLayout === "centered" ? "justify-center" : "justify-start"}
${buttonContainerClassName}
`;
const buttonDefaultClasses = `
px-4 py-2 rounded-lg font-medium transition-all duration-200
${buttonClassName}
`;
const buttonTextDefaultClasses = `
${buttonTextClassName}
`;
const renderTitle = () => {
if (titleSegments && titleSegments.length > 0) {
return (
<h3 className={titleDefaultClasses}>
{titleSegments.map((segment, index) => (
<span
key={index}
className={segment.highlight ? "text-blue-500" : ""}
>
{segment.text}
</span>
))}
</h3>
);
}
return <h3 className={titleDefaultClasses}>{title}</h3>;
};
return (
<div className={`${baseClasses} ${layoutClasses[textboxLayout]} ${className}`}>
<div className="w-full">
{tag && (
<div className={tagDefaultClasses}>
{tagIcon && <span>{tagIcon}</span>}
<span>{tag}</span>
</div>
)}
{renderTitle()}
<p className={descriptionDefaultClasses}>{description}</p>
</div>
{buttons && buttons.length > 0 && (
<div className={buttonContainerDefaultClasses}>
{buttons.map((button, index) => (
<button
key={index}
onClick={button.onClick}
className={`
${buttonDefaultClasses}
${
button.variant === "secondary"
? useInvertedBackground
? "bg-slate-700 hover:bg-slate-600 text-white"
: "bg-slate-200 hover:bg-slate-300 text-slate-900"
: useInvertedBackground
? "bg-blue-600 hover:bg-blue-700 text-white"
: "bg-blue-500 hover:bg-blue-600 text-white"
}
`}
>
<span className={buttonTextDefaultClasses}>{button.label}</span>
</button>
))}
</div>
)}
</div>
);
}

View File

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

View File

@@ -1,148 +1,93 @@
"use client";
'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 React, { 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 [isOpen, setIsOpen] = useState(false);
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) => {
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
if (theme.defaultButtonVariant === "icon-arrow") {
return { className: "justify-between" };
}
return {};
setIsOpen(false);
};
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="/" />
<nav className="sticky top-0 z-50 bg-white shadow-md">
<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 Name */}
<div className="flex-shrink-0">
<span className="text-2xl font-bold text-gray-900">
{brandName}
</span>
</div>
{/* Desktop Navigation */}
<div className="hidden md:flex items-center space-x-8">
{navItems.map((item) => (
<button
key={item.id}
onClick={() => handleNavClick(item.id)}
className="text-gray-700 hover:text-blue-600 font-medium transition-colors duration-200"
>
{item.name}
</button>
))}
</div>
{/* Mobile Menu Button */}
<div className="md:hidden">
<button
onClick={() => setIsOpen(!isOpen)}
className="inline-flex items-center justify-center p-2 rounded-md text-gray-700 hover:text-blue-600 hover:bg-gray-100 focus:outline-none transition-colors duration-200"
>
<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
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
)}
/>
{/* Mobile Navigation */}
{isOpen && (
<div className="md:hidden pb-4 space-y-2">
{navItems.map((item) => (
<button
key={item.id}
onClick={() => handleNavClick(item.id)}
className="block w-full text-left px-4 py-2 text-gray-700 hover:text-blue-600 hover:bg-gray-100 rounded-md font-medium transition-colors duration-200"
>
{item.name}
</button>
))}
</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>
</div>
<MobileMenu
menuOpen={menuOpen}
onMenuToggle={handleMenuToggle}
navItems={navItems}
onNavClick={handleMobileNavClick}
>
{button && (
<Button
{...getButtonProps(
{
...button,
onClick: () => {
button.onClick?.();
setMenuOpen(false);
},
props: { ...button.props, ...getButtonConfigProps() }
},
0,
theme.defaultButtonVariant,
cls("w-full", buttonClassName),
buttonTextClassName
)}
/>
)}
</MobileMenu>
</nav>
);
};
export default NavbarStyleApple;
}

View File

@@ -1,169 +1,113 @@
"use client";
import { memo } from "react";
import TextBox from "@/components/Textbox";
import MediaContent from "@/components/shared/MediaContent";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import { useButtonAnimation } from "@/components/hooks/useButtonAnimation";
import type { LucideIcon } from "lucide-react";
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
import type { ButtonAnimationType } from "@/types/button";
import React from "react";
import { LucideIcon } from "lucide-react";
interface Metric {
value: string;
title: string;
}
interface MetricSplitMediaAboutProps {
interface Props {
tag: string;
tagIcon: LucideIcon;
title: string;
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
metrics: Metric[];
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
mediaAnimation: ButtonAnimationType;
metricsAnimation: ButtonAnimationType;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
gridClassName?: string;
leftColumnClassName?: string;
rightColumnClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
metricsContainerClassName?: string;
metricCardClassName?: string;
metricValueClassName?: string;
metricTitleClassName?: string;
mediaWrapperClassName?: string;
imageClassName?: string;
imageSrc: string;
imageAlt: string;
useInvertedBackground?: boolean;
mediaAnimation?: string;
metricsAnimation?: string;
}
const MetricSplitMediaAbout = ({
export default function MetricSplitMediaAbout({
tag,
tagIcon: TagIcon,
title,
description,
tag,
tagIcon,
tagAnimation,
metrics,
imageSrc,
videoSrc,
imageAlt = "",
videoAriaLabel = "About section video",
mediaAnimation,
metricsAnimation,
useInvertedBackground,
ariaLabel = "About section",
className = "",
containerClassName = "",
gridClassName = "",
leftColumnClassName = "",
rightColumnClassName = "",
textBoxClassName = "",
titleClassName = "",
descriptionClassName = "",
tagClassName = "",
metricsContainerClassName = "",
metricCardClassName = "",
metricValueClassName = "",
metricTitleClassName = "",
mediaWrapperClassName = "",
imageClassName = "",
}: MetricSplitMediaAboutProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const { containerRef: mediaContainerRef } = useButtonAnimation({ animationType: mediaAnimation });
const { containerRef: metricsContainerRef } = useButtonAnimation({ animationType: metricsAnimation });
imageAlt,
useInvertedBackground = false,
mediaAnimation = "slide-up",
metricsAnimation = "slide-up",
}: Props) {
const bgClass = useInvertedBackground ? "bg-slate-900" : "bg-white";
const textClass = useInvertedBackground ? "text-white" : "text-slate-900";
const descriptionClass = useInvertedBackground
? "text-slate-300"
: "text-slate-600";
const getAnimationClass = (animation: string) => {
switch (animation) {
case "slide-up":
return "animate-in fade-in slide-in-from-bottom-4 duration-700";
case "fade":
return "animate-in fade-in duration-700";
default:
return "";
}
};
return (
<section
aria-label={ariaLabel}
className={cls(
"relative py-20 w-full",
useInvertedBackground && "bg-foreground",
className
)}
>
<div className={cls("w-content-width mx-auto", containerClassName)}>
<div className={cls("grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-10", gridClassName)}>
<div className={cls("flex flex-col gap-6 md:gap-10", leftColumnClassName)}>
<TextBox
title={title}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
useInvertedBackground={useInvertedBackground}
className={cls("gap-3 md:gap-3", textBoxClassName)}
titleClassName={cls("text-6xl font-medium text-balance", titleClassName)}
descriptionClassName={cls("text-lg leading-tight text-balance", descriptionClassName)}
tagClassName={cls("mb-1", tagClassName)}
/>
{metrics && metrics.length > 0 && (
<div ref={metricsContainerRef} className={cls(
"grid gap-6 md:gap-4",
metrics.length === 1 ? "grid-cols-1" : "grid-cols-1 md:grid-cols-2",
metricsContainerClassName
)}>
{metrics.slice(0, 2).map((metric, index) => (
<div
key={index}
className={cls(
"card rounded-theme-capped p-6 flex flex-col gap-8 md:gap-16",
metricCardClassName
)}
<section className={`${bgClass} py-16 md:py-24 transition-colors duration-300`}>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-16 items-center">
{/* Left Content */}
<div className={`${getAnimationClass(mediaAnimation)}`}>
{/* Tag */}
<div className="flex items-center gap-2 mb-6">
<TagIcon className="w-5 h-5 text-blue-600" />
<span className="text-sm font-semibold text-blue-600 uppercase tracking-wider">
{tag}
</span>
</div>
{/* Title */}
<h2
className={`text-4xl md:text-5xl font-bold ${textClass} mb-6 leading-tight`}
>
{title}
</h2>
{/* Description */}
<p className={`${descriptionClass} text-lg leading-relaxed mb-8`}>
{description}
</p>
{/* Metrics */}
<div
className={`grid grid-cols-2 gap-6 ${getAnimationClass(metricsAnimation)}`}
>
{metrics.map((metric, index) => (
<div key={index} className="flex flex-col">
<span
className={`text-3xl md:text-4xl font-bold ${textClass} mb-2`}
>
<span
className={cls(
"text-6xl font-medium truncate",
shouldUseLightText ? "text-background" : "text-foreground",
metricValueClassName
)}
>
{metric.value}
</span>
<h3
className={cls(
"text-lg",
shouldUseLightText ? "text-background" : "text-foreground",
metricTitleClassName
)}
>
{metric.title}
</h3>
</div>
))}
</div>
)}
{metric.value}
</span>
<span className={`${descriptionClass} text-sm font-medium`}>
{metric.title}
</span>
</div>
))}
</div>
</div>
<div className={cls("relative", rightColumnClassName)}>
<div ref={mediaContainerRef} className={cls(
"md:absolute md:inset-0 w-full h-full overflow-hidden card rounded-theme-capped",
mediaWrapperClassName
)}>
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("w-full h-full object-cover", imageClassName)}
{/* Right Image */}
<div className={`${getAnimationClass(mediaAnimation)}`}>
<div className="relative overflow-hidden rounded-2xl shadow-2xl">
<img
src={imageSrc}
alt={imageAlt}
className="w-full h-auto object-cover aspect-square md:aspect-auto"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent" />
</div>
</div>
</div>
</div>
</section>
);
};
MetricSplitMediaAbout.displayName = "MetricSplitMediaAbout";
export default memo(MetricSplitMediaAbout);
}

View File

@@ -1,154 +1,172 @@
"use client";
import { useState } from "react";
import CardStackTextBox from "@/components/cardStack/CardStackTextBox";
import Accordion from "@/components/Accordion";
import { cls } from "@/lib/utils";
import { useButtonAnimation } from "@/components/hooks/useButtonAnimation";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
import React, { useState } from "react";
interface FaqItem {
id: string;
title: string;
content: string;
id: string;
title: string;
content: string;
}
interface FaqBaseProps {
faqs: FaqItem[];
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
faqsAnimation: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
animationType?: "smooth" | "instant";
showCard?: boolean;
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxTitleClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
textBoxDescriptionClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
faqsContainerClassName?: string;
accordionClassName?: string;
accordionTitleClassName?: string;
accordionIconContainerClassName?: string;
accordionIconClassName?: string;
accordionContentClassName?: string;
separatorClassName?: string;
title: string;
description: string;
tag: string;
textboxLayout: "default" | "compact" | "wide";
useInvertedBackground: boolean;
faqsAnimation: "slide-up" | "fade-in" | "bounce";
faqs: FaqItem[];
}
const FaqBase = ({
faqs,
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
faqsAnimation,
textboxLayout,
useInvertedBackground,
animationType = "smooth",
showCard = true,
ariaLabel = "FAQ section",
className = "",
containerClassName = "",
textBoxTitleClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
textBoxDescriptionClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
faqsContainerClassName = "",
accordionClassName = "",
accordionTitleClassName = "",
accordionIconContainerClassName = "",
accordionIconClassName = "",
accordionContentClassName = "",
separatorClassName = "",
}: FaqBaseProps) => {
const [activeIndex, setActiveIndex] = useState<number | null>(null);
const { containerRef: faqsContainerRef } = useButtonAnimation({ animationType: faqsAnimation });
export default function FaqBase({
title,
description,
tag,
textboxLayout = "default",
useInvertedBackground = false,
faqsAnimation = "slide-up",
faqs = [],
}: FaqBaseProps) {
const [expandedId, setExpandedId] = useState<string | null>(null);
const handleToggle = (index: number) => {
setActiveIndex(activeIndex === index ? null : index);
};
const toggleExpanded = (id: string) => {
setExpandedId(expandedId === id ? null : id);
};
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)}>
{(title || description) && (
<CardStackTextBox
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
/>
)}
const getAnimationClass = () => {
switch (faqsAnimation) {
case "fade-in":
return "animate-fade-in";
case "bounce":
return "animate-bounce";
case "slide-up":
default:
return "animate-slide-up";
}
};
<div ref={faqsContainerRef} className={cls("flex flex-col gap-4", faqsContainerClassName)}>
{faqs.map((faq, index) => (
<div key={faq.id}>
<Accordion
index={index}
isActive={activeIndex === index}
onToggle={handleToggle}
title={faq.title}
content={faq.content}
animationType={animationType}
showCard={showCard}
useInvertedBackground={useInvertedBackground}
className={accordionClassName}
titleClassName={accordionTitleClassName}
iconContainerClassName={accordionIconContainerClassName}
iconClassName={accordionIconClassName}
contentClassName={accordionContentClassName}
/>
{!showCard && index < faqs.length - 1 && (
<div className={cls("w-full border-b border-foreground/10 mt-4", separatorClassName)} />
)}
</div>
))}
</div>
const getTextboxLayoutClass = () => {
switch (textboxLayout) {
case "compact":
return "max-w-2xl";
case "wide":
return "max-w-6xl";
case "default":
default:
return "max-w-4xl";
}
};
const bgClass = useInvertedBackground
? "bg-gray-900 text-white"
: "bg-white text-gray-900";
return (
<section className={`py-16 px-4 ${bgClass}`}>
<style>{`
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.animate-slide-up {
animation: slideUp 0.6s ease-out;
}
.animate-fade-in {
animation: fadeIn 0.6s ease-out;
}
`}</style>
<div className={`mx-auto ${getTextboxLayoutClass()}`}>
{/* Header Section */}
<div className="mb-12 text-center">
{tag && (
<div className="mb-4 inline-block">
<span className="inline-block bg-blue-100 text-blue-800 px-4 py-1 rounded-full text-sm font-semibold">
{tag}
</span>
</div>
</section>
);
};
)}
<h2 className="text-4xl font-bold mb-4">{title}</h2>
<p className={`text-lg ${useInvertedBackground ? "text-gray-300" : "text-gray-600"}`}>
{description}
</p>
</div>
FaqBase.displayName = "FaqBase";
{/* FAQ Items */}
<div className="space-y-4">
{faqs.map((faq, index) => (
<div
key={faq.id}
className={`${getAnimationClass()} border rounded-lg overflow-hidden transition-all duration-300 ${
useInvertedBackground
? "border-gray-700 hover:border-blue-500"
: "border-gray-200 hover:border-blue-500"
}`}
style={{
animationDelay: `${index * 0.1}s`,
}}
>
<button
onClick={() => toggleExpanded(faq.id)}
className={`w-full px-6 py-4 flex items-center justify-between font-semibold transition-colors ${
useInvertedBackground
? "bg-gray-800 hover:bg-gray-700"
: "bg-gray-50 hover:bg-gray-100"
}`}
>
<span className="text-left">{faq.title}</span>
<svg
className={`w-5 h-5 transition-transform duration-300 ${
expandedId === faq.id ? "rotate-180" : ""
}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 14l-7 7m0 0l-7-7m7 7V3"
/>
</svg>
</button>
export default FaqBase;
{expandedId === faq.id && (
<div
className={`px-6 py-4 border-t ${
useInvertedBackground
? "border-gray-700 bg-gray-900"
: "border-gray-200 bg-gray-50"
}`}
>
<p
className={`${
useInvertedBackground ? "text-gray-300" : "text-gray-700"
}`}
>
{faq.content}
</p>
</div>
)}
</div>
))}
</div>
</div>
</section>
);
}

View File

@@ -1,155 +1,145 @@
"use client";
'use client';
import CardStack from "@/components/cardStack/CardStack";
import FeatureBorderGlowItem from "./FeatureBorderGlowItem";
import { shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type {
ButtonConfig,
CardAnimationType,
TitleSegment,
ButtonAnimationType,
} from "@/components/cardStack/types";
import type {
TextboxLayout,
InvertedBackground,
} from "@/providers/themeProvider/config/constants";
import React from 'react';
import { LucideIcon } from 'lucide-react';
interface FeatureCard {
interface Feature {
icon: LucideIcon;
title: string;
description: string;
}
interface FeatureBorderGlowProps {
features: FeatureCard[];
carouselMode?: "auto" | "buttons";
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
iconContainerClassName?: string;
iconClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
cardTitleClassName?: string;
cardDescriptionClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
tag: string;
features: Feature[];
textboxLayout?: string;
}
const FeatureBorderGlow = ({
features,
carouselMode = "buttons",
uniformGridCustomHeightClasses = "min-h-75 2xl:min-h-85",
animationType,
export default function FeatureBorderGlow({
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
textboxLayout,
useInvertedBackground,
ariaLabel = "Feature section",
className = "",
containerClassName = "",
cardClassName = "",
iconContainerClassName = "",
iconClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: FeatureBorderGlowProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(
useInvertedBackground,
theme.cardStyle
);
features,
textboxLayout = 'default',
}: FeatureBorderGlowProps) {
return (
<CardStack
mode={carouselMode}
gridVariant="uniform-all-items-equal"
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{features.map((feature, index) => (
<FeatureBorderGlowItem
key={`${feature.title}-${index}`}
item={feature}
index={index}
className={cardClassName}
iconContainerClassName={iconContainerClassName}
iconClassName={iconClassName}
titleClassName={cardTitleClassName}
descriptionClassName={cardDescriptionClassName}
shouldUseLightText={shouldUseLightText}
/>
))}
</CardStack>
<section className="relative w-full py-16 px-4 sm:px-6 lg:px-8">
<div className="max-w-7xl mx-auto">
{/* Header Section */}
<div className="text-center mb-16">
<div className="inline-block mb-4">
<span className="px-4 py-2 rounded-full bg-blue-500/10 text-blue-600 text-sm font-semibold border border-blue-500/20">
{tag}
</span>
</div>
<h2 className="text-4xl sm:text-5xl font-bold text-gray-900 dark:text-white mb-4">
{title}
</h2>
<p className="text-lg text-gray-600 dark:text-gray-300 max-w-2xl mx-auto">
{description}
</p>
</div>
{/* Features Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{features.map((feature, index) => {
const Icon = feature.icon;
return (
<div
key={index}
className="group relative h-80 rounded-xl overflow-hidden bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 border border-gray-200 dark:border-gray-700 transition-all duration-300 hover:shadow-xl"
>
{/* Border Glow Effect */}
<div className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<div className="absolute inset-0 bg-gradient-to-r from-blue-500/20 via-purple-500/20 to-pink-500/20 rounded-xl blur-xl" />
</div>
{/* Front Content */}
<div className="absolute inset-0 p-6 flex flex-col justify-center items-center text-center z-10 group-hover:opacity-0 transition-opacity duration-300">
<div className="mb-4 p-3 rounded-lg bg-blue-500/10 dark:bg-blue-500/20">
<Icon className="w-8 h-8 text-blue-600 dark:text-blue-400" />
</div>
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-3">
{feature.title}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-300 leading-relaxed">
{feature.description}
</p>
</div>
{/* Back Content (Hover) */}
<div className="absolute inset-0 p-6 flex flex-col justify-center items-center text-center z-10 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<div className="mb-6">
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-4">
Connect
</h3>
</div>
<div className="flex gap-4 justify-center">
<a
href="#"
className="p-3 rounded-full bg-blue-500 hover:bg-blue-600 text-white transition-colors duration-200"
aria-label="LinkedIn"
>
<svg
className="w-5 h-5"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z" />
</svg>
</a>
<a
href="#"
className="p-3 rounded-full bg-blue-400 hover:bg-blue-500 text-white transition-colors duration-200"
aria-label="Twitter"
>
<svg
className="w-5 h-5"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M23 3a10.9 10.9 0 01-3.14 1.53 4.48 4.48 0 00-7.86 3v1A10.66 10.66 0 013 4s-4 9 5 13a11.64 11.64 0 01-7 2s9 5 20 5a9.5 9.5 0 00-9-5.5c4.75 2.25 9-1 9-5.5a4.5 4.5 0 00-.08-.83A7.72 7.72 0 0023 3z" />
</svg>
</a>
<a
href="#"
className="p-3 rounded-full bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white transition-colors duration-200"
aria-label="Instagram"
>
<svg
className="w-5 h-5"
fill="currentColor"
viewBox="0 0 24 24"
>
<rect
x="2"
y="2"
width="20"
height="20"
rx="5"
ry="5"
fill="none"
stroke="currentColor"
strokeWidth="2"
/>
<path
d="M16 11.37A4 4 0 1112.63 8 4 4 0 0116 11.37z"
fill="none"
stroke="currentColor"
strokeWidth="2"
/>
<circle cx="17.5" cy="6.5" r="1.5" fill="currentColor" />
</svg>
</a>
</div>
</div>
</div>
);
})}
</div>
</div>
</section>
);
};
FeatureBorderGlow.displayName = "FeatureBorderGlow";
export default FeatureBorderGlow;
}

View File

@@ -1,120 +1,76 @@
"use client";
import ButtonTextUnderline from "@/components/button/ButtonTextUnderline";
import FillWidthText from "@/components/shared/FillWidthText/FillWidthText";
import { ChevronRight } from "lucide-react";
import { cls } from "@/lib/utils";
import Link from "next/link";
interface FooterItem {
label: string;
href: string;
}
interface FooterColumn {
items: Array<{
label: string;
href?: string;
onClick?: () => void;
}>;
items: FooterItem[];
}
interface FooterLogoEmphasisProps {
// logoSrc?: string;
// logoAlt?: string;
columns: FooterColumn[];
logoText: string;
ariaLabel?: string;
className?: string;
containerClassName?: string;
logoClassName?: string;
columnsClassName?: string;
columnClassName?: string;
itemClassName?: string;
iconClassName?: string;
buttonClassName?: string;
columns: FooterColumn[];
}
const FooterLogoEmphasis = ({
// logoSrc,
// logoAlt = "Logo",
columns,
export default function FooterLogoEmphasis({
logoText,
ariaLabel = "Site footer",
className = "",
containerClassName = "",
logoClassName = "",
columnsClassName = "",
columnClassName = "",
itemClassName = "",
iconClassName = "",
buttonClassName = "",
}: FooterLogoEmphasisProps) => {
const columnCount = columns.length;
const useFlex = columnCount <= 3;
const gridColsClass = columnCount === 4
? "grid-cols-2 md:grid-cols-4"
: "grid-cols-2 md:grid-cols-5";
columns,
}: FooterLogoEmphasisProps) {
return (
<footer className="bg-gradient-to-b from-slate-900 to-slate-950 text-white py-12 px-4">
<div className="max-w-6xl mx-auto">
{/* Logo Section */}
<div className="mb-12 pb-8 border-b border-slate-700">
<h2 className="text-3xl font-bold bg-gradient-to-r from-blue-400 to-cyan-400 bg-clip-text text-transparent">
{logoText}
</h2>
<p className="text-slate-400 mt-2">
Building the future of technology together
</p>
</div>
return (
<footer
className={cls(
"w-full py-15 mt-20 flex justify-center relative z-1 overflow-hidden primary-button text-primary-cta-text rounded-t-theme-capped",
className
)}
role="contentinfo"
aria-label={ariaLabel}
>
<div
className={cls(
"w-content-width mx-auto flex flex-col relative z-10",
"gap-10 md:gap-20",
containerClassName
)}
>
<div className={cls("relative z-1 w-full", logoClassName)}>
<FillWidthText>
{logoText}
</FillWidthText>
</div>
{/* Links Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 mb-12">
{columns.map((column, columnIndex) => (
<div key={columnIndex} className="space-y-4">
{column.items.map((item, itemIndex) => (
<Link
key={itemIndex}
href={item.href}
className="block text-slate-300 hover:text-blue-400 transition-colors duration-200 font-medium"
>
{item.label}
</Link>
))}
</div>
))}
</div>
<div
className={cls(
"w-full mb-10",
useFlex
? cls(
"flex flex-col md:flex-row gap-8 md:gap-[var(--width-10)]",
columnCount === 1 ? "md:justify-center" : "md:justify-between"
)
: cls("grid gap-[var(--width-10)] md:gap-[calc(var(--width-10)/2)]", gridColsClass),
columnsClassName
)}
>
{columns.map((column, index) => (
<div
key={`column-${index}`}
className={cls("flex items-start flex-col gap-4", columnClassName)}
>
{column.items.map((item) => (
<div
key={`${item.label}-${index}`}
className={cls("flex items-center gap-2 text-base", itemClassName)}
>
<ChevronRight
className={cls("h-[1em] w-auto", iconClassName)}
strokeWidth={3}
aria-hidden="true"
/>
<ButtonTextUnderline
text={item.label}
href={item.href}
onClick={item.onClick}
className={cls("font-medium text-base", buttonClassName)}
/>
</div>
))}
</div>
))}
{/* Bottom Section */}
<div className="border-t border-slate-700 pt-8 flex flex-col md:flex-row justify-between items-center">
<p className="text-slate-400 text-sm">
© 2024 {logoText}. All rights reserved.
</p>
<div className="flex gap-6 mt-4 md:mt-0">
<Link
href="#"
className="text-slate-400 hover:text-blue-400 transition-colors"
>
Privacy
</Link>
<Link
href="#"
className="text-slate-400 hover:text-blue-400 transition-colors"
>
Terms
</Link>
</div>
</div>
</footer>
</div>
</footer>
);
};
FooterLogoEmphasis.displayName = "FooterLogoEmphasis";
export default FooterLogoEmphasis;
}

View File

@@ -1,168 +1,184 @@
"use client";
import { useRef } from "react";
import { useScroll, useTransform, motion } from "motion/react";
import TextBox from "@/components/Textbox";
import MediaContent from "@/components/shared/MediaContent";
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, ButtonAnimationType } from "@/types/button";
import React, { ReactNode } from "react";
import Link from "next/link";
import { LucideIcon } from "lucide-react";
type HeroBillboardScrollBackgroundProps = Extract<
HeroBackgroundVariantProps,
| { variant: "plain" }
| { variant: "animated-grid" }
| { variant: "canvas-reveal" }
| { variant: "cell-wave" }
| { variant: "downward-rays-animated" }
| { variant: "downward-rays-animated-grid" }
| { variant: "downward-rays-static" }
| { variant: "downward-rays-static-grid" }
| { variant: "gradient-bars" }
| { variant: "radial-gradient" }
| { variant: "rotated-rays-animated" }
| { variant: "rotated-rays-animated-grid" }
| { variant: "rotated-rays-static" }
| { variant: "rotated-rays-static-grid" }
| { variant: "sparkles-gradient" }
>;
interface Button {
text: string;
href: string;
}
interface Background {
variant: "animated-grid" | "solid" | "gradient";
}
interface HeroBillboardScrollProps {
title: string;
description: string;
background: HeroBillboardScrollBackgroundProps;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
cardWrapperClassName?: string;
cardInnerClassName?: string;
imageClassName?: string;
tag: string;
tagIcon: LucideIcon;
tagAnimation?: string;
background?: Background;
imageSrc: string;
imageAlt: string;
buttons: Button[];
buttonAnimation?: string;
}
const HeroBillboardScroll = ({
export default function HeroBillboardScroll({
title,
description,
background,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
tagIcon: TagIcon,
tagAnimation = "slide-up",
background = { variant: "animated-grid" },
imageSrc,
videoSrc,
imageAlt = "",
videoAriaLabel = "Hero video",
ariaLabel = "Hero section",
className = "",
containerClassName = "",
textBoxClassName = "",
titleClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
cardWrapperClassName = "",
cardInnerClassName = "",
imageClassName = "",
}: HeroBillboardScrollProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: containerRef,
});
imageAlt,
buttons,
buttonAnimation = "slide-up",
}: HeroBillboardScrollProps) {
const getBackgroundClass = () => {
switch (background.variant) {
case "animated-grid":
return "bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900";
case "solid":
return "bg-slate-900";
case "gradient":
return "bg-gradient-to-r from-blue-600 to-purple-600";
default:
return "bg-slate-900";
}
};
const rotate = useTransform(scrollYProgress, [0, 1], [20, 0]);
const scale = useTransform(scrollYProgress, [0, 1], [1.05, 1]);
const getAnimationDelay = (index: number) => {
return `${index * 100}ms`;
};
return (
<section
aria-label={ariaLabel}
ref={containerRef}
className={cls("relative h-fit flex items-center justify-center", className)}
>
<HeroBackgrounds {...background} />
<div
className={cls("py-hero-page-padding w-full relative z-10", containerClassName)}
style={{
perspective: "1000px",
}}
>
<div className="w-content-width mx-auto">
<TextBox
title={title}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
className={cls("flex flex-col gap-3 md:gap-3", textBoxClassName)}
titleClassName={cls("text-6xl font-medium text-balance", titleClassName)}
descriptionClassName={cls("text-base md:text-lg leading-tight", descriptionClassName)}
tagClassName={cls("px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-1", tagClassName)}
buttonContainerClassName={cls("flex flex-wrap gap-4 max-md:justify-center mt-2", buttonContainerClassName)}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
center={true}
/>
<section className={`relative min-h-screen w-full overflow-hidden ${getBackgroundClass()}`}>
{/* Animated Grid Background */}
{background.variant === "animated-grid" && (
<div className="absolute inset-0 opacity-10">
<div className="absolute inset-0 bg-[linear-gradient(90deg,rgba(255,255,255,.05)_1px,transparent_1px),linear-gradient(rgba(255,255,255,.05)_1px,transparent_1px)] bg-[size:40px_40px]" />
</div>
)}
{/* Gradient Orbs */}
<div className="absolute top-0 left-1/4 w-96 h-96 bg-blue-500 rounded-full mix-blend-multiply filter blur-3xl opacity-20 animate-pulse" />
<div className="absolute bottom-0 right-1/4 w-96 h-96 bg-purple-500 rounded-full mix-blend-multiply filter blur-3xl opacity-20 animate-pulse" />
<div className="relative z-10 flex items-center justify-between min-h-screen px-6 md:px-12 lg:px-20">
{/* Left Content */}
<div className="flex-1 max-w-2xl">
{/* Tag */}
<div
className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-blue-500/10 border border-blue-500/30 mb-6 animate-fade-in"
style={{
animation: `fadeInUp 0.6s ease-out ${getAnimationDelay(0)}`,
}}
>
<TagIcon className="w-4 h-4 text-blue-400" />
<span className="text-sm font-medium text-blue-300">{tag}</span>
</div>
{/* Title */}
<h1
className="text-5xl md:text-6xl lg:text-7xl font-bold text-white mb-6 leading-tight animate-fade-in"
style={{
animation: `fadeInUp 0.6s ease-out ${getAnimationDelay(1)}`,
}}
>
{title}
</h1>
{/* Description */}
<p
className="text-lg md:text-xl text-gray-300 mb-8 leading-relaxed max-w-xl animate-fade-in"
style={{
animation: `fadeInUp 0.6s ease-out ${getAnimationDelay(2)}`,
}}
>
{description}
</p>
{/* Buttons */}
<div
className="flex flex-wrap gap-4 animate-fade-in"
style={{
animation: `fadeInUp 0.6s ease-out ${getAnimationDelay(3)}`,
}}
>
{buttons.map((button, index) => (
<Link
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 hover:shadow-blue-500/50"
: "bg-transparent text-white border-2 border-white hover:bg-white hover:text-slate-900"
}`}
>
{button.text}
</Link>
))}
</div>
</div>
{/* Right Image */}
<div
className={cls("relative w-content-width h-[50svh] mt-8 mx-auto md:hidden", cardWrapperClassName)}
className="hidden lg:flex flex-1 justify-end animate-fade-in"
style={{
transform: "rotateX(20deg)",
animation: `fadeInUp 0.6s ease-out ${getAnimationDelay(4)}`,
}}
>
<div className={cls("h-full w-full overflow-hidden rounded-theme-capped card p-4", cardInnerClassName)}>
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("z-1 h-full w-full object-cover object-left-top aspect-square md:aspect-video", imageClassName)}
<div className="relative w-full max-w-md h-96 rounded-2xl overflow-hidden shadow-2xl">
<img
src={imageSrc}
alt={imageAlt}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-slate-900/40 to-transparent" />
</div>
</div>
<motion.div
style={{
rotateX: rotate,
scale,
}}
className={cls("hidden md:block relative w-content-width mt-8 h-[75svh] mx-auto", cardWrapperClassName)}
>
<div className={cls("h-full w-full overflow-hidden rounded-theme-capped card p-4", cardInnerClassName)}>
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("z-1 h-full w-full object-cover object-left-top", imageClassName)}
/>
</div>
</motion.div>
</div>
{/* Scroll Indicator */}
<div className="absolute bottom-8 left-1/2 transform -translate-x-1/2 z-10">
<div className="flex flex-col items-center gap-2 animate-bounce">
<span className="text-sm text-gray-400">Scroll to explore</span>
<svg
className="w-6 h-6 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 14l-7 7m0 0l-7-7m7 7V3"
/>
</svg>
</div>
</div>
<style jsx>{`
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in {
opacity: 0;
}
`}</style>
</section>
);
};
HeroBillboardScroll.displayName = "HeroBillboardScroll";
export default HeroBillboardScroll;
}

View File

@@ -1,238 +1,272 @@
"use client";
import { memo, useCallback } from "react";
import { useRouter } from "next/navigation";
import CardStack from "@/components/cardStack/CardStack";
import ProductImage from "@/components/shared/ProductImage";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import { useProducts } from "@/hooks/useProducts";
import type { Product } from "@/lib/api/product";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, GridVariant, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
import React, { useState } from "react";
import Image from "next/image";
import { LucideIcon } from "lucide-react";
type ProductCardFourGridVariant = Exclude<GridVariant, "timeline" | "items-top-row-full-width-bottom" | "full-width-top-items-bottom-row">;
type ProductCard = Product & {
interface Product {
id: string;
name: string;
price: string;
variant: string;
};
imageSrc: string;
imageAlt: string;
}
interface ProductCardFourProps {
products?: ProductCard[];
carouselMode?: "auto" | "buttons";
gridVariant: ProductCardFourGridVariant;
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: ButtonAnimationType;
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
imageClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
cardNameClassName?: string;
cardPriceClassName?: string;
cardVariantClassName?: string;
actionButtonClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
tag: string;
tagIcon: LucideIcon;
textboxLayout: "default" | "overlay" | "below";
gridVariant: "uniform-all-items-equal" | "varied" | "masonry";
animationType: "slide-up" | "fade-in" | "scale";
useInvertedBackground: boolean;
products: Product[];
}
interface ProductCardItemProps {
product: ProductCard;
shouldUseLightText: boolean;
cardClassName?: string;
imageClassName?: string;
cardNameClassName?: string;
cardPriceClassName?: string;
cardVariantClassName?: string;
actionButtonClassName?: string;
}
const ProductCardItem = memo(({
product,
shouldUseLightText,
cardClassName = "",
imageClassName = "",
cardNameClassName = "",
cardPriceClassName = "",
cardVariantClassName = "",
actionButtonClassName = "",
}: ProductCardItemProps) => {
return (
<article
className={cls("card group relative h-full flex flex-col gap-4 cursor-pointer p-4 rounded-theme-capped", cardClassName)}
onClick={product.onProductClick}
role="article"
aria-label={`${product.name} - ${product.price}`}
>
<ProductImage
imageSrc={product.imageSrc}
imageAlt={product.imageAlt || product.name}
isFavorited={product.isFavorited}
onFavoriteToggle={product.onFavorite}
showActionButton={true}
actionButtonAriaLabel={`View ${product.name} details`}
imageClassName={imageClassName}
actionButtonClassName={actionButtonClassName}
/>
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between gap-4">
<div className="flex flex-col gap-0 flex-1 min-w-0">
<h3 className={cls("text-base font-medium leading-[1.3]", shouldUseLightText ? "text-background" : "text-foreground", cardNameClassName)}>
{product.name}
</h3>
<p className={cls("text-sm leading-[1.3]", shouldUseLightText ? "text-background/60" : "text-foreground/60", cardVariantClassName)}>
{product.variant}
</p>
</div>
<p className={cls("text-base font-medium leading-[1.3] flex-shrink-0", shouldUseLightText ? "text-background" : "text-foreground", cardPriceClassName)}>
{product.price}
</p>
</div>
</div>
</article>
);
});
ProductCardItem.displayName = "ProductCardItem";
const ProductCardFour = ({
products: productsProp,
carouselMode = "buttons",
gridVariant,
uniformGridCustomHeightClasses = "min-h-95 2xl:min-h-105",
animationType,
export default function ProductCardFour({
title,
titleSegments,
description,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
tagIcon: TagIcon,
textboxLayout,
gridVariant,
animationType,
useInvertedBackground,
ariaLabel = "Product section",
className = "",
containerClassName = "",
cardClassName = "",
imageClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
cardNameClassName = "",
cardPriceClassName = "",
cardVariantClassName = "",
actionButtonClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: ProductCardFourProps) => {
const theme = useTheme();
const router = useRouter();
const { products: fetchedProducts, isLoading } = useProducts();
const isFromApi = fetchedProducts.length > 0;
const products = (isFromApi ? fetchedProducts : productsProp) as ProductCard[];
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
products,
}: ProductCardFourProps) {
const [flipped, setFlipped] = useState<{ [key: string]: boolean }>({});
const handleProductClick = useCallback((product: ProductCard) => {
if (isFromApi) {
router.push(`/shop/${product.id}`);
} else {
product.onProductClick?.();
const toggleFlip = (id: string) => {
setFlipped((prev) => ({
...prev,
[id]: !prev[id],
}));
};
const getGridClass = () => {
switch (gridVariant) {
case "uniform-all-items-equal":
return "grid-cols-1 md:grid-cols-2 lg:grid-cols-3";
case "varied":
return "grid-cols-1 md:grid-cols-2 lg:grid-cols-4";
case "masonry":
return "grid-cols-1 md:grid-cols-2 lg:grid-cols-3";
default:
return "grid-cols-1 md:grid-cols-2 lg:grid-cols-3";
}
}, [isFromApi, router]);
};
const getAnimationClass = () => {
switch (animationType) {
case "slide-up":
return "animate-slide-up";
case "fade-in":
return "animate-fade-in";
case "scale":
return "animate-scale";
default:
return "animate-fade-in";
}
};
if (isLoading && !productsProp) {
return (
<div className="w-content-width mx-auto py-20 text-center">
<p className="text-foreground">Loading products...</p>
</div>
);
}
if (!products || products.length === 0) {
return null;
}
const bgClass = useInvertedBackground ? "bg-gray-900" : "bg-white";
return (
<CardStack
mode={carouselMode}
gridVariant={gridVariant}
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
<section className={`py-16 px-4 sm:px-6 lg:px-8 ${bgClass}`}>
<div className="max-w-7xl mx-auto">
<div className="mb-12">
<div className="flex items-center gap-2 mb-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>
<h2
className={`text-4xl sm:text-5xl font-bold mb-4 ${
useInvertedBackground ? "text-white" : "text-gray-900"
}`}
>
{title}
</h2>
<p
className={`text-lg max-w-2xl ${
useInvertedBackground ? "text-gray-300" : "text-gray-600"
}`}
>
{description}
</p>
</div>
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{products?.map((product, index) => (
<ProductCardItem
key={`${product.id}-${index}`}
product={{ ...product, onProductClick: () => handleProductClick(product) }}
shouldUseLightText={shouldUseLightText}
cardClassName={cardClassName}
imageClassName={imageClassName}
cardNameClassName={cardNameClassName}
cardPriceClassName={cardPriceClassName}
cardVariantClassName={cardVariantClassName}
actionButtonClassName={actionButtonClassName}
/>
))}
</CardStack>
<div className={`grid ${getGridClass()} gap-8`}>
{products.map((product) => (
<div
key={product.id}
className={`h-96 cursor-pointer perspective ${getAnimationClass()}`}
onClick={() => toggleFlip(product.id)}
>
<div
className="relative w-full h-full transition-transform duration-500 transform-gpu"
style={{
transformStyle: "preserve-3d",
transform: flipped[product.id]
? "rotateY(180deg)"
: "rotateY(0deg)",
}}
>
{/* Front of card */}
<div
className="absolute w-full h-full bg-white rounded-lg shadow-lg overflow-hidden"
style={{ backfaceVisibility: "hidden" }}
>
<div className="relative w-full h-64">
<Image
src={product.imageSrc}
alt={product.imageAlt}
fill
className="object-cover"
/>
</div>
<div className="p-6">
<h3 className="text-xl font-bold text-gray-900 mb-2">
{product.name}
</h3>
<p className="text-sm text-gray-600 mb-3">
{product.variant}
</p>
<div className="flex items-center justify-between">
<span className="text-lg font-semibold text-blue-600">
{product.price}
</span>
<span className="text-xs text-gray-500">Click to flip</span>
</div>
</div>
</div>
{/* Back of card */}
<div
className="absolute w-full h-full bg-blue-600 rounded-lg shadow-lg overflow-hidden flex flex-col items-center justify-center p-6"
style={{
backfaceVisibility: "hidden",
transform: "rotateY(180deg)",
}}
>
<h3 className="text-white text-xl font-bold mb-6 text-center">
Connect With Us
</h3>
<div className="flex gap-4 flex-wrap justify-center">
<a
href="#"
className="w-12 h-12 bg-white bg-opacity-20 hover:bg-opacity-30 rounded-full flex items-center justify-center text-white transition-all duration-300"
title="LinkedIn"
>
<svg
className="w-6 h-6"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z" />
</svg>
</a>
<a
href="#"
className="w-12 h-12 bg-white bg-opacity-20 hover:bg-opacity-30 rounded-full flex items-center justify-center text-white transition-all duration-300"
title="Twitter"
>
<svg
className="w-6 h-6"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M23 3a10.9 10.9 0 01-3.14 1.53 4.48 4.48 0 00-7.86 3v1A10.66 10.66 0 013 4s-4 9 5 13a11.64 11.64 0 01-7 2s9 5 20 5a9.5 9.5 0 00-9-5.5c4.75 2.25 9 0 11-4s1-8.75 0-10c-.5-.75-1-2-3-4z" />
</svg>
</a>
<a
href="#"
className="w-12 h-12 bg-white bg-opacity-20 hover:bg-opacity-30 rounded-full flex items-center justify-center text-white transition-all duration-300"
title="Facebook"
>
<svg
className="w-6 h-6"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z" />
</svg>
</a>
<a
href="#"
className="w-12 h-12 bg-white bg-opacity-20 hover:bg-opacity-30 rounded-full flex items-center justify-center text-white transition-all duration-300"
title="Email"
>
<svg
className="w-6 h-6"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z" />
</svg>
</a>
</div>
<p className="text-white text-sm mt-6 text-center">
Click to flip back
</p>
</div>
</div>
</div>
))}
</div>
</div>
<style jsx>{`
@keyframes slide-up {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes scale {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
.animate-slide-up {
animation: slide-up 0.6s ease-out;
}
.animate-fade-in {
animation: fade-in 0.6s ease-out;
}
.animate-scale {
animation: scale 0.6s ease-out;
}
.perspective {
perspective: 1000px;
}
`}</style>
</section>
);
};
ProductCardFour.displayName = "ProductCardFour";
export default ProductCardFour;
}

View File

@@ -0,0 +1,194 @@
"use client";
import React, { useState } from "react";
import { CardStackTextBox } from "@/components/CardStackTextBox";
import { LucideIcon } from "lucide-react";
interface SocialLink {
icon: LucideIcon;
url: string;
}
interface TeamMember {
id: string;
name: string;
role: string;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
socialLinks?: SocialLink[];
}
interface TeamCardFiveFlipProps {
team: TeamMember[];
animationType: "none" | "opacity" | "slide-up" | "scale-rotate" | "blur-reveal";
title: string;
titleSegments?: Array<{ type: "text"; content: string } | { type: "image"; src: string; alt?: string }>;
description: string;
textboxLayout: "default" | "split" | "split-actions" | "split-description" | "inline-image";
useInvertedBackground: boolean;
tag?: string;
tagIcon?: LucideIcon;
tagAnimation?: "none" | "opacity" | "slide-up" | "blur-reveal";
buttons?: Array<{ text: string; onClick?: () => void; href?: string }>;
buttonAnimation?: "none" | "opacity" | "slide-up" | "blur-reveal";
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
gridClassName?: string;
cardClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
nameClassName?: string;
roleClassName?: string;
}
const TeamCardFiveFlip: React.FC<TeamCardFiveFlipProps> = ({
team,
animationType,
title,
titleSegments,
description,
textboxLayout,
useInvertedBackground,
tag,
tagIcon,
tagAnimation,
buttons,
buttonAnimation,
ariaLabel = "Team section", className = "", containerClassName = "", textBoxClassName = "", textBoxTitleClassName = "", textBoxTitleImageWrapperClassName = "", textBoxTitleImageClassName = "", textBoxDescriptionClassName = "", textBoxTagClassName = "", textBoxButtonContainerClassName = "", textBoxButtonClassName = "", textBoxButtonTextClassName = "", gridClassName = "", cardClassName = "", mediaWrapperClassName = "", mediaClassName = "", nameClassName = "", roleClassName = ""}) => {
const [flipped, setFlipped] = useState<{ [key: string]: boolean }>({});
const toggleFlip = (id: string) => {
setFlipped((prev) => ({
...prev,
[id]: !prev[id],
}));
};
return (
<div className={className}>
<div className={`w-full relative px-4 sm:px-6 md:px-8 lg:px-0 ${containerClassName}`}>
<CardStackTextBox
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
className={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
/>
<div
className={`grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mt-12 ${gridClassName}`}
role="grid"
aria-label={ariaLabel}
>
{team.map((member) => (
<div
key={member.id}
className={`h-96 cursor-pointer perspective ${cardClassName}`}
onClick={() => toggleFlip(member.id)}
role="gridcell"
>
<div
className="relative w-full h-full transition-transform duration-500 transform-gpu"
style={{
transformStyle: "preserve-3d" as any,
transform: flipped[member.id] ? "rotateY(180deg)" : "rotateY(0deg)"}}
>
{/* Front of card - Photo and name */}
<div
className="absolute w-full h-full bg-gradient-to-br from-slate-900 to-slate-800 rounded-lg overflow-hidden shadow-lg"
style={{
backfaceVisibility: "hidden" as any,
}}
>
<div className="relative w-full h-full">
{member.imageSrc && (
<img
src={member.imageSrc}
alt={member.imageAlt || member.name}
className={`w-full h-full object-cover ${mediaClassName}`}
/>
)}
{member.videoSrc && !member.imageSrc && (
<video
src={member.videoSrc}
className={`w-full h-full object-cover ${mediaClassName}`}
aria-label={member.videoAriaLabel || member.name}
/>
)}
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent" />
<div className="absolute bottom-0 left-0 right-0 p-6 text-white">
<h3 className={`text-xl font-bold mb-1 ${nameClassName}`}>{member.name}</h3>
<p className={`text-sm opacity-90 ${roleClassName}`}>{member.role}</p>
</div>
</div>
</div>
{/* Back of card - Social links */}
<div
className="absolute w-full h-full bg-gradient-to-br from-blue-600 to-blue-700 rounded-lg overflow-hidden shadow-lg flex items-center justify-center"
style={{
backfaceVisibility: "hidden" as any,
transform: "rotateY(180deg)"}}
>
<div className="flex flex-col items-center justify-center gap-6">
<p className="text-white text-center font-semibold px-4">{member.name}</p>
{member.socialLinks && member.socialLinks.length > 0 ? (
<div className="flex gap-4 justify-center">
{member.socialLinks.map((social, idx) => {
const Icon = social.icon;
return (
<a
key={idx}
href={social.url}
target="_blank"
rel="noopener noreferrer"
className="text-white hover:text-yellow-300 transition-colors duration-300 p-2 hover:bg-white/20 rounded-lg"
aria-label={`Visit ${member.name}'s profile`}
>
<Icon size={24} />
</a>
);
})}
</div>
) : (
<p className="text-white/70 text-sm">Social links coming soon</p>
)}
<p className="text-white/60 text-xs mt-2">Click to flip back</p>
</div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
);
};
export default TeamCardFiveFlip;

View File

@@ -1,106 +1,148 @@
"use client";
'use client';
import { useState, useEffect } from "react";
import AvatarGroup from "@/components/shared/AvatarGroup";
import Tag from "@/components/shared/Tag";
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 "@/components/cardStack/types";
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
import { useState } from 'react';
import Image from 'next/image';
import { LucideIcon } from 'lucide-react';
type Testimonial = {
interface Testimonial {
id: string;
name: string;
imageSrc: string;
imageAlt?: string;
};
imageAlt: string;
}
interface TestimonialCardTwelveProps {
testimonials: Testimonial[];
cardTitle: string;
cardTag: string;
cardTagIcon?: LucideIcon;
cardAnimation: ButtonAnimationType;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
avatarGroupClassName?: string;
avatarClassName?: string;
cardTitleClassName?: string;
cardTagClassName?: string;
cardAnimation?: string;
useInvertedBackground?: boolean;
}
const TestimonialCardTwelve = ({
export default function TestimonialCardTwelve({
testimonials,
cardTitle,
cardTag,
cardTagIcon,
cardAnimation,
useInvertedBackground,
ariaLabel = "Testimonials section",
className = "",
containerClassName = "",
cardClassName = "",
avatarGroupClassName = "",
avatarClassName = "",
cardTitleClassName = "",
cardTagClassName = "",
}: TestimonialCardTwelveProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const { containerRef: cardContainerRef } = useButtonAnimation({ animationType: cardAnimation });
cardTagIcon: TagIcon,
cardAnimation = 'slide-up',
useInvertedBackground = false,
}: TestimonialCardTwelveProps) {
const [flipped, setFlipped] = useState<{ [key: string]: boolean }>({});
const [isMobile, setIsMobile] = useState(false);
const toggleFlip = (id: string) => {
setFlipped((prev) => ({
...prev,
[id]: !prev[id],
}));
};
useEffect(() => {
const checkMobile = () => setIsMobile(window.innerWidth < 768);
checkMobile();
window.addEventListener("resize", checkMobile);
return () => window.removeEventListener("resize", checkMobile);
}, []);
const avatars = testimonials.map((testimonial) => ({
src: testimonial.imageSrc,
alt: testimonial.imageAlt || testimonial.name,
}));
const socialLinks = [
{ name: 'LinkedIn', icon: '🔗', url: '#' },
{ name: 'Twitter', icon: '𝕏', url: '#' },
{ name: 'GitHub', icon: '⚙️', url: '#' },
];
return (
<section
aria-label={ariaLabel}
className={cls("relative py-20 w-full", useInvertedBackground && "bg-foreground", className)}
>
<div ref={cardContainerRef} className={cls("w-content-width mx-auto", containerClassName)}>
<div className={cls("w-full card rounded-theme-capped p-8 flex flex-col items-center justify-between gap-8", cardClassName)}>
<div className="flex flex-col gap-3 items-center">
<Tag
text={cardTag}
icon={cardTagIcon}
useInvertedBackground={useInvertedBackground}
className={cardTagClassName}
/>
<h3 className={cls("relative md:max-w-7/10 text-3xl md:text-5xl font-medium leading-tight text-center text-balance", shouldUseLightText ? "text-background" : "text-foreground", cardTitleClassName)}>
{cardTitle}
</h3>
<section className={`py-16 px-4 ${useInvertedBackground ? 'bg-gray-900' : 'bg-white'}`}>
<div className="max-w-6xl mx-auto">
<div className="text-center mb-12">
<div className="flex items-center justify-center gap-2 mb-4">
{TagIcon && <TagIcon className="w-5 h-5 text-blue-600" />}
<span className="text-sm font-semibold text-blue-600 uppercase tracking-wide">
{cardTag}
</span>
</div>
<AvatarGroup
avatars={avatars}
className={avatarGroupClassName}
avatarClassName={avatarClassName}
maxVisible={isMobile ? 3 : 4}
ariaLabel="Customer testimonials"
avatarImageClassName="h-[var(--width-17_5)] md:h-[var(--width-12_5)]"
avatarOverlapClassName="-ml-8"
/>
<h2 className={`text-3xl md:text-4xl font-bold ${useInvertedBackground ? 'text-white' : 'text-gray-900'}`}>
{cardTitle}
</h2>
</div>
<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 ${cardAnimation === 'slide-up' ? 'animate-slide-up' : ''}`}
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 overflow-hidden"
style={{
backfaceVisibility: 'hidden',
}}
>
<div className="relative w-full h-full">
<Image
src={testimonial.imageSrc}
alt={testimonial.imageAlt}
fill
className="object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent flex items-end p-4">
<h3 className="text-white text-lg font-semibold">{testimonial.name}</h3>
</div>
</div>
</div>
{/* Back of card */}
<div
className="absolute w-full h-full bg-blue-600 rounded-lg shadow-lg p-6 flex flex-col items-center justify-center"
style={{
backfaceVisibility: 'hidden',
transform: 'rotateY(180deg)',
}}
>
<h3 className="text-white text-lg font-semibold mb-6 text-center">
{testimonial.name}
</h3>
<div className="flex flex-col gap-4 w-full">
{socialLinks.map((link) => (
<a
key={link.name}
href={link.url}
className="flex items-center justify-center gap-2 bg-white/20 hover:bg-white/30 text-white py-2 px-4 rounded-lg transition-colors duration-200 text-sm font-medium"
onClick={(e) => e.stopPropagation()}
>
<span>{link.icon}</span>
{link.name}
</a>
))}
</div>
</div>
</div>
</div>
))}
</div>
</div>
<style jsx>{`
@keyframes slide-up {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-slide-up {
animation: slide-up 0.6s ease-out forwards;
}
.perspective {
perspective: 1000px;
}
`}</style>
</section>
);
};
TestimonialCardTwelve.displayName = "TestimonialCardTwelve";
export default TestimonialCardTwelve;
}

View File

@@ -1,140 +1,190 @@
// "use client";
// import { memo } from "react";
// import { useTagEffects } from "./useTagEffects";
// const Tag = () => {
// const { shouldShow, handleMouseEnter, handleClick, buttonClassName } = useTagEffects();
// const handleTagClick = () => {
// window.open('https://webild.io', '_blank');
// };
// if (!shouldShow) {
// return null;
// }
// 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>
// );
// };
// Tag.displayName = "Tag";
// export default memo(Tag);
"use client";
import { memo } from "react";
import { useRive, useStateMachineInput, Layout, Fit } from "@rive-app/react-canvas";
import { useTagEffects } from "./useTagEffects";
import { useState } from "react";
import { Mail, Linkedin, Twitter, Github } from "lucide-react";
const STATE_MACHINE_NAME = "State Machine 1";
const HOVER_INPUT_NAME = "Hover";
interface TeamMember {
id: string;
name: string;
title: string;
image: string;
email?: string;
linkedin?: string;
twitter?: string;
github?: string;
}
const Tag = () => {
const { shouldShow, handleMouseEnter, handleClick } = useTagEffects();
interface TagProps {
members?: TeamMember[];
}
const { rive, RiveComponent } = useRive({
src: "/watermark-bob2.riv",
stateMachines: STATE_MACHINE_NAME,
autoplay: true,
layout: new Layout({
fit: Fit.Contain,
}),
});
const defaultMembers: TeamMember[] = [
{
id: "1",
name: "Sarah Johnson",
title: "Chief Executive Officer",
image: "https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=400&h=400&fit=crop",
email: "sarah@company.com",
linkedin: "https://linkedin.com",
twitter: "https://twitter.com",
},
{
id: "2",
name: "Michael Chen",
title: "Chief Technology Officer",
image: "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&h=400&fit=crop",
email: "michael@company.com",
linkedin: "https://linkedin.com",
github: "https://github.com",
},
{
id: "3",
name: "Emily Rodriguez",
title: "Head of Design",
image: "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=400&h=400&fit=crop",
email: "emily@company.com",
linkedin: "https://linkedin.com",
twitter: "https://twitter.com",
},
{
id: "4",
name: "David Park",
title: "VP of Operations",
image: "https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=400&h=400&fit=crop",
email: "david@company.com",
linkedin: "https://linkedin.com",
github: "https://github.com",
},
];
const hoverInput = useStateMachineInput(rive, STATE_MACHINE_NAME, HOVER_INPUT_NAME);
export default function Tag({ members = defaultMembers }: TagProps) {
const [flipped, setFlipped] = useState<{ [key: string]: boolean }>({});
const handleTagClick = () => {
window.open('https://webild.io', '_blank');
};
const toggleFlip = (id: string) => {
setFlipped((prev) => ({
...prev,
[id]: !prev[id],
}));
};
const onMouseEnter = () => {
handleMouseEnter();
if (hoverInput) {
hoverInput.value = true;
}
};
return (
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-gradient-to-b from-white to-gray-50">
<div className="max-w-7xl mx-auto">
<div className="text-center mb-12">
<h2 className="text-4xl font-bold text-gray-900 mb-4">
Meet Our Leadership
</h2>
<p className="text-lg text-gray-600">
Exceptional leaders driving innovation and excellence
</p>
</div>
const onMouseLeave = () => {
if (hoverInput) {
hoverInput.value = false;
}
};
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{members.map((member) => (
<div
key={member.id}
className="h-80 cursor-pointer perspective"
onClick={() => toggleFlip(member.id)}
>
<div
className="relative w-full h-full transition-transform duration-500 transform-gpu"
style={{
transformStyle: "preserve-3d",
transform: flipped[member.id]
? "rotateY(180deg)"
: "rotateY(0deg)",
}}
>
{/* Front of card */}
<div
className="absolute w-full h-full bg-white rounded-lg shadow-lg overflow-hidden"
style={{ backfaceVisibility: "hidden" }}
>
<img
src={member.image}
alt={member.name}
className="w-full h-64 object-cover"
/>
<div className="p-6 text-center">
<h3 className="text-xl font-bold text-gray-900 mb-2">
{member.name}
</h3>
<p className="text-sm text-gray-600">{member.title}</p>
<p className="text-xs text-gray-400 mt-3">
Hover to see social links
</p>
</div>
</div>
if (!shouldShow) {
return null;
}
{/* 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 items-center"
style={{
backfaceVisibility: "hidden",
transform: "rotateY(180deg)",
}}
>
<h3 className="text-white text-lg font-bold mb-6 text-center">
{member.name}
</h3>
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>
);
};
<div className="flex flex-col gap-4 w-full">
{member.email && (
<a
href={`mailto:${member.email}`}
className="flex items-center justify-center gap-3 bg-white bg-opacity-20 hover:bg-opacity-30 text-white py-2 px-4 rounded-lg transition-all duration-200"
onClick={(e) => e.stopPropagation()}
>
<Mail size={18} />
<span className="text-sm">Email</span>
</a>
)}
Tag.displayName = "Tag";
{member.linkedin && (
<a
href={member.linkedin}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center gap-3 bg-white bg-opacity-20 hover:bg-opacity-30 text-white py-2 px-4 rounded-lg transition-all duration-200"
onClick={(e) => e.stopPropagation()}
>
<Linkedin size={18} />
<span className="text-sm">LinkedIn</span>
</a>
)}
export default memo(Tag);
{member.twitter && (
<a
href={member.twitter}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center gap-3 bg-white bg-opacity-20 hover:bg-opacity-30 text-white py-2 px-4 rounded-lg transition-all duration-200"
onClick={(e) => e.stopPropagation()}
>
<Twitter size={18} />
<span className="text-sm">Twitter</span>
</a>
)}
{member.github && (
<a
href={member.github}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center gap-3 bg-white bg-opacity-20 hover:bg-opacity-30 text-white py-2 px-4 rounded-lg transition-all duration-200"
onClick={(e) => e.stopPropagation()}
>
<Github size={18} />
<span className="text-sm">GitHub</span>
</a>
)}
</div>
</div>
</div>
</div>
))}
</div>
</div>
</section>
);
}