Merge version_2_1780258550475 into main

Merge version_2_1780258550475 into main
This commit was merged in pull request #2.
This commit is contained in:
2026-05-31 20:16:11 +00:00
25 changed files with 1 additions and 2351 deletions

View File

@@ -1,99 +0,0 @@
import FooterSimpleCard from '@/components/sections/footer/FooterSimpleCard';
import NavbarFloatingLogo from '@/components/ui/NavbarFloatingLogo';
import SectionErrorBoundary from "@/components/ui/SectionErrorBoundary";
import SiteBackgroundSlot from "@/components/ui/SiteBackgroundSlot";
import { Outlet } from 'react-router-dom';
import { StyleProvider } from "@/components/ui/StyleProvider";
export default function Layout() {
const navItems = [
{
"name": "Home", "href": "#home"
},
{
"name": "About", "href": "#about"
},
{
"name": "Rooms", "href": "#rooms"
},
{
"name": "Amenities", "href": "#amenities"
},
{
"name": "Reviews", "href": "#reviews"
},
{
"name": "Contact", "href": "#contact"
},
{
"name": "Metrics", "href": "#metrics"
}
];
return (
<StyleProvider buttonVariant="shift" siteBackground="floatingGradient" heroBackground="cornerGlow">
<SiteBackgroundSlot />
<SectionErrorBoundary name="navbar">
<NavbarFloatingLogo
logo="The Grand Hotel"
logoImageSrc="https://storage.googleapis.com/webild/default/no-image.jpg?id=rwf84p"
ctaButton={{
text: "Book Now", href: "#contact"}}
navItems={navItems} />
</SectionErrorBoundary>
<main className="flex-grow">
<Outlet />
</main>
<SectionErrorBoundary name="footer">
<FooterSimpleCard
brand="The Grand Hotel"
columns={[
{
title: "Explore", items: [
{
label: "Rooms & Suites", href: "#rooms"},
{
label: "Dining", href: "#"},
{
label: "Spa & Wellness", href: "#"},
{
label: "Events", href: "#"},
],
},
{
title: "About Us", items: [
{
label: "Our Story", href: "#about"},
{
label: "Guest Reviews", href: "#reviews"},
{
label: "Careers", href: "#"},
{
label: "Contact", href: "#contact"},
],
},
{
title: "Legal", items: [
{
label: "Privacy Policy", href: "#"},
{
label: "Terms of Service", href: "#"},
{
label: "Cookie Policy", href: "#"},
],
},
]}
copyright="© 2024 The Grand Hotel. All rights reserved."
links={[
{
label: "Instagram", href: "#"},
{
label: "Facebook", href: "#"},
{
label: "Twitter", href: "#"},
]}
/>
</SectionErrorBoundary>
</StyleProvider>
);
}

View File

@@ -1,126 +0,0 @@
import { useEffect } from "react";
import { X, Plus, Minus, Trash2 } from "lucide-react";
import { motion, AnimatePresence } from "motion/react";
import Button from "@/components/ui/Button";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
type CartItem = {
id: string;
name: string;
price: string;
quantity: number;
imageSrc: string;
};
type ProductCartProps = {
isOpen: boolean;
onClose: () => void;
items: CartItem[];
total: string;
onQuantityChange?: (id: string, quantity: number) => void;
onRemove?: (id: string) => void;
onCheckout?: () => void;
};
const ProductCart = ({ isOpen, onClose, items, total, onQuantityChange, onRemove, onCheckout }: ProductCartProps) => {
useEffect(() => {
if (!isOpen) return;
const onKeyDown = (e: KeyboardEvent) => e.key === "Escape" && onClose();
document.addEventListener("keydown", onKeyDown);
return () => document.removeEventListener("keydown", onKeyDown);
}, [isOpen, onClose]);
useEffect(() => {
document.body.style.overflow = isOpen ? "hidden" : "";
return () => { document.body.style.overflow = ""; };
}, [isOpen]);
return (
<AnimatePresence>
{isOpen && (
<div className="fixed inset-0 z-1001">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3, ease: "easeOut" }}
className="absolute inset-0 bg-foreground/50"
onClick={onClose}
/>
<motion.aside
initial={{ x: "100%" }}
animate={{ x: 0 }}
exit={{ x: "100%" }}
transition={{ duration: 0.3, ease: [0.25, 0.1, 0.25, 1] }}
className="card absolute right-0 top-0 flex flex-col p-5 h-screen w-screen md:w-96"
>
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold text-foreground">Cart ({items.length})</h2>
<button onClick={onClose} className="card flex items-center justify-center size-8 rounded cursor-pointer" aria-label="Close cart">
<X className="size-4 text-foreground" strokeWidth={1.5} />
</button>
</div>
<div className="mt-5 h-px w-full bg-foreground/5" />
<div className="flex-1 py-5 min-h-0 overflow-y-auto">
{items.length === 0 ? (
<p className="py-20 text-center text-sm text-foreground/50">Your cart is empty</p>
) : (
<div className="flex flex-col gap-5">
{items.map((item) => (
<div key={item.id} className="flex gap-4">
<div className="shrink-0 size-24 overflow-hidden rounded">
<ImageOrVideo imageSrc={item.imageSrc} className="size-full object-cover" />
</div>
<div className="flex flex-1 flex-col justify-between min-w-0">
<div className="flex items-start justify-between gap-2">
<h3 className="text-base font-semibold text-foreground truncate">{item.name}</h3>
<p className="shrink-0 text-base font-semibold text-foreground">{item.price}</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => item.quantity > 1 && onQuantityChange?.(item.id, item.quantity - 1)}
className="card flex items-center justify-center size-8 rounded cursor-pointer"
>
<Minus className="size-4 text-foreground" strokeWidth={1.5} />
</button>
<span className="min-w-5 text-center text-sm font-semibold text-foreground">{item.quantity}</span>
<button
onClick={() => onQuantityChange?.(item.id, item.quantity + 1)}
className="card flex items-center justify-center size-8 rounded cursor-pointer"
>
<Plus className="size-4 text-foreground" strokeWidth={1.5} />
</button>
<button
onClick={() => onRemove?.(item.id)}
className="card flex items-center justify-center ml-auto size-8 rounded cursor-pointer"
>
<Trash2 className="size-4 text-foreground" strokeWidth={1.5} />
</button>
</div>
</div>
</div>
))}
</div>
)}
</div>
<div className="flex flex-col gap-5">
<div className="h-px w-full bg-foreground/5" />
<div className="flex items-center justify-between">
<span className="text-base font-semibold text-foreground">Total</span>
<span className="text-base font-semibold text-foreground">{total}</span>
</div>
<Button text="Checkout" onClick={onCheckout} variant="primary" className="w-full" />
</div>
</motion.aside>
</div>
)}
</AnimatePresence>
);
};
export default ProductCart;
export type { CartItem };

View File

@@ -1,163 +0,0 @@
import { ArrowUpRight, Loader2 } from "lucide-react";
import ScrollReveal from "@/components/ui/ScrollReveal";
import Button from "@/components/ui/Button";
import TextAnimation from "@/components/ui/TextAnimation";
import GridOrCarousel from "@/components/ui/GridOrCarousel";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
import { useButtonClick } from "@/hooks/useButtonClick";
import useBlogPosts from "@/hooks/useBlogPosts";
type BlogItem = {
title: string;
excerpt: string;
authorName: string;
authorImageSrc: string;
date: string;
tags: string[];
imageSrc: string;
href?: string;
onClick?: () => void;
};
const BlogCardItem = ({ item }: { item: BlogItem }) => {
const handleClick = useButtonClick(item.href, item.onClick);
return (
<article
className="card group flex flex-col gap-3 xl:gap-3.5 2xl:gap-4 p-3 xl:p-3.5 2xl:p-4 rounded cursor-pointer"
onClick={handleClick}
>
<div className="relative aspect-4/3 rounded overflow-hidden">
<ImageOrVideo
imageSrc={item.imageSrc}
className="size-full object-cover transition-transform duration-500 group-hover:scale-105"
/>
<div className="absolute inset-0 flex items-center justify-center group-hover:bg-background/20 group-hover:backdrop-blur-xs transition-all duration-300">
<button
className="primary-button flex items-center justify-center size-12 rounded-full opacity-0 group-hover:opacity-100 scale-75 group-hover:scale-100 transition-all duration-300 cursor-pointer"
onClick={handleClick}
>
<ArrowUpRight className="size-5 text-primary-cta-text" strokeWidth={2} />
</button>
</div>
</div>
<div className="flex flex-1 flex-col justify-between gap-2 p-3 xl:p-3.5 2xl:p-4">
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2 min-w-0 mb-1">
<ImageOrVideo
imageSrc={item.authorImageSrc}
className="size-6 rounded-full object-cover shrink-0"
/>
<span className="text-sm text-foreground/75 truncate">
{item.authorName} {item.date}
</span>
</div>
<h3 className="text-2xl font-semibold leading-snug text-balance">{item.title}</h3>
<p className="text-base leading-snug text-balance">{item.excerpt}</p>
</div>
<div className="flex flex-wrap gap-2 mt-2 md:mt-3">
{item.tags.map((tag, index) => (
<div key={index} className="px-3 py-1 text-sm primary-button text-primary-cta-text rounded">
<p>{tag}</p>
</div>
))}
</div>
</div>
</article>
);
};
type BlogTagCardsProps = {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
items?: BlogItem[];
};
const BlogTagCards = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items: itemsProp,
}: BlogTagCardsProps) => {
const { posts, isLoading } = useBlogPosts();
const isFromApi = posts.length > 0;
const items = isFromApi
? posts.map((p) => ({
title: p.title,
excerpt: p.excerpt,
authorName: p.authorName,
authorImageSrc: p.authorAvatar,
date: p.date,
tags: [p.category],
imageSrc: p.imageSrc,
onClick: p.onBlogClick,
}))
: itemsProp;
if (isLoading && !itemsProp) {
return (
<section aria-label="Blog section" className="py-20">
<div className="w-content-width mx-auto flex justify-center">
<Loader2 className="size-8 animate-spin text-foreground" strokeWidth={1.5} />
</div>
</section>
);
}
if (!items || items.length === 0) {
return null;
}
return (
<section aria-label="Blog section" className="py-20">
<div className="w-content-width mx-auto flex flex-col gap-8 md:gap-10">
<div className="flex flex-col items-center gap-2">
<div className="px-3 py-1 mb-1 text-sm card rounded w-fit">
<p>{tag}</p>
</div>
<TextAnimation
text={title}
variant="slide-up"
gradientText={true}
tag="h2"
className="md:max-w-8/10 text-6xl 2xl:text-7xl leading-[1.15] font-semibold text-center text-balance"
/>
<TextAnimation
text={description}
variant="slide-up"
gradientText={false}
tag="p"
className="md:max-w-7/10 text-lg md:text-xl leading-snug text-center text-balance"
/>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-2 md:mt-3">
{primaryButton && <Button text={primaryButton.text} href={primaryButton.href} variant="primary"/>}
{secondaryButton && <Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary"animationDelay={0.1} />}
</div>
)}
</div>
<ScrollReveal variant="fade-blur">
<GridOrCarousel>
{items.map((item, index) => (
<BlogCardItem key={index} item={item} />
))}
</GridOrCarousel>
</ScrollReveal>
</div>
</section>
);
};
export default BlogTagCards;

