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>
68 lines
1.9 KiB
TypeScript
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 });
|
|
},
|
|
});
|
|
}
|