forked from emre/www_projectmycelium_com
- Replaced MagicCard with new DarkCard component across apple-cards-carousel and HomeGlobe - Updated card styling and layout in apple-cards-carousel to work with DarkCard - Removed gradient and border styling from globe stats cards - Adjusted padding and margin values to maintain consistent appearance - Fixed spacing and alignment of chevron icon in cards
200 lines
6.0 KiB
TypeScript
200 lines
6.0 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";
|
|
import { DarkCard } from "./cards";
|
|
|
|
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" });
|
|
}
|
|
};
|
|
|
|
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-6 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}>
|
|
<DarkCard className="p-0 rounded-3xl">
|
|
<motion.div
|
|
layoutId={layout ? `card-${card.title}` : undefined}
|
|
className="relative z-10 flex h-104 w-80 flex-col justify-between overflow-hidden rounded-3xl p-8 md:h-120 md:w-96"
|
|
>
|
|
<div className="flex h-2/5 flex-col justify-center py-6 px-4">
|
|
<motion.p
|
|
layoutId={layout ? `category-${card.category}` : undefined}
|
|
className="text-left font-sans font-semibold text-cyan-500 uppercase tracking-wider text-xs md:text-sm"
|
|
>
|
|
{card.category}
|
|
</motion.p>
|
|
<motion.p
|
|
layoutId={layout ? `title-${card.title}` : undefined}
|
|
className="mt-1 max-w-xs text-left font-sans text-xl font-semibold text-white [text-wrap:balance] lg:text-2xl"
|
|
>
|
|
{card.title}
|
|
</motion.p>
|
|
<div className="mt-2 flex w-full flex-row items-center justify-between md:mt-4">
|
|
<motion.p className="max-w-xs text-left font-sans lg:text-xl text-lg text-neutral-300 lg:leading-normal leading-tight">
|
|
{card.description}
|
|
</motion.p>
|
|
<div className="flex h-6 w-6 ml-2 shrink-0 items-center justify-center rounded-full bg-[#212121] text-[#858585] transition-colors duration-200 hover:bg-[#262626] hover:text-white md:h-8 md:w-8">
|
|
<IconChevronRight className="h-4 w-4 md:h-6 md:w-6 " />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="relative flex h-3/5 w-full items-end justify-end bg-transparent">
|
|
<img
|
|
src={card.src}
|
|
alt={card.title}
|
|
className="h-full w-full origin-bottom-right scale-75 object-contain mb-10"
|
|
/>
|
|
</div>
|
|
</motion.div>
|
|
</DarkCard>
|
|
</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}
|
|
/>
|
|
);
|
|
};
|