MyLayout only pads the bottom inset globally; each top bar must add its own top inset. Several inline page headers omitted it, so their content slid under the notch in the iOS standalone PWA (home was fine — its bar has it). Wrap the back-button headers the same way ProfilePagesHeader already does (outer sticky element carries pt-[env(safe-area-inset-top)]; inner relative bar holds the content) so the absolute back arrow stays out of the notch. Fixed: /profile hub, /discounts, /collections, /collection/:id, /sellers, seller store index, and financial transactions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
265 lines
10 KiB
TypeScript
265 lines
10 KiB
TypeScript
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 lg:grid lg:grid-cols-4 lg:gap-5 lg:px-0">
|
||
{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 (mobile) */}
|
||
<div className="lg:hidden sticky top-0 z-30 bg-WHITE pt-[env(safe-area-inset-top)]">
|
||
<div className="relative flex flex-row h-[65px] 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>
|
||
</div>
|
||
|
||
{/* Content */}
|
||
<div className="flex flex-col py-6 lg:max-w-[1320px] lg:mx-auto lg:w-full lg:px-8">
|
||
<h1 className="hidden lg:block text-[28px] font-extrabold mb-6">
|
||
کالکشنها
|
||
</h1>
|
||
{status === "pending" ? (
|
||
<CollectionSkeleton />
|
||
) : status === "error" ? (
|
||
<ErrorState />
|
||
) : allCollections.length === 0 ? (
|
||
<EmptyState />
|
||
) : (
|
||
<>
|
||
<div className="flex flex-col gap-4 px-4 lg:grid lg:grid-cols-4 lg:gap-5 lg:px-0">
|
||
{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 lg:h-auto lg:aspect-[3/4] 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 },
|
||
];
|
||
};
|