PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.2
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.2
1.6.5 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 All 48 releases
fluent-cart / app / Services / Renderer / ShopAppRenderer.php

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

712 lines 31.6 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 sort_by from config filters into defaultFilters for AJAX persistence
127 $configFilters = Arr::get($config, 'filters', []);
128 if (is_string($configFilters)) {
129 $configFilters = json_decode($configFilters, true) ?: [];
130 }
131 $shortcodeSortBy = Arr::get($configFilters, 'sort_by', '');
132 if ($shortcodeSortBy) {
133 $this->defaultFilters['sort_by'] = $shortcodeSortBy;
134 if (empty($this->defaultFilters['enabled'])) {
135 $this->defaultFilters['enabled'] = true;
136 }
137 }
138
139 $priceRange = Arr::get($this->customFilters, 'price_range', false);
140 if ($priceRange) {
141 // Accepts true (default "Price" label) or ['label' => '...'] so
142 // integrations can name the price filter the same way the
143 // taxonomies config names taxonomy filters.
144 $this->filters['price_range'] = [
145 "filter_type" => "range",
146 "is_meta" => false,
147 "label" => is_array($priceRange) && !empty($priceRange['label'])
148 ? $priceRange['label']
149 : "Price",
150 "enabled" => true,
151 ];
152 }
153
154
155 if (isset($this->customFilters['taxonomies'])) {
156 // Convert string to array if needed
157 $taxonomies = $this->customFilters['taxonomies'];
158 if (!is_array($taxonomies)) {
159 $taxonomies = array_map('trim', explode(',', $taxonomies));
160 }
161
162 foreach ($taxonomies as $key => $taxonomy) {
163 if (is_array($taxonomy)) {
164 foreach ($taxonomy as $taxonomyKey => $tax) {
165 $this->filters[$taxonomyKey] = [
166 'enabled' => true,
167 'filter_type' => 'options',
168 'is_meta' => true,
169 'label' => Arr::get($tax, 'label'),
170 'multiple' => false,
171 'options' => []
172 ];
173 foreach ($tax['options'] as $option) {
174 $this->filters[$taxonomyKey]['options'][] = [
175 'value' => $option['term_id'],
176 'label' => $option['name'],
177 'parent' => $option['parent'],
178 'children' => []
179 ];
180 }
181 }
182
183 } else {
184 $this->filters[$taxonomy] = [
185 "filter_type" => "options",
186 "is_meta" => true,
187 "label" => ucfirst(str_replace('-', ' ', $taxonomy)),
188 "enabled" => true,
189 "multiple" => false,
190 ];
191 }
192 }
193 }
194
195
196 // Example of $this->filters
197 // $this->filters = [
198 // "product-categories" => [
199 // "filter_type" => "options",
200 // "is_meta" => true,
201 // "label" => "Product Categories",
202 // "enabled" => true,
203 // "multiple" => false
204 // ],
205 // "product-types" => [
206 // "filter_type" => "options",
207 // "is_meta" => true,
208 // "label" => "Product Types",
209 // "enabled" => true,
210 // "multiple" => false
211 // ]
212 // ];
213
214 }
215
216 public function render()
217 {
218 AssetLoader::loadProductArchiveAssets();
219 $isFullWidth = !$this->isFilterEnabled ? ' fct-full-container-width ' : '';
220 $renderer = new \FluentCart\App\Services\Renderer\ProductFilterRender($this->filters);
221
222 $wrapperAttributes = [
223 'class' => 'fct-products-wrapper-inner mode-' . $this->viewMode . $isFullWidth,
224 'data-fluent-cart-product-wrapper-inner' => '',
225 'data-per-page' => $this->per_page,
226 'data-order-type' => $this->order_type,
227 'data-live-filter' => $this->liveFilter,
228 'data-paginator' => $this->paginator,
229 'data-default-filters' => wp_json_encode($this->defaultFilters)
230 ];
231
232 // Shortcode filter data attributes for AJAX persistence
233 $includeIds = Arr::get($this->config, 'include_ids', []);
234 $excludeIds = Arr::get($this->config, 'exclude_ids', []);
235 $productType = Arr::get($this->config, 'product_type', '');
236 $onSale = Arr::get($this->config, 'on_sale', false);
237
238 if (!empty($includeIds)) {
239 $wrapperAttributes['data-include-ids'] = wp_json_encode($includeIds);
240 }
241 if (!empty($excludeIds)) {
242 $wrapperAttributes['data-exclude-ids'] = wp_json_encode($excludeIds);
243 }
244 if (!empty($productType)) {
245 $wrapperAttributes['data-product-type'] = esc_attr($productType);
246 }
247 if ($onSale) {
248 $wrapperAttributes['data-on-sale'] = '1';
249 }
250 // Persist hide_excerpt so AJAX pagination/filtering renders cards the
251 // same way as the initial response (Paginator.js round-trips it).
252 if (Arr::get($this->config, 'hide_excerpt', false)) {
253 $wrapperAttributes['data-hide-excerpt'] = '1';
254 }
255 ?>
256 <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'); ?>">
257 <?php $this->renderViewSwitcher(); ?>
258 <div <?php RenderHelper::renderAtts($wrapperAttributes); ?>>
259 <?php $this->renderFilter($renderer); ?>
260
261 <div class="fct-products-container grid-columns-<?php echo esc_attr($this->productBoxGridSize); ?>"
262 data-fluent-cart-shop-app-product-list
263 role="list"
264 aria-label="<?php esc_attr_e('Product list', 'fluent-cart'); ?>"
265 >
266 <?php
267 if ($this->products->count() !== 0) {
268 $this->renderProduct();
269 } else {
270 ProductRenderer::renderNoProductFound();
271 }
272 ?>
273 </div>
274 </div>
275
276 <?php
277 if ($this->paginator === 'numbers') {
278 $this->renderPaginator();
279 }
280 ?>
281
282 </div>
283 <?php
284 }
285
286 public function renderViewSwitcher()
287 {
288
289 ?>
290 <div class="fct-shop-view-switcher-wrap">
291 <?php $this->renderViewSwitcherButton(); ?>
292 <?php
293 if ($this->isFilterEnabled && $this->enableSortBy) {
294 $this->renderSortByFilter();
295 }
296 ?>
297 </div>
298 <?php
299 }
300
301 public function renderViewSwitcherButton()
302 {
303 ?>
304 <div class="fct-shop-view-switcher">
305 <button type="button" data-fluent-cart-shop-app-grid-view-button=""
306 class="<?php echo $this->viewMode === 'grid' ? 'active' : ''; ?>"
307 title="<?php echo esc_attr__('Grid View', 'fluent-cart'); ?>">
308 <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" fill="none">
309 <path
310 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"
311 stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path>
312 <path d="M13.3335 7L0.66683 7" stroke="currentColor" stroke-linecap="round"></path>
313 <path d="M7 0.666626L7 13.3333" stroke="currentColor" stroke-linecap="round"></path>
314 </svg>
315 </button>
316 <button type="button" data-fluent-cart-shop-app-list-view-button=""
317 class="<?php echo $this->viewMode === 'list' ? 'active' : ''; ?>"
318 title="<?php echo esc_attr__('List View', 'fluent-cart'); ?>">
319 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
320 <path
321 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"
322 stroke="currentColor" stroke-linecap="round"></path>
323 <path
324 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"
325 stroke="currentColor" stroke-linecap="round"></path>
326 <path
327 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"
328 stroke="currentColor" stroke-linecap="round"></path>
329 </svg>
330 </button>
331 </div>
332
333 <?php if ($this->isFilterEnabled) { ?>
334 <button type="button"
335 data-fluent-cart-shop-app-filter-toggle-button=""
336 class="fct-shop-filter-toggle-button hide" title="Toggle List">
337 <span><?php echo esc_html__('Filter', 'fluent-cart'); ?></span>
338 <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
339 <path
340 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"
341 stroke="#2F3448" stroke-width="1.2"></path>
342 <path
343 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"
344 stroke="#2F3448" stroke-width="1.2"></path>
345 <path d="M4 2.5L11 2.5" stroke="#2F3448" stroke-width="1.2" stroke-linecap="round"></path>
346 <path d="M8 9.5L1 9.5" stroke="#2F3448" stroke-width="1.2" stroke-linecap="round"></path>
347 </svg>
348 </button>
349 <?php } ?>
350 <?php
351 }
352
353 public function renderSortByFilter()
354 {
355
356 $currentSort = $this->order_by . '-' . $this->order_type;
357 $dropdownId = 'fct-sort-dropdown-' . uniqid();
358 ?>
359 <div class="fct-shop-sorting-container">
360 <button
361 class="fct-sorting-toggle"
362 data-sort-toggle
363 type="button"
364 aria-expanded="false"
365 aria-controls="<?php echo esc_attr($dropdownId); ?>"
366 aria-haspopup="true">
367 <span class="fct-sorting-label"><?php echo esc_html__('Sort By', 'fluent-cart'); ?></span>
368
369 <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">
370 <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"/>
371 </svg>
372 </button>
373
374 <div
375 class="fct-shop-sorting-dropdown"
376 data-sort-dropdown
377 id="<?php echo esc_attr($dropdownId); ?>"
378 role="menu"
379 aria-label="<?php echo esc_attr__('Sort products', 'fluent-cart'); ?>">
380 <fieldset class="fct-shop-sorting">
381 <legend class="screen-reader-text">
382 <?php echo esc_html__('Sort products by', 'fluent-cart'); ?>
383 </legend>
384
385 <div class="fct-shop-sorting-item" data-sort-item role="none">
386 <label>
387 <input
388 type="radio"
389 name="sort_by"
390 value="name-asc"
391 <?php checked($currentSort, 'name-asc'); ?>
392 aria-label="<?php echo esc_attr__('Sort alphabetically A to Z', 'fluent-cart'); ?>">
393 <span class="fct-sorting-radio" aria-hidden="true"></span>
394 <span class="fct-sorting-radio-label">
395 <?php echo esc_html__('Alphabetical (A to Z)', 'fluent-cart'); ?>
396 </span>
397 </label>
398 </div>
399
400 <div class="fct-shop-sorting-item" data-sort-item role="none">
401 <label>
402 <input
403 type="radio"
404 name="sort_by"
405 value="name-desc"
406 <?php checked($currentSort, 'name-desc'); ?>
407 aria-label="<?php echo esc_attr__('Sort alphabetically Z to A', 'fluent-cart'); ?>">
408 <span class="fct-sorting-radio" aria-hidden="true"></span>
409 <span class="fct-sorting-radio-label">
410 <?php echo esc_html__('Alphabetical (Z to A)', 'fluent-cart'); ?>
411 </span>
412 </label>
413 </div>
414
415 <div class="fct-shop-sorting-item" data-sort-item role="none">
416 <label>
417 <input
418 type="radio"
419 name="sort_by"
420 value="price-low"
421 <?php checked($currentSort, 'price-low'); ?>
422 aria-label="<?php echo esc_attr__('Sort by price low to high', 'fluent-cart'); ?>">
423 <span class="fct-sorting-radio" aria-hidden="true"></span>
424 <span class="fct-sorting-radio-label">
425 <?php echo esc_html__('Price (Low to High)', 'fluent-cart'); ?>
426 </span>
427 </label>
428 </div>
429
430 <div class="fct-shop-sorting-item" data-sort-item role="none">
431 <label>
432 <input
433 type="radio"
434 name="sort_by"
435 value="price-high"
436 <?php checked($currentSort, 'price-high'); ?>
437 aria-label="<?php echo esc_attr__('Sort by price high to low', 'fluent-cart'); ?>">
438 <span class="fct-sorting-radio" aria-hidden="true"></span>
439 <span class="fct-sorting-radio-label">
440 <?php echo esc_html__('Price (High to Low)', 'fluent-cart'); ?>
441 </span>
442 </label>
443 </div>
444
445 <div class="fct-shop-sorting-item" data-sort-item role="none">
446 <label>
447 <input
448 type="radio"
449 name="sort_by"
450 value="date-newest"
451 <?php checked($currentSort, 'date-newest'); ?>
452 aria-label="<?php echo esc_attr__('Sort by date newest first', 'fluent-cart'); ?>">
453 <span class="fct-sorting-radio" aria-hidden="true"></span>
454 <span class="fct-sorting-radio-label">
455 <?php echo esc_html__('Date (Newest First)', 'fluent-cart'); ?>
456 </span>
457 </label>
458 </div>
459
460 <div class="fct-shop-sorting-item" data-sort-item role="none">
461 <label>
462 <input
463 type="radio"
464 name="sort_by"
465 value="date-oldest"
466 <?php checked($currentSort, 'date-oldest'); ?>
467 aria-label="<?php echo esc_attr__('Sort by date oldest first', 'fluent-cart'); ?>">
468 <span class="fct-sorting-radio" aria-hidden="true"></span>
469 <span class="fct-sorting-radio-label">
470 <?php echo esc_html__('Date (Oldest First)', 'fluent-cart'); ?>
471 </span>
472 </label>
473 </div>
474 </fieldset>
475 </div>
476 </div>
477 <?php
478 }
479
480 public function renderFilter($renderer)
481 {
482 if (!$this->isFilterEnabled || !$renderer) {
483 return;
484 }
485 ?>
486 <?php if ($this->isFilterEnabled) : ?>
487 <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'); ?>">
488 <div class="fluent-cart-shop-app-filter-wrapper-inner">
489
490 <form class="fct-shop-filter-form" data-fluent-cart-product-filter-form role="search"
491 aria-label="<?php esc_attr_e('Product filter form', 'fluent-cart'); ?>">
492 <?php
493 if($this->isWildcardFilterEnabled){
494 $renderer->renderSearch();
495 }
496 ?>
497 <?php $renderer->renderOptions(); ?>
498 <?php if (!$this->liveFilter) : ?>
499 <div class="fct-shop-filter-item">
500 <div class="fct-shop-button-group">
501 <button class="fct-shop-apply-filter-button wp-block-fluent-cart-shopapp-product-filter-apply-button">
502 <?php esc_html_e('Apply Filter', 'fluent-cart'); ?>
503 </button>
504 <button class="fct-shop-reset-filter-button wp-block-fluent-cart-shopapp-product-filter-reset-button"
505 data-fluent-cart-shop-app-reset-button="">
506 <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none">
507 <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"
508 stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
509 stroke-linejoin="round"></path>
510 </svg>
511 </button>
512 </div>
513 </div>
514 <?php endif; ?>
515 </form>
516 </div>
517 </div>
518 <?php endif; ?>
519 <?php
520 }
521
522
523 public function renderProduct()
524 {
525 $products = $this->products;
526
527 $cursor = '';
528 if ($products instanceof CursorPaginator) {
529 $cursor = wp_parse_args(wp_parse_url($products->nextPageUrl(), PHP_URL_QUERY));
530 }
531 ?>
532 <?php foreach ($products as $index => $product) {
533 $cursorAttr = '';
534 if ($index === 0) {
535 $cursorAttr = Arr::get($cursor, 'cursor', '');
536 }
537
538 (new \FluentCart\App\Services\Renderer\ProductCardRender($product, ['cursor' => $cursorAttr, 'hide_excerpt' => Arr::get($this->config, 'hide_excerpt', false)]))->render();
539 ?>
540 <?php } ?>
541 <?php
542 }
543
544 public function renderTitle($product = null)
545 {
546 if (!$product || !$product === null) {
547 return '';
548 }
549
550 $render = new \FluentCart\App\Services\Renderer\ProductCardRender($product);
551 ob_start();
552 $render->renderTitle('class="fct-product-card-title"');
553 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
554 echo ob_get_clean();
555 }
556
557 public function renderImage($product = null)
558 {
559 if (!$product || !$product === null) {
560 return '';
561 }
562
563 $render = new \FluentCart\App\Services\Renderer\ProductCardRender($product);
564 ob_start();
565 $render->renderProductImage();
566 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
567 echo ob_get_clean();
568 }
569
570 public function renderPrice($product = null)
571 {
572 if (!$product || !$product === null) {
573 return '';
574 }
575
576 $render = new \FluentCart\App\Services\Renderer\ProductCardRender($product);
577 ob_start();
578 $render->renderPrices('class="fct-product-card-prices"');
579 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
580 echo ob_get_clean();
581 }
582
583 public function renderButton($product = null)
584 {
585 if (!$product || !$product === null) {
586 return '';
587 }
588
589 $render = new \FluentCart\App\Services\Renderer\ProductCardRender($product);
590 ob_start();
591 $render->showBuyButton();
592 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
593 echo ob_get_clean();
594 }
595
596 private function getInitialProducts()
597 {
598 $params = $this->config;
599
600 $products = ShopResource::get($params);
601
602 return ($products['products']->setCollection(
603 $products['products']->getCollection()->transform(function ($product) {
604 $product->setAppends(['view_url', 'has_subscription']);
605 return $product;
606 })
607 ));
608 }
609
610 public function renderPaginator()
611 {
612 $total = $this->total;
613 $lastPage = max((int)ceil($total / $this->per_page), 1);
614 $currentPage = $this->products->currentPage();
615 $from = ($currentPage - 1) * $this->per_page + 1;
616 $to = min($total, $currentPage * $this->per_page);
617 $perPage = $this->products->perPage();
618 ?>
619 <div class="fct-shop-paginator">
620 <?php $this->renderPaginatorResultWrapper(); ?>
621
622 <?php $this->renderPerPageSelector(); ?>
623
624 </div>
625 <?php
626 }
627
628 public function renderPaginatorResultWrapper($atts = '')
629 {
630 $total = $this->total;
631
632 $lastPage = max((int)ceil($total / $this->per_page), 1);
633 $currentPage = $this->products->currentPage();
634 $from = ($currentPage - 1) * $this->per_page + 1;
635 $to = min($total, $currentPage * $this->per_page);
636 $perPage = $this->products->perPage();
637 ?>
638 <div class="fct-shop-paginator-result-wrapper" aria-label="<?php echo esc_attr__('Pagination information', 'fluent-cart'); ?>">
639 <div
640 <?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 ?>
641 role="status"
642 aria-live="polite"
643 aria-atomic="true">
644 <?php
645 printf(
646 /* translators: 1: starting item number, 2: ending item number, 3: total number of items */
647 esc_html__('Showing %1$s to %2$s of %3$s Items', 'fluent-cart'),
648 '<span class="fct-shop-paginator-from" data-fluent-cart-shop-app-paginator-info-pagination-from="">' . esc_html($from) . '</span>',
649 '<span class="fct-shop-paginator-to" data-fluent-cart-shop-app-paginator-info-pagination-to="">' . esc_html($to) . '</span>',
650 '<span class="fct-shop-paginator-total" data-fluent-cart-shop-app-paginator-info-pagination-total="">' . esc_html($total) . '</span>'
651 );
652 ?>
653 </div>
654
655 <div class="fct-shop-per-page-selector">
656 <label for="fct-per-page-select" class="screen-reader-text">
657 <?php echo esc_html__('Items per page', 'fluent-cart'); ?>
658 </label>
659 <select
660 id="fct-per-page-select"
661 name="per_page"
662 data-fluent-cart-shop-app-paginator-per-page-selector=""
663 aria-label="<?php echo esc_attr__('Select number of items per page', 'fluent-cart'); ?>">
664 <option value="10" <?php selected($perPage, 10); ?>>
665 <?php echo esc_html__('10 Per page', 'fluent-cart'); ?>
666 </option>
667 <option value="20" <?php selected($perPage, 20); ?>>
668 <?php echo esc_html__('20 Per page', 'fluent-cart'); ?>
669 </option>
670 <option value="30" <?php selected($perPage, 30); ?>>
671 <?php echo esc_html__('30 Per page', 'fluent-cart'); ?>
672 </option>
673 </select>
674 </div>
675 </div>
676 <?php
677 }
678
679 public function renderPerPageSelector()
680 {
681 $total = $this->total;
682 $lastPage = max((int)ceil($total / $this->per_page), 1);
683 $currentPage = $this->products->currentPage();
684 $from = ($currentPage - 1) * $this->per_page + 1;
685 $to = min($total, $currentPage * $this->per_page);
686 $perPage = $this->products->perPage();
687
688 if ($lastPage > 1) : ?>
689 <ul class="fct-shop-paginator-pager"
690 data-fluent-cart-shop-app-paginator-items-wrapper="">
691 <?php for ($page = 1; $page <= $lastPage; $page++):
692 $isCurrent = $page == $currentPage;
693 $classes = $isCurrent ? 'active' : '';
694 ?>
695 <li class="pager-number <?php echo $page == $currentPage ? 'active' : ''; ?>">
696 <button
697 class="<?php echo esc_attr($classes); ?>"
698 data-fluent-cart-shop-app-paginator-item
699 data-page="<?php echo esc_attr($page); ?>"
700 <?php if ($isCurrent) : ?>aria-current="page"<?php endif; ?>
701 >
702 <?php echo esc_html($page); ?>
703 </button>
704 </li>
705 <?php endfor; ?>
706 </ul>
707 <?php
708 endif;
709 }
710
711 }
712