@kasuvia/sdk 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/hooks/index.d.mts +429 -0
  2. package/dist/hooks/index.d.ts +429 -0
  3. package/dist/hooks/index.js +3073 -0
  4. package/dist/hooks/index.js.map +1 -0
  5. package/dist/hooks/index.mjs +2962 -0
  6. package/dist/hooks/index.mjs.map +1 -0
  7. package/dist/index.d.mts +250 -1161
  8. package/dist/index.d.ts +250 -1161
  9. package/dist/index.js +1544 -2359
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.mjs +1527 -2258
  12. package/dist/index.mjs.map +1 -1
  13. package/dist/sdk-context-Cy51kEKT.d.mts +565 -0
  14. package/dist/sdk-context-Cy51kEKT.d.ts +565 -0
  15. package/package.json +6 -2
  16. package/src/config/index.ts +10 -91
  17. package/src/constants.ts +13 -0
  18. package/src/context/sdk-context.tsx +70 -0
  19. package/src/graphql/collections.graphql.ts +2 -10
  20. package/src/graphql/products.graphql.ts +215 -65
  21. package/src/graphql/wishlist.graphql.ts +7 -6
  22. package/src/hooks/index.ts +2 -0
  23. package/src/hooks/use-auth.ts +82 -158
  24. package/src/hooks/use-bespoke-availability.ts +25 -8
  25. package/src/hooks/use-cart.ts +222 -119
  26. package/src/hooks/use-cms.ts +179 -86
  27. package/src/hooks/use-collections.ts +52 -15
  28. package/src/hooks/use-consultation-stream.ts +4 -3
  29. package/src/hooks/use-consultations.ts +120 -51
  30. package/src/hooks/use-countries.ts +35 -13
  31. package/src/hooks/use-currencies.ts +26 -13
  32. package/src/hooks/use-google-translate.ts +22 -8
  33. package/src/hooks/use-languages.ts +40 -20
  34. package/src/hooks/use-mobile.ts +23 -0
  35. package/src/hooks/use-order-stream.ts +9 -5
  36. package/src/hooks/use-orders.ts +99 -44
  37. package/src/hooks/use-payment.ts +108 -40
  38. package/src/hooks/use-products.ts +251 -53
  39. package/src/hooks/use-service-booking.ts +152 -90
  40. package/src/hooks/use-states.ts +35 -15
  41. package/src/hooks/use-wishlist.ts +92 -91
  42. package/src/index.ts +26 -30
  43. package/src/services/auth.service.ts +209 -167
  44. package/src/services/bespoke.service.ts +12 -4
  45. package/src/services/cart.service.ts +172 -70
  46. package/src/services/cms.service.ts +70 -35
  47. package/src/services/collection.service.ts +22 -5
  48. package/src/services/config.service.ts +34 -14
  49. package/src/services/consultation.service.ts +72 -18
  50. package/src/services/logistics.service.ts +35 -11
  51. package/src/services/notifications.service.ts +27 -12
  52. package/src/services/order.service.ts +74 -15
  53. package/src/services/payment.service.ts +86 -31
  54. package/src/services/product.service.ts +279 -39
  55. package/src/services/service-booking.service.ts +98 -26
  56. package/src/services/wishlist.service.ts +69 -25
  57. package/src/types/auth/index.ts +6 -4
  58. package/src/types/business/index.ts +7 -2
  59. package/src/types/payments/index.ts +3 -3
  60. package/src/types/services/index.ts +2 -5
  61. package/src/types/store/index.ts +38 -3
  62. package/src/utils/error-handler.ts +10 -5
  63. package/src/utils/storage.ts +48 -26
  64. package/src/utils/validators.ts +85 -0
  65. package/src/client/authClient.ts +0 -135
  66. package/src/client/graphqlClient.ts +0 -53
  67. package/src/client/httpClient.ts +0 -298
@@ -1,68 +1,136 @@
1
1
  import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
2
- import { paymentService } from "../services/payment.service";
3
- import { getCacheTier } from "../utils/cache";
4
- import { isInitialized } from "../config";
2
+ import { createDedicatedAccount, initiatePayment, verifyTransaction, getTransactionStatus } from "../services/payment.service";
3
+ import type { SdkConfig } from "../config";
5
4
  import type { DVACreateRequest, PaymentInitiateRequest } from "../types/payments";
6
-
5
+ import { logger } from "../utils";
6
+ import { useSdkContext, resolveHookArgs } from "../context/sdk-context";
7
7
 
