Vitron-Front/app/routes/collection.$id.tsx
Arda Samadi fa65db8804 Desktop fixes: product seller + actions, collection cover, masonry skeleton
- product: show seller (logo + name) in the desktop info column (was missing
  after hiding the mobile header); action icons (save/share/collection/AI) are
  now circular bordered buttons on desktop instead of a bare icon row
- collection detail: cover constrained to a 360px portrait column with
  object-cover (fixes over-long image + side margins); tighter spacing
- masonry: only render the loading skeleton while actively fetching (removes
  the persistent grey block before the footer); sentinel moved above it

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 22:59:24 +03:30

336 lines
12 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 { MetaFunction } from "@remix-run/node";
import { useParams, useNavigate } from "@remix-run/react";
import { safeGoBack } from "~/utils/helpers";
import {
useCollectionDetail,
useSimilarCollections,
} from "~/requestHandler/use-collection-hooks";
import { Skeleton } from "~/components/ui/skeleton";
import {
Package,
RefreshCw,
ArrowRight,
ShoppingBag,
Calendar,
Sparkles,
} from "lucide-react";
import MondrianProductList from "~/components/MondrianProductList";
import HorizontalCollectionsList from "~/components/HorizontalCollectionsList";
import AIPromoteModal from "~/components/AIPromoteModal";
import { useState } from "react";
export default function CollectionDetail() {
const params = useParams();
const navigate = useNavigate();
const collectionId = params.id || "";
// AI Promote Modal State
const [isPromoteModalOpen, setIsPromoteModalOpen] = useState(false);
const {
data: collection,
isLoading,
isError,
refetch,
} = useCollectionDetail(collectionId);
const {
data: similarCollectionsData,
isLoading: isSimilarLoading,
isError: isSimilarError,
refetch: refetchSimilar,
} = useSimilarCollections(collectionId);
// 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);
};
const handleBack = () => {
safeGoBack(navigate);
};
// Loading state
if (isLoading) {
return (
<div className="flex flex-col min-h-screen bg-WHITE pb-20">
{/* Header */}
<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>
{/* Loading Skeleton */}
<div className="flex flex-col gap-4 p-4">
<Skeleton className="w-full mx-auto h-[65vh] rounded-xl" />
<Skeleton className="w-3/4 h-6" />
<Skeleton className="w-full h-4" />
<div className="flex gap-2">
<Skeleton className="w-24 h-8" />
<Skeleton className="w-24 h-8" />
</div>
<Skeleton className="w-full h-px mt-4" />
<div className="grid grid-cols-2 gap-4 mt-4">
{Array.from({ length: 6 }).map((_, i) => (
<Skeleton key={i} className="w-full h-48 rounded-xl" />
))}
</div>
</div>
</div>
);
}
// Error state
if (isError || !collection) {
return (
<div className="flex flex-col min-h-screen bg-WHITE pb-20">
{/* Header */}
<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>
{/* Error State */}
<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>
</div>
);
}
const products = collection.products?.results || [];
return (
<div className="flex flex-col min-h-screen bg-WHITE pb-20">
{/* Header (mobile) */}
<div className="lg:hidden 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 lg:max-w-[1320px] lg:mx-auto lg:w-full lg:px-8 lg:pt-6">
{/* Collection Header */}
<div className="flex flex-col gap-4 p-4 border-b border-inner-border lg:grid lg:grid-cols-[360px_1fr] lg:gap-8 lg:items-start lg:p-0 lg:py-6 lg:border-0">
{/* Cover Image */}
<div className="relative overflow-hidden rounded-xl w-full mx-auto h-[65vh] bg-gray-100 lg:h-auto lg:aspect-[3/4]">
{collection.coverImageUrl ? (
<img
src={collection.coverImageUrl}
alt={collection.title || "Collection"}
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full flex items-center justify-center bg-WHITE2">
<Package size={80} className="text-GRAY" />
</div>
)}
</div>
{/* Collection Info */}
<div className="flex flex-col gap-3">
<h1 className="text-B20 lg:text-[28px] font-bold text-black">
{collection.title || "بدون عنوان"}
</h1>
{collection.description && (
<p className="text-R14 text-GRAY">{collection.description}</p>
)}
{/* Stats Row */}
<div className="flex items-center gap-4 flex-wrap justify-between">
{/* Product Count */}
{collection.productCount !== undefined && (
<div className="flex items-center gap-1.5 bg-WHITE2 px-3 py-2 rounded-lg">
<ShoppingBag size={16} className="text-black" />
<span className="text-R13 text-black font-bold">
{collection.productCount} محصول
</span>
</div>
)}
{/* Created Date */}
{collection.createdAt && (
<div className="flex items-center gap-1.5">
<Calendar size={14} className="text-GRAY" />
<span className="text-R10 text-GRAY">
{formatDate(collection.createdAt)}
</span>
</div>
)}
</div>
</div>
</div>
{/* Products Section */}
<div className="flex flex-col py-4">
<div className="px-4 mb-4">
<h2 className="text-B18 font-bold text-black">محصولات کالکشن</h2>
</div>
{products.length === 0 ? (
<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>
) : (
<MondrianProductList
products={products.map((product) => ({
id: product.id || "",
title: product.title,
images: product.imageUrls || [],
basePrice: product.basePrice
? parseInt(product.basePrice)
: undefined,
}))}
fetchNextPage={() => {}}
hasNextPage={false}
isFetchingNextPage={false}
isLoading={false}
isError={false}
refetch={refetch}
emptyState={{
image: <Package size={80} className="text-GRAY" />,
title: "محصولی وجود ندارد",
description: "هنوز محصولی به این کالکشن اضافه نشده است.",
}}
/>
)}
</div>
{/* Similar Collections Section */}
{similarCollectionsData &&
similarCollectionsData?.results &&
similarCollectionsData?.results.length > 0 && (
<div className="flex flex-col py-4 border-t border-inner-border">
<div className="px-4 mb-4">
<h2 className="text-B18 font-bold text-black">
کالکشنهای مرتبط
</h2>
</div>
<HorizontalCollectionsList
collections={similarCollectionsData?.results || []}
isLoading={isSimilarLoading}
isError={isSimilarError}
refetch={refetchSimilar}
/>
</div>
)}
</div>
{/* AI Promote Floating Button - با انیمیشن ورود از پایین */}
<button
onClick={() => setIsPromoteModalOpen(true)}
className="fixed bottom-24 left-1/2 -translate-x-1/2 z-40
flex items-center gap-2 px-6 py-4 rounded-full
bg-BLACK text-WHITE font-bold shadow-2xl
hover:bg-BLACK2 hover:scale-105
active:scale-95
transition-all duration-300"
style={{
animation:
"slideUpBounce 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) forwards",
}}
>
<Sparkles className="w-5 h-5" />
<span className="text-R14 font-bold">پرو کردن با AI</span>
</button>
{/* AI Promote Modal */}
<AIPromoteModal
isOpen={isPromoteModalOpen}
onClose={() => setIsPromoteModalOpen(false)}
collectionId={collectionId}
/>
{/* Keyframe Animation for Floating Button */}
<style>{`
@keyframes slideUpBounce {
0% {
transform: translateX(-50%) translateY(100px);
opacity: 0;
}
60% {
transform: translateX(-50%) translateY(-10px);
opacity: 1;
}
80% {
transform: translateX(-50%) translateY(5px);
}
100% {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
}
`}</style>
</div>
);
}
export const meta: MetaFunction = () => {
return [
{ title: "جزئیات کالکشن - ویترون" },
{
name: "description",
content: "مشاهده جزئیات کالکشن و محصولات موجود در آن.",
},
{
name: "keywords",
content: "کالکشن، محصولات، ویترون، خرید آنلاین",
},
{ name: "robots", content: "index, follow" },
{ property: "og:type", content: "website" },
{ property: "og:title", content: "جزئیات کالکشن - ویترون" },
{
property: "og:description",
content: "مشاهده جزئیات کالکشن و محصولات موجود در آن.",
},
{ property: "og:site_name", content: "ویترون" },
{ property: "og:locale", content: "fa_IR" },
{ name: "twitter:card", content: "summary_large_image" },
{ name: "twitter:title", content: "جزئیات کالکشن - ویترون" },
{
name: "twitter:description",
content: "مشاهده جزئیات کالکشن و محصولات موجود در آن.",
},
];
};