diff --git a/src/config/queryKeys.ts b/src/config/queryKeys.ts new file mode 100644 index 00000000..dbe3a8b5 --- /dev/null +++ b/src/config/queryKeys.ts @@ -0,0 +1,84 @@ +// src/config/queryKeys.ts +/** + * Centralized query keys for TanStack Query. + * + * This file provides a single source of truth for all query keys used + * throughout the application. Using these factory functions ensures + * consistent key naming and proper cache invalidation. + * + * @example + * ```tsx + * // In a query hook + * useQuery({ + * queryKey: queryKeys.flyers(10, 0), + * queryFn: fetchFlyers, + * }); + * + * // For cache invalidation + * queryClient.invalidateQueries({ queryKey: queryKeys.watchedItems() }); + * ``` + */ +export const queryKeys = { + // User Features + flyers: (limit: number, offset: number) => ['flyers', { limit, offset }] as const, + flyerItems: (flyerId: number) => ['flyer-items', flyerId] as const, + flyerItemsBatch: (flyerIds: number[]) => + ['flyer-items-batch', flyerIds.sort().join(',')] as const, + flyerItemsCount: (flyerIds: number[]) => + ['flyer-items-count', flyerIds.sort().join(',')] as const, + masterItems: () => ['master-items'] as const, + watchedItems: () => ['watched-items'] as const, + shoppingLists: () => ['shopping-lists'] as const, + + // Auth & Profile + authProfile: () => ['auth-profile'] as const, + userAddress: (addressId: number | null) => ['user-address', addressId] as const, + userProfileData: () => ['user-profile-data'] as const, + + // Admin Features + activityLog: (limit: number, offset: number) => ['activity-log', { limit, offset }] as const, + applicationStats: () => ['application-stats'] as const, + suggestedCorrections: () => ['suggested-corrections'] as const, + categories: () => ['categories'] as const, + + // Analytics + bestSalePrices: () => ['best-sale-prices'] as const, + priceHistory: (masterItemIds: number[]) => + ['price-history', [...masterItemIds].sort((a, b) => a - b).join(',')] as const, + leaderboard: (limit: number) => ['leaderboard', limit] as const, +} as const; + +/** + * Base keys for partial matching in cache invalidation. + * + * Use these when you need to invalidate all queries of a certain type + * regardless of their parameters. + * + * @example + * ```tsx + * // Invalidate all flyer-related queries + * queryClient.invalidateQueries({ queryKey: queryKeyBases.flyers }); + * ``` + */ +export const queryKeyBases = { + flyers: ['flyers'] as const, + flyerItems: ['flyer-items'] as const, + flyerItemsBatch: ['flyer-items-batch'] as const, + flyerItemsCount: ['flyer-items-count'] as const, + masterItems: ['master-items'] as const, + watchedItems: ['watched-items'] as const, + shoppingLists: ['shopping-lists'] as const, + authProfile: ['auth-profile'] as const, + userAddress: ['user-address'] as const, + userProfileData: ['user-profile-data'] as const, + activityLog: ['activity-log'] as const, + applicationStats: ['application-stats'] as const, + suggestedCorrections: ['suggested-corrections'] as const, + categories: ['categories'] as const, + bestSalePrices: ['best-sale-prices'] as const, + priceHistory: ['price-history'] as const, + leaderboard: ['leaderboard'] as const, +} as const; + +export type QueryKeys = typeof queryKeys; +export type QueryKeyBases = typeof queryKeyBases; diff --git a/src/hooks/mutations/useAddShoppingListItemMutation.ts b/src/hooks/mutations/useAddShoppingListItemMutation.ts index 44e6bdfc..4ab0270e 100644 --- a/src/hooks/mutations/useAddShoppingListItemMutation.ts +++ b/src/hooks/mutations/useAddShoppingListItemMutation.ts @@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import * as apiClient from '../../services/apiClient'; import { notifySuccess, notifyError } from '../../services/notificationService'; +import { queryKeyBases } from '../../config/queryKeys'; interface AddShoppingListItemParams { listId: number; @@ -61,7 +62,7 @@ export const useAddShoppingListItemMutation = () => { }, onSuccess: () => { // Invalidate and refetch shopping lists to get the updated list - queryClient.invalidateQueries({ queryKey: ['shopping-lists'] }); + queryClient.invalidateQueries({ queryKey: queryKeyBases.shoppingLists }); notifySuccess('Item added to shopping list'); }, onError: (error: Error) => { diff --git a/src/hooks/mutations/useAddWatchedItemMutation.ts b/src/hooks/mutations/useAddWatchedItemMutation.ts index b6934a9e..f52a0352 100644 --- a/src/hooks/mutations/useAddWatchedItemMutation.ts +++ b/src/hooks/mutations/useAddWatchedItemMutation.ts @@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import * as apiClient from '../../services/apiClient'; import { notifySuccess, notifyError } from '../../services/notificationService'; +import { queryKeyBases } from '../../config/queryKeys'; interface AddWatchedItemParams { itemName: string; @@ -50,7 +51,7 @@ export const useAddWatchedItemMutation = () => { }, onSuccess: () => { // Invalidate and refetch watched items to get the updated list - queryClient.invalidateQueries({ queryKey: ['watched-items'] }); + queryClient.invalidateQueries({ queryKey: queryKeyBases.watchedItems }); notifySuccess('Item added to watched list'); }, onError: (error: Error) => { diff --git a/src/hooks/mutations/useAuthMutations.ts b/src/hooks/mutations/useAuthMutations.ts new file mode 100644 index 00000000..1e73ce5b --- /dev/null +++ b/src/hooks/mutations/useAuthMutations.ts @@ -0,0 +1,113 @@ +// src/hooks/mutations/useAuthMutations.ts +import { useMutation } from '@tanstack/react-query'; +import * as apiClient from '../../services/apiClient'; +import { notifyError } from '../../services/notificationService'; +import type { UserProfile } from '../../types'; + +interface AuthResponse { + userprofile: UserProfile; + token: string; +} + +/** + * Mutation hook for user login. + * + * @example + * ```tsx + * const loginMutation = useLoginMutation(); + * loginMutation.mutate({ email, password, rememberMe }); + * ``` + */ +export const useLoginMutation = () => { + return useMutation({ + mutationFn: async ({ + email, + password, + rememberMe, + }: { + email: string; + password: string; + rememberMe: boolean; + }): Promise => { + const response = await apiClient.loginUser(email, password, rememberMe); + + if (!response.ok) { + const error = await response.json().catch(() => ({ + message: `Request failed with status ${response.status}`, + })); + throw new Error(error.message || 'Failed to login'); + } + + return response.json(); + }, + onError: (error: Error) => { + notifyError(error.message || 'Failed to login'); + }, + }); +}; + +/** + * Mutation hook for user registration. + * + * @example + * ```tsx + * const registerMutation = useRegisterMutation(); + * registerMutation.mutate({ email, password, fullName }); + * ``` + */ +export const useRegisterMutation = () => { + return useMutation({ + mutationFn: async ({ + email, + password, + fullName, + }: { + email: string; + password: string; + fullName: string; + }): Promise => { + const response = await apiClient.registerUser(email, password, fullName, ''); + + if (!response.ok) { + const error = await response.json().catch(() => ({ + message: `Request failed with status ${response.status}`, + })); + throw new Error(error.message || 'Failed to register'); + } + + return response.json(); + }, + onError: (error: Error) => { + notifyError(error.message || 'Failed to register'); + }, + }); +}; + +/** + * Mutation hook for requesting a password reset. + * + * @example + * ```tsx + * const passwordResetMutation = usePasswordResetRequestMutation(); + * passwordResetMutation.mutate({ email }); + * ``` + */ +export const usePasswordResetRequestMutation = () => { + return useMutation({ + mutationFn: async ({ email }: { email: string }): Promise<{ message: string }> => { + const response = await apiClient.requestPasswordReset(email); + + if (!response.ok) { + const error = await response.json().catch(() => ({ + message: `Request failed with status ${response.status}`, + })); + throw new Error(error.message || 'Failed to request password reset'); + } + + return response.json(); + }, + onError: (error: Error) => { + notifyError(error.message || 'Failed to request password reset'); + }, + }); +}; diff --git a/src/hooks/mutations/useCreateShoppingListMutation.ts b/src/hooks/mutations/useCreateShoppingListMutation.ts index 67522c7d..33626f4c 100644 --- a/src/hooks/mutations/useCreateShoppingListMutation.ts +++ b/src/hooks/mutations/useCreateShoppingListMutation.ts @@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import * as apiClient from '../../services/apiClient'; import { notifySuccess, notifyError } from '../../services/notificationService'; +import { queryKeyBases } from '../../config/queryKeys'; interface CreateShoppingListParams { name: string; @@ -48,7 +49,7 @@ export const useCreateShoppingListMutation = () => { }, onSuccess: () => { // Invalidate and refetch shopping lists to get the updated list - queryClient.invalidateQueries({ queryKey: ['shopping-lists'] }); + queryClient.invalidateQueries({ queryKey: queryKeyBases.shoppingLists }); notifySuccess('Shopping list created'); }, onError: (error: Error) => { diff --git a/src/hooks/mutations/useDeleteShoppingListMutation.ts b/src/hooks/mutations/useDeleteShoppingListMutation.ts index 4c5f1f08..825ddaa4 100644 --- a/src/hooks/mutations/useDeleteShoppingListMutation.ts +++ b/src/hooks/mutations/useDeleteShoppingListMutation.ts @@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import * as apiClient from '../../services/apiClient'; import { notifySuccess, notifyError } from '../../services/notificationService'; +import { queryKeyBases } from '../../config/queryKeys'; interface DeleteShoppingListParams { listId: number; @@ -48,7 +49,7 @@ export const useDeleteShoppingListMutation = () => { }, onSuccess: () => { // Invalidate and refetch shopping lists to get the updated list - queryClient.invalidateQueries({ queryKey: ['shopping-lists'] }); + queryClient.invalidateQueries({ queryKey: queryKeyBases.shoppingLists }); notifySuccess('Shopping list deleted'); }, onError: (error: Error) => { diff --git a/src/hooks/mutations/useProfileMutations.ts b/src/hooks/mutations/useProfileMutations.ts new file mode 100644 index 00000000..0d6be202 --- /dev/null +++ b/src/hooks/mutations/useProfileMutations.ts @@ -0,0 +1,179 @@ +// src/hooks/mutations/useProfileMutations.ts +import { useMutation } from '@tanstack/react-query'; +import * as apiClient from '../../services/apiClient'; +import { notifyError } from '../../services/notificationService'; +import type { Profile, Address } from '../../types'; + +/** + * Mutation hook for updating user profile. + * + * @example + * ```tsx + * const updateProfile = useUpdateProfileMutation(); + * updateProfile.mutate({ full_name: 'New Name', avatar_url: 'https://...' }); + * ``` + */ +export const useUpdateProfileMutation = () => { + return useMutation({ + mutationFn: async (data: Partial): Promise => { + const response = await apiClient.updateUserProfile(data); + + if (!response.ok) { + const error = await response.json().catch(() => ({ + message: `Request failed with status ${response.status}`, + })); + throw new Error(error.message || 'Failed to update profile'); + } + + return response.json(); + }, + onError: (error: Error) => { + notifyError(error.message || 'Failed to update profile'); + }, + }); +}; + +/** + * Mutation hook for updating user address. + * + * @example + * ```tsx + * const updateAddress = useUpdateAddressMutation(); + * updateAddress.mutate({ street_address: '123 Main St', city: 'Toronto' }); + * ``` + */ +export const useUpdateAddressMutation = () => { + return useMutation({ + mutationFn: async (data: Partial
): Promise
=> { + const response = await apiClient.updateUserAddress(data); + + if (!response.ok) { + const error = await response.json().catch(() => ({ + message: `Request failed with status ${response.status}`, + })); + throw new Error(error.message || 'Failed to update address'); + } + + return response.json(); + }, + onError: (error: Error) => { + notifyError(error.message || 'Failed to update address'); + }, + }); +}; + +/** + * Mutation hook for updating user password. + * + * @example + * ```tsx + * const updatePassword = useUpdatePasswordMutation(); + * updatePassword.mutate({ password: 'newPassword123' }); + * ``` + */ +export const useUpdatePasswordMutation = () => { + return useMutation({ + mutationFn: async ({ password }: { password: string }): Promise => { + const response = await apiClient.updateUserPassword(password); + + if (!response.ok) { + const error = await response.json().catch(() => ({ + message: `Request failed with status ${response.status}`, + })); + throw new Error(error.message || 'Failed to update password'); + } + + return response.json(); + }, + onError: (error: Error) => { + notifyError(error.message || 'Failed to update password'); + }, + }); +}; + +/** + * Mutation hook for updating user preferences. + * + * @example + * ```tsx + * const updatePreferences = useUpdatePreferencesMutation(); + * updatePreferences.mutate({ darkMode: true }); + * ``` + */ +export const useUpdatePreferencesMutation = () => { + return useMutation({ + mutationFn: async (prefs: Partial): Promise => { + const response = await apiClient.updateUserPreferences(prefs); + + if (!response.ok) { + const error = await response.json().catch(() => ({ + message: `Request failed with status ${response.status}`, + })); + throw new Error(error.message || 'Failed to update preferences'); + } + + return response.json(); + }, + onError: (error: Error) => { + notifyError(error.message || 'Failed to update preferences'); + }, + }); +}; + +/** + * Mutation hook for exporting user data. + * + * @example + * ```tsx + * const exportData = useExportDataMutation(); + * exportData.mutate(); + * ``` + */ +export const useExportDataMutation = () => { + return useMutation({ + mutationFn: async (): Promise => { + const response = await apiClient.exportUserData(); + + if (!response.ok) { + const error = await response.json().catch(() => ({ + message: `Request failed with status ${response.status}`, + })); + throw new Error(error.message || 'Failed to export data'); + } + + return response.json(); + }, + onError: (error: Error) => { + notifyError(error.message || 'Failed to export data'); + }, + }); +}; + +/** + * Mutation hook for deleting user account. + * + * @example + * ```tsx + * const deleteAccount = useDeleteAccountMutation(); + * deleteAccount.mutate({ password: 'currentPassword' }); + * ``` + */ +export const useDeleteAccountMutation = () => { + return useMutation({ + mutationFn: async ({ password }: { password: string }): Promise => { + const response = await apiClient.deleteUserAccount(password); + + if (!response.ok) { + const error = await response.json().catch(() => ({ + message: `Request failed with status ${response.status}`, + })); + throw new Error(error.message || 'Failed to delete account'); + } + + return response.json(); + }, + onError: (error: Error) => { + notifyError(error.message || 'Failed to delete account'); + }, + }); +}; diff --git a/src/hooks/mutations/useRemoveShoppingListItemMutation.ts b/src/hooks/mutations/useRemoveShoppingListItemMutation.ts index e653df93..9819927b 100644 --- a/src/hooks/mutations/useRemoveShoppingListItemMutation.ts +++ b/src/hooks/mutations/useRemoveShoppingListItemMutation.ts @@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import * as apiClient from '../../services/apiClient'; import { notifySuccess, notifyError } from '../../services/notificationService'; +import { queryKeyBases } from '../../config/queryKeys'; interface RemoveShoppingListItemParams { itemId: number; @@ -48,7 +49,7 @@ export const useRemoveShoppingListItemMutation = () => { }, onSuccess: () => { // Invalidate and refetch shopping lists to get the updated list - queryClient.invalidateQueries({ queryKey: ['shopping-lists'] }); + queryClient.invalidateQueries({ queryKey: queryKeyBases.shoppingLists }); notifySuccess('Item removed from shopping list'); }, onError: (error: Error) => { diff --git a/src/hooks/mutations/useRemoveWatchedItemMutation.ts b/src/hooks/mutations/useRemoveWatchedItemMutation.ts index cdf5ad93..0537d1ea 100644 --- a/src/hooks/mutations/useRemoveWatchedItemMutation.ts +++ b/src/hooks/mutations/useRemoveWatchedItemMutation.ts @@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import * as apiClient from '../../services/apiClient'; import { notifySuccess, notifyError } from '../../services/notificationService'; +import { queryKeyBases } from '../../config/queryKeys'; interface RemoveWatchedItemParams { masterItemId: number; @@ -48,7 +49,7 @@ export const useRemoveWatchedItemMutation = () => { }, onSuccess: () => { // Invalidate and refetch watched items to get the updated list - queryClient.invalidateQueries({ queryKey: ['watched-items'] }); + queryClient.invalidateQueries({ queryKey: queryKeyBases.watchedItems }); notifySuccess('Item removed from watched list'); }, onError: (error: Error) => { diff --git a/src/hooks/mutations/useUpdateShoppingListItemMutation.ts b/src/hooks/mutations/useUpdateShoppingListItemMutation.ts index d46e630f..bc1d73f3 100644 --- a/src/hooks/mutations/useUpdateShoppingListItemMutation.ts +++ b/src/hooks/mutations/useUpdateShoppingListItemMutation.ts @@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import * as apiClient from '../../services/apiClient'; import { notifySuccess, notifyError } from '../../services/notificationService'; +import { queryKeyBases } from '../../config/queryKeys'; import type { ShoppingListItem } from '../../types'; interface UpdateShoppingListItemParams { @@ -60,7 +61,7 @@ export const useUpdateShoppingListItemMutation = () => { }, onSuccess: () => { // Invalidate and refetch shopping lists to get the updated list - queryClient.invalidateQueries({ queryKey: ['shopping-lists'] }); + queryClient.invalidateQueries({ queryKey: queryKeyBases.shoppingLists }); notifySuccess('Shopping list item updated'); }, onError: (error: Error) => { diff --git a/src/hooks/queries/useActivityLogQuery.ts b/src/hooks/queries/useActivityLogQuery.ts index 418791d1..e077bac7 100644 --- a/src/hooks/queries/useActivityLogQuery.ts +++ b/src/hooks/queries/useActivityLogQuery.ts @@ -1,6 +1,7 @@ // src/hooks/queries/useActivityLogQuery.ts import { useQuery } from '@tanstack/react-query'; import { fetchActivityLog } from '../../services/apiClient'; +import { queryKeys } from '../../config/queryKeys'; import type { ActivityLogItem } from '../../types'; /** @@ -21,7 +22,7 @@ import type { ActivityLogItem } from '../../types'; */ export const useActivityLogQuery = (limit: number = 20, offset: number = 0) => { return useQuery({ - queryKey: ['activity-log', { limit, offset }], + queryKey: queryKeys.activityLog(limit, offset), queryFn: async (): Promise => { const response = await fetchActivityLog(limit, offset); diff --git a/src/hooks/queries/useApplicationStatsQuery.ts b/src/hooks/queries/useApplicationStatsQuery.ts index b5160082..f5647007 100644 --- a/src/hooks/queries/useApplicationStatsQuery.ts +++ b/src/hooks/queries/useApplicationStatsQuery.ts @@ -1,6 +1,7 @@ // src/hooks/queries/useApplicationStatsQuery.ts import { useQuery } from '@tanstack/react-query'; import { getApplicationStats, AppStats } from '../../services/apiClient'; +import { queryKeys } from '../../config/queryKeys'; /** * Query hook for fetching application-wide statistics (admin feature). @@ -19,7 +20,7 @@ import { getApplicationStats, AppStats } from '../../services/apiClient'; */ export const useApplicationStatsQuery = () => { return useQuery({ - queryKey: ['application-stats'], + queryKey: queryKeys.applicationStats(), queryFn: async (): Promise => { const response = await getApplicationStats(); diff --git a/src/hooks/queries/useAuthProfileQuery.ts b/src/hooks/queries/useAuthProfileQuery.ts index 027a3e5c..09479229 100644 --- a/src/hooks/queries/useAuthProfileQuery.ts +++ b/src/hooks/queries/useAuthProfileQuery.ts @@ -2,13 +2,15 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'; import { getAuthenticatedUserProfile } from '../../services/apiClient'; import { getToken } from '../../services/tokenStorage'; +import { queryKeys, queryKeyBases } from '../../config/queryKeys'; import type { UserProfile } from '../../types'; /** * Query key for the authenticated user's profile. * Exported for cache invalidation purposes. + * @deprecated Use queryKeys.authProfile() from '../../config/queryKeys' instead */ -export const AUTH_PROFILE_QUERY_KEY = ['auth-profile'] as const; +export const AUTH_PROFILE_QUERY_KEY = queryKeys.authProfile(); /** * Query hook for fetching the authenticated user's profile. @@ -28,7 +30,7 @@ export const useAuthProfileQuery = (enabled: boolean = true) => { const hasToken = !!getToken(); return useQuery({ - queryKey: AUTH_PROFILE_QUERY_KEY, + queryKey: queryKeys.authProfile(), queryFn: async (): Promise => { const response = await getAuthenticatedUserProfile(); @@ -55,6 +57,6 @@ export const useInvalidateAuthProfile = () => { const queryClient = useQueryClient(); return () => { - queryClient.invalidateQueries({ queryKey: AUTH_PROFILE_QUERY_KEY }); + queryClient.invalidateQueries({ queryKey: queryKeyBases.authProfile }); }; }; diff --git a/src/hooks/queries/useBestSalePricesQuery.ts b/src/hooks/queries/useBestSalePricesQuery.ts index 27b24434..275bdd68 100644 --- a/src/hooks/queries/useBestSalePricesQuery.ts +++ b/src/hooks/queries/useBestSalePricesQuery.ts @@ -1,6 +1,7 @@ // src/hooks/queries/useBestSalePricesQuery.ts import { useQuery } from '@tanstack/react-query'; import { fetchBestSalePrices } from '../../services/apiClient'; +import { queryKeys } from '../../config/queryKeys'; import type { WatchedItemDeal } from '../../types'; /** @@ -19,7 +20,7 @@ import type { WatchedItemDeal } from '../../types'; */ export const useBestSalePricesQuery = (enabled: boolean = true) => { return useQuery({ - queryKey: ['best-sale-prices'], + queryKey: queryKeys.bestSalePrices(), queryFn: async (): Promise => { const response = await fetchBestSalePrices(); diff --git a/src/hooks/queries/useBrandsQuery.ts b/src/hooks/queries/useBrandsQuery.ts new file mode 100644 index 00000000..b9078ee9 --- /dev/null +++ b/src/hooks/queries/useBrandsQuery.ts @@ -0,0 +1,35 @@ +// src/hooks/queries/useBrandsQuery.ts +import { useQuery } from '@tanstack/react-query'; +import { fetchAllBrands } from '../../services/apiClient'; +import type { Brand } from '../../types'; + +/** + * Query hook for fetching all brands (admin feature). + * + * @param enabled - Whether the query should run (default: true) + * @returns TanStack Query result with Brand[] data + * + * @example + * ```tsx + * const { data: brands = [], isLoading, error } = useBrandsQuery(); + * ``` + */ +export const useBrandsQuery = (enabled: boolean = true) => { + return useQuery({ + queryKey: ['brands'], + queryFn: async (): Promise => { + const response = await fetchAllBrands(); + + if (!response.ok) { + const error = await response.json().catch(() => ({ + message: `Request failed with status ${response.status}`, + })); + throw new Error(error.message || 'Failed to fetch brands'); + } + + return response.json(); + }, + enabled, + staleTime: 1000 * 60 * 5, // 5 minutes - brands don't change frequently + }); +}; diff --git a/src/hooks/queries/useCategoriesQuery.ts b/src/hooks/queries/useCategoriesQuery.ts index cc2cb0d3..62d2ed0b 100644 --- a/src/hooks/queries/useCategoriesQuery.ts +++ b/src/hooks/queries/useCategoriesQuery.ts @@ -1,6 +1,7 @@ // src/hooks/queries/useCategoriesQuery.ts import { useQuery } from '@tanstack/react-query'; import { fetchCategories } from '../../services/apiClient'; +import { queryKeys } from '../../config/queryKeys'; import type { Category } from '../../types'; /** @@ -14,7 +15,7 @@ import type { Category } from '../../types'; */ export const useCategoriesQuery = () => { return useQuery({ - queryKey: ['categories'], + queryKey: queryKeys.categories(), queryFn: async (): Promise => { const response = await fetchCategories(); diff --git a/src/hooks/queries/useFlyerItemCountQuery.ts b/src/hooks/queries/useFlyerItemCountQuery.ts index acf8bc21..54a86913 100644 --- a/src/hooks/queries/useFlyerItemCountQuery.ts +++ b/src/hooks/queries/useFlyerItemCountQuery.ts @@ -1,6 +1,7 @@ // src/hooks/queries/useFlyerItemCountQuery.ts import { useQuery } from '@tanstack/react-query'; import { countFlyerItemsForFlyers } from '../../services/apiClient'; +import { queryKeys } from '../../config/queryKeys'; interface FlyerItemCount { count: number; @@ -24,7 +25,7 @@ interface FlyerItemCount { export const useFlyerItemCountQuery = (flyerIds: number[], enabled: boolean = true) => { return useQuery({ // Include flyerIds in the key so cache is per-set of flyers - queryKey: ['flyer-items-count', flyerIds.sort().join(',')], + queryKey: queryKeys.flyerItemsCount(flyerIds), queryFn: async (): Promise => { if (flyerIds.length === 0) { return { count: 0 }; diff --git a/src/hooks/queries/useFlyerItemsForFlyersQuery.ts b/src/hooks/queries/useFlyerItemsForFlyersQuery.ts index b884fae1..de757310 100644 --- a/src/hooks/queries/useFlyerItemsForFlyersQuery.ts +++ b/src/hooks/queries/useFlyerItemsForFlyersQuery.ts @@ -1,6 +1,7 @@ // src/hooks/queries/useFlyerItemsForFlyersQuery.ts import { useQuery } from '@tanstack/react-query'; import { fetchFlyerItemsForFlyers } from '../../services/apiClient'; +import { queryKeys } from '../../config/queryKeys'; import type { FlyerItem } from '../../types'; /** @@ -21,7 +22,7 @@ import type { FlyerItem } from '../../types'; export const useFlyerItemsForFlyersQuery = (flyerIds: number[], enabled: boolean = true) => { return useQuery({ // Include flyerIds in the key so cache is per-set of flyers - queryKey: ['flyer-items-batch', flyerIds.sort().join(',')], + queryKey: queryKeys.flyerItemsBatch(flyerIds), queryFn: async (): Promise => { if (flyerIds.length === 0) { return []; diff --git a/src/hooks/queries/useFlyerItemsQuery.ts b/src/hooks/queries/useFlyerItemsQuery.ts index bf8b18a8..9dee1831 100644 --- a/src/hooks/queries/useFlyerItemsQuery.ts +++ b/src/hooks/queries/useFlyerItemsQuery.ts @@ -1,6 +1,7 @@ // src/hooks/queries/useFlyerItemsQuery.ts import { useQuery } from '@tanstack/react-query'; import * as apiClient from '../../services/apiClient'; +import { queryKeys } from '../../config/queryKeys'; import type { FlyerItem } from '../../types'; /** @@ -19,7 +20,7 @@ import type { FlyerItem } from '../../types'; */ export const useFlyerItemsQuery = (flyerId: number | undefined) => { return useQuery({ - queryKey: ['flyer-items', flyerId], + queryKey: queryKeys.flyerItems(flyerId as number), queryFn: async (): Promise => { if (!flyerId) { throw new Error('Flyer ID is required'); diff --git a/src/hooks/queries/useFlyersQuery.ts b/src/hooks/queries/useFlyersQuery.ts index 5e4f58c7..9db3c035 100644 --- a/src/hooks/queries/useFlyersQuery.ts +++ b/src/hooks/queries/useFlyersQuery.ts @@ -1,6 +1,7 @@ // src/hooks/queries/useFlyersQuery.ts import { useQuery } from '@tanstack/react-query'; import * as apiClient from '../../services/apiClient'; +import { queryKeys } from '../../config/queryKeys'; import type { Flyer } from '../../types'; /** @@ -20,7 +21,7 @@ import type { Flyer } from '../../types'; */ export const useFlyersQuery = (limit: number = 20, offset: number = 0) => { return useQuery({ - queryKey: ['flyers', { limit, offset }], + queryKey: queryKeys.flyers(limit, offset), queryFn: async (): Promise => { const response = await apiClient.fetchFlyers(limit, offset); diff --git a/src/hooks/queries/useLeaderboardQuery.ts b/src/hooks/queries/useLeaderboardQuery.ts index 7a9602a0..5b43fa84 100644 --- a/src/hooks/queries/useLeaderboardQuery.ts +++ b/src/hooks/queries/useLeaderboardQuery.ts @@ -1,6 +1,7 @@ // src/hooks/queries/useLeaderboardQuery.ts import { useQuery } from '@tanstack/react-query'; import { fetchLeaderboard } from '../../services/apiClient'; +import { queryKeys } from '../../config/queryKeys'; import type { LeaderboardUser } from '../../types'; /** @@ -17,7 +18,7 @@ import type { LeaderboardUser } from '../../types'; */ export const useLeaderboardQuery = (limit: number = 10, enabled: boolean = true) => { return useQuery({ - queryKey: ['leaderboard', limit], + queryKey: queryKeys.leaderboard(limit), queryFn: async (): Promise => { const response = await fetchLeaderboard(limit); diff --git a/src/hooks/queries/useMasterItemsQuery.ts b/src/hooks/queries/useMasterItemsQuery.ts index 5d4c1c03..ac77974c 100644 --- a/src/hooks/queries/useMasterItemsQuery.ts +++ b/src/hooks/queries/useMasterItemsQuery.ts @@ -1,6 +1,7 @@ // src/hooks/queries/useMasterItemsQuery.ts import { useQuery } from '@tanstack/react-query'; import * as apiClient from '../../services/apiClient'; +import { queryKeys } from '../../config/queryKeys'; import type { MasterGroceryItem } from '../../types'; /** @@ -19,7 +20,7 @@ import type { MasterGroceryItem } from '../../types'; */ export const useMasterItemsQuery = () => { return useQuery({ - queryKey: ['master-items'], + queryKey: queryKeys.masterItems(), queryFn: async (): Promise => { const response = await apiClient.fetchMasterItems(); diff --git a/src/hooks/queries/usePriceHistoryQuery.ts b/src/hooks/queries/usePriceHistoryQuery.ts index 172b5d9a..6fb23811 100644 --- a/src/hooks/queries/usePriceHistoryQuery.ts +++ b/src/hooks/queries/usePriceHistoryQuery.ts @@ -1,6 +1,7 @@ // src/hooks/queries/usePriceHistoryQuery.ts import { useQuery } from '@tanstack/react-query'; import { fetchHistoricalPriceData } from '../../services/apiClient'; +import { queryKeys } from '../../config/queryKeys'; import type { HistoricalPriceDataPoint } from '../../types'; /** @@ -17,11 +18,8 @@ import type { HistoricalPriceDataPoint } from '../../types'; * ``` */ export const usePriceHistoryQuery = (masterItemIds: number[], enabled: boolean = true) => { - // Sort IDs for stable query key - const sortedIds = [...masterItemIds].sort((a, b) => a - b); - return useQuery({ - queryKey: ['price-history', sortedIds.join(',')], + queryKey: queryKeys.priceHistory(masterItemIds), queryFn: async (): Promise => { if (masterItemIds.length === 0) { return []; diff --git a/src/hooks/queries/useShoppingListsQuery.ts b/src/hooks/queries/useShoppingListsQuery.ts index fcfb0af3..a780e92c 100644 --- a/src/hooks/queries/useShoppingListsQuery.ts +++ b/src/hooks/queries/useShoppingListsQuery.ts @@ -1,6 +1,7 @@ // src/hooks/queries/useShoppingListsQuery.ts import { useQuery } from '@tanstack/react-query'; import * as apiClient from '../../services/apiClient'; +import { queryKeys } from '../../config/queryKeys'; import type { ShoppingList } from '../../types'; /** @@ -19,7 +20,7 @@ import type { ShoppingList } from '../../types'; */ export const useShoppingListsQuery = (enabled: boolean) => { return useQuery({ - queryKey: ['shopping-lists'], + queryKey: queryKeys.shoppingLists(), queryFn: async (): Promise => { const response = await apiClient.fetchShoppingLists(); diff --git a/src/hooks/queries/useSuggestedCorrectionsQuery.ts b/src/hooks/queries/useSuggestedCorrectionsQuery.ts index 1aa7534f..1de53b95 100644 --- a/src/hooks/queries/useSuggestedCorrectionsQuery.ts +++ b/src/hooks/queries/useSuggestedCorrectionsQuery.ts @@ -1,6 +1,7 @@ // src/hooks/queries/useSuggestedCorrectionsQuery.ts import { useQuery } from '@tanstack/react-query'; import { getSuggestedCorrections } from '../../services/apiClient'; +import { queryKeys } from '../../config/queryKeys'; import type { SuggestedCorrection } from '../../types'; /** @@ -14,7 +15,7 @@ import type { SuggestedCorrection } from '../../types'; */ export const useSuggestedCorrectionsQuery = () => { return useQuery({ - queryKey: ['suggested-corrections'], + queryKey: queryKeys.suggestedCorrections(), queryFn: async (): Promise => { const response = await getSuggestedCorrections(); diff --git a/src/hooks/queries/useUserAddressQuery.ts b/src/hooks/queries/useUserAddressQuery.ts index aedeab6b..d535fb8f 100644 --- a/src/hooks/queries/useUserAddressQuery.ts +++ b/src/hooks/queries/useUserAddressQuery.ts @@ -1,6 +1,7 @@ // src/hooks/queries/useUserAddressQuery.ts import { useQuery } from '@tanstack/react-query'; import { getUserAddress } from '../../services/apiClient'; +import { queryKeys } from '../../config/queryKeys'; import type { Address } from '../../types'; /** @@ -20,7 +21,7 @@ export const useUserAddressQuery = ( enabled: boolean = true, ) => { return useQuery({ - queryKey: ['user-address', addressId], + queryKey: queryKeys.userAddress(addressId ?? null), queryFn: async (): Promise
=> { if (!addressId) { throw new Error('Address ID is required'); diff --git a/src/hooks/queries/useUserProfileDataQuery.ts b/src/hooks/queries/useUserProfileDataQuery.ts index eadeaa02..8013e23b 100644 --- a/src/hooks/queries/useUserProfileDataQuery.ts +++ b/src/hooks/queries/useUserProfileDataQuery.ts @@ -1,6 +1,7 @@ // src/hooks/queries/useUserProfileDataQuery.ts import { useQuery } from '@tanstack/react-query'; import { getAuthenticatedUserProfile, getUserAchievements } from '../../services/apiClient'; +import { queryKeys } from '../../config/queryKeys'; import type { UserProfile, Achievement, UserAchievement } from '../../types'; interface UserProfileData { @@ -26,7 +27,7 @@ interface UserProfileData { */ export const useUserProfileDataQuery = (enabled: boolean = true) => { return useQuery({ - queryKey: ['user-profile-data'], + queryKey: queryKeys.userProfileData(), queryFn: async (): Promise => { const [profileRes, achievementsRes] = await Promise.all([ getAuthenticatedUserProfile(), diff --git a/src/hooks/queries/useWatchedItemsQuery.ts b/src/hooks/queries/useWatchedItemsQuery.ts index 5ca4b9ce..1ee4d471 100644 --- a/src/hooks/queries/useWatchedItemsQuery.ts +++ b/src/hooks/queries/useWatchedItemsQuery.ts @@ -1,6 +1,7 @@ // src/hooks/queries/useWatchedItemsQuery.ts import { useQuery } from '@tanstack/react-query'; import * as apiClient from '../../services/apiClient'; +import { queryKeys } from '../../config/queryKeys'; import type { MasterGroceryItem } from '../../types'; /** @@ -19,7 +20,7 @@ import type { MasterGroceryItem } from '../../types'; */ export const useWatchedItemsQuery = (enabled: boolean) => { return useQuery({ - queryKey: ['watched-items'], + queryKey: queryKeys.watchedItems(), queryFn: async (): Promise => { const response = await apiClient.fetchWatchedItems(); diff --git a/src/pages/admin/components/AdminBrandManager.tsx b/src/pages/admin/components/AdminBrandManager.tsx index 7f019e3f..466d07b5 100644 --- a/src/pages/admin/components/AdminBrandManager.tsx +++ b/src/pages/admin/components/AdminBrandManager.tsx @@ -1,36 +1,22 @@ // src/pages/admin/components/AdminBrandManager.tsx -import React, { useState, useCallback } from 'react'; +import React, { useState } from 'react'; import toast from 'react-hot-toast'; -import { fetchAllBrands, uploadBrandLogo } from '../../../services/apiClient'; +import { uploadBrandLogo } from '../../../services/apiClient'; import { Brand } from '../../../types'; import { ErrorDisplay } from '../../../components/ErrorDisplay'; -import { useApiOnMount } from '../../../hooks/useApiOnMount'; +import { useBrandsQuery } from '../../../hooks/queries/useBrandsQuery'; import { logger } from '../../../services/logger.client'; export const AdminBrandManager: React.FC = () => { - // Wrap the fetcher function in useCallback to prevent it from being recreated on every render. - // The hook expects a function that returns a Promise, and it will handle - // the JSON parsing and error checking internally. - const fetchBrandsWrapper = useCallback(() => { - logger.debug( - '[AdminBrandManager] The memoized fetchBrandsWrapper is being passed to useApiOnMount', - ); - // This wrapper simply calls the API client function. The hook will manage the promise. - return fetchAllBrands(); - }, []); // An empty dependency array ensures this function is created only once. + const { data: initialBrands, isLoading: loading, error } = useBrandsQuery(); - const { - data: initialBrands, - loading, - error, - } = useApiOnMount(fetchBrandsWrapper, []); // This state will hold a modified list of brands only after an optimistic update (e.g., logo upload). // It starts as null, indicating that we should use the original data from the API. const [updatedBrands, setUpdatedBrands] = useState(null); // At render time, decide which data to display. If updatedBrands exists, it takes precedence. // Otherwise, fall back to the initial data from the hook. Default to an empty array. - const brandsToRender = updatedBrands || initialBrands || []; + const brandsToRender: Brand[] = updatedBrands || initialBrands || []; logger.debug( { loading, diff --git a/src/pages/admin/components/AuthView.tsx b/src/pages/admin/components/AuthView.tsx index 0c918851..a6325677 100644 --- a/src/pages/admin/components/AuthView.tsx +++ b/src/pages/admin/components/AuthView.tsx @@ -1,18 +1,16 @@ // src/pages/admin/components/AuthView.tsx import React, { useState } from 'react'; import type { UserProfile } from '../../../types'; -import { useApi } from '../../../hooks/useApi'; -import * as apiClient from '../../../services/apiClient'; import { notifySuccess } from '../../../services/notificationService'; import { LoadingSpinner } from '../../../components/LoadingSpinner'; import { GoogleIcon } from '../../../components/icons/GoogleIcon'; import { GithubIcon } from '../../../components/icons/GithubIcon'; import { PasswordInput } from '../../../components/PasswordInput'; - -interface AuthResponse { - userprofile: UserProfile; - token: string; -} +import { + useLoginMutation, + useRegisterMutation, + usePasswordResetRequestMutation, +} from '../../../hooks/mutations/useAuthMutations'; interface AuthViewProps { onLoginSuccess: (user: UserProfile, token: string, rememberMe: boolean) => void; @@ -27,37 +25,50 @@ export const AuthView: React.FC = ({ onLoginSuccess, onClose }) = const [isForgotPassword, setIsForgotPassword] = useState(false); const [rememberMe, setRememberMe] = useState(false); - const { execute: executeLogin, loading: loginLoading } = useApi< - AuthResponse, - [string, string, boolean] - >(apiClient.loginUser); - const { execute: executeRegister, loading: registerLoading } = useApi< - AuthResponse, - [string, string, string, string] - >(apiClient.registerUser); - const { execute: executePasswordReset, loading: passwordResetLoading } = useApi< - { message: string }, - [string] - >(apiClient.requestPasswordReset); + const loginMutation = useLoginMutation(); + const registerMutation = useRegisterMutation(); + const passwordResetMutation = usePasswordResetRequestMutation(); + + const loginLoading = loginMutation.isPending; + const registerLoading = registerMutation.isPending; + const passwordResetLoading = passwordResetMutation.isPending; const handleAuthSubmit = async (e: React.FormEvent) => { e.preventDefault(); - const authResult = isRegistering - ? await executeRegister(authEmail, authPassword, authFullName, '') - : await executeLogin(authEmail, authPassword, rememberMe); - if (authResult) { - onLoginSuccess(authResult.userprofile, authResult.token, rememberMe); - onClose(); + if (isRegistering) { + registerMutation.mutate( + { email: authEmail, password: authPassword, fullName: authFullName }, + { + onSuccess: (authResult) => { + onLoginSuccess(authResult.userprofile, authResult.token, rememberMe); + onClose(); + }, + }, + ); + } else { + loginMutation.mutate( + { email: authEmail, password: authPassword, rememberMe }, + { + onSuccess: (authResult) => { + onLoginSuccess(authResult.userprofile, authResult.token, rememberMe); + onClose(); + }, + }, + ); } }; const handlePasswordResetRequest = async (e: React.FormEvent) => { e.preventDefault(); - const result = await executePasswordReset(authEmail); - if (result) { - notifySuccess(result.message); - } + passwordResetMutation.mutate( + { email: authEmail }, + { + onSuccess: (result) => { + notifySuccess(result.message); + }, + }, + ); }; const handleOAuthSignIn = (provider: 'google' | 'github') => { diff --git a/src/pages/admin/components/ProfileManager.tsx b/src/pages/admin/components/ProfileManager.tsx index 5419ab48..53e28eee 100644 --- a/src/pages/admin/components/ProfileManager.tsx +++ b/src/pages/admin/components/ProfileManager.tsx @@ -1,21 +1,27 @@ // src/pages/admin/components/ProfileManager.tsx import React, { useState, useEffect } from 'react'; import type { Profile, Address, UserProfile } from '../../../types'; -import { useApi } from '../../../hooks/useApi'; -import * as apiClient from '../../../services/apiClient'; import { notifySuccess, notifyError } from '../../../services/notificationService'; import { logger } from '../../../services/logger.client'; import { LoadingSpinner } from '../../../components/LoadingSpinner'; import { XMarkIcon } from '../../../components/icons/XMarkIcon'; import { GoogleIcon } from '../../../components/icons/GoogleIcon'; import { GithubIcon } from '../../../components/icons/GithubIcon'; -import { ConfirmationModal } from '../../../components/ConfirmationModal'; // This path is correct +import { ConfirmationModal } from '../../../components/ConfirmationModal'; import { PasswordInput } from '../../../components/PasswordInput'; import { MapView } from '../../../components/MapView'; import type { AuthStatus } from '../../../hooks/useAuth'; import { AuthView } from './AuthView'; import { AddressForm } from './AddressForm'; import { useProfileAddress } from '../../../hooks/useProfileAddress'; +import { + useUpdateProfileMutation, + useUpdateAddressMutation, + useUpdatePasswordMutation, + useUpdatePreferencesMutation, + useExportDataMutation, + useDeleteAccountMutation, +} from '../../../hooks/mutations/useProfileMutations'; export interface ProfileManagerProps { isOpen: boolean; @@ -27,23 +33,6 @@ export interface ProfileManagerProps { onLoginSuccess: (user: UserProfile, token: string, rememberMe: boolean) => void; // Add login handler } -// --- API Hook Wrappers --- -// These wrappers adapt the apiClient functions (which expect an ApiOptions object) -// to the signature expected by the useApi hook (which passes a raw AbortSignal). -// They are defined outside the component to ensure they have a stable identity -// across re-renders, preventing infinite loops in useEffect hooks. -const updateAddressWrapper = (data: Partial
, signal?: AbortSignal) => - apiClient.updateUserAddress(data, { signal }); -const updatePasswordWrapper = (password: string, signal?: AbortSignal) => - apiClient.updateUserPassword(password, { signal }); -const exportDataWrapper = (signal?: AbortSignal) => apiClient.exportUserData({ signal }); -const deleteAccountWrapper = (password: string, signal?: AbortSignal) => - apiClient.deleteUserAccount(password, { signal }); -const updatePreferencesWrapper = (prefs: Partial, signal?: AbortSignal) => - apiClient.updateUserPreferences(prefs, { signal }); -const updateProfileWrapper = (data: Partial, signal?: AbortSignal) => - apiClient.updateUserProfile(data, { signal }); - export const ProfileManager: React.FC = ({ isOpen, onClose, @@ -63,32 +52,25 @@ export const ProfileManager: React.FC = ({ const { address, initialAddress, isGeocoding, handleAddressChange, handleManualGeocode } = useProfileAddress(userProfile, isOpen); - const { execute: updateProfile, loading: profileLoading } = useApi]>( - updateProfileWrapper, - ); - const { execute: updateAddress, loading: addressLoading } = useApi]>( - updateAddressWrapper, - ); + // TanStack Query mutations + const updateProfileMutation = useUpdateProfileMutation(); + const updateAddressMutation = useUpdateAddressMutation(); + const updatePasswordMutation = useUpdatePasswordMutation(); + const updatePreferencesMutation = useUpdatePreferencesMutation(); + const exportDataMutation = useExportDataMutation(); + const deleteAccountMutation = useDeleteAccountMutation(); + + const profileLoading = updateProfileMutation.isPending; + const addressLoading = updateAddressMutation.isPending; + const passwordLoading = updatePasswordMutation.isPending; + const exportLoading = exportDataMutation.isPending; + const deleteLoading = deleteAccountMutation.isPending; // Password state const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); - const { execute: updatePassword, loading: passwordLoading } = useApi( - updatePasswordWrapper, - ); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - // Data & Privacy state - const { execute: exportData, loading: exportLoading } = useApi(exportDataWrapper); - const { execute: deleteAccount, loading: deleteLoading } = useApi( - deleteAccountWrapper, - ); - - // Preferences state - const { execute: updatePreferences } = useApi]>( - updatePreferencesWrapper, - ); - const [isConfirmingDelete, setIsConfirmingDelete] = useState(false); const [passwordForDelete, setPasswordForDelete] = useState(''); @@ -146,15 +128,16 @@ export const ProfileManager: React.FC = ({ } // Create an array of promises for the API calls that need to be made. - // Because useApi() catches errors and returns null, we can safely use Promise.all. - const promisesToRun = []; + const promisesToRun: Promise[] = []; if (profileDataChanged) { logger.debug('[handleProfileSave] Queuing profile update promise.'); - promisesToRun.push(updateProfile({ full_name: fullName, avatar_url: avatarUrl })); + promisesToRun.push( + updateProfileMutation.mutateAsync({ full_name: fullName, avatar_url: avatarUrl }), + ); } if (addressDataChanged) { logger.debug('[handleProfileSave] Queuing address update promise.'); - promisesToRun.push(updateAddress(address)); + promisesToRun.push(updateAddressMutation.mutateAsync(address)); } try { @@ -169,7 +152,7 @@ export const ProfileManager: React.FC = ({ // Determine which promises succeeded or failed. results.forEach((result, index) => { const isProfilePromise = profileDataChanged && index === 0; - if (result.status === 'rejected' || (result.status === 'fulfilled' && !result.value)) { + if (result.status === 'rejected') { anyFailures = true; } else if (result.status === 'fulfilled' && isProfilePromise) { successfulProfileUpdate = result.value as Profile; @@ -187,12 +170,11 @@ export const ProfileManager: React.FC = ({ onClose(); } else { logger.warn( - '[handleProfileSave] One or more operations failed. The useApi hook should have shown an error. The modal will remain open.', + '[handleProfileSave] One or more operations failed. The mutation hook should have shown an error. The modal will remain open.', ); } } catch (error) { - // This catch block is a safeguard. In normal operation, the useApi hook - // should prevent any promises from rejecting. + // This catch block is a safeguard for unexpected errors. logger.error( { err: error }, "[CRITICAL] An unexpected error was caught directly in handleProfileSave's catch block.", @@ -229,51 +211,66 @@ export const ProfileManager: React.FC = ({ return; } - const result = await updatePassword(password); - if (result) { - notifySuccess('Password updated successfully!'); - setPassword(''); - setConfirmPassword(''); - } + updatePasswordMutation.mutate( + { password }, + { + onSuccess: () => { + notifySuccess('Password updated successfully!'); + setPassword(''); + setConfirmPassword(''); + }, + }, + ); }; const handleExportData = async () => { - const userData = await exportData(); - if (userData) { - const jsonString = `data:text/json;charset=utf-8,${encodeURIComponent(JSON.stringify(userData, null, 2))}`; - const link = document.createElement('a'); - link.href = jsonString; - link.download = `flyer-crawler-data-export-${new Date().toISOString().split('T')[0]}.json`; - link.click(); - } + exportDataMutation.mutate(undefined, { + onSuccess: (userData) => { + const jsonString = `data:text/json;charset=utf-8,${encodeURIComponent(JSON.stringify(userData, null, 2))}`; + const link = document.createElement('a'); + link.href = jsonString; + link.download = `flyer-crawler-data-export-${new Date().toISOString().split('T')[0]}.json`; + link.click(); + }, + }); }; const handleDeleteAccount = async () => { setIsDeleteModalOpen(false); // Close the confirmation modal - const result = await deleteAccount(passwordForDelete); - - if (result) { - // useApi returns null on failure, so this check is sufficient. - notifySuccess('Account deleted successfully. You will be logged out shortly.'); - setTimeout(() => { - onClose(); - onSignOut(); - }, 3000); - } + deleteAccountMutation.mutate( + { password: passwordForDelete }, + { + onSuccess: () => { + notifySuccess('Account deleted successfully. You will be logged out shortly.'); + setTimeout(() => { + onClose(); + onSignOut(); + }, 3000); + }, + }, + ); }; const handleToggleDarkMode = async (newMode: boolean) => { - const updatedProfile = await updatePreferences({ darkMode: newMode }); - if (updatedProfile) { - onProfileUpdate(updatedProfile); - } + updatePreferencesMutation.mutate( + { darkMode: newMode }, + { + onSuccess: (updatedProfile) => { + onProfileUpdate(updatedProfile); + }, + }, + ); }; const handleToggleUnitSystem = async (newSystem: 'metric' | 'imperial') => { - const updatedProfile = await updatePreferences({ unitSystem: newSystem }); - if (updatedProfile) { - onProfileUpdate(updatedProfile); - } + updatePreferencesMutation.mutate( + { unitSystem: newSystem }, + { + onSuccess: (updatedProfile) => { + onProfileUpdate(updatedProfile); + }, + }, + ); }; if (!isOpen) return null;