Vitron-Front/app/hooks/useBadgeCounts.ts
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

68 lines
1.9 KiB
TypeScript

import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useAuthToken } from "./use-root-data";
import { getApiBaseUrl } from "~/utils/api-config";
export interface BadgeCounts {
chat: number;
order_updates: number;
seller_orders: number;
}
const EMPTY: BadgeCounts = { chat: 0, order_updates: 0, seller_orders: 0 };
export const badgeKeys = {
all: ["notif-badges"] as const,
};
/**
* Polls unread counts for the nav badges (chat / buyer order updates / seller
* new orders). Lightweight: a single endpoint, refetched on an interval and
* when the tab regains focus.
*/
export function useBadgeCounts() {
const token = useAuthToken();
const apiBase = getApiBaseUrl();
return useQuery({
queryKey: badgeKeys.all,
enabled: !!token,
refetchInterval: 30000,
refetchOnWindowFocus: true,
queryFn: async (): Promise<BadgeCounts> => {
if (!token) return EMPTY;
const res = await fetch(`${apiBase}/api/notif/v1/badges/`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) return EMPTY;
return (await res.json()) as BadgeCounts;
},
});
}
/**
* Marks a badge category ("order_updates" | "seller_orders") as read, then
* refreshes the counts. Chat clears via the existing per-thread read flow.
*/
export function useMarkBadgeRead() {
const token = useAuthToken();
const apiBase = getApiBaseUrl();
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (category: "order_updates" | "seller_orders") => {
if (!token) return;
await fetch(`${apiBase}/api/notif/v1/badges/mark-read/`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ category }),
});
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: badgeKeys.all });
},
});
}