These pages are desktop-enabled (they get the DesktopHeader via the /profile allowlist) but had no desktop layout, so their content stretched edge-to-edge on wide screens. Add a centered max-width container + a desktop title to the main profile sub-pages (bookmarks, following, orders, settings, wallet, addresses, sets). Grids use a wider max-width; single-column lists a narrower one. ProfilePagesHeader is already lg:hidden, so the mobile header still disappears. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
292 lines
9.2 KiB
TypeScript
292 lines
9.2 KiB
TypeScript
import { MetaFunction } from "@remix-run/node";
|
||
import { Link } from "@remix-run/react";
|
||
import { Layers, Trash2, Loader2, ChevronLeft, ShoppingBag } from "lucide-react";
|
||
import { useState } from "react";
|
||
import { Button } from "~/components/ui/button";
|
||
import { Skeleton } from "~/components/ui/skeleton";
|
||
import { Dialog, DialogContent, DialogOverlay } from "~/components/ui/dialog";
|
||
import {
|
||
useUserCollections,
|
||
useDeleteCollection,
|
||
} from "../requestHandler/use-user-collection-hooks";
|
||
import { UserCollection, ProductDetail } from "../../src/api/types";
|
||
import ProfilePagesHeader from "~/components/ProfilePagesHeader";
|
||
import { useQuery } from "@tanstack/react-query";
|
||
|
||
export default function ProfileSets() {
|
||
const { data: collections, isLoading, refetch } = useUserCollections();
|
||
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
||
const [selectedCollection, setSelectedCollection] =
|
||
useState<UserCollection | null>(null);
|
||
|
||
const handleDeleteClick = (
|
||
e: React.MouseEvent,
|
||
collection: UserCollection
|
||
) => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
setSelectedCollection(collection);
|
||
setDeleteModalOpen(true);
|
||
};
|
||
|
||
return (
|
||
<div className="flex flex-col min-h-screen bg-WHITE2 lg:bg-transparent lg:max-w-[1100px] lg:mx-auto lg:w-full lg:pt-6">
|
||
{/* Header */}
|
||
<ProfilePagesHeader title="ستهای من" />
|
||
<h1 className="hidden lg:block text-[28px] font-extrabold px-4 mb-2">
|
||
ستهای من
|
||
</h1>
|
||
|
||
{/* Content */}
|
||
<div className="flex-1 p-4">
|
||
{isLoading ? (
|
||
<div className="flex flex-col gap-4">
|
||
{[1, 2, 3].map((i) => (
|
||
<Skeleton key={i} className="h-28 w-full rounded-2xl" />
|
||
))}
|
||
</div>
|
||
) : collections && collections.length > 0 ? (
|
||
<div className="flex flex-col gap-4">
|
||
{collections.map((collection) => (
|
||
<SetCard
|
||
key={collection.id}
|
||
collection={collection}
|
||
onDelete={(e) => handleDeleteClick(e, collection)}
|
||
/>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div className="flex flex-col items-center justify-center py-16">
|
||
<div className="w-24 h-24 rounded-full bg-PRIMARY/10 flex items-center justify-center mb-4">
|
||
<Layers size={48} className="text-PRIMARY" />
|
||
</div>
|
||
<p className="text-B16 text-BLACK mb-2">هنوز ستی ندارید</p>
|
||
<p className="text-R14 text-GRAY text-center px-8">
|
||
با رفتن به صفحه محصولات و زدن دکمه افزودن به ست، اولین ست خود را
|
||
بسازید
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Delete confirmation modal */}
|
||
<DeleteSetModal
|
||
open={deleteModalOpen}
|
||
onOpenChange={setDeleteModalOpen}
|
||
collection={selectedCollection}
|
||
onSuccess={() => {
|
||
refetch();
|
||
setDeleteModalOpen(false);
|
||
setSelectedCollection(null);
|
||
}}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// Set Card Component
|
||
const SetCard = ({
|
||
collection,
|
||
onDelete,
|
||
}: {
|
||
collection: UserCollection;
|
||
onDelete: (e: React.MouseEvent) => void;
|
||
}) => {
|
||
const productIds = collection.products?.slice(0, 4) || [];
|
||
|
||
return (
|
||
<Link
|
||
to={`/profile/sets/${collection.id}`}
|
||
className="flex flex-col rounded-2xl bg-WHITE shadow-sm hover:shadow-md transition-all overflow-hidden"
|
||
>
|
||
{/* Product Images Row */}
|
||
<div className="flex h-24 bg-WHITE2">
|
||
{productIds.length > 0 ? (
|
||
<>
|
||
{productIds.slice(0, 4).map((productId, index) => (
|
||
<ProductImageCell
|
||
key={productId}
|
||
productId={productId}
|
||
isLast={index === 3 && (collection.products?.length || 0) > 4}
|
||
extraCount={(collection.products?.length || 0) - 4}
|
||
/>
|
||
))}
|
||
{/* Fill empty slots if less than 4 products */}
|
||
{productIds.length < 4 &&
|
||
Array.from({ length: 4 - productIds.length }).map((_, i) => (
|
||
<div
|
||
key={`empty-${i}`}
|
||
className="flex-1 bg-WHITE2 border-l border-WHITE first:border-l-0"
|
||
/>
|
||
))}
|
||
</>
|
||
) : (
|
||
<div className="flex-1 flex items-center justify-center">
|
||
<Layers size={32} className="text-GRAY/30" />
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Info Section */}
|
||
<div className="flex items-center gap-3 p-4">
|
||
<div className="w-10 h-10 rounded-full bg-PRIMARY/10 flex items-center justify-center flex-shrink-0">
|
||
<Layers size={20} className="text-PRIMARY" />
|
||
</div>
|
||
<div className="flex-1 min-w-0">
|
||
<h3 className="text-B14 font-bold text-BLACK truncate">
|
||
{collection.title}
|
||
</h3>
|
||
<p className="text-R12 text-GRAY">
|
||
{collection.products?.length || 0} محصول
|
||
</p>
|
||
</div>
|
||
<div className="flex items-center gap-1">
|
||
<button
|
||
onClick={onDelete}
|
||
className="p-2 rounded-full hover:bg-RED/10 transition-colors"
|
||
>
|
||
<Trash2 size={18} className="text-RED" />
|
||
</button>
|
||
<ChevronLeft size={20} className="text-GRAY" />
|
||
</div>
|
||
</div>
|
||
</Link>
|
||
);
|
||
};
|
||
|
||
// Product Image Cell for the grid
|
||
const ProductImageCell = ({
|
||
productId,
|
||
isLast,
|
||
extraCount,
|
||
}: {
|
||
productId: string;
|
||
isLast: boolean;
|
||
extraCount: number;
|
||
}) => {
|
||
const { data: product, isLoading } = useQuery({
|
||
queryKey: ["product-thumb-cell", productId],
|
||
queryFn: async (): Promise<ProductDetail> => {
|
||
const response = await fetch(
|
||
`${
|
||
typeof window !== "undefined"
|
||
? import.meta.env.VITE_API_BASE_URL || "https://api.prod.vitrown.com"
|
||
: process.env.VITE_API_BASE_URL || "https://api.prod.vitrown.com"
|
||
}/api/product/v1/products/${productId}/`
|
||
);
|
||
if (!response.ok) throw new Error("Failed to fetch product");
|
||
return response.json();
|
||
},
|
||
enabled: !!productId,
|
||
staleTime: 10 * 60 * 1000,
|
||
});
|
||
|
||
if (isLoading) {
|
||
return (
|
||
<div className="flex-1 border-l border-WHITE first:border-l-0">
|
||
<Skeleton className="w-full h-full" />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="flex-1 border-l border-WHITE first:border-l-0 relative">
|
||
{product?.imageUrls?.[0] ? (
|
||
<img
|
||
src={product.imageUrls[0]}
|
||
alt=""
|
||
className="w-full h-full object-cover"
|
||
/>
|
||
) : (
|
||
<div className="w-full h-full bg-GRAY/5 flex items-center justify-center">
|
||
<ShoppingBag size={20} className="text-GRAY/40" />
|
||
</div>
|
||
)}
|
||
{isLast && extraCount > 0 && (
|
||
<div className="absolute inset-0 bg-BLACK/60 flex items-center justify-center">
|
||
<span className="text-WHITE text-B16 font-bold">+{extraCount}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const DeleteSetModal = ({
|
||
open,
|
||
onOpenChange,
|
||
collection,
|
||
onSuccess,
|
||
}: {
|
||
open: boolean;
|
||
onOpenChange: (open: boolean) => void;
|
||
collection: UserCollection | null;
|
||
onSuccess: () => void;
|
||
}) => {
|
||
const { mutate: deleteCollection, isPending } = useDeleteCollection();
|
||
|
||
const handleDelete = () => {
|
||
if (!collection?.id) return;
|
||
|
||
deleteCollection(collection.id, {
|
||
onSuccess: () => {
|
||
onSuccess();
|
||
},
|
||
});
|
||
};
|
||
|
||
return (
|
||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||
<DialogOverlay />
|
||
<DialogContent className="p-4 bg-WHITE rounded-[15px] w-[calc(100%-40px)]">
|
||
<div className="flex flex-col gap-6">
|
||
<div className="flex flex-col gap-3 items-center w-full">
|
||
<div className="w-16 h-16 rounded-full bg-RED/10 flex items-center justify-center">
|
||
<Trash2 size={32} className="text-RED" />
|
||
</div>
|
||
<p className="text-B14 font-bold mt-1">حذف ست</p>
|
||
<p className="text-R12 text-GRAY text-center">
|
||
آیا از حذف ست "{collection?.title}" مطمئن هستید؟
|
||
</p>
|
||
</div>
|
||
|
||
<div className="flex flex-row gap-4 w-full">
|
||
<Button
|
||
className="w-[100%]"
|
||
size="sm"
|
||
variant="dark"
|
||
onClick={() => onOpenChange(false)}
|
||
disabled={isPending}
|
||
>
|
||
انصراف
|
||
</Button>
|
||
<Button
|
||
className="w-[100%] bg-RED hover:bg-RED/90"
|
||
variant="primary"
|
||
size="sm"
|
||
onClick={handleDelete}
|
||
disabled={isPending}
|
||
>
|
||
{isPending ? (
|
||
<>
|
||
<Loader2 className="h-4 w-4 animate-spin ml-2" />
|
||
در حال حذف...
|
||
</>
|
||
) : (
|
||
"حذف ست"
|
||
)}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
);
|
||
};
|
||
|
||
export const meta: MetaFunction = () => {
|
||
return [
|
||
{ title: "ستهای من | ویترون" },
|
||
{ name: "description", content: "مدیریت ستهای محصولات شما" },
|
||
{ name: "robots", content: "noindex, follow" },
|
||
];
|
||
};
|