Vitron-Front/app/routes/profile.tsx
Arda Samadi fef53fd003 feat(notifications): web push opt-in + unread nav badges
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>
2026-06-17 14:02:56 +03:30

103 lines
3.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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 { 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>
);
}