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

260 lines
9.8 KiB
TypeScript
Raw 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 { useEffect } from "react";
import { MetaFunction } from "@remix-run/node";
import { Link, useNavigate } from "@remix-run/react";
import { safeGoBack } from "~/utils/helpers";
import { useCollectionsInfiniteQuery } from "~/requestHandler/use-collection-hooks";
import { Skeleton } from "~/components/ui/skeleton";
import {
Package,
RefreshCw,
ArrowRight,
ShoppingBag,
Calendar,
} from "lucide-react";
export default function Collections() {
const navigate = useNavigate();
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
status,
refetch,
} = useCollectionsInfiniteQuery({
pageSize: 20,
});
// Flatten all collections from all pages
const allCollections = data?.pages.flatMap((page) => page.results) || [];
// Format date helper
const formatDate = (dateString?: string) => {
if (!dateString) return "";
const date = new Date(dateString);
return new Intl.DateTimeFormat("fa-IR", {
year: "numeric",
month: "long",
day: "numeric",
}).format(date);
};
// Loading skeleton
const CollectionSkeleton = () => (
<div className="flex flex-col gap-4 px-4">
{Array.from({ length: 6 }).map((_, index) => (
<div
key={index}
className="flex flex-col gap-3 p-4 bg-WHITE2 rounded-2xl border border-inner-border"
>
<Skeleton className="w-full h-48 rounded-xl" />
<Skeleton className="w-3/4 h-5" />
<Skeleton className="w-full h-3" />
<div className="flex gap-2">
<Skeleton className="w-20 h-6" />
<Skeleton className="w-20 h-6" />
</div>
</div>
))}
</div>
);
// Empty state
const EmptyState = () => (
<div className="flex flex-col items-center justify-center py-20 px-4">
<div className="w-20 h-20 rounded-full bg-WHITE2 flex items-center justify-center mb-4">
<Package size={40} className="text-GRAY" />
</div>
<h3 className="text-B18 font-bold text-black mb-2">کالکشنی یافت نشد</h3>
<p className="text-R14 text-GRAY text-center max-w-md">
در حال حاضر کالکشنی موجود نیست.
</p>
</div>
);
// Error state
const ErrorState = () => (
<div className="flex flex-col items-center justify-center py-20 px-4">
<div className="w-20 h-20 rounded-full bg-red-50 flex items-center justify-center mb-4">
<Package size={40} className="text-red-500" />
</div>
<h3 className="text-B18 font-bold text-black mb-2">
خطا در بارگذاری کالکشنها
</h3>
<p className="text-R14 text-GRAY text-center max-w-md mb-6">
متاسفانه در بارگذاری کالکشنها مشکلی پیش آمد.
</p>
<button
onClick={() => refetch()}
className="flex items-center gap-2 bg-black text-white px-6 py-3 rounded-full hover:bg-gray-800 transition-all"
>
<RefreshCw size={16} />
<span className="text-R14 font-bold">تلاش مجدد</span>
</button>
</div>
);
// Handle scroll for infinite loading
useEffect(() => {
const handleScroll = () => {
if (
window.innerHeight + window.scrollY >=
document.documentElement.scrollHeight - 500 &&
hasNextPage &&
!isFetchingNextPage
) {
fetchNextPage();
}
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
const handleBack = () => {
safeGoBack(navigate);
};
return (
<div className="flex flex-col bg-WHITE pb-20">
{/* Header - مطابق با ProfilePagesHeader */}
<div className="flex flex-row h-[65px] sticky top-0 z-30 bg-WHITE w-full items-center justify-center border-b border-inner-border">
<ArrowRight
onClick={handleBack}
className="absolute top-5 right-5 cursor-pointer"
/>
<p className="text-B16 font-bold">کالکشنها</p>
</div>
{/* Content */}
<div className="flex flex-col py-6">
{status === "pending" ? (
<CollectionSkeleton />
) : status === "error" ? (
<ErrorState />
) : allCollections.length === 0 ? (
<EmptyState />
) : (
<>
<div className="flex flex-col gap-4 px-4">
{allCollections.map((collection) => (
<Link
key={collection.id}
to={`/collection/${collection.id}`}
className="group cursor-pointer"
>
<div className="flex flex-col gap-3 p-4 bg-WHITE2 rounded-2xl border border-inner-border hover:border-gray-300 transition-all duration-300 hover:shadow-lg">
{/* Collection Image - بزرگ */}
<div className="relative overflow-hidden rounded-xl w-full h-48 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={64} className="text-GRAY" />
</div>
)}
{/* Product Count Badge */}
{collection.productCount !== undefined && (
<div className="absolute bottom-3 left-3">
<div className="flex items-center gap-1.5 bg-white/95 backdrop-blur-sm px-3 py-1.5 rounded-full shadow-md">
<ShoppingBag size={14} 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-2">
<div className="flex items-start justify-between gap-2">
<h3 className="text-B16 font-bold text-black line-clamp-2 flex-1">
{collection.title || "بدون عنوان"}
</h3>
<ArrowRight
size={20}
className="text-GRAY transform rotate-180 group-hover:translate-x-1 transition-transform flex-shrink-0 mt-0.5"
/>
</div>
{collection.description && (
<p className="text-R13 text-GRAY line-clamp-2">
{collection.description}
</p>
)}
{/* Bottom Info Row */}
<div className="flex items-center justify-between gap-2 pt-2 border-t border-inner-border">
<div className="flex items-center gap-4 flex-wrap">
{/* Created Date */}
{collection.createdAt && (
<div className="flex items-center gap-1.5">
<Calendar size={14} className="text-GRAY" />
<span className="text-R12 text-GRAY">
{formatDate(collection.createdAt)}
</span>
</div>
)}
</div>
</div>
</div>
</div>
</Link>
))}
</div>
{/* Loading More Indicator */}
{isFetchingNextPage && (
<div className="flex justify-center items-center py-8">
<div className="flex gap-2">
<div className="w-3 h-3 bg-black rounded-full animate-bounce [animation-delay:-0.3s]" />
<div className="w-3 h-3 bg-black rounded-full animate-bounce [animation-delay:-0.15s]" />
<div className="w-3 h-3 bg-black rounded-full animate-bounce" />
</div>
</div>
)}
</>
)}
</div>
</div>
);
}
export const meta: MetaFunction = () => {
const title = "کالکشن‌های ویترون | مجموعه محصولات منتخب";
const description =
"مشاهده مجموعه‌های منتخب از بهترین محصولات ویترون. کالکشن‌های منحصر به فرد و با کیفیت.";
const imageUrl =
"https://vitrownstatics.s3.ir-thr-at1.arvanstorage.ir/SVG-06.svg?versionId=";
return [
{ title },
{ name: "description", content: description },
{
name: "keywords",
content: "کالکشن، مجموعه محصولات، محصولات منتخب، ویترون، خرید آنلاین",
},
{ name: "robots", content: "index, follow" },
// Open Graph
{ property: "og:type", content: "website" },
{ property: "og:title", content: title },
{ property: "og:description", content: description },
{ property: "og:image", content: imageUrl },
{ property: "og:site_name", content: "ویترون" },
{ property: "og:locale", content: "fa_IR" },
// Twitter Card
{ name: "twitter:card", content: "summary_large_image" },
{ name: "twitter:title", content: title },
{ name: "twitter:description", content: description },
{ name: "twitter:image", content: imageUrl },
];
};