@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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @akinon/pz-click-collect
2
2
 
3
+ ## 1.90.0
4
+
5
+ ### Minor Changes
6
+
7
+ - c9b8c6f: ZERO-3374: Add Click & Collect component documentation and enhance type definitions
8
+
3
9
  ## 1.89.0
4
10
 
5
11
  ## 1.88.0
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akinon/pz-click-collect",
3
- "version": "1.89.0",
3
+ "version": "1.90.0",
4
4
  "main": "./src/index.tsx",
5
5
  "module": "./src/index.tsx",
6
6
  "license": "MIT",
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.find(
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.find(
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
- return (
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
- <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
- )}
365
+ {isActive && <RenderCloseButton handleDeactivate={handleDeactivate} />}
366
+ {loading && <RenderLoader />}
270
367
  {!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>
368
+ <RenderInactiveState translations={_translations} />
277
369
  ) : (
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>
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
- </div>
381
+ </RenderContainer>
311
382
  );
312
383
  }