@akinon/pz-click-collect 1.89.0 → 1.90.0
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/CHANGELOG.md +6 -0
- package/README.md +177 -0
- package/package.json +1 -1
- package/src/index.tsx +127 -56
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
@@ -16,6 +16,183 @@ npx @akinon/projectzero@latest --plugins
|
|
16
16
|
| ---------------- | -------- | -------------------------- |
|
17
17
|
| addressTypeParam | `string` | Address Type Request Param |
|
18
18
|
|
19
|
+
# Click & Collect Component
|
20
|
+
|
21
|
+
The Click & Collect component allows customers to select retail stores for pickup instead of delivery to a shipping address.
|
22
|
+
|
23
|
+
## Features
|
24
|
+
|
25
|
+
- Toggle between delivery to address and retail store pickup
|
26
|
+
- City and store selection
|
27
|
+
- Fully customizable UI through renderer props
|
28
|
+
|
29
|
+
## Basic Usage
|
30
|
+
|
31
|
+
```jsx
|
32
|
+
import { ClickCollect } from '@akinon/pz-click-collect';
|
33
|
+
|
34
|
+
// Basic usage with default styling
|
35
|
+
<ClickCollect
|
36
|
+
addressTypeParam="shippingAddressPk"
|
37
|
+
translations={{
|
38
|
+
deliveryFromTheStore: 'DELIVERY FROM THE STORE',
|
39
|
+
deliveryStore: 'Delivery Store'
|
40
|
+
}}
|
41
|
+
/>;
|
42
|
+
```
|
43
|
+
|
44
|
+
## Customization
|
45
|
+
|
46
|
+
The Click & Collect component is fully customizable through the `renderer` prop. You can override any part of the UI while keeping the core functionality.
|
47
|
+
|
48
|
+
### Renderer Props
|
49
|
+
|
50
|
+
The `renderer` prop accepts an object with the following properties:
|
51
|
+
|
52
|
+
```typescript
|
53
|
+
interface ClickCollectRendererProps {
|
54
|
+
renderContainer?: (props: {
|
55
|
+
children: React.ReactNode;
|
56
|
+
isActive: boolean;
|
57
|
+
handleActive: () => void;
|
58
|
+
handleDeactivate: () => void;
|
59
|
+
}) => React.ReactNode;
|
60
|
+
|
61
|
+
renderInactiveState?: (props: {
|
62
|
+
translations: ClickCollectTranslationsProps;
|
63
|
+
}) => React.ReactNode;
|
64
|
+
|
65
|
+
renderActiveState?: (props: {
|
66
|
+
translations: ClickCollectTranslationsProps;
|
67
|
+
cities: any[];
|
68
|
+
stores: any[];
|
69
|
+
selectedCity: any;
|
70
|
+
handleCityChange: (e: ChangeEvent<HTMLSelectElement>) => void;
|
71
|
+
handleStoreChange: (e: ChangeEvent<HTMLSelectElement>) => void;
|
72
|
+
handleDeactivate: () => void;
|
73
|
+
}) => React.ReactNode;
|
74
|
+
|
75
|
+
renderCloseButton?: (props: {
|
76
|
+
handleDeactivate: () => void;
|
77
|
+
}) => React.ReactNode;
|
78
|
+
|
79
|
+
renderLoader?: () => React.ReactNode;
|
80
|
+
}
|
81
|
+
```
|
82
|
+
|
83
|
+
### Example with Custom Styling
|
84
|
+
|
85
|
+
Here's an example of how to customize the component with branded styling:
|
86
|
+
|
87
|
+
```jsx
|
88
|
+
import { ClickCollect } from '@akinon/pz-click-collect';
|
89
|
+
|
90
|
+
const CustomClickCollect = () => {
|
91
|
+
const customRenderer = {
|
92
|
+
// Override the container
|
93
|
+
renderContainer: ({ children, isActive, handleActive }) => (
|
94
|
+
<div
|
95
|
+
role={!isActive ? 'button' : 'div'}
|
96
|
+
onClick={() => {
|
97
|
+
!isActive && handleActive();
|
98
|
+
}}
|
99
|
+
className={`
|
100
|
+
relative w-full min-h-[8rem] rounded-lg
|
101
|
+
${
|
102
|
+
isActive
|
103
|
+
? 'border-2 border-brand-primary bg-brand-primary/5'
|
104
|
+
: 'border border-gray-300 hover:border-brand-primary'
|
105
|
+
}
|
106
|
+
p-6 transition-all duration-300
|
107
|
+
`}
|
108
|
+
>
|
109
|
+
{children}
|
110
|
+
</div>
|
111
|
+
),
|
112
|
+
|
113
|
+
// Override the inactive state display
|
114
|
+
renderInactiveState: ({ translations }) => (
|
115
|
+
<div className="text-sm flex flex-col justify-center items-center h-full gap-y-3 text-brand-primary">
|
116
|
+
<svg
|
117
|
+
xmlns="http://www.w3.org/2000/svg"
|
118
|
+
width="36"
|
119
|
+
height="36"
|
120
|
+
viewBox="0 0 24 24"
|
121
|
+
fill="none"
|
122
|
+
stroke="currentColor"
|
123
|
+
strokeWidth="2"
|
124
|
+
strokeLinecap="round"
|
125
|
+
strokeLinejoin="round"
|
126
|
+
>
|
127
|
+
<path d="M3 9h18v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V9Z" />
|
128
|
+
<path d="M3 9V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v4" />
|
129
|
+
<path d="M9 13v5" />
|
130
|
+
<path d="M15 13v5" />
|
131
|
+
</svg>
|
132
|
+
<span className="font-medium tracking-wide">
|
133
|
+
{translations.deliveryFromTheStore}
|
134
|
+
</span>
|
135
|
+
</div>
|
136
|
+
)
|
137
|
+
|
138
|
+
// Custom styling for other parts...
|
139
|
+
};
|
140
|
+
|
141
|
+
return (
|
142
|
+
<ClickCollect
|
143
|
+
addressTypeParam="shippingAddressPk"
|
144
|
+
translations={{
|
145
|
+
deliveryFromTheStore: 'In-Store Pickup',
|
146
|
+
deliveryStore: 'Select Your Store'
|
147
|
+
}}
|
148
|
+
renderer={customRenderer}
|
149
|
+
/>
|
150
|
+
);
|
151
|
+
};
|
152
|
+
```
|
153
|
+
|
154
|
+
### Partial Customization
|
155
|
+
|
156
|
+
You can override only specific parts of the UI while keeping the default styling for the rest:
|
157
|
+
|
158
|
+
```jsx
|
159
|
+
<ClickCollect
|
160
|
+
addressTypeParam="shippingAddressPk"
|
161
|
+
translations={{
|
162
|
+
deliveryFromTheStore: 'DELIVERY FROM THE STORE',
|
163
|
+
deliveryStore: 'Delivery Store'
|
164
|
+
}}
|
165
|
+
renderer={{
|
166
|
+
// Override only the active state
|
167
|
+
renderActiveState: ({
|
168
|
+
translations,
|
169
|
+
cities,
|
170
|
+
stores,
|
171
|
+
handleCityChange,
|
172
|
+
handleStoreChange
|
173
|
+
}) => (
|
174
|
+
<div className="custom-active-state">
|
175
|
+
{/* Your custom UI for the active state */}
|
176
|
+
</div>
|
177
|
+
)
|
178
|
+
}}
|
179
|
+
/>
|
180
|
+
```
|
181
|
+
|
182
|
+
## Translation Support
|
183
|
+
|
184
|
+
The component accepts a `translations` prop for localization:
|
185
|
+
|
186
|
+
```jsx
|
187
|
+
<ClickCollect
|
188
|
+
addressTypeParam="shippingAddressPk"
|
189
|
+
translations={{
|
190
|
+
deliveryFromTheStore: 'Mağazadan Teslim Al', // Turkish translation
|
191
|
+
deliveryStore: 'Teslim Alınacak Mağaza' // Turkish translation
|
192
|
+
}}
|
193
|
+
/>
|
194
|
+
```
|
195
|
+
|
19
196
|
```
|
20
197
|
|
21
198
|
```
|
package/package.json
CHANGED
package/src/index.tsx
CHANGED
@@ -18,11 +18,36 @@ import {
|
|
18
18
|
|
19
19
|
import { RootState } from 'redux/store';
|
20
20
|
|
21
|
-
interface ClickCollectTranslationsProps {
|
21
|
+
export interface ClickCollectTranslationsProps {
|
22
22
|
deliveryFromTheStore: string;
|
23
23
|
deliveryStore: string;
|
24
24
|
}
|
25
25
|
|
26
|
+
export interface ClickCollectRendererProps {
|
27
|
+
renderContainer?: (props: {
|
28
|
+
children: React.ReactNode;
|
29
|
+
isActive: boolean;
|
30
|
+
handleActive: () => void;
|
31
|
+
handleDeactivate: () => void;
|
32
|
+
}) => React.ReactNode;
|
33
|
+
renderInactiveState?: (props: {
|
34
|
+
translations: ClickCollectTranslationsProps;
|
35
|
+
}) => React.ReactNode;
|
36
|
+
renderActiveState?: (props: {
|
37
|
+
translations: ClickCollectTranslationsProps;
|
38
|
+
cities: any[];
|
39
|
+
stores: any[];
|
40
|
+
selectedCity: any;
|
41
|
+
handleCityChange: (e: ChangeEvent<HTMLSelectElement>) => void;
|
42
|
+
handleStoreChange: (e: ChangeEvent<HTMLSelectElement>) => void;
|
43
|
+
handleDeactivate: () => void;
|
44
|
+
}) => React.ReactNode;
|
45
|
+
renderCloseButton?: (props: {
|
46
|
+
handleDeactivate: () => void;
|
47
|
+
}) => React.ReactNode;
|
48
|
+
renderLoader?: () => React.ReactNode;
|
49
|
+
}
|
50
|
+
|
26
51
|
const defaultTranslations = {
|
27
52
|
deliveryFromTheStore: 'DELIVERY FROM THE STORE',
|
28
53
|
deliveryStore: 'Delivery Store'
|
@@ -30,10 +55,12 @@ const defaultTranslations = {
|
|
30
55
|
|
31
56
|
export function ClickCollect({
|
32
57
|
addressTypeParam,
|
33
|
-
translations
|
58
|
+
translations,
|
59
|
+
renderer = {}
|
34
60
|
}: {
|
35
61
|
addressTypeParam: string;
|
36
62
|
translations: ClickCollectTranslationsProps;
|
63
|
+
renderer?: ClickCollectRendererProps;
|
37
64
|
}) {
|
38
65
|
const _translations = {
|
39
66
|
...defaultTranslations,
|
@@ -82,7 +109,7 @@ export function ClickCollect({
|
|
82
109
|
|
83
110
|
const defaultDeliveryOption = useMemo(
|
84
111
|
() =>
|
85
|
-
deliveryOptions
|
112
|
+
deliveryOptions?.find(
|
86
113
|
(option) => option?.delivery_option_type === 'customer'
|
87
114
|
),
|
88
115
|
[deliveryOptions]
|
@@ -90,7 +117,7 @@ export function ClickCollect({
|
|
90
117
|
|
91
118
|
const retailStoreDeliveryOption = useMemo(
|
92
119
|
() =>
|
93
|
-
deliveryOptions
|
120
|
+
deliveryOptions?.find(
|
94
121
|
(option) => option?.delivery_option_type === 'retail_store'
|
95
122
|
),
|
96
123
|
[deliveryOptions]
|
@@ -240,7 +267,7 @@ export function ClickCollect({
|
|
240
267
|
|
241
268
|
if (addressTypeParam !== 'shippingAddressPk') return;
|
242
269
|
|
243
|
-
|
270
|
+
const DefaultContainer = ({ children, isActive, handleActive }) => (
|
244
271
|
<div
|
245
272
|
role={!isActive ? 'button' : 'div'}
|
246
273
|
onClick={() => {
|
@@ -251,62 +278,106 @@ export function ClickCollect({
|
|
251
278
|
"hover:after:content-[''] hover:after:border-4 hover:after:opacity-30 hover:after:transition-opacity",
|
252
279
|
'after:border-secondary-400 after:absolute after:inset-0 after:opacity-0 after:duration-150 after:-z-10'
|
253
280
|
)}
|
281
|
+
>
|
282
|
+
{children}
|
283
|
+
</div>
|
284
|
+
);
|
285
|
+
|
286
|
+
const DefaultInactiveState = ({ translations }) => (
|
287
|
+
<div className="text-xs text-center flex justify-center items-center h-full gap-x-2">
|
288
|
+
<Icon name="plus" size={12} />
|
289
|
+
<span data-testid="click-collect-add-new-address">
|
290
|
+
{translations.deliveryFromTheStore}
|
291
|
+
</span>
|
292
|
+
</div>
|
293
|
+
);
|
294
|
+
|
295
|
+
const DefaultActiveState = ({
|
296
|
+
translations,
|
297
|
+
cities,
|
298
|
+
stores,
|
299
|
+
handleCityChange,
|
300
|
+
handleStoreChange,
|
301
|
+
handleDeactivate
|
302
|
+
}) => (
|
303
|
+
<div className="relative w-full">
|
304
|
+
<label
|
305
|
+
htmlFor="cities"
|
306
|
+
className="block mb-2 text-sm text-center font-light text-black-900"
|
307
|
+
>
|
308
|
+
{translations.deliveryStore}
|
309
|
+
</label>
|
310
|
+
<select
|
311
|
+
id="cities"
|
312
|
+
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"
|
313
|
+
onChange={handleCityChange}
|
314
|
+
>
|
315
|
+
{cities.map((city) => (
|
316
|
+
<option key={city.pk} value={city.pk}>
|
317
|
+
{city.name}
|
318
|
+
</option>
|
319
|
+
))}
|
320
|
+
</select>
|
321
|
+
<select
|
322
|
+
id="stores"
|
323
|
+
onChange={handleStoreChange}
|
324
|
+
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"
|
325
|
+
>
|
326
|
+
{stores.map((store) => (
|
327
|
+
<option key={store.pk} value={store.pk}>
|
328
|
+
{store.name}
|
329
|
+
</option>
|
330
|
+
))}
|
331
|
+
</select>
|
332
|
+
</div>
|
333
|
+
);
|
334
|
+
|
335
|
+
const DefaultCloseButton = ({ handleDeactivate }) => (
|
336
|
+
<div
|
337
|
+
role="button"
|
338
|
+
onClick={handleDeactivate}
|
339
|
+
className="absolute cursor-pointer top-2 right-2 hover:bg-byarlack-100/[.1] p-2 z-10 rounded-full"
|
340
|
+
>
|
341
|
+
<Icon name="close" size={9} />
|
342
|
+
</div>
|
343
|
+
);
|
344
|
+
|
345
|
+
const DefaultLoader = () => (
|
346
|
+
<div className="absolute top-0 left-0 w-full h-full bg-white/[.9] z-10">
|
347
|
+
<LoaderSpinner />
|
348
|
+
</div>
|
349
|
+
);
|
350
|
+
|
351
|
+
const RenderContainer = renderer.renderContainer || DefaultContainer;
|
352
|
+
const RenderInactiveState =
|
353
|
+
renderer.renderInactiveState || DefaultInactiveState;
|
354
|
+
const RenderActiveState = renderer.renderActiveState || DefaultActiveState;
|
355
|
+
const RenderCloseButton = renderer.renderCloseButton || DefaultCloseButton;
|
356
|
+
const RenderLoader = renderer.renderLoader || DefaultLoader;
|
357
|
+
|
358
|
+
return (
|
359
|
+
<RenderContainer
|
360
|
+
isActive={isActive}
|
361
|
+
handleActive={handleActive}
|
362
|
+
handleDeactivate={handleDeactivate}
|
254
363
|
>
|
255
364
|
<div className="text-xs flex justify-center items-center h-full gap-x-2">
|
256
|
-
{isActive &&
|
257
|
-
|
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
|
-
)}
|
365
|
+
{isActive && <RenderCloseButton handleDeactivate={handleDeactivate} />}
|
366
|
+
{loading && <RenderLoader />}
|
270
367
|
{!isActive ? (
|
271
|
-
<
|
272
|
-
<Icon name="plus" size={12} />
|
273
|
-
<span data-testid="click-collect-add-new-address">
|
274
|
-
{_translations.deliveryFromTheStore}
|
275
|
-
</span>
|
276
|
-
</div>
|
368
|
+
<RenderInactiveState translations={_translations} />
|
277
369
|
) : (
|
278
|
-
<
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
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>
|
370
|
+
<RenderActiveState
|
371
|
+
translations={_translations}
|
372
|
+
cities={getUniqueCities}
|
373
|
+
stores={retailStoresForCity}
|
374
|
+
selectedCity={selectedCity}
|
375
|
+
handleCityChange={handleCityChange}
|
376
|
+
handleStoreChange={handleStoreChange}
|
377
|
+
handleDeactivate={handleDeactivate}
|
378
|
+
/>
|
308
379
|
)}
|
309
380
|
</div>
|
310
|
-
</
|
381
|
+
</RenderContainer>
|
311
382
|
);
|
312
383
|
}
|