View File

@@ -1,85 +0,0 @@
import { Check, X } from "lucide-react";
import Button from "@/components/ui/Button";
import TextAnimation from "@/components/ui/TextAnimation";
import ScrollReveal from "@/components/ui/ScrollReveal";
interface FeaturesComparisonProps {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
negativeItems: string[];
positiveItems: string[];
}
const FeaturesComparison = ({
tag,
title,
description,
primaryButton,
secondaryButton,
negativeItems,
positiveItems,
}: FeaturesComparisonProps) => {
return (
<section aria-label="Features comparison section" className="py-20">
<div className="flex flex-col gap-8 md:gap-10">
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
<div className="px-3 py-1 mb-1 text-sm card rounded w-fit">
<p>{tag}</p>
</div>
<TextAnimation
text={title}
variant="fade-blur"
gradientText={true}
tag="h2"
className="md:max-w-8/10 text-6xl 2xl:text-7xl leading-[1.15] font-semibold text-center text-balance"
/>
<TextAnimation
text={description}
variant="fade-blur"
gradientText={false}
tag="p"
className="md:max-w-7/10 text-lg md:text-xl leading-snug text-center text-balance"
/>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-2 md:mt-3">
{primaryButton && <Button text={primaryButton.text} href={primaryButton.href} variant="primary"/>}
{secondaryButton && <Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary"animationDelay={0.1} />}
</div>
)}
</div>
<ScrollReveal variant="slide-up" className="grid grid-cols-1 md:grid-cols-2 w-content-width md:w-6/10 mx-auto gap-5">
<div className="flex flex-col gap-4 xl:gap-5 2xl:gap-6 p-6 xl:p-7 2xl:p-8 card rounded opacity-50">
{negativeItems.map((item) => (
<div key={item} className="flex items-start gap-3">
<div className="flex items-center justify-center shrink-0 size-6 secondary-button rounded">
<X className="size-3 text-foreground" strokeWidth={2} />
</div>
<span className="text-base">{item}</span>
</div>
))}
</div>
<div className="flex flex-col gap-4 xl:gap-5 2xl:gap-6 p-6 xl:p-7 2xl:p-8 card rounded">
{positiveItems.map((item) => (
<div key={item} className="flex items-start gap-3">
<div className="flex items-center justify-center shrink-0 size-6 primary-button rounded">
<Check className="size-3 text-primary-cta-text" strokeWidth={2} />
</div>
<span className="text-base">{item}</span>
</div>
))}
</div>
</ScrollReveal>
</div>
</section>
);
};
export default FeaturesComparison;

View File

@@ -1,94 +0,0 @@
import Button from "@/components/ui/Button";
import TextAnimation from "@/components/ui/TextAnimation";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
import ScrollReveal from "@/components/ui/ScrollReveal";
type FeatureItem = {
title: string;
description: string;
tags: string[];
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
interface FeaturesDetailedCardsProps {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
items: FeatureItem[];
}
const FeaturesDetailedCards = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
}: FeaturesDetailedCardsProps) => {
return (
<section aria-label="Features section" className="py-20">
<div className="flex flex-col gap-8 md:gap-10">
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
<div className="px-3 py-1 mb-1 text-sm card rounded w-fit">
<p>{tag}</p>
</div>
<TextAnimation
text={title}
variant="fade"
gradientText={true}
tag="h2"
className="md:max-w-8/10 text-6xl 2xl:text-7xl leading-[1.15] font-semibold text-center text-balance"
/>
<TextAnimation
text={description}
variant="fade"
gradientText={false}
tag="p"
className="md:max-w-7/10 text-lg md:text-xl leading-snug text-center text-balance"
/>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-2 md:mt-3">
{primaryButton && <Button text={primaryButton.text} href={primaryButton.href} variant="primary"/>}
{secondaryButton && <Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary"animationDelay={0.1} />}
</div>
)}
</div>
<div className="flex flex-col w-content-width mx-auto gap-5">
{items.map((item) => (
<ScrollReveal
variant="fade"
key={item.title}
className="flex flex-col md:grid md:grid-cols-2 mx-auto gap-6 md:gap-20 p-6 md:p-10 card rounded group"
>
<div className="flex flex-col justify-between gap-2">
<h3 className="text-4xl md:text-5xl font-semibold leading-[1.15] text-balance">{item.title}</h3>
<div className="flex flex-col-reverse md:flex-col gap-3">
<div className="flex flex-wrap gap-3">
{item.tags.map((itemTag) => (
<div key={itemTag} className="px-3 py-1 text-sm card rounded w-fit">
<p>{itemTag}</p>
</div>
))}
</div>
<p className="text-lg md:text-xl leading-snug text-balance">{item.description}</p>
</div>
</div>
<div className="aspect-square md:aspect-5/4 rounded overflow-hidden">
<ImageOrVideo imageSrc={item.imageSrc} videoSrc={item.videoSrc} className="transition-transform duration-500 ease-in-out group-hover:scale-105" />
</div>
</ScrollReveal>
))}
</div>
</div>
</section>
);
};
export default FeaturesDetailedCards;

View File

@@ -1,117 +0,0 @@
import { useState } from "react";
import { Plus } from "lucide-react";
import TextAnimation from "@/components/ui/TextAnimation";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
import GridOrCarousel from "@/components/ui/GridOrCarousel";
import Button from "@/components/ui/Button";
import ScrollReveal from "@/components/ui/ScrollReveal";
type FeatureItem = {
title: string;
descriptions: string[];
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
interface FeaturesFlipCardsProps {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
items: FeatureItem[];
}
const FeatureFlipCard = ({ item }: { item: FeatureItem }) => {
const [isFlipped, setIsFlipped] = useState(false);
return (
<div
className="relative w-full cursor-pointer perspective-[3000px]"
onClick={() => setIsFlipped(!isFlipped)}
>
<div
data-flipped={isFlipped}
className="relative w-full h-full transition-transform duration-500 transform-3d data-[flipped=true]:transform-[rotateY(180deg)]"
>
<div className="flex flex-col gap-3 xl:gap-3.5 2xl:gap-4 p-3 xl:p-3.5 2xl:p-4 card rounded backface-hidden">
<div className="flex items-start justify-between gap-5 p-3 xl:p-3.5 2xl:p-4">
<h3 className="text-3xl font-semibold leading-snug text-balance">{item.title}</h3>
<div className="flex items-center justify-center shrink-0 size-9 primary-button rounded-full">
<Plus className="size-4 text-primary-cta-text" strokeWidth={2} />
</div>
</div>
<div className="relative overflow-hidden aspect-4/5 rounded">
<ImageOrVideo imageSrc={item.imageSrc} videoSrc={item.videoSrc} className="absolute inset-0" />
</div>
</div>
<div className="absolute inset-0 flex flex-col gap-3 xl:gap-3.5 2xl:gap-4 p-3 xl:p-3.5 2xl:p-4 card rounded backface-hidden transform-[rotateY(180deg)]">
<div className="flex items-start justify-between gap-5 p-3 xl:p-3.5 2xl:p-4">
<h3 className="text-3xl font-semibold leading-snug text-balance">{item.title}</h3>
<div className="flex items-center justify-center shrink-0 size-9 primary-button rounded-full">
<Plus className="size-4 rotate-45 text-primary-cta-text" strokeWidth={2} />
</div>
</div>
<div className="flex-1 flex flex-col gap-2 p-3 xl:p-3.5 2xl:p-4 bg-foreground/5 shadow shadow-foreground/5 rounded">
{item.descriptions.map((desc, index) => (
<p key={index} className="text-base md:text-lg leading-snug text-balance">{desc}</p>
))}
</div>
</div>
</div>
</div>
);
};
const FeaturesFlipCards = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
}: FeaturesFlipCardsProps) => {
return (
<section aria-label="Features section" className="py-20">
<div className="flex flex-col gap-8 md:gap-10">
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
<div className="px-3 py-1 mb-1 text-sm card rounded w-fit">
<p>{tag}</p>
</div>
<TextAnimation
text={title}
variant="fade-blur"
gradientText={true}
tag="h2"
className="md:max-w-8/10 text-6xl 2xl:text-7xl leading-[1.15] font-semibold text-center text-balance"
/>
<TextAnimation
text={description}
variant="fade-blur"
gradientText={false}
tag="p"
className="md:max-w-7/10 text-lg md:text-xl leading-snug text-center text-balance"
/>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-2 md:mt-3">
{primaryButton && <Button text={primaryButton.text} href={primaryButton.href} variant="primary"/>}
{secondaryButton && <Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary"animationDelay={0.1} />}
</div>
)}
</div>
<ScrollReveal variant="fade">
<GridOrCarousel>
{items.map((item) => (
<FeatureFlipCard key={item.title} item={item} />
))}
</GridOrCarousel>
</ScrollReveal>
</div>
</section>
);
};
export default FeaturesFlipCards;

