Vitron-Front/app/routes/profile.orders.tsx
Arda Samadi a67e81ad55 fix(desktop): constrain profile sub-pages so they don't stretch on desktop
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>
2026-07-09 11:53:28 +03:30

551 lines
22 KiB
TypeScript
Raw Permalink 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, 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 },
];
};