@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.
- package/LICENSE +201 -0
- package/README.md +112 -0
- package/dist/index.d.mts +1891 -0
- package/dist/index.d.ts +1891 -0
- package/dist/index.js +3540 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +3314 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +59 -0
- package/src/client/authClient.ts +135 -0
- package/src/client/graphqlClient.ts +53 -0
- package/src/client/httpClient.ts +298 -0
- package/src/config/index.ts +102 -0
- package/src/graphql/collections.graphql.ts +56 -0
- package/src/graphql/index.ts +7 -0
- package/src/graphql/products.graphql.ts +710 -0
- package/src/graphql/wishlist.graphql.ts +33 -0
- package/src/hooks/index.ts +26 -0
- package/src/hooks/use-auth.ts +182 -0
- package/src/hooks/use-bespoke-availability.ts +20 -0
- package/src/hooks/use-cart.ts +207 -0
- package/src/hooks/use-cms.ts +165 -0
- package/src/hooks/use-collections.ts +40 -0
- package/src/hooks/use-consultation-stream.ts +141 -0
- package/src/hooks/use-consultations.ts +76 -0
- package/src/hooks/use-countries.ts +24 -0
- package/src/hooks/use-currencies.ts +27 -0
- package/src/hooks/use-google-translate.ts +164 -0
- package/src/hooks/use-is-mounted.ts +9 -0
- package/src/hooks/use-languages.ts +33 -0
- package/src/hooks/use-order-stream.ts +195 -0
- package/src/hooks/use-orders.ts +64 -0
- package/src/hooks/use-payment.ts +68 -0
- package/src/hooks/use-products.ts +113 -0
- package/src/hooks/use-service-booking.ts +118 -0
- package/src/hooks/use-states.ts +28 -0
- package/src/hooks/use-wishlist.ts +121 -0
- package/src/index.ts +65 -0
- package/src/services/app.service.ts +17 -0
- package/src/services/auth.service.ts +332 -0
- package/src/services/bespoke.service.ts +17 -0
- package/src/services/cart.service.ts +245 -0
- package/src/services/cms.service.ts +112 -0
- package/src/services/collection.service.ts +77 -0
- package/src/services/config.service.ts +42 -0
- package/src/services/consultation.service.ts +51 -0
- package/src/services/index.ts +21 -0
- package/src/services/logistics.service.ts +95 -0
- package/src/services/notifications.service.ts +50 -0
- package/src/services/order.service.ts +169 -0
- package/src/services/payment.service.ts +127 -0
- package/src/services/product.service.ts +678 -0
- package/src/services/service-booking.service.ts +86 -0
- package/src/services/wishlist.service.ts +84 -0
- package/src/types/auth/index.ts +126 -0
- package/src/types/bespoke/index.ts +39 -0
- package/src/types/business/index.ts +6 -0
- package/src/types/cms/index.ts +77 -0
- package/src/types/consultations/index.ts +79 -0
- package/src/types/index.ts +27 -0
- package/src/types/media/index.ts +41 -0
- package/src/types/payments/index.ts +120 -0
- package/src/types/services/index.ts +80 -0
- package/src/types/store/index.ts +282 -0
- package/src/types/store/shipping.ts +38 -0
- package/src/types/ui/index.ts +32 -0
- package/src/utils/cache.ts +33 -0
- package/src/utils/checkout.ts +39 -0
- package/src/utils/customer.ts +20 -0
- package/src/utils/error-handler.ts +14 -0
- package/src/utils/index.ts +10 -0
- package/src/utils/logger.ts +79 -0
- 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
|
+
}
|