@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,118 @@
|
|
|
1
|
+
import { Send } from 'lucide-react';
|
|
2
|
+
|
|
3
|
+
const labelClass = 'mb-2 block text-[11px] font-bold uppercase tracking-[0.18em] text-gray-500';
|
|
4
|
+
const fieldOk =
|
|
5
|
+
'w-full rounded-xl border border-gray-200 bg-white px-4 py-3 text-gray-900 transition-all focus:border-black focus:outline-none focus:ring-2 focus:ring-black/10';
|
|
6
|
+
const fieldErr =
|
|
7
|
+
'w-full rounded-xl border border-red-500 bg-white px-4 py-3 text-gray-900 focus:border-red-500 focus:outline-none focus:ring-2 focus:ring-red-500/20';
|
|
8
|
+
|
|
9
|
+
export const FormContact = ({ form, onSubmit, isPending }: { form: any; onSubmit: any; isPending: boolean }) => {
|
|
10
|
+
return (
|
|
11
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
12
|
+
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
|
13
|
+
<div>
|
|
14
|
+
<label htmlFor="firstName" className={labelClass}>
|
|
15
|
+
Ad *
|
|
16
|
+
</label>
|
|
17
|
+
<input
|
|
18
|
+
type="text"
|
|
19
|
+
id="firstName"
|
|
20
|
+
{...form.register('firstName')}
|
|
21
|
+
className={form.formState.errors.firstName ? fieldErr : fieldOk}
|
|
22
|
+
placeholder="Adınız"
|
|
23
|
+
/>
|
|
24
|
+
{form.formState.errors.firstName && <p className="mt-1 text-sm text-red-600">{form.formState.errors.firstName.message}</p>}
|
|
25
|
+
</div>
|
|
26
|
+
<div>
|
|
27
|
+
<label htmlFor="lastName" className={labelClass}>
|
|
28
|
+
Soyad *
|
|
29
|
+
</label>
|
|
30
|
+
<input
|
|
31
|
+
type="text"
|
|
32
|
+
id="lastName"
|
|
33
|
+
{...form.register('lastName')}
|
|
34
|
+
className={form.formState.errors.lastName ? fieldErr : fieldOk}
|
|
35
|
+
placeholder="Soyadınız"
|
|
36
|
+
/>
|
|
37
|
+
{form.formState.errors.lastName && <p className="mt-1 text-sm text-red-600">{form.formState.errors.lastName.message}</p>}
|
|
38
|
+
</div>
|
|
39
|
+
<div className="md:col-span-2">
|
|
40
|
+
<label htmlFor="email" className={labelClass}>
|
|
41
|
+
E-posta *
|
|
42
|
+
</label>
|
|
43
|
+
<input
|
|
44
|
+
type="email"
|
|
45
|
+
id="email"
|
|
46
|
+
{...form.register('email')}
|
|
47
|
+
className={form.formState.errors.email ? fieldErr : fieldOk}
|
|
48
|
+
placeholder="ornek@email.com"
|
|
49
|
+
/>
|
|
50
|
+
{form.formState.errors.email && <p className="mt-1 text-sm text-red-600">{form.formState.errors.email.message}</p>}
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
|
55
|
+
<div>
|
|
56
|
+
<label htmlFor="phoneNumber" className={labelClass}>
|
|
57
|
+
Telefon
|
|
58
|
+
</label>
|
|
59
|
+
<input
|
|
60
|
+
type="tel"
|
|
61
|
+
id="phoneNumber"
|
|
62
|
+
{...form.register('phoneNumber')}
|
|
63
|
+
className={form.formState.errors.phoneNumber ? fieldErr : fieldOk}
|
|
64
|
+
placeholder="Telefon numaranız"
|
|
65
|
+
/>
|
|
66
|
+
{form.formState.errors.phoneNumber && <p className="mt-1 text-sm text-red-600">{form.formState.errors.phoneNumber.message}</p>}
|
|
67
|
+
</div>
|
|
68
|
+
<div>
|
|
69
|
+
<label htmlFor="subject" className={labelClass}>
|
|
70
|
+
Konu *
|
|
71
|
+
</label>
|
|
72
|
+
<select id="subject" {...form.register('subject')} className={form.formState.errors.subject ? fieldErr : fieldOk}>
|
|
73
|
+
<option value="">Konu seçin</option>
|
|
74
|
+
<option value="genel">Genel bilgi</option>
|
|
75
|
+
<option value="siparis">Sipariş</option>
|
|
76
|
+
<option value="iade">İade / değişim</option>
|
|
77
|
+
<option value="urun">Ürün bilgisi</option>
|
|
78
|
+
<option value="oneri">Öneri / şikayet</option>
|
|
79
|
+
<option value="diger">Diğer</option>
|
|
80
|
+
</select>
|
|
81
|
+
{form.formState.errors.subject && <p className="mt-1 text-sm text-red-600">{form.formState.errors.subject.message}</p>}
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div>
|
|
86
|
+
<label htmlFor="message" className={labelClass}>
|
|
87
|
+
Mesajınız *
|
|
88
|
+
</label>
|
|
89
|
+
<textarea
|
|
90
|
+
id="message"
|
|
91
|
+
rows={6}
|
|
92
|
+
{...form.register('message')}
|
|
93
|
+
className={`resize-none ${form.formState.errors.message ? fieldErr : fieldOk}`}
|
|
94
|
+
placeholder="Mesajınızı buraya yazın..."
|
|
95
|
+
/>
|
|
96
|
+
{form.formState.errors.message && <p className="mt-1 text-sm text-red-600">{form.formState.errors.message.message}</p>}
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<button
|
|
100
|
+
type="submit"
|
|
101
|
+
disabled={isPending || form.formState.isSubmitting}
|
|
102
|
+
className="flex w-full items-center justify-center gap-2 rounded-xl border-2 border-black bg-black px-8 py-4 text-sm font-black uppercase tracking-widest text-white shadow-sm transition hover:bg-white hover:text-black focus:outline-none focus:ring-2 focus:ring-black/20 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
103
|
+
>
|
|
104
|
+
{isPending || form.formState.isSubmitting ? (
|
|
105
|
+
<>
|
|
106
|
+
<div className="h-5 w-5 animate-spin rounded-full border-2 border-white border-t-transparent" />
|
|
107
|
+
<span>Gönderiliyor…</span>
|
|
108
|
+
</>
|
|
109
|
+
) : (
|
|
110
|
+
<>
|
|
111
|
+
<Send className="h-5 w-5" strokeWidth={1.5} />
|
|
112
|
+
<span>Mesaj gönder</span>
|
|
113
|
+
</>
|
|
114
|
+
)}
|
|
115
|
+
</button>
|
|
116
|
+
</form>
|
|
117
|
+
);
|
|
118
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { nexineAxios } from "@/core/util/nexine.axios";
|
|
2
|
+
import { IData } from "@/core/interface/nexine.interface";
|
|
3
|
+
import { Brand, Category, Faq } from "@/core/interface/prisma.interface";
|
|
4
|
+
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
5
|
+
|
|
6
|
+
export const useFormSubmit = () => {
|
|
7
|
+
return {
|
|
8
|
+
create: ( ) => {
|
|
9
|
+
return useMutation({
|
|
10
|
+
mutationFn: async (data: any) => {
|
|
11
|
+
return await nexineAxios.post("/customer/form/submit", data);
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { nexineAxios } from "@/core/util/nexine.axios";
|
|
2
|
+
import { IData } from "@/core/interface/nexine.interface";
|
|
3
|
+
import { Invoice } from "@/core/interface/prisma.interface";
|
|
4
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
5
|
+
|
|
6
|
+
export const useInvoice = () => {
|
|
7
|
+
|
|
8
|
+
const queryClient = useQueryClient();
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
fetch: () => {
|
|
12
|
+
return useQuery({
|
|
13
|
+
queryKey: ['web', 'invoice', 'list'],
|
|
14
|
+
queryFn: async () => {
|
|
15
|
+
const response = await nexineAxios.get<IData<Invoice>>('/customer/invoice');
|
|
16
|
+
return response.data;
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
},
|
|
20
|
+
detail: (id: string) => {
|
|
21
|
+
return useQuery({
|
|
22
|
+
queryKey: ['web', 'invoice', 'detail', id],
|
|
23
|
+
enabled: !!id && id !== "" && id !== "create",
|
|
24
|
+
queryFn: async () => {
|
|
25
|
+
const response = await nexineAxios.get<Invoice>(`/customer/invoice/${id}`);
|
|
26
|
+
return response.data;
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
download: () => {
|
|
31
|
+
return useMutation({
|
|
32
|
+
mutationFn: async (id: string) => {
|
|
33
|
+
const response = await nexineAxios.get(`/customer/invoice/${id}/download`, {
|
|
34
|
+
responseType: 'blob',
|
|
35
|
+
});
|
|
36
|
+
return response.data;
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
downloadByOrder: () => {
|
|
41
|
+
return useMutation({
|
|
42
|
+
mutationFn: async (orderId: string) => {
|
|
43
|
+
const response = await nexineAxios.get(`/customer/invoice/order/${orderId}/download`, {
|
|
44
|
+
responseType: 'blob',
|
|
45
|
+
});
|
|
46
|
+
return response.data;
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { nexineAxios } from '@/core/util/nexine.axios';
|
|
2
|
+
import { Newsletter } from '@/core/interface/prisma.interface';
|
|
3
|
+
import { useMutation } from '@tanstack/react-query';
|
|
4
|
+
|
|
5
|
+
export type NewsletterPromoCode = {
|
|
6
|
+
code: string;
|
|
7
|
+
amount: number | null;
|
|
8
|
+
type: string;
|
|
9
|
+
endDate?: string | null;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type NewsletterVerifyResult = Newsletter & {
|
|
13
|
+
promoCode?: NewsletterPromoCode | null;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type NewsletterVerifyResponse = {
|
|
17
|
+
info?: {
|
|
18
|
+
title?: string;
|
|
19
|
+
message?: string;
|
|
20
|
+
statusCode?: number;
|
|
21
|
+
};
|
|
22
|
+
data?: NewsletterVerifyResult;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const getNewsletterVerifyCacheKey = (id: string, email: string) =>
|
|
26
|
+
`kuatto:newsletter-verify:${id}:${email.trim().toLowerCase()}`;
|
|
27
|
+
|
|
28
|
+
export const readNewsletterVerifyCache = (id: string, email: string): NewsletterVerifyResponse | null => {
|
|
29
|
+
if (typeof window === 'undefined' || !id || !email) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const cached = sessionStorage.getItem(getNewsletterVerifyCacheKey(id, email));
|
|
35
|
+
return cached ? (JSON.parse(cached) as NewsletterVerifyResponse) : null;
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const writeNewsletterVerifyCache = (id: string, email: string, payload: NewsletterVerifyResponse) => {
|
|
42
|
+
if (typeof window === 'undefined' || !id || !email) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
sessionStorage.setItem(getNewsletterVerifyCacheKey(id, email), JSON.stringify(payload));
|
|
48
|
+
} catch {
|
|
49
|
+
// sessionStorage dolu veya devre dışı olabilir
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const inflightVerifyRequests = new Map<string, Promise<NewsletterVerifyResponse>>();
|
|
54
|
+
|
|
55
|
+
export const verifyNewsletterOnce = async (id: string, email: string) => {
|
|
56
|
+
const normalizedEmail = email.trim().toLowerCase();
|
|
57
|
+
const cached = readNewsletterVerifyCache(id, normalizedEmail);
|
|
58
|
+
if (cached?.data) {
|
|
59
|
+
return cached;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const inflightKey = `${id}:${normalizedEmail}`;
|
|
63
|
+
const inflight = inflightVerifyRequests.get(inflightKey);
|
|
64
|
+
if (inflight) {
|
|
65
|
+
return inflight;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const request = nexineAxios
|
|
69
|
+
.post<NewsletterVerifyResponse>('/global/newsletter/verify', {
|
|
70
|
+
id,
|
|
71
|
+
email: normalizedEmail,
|
|
72
|
+
})
|
|
73
|
+
.then((response) => {
|
|
74
|
+
const payload = response.data;
|
|
75
|
+
|
|
76
|
+
if (payload.data?.promoCode?.code) {
|
|
77
|
+
writeNewsletterVerifyCache(id, normalizedEmail, payload);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return payload;
|
|
81
|
+
})
|
|
82
|
+
.finally(() => {
|
|
83
|
+
inflightVerifyRequests.delete(inflightKey);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
inflightVerifyRequests.set(inflightKey, request);
|
|
87
|
+
return request;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const useNewsletter = () => {
|
|
91
|
+
const subscribe = useMutation({
|
|
92
|
+
mutationFn: async (data: { email: string; userId?: string }) => {
|
|
93
|
+
const response = await nexineAxios.post<Newsletter>('/customer/newsletter/subscribe', data);
|
|
94
|
+
return response.data;
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const subscribeByVariant = useMutation({
|
|
99
|
+
mutationFn: async (data: { variantId?: string; productId?: string; email: string }) => {
|
|
100
|
+
const response = await nexineAxios.post<Newsletter>('/customer/newsletter/subscribe/variant', {
|
|
101
|
+
variantId: data.variantId,
|
|
102
|
+
productId: data.productId,
|
|
103
|
+
email: data.email,
|
|
104
|
+
});
|
|
105
|
+
return response.data;
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const verify = useMutation({
|
|
110
|
+
mutationFn: async (data: { id: string; email: string }) => {
|
|
111
|
+
const response = await nexineAxios.post<NewsletterVerifyResponse>('/global/newsletter/verify', data);
|
|
112
|
+
return response.data;
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const unsubscribe = useMutation({
|
|
117
|
+
mutationFn: async (data: { id: string; email: string }) => {
|
|
118
|
+
const response = await nexineAxios.post<Newsletter>('/global/newsletter/unsubscribe', data);
|
|
119
|
+
return response.data;
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return { subscribe, subscribeByVariant, verify, unsubscribe };
|
|
124
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { useRaxon } from '@/core/raxon.context';
|
|
2
|
+
import { ProductDetail } from '@/core/interface/product.interface';
|
|
3
|
+
import React, { useImperativeHandle, useRef, useState } from 'react';
|
|
4
|
+
import { Modal } from 'rizzui/modal';
|
|
5
|
+
import { Button } from 'rizzui/button';
|
|
6
|
+
import { Input } from 'rizzui/input';
|
|
7
|
+
import { useNewsletter } from '@/core/feature/newsletter/hook/use.newsletter';
|
|
8
|
+
|
|
9
|
+
export interface ModalVariantArg {
|
|
10
|
+
id: string;
|
|
11
|
+
attributeOption1?: { id?: string; label?: string; name?: string } | null;
|
|
12
|
+
attributeOption2?: { id?: string; label?: string; name?: string } | null;
|
|
13
|
+
price?: { mainPrice?: number; discountPrice?: number } | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ModalNewsletterVariantProductRef {
|
|
17
|
+
open: (product: ProductDetail, variant: ModalVariantArg | null) => void;
|
|
18
|
+
close: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ModalNewsletterVariantProductProps {}
|
|
22
|
+
|
|
23
|
+
export const ModalNewsletterVariantProduct = React.forwardRef<ModalNewsletterVariantProductRef, ModalNewsletterVariantProductProps>((props, ref) => {
|
|
24
|
+
const callbackRef = useRef<any>(null);
|
|
25
|
+
|
|
26
|
+
const { mutate: subscribeByVariant } = useNewsletter().subscribeByVariant;
|
|
27
|
+
const { profile } = useRaxon();
|
|
28
|
+
const [product, setProduct] = useState<ProductDetail | null>(null);
|
|
29
|
+
const [variant, setVariant] = useState<ModalVariantArg | null>(null);
|
|
30
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
31
|
+
const [email, setEmail] = useState('');
|
|
32
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
33
|
+
|
|
34
|
+
const onHandleSubscripeProduct = async () => {
|
|
35
|
+
if(!product) return;
|
|
36
|
+
|
|
37
|
+
const emailToUse = profile?.email || email;
|
|
38
|
+
if(!emailToUse) return;
|
|
39
|
+
|
|
40
|
+
setIsLoading(true);
|
|
41
|
+
try {
|
|
42
|
+
// Varyant varsa variantId, yoksa productId ile abone ol
|
|
43
|
+
if (variant?.id) {
|
|
44
|
+
await subscribeByVariant({
|
|
45
|
+
variantId: variant.id,
|
|
46
|
+
email: emailToUse,
|
|
47
|
+
});
|
|
48
|
+
} else {
|
|
49
|
+
await subscribeByVariant({
|
|
50
|
+
productId: product.id,
|
|
51
|
+
email: emailToUse,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
setIsOpen(false);
|
|
55
|
+
setEmail('');
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error('Newsletter subscription error:', error);
|
|
58
|
+
} finally {
|
|
59
|
+
setIsLoading(false);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
useImperativeHandle(ref, () => ({
|
|
63
|
+
open: (product: ProductDetail, variant: ModalVariantArg | null) => {
|
|
64
|
+
setProduct(product);
|
|
65
|
+
setVariant(variant);
|
|
66
|
+
setIsOpen(true);
|
|
67
|
+
setEmail('');
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
callbackRef.current = { resolve, reject };
|
|
70
|
+
resolve(true);
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
close: () => {
|
|
74
|
+
setIsOpen(false);
|
|
75
|
+
setProduct(null);
|
|
76
|
+
setVariant(null);
|
|
77
|
+
setEmail('');
|
|
78
|
+
},
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<Modal
|
|
83
|
+
isOpen={isOpen}
|
|
84
|
+
onClose={() => {
|
|
85
|
+
setIsOpen(false);
|
|
86
|
+
setProduct(null);
|
|
87
|
+
setVariant(null);
|
|
88
|
+
setEmail('');
|
|
89
|
+
}}
|
|
90
|
+
size="md"
|
|
91
|
+
>
|
|
92
|
+
<div className="p-6">
|
|
93
|
+
<div className="mb-6">
|
|
94
|
+
<h2 className="text-xl font-semibold mb-2">
|
|
95
|
+
Ürün Gelince Haber Ver
|
|
96
|
+
</h2>
|
|
97
|
+
<p className="text-gray-600">
|
|
98
|
+
Bu ürün stoğa geldiğinde size haber verelim
|
|
99
|
+
</p>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
{product && (
|
|
103
|
+
<div className="mb-6 p-4 bg-gray-50 rounded-lg">
|
|
104
|
+
<p className="font-medium mb-2">{product.name}</p>
|
|
105
|
+
{variant && (
|
|
106
|
+
<p className="text-sm text-gray-600 mb-1">
|
|
107
|
+
Varyant: {variant.attributeOption1?.label ?? variant.attributeOption1?.name} {variant.attributeOption2?.label ?? variant.attributeOption2?.name}
|
|
108
|
+
</p>
|
|
109
|
+
)}
|
|
110
|
+
{(variant?.price || product.variant?.[0]?.price) && (
|
|
111
|
+
<p className="text-sm font-medium text-green-600">
|
|
112
|
+
₺{(variant?.price?.discountPrice ?? variant?.price?.mainPrice ?? product.variant?.[0]?.price?.discountPrice ?? product.variant?.[0]?.price?.mainPrice ?? 0).toFixed(2)}
|
|
113
|
+
</p>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
{!profile?.email && (
|
|
119
|
+
<div className="mb-4">
|
|
120
|
+
<Input
|
|
121
|
+
label="E-posta Adresiniz"
|
|
122
|
+
type="email"
|
|
123
|
+
value={email}
|
|
124
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
125
|
+
placeholder="ornek@email.com"
|
|
126
|
+
required
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
{profile?.email && (
|
|
132
|
+
<div className="mb-4">
|
|
133
|
+
<p className="text-sm text-gray-600">
|
|
134
|
+
Bildirim gönderilecek e-posta: <span className="font-medium">{profile.email}</span>
|
|
135
|
+
</p>
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
|
|
139
|
+
<div className="flex gap-3 justify-end">
|
|
140
|
+
<Button
|
|
141
|
+
variant="outline"
|
|
142
|
+
onClick={() => {
|
|
143
|
+
setIsOpen(false);
|
|
144
|
+
setProduct(null);
|
|
145
|
+
setVariant(null);
|
|
146
|
+
setEmail('');
|
|
147
|
+
}}
|
|
148
|
+
disabled={isLoading}
|
|
149
|
+
>
|
|
150
|
+
İptal
|
|
151
|
+
</Button>
|
|
152
|
+
<Button
|
|
153
|
+
onClick={onHandleSubscripeProduct}
|
|
154
|
+
disabled={isLoading || (!profile?.email && !email)}
|
|
155
|
+
isLoading={isLoading}
|
|
156
|
+
>
|
|
157
|
+
Haber Ver
|
|
158
|
+
</Button>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</Modal>
|
|
162
|
+
);
|
|
163
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { nexineAxios } from "@/core/util/nexine.axios";
|
|
2
|
+
import { IData } from "@/core/interface/nexine.interface";
|
|
3
|
+
import { Address, Order } from "@/core/interface/prisma.interface";
|
|
4
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
5
|
+
|
|
6
|
+
export const useOrder = () => {
|
|
7
|
+
|
|
8
|
+
const queryClient = useQueryClient();
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
fetch : () => {
|
|
12
|
+
return useQuery({
|
|
13
|
+
queryKey: ['web','order','list'],
|
|
14
|
+
queryFn: async () => {
|
|
15
|
+
var response = await nexineAxios.get<IData<Order>>(`/customer/order`)
|
|
16
|
+
return response.data;
|
|
17
|
+
},
|
|
18
|
+
})
|
|
19
|
+
},
|
|
20
|
+
detail : (id : string) => {
|
|
21
|
+
return useQuery({
|
|
22
|
+
queryKey: ['web','order','detail',id],
|
|
23
|
+
enabled : !!id && id !== "" && id != "create",
|
|
24
|
+
queryFn: async () => {
|
|
25
|
+
var response = await nexineAxios.get<Order>(`/customer/order/${id}`)
|
|
26
|
+
return response.data;
|
|
27
|
+
},
|
|
28
|
+
})
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { PaymentMethod, PaymentProvider } from '../../interface/prisma.interface';
|
|
2
|
+
import { PaymentUiType, getPaymentUiType } from '@/core/schema/checkout.schema';
|
|
3
|
+
|
|
4
|
+
export interface CheckoutPaymentOption {
|
|
5
|
+
key: PaymentUiType;
|
|
6
|
+
label: string;
|
|
7
|
+
paymentMethodId: string | null;
|
|
8
|
+
method?: PaymentMethod;
|
|
9
|
+
/** Checkout UI'da seçilebilir (her zaman true) */
|
|
10
|
+
available: boolean;
|
|
11
|
+
/** Backend'de karşılık gelen ödeme yöntemi tanımlı mı */
|
|
12
|
+
configured: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function findGarantiPaymentMethod(methods: PaymentMethod[]): PaymentMethod | undefined {
|
|
16
|
+
return methods.find(
|
|
17
|
+
(m) =>
|
|
18
|
+
m.provider === PaymentProvider.GARANTI ||
|
|
19
|
+
/garanti/i.test(m.name) ||
|
|
20
|
+
/garanti/i.test(m.provider || '')
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const CHECKOUT_SLOTS: { key: PaymentUiType; label: string }[] = [
|
|
25
|
+
{ key: 'credit_card', label: 'Kredi Kartı' },
|
|
26
|
+
{ key: 'bank_transfer', label: 'Havale' },
|
|
27
|
+
{ key: 'garanti', label: 'GarantiPay' },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
function resolveMethodForType(methods: PaymentMethod[], type: PaymentUiType): PaymentMethod | undefined {
|
|
31
|
+
const byUi = methods.find((m) => getPaymentUiType(m.name, m.provider) === type);
|
|
32
|
+
if (byUi) return byUi;
|
|
33
|
+
|
|
34
|
+
if (type === 'credit_card') {
|
|
35
|
+
return methods.find(
|
|
36
|
+
(m) =>
|
|
37
|
+
m.provider === PaymentProvider.PAYTR ||
|
|
38
|
+
m.provider === PaymentProvider.STRIPE ||
|
|
39
|
+
/kredi|kart|card/i.test(m.name)
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (type === 'bank_transfer') {
|
|
44
|
+
return methods.find(
|
|
45
|
+
(m) =>
|
|
46
|
+
m.provider === PaymentProvider.BANK_TRANSFER ||
|
|
47
|
+
/havale|eft|peşin|pesin|transfer/i.test(m.name)
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (type === 'garanti') {
|
|
52
|
+
return findGarantiPaymentMethod(methods);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Checkout'ta gösterilecek ödeme seçenekleri (Kredi Kartı, Havale, GarantiPay). */
|
|
59
|
+
export function buildCheckoutPaymentOptions(methods: PaymentMethod[]): CheckoutPaymentOption[] {
|
|
60
|
+
const slots = CHECKOUT_SLOTS;
|
|
61
|
+
const usedIds = new Set<string>();
|
|
62
|
+
|
|
63
|
+
const options = slots.map((slot) => {
|
|
64
|
+
const method = resolveMethodForType(
|
|
65
|
+
methods.filter((m) => !usedIds.has(m.id)),
|
|
66
|
+
slot.key
|
|
67
|
+
);
|
|
68
|
+
if (method) usedIds.add(method.id);
|
|
69
|
+
|
|
70
|
+
const configured =
|
|
71
|
+
slot.key === 'garanti'
|
|
72
|
+
? Boolean(method && method.provider === PaymentProvider.GARANTI)
|
|
73
|
+
: Boolean(method);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
key: slot.key,
|
|
77
|
+
label: slot.label,
|
|
78
|
+
paymentMethodId: method?.id ?? null,
|
|
79
|
+
method,
|
|
80
|
+
available: true,
|
|
81
|
+
configured,
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Eşleşmeyen yöntemleri yalnızca uyumlu boş slotlara ata (Garanti slotuna Cari vb. gitmesin)
|
|
86
|
+
const unused = methods.filter((m) => !usedIds.has(m.id));
|
|
87
|
+
unused.forEach((method) => {
|
|
88
|
+
const slotType = getPaymentUiType(method.name, method.provider);
|
|
89
|
+
const matchingSlot = options.find(
|
|
90
|
+
(o) => !o.paymentMethodId && o.key === slotType && slotType !== 'other'
|
|
91
|
+
);
|
|
92
|
+
if (!matchingSlot) return;
|
|
93
|
+
|
|
94
|
+
matchingSlot.paymentMethodId = method.id;
|
|
95
|
+
matchingSlot.method = method;
|
|
96
|
+
matchingSlot.configured =
|
|
97
|
+
matchingSlot.key === 'garanti'
|
|
98
|
+
? method.provider === PaymentProvider.GARANTI
|
|
99
|
+
: true;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return options;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function resolveCheckoutPaymentMethodId(
|
|
106
|
+
options: CheckoutPaymentOption[],
|
|
107
|
+
selectedKey: PaymentUiType,
|
|
108
|
+
allMethods: PaymentMethod[]
|
|
109
|
+
): string | null {
|
|
110
|
+
const selected = options.find((o) => o.key === selectedKey);
|
|
111
|
+
if (selected?.paymentMethodId) return selected.paymentMethodId;
|
|
112
|
+
|
|
113
|
+
const fallback = resolveMethodForType(allMethods, selectedKey);
|
|
114
|
+
return fallback?.id ?? null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const DEFAULT_CHECKOUT_PAYMENT_KEY: PaymentUiType = 'credit_card';
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { nexineAxios } from '@/core/util/nexine.axios';
|
|
2
|
+
import { IData } from '@/core/interface/nexine.interface';
|
|
3
|
+
import { PaymentMethod } from '@/core/interface/prisma.interface';
|
|
4
|
+
import { useQuery } from '@tanstack/react-query';
|
|
5
|
+
|
|
6
|
+
async function fetchPaymentMethods() {
|
|
7
|
+
const baseParams = { isCompanySpecificVisible: true };
|
|
8
|
+
|
|
9
|
+
const [withTags, withoutTags] = await Promise.all([
|
|
10
|
+
nexineAxios.get<IData<PaymentMethod>>('/customer/payment/method', {
|
|
11
|
+
params: { ...baseParams, tags: ['WEB'] },
|
|
12
|
+
}),
|
|
13
|
+
nexineAxios.get<IData<PaymentMethod>>('/customer/payment/method', {
|
|
14
|
+
params: baseParams,
|
|
15
|
+
}),
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
const tagged = withTags.data?.data ?? [];
|
|
19
|
+
const all = withoutTags.data?.data ?? [];
|
|
20
|
+
|
|
21
|
+
if (tagged.length === 0) return withoutTags.data;
|
|
22
|
+
|
|
23
|
+
// WEB etiketli GarantiPay ile etiketsiz diğer yöntemleri birleştir
|
|
24
|
+
const merged = new Map<string, PaymentMethod>();
|
|
25
|
+
all.forEach((method) => merged.set(method.id, method));
|
|
26
|
+
tagged.forEach((method) => merged.set(method.id, method));
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
...withoutTags.data,
|
|
30
|
+
data: Array.from(merged.values()),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const usePaymentMethod = () => {
|
|
35
|
+
return {
|
|
36
|
+
fetch: (opts?: { enabled?: boolean }) => {
|
|
37
|
+
return useQuery({
|
|
38
|
+
queryKey: ['payment-method'],
|
|
39
|
+
queryFn: fetchPaymentMethods,
|
|
40
|
+
enabled: opts?.enabled !== false,
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
};
|