@kasuvia/sdk 0.0.8 → 0.0.9
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.
- package/README.md +42 -15
- package/dist/AddressAutocomplete-CjcqPElD.d.mts +46 -0
- package/dist/AddressAutocomplete-CvJHuRkf.d.ts +46 -0
- package/dist/components/index.d.mts +497 -0
- package/dist/components/index.d.ts +497 -0
- package/dist/components/index.js +4217 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/index.mjs +4194 -0
- package/dist/components/index.mjs.map +1 -0
- package/dist/config/server.d.mts +26 -0
- package/dist/config/server.d.ts +26 -0
- package/dist/config/server.js +38 -0
- package/dist/config/server.js.map +1 -0
- package/dist/config/server.mjs +13 -0
- package/dist/config/server.mjs.map +1 -0
- package/dist/hooks/index.d.mts +393 -160
- package/dist/hooks/index.d.ts +393 -160
- package/dist/hooks/index.js +2792 -2137
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/index.mjs +2734 -2121
- package/dist/hooks/index.mjs.map +1 -1
- package/dist/index-CNW_LINy.d.mts +578 -0
- package/dist/index-CNW_LINy.d.ts +578 -0
- package/dist/{index-C1chUA3P.d.mts → index-F7N6UItS.d.mts} +1 -1
- package/dist/{index-C1chUA3P.d.ts → index-F7N6UItS.d.ts} +1 -1
- package/dist/index-NkI10J_E.d.mts +40 -0
- package/dist/index-NkI10J_E.d.ts +40 -0
- package/dist/index.d.mts +20437 -103
- package/dist/index.d.ts +20437 -103
- package/dist/index.js +2111 -1802
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2006 -1786
- package/dist/index.mjs.map +1 -1
- package/dist/media.service-Cx7dQjzX.d.mts +1393 -0
- package/dist/media.service-mrBFJ2UQ.d.ts +1393 -0
- package/dist/services/index.d.mts +908 -2
- package/dist/services/index.d.ts +908 -2
- package/dist/services/index.js +1492 -1686
- package/dist/services/index.js.map +1 -1
- package/dist/services/index.mjs +1431 -1670
- package/dist/services/index.mjs.map +1 -1
- package/package.json +46 -6
- package/src/components/bespoke/BespokeAvailabilityBadge.tsx +32 -0
- package/src/components/bespoke/BespokeProgressTracker.tsx +58 -0
- package/src/components/bespoke/index.ts +3 -0
- package/src/components/booking/SlotPicker.tsx +97 -0
- package/src/components/booking/index.ts +1 -0
- package/src/components/cart/CartItem.tsx +81 -0
- package/src/components/cart/CartSidebar.tsx +115 -0
- package/src/components/cart/CartTypeMismatchBanner.tsx +55 -0
- package/src/components/cart/index.ts +3 -0
- package/src/components/index.ts +8 -0
- package/src/components/orders/DeliverableReviewer.tsx +139 -0
- package/src/components/orders/index.ts +1 -0
- package/src/components/product/ProductCard.tsx +350 -0
- package/src/components/product/ProductGrid.tsx +84 -0
- package/src/components/product/index.ts +2 -0
- package/src/components/shared/AddressAutocomplete.tsx +212 -0
- package/src/components/shared/AnalyticsScripts.tsx +56 -0
- package/src/components/shared/CookieConsentBanner.tsx +98 -0
- package/src/components/shared/ImageEditorModal.tsx +1066 -0
- package/src/components/shared/ImageViewerModal.tsx +598 -0
- package/src/components/shared/LucideReactIcon.tsx +52 -0
- package/src/components/shared/MapPinDrop.tsx +101 -0
- package/src/components/shared/MultiSelectPicker.tsx +193 -0
- package/src/components/shared/OfflineBanner.tsx +46 -0
- package/src/components/shared/SearchableInput.tsx +153 -0
- package/src/components/shared/SearchablePopover.tsx +166 -0
- package/src/components/shared/SmartImage.tsx +311 -0
- package/src/components/shared/TruncatedValue.tsx +27 -0
- package/src/components/shared/UnsavedChangesGuard.tsx +138 -0
- package/src/components/shared/index.ts +15 -0
- package/src/components/skeletons/CardSkeleton.tsx +26 -0
- package/src/components/skeletons/CartItemSkeleton.tsx +26 -0
- package/src/components/skeletons/OrderItemSkeleton.tsx +26 -0
- package/src/components/skeletons/ProductCardSkeleton.tsx +26 -0
- package/src/components/skeletons/TableSkeleton.tsx +42 -0
- package/src/components/skeletons/index.ts +5 -0
- package/src/config/index.ts +22 -4
- package/src/config/server.ts +45 -0
- package/src/context/sdk-context.tsx +5 -2
- package/src/generated/auth-openapi.ts +2935 -0
- package/src/generated/cart-openapi.ts +652 -0
- package/src/generated/cms-openapi.ts +1668 -0
- package/src/generated/crm-openapi.ts +986 -0
- package/src/generated/graphql.ts +126 -0
- package/src/generated/index.ts +11 -0
- package/src/generated/media-openapi.ts +613 -0
- package/src/generated/notification-openapi.ts +862 -0
- package/src/generated/payment-openapi.ts +2713 -0
- package/src/generated/store-openapi.ts +9510 -0
- package/src/graphql/operations/collections.graphql +32 -0
- package/src/graphql/operations/products.graphql +197 -0
- package/src/graphql/operations/wishlist.graphql +25 -0
- package/src/hooks/index.ts +9 -2
- package/src/hooks/use-auth.ts +163 -69
- package/src/hooks/use-bespoke-availability.ts +39 -22
- package/src/hooks/use-business-config.ts +51 -0
- package/src/hooks/use-cart.ts +310 -238
- package/src/hooks/use-cms.ts +107 -127
- package/src/hooks/use-collections.ts +35 -55
- package/src/hooks/use-consultations.ts +9 -136
- package/src/hooks/use-countries.ts +19 -28
- package/src/hooks/use-currencies.ts +8 -20
- package/src/hooks/use-customer-address.ts +64 -0
- package/src/hooks/use-feedback.ts +34 -0
- package/src/hooks/use-languages.ts +26 -39
- package/src/hooks/use-media-upload.ts +23 -0
- package/src/hooks/use-notification-stream.ts +88 -0
- package/src/hooks/use-notifications.ts +163 -0
- package/src/hooks/use-order-stream.ts +29 -29
- package/src/hooks/use-orders.ts +91 -73
- package/src/hooks/use-payment.ts +116 -84
- package/src/hooks/use-products.ts +65 -269
- package/src/hooks/use-push-notifications.ts +122 -0
- package/src/hooks/use-reviews.ts +175 -0
- package/src/hooks/use-service-booking.ts +109 -132
- package/src/hooks/use-states.ts +16 -30
- package/src/hooks/use-wishlist.ts +44 -69
- package/src/index.ts +15 -11
- package/src/services/auth.service.ts +422 -132
- package/src/services/bespoke.service.ts +69 -10
- package/src/services/cart.service.ts +155 -186
- package/src/services/cms.service.ts +41 -50
- package/src/services/collection.service.ts +25 -63
- package/src/services/config.service.ts +10 -21
- package/src/services/consultation.service.ts +36 -93
- package/src/services/feedback.service.ts +76 -0
- package/src/services/index.ts +7 -1
- package/src/services/legal.service.ts +62 -0
- package/src/services/logistics.service.ts +235 -53
- package/src/services/media.service.ts +115 -0
- package/src/services/notifications.service.ts +204 -20
- package/src/services/order.service.ts +498 -127
- package/src/services/payment.service.ts +149 -100
- package/src/services/product.service.ts +157 -814
- package/src/services/review.service.ts +211 -0
- package/src/services/service-booking.service.ts +198 -99
- package/src/services/wishlist.service.ts +27 -74
- package/src/types/auth/index.ts +161 -18
- package/src/types/business/index.ts +9 -1
- package/src/types/cms/index.ts +91 -6
- package/src/types/cms/legal.ts +18 -0
- package/src/types/consultations/index.ts +60 -68
- package/src/types/index.ts +5 -3
- package/src/types/media/index.ts +35 -3
- package/src/types/payments/index.ts +80 -16
- package/src/types/reviews/index.ts +116 -0
- package/src/types/services/index.ts +113 -38
- package/src/types/store/checkout.ts +53 -0
- package/src/types/store/index.ts +273 -52
- package/src/types/store/invoice.ts +31 -0
- package/src/types/store/shipping.ts +84 -3
- package/src/types/ui/index.ts +1 -1
- package/src/utils/checkout.ts +19 -3
- package/src/utils/cookieConsent.ts +34 -0
- package/src/utils/format.ts +408 -0
- package/src/utils/geolocation.ts +134 -0
- package/src/utils/graphql-url.ts +47 -0
- package/src/utils/guest-session.ts +59 -0
- package/src/utils/http-client.ts +147 -0
- package/src/utils/index.ts +11 -4
- package/src/utils/status-colors.ts +79 -0
- package/src/utils/token-refresh.ts +115 -0
- package/src/utils/validators.ts +13 -0
- package/src/utils/youtube.ts +18 -0
- package/dist/index-C5YO9gTl.d.mts +0 -507
- package/dist/index-C5YO9gTl.d.ts +0 -507
- package/dist/index-CefG7_xq.d.mts +0 -793
- package/dist/index-DeHCf3zI.d.ts +0 -793
- package/src/graphql/collections.graphql.ts +0 -48
- package/src/graphql/index.ts +0 -7
- package/src/graphql/products.graphql.ts +0 -860
- package/src/graphql/wishlist.graphql.ts +0 -34
- package/src/hooks/use-consultation-stream.ts +0 -142
- package/src/types/bespoke/index.ts +0 -39
|
@@ -1,311 +1,107 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Products Hooks
|
|
3
|
-
*
|
|
4
|
-
* React Query hooks for fetching products via
|
|
5
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* React Query hooks for fetching products via the real GraphQL storefront API.
|
|
5
|
+
*
|
|
6
|
+
* REAL BUG FIXED (2026-09-21), live-reproduced: every hook here used to fetch
|
|
7
|
+
* from a Next.js API route (`/api/products/popular`, `/api/products/featured`,
|
|
8
|
+
* etc.) — but those routes were deleted earlier this session as confirmed
|
|
9
|
+
* orphaned (an incomplete grep missed this file as a real consumer, the same
|
|
10
|
+
* mistake class already caught once for `/api/products/[slug]`). This broke
|
|
11
|
+
* every home-page rail (Featured/New/Popular/On Sale) and the "You may also
|
|
12
|
+
* crave" recommended section — all 404s. Fixed by calling the SDK's real
|
|
13
|
+
* GraphQL service functions directly (product.service.ts), resolving config
|
|
14
|
+
* from SdkContext the same way every other SDK hook does (see use-orders.ts).
|
|
15
|
+
* `useLatestProducts`/`useLowStockProducts`/`useStorefrontProducts`/
|
|
16
|
+
* `useProduct` are NOT restored — confirmed zero real UI consumers anywhere
|
|
17
|
+
* in kasuvia-storefront (ProductsListing.tsx and the product detail page
|
|
18
|
+
* both already call the SDK service functions directly, not through a hook).
|
|
6
19
|
*/
|
|
7
20
|
import { useQuery } from '@tanstack/react-query'
|
|
8
21
|
import type { ListParams } from '../types/store'
|
|
9
22
|
import { getCacheTier } from '../utils/cache'
|
|
10
|
-
import {
|
|
23
|
+
import { useSdkContext, resolveHookArgs } from '../context/sdk-context'
|
|
24
|
+
import type { SdkConfig } from '../config'
|
|
25
|
+
import {
|
|
26
|
+
getPopularProducts,
|
|
27
|
+
getFeaturedProducts,
|
|
28
|
+
getNewProducts,
|
|
29
|
+
getOnSaleProducts,
|
|
30
|
+
getRecommendedProducts,
|
|
31
|
+
} from '../services/product.service'
|
|
11
32
|
|
|
12
33
|
export const productKeys = {
|
|
13
34
|
all: ["products"] as const,
|
|
14
35
|
list: (params?: ListParams) => [...productKeys.all, "list", params] as const,
|
|
15
36
|
detail: (slug: string) => [...productKeys.all, "detail", slug] as const,
|
|
16
37
|
recommended: (params?: ListParams) => [...productKeys.all, "recommended", params] as const,
|
|
17
|
-
latest: (params?: ListParams) => [...productKeys.all, "latest", params] as const,
|
|
18
38
|
popular: (params?: ListParams) => [...productKeys.all, "popular", params] as const,
|
|
19
39
|
featured: (params?: ListParams) => [...productKeys.all, "featured", params] as const,
|
|
20
40
|
new: (params?: ListParams) => [...productKeys.all, "new", params] as const,
|
|
21
41
|
onSale: (params?: ListParams) => [...productKeys.all, "onSale", params] as const,
|
|
22
|
-
lowStock: (params?: ListParams) => [...productKeys.all, "lowStock", params] as const,
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Hook for fetching latest products.
|
|
27
|
-
* Fetches from Next.js API route to avoid exposing backend URLs.
|
|
28
|
-
*/
|
|
29
|
-
export function useLatestProducts(params?: ListParams) {
|
|
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
|
-
}
|
|
55
42
|
}
|
|
56
43
|
|
|
57
44
|
/**
|
|
58
45
|
* Hook for fetching popular products.
|
|
59
|
-
* Fetches from Next.js API route to avoid exposing backend URLs.
|
|
60
46
|
*/
|
|
61
|
-
export function usePopularProducts(params?: ListParams) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
}
|
|
47
|
+
export function usePopularProducts(params?: ListParams, config?: SdkConfig) {
|
|
48
|
+
const ctx = useSdkContext()
|
|
49
|
+
const { config: cfg } = resolveHookArgs(config, undefined, ctx)
|
|
50
|
+
return useQuery({
|
|
51
|
+
queryKey: productKeys.popular(params),
|
|
52
|
+
queryFn: () => getPopularProducts(cfg, params),
|
|
53
|
+
...getCacheTier('medium'),
|
|
54
|
+
})
|
|
87
55
|
}
|
|
88
56
|
|
|
89
57
|
/**
|
|
90
58
|
* Hook for fetching featured products.
|
|
91
|
-
* Fetches from Next.js API route to avoid exposing backend URLs.
|
|
92
59
|
*/
|
|
93
|
-
export function useFeaturedProducts(params?: ListParams) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
}
|
|
60
|
+
export function useFeaturedProducts(params?: ListParams, config?: SdkConfig) {
|
|
61
|
+
const ctx = useSdkContext()
|
|
62
|
+
const { config: cfg } = resolveHookArgs(config, undefined, ctx)
|
|
63
|
+
return useQuery({
|
|
64
|
+
queryKey: productKeys.featured(params),
|
|
65
|
+
queryFn: () => getFeaturedProducts(cfg, params),
|
|
66
|
+
...getCacheTier('medium'),
|
|
67
|
+
})
|
|
119
68
|
}
|
|
120
69
|
|
|
121
70
|
/**
|
|
122
71
|
* Hook for fetching new products.
|
|
123
|
-
* Fetches from Next.js API route to avoid exposing backend URLs.
|
|
124
72
|
*/
|
|
125
|
-
export function useNewProducts(params?: ListParams) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
}
|
|
73
|
+
export function useNewProducts(params?: ListParams, config?: SdkConfig) {
|
|
74
|
+
const ctx = useSdkContext()
|
|
75
|
+
const { config: cfg } = resolveHookArgs(config, undefined, ctx)
|
|
76
|
+
return useQuery({
|
|
77
|
+
queryKey: productKeys.new(params),
|
|
78
|
+
queryFn: () => getNewProducts(cfg, params),
|
|
79
|
+
...getCacheTier('medium'),
|
|
80
|
+
})
|
|
151
81
|
}
|
|
152
82
|
|
|
153
83
|
/**
|
|
154
84
|
* 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
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Hook for fetching all active storefront products.
|
|
219
|
-
* Fetches from Next.js API route to avoid exposing backend URLs.
|
|
220
|
-
*/
|
|
221
|
-
export function useStorefrontProducts(params?: ListParams) {
|
|
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
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Hook for fetching a single product by slug.
|
|
253
|
-
* Fetches from Next.js API route to avoid exposing backend URLs.
|
|
254
85
|
*/
|
|
255
|
-
export function
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
}
|
|
86
|
+
export function useOnSaleProducts(params?: ListParams, config?: SdkConfig) {
|
|
87
|
+
const ctx = useSdkContext()
|
|
88
|
+
const { config: cfg } = resolveHookArgs(config, undefined, ctx)
|
|
89
|
+
return useQuery({
|
|
90
|
+
queryKey: productKeys.onSale(params),
|
|
91
|
+
queryFn: () => getOnSaleProducts(cfg, params),
|
|
92
|
+
...getCacheTier('medium'),
|
|
93
|
+
})
|
|
278
94
|
}
|
|
279
95
|
|
|
280
96
|
/**
|
|
281
97
|
* Hook for fetching recommended products.
|
|
282
|
-
* Fetches from Next.js API route to avoid exposing backend URLs.
|
|
283
98
|
*/
|
|
284
|
-
export function useRecommendedProducts(params?: ListParams) {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
}
|
|
99
|
+
export function useRecommendedProducts(params?: ListParams & { excludeProductId?: string }, config?: SdkConfig) {
|
|
100
|
+
const ctx = useSdkContext()
|
|
101
|
+
const { config: cfg } = resolveHookArgs(config, undefined, ctx)
|
|
102
|
+
return useQuery({
|
|
103
|
+
queryKey: productKeys.recommended(params),
|
|
104
|
+
queryFn: () => getRecommendedProducts(cfg, params),
|
|
105
|
+
...getCacheTier('medium'),
|
|
106
|
+
})
|
|
311
107
|
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
4
|
+
import { useVapidPublicKey, useSubscribeToPush, useUnsubscribeFromPush } from './use-notifications';
|
|
5
|
+
import type { SdkConfig } from '../config';
|
|
6
|
+
import { logger } from '../utils/logger';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Converts a base64url VAPID public key (as the backend returns it) into
|
|
10
|
+
* the raw Uint8Array PushManager.subscribe's applicationServerKey expects —
|
|
11
|
+
* the standard, well-known conversion every Web Push integration needs.
|
|
12
|
+
*/
|
|
13
|
+
function urlBase64ToUint8Array(base64String: string): Uint8Array<ArrayBuffer> {
|
|
14
|
+
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
|
|
15
|
+
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
|
|
16
|
+
const rawData = atob(base64);
|
|
17
|
+
const outputArray = new Uint8Array(rawData.length);
|
|
18
|
+
for (let i = 0; i < rawData.length; i++) {
|
|
19
|
+
outputArray[i] = rawData.charCodeAt(i);
|
|
20
|
+
}
|
|
21
|
+
return outputArray;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type PushPermissionState = 'unsupported' | 'default' | 'granted' | 'denied';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Orchestrates the full Web Push subscribe/unsubscribe flow: registers
|
|
28
|
+
* /sw.js (the storefront app's own service worker — see its doc comment),
|
|
29
|
+
* requests Notification permission, subscribes via PushManager with the
|
|
30
|
+
* real VAPID key from the backend, and POSTs the subscription to
|
|
31
|
+
* kasuvia-notification-system-go. Returns current state + trigger functions
|
|
32
|
+
* — no UI of its own, so any storefront can build its own "enable
|
|
33
|
+
* notifications" affordance around it.
|
|
34
|
+
*/
|
|
35
|
+
export function usePushNotifications(config?: SdkConfig, accessToken?: string | null) {
|
|
36
|
+
const [permission, setPermission] = useState<PushPermissionState>('default');
|
|
37
|
+
const [isSubscribed, setIsSubscribed] = useState(false);
|
|
38
|
+
const [isBusy, setIsBusy] = useState(false);
|
|
39
|
+
|
|
40
|
+
const { data: vapid } = useVapidPublicKey(config);
|
|
41
|
+
const subscribeMutation = useSubscribeToPush(config, accessToken);
|
|
42
|
+
const unsubscribeMutation = useUnsubscribeFromPush(config, accessToken);
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (typeof window === 'undefined' || !('Notification' in window) || !('serviceWorker' in navigator) || !('PushManager' in window)) {
|
|
46
|
+
setPermission('unsupported');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
setPermission(Notification.permission as PushPermissionState);
|
|
50
|
+
|
|
51
|
+
navigator.serviceWorker.getRegistration('/sw.js').then(async (reg) => {
|
|
52
|
+
if (!reg) return;
|
|
53
|
+
const sub = await reg.pushManager.getSubscription();
|
|
54
|
+
setIsSubscribed(!!sub);
|
|
55
|
+
}).catch(() => {
|
|
56
|
+
// No registration yet — not subscribed.
|
|
57
|
+
});
|
|
58
|
+
}, []);
|
|
59
|
+
|
|
60
|
+
const subscribe = useCallback(async () => {
|
|
61
|
+
if (permission === 'unsupported') {
|
|
62
|
+
throw new Error('Push notifications are not supported in this browser.');
|
|
63
|
+
}
|
|
64
|
+
if (!vapid?.public_key) {
|
|
65
|
+
throw new Error('Notification service is not ready yet. Please try again in a moment.');
|
|
66
|
+
}
|
|
67
|
+
setIsBusy(true);
|
|
68
|
+
try {
|
|
69
|
+
const perm = await Notification.requestPermission();
|
|
70
|
+
setPermission(perm as PushPermissionState);
|
|
71
|
+
if (perm !== 'granted') {
|
|
72
|
+
throw new Error('Notification permission was not granted.');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const registration = await navigator.serviceWorker.register('/sw.js');
|
|
76
|
+
await navigator.serviceWorker.ready;
|
|
77
|
+
|
|
78
|
+
const existing = await registration.pushManager.getSubscription();
|
|
79
|
+
const pushSubscription = existing ?? await registration.pushManager.subscribe({
|
|
80
|
+
userVisibleOnly: true,
|
|
81
|
+
applicationServerKey: urlBase64ToUint8Array(vapid.public_key),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const json = pushSubscription.toJSON();
|
|
85
|
+
if (!json.endpoint || !json.keys?.p256dh || !json.keys?.auth) {
|
|
86
|
+
throw new Error('Browser returned an incomplete push subscription.');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await subscribeMutation.mutateAsync({
|
|
90
|
+
subscription: {
|
|
91
|
+
endpoint: json.endpoint,
|
|
92
|
+
keys: { p256dh: json.keys.p256dh, auth: json.keys.auth },
|
|
93
|
+
},
|
|
94
|
+
user_agent: navigator.userAgent,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
setIsSubscribed(true);
|
|
98
|
+
} finally {
|
|
99
|
+
setIsBusy(false);
|
|
100
|
+
}
|
|
101
|
+
}, [permission, vapid, subscribeMutation]);
|
|
102
|
+
|
|
103
|
+
const unsubscribe = useCallback(async () => {
|
|
104
|
+
setIsBusy(true);
|
|
105
|
+
try {
|
|
106
|
+
const registration = await navigator.serviceWorker.getRegistration('/sw.js');
|
|
107
|
+
const sub = await registration?.pushManager.getSubscription();
|
|
108
|
+
if (sub) {
|
|
109
|
+
await unsubscribeMutation.mutateAsync(sub.endpoint);
|
|
110
|
+
await sub.unsubscribe();
|
|
111
|
+
}
|
|
112
|
+
setIsSubscribed(false);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
logger.error('[usePushNotifications] Failed to unsubscribe:', error);
|
|
115
|
+
throw error;
|
|
116
|
+
} finally {
|
|
117
|
+
setIsBusy(false);
|
|
118
|
+
}
|
|
119
|
+
}, [unsubscribeMutation]);
|
|
120
|
+
|
|
121
|
+
return { permission, isSubscribed, isBusy, subscribe, unsubscribe };
|
|
122
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Query hooks for Reviews and Comments
|
|
3
|
+
*/
|
|
4
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|
5
|
+
import * as ReviewService from '../services/review.service'
|
|
6
|
+
import type { SdkConfig } from '../config'
|
|
7
|
+
import type {
|
|
8
|
+
ReviewCreate, ReviewUpdate, CommentCreate, CommentUpdate,
|
|
9
|
+
MerchantReplyCreate, ReviewModerationAction, CommentModerationAction
|
|
10
|
+
} from '../types/reviews'
|
|
11
|
+
import { useSdkContext, resolveHookArgs } from '../context/sdk-context'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Hook to create a new review
|
|
15
|
+
*/
|
|
16
|
+
export function useCreateReview(config?: SdkConfig) {
|
|
17
|
+
const ctx = useSdkContext()
|
|
18
|
+
const queryClient = useQueryClient()
|
|
19
|
+
const { config: cfg } = resolveHookArgs(config, undefined, ctx)
|
|
20
|
+
return useMutation({
|
|
21
|
+
mutationFn: (reviewData: ReviewCreate) => ReviewService.createReview(reviewData, cfg),
|
|
22
|
+
onSuccess: () => {
|
|
23
|
+
queryClient.invalidateQueries({ queryKey: ['product-reviews'] })
|
|
24
|
+
},
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Hook to update an existing review
|
|
30
|
+
*/
|
|
31
|
+
export function useUpdateReview(config?: SdkConfig) {
|
|
32
|
+
const ctx = useSdkContext()
|
|
33
|
+
const queryClient = useQueryClient()
|
|
34
|
+
const { config: cfg } = resolveHookArgs(config, undefined, ctx)
|
|
35
|
+
return useMutation({
|
|
36
|
+
mutationFn: ({ reviewId, reviewData }: { reviewId: string; reviewData: ReviewUpdate }) =>
|
|
37
|
+
ReviewService.updateReview(reviewId, reviewData, cfg),
|
|
38
|
+
onSuccess: () => {
|
|
39
|
+
queryClient.invalidateQueries({ queryKey: ['product-reviews'] })
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Hook to get product reviews
|
|
46
|
+
*/
|
|
47
|
+
export function useProductReviews(productId: string, params?: { page?: number; page_size?: number }, config?: SdkConfig) {
|
|
48
|
+
const ctx = useSdkContext()
|
|
49
|
+
const { config: cfg } = resolveHookArgs(config, undefined, ctx)
|
|
50
|
+
return useQuery({
|
|
51
|
+
queryKey: ['product-reviews', productId, params],
|
|
52
|
+
queryFn: () => ReviewService.getProductReviews(productId, params, cfg),
|
|
53
|
+
enabled: !!productId,
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Hook to create a new comment
|
|
59
|
+
*/
|
|
60
|
+
export function useCreateComment(config?: SdkConfig) {
|
|
61
|
+
const ctx = useSdkContext()
|
|
62
|
+
const queryClient = useQueryClient()
|
|
63
|
+
const { config: cfg } = resolveHookArgs(config, undefined, ctx)
|
|
64
|
+
return useMutation({
|
|
65
|
+
mutationFn: (commentData: CommentCreate) => ReviewService.createComment(commentData, cfg),
|
|
66
|
+
onSuccess: () => {
|
|
67
|
+
queryClient.invalidateQueries({ queryKey: ['product-comments'] })
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Hook to update an existing comment
|
|
74
|
+
*/
|
|
75
|
+
export function useUpdateComment(config?: SdkConfig) {
|
|
76
|
+
const ctx = useSdkContext()
|
|
77
|
+
const queryClient = useQueryClient()
|
|
78
|
+
const { config: cfg } = resolveHookArgs(config, undefined, ctx)
|
|
79
|
+
return useMutation({
|
|
80
|
+
mutationFn: ({ commentId, commentData }: { commentId: string; commentData: CommentUpdate }) =>
|
|
81
|
+
ReviewService.updateComment(commentId, commentData, cfg),
|
|
82
|
+
onSuccess: () => {
|
|
83
|
+
queryClient.invalidateQueries({ queryKey: ['product-comments'] })
|
|
84
|
+
},
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Hook to get product comments
|
|
90
|
+
*/
|
|
91
|
+
export function useProductComments(productId: string, params?: { page?: number; page_size?: number }, config?: SdkConfig) {
|
|
92
|
+
const ctx = useSdkContext()
|
|
93
|
+
const { config: cfg } = resolveHookArgs(config, undefined, ctx)
|
|
94
|
+
return useQuery({
|
|
95
|
+
queryKey: ['product-comments', productId, params],
|
|
96
|
+
queryFn: () => ReviewService.getProductComments(productId, params, cfg),
|
|
97
|
+
enabled: !!productId,
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Hook to get pending reviews (merchant/staff only)
|
|
103
|
+
*/
|
|
104
|
+
export function usePendingReviews(config?: SdkConfig) {
|
|
105
|
+
const ctx = useSdkContext()
|
|
106
|
+
const { config: cfg } = resolveHookArgs(config, undefined, ctx)
|
|
107
|
+
return useQuery({
|
|
108
|
+
queryKey: ['pending-reviews'],
|
|
109
|
+
queryFn: () => ReviewService.getPendingReviews(cfg),
|
|
110
|
+
refetchInterval: 30000,
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Hook to get pending comments (merchant/staff only)
|
|
116
|
+
*/
|
|
117
|
+
export function usePendingComments(config?: SdkConfig) {
|
|
118
|
+
const ctx = useSdkContext()
|
|
119
|
+
const { config: cfg } = resolveHookArgs(config, undefined, ctx)
|
|
120
|
+
return useQuery({
|
|
121
|
+
queryKey: ['pending-comments'],
|
|
122
|
+
queryFn: () => ReviewService.getPendingComments(cfg),
|
|
123
|
+
refetchInterval: 30000,
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Hook to moderate a review (merchant/staff only)
|
|
129
|
+
*/
|
|
130
|
+
export function useModerateReview(config?: SdkConfig) {
|
|
131
|
+
const ctx = useSdkContext()
|
|
132
|
+
const queryClient = useQueryClient()
|
|
133
|
+
const { config: cfg } = resolveHookArgs(config, undefined, ctx)
|
|
134
|
+
return useMutation({
|
|
135
|
+
mutationFn: ({ reviewId, action }: { reviewId: string; action: ReviewModerationAction }) =>
|
|
136
|
+
ReviewService.moderateReview(reviewId, action, cfg),
|
|
137
|
+
onSuccess: () => {
|
|
138
|
+
queryClient.invalidateQueries({ queryKey: ['pending-reviews'] })
|
|
139
|
+
queryClient.invalidateQueries({ queryKey: ['product-reviews'] })
|
|
140
|
+
},
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Hook to moderate a comment (merchant/staff only)
|
|
146
|
+
*/
|
|
147
|
+
export function useModerateComment(config?: SdkConfig) {
|
|
148
|
+
const ctx = useSdkContext()
|
|
149
|
+
const queryClient = useQueryClient()
|
|
150
|
+
const { config: cfg } = resolveHookArgs(config, undefined, ctx)
|
|
151
|
+
return useMutation({
|
|
152
|
+
mutationFn: ({ commentId, action }: { commentId: string; action: CommentModerationAction }) =>
|
|
153
|
+
ReviewService.moderateComment(commentId, action, cfg),
|
|
154
|
+
onSuccess: () => {
|
|
155
|
+
queryClient.invalidateQueries({ queryKey: ['pending-comments'] })
|
|
156
|
+
queryClient.invalidateQueries({ queryKey: ['product-comments'] })
|
|
157
|
+
},
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Hook to post a merchant reply to a review (merchant/staff only)
|
|
163
|
+
*/
|
|
164
|
+
export function useMerchantReplyToReview(config?: SdkConfig) {
|
|
165
|
+
const ctx = useSdkContext()
|
|
166
|
+
const queryClient = useQueryClient()
|
|
167
|
+
const { config: cfg } = resolveHookArgs(config, undefined, ctx)
|
|
168
|
+
return useMutation({
|
|
169
|
+
mutationFn: ({ reviewId, replyData }: { reviewId: string; replyData: MerchantReplyCreate }) =>
|
|
170
|
+
ReviewService.merchantReplyToReview(reviewId, replyData, cfg),
|
|
171
|
+
onSuccess: () => {
|
|
172
|
+
queryClient.invalidateQueries({ queryKey: ['product-reviews'] })
|
|
173
|
+
},
|
|
174
|
+
})
|
|
175
|
+
}
|