PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / trunk
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler vtrunk
1.7.0 1.6.6 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 All 50 releases
fluent-cart / app / Services / Reviews / LayoutPresets.php

LayoutPresets.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler trunk, at app/Services/Reviews/LayoutPresets.php

670 lines 31.1 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\Reviews;
4
5 /**
6 * The layouts a review section can be built from.
7 *
8 * A preset has two representations: a Gutenberg template and a builder-neutral
9 * semantic contract. Gutenberg uses the template; other builders use
10 * builderSettings() and create their own native markup.
11 *
12 * Declared here rather than in the block editor's JavaScript so every builder
13 * reads the same IDs, labels, Pro flags and semantic intent.
14 *
15 * Each template is an inner-blocks template — [name, attributes, innerBlocks] —
16 * the shape wp.blocks.createBlocksFromInnerBlocksTemplate() takes.
17 */
18 class LayoutPresets
19 {
20 /**
21 * An attribute map with nothing in it.
22 *
23 * An object, not an empty array: these templates travel to the editor as
24 * JSON, and PHP would write an empty array as [] where the editor expects
25 * {}. The two behave the same once parsed, but the wire format should say
26 * what it means.
27 */
28 protected static function noAttributes()
29 {
30 return (object) [];
31 }
32
33 /**
34 * The review card.
35 *
36 * The groups are not decoration. A field block is block-level, so a flat
37 * row stacks the avatar, the name, the stars and the badge down the card,
38 * each spanning its full width — which is what turns a Verified Purchase
39 * chip into a green bar. The header group puts the avatar beside a meta
40 * column, and the name line puts the name, stars, badge and variation on
41 * one line.
42 */
43 public static function row(array $fields = []): array
44 {
45 $fields = array_merge([
46 'photos' => [],
47 'photosFirst' => false,
48 'showAvatar' => true,
49 'showDate' => true,
50 'showTitle' => true,
51 'showFooter' => true,
52 'showPhotos' => true,
53 'showContent' => true,
54 'showMeta' => true,
55 'photosAfterMeta' => false,
56 'badgeLast' => false,
57 'ratingFirst' => false,
58 'showBadge' => true,
59 'showVariation' => true,
60 'cardClass' => '',
61 ], $fields);
62
63 $badge = ['fluent-cart/review-item-verified-badge'];
64 $stars = ['fluent-cart/review-item-rating'];
65
66 $nameLineChildren = [['fluent-cart/review-item-author-name']];
67
68 if (!$fields['ratingFirst']) {
69 $nameLineChildren[] = $stars;
70 }
71
72 if ($fields['showBadge'] && !$fields['badgeLast']) {
73 $nameLineChildren[] = $badge;
74 }
75
76 if ($fields['showVariation']) {
77 $nameLineChildren[] = ['fluent-cart/review-item-variation-title'];
78 }
79
80 $nameLine = ['core/group', [
81 'className' => 'fct-review-item-name-line',
82 'layout' => ['type' => 'flex', 'flexWrap' => 'wrap'],
83 'style' => ['spacing' => ['blockGap' => ['top' => '4px', 'left' => '8px']]],
84 ], $nameLineChildren];
85
86 $metaTextChildren = [$nameLine];
87
88 if ($fields['showDate']) {
89 $metaTextChildren[] = ['fluent-cart/review-item-date'];
90 }
91
92 $headChildren = [];
93
94 if ($fields['showAvatar']) {
95 $headChildren[] = ['fluent-cart/review-item-avatar'];
96 }
97
98 $headChildren[] = ['core/group', [
99 'className' => 'fct-review-item-meta-text',
100 'layout' => ['type' => 'flex', 'orientation' => 'vertical'],
101 'style' => ['spacing' => ['blockGap' => '2px']],
102 ], $metaTextChildren];
103
104 $head = ['core/group', [
105 'className' => 'fct-review-item-header-left',
106 'layout' => ['type' => 'flex', 'flexWrap' => 'nowrap', 'verticalAlignment' => 'center'],
107 'style' => ['spacing' => ['blockGap' => '10px']],
108 ], $headChildren];
109
110 $media = ['fluent-cart/review-item-photos', $fields['photos'] ?: static::noAttributes()];
111
112 $body = [];
113
114 if ($fields['showTitle']) {
115 $body[] = ['fluent-cart/review-item-title'];
116 }
117
118 if ($fields['showContent']) {
119 $body[] = ['fluent-cart/review-item-content'];
120 }
121
122 // Reply left, votes right. PRO's script looks for this group when it
123 // appends the Helpful buttons, and its space-between is what puts them
124 // on the right.
125 $footer = ['core/group', [
126 'className' => 'fct-review-footer',
127 'layout' => ['type' => 'flex', 'flexWrap' => 'wrap', 'justifyContent' => 'space-between'],
128 'style' => ['spacing' => ['blockGap' => '10px']],
129 ], [
130 ['fluent-cart/review-item-reply'],
131 ['fluent-cart/review-item-votes'],
132 ]];
133
134 $tail = $fields['showFooter'] ? [$footer] : [];
135 $picture = $fields['showPhotos'] ? [$media] : [];
136 // Who wrote it, and how they rated it. Off, the card is whatever is
137 // left — which for a photo strip is the photograph alone.
138 $meta = $fields['showMeta'] ? [$head] : [];
139
140 // Signed off at the end rather than beside the name: on a card the eye
141 // reads downwards, the badge is the last word, not part of the greeting.
142 //
143 // Wrapped, not bare. The badge paints a background and a field block is
144 // block-level, so on its own it stretches the green pill across the
145 // whole card -- the same reason the name line and the footer are groups
146 // rather than loose blocks. A flex parent sizes it to its text.
147 $sign = ($fields['badgeLast'] && $fields['showBadge']) ? [['core/group', [
148 'className' => 'fct-review-item-signoff',
149 'layout' => ['type' => 'flex', 'flexWrap' => 'wrap'],
150 'style' => ['spacing' => ['blockGap' => ['top' => '4px', 'left' => '8px']]],
151 ], [$badge]]] : [];
152
153 // Stars above the words rather than beside the name: on a card the
154 // rating is the headline, and a wall of cards is read by its stars long
155 // before any of its sentences.
156 $lead = $fields['ratingFirst'] ? [$stars] : [];
157
158 // A class on the card, not a preset name read at render time: the same
159 // way this file already names the header row, the footer and the
160 // signoff. Left off entirely when there is none, so the presets that do
161 // not set one keep matching their own templates.
162 $card = $fields['cardClass'] ? ['className' => $fields['cardClass']] : static::noAttributes();
163
164 // Three arrangements. Photographs first is a card the picture sells and
165 // the words explain. Meta first with the photograph under it is the one
166 // a testimonial wants: who is speaking, then their face of the product,
167 // then what they said. Otherwise the words lead and the picture
168 // supports.
169 if ($fields['photosAfterMeta']) {
170 return ['fluent-cart/review-item', $card, array_merge($meta, $picture, $lead, $body, $sign, $tail)];
171 }
172
173 return ['fluent-cart/review-item', $card, $fields['photosFirst']
174 ? array_merge($picture, $lead, $body, $meta, $sign, $tail)
175 : array_merge($meta, $lead, $body, $picture, $sign, $tail)];
176 }
177
178 /**
179 * What sits above the rows. Each control is its own block, so a layout that
180 * does not want one leaves it out — which is the only way to say "no filter
181 * bar", a filter bar being a block rather than a setting.
182 *
183 * The chips and the sort share a line: separately they are two block-level
184 * elements and stack, stranding the sort on a line of its own.
185 */
186 public static function listHeader(array $head = []): array
187 {
188 $head = array_merge(['count' => true, 'filter' => false, 'sorting' => false], $head);
189
190 $controls = [];
191
192 if ($head['filter']) {
193 $controls[] = ['fluent-cart/review-list-filter'];
194 }
195
196 if ($head['sorting']) {
197 $controls[] = ['fluent-cart/review-list-sorting'];
198 }
199
200 $blocks = [];
201
202 if ($head['count']) {
203 $blocks[] = ['fluent-cart/review-list-count'];
204 }
205
206 if ($controls) {
207 $blocks[] = ['core/group', [
208 'className' => 'fct-reviews-controls',
209 'layout' => [
210 'type' => 'flex',
211 'flexWrap' => 'wrap',
212 'justifyContent' => 'space-between',
213 'verticalAlignment' => 'center',
214 ],
215 ], $controls];
216 }
217
218 return $blocks;
219 }
220
221 /**
222 * The whole section.
223 *
224 * 'side' puts the score in a 320px column beside the reviews, which is what
225 * the block scaffolds. 'top' puts it across the full width above them — the
226 * arrangement a shopper meets on the large marketplaces. 'cta' keeps the
227 * column but puts only the invitation to write a review in it.
228 */
229 public static function section(array $args = []): array
230 {
231 $args = array_merge([
232 'list' => [],
233 'head' => [],
234 'pagination' => false,
235 'fields' => [],
236 'summaryOn' => 'side',
237 ], $args);
238
239 $summary = ['fluent-cart/product-review-summary-group', static::noAttributes(), [
240 ['fluent-cart/product-review-summary', static::noAttributes()],
241 ['fluent-cart/write-a-review-button', static::noAttributes()],
242 ]];
243
244 $listChildren = array_merge(
245 static::listHeader($args['head']),
246 [static::row($args['fields'])]
247 );
248
249 if ($args['pagination']) {
250 $listChildren[] = ['fluent-cart/review-list-pagination', ['paginationType' => $args['pagination']]];
251 }
252
253 $reviews = ['fluent-cart/product-review-list', $args['list'] ?: static::noAttributes(), $listChildren];
254
255 if ($args['summaryOn'] === 'top') {
256 return [
257 $summary,
258 ['core/spacer', ['height' => '24px']],
259 $reviews,
260 ];
261 }
262
263 // A strip is meant to sit above a review list rather than replace one,
264 // and that list is where a shopper reads the average and the breakdown
265 // — repeating the score card beside a band of photographs gives the
266 // section two things competing for the eye and leaves the card running
267 // twice the height of the band.
268 if ($args['summaryOn'] === 'cta') {
269 return [
270 ['core/columns', ['style' => ['spacing' => ['blockGap' => '24px']]], [
271 ['core/column', ['width' => '320px'], [
272 ['fluent-cart/product-review-summary-group', static::noAttributes(), [
273 ['fluent-cart/write-a-review-button', static::noAttributes()],
274 ]],
275 ]],
276 ['core/column', static::noAttributes(), [$reviews]],
277 ]],
278 ];
279 }
280
281 return [
282 ['core/columns', ['style' => ['spacing' => ['blockGap' => '24px']]], [
283 ['core/column', ['width' => '320px'], [$summary]],
284 ['core/column', static::noAttributes(), [$reviews]],
285 ]],
286 ];
287 }
288
289 /**
290 * Builder-neutral intent for a preset.
291 *
292 * Gutenberg consumes the block template returned by all(). Other builders
293 * should consume this contract and create their own native markup. The
294 * values describe the review section, rather than naming Gutenberg blocks.
295 *
296 * item.style and item.class are not the same thing and neither is
297 * redundant. The class is core's stylesheet: a builder drawing its rows
298 * with ProductReviewRenderer puts it on the card and gets the card core
299 * draws. The style is the intent behind it -- 'card', 'testimonial',
300 * 'photo-only' -- for a builder that writes its own markup and has its own
301 * stylesheet, which a core class name would tell nothing. Nothing in this
302 * repository reads style today, because both builders here render through
303 * core; it is the field that lets a third stop doing that.
304 *
305 * @return array<string, mixed>
306 */
307 public static function builderSettings(string $preset): array
308 {
309 $settings = [
310 'classic' => [
311 'section' => ['summary' => 'side'],
312 'header' => ['count' => true, 'filters' => true, 'sorting' => true],
313 'list' => ['view_mode' => 'list', 'columns' => 1, 'per_page' => 10, 'pagination' => 'numbers'],
314 'item' => ['style' => 'classic'],
315 ],
316 'minimal' => [
317 'section' => ['summary' => 'side'],
318 'header' => ['count' => false, 'filters' => false, 'sorting' => false],
319 'list' => ['view_mode' => 'list', 'columns' => 1, 'per_page' => 5, 'pagination' => 'numbers'],
320 'item' => ['style' => 'minimal', 'show_date' => false],
321 ],
322 'compact' => [
323 'section' => ['summary' => 'side'],
324 'header' => ['count' => false, 'filters' => false, 'sorting' => false],
325 'list' => ['view_mode' => 'list', 'columns' => 1, 'per_page' => 3, 'pagination' => 'none'],
326 'item' => [
327 'style' => 'compact',
328 'show_avatar' => false,
329 'show_date' => false,
330 'show_title' => false,
331 'show_footer' => false,
332 'show_photos' => false,
333 ],
334 ],
335 'grid' => [
336 'section' => ['summary' => 'side'],
337 'header' => ['count' => true, 'filters' => true, 'sorting' => true],
338 'list' => ['view_mode' => 'grid', 'columns' => 2, 'per_page' => 10, 'pagination' => 'fraction'],
339 'item' => ['style' => 'card'],
340 ],
341 'masonry' => [
342 'section' => ['summary' => 'side'],
343 'header' => ['count' => true, 'filters' => true, 'sorting' => true],
344 'list' => ['view_mode' => 'masonry', 'columns' => 3, 'per_page' => 12, 'pagination' => 'numbers'],
345 'item' => ['style' => 'card'],
346 ],
347 'summary-top' => [
348 'section' => ['summary' => 'top'],
349 'header' => ['count' => true, 'filters' => true, 'sorting' => true],
350 'list' => ['view_mode' => 'list', 'columns' => 1, 'per_page' => 10, 'pagination' => 'numbers'],
351 'item' => ['style' => 'classic'],
352 ],
353 'photo-grid' => [
354 'section' => ['summary' => 'side'],
355 'header' => ['count' => true, 'filters' => true, 'sorting' => false],
356 'list' => [
357 'view_mode' => 'grid', 'columns' => 3, 'per_page' => 6,
358 'pagination' => 'fraction', 'has_media' => true,
359 ],
360 'item' => [
361 'style' => 'photo-grid', 'class' => 'fct-review-photo-grid',
362 'photos_first' => true, 'show_date' => false,
363 'media' => ['visible' => 1, 'full_width' => true, 'flush' => true],
364 ],
365 ],
366 'photo-wall' => [
367 'section' => ['summary' => 'side'],
368 'header' => ['count' => true, 'filters' => false, 'sorting' => false],
369 'list' => [
370 'view_mode' => 'grid', 'columns' => 4, 'per_page' => 8,
371 'pagination' => 'numbers', 'has_media' => true,
372 ],
373 'item' => [
374 'style' => 'photo-wall',
375 'photos_first' => true, 'rating_first' => true,
376 'show_title' => false,
377 'show_content' => false, 'show_footer' => false, 'show_badge' => false,
378 'show_variation' => false,
379 'media' => ['visible' => 1, 'full_width' => true, 'backdrop' => true],
380 ],
381 ],
382 'photo-strip' => [
383 'section' => ['summary' => 'cta'],
384 'header' => ['count' => true, 'filters' => false, 'sorting' => false],
385 'list' => [
386 'view_mode' => 'slider', 'columns' => 6, 'per_page' => 0,
387 'pagination' => 'none', 'has_media' => true,
388 'slider' => [
389 'arrows' => true, 'arrows_size' => 'sm', 'arrows_position' => 'outside',
390 'pagination' => false, 'infinite' => false,
391 ],
392 ],
393 'item' => [
394 'style' => 'photo-only', 'class' => 'fct-review-photo-only',
395 'photos_first' => true, 'show_avatar' => false,
396 'show_date' => false, 'show_title' => false, 'show_content' => false,
397 'show_footer' => false, 'show_badge' => false, 'show_variation' => false,
398 'show_meta' => false,
399 'media' => ['visible' => 1, 'full_width' => true, 'flush' => true],
400 ],
401 ],
402 'carousel' => [
403 'section' => ['summary' => 'side'],
404 'header' => ['count' => true, 'filters' => false, 'sorting' => false],
405 'list' => [
406 'view_mode' => 'slider', 'columns' => 2, 'per_page' => 0,
407 'pagination' => 'none',
408 'slider' => [
409 'arrows' => true, 'arrows_size' => 'md', 'arrows_position' => 'overlap',
410 'pagination' => true, 'pagination_type' => 'bullets', 'infinite' => false,
411 ],
412 ],
413 'item' => ['style' => 'card'],
414 ],
415 'testimonials' => [
416 'section' => ['summary' => 'side'],
417 'header' => ['count' => false, 'filters' => false, 'sorting' => false],
418 'list' => [
419 'view_mode' => 'slider', 'columns' => 3, 'per_page' => 0,
420 'pagination' => 'none', 'sort' => ['by' => 'rating', 'order' => 'DESC'],
421 'slider' => [
422 'arrows' => false, 'arrows_size' => 'md', 'arrows_position' => 'bottom',
423 'pagination' => true, 'pagination_type' => 'bullets',
424 'autoplay' => true, 'autoplay_delay' => 5000, 'infinite' => true,
425 ],
426 ],
427 'item' => [
428 'style' => 'testimonial', 'class' => 'fct-review-testimonial',
429 // With the photographs off this is what puts the byline
430 // under the quote instead of above it: row() places the
431 // header after the words whenever the picture leads,
432 // whether or not there is a picture to lead with.
433 'photos_first' => true,
434 'show_photos' => false, 'show_date' => false,
435 'show_footer' => false, 'show_variation' => false, 'badge_last' => true,
436 ],
437 ],
438 ];
439
440 return $settings[$preset] ?? [];
441 }
442
443 /**
444 * Every layout, and what the picker says about it.
445 *
446 * Only Classic is free. The rest are Pro — the picker marks them and core
447 * refuses their view modes on render, so a locked layout cannot be reached
448 * by editing an attribute by hand.
449 */
450 public static function all(): array
451 {
452 return [
453 // ── The everyday ones ─────────────────────────────────────────
454 'classic' => [
455 'category' => 'list',
456 'pro' => false,
457 'label' => __('Classic', 'fluent-cart'),
458 'help' => __('The complete section: count, filter chips, sorting and numbered pages. What most stores want.', 'fluent-cart'),
459 'template' => static::section([
460 'list' => ['viewMode' => 'list', 'showSortControls' => true, 'perPage' => 10],
461 'head' => ['count' => true, 'filter' => true, 'sorting' => true],
462 'pagination' => 'numbers',
463 ]),
464 ],
465 'minimal' => [
466 'category' => 'list',
467 'pro' => true,
468 'label' => __('Minimal List', 'fluent-cart'),
469 'help' => __('One review per row and nothing above them, for a page that already has plenty going on.', 'fluent-cart'),
470 'template' => static::section([
471 'list' => ['viewMode' => 'list', 'showSortControls' => false, 'perPage' => 5],
472 'head' => ['count' => false],
473 'pagination' => 'numbers',
474 'fields' => ['showDate' => false],
475 ]),
476 ],
477 'compact' => [
478 'category' => 'list',
479 'pro' => true,
480 'label' => __('Compact', 'fluent-cart'),
481 'help' => __('Stars, a couple of lines and a Read more. For somewhere a review is a mention rather than the point.', 'fluent-cart'),
482 'template' => static::section([
483 'list' => ['viewMode' => 'list', 'showSortControls' => false, 'perPage' => 3],
484 'head' => ['count' => false],
485 'fields' => [
486 'showAvatar' => false,
487 'showDate' => false,
488 'showTitle' => false,
489 'showFooter' => false,
490 'showPhotos' => false,
491 ],
492 ]),
493 ],
494
495 // ── Grids ─────────────────────────────────────────────────────
496 'grid' => [
497 'category' => 'grid',
498 'pro' => true,
499 'label' => __('Card Grid', 'fluent-cart'),
500 'help' => __('Two reviews to a row, paged as "Page 2 of 7" — one line however many pages there are.', 'fluent-cart'),
501 'template' => static::section([
502 'list' => ['viewMode' => 'grid', 'gridColumns' => 2, 'showSortControls' => true, 'perPage' => 10],
503 'head' => ['count' => true, 'filter' => true, 'sorting' => true],
504 'pagination' => 'fraction',
505 ]),
506 ],
507 'masonry' => [
508 'category' => 'grid',
509 'pro' => true,
510 'label' => __('Masonry', 'fluent-cart'),
511 'help' => __('Three to a row, each card as tall as its own review rather than stretched to match its neighbours.', 'fluent-cart'),
512 'template' => static::section([
513 'list' => ['viewMode' => 'masonry', 'gridColumns' => 3, 'showSortControls' => true, 'perPage' => 12],
514 'head' => ['count' => true, 'filter' => true, 'sorting' => true],
515 'pagination' => 'numbers',
516 ]),
517 ],
518
519 // ── Summary first, the way the marketplaces do it ─────────────
520 'summary-top' => [
521 'category' => 'list',
522 'pro' => true,
523 'label' => __('Summary on Top', 'fluent-cart'),
524 'help' => __('The average and star breakdown across the top, reviews underneath. The arrangement shoppers already know.', 'fluent-cart'),
525 'template' => static::section([
526 'summaryOn' => 'top',
527 'list' => ['viewMode' => 'list', 'showSortControls' => true, 'perPage' => 10],
528 'head' => ['count' => true, 'filter' => true, 'sorting' => true],
529 'pagination' => 'numbers',
530 ]),
531 ],
532
533 // ── Photographs first ─────────────────────────────────────────
534 'photo-grid' => [
535 'category' => 'photo',
536 'pro' => true,
537 'label' => __('Photo Grid', 'fluent-cart'),
538 'help' => __('Three to a row, the photograph edge to edge across the top of the card and the review under it. Reviews with photographs only.', 'fluent-cart'),
539 'template' => static::section([
540 'list' => ['viewMode' => 'grid', 'gridColumns' => 3, 'hasMedia' => true, 'showSortControls' => false, 'perPage' => 6],
541 'head' => ['count' => true, 'filter' => true],
542 'pagination' => 'fraction',
543 'fields' => [
544 'photosFirst' => true,
545 'showDate' => false,
546 'cardClass' => 'fct-review-photo-grid',
547 'photos' => ['visibleCount' => 1, 'mediaFullWidth' => true, 'mediaHeight' => 0, 'mediaFlush' => true],
548 ],
549 ]),
550 ],
551 'photo-wall' => [
552 'category' => 'photo',
553 'pro' => true,
554 'label' => __('Photo Wall', 'fluent-cart'),
555 'help' => __('The photograph fills its card, with the stars, the name and the date over the foot of it. Reviews with photographs only.', 'fluent-cart'),
556 'template' => static::section([
557 'list' => ['viewMode' => 'grid', 'gridColumns' => 4, 'hasMedia' => true, 'showSortControls' => false, 'perPage' => 8],
558 'head' => ['count' => true],
559 'pagination' => 'numbers',
560 'fields' => [
561 'photosFirst' => true,
562 'ratingFirst' => true,
563 'showTitle' => false,
564 'showContent' => false,
565 'showDate' => true,
566 'showFooter' => false,
567 'showBadge' => false,
568 'showVariation' => false,
569 'photos' => [
570 'visibleCount' => 1,
571 'mediaFullWidth' => true,
572 'mediaHeight' => 0,
573 'mediaBackdrop' => true,
574 ],
575 ],
576 ]),
577 ],
578 'photo-strip' => [
579 'category' => 'photo',
580 'pro' => true,
581 'label' => __('Photo Strip', 'fluent-cart'),
582 'help' => __('A band of customer photographs and nothing else, each opening in the lightbox. Meant to sit above a review list, not replace it.', 'fluent-cart'),
583 'template' => static::section([
584 'list' => [
585 'viewMode' => 'slider',
586 'gridColumns' => 6,
587 'hasMedia' => true,
588 'showSortControls' => false,
589 'perPage' => 0,
590 'sliderSettings' => [
591 'arrows' => 'yes', 'arrowsSize' => 'sm', 'arrowsPosition' => 'outside',
592 'pagination' => 'no', 'paginationType' => 'bullets',
593 'autoplay' => 'no', 'autoplayDelay' => 3000, 'infinite' => 'no',
594 ],
595 ],
596 'summaryOn' => 'cta',
597 'head' => ['count' => true],
598 'fields' => [
599 'photosFirst' => true,
600 'showAvatar' => false,
601 'showDate' => false,
602 'showTitle' => false,
603 'showContent' => false,
604 'showFooter' => false,
605 'showMeta' => false,
606 'photos' => ['visibleCount' => 1, 'mediaFullWidth' => true, 'mediaHeight' => 0, 'mediaFlush' => true],
607 'cardClass' => 'fct-review-photo-only',
608 ],
609 ]),
610 ],
611
612 // ── Sliders ───────────────────────────────────────────────────
613 'carousel' => [
614 'category' => 'carousel',
615 'pro' => true,
616 'label' => __('Carousel', 'fluent-cart'),
617 'help' => __('Two at a time, arrows on the reviews and dots underneath. No pager — the slider is its own.', 'fluent-cart'),
618 'template' => static::section([
619 'list' => [
620 'viewMode' => 'slider',
621 'gridColumns' => 2,
622 'showSortControls' => false,
623 'perPage' => 0,
624 'sliderSettings' => [
625 'arrows' => 'yes', 'arrowsSize' => 'md', 'arrowsPosition' => 'overlap',
626 'pagination' => 'yes', 'paginationType' => 'bullets',
627 'autoplay' => 'no', 'autoplayDelay' => 3000, 'infinite' => 'no',
628 ],
629 ],
630 'head' => ['count' => true],
631 ]),
632 ],
633 'testimonials' => [
634 'category' => 'carousel',
635 'pro' => true,
636 'label' => __('Testimonials', 'fluent-cart'),
637 'help' => __('The quote first, centred under a quotation mark, signed off underneath with the face, the name and the stars. Three at a time, moving on their own, highest rated first.', 'fluent-cart'),
638 'template' => static::section([
639 'list' => [
640 'viewMode' => 'slider',
641 'gridColumns' => 3,
642 'hasMedia' => false,
643 'showSortControls' => false,
644 'perPage' => 0,
645 'defaultSortBy' => 'rating',
646 'defaultSortOrder' => 'DESC',
647 'sliderSettings' => [
648 'arrows' => 'no', 'arrowsSize' => 'md', 'arrowsPosition' => 'bottom',
649 'pagination' => 'yes', 'paginationType' => 'bullets',
650 'autoplay' => 'yes', 'autoplayDelay' => 5000, 'infinite' => 'yes',
651 ],
652 ],
653 'head' => ['count' => false],
654 'fields' => [
655 'photosFirst' => true,
656 'showPhotos' => false,
657 'showTitle' => true,
658 'showAvatar' => true,
659 'showDate' => false,
660 'showFooter' => false,
661 'showVariation' => false,
662 'badgeLast' => true,
663 'cardClass' => 'fct-review-testimonial',
664 ],
665 ]),
666 ],
667 ];
668 }
669 }
670