PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.3.27
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.3.27
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 / Modules / Templating / Bricks / Elements / ProductsCollection.php

ProductsCollection.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.3.27, at app/Modules/Templating/Bricks/Elements/ProductsCollection.php

774 lines 25.7 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\Modules\Templating\Bricks\Elements;
4
5 use Bricks\Custom_Render_Element;
6 use Bricks\Helpers;
7 use Bricks\Query;
8 use FluentCart\App\Modules\Data\ProductDataSetup;
9 use FluentCart\App\Modules\Data\ProductQuery;
10 use FluentCart\App\Modules\Templating\AssetLoader;
11 use FluentCart\App\Modules\Templating\Bricks\BricksHelper;
12 use FluentCart\App\Services\Renderer\RenderHelper;
13 use FluentCart\App\Services\Renderer\ShopAppRenderer;
14 use FluentCart\Framework\Support\Arr;
15 use FluentCart\App\Services\Renderer\ProductFilterRender;
16
17 if (!defined('ABSPATH')) exit; // Exit if accessed directly
18
19 class ProductsCollection extends Custom_Render_Element
20 {
21 public $category = 'fluentcart';
22 public $name = 'fct-products';
23 public $icon = 'ti-archive';
24
25 protected $cssRoot = '.fct-products-wrapper-inner .fct-products-container';
26
27 public function enqueue_scripts()
28 {
29 AssetLoader::loadProductArchiveAssets();
30 }
31
32 public function get_label()
33 {
34 return esc_html__('Products', 'fluent-cart');
35 }
36
37 public function set_control_groups()
38 {
39 $this->control_groups['query'] = [
40 'title' => esc_html__('Query', 'fluent-cart'),
41 'tab' => 'content',
42 ];
43
44 $this->control_groups['fields'] = [
45 'title' => esc_html__('Fields', 'fluent-cart'),
46 'tab' => 'content',
47 ];
48
49 $this->control_groups['display'] = [
50 'title' => esc_html__('Display', 'fluent-cart'),
51 'tab' => 'content',
52 ];
53
54 $this->control_groups['filter'] = [
55 'title' => esc_html__('Filter', 'fluent-cart'),
56 'tab' => 'content',
57 ];
58
59 $this->control_groups['widgets'] = [
60 'title' => esc_html__('Widgets', 'fluent-cart'),
61 'tab' => 'widgets',
62 ];
63 }
64
65 public function set_controls()
66 {
67 // LAYOUT
68 $this->controls['viewMode'] = [
69 'tab' => 'content',
70 'group' => 'display',
71 'label' => esc_html__('View Mode', 'fluent-cart'),
72 'type' => 'select',
73 'options' => [
74 'grid' => esc_html__('Grid', 'fluent-cart'),
75 'list' => esc_html__('List', 'fluent-cart'),
76 ],
77 'placeholder' => esc_html__('Grid', 'fluent-cart'),
78 'rerender' => true,
79 ];
80
81 $this->controls['showViewSwitcher'] = [
82 'tab' => 'content',
83 'group' => 'display',
84 'label' => esc_html__('Show View Switcher', 'fluent-cart'),
85 'type' => 'checkbox',
86 'inline' => true,
87 'default' => true,
88 ];
89
90 $this->controls['paginationType'] = [
91 'tab' => 'content',
92 'group' => 'display',
93 'label' => esc_html__('Pagination Type', 'fluent-cart'),
94 'type' => 'select',
95 'options' => [
96 'scroll' => esc_html__('Scroll', 'fluent-cart'),
97 'numbers' => esc_html__('Numbers', 'fluent-cart'),
98 ],
99 'placeholder' => esc_html__('Numbers', 'fluent-cart'),
100 ];
101
102 $this->controls['columns'] = [
103 'tab' => 'content',
104 'label' => esc_html__('Columns', 'fluent-cart'),
105 'type' => 'number',
106 'min' => 1,
107 'max' => 5,
108 'breakpoints' => true,
109 'placeholder' => 4,
110 'rerender' => true,
111 ];
112
113 $this->controls['gap'] = [
114 'tab' => 'content',
115 'label' => esc_html__('Gap', 'fluent-cart'),
116 'type' => 'number',
117 'units' => true,
118 'css' => [
119 [
120 'selector' => $this->cssRoot,
121 'property' => 'gap',
122 ],
123 ],
124 'placeholder' => 30,
125 ];
126
127 $this->controls['posts_per_page'] = [
128 'tab' => 'content',
129 'label' => esc_html__('Products per page', 'fluent-cart'),
130 'type' => 'number',
131 'min' => 1,
132 'max' => 100,
133 'placeholder' => 10,
134 'step' => 1,
135 ];
136
137 $this->controls['is_main_query'] = [
138 'tab' => 'content',
139 'label' => esc_html__('Is main query', 'fluent-cart'),
140 'type' => 'checkbox',
141 'inline' => true,
142 ];
143
144 // QUERY
145 $this->controls['orderby'] = [
146 'tab' => 'content',
147 'label' => esc_html__('Order by', 'fluent-cart'),
148 'type' => 'select',
149 // id|date|title|price
150 'options' => [
151 'price' => esc_html__('Price', 'fluent-cart'),
152 'title' => esc_html__('Product Name', 'fluent-cart'),
153 'date' => esc_html__('Published date', 'fluent-cart'),
154 'id' => esc_html__('Product ID', 'fluent-cart')
155 ],
156 'inline' => true,
157 'placeholder' => esc_html__('Default', 'fluent-cart'),
158 ];
159
160 $this->controls['order'] = [
161 'tab' => 'content',
162 'label' => esc_html__('Order', 'fluent-cart'),
163 'type' => 'select',
164 'options' => [
165 'ASC' => esc_html__('Ascending', 'fluent-cart'),
166 'DESC' => esc_html__('Descending', 'fluent-cart'),
167 ],
168 'inline' => true,
169 'placeholder' => esc_html__('Descending', 'fluent-cart'),
170 ];
171
172 $this->controls['main_query_info'] = [
173 'tab' => 'content',
174 'group' => 'query',
175 'type' => 'info',
176 'content' => esc_html__('The query settings will be ignored when Is main query is enabled.', 'fluent-cart'),
177 ];
178
179 $this->controls['productType'] = [
180 'tab' => 'content',
181 'group' => 'query',
182 'label' => esc_html__('Product type', 'fluent-cart'),
183 'type' => 'select',
184 // physical|digital|subscription|onetime|simple|variations
185 'options' => [
186 'simple' => esc_html__('Simple', 'fluent-cart'),
187 'physical' => esc_html__('Physical', 'fluent-cart'),
188 'digital' => esc_html__('Digital', 'fluent-cart'),
189 'variations' => esc_html__('Variations', 'fluent-cart'),
190 'ontime' => esc_html__('One-time', 'fluent-cart'),
191 'subscription' => esc_html__('Subscriptions', 'fluent-cart'),
192 ],
193 'multiple' => false,
194 'placeholder' => esc_html__('all product types', 'fluent-cart'),
195 ];
196
197 $this->controls['include'] = [
198 'tab' => 'content',
199 'group' => 'query',
200 'label' => esc_html__('Include', 'fluent-cart'),
201 'type' => 'select',
202 'optionsAjax' => [
203 'action' => 'bricks_get_posts',
204 'postType' => 'fluent-products',
205 ],
206 'multiple' => true,
207 'searchable' => true,
208 'placeholder' => esc_html__('Select products', 'fluent-cart'),
209 ];
210
211 //phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude
212 $this->controls['exclude'] = [
213 'tab' => 'content',
214 'group' => 'query',
215 'label' => esc_html__('Exclude', 'fluent-cart'),
216 'type' => 'select',
217 'optionsAjax' => [
218 'action' => 'bricks_get_posts',
219 'postType' => 'fluent-products',
220 ],
221 'multiple' => true,
222 'searchable' => true,
223 'placeholder' => esc_html__('Select products', 'fluent-cart'),
224 ];
225
226 $this->controls['categories'] = [
227 'tab' => 'content',
228 'group' => 'query',
229 'label' => esc_html__('Product categories', 'fluent-cart'),
230 'type' => 'select',
231 'options' => BricksHelper::getCategoriesOptions(),
232 'multiple' => true,
233 ];
234
235 $this->controls['onSale'] = [
236 'tab' => 'content',
237 'group' => 'query',
238 'label' => esc_html__('On sale Products only', 'fluent-cart'),
239 'type' => 'checkbox',
240 ];
241
242 $this->controls['allowOutOfStock'] = [
243 'tab' => 'content',
244 'group' => 'query',
245 'label' => esc_html__('Allow Out Of Stock', 'fluent-cart'),
246 'type' => 'checkbox',
247 ];
248
249 // FILTER
250 $this->controls['enableFilter'] = [
251 'tab' => 'content',
252 'group' => 'filter',
253 'label' => esc_html__('Enable Filter', 'fluent-cart'),
254 'type' => 'checkbox',
255 'inline' => true,
256 ];
257
258 $this->controls['liveFilter'] = [
259 'tab' => 'content',
260 'group' => 'filter',
261 'label' => esc_html__('Live Filter', 'fluent-cart'),
262 'type' => 'checkbox',
263 'inline' => true,
264 'required' => ['enableFilter', '=', true],
265 ];
266
267 $this->controls['wildcardFilter'] = [
268 'tab' => 'content',
269 'group' => 'filter',
270 'label' => esc_html__('Wildcard Filter', 'fluent-cart'),
271 'type' => 'checkbox',
272 'inline' => true,
273 'required' => ['enableFilter', '=', true],
274 ];
275
276 $this->controls['productCategories'] = [
277 'tab' => 'content',
278 'group' => 'filter',
279 'label' => esc_html__( 'Product Categories', 'fluent-cart' ),
280 'type' => 'checkbox',
281 'inline' => true,
282 'required' => ['enableFilter', '=', true],
283 ];
284
285 $this->controls['displayNameCategories'] = [
286 'tab' => 'content',
287 'group' => 'filter',
288 'label' => esc_html__('Display Name', 'fluent-cart'),
289 'type' => 'text',
290 'placeholder' => esc_html__('Custom filter label', 'fluent-cart'),
291 'required' => [
292 ['enableFilter', '=', true],
293 ['productCategories', '=', true],
294 ],
295 ];
296
297 $this->controls['productBrands'] = [
298 'tab' => 'content',
299 'group' => 'filter',
300 'label' => esc_html__( 'Product Brands', 'fluent-cart' ),
301 'type' => 'checkbox',
302 'inline' => true,
303 'required' => ['enableFilter', '=', true],
304 ];
305
306 $this->controls['displayNameBrands'] = [
307 'tab' => 'content',
308 'group' => 'filter',
309 'label' => esc_html__('Display Name', 'fluent-cart'),
310 'type' => 'text',
311 'placeholder' => esc_html__('Custom filter label', 'fluent-cart'),
312 'required' => [
313 ['enableFilter', '=', true],
314 ['productBrands', '=', true],
315 ],
316 ];
317
318 $this->controls['priceRange'] = [
319 'tab' => 'content',
320 'group' => 'filter',
321 'label' => esc_html__( 'Price Range', 'fluent-cart' ),
322 'type' => 'checkbox',
323 'inline' => true,
324 'required' => ['enableFilter', '=', true],
325 ];
326
327 $this->controls['displayNamePriceRange'] = [
328 'tab' => 'content',
329 'group' => 'filter',
330 'label' => esc_html__('Display Name', 'fluent-cart'),
331 'type' => 'text',
332 'placeholder' => esc_html__('Custom filter label', 'fluent-cart'),
333 'required' => [
334 ['enableFilter', '=', true],
335 ['priceRange', '=', true],
336 ],
337 ];
338
339 // FIELDS
340 $fields = $this->get_post_fields();
341
342 // Remove field settings
343 unset($fields['fields']['fields']['overlay']);
344 unset($fields['fields']['fields']['dynamicPadding']);
345 unset($fields['fields']['fields']['dynamicBackground']);
346 unset($fields['fields']['fields']['dynamicBorder']);
347
348 // Set fields defaults fields set
349 $fields['fields']['default'] = [
350 [
351 'dynamicData' => '{fct_product_image:link}',
352 'id' => Helpers::generate_random_id(false),
353 ],
354 [
355 'dynamicData' => '{fct_product_title:linked}',
356 'tag' => 'h5',
357 'id' => Helpers::generate_random_id(false),
358 ],
359 [
360 'dynamicData' => '{fct_product_excerpt}',
361 'tag' => 'p',
362 'id' => Helpers::generate_random_id(false),
363 ],
364 [
365 'dynamicData' => '{fct_product_price}',
366 'id' => Helpers::generate_random_id(false),
367 ],
368 [
369 'dynamicData' => '{fct_product_button}',
370 'id' => Helpers::generate_random_id(false),
371 ]
372 ];
373
374 $this->controls = array_replace_recursive($this->controls, $fields);
375
376 $this->controls['linkProduct'] = [
377 'tab' => 'content',
378 'group' => 'fields',
379 'label' => esc_html__('Link entire product', 'fluent-cart'),
380 'type' => 'checkbox',
381 'inline' => true,
382 'description' => esc_html__('Only added if none of your product fields contains any links.', 'fluent-cart'),
383 ];
384 }
385
386 public function render()
387 {
388 $settings = $this->settings;
389
390 $this->setBricksQuery();
391
392 $columns = $this->normalizeColumns(Arr::get($settings, 'columns', 4));
393
394 $viewMode = Arr::get($settings, 'viewMode', 'grid');
395 $showViewSwitcher = !empty($settings['showViewSwitcher']);
396 $paginationType = Arr::get($settings, 'paginationType', 'numbers');
397 $isMainQuery = Arr::get($settings, 'is_main_query', false) && $this->is_frontend;
398 $perPage = (int)Arr::get($settings, 'posts_per_page', 10);
399 $enableFilter = !empty($settings['enableFilter']);
400 $priceRange = !empty($settings['priceRange']);
401 $liveFilter = !empty($settings['liveFilter']);
402
403 $this->storeSettingsTransient($settings);
404
405 if ($perPage <= 0) {
406 $perPage = 10;
407 }
408
409 if ($perPage > 100) {
410 $perPage = 100;
411 }
412
413 $args = $this->buildQueryArgs(
414 $settings,
415 $isMainQuery,
416 $perPage,
417 $viewMode
418 );
419
420 $productsQuery = (new ProductQuery($args));
421 $products = $productsQuery->get();
422 $defaultFilters = $productsQuery->getDefaultFilters();
423 $ajaxDefaultFilters = $this->getAjaxDefaultFilters(
424 $defaultFilters,
425 $settings
426 );
427
428 $filters = $this->getFilters($settings);
429
430 $wrapperClass = 'fct-products-wrapper-inner ' . ($viewMode === 'list' ? 'mode-list' : 'mode-grid') . (!$enableFilter ? ' fct-full-container-width' : '');
431
432 $wrapperAttributes = [
433 'class' => $wrapperClass,
434 'data-fluent-cart-product-wrapper-inner' => '',
435 'data-per-page' => $perPage,
436 'data-order-type' => Arr::get($defaultFilters, 'sort_type'),
437 'data-live-filter' => $liveFilter,
438 'data-paginator' => esc_attr($paginationType),
439 'data-default-filters' => wp_json_encode($ajaxDefaultFilters)
440 ];
441
442 $productWrapperClasses = [
443 'fct-products-wrapper',
444 'fct-brick-products-wrapper',
445 ];
446
447 if (!$this->is_frontend) {
448 $productWrapperClasses[] = 'fct-bricks-editor-mode';
449 }
450
451
452 ?>
453 <div
454 <?php
455 //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- render_attributes() handles escaping internally
456 echo $this->render_attributes('_root');
457 ?>
458 >
459 <div
460 <?php
461 //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- render_attributes() handles escaping internally
462 echo $this->render_attributes('wrapper');
463 ?>
464 >
465 <div
466 class="<?php echo esc_attr(implode(' ', $productWrapperClasses)); ?>"
467 data-fluent-cart-shop-app
468 data-fluent-cart-product-wrapper
469 >
470 <!-- View switcher -->
471 <?php
472 if ($showViewSwitcher) {
473 $renderer = new ShopAppRenderer(
474 $products,
475 ['view_mode' => $viewMode]
476 );
477
478 $renderer->renderViewSwitcher();
479 }
480 ?>
481
482 <!-- Products Container -->
483 <div <?php RenderHelper::renderAtts($wrapperAttributes); ?>>
484 <!-- Filter render here -->
485 <?php
486 if ($enableFilter) {
487 $shopAppRenderer = new ShopAppRenderer(
488 $products,
489 [
490 'view_mode' => $viewMode,
491 'enabled' => $enableFilter,
492 'enable_wildcard_filter' => !empty($settings['wildcardFilter']),
493 'custom_filters' => [
494 'price_range' => $priceRange,
495 'live_filter' => $liveFilter
496 ],
497 ]
498 );
499
500 $productFilterRenderer = new ProductFilterRender($filters);
501
502 $shopAppRenderer->renderFilter($productFilterRenderer);
503 }
504 ?>
505
506 <!-- Products -->
507 <div
508 data-fluent-cart-shop-app-product-list
509 class="fct-products-container grid-columns-<?php echo esc_attr($columns); ?>"
510 >
511 <?php $this->renderProducts($products); ?>
512 </div>
513
514 </div>
515
516 <!-- Pagination -->
517 <?php
518 if ($paginationType !== 'scroll') {
519 $this->renderPagination(
520 $products,
521 $defaultFilters,
522 $paginationType,
523 $perPage,
524 $viewMode
525 );
526 }
527 ?>
528
529
530 </div>
531 </div>
532 </div>
533
534 <?php
535 }
536
537 /**
538 * Normalize columns count.
539 */
540 private function normalizeColumns($columns)
541 {
542 $columns = (int)$columns;
543
544 if (!$columns) {
545 return 4;
546 }
547
548 if ($columns > 5) {
549 return 5;
550 }
551
552 return $columns;
553 }
554
555 /**
556 * Store settings in transient.
557 */
558 private function storeSettingsTransient($settings)
559 {
560 $uuid = 'fc_bx_collection_' . $this->uid;
561 if (!get_transient($uuid)) {
562 // save the settings as transient
563 set_transient($uuid, $settings, 48 * HOUR_IN_SECONDS);
564 }
565 }
566
567 /**
568 * Build query args.
569 */
570 private function buildQueryArgs($settings, $isMainQuery, $perPage, $viewMode) {
571 $args = array_filter([
572 'paginate' => 'simple',
573 'is_main_query' => $isMainQuery,
574 'sort_by' => Arr::get($settings, 'orderby', 'date'),
575 'sort_type' => Arr::get($settings, 'order', 'desc'),
576 'per_page' => $perPage,
577 'view_mode' => $viewMode,
578 ]);
579
580 if (!$isMainQuery) {
581 $includeIds = Arr::get($settings, 'include', []);
582 if ($includeIds) {
583 $args['include_ids'] = $includeIds;
584 }
585
586 $excludeIds = Arr::get($settings, 'exclude', []);
587 if ($excludeIds) {
588 $args['exclude_ids'] = $excludeIds;
589 }
590
591 $productType = Arr::get($settings, 'productType', []);
592 if ($productType) {
593 $args['product_type'] = $productType;
594 }
595
596 $onSale = Arr::get($settings, 'onSale', false);
597
598 if ($onSale) {
599 $args['on_sale'] = true;
600 }
601
602 $allowOutOfStock = Arr::get($settings, 'allowOutOfStock', false);
603 if ($allowOutOfStock) {
604 $args['allow_out_of_stock'] = true;
605 }
606
607 $categories = Arr::get($settings, 'categories', []);
608 if ($categories) {
609 //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
610 $args['tax_query'] = [
611 'product-categories' => $categories,
612 ];
613 }
614 }
615
616 return $args;
617 }
618
619 /**
620 * Render product loop.
621 */
622
623 private function renderProducts($products) {
624 ProductDataSetup::setProductsCache($products);
625
626 $postIndex = 1;
627
628 foreach ($products as $product) {
629 $post = get_post($product->ID);
630
631 setup_postdata($post);
632
633 $this->set_loop_object($post);
634
635 $this->render_fields($post, $postIndex);
636
637 $this->next_iteration();
638
639 $postIndex++;
640 }
641
642 wp_reset_postdata();
643
644 $this->end_iteration();
645 }
646
647 /**
648 * Render pagination.
649 */
650 private function renderPagination(
651 $products,
652 $defaultFilters,
653 $paginationType,
654 $perPage,
655 $viewMode
656 ) {
657 $renderer = new ShopAppRenderer(
658 [
659 'products' => $products,
660 'total' => $products->total(),
661 ],
662 [
663 'default_filters' => $defaultFilters,
664 'pagination_type' => $paginationType,
665 'per_page' => $perPage,
666 'view_mode' => $viewMode,
667 ]
668 );
669
670 $renderer->renderPaginator();
671 }
672
673 /**
674 * Get available product filters.
675 */
676 private function getFilters(array $settings): array
677 {
678 $categoriesLabel = !empty($settings['displayNameCategories']) ? $settings['displayNameCategories'] : __('Product Categories', 'fluent-cart');
679 $brandsLabel = !empty($settings['displayNameBrands']) ? $settings['displayNameBrands'] : __('Product Brands', 'fluent-cart');
680 $priceRangeLabel = !empty($settings['displayNamePriceRange']) ? $settings['displayNamePriceRange'] : __('Price', 'fluent-cart');
681
682 $filters = [
683 'product-categories' => [
684 'filter_type' => 'options',
685 'is_meta' => true,
686 'label' => $categoriesLabel,
687 'enabled' => !empty($settings['productCategories']),
688 'multiple' => false,
689 ],
690 'product-brands' => [
691 'filter_type' => 'options',
692 'is_meta' => true,
693 'label' => $brandsLabel,
694 'enabled' => !empty($settings['productBrands']),
695 'multiple' => false,
696 ],
697 'price_range' => [
698 'filter_type' => 'range',
699 'is_meta' => false,
700 'label' => $priceRangeLabel,
701 'enabled' => !empty($settings['priceRange'])
702 ],
703 ];
704
705 return $filters;
706 }
707
708 private function setBricksQuery()
709 {
710 $query_object = new Query(
711 [
712 'id' => $this->id,
713 'name' => $this->name,
714 'settings' => $this->settings,
715 ]
716 );
717
718 // Set $bricks_query (@since 1.10.2)
719 $this->set_bricks_query($query_object);
720 $this->start_iteration();
721 }
722
723 public function render_fields($post, $post_index)
724 {
725 BricksHelper::renderCollectionCard($this->settings, $post, $post_index, $this->uid);
726 }
727
728 public function renderAjaxContents($products, $settings)
729 {
730
731 $this->settings = $settings;
732
733 $this->setBricksQuery();
734
735 ProductDataSetup::setProductsCache($products);
736 $postIndex = 1;
737 foreach ($products as $product) {
738 $post = get_post($product->ID);
739 setup_postdata($post);
740 $this->set_loop_object($post);
741 $this->render_fields($post, $postIndex);
742 $this->next_iteration();
743 $postIndex++;
744 }
745 wp_reset_postdata();
746
747 $this->end_iteration();
748 }
749
750 private function getAjaxDefaultFilters(array $defaultFilters, array $settings): array
751 {
752 $filters = [];
753 $taxQuery = Arr::get($defaultFilters, 'tax_query', []);
754
755 if (!empty($taxQuery)) {
756 foreach ($taxQuery as $taxonomy => $termIds) {
757 $filters[$taxonomy] = $termIds;
758 }
759 }
760
761 $allowOutOfStock = Arr::get($settings, 'allowOutOfStock', false);
762 if ($allowOutOfStock) {
763 $filters['allow_out_of_stock'] = true;
764 }
765
766 if (!empty($filters)) {
767 $filters['enabled'] = true;
768 }
769
770 return $filters;
771 }
772
773 }
774