View File

@@ -1,103 +0,0 @@
import { Info } from "lucide-react";
import TextAnimation from "@/components/ui/TextAnimation";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
import GridOrCarousel from "@/components/ui/GridOrCarousel";
import Button from "@/components/ui/Button";
import ScrollReveal from "@/components/ui/ScrollReveal";
type FeatureItem = {
title: string;
description: string;
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
interface FeaturesRevealCardsProps {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
items: FeatureItem[];
}
const FeaturesRevealCards = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
}: FeaturesRevealCardsProps) => {
return (
<section aria-label="Features section" className="py-20">
<div className="flex flex-col gap-8 md:gap-10">
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
<div className="px-3 py-1 mb-1 text-sm card rounded w-fit">
<p>{tag}</p>
</div>
<TextAnimation
text={title}
variant="slide-up"
gradientText={true}
tag="h2"
className="md:max-w-8/10 text-6xl 2xl:text-7xl leading-[1.15] font-semibold text-center text-balance"
/>
<TextAnimation
text={description}
variant="slide-up"
gradientText={false}
tag="p"
className="md:max-w-7/10 text-lg md:text-xl leading-snug text-center text-balance"
/>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-2 md:mt-3">
{primaryButton && <Button text={primaryButton.text} href={primaryButton.href} variant="primary" />}
{secondaryButton && <Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary" animationDelay={0.1} />}
</div>
)}
</div>
<ScrollReveal variant="fade">
<GridOrCarousel>
{items.map((item, index) => (
<div key={item.title} className="group relative overflow-hidden aspect-6/7 rounded">
<ImageOrVideo imageSrc={item.imageSrc} videoSrc={item.videoSrc} className="absolute inset-0" />
<div className="absolute top-4 left-4 xl:top-6 xl:left-6 2xl:top-8 2xl:left-8 z-20 perspective-[1000px]">
<div className="relative size-8 transform-3d transition-transform duration-400 group-hover:rotate-y-180">
<div className="absolute inset-0 flex items-center justify-center text-sm rounded bg-background backface-hidden text-foreground">
<p>{index + 1}</p>
</div>
<div className="absolute inset-0 flex items-center justify-center rounded bg-background backface-hidden rotate-y-180">
<Info className="h-1/2 w-1/2 text-foreground" strokeWidth={1.5} />
</div>
</div>
</div>
<div className="absolute -inset-x-px -bottom-px h-2/5 backdrop-blur-xl mask-fade-top-overlay" aria-hidden="true" />
<div className="absolute inset-x-2 bottom-2 xl:inset-x-3 xl:bottom-3 2xl:inset-x-4 2xl:bottom-4 z-10">
<div className="relative flex flex-col gap-0 group-hover:gap-1 xl:group-hover:gap-2 2xl:group-hover:gap-3 p-2 xl:p-3 2xl:p-4 transition-all duration-400">
<div className="absolute inset-0 -z-10 card rounded translate-y-full opacity-0 transition-all duration-400 ease-out group-hover:translate-y-0 group-hover:opacity-100" />
<h3 className="text-2xl font-semibold leading-snug text-white transition-colors duration-400 group-hover:text-foreground">
{item.title}
</h3>
<div className="grid grid-rows-[0fr] transition-all duration-400 ease-out group-hover:grid-rows-[1fr]">
<p className="overflow-hidden text-sm leading-snug text-foreground opacity-0 transition-opacity duration-400 group-hover:opacity-100">
{item.description}
</p>
</div>
</div>
</div>
</div>
))}
</GridOrCarousel>
</ScrollReveal>
</div>
</section>
);
};
export default FeaturesRevealCards;

View File

@@ -1,109 +0,0 @@
import Button from "@/components/ui/Button";
import TextAnimation from "@/components/ui/TextAnimation";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
import ScrollReveal from "@/components/ui/ScrollReveal";
import { cls } from "@/lib/utils";
type FeatureItem = {
title: string;
description: string;
href: string;
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
interface FeaturesRevealCardsBentoSharpProps {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
items: [FeatureItem, FeatureItem, FeatureItem, FeatureItem, FeatureItem, FeatureItem, FeatureItem];
}
const FeaturesRevealCardsBentoSharp = ({ tag, title, description, primaryButton, secondaryButton, items }: FeaturesRevealCardsBentoSharpProps) => {
const gridClasses = [
"md:col-span-2",
"md:col-span-4",
"md:col-span-3",
"md:col-span-3",
"md:col-span-2",
"md:col-span-2",
"md:col-span-2",
];
const staggerDelays = [
0,
0.1,
0,
0.1,
0,
0.1,
0.2,
];
return (
<section aria-label="Features reveal cards bento section" className="py-20">
<div className="flex flex-col gap-8 md:gap-10">
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
<div className="px-3 py-1 mb-1 text-sm card rounded w-fit">
<p>{tag}</p>
</div>
<TextAnimation
text={title}
variant="slide-up"
gradientText={true}
tag="h2"
className="md:max-w-8/10 text-6xl 2xl:text-7xl leading-[1.15] font-semibold text-center text-balance"
/>
<TextAnimation
text={description}
variant="slide-up"
gradientText={false}
tag="p"
className="md:max-w-7/10 text-lg md:text-xl leading-snug text-center text-balance"
/>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-2 md:mt-3">
{primaryButton && <Button text={primaryButton.text} href={primaryButton.href} variant="primary"/>}
{secondaryButton && <Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary" animationDelay={0.1} />}
</div>
)}
</div>
<div className="w-content-width mx-auto grid grid-cols-1 md:grid-cols-6 gap-3">
{items.map((item, index) => (
<ScrollReveal key={item.title} variant="fade" delay={staggerDelays[index]} className={cls("col-span-1 group", gridClasses[index])}>
<a href={item.href} className="block relative overflow-hidden rounded-none">
<div className="h-80 xl:h-100 2xl:h-120 overflow-hidden">
<ImageOrVideo
imageSrc={item.imageSrc}
videoSrc={item.videoSrc}
className="rounded-none group-hover:scale-105 transition-transform duration-500"
/>
</div>
<div className="absolute -inset-x-px -bottom-px h-2/5 backdrop-blur-xl mask-fade-top-overlay" aria-hidden="true" />
<div className="absolute inset-x-3 bottom-3 2xl:inset-x-4 2xl:bottom-4 z-10">
<div className="relative flex flex-col gap-1 md:gap-0 md:group-hover:gap-1 p-3 2xl:p-4 transition-all duration-400">
<div className="absolute inset-0 -z-10 card rounded-none translate-y-0 opacity-100 md:translate-y-full md:opacity-0 transition-all duration-400 ease-out md:group-hover:translate-y-0 md:group-hover:opacity-100" />
<h3 className="text-2xl font-semibold leading-snug text-foreground md:text-white transition-colors duration-400 md:group-hover:text-foreground">
{item.title}
</h3>
<div className="grid grid-rows-[1fr] md:grid-rows-[0fr] transition-all duration-400 ease-out md:group-hover:grid-rows-[1fr]">
<p className="overflow-hidden text-base leading-snug text-foreground opacity-100 md:opacity-0 transition-opacity duration-400 md:group-hover:opacity-100">
{item.description}
</p>
</div>
</div>
</div>
</a>
</ScrollReveal>
))}
</div>
</div>
</section>
);
};
export default FeaturesRevealCardsBentoSharp;

View File

