@akinon/pz-similar-products 1.92.0-rc.16

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.
@@ -0,0 +1,591 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import { LoaderSpinner } from '@akinon/next/components';
5
+ import { useLocalization } from '@akinon/next/hooks';
6
+ import { ResultsGridProps } from '../types';
7
+ import { twMerge } from 'tailwind-merge';
8
+ import dynamic from 'next/dynamic';
9
+
10
+ const ProductItem = dynamic(
11
+ () => import('@theme/views/product-item').then((mod) => mod.ProductItem),
12
+ {
13
+ ssr: false,
14
+ loading: () => (
15
+ <div className="w-full h-64 bg-gray-100 animate-pulse rounded"></div>
16
+ )
17
+ }
18
+ );
19
+
20
+ export function SimilarProductsResultsGrid({
21
+ searchResults,
22
+ resultsKey,
23
+ isLoading,
24
+ handlePageChange,
25
+ handleLoadMore,
26
+ loadedPages,
27
+ settings,
28
+ className
29
+ }: ResultsGridProps) {
30
+ const { t } = useLocalization();
31
+
32
+ const onPageChange = (page: number) => {
33
+ handlePageChange(page);
34
+ };
35
+
36
+ const gridClassName = twMerge(
37
+ 'flex-1 flex flex-col p-3 md:p-4 overflow-y-auto relative',
38
+ className
39
+ );
40
+
41
+ if (settings?.customRenderers?.render?.resultsGrid?.renderGrid) {
42
+ return settings.customRenderers.render.resultsGrid.renderGrid({
43
+ searchResults,
44
+ resultsKey,
45
+ isLoading,
46
+ handlePageChange,
47
+ handleLoadMore,
48
+ loadedPages,
49
+ settings,
50
+ className: gridClassName
51
+ });
52
+ }
53
+
54
+ const renderLoadingState = () => {
55
+ if (settings?.customRenderers?.render?.resultsGrid?.renderLoadingState) {
56
+ return settings.customRenderers.render.resultsGrid.renderLoadingState();
57
+ }
58
+
59
+ const loadingStateClassName = twMerge(
60
+ 'flex-1 flex justify-center items-center p-3 md:p-4',
61
+ settings?.customStyles?.loadingSpinner
62
+ );
63
+
64
+ const loadingSpinnerClassName = twMerge(
65
+ 'h-8 w-8',
66
+ settings?.customStyles?.loadingSpinner
67
+ );
68
+
69
+ return (
70
+ <div className={loadingStateClassName}>
71
+ <LoaderSpinner className={loadingSpinnerClassName} />
72
+ </div>
73
+ );
74
+ };
75
+
76
+ const renderEmptyState = () => {
77
+ if (settings?.customRenderers?.render?.resultsGrid?.renderEmptyState) {
78
+ return settings.customRenderers.render.resultsGrid.renderEmptyState();
79
+ }
80
+
81
+ const emptyStateClassName = twMerge(
82
+ 'flex-1 flex flex-col justify-center items-center',
83
+ settings?.customStyles?.emptyState
84
+ );
85
+
86
+ const emptyStateIconClassName = twMerge(
87
+ 'mx-auto h-24 w-24 text-gray-400 mb-4',
88
+ settings?.customStyles?.emptyStateIcon
89
+ );
90
+
91
+ const emptyStateTextClassName = twMerge(
92
+ 'text-lg font-medium text-gray-900 mb-2',
93
+ settings?.customStyles?.emptyStateText
94
+ );
95
+
96
+ return (
97
+ <div className={emptyStateClassName}>
98
+ <div className="text-center">
99
+ <svg
100
+ className={emptyStateIconClassName}
101
+ fill="none"
102
+ viewBox="0 0 24 24"
103
+ stroke="currentColor"
104
+ aria-hidden="true"
105
+ >
106
+ <path
107
+ strokeLinecap="round"
108
+ strokeLinejoin="round"
109
+ strokeWidth={1}
110
+ d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
111
+ />
112
+ </svg>
113
+ <h3 className={emptyStateTextClassName}>
114
+ {t('common.product.no_products')}
115
+ </h3>
116
+ </div>
117
+ </div>
118
+ );
119
+ };
120
+
121
+ const renderProductItem = (product: any, index: number) => {
122
+ if (settings?.customRenderers?.render?.resultsGrid?.renderProductItem) {
123
+ return settings.customRenderers.render.resultsGrid.renderProductItem({
124
+ product,
125
+ index
126
+ });
127
+ }
128
+
129
+ const productItemClassName = twMerge(
130
+ 'product-item',
131
+ settings?.customStyles?.productItem
132
+ );
133
+
134
+ const renderProductImage = () => {
135
+ if (settings?.customRenderers?.render?.resultsGrid?.renderProductImage) {
136
+ return settings.customRenderers.render.resultsGrid.renderProductImage({
137
+ product,
138
+ imageUrl: product.productimage_set?.[0]?.image || '',
139
+ alt: product.name,
140
+ index
141
+ });
142
+ }
143
+ return null;
144
+ };
145
+
146
+ const renderProductInfo = () => {
147
+ if (settings?.customRenderers?.render?.resultsGrid?.renderProductInfo) {
148
+ return settings.customRenderers.render.resultsGrid.renderProductInfo({
149
+ product,
150
+ index
151
+ });
152
+ }
153
+ return null;
154
+ };
155
+
156
+ const renderProductTitle = () => {
157
+ if (settings?.customRenderers?.render?.resultsGrid?.renderProductTitle) {
158
+ return settings.customRenderers.render.resultsGrid.renderProductTitle({
159
+ product,
160
+ title: product.name
161
+ });
162
+ }
163
+ return null;
164
+ };
165
+
166
+ const renderProductPrice = () => {
167
+ if (settings?.customRenderers?.render?.resultsGrid?.renderProductPrice) {
168
+ return settings.customRenderers.render.resultsGrid.renderProductPrice({
169
+ product,
170
+ price: product.price,
171
+ oldPrice: product.old_price
172
+ });
173
+ }
174
+ return null;
175
+ };
176
+
177
+ if (
178
+ renderProductImage() ||
179
+ renderProductInfo() ||
180
+ renderProductTitle() ||
181
+ renderProductPrice()
182
+ ) {
183
+ const productImageWrapperClassName = twMerge(
184
+ 'product-image-wrapper',
185
+ settings?.customStyles?.productImageWrapper
186
+ );
187
+
188
+ const productImageClassName = twMerge(
189
+ 'product-image',
190
+ settings?.customStyles?.productImage
191
+ );
192
+
193
+ const productInfoClassName = twMerge(
194
+ 'product-info',
195
+ settings?.customStyles?.productInfo
196
+ );
197
+
198
+ const productTitleClassName = twMerge(
199
+ 'product-title',
200
+ settings?.customStyles?.productTitle
201
+ );
202
+
203
+ const productPriceClassName = twMerge(
204
+ 'product-price',
205
+ settings?.customStyles?.productPrice
206
+ );
207
+
208
+ const productOldPriceClassName = twMerge(
209
+ 'product-old-price',
210
+ settings?.customStyles?.productOldPrice
211
+ );
212
+
213
+ return (
214
+ <div key={product.pk} className={productItemClassName}>
215
+ {renderProductImage() || (
216
+ <div className={productImageWrapperClassName}>
217
+ <img
218
+ src={product.productimage_set?.[0]?.image}
219
+ alt={product.name}
220
+ className={productImageClassName}
221
+ />
222
+ </div>
223
+ )}
224
+
225
+ {renderProductInfo() || (
226
+ <div className={productInfoClassName}>
227
+ {renderProductTitle() || (
228
+ <h3 className={productTitleClassName}>{product.name}</h3>
229
+ )}
230
+
231
+ {renderProductPrice() || (
232
+ <div className="price-container">
233
+ <span className={productPriceClassName}>
234
+ {product.price} ₺
235
+ </span>
236
+ {product.old_price && (
237
+ <span className={productOldPriceClassName}>
238
+ {product.old_price} ₺
239
+ </span>
240
+ )}
241
+ </div>
242
+ )}
243
+ </div>
244
+ )}
245
+ </div>
246
+ );
247
+ }
248
+
249
+ return (
250
+ <div key={product.pk} className={productItemClassName}>
251
+ <ProductItem product={product} width={340} height={510} index={index} />
252
+ </div>
253
+ );
254
+ };
255
+
256
+ const renderLoadMore = () => {
257
+ if (!searchResults?.pagination || !handleLoadMore) return null;
258
+
259
+ const hasMore =
260
+ searchResults.pagination.current_page <
261
+ searchResults.pagination.num_pages;
262
+ if (!hasMore) return null;
263
+
264
+ if (settings?.customRenderers?.render?.resultsGrid?.renderLoadMore) {
265
+ return settings.customRenderers.render.resultsGrid.renderLoadMore({
266
+ onLoadMore: handleLoadMore,
267
+ hasMore,
268
+ isLoading,
269
+ currentPage: searchResults.pagination.current_page,
270
+ totalPages: searchResults.pagination.num_pages,
271
+ loadedPages: loadedPages?.length || 1
272
+ });
273
+ }
274
+
275
+ const loadMoreContainerClassName = twMerge(
276
+ 'flex justify-center items-center mt-8',
277
+ settings?.customStyles?.loadMoreContainer
278
+ );
279
+
280
+ const renderLoadMoreButton = () => {
281
+ if (
282
+ settings?.customRenderers?.render?.resultsGrid?.renderLoadMoreButton
283
+ ) {
284
+ return settings.customRenderers.render.resultsGrid.renderLoadMoreButton(
285
+ {
286
+ onClick: handleLoadMore,
287
+ disabled: isLoading,
288
+ isLoading,
289
+ text: settings?.loadMoreText || 'Load More'
290
+ }
291
+ );
292
+ }
293
+
294
+ const loadMoreButtonClassName = twMerge(
295
+ 'px-8 py-3 bg-gray-900 text-white rounded-lg hover:bg-gray-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors',
296
+ settings?.customStyles?.loadMoreButton
297
+ );
298
+
299
+ return (
300
+ <button
301
+ onClick={handleLoadMore}
302
+ disabled={isLoading}
303
+ className={loadMoreButtonClassName}
304
+ >
305
+ {isLoading ? (
306
+ <div className="flex items-center space-x-2">
307
+ <LoaderSpinner className="h-4 w-4" />
308
+ <span>Loading...</span>
309
+ </div>
310
+ ) : (
311
+ settings?.loadMoreText || 'Load More'
312
+ )}
313
+ </button>
314
+ );
315
+ };
316
+
317
+ return (
318
+ <div className={loadMoreContainerClassName}>{renderLoadMoreButton()}</div>
319
+ );
320
+ };
321
+
322
+ const renderPagination = () => {
323
+ if (!searchResults?.pagination || searchResults.pagination.num_pages <= 1) {
324
+ return null;
325
+ }
326
+
327
+ if (
328
+ settings?.paginationType === 'load-more' ||
329
+ settings?.paginationType === 'infinite-scroll'
330
+ ) {
331
+ return renderLoadMore();
332
+ }
333
+
334
+ if (settings?.customRenderers?.render?.resultsGrid?.renderPagination) {
335
+ return settings.customRenderers.render.resultsGrid.renderPagination({
336
+ pagination: searchResults.pagination,
337
+ onPageChange,
338
+ isLoading
339
+ });
340
+ }
341
+
342
+ const paginationContainerClassName = twMerge(
343
+ 'mb-4 mt-8 flex items-center justify-center',
344
+ settings?.customStyles?.paginationContainer
345
+ );
346
+
347
+ const paginationInfoClassName = twMerge(
348
+ 'pagination-info',
349
+ settings?.customStyles?.paginationInfo
350
+ );
351
+
352
+ const renderPaginationInfo = () => {
353
+ if (
354
+ settings?.customRenderers?.render?.resultsGrid?.renderPaginationInfo
355
+ ) {
356
+ return settings.customRenderers.render.resultsGrid.renderPaginationInfo(
357
+ {
358
+ currentPage: searchResults.pagination.current_page,
359
+ totalPages: searchResults.pagination.num_pages,
360
+ totalCount: searchResults.pagination.total_count || 0
361
+ }
362
+ );
363
+ }
364
+ return null;
365
+ };
366
+
367
+ const renderPaginationPrevious = () => {
368
+ if (searchResults.pagination.current_page <= 1) return null;
369
+
370
+ if (
371
+ settings?.customRenderers?.render?.resultsGrid?.renderPaginationPrevious
372
+ ) {
373
+ return settings.customRenderers.render.resultsGrid.renderPaginationPrevious(
374
+ {
375
+ onClick: () =>
376
+ onPageChange(searchResults.pagination.current_page - 1),
377
+ disabled: isLoading
378
+ }
379
+ );
380
+ }
381
+
382
+ const paginationPreviousClassName = twMerge(
383
+ 'flex cursor-pointer px-2 text-sm items-center disabled:opacity-50 disabled:cursor-not-allowed',
384
+ settings?.customStyles?.paginationPrevious
385
+ );
386
+
387
+ return (
388
+ <li>
389
+ <button
390
+ onClick={() =>
391
+ onPageChange(searchResults.pagination.current_page - 1)
392
+ }
393
+ disabled={isLoading}
394
+ className={paginationPreviousClassName}
395
+ >
396
+ <span>&lt;</span>
397
+ <span className="ms-4 hidden lg:inline-block">
398
+ {t('category.pagination.previous')}
399
+ </span>
400
+ </button>
401
+ </li>
402
+ );
403
+ };
404
+
405
+ const renderPaginationNext = () => {
406
+ if (
407
+ searchResults.pagination.current_page >=
408
+ searchResults.pagination.num_pages
409
+ )
410
+ return null;
411
+
412
+ if (
413
+ settings?.customRenderers?.render?.resultsGrid?.renderPaginationNext
414
+ ) {
415
+ return settings.customRenderers.render.resultsGrid.renderPaginationNext(
416
+ {
417
+ onClick: () =>
418
+ onPageChange(searchResults.pagination.current_page + 1),
419
+ disabled: isLoading
420
+ }
421
+ );
422
+ }
423
+
424
+ const paginationNextClassName = twMerge(
425
+ 'flex cursor-pointer px-2 text-xs items-center disabled:opacity-50 disabled:cursor-not-allowed',
426
+ settings?.customStyles?.paginationNext
427
+ );
428
+
429
+ return (
430
+ <li>
431
+ <button
432
+ onClick={() =>
433
+ onPageChange(searchResults.pagination.current_page + 1)
434
+ }
435
+ disabled={isLoading}
436
+ className={paginationNextClassName}
437
+ >
438
+ <span className="me-4 hidden lg:inline-block">
439
+ {t('category.pagination.next')}
440
+ </span>
441
+ <span>&gt;</span>
442
+ </button>
443
+ </li>
444
+ );
445
+ };
446
+
447
+ const renderPaginationButton = (pageNum: number) => {
448
+ const isCurrentPage = pageNum === searchResults.pagination.current_page;
449
+
450
+ if (
451
+ settings?.customRenderers?.render?.resultsGrid?.renderPaginationButton
452
+ ) {
453
+ return settings.customRenderers.render.resultsGrid.renderPaginationButton(
454
+ {
455
+ page: pageNum,
456
+ isActive: isCurrentPage,
457
+ isDisabled: isLoading || isCurrentPage,
458
+ onClick: () => onPageChange(pageNum),
459
+ children: pageNum
460
+ }
461
+ );
462
+ }
463
+
464
+ const paginationButtonClassName = twMerge(
465
+ `cursor-pointer px-2 text-xs items-center disabled:cursor-not-allowed`,
466
+ isCurrentPage
467
+ ? 'font-semibold text-black-800'
468
+ : 'text-gray-400 hover:text-gray-600',
469
+ settings?.customStyles?.paginationButton
470
+ );
471
+
472
+ const activeButtonClassName = twMerge(
473
+ paginationButtonClassName,
474
+ isCurrentPage ? settings?.customStyles?.paginationButtonActive : ''
475
+ );
476
+
477
+ const disabledButtonClassName = twMerge(
478
+ activeButtonClassName,
479
+ isLoading || isCurrentPage
480
+ ? settings?.customStyles?.paginationButtonDisabled
481
+ : ''
482
+ );
483
+
484
+ return (
485
+ <li key={pageNum}>
486
+ <button
487
+ onClick={() => onPageChange(pageNum)}
488
+ disabled={isLoading || isCurrentPage}
489
+ className={disabledButtonClassName}
490
+ >
491
+ {pageNum}
492
+ </button>
493
+ </li>
494
+ );
495
+ };
496
+
497
+ return (
498
+ <div className={paginationContainerClassName}>
499
+ {renderPaginationInfo()}
500
+ <ul
501
+ className={twMerge(
502
+ 'flex items-center',
503
+ settings?.customStyles?.pagination
504
+ )}
505
+ >
506
+ {renderPaginationPrevious()}
507
+
508
+ {Array.from(
509
+ { length: searchResults.pagination.num_pages },
510
+ (_, i) => i + 1
511
+ ).map((pageNum) => renderPaginationButton(pageNum))}
512
+
513
+ {renderPaginationNext()}
514
+ </ul>
515
+ </div>
516
+ );
517
+ };
518
+
519
+ if (isLoading && (!searchResults || !searchResults.products)) {
520
+ return renderLoadingState();
521
+ }
522
+
523
+ const renderLoadingOverlay = () => {
524
+ if (settings?.customRenderers?.render?.resultsGrid?.renderLoadingOverlay) {
525
+ return settings.customRenderers.render.resultsGrid.renderLoadingOverlay();
526
+ }
527
+
528
+ const loadingOverlayClassName = twMerge(
529
+ 'absolute inset-0 bg-white bg-opacity-75 flex justify-center items-center z-10',
530
+ settings?.customStyles?.loadingOverlay
531
+ );
532
+
533
+ const loadingSpinnerClassName = twMerge(
534
+ 'h-8 w-8',
535
+ settings?.customStyles?.loadingSpinner
536
+ );
537
+
538
+ return (
539
+ <div className={loadingOverlayClassName}>
540
+ <LoaderSpinner className={loadingSpinnerClassName} />
541
+ </div>
542
+ );
543
+ };
544
+
545
+ const renderGridContainer = () => {
546
+ if (settings?.customRenderers?.render?.resultsGrid?.renderGridContainer) {
547
+ return settings.customRenderers.render.resultsGrid.renderGridContainer({
548
+ children: searchResults.products.map((product, index) =>
549
+ renderProductItem(product, index)
550
+ ),
551
+ resultsKey
552
+ });
553
+ }
554
+
555
+ const gridContainerClassName = twMerge(
556
+ 'grid gap-x-4 gap-y-12 grid-cols-2 md:grid-cols-3 flex-1',
557
+ settings?.customStyles?.gridContainer
558
+ );
559
+
560
+ return (
561
+ <div key={resultsKey} className={gridContainerClassName}>
562
+ {searchResults.products.map((product, index) =>
563
+ renderProductItem(product, index)
564
+ )}
565
+ </div>
566
+ );
567
+ };
568
+
569
+ const resultsContainerClassName = twMerge(
570
+ 'results-container',
571
+ settings?.customStyles?.resultsContainer
572
+ );
573
+
574
+ return (
575
+ <div className={gridClassName}>
576
+ {isLoading &&
577
+ searchResults &&
578
+ searchResults.products &&
579
+ renderLoadingOverlay()}
580
+
581
+ {searchResults && searchResults.products?.length > 0 ? (
582
+ <div className={resultsContainerClassName}>
583
+ {renderGridContainer()}
584
+ {renderPagination()}
585
+ </div>
586
+ ) : (
587
+ renderEmptyState()
588
+ )}
589
+ </div>
590
+ );
591
+ }
@@ -0,0 +1,38 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import { Icon } from '@akinon/next/components';
5
+ import { useLocalization } from '@akinon/next/hooks';
6
+
7
+ interface SimilarProductsButtonProps {
8
+ onClick: () => void;
9
+ className?: string;
10
+ isLoading?: boolean;
11
+ disabled?: boolean;
12
+ }
13
+
14
+ export function SimilarProductsButton({
15
+ onClick,
16
+ className = '',
17
+ isLoading = false,
18
+ disabled = false
19
+ }: SimilarProductsButtonProps) {
20
+ const { t } = useLocalization();
21
+
22
+ return (
23
+ <button
24
+ onClick={onClick}
25
+ disabled={disabled || isLoading}
26
+ className={`flex items-center justify-center p-2 bg-amber-200 rounded-md ${
27
+ disabled || isLoading ? 'opacity-50 cursor-not-allowed' : ''
28
+ } ${className}`}
29
+ >
30
+ <div className="flex items-center gap-2">
31
+ <Icon name="search" size={16} className="fill-black" />
32
+ <span className="text-xs font-medium text-black uppercase">
33
+ {t('common.product.view_similar_styles')}
34
+ </span>
35
+ </div>
36
+ </button>
37
+ );
38
+ }