jurmy24's picture
feat: add viewer code
72f0edb
raw
history blame
6.06 kB
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;