PluginProbe ʕ •ᴥ•ʔ
FrontBlocks for Gutenberg/GeneratePress / 1.5.1
FrontBlocks for Gutenberg/GeneratePress v1.5.1
1.5.2 1.5.1 1.4.0 1.5.0 trunk 0.2.0 0.2.1 0.2.2 0.2.3 0.2.4 0.2.5 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.1.0 1.2.0 1.2.1 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 ci-artifacts
frontblocks / includes / Admin / Settings.php
frontblocks / includes / Admin Last commit date
ReviewNotice.php 1 week ago Settings.php 1 week ago UI.php 2 months ago
Settings.php
3223 lines
1 <?php
2 /**
3 * Settings page
4 *
5 * @package FrontBlocks
6 * @author Closemarketing
7 * @copyright 2025 Closemarketing
8 * @version 1.0.0
9 */
10
11 namespace FrontBlocks\Admin;
12
13 defined( 'ABSPATH' ) || exit;
14
15 use FrontBlocks\Admin\UI;
16
17 /**
18 * Settings class
19 */
20 class Settings {
21
22 /**
23 * Option key for testimonials feature.
24 *
25 * @var string
26 */
27 private $option_enable_testimonials = 'enable_testimonials';
28
29 /**
30 * Option key for reading progress bar feature.
31 *
32 * @var string
33 */
34 private $option_enable_reading_progress = 'enable_reading_progress';
35
36 /**
37 * Option key for back button feature.
38 *
39 * @var string
40 */
41 private $option_enable_back_button = 'enable_back_button';
42
43 /**
44 * Option key for scroll to top feature.
45 *
46 * @var string
47 */
48 private $option_enable_scroll_top = 'enable_scroll_top';
49
50 /**
51 * Option key for scroll to top button position.
52 *
53 * @var string
54 */
55 private $option_scroll_top_position = 'scroll_top_position';
56
57 /**
58 * Option key for scroll to top custom icon URL.
59 *
60 * @var string
61 */
62 private $option_scroll_top_icon_url = 'scroll_top_icon_url';
63
64 /**
65 * Option key for events CPT feature.
66 *
67 * @var string
68 */
69 private $option_enable_events = 'enable_events';
70
71 /**
72 * Option key for events type (cpt or posts).
73 *
74 * @var string
75 */
76 private $option_events_type = 'events_type';
77
78 /**
79 * Option key for maintenance mode feature.
80 *
81 * @var string
82 */
83 private $option_enable_maintenance = 'enable_maintenance';
84
85 /**
86 * Option key for maintenance page title.
87 *
88 * @var string
89 */
90 private $option_maintenance_title = 'maintenance_title';
91
92 /**
93 * Option key for maintenance page background image (attachment ID).
94 *
95 * @var string
96 */
97 private $option_maintenance_image = 'maintenance_image';
98
99 /**
100 * Option key for the cookie notice feature.
101 *
102 * @var string
103 */
104 private $option_enable_cookie_notice = 'enable_cookie_notice';
105
106 /**
107 * Option key for the cookie notice message.
108 *
109 * @var string
110 */
111 private $option_cookie_notice_message = 'cookie_notice_message';
112
113 /**
114 * Option key for the cookie notice accept button label.
115 *
116 * @var string
117 */
118 private $option_cookie_notice_accept_label = 'cookie_notice_accept_label';
119
120 /**
121 * Option key for the cookie notice reject button label.
122 *
123 * @var string
124 */
125 private $option_cookie_notice_reject_label = 'cookie_notice_reject_label';
126
127 /**
128 * Option key for the cookie policy page ID.
129 *
130 * @var string
131 */
132 private $option_cookie_notice_policy_page_id = 'cookie_notice_policy_page_id';
133
134 /**
135 * Option key for the cookie notice layout variant.
136 *
137 * @var string
138 */
139 private $option_cookie_notice_layout = 'cookie_notice_layout';
140
141 /**
142 * Option key for the cookie notice boxed panel position.
143 *
144 * @var string
145 */
146 private $option_cookie_notice_position = 'cookie_notice_position';
147
148 /**
149 * Option key for the cookie notice accent color.
150 *
151 * @var string
152 */
153 private $option_cookie_notice_color = 'cookie_notice_color';
154
155 /**
156 * Option key for the cookie notice expiration (in days).
157 *
158 * @var string
159 */
160 private $option_cookie_notice_expiration_days = 'cookie_notice_expiration_days';
161
162 /**
163 * Option key for the Google Tag Manager container ID.
164 *
165 * @var string
166 */
167 private $option_cookie_notice_gtm_id = 'cookie_notice_gtm_id';
168
169 /**
170 * Option key for the GA4 Measurement ID.
171 *
172 * @var string
173 */
174 private $option_cookie_notice_ga4_id = 'cookie_notice_ga4_id';
175
176 /**
177 * Option key for popups feature.
178 *
179 * @var string
180 */
181 private $option_enable_popups = 'enable_popups';
182
183 /**
184 * Option key for fluid typography feature.
185 *
186 * @var string
187 */
188 private $option_enable_fluid_typography = 'enable_fluid_typography';
189
190 /**
191 * Option key for Gutenberg in products (PRO).
192 *
193 * @var string
194 */
195 private $option_enable_gutenberg = 'enable_gutenberg';
196
197 /**
198 * Option key for Simple Prices Variable Products (PRO).
199 *
200 * @var string
201 */
202 private $option_enable_simple_prices_variable_products = 'enable_simple_prices_variable_products';
203
204 /**
205 * Option key for After Add to Cart Block (PRO).
206 *
207 * @var string
208 */
209 private $option_enable_after_add_to_cart = 'enable_after_add_to_cart';
210
211 /**
212 * Option key for deactivate short description (PRO).
213 *
214 * @var string
215 */
216 private $option_deactivate_short_description = 'deactivate_short_description';
217
218 /**
219 * Option key for move content to short description (PRO).
220 *
221 * @var string
222 */
223 private $option_move_content_to_short_description = 'move_content_to_short_description';
224
225 /**
226 * Option key for disable zoom in WooCommerce images (PRO).
227 *
228 * @var string
229 */
230 private $option_disable_zoom_images = 'disable_zoom_images';
231
232 /**
233 * Option key for add share buttons in product page (PRO).
234 *
235 * @var string
236 */
237 private $option_add_share_buttons = 'add_share_buttons';
238
239 /**
240 * Option key for deactivate product tabs (PRO).
241 *
242 * @var string
243 */
244 private $option_deactivate_product_tabs = 'deactivate_product_tabs';
245
246 /**
247 * Option key for horizontal product form layout (PRO).
248 *
249 * @var string
250 */
251 private $option_horizontal_product_form = 'horizontal_product_form';
252
253 /**
254 * Option key for enabling variant display mode per-attribute selector (PRO).
255 *
256 * @var string
257 */
258 private $option_enable_variant_display_mode = 'enable_variant_display_mode';
259
260 /**
261 * Option key for disabling coupon field in cart (PRO).
262 *
263 * @var string
264 */
265 private $option_disable_cart_coupon = 'disable_cart_coupon';
266
267 /**
268 * Option key for disabling cross-sells in cart (PRO).
269 *
270 * @var string
271 */
272 private $option_disable_cart_cross_sells = 'disable_cart_cross_sells';
273
274 /**
275 * Option key for disabling coupon form in checkout (PRO).
276 *
277 * @var string
278 */
279 private $option_disable_checkout_coupon = 'disable_checkout_coupon';
280
281 /**
282 * Option key for disabling order notes in checkout (PRO).
283 *
284 * @var string
285 */
286 private $option_disable_checkout_order_notes = 'disable_checkout_order_notes';
287
288 /**
289 * Option key for disabling login prompt in checkout (PRO).
290 *
291 * @var string
292 */
293 private $option_disable_checkout_login_prompt = 'disable_checkout_login_prompt';
294
295 /**
296 * Option key for custom post types builder (PRO).
297 *
298 * @var string
299 */
300 private $option_enable_custom_post_types = 'enable_custom_post_types';
301
302 /**
303 * Option key for full page scroll feature (PRO).
304 *
305 * @var string
306 */
307 private $option_enable_fullpage_scroll = 'enable_fullpage_scroll';
308
309 /**
310 * Option key for language banner feature (PRO).
311 *
312 * @var string
313 */
314 private $option_enable_language_banner = 'enable_language_banner';
315
316 /**
317 * Option key for checkout inline fields (PRO).
318 *
319 * @var string
320 */
321 private $option_checkout_inline = 'checkout_inline';
322
323 /**
324 * Page slug.
325 *
326 * @var string
327 */
328 private $page_slug = 'frontblocks-settings';
329
330 /**
331 * Is license valid.
332 *
333 * @var bool
334 */
335 private $is_license_valid = false;
336
337 /**
338 * Constructor.
339 */
340 public function __construct() {
341 // Check license via FrontBlocks PRO helper function.
342 $this->is_license_valid = function_exists( 'frblp_is_license_valid' ) && frblp_is_license_valid();
343
344 add_action( 'admin_menu', array( $this, 'register_menu' ) );
345 add_action( 'admin_init', array( $this, 'register_settings' ) );
346 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_styles' ) );
347 add_action( 'admin_head', array( $this, 'add_menu_icon_styles' ) );
348 }
349
350 /**
351 * Add menu icon styles.
352 *
353 * @return void
354 */
355 public function add_menu_icon_styles() {
356 ?>
357 <style>
358 #toplevel_page_frontblocks-settings .wp-menu-image img,
359 #toplevel_page_frontblocks-settings .wp-menu-image svg {
360 width: 20px !important;
361 height: 20px !important;
362 max-width: 20px !important;
363 max-height: 20px !important;
364 }
365 </style>
366 <?php
367 }
368
369 /**
370 * Enqueue admin styles for settings page.
371 *
372 * @param string $hook Current admin page hook.
373 * @return void
374 */
375 public function enqueue_admin_styles( $hook ) {
376 if ( 'appearance_page_' . $this->page_slug !== $hook ) {
377 return;
378 }
379
380 wp_enqueue_style(
381 'frontblocks-admin-settings',
382 FRBL_PLUGIN_URL . 'assets/admin/settings.css',
383 array(),
384 FRBL_VERSION
385 );
386
387 // Reuse the real frontend banner styles so the settings-page preview
388 // renders pixel-identical to what visitors will actually see.
389 wp_enqueue_style(
390 'frontblocks-cookie-notice',
391 FRBL_PLUGIN_URL . 'assets/cookie-notice/frontblocks-cookie-notice.css',
392 array(),
393 FRBL_VERSION
394 );
395
396 wp_enqueue_media();
397
398 wp_add_inline_script(
399 'jquery',
400 "
401 document.addEventListener('DOMContentLoaded', function() {
402 const deactivateCheckbox = document.getElementById('deactivate_short_description');
403 const moveContentCheckbox = document.getElementById('move_content_to_short_description');
404
405 if (deactivateCheckbox && moveContentCheckbox) {
406 function updateMutualExclusion() {
407 const deactivateWrapper = deactivateCheckbox.closest('.tw:flex');
408 const moveContentWrapper = moveContentCheckbox.closest('.tw:flex');
409
410 // Check if license is valid (not just PRO active).
411 const isLicenseValid = " . ( $this->is_license_valid ? 'true' : 'false' ) . ";
412
413 if (deactivateCheckbox.checked) {
414 moveContentCheckbox.disabled = true;
415 if (moveContentWrapper) {
416 moveContentWrapper.style.opacity = '0.5';
417 moveContentWrapper.style.filter = 'grayscale(100%)';
418 const toggle = moveContentWrapper.querySelector('.frbl-toggle');
419 if (toggle) {
420 toggle.style.borderColor = '#ef4444';
421 toggle.style.opacity = '0.7';
422 }
423 }
424 } else {
425 moveContentCheckbox.disabled = !isLicenseValid;
426 if (moveContentWrapper) {
427 moveContentWrapper.style.opacity = isLicenseValid ? '1' : '0.5';
428 moveContentWrapper.style.filter = '';
429 const toggle = moveContentWrapper.querySelector('.frbl-toggle');
430 if (toggle) {
431 toggle.style.borderColor = '';
432 toggle.style.opacity = '';
433 }
434 }
435 }
436
437 if (moveContentCheckbox.checked) {
438 deactivateCheckbox.disabled = true;
439 if (deactivateWrapper) {
440 deactivateWrapper.style.opacity = '0.5';
441 deactivateWrapper.style.filter = 'grayscale(100%)';
442 const toggle = deactivateWrapper.querySelector('.frbl-toggle');
443 if (toggle) {
444 toggle.style.borderColor = '#ef4444';
445 toggle.style.opacity = '0.7';
446 }
447 }
448 } else {
449 deactivateCheckbox.disabled = !isLicenseValid;
450 if (deactivateWrapper) {
451 deactivateWrapper.style.opacity = isLicenseValid ? '1' : '0.5';
452 deactivateWrapper.style.filter = '';
453 const toggle = deactivateWrapper.querySelector('.frbl-toggle');
454 if (toggle) {
455 toggle.style.borderColor = '';
456 toggle.style.opacity = '';
457 }
458 }
459 }
460 }
461
462 deactivateCheckbox.addEventListener('change', updateMutualExclusion);
463 moveContentCheckbox.addEventListener('change', updateMutualExclusion);
464
465 updateMutualExclusion();
466 }
467
468 // Show/hide events type select based on toggle state.
469 const eventsCheckbox = document.getElementById('enable_events');
470 const eventsTypeWrapper = document.getElementById('events-type-wrapper');
471
472 if (eventsCheckbox && eventsTypeWrapper) {
473 // Find the parent feature card and feature content.
474 const featureCard = eventsCheckbox.closest('.frbl-feature-card');
475 const featureContent = featureCard ? featureCard.querySelector('.frbl-feature-content') : null;
476
477 // Move the wrapper outside of frbl-feature-content but inside frbl-feature-card.
478 // This ensures it appears below the entire horizontal row (icon, text, toggle).
479 if (featureCard && featureContent) {
480 // Check if wrapper is still inside feature-content and move it.
481 if (featureContent.contains(eventsTypeWrapper)) {
482 // Move it to be a direct child of feature-card, after feature-content.
483 featureCard.appendChild(eventsTypeWrapper);
484 }
485 }
486
487 function updateEventsTypeVisibility() {
488 if (eventsCheckbox.checked) {
489 eventsTypeWrapper.style.display = 'block';
490 eventsTypeWrapper.style.width = '100%';
491 eventsTypeWrapper.style.minWidth = '100%';
492 eventsTypeWrapper.style.marginTop = '1rem';
493 eventsTypeWrapper.style.paddingTop = '1rem';
494 eventsTypeWrapper.style.paddingLeft = '1rem';
495 eventsTypeWrapper.style.paddingRight = '1rem';
496 eventsTypeWrapper.style.paddingBottom = '1rem';
497 eventsTypeWrapper.style.borderTop = '1px solid #e5e7eb';
498 eventsTypeWrapper.style.backgroundColor = '#f9fafb';
499 // Set feature card to column layout.
500 if (featureCard) {
501 featureCard.style.display = 'flex';
502 featureCard.style.flexDirection = 'column';
503 }
504 // Keep the feature content horizontal - never change it.
505 if (featureContent) {
506 featureContent.style.flexDirection = 'row';
507 featureContent.style.alignItems = 'center';
508 featureContent.style.justifyContent = 'space-between';
509 }
510 } else {
511 eventsTypeWrapper.style.display = 'none';
512 // Reset feature card layout.
513 if (featureCard) {
514 featureCard.style.display = '';
515 featureCard.style.flexDirection = '';
516 }
517 // Keep the feature content horizontal.
518 if (featureContent) {
519 featureContent.style.flexDirection = 'row';
520 featureContent.style.alignItems = 'center';
521 featureContent.style.justifyContent = 'space-between';
522 }
523 }
524 }
525
526 // Run immediately to move the element on page load.
527 if (featureCard && featureContent && featureContent.contains(eventsTypeWrapper)) {
528 featureCard.appendChild(eventsTypeWrapper);
529 }
530
531 eventsCheckbox.addEventListener('change', updateEventsTypeVisibility);
532 updateEventsTypeVisibility();
533 }
534
535 // Show/hide scroll-top sub-settings based on toggle state.
536 const scrollTopCheckbox = document.getElementById('enable_scroll_top');
537 const scrollTopWrapper = document.getElementById('scroll-top-settings-wrapper');
538
539 if (scrollTopCheckbox && scrollTopWrapper) {
540 const scrollTopCard = scrollTopCheckbox.closest('.frbl-feature-card');
541 const scrollTopContent = scrollTopCard ? scrollTopCard.querySelector('.frbl-feature-content') : null;
542
543 if (scrollTopCard && scrollTopContent && scrollTopContent.contains(scrollTopWrapper)) {
544 scrollTopCard.appendChild(scrollTopWrapper);
545 }
546
547 function updateScrollTopVisibility() {
548 if (scrollTopCheckbox.checked) {
549 scrollTopWrapper.style.display = 'block';
550 scrollTopWrapper.style.width = '100%';
551 scrollTopWrapper.style.minWidth = '100%';
552 scrollTopWrapper.style.marginTop = '1rem';
553 scrollTopWrapper.style.paddingTop = '1rem';
554 scrollTopWrapper.style.paddingLeft = '1rem';
555 scrollTopWrapper.style.paddingRight = '1rem';
556 scrollTopWrapper.style.paddingBottom = '1rem';
557 scrollTopWrapper.style.borderTop = '1px solid #e5e7eb';
558 scrollTopWrapper.style.backgroundColor = '#f9fafb';
559 if (scrollTopCard) {
560 scrollTopCard.style.display = 'flex';
561 scrollTopCard.style.flexDirection = 'column';
562 }
563 if (scrollTopContent) {
564 scrollTopContent.style.flexDirection = 'row';
565 scrollTopContent.style.alignItems = 'center';
566 scrollTopContent.style.justifyContent = 'space-between';
567 }
568 } else {
569 scrollTopWrapper.style.display = 'none';
570 if (scrollTopCard) {
571 scrollTopCard.style.display = '';
572 scrollTopCard.style.flexDirection = '';
573 }
574 if (scrollTopContent) {
575 scrollTopContent.style.flexDirection = 'row';
576 scrollTopContent.style.alignItems = 'center';
577 scrollTopContent.style.justifyContent = 'space-between';
578 }
579 }
580 }
581
582 scrollTopCheckbox.addEventListener('change', updateScrollTopVisibility);
583 updateScrollTopVisibility();
584
585 // Media uploader for custom icon.
586 const uploadBtn = document.getElementById('scroll-top-icon-upload');
587 const removeBtn = document.getElementById('scroll-top-icon-remove');
588 const iconInput = document.getElementById('scroll_top_icon_url');
589 const iconPreview = document.getElementById('scroll-top-icon-preview');
590 const iconImg = document.getElementById('scroll-top-icon-img');
591
592 if (uploadBtn && iconInput && typeof wp !== 'undefined' && wp.media) {
593 var mediaFrame;
594
595 uploadBtn.addEventListener('click', function(e) {
596 e.preventDefault();
597
598 if (mediaFrame) {
599 mediaFrame.open();
600 return;
601 }
602
603 mediaFrame = wp.media({
604 title: '" . esc_js( __( 'Select Icon', 'frontblocks' ) ) . "',
605 button: { text: '" . esc_js( __( 'Use this image', 'frontblocks' ) ) . "' },
606 multiple: false
607 });
608
609 mediaFrame.on('select', function() {
610 const attachment = mediaFrame.state().get('selection').first().toJSON();
611 iconInput.value = attachment.url;
612 if (iconImg) { iconImg.src = attachment.url; }
613 if (iconPreview) { iconPreview.style.display = ''; }
614 if (removeBtn) { removeBtn.style.display = ''; }
615 });
616
617 mediaFrame.open();
618 });
619 }
620
621 if (removeBtn && iconInput) {
622 removeBtn.addEventListener('click', function(e) {
623 e.preventDefault();
624 iconInput.value = '';
625 if (iconImg) { iconImg.src = ''; }
626 if (iconPreview) { iconPreview.style.display = 'none'; }
627 removeBtn.style.display = 'none';
628 });
629 }
630 }
631
632 });
633 "
634 );
635
636 // Separate, isolated inline script for the maintenance mode fields: kept apart from the
637 // script above so that an error there can never prevent this one from running.
638 wp_add_inline_script(
639 'jquery',
640 "
641 document.addEventListener('DOMContentLoaded', function() {
642 const maintenanceCheckbox = document.getElementById('enable_maintenance');
643 const maintenanceWrapper = document.getElementById('maintenance-fields-wrapper');
644
645 if (maintenanceCheckbox && maintenanceWrapper) {
646 maintenanceCheckbox.addEventListener('change', function () {
647 maintenanceWrapper.style.display = maintenanceCheckbox.checked ? 'block' : 'none';
648 });
649 }
650
651 // Event delegation: works even if the button is added/replaced later, and does not
652 // depend on any other inline script having run successfully first.
653 let mediaFrame;
654
655 document.addEventListener('click', function (event) {
656 const selectButton = event.target.closest('.frbl-maintenance-select-image');
657 const removeButton = event.target.closest('.frbl-maintenance-remove-image');
658
659 if (! selectButton && ! removeButton) {
660 return;
661 }
662
663 event.preventDefault();
664
665 const imageInput = document.getElementById('maintenance_image');
666 const previewWrapper = document.querySelector('.frbl-maintenance-image-preview');
667 const previewImg = previewWrapper ? previewWrapper.querySelector('img') : null;
668
669 if (removeButton) {
670 if (imageInput) {
671 imageInput.value = '';
672 }
673 if (previewWrapper) {
674 previewWrapper.style.display = 'none';
675 }
676 removeButton.style.display = 'none';
677 return;
678 }
679
680 if (! window.wp || ! window.wp.media) {
681 window.alert('" . esc_js( __( 'The media library failed to load. Please reload the page and try again.', 'frontblocks' ) ) . "');
682 return;
683 }
684
685 if (mediaFrame) {
686 mediaFrame.open();
687 return;
688 }
689
690 mediaFrame = window.wp.media({
691 title: selectButton.textContent,
692 button: { text: selectButton.textContent },
693 multiple: false,
694 library: { type: 'image' },
695 });
696
697 mediaFrame.on('select', function () {
698 const attachment = mediaFrame.state().get('selection').first().toJSON();
699
700 if (imageInput) {
701 imageInput.value = attachment.id;
702 }
703 if (previewImg) {
704 previewImg.src = attachment.sizes && attachment.sizes.medium ? attachment.sizes.medium.url : attachment.url;
705 }
706 if (previewWrapper) {
707 previewWrapper.style.display = 'block';
708 }
709
710 const currentRemoveButton = document.querySelector('.frbl-maintenance-remove-image');
711 if (currentRemoveButton) {
712 currentRemoveButton.style.display = '';
713 }
714 });
715
716 mediaFrame.open();
717 });
718 });
719 "
720 );
721
722 // Enqueue the WordPress media uploader for the maintenance background image field.
723 wp_enqueue_media();
724
725 // Separate, isolated inline script for the cookie notice fields, kept apart so a
726 // failure in another script can never prevent this one from running.
727 wp_add_inline_script(
728 'jquery',
729 "
730 document.addEventListener('DOMContentLoaded', function() {
731 const cookieCheckbox = document.getElementById('enable_cookie_notice');
732 const cookieWrapper = document.getElementById('cookie-notice-fields-wrapper');
733
734 if (cookieCheckbox && cookieWrapper) {
735 cookieCheckbox.addEventListener('change', function () {
736 cookieWrapper.style.display = cookieCheckbox.checked ? 'block' : 'none';
737 });
738 }
739
740 const layoutSelect = document.getElementById('cookie_notice_layout');
741 const positionSelect = document.getElementById('cookie_notice_position');
742 const positionWrapper = document.getElementById('cookie-notice-position-wrapper');
743 const preview = document.getElementById('frbl-cookie-notice-preview');
744
745 if (layoutSelect && positionWrapper) {
746 layoutSelect.addEventListener('change', function () {
747 positionWrapper.style.display = layoutSelect.value === 'box' ? 'block' : 'none';
748 });
749 }
750
751 function updatePreviewLayout() {
752 if (!preview) {
753 return;
754 }
755
756 var layout = layoutSelect ? layoutSelect.value : 'bar';
757 var position = positionSelect ? positionSelect.value : 'bottom-right';
758
759 preview.className = 'frbl-cookie-notice frbl-cookie-notice-preview frbl-cookie-notice--' + layout;
760
761 if (layout === 'box') {
762 preview.className += ' frbl-cookie-notice--' + (position === 'bottom-left' ? 'left' : 'right');
763 }
764 }
765
766 if (layoutSelect) {
767 layoutSelect.addEventListener('change', updatePreviewLayout);
768 }
769
770 if (positionSelect) {
771 positionSelect.addEventListener('change', updatePreviewLayout);
772 }
773 });
774 "
775 );
776
777 // Enqueue script for custom post types if PRO is active and license is valid.
778 if ( frbl_is_pro_active() && $this->is_license_valid ) {
779 wp_enqueue_script(
780 'frontblocks-cpt-admin',
781 FRBL_PLUGIN_URL . 'assets/admin/custom-post-types.js',
782 array( 'jquery' ),
783 FRBL_VERSION,
784 true
785 );
786
787 wp_localize_script(
788 'frontblocks-cpt-admin',
789 'frontblocksCpt',
790 array(
791 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
792 'nonce' => wp_create_nonce( 'frontblocks_create_cpt' ),
793 'i18n' => array(
794 'creating' => __( 'Creating...', 'frontblocks' ),
795 'error' => __( 'Error creating post type. Please try again.', 'frontblocks' ),
796 'success' => __( 'Post type created successfully!', 'frontblocks' ),
797 ),
798 )
799 );
800 }
801 }
802
803 /**
804 * Register options page as dedicated menu.
805 *
806 * @return void
807 */
808 public function register_menu() {
809 add_theme_page(
810 __( 'FrontBlocks Settings', 'frontblocks' ),
811 __( 'FrontBlocks', 'frontblocks' ),
812 'edit_theme_options',
813 $this->page_slug,
814 array( $this, 'render_page' )
815 );
816 }
817
818 /**
819 * Register settings, sections and fields.
820 *
821 * @return void
822 */
823 public function register_settings() {
824 register_setting(
825 'frontblocks_settings',
826 'frontblocks_settings',
827 array(
828 'type' => 'array',
829 'sanitize_callback' => array( $this, 'sanitize_settings' ),
830 'default' => array(),
831 'show_in_rest' => false,
832 )
833 );
834
835 // Register license setting group for FrontBlocks PRO.
836 global $frblp_license;
837 if ( $frblp_license && class_exists( '\Closemarketing\WPLicenseManager\License' ) ) {
838 // Register each individual license field.
839 register_setting(
840 'frontblocks-pro_license',
841 'frontblocks-pro_license_apikey',
842 array(
843 'type' => 'string',
844 'sanitize_callback' => 'sanitize_text_field',
845 )
846 );
847
848 register_setting(
849 'frontblocks-pro_license',
850 'frontblocks-pro_license_deactivate_checkbox',
851 array(
852 'type' => 'string',
853 'sanitize_callback' => 'sanitize_text_field',
854 )
855 );
856
857 // Hook into admin_init to process license activation/deactivation.
858 add_action(
859 'admin_init',
860 function () use ( $frblp_license ) {
861 // Check if license form was submitted and verify nonce.
862 if ( isset( $_POST['option_page'], $_POST['_wpnonce'] ) && 'frontblocks-pro_license' === $_POST['option_page'] && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), 'frontblocks-pro_license-options' ) ) {
863 if ( isset( $_POST['submit_license'] ) ) {
864 // Build input array for validate_license.
865 $input = array(
866 'frontblocks-pro_license_apikey' => isset( $_POST['frontblocks-pro_license_apikey'] ) ? sanitize_text_field( wp_unslash( $_POST['frontblocks-pro_license_apikey'] ) ) : '',
867 'frontblocks-pro_license_deactivate_checkbox' => isset( $_POST['frontblocks-pro_license_deactivate_checkbox'] ) ? sanitize_text_field( wp_unslash( $_POST['frontblocks-pro_license_deactivate_checkbox'] ) ) : '',
868 );
869
870 // Call the license validation.
871 $frblp_license->validate_license( $input );
872 }
873 }
874 },
875 15
876 );
877 }
878
879 // Always Active Blocks section.
880 add_settings_section(
881 'frontblocks_section_active_blocks',
882 __( 'Active Blocks & Features', 'frontblocks' ),
883 array( $this, 'section_active_blocks_callback' ),
884 $this->page_slug
885 );
886
887 add_settings_section(
888 'frontblocks_section_features',
889 __( 'Optional Features', 'frontblocks' ),
890 array( $this, 'section_features_callback' ),
891 $this->page_slug
892 );
893
894 add_settings_field(
895 $this->option_enable_testimonials,
896 __( 'Enable testimonials', 'frontblocks' ),
897 array( $this, 'field_enable_testimonials' ),
898 $this->page_slug,
899 'frontblocks_section_features'
900 );
901
902 add_settings_field(
903 $this->option_enable_reading_progress,
904 __( 'Enable reading progress bar', 'frontblocks' ),
905 array( $this, 'field_enable_reading_progress' ),
906 $this->page_slug,
907 'frontblocks_section_features'
908 );
909
910 add_settings_field(
911 $this->option_enable_back_button,
912 __( 'Enable Back Button', 'frontblocks' ),
913 array( $this, 'field_enable_back_button' ),
914 $this->page_slug,
915 'frontblocks_section_features'
916 );
917
918 add_settings_field(
919 $this->option_enable_scroll_top,
920 __( 'Enable Scroll to Top', 'frontblocks' ),
921 array( $this, 'field_enable_scroll_top' ),
922 $this->page_slug,
923 'frontblocks_section_features'
924 );
925
926 add_settings_field(
927 $this->option_enable_events,
928 __( 'Enable Events', 'frontblocks' ),
929 array( $this, 'field_enable_events' ),
930 $this->page_slug,
931 'frontblocks_section_features'
932 );
933
934 add_settings_field(
935 $this->option_enable_fluid_typography,
936 __( 'Enable Fluid Typography', 'frontblocks' ),
937 array( $this, 'field_enable_fluid_typography' ),
938 $this->page_slug,
939 'frontblocks_section_features'
940 );
941
942 // Maintenance Mode section (own full-width section, needs room for title + image fields).
943 add_settings_section(
944 'frontblocks_section_maintenance',
945 __( 'Maintenance Mode', 'frontblocks' ),
946 array( $this, 'section_maintenance_callback' ),
947 $this->page_slug
948 );
949
950 add_settings_field(
951 $this->option_enable_maintenance,
952 __( 'Enable Maintenance Mode', 'frontblocks' ),
953 array( $this, 'field_enable_maintenance' ),
954 $this->page_slug,
955 'frontblocks_section_maintenance'
956 );
957
958 // Cookie Notice section (own full-width section: banner copy, layout, colors, GTM/GA4 and stats).
959 add_settings_section(
960 'frontblocks_section_cookie_notice',
961 __( 'Cookie Notice', 'frontblocks' ),
962 array( $this, 'section_cookie_notice_callback' ),
963 $this->page_slug
964 );
965
966 add_settings_field(
967 $this->option_enable_cookie_notice,
968 __( 'Enable Cookie Notice', 'frontblocks' ),
969 array( $this, 'field_enable_cookie_notice' ),
970 $this->page_slug,
971 'frontblocks_section_cookie_notice'
972 );
973
974 // PRO Features section.
975 add_settings_section(
976 'frontblocks_section_woocommerce_features',
977 __( 'WooCommerce Features', 'frontblocks' ),
978 array( $this, 'section_woo_features_callback' ),
979 $this->page_slug
980 );
981
982 add_settings_field(
983 $this->option_enable_gutenberg,
984 __( 'Enable Gutenberg in Products', 'frontblocks' ),
985 array( $this, 'field_enable_gutenberg' ),
986 $this->page_slug,
987 'frontblocks_section_woocommerce_features'
988 );
989
990 add_settings_field(
991 $this->option_enable_simple_prices_variable_products,
992 __( 'Enable Simple Prices Variable Products', 'frontblocks' ),
993 array( $this, 'field_enable_simple_prices_variable_products' ),
994 $this->page_slug,
995 'frontblocks_section_woocommerce_features'
996 );
997
998 add_settings_field(
999 $this->option_enable_after_add_to_cart,
1000 __( 'Enable After Add to Cart Block', 'frontblocks' ),
1001 array( $this, 'field_enable_after_add_to_cart' ),
1002 $this->page_slug,
1003 'frontblocks_section_woocommerce_features'
1004 );
1005
1006 add_settings_field(
1007 $this->option_deactivate_short_description,
1008 __( 'Deactivate Short Description', 'frontblocks' ),
1009 array( $this, 'field_deactivate_short_description' ),
1010 $this->page_slug,
1011 'frontblocks_section_woocommerce_features'
1012 );
1013
1014 add_settings_field(
1015 $this->option_move_content_to_short_description,
1016 __( 'Move Content to Short Description', 'frontblocks' ),
1017 array( $this, 'field_move_content_to_short_description' ),
1018 $this->page_slug,
1019 'frontblocks_section_woocommerce_features'
1020 );
1021
1022 add_settings_field(
1023 $this->option_disable_zoom_images,
1024 __( 'Disable Zoom in Product Images', 'frontblocks' ),
1025 array( $this, 'field_disable_zoom_images' ),
1026 $this->page_slug,
1027 'frontblocks_section_woocommerce_features'
1028 );
1029
1030 add_settings_field(
1031 $this->option_add_share_buttons,
1032 __( 'Add Share Buttons in Product Page', 'frontblocks' ),
1033 array( $this, 'field_add_share_buttons' ),
1034 $this->page_slug,
1035 'frontblocks_section_woocommerce_features'
1036 );
1037
1038 add_settings_field(
1039 $this->option_deactivate_product_tabs,
1040 __( 'Deactivate Product Tabs', 'frontblocks' ),
1041 array( $this, 'field_deactivate_product_tabs' ),
1042 $this->page_slug,
1043 'frontblocks_section_woocommerce_features'
1044 );
1045
1046 add_settings_field(
1047 $this->option_horizontal_product_form,
1048 __( 'Horizontal Product Form Layout', 'frontblocks' ),
1049 array( $this, 'field_horizontal_product_form' ),
1050 $this->page_slug,
1051 'frontblocks_section_woocommerce_features'
1052 );
1053
1054 add_settings_field(
1055 $this->option_enable_variant_display_mode,
1056 __( 'Variant Display Mode', 'frontblocks' ),
1057 array( $this, 'field_enable_variant_display_mode' ),
1058 $this->page_slug,
1059 'frontblocks_section_woocommerce_features'
1060 );
1061
1062 add_settings_field(
1063 $this->option_disable_cart_coupon,
1064 __( 'Disable Coupon Field in Cart', 'frontblocks' ),
1065 array( $this, 'field_disable_cart_coupon' ),
1066 $this->page_slug,
1067 'frontblocks_section_woocommerce_features'
1068 );
1069
1070 add_settings_field(
1071 $this->option_disable_cart_cross_sells,
1072 __( 'Disable Cross-Sells in Cart', 'frontblocks' ),
1073 array( $this, 'field_disable_cart_cross_sells' ),
1074 $this->page_slug,
1075 'frontblocks_section_woocommerce_features'
1076 );
1077
1078 add_settings_field(
1079 $this->option_disable_checkout_coupon,
1080 __( 'Disable Coupon Field in Checkout', 'frontblocks' ),
1081 array( $this, 'field_disable_checkout_coupon' ),
1082 $this->page_slug,
1083 'frontblocks_section_woocommerce_features'
1084 );
1085
1086 add_settings_field(
1087 $this->option_disable_checkout_order_notes,
1088 __( 'Disable Order Notes in Checkout', 'frontblocks' ),
1089 array( $this, 'field_disable_checkout_order_notes' ),
1090 $this->page_slug,
1091 'frontblocks_section_woocommerce_features'
1092 );
1093
1094 add_settings_field(
1095 $this->option_disable_checkout_login_prompt,
1096 __( 'Disable Login Prompt in Checkout', 'frontblocks' ),
1097 array( $this, 'field_disable_checkout_login_prompt' ),
1098 $this->page_slug,
1099 'frontblocks_section_woocommerce_features'
1100 );
1101
1102 add_settings_field(
1103 $this->option_enable_fullpage_scroll,
1104 __( 'Enable Full Page Scroll', 'frontblocks' ),
1105 array( $this, 'field_enable_fullpage_scroll' ),
1106 $this->page_slug,
1107 'frontblocks_section_woocommerce_features'
1108 );
1109
1110 add_settings_field(
1111 $this->option_enable_language_banner,
1112 __( 'Enable Language Banner', 'frontblocks' ),
1113 array( $this, 'field_enable_language_banner' ),
1114 $this->page_slug,
1115 'frontblocks_section_woocommerce_features'
1116 );
1117
1118 add_settings_field(
1119 $this->option_enable_popups,
1120 __( 'Enable Popups', 'frontblocks' ),
1121 array( $this, 'field_enable_popups' ),
1122 $this->page_slug,
1123 'frontblocks_section_woocommerce_features'
1124 );
1125
1126 add_settings_field(
1127 $this->option_checkout_inline,
1128 __( 'Checkout Inline Fields', 'frontblocks' ),
1129 array( $this, 'field_checkout_inline' ),
1130 $this->page_slug,
1131 'frontblocks_section_woocommerce_features'
1132 );
1133
1134 // Custom Post Types section (PRO).
1135 if ( frbl_is_pro_active() ) {
1136 add_settings_section(
1137 'frontblocks_section_custom_post_types',
1138 __( 'Custom Post Types', 'frontblocks' ),
1139 array( $this, 'section_custom_post_types_callback' ),
1140 $this->page_slug
1141 );
1142
1143 add_settings_field(
1144 $this->option_enable_custom_post_types,
1145 __( 'Enable Custom Post Types Builder', 'frontblocks' ),
1146 array( $this, 'field_enable_custom_post_types' ),
1147 $this->page_slug,
1148 'frontblocks_section_custom_post_types'
1149 );
1150 }
1151
1152 // Note: License section is rendered separately outside the main form.
1153 // See render_license_section() method called from render_page().
1154
1155 do_action( 'frontblocks_register_settings' );
1156 }
1157
1158 /**
1159 * Render settings page.
1160 *
1161 * @return void
1162 */
1163 public function render_page() {
1164 if ( ! current_user_can( 'edit_theme_options' ) ) {
1165 return;
1166 }
1167 ?>
1168 <div class="frbl-settings-wrapper tw:min-h-screen tw:bg-gray-50 tw:py-8">
1169 <div class="tw:max-w-5xl tw:mx-auto tw:px-4 tw:sm:px-6 tw:lg:px-8">
1170 <!-- Header Section -->
1171 <div class="tw:mb-8 frbl-animate-slide-in">
1172 <div class="tw:flex tw:items-center tw:justify-between">
1173 <div>
1174 <h1 class="tw:text-3xl tw:font-bold tw:text-gray-900 tw:mb-2">
1175 <?php echo esc_html__( 'FrontBlocks Settings', 'frontblocks' ); ?>
1176 </h1>
1177 <p class="tw:text-gray-600">
1178 <?php echo esc_html__( 'Add visual enhancements to your website with FrontBlocks.', 'frontblocks' ); ?>
1179 </p>
1180 </div>
1181 <div class="tw:flex tw:items-center tw:space-x-2">
1182 <span class="tw:inline-flex tw:items-center tw:px-3 tw:py-1 tw:rounded-full tw:text-sm tw:font-medium tw:bg-primary-100 tw:text-primary-700">
1183 <?php echo esc_html__( 'Version', 'frontblocks' ) . ' ' . esc_html( FRBL_VERSION ); ?>
1184 </span>
1185 </div>
1186 </div>
1187 </div>
1188
1189 <?php
1190 // Show success message after settings are saved.
1191 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1192 if ( isset( $_GET['settings-updated'] ) && 'true' === sanitize_text_field( wp_unslash( $_GET['settings-updated'] ) ) ) :
1193 ?>
1194 <div style="background-color: #f0fdf4; border-left: 4px solid #4ade80; border-radius: 0.5rem; padding: 1rem; margin-bottom: 1.5rem; box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);">
1195 <div class="tw:flex">
1196 <div class="tw:flex-shrink-0">
1197 <svg class="tw:h-5 tw:w-5" style="color: #4ade80;" viewBox="0 0 20 20" fill="currentColor">
1198 <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
1199 </svg>
1200 </div>
1201 <div class="tw:ml-3">
1202 <p class="tw:text-sm tw:font-medium" style="color: #15803d; margin: 0;">
1203 <?php esc_html_e( 'Changes saved successfully', 'frontblocks' ); ?>
1204 </p>
1205 </div>
1206 </div>
1207 </div>
1208 <?php
1209 endif;
1210
1211 // Show error message if saving failed.
1212 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1213 if ( isset( $_GET['settings-error'] ) && 'true' === sanitize_text_field( wp_unslash( $_GET['settings-error'] ) ) ) :
1214 ?>
1215 <div style="background-color: #fef2f2; border-left: 4px solid #f87171; border-radius: 0.5rem; padding: 1rem; margin-bottom: 1.5rem; box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);">
1216 <div class="tw:flex">
1217 <div class="tw:flex-shrink-0">
1218 <svg class="tw:h-5 tw:w-5" style="color: #f87171;" viewBox="0 0 20 20" fill="currentColor">
1219 <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
1220 </svg>
1221 </div>
1222 <div class="tw:ml-3">
1223 <p class="tw:text-sm tw:font-medium" style="color: #991b1b; margin: 0;">
1224 <?php esc_html_e( 'Failed to save changes. Please try again.', 'frontblocks' ); ?>
1225 </p>
1226 </div>
1227 </div>
1228 </div>
1229 <?php
1230 endif;
1231
1232 // Show license activated message.
1233 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1234 if ( isset( $_GET['license_activated'] ) && '1' === sanitize_text_field( wp_unslash( $_GET['license_activated'] ) ) ) :
1235 ?>
1236 <div style="background-color: #f0fdf4; border-left: 4px solid #4ade80; border-radius: 0.5rem; padding: 1rem; margin-bottom: 1.5rem; box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);">
1237 <div class="tw:flex">
1238 <div class="tw:flex-shrink-0">
1239 <svg class="tw:h-5 tw:w-5" style="color: #4ade80;" viewBox="0 0 20 20" fill="currentColor">
1240 <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
1241 </svg>
1242 </div>
1243 <div class="tw:ml-3">
1244 <p class="tw:text-sm tw:font-medium" style="color: #15803d; margin: 0;">
1245 <?php esc_html_e( 'License activated successfully!', 'frontblocks' ); ?>
1246 </p>
1247 </div>
1248 </div>
1249 </div>
1250 <?php
1251 endif;
1252
1253 // Show license deactivated message.
1254 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1255 if ( isset( $_GET['license_deactivated'] ) && '1' === sanitize_text_field( wp_unslash( $_GET['license_deactivated'] ) ) ) :
1256 ?>
1257 <div style="background-color: #fffbeb; border-left: 4px solid #fbbf24; border-radius: 0.5rem; padding: 1rem; margin-bottom: 1.5rem; box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);">
1258 <div class="tw:flex">
1259 <div class="tw:flex-shrink-0">
1260 <svg class="tw:h-5 tw:w-5" style="color: #fbbf24;" viewBox="0 0 20 20" fill="currentColor">
1261 <path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
1262 </svg>
1263 </div>
1264 <div class="tw:ml-3">
1265 <p class="tw:text-sm tw:font-medium" style="color: #92400e; margin: 0;">
1266 <?php esc_html_e( 'License deactivated successfully.', 'frontblocks' ); ?>
1267 </p>
1268 </div>
1269 </div>
1270 </div>
1271 <?php
1272 endif;
1273
1274 // Show license error message.
1275 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1276 if ( isset( $_GET['license_error'] ) && '1' === sanitize_text_field( wp_unslash( $_GET['license_error'] ) ) ) :
1277 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1278 $error_msg = isset( $_GET['error_msg'] ) ? sanitize_text_field( wp_unslash( $_GET['error_msg'] ) ) : '';
1279 ?>
1280 <div style="background-color: #fef2f2; border-left: 4px solid #f87171; border-radius: 0.5rem; padding: 1rem; margin-bottom: 1.5rem; box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);">
1281 <div class="tw:flex">
1282 <div class="tw:flex-shrink-0">
1283 <svg class="tw:h-5 tw:w-5" style="color: #f87171;" viewBox="0 0 20 20" fill="currentColor">
1284 <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
1285 </svg>
1286 </div>
1287 <div class="tw:ml-3">
1288 <p class="tw:text-sm tw:font-medium" style="color: #991b1b; margin: 0;">
1289 <?php
1290 if ( ! empty( $error_msg ) ) {
1291 echo esc_html__( 'Failed to activate license: ', 'frontblocks' ) . '<br><strong>' . esc_html( $error_msg ) . '</strong>';
1292 } else {
1293 esc_html_e( 'Failed to activate license. Please check your license key and try again.', 'frontblocks' );
1294 }
1295 ?>
1296 </p>
1297 </div>
1298 </div>
1299 </div>
1300 <?php
1301 endif;
1302 ?>
1303
1304 <!-- Settings Form -->
1305 <form method="post" action="options.php" class="tw:space-y-6">
1306 <?php settings_fields( 'frontblocks_settings' ); ?>
1307
1308 <?php
1309 // Get all sections for this page.
1310 global $wp_settings_sections, $wp_settings_fields;
1311
1312 if ( ! isset( $wp_settings_sections[ $this->page_slug ] ) ) {
1313 return;
1314 }
1315
1316 foreach ( (array) $wp_settings_sections[ $this->page_slug ] as $section ) {
1317 $this->render_settings_section( $section );
1318 }
1319 ?>
1320
1321 <!-- Submit Button -->
1322 <div class="frbl-settings-save-bar tw:flex tw:items-center tw:justify-between">
1323 <div class="tw:text-sm tw:text-gray-500">
1324 <?php echo esc_html__( 'Changes will be applied immediately after saving.', 'frontblocks' ); ?>
1325 </div>
1326 <button type="submit" class="tw:inline-flex tw:items-center tw:px-4 tw:py-3 tw:border tw:border-transparent tw:text-base tw:font-medium tw:rounded-lg tw:shadow-sm tw:text-white tw:bg-primary-500 tw:hover:bg-primary-600 tw:focus:outline-none tw:focus:ring-2 tw:focus:ring-offset-2 tw:focus:ring-primary-500 tw:transition-colors tw:duration-200">
1327 <svg class="tw:w-5 tw:h-5 tw:mr-2 tw:-ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1328 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
1329 </svg>
1330 <?php echo esc_html__( 'Save Settings', 'frontblocks' ); ?>
1331 </button>
1332 </div>
1333 </form>
1334
1335 <?php
1336 // Render license section separately (outside main form) if PRO is active.
1337 if ( frbl_is_pro_active() ) {
1338 $this->render_license_section();
1339 }
1340 ?>
1341
1342 <!-- Footer Info -->
1343 <div class="tw:mt-8 tw:text-center tw:text-sm tw:text-gray-500">
1344 <?php
1345 printf(
1346 /* translators: %s: Close·marketing link */
1347 esc_html__( 'Made with ❤️ by %s', 'frontblocks' ),
1348 '<a href="https://close.technology/?utm_source=frontblocks&utm_medium=plugin&utm_campaign=settings" target="_blank" rel="noopener noreferrer" class="tw:text-primary-500 tw:hover:text-primary-600 tw:font-medium">Close·Technology</a>'
1349 );
1350 ?>
1351 </div>
1352
1353 <?php $this->render_debug_section(); ?>
1354 </div>
1355 </div>
1356 <?php
1357 }
1358
1359 /**
1360 * Render debug section for Fluid Typography.
1361 *
1362 * @return void
1363 */
1364 private function render_debug_section() {
1365 // Only show if Fluid Typography is enabled and user requested debug.
1366 $options = get_option( 'frontblocks_settings', array() );
1367 $enabled = ! empty( $options['enable_fluid_typography'] );
1368
1369 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1370 if ( ! $enabled || ! isset( $_GET['frbl_debug_typography'] ) ) {
1371 return;
1372 }
1373
1374 // Get GeneratePress settings.
1375 $gp_settings = get_option( 'generate_settings', array() );
1376
1377 // Filter only font-related settings.
1378 $font_settings = array();
1379 foreach ( $gp_settings as $key => $value ) {
1380 if ( strpos( $key, 'font' ) !== false || strpos( $key, 'heading' ) !== false ) {
1381 $font_settings[ $key ] = $value;
1382 }
1383 }
1384
1385 ?>
1386 <div class="tw:mt-8 tw:p-6 tw:bg-yellow-50 tw:border tw:border-yellow-200 tw:rounded-lg">
1387 <h3 class="tw:text-lg tw:font-semibold tw:text-gray-900 tw:mb-4">
1388 🐛 Debug: Fluid Typography Settings
1389 </h3>
1390 <p class="tw:text-sm tw:text-gray-600 tw:mb-4">
1391 <?php echo esc_html__( 'This shows the GeneratePress font settings being used by the Fluid Typography module.', 'frontblocks' ); ?>
1392 </p>
1393 <div class="tw:bg-white tw:p-4 tw:rounded tw:border tw:border-gray-300 tw:overflow-auto" style="max-height: 400px;">
1394 <pre style="margin: 0; font-size: 12px;"><?php print_r( $font_settings ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r ?></pre>
1395 </div>
1396 <p class="tw:text-xs tw:text-gray-500 tw:mt-4">
1397 <?php
1398 printf(
1399 /* translators: %s: URL parameter */
1400 esc_html__( 'To hide this debug info, remove %s from the URL.', 'frontblocks' ),
1401 '<code>?frbl_debug_typography=1</code>'
1402 );
1403 ?>
1404 </p>
1405 </div>
1406 <?php
1407 }
1408
1409 /**
1410 * Active Blocks section callback.
1411 *
1412 * @return void
1413 */
1414 private function section_active_blocks_callback() {
1415 $active_blocks = apply_filters(
1416 'frbl_active_blocks',
1417 array(
1418 array(
1419 'icon' => 'animations',
1420 'title' => __( 'Animations', 'frontblocks' ),
1421 'desc' => __( 'Add animations to any block using Animate.css', 'frontblocks' ),
1422 ),
1423 array(
1424 'icon' => 'carousel',
1425 'title' => __( 'Carousel/Slider', 'frontblocks' ),
1426 'desc' => __( 'Transform any Grid block into a carousel or slider', 'frontblocks' ),
1427 ),
1428 array(
1429 'icon' => 'gallery',
1430 'title' => __( 'Native Gallery', 'frontblocks' ),
1431 'desc' => __( 'Enhanced gallery block with carousel and masonry options', 'frontblocks' ),
1432 ),
1433 array(
1434 'icon' => 'sticky',
1435 'title' => __( 'Sticky Columns', 'frontblocks' ),
1436 'desc' => __( 'Make Grid blocks sticky when scrolling', 'frontblocks' ),
1437 ),
1438 array(
1439 'icon' => 'insert_post',
1440 'title' => __( 'Insert Post Block', 'frontblocks' ),
1441 'desc' => __( 'Display content from other posts, pages or custom post types', 'frontblocks' ),
1442 ),
1443 array(
1444 'icon' => 'counter',
1445 'title' => __( 'Counter Block', 'frontblocks' ),
1446 'desc' => __( 'Display animated counters with start and end values', 'frontblocks' ),
1447 ),
1448 array(
1449 'icon' => 'reading_time',
1450 'title' => __( 'Reading Time Block', 'frontblocks' ),
1451 'desc' => __( 'Show estimated reading time for posts', 'frontblocks' ),
1452 ),
1453 array(
1454 'icon' => 'stacked_images',
1455 'title' => __( 'Stacked Images Block', 'frontblocks' ),
1456 'desc' => __( 'Display images with animated stacking effect from different directions', 'frontblocks' ),
1457 ),
1458 array(
1459 'icon' => 'product_categories',
1460 'title' => __( 'Product Categories Block', 'frontblocks' ),
1461 'desc' => __( 'Display WooCommerce product categories', 'frontblocks' ),
1462 ),
1463 array(
1464 'icon' => 'headline_marquee',
1465 'title' => __( 'Headline Marquee', 'frontblocks' ),
1466 'desc' => __( 'Infinite scrolling marquee effect for headline/text blocks with customizable speed', 'frontblocks' ),
1467 ),
1468 array(
1469 'icon' => 'svg_upload',
1470 'title' => __( 'SVG Uploads', 'frontblocks' ),
1471 'desc' => __( 'Upload SVG files to the media library. Files are automatically sanitized to prevent security risks.', 'frontblocks' ),
1472 ),
1473 )
1474 );
1475
1476 $pro_blocks = apply_filters( 'frbl_pro_blocks', $this->get_default_pro_blocks() );
1477
1478 ?>
1479 <p class="tw:text-sm tw:text-gray-600 tw:mt-0 tw:mb-4">
1480 <?php echo esc_html__( 'These blocks and features are always active and available in the block editor.', 'frontblocks' ); ?>
1481 </p>
1482 <div class="frbl-features-grid">
1483 <?php
1484 foreach ( $active_blocks as $block ) {
1485 UI::show_info_card( $block['icon'], $block['title'], $block['desc'] );
1486 }
1487
1488 foreach ( $pro_blocks as $block ) {
1489 UI::show_pro_info_card( $block['icon'], $block['title'], $block['desc'] );
1490 }
1491 ?>
1492 </div>
1493 <?php
1494 }
1495
1496 /**
1497 * Features section callback.
1498 *
1499 * @return void
1500 */
1501 private function section_features_callback() {
1502 $license_valid = function_exists( 'frblp_is_license_valid' ) && frblp_is_license_valid();
1503 ?>
1504 <p class="tw:text-sm tw:text-gray-600 tw:mt-0 tw:mb-4">
1505 <?php echo esc_html__( 'Enable or disable these optional features as needed.', 'frontblocks' ); ?>
1506 </p>
1507 <?php
1508 }
1509
1510 /**
1511 * Maintenance mode section callback.
1512 *
1513 * @return void
1514 */
1515 private function section_maintenance_callback() {
1516 ?>
1517 <p class="tw:text-sm tw:text-gray-600 tw:mt-0 tw:mb-4">
1518 <?php echo esc_html__( 'Show a full-screen curtain page on every URL of the site while you work on it.', 'frontblocks' ); ?>
1519 </p>
1520 <?php
1521 }
1522
1523 /**
1524 * Cookie Notice section callback.
1525 *
1526 * @return void
1527 */
1528 private function section_cookie_notice_callback() {
1529 ?>
1530 <p class="tw:text-sm tw:text-gray-600 tw:mt-0 tw:mb-4">
1531 <?php echo esc_html__( 'Show a cookie consent banner and only load Google Tag Manager / GA4 after a visitor accepts.', 'frontblocks' ); ?>
1532 </p>
1533 <?php
1534 $this->render_advanced_cookies_upsell();
1535 }
1536
1537 /**
1538 * Show what FrontBlocks PRO's Advanced Cookie Management adds on top of
1539 * this banner. The feature's own settings (and the fields to configure
1540 * it) live entirely in the PRO plugin — this is only a plain message,
1541 * shown when PRO isn't active/licensed yet.
1542 *
1543 * @return void
1544 */
1545 private function render_advanced_cookies_upsell() {
1546 if ( frbl_is_pro_active() && $this->is_license_valid ) {
1547 // Already unlocked: the PRO plugin renders its own settings section
1548 // right below this one.
1549 return;
1550 }
1551
1552 if ( ! frbl_is_pro_active() ) {
1553 echo '<div class="tw:bg-blue-50 tw:border-l-4 tw:border-blue-400 tw:p-4 tw:mb-4">';
1554 echo '<p class="tw:text-sm tw:text-blue-700 tw:font-medium tw:mb-1">' . esc_html__( 'Want per-category consent?', 'frontblocks' ) . '</p>';
1555 echo '<p class="tw:text-sm tw:text-blue-700">';
1556 printf(
1557 /* translators: %s: FrontBlocks PRO link */
1558 esc_html__( '%s adds a "Customize" option so visitors can accept Analytics and Marketing cookies separately, with native Google Ads, Meta Pixel and Microsoft Clarity integrations that only load once accepted.', 'frontblocks' ),
1559 '<a href="https://close.technology/wordpress-plugins/frontblocks-pro/?utm_source=frontblocks&utm_medium=plugin&utm_campaign=settings-cookie-notice" target="_blank" rel="noopener noreferrer" class="tw:font-medium tw:underline">FrontBlocks PRO</a>'
1560 );
1561 echo '</p>';
1562 echo '</div>';
1563 } else {
1564 echo '<div class="tw:bg-yellow-50 tw:border-l-4 tw:border-yellow-400 tw:p-4 tw:mb-4">';
1565 echo '<p class="tw:text-sm tw:text-yellow-700">';
1566 printf(
1567 /* translators: %s: License section link */
1568 esc_html__( 'Advanced Cookie Management (per-category consent, Google Ads / Meta Pixel / Microsoft Clarity) is included with FrontBlocks PRO. Activate your license in the %s section below to unlock it.', 'frontblocks' ),
1569 '<a href="#frontblocks_section_license" class="tw:font-medium tw:underline">' . esc_html__( 'License', 'frontblocks' ) . '</a>'
1570 );
1571 echo '</p>';
1572 echo '</div>';
1573 }
1574 }
1575
1576 /**
1577 * Render a single settings section as a card.
1578 *
1579 * @param array $section Section data.
1580 * @return void
1581 */
1582 private function render_settings_section( $section ) {
1583 global $wp_settings_fields;
1584
1585 $has_fields = isset( $wp_settings_fields[ $this->page_slug ][ $section['id'] ] );
1586
1587 // Si no hay campos Y no hay callback, no renderizar nada.
1588 if ( ! $has_fields && ! $section['callback'] ) {
1589 return;
1590 }
1591
1592 // Check if this is a section with callback only (like active_blocks).
1593 $is_callback_only = ! $has_fields && $section['callback'];
1594
1595 // Check if this is a section that needs full width (rich fields, not a simple toggle grid).
1596 $is_cpt_section = in_array( $section['id'], array( 'frontblocks_section_custom_post_types', 'frontblocks_section_maintenance', 'frontblocks_section_cookie_notice' ), true );
1597
1598 // Show PRO CTA button before the Optional Features section.
1599 if ( 'frontblocks_section_features' === $section['id'] && ! $this->is_license_valid ) {
1600 ?>
1601 <div class="tw:mb-4">
1602 <a
1603 href="https://close.technology/wordpress-plugins/frontblocks-pro/?utm_source=frontblocks&utm_medium=plugin&utm_campaign=settings-optional-cta"
1604 target="_blank"
1605 rel="noopener noreferrer"
1606 class="tw:inline-flex tw:items-center tw:border tw:border-transparent tw:text-sm tw:font-medium tw:rounded-lg tw:shadow-sm tw:transition-colors tw:duration-200"
1607 style="background-color: #ef4444; color: #fff; padding: 10px 20px;"
1608 onmouseover="this.style.backgroundColor='#dc2626'"
1609 onmouseout="this.style.backgroundColor='#ef4444'"
1610 >
1611 <?php echo esc_html__( 'Get FrontBlocks PRO', 'frontblocks' ); ?>
1612 </a>
1613 </div>
1614 <?php
1615 }
1616
1617 if ( $is_callback_only ) {
1618 // Render section with only callback (no fields).
1619 ?>
1620 <div class="frbl-section-wrapper">
1621 <div class="frbl-section-header">
1622 <h2 class="tw:text-2xl tw:font-bold tw:text-gray-900 tw:mb-0">
1623 <?php echo esc_html( $section['title'] ); ?>
1624 </h2>
1625 </div>
1626 <?php call_user_func( $section['callback'], $section ); ?>
1627 </div>
1628 <?php
1629 return;
1630 }
1631
1632 if ( $is_cpt_section ) {
1633 // Render CPT section as a full-width card.
1634 ?>
1635 <div class="frbl-card tw:bg-white tw:rounded-lg tw:shadow-sm tw:border tw:border-gray-200 tw:overflow-hidden frbl-animate-slide-in tw:mb-8">
1636 <div class="tw:px-6 tw:py-5 tw:border-b tw:border-gray-200 tw:bg-gradient-to-r tw:from-gray-50 tw:to-white">
1637 <h2 class="tw:text-xl tw:font-semibold tw:text-gray-900">
1638 <?php echo esc_html( $section['title'] ); ?>
1639 </h2>
1640 <?php
1641 if ( $section['callback'] ) {
1642 echo '<div class="tw:mt-2 tw:text-sm tw:text-gray-600">';
1643 call_user_func( $section['callback'], $section );
1644 echo '</div>';
1645 }
1646 ?>
1647 </div>
1648 <div class="tw:px-6 tw:py-5">
1649 <?php
1650 foreach ( (array) $wp_settings_fields[ $this->page_slug ][ $section['id'] ] as $field ) {
1651 call_user_func( $field['callback'], $field['args'] );
1652 }
1653 ?>
1654 </div>
1655 </div>
1656 <?php
1657 } else {
1658 // Render regular sections with feature grid.
1659 ?>
1660 <div class="frbl-section-wrapper">
1661 <!-- Section Header -->
1662 <div class="frbl-section-header">
1663 <h2 class="tw:text-2xl tw:font-bold tw:text-gray-900 tw:mb-0">
1664 <?php echo esc_html( $section['title'] ); ?>
1665 </h2>
1666 <?php
1667 if ( $section['callback'] ) {
1668 echo '<div class="tw:text-sm tw:text-gray-600">';
1669 call_user_func( $section['callback'], $section );
1670 echo '</div>';
1671 }
1672 ?>
1673 </div>
1674
1675 <!-- Features Grid -->
1676 <div class="frbl-features-grid">
1677 <?php
1678 foreach ( (array) $wp_settings_fields[ $this->page_slug ][ $section['id'] ] as $field ) {
1679 $this->render_settings_field( $field );
1680 }
1681 ?>
1682 </div>
1683 </div>
1684 <?php
1685 }
1686 }
1687
1688 /**
1689 * Render a single settings field as a card.
1690 *
1691 * @param array $field Field data.
1692 * @return void
1693 */
1694 private function render_settings_field( $field ) {
1695 // Determine if this is a PRO feature (always, regardless of license status).
1696 $is_pro_feature = in_array(
1697 $field['id'],
1698 array(
1699 $this->option_enable_gutenberg,
1700 $this->option_enable_simple_prices_variable_products,
1701 $this->option_enable_after_add_to_cart,
1702 $this->option_deactivate_short_description,
1703 $this->option_move_content_to_short_description,
1704 $this->option_disable_zoom_images,
1705 $this->option_add_share_buttons,
1706 $this->option_deactivate_product_tabs,
1707 $this->option_horizontal_product_form,
1708 $this->option_enable_variant_display_mode,
1709 $this->option_enable_fullpage_scroll,
1710 $this->option_enable_language_banner,
1711 $this->option_enable_popups,
1712 $this->option_checkout_inline,
1713 $this->option_disable_cart_coupon,
1714 $this->option_disable_cart_cross_sells,
1715 $this->option_disable_checkout_coupon,
1716 $this->option_disable_checkout_order_notes,
1717 $this->option_disable_checkout_login_prompt,
1718 ),
1719 true
1720 );
1721
1722 // Apply PRO styling only if license is not valid.
1723 $needs_license = $is_pro_feature && ! $this->is_license_valid;
1724
1725 // Get icon for this feature.
1726 $icon = $this->get_feature_icon( $field['id'] );
1727
1728 // Short descriptions for each field.
1729 $descriptions = array(
1730 $this->option_enable_testimonials => __( 'Add a testimonials block to display customer reviews.', 'frontblocks' ),
1731 $this->option_enable_reading_progress => __( 'Show a progress bar at the top of the page while reading posts.', 'frontblocks' ),
1732 $this->option_enable_back_button => __( 'Add a floating back button for easy navigation.', 'frontblocks' ),
1733 $this->option_enable_scroll_top => __( 'Add a floating button to scroll back to the top of the page.', 'frontblocks' ),
1734 $this->option_enable_events => __( 'Register and display events using a CPT or blog posts.', 'frontblocks' ),
1735 $this->option_enable_fluid_typography => __( 'Font sizes scale smoothly between mobile and desktop using CSS clamp().', 'frontblocks' ),
1736 $this->option_enable_popups => __( 'Create popups with the block editor and configure when and where they appear.', 'frontblocks' ),
1737 $this->option_enable_gutenberg => __( 'Use the block editor to write WooCommerce product descriptions.', 'frontblocks' ),
1738 $this->option_enable_simple_prices_variable_products => __( 'Show a simplified price range for variable products.', 'frontblocks' ),
1739 $this->option_enable_after_add_to_cart => __( 'Insert custom block content right after the Add to Cart button.', 'frontblocks' ),
1740 $this->option_deactivate_short_description => __( 'Remove the short description field from product pages.', 'frontblocks' ),
1741 $this->option_move_content_to_short_description => __( 'Move the main product content into the short description area.', 'frontblocks' ),
1742 $this->option_disable_zoom_images => __( 'Remove the zoom effect on WooCommerce product images.', 'frontblocks' ),
1743 $this->option_add_share_buttons => __( 'Add social share buttons to WooCommerce product pages.', 'frontblocks' ),
1744 $this->option_deactivate_product_tabs => __( 'Remove the default description, reviews and attributes tabs.', 'frontblocks' ),
1745 $this->option_horizontal_product_form => __( 'Display quantity and Add to Cart button side by side.', 'frontblocks' ),
1746 $this->option_enable_variant_display_mode => __( 'Choose per attribute whether to display product variations as a dropdown or radio buttons.', 'frontblocks' ),
1747 $this->option_enable_fullpage_scroll => __( 'Enable full-page scroll navigation between sections.', 'frontblocks' ),
1748 $this->option_enable_language_banner => __( 'Show a banner when the visitor language differs from the site language.', 'frontblocks' ),
1749 $this->option_checkout_inline => __( 'Display address, email and phone fields side by side in the WooCommerce checkout.', 'frontblocks' ),
1750 $this->option_disable_zoom_images => __( 'Remove the zoom effect on WooCommerce product images.', 'frontblocks' ),
1751 $this->option_add_share_buttons => __( 'Add social share buttons to WooCommerce product pages.', 'frontblocks' ),
1752 $this->option_deactivate_product_tabs => __( 'Remove the default description, reviews and attributes tabs.', 'frontblocks' ),
1753 $this->option_horizontal_product_form => __( 'Display quantity and Add to Cart button side by side.', 'frontblocks' ),
1754 $this->option_enable_fullpage_scroll => __( 'Enable full-page scroll navigation between sections.', 'frontblocks' ),
1755 $this->option_enable_language_banner => __( 'Show a banner when the visitor language differs from the site language.', 'frontblocks' ),
1756 $this->option_disable_cart_coupon => __( 'Hide the coupon code input field from the Cart page.', 'frontblocks' ),
1757 $this->option_disable_cart_cross_sells => __( 'Remove the cross-sell product suggestions from the Cart page.', 'frontblocks' ),
1758 $this->option_disable_checkout_coupon => __( 'Hide the coupon code form shown above the Checkout form.', 'frontblocks' ),
1759 $this->option_disable_checkout_order_notes => __( 'Remove the order notes / additional information field from Checkout.', 'frontblocks' ),
1760 $this->option_disable_checkout_login_prompt => __( 'Hide the login and registration prompt shown before the Checkout form.', 'frontblocks' ),
1761 );
1762
1763 $desc = $descriptions[ $field['id'] ] ?? '';
1764
1765 ?>
1766 <div class="frbl-feature-card <?php echo $needs_license ? 'frbl-feature-pro' : ''; ?>">
1767 <?php if ( $is_pro_feature ) : ?>
1768 <div class="frbl-pro-badge">PRO</div>
1769 <?php endif; ?>
1770
1771 <div class="frbl-feature-content">
1772 <div class="frbl-feature-icon">
1773 <?php echo $icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
1774 </div>
1775 <div class="frbl-feature-info">
1776 <h3 class="frbl-feature-title">
1777 <?php echo esc_html( $field['title'] ); ?>
1778 </h3>
1779 <?php if ( $desc ) : ?>
1780 <p class="frbl-feature-description">
1781 <?php echo esc_html( $desc ); ?>
1782 </p>
1783 <?php endif; ?>
1784 </div>
1785 <div class="frbl-feature-toggle">
1786 <?php call_user_func( $field['callback'], $field['args'] ); ?>
1787 </div>
1788 </div>
1789 </div>
1790 <?php
1791 }
1792
1793 /**
1794 * Get icon SVG for a feature.
1795 *
1796 * @param string $field_id Field ID.
1797 * @return string SVG icon markup.
1798 */
1799 private function get_feature_icon( $field_id ) {
1800 // Map field IDs to icon file names.
1801 $icon_map = array(
1802 $this->option_enable_testimonials => 'testimonials',
1803 $this->option_enable_reading_progress => 'reading-progress',
1804 $this->option_enable_back_button => 'back-button',
1805 $this->option_enable_scroll_top => 'scroll-top',
1806 $this->option_enable_events => 'events',
1807 $this->option_enable_fluid_typography => 'fluid-typography',
1808 $this->option_enable_popups => 'popups',
1809 $this->option_enable_gutenberg => 'gutenberg',
1810 $this->option_enable_simple_prices_variable_products => 'simple-prices',
1811 $this->option_enable_after_add_to_cart => 'after-add-to-cart',
1812 $this->option_deactivate_short_description => 'deactivate-description',
1813 $this->option_move_content_to_short_description => 'move-content',
1814 $this->option_disable_zoom_images => 'disable-zoom',
1815 $this->option_add_share_buttons => 'share-buttons',
1816 $this->option_deactivate_product_tabs => 'deactivate-tabs',
1817 $this->option_horizontal_product_form => 'horizontal-form',
1818 $this->option_enable_variant_display_mode => 'default',
1819 $this->option_enable_fullpage_scroll => 'fullpage-scroll',
1820 $this->option_enable_language_banner => 'language-banner',
1821 $this->option_checkout_inline => 'horizontal-form',
1822 $this->option_disable_zoom_images => 'disable-zoom',
1823 $this->option_add_share_buttons => 'share-buttons',
1824 $this->option_deactivate_product_tabs => 'deactivate-tabs',
1825 $this->option_horizontal_product_form => 'horizontal-form',
1826 $this->option_enable_fullpage_scroll => 'fullpage-scroll',
1827 $this->option_enable_language_banner => 'language-banner',
1828 $this->option_disable_cart_coupon => 'default',
1829 $this->option_disable_cart_cross_sells => 'default',
1830 $this->option_disable_checkout_coupon => 'default',
1831 $this->option_disable_checkout_order_notes => 'default',
1832 $this->option_disable_checkout_login_prompt => 'default',
1833 );
1834
1835 $icon_name = $icon_map[ $field_id ] ?? 'default';
1836 $icon_path = FRBL_PLUGIN_PATH . 'assets/admin/icons/' . $icon_name . '.svg';
1837
1838 if ( file_exists( $icon_path ) ) {
1839 $svg_content = file_get_contents( $icon_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
1840 return $svg_content ? $svg_content : '';
1841 }
1842
1843 return '';
1844 }
1845
1846 /**
1847 * PRO Features section description.
1848 *
1849 * @return void
1850 */
1851 public function section_woo_features_callback() {
1852 if ( ! frbl_is_pro_active() ) {
1853 echo '<div class="tw:bg-blue-50 tw:border-l-4 tw:border-blue-400 tw:p-4 tw:mb-4">';
1854 echo '<div class="tw:flex">';
1855 echo '<div class="tw:flex-shrink-0">';
1856 echo '<svg class="tw:h-5 tw:w-5 tw:text-blue-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/></svg>';
1857 echo '</div>';
1858 echo '<div class="tw:ml-3">';
1859 echo '<p class="tw:text-sm tw:text-blue-700">';
1860 printf(
1861 /* translators: %s: FrontBlocks PRO link */
1862 esc_html__( 'These features require %s. Upgrade to unlock advanced functionality.', 'frontblocks' ),
1863 '<a href="https://close.technology/wordpress-plugins/frontblocks-pro/?utm_source=frontblocks&utm_medium=plugin&utm_campaign=settings" target="_blank" class="tw:font-medium tw:underline">FrontBlocks PRO</a>'
1864 );
1865 echo '</p>';
1866 echo '</div>';
1867 echo '</div>';
1868 echo '</div>';
1869 } elseif ( ! $this->is_license_valid ) {
1870 echo '<div class="tw:bg-yellow-50 tw:border-l-4 tw:border-yellow-400 tw:p-4 tw:mb-4">';
1871 echo '<div class="tw:flex">';
1872 echo '<div class="tw:flex-shrink-0">';
1873 echo '<svg class="tw:h-5 tw:w-5 tw:text-yellow-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>';
1874 echo '</div>';
1875 echo '<div class="tw:ml-3">';
1876 echo '<p class="tw:text-sm tw:text-yellow-700">';
1877 printf(
1878 /* translators: %s: License section link */
1879 esc_html__( 'License is not activated. Please activate your license in the %s section below to enable these features.', 'frontblocks' ),
1880 '<a href="#frontblocks_section_license" class="tw:font-medium tw:underline">' . esc_html__( 'License', 'frontblocks' ) . '</a>'
1881 );
1882 echo '</p>';
1883 echo '</div>';
1884 echo '</div>';
1885 echo '</div>';
1886 } else {
1887 ?>
1888 <p class="tw:text-sm tw:text-gray-600 tw:mt-0 tw:mb-4">
1889 <?php echo esc_html__( 'Advanced features for WooCommerce and more.', 'frontblocks' ); ?>
1890 </p>
1891 <?php
1892 }
1893 }
1894
1895 /**
1896 * Render toggle field for enable testimonials.
1897 *
1898 * @return void
1899 */
1900 public function field_enable_testimonials() {
1901 $options = get_option( 'frontblocks_settings', array() );
1902 $enabled = (bool) ( $options[ $this->option_enable_testimonials ] ?? false );
1903 ?>
1904 <label class="frbl-toggle">
1905 <input type="checkbox"
1906 id="<?php echo esc_attr( $this->option_enable_testimonials ); ?>"
1907 name="frontblocks_settings[<?php echo esc_attr( $this->option_enable_testimonials ); ?>]"
1908 value="1"
1909 <?php checked( true, $enabled ); ?>
1910 />
1911 <span></span>
1912 </label>
1913 <?php
1914 }
1915
1916 /**
1917 * Render toggle field for enable reading progress bar.
1918 *
1919 * @return void
1920 */
1921 public function field_enable_reading_progress() {
1922 $options = get_option( 'frontblocks_settings', array() );
1923 $enabled = (bool) ( $options[ $this->option_enable_reading_progress ] ?? false );
1924 ?>
1925 <label class="frbl-toggle">
1926 <input type="checkbox"
1927 id="<?php echo esc_attr( $this->option_enable_reading_progress ); ?>"
1928 name="frontblocks_settings[<?php echo esc_attr( $this->option_enable_reading_progress ); ?>]"
1929 value="1"
1930 <?php checked( true, $enabled ); ?>
1931 />
1932 <span></span>
1933 </label>
1934 <?php
1935 }
1936
1937 /**
1938 * Render toggle field for enable back button.
1939 *
1940 * @return void
1941 */
1942 public function field_enable_back_button() {
1943 $options = get_option( 'frontblocks_settings', array() );
1944 $enabled = (bool) ( $options[ $this->option_enable_back_button ] ?? false );
1945 ?>
1946 <label class="frbl-toggle">
1947 <input type="checkbox"
1948 id="<?php echo esc_attr( $this->option_enable_back_button ); ?>"
1949 name="frontblocks_settings[<?php echo esc_attr( $this->option_enable_back_button ); ?>]"
1950 value="1"
1951 <?php checked( true, $enabled ); ?>
1952 />
1953 <span></span>
1954 </label>
1955 <?php
1956 }
1957
1958 /**
1959 * Render toggle field and sub-settings for scroll to top.
1960 *
1961 * @return void
1962 */
1963 public function field_enable_scroll_top() {
1964 $options = get_option( 'frontblocks_settings', array() );
1965 $enabled = (bool) ( $options[ $this->option_enable_scroll_top ] ?? false );
1966 $position = sanitize_text_field( $options[ $this->option_scroll_top_position ] ?? 'bottom-right' );
1967 $icon_url = esc_url( $options[ $this->option_scroll_top_icon_url ] ?? '' );
1968 ?>
1969 <label class="frbl-toggle">
1970 <input type="checkbox"
1971 id="<?php echo esc_attr( $this->option_enable_scroll_top ); ?>"
1972 name="frontblocks_settings[<?php echo esc_attr( $this->option_enable_scroll_top ); ?>]"
1973 value="1"
1974 <?php checked( true, $enabled ); ?>
1975 />
1976 <span></span>
1977 </label>
1978
1979 <!-- Sub-settings - moved below the card by JavaScript -->
1980 <div id="scroll-top-settings-wrapper" style="<?php echo $enabled ? '' : 'display: none;'; ?>">
1981 <div class="tw-mb-4">
1982 <label for="<?php echo esc_attr( $this->option_scroll_top_position ); ?>" class="tw-block tw-text-sm tw-font-medium tw-text-gray-700 tw-mb-2">
1983 <?php echo esc_html__( 'Position', 'frontblocks' ); ?>
1984 </label>
1985 <select
1986 id="<?php echo esc_attr( $this->option_scroll_top_position ); ?>"
1987 name="frontblocks_settings[<?php echo esc_attr( $this->option_scroll_top_position ); ?>]"
1988 class="tw-block tw-pl-3 tw-pr-8 tw-py-2 tw-border tw-border-gray-300 tw-rounded-lg tw-text-sm focus:tw-outline-none focus:tw-ring-2 focus:tw-ring-primary-500 focus:tw-border-transparent"
1989 >
1990 <option value="bottom-right" <?php selected( $position, 'bottom-right' ); ?>>
1991 <?php echo esc_html__( 'Bottom right', 'frontblocks' ); ?>
1992 </option>
1993 <option value="bottom-left" <?php selected( $position, 'bottom-left' ); ?>>
1994 <?php echo esc_html__( 'Bottom left', 'frontblocks' ); ?>
1995 </option>
1996 </select>
1997 </div>
1998
1999 <div>
2000 <label class="tw-block tw-text-sm tw-font-medium tw-text-gray-700 tw-mb-2">
2001 <?php echo esc_html__( 'Custom icon (optional)', 'frontblocks' ); ?>
2002 </label>
2003 <div id="scroll-top-icon-preview" class="tw-mb-2" style="<?php echo $icon_url ? '' : 'display: none;'; ?>">
2004 <img id="scroll-top-icon-img" src="<?php echo esc_url( $icon_url ); ?>" alt="" style="width: 48px; height: 48px; object-fit: contain; border: 1px solid #e5e7eb; border-radius: 0.375rem; padding: 4px;" />
2005 </div>
2006 <input type="hidden" id="scroll_top_icon_url" name="frontblocks_settings[<?php echo esc_attr( $this->option_scroll_top_icon_url ); ?>]" value="<?php echo esc_attr( $icon_url ); ?>" />
2007 <div class="tw-flex tw-gap-2 tw-items-center">
2008 <button type="button" id="scroll-top-icon-upload" class="tw-px-3 tw-py-1.5 tw-text-sm tw-border tw-border-gray-300 tw-rounded-lg tw-bg-white hover:tw-bg-gray-50 tw-text-gray-700 tw-transition-colors">
2009 <?php echo esc_html__( 'Select image', 'frontblocks' ); ?>
2010 </button>
2011 <button type="button" id="scroll-top-icon-remove" class="tw-px-3 tw-py-1.5 tw-text-sm tw-border tw-border-red-200 tw-rounded-lg tw-bg-white hover:tw-bg-red-50 tw-text-red-600 tw-transition-colors" style="<?php echo $icon_url ? '' : 'display: none;'; ?>">
2012 <?php echo esc_html__( 'Remove', 'frontblocks' ); ?>
2013 </button>
2014 </div>
2015 <p class="tw-text-xs tw-text-gray-500 tw-mt-2">
2016 <?php echo esc_html__( 'Upload an SVG, PNG or any image. Leave empty to use the default arrow icon.', 'frontblocks' ); ?>
2017 </p>
2018 </div>
2019 </div>
2020 <?php
2021 }
2022
2023 /**
2024 * Render toggle field for enable events.
2025 *
2026 * @return void
2027 */
2028 public function field_enable_events() {
2029 $options = get_option( 'frontblocks_settings', array() );
2030 $enabled = (bool) ( $options[ $this->option_enable_events ] ?? false );
2031 $events_type = sanitize_text_field( $options[ $this->option_events_type ] ?? 'cpt' );
2032 ?>
2033 <!-- Toggle - stays in horizontal layout with icon and text -->
2034 <label class="frbl-toggle">
2035 <input type="checkbox"
2036 id="<?php echo esc_attr( $this->option_enable_events ); ?>"
2037 name="frontblocks_settings[<?php echo esc_attr( $this->option_enable_events ); ?>]"
2038 value="1"
2039 <?php checked( true, $enabled ); ?>
2040 />
2041 <span></span>
2042 </label>
2043
2044 <!-- Select and description - will be moved below the card by JavaScript -->
2045 <div id="events-type-wrapper" class="tw:mt-4" style="<?php echo $enabled ? 'width: 100%; min-width: 100%; display: block;' : 'display: none;'; ?>">
2046 <label for="<?php echo esc_attr( $this->option_events_type ); ?>" class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mb-2">
2047 <?php echo esc_html__( 'Event type', 'frontblocks' ); ?>
2048 </label>
2049 <select
2050 id="<?php echo esc_attr( $this->option_events_type ); ?>"
2051 name="frontblocks_settings[<?php echo esc_attr( $this->option_events_type ); ?>]"
2052 class="tw:block tw:w-full tw:px-3 tw:py-2 tw:border tw:border-gray-300 tw:rounded-lg tw:text-base tw:focus:outline-none tw:focus:ring-2 tw:focus:ring-primary-500 tw:focus:border-transparent"
2053 style="width: 100%; min-width: 100%; max-width: 100%; box-sizing: border-box;"
2054 >
2055 <option value="cpt" <?php selected( $events_type, 'cpt' ); ?>>
2056 <?php echo esc_html__( 'Custom Post Type (CPT)', 'frontblocks' ); ?>
2057 </option>
2058 <option value="posts" <?php selected( $events_type, 'posts' ); ?>>
2059 <?php echo esc_html__( 'Blog posts', 'frontblocks' ); ?>
2060 </option>
2061 </select>
2062 <p class="tw:text-xs tw:text-gray-500 tw:mt-2">
2063 <?php echo esc_html__( 'Choose whether events will be created in a dedicated CPT or in regular blog posts.', 'frontblocks' ); ?>
2064 </p>
2065 </div>
2066 <?php
2067 }
2068
2069 /**
2070 * Render toggle field for enable fluid typography.
2071 *
2072 * @return void
2073 */
2074 public function field_enable_fluid_typography() {
2075 $options = get_option( 'frontblocks_settings', array() );
2076 $enabled = (bool) ( $options[ $this->option_enable_fluid_typography ] ?? true );
2077 ?>
2078 <label class="frbl-toggle">
2079 <input type="checkbox"
2080 id="<?php echo esc_attr( $this->option_enable_fluid_typography ); ?>"
2081 name="frontblocks_settings[<?php echo esc_attr( $this->option_enable_fluid_typography ); ?>]"
2082 value="1"
2083 <?php checked( true, $enabled ); ?>
2084 />
2085 <span></span>
2086 </label>
2087 <?php
2088 }
2089
2090 /**
2091 * Render toggle field for enable maintenance mode.
2092 *
2093 * @return void
2094 */
2095 public function field_enable_maintenance() {
2096 $options = get_option( 'frontblocks_settings', array() );
2097 $enabled = (bool) ( $options[ $this->option_enable_maintenance ] ?? false );
2098 $title = (string) ( $options[ $this->option_maintenance_title ] ?? '' );
2099 $image_id = (int) ( $options[ $this->option_maintenance_image ] ?? 0 );
2100 $image_url = $image_id ? wp_get_attachment_image_url( $image_id, 'medium' ) : '';
2101 ?>
2102 <div class="frbl-maintenance-wrapper">
2103 <div class="tw:flex tw:items-center tw:justify-between tw:mb-4">
2104 <label for="<?php echo esc_attr( $this->option_enable_maintenance ); ?>" class="tw:text-base tw:font-medium tw:text-gray-900">
2105 <?php echo esc_html__( 'Enable Maintenance Mode', 'frontblocks' ); ?>
2106 </label>
2107 <label class="frbl-toggle">
2108 <input type="checkbox"
2109 id="<?php echo esc_attr( $this->option_enable_maintenance ); ?>"
2110 name="frontblocks_settings[<?php echo esc_attr( $this->option_enable_maintenance ); ?>]"
2111 value="1"
2112 <?php checked( true, $enabled ); ?>
2113 />
2114 <span></span>
2115 </label>
2116 </div>
2117
2118 <div id="maintenance-fields-wrapper" style="<?php echo $enabled ? '' : 'display: none;'; ?>">
2119 <div class="tw:p-4 tw:bg-gray-50 tw:rounded-lg tw:border tw:border-gray-200">
2120 <label for="<?php echo esc_attr( $this->option_maintenance_title ); ?>" class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mb-2">
2121 <?php echo esc_html__( 'Maintenance page title', 'frontblocks' ); ?>
2122 </label>
2123 <input
2124 type="text"
2125 id="<?php echo esc_attr( $this->option_maintenance_title ); ?>"
2126 name="frontblocks_settings[<?php echo esc_attr( $this->option_maintenance_title ); ?>]"
2127 value="<?php echo esc_attr( $title ); ?>"
2128 placeholder="<?php echo esc_attr__( 'We are currently performing maintenance', 'frontblocks' ); ?>"
2129 class="tw:block tw:w-full tw:px-3 tw:py-2 tw:border tw:border-gray-300 tw:rounded-lg tw:text-base tw:focus:outline-none tw:focus:ring-2 tw:focus:ring-primary-500 tw:focus:border-transparent"
2130 />
2131
2132 <label class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mt-4 tw:mb-2">
2133 <?php echo esc_html__( 'Background image', 'frontblocks' ); ?>
2134 </label>
2135 <input type="hidden" id="<?php echo esc_attr( $this->option_maintenance_image ); ?>" name="frontblocks_settings[<?php echo esc_attr( $this->option_maintenance_image ); ?>]" value="<?php echo esc_attr( $image_id ); ?>" />
2136 <div class="frbl-maintenance-image-preview tw:mb-2" style="<?php echo $image_url ? '' : 'display:none;'; ?>">
2137 <img src="<?php echo esc_url( $image_url ? $image_url : '' ); ?>" alt="" style="max-width: 200px; height: auto; border-radius: 8px; display: block;" />
2138 </div>
2139 <div class="tw:flex tw:gap-2">
2140 <button type="button" class="button frbl-maintenance-select-image">
2141 <?php echo esc_html__( 'Select image', 'frontblocks' ); ?>
2142 </button>
2143 <button type="button" class="button frbl-maintenance-remove-image" style="<?php echo $image_url ? '' : 'display:none;'; ?>">
2144 <?php echo esc_html__( 'Remove image', 'frontblocks' ); ?>
2145 </button>
2146 </div>
2147 <p class="tw:text-xs tw:text-gray-500 tw:mt-2 tw:mb-0">
2148 <?php echo esc_html__( 'Shown as the full-screen background while maintenance mode is active. Recommended size: 1920×1080px.', 'frontblocks' ); ?>
2149 </p>
2150 </div>
2151 </div>
2152 </div>
2153 <?php
2154 }
2155
2156 /**
2157 * Render toggle field and sub-settings for the cookie notice banner.
2158 *
2159 * @return void
2160 */
2161 public function field_enable_cookie_notice() {
2162 $options = get_option( 'frontblocks_settings', array() );
2163 $enabled = (bool) ( $options[ $this->option_enable_cookie_notice ] ?? false );
2164 $message = (string) ( $options[ $this->option_cookie_notice_message ] ?? '' );
2165 $accept_label = (string) ( $options[ $this->option_cookie_notice_accept_label ] ?? '' );
2166 $reject_label = (string) ( $options[ $this->option_cookie_notice_reject_label ] ?? '' );
2167 $policy_page_id = (int) ( $options[ $this->option_cookie_notice_policy_page_id ] ?? 0 );
2168 $layout = (string) ( $options[ $this->option_cookie_notice_layout ] ?? 'bar' );
2169 $position = (string) ( $options[ $this->option_cookie_notice_position ] ?? 'bottom-right' );
2170 $color = (string) ( $options[ $this->option_cookie_notice_color ] ?? '#687df9' );
2171 $expiration = (int) ( $options[ $this->option_cookie_notice_expiration_days ] ?? 365 );
2172 $gtm_id = (string) ( $options[ $this->option_cookie_notice_gtm_id ] ?? '' );
2173 $ga4_id = (string) ( $options[ $this->option_cookie_notice_ga4_id ] ?? '' );
2174 $accepted_count = (int) get_option( \FrontBlocks\Frontend\CookieNotice::STATS_OPTION_ACCEPTED, 0 );
2175 $rejected_count = (int) get_option( \FrontBlocks\Frontend\CookieNotice::STATS_OPTION_REJECTED, 0 );
2176 $total_count = $accepted_count + $rejected_count;
2177 $acceptance_pct = $total_count > 0 ? round( ( $accepted_count / $total_count ) * 100, 1 ) : 0;
2178 ?>
2179 <div class="frbl-cookie-notice-wrapper">
2180 <div class="tw:flex tw:items-center tw:justify-between tw:mb-4">
2181 <label for="<?php echo esc_attr( $this->option_enable_cookie_notice ); ?>" class="tw:text-base tw:font-medium tw:text-gray-900">
2182 <?php echo esc_html__( 'Enable Cookie Notice', 'frontblocks' ); ?>
2183 </label>
2184 <label class="frbl-toggle">
2185 <input type="checkbox"
2186 id="<?php echo esc_attr( $this->option_enable_cookie_notice ); ?>"
2187 name="frontblocks_settings[<?php echo esc_attr( $this->option_enable_cookie_notice ); ?>]"
2188 value="1"
2189 <?php checked( true, $enabled ); ?>
2190 />
2191 <span></span>
2192 </label>
2193 </div>
2194
2195 <div id="cookie-notice-fields-wrapper" style="<?php echo $enabled ? '' : 'display: none;'; ?>">
2196
2197 <?php if ( $total_count > 0 ) : ?>
2198 <div class="tw:flex tw:gap-4 tw:mb-4 tw:p-4 tw:bg-gray-50 tw:rounded-lg tw:border tw:border-gray-200">
2199 <div class="tw:flex-1">
2200 <p class="tw:text-xs tw:text-gray-500 tw:mb-1"><?php echo esc_html__( 'Acceptance rate', 'frontblocks' ); ?></p>
2201 <p class="tw:text-2xl tw:font-bold tw:text-gray-900 tw:m-0"><?php echo esc_html( $acceptance_pct ); ?>%</p>
2202 </div>
2203 <div class="tw:flex-1">
2204 <p class="tw:text-xs tw:text-gray-500 tw:mb-1"><?php echo esc_html__( 'Accepted', 'frontblocks' ); ?></p>
2205 <p class="tw:text-2xl tw:font-bold tw:text-gray-900 tw:m-0"><?php echo esc_html( $accepted_count ); ?></p>
2206 </div>
2207 <div class="tw:flex-1">
2208 <p class="tw:text-xs tw:text-gray-500 tw:mb-1"><?php echo esc_html__( 'Rejected', 'frontblocks' ); ?></p>
2209 <p class="tw:text-2xl tw:font-bold tw:text-gray-900 tw:m-0"><?php echo esc_html( $rejected_count ); ?></p>
2210 </div>
2211 </div>
2212 <?php endif; ?>
2213
2214 <div class="tw:p-4 tw:bg-gray-50 tw:rounded-lg tw:border tw:border-gray-200 tw:mb-4">
2215 <label for="<?php echo esc_attr( $this->option_cookie_notice_message ); ?>" class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mb-2">
2216 <?php echo esc_html__( 'Message', 'frontblocks' ); ?>
2217 </label>
2218 <textarea
2219 id="<?php echo esc_attr( $this->option_cookie_notice_message ); ?>"
2220 name="frontblocks_settings[<?php echo esc_attr( $this->option_cookie_notice_message ); ?>]"
2221 rows="3"
2222 placeholder="<?php echo esc_attr__( 'We use cookies to improve your experience on our website. By browsing this website, you agree to our use of cookies.', 'frontblocks' ); ?>"
2223 class="tw:block tw:w-full tw:px-3 tw:py-2 tw:border tw:border-gray-300 tw:rounded-lg tw:text-base tw:focus:outline-none tw:focus:ring-2 tw:focus:ring-primary-500 tw:focus:border-transparent"
2224 ><?php echo esc_textarea( $message ); ?></textarea>
2225
2226 <div class="tw:grid tw:grid-cols-2 tw:gap-4 tw:mt-4">
2227 <div>
2228 <label for="<?php echo esc_attr( $this->option_cookie_notice_accept_label ); ?>" class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mb-2">
2229 <?php echo esc_html__( 'Accept button label', 'frontblocks' ); ?>
2230 </label>
2231 <input
2232 type="text"
2233 id="<?php echo esc_attr( $this->option_cookie_notice_accept_label ); ?>"
2234 name="frontblocks_settings[<?php echo esc_attr( $this->option_cookie_notice_accept_label ); ?>]"
2235 value="<?php echo esc_attr( $accept_label ); ?>"
2236 placeholder="<?php echo esc_attr__( 'Accept', 'frontblocks' ); ?>"
2237 class="tw:block tw:w-full tw:px-3 tw:py-2 tw:border tw:border-gray-300 tw:rounded-lg tw:text-base tw:focus:outline-none tw:focus:ring-2 tw:focus:ring-primary-500 tw:focus:border-transparent"
2238 />
2239 </div>
2240 <div>
2241 <label for="<?php echo esc_attr( $this->option_cookie_notice_reject_label ); ?>" class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mb-2">
2242 <?php echo esc_html__( 'Reject button label', 'frontblocks' ); ?>
2243 </label>
2244 <input
2245 type="text"
2246 id="<?php echo esc_attr( $this->option_cookie_notice_reject_label ); ?>"
2247 name="frontblocks_settings[<?php echo esc_attr( $this->option_cookie_notice_reject_label ); ?>]"
2248 value="<?php echo esc_attr( $reject_label ); ?>"
2249 placeholder="<?php echo esc_attr__( 'Reject', 'frontblocks' ); ?>"
2250 class="tw:block tw:w-full tw:px-3 tw:py-2 tw:border tw:border-gray-300 tw:rounded-lg tw:text-base tw:focus:outline-none tw:focus:ring-2 tw:focus:ring-primary-500 tw:focus:border-transparent"
2251 />
2252 </div>
2253 </div>
2254
2255 <label for="<?php echo esc_attr( $this->option_cookie_notice_policy_page_id ); ?>" class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mt-4 tw:mb-2">
2256 <?php echo esc_html__( 'Cookie policy page (optional)', 'frontblocks' ); ?>
2257 </label>
2258 <select
2259 id="<?php echo esc_attr( $this->option_cookie_notice_policy_page_id ); ?>"
2260 name="frontblocks_settings[<?php echo esc_attr( $this->option_cookie_notice_policy_page_id ); ?>]"
2261 class="tw:block tw:w-full tw:px-3 tw:py-2 tw:border tw:border-gray-300 tw:rounded-lg tw:text-base tw:focus:outline-none tw:focus:ring-2 tw:focus:ring-primary-500 tw:focus:border-transparent"
2262 >
2263 <option value=""><?php echo esc_html__( '— None —', 'frontblocks' ); ?></option>
2264 <?php
2265 $pages_limit = 300;
2266 $pages = get_pages(
2267 array(
2268 'sort_column' => 'post_title',
2269 'number' => $pages_limit,
2270 )
2271 );
2272
2273 // The saved page may fall outside the limited list above (e.g. sorted
2274 // past it alphabetically on a large site) — make sure it still shows up
2275 // and stays selected instead of silently disappearing from the dropdown.
2276 $selected_page_listed = 0 === $policy_page_id;
2277 foreach ( $pages as $page ) {
2278 if ( $policy_page_id === $page->ID ) {
2279 $selected_page_listed = true;
2280 break;
2281 }
2282 }
2283
2284 if ( ! $selected_page_listed ) {
2285 $selected_page = get_post( $policy_page_id );
2286 if ( $selected_page instanceof \WP_Post ) {
2287 array_unshift( $pages, $selected_page );
2288 }
2289 }
2290
2291 foreach ( $pages as $page ) {
2292 printf(
2293 '<option value="%1$d" %2$s>%3$s</option>',
2294 (int) $page->ID,
2295 selected( $policy_page_id, $page->ID, false ),
2296 esc_html( $page->post_title )
2297 );
2298 }
2299 ?>
2300 </select>
2301 <?php if ( count( $pages ) >= $pages_limit ) : ?>
2302 <p class="tw:text-xs tw:text-amber-600 tw:mt-2">
2303 <?php
2304 printf(
2305 /* translators: %d: number of pages shown in the dropdown. */
2306 esc_html__( 'Showing the first %d pages. If the page you need is missing, search for it in the Pages list to find its ID and set it via the frontblocks_settings option.', 'frontblocks' ),
2307 (int) $pages_limit
2308 );
2309 ?>
2310 </p>
2311 <?php endif; ?>
2312 <p class="tw:text-xs tw:text-gray-500 tw:mt-2">
2313 <?php echo esc_html__( 'The banner is hidden on this page so visitors can read it before deciding. A "Learn more" link to it is added to the message.', 'frontblocks' ); ?>
2314 </p>
2315 </div>
2316
2317 <div class="tw:p-4 tw:bg-gray-50 tw:rounded-lg tw:border tw:border-gray-200 tw:mb-4">
2318 <div class="tw:grid tw:grid-cols-3 tw:gap-4">
2319 <div>
2320 <label for="<?php echo esc_attr( $this->option_cookie_notice_layout ); ?>" class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mb-2">
2321 <?php echo esc_html__( 'Layout', 'frontblocks' ); ?>
2322 </label>
2323 <select
2324 id="<?php echo esc_attr( $this->option_cookie_notice_layout ); ?>"
2325 name="frontblocks_settings[<?php echo esc_attr( $this->option_cookie_notice_layout ); ?>]"
2326 class="tw:block tw:w-full tw:px-3 tw:py-2 tw:border tw:border-gray-300 tw:rounded-lg tw:text-base tw:focus:outline-none tw:focus:ring-2 tw:focus:ring-primary-500 tw:focus:border-transparent"
2327 >
2328 <option value="bar" <?php selected( $layout, 'bar' ); ?>><?php echo esc_html__( 'Full-width bar', 'frontblocks' ); ?></option>
2329 <option value="box" <?php selected( $layout, 'box' ); ?>><?php echo esc_html__( 'Boxed panel', 'frontblocks' ); ?></option>
2330 <option value="popup" <?php selected( $layout, 'popup' ); ?>><?php echo esc_html__( 'Centered popup', 'frontblocks' ); ?></option>
2331 </select>
2332 </div>
2333 <div id="cookie-notice-position-wrapper" style="<?php echo 'box' === $layout ? '' : 'display: none;'; ?>">
2334 <label for="<?php echo esc_attr( $this->option_cookie_notice_position ); ?>" class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mb-2">
2335 <?php echo esc_html__( 'Position', 'frontblocks' ); ?>
2336 </label>
2337 <select
2338 id="<?php echo esc_attr( $this->option_cookie_notice_position ); ?>"
2339 name="frontblocks_settings[<?php echo esc_attr( $this->option_cookie_notice_position ); ?>]"
2340 class="tw:block tw:w-full tw:px-3 tw:py-2 tw:border tw:border-gray-300 tw:rounded-lg tw:text-base tw:focus:outline-none tw:focus:ring-2 tw:focus:ring-primary-500 tw:focus:border-transparent"
2341 >
2342 <option value="bottom-right" <?php selected( $position, 'bottom-right' ); ?>><?php echo esc_html__( 'Bottom right', 'frontblocks' ); ?></option>
2343 <option value="bottom-left" <?php selected( $position, 'bottom-left' ); ?>><?php echo esc_html__( 'Bottom left', 'frontblocks' ); ?></option>
2344 </select>
2345 </div>
2346 <div>
2347 <label for="<?php echo esc_attr( $this->option_cookie_notice_color ); ?>" class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mb-2">
2348 <?php echo esc_html__( 'Accent color', 'frontblocks' ); ?>
2349 </label>
2350 <input
2351 type="color"
2352 id="<?php echo esc_attr( $this->option_cookie_notice_color ); ?>"
2353 name="frontblocks_settings[<?php echo esc_attr( $this->option_cookie_notice_color ); ?>]"
2354 value="<?php echo esc_attr( $color ); ?>"
2355 class="tw:h-10 tw:w-full tw:border tw:border-gray-300 tw:rounded-lg"
2356 />
2357 </div>
2358 </div>
2359
2360 <div class="tw:mt-4">
2361 <p class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mb-2">
2362 <?php echo esc_html__( 'Preview', 'frontblocks' ); ?>
2363 </p>
2364 <?php
2365 $preview_accent_text = \FrontBlocks\Frontend\CookieNotice::get_readable_text_color( $color );
2366 $preview_accent_link = \FrontBlocks\Frontend\CookieNotice::get_readable_on_white_color( $color );
2367 ?>
2368 <div id="frbl-cookie-notice-preview-stage" class="frbl-cookie-notice-preview-stage">
2369 <div
2370 id="frbl-cookie-notice-preview"
2371 class="frbl-cookie-notice frbl-cookie-notice-preview frbl-cookie-notice--<?php echo esc_attr( $layout ); ?><?php echo 'box' === $layout ? ' frbl-cookie-notice--' . ( 'bottom-left' === $position ? 'left' : 'right' ) : ''; ?>"
2372 style="--frbl-cookie-accent: <?php echo esc_attr( $color ); ?>; --frbl-cookie-accent-contrast: <?php echo esc_attr( $preview_accent_text ); ?>; --frbl-cookie-accent-on-light: <?php echo esc_attr( $preview_accent_link ); ?>;"
2373 >
2374 <div class="frbl-cookie-notice__panel">
2375 <span id="frbl-cookie-notice-preview-icon" class="frbl-cookie-notice__icon">
2376 <?php echo \FrontBlocks\Frontend\CookieNotice::get_cookie_icon_svg(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static inline SVG, no dynamic data. ?>
2377 </span>
2378 <p class="frbl-cookie-notice__message">
2379 <?php echo esc_html( '' !== $message ? $message : __( 'We use cookies to improve your experience on our website. By browsing this website, you agree to our use of cookies.', 'frontblocks' ) ); ?>
2380 </p>
2381 <div class="frbl-cookie-notice__actions">
2382 <button type="button" class="frbl-cookie-notice__button frbl-cookie-notice__button--reject" disabled>
2383 <?php echo esc_html( '' !== $reject_label ? $reject_label : __( 'Reject', 'frontblocks' ) ); ?>
2384 </button>
2385 <button type="button" class="frbl-cookie-notice__button frbl-cookie-notice__button--accept" disabled>
2386 <?php echo esc_html( '' !== $accept_label ? $accept_label : __( 'Accept', 'frontblocks' ) ); ?>
2387 </button>
2388 </div>
2389 </div>
2390 </div>
2391 </div>
2392 </div>
2393
2394 <label for="<?php echo esc_attr( $this->option_cookie_notice_expiration_days ); ?>" class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mt-4 tw:mb-2">
2395 <?php echo esc_html__( 'Cookie expiration (days)', 'frontblocks' ); ?>
2396 </label>
2397 <input
2398 type="number"
2399 min="1"
2400 max="730"
2401 id="<?php echo esc_attr( $this->option_cookie_notice_expiration_days ); ?>"
2402 name="frontblocks_settings[<?php echo esc_attr( $this->option_cookie_notice_expiration_days ); ?>]"
2403 value="<?php echo esc_attr( $expiration ); ?>"
2404 class="tw:block tw:w-32 tw:px-3 tw:py-2 tw:border tw:border-gray-300 tw:rounded-lg tw:text-base tw:focus:outline-none tw:focus:ring-2 tw:focus:ring-primary-500 tw:focus:border-transparent"
2405 />
2406 </div>
2407
2408 <div class="tw:p-4 tw:bg-gray-50 tw:rounded-lg tw:border tw:border-gray-200">
2409 <p class="tw:text-sm tw:text-gray-600 tw:mt-0 tw:mb-4">
2410 <?php echo esc_html__( 'Scripts are only requested after a visitor accepts — never before.', 'frontblocks' ); ?>
2411 </p>
2412 <div class="tw:grid tw:grid-cols-2 tw:gap-4">
2413 <div>
2414 <label for="<?php echo esc_attr( $this->option_cookie_notice_gtm_id ); ?>" class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mb-2">
2415 <?php echo esc_html__( 'Google Tag Manager ID', 'frontblocks' ); ?>
2416 </label>
2417 <input
2418 type="text"
2419 id="<?php echo esc_attr( $this->option_cookie_notice_gtm_id ); ?>"
2420 name="frontblocks_settings[<?php echo esc_attr( $this->option_cookie_notice_gtm_id ); ?>]"
2421 value="<?php echo esc_attr( $gtm_id ); ?>"
2422 placeholder="GTM-XXXXXXX"
2423 class="tw:block tw:w-full tw:px-3 tw:py-2 tw:border tw:border-gray-300 tw:rounded-lg tw:text-base tw:focus:outline-none tw:focus:ring-2 tw:focus:ring-primary-500 tw:focus:border-transparent"
2424 />
2425 </div>
2426 <div>
2427 <label for="<?php echo esc_attr( $this->option_cookie_notice_ga4_id ); ?>" class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mb-2">
2428 <?php echo esc_html__( 'GA4 Measurement ID', 'frontblocks' ); ?>
2429 </label>
2430 <input
2431 type="text"
2432 id="<?php echo esc_attr( $this->option_cookie_notice_ga4_id ); ?>"
2433 name="frontblocks_settings[<?php echo esc_attr( $this->option_cookie_notice_ga4_id ); ?>]"
2434 value="<?php echo esc_attr( $ga4_id ); ?>"
2435 placeholder="G-XXXXXXXXXX"
2436 class="tw:block tw:w-full tw:px-3 tw:py-2 tw:border tw:border-gray-300 tw:rounded-lg tw:text-base tw:focus:outline-none tw:focus:ring-2 tw:focus:ring-primary-500 tw:focus:border-transparent"
2437 />
2438 </div>
2439 </div>
2440 </div>
2441 </div>
2442 </div>
2443 <?php
2444 }
2445
2446 /**
2447 * Render toggle field for enable popups (PRO feature).
2448 *
2449 * @return void
2450 */
2451 public function field_enable_popups() {
2452 $this->render_pro_toggle( $this->option_enable_popups );
2453 }
2454
2455 /**
2456 * Render toggle field for enable Gutenberg in products (PRO).
2457 *
2458 * @return void
2459 */
2460 public function field_enable_gutenberg() {
2461 $this->render_pro_toggle( $this->option_enable_gutenberg );
2462 }
2463
2464 /**
2465 * Render toggle field for enable Simple Prices Variable Products (PRO).
2466 *
2467 * @return void
2468 */
2469 public function field_enable_simple_prices_variable_products() {
2470 $this->render_pro_toggle( $this->option_enable_simple_prices_variable_products );
2471 }
2472
2473 /**
2474 * Render After Add to Cart Block field.
2475 *
2476 * @return void
2477 */
2478 public function field_enable_after_add_to_cart() {
2479 $this->render_pro_toggle( $this->option_enable_after_add_to_cart );
2480 }
2481
2482 /**
2483 * Render Deactivate Short Description field.
2484 *
2485 * @return void
2486 */
2487 public function field_deactivate_short_description() {
2488 $this->render_pro_toggle( $this->option_deactivate_short_description );
2489 }
2490
2491 /**
2492 * Render Move Content to Short Description field.
2493 *
2494 * @return void
2495 */
2496 public function field_move_content_to_short_description() {
2497 $this->render_pro_toggle( $this->option_move_content_to_short_description );
2498 }
2499
2500 /**
2501 * Render Disable Zoom in Product Images field.
2502 *
2503 * @return void
2504 */
2505 public function field_disable_zoom_images() {
2506 $this->render_pro_toggle( $this->option_disable_zoom_images );
2507 }
2508
2509 /**
2510 * Render Add Share Buttons in Product Page field.
2511 *
2512 * @return void
2513 */
2514 public function field_add_share_buttons() {
2515 $this->render_pro_toggle( $this->option_add_share_buttons );
2516 }
2517
2518 /**
2519 * Render Deactivate Product Tabs field.
2520 *
2521 * @return void
2522 */
2523 public function field_deactivate_product_tabs() {
2524 $this->render_pro_toggle( $this->option_deactivate_product_tabs );
2525 }
2526
2527 /**
2528 * Render Horizontal Product Form Layout field.
2529 *
2530 * @return void
2531 */
2532 public function field_horizontal_product_form() {
2533 $this->render_pro_toggle( $this->option_horizontal_product_form );
2534 }
2535
2536 /**
2537 * Render Variant Display Mode toggle field.
2538 *
2539 * @return void
2540 */
2541 public function field_enable_variant_display_mode() {
2542 $this->render_pro_toggle( $this->option_enable_variant_display_mode );
2543 }
2544
2545 /**
2546 * Render Disable Coupon Field in Cart field.
2547 *
2548 * @return void
2549 */
2550 public function field_disable_cart_coupon() {
2551 $this->render_pro_toggle( $this->option_disable_cart_coupon );
2552 }
2553
2554 /**
2555 * Render Disable Cross-Sells in Cart field.
2556 *
2557 * @return void
2558 */
2559 public function field_disable_cart_cross_sells() {
2560 $this->render_pro_toggle( $this->option_disable_cart_cross_sells );
2561 }
2562
2563 /**
2564 * Render Disable Coupon Field in Checkout field.
2565 *
2566 * @return void
2567 */
2568 public function field_disable_checkout_coupon() {
2569 $this->render_pro_toggle( $this->option_disable_checkout_coupon );
2570 }
2571
2572 /**
2573 * Render Disable Order Notes in Checkout field.
2574 *
2575 * @return void
2576 */
2577 public function field_disable_checkout_order_notes() {
2578 $this->render_pro_toggle( $this->option_disable_checkout_order_notes );
2579 }
2580
2581 /**
2582 * Render Disable Login Prompt in Checkout field.
2583 *
2584 * @return void
2585 */
2586 public function field_disable_checkout_login_prompt() {
2587 $this->render_pro_toggle( $this->option_disable_checkout_login_prompt );
2588 }
2589
2590 /**
2591 * Render Enable Full Page Scroll field.
2592 *
2593 * @return void
2594 */
2595 public function field_enable_fullpage_scroll() {
2596 $this->render_pro_toggle( $this->option_enable_fullpage_scroll );
2597 }
2598
2599 /**
2600 * Render Enable Language Banner field.
2601 *
2602 * @return void
2603 */
2604 public function field_enable_language_banner() {
2605 $this->render_pro_toggle( $this->option_enable_language_banner );
2606 }
2607
2608 /**
2609 * Render toggle field for checkout inline fields (PRO).
2610 *
2611 * @return void
2612 */
2613 public function field_checkout_inline() {
2614 $this->render_pro_toggle( $this->option_checkout_inline );
2615 }
2616
2617 /**
2618 * Custom Post Types section callback.
2619 *
2620 * @return void
2621 */
2622 public function section_custom_post_types_callback() {
2623 if ( ! frbl_is_pro_active() ) {
2624 echo '<div class="tw:bg-blue-50 tw:border-l-4 tw:border-blue-400 tw:p-4 tw:mb-4">';
2625 echo '<div class="tw:flex">';
2626 echo '<div class="tw:flex-shrink-0">';
2627 echo '<svg class="tw:h-5 tw:w-5 tw:text-blue-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/></svg>';
2628 echo '</div>';
2629 echo '<div class="tw:ml-3">';
2630 echo '<p class="tw:text-sm tw:text-blue-700">';
2631 printf(
2632 /* translators: %s: FrontBlocks PRO link */
2633 esc_html__( 'This feature requires %s. Upgrade to unlock advanced functionality.', 'frontblocks' ),
2634 '<a href="https://close.technology/wordpress-plugins/frontblocks-pro/?utm_source=frontblocks&utm_medium=plugin&utm_campaign=settings" target="_blank" class="tw:font-medium tw:underline">FrontBlocks PRO</a>'
2635 );
2636 echo '</p>';
2637 echo '</div>';
2638 echo '</div>';
2639 echo '</div>';
2640 } elseif ( ! $this->is_license_valid ) {
2641 echo '<div class="tw:bg-yellow-50 tw:border-l-4 tw:border-yellow-400 tw:p-4 tw:mb-4">';
2642 echo '<div class="tw:flex">';
2643 echo '<div class="tw:flex-shrink-0">';
2644 echo '<svg class="tw:h-5 tw:w-5 tw:text-yellow-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>';
2645 echo '</div>';
2646 echo '<div class="tw:ml-3">';
2647 echo '<p class="tw:text-sm tw:text-yellow-700">';
2648 printf(
2649 /* translators: %s: License section link */
2650 esc_html__( 'License is not activated. Please activate your license in the %s section below to enable these features.', 'frontblocks' ),
2651 '<a href="#frontblocks_section_license" class="tw:font-medium tw:underline">' . esc_html__( 'License', 'frontblocks' ) . '</a>'
2652 );
2653 echo '</p>';
2654 echo '</div>';
2655 echo '</div>';
2656 echo '</div>';
2657 } else {
2658 ?>
2659 <p class="tw:text-sm tw:text-gray-600 tw:mt-0 tw:mb-4">
2660 <?php echo esc_html__( 'Create and manage custom post types with advanced configuration options.', 'frontblocks' ); ?>
2661 </p>
2662 <?php
2663 }
2664 }
2665
2666 /**
2667 * Render toggle field for enable custom post types.
2668 *
2669 * @return void
2670 */
2671 public function field_enable_custom_post_types() {
2672 $options = get_option( 'frontblocks_settings', array() );
2673 $enabled = (bool) ( $options[ $this->option_enable_custom_post_types ] ?? false );
2674 $is_enabled = $this->is_license_valid;
2675 $disabled = ! $is_enabled ? 'disabled' : '';
2676 ?>
2677 <div class="frbl-custom-post-types-wrapper">
2678 <div class="tw:flex tw:items-center tw:justify-between tw:mb-4">
2679 <label for="<?php echo esc_attr( $this->option_enable_custom_post_types ); ?>" class="tw:text-base tw:font-medium tw:text-gray-900">
2680 <?php echo esc_html__( 'Enable Custom Post Types Builder', 'frontblocks' ); ?>
2681 </label>
2682 <label class="frbl-toggle">
2683 <input type="checkbox"
2684 id="<?php echo esc_attr( $this->option_enable_custom_post_types ); ?>"
2685 name="frontblocks_settings[<?php echo esc_attr( $this->option_enable_custom_post_types ); ?>]"
2686 value="1"
2687 <?php checked( true, $enabled ); ?>
2688 <?php echo esc_attr( $disabled ); ?>
2689 />
2690 <span></span>
2691 </label>
2692 </div>
2693
2694 <?php if ( $is_enabled ) : ?>
2695 <div id="frbl-cpt-builder" class="frbl-cpt-builder" style="<?php echo $enabled ? '' : 'display: none;'; ?>">
2696 <div class="tw:mt-4 tw:p-4 tw:bg-gray-50 tw:rounded-lg tw:border tw:border-gray-200">
2697 <label for="frbl-cpt-name" class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mb-2">
2698 <?php echo esc_html__( 'Post Type Name', 'frontblocks' ); ?>
2699 </label>
2700 <div class="tw:flex tw:gap-2">
2701 <input
2702 type="text"
2703 id="frbl-cpt-name"
2704 class="tw:flex-1 tw:px-3 tw:py-2 tw:border tw:border-gray-300 tw:rounded-lg tw:focus:outline-none tw:focus:ring-2 tw:focus:ring-primary-500 tw:focus:border-transparent"
2705 placeholder="<?php echo esc_attr__( 'e.g., Portfolio, Team, Services', 'frontblocks' ); ?>"
2706 />
2707 <button
2708 type="button"
2709 id="frbl-create-cpt-btn"
2710 class="tw:px-4 tw:py-2 tw:bg-primary-500 tw:text-white tw:rounded-lg tw:hover:bg-primary-600 tw:focus:outline-none tw:focus:ring-2 tw:focus:ring-primary-500 tw:transition-colors"
2711 >
2712 <?php echo esc_html__( 'Create', 'frontblocks' ); ?>
2713 </button>
2714 </div>
2715 <p class="tw:text-xs tw:text-gray-500 tw:mt-2">
2716 <?php echo esc_html__( 'Enter a singular name for your custom post type (e.g., "Portfolio" will create "portfolio" post type).', 'frontblocks' ); ?>
2717 </p>
2718 </div>
2719
2720 <?php do_action( 'frontblocks_render_existing_cpts' ); ?>
2721 </div>
2722 <?php endif; ?>
2723 </div>
2724 <?php
2725 }
2726
2727 /**
2728 * Render PRO features section after the main form.
2729 * Shows when frbl_pro_blocks filter returns blocks and license is not active.
2730 *
2731 * @return void
2732 */
2733 /**
2734 * Default PRO blocks list — shown as promotional cards when PRO is not installed.
2735 * The frbl_pro_blocks filter lets the PRO plugin override this list.
2736 *
2737 * @return array<int, array<string, string>>
2738 */
2739 private function get_default_pro_blocks(): array {
2740 return array(
2741 array(
2742 'icon' => 'meta-fields',
2743 'title' => __( 'Dynamic Meta Fields', 'frontblocks' ),
2744 'desc' => __( 'Bind any paragraph or heading to a custom post meta field, editable from the block editor.', 'frontblocks' ),
2745 ),
2746 array(
2747 'icon' => 'user-text',
2748 'title' => __( 'User Data Block', 'frontblocks' ),
2749 'desc' => __( 'Display logged-in user data with placeholders like {nombre}, {email}, {username}.', 'frontblocks' ),
2750 ),
2751 array(
2752 'icon' => 'fullpage-scroll',
2753 'title' => __( 'Full Page Scroll', 'frontblocks' ),
2754 'desc' => __( 'Full-page scroll navigation between sections with smooth transitions.', 'frontblocks' ),
2755 ),
2756 array(
2757 'icon' => 'language-banner',
2758 'title' => __( 'Language Banner', 'frontblocks' ),
2759 'desc' => __( 'Detect visitor language and show a recommendation banner (WPML/Polylang).', 'frontblocks' ),
2760 ),
2761 array(
2762 'icon' => 'gutenberg',
2763 'title' => __( 'Gutenberg in Products', 'frontblocks' ),
2764 'desc' => __( 'Use the full block editor to build WooCommerce product descriptions.', 'frontblocks' ),
2765 ),
2766 array(
2767 'icon' => 'after-add-to-cart',
2768 'title' => __( 'After Add to Cart Block', 'frontblocks' ),
2769 'desc' => __( 'Insert custom block content right after the Add to Cart button.', 'frontblocks' ),
2770 ),
2771 array(
2772 'icon' => 'simple-prices',
2773 'title' => __( 'Simple Prices Variable Products', 'frontblocks' ),
2774 'desc' => __( 'Simplified price display for variable WooCommerce products.', 'frontblocks' ),
2775 ),
2776 array(
2777 'icon' => 'horizontal-form',
2778 'title' => __( 'Horizontal Product Form', 'frontblocks' ),
2779 'desc' => __( 'Switch the WooCommerce product form to a horizontal layout.', 'frontblocks' ),
2780 ),
2781 array(
2782 'icon' => 'deactivate-tabs',
2783 'title' => __( 'Deactivate Product Tabs', 'frontblocks' ),
2784 'desc' => __( 'Remove the default tabs from WooCommerce product pages.', 'frontblocks' ),
2785 ),
2786 array(
2787 'icon' => 'disable-zoom',
2788 'title' => __( 'Disable Product Image Zoom', 'frontblocks' ),
2789 'desc' => __( 'Remove zoom effect on WooCommerce product images.', 'frontblocks' ),
2790 ),
2791 array(
2792 'icon' => 'share-buttons',
2793 'title' => __( 'Share Buttons', 'frontblocks' ),
2794 'desc' => __( 'Add social share buttons to WooCommerce product pages.', 'frontblocks' ),
2795 ),
2796 array(
2797 'icon' => 'deactivate-description',
2798 'title' => __( 'Manage Short Description', 'frontblocks' ),
2799 'desc' => __( 'Deactivate or move the WooCommerce product short description.', 'frontblocks' ),
2800 ),
2801 array(
2802 'icon' => 'default',
2803 'title' => __( 'Custom Post Types Builder', 'frontblocks' ),
2804 'desc' => __( 'Create and manage custom post types directly from the admin panel.', 'frontblocks' ),
2805 ),
2806 array(
2807 'icon' => 'horizontal-form',
2808 'title' => __( 'Checkout Inline Fields', 'frontblocks' ),
2809 'desc' => __( 'Display address, email and phone fields side by side in the WooCommerce checkout.', 'frontblocks' ),
2810 ),
2811 array(
2812 'icon' => 'popups',
2813 'title' => __( 'Popups', 'frontblocks' ),
2814 'desc' => __( 'Create popups with the block editor and configure when and where they appear.', 'frontblocks' ),
2815 ),
2816 );
2817 }
2818
2819 /**
2820 * Render PRO features section after the main form.
2821 *
2822 * @return void
2823 */
2824 private function render_pro_section(): void {
2825 $pro_blocks = apply_filters( 'frbl_pro_blocks', $this->get_default_pro_blocks() );
2826 $license_valid = function_exists( 'frblp_is_license_valid' ) && frblp_is_license_valid();
2827
2828 if ( empty( $pro_blocks ) || $license_valid ) {
2829 return;
2830 }
2831
2832 ?>
2833 <div class="frbl-section-wrapper tw:mt-6">
2834 <div class="frbl-section-header">
2835 <h2 class="tw:text-2xl tw:font-bold tw:text-gray-900 tw:mb-0">
2836 <?php echo esc_html__( 'FrontBlocks PRO Features', 'frontblocks' ); ?>
2837 </h2>
2838 <div class="tw:text-sm tw:text-gray-600">
2839 <p class="tw:mt-0 tw:mb-4">
2840 <?php
2841 $anchor = frbl_is_pro_active()
2842 ? '<a href="#frontblocks_section_license" class="tw:font-medium tw:text-red-600 tw:hover:text-red-700 tw:underline">' . esc_html__( 'License', 'frontblocks' ) . '</a>'
2843 : '<a href="https://close.technology/wordpress-plugins/frontblocks-pro/?utm_source=frontblocks&utm_medium=plugin&utm_campaign=settings-pro-showcase" target="_blank" rel="noopener noreferrer" class="tw:font-medium tw:text-red-600 tw:hover:text-red-700 tw:underline">FrontBlocks PRO</a>';
2844 printf(
2845 frbl_is_pro_active()
2846 /* translators: %s: license section link */
2847 ? esc_html__( 'Activate your license in the %s section below to unlock all features.', 'frontblocks' )
2848 /* translators: %s: FrontBlocks PRO link */
2849 : esc_html__( 'Unlock these features with %s.', 'frontblocks' ),
2850 $anchor // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
2851 );
2852 ?>
2853 </p>
2854 </div>
2855 </div>
2856
2857 <div class="frbl-features-grid">
2858 <?php foreach ( $pro_blocks as $block ) : ?>
2859 <?php UI::show_pro_info_card( $block['icon'], $block['title'], $block['desc'] ); ?>
2860 <?php endforeach; ?>
2861 </div>
2862
2863 <?php if ( ! frbl_is_pro_active() ) : ?>
2864 <div class="tw:mt-6 tw:text-center">
2865 <a
2866 href="https://close.technology/wordpress-plugins/frontblocks-pro/?utm_source=frontblocks&utm_medium=plugin&utm_campaign=settings-pro-cta"
2867 target="_blank"
2868 rel="noopener noreferrer"
2869 class="tw:inline-flex tw:items-center tw:px-6 tw:py-3 tw:border tw:border-transparent tw:text-base tw:font-medium tw:rounded-lg tw:shadow-sm tw:transition-colors tw:duration-200"
2870 style="background-color: #ef4444; color: #fff;"
2871 onmouseover="this.style.backgroundColor='#dc2626'"
2872 onmouseout="this.style.backgroundColor='#ef4444'"
2873 >
2874 <?php echo esc_html__( 'Get FrontBlocks PRO', 'frontblocks' ); ?>
2875 </a>
2876 </div>
2877 <?php endif; ?>
2878 </div>
2879 <?php
2880 }
2881
2882 /**
2883 * Render license section (separate from main form).
2884 *
2885 * @return void
2886 */
2887 private function render_license_section() {
2888 global $frblp_license;
2889
2890 ?>
2891 <div class="tw:mt-6" id="frontblocks_section_license">
2892 <?php
2893 // Check if license instance exists.
2894 if ( ! $frblp_license ) {
2895 ?>
2896 <div class="tw:p-4 tw:rounded-lg tw:bg-red-50 tw:border tw:border-red-200">
2897 <p class="tw:text-sm tw:text-red-700">
2898 <?php echo esc_html__( 'License manager not initialized.', 'frontblocks' ); ?>
2899 </p>
2900 </div>
2901 <?php
2902 return;
2903 }
2904
2905 // Check if License class exists (requires FrontBlocks PRO).
2906 if ( ! class_exists( '\Closemarketing\WPLicenseManager\License' ) ) {
2907 ?>
2908 <div class="tw:p-4 tw:rounded-lg tw:bg-yellow-50 tw:border tw:border-yellow-200">
2909 <p class="tw:text-sm tw:text-yellow-700">
2910 <?php echo esc_html__( 'License management requires FrontBlocks PRO to be installed and active.', 'frontblocks' ); ?>
2911 </p>
2912 </div>
2913 <?php
2914 return;
2915 }
2916
2917 // Render license settings inline.
2918 $this->render_inline_license_settings( $frblp_license );
2919 ?>
2920 </div>
2921 <?php
2922 }
2923
2924 /**
2925 * Render inline license settings.
2926 *
2927 * @param \Closemarketing\WPLicenseManager\License $license License instance.
2928 * @return void
2929 */
2930 private function render_inline_license_settings( $license ) {
2931 // Get license data.
2932 $license_key = $license->get_option_value( 'apikey' );
2933 $is_active = $license->is_license_active();
2934 $license_status = get_option( 'frontblocks-pro_license_activated', 'Deactivated' );
2935
2936 ?>
2937 <div class="formscrm-license-wrapper">
2938 <!-- Main Card -->
2939 <div class="formscrm-card">
2940 <!-- Header -->
2941 <div class="formscrm-card-header">
2942 <h2><?php echo esc_html__( 'FrontBlocks PRO License', 'frontblocks' ); ?></h2>
2943 <p><?php echo esc_html__( 'Manage your license to receive automatic updates and support.', 'frontblocks' ); ?></p>
2944 </div>
2945
2946 <!-- License Status -->
2947 <div class="formscrm-form-group">
2948 <?php if ( $is_active ) : ?>
2949 <div class="formscrm-status-box formscrm-status-active">
2950 <span class="formscrm-status-icon">
2951 <svg class="formscrm-icon" fill="currentColor" viewBox="0 0 20 20">
2952 <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
2953 </svg>
2954 </span>
2955 <span class="formscrm-status-text"><?php echo esc_html__( 'License Active', 'frontblocks' ); ?></span>
2956 </div>
2957 <?php else : ?>
2958 <div class="formscrm-status-box formscrm-status-inactive">
2959 <span class="formscrm-status-icon">
2960 <svg class="formscrm-icon" fill="currentColor" viewBox="0 0 20 20">
2961 <path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
2962 </svg>
2963 </span>
2964 <span class="formscrm-status-text"><?php echo esc_html__( 'License Inactive', 'frontblocks' ); ?></span>
2965 </div>
2966 <?php endif; ?>
2967 </div>
2968
2969 <!-- License Form -->
2970 <form method="post" action="options.php" class="formscrm-license-form">
2971 <?php settings_fields( 'frontblocks-pro_license' ); ?>
2972 <?php wp_nonce_field( 'Update_License_Options', 'license_nonce' ); ?>
2973
2974 <!-- License Key Field -->
2975 <div class="formscrm-form-group">
2976 <label class="formscrm-label" for="frontblocks-pro_license_apikey">
2977 <?php echo esc_html__( 'License Key', 'frontblocks' ); ?>
2978 </label>
2979 <div class="formscrm-input-group">
2980 <input
2981 type="text"
2982 id="frontblocks-pro_license_apikey"
2983 name="frontblocks-pro_license_apikey"
2984 value="<?php echo esc_attr( $license_key ); ?>"
2985 class="formscrm-input"
2986 placeholder="<?php echo esc_attr__( 'CTECH-XXXXX-XXXXX-XXXXX-XXXXX', 'frontblocks' ); ?>"
2987 <?php echo $is_active ? 'readonly' : ''; ?>
2988 />
2989 <?php if ( $is_active ) : ?>
2990 <label class="formscrm-deactivate-label">
2991 <input type="checkbox" name="frontblocks-pro_license_deactivate_checkbox" value="on" />
2992 <span><?php echo esc_html__( 'Deactivate', 'frontblocks' ); ?></span>
2993 </label>
2994 <?php endif; ?>
2995 </div>
2996 <p class="formscrm-help-text">
2997 <?php
2998 printf(
2999 /* translators: %s: Purchase URL */
3000 esc_html__( 'Enter your license key. You can find it in %s.', 'frontblocks' ),
3001 '<a href="https://close.technology/my-account/" target="_blank">' . esc_html__( 'your account', 'frontblocks' ) . '</a>'
3002 );
3003 ?>
3004 </p>
3005 </div>
3006
3007 <!-- License Status -->
3008 <div class="formscrm-form-group">
3009 <label class="formscrm-label"><?php echo esc_html__( 'License Status', 'frontblocks' ); ?></label>
3010 <div class="formscrm-status-box <?php echo $is_active ? 'formscrm-status-active' : 'formscrm-status-inactive'; ?>">
3011 <span class="formscrm-status-icon">
3012 <?php if ( $is_active ) : ?>
3013 <svg class="formscrm-icon" fill="currentColor" viewBox="0 0 20 20">
3014 <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
3015 </svg>
3016 <?php else : ?>
3017 <svg class="formscrm-icon" fill="currentColor" viewBox="0 0 20 20">
3018 <path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
3019 </svg>
3020 <?php endif; ?>
3021 </span>
3022 <span class="formscrm-status-text">
3023 <?php echo $is_active ? esc_html__( 'Active', 'frontblocks' ) : esc_html__( 'Not Activated', 'frontblocks' ); ?>
3024 </span>
3025 </div>
3026 </div>
3027
3028 <!-- Submit Button -->
3029 <div class="formscrm-form-actions">
3030 <button type="submit" name="submit_license" class="formscrm-button formscrm-button-primary">
3031 <?php echo $is_active ? esc_html__( 'Update License', 'frontblocks' ) : esc_html__( 'Activate License', 'frontblocks' ); ?>
3032 </button>
3033 </div>
3034 </form>
3035 </div>
3036
3037 <!-- Sidebar Info -->
3038 <div class="formscrm-info-card">
3039 <h3><?php echo esc_html__( 'License Benefits', 'frontblocks' ); ?></h3>
3040 <p><?php echo esc_html__( 'An active license provides the following benefits:', 'frontblocks' ); ?></p>
3041
3042 <ul class="formscrm-benefits-list">
3043 <li><?php echo esc_html__( 'Automatic plugin updates', 'frontblocks' ); ?></li>
3044 <li><?php echo esc_html__( 'Access to new features', 'frontblocks' ); ?></li>
3045 <li><?php echo esc_html__( 'Priority support', 'frontblocks' ); ?></li>
3046 <li><?php echo esc_html__( 'Security patches', 'frontblocks' ); ?></li>
3047 </ul>
3048
3049 <hr style="margin: 20px 0; border: none; border-top: 1px solid #e2e8f0;">
3050
3051 <div style="font-size: 0.875rem; color: #64748b;">
3052 <p style="margin-bottom: 8px;">
3053 <strong><?php echo esc_html__( 'Need Help?', 'frontblocks' ); ?></strong>
3054 </p>
3055 <p style="margin-bottom: 8px;">
3056 <a href="https://close.technology/wordpress-plugins/frontblocks-pro/" target="_blank" style="color: #8b5cf6; text-decoration: none;">
3057 <?php echo esc_html__( 'Purchase License', 'frontblocks' ); ?>
3058 </a>
3059 </p>
3060 <p style="margin-bottom: 8px;">
3061 <a href="https://close.technology/my-account/" target="_blank" style="color: #8b5cf6; text-decoration: none;">
3062 <?php echo esc_html__( 'My Account', 'frontblocks' ); ?>
3063 </a>
3064 </p>
3065 <p>
3066 <a href="https://close.technology/support/" target="_blank" style="color: #8b5cf6; text-decoration: none;">
3067 <?php echo esc_html__( 'Support', 'frontblocks' ); ?>
3068 </a>
3069 </p>
3070 </div>
3071 </div>
3072 </div>
3073 <?php
3074 }
3075
3076 /**
3077 * Helper method to render PRO toggle fields.
3078 *
3079 * @param string $option_key Option key.
3080 * @return void
3081 */
3082 private function render_pro_toggle( $option_key ) {
3083 $options = get_option( 'frontblocks_settings', array() );
3084 $enabled = (bool) ( $options[ $option_key ] ?? false );
3085 $is_enabled = $this->is_license_valid;
3086 $disabled = ! $is_enabled ? 'disabled' : '';
3087 ?>
3088 <label class="frbl-toggle">
3089 <input type="checkbox"
3090 id="<?php echo esc_attr( $option_key ); ?>"
3091 name="frontblocks_settings[<?php echo esc_attr( $option_key ); ?>]"
3092 value="1"
3093 <?php checked( true, $enabled ); ?>
3094 <?php echo esc_attr( $disabled ); ?>
3095 />
3096 <span></span>
3097 </label>
3098 <?php
3099 }
3100
3101 /**
3102 * Sanitize settings array.
3103 *
3104 * @param array $value Raw value.
3105 * @return array
3106 */
3107 public function sanitize_settings( $value ) {
3108 // Nonce verification.
3109 $nonce = isset( $_POST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
3110 if ( empty( $nonce ) || ! wp_verify_nonce( $nonce, 'frontblocks_settings-options' ) ) {
3111 add_settings_error( 'frontblocks_settings', 'frontblocks_settings_nonce', esc_html__( 'Security check failed. Please try again.', 'frontblocks' ), 'error' );
3112
3113 return get_option( 'frontblocks_settings', array() );
3114 }
3115
3116 if ( ! is_array( $value ) ) {
3117 return array();
3118 }
3119
3120 // Get current options to preserve unchecked checkboxes.
3121 $current_options = get_option( 'frontblocks_settings', array() );
3122
3123 // Initialize sanitized array with current values.
3124 $sanitized = $current_options;
3125
3126 // List of all boolean options (checkboxes).
3127 $boolean_options = array(
3128 $this->option_enable_testimonials,
3129 $this->option_enable_reading_progress,
3130 $this->option_enable_back_button,
3131 $this->option_enable_scroll_top,
3132 $this->option_enable_events,
3133 $this->option_enable_fluid_typography,
3134 $this->option_enable_maintenance,
3135 $this->option_enable_cookie_notice,
3136 $this->option_enable_gutenberg,
3137 $this->option_enable_simple_prices_variable_products,
3138 $this->option_enable_after_add_to_cart,
3139 $this->option_deactivate_short_description,
3140 $this->option_move_content_to_short_description,
3141 $this->option_disable_zoom_images,
3142 $this->option_add_share_buttons,
3143 $this->option_deactivate_product_tabs,
3144 $this->option_horizontal_product_form,
3145 $this->option_enable_variant_display_mode,
3146 $this->option_enable_custom_post_types,
3147 $this->option_enable_fullpage_scroll,
3148 $this->option_enable_language_banner,
3149 $this->option_enable_popups,
3150 $this->option_checkout_inline,
3151 $this->option_disable_cart_coupon,
3152 $this->option_disable_cart_cross_sells,
3153 $this->option_disable_checkout_coupon,
3154 $this->option_disable_checkout_order_notes,
3155 $this->option_disable_checkout_login_prompt,
3156 );
3157
3158 // Initialize all boolean options to false (unchecked checkboxes are not submitted).
3159 foreach ( $boolean_options as $option ) {
3160 $sanitized[ $option ] = false;
3161 }
3162
3163 // Process submitted values.
3164 foreach ( $value as $key => $val ) {
3165 if ( in_array( $key, $boolean_options, true ) ) {
3166 $sanitized[ $key ] = (bool) $val;
3167 } elseif ( $this->option_events_type === $key ) {
3168 // Sanitize events type: only allow 'cpt' or 'posts'.
3169 $sanitized[ $key ] = in_array( $val, array( 'cpt', 'posts' ), true ) ? $val : 'cpt';
3170 } elseif ( $this->option_scroll_top_position === $key ) {
3171 $sanitized[ $key ] = in_array( $val, array( 'bottom-right', 'bottom-left' ), true ) ? $val : 'bottom-right';
3172 } elseif ( $this->option_scroll_top_icon_url === $key ) {
3173 $sanitized[ $key ] = esc_url_raw( $val );
3174 } elseif ( $this->option_maintenance_title === $key ) {
3175 $sanitized[ $key ] = sanitize_text_field( $val );
3176 } elseif ( $this->option_maintenance_image === $key ) {
3177 $sanitized[ $key ] = absint( $val );
3178 } elseif ( $this->option_cookie_notice_message === $key ) {
3179 $sanitized[ $key ] = sanitize_textarea_field( $val );
3180 } elseif ( in_array( $key, array( $this->option_cookie_notice_accept_label, $this->option_cookie_notice_reject_label ), true ) ) {
3181 $sanitized[ $key ] = sanitize_text_field( $val );
3182 } elseif ( $this->option_cookie_notice_policy_page_id === $key ) {
3183 $sanitized[ $key ] = absint( $val );
3184 } elseif ( $this->option_cookie_notice_layout === $key ) {
3185 $sanitized[ $key ] = in_array( $val, array( 'bar', 'box', 'popup' ), true ) ? $val : 'bar';
3186 } elseif ( $this->option_cookie_notice_position === $key ) {
3187 $sanitized[ $key ] = in_array( $val, array( 'bottom-right', 'bottom-left' ), true ) ? $val : 'bottom-right';
3188 } elseif ( $this->option_cookie_notice_color === $key ) {
3189 $hex_color = sanitize_hex_color( $val );
3190 $sanitized[ $key ] = $hex_color ? $hex_color : '#687df9';
3191 } elseif ( $this->option_cookie_notice_expiration_days === $key ) {
3192 $days = absint( $val );
3193 $sanitized[ $key ] = $days > 0 ? min( $days, 730 ) : 365;
3194 } elseif ( $this->option_cookie_notice_gtm_id === $key ) {
3195 $gtm_id = strtoupper( sanitize_text_field( $val ) );
3196 $sanitized[ $key ] = preg_match( '/^GTM-[A-Z0-9]+$/', $gtm_id ) ? $gtm_id : '';
3197 } elseif ( $this->option_cookie_notice_ga4_id === $key ) {
3198 $ga4_id = strtoupper( sanitize_text_field( $val ) );
3199 $sanitized[ $key ] = preg_match( '/^G-[A-Z0-9]+$/', $ga4_id ) ? $ga4_id : '';
3200 }
3201 }
3202
3203 // Ensure mutual exclusion: if both description options are enabled, keep only the last one changed.
3204 if ( ! empty( $sanitized[ $this->option_deactivate_short_description ] ) && ! empty( $sanitized[ $this->option_move_content_to_short_description ] ) ) {
3205 // Get current saved values to determine which one was just changed.
3206 $current_deactivate = ! empty( $current_options[ $this->option_deactivate_short_description ] );
3207 $current_move = ! empty( $current_options[ $this->option_move_content_to_short_description ] );
3208
3209 // If deactivate was already on, turn it off (move is the new one).
3210 if ( $current_deactivate ) {
3211 $sanitized[ $this->option_deactivate_short_description ] = false;
3212 } else {
3213 // Otherwise turn off move (deactivate is the new one).
3214 $sanitized[ $this->option_move_content_to_short_description ] = false;
3215 }
3216 }
3217
3218 do_action( 'frontblocks_sanitize_settings', $sanitized );
3219
3220 return $sanitized;
3221 }
3222 }
3223