PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.4
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.4
1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 trunk 1.2.0 All 47 releases
fluent-cart / app / Services / Renderer / ShopAppRenderer.php

ShopAppRenderer.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.6.4, at app/Services/Renderer/ShopAppRenderer.php

721 lines 32.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Services\Renderer;
4
5 use FluentCart\Api\Resource\ShopResource;
6 use FluentCart\Api\Taxonomy;
7 use FluentCart\App\Helpers\Helper;
8 use FluentCart\App\Http\Routes\WebRoutes;
9 use FluentCart\App\Modules\Templating\AssetLoader;
10 use FluentCart\Framework\Pagination\CursorPaginator;
11 use FluentCart\Framework\Support\Arr;
12
13 class ShopAppRenderer
14 {
15 protected $viewMode = 'grid';
16
17 protected $isFilterEnabled = false;
18
19 protected $per_page = 10;
20
21 protected $order_type = 'DESC';
22
23 protected $order_by = 'id';
24
25 protected $liveFilter = true;
26
27 protected $priceFormat = 'starts_from';
28
29 protected $paginator = 'scroll';
30 protected $defaultFilters = [];
31
32 protected $productBoxGridSize = 4;
33
34 protected $config = [];
35
36 protected $filters = [];
37
38 protected $products = [];
39
40 protected $customFilters = [];
41
42 protected $total = 0;
43
44 protected $isWildcardFilterEnabled = false;
45
46 protected $enableSortBy = true;
47
48 public function __construct($products = [], $config = [])
49 {
50
51 $defaultFilters = Arr::get($config, 'default_filters', []);
52 $customFilters = Arr::get($config, 'custom_filters', []);
53 $this->customFilters = $customFilters;
54 $enableFilter = false;
55 if (is_string($customFilters)) {
56 $customFilters = json_decode($customFilters, true);
57 $this->customFilters = $customFilters;
58 }
59 if (!empty($customFilters)) {
60 $enableFilter = true;
61 if (Arr::has($customFilters, 'enabled')) {
62 $enableFilter = Arr::get($customFilters, 'enabled');
63 }
64 }
65 $this->config = $config;
66 $this->viewMode = !empty($config['view_mode']) ? $config['view_mode'] : 'grid';
67 $this->isFilterEnabled = $enableFilter;
68 $this->per_page = Arr::get($config, 'per_page', Arr::get($defaultFilters, 'per_page', 10));
69 $this->order_type = Arr::get($defaultFilters, 'sort_type', 'DESC');
70 $this->order_by = Arr::get($defaultFilters, 'sort_by', 'id');
71 $this->liveFilter = Arr::get($this->customFilters, 'live_filter', true);
72 $this->priceFormat = $config['price_format'] ?? 'starts_from';
73 $this->paginator = Arr::get($config, 'pagination_type', false);
74
75 if ($this->paginator === 'simple') {
76 $this->paginator = 'numbers';
77 } else if ($this->paginator === 'cursor') {
78 $this->paginator = 'scroll';
79 }
80 $this->productBoxGridSize = $config['product_box_grid_size'] ?? 4;
81 $this->isWildcardFilterEnabled = Arr::get($config, 'enable_wildcard_filter', false);
82 $this->enableSortBy = Arr::get($config, 'enable_sort_by', true);
83
84 if (Arr::get($products, 'products', [])) {
85 $this->products = Arr::get($products, 'products', []);
86 } else {
87 $this->products = $products;
88 }
89
90 if($this->products instanceof CursorPaginator){
91 $this->total = 0;
92 }else{
93 $this->total = Arr::get($products, 'total', 0);
94 }
95 //$this->total = $products['total'];
96
97
98 $this->defaultFilters = array_merge($this->defaultFilters, Arr::get($defaultFilters, 'tax_query', []));
99
100 // Merge shortcode taxonomy_filters into defaultFilters so they persist in AJAX pagination
101 $taxonomyFilters = Arr::get($config, 'taxonomy_filters', []);
102 foreach ($taxonomyFilters as $taxonomy => $termIds) {
103 if (!empty($termIds)) {
104 if (!is_array($termIds)) {
105 $termIds = [$termIds];
106 }
107 $existing = Arr::get($this->defaultFilters, $taxonomy, []);
108 if (is_string($existing) && $existing !== '') {
109 $existing = array_map('trim', explode(',', $existing));
110 } elseif (!is_array($existing)) {
111 $existing = [];
112 }
113 $this->defaultFilters[$taxonomy] = array_unique(array_merge($existing, $termIds));
114 }
115 }
116
117 if (!empty($this->defaultFilters)) {
118 $this->defaultFilters['enabled'] = true;
119 }
120
121 // Merge allow_out_of_stock from config into defaultFilters
122 if (Arr::get($config, 'allow_out_of_stock')) {
123 $this->defaultFilters['allow_out_of_stock'] = true;
124 }
125
126 // Merge wildcard search preset into defaultFilters so it persists in AJAX re-fetches
127 $wildcard = Arr::get($defaultFilters, 'wildcard', '');
128 if ($wildcard !== '') {
129 $this->defaultFilters['wildcard'] = $wildcard;
130 if (empty($this->defaultFilters['enabled'])) {
131 $this->defaultFilters['enabled'] = true;
132 }
133 }
134
135 // Merge sort_by from config filters into defaultFilters for AJAX persistence
136 $configFilters = Arr::get($config, 'filters', []);
137 if (is_string($configFilters)) {
138 $configFilters = json_decode($configFilters, true) ?: [];
139 }
140 $shortcodeSortBy = Arr::get($configFilters, 'sort_by', '');
141 if ($shortcodeSortBy) {
142 $this->defaultFilters['sort_by'] = $shortcodeSortBy;
143 if (empty($this->defaultFilters['enabled'])) {
144 $this->defaultFilters['enabled'] = true;
145 }
146 }
147
148 $priceRange = Arr::get($this->customFilters, 'price_range', false);
149 if ($priceRange) {
150 // Accepts true (default "Price" label) or ['label' => '...'] so
151 // integrations can name the price filter the same way the
152 // taxonomies config names taxonomy filters.
153 $this->filters['price_range'] = [
154 "filter_type" => "range",
155 "is_meta" => false,
156 "label" => is_array($priceRange) && !empty($priceRange['label'])
157 ? $priceRange['label']
158 : "Price",
159 "enabled" => true,
160 ];
161 }
162
163
164 if (isset($this->customFilters['taxonomies'])) {
165 // Convert string to array if needed
166 $taxonomies = $this->customFilters['taxonomies'];
167 if (!is_array($taxonomies)) {
168 $taxonomies = array_map('trim', explode(',', $taxonomies));
169 }
170
171 foreach ($taxonomies as $key => $taxonomy) {
172 if (is_array($taxonomy)) {
173 foreach ($taxonomy as $taxonomyKey => $tax) {
174 $this->filters[$taxonomyKey] = [
175 'enabled' => true,
176 'filter_type' => 'options',
177 'is_meta' => true,
178 'label' => Arr::get($tax, 'label'),
179 'multiple' => false,
180 'options' => []
181 ];
182 foreach ($tax['options'] as $option) {
183 $this->filters[$taxonomyKey]['options'][] = [
184 'value' => $option['term_id'],
185 'label' => $option['name'],
186 'parent' => $option['parent'],
187 'children' => []
188 ];
189 }
190 }
191
192 } else {
193 $this->filters[$taxonomy] = [
194 "filter_type" => "options",
195 "is_meta" => true,
196 "label" => ucfirst(str_replace('-', ' ', $taxonomy)),
197 "enabled" => true,
198 "multiple" => false,
199 ];
200 }
201 }
202 }
203
204
205 // Example of $this->filters
206 // $this->filters = [
207 // "product-categories" => [
208 // "filter_type" => "options",
209 // "is_meta" => true,
210 // "label" => "Product Categories",
211 // "enabled" => true,
212 // "multiple" => false
213 // ],
214 // "product-types" => [
215 // "filter_type" => "options",
216 // "is_meta" => true,
217 // "label" => "Product Types",
218 // "enabled" => true,
219 // "multiple" => false
220 // ]
221 // ];
222
223 }
224
225 public function render()
226 {
227 AssetLoader::loadProductArchiveAssets();
228 $isFullWidth = !$this->isFilterEnabled ? ' fct-full-container-width ' : '';
229 $renderer = new \FluentCart\App\Services\Renderer\ProductFilterRender($this->filters);
230
231 $wrapperAttributes = [
232 'class' => 'fct-products-wrapper-inner mode-' . $this->viewMode . $isFullWidth,
233 'data-fluent-cart-product-wrapper-inner' => '',
234 'data-per-page' => $this->per_page,
235 'data-order-type' => $this->order_type,
236 'data-live-filter' => $this->liveFilter,
237 'data-paginator' => $this->paginator,
238 'data-default-filters' => wp_json_encode($this->defaultFilters)
239 ];
240
241 // Shortcode filter data attributes for AJAX persistence
242 $includeIds = Arr::get($this->config, 'include_ids', []);
243 $excludeIds = Arr::get($this->config, 'exclude_ids', []);
244 $productType = Arr::get($this->config, 'product_type', '');
245 $onSale = Arr::get($this->config, 'on_sale', false);
246
247 if (!empty($includeIds)) {
248 $wrapperAttributes['data-include-ids'] = wp_json_encode($includeIds);
249 }
250 if (!empty($excludeIds)) {
251 $wrapperAttributes['data-exclude-ids'] = wp_json_encode($excludeIds);
252 }
253 if (!empty($productType)) {
254 $wrapperAttributes['data-product-type'] = esc_attr($productType);
255 }
256 if ($onSale) {
257 $wrapperAttributes['data-on-sale'] = '1';
258 }
259 // Persist hide_excerpt so AJAX pagination/filtering renders cards the
260 // same way as the initial response (Paginator.js round-trips it).
261 if (Arr::get($this->config, 'hide_excerpt', false)) {
262 $wrapperAttributes['data-hide-excerpt'] = '1';
263 }
264 ?>
265 <div class="fct-products-wrapper" data-fluent-cart-shop-app data-fluent-cart-product-wrapper role="main" aria-label="<?php esc_attr_e('Products', 'fluent-cart'); ?>">
266 <?php $this->renderViewSwitcher(); ?>
267 <div <?php RenderHelper::renderAtts($wrapperAttributes); ?>>
268 <?php $this->renderFilter($renderer); ?>
269
270 <div class="fct-products-container grid-columns-<?php echo esc_attr($this->productBoxGridSize); ?>"
271 data-fluent-cart-shop-app-product-list
272 role="list"
273 aria-label="<?php esc_attr_e('Product list', 'fluent-cart'); ?>"
274 >
275 <?php
276 if ($this->products->count() !== 0) {
277 $this->renderProduct();
278 } else {
279 ProductRenderer::renderNoProductFound();
280 }
281 ?>
282 </div>
283 </div>
284
285 <?php
286 if ($this->paginator === 'numbers') {
287 $this->renderPaginator();
288 }
289 ?>
290
291 </div>
292 <?php
293 }
294
295 public function renderViewSwitcher()
296 {
297
298 ?>
299 <div class="fct-shop-view-switcher-wrap">
300 <?php $this->renderViewSwitcherButton(); ?>
301 <?php
302 if ($this->isFilterEnabled && $this->enableSortBy) {
303 $this->renderSortByFilter();
304 }
305 ?>
306 </div>
307 <?php
308 }
309
310 public function renderViewSwitcherButton()
311 {
312 ?>
313 <div class="fct-shop-view-switcher">
314 <button type="button" data-fluent-cart-shop-app-grid-view-button=""
315 class="<?php echo $this->viewMode === 'grid' ? 'active' : ''; ?>"
316 title="<?php echo esc_attr__('Grid View', 'fluent-cart'); ?>">
317 <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" fill="none">
318 <path
319 d="M12.4059 1.59412C13.3334 2.52162 13.3334 4.0144 13.3334 6.99996C13.3334 9.98552 13.3334 11.4783 12.4059 12.4058C11.4784 13.3333 9.98564 13.3333 7.00008 13.3333C4.01452 13.3333 2.52174 13.3333 1.59424 12.4058C0.666748 11.4783 0.666748 9.98552 0.666748 6.99996C0.666748 4.0144 0.666748 2.52162 1.59424 1.59412C2.52174 0.666626 4.01452 0.666626 7.00008 0.666626C9.98564 0.666626 11.4784 0.666626 12.4059 1.59412Z"
320 stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path>
321 <path d="M13.3335 7L0.66683 7" stroke="currentColor" stroke-linecap="round"></path>
322 <path d="M7 0.666626L7 13.3333" stroke="currentColor" stroke-linecap="round"></path>
323 </svg>
324 </button>
325 <button type="button" data-fluent-cart-shop-app-list-view-button=""
326 class="<?php echo $this->viewMode === 'list' ? 'active' : ''; ?>"
327 title="<?php echo esc_attr__('List View', 'fluent-cart'); ?>">
328 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
329 <path
330 d="M1.33325 7.60008C1.33325 6.8279 1.49441 6.66675 2.26659 6.66675H13.7333C14.5054 6.66675 14.6666 6.8279 14.6666 7.60008V8.40008C14.6666 9.17226 14.5054 9.33341 13.7333 9.33341H2.26659C1.49441 9.33341 1.33325 9.17226 1.33325 8.40008V7.60008Z"
331 stroke="currentColor" stroke-linecap="round"></path>
332 <path
333 d="M1.33325 2.26671C1.33325 1.49453 1.49441 1.33337 2.26659 1.33337H13.7333C14.5054 1.33337 14.6666 1.49453 14.6666 2.26671V3.06671C14.6666 3.83889 14.5054 4.00004 13.7333 4.00004H2.26659C1.49441 4.00004 1.33325 3.83888 1.33325 3.06671V2.26671Z"
334 stroke="currentColor" stroke-linecap="round"></path>
335 <path
336 d="M1.33325 12.9333C1.33325 12.1612 1.49441 12 2.26659 12H13.7333C14.5054 12 14.6666 12.1612 14.6666 12.9333V13.7333C14.6666 14.5055 14.5054 14.6667 13.7333 14.6667H2.26659C1.49441 14.6667 1.33325 14.5055 1.33325 13.7333V12.9333Z"
337 stroke="currentColor" stroke-linecap="round"></path>
338 </svg>
339 </button>
340 </div>
341
342 <?php if ($this->isFilterEnabled) { ?>
343 <button type="button"
344 data-fluent-cart-shop-app-filter-toggle-button=""
345 class="fct-shop-filter-toggle-button hide" title="Toggle List">
346 <span><?php echo esc_html__('Filter', 'fluent-cart'); ?></span>
347 <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
348 <path
349 d="M2.5 4C1.67157 4 1 3.32843 1 2.5C1 1.67157 1.67157 1 2.5 1C3.32843 1 4 1.67157 4 2.5C4 3.32843 3.32843 4 2.5 4Z"
350 stroke="#2F3448" stroke-width="1.2"></path>
351 <path
352 d="M9.5 11C10.3284 11 11 10.3284 11 9.5C11 8.67157 10.3284 8 9.5 8C8.67157 8 8 8.67157 8 9.5C8 10.3284 8.67157 11 9.5 11Z"
353 stroke="#2F3448" stroke-width="1.2"></path>
354 <path d="M4 2.5L11 2.5" stroke="#2F3448" stroke-width="1.2" stroke-linecap="round"></path>
355 <path d="M8 9.5L1 9.5" stroke="#2F3448" stroke-width="1.2" stroke-linecap="round"></path>
356 </svg>
357 </button>
358 <?php } ?>
359 <?php
360 }
361
362 public function renderSortByFilter()
363 {
364
365 $currentSort = $this->order_by . '-' . $this->order_type;
366 $dropdownId = 'fct-sort-dropdown-' . uniqid();
367 ?>
368 <div class="fct-shop-sorting-container">
369 <button
370 class="fct-sorting-toggle"
371 data-sort-toggle
372 type="button"
373 aria-expanded="false"
374 aria-controls="<?php echo esc_attr($dropdownId); ?>"
375 aria-haspopup="true">
376 <span class="fct-sorting-label"><?php echo esc_html__('Sort By', 'fluent-cart'); ?></span>
377
378 <svg class="fct-sorting-arrow" xmlns="http://www.w3.org/2000/svg" width="14" height="8" viewBox="0 0 14 8" fill="none" aria-hidden="true" focusable="false">
379 <path d="M1.5 1.25L6.29289 6.04289C6.62623 6.37623 6.79289 6.54289 7 6.54289C7.20711 6.54289 7.37377 6.37623 7.70711 6.04289L12.5 1.25" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
380 </svg>
381 </button>
382
383 <div
384 class="fct-shop-sorting-dropdown"
385 data-sort-dropdown
386 id="<?php echo esc_attr($dropdownId); ?>"
387 role="menu"
388 aria-label="<?php echo esc_attr__('Sort products', 'fluent-cart'); ?>">
389 <fieldset class="fct-shop-sorting">
390 <legend class="screen-reader-text">
391 <?php echo esc_html__('Sort products by', 'fluent-cart'); ?>
392 </legend>
393
394 <div class="fct-shop-sorting-item" data-sort-item role="none">
395 <label>
396 <input
397 type="radio"
398 name="sort_by"
399 value="name-asc"
400 <?php checked($currentSort, 'name-asc'); ?>
401 aria-label="<?php echo esc_attr__('Sort alphabetically A to Z', 'fluent-cart'); ?>">
402 <span class="fct-sorting-radio" aria-hidden="true"></span>
403 <span class="fct-sorting-radio-label">
404 <?php echo esc_html__('Alphabetical (A to Z)', 'fluent-cart'); ?>
405 </span>
406 </label>
407 </div>
408
409 <div class="fct-shop-sorting-item" data-sort-item role="none">
410 <label>
411 <input
412 type="radio"
413 name="sort_by"
414 value="name-desc"
415 <?php checked($currentSort, 'name-desc'); ?>
416 aria-label="<?php echo esc_attr__('Sort alphabetically Z to A', 'fluent-cart'); ?>">
417 <span class="fct-sorting-radio" aria-hidden="true"></span>
418 <span class="fct-sorting-radio-label">
419 <?php echo esc_html__('Alphabetical (Z to A)', 'fluent-cart'); ?>
420 </span>
421 </label>
422 </div>
423
424 <div class="fct-shop-sorting-item" data-sort-item role="none">
425 <label>
426 <input
427 type="radio"
428 name="sort_by"
429 value="price-low"
430 <?php checked($currentSort, 'price-low'); ?>
431 aria-label="<?php echo esc_attr__('Sort by price low to high', 'fluent-cart'); ?>">
432 <span class="fct-sorting-radio" aria-hidden="true"></span>
433 <span class="fct-sorting-radio-label">
434 <?php echo esc_html__('Price (Low to High)', 'fluent-cart'); ?>
435 </span>
436 </label>
437 </div>
438
439 <div class="fct-shop-sorting-item" data-sort-item role="none">
440 <label>
441 <input
442 type="radio"
443 name="sort_by"
444 value="price-high"
445 <?php checked($currentSort, 'price-high'); ?>
446 aria-label="<?php echo esc_attr__('Sort by price high to low', 'fluent-cart'); ?>">
447 <span class="fct-sorting-radio" aria-hidden="true"></span>
448 <span class="fct-sorting-radio-label">
449 <?php echo esc_html__('Price (High to Low)', 'fluent-cart'); ?>
450 </span>
451 </label>
452 </div>
453
454 <div class="fct-shop-sorting-item" data-sort-item role="none">
455 <label>
456 <input
457 type="radio"
458 name="sort_by"
459 value="date-newest"
460 <?php checked($currentSort, 'date-newest'); ?>
461 aria-label="<?php echo esc_attr__('Sort by date newest first', 'fluent-cart'); ?>">
462 <span class="fct-sorting-radio" aria-hidden="true"></span>
463 <span class="fct-sorting-radio-label">
464 <?php echo esc_html__('Date (Newest First)', 'fluent-cart'); ?>
465 </span>
466 </label>
467 </div>
468
469 <div class="fct-shop-sorting-item" data-sort-item role="none">
470 <label>
471 <input
472 type="radio"
473 name="sort_by"
474 value="date-oldest"
475 <?php checked($currentSort, 'date-oldest'); ?>
476 aria-label="<?php echo esc_attr__('Sort by date oldest first', 'fluent-cart'); ?>">
477 <span class="fct-sorting-radio" aria-hidden="true"></span>
478 <span class="fct-sorting-radio-label">
479 <?php echo esc_html__('Date (Oldest First)', 'fluent-cart'); ?>
480 </span>
481 </label>
482 </div>
483 </fieldset>
484 </div>
485 </div>
486 <?php
487 }
488
489 public function renderFilter($renderer)
490 {
491 if (!$this->isFilterEnabled || !$renderer) {
492 return;
493 }
494 ?>
495 <?php if ($this->isFilterEnabled) : ?>
496 <div class="fct-shop-filter-wrapper fluent-cart-shop-app-filter-wrapper" data-fluent-cart-shop-app-filter-wrapper role="search" aria-label="<?php esc_attr_e('Product filters', 'fluent-cart'); ?>">
497 <div class="fluent-cart-shop-app-filter-wrapper-inner">
498
499 <form class="fct-shop-filter-form" data-fluent-cart-product-filter-form role="search"
500 aria-label="<?php esc_attr_e('Product filter form', 'fluent-cart'); ?>">
501 <?php
502 if($this->isWildcardFilterEnabled){
503 $renderer->renderSearch();
504 }
505 ?>
506 <?php $renderer->renderOptions(); ?>
507 <?php if (!$this->liveFilter) : ?>
508 <div class="fct-shop-filter-item">
509 <div class="fct-shop-button-group">
510 <button class="fct-shop-apply-filter-button wp-block-fluent-cart-shopapp-product-filter-apply-button">
511 <?php esc_html_e('Apply Filter', 'fluent-cart'); ?>
512 </button>
513 <button class="fct-shop-reset-filter-button wp-block-fluent-cart-shopapp-product-filter-reset-button"
514 data-fluent-cart-shop-app-reset-button="">
515 <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none">
516 <path d="M14.2501 9.75V15.75M9.75012 9.75V15.75M20.2498 5.25L3.74976 5.25001M18.7501 5.25V19.5C18.7501 19.6989 18.6711 19.8897 18.5305 20.0303C18.3898 20.171 18.199 20.25 18.0001 20.25H6.00012C5.80121 20.25 5.61044 20.171 5.46979 20.0303C5.32914 19.8897 5.25012 19.6989 5.25012 19.5V5.25M15.7501 5.25V3.75C15.7501 3.35218 15.5921 2.97064 15.3108 2.68934C15.0295 2.40804 14.6479 2.25 14.2501 2.25H9.75012C9.3523 2.25 8.97077 2.40804 8.68946 2.68934C8.40816 2.97064 8.25012 3.35218 8.25012 3.75V5.25"
517 stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
518 stroke-linejoin="round"></path>
519 </svg>
520 </button>
521 </div>
522 </div>
523 <?php endif; ?>
524 </form>
525 </div>
526 </div>
527 <?php endif; ?>
528 <?php
529 }
530
531
532 public function renderProduct()
533 {
534 $products = $this->products;
535
536 $cursor = '';
537 if ($products instanceof CursorPaginator) {
538 $cursor = wp_parse_args(wp_parse_url($products->nextPageUrl(), PHP_URL_QUERY));
539 }
540 ?>
541 <?php foreach ($products as $index => $product) {
542 $cursorAttr = '';
543 if ($index === 0) {
544 $cursorAttr = Arr::get($cursor, 'cursor', '');
545 }
546
547 (new \FluentCart\App\Services\Renderer\ProductCardRender($product, ['cursor' => $cursorAttr, 'hide_excerpt' => Arr::get($this->config, 'hide_excerpt', false)]))->render();
548 ?>
549 <?php } ?>
550 <?php
551 }
552
553 public function renderTitle($product = null)
554 {
555 if (!$product || !$product === null) {
556 return '';
557 }
558
559 $render = new \FluentCart\App\Services\Renderer\ProductCardRender($product);
560 ob_start();
561 $render->renderTitle('class="fct-product-card-title"');
562 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
563 echo ob_get_clean();
564 }
565
566 public function renderImage($product = null)
567 {
568 if (!$product || !$product === null) {
569 return '';
570 }
571
572 $render = new \FluentCart\App\Services\Renderer\ProductCardRender($product);
573 ob_start();
574 $render->renderProductImage();
575 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
576 echo ob_get_clean();
577 }
578
579 public function renderPrice($product = null)
580 {
581 if (!$product || !$product === null) {
582 return '';
583 }
584
585 $render = new \FluentCart\App\Services\Renderer\ProductCardRender($product);
586 ob_start();
587 $render->renderPrices('class="fct-product-card-prices"');
588 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
589 echo ob_get_clean();
590 }
591
592 public function renderButton($product = null)
593 {
594 if (!$product || !$product === null) {
595 return '';
596 }
597
598 $render = new \FluentCart\App\Services\Renderer\ProductCardRender($product);
599 ob_start();
600 $render->showBuyButton();
601 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
602 echo ob_get_clean();
603 }
604
605 private function getInitialProducts()
606 {
607 $params = $this->config;
608
609 $products = ShopResource::get($params);
610
611 return ($products['products']->setCollection(
612 $products['products']->getCollection()->transform(function ($product) {
613 $product->setAppends(['view_url', 'has_subscription']);
614 return $product;
615 })
616 ));
617 }
618
619 public function renderPaginator()
620 {
621 $total = $this->total;
622 $lastPage = max((int)ceil($total / $this->per_page), 1);
623 $currentPage = $this->products->currentPage();
624 $from = ($currentPage - 1) * $this->per_page + 1;
625 $to = min($total, $currentPage * $this->per_page);
626 $perPage = $this->products->perPage();
627 ?>
628 <div class="fct-shop-paginator">
629 <?php $this->renderPaginatorResultWrapper(); ?>
630
631 <?php $this->renderPerPageSelector(); ?>
632
633 </div>
634 <?php
635 }
636
637 public function renderPaginatorResultWrapper($atts = '')
638 {
639 $total = $this->total;
640
641 $lastPage = max((int)ceil($total / $this->per_page), 1);
642 $currentPage = $this->products->currentPage();
643 $from = ($currentPage - 1) * $this->per_page + 1;
644 $to = min($total, $currentPage * $this->per_page);
645 $perPage = $this->products->perPage();
646 ?>
647 <div class="fct-shop-paginator-result-wrapper" aria-label="<?php echo esc_attr__('Pagination information', 'fluent-cart'); ?>">
648 <div
649 <?php echo !empty($atts) ? $atts : 'class="fct-shop-paginator-results wc-block-grid__fluent-cart-shop-app-paginator-results wp-block-fluent-cart-product-paginator-info"'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
650 role="status"
651 aria-live="polite"
652 aria-atomic="true">
653 <?php
654 printf(
655 /* translators: 1: starting item number, 2: ending item number, 3: total number of items */
656 esc_html__('Showing %1$s to %2$s of %3$s Items', 'fluent-cart'),
657 '<span class="fct-shop-paginator-from" data-fluent-cart-shop-app-paginator-info-pagination-from="">' . esc_html($from) . '</span>',
658 '<span class="fct-shop-paginator-to" data-fluent-cart-shop-app-paginator-info-pagination-to="">' . esc_html($to) . '</span>',
659 '<span class="fct-shop-paginator-total" data-fluent-cart-shop-app-paginator-info-pagination-total="">' . esc_html($total) . '</span>'
660 );
661 ?>
662 </div>
663
664 <div class="fct-shop-per-page-selector">
665 <label for="fct-per-page-select" class="screen-reader-text">
666 <?php echo esc_html__('Items per page', 'fluent-cart'); ?>
667 </label>
668 <select
669 id="fct-per-page-select"
670 name="per_page"
671 data-fluent-cart-shop-app-paginator-per-page-selector=""
672 aria-label="<?php echo esc_attr__('Select number of items per page', 'fluent-cart'); ?>">
673 <option value="10" <?php selected($perPage, 10); ?>>
674 <?php echo esc_html__('10 Per page', 'fluent-cart'); ?>
675 </option>
676 <option value="20" <?php selected($perPage, 20); ?>>
677 <?php echo esc_html__('20 Per page', 'fluent-cart'); ?>
678 </option>
679 <option value="30" <?php selected($perPage, 30); ?>>
680 <?php echo esc_html__('30 Per page', 'fluent-cart'); ?>
681 </option>
682 </select>
683 </div>
684 </div>
685 <?php
686 }
687
688 public function renderPerPageSelector()
689 {
690 $total = $this->total;
691 $lastPage = max((int)ceil($total / $this->per_page), 1);
692 $currentPage = $this->products->currentPage();
693 $from = ($currentPage - 1) * $this->per_page + 1;
694 $to = min($total, $currentPage * $this->per_page);
695 $perPage = $this->products->perPage();
696
697 if ($lastPage > 1) : ?>
698 <ul class="fct-shop-paginator-pager"
699 data-fluent-cart-shop-app-paginator-items-wrapper="">
700 <?php for ($page = 1; $page <= $lastPage; $page++):
701 $isCurrent = $page == $currentPage;
702 $classes = $isCurrent ? 'active' : '';
703 ?>
704 <li class="pager-number <?php echo $page == $currentPage ? 'active' : ''; ?>">
705 <button
706 class="<?php echo esc_attr($classes); ?>"
707 data-fluent-cart-shop-app-paginator-item
708 data-page="<?php echo esc_attr($page); ?>"
709 <?php if ($isCurrent) : ?>aria-current="page"<?php endif; ?>
710 >
711 <?php echo esc_html($page); ?>
712 </button>
713 </li>
714 <?php endfor; ?>
715 </ul>
716 <?php
717 endif;
718 }
719
720 }
721