Vitron-Front/app/routes/profile._index.tsx
2026-04-29 01:44:16 +03:30

430 lines
12 KiB
TypeScript
Raw 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 { MetaFunction } from "@remix-run/node";
import {
BookmarkMinus,
ChevronLeft,
MapPin,
Wallet,
MessageCircle,
Store,
ShoppingBag,
CircleFadingPlus,
Headset,
Info,
LogOut,
CircleUserRound,
FileQuestion,
Layers,
} from "lucide-react";
import { Link, useNavigate } from "@remix-run/react";
import { useServiceGetProfile } from "~/utils/RequestHandler";
import { Skeleton } from "~/components/ui/skeleton";
import { useState } from "react";
import { Button } from "~/components/ui/button";
import { Dialog, DialogContent, DialogOverlay } from "~/components/ui/dialog";
import { useRootData } from "~/hooks/use-root-data";
import { useServiceGetFollowedSellers } from "~/requestHandler/use-following-hooks";
import { User } from "~/utils/token-manager.server";
import { SunMoonToggle } from "~/components/SunMoonToggle";
import { LoginRequiredDialog } from "~/components/LoginRequiredDialog";
interface MenuItemType {
title: string;
link?: string;
pageName: string;
hasBorder?: boolean;
image: React.ReactNode;
}
export default function Profile() {
const { user, theme: initialTheme } = useRootData();
const [storeModalOpen, setStoreModalOpen] = useState(false);
const [loginModalOpen, setLoginModalOpen] = useState(false);
const [logoutModalOpen, setLogoutModalOpen] = useState(false);
const { data: profileData } = useServiceGetProfile();
const { data: followedSellers, isLoading: isFollowedSellersLoading } =
useServiceGetFollowedSellers();
// Calculate the count of followed sellers
const followedCount = followedSellers?.length || 0;
// Create dynamic items with the followed count
const menuItems: MenuItemType[] = [
{
title: "سفارش‌های من",
link: "/profile/orders",
pageName: "orders",
hasBorder: true,
image: <ShoppingBag size={20} />,
},
{
title: isFollowedSellersLoading
? "دنبال شده‌ها"
: `دنبال شده‌ها(${followedCount})`,
link: "/profile/following",
pageName: "following",
image: <CircleFadingPlus size={20} />,
},
{
title: "پیام‌ها",
link: "/profile/threads",
pageName: "messages",
image: <MessageCircle size={20} />,
},
{
title: "کیف پول",
link: "/profile/wallet",
pageName: "wallet",
image: <Wallet size={20} />,
},
{
title: "آدرس های من",
link: "/profile/location",
pageName: "location",
image: <MapPin size={20} />,
},
{
title: "ورود به فروشگاه من",
link: "",
pageName: "store",
image: <Store size={20} />,
},
{
title: "ذخیره شده‌ها",
link: "/profile/bookmarks",
pageName: "bookmarks",
image: <BookmarkMinus size={20} />,
},
{
title: "ست‌های من",
link: "/profile/sets",
pageName: "sets",
image: <Layers size={20} />,
},
{
title: "تماس با پشتیبانی",
pageName: "support",
hasBorder: true,
image: <Headset size={20} />,
},
{
title: "سوالات متداول",
link: "/profile/faq",
pageName: "faq",
image: <FileQuestion size={20} />,
},
{
title: "درباره ویترون",
link: "/profile/about",
pageName: "about",
image: <Info size={20} />,
},
{
title: "خروج",
pageName: "logout",
link: "/logout",
image: <LogOut size={20} />,
},
];
const navigate = useNavigate();
return (
<div className="flex flex-col gap-0">
<ProfileTopBar user={user} />
<div
className={`flex flex-row h-[55px] w-full items-center gap-2 justify-between px-[16px] active:bg-hover transition`}
>
<div className="flex flex-row gap-2 items-center">
<p className={"text-R12"}>
{initialTheme === "dark" ? "حالت تاریک" : "حالت روشن"}
</p>
</div>
<SunMoonToggle theme={initialTheme} />
</div>
<MenuItems
items={menuItems}
onStoreClick={() => {
if (profileData?.sellerUsername) {
navigate(`/store/${profileData.sellerUsername}`);
} else {
if (user?.access_token) {
setStoreModalOpen(true);
} else {
setLoginModalOpen(true);
}
}
}}
onLogoutClick={() => {
setLogoutModalOpen(true);
}}
/>
<StoreModal open={storeModalOpen} onOpenChange={setStoreModalOpen} />
<LogoutModal open={logoutModalOpen} onOpenChange={setLogoutModalOpen} />
<LoginRequiredDialog
loginTitle="برای ورود به فروشگاه من ابتدا باید وارد حساب کاربری خود شوید."
isOpen={loginModalOpen}
onOpenChange={setLoginModalOpen}
/>
</div>
);
}
const MenuItems = ({
items,
onStoreClick,
onLogoutClick,
}: {
items: MenuItemType[];
onStoreClick: () => void;
onLogoutClick: () => void;
}) => {
const { user } = useRootData();
const handleSupportClick = () => {
const supportPhone = import.meta.env.VITE_SUPPORT_PHONE || "";
window.location.href = `tel:${supportPhone}`;
};
return (
<div className="flex flex-col gap-0 w-full">
{items.map((item: MenuItemType, index: number) => {
// Special handling for "support" item
if (item.pageName === "support") {
return (
<MenuItem key={index} item={item} onClick={handleSupportClick} />
);
}
// Special handling for "store" item
if (item.pageName === "store") {
return <MenuItem key={index} item={item} onClick={onStoreClick} />;
}
// Special handling for "logout" item
if (item.pageName === "logout") {
if (user?.access_token) {
return <MenuItem key={index} item={item} onClick={onLogoutClick} />;
}
return null;
}
// Default rendering for other items with Link
return (
<Link key={index} to={item.link || ""} className="w-full">
<MenuItem.Content item={item} />
</Link>
);
})}
</div>
);
};
// Reusable MenuItem component
const MenuItem = ({
item,
onClick,
}: {
item: MenuItemType;
onClick: () => void;
}) => {
return (
<div
className="w-full"
onClick={onClick}
role="button"
tabIndex={0}
onKeyDown={(e) => e.key === "Enter" && onClick()}
>
<MenuItem.Content item={item} />
</div>
);
};
// Content subcomponent to be used with or without clickable wrapper
const MenuItemContent = ({ item }: { item: MenuItemType }) => {
return (
<div
className={`flex flex-row gap-2 items-center justify-between px-[16px] active:bg-hover transition cursor-pointer ${item.hasBorder && "border-t-[2px]"} h-[55px] w-full`}
>
<div className="flex flex-row gap-2 items-center">
{item.image}
<p className={"text-R12"}>{item.title}</p>
</div>
<ChevronLeft className="size-4" />
</div>
);
};
// Assign the content component as a property of MenuItem
MenuItem.Content = MenuItemContent;
MenuItemContent.displayName = "MenuItem.Content";
const ProfileTopBar = ({ user }: { user: User }) => {
const { data: dataProfile, isFetching: isFetchingProfile } =
useServiceGetProfile();
return (
<div className="flex flex-row h-[65px] w-full items-center gap-2 justify-between px-[16px]">
<div className="sticky top-0 z-30 flex flex-row gap-2 items-center">
<Link to={"/profile/setting"}>
<CircleUserRound size={28} />
</Link>
{isFetchingProfile ? (
<CurvedButton title="" isLoading={true} />
) : user && user.access_token ? (
dataProfile && dataProfile.username ? (
<p className="text-R14">{dataProfile.username}</p>
) : (
<Link to={"/profile/setting"}>
<CurvedButton title="تکمیل اطلاعات کاربری" />
</Link>
)
) : (
<Link to={"/login"}>
<CurvedButton title="ورود / ثبت نام" />
</Link>
)}
</div>
</div>
);
};
export const CurvedButton = ({
title,
isLoading,
}: {
title: string;
isLoading?: boolean;
}) => {
return (
<>
{isLoading ? (
<Skeleton className="w-[100px] h-[30px] bg-VITROWN_BLUE text-white" />
) : (
<div
className={`px-4 py-1 rounded-[100px] bg-VITROWN_BLUE text-white`}
>
<p className="text-B14 font-bold">{title}</p>
</div>
)}
</>
);
};
const LogoutModal = ({
open,
onOpenChange,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
}) => {
const navigate = useNavigate();
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="flex flex-col gap-3 items-center w-full">
<LogOut size={24} />
<p className="text-B12 font-bold mt-1">خروج از حساب کاربری</p>
<p className="text-R12">میخواهید از حساب کاربری خود خارج شوید؟</p>
</div>
<div className="flex flex-row gap-6 w-full">
<Button
className="w-[100%]"
size={"sm"}
variant="dark"
onClick={() => onOpenChange(false)}
>
انصراف
</Button>
<Button
className="w-[100%]"
variant="primary"
size={"sm"}
onClick={() => navigate("/logout")}
>
خروج از حساب
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
};
const StoreModal = ({
open,
onOpenChange,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
}) => {
const navigate = useNavigate();
const handleActionClick = () => {
navigate("/store/create");
onOpenChange(false);
};
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="flex flex-col gap-3 items-center w-full">
<Store size={24} />
<p className="text-B12 font-bold mt-1">
شما در فروشگاه ثبت نشدهاید
</p>
<p className="text-R12">میخواهید فروشگاه خود را ایجاد کنید؟</p>
</div>
<div className="flex flex-row gap-6 w-full">
<Button
className="w-[100%]"
size={"sm"}
variant="dark"
onClick={() => onOpenChange(false)}
>
انصراف
</Button>
<Button
className="w-[100%]"
variant="primary"
size={"sm"}
onClick={handleActionClick}
>
ایجاد فروشگاه
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
};
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 },
];
};