Vitron-Front/app/requestHandler/use-view-hooks.ts
Arda Samadi bb10907463 feat(product): record product views for recommendations
Fire a fire-and-forget view event when an authenticated, non-owner user
opens a product page (deduped per product), POSTing to the new
/api/engagement/v1/views/ endpoint. Silent on failure so tracking never
disrupts browsing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 13:57:04 +03:30

70 lines
2.3 KiB
TypeScript

import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useEffect, useRef } from "react";
import { useAuthToken } from "../hooks/use-root-data";
import { getApiBaseUrl } from "../utils/api-config";
/**
* Records that the authenticated user viewed a product. Used as a signal for
* the recommendation system (alongside bookmarks, follows, orders, semantic
* proximity). Fire-and-forget: failures are swallowed so tracking never
* disrupts the browsing experience.
*
* NOTE: this hits /api/engagement/v1/views/ via a direct fetch because the
* endpoint is newer than the generated OpenAPI client. Swap to the generated
* EngagementApi method once the client is regenerated.
*/
export function useTrackProductView() {
const token = useAuthToken();
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (productId: string) => {
if (!token || !productId) return { skipped: true };
const res = await fetch(`${getApiBaseUrl()}/api/engagement/v1/views/`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ productId }),
});
if (!res.ok) {
throw new Error(`Failed to track view: ${res.status}`);
}
return res.json();
},
onSuccess: () => {
// Keep "recently viewed" fresh if/when it's rendered.
queryClient.invalidateQueries({ queryKey: ["recentlyViewedProducts"] });
},
// View tracking is best-effort; never surface errors to the user.
onError: () => {},
});
}
/**
* Fires a single product-view event when a product page is opened. Dedupes per
* productId so React strict-mode double-mounts / re-renders don't double-count
* within the same mount, and skips when not authenticated.
*/
export function useRecordProductView(
productId: string | undefined,
options?: { enabled?: boolean }
) {
const token = useAuthToken();
const { mutate } = useTrackProductView();
const lastTrackedRef = useRef<string | null>(null);
const enabled = options?.enabled ?? true;
useEffect(() => {
if (!enabled) return;
if (!token || !productId) return;
if (lastTrackedRef.current === productId) return;
lastTrackedRef.current = productId;
mutate(productId);
}, [productId, token, enabled, mutate]);
}