feat: update card carousel to use direct background images

- Replaced BlurImage component with direct background image styling for better performance
- Added new 'bg' property to Card type to support background image imports
- Imported slider background images for each card category
- Removed redundant background color class and BlurImage component
- Updated card styling to maintain visual consistency with background images
This commit is contained in:
2025-10-27 13:29:05 +01:00
parent b543aebce9
commit a52e838d17
9 changed files with 53 additions and 120 deletions

View File

@@ -1,19 +1,16 @@
"use client"; "use client";
import React, { import React, {
useEffect, useEffect,
useRef,
useState, useState,
createContext,
useContext,
} from "react"; } from "react";
import { import {
IconArrowNarrowLeft, IconArrowNarrowLeft,
IconArrowNarrowRight, IconArrowNarrowRight,
IconX, IconChevronRight,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { AnimatePresence, motion } from "motion/react"; import { Link } from "react-router-dom";
import { useOutsideClick } from "@/hooks/use-outside-click"; import { motion } from "motion/react";
interface CarouselProps { interface CarouselProps {
items: JSX.Element[]; items: JSX.Element[];
@@ -24,22 +21,15 @@ type Card = {
src: string; src: string;
title: string; title: string;
category: string; category: string;
content: React.ReactNode; description: string;
link: string;
bg: any;
}; };
export const CarouselContext = createContext<{
onCardClose: (index: number) => void;
currentIndex: number;
}>({
onCardClose: () => {},
currentIndex: 0,
});
export const Carousel = ({ items, initialScroll = 0 }: CarouselProps) => { export const Carousel = ({ items, initialScroll = 0 }: CarouselProps) => {
const carouselRef = React.useRef<HTMLDivElement>(null); const carouselRef = React.useRef<HTMLDivElement>(null);
const [canScrollLeft, setCanScrollLeft] = React.useState(false); const [canScrollLeft, setCanScrollLeft] = React.useState(false);
const [canScrollRight, setCanScrollRight] = React.useState(true); const [canScrollRight, setCanScrollRight] = React.useState(true);
const [currentIndex, setCurrentIndex] = useState(0);
useEffect(() => { useEffect(() => {
if (carouselRef.current) { if (carouselRef.current) {
@@ -68,27 +58,12 @@ export const Carousel = ({ items, initialScroll = 0 }: CarouselProps) => {
} }
}; };
const handleCardClose = (index: number) => {
if (carouselRef.current) {
const cardWidth = isMobile() ? 230 : 384; // (md:w-96)
const gap = isMobile() ? 4 : 8;
const scrollPosition = (cardWidth + gap) * (index + 1);
carouselRef.current.scrollTo({
left: scrollPosition,
behavior: "smooth",
});
setCurrentIndex(index);
}
};
const isMobile = () => { const isMobile = () => {
return window && window.innerWidth < 768; return window && window.innerWidth < 768;
}; };
return ( return (
<CarouselContext.Provider
value={{ onCardClose: handleCardClose, currentIndex }}
>
<div className="relative w-full"> <div className="relative w-full">
<div <div
className="flex w-full overflow-x-scroll overscroll-x-auto scroll-smooth py-10 [scrollbar-width:none] md:py-20" className="flex w-full overflow-x-scroll overscroll-x-auto scroll-smooth py-10 [scrollbar-width:none] md:py-20"
@@ -147,100 +122,30 @@ export const Carousel = ({ items, initialScroll = 0 }: CarouselProps) => {
</button> </button>
</div> </div>
</div> </div>
</CarouselContext.Provider>
); );
}; };
export const Card = ({ export const Card = ({
card, card,
index,
layout = false, layout = false,
}: { }: {
card: Card; card: Card;
index: number;
layout?: boolean; layout?: boolean;
}) => { }) => {
const [open, setOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const { onCardClose } = useContext(CarouselContext);
useEffect(() => {
function onKeyDown(event: KeyboardEvent) {
if (event.key === "Escape") {
handleClose();
}
}
if (open) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "auto";
}
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
}, [open]);
useOutsideClick(containerRef, () => handleClose());
const handleOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
onCardClose(index);
};
return ( return (
<> <Link to={card.link}>
<AnimatePresence> <motion.div
{open && (
<div className="fixed inset-0 z-50 h-screen overflow-auto">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 h-full w-full bg-black/80 backdrop-blur-lg"
/>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
ref={containerRef}
layoutId={layout ? `card-${card.title}` : undefined}
className="relative z-[60] mx-auto my-10 h-fit max-w-5xl rounded-3xl bg-neutral-900 p-4 font-sans md:p-10"
>
<button
className="sticky top-4 right-0 ml-auto flex h-8 w-8 items-center justify-center rounded-full bg-black dark:bg-white"
onClick={handleClose}
>
<IconX className="h-6 w-6 text-neutral-100 dark:text-neutral-900" />
</button>
<motion.p
layoutId={layout ? `category-${card.title}` : undefined}
className="text-base font-medium text-black dark:text-white"
>
{card.category}
</motion.p>
<motion.p
layoutId={layout ? `title-${card.title}` : undefined}
className="mt-4 text-2xl font-semibold text-neutral-700 md:text-5xl dark:text-white"
>
{card.title}
</motion.p>
<div className="py-10">{card.content}</div>
</motion.div>
</div>
)}
</AnimatePresence>
<motion.button
layoutId={layout ? `card-${card.title}` : undefined} layoutId={layout ? `card-${card.title}` : undefined}
onClick={handleOpen} className="relative z-10 flex h-60 w-56 flex-col items-start justify-start overflow-hidden rounded-3xl md:h-120 md:w-96 hover:scale-105 transition-transform duration-200"
className="relative z-10 flex h-60 w-56 flex-col items-start justify-start overflow-hidden rounded-3xl bg-neutral-900 md:h-120 md:w-96" style={{
backgroundImage: `url(${card.bg})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
> >
<div className="pointer-events-none absolute inset-x-0 top-0 z-30 h-full bg-gradient-to-b from-black/50 via-transparent to-transparent" /> <div className="pointer-events-none absolute inset-x-0 top-0 z-30 h-full bg-gradient-to-b from-black/50 via-transparent to-transparent" />
<div className="relative z-40 p-8"> <div className="relative z-40 p-8 w-full">
<motion.p <motion.p
layoutId={layout ? `category-${card.category}` : undefined} layoutId={layout ? `category-${card.category}` : undefined}
className="text-left font-sans text-sm font-medium text-white md:text-base" className="text-left font-sans text-sm font-medium text-white md:text-base"
@@ -253,14 +158,17 @@ export const Card = ({
> >
{card.title} {card.title}
</motion.p> </motion.p>
<div className="flex flex-row justify-between items-center w-full mt-4">
<motion.p className="max-w-xs text-left font-sans text-sm text-neutral-300">
{card.description}
</motion.p>
<div className="h-8 w-8 bg-[#212121] rounded-full flex items-center justify-center text-[#858585] shrink-0 hover:bg-[#262626] hover:text-white active:bg-[#262626] active:text-white transition-colors duration-200">
<IconChevronRight className="h-6 w-6" />
</div>
</div>
</div> </div>
<BlurImage </motion.div>
src={card.src} </Link>
alt={card.title}
className="absolute inset-0 z-10 h-full w-full object-cover"
/>
</motion.button>
</>
); );
}; };

BIN
src/images/agent1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
src/images/slider/gpu1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -10,7 +10,7 @@ export function HomeSlider() {
)); ));
return ( return (
<div className="w-full h-full py-20 bg-black"> <div className="w-full h-full py-20 bg-[#0b0b0b]">
<div className="max-w-7xl mx-auto pl-4"> <div className="max-w-7xl mx-auto pl-4">
<H3 className="text-left text-white"> <H3 className="text-left text-white">
Discover the Mycelium Ecosystem Discover the Mycelium Ecosystem
@@ -43,42 +43,67 @@ const DummyContent = () => {
); );
}; };
import networkImage from "@/images/slider/network1.jpg";
import agentImage from "@/images/slider/agent1.jpg";
import cloudImage from "@/images/slider/cloud1.jpg";
import gpuImage from "@/images/slider/gpu1.jpg";
import computeImage from "@/images/slider/compute1.jpg";
import storageImage from "@/images/slider/storage1.jpg";
const data = [ const data = [
{ {
category: "DePIN", category: "DePIN",
title: "Mycelium Network", title: "Mycelium Network",
description: "A decentralized network for distributed computing.",
src: "/images/gallery/9.webp", src: "/images/gallery/9.webp",
content: <DummyContent />, content: <DummyContent />,
bg: networkImage,
link: "/network",
}, },
{ {
category: "AI Agent", category: "AI Agent",
title: "Mycelium Agent", title: "Mycelium Agent",
description: "An intelligent agent for task automation.",
src: "/images/gallery/2.webp", src: "/images/gallery/2.webp",
content: <DummyContent />, content: <DummyContent />,
bg: agentImage,
link: "/agent",
}, },
{ {
category: "Cloud", category: "Cloud",
title: "Mycelium Cloud", title: "Mycelium Cloud",
description: "Decentralized cloud storage and services.",
src: "/images/gallery/3.webp", src: "/images/gallery/3.webp",
content: <DummyContent />, content: <DummyContent />,
bg: cloudImage,
link: "/cloud",
}, },
{ {
category: "GPU", category: "GPU",
title: "Mycelium GPU", title: "Mycelium GPU",
description: "Access to a global network of GPUs.",
src: "/images/gallery/4.webp", src: "/images/gallery/4.webp",
content: <DummyContent />, content: <DummyContent />,
bg: gpuImage,
link: "/gpu",
}, },
{ {
category: "Compute", category: "Compute",
title: "Mycelium Compute", title: "Mycelium Compute",
description: "Run computations on a distributed network.",
src: "/images/gallery/5.webp", src: "/images/gallery/5.webp",
content: <DummyContent />, content: <DummyContent />,
bg: computeImage,
link: "/compute",
}, },
{ {
category: "Storage", category: "Storage",
title: "Mycelium Storage", title: "Mycelium Storage",
description: "Secure and decentralized data storage.",
src: "/images/gallery/6.webp", src: "/images/gallery/6.webp",
content: <DummyContent />, content: <DummyContent />,
bg: storageImage,
link: "/storage",
}, },
]; ];