Vitron-Front/app/routes/store.$storeId.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

104 lines
3.6 KiB
TypeScript
Raw 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, useParams } from "@remix-run/react";
import {
LayoutGrid,
Package,
ClipboardList,
Wallet,
Truck,
MessageCircle,
Settings,
ArrowRight,
} from "lucide-react";
import { useSellerData } from "~/requestHandler/use-seller-hooks";
import SellerLogo from "~/components/SellerLogo";
import { useBadgeCounts } from "~/hooks/useBadgeCounts";
/**
* Desktop layout for the seller dashboard. Renders a sticky admin sidebar
* next to the page content at lg+; on mobile it's just the page content
* (the existing mobile dashboard, with its store-mode bottom nav, is unchanged).
*/
export default function StoreLayout() {
const { storeId } = useParams();
const location = useLocation();
const { data } = useSellerData(storeId);
const { data: badges } = useBadgeCounts();
const base = `/store/${storeId}`;
const NAV = [
{ label: "فروشگاه", to: base, icon: LayoutGrid, exact: true },
{ label: "محصولات", to: `${base}/products`, icon: Package },
{
label: "سفارش‌ها",
to: `${base}/orders`,
icon: ClipboardList,
badge: badges?.seller_orders || 0,
},
{ label: "مالی", to: `${base}/financial-dashboard`, icon: Wallet },
{ label: "روش ارسال", to: `${base}/shipping-method`, icon: Truck },
{
label: "پیام‌ها",
to: `${base}/threads`,
icon: MessageCircle,
badge: badges?.chat || 0,
},
{ label: "تنظیمات", to: `${base}/setting`, icon: Settings },
];
const isActive = (item: { to: string; exact?: boolean }) =>
item.exact
? location.pathname === item.to
: location.pathname.startsWith(item.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-[260px_1fr] lg:gap-7 lg:items-start">
{/* Desktop admin sidebar */}
<aside className="hidden lg:block lg:sticky lg:top-6 border border-WHITE3 rounded-2xl overflow-hidden">
<div className="p-4 border-b border-WHITE3 flex items-center gap-3">
<SellerLogo size="sm" src={data?.storeLogo} alt="store" />
<div className="min-w-0">
<p className="font-bold text-[15px] truncate">
{data?.storeName || "فروشگاه من"}
</p>
<p className="text-GRAY text-[12px]">پنل مدیریت</p>
</div>
</div>
<nav className="p-2.5 flex flex-col gap-0.5">
{NAV.map((n) => (
<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)
? "bg-BLACK text-white"
: "text-BLACK2 hover:bg-WHITE2"
}`}
>
<n.icon size={20} />
{n.label}
{n.badge && n.badge > 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">
{n.badge > 99 ? "99+" : n.badge}
</span>
) : null}
</Link>
))}
<Link
to="/"
className="flex items-center gap-3 px-3.5 py-3 rounded-xl text-[14.5px] font-semibold text-BLACK2 hover:bg-WHITE2 transition-colors"
>
<ArrowRight size={20} />
بازگشت به فروشگاه
</Link>
</nav>
</aside>
{/* Page content */}
<div className="min-w-0">
<Outlet />
</div>
</div>
);
}