feat(18-04): add global item hooks, catalog browse page, and detail page

- useGlobalItems/useGlobalItem/useLinkItem/useUnlinkItem hooks
- Global catalog browse page with search, debounce, and skeleton loading
- Global item detail page with owner count badge
- GlobalItemCard component with brand, model, specs badges
This commit is contained in:
2026-04-05 13:17:39 +02:00
parent 2843351d90
commit f53f66d321
4 changed files with 424 additions and 0 deletions

View File

@@ -0,0 +1,74 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { ApiError, apiDelete, apiGet, apiPost } from "../lib/api";
interface GlobalItem {
id: number;
brand: string;
model: string;
category: string | null;
weightGrams: number | null;
priceCents: number | null;
imageUrl: string | null;
description: string | null;
createdAt: string;
}
interface GlobalItemWithOwnerCount extends GlobalItem {
ownerCount: number;
}
interface ItemGlobalLink {
id: number;
itemId: number;
globalItemId: number;
}
export function useGlobalItems(query?: string) {
return useQuery({
queryKey: ["global-items", query ?? ""],
queryFn: () =>
apiGet<GlobalItem[]>(
`/api/global-items${query ? `?q=${encodeURIComponent(query)}` : ""}`,
),
});
}
export function useGlobalItem(id: number | null) {
return useQuery({
queryKey: ["global-items", id],
queryFn: () => apiGet<GlobalItemWithOwnerCount>(`/api/global-items/${id}`),
enabled: id != null,
retry: (count, error) =>
error instanceof ApiError && error.status === 404 ? false : count < 3,
});
}
export function useLinkItem() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
itemId,
globalItemId,
}: {
itemId: number;
globalItemId: number;
}) =>
apiPost<ItemGlobalLink>(`/api/items/${itemId}/link`, { globalItemId }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["items"] });
queryClient.invalidateQueries({ queryKey: ["global-items"] });
},
});
}
export function useUnlinkItem() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (itemId: number) =>
apiDelete<{ success: boolean }>(`/api/items/${itemId}/link`),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["items"] });
queryClient.invalidateQueries({ queryKey: ["global-items"] });
},
});
}