@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,111 @@
|
|
|
1
|
+
import {ProductContent, ProductType, Property, SaleType, WeightType} from '@/core/interface/prisma.interface';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
export interface Price {
|
|
5
|
+
mainPrice: number;
|
|
6
|
+
discountPrice: number;
|
|
7
|
+
taxAmount?: number;
|
|
8
|
+
depositAmount?: number;
|
|
9
|
+
basketPrice?: number;
|
|
10
|
+
payPrice?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface Product {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
saleType: SaleType;
|
|
16
|
+
createdAt?: string;
|
|
17
|
+
updatedAt?: string;
|
|
18
|
+
type: ProductType;
|
|
19
|
+
slug: string;
|
|
20
|
+
stock?: number;
|
|
21
|
+
sku?: string;
|
|
22
|
+
productContent? : ProductContent
|
|
23
|
+
articleNumber?: string;
|
|
24
|
+
tags?: string[];
|
|
25
|
+
currentStock?: number;
|
|
26
|
+
icon?: string;
|
|
27
|
+
shortDescription?: string;
|
|
28
|
+
brand?: string;
|
|
29
|
+
weightType: WeightType;
|
|
30
|
+
weightValue: number;
|
|
31
|
+
barcode?: string;
|
|
32
|
+
dynamicContent?: any;
|
|
33
|
+
isFavorite?: boolean;
|
|
34
|
+
status?: 'PUBLISHED' | 'OUT_OF_STOCK' | 'UNKNOWN';
|
|
35
|
+
price?: Price;
|
|
36
|
+
review: {
|
|
37
|
+
count: number;
|
|
38
|
+
rating: number;
|
|
39
|
+
};
|
|
40
|
+
images: {
|
|
41
|
+
relativePath: string;
|
|
42
|
+
variantIds: string[];
|
|
43
|
+
attributeOptionId: string;
|
|
44
|
+
id: string;
|
|
45
|
+
}[];
|
|
46
|
+
categories: {
|
|
47
|
+
id: string;
|
|
48
|
+
name: string;
|
|
49
|
+
slug: string;
|
|
50
|
+
}[];
|
|
51
|
+
productUnit?: {
|
|
52
|
+
id: string;
|
|
53
|
+
unit: {
|
|
54
|
+
id: string;
|
|
55
|
+
name: string;
|
|
56
|
+
};
|
|
57
|
+
quantityPerUnitOfMeasure: number;
|
|
58
|
+
price?: Price;
|
|
59
|
+
}[];
|
|
60
|
+
variant: {
|
|
61
|
+
id: string;
|
|
62
|
+
price: Price;
|
|
63
|
+
stock: number;
|
|
64
|
+
attributeOption1: {
|
|
65
|
+
id: string;
|
|
66
|
+
label: string;
|
|
67
|
+
value?: string | null;
|
|
68
|
+
icon?: string | null;
|
|
69
|
+
attributeId?: string | null;
|
|
70
|
+
};
|
|
71
|
+
attributeOption2: {
|
|
72
|
+
id: string;
|
|
73
|
+
label: string;
|
|
74
|
+
value?: string | null;
|
|
75
|
+
icon?: string | null;
|
|
76
|
+
attributeId?: string | null;
|
|
77
|
+
};
|
|
78
|
+
}[]
|
|
79
|
+
}
|
|
80
|
+
export interface ProductDetail extends Product {
|
|
81
|
+
baseUnit: string;
|
|
82
|
+
productUnits: {
|
|
83
|
+
id: string;
|
|
84
|
+
unit: string;
|
|
85
|
+
quantityPerUnitOfMeasure: number;
|
|
86
|
+
}[];
|
|
87
|
+
richContent: string;
|
|
88
|
+
description: string;
|
|
89
|
+
saleUnit: string;
|
|
90
|
+
property : Property[];
|
|
91
|
+
purchaseUnit: string;
|
|
92
|
+
shortDescription: string;
|
|
93
|
+
productContent: ProductContent;
|
|
94
|
+
matrix?: ProductPriceMatrixResponse[] | null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface ProductPriceMatrixResponse {
|
|
98
|
+
supplier: {
|
|
99
|
+
id: string;
|
|
100
|
+
name: string;
|
|
101
|
+
} | null;
|
|
102
|
+
unit: {
|
|
103
|
+
id: string;
|
|
104
|
+
name: string;
|
|
105
|
+
} | null;
|
|
106
|
+
price: {
|
|
107
|
+
mainPrice: number;
|
|
108
|
+
discountPrice: number;
|
|
109
|
+
minimumQuantity: number;
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Article,
|
|
5
|
+
BankAccount,
|
|
6
|
+
Branch,
|
|
7
|
+
Campaign,
|
|
8
|
+
CampaignType,
|
|
9
|
+
Category,
|
|
10
|
+
Collection,
|
|
11
|
+
DeliveryMethod,
|
|
12
|
+
DynamicData,
|
|
13
|
+
Faq,
|
|
14
|
+
Feed,
|
|
15
|
+
Holiday,
|
|
16
|
+
Material,
|
|
17
|
+
PaymentMethod,
|
|
18
|
+
Review,
|
|
19
|
+
User,
|
|
20
|
+
} from "@/core/interface/prisma.interface";
|
|
21
|
+
import { createContext, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
22
|
+
import { nexineAxios } from "@/core/util/nexine.axios";
|
|
23
|
+
import { Product as CustomProduct } from "@/core/interface/product.interface";
|
|
24
|
+
import { ModalAuth, ModalAuthRef } from "@/core/feature/auth/modal/modal.auth";
|
|
25
|
+
import { ModalNewsletterVariantProduct, ModalNewsletterVariantProductRef } from "@/core/feature/newsletter/modal/modal.newsletter.product";
|
|
26
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
27
|
+
import { useSecurityState } from "@/core/context/security.context";
|
|
28
|
+
import { useCartState, CartState } from "@/core/context/cart.context";
|
|
29
|
+
import { AnalyticEventProvider } from "@/core/feature/analytic-event/analytic.event.context";
|
|
30
|
+
import { RaxonContextBrand } from "./interface/context.interface";
|
|
31
|
+
import { RaxonBootstrapPayload } from "./interface/bootstrap.interface";
|
|
32
|
+
|
|
33
|
+
export const RaxonContext = createContext<RaxonContextType | undefined>(undefined);
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
interface RaxonBootstrapData {
|
|
37
|
+
campaign: Campaign[];
|
|
38
|
+
basketCampaign: Campaign[];
|
|
39
|
+
branch: Branch | null;
|
|
40
|
+
banner: Collection[];
|
|
41
|
+
collection: Collection[];
|
|
42
|
+
subHeroCollection: Collection[];
|
|
43
|
+
category: Category[];
|
|
44
|
+
flatCategory: Category[];
|
|
45
|
+
review: Review[];
|
|
46
|
+
article: Article[];
|
|
47
|
+
faq: Faq[];
|
|
48
|
+
feed: Feed[];
|
|
49
|
+
material: Material[];
|
|
50
|
+
holiday: Holiday[];
|
|
51
|
+
product: CustomProduct[];
|
|
52
|
+
bankAccount: BankAccount[];
|
|
53
|
+
bestSeller: CustomProduct[];
|
|
54
|
+
dynamicData: DynamicData[];
|
|
55
|
+
deliveryMethod: DeliveryMethod[];
|
|
56
|
+
paymentMethod: PaymentMethod[];
|
|
57
|
+
defaultDeliveryMethod: DeliveryMethod | null;
|
|
58
|
+
brand: RaxonContextBrand[];
|
|
59
|
+
modalAuthRef: React.RefObject<ModalAuthRef | null>;
|
|
60
|
+
modalNewsletterVariantProductRef: React.RefObject<ModalNewsletterVariantProductRef | null>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface RaxonContextType extends RaxonBootstrapData, CartState {
|
|
64
|
+
isLoading: boolean;
|
|
65
|
+
profile: User | null | undefined;
|
|
66
|
+
authLoading: boolean;
|
|
67
|
+
isAuthenticated: boolean;
|
|
68
|
+
isGuest: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface RaxonProviderProps {
|
|
72
|
+
children: React.ReactNode;
|
|
73
|
+
apiKey: string;
|
|
74
|
+
apiUrl: string;
|
|
75
|
+
productPathPrefix?: string;
|
|
76
|
+
analyticAutoTrack?: boolean;
|
|
77
|
+
/** Varsayılan: `/api/bootstrap` — Next.js BFF veya özel proxy */
|
|
78
|
+
bootstrapUrl?: string;
|
|
79
|
+
/** SSR ile layout'tan geçirilen bootstrap verisi */
|
|
80
|
+
initialBootstrapData?: RaxonBootstrapPayload | null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function normalizeBootstrapPayload(raxon: RaxonBootstrapPayload | null | undefined) {
|
|
84
|
+
if (!raxon) return null;
|
|
85
|
+
|
|
86
|
+
const flatten = (categories: Category[]): Category[] => {
|
|
87
|
+
return categories.flatMap((cat) => [cat, ...flatten(cat.children ?? [])]);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const category = Array.isArray(raxon.category) ? raxon.category : [];
|
|
91
|
+
const flatCategory = flatten(category);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
campaign: raxon.campaign ?? [],
|
|
95
|
+
basketCampaign: (raxon.campaign ?? []).filter((it) => it.type !== CampaignType.COLLECTION),
|
|
96
|
+
branch: raxon.branch ?? null,
|
|
97
|
+
collection: raxon.collection ?? [],
|
|
98
|
+
subHeroCollection: raxon.subHeroCollection ?? [],
|
|
99
|
+
banner: (raxon.collection ?? []).filter((it) => it.tags?.includes("banner")),
|
|
100
|
+
category,
|
|
101
|
+
flatCategory,
|
|
102
|
+
faq: raxon.faq ?? [],
|
|
103
|
+
product: raxon.product ?? [],
|
|
104
|
+
bankAccount: raxon.bankAccount ?? [],
|
|
105
|
+
material: raxon.material ?? [],
|
|
106
|
+
holiday: raxon.holiday ?? [],
|
|
107
|
+
bestSeller: raxon.bestSeller ?? [],
|
|
108
|
+
dynamicData: raxon.dynamicData ?? [],
|
|
109
|
+
deliveryMethod: raxon.deliveryMethod ?? [],
|
|
110
|
+
paymentMethod: raxon.paymentMethod ?? [],
|
|
111
|
+
defaultDeliveryMethod: (raxon.deliveryMethod ?? []).find((it) => it.isDefault) ?? null,
|
|
112
|
+
review: raxon.review ?? [],
|
|
113
|
+
article: raxon.article ?? [],
|
|
114
|
+
feed: raxon.feed ?? [],
|
|
115
|
+
brand: raxon.brand ?? [],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const RaxonProviderInner = ({
|
|
120
|
+
children,
|
|
121
|
+
apiKey,
|
|
122
|
+
apiUrl,
|
|
123
|
+
productPathPrefix,
|
|
124
|
+
analyticAutoTrack,
|
|
125
|
+
bootstrapUrl = "/api/bootstrap",
|
|
126
|
+
initialBootstrapData = null,
|
|
127
|
+
}: RaxonProviderProps) => {
|
|
128
|
+
const initialNormalized = useMemo(
|
|
129
|
+
() => normalizeBootstrapPayload(initialBootstrapData),
|
|
130
|
+
[initialBootstrapData],
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const [raxon, setRaxon] = useState<RaxonBootstrapPayload | null>(
|
|
134
|
+
initialBootstrapData ?? null,
|
|
135
|
+
);
|
|
136
|
+
const [isLoading, setIsLoading] = useState<boolean>(!initialNormalized);
|
|
137
|
+
const [hasFetched, setHasFetched] = useState<boolean>(!!initialNormalized);
|
|
138
|
+
|
|
139
|
+
const modalAuthRef = useRef<ModalAuthRef>(null);
|
|
140
|
+
const modalNewsletterVariantProductRef = useRef<ModalNewsletterVariantProductRef>(null);
|
|
141
|
+
|
|
142
|
+
if (typeof window !== "undefined") {
|
|
143
|
+
(window as any).__RAXON_API_KEY__ = apiKey;
|
|
144
|
+
(window as any).__RAXON_API_URL__ = apiUrl;
|
|
145
|
+
nexineAxios.defaults.baseURL = apiUrl;
|
|
146
|
+
nexineAxios.defaults.headers.common["x-api-key"] = apiKey;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const { profile, authLoading, isAuthenticated, isGuest } = useSecurityState();
|
|
150
|
+
const cartState = useCartState(isAuthenticated);
|
|
151
|
+
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
if (hasFetched || !bootstrapUrl) return;
|
|
154
|
+
setHasFetched(true);
|
|
155
|
+
|
|
156
|
+
fetch(bootstrapUrl, { credentials: "same-origin" })
|
|
157
|
+
.then((res) => {
|
|
158
|
+
if (!res.ok) throw new Error(`Bootstrap ${res.status}`);
|
|
159
|
+
return res.json();
|
|
160
|
+
})
|
|
161
|
+
.then((data: RaxonBootstrapPayload) => {
|
|
162
|
+
setRaxon(data);
|
|
163
|
+
setIsLoading(false);
|
|
164
|
+
})
|
|
165
|
+
.catch((err) => {
|
|
166
|
+
console.error("[RAXON] ERROR:", err);
|
|
167
|
+
setIsLoading(false);
|
|
168
|
+
});
|
|
169
|
+
}, [hasFetched, bootstrapUrl]);
|
|
170
|
+
|
|
171
|
+
const normalized = useMemo(
|
|
172
|
+
() => normalizeBootstrapPayload(raxon) ?? initialNormalized,
|
|
173
|
+
[raxon, initialNormalized],
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const value = useMemo<RaxonContextType>(
|
|
177
|
+
() => ({
|
|
178
|
+
campaign: normalized?.campaign ?? [],
|
|
179
|
+
basketCampaign: normalized?.basketCampaign ?? [],
|
|
180
|
+
branch: normalized?.branch ?? null,
|
|
181
|
+
collection: normalized?.collection ?? [],
|
|
182
|
+
subHeroCollection: normalized?.subHeroCollection ?? [],
|
|
183
|
+
banner: normalized?.banner ?? [],
|
|
184
|
+
category: normalized?.category ?? [],
|
|
185
|
+
flatCategory: normalized?.flatCategory ?? [],
|
|
186
|
+
faq: normalized?.faq ?? [],
|
|
187
|
+
product: normalized?.product ?? [],
|
|
188
|
+
bankAccount: normalized?.bankAccount ?? [],
|
|
189
|
+
material: normalized?.material ?? [],
|
|
190
|
+
holiday: normalized?.holiday ?? [],
|
|
191
|
+
bestSeller: normalized?.bestSeller ?? [],
|
|
192
|
+
isLoading,
|
|
193
|
+
dynamicData: normalized?.dynamicData ?? [],
|
|
194
|
+
deliveryMethod: normalized?.deliveryMethod ?? [],
|
|
195
|
+
paymentMethod: normalized?.paymentMethod ?? [],
|
|
196
|
+
defaultDeliveryMethod: normalized?.defaultDeliveryMethod ?? null,
|
|
197
|
+
review: normalized?.review ?? [],
|
|
198
|
+
article: normalized?.article ?? [],
|
|
199
|
+
feed: normalized?.feed ?? [],
|
|
200
|
+
brand: normalized?.brand ?? [],
|
|
201
|
+
modalAuthRef,
|
|
202
|
+
modalNewsletterVariantProductRef,
|
|
203
|
+
profile,
|
|
204
|
+
authLoading,
|
|
205
|
+
isAuthenticated,
|
|
206
|
+
isGuest,
|
|
207
|
+
...cartState,
|
|
208
|
+
}),
|
|
209
|
+
[normalized, isLoading, profile, authLoading, isAuthenticated, isGuest, cartState],
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<RaxonContext.Provider value={value}>
|
|
214
|
+
<AnalyticEventProvider productPathPrefix={productPathPrefix} autoTrack={analyticAutoTrack}>
|
|
215
|
+
{children}
|
|
216
|
+
</AnalyticEventProvider>
|
|
217
|
+
<ModalAuth ref={modalAuthRef} />
|
|
218
|
+
<ModalNewsletterVariantProduct ref={modalNewsletterVariantProductRef} />
|
|
219
|
+
</RaxonContext.Provider>
|
|
220
|
+
);
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
export const RaxonProvider = ({
|
|
224
|
+
children,
|
|
225
|
+
apiKey,
|
|
226
|
+
apiUrl,
|
|
227
|
+
productPathPrefix,
|
|
228
|
+
analyticAutoTrack,
|
|
229
|
+
bootstrapUrl,
|
|
230
|
+
initialBootstrapData,
|
|
231
|
+
}: RaxonProviderProps) => {
|
|
232
|
+
const [queryClient] = useState(() => new QueryClient());
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<QueryClientProvider client={queryClient}>
|
|
236
|
+
<RaxonProviderInner
|
|
237
|
+
apiKey={apiKey}
|
|
238
|
+
apiUrl={apiUrl}
|
|
239
|
+
productPathPrefix={productPathPrefix}
|
|
240
|
+
analyticAutoTrack={analyticAutoTrack}
|
|
241
|
+
bootstrapUrl={bootstrapUrl}
|
|
242
|
+
initialBootstrapData={initialBootstrapData}
|
|
243
|
+
>
|
|
244
|
+
{children}
|
|
245
|
+
</RaxonProviderInner>
|
|
246
|
+
</QueryClientProvider>
|
|
247
|
+
);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
export const useRaxon = (): RaxonContextType => {
|
|
251
|
+
const context = useContext(RaxonContext);
|
|
252
|
+
if (context === undefined) {
|
|
253
|
+
throw new Error("useRaxon must be used within a RaxonProvider");
|
|
254
|
+
}
|
|
255
|
+
return context;
|
|
256
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const addressCreateSchema = z.object({
|
|
4
|
+
title: z.string().min(1, 'Adres başlığı zorunludur'),
|
|
5
|
+
fullName: z.string().min(3, 'Ad soyad zorunludur'),
|
|
6
|
+
phoneNumber: z.string().min(10, 'Geçerli telefon giriniz'),
|
|
7
|
+
country: z.string().min(1, 'Ülke seçiniz'),
|
|
8
|
+
administrativeAreaLevel1: z.string().min(1, 'İl seçiniz'),
|
|
9
|
+
administrativeAreaLevel2: z.string().min(1, 'İlçe seçiniz'),
|
|
10
|
+
fullAddress: z.string().min(5, 'Adres zorunludur'),
|
|
11
|
+
postalCode: z.string().optional(),
|
|
12
|
+
invoiceType: z.enum(['individual', 'corporate']),
|
|
13
|
+
taxNumber: z.string().optional(),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const checkoutSchema = z.object({
|
|
17
|
+
deliveryAddressId: z.string().optional(),
|
|
18
|
+
invoiceAddressId: z.string().optional(),
|
|
19
|
+
differentInvoiceAddress: z.boolean().optional(),
|
|
20
|
+
deliveryMethodId: z.string().min(1, 'Teslimat yöntemi seçiniz'),
|
|
21
|
+
paymentMethodId: z.string().min(1, 'Ödeme yöntemi seçiniz'),
|
|
22
|
+
bankAccountId: z.string().optional(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export type AddressCreateFormData = z.infer<typeof addressCreateSchema>;
|
|
26
|
+
export type CheckoutFormData = z.infer<typeof checkoutSchema>;
|
|
27
|
+
|
|
28
|
+
export function getDeliveryFee(
|
|
29
|
+
method: { courierFee?: number | null; minimumOrderAmount?: number | null; isForceFee?: boolean },
|
|
30
|
+
subtotal: number
|
|
31
|
+
) {
|
|
32
|
+
const fee = Number(method.courierFee ?? 0);
|
|
33
|
+
const minAmount = Number(method.minimumOrderAmount ?? 0);
|
|
34
|
+
|
|
35
|
+
if (minAmount > 0 && subtotal >= minAmount && !method.isForceFee) {
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return fee;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function formatTry(amount: number) {
|
|
43
|
+
return new Intl.NumberFormat('tr-TR', {
|
|
44
|
+
style: 'currency',
|
|
45
|
+
currency: 'TRY',
|
|
46
|
+
maximumFractionDigits: 2,
|
|
47
|
+
}).format(amount);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type PaymentUiType = 'credit_card' | 'bank_transfer' | 'garanti' | 'other';
|
|
51
|
+
|
|
52
|
+
export function getPaymentUiType(name: string, provider?: string): PaymentUiType {
|
|
53
|
+
const n = name.toLowerCase();
|
|
54
|
+
const p = (provider || '').toUpperCase();
|
|
55
|
+
|
|
56
|
+
if (p === 'GARANTI' || p.includes('GARANTI') || n.includes('garanti')) return 'garanti';
|
|
57
|
+
if (p === 'PAYTR' || p.includes('PAYTR') || p === 'STRIPE' || n.includes('kredi') || n.includes('kart')) {
|
|
58
|
+
return 'credit_card';
|
|
59
|
+
}
|
|
60
|
+
if (
|
|
61
|
+
p === 'BANK_TRANSFER' ||
|
|
62
|
+
p.includes('BANK') ||
|
|
63
|
+
n.includes('havale') ||
|
|
64
|
+
n.includes('eft') ||
|
|
65
|
+
n.includes('peşin') ||
|
|
66
|
+
n.includes('pesin')
|
|
67
|
+
) {
|
|
68
|
+
return 'bank_transfer';
|
|
69
|
+
}
|
|
70
|
+
return 'other';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function getPaymentDisplayName(name: string, provider?: string): string {
|
|
74
|
+
const type = getPaymentUiType(name, provider);
|
|
75
|
+
if (type === 'credit_card') return 'Kredi Kartı';
|
|
76
|
+
if (type === 'bank_transfer') return 'Havale';
|
|
77
|
+
if (type === 'garanti') return 'GarantiPay';
|
|
78
|
+
return name;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function sortPaymentMethods<T extends { name: string; provider?: string }>(methods: T[]): T[] {
|
|
82
|
+
const order: PaymentUiType[] = ['credit_card', 'bank_transfer', 'garanti', 'other'];
|
|
83
|
+
return [...methods].sort((a, b) => {
|
|
84
|
+
const ai = order.indexOf(getPaymentUiType(a.name, a.provider));
|
|
85
|
+
const bi = order.indexOf(getPaymentUiType(b.name, b.provider));
|
|
86
|
+
return ai - bi;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Checkout'ta gösterilecek ödeme yöntemleri: Kredi Kartı → Havale → GarantiPay */
|
|
91
|
+
export function getCheckoutPaymentMethods<T extends { id: string; name: string; provider?: string }>(
|
|
92
|
+
methods: T[]
|
|
93
|
+
): T[] {
|
|
94
|
+
const order: PaymentUiType[] = ['credit_card', 'bank_transfer', 'garanti'];
|
|
95
|
+
const picked: T[] = [];
|
|
96
|
+
|
|
97
|
+
for (const type of order) {
|
|
98
|
+
const match = methods.find((m) => getPaymentUiType(m.name, m.provider) === type);
|
|
99
|
+
if (match) picked.push(match);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return picked;
|
|
103
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { BasketItemSummaryInterface } from '../interface/basket.interface';
|
|
2
|
+
|
|
3
|
+
export function formatBasketItemVariantLine(item: BasketItemSummaryInterface): string | null {
|
|
4
|
+
const v = item.variant;
|
|
5
|
+
if (!v) return null;
|
|
6
|
+
const parts: string[] = [];
|
|
7
|
+
if (v.attributeOption1) {
|
|
8
|
+
const n = v.attributeOption1.name;
|
|
9
|
+
const label = v.attributeOption1.attribute?.name;
|
|
10
|
+
parts.push(label && n ? `${label}: ${n}` : n || '');
|
|
11
|
+
}
|
|
12
|
+
if (v.attributeOption2) {
|
|
13
|
+
const n = v.attributeOption2.name;
|
|
14
|
+
const label = v.attributeOption2.attribute?.name;
|
|
15
|
+
parts.push(label && n ? `${label}: ${n}` : n || '');
|
|
16
|
+
}
|
|
17
|
+
const s = parts.filter(Boolean).join(' · ');
|
|
18
|
+
return s.length > 0 ? s : null;
|
|
19
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import capitalize from 'lodash/capitalize';
|
|
2
|
+
import { Category } from '../interface/prisma.interface';
|
|
3
|
+
|
|
4
|
+
/** Header/footer ile aynı görünen kategori adı (çok dilli + eski getName formatı) */
|
|
5
|
+
export function categoryNavLabel(cat: Category): string {
|
|
6
|
+
if (Array.isArray(cat.name) && cat.name.length > 0) {
|
|
7
|
+
return cat.name[0]?.value || '';
|
|
8
|
+
}
|
|
9
|
+
const raw = (cat.name as unknown as { getName?: () => string })?.getName?.() ?? '';
|
|
10
|
+
return raw ? capitalize(raw) : '';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getCategoryNavProductCount(cat: Category): number {
|
|
14
|
+
const c = cat as Category & {
|
|
15
|
+
totalProductCount?: number;
|
|
16
|
+
_count?: { product?: number; products?: number };
|
|
17
|
+
};
|
|
18
|
+
return c.totalProductCount ?? c._count?.product ?? c._count?.products ?? 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function categoryHasStoredParent(cat: Category): boolean {
|
|
22
|
+
const p = cat.parentId;
|
|
23
|
+
return p != null && String(p).trim().length > 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Header/footer menü kökleri: bazı API yanıtlarında tek şemsiye düğüm veya üst dizide hep dolu parentId gelir.
|
|
28
|
+
*/
|
|
29
|
+
export function resolveCategoryNavRoots(categoryTreeTopLevel: Category[]): Category[] {
|
|
30
|
+
if (!categoryTreeTopLevel?.length) return [];
|
|
31
|
+
|
|
32
|
+
const aliveTop = categoryTreeTopLevel.filter(c => !c.deletedAt);
|
|
33
|
+
const rootsWithoutParent = aliveTop.filter(c => !categoryHasStoredParent(c));
|
|
34
|
+
|
|
35
|
+
if (rootsWithoutParent.length === 1) {
|
|
36
|
+
const only = rootsWithoutParent[0];
|
|
37
|
+
const children = (only.children ?? []).filter(c => !c.deletedAt);
|
|
38
|
+
if (children.length > 0) return children;
|
|
39
|
+
return [only];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (rootsWithoutParent.length > 0) return rootsWithoutParent;
|
|
43
|
+
|
|
44
|
+
/** parentId hep dolu: sunucunun döndürdüğü üst diziyi menü seviyesi kabul et */
|
|
45
|
+
return aliveTop;
|
|
46
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const CLIENT_IP_STORAGE_KEY = 'kuatto-client-ip';
|
|
2
|
+
|
|
3
|
+
let clientIpPromise: Promise<string | null> | null = null;
|
|
4
|
+
|
|
5
|
+
function isValidIp(ip: string): boolean {
|
|
6
|
+
const trimmed = ip.trim();
|
|
7
|
+
if (!trimmed) return false;
|
|
8
|
+
return /^(?:\d{1,3}\.){3}\d{1,3}$/.test(trimmed) || /^[0-9a-f:]+$/i.test(trimmed);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function resolveClientIp(): Promise<string | null> {
|
|
12
|
+
if (typeof window === 'undefined') {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const cached = sessionStorage.getItem(CLIENT_IP_STORAGE_KEY);
|
|
17
|
+
if (cached && isValidIp(cached)) {
|
|
18
|
+
return cached;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!clientIpPromise) {
|
|
22
|
+
clientIpPromise = fetch('https://api.ipify.org?format=json', { cache: 'no-store' })
|
|
23
|
+
.then(async response => {
|
|
24
|
+
if (!response.ok) return null;
|
|
25
|
+
const data = (await response.json()) as { ip?: string };
|
|
26
|
+
const ip = data.ip?.trim();
|
|
27
|
+
if (!ip || !isValidIp(ip)) return null;
|
|
28
|
+
sessionStorage.setItem(CLIENT_IP_STORAGE_KEY, ip);
|
|
29
|
+
return ip;
|
|
30
|
+
})
|
|
31
|
+
.catch(() => null);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return clientIpPromise;
|
|
35
|
+
}
|