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>
297 lines
9.5 KiB
TypeScript
297 lines
9.5 KiB
TypeScript
import { Link, useNavigate } from "@remix-run/react";
|
||
import { MapPin, Menu, Pencil, Plus, Trash, X } from "lucide-react";
|
||
import { useEffect, useState } from "react";
|
||
import ProfilePagesHeader from "../components/ProfilePagesHeader";
|
||
import { MetaFunction } from "@remix-run/node";
|
||
import {
|
||
useServiceDeleteAddress,
|
||
useServiceGetAddresses,
|
||
} from "../utils/RequestHandler";
|
||
import { Button } from "../components/ui/button";
|
||
import { Drawer, DrawerContent, DrawerHeader } from "../components/ui/drawer";
|
||
import { Dialog, DialogContent, DialogOverlay } from "../components/ui/dialog";
|
||
import { useToast } from "../hooks/use-toast";
|
||
import { Address } from "src/api/types";
|
||
import UiProvider from "~/components/UiProvider";
|
||
|
||
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_large_image" },
|
||
{ name: "twitter:title", content: title },
|
||
{ name: "twitter:description", content: description },
|
||
{ name: "twitter:image", content: imageUrl },
|
||
];
|
||
};
|
||
|
||
export default function Locaton() {
|
||
return (
|
||
<div className="flex flex-col gap-4 w-full items-center lg:max-w-[820px] lg:mx-auto lg:pt-6">
|
||
<ProfilePagesHeader title="آدرسهای من" />
|
||
<h1 className="hidden lg:block w-full text-[28px] font-extrabold px-4">
|
||
آدرسهای من
|
||
</h1>
|
||
<MainContent />
|
||
<Link
|
||
to={"/profile/location/add"}
|
||
className="fixed bottom-16 z-10 lg:static lg:bottom-auto lg:z-auto lg:mt-4"
|
||
>
|
||
<Button size={"xl"} variant="dark">
|
||
<Plus className="text-WHITE size-6" />
|
||
افزودن آدرس جدید
|
||
</Button>
|
||
</Link>
|
||
</div>
|
||
);
|
||
}
|
||
const MainContent = () => {
|
||
const {
|
||
data: dataAddress,
|
||
isFetching: isFetchingAddress,
|
||
isError,
|
||
refetch,
|
||
} = useServiceGetAddresses();
|
||
return (
|
||
<UiProvider
|
||
isLoading={isFetchingAddress}
|
||
isEmpty={!!(dataAddress && dataAddress?.length === 0)}
|
||
isError={isError}
|
||
onRetry={refetch}
|
||
type="location"
|
||
>
|
||
<div className="flex flex-col gap-0 items-center px-4 w-full">
|
||
{dataAddress?.map((address: Address, index: number) => {
|
||
return (
|
||
<AddressRow
|
||
address={address}
|
||
key={index}
|
||
isLastAddress={index !== dataAddress?.length - 1}
|
||
/>
|
||
);
|
||
})}
|
||
</div>
|
||
</UiProvider>
|
||
);
|
||
};
|
||
const AddressRow = ({
|
||
address,
|
||
isLastAddress,
|
||
}: {
|
||
address: Address;
|
||
isLastAddress: boolean;
|
||
}) => {
|
||
const [isOpen, setIsOpen] = useState(false);
|
||
|
||
const toggleDrawer = () => {
|
||
setIsOpen(!isOpen);
|
||
};
|
||
|
||
return (
|
||
<>
|
||
<div
|
||
onClick={() => toggleDrawer()}
|
||
onKeyDown={() => {}}
|
||
role="button"
|
||
tabIndex={0}
|
||
className={`w-full flex gap-2 items-center py-4 ${isLastAddress ? "border-b" : ""}`}
|
||
>
|
||
<MapPin className="text-GRAY text-6" />
|
||
<div className="flex flex-col gap-0 w-full">
|
||
<p className="text-R14">{address?.title}</p>
|
||
<p className="text-R12 line-clamp-1">{address?.mainAddress}</p>
|
||
</div>
|
||
<Menu className="text-6" />
|
||
</div>
|
||
<AdderssSettingDrawer
|
||
address={address}
|
||
isOpen={isOpen}
|
||
setIsOpen={setIsOpen}
|
||
/>
|
||
</>
|
||
);
|
||
};
|
||
export const AdderssSettingDrawer = ({
|
||
isOpen,
|
||
setIsOpen,
|
||
address,
|
||
}: {
|
||
isOpen: boolean;
|
||
setIsOpen: (open: boolean) => void;
|
||
address: Address | null;
|
||
}) => {
|
||
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
||
const navigate = useNavigate();
|
||
return (
|
||
<>
|
||
<Drawer open={isOpen} onOpenChange={setIsOpen}>
|
||
<DrawerContent className="max-h-[85vh] overflow-hidden">
|
||
<DrawerHeader className="sticky top-0 z-10 bg-WHITE">
|
||
<div className="flex flex-row gap-0 w-full relative items-center justify-center">
|
||
<X
|
||
onClick={() => setIsOpen(false)}
|
||
className="absolute size-6 top-2 right-3"
|
||
/>
|
||
<p className={"text-B12 font-bold pt-3"}>تنظیمات آدرس</p>
|
||
</div>
|
||
</DrawerHeader>
|
||
<div className="overflow-y-auto max-h-[calc(85vh-60px)] pb-4">
|
||
<div className="flex flex-col gap-6 w-full px-4 py-6">
|
||
<div className="flex flex-col gap-2 w-full">
|
||
<p className="text-B14">{address?.title}</p>
|
||
<p className="text-R14 line-clamp-1">{address?.mainAddress}</p>
|
||
</div>
|
||
<div className="flex flex-wrap gap-3 w-full">
|
||
<Button
|
||
className="w-[calc(50%-6px)]"
|
||
size={"sm"}
|
||
variant="dark"
|
||
onClick={() => {
|
||
setIsOpen(false);
|
||
navigate(`/profile/location/edit/${address?.id}`);
|
||
}}
|
||
>
|
||
<Pencil className="text-WHITE size-6" />
|
||
ویرایش آدرس
|
||
</Button>
|
||
<Button
|
||
onClick={() => {
|
||
setDeleteModalOpen(true);
|
||
setIsOpen(false);
|
||
}}
|
||
className="w-[calc(50%-6px)]"
|
||
size={"sm"}
|
||
variant="primary"
|
||
>
|
||
<Trash className=" size-6" />
|
||
حذف آدرس
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</DrawerContent>
|
||
</Drawer>
|
||
<DeleteAddressModal
|
||
address={address}
|
||
open={deleteModalOpen}
|
||
onOpenChange={setDeleteModalOpen}
|
||
/>
|
||
</>
|
||
);
|
||
};
|
||
const DeleteAddressModal = ({
|
||
address,
|
||
open,
|
||
onOpenChange,
|
||
}: {
|
||
address: Address | null;
|
||
open: boolean;
|
||
onOpenChange: (open: boolean) => void;
|
||
}) => {
|
||
const { toast } = useToast();
|
||
const deleteAddressMutation = useServiceDeleteAddress(
|
||
address?.id || "",
|
||
address?.title || "",
|
||
toast
|
||
);
|
||
|
||
useEffect(() => {
|
||
if (deleteAddressMutation.data) {
|
||
onOpenChange(false);
|
||
}
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [deleteAddressMutation.data, deleteAddressMutation.error]);
|
||
|
||
const onDelete = async () => {
|
||
if (!deleteAddressMutation.isPending) {
|
||
try {
|
||
onOpenChange(false);
|
||
await deleteAddressMutation.mutateAsync();
|
||
toast({
|
||
title: "آدرس با موفقیت حذف شد",
|
||
description: "آدرس شما با موفقیت حذف شد.",
|
||
});
|
||
} catch (error) {
|
||
toast({
|
||
title: "خطا در حذف آدرس",
|
||
description:
|
||
"متاسفانه مشکلی در حذف آدرس پیش آمده است. لطفا دوباره تلاش کنید.",
|
||
variant: "destructive",
|
||
});
|
||
}
|
||
}
|
||
};
|
||
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-10">
|
||
<div className={"gap-3 items-center w-full"}>
|
||
<Trash className="text-RED text-6" />
|
||
<p className="text-B12 font-bold mt-1">حذف آدرس</p>
|
||
<p className="text-R12">
|
||
مطمئن هستید که آدرس ''{address?.title}'' حذف
|
||
شود؟
|
||
</p>
|
||
</div>
|
||
<div className="flex flex-row gap-6 w-full">
|
||
<Button
|
||
className="w-[100%]"
|
||
size={"sm"}
|
||
variant="dark"
|
||
onClick={() => onOpenChange(false)}
|
||
>
|
||
انصراف
|
||
</Button>
|
||
<Button
|
||
disabled={deleteAddressMutation.isPending}
|
||
className="w-[100%]"
|
||
size={"sm"}
|
||
variant="primary"
|
||
onClick={() => onDelete()}
|
||
>
|
||
{deleteAddressMutation.isPending ? (
|
||
<>
|
||
<div className="relative w-5 h-5 mr-2">
|
||
<div className="absolute top-0 left-0 w-full h-full border-2 border-t-RED border-r-transparent border-b-transparent border-l-transparent rounded-full animate-spin"></div>
|
||
<div
|
||
className="absolute top-0.5 left-0.5 w-4 h-4 border-2 border-t-transparent border-r-RED border-b-transparent border-l-transparent rounded-full animate-spin"
|
||
style={{
|
||
animationDirection: "reverse",
|
||
animationDuration: "0.7s",
|
||
}}
|
||
></div>
|
||
</div>
|
||
درحال حذف...
|
||
</>
|
||
) : (
|
||
"حذف آدرس"
|
||
)}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
);
|
||
};
|