@@ -1,233 +0,0 @@
"use client";
import { useLayoutEffect, useRef } from "react";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import Button from "@/components/ui/Button";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
import { cls } from "@/lib/utils";
gsap.registerPlugin(ScrollTrigger);
type FeatureItem = {
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
} & (
| { leftImageSrc: string; leftVideoSrc?: never }
| { leftVideoSrc: string; leftImageSrc?: never }
) & (
| { rightImageSrc: string; rightVideoSrc?: never }
| { rightVideoSrc: string; rightImageSrc?: never }
);
interface FeaturesStickyCardsProps {
items: FeatureItem[];
}
const CardFrame = ({
imageSrc,
videoSrc,
cardRef,
className = "",
}: {
imageSrc?: string;
videoSrc?: string;
cardRef: (el: HTMLDivElement | null) => void;
className?: string;
}) => (
<div ref={cardRef} className={cls("card rounded p-1 overflow-hidden", className)}>
<ImageOrVideo
imageSrc={imageSrc}
videoSrc={videoSrc}
className="w-full h-full object-cover rounded"
/>
</div>
);
const FeaturesStickyCards = ({
items,
}: FeaturesStickyCardsProps) => {
const imageRefs = useRef<(HTMLDivElement | null)[]>([]);
const mobileImageRefs = useRef<(HTMLDivElement | null)[]>([]);
const triggerRefs = useRef<(HTMLDivElement | null)[]>([]);
useLayoutEffect(() => {
const mm = gsap.matchMedia();
const getAnimationConfig = (itemIndex: number, isLeftCard: boolean) => {
const isOddItem = itemIndex % 2 === 1;
if (isLeftCard) {
return {
from: { xPercent: -225, rotation: -45 },
to: { rotation: isOddItem ? 10 : -10 },
};
} else {
return {
from: { xPercent: 225, rotation: 45 },
to: { rotation: isOddItem ? -10 : 10 },
};
}
};
const animateCards = (isMobile: boolean) => {
items.forEach((_, itemIndex) => {
[0, 1].forEach((cardIndex) => {
const refIndex = itemIndex * 2 + cardIndex;
const element = isMobile
? mobileImageRefs.current[refIndex]
: imageRefs.current[refIndex];
if (element) {
const isLeftCard = cardIndex === 0;
const fromConfig = isMobile
? {
xPercent: isLeftCard ? -150 : 150,
rotation: isLeftCard ? -25 : 25,
}
: getAnimationConfig(itemIndex, isLeftCard).from;
const toConfig = isMobile
? {
xPercent: 0,
rotation: 0,
duration: 1,
scrollTrigger: {
trigger: element,
start: "top 90%",
end: "top 50%",
scrub: 1,
},
}
: {
xPercent: 0,
rotation: getAnimationConfig(itemIndex, isLeftCard).to.rotation,
scrollTrigger: {
trigger: triggerRefs.current[itemIndex],
start: "top bottom",
end: "top top",
scrub: 1,
},
};
gsap.fromTo(element, fromConfig, toConfig);
}
});
});
};
mm.add("(max-width: 767px)", () => animateCards(true));
mm.add("(min-width: 768px)", () => animateCards(false));
return () => {
mm.revert();
imageRefs.current = [];
mobileImageRefs.current = [];
triggerRefs.current = [];
};
}, [items]);
const sectionHeightStyle = { height: `${items.length * 100}vh` };
return (
<section aria-label="Features sticky cards section" className="py-20 overflow-hidden md:overflow-visible">
<div className="flex flex-col gap-8">
<div className="hidden md:flex relative" style={sectionHeightStyle}>
<div
className="absolute top-0 left-0 flex flex-col w-6/10 mx-auto right-0 z-10"
style={sectionHeightStyle}
>
{items.map((item, index) => (
<div
key={index}
ref={(el) => { triggerRefs.current[index] = el; }}
className="w-full mx-auto h-screen flex justify-center items-center"
>
<div className="flex flex-col items-center gap-2">
<div className="flex flex-col items-center justify-center text-sm card rounded h-8 w-8 mb-1">
<p>{index + 1}</p>
</div>
<h3 className="text-5xl md:text-6xl font-semibold text-center text-balance">{item.title}</h3>
<p className="md:max-w-6/10 text-lg leading-snug text-center">{item.description}</p>
{(item.primaryButton || item.secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-2 md:mt-3">
{item.primaryButton && <Button text={item.primaryButton.text} href={item.primaryButton.href} variant="primary" />}
{item.secondaryButton && <Button text={item.secondaryButton.text} href={item.secondaryButton.href} variant="secondary" animationDelay={0.1} />}
</div>
)}
</div>
</div>
))}
</div>
<div className="sticky top-0 left-0 h-screen w-full overflow-hidden">
{items.map((item, itemIndex) => (
<div key={itemIndex} className="h-screen w-full absolute top-0 left-0">
<div className="w-content-width mx-auto h-full flex flex-row justify-between items-center">
<CardFrame
imageSrc={item.leftImageSrc}
videoSrc={item.leftVideoSrc}
cardRef={(el) => {
imageRefs.current[itemIndex * 2] = el;
}}
className="w-25/100 xl:w-27/100 2xl:w-29/100 h-[70vh]"
/>
<CardFrame
imageSrc={item.rightImageSrc}
videoSrc={item.rightVideoSrc}
cardRef={(el) => {
imageRefs.current[itemIndex * 2 + 1] = el;
}}
className="w-25/100 xl:w-27/100 2xl:w-28/100 h-[70vh]"
/>
</div>
</div>
))}
</div>
</div>
<div className="md:hidden flex flex-col gap-20 w-content-width mx-auto">
{items.map((item, itemIndex) => (
<div key={itemIndex} className="flex flex-col gap-8">
<div className="flex flex-col items-center gap-2">
<div className="flex flex-col items-center justify-center text-sm card rounded h-8 w-8 mb-1">
<p>{itemIndex + 1}</p>
</div>
<h3 className="text-4xl md:text-5xl font-semibold text-center text-balance">{item.title}</h3>
<p className="text-base md:text-lg leading-snug text-center">{item.description}</p>
{(item.primaryButton || item.secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-2 md:mt-3">
{item.primaryButton && <Button text={item.primaryButton.text} href={item.primaryButton.href} variant="primary" />}
{item.secondaryButton && <Button text={item.secondaryButton.text} href={item.secondaryButton.href} variant="secondary" animationDelay={0.1} />}
</div>
)}
</div>
<div className="flex flex-row gap-3 justify-center">
<CardFrame
imageSrc={item.leftImageSrc}
videoSrc={item.leftVideoSrc}
cardRef={(el) => {
mobileImageRefs.current[itemIndex * 2] = el;
}}
className="w-1/2 aspect-9/16"
/>
<CardFrame
imageSrc={item.rightImageSrc}
videoSrc={item.rightVideoSrc}
cardRef={(el) => {
mobileImageRefs.current[itemIndex * 2 + 1] = el;
}}
className="w-1/2 aspect-9/16"
/>
</div>
</div>
))}
</div>
</div>
</section>
);
};
export default FeaturesStickyCards;

View File

@@ -1,66 +0,0 @@
import { ChevronRight } from "lucide-react";
import { useButtonClick } from "@/hooks/useButtonClick";
import AutoFillText from "@/components/ui/AutoFillText";
import { cls } from "@/lib/utils";
type FooterLink = {
label: string;
href?: string;
onClick?: () => void;
};
type FooterColumn = {
items: FooterLink[];
};
const FooterLinkItem = ({ label, href, onClick }: FooterLink) => {
const handleClick = useButtonClick(href, onClick);
return (
<div className="flex items-center gap-2 text-base">
<ChevronRight className="size-4" strokeWidth={3} aria-hidden="true" />
<button
onClick={handleClick}
className="text-base text-primary-cta-text font-semibold hover:opacity-75 transition-opacity cursor-pointer"
>
{label}
</button>
</div>
);
};
const FooterBrand = ({
brand,
columns,
}: {
brand: string;
columns: FooterColumn[];
}) => {
return (
<footer
aria-label="Site footer"
className="w-full py-15 mt-20 rounded-t-lg overflow-hidden primary-button text-primary-cta-text"
>
<div className="w-content-width mx-auto flex flex-col gap-10 md:gap-20">
<AutoFillText className="font-semibold">{brand}</AutoFillText>
<div
className={cls(
"flex flex-col gap-8 mb-10 md:flex-row",
columns.length === 1 ? "md:justify-center" : "md:justify-between"
)}
>
{columns.map((column, index) => (
<div key={index} className="flex flex-col items-start gap-3">
{column.items.map((item) => (
<FooterLinkItem key={item.label} label={item.label} href={item.href} onClick={item.onClick} />
))}
</div>
))}
</div>
</div>
</footer>
);
};
export default FooterBrand;

View File

