Files
www_projectmycelium_com/src/components/ui/apple-cards-carousel.tsx
sasha-astiadi a52e838d17 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
2025-10-27 13:29:05 +01:00

202 lines
5.9 KiB
TypeScript

"use client";
import React, {
useEffect,
useState,
} from "react";
import {
IconArrowNarrowLeft,
IconArrowNarrowRight,
IconChevronRight,
} from "@tabler/icons-react";
import { cn } from "@/lib/utils";
import { Link } from "react-router-dom";
import { motion } from "motion/react";
interface CarouselProps {
items: JSX.Element[];
initialScroll?: number;
}
type Card = {
src: string;
title: string;
category: string;
description: string;
link: string;
bg: any;
};
export const Carousel = ({ items, initialScroll = 0 }: CarouselProps) => {
const carouselRef = React.useRef<HTMLDivElement>(null);
const [canScrollLeft, setCanScrollLeft] = React.useState(false);
const [canScrollRight, setCanScrollRight] = React.useState(true);
useEffect(() => {
if (carouselRef.current) {
carouselRef.current.scrollLeft = initialScroll;
checkScrollability();
}
}, [initialScroll]);
const checkScrollability = () => {
if (carouselRef.current) {
const { scrollLeft, scrollWidth, clientWidth } = carouselRef.current;
setCanScrollLeft(scrollLeft > 0);
setCanScrollRight(scrollLeft < scrollWidth - clientWidth);
}
};
const scrollLeft = () => {
if (carouselRef.current) {
carouselRef.current.scrollBy({ left: -300, behavior: "smooth" });
}
};
const scrollRight = () => {
if (carouselRef.current) {
carouselRef.current.scrollBy({ left: 300, behavior: "smooth" });
}
};
const isMobile = () => {
return window && window.innerWidth < 768;
};
return (
<div className="relative w-full">
<div
className="flex w-full overflow-x-scroll overscroll-x-auto scroll-smooth py-10 [scrollbar-width:none] md:py-20"
ref={carouselRef}
onScroll={checkScrollability}
>
<div
className={cn(
"absolute right-0 z-[1000] h-auto w-[5%] overflow-hidden bg-gradient-to-l",
)}
></div>
<div
className={cn(
"flex flex-row justify-start gap-4 pl-4",
"mx-auto max-w-7xl", // remove max-w-4xl if you want the carousel to span the full width of its container
)}
>
{items.map((item, index) => (
<motion.div
initial={{
opacity: 0,
y: 20,
}}
animate={{
opacity: 1,
y: 0,
transition: {
duration: 0.5,
delay: 0.2 * index,
ease: "easeOut",
},
}}
key={"card" + index}
className="rounded-3xl last:pr-[5%] md:last:pr-[33%]"
>
{item}
</motion.div>
))}
</div>
</div>
<div className="mr-10 flex justify-end gap-2">
<button
className="relative z-40 flex h-10 w-10 items-center justify-center rounded-full bg-neutral-800 disabled:opacity-50"
onClick={scrollLeft}
disabled={!canScrollLeft}
>
<IconArrowNarrowLeft className="h-6 w-6 text-white" />
</button>
<button
className="relative z-40 flex h-10 w-10 items-center justify-center rounded-full bg-neutral-800 disabled:opacity-50"
onClick={scrollRight}
disabled={!canScrollRight}
>
<IconArrowNarrowRight className="h-6 w-6 text-white" />
</button>
</div>
</div>
);
};
export const Card = ({
card,
layout = false,
}: {
card: Card;
layout?: boolean;
}) => {
return (
<Link to={card.link}>
<motion.div
layoutId={layout ? `card-${card.title}` : undefined}
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"
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="relative z-40 p-8 w-full">
<motion.p
layoutId={layout ? `category-${card.category}` : undefined}
className="text-left font-sans text-sm font-medium text-white md:text-base"
>
{card.category}
</motion.p>
<motion.p
layoutId={layout ? `title-${card.title}` : undefined}
className="mt-2 max-w-xs text-left font-sans text-xl font-semibold [text-wrap:balance] text-white md:text-3xl"
>
{card.title}
</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>
</motion.div>
</Link>
);
};
export const BlurImage = ({
src,
className,
width,
height,
alt,
...rest
}: React.ImgHTMLAttributes<HTMLImageElement>) => {
const [isLoading, setLoading] = useState(true);
return (
<img
className={cn(
"h-full w-full transition duration-300",
isLoading ? "blur-sm" : "blur-0",
className,
)}
onLoad={() => setLoading(false)}
src={src as string}
width={width}
height={height}
loading="lazy"
decoding="async"
alt={alt ? alt : "Background of a beautiful view"}
{...rest}
/>
);
};