8
8
  /**
9
9
  * Hook for creating a Dedicated Virtual Account (DVA)
10
+ * Optional config and accessToken — read from SdkContext if omitted.
10
11
  */
11
- export function useCreateDVA() {
12
- const queryClient = useQueryClient();
12
+ export function useCreateDVA(config?: SdkConfig, accessToken?: string | null) {
13
+ const ctx = useSdkContext();
14
+ try {
15
+ const { config: cfg, accessToken: token } = resolveHookArgs(config, accessToken, ctx);
16
+ const queryClient = useQueryClient();
13
17
 
14
- return useMutation({
15
- mutationFn: (request: DVACreateRequest) =>
16
- paymentService.createDedicatedAccount(request),
17
- onSuccess: () => {
18
- queryClient.invalidateQueries({ queryKey: ['payment'] });
19
- },
20
- });
18
+ return useMutation({
19
+ mutationFn: (request: DVACreateRequest) =>
20
+ token ? createDedicatedAccount(cfg, token, request) : Promise.reject('No access token'),
21
+ onSuccess: () => {
22
+ queryClient.invalidateQueries({ queryKey: ['payment'] });
23
+ },
24
+ });
25
+ } catch (error) {
26
+ logger.warn('[useCreateDVA] Called outside QueryClientProvider, returning no-op mutation');
27
+ return {
28
+ mutate: () => {},
29
+ mutateAsync: () => Promise.resolve(undefined),
30
+ } as any;
31
+ }
21
32
  }
22
33
 
23
34
  /**
24
35
  * Hook for initiating card payment
36
+ * Optional config and accessToken — read from SdkContext if omitted.
25
37
  */
26
- export function useInitiatePayment() {
27
- return useMutation({
28
- mutationFn: (request: PaymentInitiateRequest) =>
29
- paymentService.initiatePayment(request),
30
- });
38
+ export function useInitiatePayment(config?: SdkConfig, accessToken?: string | null) {
39
+ const ctx = useSdkContext();
40
+ try {
41
+ const { config: cfg, accessToken: token } = resolveHookArgs(config, accessToken, ctx);
42
+ return useMutation({
43
+ mutationFn: (request: PaymentInitiateRequest) =>
44
+ token ? initiatePayment(cfg, token, request) : Promise.reject('No access token'),
45
+ });
46
+ } catch (error) {
47
+ logger.warn('[useInitiatePayment] Called outside QueryClientProvider, returning no-op mutation');
48
+ return {
49
+ mutate: () => {},
50
+ mutateAsync: () => Promise.resolve(undefined),
51
+ } as any;
52
+ }
31
53
  }
32
54
 
33
55
  /**
34
56
  * Hook for verifying a transaction
57
+ * Optional config — read from SdkContext if omitted.
35
58
  */
36
- export function useVerifyTransaction() {
37
- return useMutation({
38
- mutationFn: (reference: string) =>
39
- paymentService.verifyTransaction(reference),
40
- });
59
+ export function useVerifyTransaction(config?: SdkConfig) {
60
+ const ctx = useSdkContext();
61
+ try {
62
+ const { config: cfg } = resolveHookArgs(config, undefined, ctx);
63
+ return useMutation({
64
+ mutationFn: (reference: string) => verifyTransaction(cfg, reference),
65
+ });
66
+ } catch (error) {
67
+ logger.warn('[useVerifyTransaction] Called outside QueryClientProvider, returning no-op mutation');
68
+ return {
69
+ mutate: () => {},
70
+ mutateAsync: () => Promise.resolve(undefined),
71
+ } as any;
72
+ }
41
73
  }
42
74
 
43
75
  /**
44
76
  * Hook for getting transaction status
77
+ * Optional config — read from SdkContext if omitted.
45
78
  */
46
- export function useTransactionStatus(transactionId: string, enabled = false) {
47
- return useQuery({
48
- queryKey: ['transaction', 'status', transactionId],
49
- queryFn: () => paymentService.getTransactionStatus(transactionId),
50
- enabled: isInitialized() && enabled,
51
- ...getCacheTier('none'),
52
- refetchInterval: 3000, // Poll every 3 seconds
53
- });
79
+ export function useTransactionStatus(arg1?: SdkConfig | string, arg2?: string | boolean, arg3?: boolean) {
80
+ const ctx = useSdkContext();
81
+ const isArg1Config = typeof arg1 === 'object' && arg1 !== null && 'businessId' in arg1;
82
+ const explicitConfig = isArg1Config ? (arg1 as SdkConfig) : undefined;
83
+ const transactionId = isArg1Config ? (arg2 as string) : (typeof arg1 === 'string' ? arg1 : '');
84
+ const enabled = isArg1Config ? (arg3 ?? false) : (typeof arg2 === 'boolean' ? arg2 : false);
85
+
86
+ try {
87
+ const { config: cfg } = resolveHookArgs(explicitConfig, undefined, ctx);
88
+ return useQuery({
89
+ queryKey: ['transaction', 'status', transactionId],
90
+ queryFn: () => getTransactionStatus(cfg, transactionId),
91
+ enabled: enabled && !!transactionId,
92
+ refetchInterval: 3000,
93
+ });
94
+ } catch (error) {
95
+ logger.warn('[useTransactionStatus] Called outside QueryClientProvider, returning undefined');
96
+ return {
97
+ data: undefined,
98
+ isLoading: false,
99
+ error: null,
100
+ isFetching: false,
101
+ refetch: () => Promise.resolve({ data: undefined }),
102
+ } as any;
103
+ }
54
104
  }
55
105
 
56
106
  /**
57
107
  * Hook for checking transaction status with automatic polling
108
+ * Optional config — read from SdkContext if omitted.
58
109
  */
59
- export function usePollTransactionStatus(reference: string, enabled = false) {
60
- return useQuery({
61
- queryKey: ['transaction', 'poll', reference],
62
- queryFn: () => paymentService.verifyTransaction(reference),
63
- enabled: isInitialized() && enabled,
64
- ...getCacheTier('none'),
65
- refetchInterval: 2000, // Poll every 2 seconds
66
- retry: 10, // Retry up to 10 times
67
- });
110
+ export function usePollTransactionStatus(arg1?: SdkConfig | string, arg2?: string | boolean, arg3?: boolean) {
111
+ const ctx = useSdkContext();
112
+ const isArg1Config = typeof arg1 === 'object' && arg1 !== null && 'businessId' in arg1;
113
+ const explicitConfig = isArg1Config ? (arg1 as SdkConfig) : undefined;
114
+ const reference = isArg1Config ? (arg2 as string) : (typeof arg1 === 'string' ? arg1 : '');
115
+ const enabled = isArg1Config ? (arg3 ?? false) : (typeof arg2 === 'boolean' ? arg2 : false);
116
+
117
+ try {
118
+ const { config: cfg } = resolveHookArgs(explicitConfig, undefined, ctx);
119
+ return useQuery({
120
+ queryKey: ['transaction', 'poll', reference],
121
+ queryFn: () => verifyTransaction(cfg, reference),
122
+ enabled: enabled && !!reference,
123
+ refetchInterval: 2000,
124
+ retry: 10,
125
+ });
126
+ } catch (error) {
127
+ logger.warn('[usePollTransactionStatus] Called outside QueryClientProvider, returning undefined');
128
+ return {
129
+ data: undefined,
130
+ isLoading: false,
131
+ error: null,
132
+ isFetching: false,
133
+ refetch: () => Promise.resolve({ data: undefined }),
134
+ } as any;
135
+ }
68
136
  }
@@ -1,20 +1,13 @@
1
1
  /**
2
2
  * Products Hooks
3
3
  *
4
- * React Query hooks for fetching products.
4
+ * React Query hooks for fetching products via Next.js API routes.
5
+ * Client-side hooks fetch from API routes to avoid exposing backend URLs to browser.
5
6
  */
6
7
  import { useQuery } from '@tanstack/react-query'
7
- import {
8
- getStorefrontProducts,
9
- getLatestProducts,
10
- getPopularProducts,
11
- getFeaturedProducts,
12
- getNewProducts,
13
- getStorefrontProductBySlug,
14
- getRecommendedProducts
15
- } from '../services/product.service'
16
8
  import type { ListParams } from '../types/store'
17
9
  import { getCacheTier } from '../utils/cache'
10
+ import { logger } from '../utils'
18
11
 
19
12
  export const productKeys = {
20
13
  all: ["products"] as const,
@@ -25,89 +18,294 @@ export const productKeys = {
25
18
  popular: (params?: ListParams) => [...productKeys.all, "popular", params] as const,
26
19
  featured: (params?: ListParams) => [...productKeys.all, "featured", params] as const,
27
20
  new: (params?: ListParams) => [...productKeys.all, "new", params] as const,
21
+ onSale: (params?: ListParams) => [...productKeys.all, "onSale", params] as const,
22
+ lowStock: (params?: ListParams) => [...productKeys.all, "lowStock", params] as const,
28
23
  }
29
24
 
30
25
  /**
31
26
  * Hook for fetching latest products.
27
+ * Fetches from Next.js API route to avoid exposing backend URLs.
32
28
  */
33
29
  export function useLatestProducts(params?: ListParams) {
34
- return useQuery({
35
- queryKey: productKeys.latest(params),
36
- queryFn: () => getLatestProducts(params),
37
- // medium — latest products change infrequently
38
- ...getCacheTier('medium'),
39
- })
30
+ try {
31
+ const queryParams = new URLSearchParams()
32
+ if (params?.page) queryParams.set('page', params.page.toString())
33
+ if (params?.pageSize) queryParams.set('pageSize', params.pageSize.toString())
34
+
35
+ return useQuery({
36
+ queryKey: productKeys.latest(params),
37
+ queryFn: async () => {
38
+ const response = await fetch(`/api/products/latest?${queryParams.toString()}`)
39
+ if (!response.ok) throw new Error('Failed to fetch latest products')
40
+ return response.json()
41
+ },
42
+ // medium — latest products change infrequently
43
+ ...getCacheTier('medium'),
44
+ })
45
+ } catch (error) {
46
+ logger.warn('[useLatestProducts] Called outside QueryClientProvider, returning empty list')
47
+ return {
48
+ data: undefined,
49
+ isLoading: false,
50
+ error: null,
51
+ isFetching: false,
52
+ refetch: () => Promise.resolve({ data: undefined }),
53
+ } as any
54
+ }
40
55
  }
41
56
 
42
57
  /**
43
58
  * Hook for fetching popular products.
59
+ * Fetches from Next.js API route to avoid exposing backend URLs.
44
60
  */
45
61
  export function usePopularProducts(params?: ListParams) {
46
- return useQuery({
47
- queryKey: productKeys.popular(params),
48
- queryFn: () => getPopularProducts(params),
49
- // medium — popular products change infrequently
50
- ...getCacheTier('medium'),
51
- })
62
+ try {
63
+ const queryParams = new URLSearchParams()
64
+ if (params?.page) queryParams.set('page', params.page.toString())
65
+ if (params?.pageSize) queryParams.set('pageSize', params.pageSize.toString())
66
+
67
+ return useQuery({
68
+ queryKey: productKeys.popular(params),
69
+ queryFn: async () => {
70
+ const response = await fetch(`/api/products/popular?${queryParams.toString()}`)
71
+ if (!response.ok) throw new Error('Failed to fetch popular products')
72
+ return response.json()
73
+ },
74
+ // medium — popular products change infrequently
75
+ ...getCacheTier('medium'),
76
+ })
77
+ } catch (error) {
78
+ logger.warn('[usePopularProducts] Called outside QueryClientProvider, returning empty list')
79
+ return {
80
+ data: undefined,
81
+ isLoading: false,
82
+ error: null,
83
+ isFetching: false,
84
+ refetch: () => Promise.resolve({ data: undefined }),
85
+ } as any
86
+ }
52
87
  }
53
88
 
54
89
  /**
55
90
  * Hook for fetching featured products.
91
+ * Fetches from Next.js API route to avoid exposing backend URLs.
56
92
  */
57
93
  export function useFeaturedProducts(params?: ListParams) {
58
- return useQuery({
59
- queryKey: productKeys.featured(params),
60
- queryFn: () => getFeaturedProducts(params),
61
- // medium — featured products change infrequently
62
- ...getCacheTier('medium'),
63
- })
94
+ try {
95
+ const queryParams = new URLSearchParams()
96
+ if (params?.page) queryParams.set('page', params.page.toString())
97
+ if (params?.pageSize) queryParams.set('pageSize', params.pageSize.toString())
98
+
99
+ return useQuery({
100
+ queryKey: productKeys.featured(params),
101
+ queryFn: async () => {
102
+ const response = await fetch(`/api/products/featured?${queryParams.toString()}`)
103
+ if (!response.ok) throw new Error('Failed to fetch featured products')
104
+ return response.json()
105
+ },
106
+ // medium — featured products change infrequently
107
+ ...getCacheTier('medium'),
108
+ })
109
+ } catch (error) {
110
+ logger.warn('[useFeaturedProducts] Called outside QueryClientProvider, returning empty list')
111
+ return {
112
+ data: undefined,
113
+ isLoading: false,
114
+ error: null,
115
+ isFetching: false,
116
+ refetch: () => Promise.resolve({ data: undefined }),
117
+ } as any
118
+ }
64
119
  }
65
120
 
66
121
  /**
67
122
  * Hook for fetching new products.
123
+ * Fetches from Next.js API route to avoid exposing backend URLs.
68
124
  */
69
125
  export function useNewProducts(params?: ListParams) {
70
- return useQuery({
71
- queryKey: productKeys.new(params),
72
- queryFn: () => getNewProducts(params),
73
- // medium — new products change infrequently
74
- ...getCacheTier('medium'),
75
- })
126
+ try {
127
+ const queryParams = new URLSearchParams()
128
+ if (params?.page) queryParams.set('page', params.page.toString())
129
+ if (params?.pageSize) queryParams.set('pageSize', params.pageSize.toString())
130
+
131
+ return useQuery({
132
+ queryKey: productKeys.new(params),
133
+ queryFn: async () => {
134
+ const response = await fetch(`/api/products/new?${queryParams.toString()}`)
135
+ if (!response.ok) throw new Error('Failed to fetch new products')
136
+ return response.json()
137
+ },
138
+ // medium — new products change infrequently
139
+ ...getCacheTier('medium'),
140
+ })
141
+ } catch (error) {
142
+ logger.warn('[useNewProducts] Called outside QueryClientProvider, returning empty list')
143
+ return {
144
+ data: undefined,
145
+ isLoading: false,
146
+ error: null,
147
+ isFetching: false,
148
+ refetch: () => Promise.resolve({ data: undefined }),
149
+ } as any
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Hook for fetching products currently on sale.
155
+ * Fetches from Next.js API route to avoid exposing backend URLs.
156
+ */
157
+ export function useOnSaleProducts(params?: ListParams) {
158
+ try {
159
+ const queryParams = new URLSearchParams()
160
+ if (params?.page) queryParams.set('page', params.page.toString())
161
+ if (params?.pageSize) queryParams.set('pageSize', params.pageSize.toString())
162
+
163
+ return useQuery({
164
+ queryKey: productKeys.onSale(params),
165
+ queryFn: async () => {
166
+ const response = await fetch(`/api/products/on-sale?${queryParams.toString()}`)
167
+ if (!response.ok) throw new Error('Failed to fetch on-sale products')
168
+ return response.json()
169
+ },
170
+ // medium — sale products change infrequently
171
+ ...getCacheTier('medium'),
172
+ })
173
+ } catch (error) {
174
+ logger.warn('[useOnSaleProducts] Called outside QueryClientProvider, returning empty list')
175
+ return {
176
+ data: undefined,
177
+ isLoading: false,
178
+ error: null,
179
+ isFetching: false,
180
+ refetch: () => Promise.resolve({ data: undefined }),
181
+ } as any
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Hook for fetching products with low stock.
187
+ * Fetches from Next.js API route to avoid exposing backend URLs.
188
+ */
189
+ export function useLowStockProducts(params?: ListParams) {
190
+ try {
191
+ const queryParams = new URLSearchParams()
192
+ if (params?.page) queryParams.set('page', params.page.toString())
193
+ if (params?.pageSize) queryParams.set('pageSize', params.pageSize.toString())
194
+
195
+ return useQuery({
196
+ queryKey: productKeys.lowStock(params),
197
+ queryFn: async () => {
198
+ const response = await fetch(`/api/products/low-stock?${queryParams.toString()}`)
199
+ if (!response.ok) throw new Error('Failed to fetch low-stock products')
200
+ return response.json()
201
+ },
202
+ // low — low stock status changes frequently
203
+ ...getCacheTier('low'),
204
+ })
205
+ } catch (error) {
206
+ logger.warn('[useLowStockProducts] Called outside QueryClientProvider, returning empty list')
207
+ return {
208
+ data: undefined,
209
+ isLoading: false,
210
+ error: null,
211
+ isFetching: false,
212
+ refetch: () => Promise.resolve({ data: undefined }),
213
+ } as any
214
+ }
76
215
  }
77
216
 
78
217
  /**
79
218
  * Hook for fetching all active storefront products.
219
+ * Fetches from Next.js API route to avoid exposing backend URLs.
80
220
  */
81
221
  export function useStorefrontProducts(params?: ListParams) {
82
- return useQuery({
83
- queryKey: productKeys.list(params),
84
- queryFn: () => getStorefrontProducts(params),
85
- // medium — product catalog changes infrequently
86
- ...getCacheTier('medium'),
87
- })
222
+ try {
223
+ const queryParams = new URLSearchParams()
224
+ if (params?.page) queryParams.set('page', params.page.toString())
225
+ if (params?.pageSize) queryParams.set('pageSize', params.pageSize.toString())
226
+ if (params?.collectionSlug) queryParams.set('collectionSlug', params.collectionSlug)
227
+ if (params?.search) queryParams.set('search', params.search)
228
+
229
+ return useQuery({
230
+ queryKey: productKeys.list(params),
231
+ queryFn: async () => {
232
+ const response = await fetch(`/api/products?${queryParams.toString()}`)
233
+ if (!response.ok) throw new Error('Failed to fetch products')
234
+ return response.json()
235
+ },
236
+ // medium — product catalog changes infrequently
237
+ ...getCacheTier('medium'),
238
+ })
239
+ } catch (error) {
240
+ logger.warn('[useStorefrontProducts] Called outside QueryClientProvider, returning empty list')
241
+ return {
242
+ data: undefined,
243
+ isLoading: false,
244
+ error: null,
245
+ isFetching: false,
246
+ refetch: () => Promise.resolve({ data: undefined }),
247
+ } as any
248
+ }
88
249
  }
89
250
 
90
251
  /**
91
252
  * Hook for fetching a single product by slug.
253
+ * Fetches from Next.js API route to avoid exposing backend URLs.
92
254
  */
93
255
  export function useProduct(slug: string) {
94
- return useQuery({
95
- queryKey: productKeys.detail(slug),
96
- queryFn: () => getStorefrontProductBySlug(slug),
97
- // medium — product details change infrequently
98
- ...getCacheTier('medium'),
99
- enabled: !!slug,
100
- })
256
+ try {
257
+ return useQuery({
258
+ queryKey: productKeys.detail(slug),
259
+ queryFn: async () => {
260
+ const response = await fetch(`/api/products/${slug}`)
261
+ if (!response.ok) throw new Error('Failed to fetch product')
262
+ return response.json()
263
+ },
264
+ // medium — product details change infrequently
265
+ ...getCacheTier('medium'),
266
+ enabled: !!slug,
267
+ })
268
+ } catch (error) {
269
+ logger.warn('[useProduct] Called outside QueryClientProvider, returning undefined')
270
+ return {
271
+ data: undefined,
272
+ isLoading: false,
273
+ error: null,
274
+ isFetching: false,
275
+ refetch: () => Promise.resolve({ data: undefined }),
276
+ } as any
277
+ }
101
278
  }
102
279
 
103
280
  /**
104
281
  * Hook for fetching recommended products.
282
+ * Fetches from Next.js API route to avoid exposing backend URLs.
105
283
  */
106
284
  export function useRecommendedProducts(params?: ListParams) {
107
- return useQuery({
108
- queryKey: productKeys.recommended(params),
109
- queryFn: () => getRecommendedProducts(params),
110
- // medium — recommendations change infrequently
111
- ...getCacheTier('medium'),
112
- })
285
+ try {
286
+ const queryParams = new URLSearchParams()
287
+ if (params?.page) queryParams.set('page', params.page.toString())
288
+ if (params?.pageSize) queryParams.set('pageSize', params.pageSize.toString())
289
+ if (params?.excludeProductId) queryParams.set('excludeProductId', params.excludeProductId)
290
+
291
+ return useQuery({
292
+ queryKey: productKeys.recommended(params),
293
+ queryFn: async () => {
294
+ const response = await fetch(`/api/products/recommended?${queryParams.toString()}`)
295
+ if (!response.ok) throw new Error('Failed to fetch recommended products')
296
+ return response.json()
297
+ },
298
+ // medium — recommendations change infrequently
299
+ ...getCacheTier('medium'),
300
+ })
301
+ } catch (error) {
302
+ logger.warn('[useRecommendedProducts] Called outside QueryClientProvider, returning empty list')
303
+ return {
304
+ data: undefined,
305
+ isLoading: false,
306
+ error: null,
307
+ isFetching: false,
308
+ refetch: () => Promise.resolve({ data: undefined }),
309
+ } as any
310
+ }
113
311
  }