@@ -1,101 +0,0 @@
import { useRef, useEffect, useState } from "react";
import { ChevronRight } from "lucide-react";
import { useButtonClick } from "@/hooks/useButtonClick";
import AutoFillText from "@/components/ui/AutoFillText";
import { cls } from "@/lib/utils";
type FooterLink = {
label: string;
href?: string;
onClick?: () => void;
};
type FooterColumn = {
items: FooterLink[];
};
const FooterLinkItem = ({ label, href, onClick }: FooterLink) => {
const handleClick = useButtonClick(href, onClick);
return (
<div className="flex items-center gap-2 text-base">
<ChevronRight className="size-4" strokeWidth={3} aria-hidden="true" />
<button
onClick={handleClick}
className="text-base text-primary-cta-text font-semibold hover:opacity-75 transition-opacity cursor-pointer"
>
{label}
</button>
</div>
);
};
const FooterBrandReveal = ({
brand,
columns,
}: {
brand: string;
columns: FooterColumn[];
}) => {
const footerRef = useRef<HTMLDivElement>(null);
const [footerHeight, setFooterHeight] = useState(0);
useEffect(() => {
const updateHeight = () => {
if (footerRef.current) {
setFooterHeight(footerRef.current.offsetHeight);
}
};
updateHeight();
const resizeObserver = new ResizeObserver(updateHeight);
if (footerRef.current) {
resizeObserver.observe(footerRef.current);
}
return () => resizeObserver.disconnect();
}, []);
return (
<section
className="relative z-0 w-full mt-20"
style={{
height: footerHeight ? `${footerHeight}px` : "auto",
clipPath: "polygon(0% 0, 100% 0%, 100% 100%, 0 100%)",
}}
>
<div
className="fixed bottom-0 w-full"
style={{ height: footerHeight ? `${footerHeight}px` : "auto" }}
>
<footer
ref={footerRef}
aria-label="Site footer"
className="w-full py-15 rounded-t-lg overflow-hidden primary-button text-primary-cta-text"
>
<div className="w-content-width mx-auto flex flex-col gap-10 md:gap-20">
<AutoFillText className="font-semibold">{brand}</AutoFillText>
<div
className={cls(
"flex flex-col gap-8 mb-10 md:flex-row",
columns.length === 1 ? "md:justify-center" : "md:justify-between"
)}
>
{columns.map((column, index) => (
<div key={index} className="flex flex-col items-start gap-3">
{column.items.map((item) => (
<FooterLinkItem key={item.label} label={item.label} href={item.href} onClick={item.onClick} />
))}
</div>
))}
</div>
</div>
</footer>
</div>
</section>
);
};
export default FooterBrandReveal;

View File

@@ -1,120 +0,0 @@
import { useRef, useEffect, useState } from "react";
import { useButtonClick } from "@/hooks/useButtonClick";
type FooterColumn = {
title: string;
items: { label: string; href?: string; onClick?: () => void }[];
};
type FooterLink = {
label: string;
href?: string;
onClick?: () => void;
};
const FooterLinkItem = ({ label, href, onClick }: FooterLink) => {
const handleClick = useButtonClick(href, onClick);
return (
<button
onClick={handleClick}
className="text-base text-primary-cta-text hover:opacity-75 transition-opacity cursor-pointer"
>
{label}
</button>
);
};
const FooterBottomLink = ({ label, href, onClick }: FooterLink) => {
const handleClick = useButtonClick(href, onClick);
return (
<button
onClick={handleClick}
className="text-sm opacity-50 hover:opacity-75 transition-opacity cursor-pointer"
>
{label}
</button>
);
};
const FooterSimpleReveal = ({
brand,
columns,
copyright,
links,
}: {
brand: string;
columns: FooterColumn[];
copyright: string;
links: FooterLink[];
}) => {
const footerRef = useRef<HTMLDivElement>(null);
const [footerHeight, setFooterHeight] = useState(0);
useEffect(() => {
const updateHeight = () => {
if (footerRef.current) {
setFooterHeight(footerRef.current.offsetHeight);
}
};
updateHeight();
const resizeObserver = new ResizeObserver(updateHeight);
if (footerRef.current) {
resizeObserver.observe(footerRef.current);
}
return () => resizeObserver.disconnect();
}, []);
return (
<section
className="relative z-0 w-full mt-20"
style={{
height: footerHeight ? `${footerHeight}px` : "auto",
clipPath: "polygon(0% 0, 100% 0%, 100% 100%, 0 100%)",
}}
>
<div
className="fixed bottom-0 w-full"
style={{ height: footerHeight ? `${footerHeight}px` : "auto" }}
>
<footer
ref={footerRef}
aria-label="Site footer"
className="w-full py-15 primary-button text-primary-cta-text"
>
<div className="w-content-width mx-auto">
<div className="flex flex-col md:flex-row gap-10 md:gap-0 justify-between items-start mb-10">
<h2 className="text-4xl font-semibold">{brand}</h2>
<div className="w-full md:w-fit flex flex-wrap gap-y-10 md:gap-12">
{columns.map((column) => (
<div key={column.title} className="w-1/2 md:w-auto flex flex-col items-start gap-3">
<h3 className="text-sm opacity-50 truncate">{column.title}</h3>
{column.items.map((item) => (
<FooterLinkItem key={item.label} label={item.label} href={item.href} onClick={item.onClick} />
))}
</div>
))}
</div>
</div>
<div className="flex items-center justify-between pt-8 border-t border-primary-cta-text/20">
<span className="text-sm opacity-50">{copyright}</span>
<div className="flex items-center gap-3">
{links.map((link) => (
<FooterBottomLink key={link.label} label={link.label} href={link.href} onClick={link.onClick} />
))}
</div>
</div>
</div>
</footer>
</div>
</section>
);
};
export default FooterSimpleReveal;

View File

@@ -1,52 +0,0 @@
import Button from "@/components/ui/Button";
import HeroBackgroundSlot from "@/components/ui/HeroBackgroundSlot";
import TextAnimation from "@/components/ui/TextAnimation";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
import AutoFillText from "@/components/ui/AutoFillText";
import ScrollReveal from "@/components/ui/ScrollReveal";
type HeroBillboardBrandProps = {
brand: string;
description: string;
primaryButton: { text: string; href: string };
secondaryButton: { text: string; href: string };
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
const HeroBillboardBrand = ({
brand,
description,
primaryButton,
secondaryButton,
imageSrc,
videoSrc,
}: HeroBillboardBrandProps) => {
return (
<section aria-label="Hero section" className="relative pt-25 pb-20 md:pt-30">
<HeroBackgroundSlot />
<div className="flex flex-col gap-10 md:gap-12 w-content-width mx-auto">
<div className="flex flex-col items-end gap-5">
<AutoFillText className="w-full font-semibold" paddingY="">{brand}</AutoFillText>
<TextAnimation
text={description}
variant="slide-up"
gradientText={false}
tag="p"
className="w-full md:w-1/2 text-lg md:text-2xl leading-snug text-balance text-right"
/>
<div className="flex flex-wrap justify-end gap-3 mt-1 md:mt-2">
<Button text={primaryButton.text} href={primaryButton.href} variant="primary"/>
<Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary"animationDelay={0.1} />
</div>
</div>
<ScrollReveal variant="fade-blur" delay={0.2} className="w-full p-2 xl:p-3 2xl:p-4 card rounded overflow-hidden">
<ImageOrVideo imageSrc={imageSrc} videoSrc={videoSrc} className="aspect-4/5 md:aspect-video" />
</ScrollReveal>
</div>
</section>
);
};
export default HeroBillboardBrand;

View File

@@ -1,75 +0,0 @@
import Button from "@/components/ui/Button";
import HeroBackgroundSlot from "@/components/ui/HeroBackgroundSlot";
import TextAnimation from "@/components/ui/TextAnimation";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
type HeroBillboardCarouselProps = {
tag: string;
title: string;
description: string;
primaryButton: { text: string; href: string };
secondaryButton: { text: string; href: string };
items: ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never })[];
};
const HeroBillboardCarousel = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
}: HeroBillboardCarouselProps) => {
const duplicated = [...items, ...items, ...items, ...items];
return (
<section
aria-label="Hero section"
className="relative flex flex-col items-center justify-center gap-8 md:gap-10 w-full min-h-svh pt-25 pb-20 md:pt-30"
>
<HeroBackgroundSlot />
<div className="flex flex-col items-center gap-3 w-content-width mx-auto text-center">
<div className="px-3 py-1 mb-1 text-sm card rounded w-fit">
<p>{tag}</p>
</div>
<TextAnimation
text={title}
variant="fade-blur"
gradientText={true}
tag="h1"
className="md:max-w-8/10 text-7xl 2xl:text-8xl leading-[1.15] font-semibold text-center text-balance"
/>
<TextAnimation
text={description}
variant="fade-blur"
gradientText={false}
tag="p"
className="md:max-w-7/10 text-lg md:text-xl leading-snug text-balance"
/>
<div className="flex flex-wrap justify-center gap-3 mt-2 md:mt-3">
<Button text={primaryButton.text} href={primaryButton.href} variant="primary"/>
<Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary"animationDelay={0.1} />
</div>
</div>
<div className="w-content-width mx-auto overflow-hidden mask-fade-x">
<div className="flex w-max animate-marquee-horizontal" style={{ animationDuration: "60s" }}>
{duplicated.map((item, i) => (
<div key={i} className="shrink-0 w-60 md:w-75 2xl:w-80 aspect-4/5 mr-3 md:mr-5 p-2 xl:p-3 2xl:p-4 card rounded-lg overflow-hidden">
<ImageOrVideo
imageSrc={item.imageSrc}
videoSrc={item.videoSrc}
className="w-full h-full rounded-lg object-cover"
/>
</div>
))}
</div>
</div>
</section>
);
};
export default HeroBillboardCarousel;

View File

