claude 1
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 43s

This commit is contained in:
2026-01-08 07:47:29 -08:00
parent ab63f83f50
commit d356d9dfb6
45 changed files with 4022 additions and 286 deletions

View File

@@ -0,0 +1,60 @@
// src/hooks/mutations/useAddWatchedItemMutation.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import * as apiClient from '../../services/apiClient';
import { notifySuccess, notifyError } from '../../services/notificationService';
interface AddWatchedItemParams {
itemName: string;
category?: string;
}
/**
* Mutation hook for adding an item to the user's watched items list.
*
* This hook provides optimistic updates and automatic cache invalidation.
* When the mutation succeeds, it invalidates the watched-items query to
* trigger a refetch of the updated list.
*
* @returns Mutation object with mutate function and state
*
* @example
* ```tsx
* const addWatchedItem = useAddWatchedItemMutation();
*
* const handleAdd = () => {
* addWatchedItem.mutate(
* { itemName: 'Milk', category: 'Dairy' },
* {
* onSuccess: () => console.log('Added!'),
* onError: (error) => console.error(error),
* }
* );
* };
* ```
*/
export const useAddWatchedItemMutation = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ itemName, category }: AddWatchedItemParams) => {
const response = await apiClient.addWatchedItem(itemName, category);
if (!response.ok) {
const error = await response.json().catch(() => ({
message: `Request failed with status ${response.status}`,
}));
throw new Error(error.message || 'Failed to add watched item');
}
return response.json();
},
onSuccess: () => {
// Invalidate and refetch watched items to get the updated list
queryClient.invalidateQueries({ queryKey: ['watched-items'] });
notifySuccess('Item added to watched list');
},
onError: (error: Error) => {
notifyError(error.message || 'Failed to add item to watched list');
},
});
};

View File

@@ -0,0 +1,46 @@
// src/hooks/queries/useFlyerItemsQuery.ts
import { useQuery } from '@tanstack/react-query';
import * as apiClient from '../../services/apiClient';
import type { FlyerItem } from '../../types';
/**
* Query hook for fetching items for a specific flyer.
*
* This hook is automatically disabled when no flyer ID is provided,
* and caches data per-flyer to avoid refetching the same data.
*
* @param flyerId - The ID of the flyer to fetch items for
* @returns Query result with flyer items data, loading state, and error state
*
* @example
* ```tsx
* const { data: flyerItems, isLoading, error } = useFlyerItemsQuery(flyer?.flyer_id);
* ```
*/
export const useFlyerItemsQuery = (flyerId: number | undefined) => {
return useQuery({
queryKey: ['flyer-items', flyerId],
queryFn: async (): Promise<FlyerItem[]> => {
if (!flyerId) {
throw new Error('Flyer ID is required');
}
const response = await apiClient.fetchFlyerItems(flyerId);
if (!response.ok) {
const error = await response.json().catch(() => ({
message: `Request failed with status ${response.status}`,
}));
throw new Error(error.message || 'Failed to fetch flyer items');
}
const data = await response.json();
// API returns { items: FlyerItem[] }
return data.items || [];
},
// Only run the query if we have a valid flyer ID
enabled: !!flyerId,
// Flyer items don't change, so cache them longer
staleTime: 1000 * 60 * 5,
});
};

View File

@@ -0,0 +1,39 @@
// src/hooks/queries/useFlyersQuery.ts
import { useQuery } from '@tanstack/react-query';
import * as apiClient from '../../services/apiClient';
import type { Flyer } from '../../types';
/**
* Query hook for fetching flyers with pagination.
*
* This replaces the custom useInfiniteQuery hook with TanStack Query,
* providing automatic caching, background refetching, and better state management.
*
* @param limit - Maximum number of flyers to fetch
* @param offset - Number of flyers to skip
* @returns Query result with flyers data, loading state, and error state
*
* @example
* ```tsx
* const { data: flyers, isLoading, error, refetch } = useFlyersQuery(20, 0);
* ```
*/
export const useFlyersQuery = (limit: number = 20, offset: number = 0) => {
return useQuery({
queryKey: ['flyers', { limit, offset }],
queryFn: async (): Promise<Flyer[]> => {
const response = await apiClient.fetchFlyers(limit, offset);
if (!response.ok) {
const error = await response.json().catch(() => ({
message: `Request failed with status ${response.status}`,
}));
throw new Error(error.message || 'Failed to fetch flyers');
}
return response.json();
},
// Keep data fresh for 2 minutes since flyers don't change frequently
staleTime: 1000 * 60 * 2,
});
};

View File

