Surfaces three events without a full notification center: - service worker (public/sw.js) gains push + notificationclick handlers (RTL, focuses an existing tab or opens the target URL). - usePushNotifications hook: permission + VAPID subscribe/unsubscribe against the backend push endpoints. - opt-in UI both ways: a one-time dismissible post-login banner (PushPermissionBanner, rendered in MyLayout) and a permanent toggle (PushSettingToggle) on buyer profile settings and seller store settings. - useBadgeCounts polls /api/notif/v1/badges/; small red badges on the bottom-nav profile icon (chat + order updates), the profile sidebar (پیامها / سفارشها) and the seller dashboard sidebar. Visiting orders clears its badge. Degrades gracefully: if the backend endpoints are absent the counts fall back to zero and nothing throws. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
103 lines
3.9 KiB
TypeScript
103 lines
3.9 KiB
TypeScript
import { Link, Outlet, useLocation } from "@remix-run/react";
|
||
import {
|
||
ShoppingBag,
|
||
MessageCircle,
|
||
CircleFadingPlus,
|
||
BookmarkMinus,
|
||
Layers,
|
||
Wallet,
|
||
MapPin,
|
||
Settings,
|
||
LogOut,
|
||
CircleUserRound,
|
||
} from "lucide-react";
|
||
import { useServiceGetProfile } from "~/utils/RequestHandler";
|
||
import { useRootData } from "~/hooks/use-root-data";
|
||
import { useBadgeCounts } from "~/hooks/useBadgeCounts";
|
||
|
||
const NAV = [
|
||
{ label: "سفارشها", to: "/profile/orders", icon: ShoppingBag, badgeKey: "order_updates" as const },
|
||
{ label: "پیامها", to: "/profile/threads", icon: MessageCircle, badgeKey: "chat" as const },
|
||
{ label: "دنبالشدهها", to: "/profile/following", icon: CircleFadingPlus },
|
||
{ label: "ذخیرهشدهها", to: "/profile/bookmarks", icon: BookmarkMinus },
|
||
{ label: "ستهای من", to: "/profile/sets", icon: Layers },
|
||
{ label: "کیف پول", to: "/profile/wallet", icon: Wallet },
|
||
{ label: "آدرسها", to: "/profile/location", icon: MapPin },
|
||
{ label: "تنظیمات", to: "/profile/setting", icon: Settings },
|
||
];
|
||
|
||
/**
|
||
* Layout for the account area. On desktop it renders a sticky sidebar
|
||
* (user card + menu) next to the page content; on mobile it's just the
|
||
* page content (each profile page keeps its existing mobile UI).
|
||
*/
|
||
export default function ProfileLayout() {
|
||
const location = useLocation();
|
||
const { user } = useRootData();
|
||
const { data: profile } = useServiceGetProfile();
|
||
const { data: badges } = useBadgeCounts();
|
||
|
||
const isActive = (to: string) =>
|
||
location.pathname === to || location.pathname.startsWith(to + "/");
|
||
|
||
return (
|
||
<div className="lg:max-w-[1320px] lg:mx-auto lg:w-full lg:px-8 lg:py-8 lg:grid lg:grid-cols-[280px_1fr] lg:gap-7 lg:items-start">
|
||
{/* Desktop sidebar */}
|
||
<aside className="hidden lg:block lg:sticky lg:top-[88px] border border-WHITE3 rounded-2xl overflow-hidden">
|
||
<div className="p-5 border-b border-WHITE3 text-center">
|
||
<div className="w-20 h-20 rounded-full bg-WHITE3 grid place-items-center mx-auto mb-3 text-GRAY">
|
||
<CircleUserRound size={40} />
|
||
</div>
|
||
<p className="font-bold text-[16px]">
|
||
{profile?.username || "کاربر ویترون"}
|
||
</p>
|
||
{profile?.phoneNumber ? (
|
||
<p className="text-GRAY text-[13px] mt-1" dir="ltr">
|
||
{profile.phoneNumber}
|
||
</p>
|
||
) : null}
|
||
</div>
|
||
<nav className="p-2.5 flex flex-col gap-0.5">
|
||
{NAV.map((n) => {
|
||
const count = n.badgeKey ? badges?.[n.badgeKey] || 0 : 0;
|
||
return (
|
||
<Link
|
||
key={n.to}
|
||
to={n.to}
|
||
prefetch="intent"
|
||
className={`flex items-center gap-3 px-3.5 py-3 rounded-xl text-[14.5px] font-semibold transition-colors ${
|
||
isActive(n.to)
|
||
? "bg-BLACK text-white"
|
||
: "text-BLACK2 hover:bg-WHITE2"
|
||
}`}
|
||
>
|
||
<n.icon size={20} />
|
||
{n.label}
|
||
{count > 0 ? (
|
||
<span className="ml-auto min-w-[18px] h-[18px] px-1 rounded-full bg-RED text-white text-[10px] font-bold flex items-center justify-center leading-none">
|
||
{count > 99 ? "99+" : count}
|
||
</span>
|
||
) : null}
|
||
</Link>
|
||
);
|
||
})}
|
||
{user?.access_token ? (
|
||
<Link
|
||
to="/logout"
|
||
className="flex items-center gap-3 px-3.5 py-3 rounded-xl text-[14.5px] font-semibold text-RED hover:bg-red-50 transition-colors"
|
||
>
|
||
<LogOut size={20} />
|
||
خروج از حساب
|
||
</Link>
|
||
) : null}
|
||
</nav>
|
||
</aside>
|
||
|
||
{/* Page content */}
|
||
<div className="min-w-0">
|
||
<Outlet />
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|