@@ -1,80 +0,0 @@
import Button from "@/components/ui/Button";
import HeroBackgroundSlot from "@/components/ui/HeroBackgroundSlot";
import TextAnimation from "@/components/ui/TextAnimation";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
import AvatarGroup from "@/components/ui/AvatarGroup";
type HeroCenteredLogosProps = {
avatarsSrc: string[];
avatarText: string;
title: string;
description: string;
primaryButton: { text: string; href: string };
secondaryButton: { text: string; href: string };
logos: string[];
hideMedia?: boolean;
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
const HeroCenteredLogos = ({
avatarsSrc,
avatarText,
title,
description,
primaryButton,
secondaryButton,
logos,
imageSrc,
videoSrc,
hideMedia = false,
}: HeroCenteredLogosProps) => {
return (
<section aria-label="Hero section" className="relative h-svh flex flex-col mb-20">
<HeroBackgroundSlot />
{!hideMedia && (
<div className="absolute inset-0 z-0">
<ImageOrVideo imageSrc={imageSrc} videoSrc={videoSrc} className="size-full object-cover" />
<div className="absolute inset-0 bg-background/80" />
</div>
)}
<div className="relative z-10 flex-1 flex items-center justify-center">
<div className="flex flex-col items-center gap-3 pt-8 w-content-width mx-auto text-center">
<AvatarGroup avatarsSrc={avatarsSrc} label={avatarText} size="lg" />
<TextAnimation
text={title}
variant="slide-up"
gradientText={true}
tag="h1"
className="md:max-w-8/10 text-7xl 2xl:text-8xl leading-[1.15] font-semibold text-center text-balance"
/>
<TextAnimation
text={description}
variant="slide-up"
gradientText={false}
tag="p"
className="md:max-w-7/10 text-lg md:text-xl leading-snug text-balance"
/>
<div className="flex flex-wrap justify-center gap-3 mt-2 md:mt-3">
<Button text={primaryButton.text} href={primaryButton.href} variant="primary" />
<Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary" animationDelay={0.1} />
</div>
</div>
</div>
<div className="relative z-10 w-content-width mx-auto pb-8 overflow-hidden mask-fade-x">
<div className="flex w-max animate-marquee-horizontal" style={{ animationDuration: "30s" }}>
{[...logos, ...logos, ...logos, ...logos].map((logo, index) => (
<div key={index} className="shrink-0 mx-3 px-4 py-2 card rounded">
<span className="text-xl font-semibold whitespace-nowrap text-foreground/75">{logo}</span>
</div>
))}
</div>
</div>
</section>
);
};
export default HeroCenteredLogos;

View File

@@ -1,283 +0,0 @@
import { useRef, useEffect } from "react";
import { motion } from "motion/react";
import { ArrowRight } from "lucide-react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
import HeroBackgroundSlot from "@/components/ui/HeroBackgroundSlot";
import TextAnimation from "@/components/ui/TextAnimation";
import { useButtonClick } from "@/hooks/useButtonClick";
gsap.registerPlugin(ScrollTrigger);
interface HeroWorkScrollStackProps {
tag: string;
title: string;
titleHighlight: string;
description: string;
descriptionMuted: string;
primaryButton: { text: string; href: string; avatarSrc: string; avatarLabel: string };
sectionTag: string;
sectionTitle: string;
sectionDescription: string;
items: [
{ title: string; description: string; imageSrc: string; tag: string },
{ title: string; description: string; imageSrc: string; tag: string },
{ title: string; description: string; imageSrc: string; tag: string }
];
secondaryButton?: { text: string; href: string };
heroAnimationDelay?: number;
}
const HeroWorkScrollStack = ({
tag,
title,
titleHighlight,
description,
descriptionMuted,
primaryButton,
sectionTag,
sectionTitle,
sectionDescription,
items,
secondaryButton,
heroAnimationDelay,
}: HeroWorkScrollStackProps) => {
const animationRef = useRef<HTMLDivElement>(null);
const placeholderRef = useRef<HTMLDivElement>(null);
const card1Ref = useRef<HTMLDivElement>(null);
const card2Ref = useRef<HTMLDivElement>(null);
const card3Ref = useRef<HTMLDivElement>(null);
const handlePrimaryClick = useButtonClick(primaryButton.href);
const handleSecondaryClick = useButtonClick(secondaryButton?.href || "#");
useEffect(() => {
const isDesktop = window.matchMedia("(min-width: 768px)").matches;
const ctx = gsap.context(() => {
const cardRefs = [card1Ref.current, card2Ref.current, card3Ref.current];
const placeholder = placeholderRef.current;
if (!placeholder) return;
const placeholderRect = placeholder.getBoundingClientRect();
const placeholderCenterY = placeholderRect.top + placeholderRect.height / 2;
if (isDesktop) {
// DESKTOP: Scrub animation tied to scroll position
const xOffsets = ["32rem", "14.5rem", "-1.8rem"];
const yAdjustments = [0, -48, 0];
const rotations = [-5, 0, 5];
const scales = [1.35, 1.3, 1.25];
const zIndexes = [30, 20, 10];
const tl = gsap.timeline({
scrollTrigger: {
trigger: animationRef.current,
start: "top top",
end: "bottom bottom",
scrub: 1,
},
});
cardRefs.forEach((card, i) => {
if (!card) return;
const cardRect = card.getBoundingClientRect();
const cardCenterY = cardRect.top + cardRect.height / 2;
const yOffset = placeholderCenterY - cardCenterY;
gsap.set(card, {
x: xOffsets[i],
y: yOffset + yAdjustments[i],
rotation: rotations[i],
scale: scales[i],
zIndex: zIndexes[i],
willChange: "transform",
force3D: true,
});
tl.to(card, { x: 0, y: 0, rotation: 0, scale: 1, duration: 0.4, ease: "none" }, 0);
tl.to(card, { zIndex: 1, duration: 0.1, ease: "none" }, 0.3);
});
} else {
// MOBILE: Toggle animation - play/reverse on scroll
const xOffsets = ["2.5rem", "0.5rem", "-1rem"];
const yAdjustments = [-10, -30, 10];
const rotations = [-5, 0, 5];
const scales = [0.65, 0.7, 0.75];
const zIndexes = [30, 20, 10];
cardRefs.forEach((card, i) => {
if (!card) return;
const cardRect = card.getBoundingClientRect();
const cardCenterY = cardRect.top + cardRect.height / 2;
const yOffset = placeholderCenterY - cardCenterY;
gsap.set(card, {
x: xOffsets[i],
y: yOffset + yAdjustments[i],
rotation: rotations[i],
scale: scales[i],
zIndex: zIndexes[i],
willChange: "transform",
force3D: true,
});
gsap.to(card, {
x: 0,
y: 0,
rotation: 0,
scale: 1,
duration: 1.2,
ease: "power2.inOut",
scrollTrigger: {
trigger: placeholder,
start: "top 35%",
toggleActions: "play none none reverse",
},
});
});
}
}, animationRef);
return () => ctx.revert();
}, []);
return (
<div ref={animationRef}>
<div id="hero" data-section="hero">
<section aria-label="Hero section" className="relative h-fit md:h-svh pt-30 pb-20 md:py-0 flex items-center overflow-hidden md:overflow-visible">
<HeroBackgroundSlot />
<div className="w-content-width mx-auto">
<div className="flex flex-col md:flex-row items-center gap-10 md:gap-20 w-full">
<motion.div
initial={{ y: 10, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 1.8, ease: [0.16, 1, 0.3, 1], delay: heroAnimationDelay ?? 0 }}
className="w-full md:w-[46%] flex flex-col items-center md:items-start gap-3"
>
<div className="card backdrop-blur flex items-center gap-2 px-3 py-1 rounded">
<span className="size-2 rounded-full bg-green-500 animate-pulsate [--accent:#22c55e]" />
<p className="text-sm leading-snug font-medium text-foreground">{tag}</p>
</div>
<h1 className="text-6xl md:text-7xl 2xl:text-8xl font-medium leading-[1.05] tracking-tight text-center md:text-left">
<span className="inline pb-[0.1em] -mb-[0.1em] bg-linear-to-r from-foreground to-primary-cta bg-clip-text text-transparent">
{title}{" "}
<span className="font-bold">{titleHighlight}</span>
</span>
</h1>
<p className="text-base md:text-lg font-medium leading-snug text-center md:text-left max-w-[95%]">
{description}{" "}
<span className="text-foreground/50">{descriptionMuted}</span>
</p>
<a
href={primaryButton.href}
onClick={handlePrimaryClick}
className="group flex items-center gap-3 mt-2 text-primary-cta-text rounded-full pl-3 pr-6 py-3 w-fit primary-button transition-all duration-300"
>
<div className="flex items-center">
<div className="card p-px rounded-full transition-transform duration-500 ease-out group-hover:-rotate-6">
<img
src={primaryButton.avatarSrc}
className="w-9 h-9 rounded-full object-cover"
alt=""
/>
</div>
<div className="grid grid-cols-[0fr] group-hover:grid-cols-[1fr] transition-all duration-500 ease-out">
<div className="overflow-hidden flex items-center">
<span className="text-primary-cta-text text-sm font-medium mx-2 transition-transform duration-500 ease-out -translate-x-3 group-hover:translate-x-0">
+
</span>
<div className="card p-px rounded-full shrink-0 transition-transform duration-500 ease-out -translate-x-5 group-hover:translate-x-0 group-hover:rotate-6">
<span className="w-9 h-9 rounded-full flex items-center justify-center">
<span className="text-foreground text-xs font-bold">{primaryButton.avatarLabel}</span>
</span>
</div>
</div>
</div>
</div>
<span className="text-base font-medium whitespace-nowrap">{primaryButton.text}</span>
</a>
</motion.div>
<div ref={placeholderRef} className="w-full md:w-[54%] relative h-80 md:h-96">
<div className="absolute inset-0 card rounded-2xl md:hidden" />
</div>
</div>
</div>
</section>
</div>
<div id="work" data-section="work">
<section aria-label="Work section" className="py-20 md:pt-0">
<div className="flex flex-col gap-8 w-content-width mx-auto">
<div className="flex flex-col items-center gap-2">
<div className="px-3 py-1 mb-1 text-sm card rounded w-fit">
<p>{sectionTag}</p>
</div>
<TextAnimation
text={sectionTitle}
variant="fade-blur"
gradientText={true}
tag="h2"
className="md:max-w-8/10 text-6xl 2xl:text-7xl leading-[1.15] font-semibold text-center text-balance"
/>
<TextAnimation
text={sectionDescription}
variant="fade-blur"
gradientText={false}
tag="p"
className="md:max-w-7/10 text-lg md:text-xl leading-snug text-center text-balance"
/>
</div>
<div className="grid md:grid-cols-3 gap-5">
{items.map((item, index) => {
const cardRef = index === 0 ? card1Ref : index === 1 ? card2Ref : card3Ref;
return (
<div key={item.title} className="flex flex-col gap-3 xl:gap-4 2xl:gap-5">
<div
ref={cardRef}
className="aspect-4/3 rounded-2xl shadow-2xl relative card p-2 xl:p-3 2xl:p-4"
>
<div className="w-full h-full rounded-xl overflow-hidden relative">
<ImageOrVideo imageSrc={item.imageSrc} className="w-full h-full object-cover" />
<span className="absolute bottom-2 left-2 xl:bottom-3 xl:left-3 2xl:bottom-4 2xl:left-4 px-3 py-1.5 text-xs font-medium text-primary-cta-text rounded-full backdrop-blur-xl bg-primary-cta-text/15 border border-primary-cta-text/20">
{item.tag}
</span>
</div>
</div>
<p className="text-lg md:text-xl lg:text-2xl leading-snug">
<span className="font-semibold text-foreground">{item.title}. </span>
<span className="text-foreground/50">{item.description}</span>
</p>
</div>
);
})}
</div>
{secondaryButton && (
<div className="flex justify-center">
<a
href={secondaryButton.href}
onClick={handleSecondaryClick}
className="group flex items-center gap-2 px-6 py-3 text-base font-medium rounded-full secondary-button text-secondary-cta-text transition-all duration-300"
>
<span>{secondaryButton.text}</span>
<ArrowRight className="size-4 transition-transform duration-300 group-hover:translate-x-1" />
</a>
</div>
)}
</div>
</section>
</div>
</div>
);
};
export default HeroWorkScrollStack;

View File

@@ -1,113 +0,0 @@
import Button from "@/components/ui/Button";
import TextAnimation from "@/components/ui/TextAnimation";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
import ScrollReveal from "@/components/ui/ScrollReveal";
type Testimonial = {
name: string;
role: string;
quote: string;
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
const TestimonialMarqueeCards = ({
tag,
title,
description,
primaryButton,
secondaryButton,
testimonials,
}: {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
testimonials: Testimonial[];
}) => {
const half = Math.ceil(testimonials.length / 2);
const topRow = testimonials.slice(0, half);
const bottomRow = testimonials.slice(half);
return (
<section aria-label="Testimonials section" className="pt-20 pb-10">
<div className="flex flex-col gap-8 md:gap-10">
<div className="flex flex-col items-center gap-2 w-content-width mx-auto">
<div className="px-3 py-1 mb-1 text-sm card rounded w-fit">
<p>{tag}</p>
</div>
<TextAnimation
text={title}
variant="fade-blur"
gradientText={true}
tag="h2"
className="md:max-w-8/10 text-6xl 2xl:text-7xl leading-[1.15] font-semibold text-center text-balance"
/>
<TextAnimation
text={description}
variant="fade-blur"
gradientText={false}
tag="p"
className="md:max-w-7/10 text-lg md:text-xl leading-snug text-center text-balance"
/>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-2 md:mt-3">
{primaryButton && <Button text={primaryButton.text} href={primaryButton.href} variant="primary"/>}
{secondaryButton && <Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary"animationDelay={0.1} />}
</div>
)}
</div>
<ScrollReveal variant="fade-blur" className="flex flex-col w-content-width mx-auto">
<div className="overflow-hidden mask-fade-x">
<div className="flex w-max animate-marquee-horizontal mb-5" style={{ animationDuration: "30s" }}>
{[...topRow, ...topRow, ...topRow, ...topRow].map((testimonial, index) => (
<div key={`top-${index}`} className="flex flex-col justify-between gap-4 xl:gap-5 2xl:gap-6 shrink-0 w-72 md:w-80 mr-5 p-6 xl:p-7 2xl:p-8 rounded card">
<p className="text-lg leading-snug line-clamp-3">{testimonial.quote}</p>
<div className="flex items-center gap-3">
<ImageOrVideo
imageSrc={testimonial.imageSrc}
videoSrc={testimonial.videoSrc}
className="size-10 md:size-11 2xl:size-12 rounded-full object-cover"
/>
<div className="flex flex-col min-w-0">
<span className="text-base text-foreground font-semibold leading-snug truncate">{testimonial.name}</span>
<span className="text-base text-foreground/75 leading-snug truncate">{testimonial.role}</span>
</div>
</div>
</div>
))}
</div>
</div>
<div className="overflow-hidden mask-fade-x">
<div className="flex w-max animate-marquee-horizontal-reverse mb-10" style={{ animationDuration: "30s" }}>
{[...bottomRow, ...bottomRow, ...bottomRow, ...bottomRow].map((testimonial, index) => (
<div key={`bottom-${index}`} className="flex flex-col justify-between gap-4 xl:gap-5 2xl:gap-6 shrink-0 w-72 md:w-80 mr-5 p-6 xl:p-7 2xl:p-8 rounded card">
<p className="text-lg leading-snug line-clamp-3">{testimonial.quote}</p>
<div className="flex items-center gap-3">
<ImageOrVideo
imageSrc={testimonial.imageSrc}
videoSrc={testimonial.videoSrc}
className="size-10 md:size-11 2xl:size-12 rounded-full object-cover"
/>
<div className="flex flex-col min-w-0">
<span className="text-base text-foreground font-semibold leading-snug truncate">{testimonial.name}</span>
<span className="text-base text-foreground/75 leading-snug truncate">{testimonial.role}</span>
</div>
</div>
</div>
))}
</div>
</div>
</ScrollReveal>
</div>
</section>
);
};
export default TestimonialMarqueeCards;

View File

@@ -1,101 +0,0 @@
import { Star } from "lucide-react";
import Button from "@/components/ui/Button";
import TextAnimation from "@/components/ui/TextAnimation";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
import ScrollReveal from "@/components/ui/ScrollReveal";
import GridOrCarousel from "@/components/ui/GridOrCarousel";
import { cls } from "@/lib/utils";
type Testimonial = {
name: string;
role: string;
quote: string;
rating: number;
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
const TestimonialRatingCards = ({
tag,
title,
description,
primaryButton,
secondaryButton,
testimonials,
}: {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
testimonials: Testimonial[];
}) => {
return (
<section aria-label="Testimonials section" className="py-20">
<div className="flex flex-col gap-8 md:gap-10">
<div className="flex flex-col items-center gap-2 w-content-width mx-auto">
<div className="px-3 py-1 mb-1 text-sm card rounded w-fit">
<p>{tag}</p>
</div>
<TextAnimation
text={title}
variant="fade"
gradientText={true}
tag="h2"
className="md:max-w-8/10 text-6xl 2xl:text-7xl leading-[1.15] font-semibold text-center text-balance"
/>
<TextAnimation
text={description}
variant="fade"
gradientText={false}
tag="p"
className="md:max-w-7/10 text-lg md:text-xl leading-snug text-center text-balance"
/>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-2 md:mt-3">
{primaryButton && <Button text={primaryButton.text} href={primaryButton.href} variant="primary"/>}
{secondaryButton && <Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary"animationDelay={0.1} />}
</div>
)}
</div>
<ScrollReveal variant="fade-blur">
<GridOrCarousel>
{testimonials.map((testimonial) => (
<div key={testimonial.name} className="flex flex-col justify-between gap-4 xl:gap-5 2xl:gap-6 h-full p-6 xl:p-7 2xl:p-8 rounded card">
<div className="flex flex-col items-start gap-4 xl:gap-5 2xl:gap-6">
<div className="flex gap-1.5">
{Array.from({ length: 5 }).map((_, index) => (
<Star
key={index}
className={cls("size-5 text-accent", index < testimonial.rating ? "fill-accent" : "fill-transparent")}
strokeWidth={1.5}
/>
))}
</div>
<p className="text-lg leading-snug">{testimonial.quote}</p>
</div>
<div className="flex items-center gap-3">
<ImageOrVideo
imageSrc={testimonial.imageSrc}
videoSrc={testimonial.videoSrc}
className="size-10 md:size-11 2xl:size-12 rounded-full object-cover"
/>
<div className="flex flex-col min-w-0">
<span className="text-base text-foreground font-semibold leading-snug truncate">{testimonial.name}</span>
<span className="text-base text-foreground/75 leading-snug truncate">{testimonial.role}</span>
</div>
</div>
</div>
))}
</GridOrCarousel>
</ScrollReveal>
</div>
</section>
);
};
export default TestimonialRatingCards;

View File

@@ -1,74 +0,0 @@
import { Star } from "lucide-react";
import TextAnimation from "@/components/ui/TextAnimation";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
import ScrollReveal from "@/components/ui/ScrollReveal";
import { cls } from "@/lib/utils";
type Avatar = {
name: string;
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
const TestimonialTrustCard = ({
quote,
rating,
author,
avatars,
}: {
quote: string;
rating: number;
author: string;
avatars: Avatar[];
}) => {
const visibleAvatars = avatars.slice(0, 6);
const remainingCount = avatars.length - visibleAvatars.length;
return (
<section aria-label="Testimonials section" className="py-20">
<div className="flex flex-col items-center gap-5 w-content-width mx-auto">
<ScrollReveal variant="fade-blur" className="flex gap-1.5">
{Array.from({ length: 5 }).map((_, index) => (
<Star
key={index}
className={cls("size-6 text-accent", index < rating ? "fill-accent" : "fill-transparent")}
strokeWidth={1.5}
/>
))}
</ScrollReveal>
<TextAnimation
text={quote}
variant="fade-blur"
gradientText={false}
tag="p"
className="md:max-w-8/10 text-5xl 2xl:text-6xl leading-[1.15] font-semibold text-center text-balance"
/>
<ScrollReveal variant="fade-blur" delay={0.1} className="md:max-w-7/10 text-lg md:text-xl leading-snug text-center text-balance">
<p>{author}</p>
</ScrollReveal>
<ScrollReveal variant="fade-blur" delay={0.2} className="flex items-center justify-center mt-1">
{visibleAvatars.map((avatar, index) => (
<div
key={avatar.name}
className={cls("relative size-12 md:size-16 overflow-hidden rounded-full border-2 border-background", index > 0 && "-ml-5")}
style={{ zIndex: visibleAvatars.length - index }}
>
<ImageOrVideo imageSrc={avatar.imageSrc} videoSrc={avatar.videoSrc} />
</div>
))}
{remainingCount > 0 && (
<div
className="flex items-center justify-center size-12 md:size-16 -ml-5 rounded-full border-2 border-background card"
style={{ zIndex: 0 }}
>
<span className="text-sm md:text-base font-semibold">+{remainingCount}</span>
</div>
)}
</ScrollReveal>
</div>
</section>
);
};
export default TestimonialTrustCard;

View File

@@ -1,15 +0,0 @@
import type { ReactNode } from "react";
import { cls } from "@/lib/utils";
interface CardProps {
children: ReactNode;
className?: string;
}
const Card = ({ children, className = "" }: CardProps) => (
<div className={cls("p-6 xl:p-7 2xl:p-8 card rounded", className)}>
{children}
</div>
);
export default Card;

View File

@@ -1,32 +0,0 @@
"use client";
import { useStyle } from "@/components/ui/useStyle";
import CornerGlowBackground from "@/components/ui/CornerGlowBackground";
import GradientBarsBackground from "@/components/ui/GradientBarsBackground";
import HorizonGlowBackground from "@/components/ui/HorizonGlowBackground";
import LightRaysCenterBackground from "@/components/ui/LightRaysCenterBackground";
import LightRaysCornerBackground from "@/components/ui/LightRaysCornerBackground";
import RadialGradientBackground from "@/components/ui/RadialGradientBackground";
const HeroBackgroundSlot = () => {
const { heroBackground } = useStyle();
switch (heroBackground) {
case "cornerGlow":
return <CornerGlowBackground position="absolute" />;
case "gradientBars":
return <GradientBarsBackground position="absolute" />;
case "horizonGlow":
return <HorizonGlowBackground position="absolute" />;
case "lightRaysCenter":
return <LightRaysCenterBackground position="absolute" />;
case "lightRaysCorner":
return <LightRaysCornerBackground position="absolute" />;
case "radialGradient":
return <RadialGradientBackground position="absolute" />;
default:
return null;
}
};
export default HeroBackgroundSlot;

View File

@@ -1,37 +0,0 @@
import type { LucideIcon } from "lucide-react";
import { resolveIcon } from "@/utils/resolve-icon";
const OrbitingIcons = ({ centerIcon, items }: { centerIcon: string | LucideIcon; items: (string | LucideIcon)[] }) => {
const CenterIcon = resolveIcon(centerIcon);
return (
<div
className="relative flex items-center justify-center h-full overflow-hidden"
style={{ perspective: "2000px", maskImage: "linear-gradient(to bottom, transparent, black 10%, black 90%, transparent), linear-gradient(to right, transparent, black 10%, black 90%, transparent)", maskComposite: "intersect" }}
>
<div className="flex items-center justify-center w-full h-full" style={{ transform: "rotateY(20deg) rotateX(20deg) rotateZ(-20deg)" }}>
<div className="absolute size-60 opacity-85 border border-background-accent shadow rounded-full" />
<div className="absolute size-80 opacity-75 border border-background-accent shadow rounded-full" />
<div className="absolute size-100 opacity-65 border border-background-accent shadow rounded-full" />
<div className="absolute flex items-center justify-center size-40 border border-background-accent shadow rounded-full">
<div className="flex items-center justify-center size-20 primary-button rounded-full">
<CenterIcon className="size-10 text-primary-cta-text" strokeWidth={1.25} />
</div>
{items.map((iconInput, i) => {
const Icon = resolveIcon(iconInput);
return (
<div
key={i}
className="absolute flex items-center justify-center size-10 rounded shadow card -ml-5 -mt-5"
style={{ top: "50%", left: "50%", animation: "orbit 12s linear infinite", "--initial-position": `${(360 / items.length) * i}deg`, "--translate-position": "160px" } as React.CSSProperties}
>
<Icon className="size-4" strokeWidth={1.5} />
</div>
);
})}
</div>
</div>
</div>
);
};
export default OrbitingIcons;

View File

@@ -1,32 +0,0 @@
"use client";
import { useStyle } from "@/components/ui/useStyle";
import AuroraBackground from "@/components/ui/AuroraBackground";
import CornerGlowBackground from "@/components/ui/CornerGlowBackground";
import FloatingGradientBackground from "@/components/ui/FloatingGradientBackground";
import GridLinesBackground from "@/components/ui/GridLinesBackground";
import NoiseBackground from "@/components/ui/NoiseBackground";
import NoiseGradientBackground from "@/components/ui/NoiseGradientBackground";
const SiteBackgroundSlot = () => {
const { siteBackground } = useStyle();
switch (siteBackground) {
case "aurora":
return <AuroraBackground position="fixed" />;
case "cornerGlow":
return <CornerGlowBackground position="fixed" />;
case "floatingGradient":
return <FloatingGradientBackground position="fixed" />;
case "gridLines":
return <GridLinesBackground position="fixed" />;
case "noise":
return <NoiseBackground position="fixed" />;
case "noiseGradient":
return <NoiseGradientBackground position="fixed" />;
default:
return null;
}
};
export default SiteBackgroundSlot;

View File

@@ -1,40 +0,0 @@
import { useEffect, useState } from "react";
import { fetchProducts, type Product } from "@/lib/api/product";
const useProducts = () => {
const [products, setProducts] = useState<Product[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let isMounted = true;
const loadProducts = async () => {
try {
const data = await fetchProducts();
if (isMounted) {
setProducts(data);
}
} catch (err) {
if (isMounted) {
setError(err instanceof Error ? err : new Error("Failed to fetch products"));
}
} finally {
if (isMounted) {
setIsLoading(false);
}
}
};
loadProducts();
return () => {
isMounted = false;
};
}, []);
return { products, isLoading, error };
};
export default useProducts;
export type { Product };

View File

@@ -19,7 +19,7 @@ export default function HomePage() {
avatarsSrc={[
"http://img.b2bpic.net/free-photo/hotel-concierge-serving-coffee-client_482257-91383.jpg", "http://img.b2bpic.net/free-photo/smiling-woman-sitting-elegant-restaurant-with-tablet-talking-phone_1157-2100.jpg", "http://img.b2bpic.net/free-photo/smiling-tender-parisian-girl-stylish-outfit-sends-air-kiss-portrait-young-woman-with-expressive-look_197531-12004.jpg", "http://img.b2bpic.net/free-photo/front-view-valet-suit-working_23-2150274536.jpg"]}
avatarText="Our dedicated team of hosts"
title="Experience Unrivaled Luxury at The Grand Hotel"
title="Unrivaled Luxury at The Grand Hotel"
description="Indulge in sophisticated elegance, impeccable service, and breathtaking views. Your unforgettable escape begins here."
primaryButton={{
text: "Book Your Stay", href: "#contact"}}