@@ -0,0 +1,40 @@
// src/hooks/queries/useMasterItemsQuery.ts
import { useQuery } from '@tanstack/react-query';
import * as apiClient from '../../services/apiClient';
import type { MasterGroceryItem } from '../../types';
/**
* Query hook for fetching all master grocery items.
*
* Master items are the canonical list of grocery items that users can watch
* and that flyer items are mapped to. This data changes infrequently, so it's
* cached with a longer stale time.
*
* @returns Query result with master items data, loading state, and error state
*
* @example
* ```tsx
* const { data: masterItems, isLoading, error } = useMasterItemsQuery();
* ```
*/
export const useMasterItemsQuery = () => {
return useQuery({
queryKey: ['master-items'],
queryFn: async (): Promise<MasterGroceryItem[]> => {
const response = await apiClient.fetchMasterItems();
if (!response.ok) {
const error = await response.json().catch(() => ({
message: `Request failed with status ${response.status}`,
}));
throw new Error(error.message || 'Failed to fetch master items');
}
return response.json();
},
// Master items change infrequently, keep data fresh for 10 minutes
staleTime: 1000 * 60 * 10,
// Cache for 30 minutes
gcTime: 1000 * 60 * 30,
});
};

View File

@@ -0,0 +1,39 @@
// src/hooks/queries/useShoppingListsQuery.ts
import { useQuery } from '@tantml:parameter>
import * as apiClient from '../../services/apiClient';
import type { ShoppingList } from '../../types';
/**
* Query hook for fetching the user's shopping lists.
*
* This hook is automatically disabled when the user is not authenticated,
* and the cached data is invalidated when the user logs out.
*
* @param enabled - Whether the query should run (typically based on auth status)
* @returns Query result with shopping lists data, loading state, and error state
*
* @example
* ```tsx
* const { data: shoppingLists, isLoading, error } = useShoppingListsQuery(!!user);
* ```
*/
export const useShoppingListsQuery = (enabled: boolean) => {
return useQuery({
queryKey: ['shopping-lists'],
queryFn: async (): Promise<ShoppingList[]> => {
const response = await apiClient.fetchShoppingLists();
if (!response.ok) {
const error = await response.json().catch(() => ({
message: `Request failed with status ${response.status}`,
}));
throw new Error(error.message || 'Failed to fetch shopping lists');
}
return response.json();
},
enabled,
// Keep data fresh for 1 minute since users actively manage shopping lists
staleTime: 1000 * 60,
});
};

View File

@@ -0,0 +1,39 @@
// src/hooks/queries/useWatchedItemsQuery.ts
import { useQuery } from '@tanstack/react-query';
import * as apiClient from '../../services/apiClient';
import type { MasterGroceryItem } from '../../types';
/**
* Query hook for fetching the user's watched items.
*
* This hook is automatically disabled when the user is not authenticated,
* and the cached data is invalidated when the user logs out.
*
* @param enabled - Whether the query should run (typically based on auth status)
* @returns Query result with watched items data, loading state, and error state
*
* @example
* ```tsx
* const { data: watchedItems, isLoading, error } = useWatchedItemsQuery(!!user);
* ```
*/
export const useWatchedItemsQuery = (enabled: boolean) => {
return useQuery({
queryKey: ['watched-items'],
queryFn: async (): Promise<MasterGroceryItem[]> => {
const response = await apiClient.fetchWatchedItems();
if (!response.ok) {
const error = await response.json().catch(() => ({
message: `Request failed with status ${response.status}`,
}));
throw new Error(error.message || 'Failed to fetch watched items');
}
return response.json();
},
enabled,
// Keep data fresh for 1 minute since users actively manage watched items
staleTime: 1000 * 60,
});
};

View File

@@ -1,28 +1,31 @@
// src/hooks/useFlyerItems.ts
import type { Flyer, FlyerItem } from '../types';
import { useApiOnMount } from './useApiOnMount';
import * as apiClient from '../services/apiClient';
import type { Flyer } from '../types';
import { useFlyerItemsQuery } from './queries/useFlyerItemsQuery';
/**
* A custom hook to fetch the items for a given flyer.
* A custom hook to fetch the items for a given flyer using TanStack Query (ADR-0005).
*
* This replaces the previous useApiOnMount implementation with TanStack Query
* for automatic caching and better state management.
*
* @param selectedFlyer The flyer for which to fetch items.
* @returns An object containing the flyer items, loading state, and any errors.
*
* @example
* ```tsx
* const { flyerItems, isLoading, error } = useFlyerItems(selectedFlyer);
* ```
*/
export const useFlyerItems = (selectedFlyer: Flyer | null) => {
const wrappedFetcher = (flyerId?: number): Promise<Response> => {
// This should not be called with undefined due to the `enabled` flag,
// but this wrapper satisfies the type checker.
if (flyerId === undefined) {
return Promise.reject(new Error('Cannot fetch items for an undefined flyer ID.'));
}
return apiClient.fetchFlyerItems(flyerId);
};
const {
data: flyerItems = [],
isLoading,
error,
} = useFlyerItemsQuery(selectedFlyer?.flyer_id);
const { data, loading, error } = useApiOnMount<{ items: FlyerItem[] }, [number?]>(
wrappedFetcher,
[selectedFlyer],
{ enabled: !!selectedFlyer },
selectedFlyer?.flyer_id,
);
return { flyerItems: data?.items || [], isLoading: loading, error };
return {
flyerItems,
isLoading,
error,
};
};