Vitron-Front/app/components/HorizontalCollectionsList.tsx
2026-04-29 01:44:16 +03:30

279 lines
10 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Link } from "@remix-run/react";
import { useRef, useState, useEffect, useCallback } from "react";
import { motion } from "framer-motion";
import { ChevronLeft, ChevronRight, Package, ShoppingBag } from "lucide-react";
import { Skeleton } from "./ui/skeleton";
import { ErrorState } from "./UiProvider";
interface HorizontalCollection {
id?: string;
title?: string;
description?: string | null;
coverImageUrl?: string | null;
productCount?: number;
}
const HorizontalCollectionsList = ({
collections,
isLoading,
isError,
refetch,
showViewAllButton = false,
}: {
collections: HorizontalCollection[];
isLoading: boolean;
isError: boolean;
refetch: () => void;
showViewAllButton?: boolean;
}) => {
const scrollContainerRef = useRef<HTMLDivElement>(null);
const [showLeftButton, setShowLeftButton] = useState(false);
const [showRightButton, setShowRightButton] = useState(false);
const [currentPage, setCurrentPage] = useState(0);
const totalPages = collections.length;
// Helper function to calculate card width consistently
const getCardWidth = useCallback(() => {
// Same calculation as scrollToNext/scrollToPrev: 85vw (max 300px) + 16px gap
return Math.min(window.innerWidth * 0.85, 300) + 16;
}, []);
const checkScrollButtons = useCallback(() => {
if (!scrollContainerRef.current) return;
const { scrollLeft, scrollWidth, clientWidth } = scrollContainerRef.current;
// Calculate max scroll position
const maxScroll = scrollWidth - clientWidth;
// For RTL, scrollLeft might be negative or start from max
// Check if we can scroll in each direction
const canScrollRight = Math.abs(scrollLeft) > 100;
const canScrollLeft = Math.abs(scrollLeft) < maxScroll - 100;
setShowRightButton(canScrollRight);
setShowLeftButton(canScrollLeft);
// Calculate card width using the same logic as scroll functions
const cardWidth = getCardWidth();
// Calculate current page based on scroll position
const page = Math.round(Math.abs(scrollLeft) / cardWidth);
setCurrentPage(Math.min(page, collections.length - 1));
}, [collections.length, getCardWidth]);
useEffect(() => {
// Small delay to ensure content is rendered
const timer = setTimeout(() => {
checkScrollButtons();
}, 100);
const container = scrollContainerRef.current;
if (container) {
container.addEventListener("scroll", checkScrollButtons);
window.addEventListener("resize", checkScrollButtons);
return () => {
container.removeEventListener("scroll", checkScrollButtons);
window.removeEventListener("resize", checkScrollButtons);
};
}
return () => clearTimeout(timer);
}, [collections, checkScrollButtons]);
const scrollToNext = () => {
if (!scrollContainerRef.current) return;
const container = scrollContainerRef.current;
const cardWidth = getCardWidth();
container.scrollBy({ left: cardWidth, behavior: "smooth" });
};
const scrollToPrev = () => {
if (!scrollContainerRef.current) return;
const container = scrollContainerRef.current;
const cardWidth = getCardWidth();
container.scrollBy({ left: -cardWidth, behavior: "smooth" });
};
if (isLoading) {
return <HorizontalSkeleton />;
}
if (isError) {
return (
<div className="px-4">
<ErrorState onRetry={refetch} />
</div>
);
}
if (collections.length === 0) {
return null;
}
return (
<div className="w-full flex flex-col gap-3">
{/* Scroll Container */}
<div
ref={scrollContainerRef}
className="w-full overflow-x-auto scrollbar-hide snap-x snap-mandatory px-4"
>
<div className="flex" style={{ width: "max-content" }}>
{collections.map((collection, index) => (
<Link
key={collection.id || index}
to={`/collection/${collection.id}`}
className="flex flex-col pr-2 md:pr-4 snap-start group"
style={{ width: "calc(85vw)", maxWidth: "300px" }}
>
<div className="flex flex-col gap-3 p-3 bg-WHITE2 rounded-2xl border border-inner-border hover:border-gray-300 transition-all duration-300 hover:shadow-lg h-full">
{/* Collection Image */}
<div className="relative overflow-hidden rounded-xl w-full h-64 bg-gray-100">
{collection.coverImageUrl ? (
<img
src={collection.coverImageUrl}
alt={collection.title || "Collection"}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
/>
) : (
<div className="w-full h-full flex items-center justify-center bg-WHITE2">
<Package size={48} className="text-GRAY" />
</div>
)}
{/* Product Count Badge */}
{collection.productCount !== undefined && (
<div className="absolute bottom-2 left-2">
<div className="flex items-center gap-1.5 bg-white/95 backdrop-blur-sm px-2.5 py-1 rounded-full shadow-md">
<ShoppingBag size={12} className="text-black" />
<span className="text-R12 text-black font-bold">
{collection.productCount} محصول
</span>
</div>
</div>
)}
</div>
{/* Collection Info */}
<div className="flex flex-col gap-1.5">
<h3 className="text-B14 font-bold text-black line-clamp-2">
{collection.title || "بدون عنوان"}
</h3>
{collection.description && (
<p className="text-R12 text-GRAY line-clamp-2">
{collection.description}
</p>
)}
</div>
</div>
</Link>
))}
{/* View All Button Card */}
{showViewAllButton && (
<Link
to="/collections"
className="flex flex-col pr-2 md:pr-4 snap-start group"
style={{ width: "calc(85vw)", maxWidth: "300px" }}
>
<div className="flex flex-col items-center justify-center gap-3 p-3 bg-gradient-to-br from-black to-gray-800 rounded-2xl border border-inner-border hover:border-gray-600 transition-all duration-300 hover:shadow-lg h-full min-h-[304px]">
<div className="flex flex-col items-center justify-center gap-3">
<div className="w-16 h-16 rounded-full bg-white/10 flex items-center justify-center group-hover:scale-110 transition-transform duration-300">
<Package size={32} className="text-white" />
</div>
<div className="flex flex-col items-center gap-2">
<h3 className="text-B16 font-bold text-white text-center">
مشاهده همه کالکشنها
</h3>
<p className="text-R12 text-white/70 text-center">
کاوش در کالکشنهای بیشتر
</p>
</div>
<div className="flex items-center gap-2 mt-2 text-white group-hover:gap-3 transition-all duration-300">
<span className="text-R14 font-bold">مشاهده همه</span>
<ChevronLeft
size={18}
className="group-hover:translate-x-[-4px] transition-transform duration-300"
/>
</div>
</div>
</div>
</Link>
)}
</div>
</div>
{/* Navigation Buttons Row */}
{collections.length > 1 && (
<div className="flex items-center justify-center gap-3 px-4">
<motion.button
whileHover={!showRightButton ? {} : { scale: 1.1 }}
whileTap={!showRightButton ? {} : { scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 25 }}
onClick={scrollToNext}
disabled={!showRightButton}
aria-label="Next collections"
>
<ChevronRight
size={20}
className={showRightButton ? "text-BLACK" : "text-GRAY"}
/>
</motion.button>
{/* Dots indicator */}
<div className="flex gap-1.5">
{Array.from({ length: totalPages }).map((_, i) => (
<div
key={i}
className={`h-1.5 rounded-full transition-all duration-300 ${
i === currentPage ? "w-6 bg-BLACK" : "w-1.5 bg-GRAY3"
}`}
/>
))}
</div>
<motion.button
whileHover={!showLeftButton ? {} : { scale: 1.1 }}
whileTap={!showLeftButton ? {} : { scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 25 }}
onClick={scrollToPrev}
disabled={!showLeftButton}
aria-label="Previous collections"
>
<ChevronLeft
size={20}
className={showLeftButton ? "text-BLACK" : "text-GRAY"}
/>
</motion.button>
</div>
)}
</div>
);
};
const HorizontalSkeleton = () => {
return (
<div className="w-full overflow-x-auto scrollbar-hide">
<div className="flex" style={{ width: "max-content" }}>
{Array(4)
.fill(1)
.map((_, index) => (
<div
key={index}
className="flex flex-col pr-2 md:pr-4 snap-start group"
style={{ width: "calc(85vw)", maxWidth: "300px" }}
>
<div className="flex flex-col gap-3 p-3 bg-WHITE2 rounded-2xl border border-inner-border">
<Skeleton className="w-full h-64 rounded-xl" />
<Skeleton className="w-3/4 h-4" />
<Skeleton className="w-full h-3" />
</div>
</div>
))}
</div>
</div>
);
};
export default HorizontalCollectionsList;