@kasuvia/sdk 0.0.1

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 (73) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +112 -0
  3. package/dist/index.d.mts +1891 -0
  4. package/dist/index.d.ts +1891 -0
  5. package/dist/index.js +3540 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/index.mjs +3314 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/package.json +59 -0
  10. package/src/client/authClient.ts +135 -0
  11. package/src/client/graphqlClient.ts +53 -0
  12. package/src/client/httpClient.ts +298 -0
  13. package/src/config/index.ts +102 -0
  14. package/src/graphql/collections.graphql.ts +56 -0
  15. package/src/graphql/index.ts +7 -0
  16. package/src/graphql/products.graphql.ts +710 -0
  17. package/src/graphql/wishlist.graphql.ts +33 -0
  18. package/src/hooks/index.ts +26 -0
  19. package/src/hooks/use-auth.ts +182 -0
  20. package/src/hooks/use-bespoke-availability.ts +20 -0
  21. package/src/hooks/use-cart.ts +207 -0
  22. package/src/hooks/use-cms.ts +165 -0
  23. package/src/hooks/use-collections.ts +40 -0
  24. package/src/hooks/use-consultation-stream.ts +141 -0
  25. package/src/hooks/use-consultations.ts +76 -0
  26. package/src/hooks/use-countries.ts +24 -0
  27. package/src/hooks/use-currencies.ts +27 -0
  28. package/src/hooks/use-google-translate.ts +164 -0
  29. package/src/hooks/use-is-mounted.ts +9 -0
  30. package/src/hooks/use-languages.ts +33 -0
  31. package/src/hooks/use-order-stream.ts +195 -0
  32. package/src/hooks/use-orders.ts +64 -0
  33. package/src/hooks/use-payment.ts +68 -0
  34. package/src/hooks/use-products.ts +113 -0
  35. package/src/hooks/use-service-booking.ts +118 -0
  36. package/src/hooks/use-states.ts +28 -0
  37. package/src/hooks/use-wishlist.ts +121 -0
  38. package/src/index.ts +65 -0
  39. package/src/services/app.service.ts +17 -0
  40. package/src/services/auth.service.ts +332 -0
  41. package/src/services/bespoke.service.ts +17 -0
  42. package/src/services/cart.service.ts +245 -0
  43. package/src/services/cms.service.ts +112 -0
  44. package/src/services/collection.service.ts +77 -0
  45. package/src/services/config.service.ts +42 -0
  46. package/src/services/consultation.service.ts +51 -0
  47. package/src/services/index.ts +21 -0
  48. package/src/services/logistics.service.ts +95 -0
  49. package/src/services/notifications.service.ts +50 -0
  50. package/src/services/order.service.ts +169 -0
  51. package/src/services/payment.service.ts +127 -0
  52. package/src/services/product.service.ts +678 -0
  53. package/src/services/service-booking.service.ts +86 -0
  54. package/src/services/wishlist.service.ts +84 -0
  55. package/src/types/auth/index.ts +126 -0
  56. package/src/types/bespoke/index.ts +39 -0
  57. package/src/types/business/index.ts +6 -0
  58. package/src/types/cms/index.ts +77 -0
  59. package/src/types/consultations/index.ts +79 -0
  60. package/src/types/index.ts +27 -0
  61. package/src/types/media/index.ts +41 -0
  62. package/src/types/payments/index.ts +120 -0
  63. package/src/types/services/index.ts +80 -0
  64. package/src/types/store/index.ts +282 -0
  65. package/src/types/store/shipping.ts +38 -0
  66. package/src/types/ui/index.ts +32 -0
  67. package/src/utils/cache.ts +33 -0
  68. package/src/utils/checkout.ts +39 -0
  69. package/src/utils/customer.ts +20 -0
  70. package/src/utils/error-handler.ts +14 -0
  71. package/src/utils/index.ts +10 -0
  72. package/src/utils/logger.ts +79 -0
  73. package/src/utils/sanitize-url.ts +55 -0
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Hooks Barrel Export
3
+ *
4
+ * Central export point for all React Query hooks.
5
+ */
6
+
7
+ export * from './use-cart'
8
+ export * from './use-orders'
9
+ export * from './use-auth'
10
+ export * from './use-countries'
11
+ export * from './use-states'
12
+ export * from './use-languages'
13
+ export * from './use-currencies'
14
+ export * from './use-bespoke-availability'
15
+ export * from './use-collections'
16
+ export * from './use-products'
17
+ export * from './use-wishlist'
18
+ export * from './use-service-booking'
19
+ export * from './use-consultations'
20
+ export * from './use-consultation-stream'
21
+ export * from './use-order-stream'
22
+ export * from './use-is-mounted'
23
+ export * from './use-google-translate'
24
+ export * from './use-payment'
25
+ export * from './use-cms'
26
+
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Auth Hook & Context Provider
3
+ *
4
+ * Provides customer authentication state and modal controls across the storefront.
5
+ */
6
+ import React, { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react'
7
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
8
+ import { getAuthUser, loginCustomer, registerCustomer, updateProfile, logout as logoutService, silentRefresh } from '../services/auth.service'
9
+ import { tokenStore } from '../client/authClient'
10
+ import { resetGraphQLClient } from '../client/graphqlClient'
11
+ import { logger } from '../utils/logger'
12
+ import type {
13
+ CustomerLoginPayload,
14
+ CustomerRegisterPayload,
15
+ UpdateProfilePayload,
16
+ CustomerUser,
17
+ AuthContextValue,
18
+ } from '../types/auth'
19
+
20
+ // ── React Context ────────────────────────────────────────────────────────────
21
+
22
+ export const AuthCtx = React.createContext ? React.createContext<AuthContextValue | null>(null) : null
23
+
24
+ export function AuthProvider({ children }: { children: ReactNode }) {
25
+ const [customer, setCustomer] = useState<CustomerUser | null>(null)
26
+ const [isLoading, setIsLoading] = useState(true)
27
+ const [isModalOpen, setIsModalOpen] = useState(false)
28
+
29
+ // Rehydrate session from stored refresh token on app boot
30
+ useEffect(() => {
31
+ let voided = false
32
+ ;(async () => {
33
+ setIsLoading(true)
34
+ try {
35
+ const result = await silentRefresh()
36
+ if (!voided && result) {
37
+ setCustomer(result.user)
38
+ resetGraphQLClient()
39
+ }
40
+ } catch (error: unknown) {
41
+ logger.error('[AuthProvider] Session restore failed:', error)
42
+ if (!voided) setCustomer(null)
43
+ } finally {
44
+ if (!voided) setIsLoading(false)
45
+ }
46
+ })()
47
+ return () => {
48
+ voided = true
49
+ }
50
+ }, [])
51
+
52
+ const openModal = useCallback(() => setIsModalOpen(true), [])
53
+ const closeModal = useCallback(() => {
54
+ setIsModalOpen(false)
55
+ if (typeof window !== 'undefined' && (window.location.hash === '#login' || window.location.hash === '#signup')) {
56
+ window.history.replaceState(null, '', window.location.pathname + window.location.search)
57
+ }
58
+ }, [])
59
+
60
+ // Listen for #login and #signup hashes to open modal automatically
61
+ useEffect(() => {
62
+ const handleHash = () => {
63
+ if (typeof window === 'undefined') return
64
+ if (window.location.hash === '#login' || window.location.hash === '#signup') {
65
+ if (!isLoading && customer) {
66
+ window.history.replaceState(null, '', window.location.pathname + window.location.search)
67
+ } else if (!isLoading && !customer) {
68
+ setIsModalOpen(true)
69
+ }
70
+ }
71
+ }
72
+ handleHash()
73
+ window.addEventListener('hashchange', handleHash)
74
+ return () => window.removeEventListener('hashchange', handleHash)
75
+ }, [customer, isLoading])
76
+
77
+ // Reset GraphQL client when customer state changes to ensure auth headers are included
78
+ useEffect(() => {
79
+ resetGraphQLClient()
80
+ }, [customer])
81
+
82
+ const handleLogout = useCallback(async () => {
83
+ await logoutService()
84
+ setCustomer(null)
85
+ setIsModalOpen(false)
86
+ resetGraphQLClient()
87
+ }, [])
88
+
89
+ const value: AuthContextValue = {
90
+ customer,
91
+ isLoading,
92
+ isModalOpen,
93
+ openModal,
94
+ closeModal,
95
+ setCustomer,
96
+ logout: handleLogout,
97
+ }
98
+
99
+ if (!AuthCtx) return children
100
+ return React.createElement(AuthCtx.Provider, { value }, children)
101
+ }
102
+
103
+ // ── Hooks ────────────────────────────────────────────────────────────────────
104
+
105
+ export function useAuthUser() {
106
+ return useQuery({
107
+ queryKey: ['auth-user'],
108
+ queryFn: getAuthUser,
109
+ enabled: !!tokenStore.getAccessToken(),
110
+ })
111
+ }
112
+
113
+ export function useLogin() {
114
+ const queryClient = useQueryClient()
115
+
116
+ return useMutation({
117
+ mutationFn: loginCustomer,
118
+ onSuccess: () => {
119
+ queryClient.invalidateQueries({ queryKey: ['auth-user'] })
120
+ queryClient.invalidateQueries({ queryKey: ['cart'] })
121
+ },
122
+ })
123
+ }
124
+
125
+ export function useRegister() {
126
+ const queryClient = useQueryClient()
127
+
128
+ return useMutation({
129
+ mutationFn: registerCustomer,
130
+ onSuccess: () => {
131
+ queryClient.invalidateQueries({ queryKey: ['auth-user'] })
132
+ queryClient.invalidateQueries({ queryKey: ['cart'] })
133
+ },
134
+ })
135
+ }
136
+
137
+ export function useUpdateProfile() {
138
+ const queryClient = useQueryClient()
139
+
140
+ return useMutation({
141
+ mutationFn: updateProfile,
142
+ onSuccess: () => {
143
+ queryClient.invalidateQueries({ queryKey: ['auth-user'] })
144
+ },
145
+ })
146
+ }
147
+
148
+ export function useLogout() {
149
+ const queryClient = useQueryClient()
150
+
151
+ return useMutation({
152
+ mutationFn: logoutService,
153
+ onSuccess: () => {
154
+ queryClient.clear()
155
+ },
156
+ })
157
+ }
158
+
159
+ /**
160
+ * useAuth()
161
+ * Consumes the AuthCtx provider value. Falls back to local hook query if no provider exists.
162
+ */
163
+ export function useAuth(): AuthContextValue {
164
+ const context = AuthCtx ? useContext(AuthCtx) : null
165
+ if (context) return context
166
+
167
+ // Fallback if useAuth is called outside AuthProvider (e.g. simple SSR/unit tests)
168
+ const authUserQuery = useAuthUser()
169
+ const logoutMutation = useLogout()
170
+
171
+ return {
172
+ customer: authUserQuery.data as CustomerUser | null,
173
+ isLoading: authUserQuery.isLoading,
174
+ isModalOpen: false,
175
+ openModal: () => {},
176
+ closeModal: () => {},
177
+ setCustomer: () => {},
178
+ logout: async () => {
179
+ await logoutMutation.mutateAsync()
180
+ },
181
+ }
182
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Bespoke Availability Hook
3
+ *
4
+ * React Query hook for checking bespoke service availability.
5
+ */
6
+ import { useQuery } from '@tanstack/react-query'
7
+ import { bespokeService } from '../services/bespoke.service'
8
+ import type { BespokeAvailability } from '../types/consultations'
9
+
10
+ /**
11
+ * Get bespoke availability for a product
12
+ */
13
+ export function useBespokeAvailability(product_id: string) {
14
+ return useQuery({
15
+ queryKey: ['bespoke-availability', product_id],
16
+ queryFn: () => bespokeService.checkAvailability(product_id),
17
+ enabled: !!product_id,
18
+ retry: false, // Don't retry if no config exists (returns 404)
19
+ })
20
+ }
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Cart Hook & Context Provider
3
+ *
4
+ * React Query hooks for cart operations with unified drawer state sharing.
5
+ */
6
+ import React, { createContext, useContext, useState, type ReactNode } from 'react'
7
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
8
+ import { getCart, addToCart, updateCartItem, removeFromCart, clearCart, mergeGuestCartIfPresent } from '../services/cart.service'
9
+ import { isInitialized } from '../config'
10
+ import type { CartItem, CartResponse, Product } from '../types/store'
11
+ import axios from 'axios'
12
+
13
+ interface ProductTypeMismatchDetail {
14
+ code?: string
15
+ message?: string
16
+ existing_type?: string
17
+ new_type?: string
18
+ }
19
+
20
+ export interface UseCartReturn {
21
+ cart: CartResponse | undefined
22
+ items: CartItem[]
23
+ count: number
24
+ isOpen: boolean
25
+ setOpen: (open: boolean) => void
26
+ isLoading: boolean
27
+ error: Error | null
28
+ addToCart: (item: {
29
+ product_id: string
30
+ product_slug: string
31
+ product_name: string
32
+ image?: string
33
+ variant_id?: string
34
+ variant_attributes?: string
35
+ unit_price: number
36
+ quantity: number
37
+ custom_measurements?: string
38
+ is_bespoke: boolean
39
+ }) => void
40
+ updateCartItem: (variables: { itemId: string; quantity: number }) => void
41
+ removeFromCart: (itemId: string) => void
42
+ clearCart: () => void
43
+ clear: () => void
44
+ mergeGuestCart: () => void
45
+ addItem: (product: Product, variantId?: string, quantity?: number, customMeasurements?: Record<string, string>) => void
46
+ updateQty: (itemId: string, quantity: number) => void
47
+ removeItem: (itemId: string) => void
48
+ subtotal: number
49
+ productTypeMismatch: {
50
+ message: string
51
+ existing_type: string
52
+ new_type: string
53
+ } | null
54
+ handleClearCartAndAdd: () => void
55
+ handleKeepCurrentCart: () => void
56
+ }
57
+
58
+ // ── React Context for Shared Drawer State ─────────────────────────────────────
59
+
60
+ export interface CartContextValue {
61
+ isOpen: boolean
62
+ setOpen: (open: boolean) => void
63
+ }
64
+
65
+ export const CartCtx = React.createContext ? React.createContext<CartContextValue | null>(null) : null
66
+
67
+ export function CartProvider({ children }: { children: ReactNode }) {
68
+ const [isOpen, setIsOpen] = useState(false)
69
+ if (!CartCtx) return children
70
+ return React.createElement(CartCtx.Provider, { value: { isOpen, setOpen: setIsOpen } }, children)
71
+ }
72
+
73
+ // ── Hook ──────────────────────────────────────────────────────────────────────
74
+
75
+ export function useCart(): UseCartReturn {
76
+ const queryClient = useQueryClient()
77
+ const context = CartCtx ? useContext(CartCtx) : null
78
+
79
+ const isOpen = context ? context.isOpen : false
80
+ const setOpen = context ? context.setOpen : () => {}
81
+
82
+ // Track product type mismatch errors from cart operations
83
+ const [productTypeMismatch, setProductTypeMismatch] = useState<{
84
+ message: string
85
+ existing_type: string
86
+ new_type: string
87
+ } | null>(null)
88
+
89
+ const cartQuery = useQuery({
90
+ queryKey: ['cart'],
91
+ queryFn: getCart,
92
+ enabled: isInitialized(),
93
+ })
94
+
95
+ const addToCartMutation = useMutation({
96
+ mutationFn: addToCart,
97
+ onSuccess: () => {
98
+ setProductTypeMismatch(null) // Clear error on success
99
+ queryClient.invalidateQueries({ queryKey: ['cart'] })
100
+ },
101
+ onError: (error: unknown) => {
102
+ // Handle product type mismatch errors from backend
103
+ if (axios.isAxiosError(error) && error.response?.status === 409) {
104
+ const responseData = error.response?.data as { detail?: ProductTypeMismatchDetail } | undefined
105
+ const errorDetail = responseData?.detail
106
+ if (errorDetail?.code === 'CART_PRODUCT_TYPE_MISMATCH') {
107
+ setProductTypeMismatch({
108
+ message: errorDetail.message ?? 'Your cart contains incompatible items.',
109
+ existing_type: errorDetail.existing_type ?? 'physical',
110
+ new_type: errorDetail.new_type ?? 'physical',
111
+ })
112
+ }
113
+ }
114
+ },
115
+ })
116
+
117
+ const updateCartItemMutation = useMutation({
118
+ mutationFn: ({ itemId, quantity }: { itemId: string; quantity: number }) =>
119
+ updateCartItem(itemId, quantity),
120
+ onSuccess: () => {
121
+ queryClient.invalidateQueries({ queryKey: ['cart'] })
122
+ },
123
+ })
124
+
125
+ const removeFromCartMutation = useMutation({
126
+ mutationFn: removeFromCart,
127
+ onSuccess: () => {
128
+ queryClient.invalidateQueries({ queryKey: ['cart'] })
129
+ },
130
+ })
131
+
132
+ const clearCartMutation = useMutation({
133
+ mutationFn: clearCart,
134
+ onSuccess: () => {
135
+ queryClient.invalidateQueries({ queryKey: ['cart'] })
136
+ },
137
+ })
138
+
139
+ const mergeGuestCartMutation = useMutation({
140
+ mutationFn: mergeGuestCartIfPresent,
141
+ onSuccess: () => {
142
+ queryClient.invalidateQueries({ queryKey: ['cart'] })
143
+ },
144
+ })
145
+
146
+ // Convenience method for adding items with product object
147
+ const addItem = (product: Product, variantId?: string, quantity: number = 1, customMeasurements?: Record<string, string>) => {
148
+ const variant = variantId ? product.variants.find(v => v.id === variantId) : undefined
149
+ return addToCartMutation.mutate({
150
+ product_id: product.id,
151
+ product_slug: product.slug,
152
+ product_name: product.name,
153
+ image: product.images[0]?.url,
154
+ variant_id: variantId,
155
+ variant_attributes: variant?.attributes ? JSON.stringify(variant.attributes) : undefined,
156
+ unit_price: variant?.price ?? product.base_price,
157
+ quantity,
158
+ custom_measurements: customMeasurements ? JSON.stringify(customMeasurements) : undefined,
159
+ is_bespoke: product.is_bespoke,
160
+ })
161
+ }
162
+
163
+ // Helper methods for storefront compatibility
164
+ const items = cartQuery.data?.items || []
165
+ const subtotal = cartQuery.data?.subtotal || 0
166
+ const count = items.reduce((total, item) => total + item.quantity, 0)
167
+
168
+ const updateQty = (itemId: string, quantity: number) => {
169
+ updateCartItemMutation.mutate({ itemId, quantity })
170
+ }
171
+
172
+ const removeItem = (itemId: string) => {
173
+ removeFromCartMutation.mutate(itemId)
174
+ }
175
+
176
+ const handleClearCartAndAdd = () => {
177
+ clearCartMutation.mutate()
178
+ setProductTypeMismatch(null)
179
+ }
180
+
181
+ const handleKeepCurrentCart = () => {
182
+ setProductTypeMismatch(null)
183
+ }
184
+
185
+ return {
186
+ cart: cartQuery.data,
187
+ items,
188
+ count,
189
+ isOpen,
190
+ setOpen,
191
+ isLoading: cartQuery.isLoading,
192
+ error: cartQuery.error,
193
+ addToCart: addToCartMutation.mutate,
194
+ updateCartItem: updateCartItemMutation.mutate,
195
+ removeFromCart: removeFromCartMutation.mutate,
196
+ clearCart: clearCartMutation.mutate,
197
+ clear: clearCartMutation.mutate, // Alias for storefront compatibility
198
+ mergeGuestCart: mergeGuestCartMutation.mutate,
199
+ addItem,
200
+ updateQty,
201
+ removeItem,
202
+ subtotal,
203
+ productTypeMismatch,
204
+ handleClearCartAndAdd,
205
+ handleKeepCurrentCart,
206
+ }
207
+ }
@@ -0,0 +1,165 @@
1
+ /**
2
+ * CMS Hooks
3
+ *
4
+ * React Query hooks for CMS model and entry operations.
5
+ */
6
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
7
+ import {
8
+ listCmsEntries,
9
+ getCmsEntry,
10
+ createCmsEntry,
11
+ updateCmsEntry,
12
+ deleteCmsEntry,
13
+ } from '../services/cms.service'
14
+ import { isInitialized } from '../config'
15
+ import type {
16
+ CmsEntry,
17
+ CmsEntryListResponse,
18
+ CreateCmsEntryParams,
19
+ UpdateCmsEntryParams,
20
+ ListCmsEntriesParams,
21
+ } from '../types/cms'
22
+
23
+ export interface UseCmsEntriesReturn {
24
+ entries: CmsEntry[]
25
+ total: number
26
+ page: number
27
+ page_size: number
28
+ isLoading: boolean
29
+ error: Error | null
30
+ refetch: () => void
31
+ }
32
+
33
+ /**
34
+ * Hook to list CMS entries for a specific model.
35
+ */
36
+ export function useCmsEntries(
37
+ modelSlug: string,
38
+ params?: ListCmsEntriesParams
39
+ ): UseCmsEntriesReturn {
40
+ const query = useQuery({
41
+ queryKey: ['cms-entries', modelSlug, params],
42
+ queryFn: () => listCmsEntries(modelSlug, params),
43
+ enabled: isInitialized() && !!modelSlug,
44
+ })
45
+
46
+ return {
47
+ entries: query.data?.entries || [],
48
+ total: query.data?.total || 0,
49
+ page: query.data?.page || 1,
50
+ page_size: query.data?.page_size || 50,
51
+ isLoading: query.isLoading,
52
+ error: query.error as Error | null,
53
+ refetch: query.refetch,
54
+ }
55
+ }
56
+
57
+ export interface UseCmsEntryReturn {
58
+ entry: CmsEntry | undefined
59
+ isLoading: boolean
60
+ error: Error | null
61
+ refetch: () => void
62
+ }
63
+
64
+ /**
65
+ * Hook to get a single CMS entry by ID.
66
+ */
67
+ export function useCmsEntry(
68
+ modelSlug: string,
69
+ entryId: string
70
+ ): UseCmsEntryReturn {
71
+ const query = useQuery({
72
+ queryKey: ['cms-entry', modelSlug, entryId],
73
+ queryFn: () => getCmsEntry(modelSlug, entryId),
74
+ enabled: isInitialized() && !!modelSlug && !!entryId,
75
+ })
76
+
77
+ return {
78
+ entry: query.data,
79
+ isLoading: query.isLoading,
80
+ error: query.error as Error | null,
81
+ refetch: query.refetch,
82
+ }
83
+ }
84
+
85
+ export interface UseCreateCmsEntryReturn {
86
+ createEntry: (params: CreateCmsEntryParams) => void
87
+ isLoading: boolean
88
+ error: Error | null
89
+ }
90
+
91
+ /**
92
+ * Hook to create a new CMS entry.
93
+ */
94
+ export function useCreateCmsEntry(modelSlug: string): UseCreateCmsEntryReturn {
95
+ const queryClient = useQueryClient()
96
+
97
+ const mutation = useMutation({
98
+ mutationFn: (params: CreateCmsEntryParams) => createCmsEntry(modelSlug, params),
99
+ onSuccess: () => {
100
+ queryClient.invalidateQueries({ queryKey: ['cms-entries', modelSlug] })
101
+ },
102
+ })
103
+
104
+ return {
105
+ createEntry: mutation.mutate,
106
+ isLoading: mutation.isPending,
107
+ error: mutation.error as Error | null,
108
+ }
109
+ }
110
+
111
+ export interface UseUpdateCmsEntryReturn {
112
+ updateEntry: (entryId: string, params: UpdateCmsEntryParams) => void
113
+ isLoading: boolean
114
+ error: Error | null
115
+ }
116
+
117
+ /**
118
+ * Hook to update an existing CMS entry.
119
+ */
120
+ export function useUpdateCmsEntry(modelSlug: string): UseUpdateCmsEntryReturn {
121
+ const queryClient = useQueryClient()
122
+
123
+ const mutation = useMutation({
124
+ mutationFn: ({ entryId, params }: { entryId: string; params: UpdateCmsEntryParams }) =>
125
+ updateCmsEntry(modelSlug, entryId, params),
126
+ onSuccess: () => {
127
+ queryClient.invalidateQueries({ queryKey: ['cms-entries', modelSlug] })
128
+ queryClient.invalidateQueries({ queryKey: ['cms-entry', modelSlug] })
129
+ },
130
+ })
131
+
132
+ return {
133
+ updateEntry: (entryId: string, params: UpdateCmsEntryParams) =>
134
+ mutation.mutate({ entryId, params }),
135
+ isLoading: mutation.isPending,
136
+ error: mutation.error as Error | null,
137
+ }
138
+ }
139
+
140
+ export interface UseDeleteCmsEntryReturn {
141
+ deleteEntry: (entryId: string) => void
142
+ isLoading: boolean
143
+ error: Error | null
144
+ }
145
+
146
+ /**
147
+ * Hook to delete a CMS entry.
148
+ */
149
+ export function useDeleteCmsEntry(modelSlug: string): UseDeleteCmsEntryReturn {
150
+ const queryClient = useQueryClient()
151
+
152
+ const mutation = useMutation({
153
+ mutationFn: (entryId: string) => deleteCmsEntry(modelSlug, entryId),
154
+ onSuccess: () => {
155
+ queryClient.invalidateQueries({ queryKey: ['cms-entries', modelSlug] })
156
+ queryClient.invalidateQueries({ queryKey: ['cms-entry', modelSlug] })
157
+ },
158
+ })
159
+
160
+ return {
161
+ deleteEntry: mutation.mutate,
162
+ isLoading: mutation.isPending,
163
+ error: mutation.error as Error | null,
164
+ }
165
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Collections Hooks
3
+ *
4
+ * React Query hooks for fetching collections.
5
+ */
6
+ import { useQuery } from '@tanstack/react-query'
7
+ import { getStorefrontCollections, getStorefrontCollectionBySlug } from '../services/collection.service'
8
+ import type { ListParams } from '../types/store'
9
+ import { getCacheTier } from '../utils/cache'
10
+
11
+ export const collectionKeys = {
12
+ all: ['core', 'collections'] as const,
13
+ list: (params?: ListParams) => ['core', 'collections', 'list', params ?? {}] as const,
14
+ slug: (slug: string) => ['core', 'collections', 'slug', slug] as const,
15
+ }
16
+
17
+ /**
18
+ * Hook to fetch paginated collections for the storefront.
19
+ */
20
+ export function useStorefrontCollections(params?: ListParams) {
21
+ return useQuery({
22
+ queryKey: collectionKeys.list(params),
23
+ queryFn: () => getStorefrontCollections(params),
24
+ // medium — collections change infrequently
25
+ ...getCacheTier('medium'),
26
+ })
27
+ }
28
+
29
+ /**
30
+ * Hook to fetch a single collection by slug for the storefront.
31
+ */
32
+ export function useStorefrontCollection(slug: string) {
33
+ return useQuery({
34
+ queryKey: collectionKeys.slug(slug),
35
+ queryFn: () => getStorefrontCollectionBySlug(slug),
36
+ // medium — collection details change infrequently
37
+ ...getCacheTier('medium'),
38
+ enabled: !!slug,
39
+ })
40
+ }