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
@@ -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
|
After Width: | Height: | Size: 27 KiB |
BIN
src/images/slider/agent1.jpg
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
src/images/slider/cloud1.jpg
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
src/images/slider/compute1.jpg
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
src/images/slider/gpu1.jpg
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
src/images/slider/network1.jpg
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
src/images/slider/storage1.jpg
Normal file
|
After Width: | Height: | Size: 32 KiB |
@@ -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",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||