Merge version_2 into main #2
@@ -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: {
|
||||
|
||||
@@ -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" }
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
163
src/components/CardStackTextBox.tsx
Normal file
163
src/components/CardStackTextBox.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
194
src/components/sections/team/TeamCardFiveFlip.tsx
Normal file
194
src/components/sections/team/TeamCardFiveFlip.tsx
Normal 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;
|
||||
@@ -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;
|
||||
}
|
||||
304
src/tag/Tag.tsx
304
src/tag/Tag.tsx
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user