Spaces:
Running
Running
File size: 6,061 Bytes
72f0edb |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 |
import React, { useRef, useState, useEffect } from "react";
import { ContentItem } from "../lib/types";
import { useNavigate } from "react-router-dom";
import { ChevronLeft, ChevronRight, Star } from "lucide-react";
import { cn } from "@/lib/utils";
import { useIsMobile } from "@/hooks/use-mobile";
import { useMyList } from "@/hooks/use-my-list";
import { supabase } from "@/integrations/supabase/client";
import { useUrdf } from "@/hooks/useUrdf";
interface CarouselProps {
title: string;
items: ContentItem[];
className?: string;
}
const Carousel: React.FC<CarouselProps> = ({ title, items, className }) => {
const carouselRef = useRef<HTMLDivElement>(null);
const navigate = useNavigate();
const isMobile = useIsMobile();
const { myList, addToMyList, removeFromMyList, isInMyList } = useMyList();
const [imageUrls, setImageUrls] = useState<Record<string, string>>({});
const { urdfProcessor, processUrdfFiles } = useUrdf();
// Fetch image URLs for all items
useEffect(() => {
const fetchImages = async () => {
const urls: Record<string, string> = {};
for (const item of items) {
if (
item.imageUrl &&
!item.imageUrl.startsWith("http") &&
!item.imageUrl.startsWith("data:")
) {
try {
// Get public URL for the image
const { data, error } = await supabase.storage
.from("urdf-images")
.createSignedUrl(item.imageUrl, 3600); // 1 hour expiry
if (data && !error) {
urls[item.id] = data.signedUrl;
} else {
// Fallback to placeholder
urls[item.id] = "/placeholder.svg";
}
} catch (error) {
console.error(`Error fetching image for ${item.id}:`, error);
urls[item.id] = "/placeholder.svg";
}
} else {
// For URLs that are already full URLs or data URIs
urls[item.id] = item.imageUrl || "/placeholder.svg";
}
}
setImageUrls(urls);
};
if (items.length > 0) {
fetchImages();
}
}, [items]);
const handleScrollLeft = () => {
if (carouselRef.current) {
const scrollAmount = carouselRef.current.offsetWidth / 4.1;
carouselRef.current.scrollBy({ left: -scrollAmount, behavior: "smooth" });
}
};
const handleScrollRight = () => {
if (carouselRef.current) {
const scrollAmount = carouselRef.current.offsetWidth / 4.1;
carouselRef.current.scrollBy({ left: scrollAmount, behavior: "smooth" });
}
};
const handleItemClick = async (item: ContentItem) => {
// Only navigate to the content detail page, let the detail page handle loading
navigate(`/content/${item.id}`);
// We've removed the URDF loading here to prevent duplication with ContentDetail's loading
};
const handleStarClick = (e: React.MouseEvent, item: ContentItem) => {
e.stopPropagation(); // Prevent navigation on star click
if (isInMyList(item.id)) {
removeFromMyList(item.id);
} else {
addToMyList(item);
}
};
// If no items, don't render the carousel
if (items.length === 0) return null;
return (
<div className={cn("my-5", className)}>
<div className="relative group">
<div
ref={carouselRef}
className="carousel-container flex items-center gap-2 overflow-x-auto py-2 px-4 scroll-smooth"
>
{items.map((item) => (
<div
key={item.id}
className="carousel-item flex-shrink-0 cursor-pointer relative hover:z-10"
style={{
width: "calc(100% / 4.1)",
}}
onClick={() => handleItemClick(item)}
>
<div className="relative rounded-md w-full h-full group/item">
{/* Image container with darker overlay on hover */}
<div className="rounded-md overflow-hidden w-full h-full bg-black">
<img
src={imageUrls[item.id] || "/placeholder.svg"}
alt={item.title}
className="w-full h-full object-cover rounded-md transition-all duration-300 group-hover/item:brightness-90"
style={{
aspectRatio: "0.8",
}}
/>
</div>
{/* Star button */}
<div
className="absolute top-4 right-4 p-2 z-20 invisible group-hover/item:visible"
onClick={(e) => handleStarClick(e, item)}
>
<Star
size={24}
className={cn(
"transition-colors duration-300",
isInMyList(item.id)
? "fill-yellow-400 text-yellow-400"
: "text-white hover:text-yellow-400"
)}
/>
</div>
{/* Title overlay - visible on hover without gradient */}
<div className="absolute bottom-4 left-4 opacity-0 group-hover/item:opacity-100 transition-opacity duration-300">
<h3 className="text-gray-400 text-6xl font-bold drop-shadow-xl">
{item.title}
</h3>
</div>
</div>
</div>
))}
</div>
{/* Scroll buttons - changed to always visible */}
<button
onClick={handleScrollLeft}
className="absolute left-0 top-1/2 -translate-y-1/2 bg-black text-white p-1 rounded-full z-40"
aria-label="Scroll left"
>
<ChevronLeft size={24} />
</button>
<button
onClick={handleScrollRight}
className="absolute right-0 top-1/2 -translate-y-1/2 bg-black text-white p-1 rounded-full z-40"
aria-label="Scroll right"
>
<ChevronRight size={24} />
</button>
</div>
</div>
);
};
export default Carousel;
|