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>
70 lines
2.3 KiB
TypeScript
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]);
|
|
}
|