@raxonltd/raxon-core 1.1.7 → 1.1.8
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/core/component/general.image.tsx +86 -0
- package/core/context/cart.context.tsx +446 -0
- package/core/context/security.context.tsx +151 -0
- package/core/feature/address/api/places.api.ts +76 -0
- package/core/feature/address/form/address-search-input.tsx +125 -0
- package/core/feature/address/hook/use.addres.tsx +63 -0
- package/core/feature/address/hook/use.address-autocomplete.ts +116 -0
- package/core/feature/address/util/address.types.ts +38 -0
- package/core/feature/address/util/parse-google-place.ts +66 -0
- package/core/feature/analytic-event/analytic.event.api.ts +27 -0
- package/core/feature/analytic-event/analytic.event.context.tsx +180 -0
- package/core/feature/analytic-event/analytic.event.util.ts +42 -0
- package/core/feature/analytic-event/use.analytic.auto.tsx +114 -0
- package/core/feature/article/hook/use.article.tsx +33 -0
- package/core/feature/attribute/hook/use.attribute.tsx +24 -0
- package/core/feature/auth/hook/use.auth.tsx +141 -0
- package/core/feature/auth/modal/modal.auth.tsx +80 -0
- package/core/feature/auth/view/view.login.tsx +199 -0
- package/core/feature/auth/view/view.register.tsx +333 -0
- package/core/feature/bank-account/hook/use.bank.account.tsx +47 -0
- package/core/feature/brand/hook/use.brand.tsx +24 -0
- package/core/feature/cart/component/cart.order.summary.tsx +89 -0
- package/core/feature/cart/component/cart.promo.code.section.tsx +208 -0
- package/core/feature/cart/hook/use.cart.tsx +267 -0
- package/core/feature/cart/util/basket-pay.response.ts +67 -0
- package/core/feature/cart/util/cart-optimistic.ts +425 -0
- package/core/feature/cart/util/garanti-payment.ts +27 -0
- package/core/feature/collection/hook/use.collection.tsx +32 -0
- package/core/feature/delivery-method/hook/use.delivery.method.tsx +40 -0
- package/core/feature/delivery-method/util/checkout.delivery.method.ts +11 -0
- package/core/feature/faq/hook/use.faq.tsx +23 -0
- package/core/feature/favorite/hook/use.favorite.tsx +48 -0
- package/core/feature/form-submit/form/form.contact.tsx +118 -0
- package/core/feature/form-submit/hook/use.form.submit.tsx +16 -0
- package/core/feature/invoice/hook/use.invoice.tsx +51 -0
- package/core/feature/newsletter/hook/use.newsletter.tsx +124 -0
- package/core/feature/newsletter/modal/modal.newsletter.product.tsx +163 -0
- package/core/feature/order/hook/use.order.tsx +31 -0
- package/core/feature/payment-method/checkout.payment.options.ts +117 -0
- package/core/feature/payment-method/hook/use.payment.method.tsx +44 -0
- package/core/feature/product/hook/use.product.tsx +122 -0
- package/core/feature/profile/hook/use.profile.tsx +126 -0
- package/core/feature/promo-code/hook/use.promo.code.tsx +27 -0
- package/core/interface/basket.interface.ts +360 -0
- package/core/interface/bootstrap.interface.ts +39 -0
- package/core/interface/context.interface.ts +9 -0
- package/core/interface/inventory.interface.ts +88 -0
- package/core/interface/nexine.interface.ts +4 -0
- package/core/interface/prisma.interface.ts +8844 -0
- package/core/interface/product.interface.ts +111 -0
- package/core/raxon.context.tsx +256 -0
- package/core/schema/checkout.schema.ts +103 -0
- package/core/util/basket.item.display.ts +19 -0
- package/core/util/category.nav.ts +46 -0
- package/core/util/client-ip.ts +35 -0
- package/core/util/collection.util.ts +433 -0
- package/core/util/fetch.bootstrap.ts +21 -0
- package/core/util/garanti-payment.ts +5 -0
- package/core/util/nexine.axios.tsx +104 -0
- package/core/util/no-cache.ts +6 -0
- package/core/util/util.ts +191 -0
- package/core/view/view.checkout.tsx +1964 -0
- package/dist/core/view/view.checkout.js +2 -2
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +12 -3
- package/tailwind.css +11 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import Image from 'next/image';
|
|
5
|
+
|
|
6
|
+
interface GeneralImageProps {
|
|
7
|
+
src?: string;
|
|
8
|
+
alt: string;
|
|
9
|
+
|
|
10
|
+
width?: number;
|
|
11
|
+
height?: number;
|
|
12
|
+
className?: string;
|
|
13
|
+
fill?: boolean;
|
|
14
|
+
aspectRatio?: string;
|
|
15
|
+
style?: React.CSSProperties;
|
|
16
|
+
wrapperClassName?: string;
|
|
17
|
+
sizes?: string;
|
|
18
|
+
priority?: boolean;
|
|
19
|
+
quality?: number;
|
|
20
|
+
placeholder?: 'blur' | 'empty';
|
|
21
|
+
blurDataURL?: string;
|
|
22
|
+
onLoad?: () => void;
|
|
23
|
+
onError?: () => void;
|
|
24
|
+
loading?: 'lazy' | 'eager';
|
|
25
|
+
fetchPriority?: 'low' | 'high';
|
|
26
|
+
objectFit?: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
|
|
27
|
+
fallback?: 'placeholder' | 'hide' | 'error';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const GeneralImage = (props: GeneralImageProps) => {
|
|
31
|
+
// Placeholder image URL
|
|
32
|
+
const placeholderImage = 'https://placehold.co/600x400';
|
|
33
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
34
|
+
|
|
35
|
+
if(!props.src) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let mageData = (
|
|
40
|
+
<>
|
|
41
|
+
{isLoading && (
|
|
42
|
+
<div className="absolute inset-0 flex items-center justify-center bg-gray-100">
|
|
43
|
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
|
|
44
|
+
</div>
|
|
45
|
+
)}
|
|
46
|
+
<Image
|
|
47
|
+
placeholder="empty"
|
|
48
|
+
src={props.src!}
|
|
49
|
+
alt={props.alt}
|
|
50
|
+
fill={props.fill}
|
|
51
|
+
style={{
|
|
52
|
+
...props.style,
|
|
53
|
+
opacity: isLoading ? 0 : 1,
|
|
54
|
+
transition: 'opacity 0.3s ease-in-out'
|
|
55
|
+
}}
|
|
56
|
+
className={props.className}
|
|
57
|
+
width={props.width}
|
|
58
|
+
height={props.height}
|
|
59
|
+
priority={props.priority}
|
|
60
|
+
quality={props.quality ?? 80}
|
|
61
|
+
objectFit={props.objectFit}
|
|
62
|
+
fetchPriority={props.fetchPriority}
|
|
63
|
+
onLoad={() => {
|
|
64
|
+
setIsLoading(false);
|
|
65
|
+
props.onLoad?.();
|
|
66
|
+
}}
|
|
67
|
+
onError={() => {
|
|
68
|
+
setIsLoading(false);
|
|
69
|
+
props.onError?.();
|
|
70
|
+
}}
|
|
71
|
+
/>
|
|
72
|
+
</>
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
if (props.aspectRatio) {
|
|
76
|
+
return (
|
|
77
|
+
<>
|
|
78
|
+
<div className={`aspect-[${props.aspectRatio}] ${props.wrapperClassName}`}>{mageData}</div>
|
|
79
|
+
</>
|
|
80
|
+
);
|
|
81
|
+
} else {
|
|
82
|
+
return mageData;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export { GeneralImage };
|
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
5
|
+
import { useCart } from '@/core/feature/cart/hook/use.cart';
|
|
6
|
+
import toast from 'react-hot-toast';
|
|
7
|
+
import { BasketSummaryInterface } from '@/core/interface/basket.interface';
|
|
8
|
+
import { PromoCode } from '@/core/interface/prisma.interface';
|
|
9
|
+
import {
|
|
10
|
+
applyPendingAdds,
|
|
11
|
+
applyPendingQuantities,
|
|
12
|
+
cartLineKey,
|
|
13
|
+
CART_QUANTITY_DEBOUNCE_MS,
|
|
14
|
+
createEmptyOptimisticCart,
|
|
15
|
+
extractBasketFromInsertResponse,
|
|
16
|
+
extractInsertCartItem,
|
|
17
|
+
findCartLine,
|
|
18
|
+
applyBasketInsertResponse,
|
|
19
|
+
mergeInsertItemIntoCart,
|
|
20
|
+
patchCartCache,
|
|
21
|
+
patchCartItemQuantity,
|
|
22
|
+
readCartCache,
|
|
23
|
+
removeCartLineByKey,
|
|
24
|
+
PendingCartAdd,
|
|
25
|
+
} from '@/core/feature/cart/util/cart-optimistic';
|
|
26
|
+
|
|
27
|
+
interface PendingQuantityUpdate {
|
|
28
|
+
quantity: number;
|
|
29
|
+
productId?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface AddToCartPayload {
|
|
33
|
+
productId: string | number;
|
|
34
|
+
quantity: number;
|
|
35
|
+
variantId?: string | number;
|
|
36
|
+
linePay?: number;
|
|
37
|
+
type?: string;
|
|
38
|
+
deposit?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface CartState {
|
|
42
|
+
cart: BasketSummaryInterface | null;
|
|
43
|
+
cartLoading: boolean;
|
|
44
|
+
isAddingToCart: boolean;
|
|
45
|
+
isUpdatingCart: boolean;
|
|
46
|
+
isProductAdding: (productId: string | number, variantId?: string | number | null) => boolean;
|
|
47
|
+
addToCart: (productId: string | number, quantity: number, variantId?: string | number, options?: { linePay?: number }) => void;
|
|
48
|
+
updateQuantity: (itemId: string | number, quantity: number, productId?: string) => void;
|
|
49
|
+
changeQuantity: (itemId: string | number, delta: number, productId?: string, variantId?: string | number | null) => void;
|
|
50
|
+
removeItem: (itemId: string | number) => void;
|
|
51
|
+
cartTotal: number;
|
|
52
|
+
promoCode: PromoCode | null;
|
|
53
|
+
setPromoCode: (promoCode: PromoCode | null) => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const useCartState = (isAuthenticated: boolean): CartState => {
|
|
57
|
+
const queryClient = useQueryClient();
|
|
58
|
+
const [promoCode, setPromoCode] = useState<PromoCode | null>(null);
|
|
59
|
+
const [pendingQuantities, setPendingQuantities] = useState<Record<string, number>>({});
|
|
60
|
+
const [pendingAdds, setPendingAdds] = useState<Record<string, PendingCartAdd>>({});
|
|
61
|
+
const [addingKeys, setAddingKeys] = useState<Record<string, boolean>>({});
|
|
62
|
+
|
|
63
|
+
const pendingUpdatesRef = useRef<Map<string, PendingQuantityUpdate>>(new Map());
|
|
64
|
+
const debounceTimersRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());
|
|
65
|
+
const pendingAddsRef = useRef<Record<string, PendingCartAdd>>({});
|
|
66
|
+
const pendingQuantitiesRef = useRef<Record<string, number>>({});
|
|
67
|
+
const insertQueueRef = useRef<Promise<void>>(Promise.resolve());
|
|
68
|
+
const cancelledInsertKeysRef = useRef<Set<string>>(new Set());
|
|
69
|
+
const displayCartRef = useRef<BasketSummaryInterface | null>(null);
|
|
70
|
+
|
|
71
|
+
const cartHook = useCart();
|
|
72
|
+
|
|
73
|
+
const { data: cart, isLoading: cartLoading } = cartHook.fetch({
|
|
74
|
+
isEnabled: isAuthenticated,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const insertMutation = cartHook.insert();
|
|
78
|
+
const updateItemMutation = cartHook.updateItem();
|
|
79
|
+
const removeMutation = cartHook.remove();
|
|
80
|
+
|
|
81
|
+
const isUpdatingCart = updateItemMutation.isPending || removeMutation.isPending;
|
|
82
|
+
const isAddingToCart = Object.keys(addingKeys).length > 0 || insertMutation.isPending;
|
|
83
|
+
|
|
84
|
+
const serverCart = useMemo(() => readCartCache(queryClient) ?? cart, [cart, queryClient]);
|
|
85
|
+
|
|
86
|
+
const displayCart = useMemo(() => {
|
|
87
|
+
if (!serverCart && Object.keys(pendingAdds).length === 0) return null;
|
|
88
|
+
|
|
89
|
+
const base = serverCart ?? createEmptyOptimisticCart();
|
|
90
|
+
|
|
91
|
+
let result = base;
|
|
92
|
+
if (Object.keys(pendingAdds).length > 0) {
|
|
93
|
+
result = applyPendingAdds(result, pendingAdds);
|
|
94
|
+
}
|
|
95
|
+
if (Object.keys(pendingQuantities).length > 0) {
|
|
96
|
+
result = applyPendingQuantities(result, pendingQuantities);
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}, [serverCart, pendingAdds, pendingQuantities]);
|
|
100
|
+
|
|
101
|
+
displayCartRef.current = displayCart;
|
|
102
|
+
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
pendingAddsRef.current = pendingAdds;
|
|
105
|
+
}, [pendingAdds]);
|
|
106
|
+
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
pendingQuantitiesRef.current = pendingQuantities;
|
|
109
|
+
}, [pendingQuantities]);
|
|
110
|
+
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
if (!cart) return;
|
|
113
|
+
if (cart.promoCode?.id) {
|
|
114
|
+
setPromoCode((prev) => (prev?.id === cart.promoCode?.id ? prev : (cart.promoCode as PromoCode)));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (!cart.promoCode?.id) {
|
|
118
|
+
setPromoCode(null);
|
|
119
|
+
}
|
|
120
|
+
}, [cart?.promoCode?.id, cart?.promoCode?.code]);
|
|
121
|
+
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
return () => {
|
|
124
|
+
debounceTimersRef.current.forEach((timer) => clearTimeout(timer));
|
|
125
|
+
debounceTimersRef.current.clear();
|
|
126
|
+
};
|
|
127
|
+
}, []);
|
|
128
|
+
|
|
129
|
+
const isProductAdding = (productId: string | number, variantId?: string | number | null) => {
|
|
130
|
+
return Boolean(addingKeys[cartLineKey(productId, variantId)]);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const setAdding = (key: string, value: boolean) => {
|
|
134
|
+
setAddingKeys((prev) => {
|
|
135
|
+
if (value) return { ...prev, [key]: true };
|
|
136
|
+
if (!(key in prev)) return prev;
|
|
137
|
+
const next = { ...prev };
|
|
138
|
+
delete next[key];
|
|
139
|
+
return next;
|
|
140
|
+
});
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const applyLocalQuantity = (itemId: string, quantity: number) => {
|
|
144
|
+
setPendingQuantities((prev) => {
|
|
145
|
+
const next = { ...prev, [itemId]: quantity };
|
|
146
|
+
pendingQuantitiesRef.current = next;
|
|
147
|
+
return next;
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const getLineQuantity = (itemId: string) => {
|
|
152
|
+
if (itemId in pendingQuantitiesRef.current) {
|
|
153
|
+
return Number(pendingQuantitiesRef.current[itemId]);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (itemId.startsWith('optimistic-')) {
|
|
157
|
+
const key = itemId.replace('optimistic-', '');
|
|
158
|
+
const add = pendingAddsRef.current[key];
|
|
159
|
+
if (add) return Number(add.quantity);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const item = displayCartRef.current?.items?.find((line) => String(line.id) === itemId);
|
|
163
|
+
return Number(item?.quantity || 0);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const clearPendingQuantity = (itemId: string) => {
|
|
167
|
+
setPendingQuantities((prev) => {
|
|
168
|
+
if (!(itemId in prev)) return prev;
|
|
169
|
+
const next = { ...prev };
|
|
170
|
+
delete next[itemId];
|
|
171
|
+
return next;
|
|
172
|
+
});
|
|
173
|
+
pendingUpdatesRef.current.delete(itemId);
|
|
174
|
+
|
|
175
|
+
const timer = debounceTimersRef.current.get(itemId);
|
|
176
|
+
if (timer) {
|
|
177
|
+
clearTimeout(timer);
|
|
178
|
+
debounceTimersRef.current.delete(itemId);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const flushQuantityUpdate = (itemId: string) => {
|
|
183
|
+
const pending = pendingUpdatesRef.current.get(itemId);
|
|
184
|
+
if (!pending) return;
|
|
185
|
+
|
|
186
|
+
pendingUpdatesRef.current.delete(itemId);
|
|
187
|
+
debounceTimersRef.current.delete(itemId);
|
|
188
|
+
|
|
189
|
+
updateItemMutation.mutate(
|
|
190
|
+
{ id: itemId, quantity: pending.quantity, productId: pending.productId },
|
|
191
|
+
{
|
|
192
|
+
onSuccess: (data) => {
|
|
193
|
+
patchCartCache(queryClient, (old) => applyBasketInsertResponse(data, old));
|
|
194
|
+
if (!pendingUpdatesRef.current.has(itemId)) {
|
|
195
|
+
setPendingQuantities((prev) => {
|
|
196
|
+
if (!(itemId in prev)) return prev;
|
|
197
|
+
const next = { ...prev };
|
|
198
|
+
delete next[itemId];
|
|
199
|
+
return next;
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
onError: (error: any) => {
|
|
204
|
+
queryClient.invalidateQueries({ queryKey: ['organization', 'cart'] });
|
|
205
|
+
setPendingQuantities((prev) => {
|
|
206
|
+
if (!(itemId in prev)) return prev;
|
|
207
|
+
const next = { ...prev };
|
|
208
|
+
delete next[itemId];
|
|
209
|
+
return next;
|
|
210
|
+
});
|
|
211
|
+
toast.error(error?.response?.data?.info?.message || 'Hata oluştu');
|
|
212
|
+
},
|
|
213
|
+
}
|
|
214
|
+
);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const applyInsertSuccess = (response: unknown, key: string, productId: string | number) => {
|
|
218
|
+
if (cancelledInsertKeysRef.current.has(key)) {
|
|
219
|
+
cancelledInsertKeysRef.current.delete(key);
|
|
220
|
+
const insertItem = extractInsertCartItem(response);
|
|
221
|
+
if (insertItem?.id) {
|
|
222
|
+
removeMutation.mutate(insertItem.id);
|
|
223
|
+
}
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const pendingAdd = pendingAddsRef.current[key];
|
|
228
|
+
const pendingQuantity = pendingAdd?.quantity ?? 1;
|
|
229
|
+
|
|
230
|
+
const basket = extractBasketFromInsertResponse(response);
|
|
231
|
+
const insertItem = extractInsertCartItem(response);
|
|
232
|
+
|
|
233
|
+
if (basket) {
|
|
234
|
+
patchCartCache(queryClient, (old) => applyBasketInsertResponse(basket, old));
|
|
235
|
+
} else if (insertItem) {
|
|
236
|
+
patchCartCache(queryClient, (old) => {
|
|
237
|
+
const base = old ?? createEmptyOptimisticCart();
|
|
238
|
+
return mergeInsertItemIntoCart(
|
|
239
|
+
base,
|
|
240
|
+
insertItem,
|
|
241
|
+
pendingAdd ? { ...pendingAdd, quantity: pendingQuantity } : undefined
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
} else {
|
|
245
|
+
queryClient.invalidateQueries({ queryKey: ['organization', 'cart'] });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (insertItem || basket) {
|
|
249
|
+
const updatedCart = readCartCache(queryClient);
|
|
250
|
+
if (findCartLine(updatedCart, productId, pendingAdd?.variantId)) {
|
|
251
|
+
setPendingAdds((prev) => {
|
|
252
|
+
if (!(key in prev)) return prev;
|
|
253
|
+
const next = { ...prev };
|
|
254
|
+
delete next[key];
|
|
255
|
+
pendingAddsRef.current = next;
|
|
256
|
+
return next;
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (insertItem && pendingQuantity > insertItem.quantity) {
|
|
262
|
+
updateItemMutation.mutate(
|
|
263
|
+
{ id: insertItem.id, quantity: pendingQuantity, productId: String(productId) },
|
|
264
|
+
{
|
|
265
|
+
onSuccess: (data) => {
|
|
266
|
+
patchCartCache(queryClient, (old) => applyBasketInsertResponse(data, old));
|
|
267
|
+
},
|
|
268
|
+
}
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const enqueueInsert = (payload: AddToCartPayload, key: string) => {
|
|
274
|
+
setAdding(key, true);
|
|
275
|
+
|
|
276
|
+
insertQueueRef.current = insertQueueRef.current
|
|
277
|
+
.then(() => insertMutation.mutateAsync(payload))
|
|
278
|
+
.then((response) => {
|
|
279
|
+
applyInsertSuccess(response, key, payload.productId);
|
|
280
|
+
})
|
|
281
|
+
.catch((error: any) => {
|
|
282
|
+
setPendingAdds((prev) => {
|
|
283
|
+
if (!(key in prev)) return prev;
|
|
284
|
+
const next = { ...prev };
|
|
285
|
+
delete next[key];
|
|
286
|
+
pendingAddsRef.current = next;
|
|
287
|
+
return next;
|
|
288
|
+
});
|
|
289
|
+
patchCartCache(queryClient, (old) => {
|
|
290
|
+
const base = old ?? createEmptyOptimisticCart();
|
|
291
|
+
return removeCartLineByKey(base, key);
|
|
292
|
+
});
|
|
293
|
+
queryClient.invalidateQueries({ queryKey: ['organization', 'cart'] });
|
|
294
|
+
toast.error(error?.response?.data?.info?.message || 'Sepete eklenirken bir hata oluştu');
|
|
295
|
+
})
|
|
296
|
+
.finally(() => {
|
|
297
|
+
setAdding(key, false);
|
|
298
|
+
});
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const addToCart = (
|
|
302
|
+
productId: string | number,
|
|
303
|
+
quantity: number,
|
|
304
|
+
variantId?: string | number,
|
|
305
|
+
options?: { linePay?: number }
|
|
306
|
+
) => {
|
|
307
|
+
const key = cartLineKey(productId, variantId);
|
|
308
|
+
const existing = findCartLine(displayCart, productId, variantId);
|
|
309
|
+
|
|
310
|
+
if (existing) {
|
|
311
|
+
changeQuantity(existing.id, quantity, String(productId), variantId);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const pendingAdd: PendingCartAdd = {
|
|
316
|
+
productId: String(productId),
|
|
317
|
+
variantId: variantId != null ? String(variantId) : undefined,
|
|
318
|
+
quantity,
|
|
319
|
+
linePay: options?.linePay,
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const nextPending = { ...pendingAddsRef.current, [key]: pendingAdd };
|
|
323
|
+
pendingAddsRef.current = nextPending;
|
|
324
|
+
setPendingAdds(nextPending);
|
|
325
|
+
|
|
326
|
+
patchCartCache(queryClient, (old) => {
|
|
327
|
+
const base = old ?? createEmptyOptimisticCart();
|
|
328
|
+
return applyPendingAdds(base, { [key]: pendingAdd });
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
enqueueInsert(
|
|
332
|
+
{
|
|
333
|
+
productId,
|
|
334
|
+
quantity,
|
|
335
|
+
variantId,
|
|
336
|
+
type: 'increment',
|
|
337
|
+
deposit: 'disable',
|
|
338
|
+
linePay: options?.linePay,
|
|
339
|
+
},
|
|
340
|
+
key
|
|
341
|
+
);
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const updateQuantity = (
|
|
345
|
+
itemId: string | number,
|
|
346
|
+
quantity: number,
|
|
347
|
+
productId?: string,
|
|
348
|
+
variantId?: string | number | null
|
|
349
|
+
) => {
|
|
350
|
+
const id = itemId.toString();
|
|
351
|
+
if (quantity < 1) return;
|
|
352
|
+
|
|
353
|
+
if (id.startsWith('optimistic-')) {
|
|
354
|
+
const key = id.replace('optimistic-', '');
|
|
355
|
+
const currentAdd = pendingAddsRef.current[key];
|
|
356
|
+
if (!currentAdd) return;
|
|
357
|
+
|
|
358
|
+
const nextAdd = { ...currentAdd, quantity };
|
|
359
|
+
const nextPending = { ...pendingAddsRef.current, [key]: nextAdd };
|
|
360
|
+
pendingAddsRef.current = nextPending;
|
|
361
|
+
setPendingAdds(nextPending);
|
|
362
|
+
|
|
363
|
+
patchCartCache(queryClient, (old) => {
|
|
364
|
+
const base = old ?? createEmptyOptimisticCart();
|
|
365
|
+
return applyPendingAdds(base, { [key]: nextAdd });
|
|
366
|
+
});
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
applyLocalQuantity(id, quantity);
|
|
371
|
+
|
|
372
|
+
patchCartCache(queryClient, (old) => {
|
|
373
|
+
const base = old ?? createEmptyOptimisticCart();
|
|
374
|
+
return patchCartItemQuantity(base, id, quantity, productId, variantId);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
pendingUpdatesRef.current.set(id, { quantity, productId });
|
|
378
|
+
|
|
379
|
+
const existingTimer = debounceTimersRef.current.get(id);
|
|
380
|
+
if (existingTimer) clearTimeout(existingTimer);
|
|
381
|
+
|
|
382
|
+
debounceTimersRef.current.set(
|
|
383
|
+
id,
|
|
384
|
+
setTimeout(() => flushQuantityUpdate(id), CART_QUANTITY_DEBOUNCE_MS)
|
|
385
|
+
);
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const changeQuantity = (
|
|
389
|
+
itemId: string | number,
|
|
390
|
+
delta: number,
|
|
391
|
+
productId?: string,
|
|
392
|
+
variantId?: string | number | null
|
|
393
|
+
) => {
|
|
394
|
+
const id = itemId.toString();
|
|
395
|
+
const nextQuantity = getLineQuantity(id) + delta;
|
|
396
|
+
|
|
397
|
+
if (nextQuantity < 1) {
|
|
398
|
+
removeItem(id);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
updateQuantity(id, nextQuantity, productId, variantId);
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const removeItem = (itemId: string | number) => {
|
|
406
|
+
const id = itemId.toString();
|
|
407
|
+
clearPendingQuantity(id);
|
|
408
|
+
|
|
409
|
+
if (id.startsWith('optimistic-')) {
|
|
410
|
+
const key = id.replace('optimistic-', '');
|
|
411
|
+
cancelledInsertKeysRef.current.add(key);
|
|
412
|
+
setPendingAdds((prev) => {
|
|
413
|
+
if (!(key in prev)) return prev;
|
|
414
|
+
const next = { ...prev };
|
|
415
|
+
delete next[key];
|
|
416
|
+
return next;
|
|
417
|
+
});
|
|
418
|
+
setAdding(key, false);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
removeMutation.mutate(id, {
|
|
423
|
+
onSuccess: () => {
|
|
424
|
+
toast.success('Ürün sepetten çıkarıldı');
|
|
425
|
+
},
|
|
426
|
+
onError: (error: any) => {
|
|
427
|
+
toast.error(error?.response?.data?.info?.message || 'Silinirken hata oluştu');
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
return {
|
|
433
|
+
cart: displayCart,
|
|
434
|
+
cartLoading,
|
|
435
|
+
isAddingToCart,
|
|
436
|
+
isUpdatingCart,
|
|
437
|
+
isProductAdding,
|
|
438
|
+
addToCart,
|
|
439
|
+
updateQuantity,
|
|
440
|
+
changeQuantity,
|
|
441
|
+
removeItem,
|
|
442
|
+
cartTotal: displayCart?.info?.payPrice?.pay || 0,
|
|
443
|
+
promoCode,
|
|
444
|
+
setPromoCode,
|
|
445
|
+
};
|
|
446
|
+
};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
4
|
+
import { useRouter, usePathname } from 'next/navigation';
|
|
5
|
+
import { LoginType, User } from '@/core/interface/prisma.interface';
|
|
6
|
+
import { useAuth } from '@/core/feature/auth/hook/use.auth';
|
|
7
|
+
import { useProfile } from '@/core/feature/profile/hook/use.profile';
|
|
8
|
+
|
|
9
|
+
export interface SecurityState {
|
|
10
|
+
profile: User | null | undefined;
|
|
11
|
+
authLoading: boolean;
|
|
12
|
+
isAuthenticated: boolean;
|
|
13
|
+
isGuest: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const useSecurityState = (): SecurityState => {
|
|
17
|
+
const router = useRouter();
|
|
18
|
+
const pathname = usePathname();
|
|
19
|
+
|
|
20
|
+
const [authLoading, setAuthLoading] = useState<boolean>(true);
|
|
21
|
+
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
|
|
22
|
+
const [isAuthEstablished, setIsAuthEstablished] = useState<boolean>(false);
|
|
23
|
+
|
|
24
|
+
const { mutate: tokenCheck } = useAuth().token();
|
|
25
|
+
const { mutate: loginGuest } = useAuth().loginGuest();
|
|
26
|
+
|
|
27
|
+
const profileQuery = useProfile().fetch({ isEnabled: isAuthEstablished && isAuthenticated });
|
|
28
|
+
const profile = profileQuery.data;
|
|
29
|
+
|
|
30
|
+
const isGuest = useMemo(() => {
|
|
31
|
+
return profile?.loginType === LoginType.GUEST;
|
|
32
|
+
}, [profile]);
|
|
33
|
+
|
|
34
|
+
const isInvalidTokenError = (error: any) => {
|
|
35
|
+
const status = error?.response?.status;
|
|
36
|
+
return status === 401 || status === 403;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const isServerAccessError = (error: any) => {
|
|
40
|
+
const status = error?.response?.status;
|
|
41
|
+
return !error?.response || (typeof status === 'number' && status >= 500);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const ensureGuestLogin = useCallback(() => {
|
|
45
|
+
loginGuest(undefined, {
|
|
46
|
+
onSuccess: () => {
|
|
47
|
+
setIsAuthEstablished(true);
|
|
48
|
+
setIsAuthenticated(true);
|
|
49
|
+
setAuthLoading(false);
|
|
50
|
+
},
|
|
51
|
+
onError: () => {
|
|
52
|
+
setIsAuthEstablished(false);
|
|
53
|
+
setIsAuthenticated(false);
|
|
54
|
+
setAuthLoading(false);
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}, [loginGuest]);
|
|
58
|
+
|
|
59
|
+
const checkToken = useCallback(() => {
|
|
60
|
+
if (typeof window !== 'undefined') {
|
|
61
|
+
const token = localStorage.getItem('koksal-token');
|
|
62
|
+
if (token && token.trim() !== '' && token.length > 3) {
|
|
63
|
+
setAuthLoading(true);
|
|
64
|
+
tokenCheck(undefined, {
|
|
65
|
+
onSuccess: () => {
|
|
66
|
+
setIsAuthEstablished(true);
|
|
67
|
+
setIsAuthenticated(true);
|
|
68
|
+
setAuthLoading(false);
|
|
69
|
+
},
|
|
70
|
+
onError: (error: any) => {
|
|
71
|
+
if (isServerAccessError(error)) {
|
|
72
|
+
setIsAuthEstablished(true);
|
|
73
|
+
setIsAuthenticated(true);
|
|
74
|
+
setAuthLoading(false);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (isInvalidTokenError(error)) {
|
|
79
|
+
localStorage.removeItem('koksal-token');
|
|
80
|
+
ensureGuestLogin();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
ensureGuestLogin();
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
} else {
|
|
88
|
+
setAuthLoading(true);
|
|
89
|
+
ensureGuestLogin();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}, [tokenCheck, ensureGuestLogin]);
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
checkToken();
|
|
96
|
+
}, [checkToken]);
|
|
97
|
+
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (typeof window !== 'undefined') {
|
|
100
|
+
const handleStorageChange = (e: StorageEvent) => {
|
|
101
|
+
if (e.key === 'koksal-token') {
|
|
102
|
+
checkToken();
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const handleCustomStorageChange = () => {
|
|
107
|
+
checkToken();
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
window.addEventListener('storage', handleStorageChange);
|
|
111
|
+
window.addEventListener('koksal-token-changed', handleCustomStorageChange);
|
|
112
|
+
|
|
113
|
+
return () => {
|
|
114
|
+
window.removeEventListener('storage', handleStorageChange);
|
|
115
|
+
window.removeEventListener('koksal-token-changed', handleCustomStorageChange);
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}, [checkToken]);
|
|
119
|
+
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
const isLoading =
|
|
122
|
+
authLoading || (isAuthEstablished && (profileQuery.isLoading || profileQuery.isFetching));
|
|
123
|
+
if (isLoading) return;
|
|
124
|
+
|
|
125
|
+
if (pathname.startsWith('/guvenlik') && isAuthenticated && profile?.loginType !== LoginType.GUEST) {
|
|
126
|
+
router.push('/hesabim');
|
|
127
|
+
}
|
|
128
|
+
if (pathname.startsWith('/hesabim') && (!isAuthenticated || profile?.loginType === LoginType.GUEST)) {
|
|
129
|
+
router.push('/guvenlik/giris-yap');
|
|
130
|
+
}
|
|
131
|
+
}, [
|
|
132
|
+
pathname,
|
|
133
|
+
isAuthenticated,
|
|
134
|
+
authLoading,
|
|
135
|
+
isAuthEstablished,
|
|
136
|
+
profileQuery.isLoading,
|
|
137
|
+
profileQuery.isFetching,
|
|
138
|
+
router,
|
|
139
|
+
profile?.loginType,
|
|
140
|
+
]);
|
|
141
|
+
|
|
142
|
+
const resolvedAuthLoading =
|
|
143
|
+
authLoading || (isAuthEstablished && (profileQuery.isLoading || profileQuery.isFetching));
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
profile,
|
|
147
|
+
authLoading: resolvedAuthLoading,
|
|
148
|
+
isAuthenticated,
|
|
149
|
+
isGuest,
|
|
150
|
+
};
|
|
151
|
+
};
|