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>
551 lines
22 KiB
TypeScript
551 lines
22 KiB
TypeScript
import { useEffect, useState } from "react";
|
||
import { MetaFunction } from "@remix-run/node";
|
||
import ProfilePagesHeader from "../components/ProfilePagesHeader";
|
||
import placeholderImage from "~/assets/icons/product.png";
|
||
import placeholderImageDark from "~/assets/icons/product-dark.png";
|
||
import {
|
||
useGetUserOrders,
|
||
useGetOrderDetail,
|
||
} from "../requestHandler/use-order-hooks";
|
||
import { useServiceGetCouriers } from "../requestHandler/use-courier-hooks";
|
||
import {
|
||
Archive,
|
||
ArrowRight,
|
||
ArrowUpLeft,
|
||
Component,
|
||
SquareCheckBig,
|
||
SquareX,
|
||
X,
|
||
} from "lucide-react";
|
||
import { formatDate, formatPrice } from "~/utils/helpers";
|
||
import { Dialog, DialogContent, DialogOverlay } from "../components/ui/dialog";
|
||
import { Button } from "~/components/ui/button";
|
||
import { useNavigate } from "@remix-run/react";
|
||
import { formatDateRange } from "~/components/cart/ShippingInformation";
|
||
import { OrderItem, OrderItemSummary, OrderList } from "src/api/types";
|
||
import UiProvider from "~/components/UiProvider";
|
||
import { isVideo } from "~/components/MondrianProductList";
|
||
import SellerLogo from "~/components/SellerLogo";
|
||
import { useRootData } from "~/hooks/use-root-data";
|
||
import { useMarkBadgeRead } from "~/hooks/useBadgeCounts";
|
||
// Product Modal Component
|
||
const ProductModal = ({
|
||
product,
|
||
isOpen,
|
||
onClose,
|
||
}: {
|
||
product: OrderItemSummary | null;
|
||
isOpen: boolean;
|
||
onClose: () => void;
|
||
}) => {
|
||
const navigate = useNavigate();
|
||
if (!product) return null;
|
||
|
||
return (
|
||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||
<DialogOverlay />
|
||
<DialogContent
|
||
closeButton={true}
|
||
className="p-4 bg-WHITE overflow-y-auto max-h-[90vh] rounded-[12px] w-[calc(100%-40px)]"
|
||
>
|
||
<div className="flex w-full items-center flex-col gap-4">
|
||
<X
|
||
size={16}
|
||
onClick={() => {
|
||
onClose();
|
||
}}
|
||
/>
|
||
<img
|
||
src={product.productImages?.[0] || ""}
|
||
alt={product.productTitle}
|
||
className="rounded-lg w-full aspect-square"
|
||
/>
|
||
<p className="text-R14">{product.productTitle}</p>
|
||
|
||
<Button
|
||
onClick={() => {
|
||
navigate(`/product/${product.productId}`);
|
||
}}
|
||
size="lg"
|
||
variant="dark"
|
||
className="w-full py-1"
|
||
>
|
||
مشاهده این پست
|
||
</Button>
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
);
|
||
};
|
||
|
||
export default function Orders() {
|
||
const { data, isLoading, isError, error, refetch } = useGetUserOrders();
|
||
const markBadgeRead = useMarkBadgeRead();
|
||
const [currentData, setCurrentData] = useState<OrderList[]>([]);
|
||
const [activeTab, setActiveTab] = useState(0);
|
||
const [activeFactorId, setActiveFactorId] = useState("");
|
||
const { theme } = useRootData();
|
||
const [showModal, setShowModal] = useState(false);
|
||
const [selectedProduct, setSelectedProduct] =
|
||
useState<OrderItemSummary | null>(null);
|
||
|
||
// Visiting the orders page clears the "order status changed" badge.
|
||
useEffect(() => {
|
||
markBadgeRead.mutate("order_updates");
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, []);
|
||
|
||
// Update currentData when data or activeTab changes
|
||
useEffect(() => {
|
||
if (data) {
|
||
let filteredData: OrderList[] = [];
|
||
if (activeTab === 0) {
|
||
filteredData = data;
|
||
} else if (activeTab === 1) {
|
||
filteredData = data.filter(
|
||
(order: OrderList) =>
|
||
order.status === "confirmed" || order.status === "pending"
|
||
);
|
||
} else if (activeTab === 2) {
|
||
filteredData = data.filter(
|
||
(order: OrderList) =>
|
||
order.status === "delivered" || order.status === "shipped"
|
||
);
|
||
} else if (activeTab === 3) {
|
||
filteredData = data.filter(
|
||
(order: OrderList) => order.status === "cancelled"
|
||
);
|
||
}
|
||
setCurrentData(filteredData);
|
||
}
|
||
if (isError) {
|
||
console.error("Error fetching orders:", error);
|
||
setCurrentData([]);
|
||
}
|
||
}, [data, isError, error, activeTab]);
|
||
|
||
const orderTabs = [
|
||
{ title: "همه", logo: <Component size={16} /> },
|
||
{ title: "جاری", logo: <Archive size={16} /> },
|
||
{ title: "ارسال شده", logo: <SquareCheckBig size={16} /> },
|
||
{ title: "لغو شده", logo: <SquareX size={16} /> },
|
||
];
|
||
|
||
const handleProductClick = (item: OrderItemSummary) => {
|
||
setSelectedProduct(item);
|
||
setShowModal(true);
|
||
};
|
||
|
||
const closeModal = () => {
|
||
setShowModal(false);
|
||
};
|
||
|
||
return (
|
||
<>
|
||
{activeFactorId ? (
|
||
<FactorDetail
|
||
factorId={activeFactorId}
|
||
setActiveFactorId={setActiveFactorId}
|
||
/>
|
||
) : (
|
||
<div className="flex flex-col w-full items-center lg:max-w-[820px] lg:mx-auto lg:pt-4">
|
||
<ProfilePagesHeader title="سفارشهای من" />
|
||
<h1 className="hidden lg:block w-full text-[28px] font-extrabold px-6 mb-2">
|
||
سفارشهای من
|
||
</h1>
|
||
<div
|
||
className={`pt-4 px-6 pb-[2px] flex w-full justify-between border-b`}
|
||
>
|
||
{orderTabs.map(
|
||
(
|
||
tab: { title: string; logo: React.ReactNode },
|
||
index: number
|
||
) => (
|
||
<div
|
||
key={index}
|
||
role="button"
|
||
tabIndex={index}
|
||
onKeyDown={() => {}}
|
||
onClick={() => {
|
||
setActiveTab(index);
|
||
}}
|
||
className={`flex flex-col items-center gap-1 ${activeTab === index ? "text-BLACK border-b border-BLACK" : "text-GRAY"}`}
|
||
>
|
||
{tab.logo}
|
||
<p className="text-R12">{tab.title}</p>
|
||
</div>
|
||
)
|
||
)}
|
||
</div>
|
||
|
||
{/* Add min-height to this container to prevent layout shift */}
|
||
<div className="w-full min-h-[80vh]">
|
||
<UiProvider
|
||
isLoading={isLoading}
|
||
isEmpty={
|
||
currentData &&
|
||
currentData.length === 0 &&
|
||
!isLoading &&
|
||
!isError
|
||
}
|
||
isError={isError}
|
||
onRetry={refetch}
|
||
type="order"
|
||
>
|
||
<>
|
||
{currentData &&
|
||
currentData.length > 0 &&
|
||
currentData.map((order: OrderList, index: number) => {
|
||
return (
|
||
<div
|
||
key={index}
|
||
className="w-full flex flex-col p-4 border-b gap-4"
|
||
>
|
||
<div className="flex justify-between w-full">
|
||
<div className="flex gap-1">
|
||
{getOrderStatusIcon(order.status || "")}
|
||
<p className="text-R12">
|
||
{getOrderStatus(order.status || "")}
|
||
</p>
|
||
</div>
|
||
<div className="flex gap-2 text-GRAY">
|
||
<p className="text-R12">شماره سفارش: </p>
|
||
<p className="text-R12">{order.invoiceNumber}</p>
|
||
</div>
|
||
</div>
|
||
<div className="flex justify-between w-full">
|
||
<p className="text-R12">تاریخ ثبت سفارش: </p>
|
||
<p className="text-R12">
|
||
{formatDate(order.createdAt || "")}
|
||
</p>
|
||
</div>
|
||
<div className="flex justify-between w-full">
|
||
<p className="text-R12">مبلغ پرداخت شده: </p>
|
||
<p className="text-R12">
|
||
{formatPrice(order.totalPrice)} تومان
|
||
</p>
|
||
</div>
|
||
<div className="flex gap-2 w-full flex-col">
|
||
<p className="text-R12">اقلام سبد خرید:</p>
|
||
<div className="flex w-full gap-2 overflow-hidden">
|
||
{order.items?.map(
|
||
(item: OrderItemSummary, index: number) => {
|
||
return (
|
||
<div
|
||
className="relative rounded-lg overflow-hidden cursor-pointer"
|
||
key={index}
|
||
onClick={() => handleProductClick(item)}
|
||
role="button"
|
||
tabIndex={0}
|
||
onKeyDown={(e) => {
|
||
if (e.key === "Enter")
|
||
handleProductClick(item);
|
||
}}
|
||
>
|
||
{isVideo(item.productImages?.[0] || "") ? (
|
||
<video
|
||
src={item.productImages?.[0] || ""}
|
||
className="w-14 h-14 object-cover"
|
||
autoPlay
|
||
muted
|
||
loop
|
||
playsInline
|
||
/>
|
||
) : (
|
||
<img
|
||
src={item.productImages?.[0] || ""}
|
||
alt={item.productTitle}
|
||
className="w-14 h-14 object-cover"
|
||
onError={(e) => {
|
||
e.currentTarget.src =
|
||
theme === "dark"
|
||
? placeholderImageDark
|
||
: placeholderImage;
|
||
e.currentTarget.style.objectFit =
|
||
"contain";
|
||
e.currentTarget.style.backgroundColor =
|
||
theme === "dark"
|
||
? "#121212"
|
||
: "#f2f0f0";
|
||
}}
|
||
/>
|
||
)}
|
||
<div className="absolute bottom-1 left-1 w-4 h-4 rounded-full bg-BLACK/80 flex items-center justify-center">
|
||
<p className="text-R8 text-WHITE">x1</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
)}
|
||
</div>
|
||
</div>
|
||
<div
|
||
role="button"
|
||
tabIndex={index}
|
||
onKeyDown={() => {}}
|
||
onClick={() => setActiveFactorId(order.id || "")}
|
||
className="flex text-BLUE gap-2 cursor-pointer justify-center w-full"
|
||
>
|
||
<ArrowUpLeft size={24} />
|
||
<p className="text-R12">مشاهده فاکتور</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</>
|
||
</UiProvider>
|
||
</div>
|
||
|
||
{/* Render product modal component */}
|
||
<ProductModal
|
||
product={selectedProduct}
|
||
isOpen={showModal}
|
||
onClose={closeModal}
|
||
/>
|
||
</div>
|
||
)}
|
||
</>
|
||
);
|
||
}
|
||
|
||
const FactorDetail = ({
|
||
factorId,
|
||
setActiveFactorId,
|
||
}: {
|
||
factorId: string;
|
||
setActiveFactorId: (id: string) => void;
|
||
}) => {
|
||
const { data: factorDetail, isLoading: isLoadingDetail } =
|
||
useGetOrderDetail(factorId);
|
||
const sellerId = factorDetail?.seller || "";
|
||
const { data: sellerCouriers } = useServiceGetCouriers(sellerId);
|
||
|
||
const courierName =
|
||
factorDetail?.shippingInfo?.sellerCourierDetail?.courierName || "-";
|
||
return (
|
||
<div className="flex flex-col w-full pb-4">
|
||
<div className="flex flex-row h-[65px] w-full relative items-center justify-center border-b border-inner-border">
|
||
<ArrowRight
|
||
onClick={() => setActiveFactorId("")}
|
||
className="absolute top-5 right-5"
|
||
/>
|
||
<p className={"text-B16 font-bold"}>جزییات فاکتور</p>
|
||
</div>
|
||
{isLoadingDetail ? (
|
||
<p className="text-GRAY">در حال بارگذاری فاکتور...</p>
|
||
) : (
|
||
<div className="w-full">
|
||
<div className="w-full px-4">
|
||
<div className="flex flex-col gap-4 py-4 w-full border-b border-TRANSPARENT_BLACK_10 pb-4">
|
||
<div className="w-full flex gap-1 justify-between">
|
||
<p className="text-R12">وضعیت سفارش</p>
|
||
<p className="text-R12">
|
||
{getOrderStatus(factorDetail?.status || "")}
|
||
</p>
|
||
</div>
|
||
<div className="w-full flex gap-1 justify-between">
|
||
<p className="text-R12">شماره سفارش</p>
|
||
<p className="text-R12">
|
||
...{factorDetail?.invoice?.id?.slice(0, 10)}
|
||
</p>
|
||
</div>
|
||
{factorDetail?.shippingInfo?.trackingCode && (
|
||
<div className="w-full flex gap-1 justify-between">
|
||
<p className="text-R12">کد پیگیری پستی</p>
|
||
<p className="text-R12">
|
||
{factorDetail?.shippingInfo?.trackingCode || "-"}
|
||
</p>
|
||
</div>
|
||
)}
|
||
<div className="w-full flex gap-1 justify-between">
|
||
<p className="text-R12">تاریخ ثبت سفارش</p>
|
||
<p className="text-R12">
|
||
{formatDate(factorDetail?.createdAt || "")}
|
||
</p>
|
||
</div>
|
||
<div className="w-full flex gap-1 justify-between">
|
||
<p className="text-R12">روش ارسال</p>
|
||
<p className="text-R12">{courierName}</p>
|
||
</div>
|
||
</div>
|
||
<div className="flex flex-col gap-4 w-full border-b py-4">
|
||
{factorDetail?.items?.map((item: OrderItem, index: number) => (
|
||
<div key={index} className="w-full flex flex-col gap-1">
|
||
<div className="w-full justify-between flex gap-1">
|
||
<p className="text-R12">
|
||
{item.productTitle || item.variant?.variantName || ""}
|
||
</p>
|
||
<p className="text-R12 tracking-wide">
|
||
{formatPrice(item.variant?.price || item.unitPrice || "")}
|
||
x{item.quantity} تومان
|
||
</p>
|
||
</div>
|
||
<div className="flex items-center gap-4">
|
||
{item?.variant?.color?.value &&
|
||
item?.variant?.color?.value !== "UNSELECTED" && (
|
||
<div className="flex items-center gap-1">
|
||
<span className="text-R12">رنگ:</span>
|
||
<span
|
||
className="inline-block w-3 h-3 rounded-full border border-WHITE shadow"
|
||
style={{
|
||
backgroundColor:
|
||
item?.variant?.color?.info?.detail || "#000",
|
||
}}
|
||
aria-label="color"
|
||
/>
|
||
{item?.variant?.color?.info?.persian && (
|
||
<span className="text-R12">
|
||
{item?.variant?.color?.info?.persian}
|
||
</span>
|
||
)}
|
||
</div>
|
||
)}
|
||
{item?.variant?.size?.value &&
|
||
item?.variant?.size?.value !== "UNSELECTED" && (
|
||
<div className="flex items-center gap-1">
|
||
<span className="text-R12">سایز:</span>
|
||
<span className="text-R12">
|
||
{item?.variant?.size?.info?.persian ||
|
||
item?.variant?.size?.value}
|
||
</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
))}
|
||
<div className="w-full justify-between flex gap-1">
|
||
<p className="text-R12">مقدار تخفیف</p>
|
||
<p className="text-R12 tracking-wide">
|
||
{formatPrice(factorDetail?.invoice?.discountValue || "0")}
|
||
تومان
|
||
</p>
|
||
</div>
|
||
<div className="w-full justify-between flex gap-1">
|
||
<p className="text-R12">هزینه ارسال</p>
|
||
<p className="text-R12 tracking-wide">
|
||
{factorDetail?.shippingInfo?.sellerCourierDetail?.courierIsFreightCollect
|
||
? "پس پرداخت"
|
||
: `${formatPrice(
|
||
factorDetail?.shippingInfo?.deliveryPrice || ""
|
||
)} تومان`}
|
||
</p>
|
||
</div>
|
||
<div className="w-full justify-between flex gap-1">
|
||
<p className="text-B16 font-bold">مبلغ کل فاکتور</p>
|
||
<p className="text-B16 font-bold tracking-wide">
|
||
{formatPrice(factorDetail?.totalPrice || "")}
|
||
تومان
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div className="flex flex-col gap-4 py-4 w-full border-b border-TRANSPARENT_BLACK_10 pb-4">
|
||
<div className="w-full flex gap-1 justify-between">
|
||
<p className="text-R12">تحویل گیرنده</p>
|
||
<p className="text-R12">
|
||
{factorDetail?.shippingInfo?.receiverName || "حضوری"}
|
||
</p>
|
||
</div>
|
||
<div className="w-full flex gap-1 justify-between">
|
||
<p className="text-R12">شماره موبایل</p>
|
||
<p className="text-R12">
|
||
{factorDetail?.shippingInfo?.receiverPhone || "-"}
|
||
</p>
|
||
</div>
|
||
<div className="w-full flex gap-1 justify-between">
|
||
<p className="text-R12">زمان تحویل</p>
|
||
<p className="text-R12">
|
||
{formatDateRange(
|
||
factorDetail?.shippingInfo?.estimatedDeliveryDays,
|
||
factorDetail?.shippingInfo?.createdAt
|
||
)}
|
||
</p>
|
||
</div>
|
||
{factorDetail?.shippingInfo?.mainAddress && (
|
||
<div className="w-full flex gap-1">
|
||
<p className="text-R12">
|
||
آدرس:{factorDetail?.shippingInfo?.mainAddress}
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<div className="w-full flex flex-col gap-4 px-4 pt-10 items-center justify-center">
|
||
<a
|
||
href={`/seller/${factorDetail?.seller}`}
|
||
className="gap-2 flex items-center"
|
||
>
|
||
<SellerLogo src={factorDetail?.storeLogo} alt="sellerLogo" />
|
||
<p className="text-B12 font-bold">
|
||
{factorDetail?.storeName || ""}
|
||
</p>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
const getOrderStatus = (status: string) => {
|
||
switch (status) {
|
||
case "confirmed":
|
||
return "جاری";
|
||
case "delivered":
|
||
return "تحویل شده";
|
||
case "cancelled":
|
||
return "لغو شده";
|
||
case "pending":
|
||
return "پرداخت شده";
|
||
case "shipped":
|
||
return "ارسال شده";
|
||
default:
|
||
return "نامشخص";
|
||
}
|
||
};
|
||
|
||
const getOrderStatusIcon = (status: string) => {
|
||
switch (status) {
|
||
case "confirmed":
|
||
return <Archive size={18} className="text-GREEN" />;
|
||
case "delivered":
|
||
return <SquareCheckBig size={18} className="text-GREEN" />;
|
||
case "cancelled":
|
||
return <SquareX size={18} className="text-RED" />;
|
||
case "pending":
|
||
return <Archive size={18} className="text-YELLOW" />;
|
||
case "shipped":
|
||
return <Archive size={18} className="text-YELLOW" />;
|
||
default:
|
||
return <Archive size={18} className="text-GRAY" />;
|
||
}
|
||
};
|
||
|
||
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: "noindex, 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" },
|
||
{ name: "twitter:title", content: title },
|
||
{ name: "twitter:description", content: description },
|
||
];
|
||
};
|