@akinon/pz-click-collect 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
package/.gitattributes ADDED
@@ -0,0 +1,15 @@
1
+ *.js text eol=lf
2
+ *.jsx text eol=lf
3
+ *.ts text eol=lf
4
+ *.tsx text eol=lf
5
+ *.json text eol=lf
6
+ *.md text eol=lf
7
+
8
+ .eslintignore text eol=lf
9
+ .eslintrc text eol=lf
10
+ .gitignore text eol=lf
11
+ .prettierrc text eol=lf
12
+ .yarnrc text eol=lf
13
+
14
+ * text=auto
15
+
package/.prettierrc ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "bracketSameLine": false,
3
+ "tabWidth": 2,
4
+ "singleQuote": true,
5
+ "jsxSingleQuote": false,
6
+ "bracketSpacing": true,
7
+ "semi": true,
8
+ "useTabs": false,
9
+ "arrowParens": "always",
10
+ "endOfLine": "lf",
11
+ "proseWrap": "never",
12
+ "trailingComma": "none"
13
+ }
package/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # pz-click-collect
2
+
3
+ ### Install the npm package
4
+
5
+ ```bash
6
+ # For latest version
7
+ yarn add ​git+ssh://git@bitbucket.org:akinonteam/pz-click-collect.git
8
+
9
+ # For specific version
10
+ yarn add ​git+ssh://git@bitbucket.org:akinonteam/pz-click-collect.git#COMMIT_HASH
11
+ ```
12
+
13
+ ### Next Config Configuration
14
+
15
+ ##### next.config.js**
16
+
17
+ ```javascript
18
+ const withTM = require("next-transpile-modules")(["pz-click-collect"]);
19
+ ```
20
+
21
+ ### Example Usage
22
+ ##### File Path: src/views/checkout/steps/shipping/addresses.tsx
23
+
24
+
25
+ ```javascript
26
+ import ClickCollect from 'pz-click-collect';
27
+
28
+ <ClickCollect addressTypeParam={addressType.requestParam}/>
29
+ ```
30
+
31
+ ### Props
32
+
33
+ | Properties | Type | Description |
34
+ | ---------- | -------- | -------------------- |
35
+ | addressTypeParam | `string` | Address Type Request Param |
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@akinon/pz-click-collect",
3
+ "version": "1.0.0",
4
+ "main": "./src/index.tsx",
5
+ "module": "./src/index.tsx",
6
+ "license": "MIT",
7
+ "devDependencies": {
8
+ "@types/react": "^18.0.15",
9
+ "@types/react-dom": "^18.0.6",
10
+ "react": "^18.2.0",
11
+ "react-dom": "^18.2.0",
12
+ "typescript": "^4.7.4"
13
+ },
14
+ "peerDependencies": {
15
+ "react": "^16.8.0",
16
+ "react-dom": "^16.8.0"
17
+ }
18
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,312 @@
1
+ import React, {
2
+ useEffect,
3
+ useState,
4
+ useMemo,
5
+ useCallback,
6
+ useRef,
7
+ ChangeEvent
8
+ } from 'react';
9
+ import clsx from 'clsx';
10
+ import { Icon, LoaderSpinner } from '@akinon/next/components';
11
+ import { useAppSelector } from '@akinon/next/redux/hooks';
12
+ import {
13
+ useSetDeliveryOptionMutation,
14
+ useSetRetailStoreMutation,
15
+ useSetShippingOptionMutation,
16
+ useSetAddressesMutation
17
+ } from '@akinon/next/data/client/checkout';
18
+
19
+ import { RootState } from 'redux/store';
20
+
21
+ interface ClickCollectTranslationsProps {
22
+ deliveryFromTheStore: string;
23
+ deliveryStore: string;
24
+ }
25
+
26
+ const defaultTranslations = {
27
+ deliveryFromTheStore: 'DELIVERY FROM THE STORE',
28
+ deliveryStore: 'Delivery Store'
29
+ };
30
+
31
+ export function ClickCollect({
32
+ addressTypeParam,
33
+ translations
34
+ }: {
35
+ addressTypeParam: string;
36
+ translations: ClickCollectTranslationsProps;
37
+ }) {
38
+ const _translations = {
39
+ ...defaultTranslations,
40
+ ...translations
41
+ };
42
+
43
+ const [loading, setLoading] = useState(false);
44
+
45
+ const { deliveryOptions, retailStores } = useAppSelector(
46
+ (state: RootState) => state.checkout
47
+ );
48
+
49
+ const { billing_address, shipping_address, retail_store, delivery_option } =
50
+ useAppSelector((state: RootState) => state.checkout.preOrder) ?? {};
51
+
52
+ const cityPkRef = useRef(null);
53
+
54
+ const retailStore = useMemo(
55
+ () =>
56
+ deliveryOptions?.find(
57
+ (value) => value?.delivery_option_type === 'retail_store'
58
+ ),
59
+ [deliveryOptions]
60
+ );
61
+ const [stores, setStores] = useState([]);
62
+
63
+ const [setDeliveryOption, { isLoading: deliveryLoading }] =
64
+ useSetDeliveryOptionMutation();
65
+
66
+ const [setRetailStore, { isLoading: retailLoading }] =
67
+ useSetRetailStoreMutation();
68
+
69
+ const [setShippingOption, { isLoading: shippingLoading }] =
70
+ useSetShippingOptionMutation();
71
+
72
+ const [setAddresses, { isLoading: addressLoading }] =
73
+ useSetAddressesMutation();
74
+
75
+ const [selectedCity, setSelectedCity] = useState(null);
76
+
77
+ const [selectedStore, setSelectedStore] = useState(null);
78
+
79
+ const [isActive, setIsActive] = useState(false);
80
+
81
+ const [retailStoresForCity, setRetailStoresForCity] = useState([]);
82
+
83
+ const defaultDeliveryOption = useMemo(
84
+ () =>
85
+ deliveryOptions.find(
86
+ (option) => option?.delivery_option_type === 'customer'
87
+ ),
88
+ [deliveryOptions]
89
+ );
90
+
91
+ const retailStoreDeliveryOption = useMemo(
92
+ () =>
93
+ deliveryOptions.find(
94
+ (option) => option?.delivery_option_type === 'retail_store'
95
+ ),
96
+ [deliveryOptions]
97
+ );
98
+
99
+ const postRetailStore = async ({ retailStorePk, billingAddressPk }) => {
100
+ await setRetailStore({
101
+ retailStorePk: retailStorePk,
102
+ billingAddressPk: billingAddressPk
103
+ });
104
+ };
105
+
106
+ const handleDeactivate = useCallback(async () => {
107
+ try {
108
+ await setDeliveryOption(defaultDeliveryOption?.pk);
109
+
110
+ await setAddresses({
111
+ shippingAddressPk: shipping_address?.pk,
112
+ billingAddressPk: billing_address?.pk
113
+ });
114
+ } catch (error) {
115
+ console.error('Error updating details:', error);
116
+ }
117
+ }, [setDeliveryOption, setAddresses]);
118
+
119
+ const handleActive = useCallback(async () => {
120
+ if (retailStore) {
121
+ await setShippingOption(retailStore.pk).unwrap();
122
+ }
123
+
124
+ const response = await setDeliveryOption(
125
+ retailStoreDeliveryOption.pk
126
+ ).unwrap();
127
+ const retailStores = response?.context_list?.find(
128
+ (context) => context.page_slug === 'retailstoreselectionpage'
129
+ );
130
+
131
+ setStores(retailStores?.page_context['retail_stores']);
132
+
133
+ postRetailStore({
134
+ retailStorePk: retail_store
135
+ ? retail_store.pk
136
+ : retailStores.page_context['retail_stores'][0]?.pk,
137
+ billingAddressPk: billing_address ? billing_address.pk : null
138
+ });
139
+ }, [
140
+ retailStore,
141
+ retailStoreDeliveryOption,
142
+ setStores,
143
+ postRetailStore,
144
+ setShippingOption,
145
+ setDeliveryOption,
146
+ retail_store,
147
+ billing_address
148
+ ]);
149
+
150
+ const handleCityChange = useCallback(
151
+ async (e: ChangeEvent<HTMLSelectElement>) => {
152
+ const cityPk = parseInt(e.target.value);
153
+ cityPkRef.current = cityPk;
154
+ const correspondingCity = retailStores.find(
155
+ (store) => store?.township?.city?.pk === cityPk
156
+ )?.township.city;
157
+ setSelectedCity(correspondingCity ? correspondingCity.name : null);
158
+ const storesInCity = retailStores.filter(
159
+ (store) => store?.township?.city?.pk === cityPk
160
+ );
161
+ setRetailStoresForCity(storesInCity);
162
+
163
+ if (storesInCity.length > 0) {
164
+ setSelectedStore(storesInCity[0].pk);
165
+ await postRetailStore({
166
+ retailStorePk: storesInCity[0].pk,
167
+ billingAddressPk: billing_address?.pk
168
+ });
169
+ }
170
+ },
171
+ [retailStores]
172
+ );
173
+
174
+ const handleStoreChange = useCallback(
175
+ (e: ChangeEvent<HTMLSelectElement>) => {
176
+ const storePk = parseInt(e.target.value);
177
+ setSelectedStore(storePk);
178
+ postRetailStore({
179
+ retailStorePk: storePk,
180
+ billingAddressPk: billing_address?.pk
181
+ });
182
+ },
183
+ [postRetailStore, billing_address?.pk]
184
+ );
185
+
186
+ const getUniqueCities = useMemo(() => {
187
+ const uniqueCities = [];
188
+
189
+ retailStores.forEach((store: any) => {
190
+ if (
191
+ !uniqueCities.some((city) => city?.pk === store?.township?.city?.pk)
192
+ ) {
193
+ uniqueCities.push(store?.township?.city);
194
+ }
195
+ });
196
+
197
+ return uniqueCities;
198
+ }, [retailStores]);
199
+
200
+ useEffect(() => {
201
+ setLoading(
202
+ [deliveryLoading, retailLoading, shippingLoading, addressLoading].some(
203
+ Boolean
204
+ )
205
+ );
206
+ }, [deliveryLoading, retailLoading, shippingLoading, addressLoading]);
207
+
208
+ useEffect(() => {
209
+ setIsActive(delivery_option?.pk === retailStoreDeliveryOption?.pk);
210
+ }, [delivery_option?.pk, retailStoreDeliveryOption]);
211
+
212
+ useEffect(() => {
213
+ if (retailStores.length > 0) {
214
+ const defaultCity = retailStores[0]?.township?.city;
215
+ const defaultStore = retailStores.find(
216
+ (store) => store.township.city.pk === defaultCity.pk
217
+ );
218
+
219
+ if (!selectedCity) {
220
+ setSelectedCity(defaultCity.pk);
221
+ cityPkRef.current = defaultCity.pk;
222
+ }
223
+
224
+ if (!selectedStore) {
225
+ setSelectedStore(defaultStore.pk);
226
+ postRetailStore({
227
+ retailStorePk: defaultStore.pk,
228
+ billingAddressPk: billing_address?.pk
229
+ });
230
+ }
231
+
232
+ if (cityPkRef.current) {
233
+ const storesInCity = retailStores.filter(
234
+ (store) => store.township.city.pk === cityPkRef.current
235
+ );
236
+ setRetailStoresForCity(storesInCity);
237
+ }
238
+ }
239
+ }, [retailStores, cityPkRef.current]);
240
+
241
+ if (addressTypeParam !== 'shippingAddressPk') return;
242
+
243
+ return (
244
+ <div
245
+ role={!isActive ? 'button' : 'div'}
246
+ onClick={() => {
247
+ !isActive && handleActive();
248
+ }}
249
+ className={clsx(
250
+ 'relative w-full min-h-[8rem] border shadow p-4',
251
+ "hover:after:content-[''] hover:after:border-4 hover:after:opacity-30 hover:after:transition-opacity",
252
+ 'after:border-secondary-400 after:absolute after:inset-0 after:opacity-0 after:duration-150 after:-z-10'
253
+ )}
254
+ >
255
+ <div className="text-xs flex justify-center items-center h-full gap-x-2">
256
+ {isActive && (
257
+ <div
258
+ role="button"
259
+ onClick={handleDeactivate}
260
+ className="absolute cursor-pointer top-2 right-2 hover:bg-byarlack-100/[.1] p-2 z-10 rounded-full "
261
+ >
262
+ <Icon name="close" size={9} />
263
+ </div>
264
+ )}
265
+ {loading && (
266
+ <div className="absolute top-0 left-0 w-full h-full bg-white/[.9] z-10">
267
+ <LoaderSpinner />
268
+ </div>
269
+ )}
270
+ {!isActive ? (
271
+ <div className="text-xs text-center flex justify-center items-center h-full gap-x-2">
272
+ <Icon name="plus" size={12} />
273
+ <span data-testid="click-collect-add-new-address">
274
+ {_translations.deliveryFromTheStore}
275
+ </span>
276
+ </div>
277
+ ) : (
278
+ <div className="relative w-full">
279
+ <label
280
+ htmlFor="cities"
281
+ className="block mb-2 text-sm text-center font-light text-black-900 "
282
+ >
283
+ {_translations.deliveryStore}
284
+ </label>
285
+ <select
286
+ id="cities"
287
+ className="bg-white border border-gray-300 font-light text-black-900 text-sm block w-full p-1 mb-2 focus:ring-black-500 focus:border-black-500"
288
+ onChange={handleCityChange}
289
+ >
290
+ {getUniqueCities.map((city) => (
291
+ <option key={city.pk} value={city.pk}>
292
+ {city.name}
293
+ </option>
294
+ ))}
295
+ </select>
296
+ <select
297
+ id="stores"
298
+ onChange={handleStoreChange}
299
+ className="bg-white border border-gray-300 font-light text-black-900 text-sm block w-full p-1 mb-2 focus:ring-black-500 focus:border-black-500"
300
+ >
301
+ {retailStoresForCity.map((store) => (
302
+ <option key={store.pk} value={store.pk}>
303
+ {store.name}
304
+ </option>
305
+ ))}
306
+ </select>
307
+ </div>
308
+ )}
309
+ </div>
310
+ </div>
311
+ );
312
+ }