@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.
- package/.gitattributes +15 -0
- package/.prettierrc +13 -0
- package/CHANGELOG.md +3 -0
- package/README.md +1372 -0
- package/package.json +21 -0
- package/src/data/endpoints.ts +122 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/use-image-cropper.ts +264 -0
- package/src/hooks/use-image-search-feature.ts +32 -0
- package/src/hooks/use-similar-products.ts +939 -0
- package/src/index.ts +33 -0
- package/src/types/index.ts +419 -0
- package/src/utils/image-validation.ts +303 -0
- package/src/utils/index.ts +161 -0
- package/src/views/filters.tsx +858 -0
- package/src/views/header-image-search-feature.tsx +68 -0
- package/src/views/image-search-button.tsx +47 -0
- package/src/views/image-search.tsx +152 -0
- package/src/views/main.tsx +200 -0
- package/src/views/product-image-search-feature.tsx +48 -0
- package/src/views/results.tsx +591 -0
- package/src/views/search-button.tsx +38 -0
- package/src/views/search-modal.tsx +422 -0
package/README.md
ADDED
|
@@ -0,0 +1,1372 @@
|
|
|
1
|
+
# Similar Products
|
|
2
|
+
|
|
3
|
+
🔍 **AI-powered visual search plugin for e-commerce platforms with comprehensive customization options.**
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
### 📋 Main Sections
|
|
10
|
+
|
|
11
|
+
- [Features](#markdown-header-features)
|
|
12
|
+
- [Installation](#markdown-header-installation)
|
|
13
|
+
- [Quick Start](#markdown-header-quick-start)
|
|
14
|
+
- [Available Components](#markdown-header-available-components)
|
|
15
|
+
- [Configuration](#markdown-header-configuration)
|
|
16
|
+
- [Customization Strategies](#markdown-header-customization-strategies)
|
|
17
|
+
- [Professional Examples](#markdown-header-professional-examples)
|
|
18
|
+
- [Usage Examples](#markdown-header-usage-examples)
|
|
19
|
+
- [Complete Styling Reference](#markdown-header-complete-styling-reference)
|
|
20
|
+
- [Best Practices](#markdown-header-best-practices)
|
|
21
|
+
- [Summary](#markdown-header-summary)
|
|
22
|
+
|
|
23
|
+
### 🔧 Quick Start Guide
|
|
24
|
+
|
|
25
|
+
- [Plugin Module Integration](#markdown-header-plugin-module-integration)
|
|
26
|
+
|
|
27
|
+
### 📦 Available Components
|
|
28
|
+
|
|
29
|
+
- [ProductImageSearchFeature](#markdown-header-productimasearchfeature)
|
|
30
|
+
- [HeaderImageSearchFeature](#markdown-header-headerimasearchfeature)
|
|
31
|
+
- [SimilarProductsPlugin](#markdown-header-similarproductsplugin)
|
|
32
|
+
|
|
33
|
+
### ⚙️ Configuration
|
|
34
|
+
|
|
35
|
+
- [SimilarProductsSettings Interface](#markdown-header-similarproductssettings-interface)
|
|
36
|
+
|
|
37
|
+
### 🎨 Professional Examples
|
|
38
|
+
|
|
39
|
+
- [E-commerce Store Integration](#markdown-header-e-commerce-store-integration)
|
|
40
|
+
- [Brand Customization](#markdown-header-brand-customization)
|
|
41
|
+
- [Custom Product Cards with Badges](#markdown-header-custom-product-cards-with-badges)
|
|
42
|
+
- [Advanced Filter Sidebar with Dynamic Icons](#markdown-header-advanced-filter-sidebar-with-dynamic-icons)
|
|
43
|
+
- [Header Integration](#markdown-header-header-integration)
|
|
44
|
+
- [Modern Pagination with Progress](#markdown-header-modern-pagination-with-progress)
|
|
45
|
+
- [Advanced Modal with Custom Header & Controls](#markdown-header-advanced-modal-with-custom-header-controls)
|
|
46
|
+
- [Complete Brand Theme Integration](#markdown-header-complete-brand-theme-integration)
|
|
47
|
+
|
|
48
|
+
### 📖 Styling Reference
|
|
49
|
+
|
|
50
|
+
- [Available Style Targets (40+ options)](#markdown-header-available-style-targets-40-options)
|
|
51
|
+
- [Available Render Functions (25+ options)](#markdown-header-available-render-functions-25-options)
|
|
52
|
+
|
|
53
|
+
### ✅ Best Practices
|
|
54
|
+
|
|
55
|
+
- [Performance Optimization](#markdown-header-performance-optimization)
|
|
56
|
+
- [Accessibility](#markdown-header-accessibility)
|
|
57
|
+
- [Essential Props](#markdown-header-essential-props)
|
|
58
|
+
- [Common Settings](#markdown-header-common-settings)
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Features
|
|
63
|
+
|
|
64
|
+
- **🖼️ Visual Search**: AI-powered image-based product discovery
|
|
65
|
+
- **✂️ Image Cropping**: Built-in cropping functionality with manual confirmation for precise searches
|
|
66
|
+
- **🎛️ Advanced Filtering**: Dynamic facet-based filtering system
|
|
67
|
+
- **📄 Multiple Pagination Modes**: Traditional pagination, load more button, and infinite scroll
|
|
68
|
+
- **🎨 Granular Customization**: 40+ style targets and 25+ render points
|
|
69
|
+
- **📱 Mobile Responsive**: Optimized for all device sizes
|
|
70
|
+
- **⚡ Performance Optimized**: Lazy loading and efficient rendering
|
|
71
|
+
- **🔧 TypeScript**: Full TypeScript support with detailed type definitions
|
|
72
|
+
|
|
73
|
+
## Installation
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npx @akinon/projectzero@latest --plugins
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Select `pz-similar-products` from the plugin list during installation.
|
|
80
|
+
|
|
81
|
+
## Quick Start
|
|
82
|
+
|
|
83
|
+
### Plugin Module Integration
|
|
84
|
+
|
|
85
|
+
The recommended way to integrate the plugin is using ProjectZero's PluginModule system:
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
import PluginModule, { Component } from '@akinon/next/components/plugin-module';
|
|
89
|
+
|
|
90
|
+
// Product detail page integration
|
|
91
|
+
<PluginModule
|
|
92
|
+
component={Component.ProductImageSearchFeature}
|
|
93
|
+
props={{
|
|
94
|
+
product,
|
|
95
|
+
activeIndex,
|
|
96
|
+
showResetButton: true,
|
|
97
|
+
settings: {
|
|
98
|
+
maxFileSize: 5,
|
|
99
|
+
enableCropping: true,
|
|
100
|
+
customStyles: {
|
|
101
|
+
modal: 'max-w-6xl rounded-xl shadow-2xl',
|
|
102
|
+
productItem: 'rounded-lg shadow-sm hover:shadow-lg'
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}}
|
|
106
|
+
/>
|
|
107
|
+
|
|
108
|
+
// Header search integration
|
|
109
|
+
<PluginModule
|
|
110
|
+
component={Component.HeaderImageSearchFeature}
|
|
111
|
+
props={{
|
|
112
|
+
isEnabled: true,
|
|
113
|
+
settings: {
|
|
114
|
+
maxFileSize: 5,
|
|
115
|
+
customStyles: {
|
|
116
|
+
imageSearchModal: 'max-w-lg rounded-xl'
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}}
|
|
120
|
+
/>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Available Components
|
|
124
|
+
|
|
125
|
+
### ProductImageSearchFeature
|
|
126
|
+
|
|
127
|
+
Ready-to-use component for product detail pages with visual search functionality.
|
|
128
|
+
|
|
129
|
+
**Props:**
|
|
130
|
+
|
|
131
|
+
- `product`: Product object (required)
|
|
132
|
+
- `activeIndex`: Current image index in product gallery
|
|
133
|
+
- `showResetButton`: Show reset to original image button
|
|
134
|
+
- `settings`: Customization settings object
|
|
135
|
+
|
|
136
|
+
### HeaderImageSearchFeature
|
|
137
|
+
|
|
138
|
+
Header search integration component for global image upload functionality.
|
|
139
|
+
|
|
140
|
+
**Props:**
|
|
141
|
+
|
|
142
|
+
- `isEnabled`: Enable/disable the feature
|
|
143
|
+
- `settings`: Customization settings object
|
|
144
|
+
|
|
145
|
+
### SimilarProductsPlugin
|
|
146
|
+
|
|
147
|
+
Core modal component for advanced integration scenarios.
|
|
148
|
+
|
|
149
|
+
**Props:**
|
|
150
|
+
|
|
151
|
+
- `product`: Product object (required)
|
|
152
|
+
- `isOpen`: Modal open state
|
|
153
|
+
- `onClose`: Close handler function
|
|
154
|
+
- `settings`: Customization settings object
|
|
155
|
+
|
|
156
|
+
## Configuration
|
|
157
|
+
|
|
158
|
+
### SimilarProductsSettings Interface
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
interface SimilarProductsSettings {
|
|
162
|
+
// Core functionality
|
|
163
|
+
maxFileSize?: number; // MB (default: 5)
|
|
164
|
+
imageFormats?: string[]; // ['jpg', 'jpeg', 'png', 'webp']
|
|
165
|
+
cropAspectRatio?: number; // Fixed aspect ratio
|
|
166
|
+
resultsPerPage?: number; // Products per page (default: 20)
|
|
167
|
+
enableCropping?: boolean; // Enable image cropping (default: true)
|
|
168
|
+
enableFileUpload?: boolean; // Enable file upload (default: true)
|
|
169
|
+
|
|
170
|
+
// Pagination settings
|
|
171
|
+
paginationType?: 'pagination' | 'load-more' | 'infinite-scroll'; // Pagination mode (default: 'pagination')
|
|
172
|
+
loadMoreText?: string; // Load more button text (default: 'Load More')
|
|
173
|
+
loadMoreStyle?: 'button' | 'auto'; // Load more behavior (default: 'button')
|
|
174
|
+
loadMoreThreshold?: number; // Pixels from bottom for auto load (default: 100)
|
|
175
|
+
maxPagesLoadMore?: number; // Maximum pages to load in load-more mode
|
|
176
|
+
|
|
177
|
+
// 40+ granular style targets
|
|
178
|
+
customStyles?: {
|
|
179
|
+
// Main components
|
|
180
|
+
modal?: string;
|
|
181
|
+
filterSidebar?: string;
|
|
182
|
+
resultsGrid?: string;
|
|
183
|
+
|
|
184
|
+
// Deep nested elements
|
|
185
|
+
productItem?: string;
|
|
186
|
+
productImage?: string;
|
|
187
|
+
productTitle?: string;
|
|
188
|
+
productPrice?: string;
|
|
189
|
+
paginationButton?: string;
|
|
190
|
+
loadMoreButton?: string;
|
|
191
|
+
loadMoreContainer?: string;
|
|
192
|
+
activeFilterTag?: string;
|
|
193
|
+
filterGroup?: string;
|
|
194
|
+
imageSection?: string;
|
|
195
|
+
loadingSpinner?: string;
|
|
196
|
+
// ... see examples for complete list
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// 25+ render functions for granular control
|
|
200
|
+
customRenderers?: {
|
|
201
|
+
render?: {
|
|
202
|
+
modal?: {
|
|
203
|
+
renderHeader?: (props) => React.ReactNode;
|
|
204
|
+
renderActiveFilters?: (props) => React.ReactNode;
|
|
205
|
+
renderControls?: (props) => React.ReactNode;
|
|
206
|
+
renderItemCount?: (props) => React.ReactNode;
|
|
207
|
+
// ... see examples for complete list
|
|
208
|
+
};
|
|
209
|
+
resultsGrid?: {
|
|
210
|
+
renderProductItem?: (props) => React.ReactNode;
|
|
211
|
+
renderPagination?: (props) => React.ReactNode;
|
|
212
|
+
renderGridContainer?: (props) => React.ReactNode;
|
|
213
|
+
// ... see examples for complete list
|
|
214
|
+
};
|
|
215
|
+
filterSidebar?: {
|
|
216
|
+
renderImageSection?: (props) => React.ReactNode;
|
|
217
|
+
renderFilterGroup?: (props) => React.ReactNode;
|
|
218
|
+
renderCropButton?: (props) => React.ReactNode;
|
|
219
|
+
// ... see examples for complete list
|
|
220
|
+
};
|
|
221
|
+
};
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// Theme configuration
|
|
225
|
+
theme?: {
|
|
226
|
+
colors?: Record<string, string>;
|
|
227
|
+
spacing?: Record<string, string>;
|
|
228
|
+
borderRadius?: Record<string, string>;
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Image Cropping with Manual Confirmation
|
|
234
|
+
|
|
235
|
+
The plugin features an advanced image cropping system with manual confirmation for precise control over search queries.
|
|
236
|
+
|
|
237
|
+
### Cropping Workflow
|
|
238
|
+
|
|
239
|
+
1. **Activate Crop Mode**: Click the crop button (✂️) to enter cropping mode
|
|
240
|
+
2. **Select Area**: Drag to select the desired area of the image
|
|
241
|
+
3. **Confirm Selection**: Click the green tick button (✓) to process the crop
|
|
242
|
+
4. **Cancel/Reset**: Click the X button to cancel current selection or return to previous successful crop
|
|
243
|
+
|
|
244
|
+
### Smart Crop Memory
|
|
245
|
+
|
|
246
|
+
The system intelligently manages crop states:
|
|
247
|
+
|
|
248
|
+
- **Successful Crops**: Remembered and can be restored if new crop is cancelled
|
|
249
|
+
- **Pending Crops**: Automatically discarded when crop mode is cancelled
|
|
250
|
+
- **Visual Feedback**: Crop overlay remains visible when not in crop mode
|
|
251
|
+
|
|
252
|
+
### Customization Options
|
|
253
|
+
|
|
254
|
+
```tsx
|
|
255
|
+
const cropSettings = {
|
|
256
|
+
enableCropping: true, // Enable/disable cropping feature
|
|
257
|
+
cropAspectRatio: 1, // Fixed aspect ratio (optional)
|
|
258
|
+
|
|
259
|
+
customStyles: {
|
|
260
|
+
cropButton: 'bg-white shadow-lg hover:shadow-xl transition-shadow',
|
|
261
|
+
tickButton: 'bg-green-50 text-green-600 hover:bg-green-100', // New tick button styling
|
|
262
|
+
imageContainer: 'border-2 border-dashed border-blue-300'
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
customRenderers: {
|
|
266
|
+
render: {
|
|
267
|
+
filterSidebar: {
|
|
268
|
+
renderCropButton: ({ isCropping, onClick, disabled }) => (
|
|
269
|
+
<button
|
|
270
|
+
onClick={onClick}
|
|
271
|
+
disabled={disabled}
|
|
272
|
+
className={`custom-crop-btn ${isCropping ? 'active' : ''}`}
|
|
273
|
+
>
|
|
274
|
+
{isCropping ? '✕ Cancel' : '✂️ Crop'}
|
|
275
|
+
</button>
|
|
276
|
+
),
|
|
277
|
+
renderTickButton: ({ onClick, disabled }) => (
|
|
278
|
+
<button
|
|
279
|
+
onClick={onClick}
|
|
280
|
+
disabled={disabled}
|
|
281
|
+
className="custom-tick-btn bg-green-500 text-white rounded-full"
|
|
282
|
+
>
|
|
283
|
+
✓ Apply Crop
|
|
284
|
+
</button>
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Loading States
|
|
293
|
+
|
|
294
|
+
Both crop and confirmation buttons show loading spinners during processing:
|
|
295
|
+
|
|
296
|
+
```tsx
|
|
297
|
+
const loadingSettings = {
|
|
298
|
+
customStyles: {
|
|
299
|
+
cropButton: 'transition-opacity duration-200',
|
|
300
|
+
tickButton: 'transition-opacity duration-200 disabled:opacity-50'
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Load More & Pagination Modes
|
|
306
|
+
|
|
307
|
+
The plugin supports three pagination modes for different user experiences:
|
|
308
|
+
|
|
309
|
+
### Pagination Types
|
|
310
|
+
|
|
311
|
+
1. **Traditional Pagination** (default): Classic page numbers with previous/next buttons
|
|
312
|
+
2. **Load More Button**: Shows "Load More" button to append additional results
|
|
313
|
+
3. **Infinite Scroll**: Automatically loads more content as user scrolls
|
|
314
|
+
|
|
315
|
+
### Load More Configuration
|
|
316
|
+
|
|
317
|
+
```tsx
|
|
318
|
+
const loadMoreSettings = {
|
|
319
|
+
paginationType: 'load-more', // Enable load more mode
|
|
320
|
+
loadMoreText: 'Show More Products', // Custom button text
|
|
321
|
+
loadMoreStyle: 'button', // 'button' or 'auto' (for infinite scroll)
|
|
322
|
+
loadMoreThreshold: 100, // Pixels from bottom for auto-trigger
|
|
323
|
+
maxPagesLoadMore: 5, // Limit max pages in load-more mode
|
|
324
|
+
|
|
325
|
+
customStyles: {
|
|
326
|
+
loadMoreButton:
|
|
327
|
+
'px-8 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors',
|
|
328
|
+
loadMoreContainer: 'flex justify-center items-center mt-8'
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Custom Load More Renderers
|
|
334
|
+
|
|
335
|
+
Full control over load more functionality:
|
|
336
|
+
|
|
337
|
+
```tsx
|
|
338
|
+
const customLoadMoreSettings = {
|
|
339
|
+
paginationType: 'load-more',
|
|
340
|
+
|
|
341
|
+
customRenderers: {
|
|
342
|
+
render: {
|
|
343
|
+
resultsGrid: {
|
|
344
|
+
renderLoadMore: ({
|
|
345
|
+
onLoadMore,
|
|
346
|
+
hasMore,
|
|
347
|
+
isLoading,
|
|
348
|
+
currentPage,
|
|
349
|
+
totalPages
|
|
350
|
+
}) => (
|
|
351
|
+
<div className="flex flex-col items-center space-y-4 mt-12">
|
|
352
|
+
{/* Progress indicator */}
|
|
353
|
+
<div className="text-center">
|
|
354
|
+
<div className="text-sm text-gray-600 mb-2">
|
|
355
|
+
Page {currentPage} of {totalPages}
|
|
356
|
+
</div>
|
|
357
|
+
<div className="w-64 bg-gray-200 rounded-full h-2">
|
|
358
|
+
<div
|
|
359
|
+
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
|
360
|
+
style={{
|
|
361
|
+
width: `${(currentPage / totalPages) * 100}%`
|
|
362
|
+
}}
|
|
363
|
+
></div>
|
|
364
|
+
</div>
|
|
365
|
+
</div>
|
|
366
|
+
|
|
367
|
+
{/* Load more button */}
|
|
368
|
+
{hasMore && (
|
|
369
|
+
<button
|
|
370
|
+
onClick={onLoadMore}
|
|
371
|
+
disabled={isLoading}
|
|
372
|
+
className="flex items-center space-x-2 px-8 py-3 bg-gradient-to-r from-blue-600 to-purple-600 text-white rounded-lg hover:from-blue-700 hover:to-purple-700 disabled:opacity-50 transition-all"
|
|
373
|
+
>
|
|
374
|
+
{isLoading ? (
|
|
375
|
+
<>
|
|
376
|
+
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
|
377
|
+
<span>Loading...</span>
|
|
378
|
+
</>
|
|
379
|
+
) : (
|
|
380
|
+
<>
|
|
381
|
+
<span>Load More Products</span>
|
|
382
|
+
<svg
|
|
383
|
+
className="w-4 h-4"
|
|
384
|
+
fill="none"
|
|
385
|
+
stroke="currentColor"
|
|
386
|
+
viewBox="0 0 24 24"
|
|
387
|
+
>
|
|
388
|
+
<path
|
|
389
|
+
strokeLinecap="round"
|
|
390
|
+
strokeLinejoin="round"
|
|
391
|
+
strokeWidth={2}
|
|
392
|
+
d="M19 9l-7 7-7-7"
|
|
393
|
+
/>
|
|
394
|
+
</svg>
|
|
395
|
+
</>
|
|
396
|
+
)}
|
|
397
|
+
</button>
|
|
398
|
+
)}
|
|
399
|
+
|
|
400
|
+
{/* Completion message */}
|
|
401
|
+
{!hasMore && (
|
|
402
|
+
<div className="text-center text-gray-500">
|
|
403
|
+
<p>All products loaded</p>
|
|
404
|
+
</div>
|
|
405
|
+
)}
|
|
406
|
+
</div>
|
|
407
|
+
),
|
|
408
|
+
|
|
409
|
+
renderLoadMoreButton: ({ onClick, disabled, isLoading, text }) => (
|
|
410
|
+
<button
|
|
411
|
+
onClick={onClick}
|
|
412
|
+
disabled={disabled}
|
|
413
|
+
className="px-6 py-3 bg-black text-white rounded-md hover:bg-gray-800 disabled:opacity-50 transition-colors"
|
|
414
|
+
>
|
|
415
|
+
{isLoading ? (
|
|
416
|
+
<div className="flex items-center space-x-2">
|
|
417
|
+
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
|
418
|
+
<span>Loading...</span>
|
|
419
|
+
</div>
|
|
420
|
+
) : (
|
|
421
|
+
text
|
|
422
|
+
)}
|
|
423
|
+
</button>
|
|
424
|
+
)
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### Infinite Scroll Example
|
|
432
|
+
|
|
433
|
+
Automatic loading with intersection observer:
|
|
434
|
+
|
|
435
|
+
```tsx
|
|
436
|
+
const infiniteScrollSettings = {
|
|
437
|
+
paginationType: 'infinite-scroll',
|
|
438
|
+
loadMoreThreshold: 200, // Start loading 200px before bottom
|
|
439
|
+
maxPagesLoadMore: 10, // Prevent endless loading
|
|
440
|
+
|
|
441
|
+
customStyles: {
|
|
442
|
+
loadMoreContainer: 'flex justify-center py-8',
|
|
443
|
+
loadingSpinner: 'h-8 w-8 text-blue-600'
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
## Customization Strategies
|
|
449
|
+
|
|
450
|
+
### 1. Quick Styling with CSS Classes
|
|
451
|
+
|
|
452
|
+
For rapid customization, apply CSS classes to specific elements:
|
|
453
|
+
|
|
454
|
+
```tsx
|
|
455
|
+
const settings = {
|
|
456
|
+
customStyles: {
|
|
457
|
+
modal: 'max-w-6xl rounded-2xl shadow-2xl border-4 border-blue-500',
|
|
458
|
+
productItem: 'hover:scale-105 transition-all duration-300 shadow-lg',
|
|
459
|
+
filterSidebar: 'bg-gradient-to-b from-gray-50 to-white',
|
|
460
|
+
paginationButton: 'rounded-full px-4 py-2 bg-blue-600 text-white',
|
|
461
|
+
activeFilterTag: 'bg-blue-100 text-blue-800 px-3 py-1 rounded-full'
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### 2. Advanced Customization with Render Functions
|
|
467
|
+
|
|
468
|
+
For complete control over specific interface elements:
|
|
469
|
+
|
|
470
|
+
```tsx
|
|
471
|
+
const settings = {
|
|
472
|
+
customRenderers: {
|
|
473
|
+
render: {
|
|
474
|
+
modal: {
|
|
475
|
+
renderHeader: ({ title, onClose }) => (
|
|
476
|
+
<div className="bg-gradient-to-r from-blue-600 to-purple-600 text-white p-6">
|
|
477
|
+
<div className="flex justify-between items-center">
|
|
478
|
+
<h2 className="text-2xl font-bold">🔍 Visual Search</h2>
|
|
479
|
+
<button
|
|
480
|
+
onClick={onClose}
|
|
481
|
+
className="text-white hover:text-blue-200"
|
|
482
|
+
>
|
|
483
|
+
✕
|
|
484
|
+
</button>
|
|
485
|
+
</div>
|
|
486
|
+
</div>
|
|
487
|
+
)
|
|
488
|
+
},
|
|
489
|
+
resultsGrid: {
|
|
490
|
+
renderProductItem: ({ product, index }) => (
|
|
491
|
+
<div className="bg-white rounded-xl shadow-lg hover:shadow-xl transition-shadow">
|
|
492
|
+
<div className="aspect-square overflow-hidden rounded-t-xl">
|
|
493
|
+
<img
|
|
494
|
+
src={product.productimage_set?.[0]?.image}
|
|
495
|
+
alt={product.name}
|
|
496
|
+
className="w-full h-full object-cover hover:scale-110 transition-transform"
|
|
497
|
+
/>
|
|
498
|
+
</div>
|
|
499
|
+
<div className="p-4">
|
|
500
|
+
<h3 className="font-semibold text-gray-900 mb-2">
|
|
501
|
+
{product.name}
|
|
502
|
+
</h3>
|
|
503
|
+
<p className="text-xl font-bold text-green-600">
|
|
504
|
+
{product.price} ₺
|
|
505
|
+
</p>
|
|
506
|
+
</div>
|
|
507
|
+
</div>
|
|
508
|
+
)
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
## Professional Examples
|
|
516
|
+
|
|
517
|
+
### E-commerce Store Integration
|
|
518
|
+
|
|
519
|
+
Complete production-ready implementation:
|
|
520
|
+
|
|
521
|
+
```tsx
|
|
522
|
+
const storeSettings = {
|
|
523
|
+
// Performance & functionality
|
|
524
|
+
maxFileSize: 5,
|
|
525
|
+
resultsPerPage: 24,
|
|
526
|
+
enableCropping: true,
|
|
527
|
+
|
|
528
|
+
// Professional styling
|
|
529
|
+
customStyles: {
|
|
530
|
+
modal: 'max-w-7xl rounded-xl shadow-2xl',
|
|
531
|
+
productItem: 'rounded-lg shadow-sm hover:shadow-lg transition-shadow',
|
|
532
|
+
paginationButton: 'px-4 py-2 rounded-lg border hover:bg-gray-50',
|
|
533
|
+
activeFilterTag: 'bg-blue-100 text-blue-800 px-3 py-1 rounded-full'
|
|
534
|
+
},
|
|
535
|
+
|
|
536
|
+
// Custom branding
|
|
537
|
+
customRenderers: {
|
|
538
|
+
render: {
|
|
539
|
+
modal: {
|
|
540
|
+
renderHeader: ({ title, onClose }) => (
|
|
541
|
+
<div className="bg-blue-600 text-white p-6 rounded-t-xl">
|
|
542
|
+
<div className="flex justify-between items-center">
|
|
543
|
+
<h2 className="text-2xl font-bold">Find Similar Products</h2>
|
|
544
|
+
<button
|
|
545
|
+
onClick={onClose}
|
|
546
|
+
className="text-white hover:text-blue-200"
|
|
547
|
+
>
|
|
548
|
+
✕
|
|
549
|
+
</button>
|
|
550
|
+
</div>
|
|
551
|
+
</div>
|
|
552
|
+
)
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
<PluginModule
|
|
559
|
+
component={Component.ProductImageSearchFeature}
|
|
560
|
+
props={{
|
|
561
|
+
product,
|
|
562
|
+
activeIndex,
|
|
563
|
+
showResetButton: true,
|
|
564
|
+
settings: storeSettings
|
|
565
|
+
}}
|
|
566
|
+
/>;
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
### Brand Customization
|
|
570
|
+
|
|
571
|
+
Customize colors, spacing and styling to match your brand:
|
|
572
|
+
|
|
573
|
+
```tsx
|
|
574
|
+
const brandSettings = {
|
|
575
|
+
theme: {
|
|
576
|
+
colors: {
|
|
577
|
+
primary: '#FF6B35', // Your brand color
|
|
578
|
+
secondary: '#004E64',
|
|
579
|
+
background: '#F8F9FA'
|
|
580
|
+
}
|
|
581
|
+
},
|
|
582
|
+
|
|
583
|
+
customStyles: {
|
|
584
|
+
modal: 'border-4 border-orange-500 rounded-2xl',
|
|
585
|
+
productItem: 'hover:scale-105 transition-transform',
|
|
586
|
+
activeFilterTag: 'bg-orange-100 text-orange-800 px-3 py-1 rounded-full'
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
### Custom Product Cards with Badges
|
|
592
|
+
|
|
593
|
+
Create custom product displays with badges, animations and enhanced styling:
|
|
594
|
+
|
|
595
|
+
```tsx
|
|
596
|
+
const customProductSettings = {
|
|
597
|
+
// Granular styling for product elements
|
|
598
|
+
customStyles: {
|
|
599
|
+
productItem:
|
|
600
|
+
'group relative bg-white rounded-xl shadow-sm hover:shadow-xl transition-all overflow-hidden',
|
|
601
|
+
productImageWrapper: 'relative aspect-square overflow-hidden',
|
|
602
|
+
productImage:
|
|
603
|
+
'w-full h-full object-cover group-hover:scale-105 transition-transform duration-300',
|
|
604
|
+
productInfo: 'p-4',
|
|
605
|
+
productTitle: 'font-semibold text-gray-900 mb-2 line-clamp-2',
|
|
606
|
+
productPrice: 'text-lg font-bold text-green-600',
|
|
607
|
+
productOldPrice: 'text-sm text-gray-500 line-through ml-2'
|
|
608
|
+
},
|
|
609
|
+
|
|
610
|
+
// Custom render function for complete control
|
|
611
|
+
customRenderers: {
|
|
612
|
+
render: {
|
|
613
|
+
resultsGrid: {
|
|
614
|
+
renderProductItem: ({ product, index }) => (
|
|
615
|
+
<div className="group relative bg-white rounded-xl shadow-sm hover:shadow-xl transition-all duration-300 overflow-hidden">
|
|
616
|
+
{/* Custom badges */}
|
|
617
|
+
{index < 3 && (
|
|
618
|
+
<div className="absolute top-2 left-2 z-10 bg-red-500 text-white text-xs px-2 py-1 rounded-full font-bold">
|
|
619
|
+
TOP {index + 1}
|
|
620
|
+
</div>
|
|
621
|
+
)}
|
|
622
|
+
{product.discount && (
|
|
623
|
+
<div className="absolute top-2 right-2 z-10 bg-orange-500 text-white text-xs px-2 py-1 rounded-full">
|
|
624
|
+
-{product.discount}%
|
|
625
|
+
</div>
|
|
626
|
+
)}
|
|
627
|
+
|
|
628
|
+
{/* Image with hover effect */}
|
|
629
|
+
<div className="aspect-square overflow-hidden">
|
|
630
|
+
<img
|
|
631
|
+
src={product.productimage_set?.[0]?.image}
|
|
632
|
+
alt={product.name}
|
|
633
|
+
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-300"
|
|
634
|
+
/>
|
|
635
|
+
</div>
|
|
636
|
+
|
|
637
|
+
{/* Enhanced product info */}
|
|
638
|
+
<div className="p-4">
|
|
639
|
+
<h3 className="font-semibold text-gray-900 mb-2">
|
|
640
|
+
{product.name}
|
|
641
|
+
</h3>
|
|
642
|
+
<div className="flex items-center justify-between">
|
|
643
|
+
<div className="flex items-center gap-2">
|
|
644
|
+
<span className="text-xl font-bold text-green-600">
|
|
645
|
+
{product.price} ₺
|
|
646
|
+
</span>
|
|
647
|
+
{product.old_price && (
|
|
648
|
+
<span className="text-sm text-gray-500 line-through">
|
|
649
|
+
{product.old_price} ₺
|
|
650
|
+
</span>
|
|
651
|
+
)}
|
|
652
|
+
</div>
|
|
653
|
+
<button className="bg-blue-600 text-white px-3 py-1 rounded-full text-sm hover:bg-blue-700 transition-colors">
|
|
654
|
+
Add to Cart
|
|
655
|
+
</button>
|
|
656
|
+
</div>
|
|
657
|
+
</div>
|
|
658
|
+
</div>
|
|
659
|
+
)
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
### Advanced Filter Sidebar with Dynamic Icons
|
|
667
|
+
|
|
668
|
+
Customize filters with dynamic icons, enhanced mobile header, and professional styling:
|
|
669
|
+
|
|
670
|
+
```tsx
|
|
671
|
+
const advancedFilterSettings = {
|
|
672
|
+
customStyles: {
|
|
673
|
+
filterSidebarMobileHeader:
|
|
674
|
+
'bg-gradient-to-r from-purple-600 to-blue-600 text-white p-6 -m-6 mb-6',
|
|
675
|
+
filterGroup: 'mb-6 border-b border-gray-200 pb-4',
|
|
676
|
+
filterGroupTitle:
|
|
677
|
+
'text-lg font-semibold text-gray-900 mb-3 flex items-center',
|
|
678
|
+
filterItem:
|
|
679
|
+
'flex items-center justify-between py-2 hover:bg-gray-50 px-2 rounded transition-colors',
|
|
680
|
+
filterItemLabel: 'text-sm text-gray-700 flex-1',
|
|
681
|
+
filterItemCount: 'text-xs bg-gray-100 px-2 py-1 rounded-full ml-2',
|
|
682
|
+
imageSection: 'border-2 border-dashed border-purple-300 rounded-lg p-4',
|
|
683
|
+
cropButton: 'bg-purple-600 text-white shadow-lg hover:bg-purple-700',
|
|
684
|
+
uploadButton:
|
|
685
|
+
'bg-gradient-to-r from-blue-500 to-purple-600 text-white border-0 hover:from-blue-600 hover:to-purple-700',
|
|
686
|
+
resetButton:
|
|
687
|
+
'bg-gradient-to-r from-green-500 to-blue-500 text-white border-0'
|
|
688
|
+
},
|
|
689
|
+
|
|
690
|
+
customRenderers: {
|
|
691
|
+
render: {
|
|
692
|
+
filterSidebar: {
|
|
693
|
+
// Enhanced mobile header with icon and item count
|
|
694
|
+
renderMobileHeader: ({ title, itemCount, onClose }) => (
|
|
695
|
+
<div className="bg-gradient-to-r from-purple-600 to-blue-600 text-white p-6 -m-6 mb-6 rounded-lg">
|
|
696
|
+
<div className="flex justify-between items-center">
|
|
697
|
+
<div>
|
|
698
|
+
<h3 className="text-xl font-bold flex items-center">
|
|
699
|
+
<svg
|
|
700
|
+
className="w-6 h-6 mr-2"
|
|
701
|
+
fill="currentColor"
|
|
702
|
+
viewBox="0 0 20 20"
|
|
703
|
+
>
|
|
704
|
+
<path d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V4z" />
|
|
705
|
+
</svg>
|
|
706
|
+
{title}
|
|
707
|
+
</h3>
|
|
708
|
+
<p className="text-purple-100 text-sm">
|
|
709
|
+
{itemCount} products found
|
|
710
|
+
</p>
|
|
711
|
+
</div>
|
|
712
|
+
<button
|
|
713
|
+
onClick={onClose}
|
|
714
|
+
className="text-white hover:text-purple-200"
|
|
715
|
+
>
|
|
716
|
+
<svg
|
|
717
|
+
className="w-6 h-6"
|
|
718
|
+
fill="none"
|
|
719
|
+
stroke="currentColor"
|
|
720
|
+
viewBox="0 0 24 24"
|
|
721
|
+
>
|
|
722
|
+
<path
|
|
723
|
+
strokeLinecap="round"
|
|
724
|
+
strokeLinejoin="round"
|
|
725
|
+
strokeWidth={2}
|
|
726
|
+
d="M6 18L18 6M6 6l12 12"
|
|
727
|
+
/>
|
|
728
|
+
</svg>
|
|
729
|
+
</button>
|
|
730
|
+
</div>
|
|
731
|
+
</div>
|
|
732
|
+
),
|
|
733
|
+
|
|
734
|
+
// Dynamic icons based on filter type
|
|
735
|
+
renderFilterGroup: ({ facet, onFacetChange, isLoading }) => (
|
|
736
|
+
<div className="mb-6 bg-white rounded-lg border border-gray-200 p-4">
|
|
737
|
+
<h4 className="font-bold text-gray-900 mb-3 flex items-center">
|
|
738
|
+
{/* Dynamic icons based on facet type */}
|
|
739
|
+
{facet.key === 'color' && (
|
|
740
|
+
<div className="w-4 h-4 bg-gradient-to-r from-red-500 to-blue-500 rounded-full mr-2"></div>
|
|
741
|
+
)}
|
|
742
|
+
{facet.key === 'size' && (
|
|
743
|
+
<svg
|
|
744
|
+
className="w-4 h-4 mr-2"
|
|
745
|
+
fill="currentColor"
|
|
746
|
+
viewBox="0 0 20 20"
|
|
747
|
+
>
|
|
748
|
+
<path d="M4 3a2 2 0 000 4h12a2 2 0 000-4H4zM4 9a2 2 0 000 4h12a2 2 0 000-4H4zM4 15a2 2 0 000 4h12a2 2 0 000-4H4z" />
|
|
749
|
+
</svg>
|
|
750
|
+
)}
|
|
751
|
+
{facet.key === 'brand' && (
|
|
752
|
+
<svg
|
|
753
|
+
className="w-4 h-4 mr-2"
|
|
754
|
+
fill="currentColor"
|
|
755
|
+
viewBox="0 0 20 20"
|
|
756
|
+
>
|
|
757
|
+
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
|
758
|
+
</svg>
|
|
759
|
+
)}
|
|
760
|
+
{facet.name}
|
|
761
|
+
</h4>
|
|
762
|
+
<div className="space-y-2">
|
|
763
|
+
{facet.data.choices?.slice(0, 8).map((choice) => (
|
|
764
|
+
<label
|
|
765
|
+
key={choice.value}
|
|
766
|
+
className="flex items-center justify-between cursor-pointer p-2 hover:bg-gray-50 rounded transition-colors"
|
|
767
|
+
>
|
|
768
|
+
<div className="flex items-center flex-1">
|
|
769
|
+
<input
|
|
770
|
+
type="checkbox"
|
|
771
|
+
checked={choice.is_selected}
|
|
772
|
+
onChange={() => onFacetChange(facet.key, choice.value)}
|
|
773
|
+
disabled={isLoading}
|
|
774
|
+
className="mr-3 rounded border-gray-300 text-purple-600 focus:ring-purple-500"
|
|
775
|
+
/>
|
|
776
|
+
<span className="text-sm font-medium">{choice.label}</span>
|
|
777
|
+
</div>
|
|
778
|
+
<span className="text-xs bg-purple-100 text-purple-800 px-2 py-1 rounded-full font-medium">
|
|
779
|
+
{choice.quantity}
|
|
780
|
+
</span>
|
|
781
|
+
</label>
|
|
782
|
+
))}
|
|
783
|
+
</div>
|
|
784
|
+
</div>
|
|
785
|
+
)
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
### Header Integration
|
|
793
|
+
|
|
794
|
+
Add visual search to your site header:
|
|
795
|
+
|
|
796
|
+
```tsx
|
|
797
|
+
import PluginModule, { Component } from '@akinon/next/components/plugin-module';
|
|
798
|
+
|
|
799
|
+
export function SiteHeader() {
|
|
800
|
+
return (
|
|
801
|
+
<header className="flex items-center justify-between p-4">
|
|
802
|
+
<div className="logo">Your Store</div>
|
|
803
|
+
|
|
804
|
+
<div className="search-area flex items-center gap-4">
|
|
805
|
+
{/* Regular search */}
|
|
806
|
+
<input
|
|
807
|
+
type="text"
|
|
808
|
+
placeholder="Search products..."
|
|
809
|
+
className="border rounded-lg px-4 py-2"
|
|
810
|
+
/>
|
|
811
|
+
|
|
812
|
+
{/* Visual search */}
|
|
813
|
+
<PluginModule
|
|
814
|
+
component={Component.HeaderImageSearchFeature}
|
|
815
|
+
props={{
|
|
816
|
+
isEnabled: true,
|
|
817
|
+
settings: {
|
|
818
|
+
maxFileSize: 5,
|
|
819
|
+
customStyles: {
|
|
820
|
+
imageSearchModal: 'max-w-lg rounded-xl'
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}}
|
|
824
|
+
/>
|
|
825
|
+
</div>
|
|
826
|
+
</header>
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
### Modern Pagination with Progress
|
|
832
|
+
|
|
833
|
+
Create advanced pagination with progress bars and smart page navigation:
|
|
834
|
+
|
|
835
|
+
```tsx
|
|
836
|
+
const modernPaginationSettings = {
|
|
837
|
+
customStyles: {
|
|
838
|
+
paginationContainer: 'flex flex-col items-center space-y-4 mt-8',
|
|
839
|
+
paginationInfo: 'text-sm text-gray-600 font-medium',
|
|
840
|
+
paginationButton:
|
|
841
|
+
'px-3 py-2 text-sm border border-gray-300 rounded-md hover:bg-gray-50 transition-colors',
|
|
842
|
+
paginationButtonActive:
|
|
843
|
+
'bg-blue-600 text-white border-blue-600 hover:bg-blue-700',
|
|
844
|
+
paginationPrevious:
|
|
845
|
+
'px-4 py-2 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors',
|
|
846
|
+
paginationNext:
|
|
847
|
+
'px-4 py-2 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors'
|
|
848
|
+
},
|
|
849
|
+
|
|
850
|
+
customRenderers: {
|
|
851
|
+
render: {
|
|
852
|
+
resultsGrid: {
|
|
853
|
+
renderPagination: ({ pagination, onPageChange, isLoading }) => (
|
|
854
|
+
<div className="flex flex-col items-center space-y-6 mt-12">
|
|
855
|
+
{/* Progress bar */}
|
|
856
|
+
<div className="text-center">
|
|
857
|
+
<div className="text-sm text-gray-600 mb-2">
|
|
858
|
+
Page {pagination.current_page} of {pagination.num_pages}
|
|
859
|
+
</div>
|
|
860
|
+
<div className="w-full bg-gray-200 rounded-full h-2 max-w-xs">
|
|
861
|
+
<div
|
|
862
|
+
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
|
863
|
+
style={{
|
|
864
|
+
width: `${
|
|
865
|
+
(pagination.current_page / pagination.num_pages) * 100
|
|
866
|
+
}%`
|
|
867
|
+
}}
|
|
868
|
+
></div>
|
|
869
|
+
</div>
|
|
870
|
+
</div>
|
|
871
|
+
|
|
872
|
+
{/* Smart pagination buttons */}
|
|
873
|
+
<div className="flex items-center space-x-2">
|
|
874
|
+
<button
|
|
875
|
+
onClick={() => onPageChange(pagination.current_page - 1)}
|
|
876
|
+
disabled={pagination.current_page === 1 || isLoading}
|
|
877
|
+
className="flex items-center px-4 py-2 bg-gray-100 rounded-lg hover:bg-gray-200 disabled:opacity-50 transition-colors"
|
|
878
|
+
>
|
|
879
|
+
<svg
|
|
880
|
+
className="w-4 h-4 mr-2"
|
|
881
|
+
fill="none"
|
|
882
|
+
stroke="currentColor"
|
|
883
|
+
viewBox="0 0 24 24"
|
|
884
|
+
>
|
|
885
|
+
<path
|
|
886
|
+
strokeLinecap="round"
|
|
887
|
+
strokeLinejoin="round"
|
|
888
|
+
strokeWidth={2}
|
|
889
|
+
d="M15 19l-7-7 7-7"
|
|
890
|
+
/>
|
|
891
|
+
</svg>
|
|
892
|
+
Previous
|
|
893
|
+
</button>
|
|
894
|
+
|
|
895
|
+
{Array.from(
|
|
896
|
+
{ length: Math.min(5, pagination.num_pages) },
|
|
897
|
+
(_, i) => {
|
|
898
|
+
const page = i + Math.max(1, pagination.current_page - 2);
|
|
899
|
+
const isActive = page === pagination.current_page;
|
|
900
|
+
|
|
901
|
+
return (
|
|
902
|
+
<button
|
|
903
|
+
key={page}
|
|
904
|
+
onClick={() => onPageChange(page)}
|
|
905
|
+
disabled={isLoading}
|
|
906
|
+
className={`px-4 py-2 rounded-lg transition-colors ${
|
|
907
|
+
isActive
|
|
908
|
+
? 'bg-blue-600 text-white'
|
|
909
|
+
: 'bg-gray-100 hover:bg-gray-200 text-gray-700'
|
|
910
|
+
}`}
|
|
911
|
+
>
|
|
912
|
+
{page}
|
|
913
|
+
</button>
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
)}
|
|
917
|
+
|
|
918
|
+
<button
|
|
919
|
+
onClick={() => onPageChange(pagination.current_page + 1)}
|
|
920
|
+
disabled={
|
|
921
|
+
pagination.current_page === pagination.num_pages || isLoading
|
|
922
|
+
}
|
|
923
|
+
className="flex items-center px-4 py-2 bg-gray-100 rounded-lg hover:bg-gray-200 disabled:opacity-50 transition-colors"
|
|
924
|
+
>
|
|
925
|
+
Next
|
|
926
|
+
<svg
|
|
927
|
+
className="w-4 h-4 ml-2"
|
|
928
|
+
fill="none"
|
|
929
|
+
stroke="currentColor"
|
|
930
|
+
viewBox="0 0 24 24"
|
|
931
|
+
>
|
|
932
|
+
<path
|
|
933
|
+
strokeLinecap="round"
|
|
934
|
+
strokeLinejoin="round"
|
|
935
|
+
strokeWidth={2}
|
|
936
|
+
d="M9 5l7 7-7 7"
|
|
937
|
+
/>
|
|
938
|
+
</svg>
|
|
939
|
+
</button>
|
|
940
|
+
</div>
|
|
941
|
+
</div>
|
|
942
|
+
)
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
};
|
|
947
|
+
```
|
|
948
|
+
|
|
949
|
+
### Advanced Modal with Custom Header & Controls
|
|
950
|
+
|
|
951
|
+
Create a fully branded modal experience:
|
|
952
|
+
|
|
953
|
+
```tsx
|
|
954
|
+
const advancedModalSettings = {
|
|
955
|
+
customStyles: {
|
|
956
|
+
modal: 'max-w-7xl rounded-2xl shadow-2xl border-4 border-blue-500',
|
|
957
|
+
activeFiltersContainer:
|
|
958
|
+
'bg-gradient-to-r from-blue-50 to-purple-50 border-l-4 border-blue-500 p-4',
|
|
959
|
+
activeFilterTag:
|
|
960
|
+
'bg-white border border-blue-200 text-blue-800 px-3 py-1 rounded-full shadow-sm',
|
|
961
|
+
controlsContainer: 'bg-gray-50 border-y border-gray-200',
|
|
962
|
+
itemCount:
|
|
963
|
+
'text-sm font-medium text-green-600 bg-green-100 px-3 py-1 rounded-full',
|
|
964
|
+
sortDropdown:
|
|
965
|
+
'bg-white border-2 border-gray-300 rounded-lg px-4 py-2 focus:border-blue-500 focus:ring-2 focus:ring-blue-200'
|
|
966
|
+
},
|
|
967
|
+
|
|
968
|
+
customRenderers: {
|
|
969
|
+
render: {
|
|
970
|
+
modal: {
|
|
971
|
+
renderHeader: ({ title, onClose }) => (
|
|
972
|
+
<div className="bg-gradient-to-r from-blue-600 via-purple-600 to-blue-800 text-white p-6">
|
|
973
|
+
<div className="flex justify-between items-center">
|
|
974
|
+
<div className="flex items-center">
|
|
975
|
+
<div className="w-10 h-10 bg-white bg-opacity-20 rounded-full flex items-center justify-center mr-4">
|
|
976
|
+
<span className="text-2xl">🔍</span>
|
|
977
|
+
</div>
|
|
978
|
+
<div>
|
|
979
|
+
<h2 className="text-2xl font-bold mb-1">
|
|
980
|
+
🎯 AI Visual Search
|
|
981
|
+
</h2>
|
|
982
|
+
<p className="text-blue-100">
|
|
983
|
+
Discover your perfect match with advanced AI
|
|
984
|
+
</p>
|
|
985
|
+
</div>
|
|
986
|
+
</div>
|
|
987
|
+
<button
|
|
988
|
+
onClick={onClose}
|
|
989
|
+
className="text-white hover:text-blue-200 bg-white bg-opacity-20 rounded-full p-2 transition-colors"
|
|
990
|
+
>
|
|
991
|
+
✕
|
|
992
|
+
</button>
|
|
993
|
+
</div>
|
|
994
|
+
</div>
|
|
995
|
+
),
|
|
996
|
+
renderItemCount: ({ count, isLoading }) => (
|
|
997
|
+
<div className="flex items-center bg-green-100 text-green-800 px-3 py-2 rounded-lg">
|
|
998
|
+
<div
|
|
999
|
+
className={`w-2 h-2 rounded-full mr-2 ${
|
|
1000
|
+
isLoading ? 'bg-yellow-500 animate-pulse' : 'bg-green-500'
|
|
1001
|
+
}`}
|
|
1002
|
+
></div>
|
|
1003
|
+
<span className="text-sm font-medium">
|
|
1004
|
+
{count} {count === 1 ? 'product' : 'products'} found
|
|
1005
|
+
</span>
|
|
1006
|
+
</div>
|
|
1007
|
+
),
|
|
1008
|
+
renderActiveFilterTag: ({ filter, onRemove, isLoading }) => (
|
|
1009
|
+
<div className="inline-flex items-center bg-gradient-to-r from-blue-100 to-purple-100 border border-blue-300 text-blue-800 px-3 py-2 rounded-full text-sm font-medium shadow-sm mr-2 mb-2">
|
|
1010
|
+
<span className="mr-2">{filter.label}</span>
|
|
1011
|
+
<button
|
|
1012
|
+
onClick={onRemove}
|
|
1013
|
+
disabled={isLoading}
|
|
1014
|
+
className="text-blue-600 hover:text-blue-800 hover:bg-blue-200 rounded-full p-1 transition-colors disabled:opacity-50"
|
|
1015
|
+
>
|
|
1016
|
+
✕
|
|
1017
|
+
</button>
|
|
1018
|
+
</div>
|
|
1019
|
+
)
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
};
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
### Complete Brand Theme Integration
|
|
1027
|
+
|
|
1028
|
+
Full brand customization with theme variables, CSS styling, and custom components:
|
|
1029
|
+
|
|
1030
|
+
```tsx
|
|
1031
|
+
const brandThemeSettings = {
|
|
1032
|
+
// CSS Variables and Theme
|
|
1033
|
+
theme: {
|
|
1034
|
+
colors: {
|
|
1035
|
+
primary: '#FF6B35', // Brand orange
|
|
1036
|
+
secondary: '#004E64', // Brand navy
|
|
1037
|
+
accent: '#FFB700', // Brand yellow
|
|
1038
|
+
background: '#F8F9FA',
|
|
1039
|
+
text: '#212529'
|
|
1040
|
+
},
|
|
1041
|
+
spacing: {
|
|
1042
|
+
xs: '0.25rem',
|
|
1043
|
+
sm: '0.5rem',
|
|
1044
|
+
md: '1rem',
|
|
1045
|
+
lg: '1.5rem',
|
|
1046
|
+
xl: '2rem'
|
|
1047
|
+
},
|
|
1048
|
+
borderRadius: {
|
|
1049
|
+
sm: '0.25rem',
|
|
1050
|
+
md: '0.5rem',
|
|
1051
|
+
lg: '1rem'
|
|
1052
|
+
}
|
|
1053
|
+
},
|
|
1054
|
+
|
|
1055
|
+
// Comprehensive brand styling
|
|
1056
|
+
customStyles: {
|
|
1057
|
+
modal: 'max-w-7xl rounded-2xl shadow-2xl border-4 border-orange-500',
|
|
1058
|
+
activeFiltersContainer: 'bg-orange-50 border-l-4 border-orange-500',
|
|
1059
|
+
activeFilterTag: 'bg-orange-100 text-orange-800 border border-orange-300',
|
|
1060
|
+
controlsContainer: 'bg-navy-900 text-white',
|
|
1061
|
+
itemCount: 'bg-yellow-100 text-yellow-800 font-bold',
|
|
1062
|
+
sortDropdown:
|
|
1063
|
+
'border-orange-300 focus:border-orange-500 focus:ring-orange-200',
|
|
1064
|
+
filterGroup: 'bg-white border-l-4 border-orange-500 shadow-lg',
|
|
1065
|
+
filterGroupTitle: 'text-navy-900 font-bold text-lg',
|
|
1066
|
+
productItem:
|
|
1067
|
+
'border-2 border-orange-200 hover:border-orange-500 hover:shadow-lg',
|
|
1068
|
+
productPrice: 'text-2xl font-bold text-orange-600',
|
|
1069
|
+
pagination: 'space-x-2',
|
|
1070
|
+
paginationButton: 'border-orange-300 text-orange-600 hover:bg-orange-50',
|
|
1071
|
+
paginationButtonActive: 'bg-orange-600 text-white border-orange-600'
|
|
1072
|
+
},
|
|
1073
|
+
|
|
1074
|
+
// Brand-specific custom components
|
|
1075
|
+
customRenderers: {
|
|
1076
|
+
render: {
|
|
1077
|
+
modal: {
|
|
1078
|
+
renderHeader: ({ title, onClose }) => (
|
|
1079
|
+
<div className="bg-gradient-to-r from-orange-500 to-red-600 text-white p-6">
|
|
1080
|
+
<div className="flex justify-between items-center">
|
|
1081
|
+
<div className="flex items-center">
|
|
1082
|
+
<div className="w-12 h-12 bg-white bg-opacity-20 rounded-full flex items-center justify-center mr-4">
|
|
1083
|
+
<span className="text-2xl">🔥</span>
|
|
1084
|
+
</div>
|
|
1085
|
+
<div>
|
|
1086
|
+
<h2 className="text-3xl font-bold mb-1">Visual Search</h2>
|
|
1087
|
+
<p className="text-orange-100">Powered by AI • Brand Store</p>
|
|
1088
|
+
</div>
|
|
1089
|
+
</div>
|
|
1090
|
+
<button
|
|
1091
|
+
onClick={onClose}
|
|
1092
|
+
className="text-white hover:text-orange-200 bg-white bg-opacity-20 rounded-full p-3 transition-colors"
|
|
1093
|
+
>
|
|
1094
|
+
✕
|
|
1095
|
+
</button>
|
|
1096
|
+
</div>
|
|
1097
|
+
</div>
|
|
1098
|
+
)
|
|
1099
|
+
},
|
|
1100
|
+
resultsGrid: {
|
|
1101
|
+
renderProductItem: ({ product, index }) => (
|
|
1102
|
+
<div className="bg-white rounded-xl border-2 border-orange-200 hover:border-orange-500 hover:shadow-xl transition-all duration-300 overflow-hidden">
|
|
1103
|
+
{/* Brand-specific badges */}
|
|
1104
|
+
{product.is_featured && (
|
|
1105
|
+
<div className="absolute top-3 left-3 z-10 bg-gradient-to-r from-orange-500 to-red-500 text-white text-xs px-3 py-1 rounded-full font-bold shadow-lg">
|
|
1106
|
+
🔥 FEATURED
|
|
1107
|
+
</div>
|
|
1108
|
+
)}
|
|
1109
|
+
|
|
1110
|
+
<div className="aspect-square overflow-hidden bg-gray-100">
|
|
1111
|
+
<img
|
|
1112
|
+
src={product.productimage_set?.[0]?.image}
|
|
1113
|
+
alt={product.name}
|
|
1114
|
+
className="w-full h-full object-cover hover:scale-105 transition-transform duration-300"
|
|
1115
|
+
/>
|
|
1116
|
+
</div>
|
|
1117
|
+
|
|
1118
|
+
<div className="p-4 border-t-2 border-orange-100">
|
|
1119
|
+
<h3 className="font-bold text-navy-900 mb-2 line-clamp-2">
|
|
1120
|
+
{product.name}
|
|
1121
|
+
</h3>
|
|
1122
|
+
<div className="flex items-center justify-between">
|
|
1123
|
+
<div>
|
|
1124
|
+
<span className="text-2xl font-bold text-orange-600">
|
|
1125
|
+
{product.price} ₺
|
|
1126
|
+
</span>
|
|
1127
|
+
{product.old_price && (
|
|
1128
|
+
<span className="text-sm text-gray-500 line-through ml-2">
|
|
1129
|
+
{product.old_price} ₺
|
|
1130
|
+
</span>
|
|
1131
|
+
)}
|
|
1132
|
+
</div>
|
|
1133
|
+
<button className="bg-gradient-to-r from-orange-500 to-red-500 text-white px-4 py-2 rounded-lg font-bold hover:from-orange-600 hover:to-red-600 transition-all shadow-lg">
|
|
1134
|
+
ADD TO CART
|
|
1135
|
+
</button>
|
|
1136
|
+
</div>
|
|
1137
|
+
</div>
|
|
1138
|
+
</div>
|
|
1139
|
+
)
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
};
|
|
1144
|
+
```
|
|
1145
|
+
|
|
1146
|
+
## Usage Examples
|
|
1147
|
+
|
|
1148
|
+
Here's how to use these customization settings in your components:
|
|
1149
|
+
|
|
1150
|
+
```tsx
|
|
1151
|
+
import PluginModule, { Component } from '@akinon/next/components/plugin-module';
|
|
1152
|
+
|
|
1153
|
+
// Example 1: E-commerce store with custom product cards
|
|
1154
|
+
<PluginModule
|
|
1155
|
+
component={Component.ProductImageSearchFeature}
|
|
1156
|
+
props={{
|
|
1157
|
+
product,
|
|
1158
|
+
activeIndex,
|
|
1159
|
+
showResetButton: true,
|
|
1160
|
+
settings: customProductSettings
|
|
1161
|
+
}}
|
|
1162
|
+
/>
|
|
1163
|
+
|
|
1164
|
+
// Example 2: Advanced filtering with dynamic icons
|
|
1165
|
+
<PluginModule
|
|
1166
|
+
component={Component.ProductImageSearchFeature}
|
|
1167
|
+
props={{
|
|
1168
|
+
product,
|
|
1169
|
+
activeIndex,
|
|
1170
|
+
settings: advancedFilterSettings
|
|
1171
|
+
}}
|
|
1172
|
+
/>
|
|
1173
|
+
|
|
1174
|
+
// Example 3: Modern pagination experience
|
|
1175
|
+
<PluginModule
|
|
1176
|
+
component={Component.ProductImageSearchFeature}
|
|
1177
|
+
props={{
|
|
1178
|
+
product,
|
|
1179
|
+
activeIndex,
|
|
1180
|
+
settings: modernPaginationSettings
|
|
1181
|
+
}}
|
|
1182
|
+
/>
|
|
1183
|
+
|
|
1184
|
+
// Example 4: Advanced modal with custom controls
|
|
1185
|
+
<PluginModule
|
|
1186
|
+
component={Component.ProductImageSearchFeature}
|
|
1187
|
+
props={{
|
|
1188
|
+
product,
|
|
1189
|
+
activeIndex,
|
|
1190
|
+
settings: advancedModalSettings
|
|
1191
|
+
}}
|
|
1192
|
+
/>
|
|
1193
|
+
|
|
1194
|
+
// Example 5: Complete brand integration
|
|
1195
|
+
<PluginModule
|
|
1196
|
+
component={Component.ProductImageSearchFeature}
|
|
1197
|
+
props={{
|
|
1198
|
+
product,
|
|
1199
|
+
activeIndex,
|
|
1200
|
+
settings: brandThemeSettings
|
|
1201
|
+
}}
|
|
1202
|
+
/>
|
|
1203
|
+
|
|
1204
|
+
// Mix and match approaches
|
|
1205
|
+
const hybridSettings = {
|
|
1206
|
+
// Use brand colors
|
|
1207
|
+
theme: brandThemeSettings.theme,
|
|
1208
|
+
|
|
1209
|
+
// Use modern pagination
|
|
1210
|
+
customRenderers: {
|
|
1211
|
+
render: {
|
|
1212
|
+
resultsGrid: {
|
|
1213
|
+
renderPagination: modernPaginationSettings.customRenderers.render.resultsGrid.renderPagination
|
|
1214
|
+
},
|
|
1215
|
+
modal: {
|
|
1216
|
+
renderHeader: brandThemeSettings.customRenderers.render.modal.renderHeader
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
},
|
|
1220
|
+
|
|
1221
|
+
// Use custom product cards styling
|
|
1222
|
+
customStyles: {
|
|
1223
|
+
...customProductSettings.customStyles,
|
|
1224
|
+
modal: brandThemeSettings.customStyles.modal
|
|
1225
|
+
}
|
|
1226
|
+
};
|
|
1227
|
+
```
|
|
1228
|
+
|
|
1229
|
+
## Complete Styling Reference
|
|
1230
|
+
|
|
1231
|
+
### Available Style Targets (40+ options)
|
|
1232
|
+
|
|
1233
|
+
**Modal & Layout:**
|
|
1234
|
+
|
|
1235
|
+
- `modal`, `activeFiltersContainer`, `controlsContainer`, `imageSearchModal`
|
|
1236
|
+
|
|
1237
|
+
**Filter Sidebar:**
|
|
1238
|
+
|
|
1239
|
+
- `filterSidebar`, `filterSidebarMobileHeader`, `filterGroup`, `filterGroupTitle`, `filterGroupContent`
|
|
1240
|
+
- `filterItem`, `filterItemInput`, `filterItemLabel`, `filterItemCount`
|
|
1241
|
+
- `imageSection`, `imageContainer`, `imageWrapper`, `cropButton`, `tickButton`, `uploadButton`, `resetButton`, `errorMessage`, `cropControls`
|
|
1242
|
+
- `mobileActiveFilters`, `mobileActiveFilterTag`, `mobileClearAllButton`
|
|
1243
|
+
|
|
1244
|
+
**Products & Results:**
|
|
1245
|
+
|
|
1246
|
+
- `resultsGrid`, `resultsContainer`, `gridContainer`
|
|
1247
|
+
- `productItem`, `productImageWrapper`, `productImage`, `productInfo`, `productTitle`, `productPrice`, `productOldPrice`
|
|
1248
|
+
|
|
1249
|
+
**Pagination:**
|
|
1250
|
+
|
|
1251
|
+
- `pagination`, `paginationContainer`, `paginationInfo`, `paginationButton`, `paginationButtonActive`, `paginationButtonDisabled`, `paginationPrevious`, `paginationNext`, `loadMoreButton`, `loadMoreContainer`
|
|
1252
|
+
|
|
1253
|
+
**Controls & Active Filters:**
|
|
1254
|
+
|
|
1255
|
+
- `itemCount`, `sortDropdown`, `filterToggleButton`, `activeFilterTag`, `activeFilterTagButton`
|
|
1256
|
+
|
|
1257
|
+
**Loading & States:**
|
|
1258
|
+
|
|
1259
|
+
- `loadingSpinner`, `loadingOverlay`, `emptyState`, `emptyStateIcon`, `emptyStateText`
|
|
1260
|
+
|
|
1261
|
+
### Available Render Functions (25+ options)
|
|
1262
|
+
|
|
1263
|
+
**Modal Renderers:**
|
|
1264
|
+
|
|
1265
|
+
- `renderModal`, `renderHeader`, `renderCloseButton`, `renderActiveFilters`, `renderActiveFilterTag`
|
|
1266
|
+
- `renderControls`, `renderSortDropdown`, `renderItemCount`, `renderFilterToggleButton`, `renderEmptyState`
|
|
1267
|
+
|
|
1268
|
+
**Filter Sidebar Renderers:**
|
|
1269
|
+
|
|
1270
|
+
- `renderSidebar`, `renderMobileHeader`, `renderImageSection`, `renderImageContainer`
|
|
1271
|
+
- `renderCropButton`, `renderTickButton`, `renderUploadButton`, `renderResetButton`, `renderErrorMessage`
|
|
1272
|
+
- `renderFilterGroup`, `renderFilterGroupTitle`, `renderFilterItem`, `renderFilterItemInput`, `renderFilterItemLabel`, `renderFilterItemCount`
|
|
1273
|
+
- `renderMobileActiveFilters`
|
|
1274
|
+
|
|
1275
|
+
**Results Grid Renderers:**
|
|
1276
|
+
|
|
1277
|
+
- `renderGrid`, `renderGridContainer`, `renderProductItem`, `renderProductImage`, `renderProductInfo`, `renderProductTitle`, `renderProductPrice`
|
|
1278
|
+
- `renderPagination`, `renderPaginationButton`, `renderPaginationInfo`, `renderPaginationPrevious`, `renderPaginationNext`, `renderLoadMore`, `renderLoadMoreButton`
|
|
1279
|
+
- `renderLoadingState`, `renderLoadingOverlay`, `renderEmptyState`
|
|
1280
|
+
|
|
1281
|
+
## Best Practices
|
|
1282
|
+
|
|
1283
|
+
### Performance Optimization
|
|
1284
|
+
|
|
1285
|
+
```tsx
|
|
1286
|
+
const optimizedSettings = {
|
|
1287
|
+
maxFileSize: 5, // Reasonable limit
|
|
1288
|
+
resultsPerPage: 20, // Optimal for performance
|
|
1289
|
+
enableCropping: true, // Better search accuracy
|
|
1290
|
+
|
|
1291
|
+
customStyles: {
|
|
1292
|
+
loadingSpinner: 'text-blue-600', // Brand consistency
|
|
1293
|
+
productItem: 'hover:shadow-lg transition-shadow' // Smooth interactions
|
|
1294
|
+
}
|
|
1295
|
+
};
|
|
1296
|
+
```
|
|
1297
|
+
|
|
1298
|
+
### Accessibility
|
|
1299
|
+
|
|
1300
|
+
```tsx
|
|
1301
|
+
const accessibleSettings = {
|
|
1302
|
+
customRenderers: {
|
|
1303
|
+
render: {
|
|
1304
|
+
modal: {
|
|
1305
|
+
renderHeader: ({ title, onClose }) => (
|
|
1306
|
+
<div className="bg-blue-600 text-white p-6">
|
|
1307
|
+
<h2 className="text-xl font-bold" id="modal-title">
|
|
1308
|
+
{title}
|
|
1309
|
+
</h2>
|
|
1310
|
+
<button
|
|
1311
|
+
onClick={onClose}
|
|
1312
|
+
aria-label="Close modal"
|
|
1313
|
+
className="text-white hover:text-blue-200"
|
|
1314
|
+
>
|
|
1315
|
+
✕
|
|
1316
|
+
</button>
|
|
1317
|
+
</div>
|
|
1318
|
+
)
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
};
|
|
1323
|
+
```
|
|
1324
|
+
|
|
1325
|
+
### Essential Props
|
|
1326
|
+
|
|
1327
|
+
```tsx
|
|
1328
|
+
// Basic integration
|
|
1329
|
+
<PluginModule
|
|
1330
|
+
component={Component.ProductImageSearchFeature}
|
|
1331
|
+
props={{
|
|
1332
|
+
product, // Required: Product object
|
|
1333
|
+
activeIndex, // Current image index
|
|
1334
|
+
showResetButton: true, // Show reset functionality
|
|
1335
|
+
settings: {
|
|
1336
|
+
/* Your customization */
|
|
1337
|
+
}
|
|
1338
|
+
}}
|
|
1339
|
+
/>
|
|
1340
|
+
```
|
|
1341
|
+
|
|
1342
|
+
### Common Settings
|
|
1343
|
+
|
|
1344
|
+
```tsx
|
|
1345
|
+
const commonSettings = {
|
|
1346
|
+
maxFileSize: 5, // 5MB file limit
|
|
1347
|
+
resultsPerPage: 20, // 20 products per page
|
|
1348
|
+
enableCropping: true, // Enable image cropping
|
|
1349
|
+
enableFileUpload: true, // Enable file uploads
|
|
1350
|
+
|
|
1351
|
+
customStyles: {
|
|
1352
|
+
modal: 'max-w-6xl rounded-xl shadow-2xl',
|
|
1353
|
+
productItem: 'rounded-lg shadow-sm hover:shadow-lg'
|
|
1354
|
+
}
|
|
1355
|
+
};
|
|
1356
|
+
```
|
|
1357
|
+
|
|
1358
|
+
## Summary
|
|
1359
|
+
|
|
1360
|
+
### 🚀 **Getting Started in 3 Steps:**
|
|
1361
|
+
|
|
1362
|
+
1. **Install**: `npx @akinon/projectzero@latest --plugins`
|
|
1363
|
+
2. **Integrate**: Add `<PluginModule component={Component.ProductImageSearchFeature} />`
|
|
1364
|
+
3. **Customize**: Use any of the examples above or create your own
|
|
1365
|
+
|
|
1366
|
+
### 🎨 **Customization Approaches:**
|
|
1367
|
+
|
|
1368
|
+
- **Quick Styling**: Use `customStyles` for rapid CSS class customization
|
|
1369
|
+
- **Advanced Control**: Use `customRenderers` for complete component replacement
|
|
1370
|
+
- **Hybrid Approach**: Mix and match both methods for maximum flexibility
|
|
1371
|
+
|
|
1372
|
+
**Made by the ProjectZero team**
|