Items in shared/public setups are now viewable without auth. Clicking an item in a shared setup navigates to /items/:id?setup=:setupId&share=token which fetches the item via a public endpoint authorized by the setup's visibility or share token. Read-only mode hides all owner controls. - Added getSetupItemById service function - Added GET /api/shared/:token/items/:itemId endpoint - Added GET /api/setups/:setupId/items/:itemId/public endpoint - Added usePublicSetupItem and useSharedSetupItem hooks - Item detail page detects setup context and switches to public fetch - Back link returns to setup instead of collection in setup context Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
207 lines
5.0 KiB
TypeScript
207 lines
5.0 KiB
TypeScript
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import {
|
|
ApiError,
|
|
apiDelete,
|
|
apiGet,
|
|
apiPatch,
|
|
apiPost,
|
|
apiPut,
|
|
} from "../lib/api";
|
|
|
|
interface SetupListItem {
|
|
id: number;
|
|
name: string;
|
|
visibility: "private" | "link" | "public";
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
itemCount: number;
|
|
totalWeight: number;
|
|
totalCost: number;
|
|
}
|
|
|
|
interface SetupItemWithCategory {
|
|
id: number;
|
|
name: string;
|
|
weightGrams: number | null;
|
|
priceCents: number | null;
|
|
quantity: number;
|
|
categoryId: number;
|
|
notes: string | null;
|
|
productUrl: string | null;
|
|
imageFilename: string | null;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
categoryName: string;
|
|
categoryIcon: string;
|
|
classification: string;
|
|
}
|
|
|
|
interface SetupWithItems {
|
|
id: number;
|
|
name: string;
|
|
visibility: "private" | "link" | "public";
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
items: SetupItemWithCategory[];
|
|
}
|
|
|
|
export type { SetupItemWithCategory, SetupListItem, SetupWithItems };
|
|
|
|
export function useSetups() {
|
|
return useQuery({
|
|
queryKey: ["setups"],
|
|
queryFn: () => apiGet<SetupListItem[]>("/api/setups"),
|
|
});
|
|
}
|
|
|
|
export function useSetup(setupId: number | null) {
|
|
return useQuery({
|
|
queryKey: ["setups", setupId],
|
|
queryFn: () => apiGet<SetupWithItems>(`/api/setups/${setupId}`),
|
|
enabled: setupId != null,
|
|
retry: (count, error) =>
|
|
error instanceof ApiError && error.status === 404 ? false : count < 3,
|
|
});
|
|
}
|
|
|
|
export function usePublicSetup(setupId: number | null) {
|
|
return useQuery({
|
|
queryKey: ["setups", setupId, "public"],
|
|
queryFn: () => apiGet<SetupWithItems>(`/api/setups/${setupId}/public`),
|
|
enabled: setupId != null,
|
|
retry: (count, error) =>
|
|
error instanceof ApiError && error.status === 404 ? false : count < 3,
|
|
});
|
|
}
|
|
|
|
export function useSharedSetup(token: string | null) {
|
|
return useQuery({
|
|
queryKey: ["shared-setup", token],
|
|
queryFn: () => apiGet<SetupWithItems>(`/api/shared/${token}`),
|
|
enabled: !!token,
|
|
retry: false,
|
|
});
|
|
}
|
|
|
|
interface SetupItem {
|
|
id: number;
|
|
name: string;
|
|
brand?: string | null;
|
|
weightGrams: number | null;
|
|
priceCents: number | null;
|
|
quantity: number;
|
|
categoryId: number;
|
|
notes: string | null;
|
|
productUrl: string | null;
|
|
imageFilename: string | null;
|
|
imageUrl?: string | null;
|
|
globalItemId: number | null;
|
|
createdAt: string;
|
|
categoryName: string;
|
|
categoryIcon: string;
|
|
classification: string;
|
|
}
|
|
|
|
export function usePublicSetupItem(
|
|
setupId: number | null,
|
|
itemId: number | null,
|
|
) {
|
|
return useQuery({
|
|
queryKey: ["setups", setupId, "items", itemId, "public"],
|
|
queryFn: () =>
|
|
apiGet<SetupItem>(`/api/setups/${setupId}/items/${itemId}/public`),
|
|
enabled: setupId != null && itemId != null,
|
|
retry: (count, error) =>
|
|
error instanceof ApiError && error.status === 404 ? false : count < 3,
|
|
});
|
|
}
|
|
|
|
export function useSharedSetupItem(
|
|
token: string | null,
|
|
itemId: number | null,
|
|
) {
|
|
return useQuery({
|
|
queryKey: ["shared-setup", token, "items", itemId],
|
|
queryFn: () => apiGet<SetupItem>(`/api/shared/${token}/items/${itemId}`),
|
|
enabled: !!token && itemId != null,
|
|
retry: false,
|
|
});
|
|
}
|
|
|
|
export function useCreateSetup() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (data: { name: string }) =>
|
|
apiPost<SetupListItem>("/api/setups", data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["setups"] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useUpdateSetup(setupId: number) {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (data: {
|
|
name?: string;
|
|
visibility?: "private" | "link" | "public";
|
|
}) => apiPut<SetupListItem>(`/api/setups/${setupId}`, data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["setups"] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useDeleteSetup() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (id: number) =>
|
|
apiDelete<{ success: boolean }>(`/api/setups/${id}`),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["setups"] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useSyncSetupItems(setupId: number) {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (itemIds: number[]) =>
|
|
apiPut<{ success: boolean }>(`/api/setups/${setupId}/items`, { itemIds }),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["setups"] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useRemoveSetupItem(setupId: number) {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (itemId: number) =>
|
|
apiDelete<{ success: boolean }>(`/api/setups/${setupId}/items/${itemId}`),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["setups"] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useUpdateItemClassification(setupId: number) {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: ({
|
|
itemId,
|
|
classification,
|
|
}: {
|
|
itemId: number;
|
|
classification: string;
|
|
}) =>
|
|
apiPatch<{ success: boolean }>(
|
|
`/api/setups/${setupId}/items/${itemId}/classification`,
|
|
{ classification },
|
|
),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["setups", setupId] });
|
|
},
|
|
});
|
|
}
|