PluginProbe ʕ •ᴥ•ʔ
FrontBlocks for Gutenberg/GeneratePress / 1.5.0
FrontBlocks for Gutenberg/GeneratePress v1.5.0
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
Settings.php 1 week ago UI.php 2 months ago
Settings.php
3183 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 }
1535
1536 /**
1537 * Render a single settings section as a card.
1538 *
1539 * @param array $section Section data.
1540 * @return void
1541 */
1542 private function render_settings_section( $section ) {
1543 global $wp_settings_fields;
1544
1545 $has_fields = isset( $wp_settings_fields[ $this->page_slug ][ $section['id'] ] );
1546
1547 // Si no hay campos Y no hay callback, no renderizar nada.
1548 if ( ! $has_fields && ! $section['callback'] ) {
1549 return;
1550 }
1551
1552 // Check if this is a section with callback only (like active_blocks).
1553 $is_callback_only = ! $has_fields && $section['callback'];
1554
1555 // Check if this is a section that needs full width (rich fields, not a simple toggle grid).
1556 $is_cpt_section = in_array( $section['id'], array( 'frontblocks_section_custom_post_types', 'frontblocks_section_maintenance', 'frontblocks_section_cookie_notice' ), true );
1557
1558 // Show PRO CTA button before the Optional Features section.
1559 if ( 'frontblocks_section_features' === $section['id'] && ! $this->is_license_valid ) {
1560 ?>
1561 <div class="tw:mb-4">
1562 <a
1563 href="https://close.technology/wordpress-plugins/frontblocks-pro/?utm_source=frontblocks&utm_medium=plugin&utm_campaign=settings-optional-cta"
1564 target="_blank"
1565 rel="noopener noreferrer"
1566 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"
1567 style="background-color: #ef4444; color: #fff; padding: 10px 20px;"
1568 onmouseover="this.style.backgroundColor='#dc2626'"
1569 onmouseout="this.style.backgroundColor='#ef4444'"
1570 >
1571 <?php echo esc_html__( 'Get FrontBlocks PRO', 'frontblocks' ); ?>
1572 </a>
1573 </div>
1574 <?php
1575 }
1576
1577 if ( $is_callback_only ) {
1578 // Render section with only callback (no fields).
1579 ?>
1580 <div class="frbl-section-wrapper">
1581 <div class="frbl-section-header">
1582 <h2 class="tw:text-2xl tw:font-bold tw:text-gray-900 tw:mb-0">
1583 <?php echo esc_html( $section['title'] ); ?>
1584 </h2>
1585 </div>
1586 <?php call_user_func( $section['callback'], $section ); ?>
1587 </div>
1588 <?php
1589 return;
1590 }
1591
1592 if ( $is_cpt_section ) {
1593 // Render CPT section as a full-width card.
1594 ?>
1595 <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">
1596 <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">
1597 <h2 class="tw:text-xl tw:font-semibold tw:text-gray-900">
1598 <?php echo esc_html( $section['title'] ); ?>
1599 </h2>
1600 <?php
1601 if ( $section['callback'] ) {
1602 echo '<div class="tw:mt-2 tw:text-sm tw:text-gray-600">';
1603 call_user_func( $section['callback'], $section );
1604 echo '</div>';
1605 }
1606 ?>
1607 </div>
1608 <div class="tw:px-6 tw:py-5">
1609 <?php
1610 foreach ( (array) $wp_settings_fields[ $this->page_slug ][ $section['id'] ] as $field ) {
1611 call_user_func( $field['callback'], $field['args'] );
1612 }
1613 ?>
1614 </div>
1615 </div>
1616 <?php
1617 } else {
1618 // Render regular sections with feature grid.
1619 ?>
1620 <div class="frbl-section-wrapper">
1621 <!-- Section Header -->
1622 <div class="frbl-section-header">
1623 <h2 class="tw:text-2xl tw:font-bold tw:text-gray-900 tw:mb-0">
1624 <?php echo esc_html( $section['title'] ); ?>
1625 </h2>
1626 <?php
1627 if ( $section['callback'] ) {
1628 echo '<div class="tw:text-sm tw:text-gray-600">';
1629 call_user_func( $section['callback'], $section );
1630 echo '</div>';
1631 }
1632 ?>
1633 </div>
1634
1635 <!-- Features Grid -->
1636 <div class="frbl-features-grid">
1637 <?php
1638 foreach ( (array) $wp_settings_fields[ $this->page_slug ][ $section['id'] ] as $field ) {
1639 $this->render_settings_field( $field );
1640 }
1641 ?>
1642 </div>
1643 </div>
1644 <?php
1645 }
1646 }
1647
1648 /**
1649 * Render a single settings field as a card.
1650 *
1651 * @param array $field Field data.
1652 * @return void
1653 */
1654 private function render_settings_field( $field ) {
1655 // Determine if this is a PRO feature (always, regardless of license status).
1656 $is_pro_feature = in_array(
1657 $field['id'],
1658 array(
1659 $this->option_enable_gutenberg,
1660 $this->option_enable_simple_prices_variable_products,
1661 $this->option_enable_after_add_to_cart,
1662 $this->option_deactivate_short_description,
1663 $this->option_move_content_to_short_description,
1664 $this->option_disable_zoom_images,
1665 $this->option_add_share_buttons,
1666 $this->option_deactivate_product_tabs,
1667 $this->option_horizontal_product_form,
1668 $this->option_enable_variant_display_mode,
1669 $this->option_enable_fullpage_scroll,
1670 $this->option_enable_language_banner,
1671 $this->option_enable_popups,
1672 $this->option_checkout_inline,
1673 $this->option_disable_cart_coupon,
1674 $this->option_disable_cart_cross_sells,
1675 $this->option_disable_checkout_coupon,
1676 $this->option_disable_checkout_order_notes,
1677 $this->option_disable_checkout_login_prompt,
1678 ),
1679 true
1680 );
1681
1682 // Apply PRO styling only if license is not valid.
1683 $needs_license = $is_pro_feature && ! $this->is_license_valid;
1684
1685 // Get icon for this feature.
1686 $icon = $this->get_feature_icon( $field['id'] );
1687
1688 // Short descriptions for each field.
1689 $descriptions = array(
1690 $this->option_enable_testimonials => __( 'Add a testimonials block to display customer reviews.', 'frontblocks' ),
1691 $this->option_enable_reading_progress => __( 'Show a progress bar at the top of the page while reading posts.', 'frontblocks' ),
1692 $this->option_enable_back_button => __( 'Add a floating back button for easy navigation.', 'frontblocks' ),
1693 $this->option_enable_scroll_top => __( 'Add a floating button to scroll back to the top of the page.', 'frontblocks' ),
1694 $this->option_enable_events => __( 'Register and display events using a CPT or blog posts.', 'frontblocks' ),
1695 $this->option_enable_fluid_typography => __( 'Font sizes scale smoothly between mobile and desktop using CSS clamp().', 'frontblocks' ),
1696 $this->option_enable_popups => __( 'Create popups with the block editor and configure when and where they appear.', 'frontblocks' ),
1697 $this->option_enable_gutenberg => __( 'Use the block editor to write WooCommerce product descriptions.', 'frontblocks' ),
1698 $this->option_enable_simple_prices_variable_products => __( 'Show a simplified price range for variable products.', 'frontblocks' ),
1699 $this->option_enable_after_add_to_cart => __( 'Insert custom block content right after the Add to Cart button.', 'frontblocks' ),
1700 $this->option_deactivate_short_description => __( 'Remove the short description field from product pages.', 'frontblocks' ),
1701 $this->option_move_content_to_short_description => __( 'Move the main product content into the short description area.', 'frontblocks' ),
1702 $this->option_disable_zoom_images => __( 'Remove the zoom effect on WooCommerce product images.', 'frontblocks' ),
1703 $this->option_add_share_buttons => __( 'Add social share buttons to WooCommerce product pages.', 'frontblocks' ),
1704 $this->option_deactivate_product_tabs => __( 'Remove the default description, reviews and attributes tabs.', 'frontblocks' ),
1705 $this->option_horizontal_product_form => __( 'Display quantity and Add to Cart button side by side.', 'frontblocks' ),
1706 $this->option_enable_variant_display_mode => __( 'Choose per attribute whether to display product variations as a dropdown or radio buttons.', 'frontblocks' ),
1707 $this->option_enable_fullpage_scroll => __( 'Enable full-page scroll navigation between sections.', 'frontblocks' ),
1708 $this->option_enable_language_banner => __( 'Show a banner when the visitor language differs from the site language.', 'frontblocks' ),
1709 $this->option_checkout_inline => __( 'Display address, email and phone fields side by side in the WooCommerce checkout.', 'frontblocks' ),
1710 $this->option_disable_zoom_images => __( 'Remove the zoom effect on WooCommerce product images.', 'frontblocks' ),
1711 $this->option_add_share_buttons => __( 'Add social share buttons to WooCommerce product pages.', 'frontblocks' ),
1712 $this->option_deactivate_product_tabs => __( 'Remove the default description, reviews and attributes tabs.', 'frontblocks' ),
1713 $this->option_horizontal_product_form => __( 'Display quantity and Add to Cart button side by side.', 'frontblocks' ),
1714 $this->option_enable_fullpage_scroll => __( 'Enable full-page scroll navigation between sections.', 'frontblocks' ),
1715 $this->option_enable_language_banner => __( 'Show a banner when the visitor language differs from the site language.', 'frontblocks' ),
1716 $this->option_disable_cart_coupon => __( 'Hide the coupon code input field from the Cart page.', 'frontblocks' ),
1717 $this->option_disable_cart_cross_sells => __( 'Remove the cross-sell product suggestions from the Cart page.', 'frontblocks' ),
1718 $this->option_disable_checkout_coupon => __( 'Hide the coupon code form shown above the Checkout form.', 'frontblocks' ),
1719 $this->option_disable_checkout_order_notes => __( 'Remove the order notes / additional information field from Checkout.', 'frontblocks' ),
1720 $this->option_disable_checkout_login_prompt => __( 'Hide the login and registration prompt shown before the Checkout form.', 'frontblocks' ),
1721 );
1722
1723 $desc = $descriptions[ $field['id'] ] ?? '';
1724
1725 ?>
1726 <div class="frbl-feature-card <?php echo $needs_license ? 'frbl-feature-pro' : ''; ?>">
1727 <?php if ( $is_pro_feature ) : ?>
1728 <div class="frbl-pro-badge">PRO</div>
1729 <?php endif; ?>
1730
1731 <div class="frbl-feature-content">
1732 <div class="frbl-feature-icon">
1733 <?php echo $icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
1734 </div>
1735 <div class="frbl-feature-info">
1736 <h3 class="frbl-feature-title">
1737 <?php echo esc_html( $field['title'] ); ?>
1738 </h3>
1739 <?php if ( $desc ) : ?>
1740 <p class="frbl-feature-description">
1741 <?php echo esc_html( $desc ); ?>
1742 </p>
1743 <?php endif; ?>
1744 </div>
1745 <div class="frbl-feature-toggle">
1746 <?php call_user_func( $field['callback'], $field['args'] ); ?>
1747 </div>
1748 </div>
1749 </div>
1750 <?php
1751 }
1752
1753 /**
1754 * Get icon SVG for a feature.
1755 *
1756 * @param string $field_id Field ID.
1757 * @return string SVG icon markup.
1758 */
1759 private function get_feature_icon( $field_id ) {
1760 // Map field IDs to icon file names.
1761 $icon_map = array(
1762 $this->option_enable_testimonials => 'testimonials',
1763 $this->option_enable_reading_progress => 'reading-progress',
1764 $this->option_enable_back_button => 'back-button',
1765 $this->option_enable_scroll_top => 'scroll-top',
1766 $this->option_enable_events => 'events',
1767 $this->option_enable_fluid_typography => 'fluid-typography',
1768 $this->option_enable_popups => 'popups',
1769 $this->option_enable_gutenberg => 'gutenberg',
1770 $this->option_enable_simple_prices_variable_products => 'simple-prices',
1771 $this->option_enable_after_add_to_cart => 'after-add-to-cart',
1772 $this->option_deactivate_short_description => 'deactivate-description',
1773 $this->option_move_content_to_short_description => 'move-content',
1774 $this->option_disable_zoom_images => 'disable-zoom',
1775 $this->option_add_share_buttons => 'share-buttons',
1776 $this->option_deactivate_product_tabs => 'deactivate-tabs',
1777 $this->option_horizontal_product_form => 'horizontal-form',
1778 $this->option_enable_variant_display_mode => 'default',
1779 $this->option_enable_fullpage_scroll => 'fullpage-scroll',
1780 $this->option_enable_language_banner => 'language-banner',
1781 $this->option_checkout_inline => 'horizontal-form',
1782 $this->option_disable_zoom_images => 'disable-zoom',
1783 $this->option_add_share_buttons => 'share-buttons',
1784 $this->option_deactivate_product_tabs => 'deactivate-tabs',
1785 $this->option_horizontal_product_form => 'horizontal-form',
1786 $this->option_enable_fullpage_scroll => 'fullpage-scroll',
1787 $this->option_enable_language_banner => 'language-banner',
1788 $this->option_disable_cart_coupon => 'default',
1789 $this->option_disable_cart_cross_sells => 'default',
1790 $this->option_disable_checkout_coupon => 'default',
1791 $this->option_disable_checkout_order_notes => 'default',
1792 $this->option_disable_checkout_login_prompt => 'default',
1793 );
1794
1795 $icon_name = $icon_map[ $field_id ] ?? 'default';
1796 $icon_path = FRBL_PLUGIN_PATH . 'assets/admin/icons/' . $icon_name . '.svg';
1797
1798 if ( file_exists( $icon_path ) ) {
1799 $svg_content = file_get_contents( $icon_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
1800 return $svg_content ? $svg_content : '';
1801 }
1802
1803 return '';
1804 }
1805
1806 /**
1807 * PRO Features section description.
1808 *
1809 * @return void
1810 */
1811 public function section_woo_features_callback() {
1812 if ( ! frbl_is_pro_active() ) {
1813 echo '<div class="tw:bg-blue-50 tw:border-l-4 tw:border-blue-400 tw:p-4 tw:mb-4">';
1814 echo '<div class="tw:flex">';
1815 echo '<div class="tw:flex-shrink-0">';
1816 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>';
1817 echo '</div>';
1818 echo '<div class="tw:ml-3">';
1819 echo '<p class="tw:text-sm tw:text-blue-700">';
1820 printf(
1821 /* translators: %s: FrontBlocks PRO link */
1822 esc_html__( 'These features require %s. Upgrade to unlock advanced functionality.', 'frontblocks' ),
1823 '<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>'
1824 );
1825 echo '</p>';
1826 echo '</div>';
1827 echo '</div>';
1828 echo '</div>';
1829 } elseif ( ! $this->is_license_valid ) {
1830 echo '<div class="tw:bg-yellow-50 tw:border-l-4 tw:border-yellow-400 tw:p-4 tw:mb-4">';
1831 echo '<div class="tw:flex">';
1832 echo '<div class="tw:flex-shrink-0">';
1833 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>';
1834 echo '</div>';
1835 echo '<div class="tw:ml-3">';
1836 echo '<p class="tw:text-sm tw:text-yellow-700">';
1837 printf(
1838 /* translators: %s: License section link */
1839 esc_html__( 'License is not activated. Please activate your license in the %s section below to enable these features.', 'frontblocks' ),
1840 '<a href="#frontblocks_section_license" class="tw:font-medium tw:underline">' . esc_html__( 'License', 'frontblocks' ) . '</a>'
1841 );
1842 echo '</p>';
1843 echo '</div>';
1844 echo '</div>';
1845 echo '</div>';
1846 } else {
1847 ?>
1848 <p class="tw:text-sm tw:text-gray-600 tw:mt-0 tw:mb-4">
1849 <?php echo esc_html__( 'Advanced features for WooCommerce and more.', 'frontblocks' ); ?>
1850 </p>
1851 <?php
1852 }
1853 }
1854
1855 /**
1856 * Render toggle field for enable testimonials.
1857 *
1858 * @return void
1859 */
1860 public function field_enable_testimonials() {
1861 $options = get_option( 'frontblocks_settings', array() );
1862 $enabled = (bool) ( $options[ $this->option_enable_testimonials ] ?? false );
1863 ?>
1864 <label class="frbl-toggle">
1865 <input type="checkbox"
1866 id="<?php echo esc_attr( $this->option_enable_testimonials ); ?>"
1867 name="frontblocks_settings[<?php echo esc_attr( $this->option_enable_testimonials ); ?>]"
1868 value="1"
1869 <?php checked( true, $enabled ); ?>
1870 />
1871 <span></span>
1872 </label>
1873 <?php
1874 }
1875
1876 /**
1877 * Render toggle field for enable reading progress bar.
1878 *
1879 * @return void
1880 */
1881 public function field_enable_reading_progress() {
1882 $options = get_option( 'frontblocks_settings', array() );
1883 $enabled = (bool) ( $options[ $this->option_enable_reading_progress ] ?? false );
1884 ?>
1885 <label class="frbl-toggle">
1886 <input type="checkbox"
1887 id="<?php echo esc_attr( $this->option_enable_reading_progress ); ?>"
1888 name="frontblocks_settings[<?php echo esc_attr( $this->option_enable_reading_progress ); ?>]"
1889 value="1"
1890 <?php checked( true, $enabled ); ?>
1891 />
1892 <span></span>
1893 </label>
1894 <?php
1895 }
1896
1897 /**
1898 * Render toggle field for enable back button.
1899 *
1900 * @return void
1901 */
1902 public function field_enable_back_button() {
1903 $options = get_option( 'frontblocks_settings', array() );
1904 $enabled = (bool) ( $options[ $this->option_enable_back_button ] ?? false );
1905 ?>
1906 <label class="frbl-toggle">
1907 <input type="checkbox"
1908 id="<?php echo esc_attr( $this->option_enable_back_button ); ?>"
1909 name="frontblocks_settings[<?php echo esc_attr( $this->option_enable_back_button ); ?>]"
1910 value="1"
1911 <?php checked( true, $enabled ); ?>
1912 />
1913 <span></span>
1914 </label>
1915 <?php
1916 }
1917
1918 /**
1919 * Render toggle field and sub-settings for scroll to top.
1920 *
1921 * @return void
1922 */
1923 public function field_enable_scroll_top() {
1924 $options = get_option( 'frontblocks_settings', array() );
1925 $enabled = (bool) ( $options[ $this->option_enable_scroll_top ] ?? false );
1926 $position = sanitize_text_field( $options[ $this->option_scroll_top_position ] ?? 'bottom-right' );
1927 $icon_url = esc_url( $options[ $this->option_scroll_top_icon_url ] ?? '' );
1928 ?>
1929 <label class="frbl-toggle">
1930 <input type="checkbox"
1931 id="<?php echo esc_attr( $this->option_enable_scroll_top ); ?>"
1932 name="frontblocks_settings[<?php echo esc_attr( $this->option_enable_scroll_top ); ?>]"
1933 value="1"
1934 <?php checked( true, $enabled ); ?>
1935 />
1936 <span></span>
1937 </label>
1938
1939 <!-- Sub-settings - moved below the card by JavaScript -->
1940 <div id="scroll-top-settings-wrapper" style="<?php echo $enabled ? '' : 'display: none;'; ?>">
1941 <div class="tw-mb-4">
1942 <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">
1943 <?php echo esc_html__( 'Position', 'frontblocks' ); ?>
1944 </label>
1945 <select
1946 id="<?php echo esc_attr( $this->option_scroll_top_position ); ?>"
1947 name="frontblocks_settings[<?php echo esc_attr( $this->option_scroll_top_position ); ?>]"
1948 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"
1949 >
1950 <option value="bottom-right" <?php selected( $position, 'bottom-right' ); ?>>
1951 <?php echo esc_html__( 'Bottom right', 'frontblocks' ); ?>
1952 </option>
1953 <option value="bottom-left" <?php selected( $position, 'bottom-left' ); ?>>
1954 <?php echo esc_html__( 'Bottom left', 'frontblocks' ); ?>
1955 </option>
1956 </select>
1957 </div>
1958
1959 <div>
1960 <label class="tw-block tw-text-sm tw-font-medium tw-text-gray-700 tw-mb-2">
1961 <?php echo esc_html__( 'Custom icon (optional)', 'frontblocks' ); ?>
1962 </label>
1963 <div id="scroll-top-icon-preview" class="tw-mb-2" style="<?php echo $icon_url ? '' : 'display: none;'; ?>">
1964 <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;" />
1965 </div>
1966 <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 ); ?>" />
1967 <div class="tw-flex tw-gap-2 tw-items-center">
1968 <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">
1969 <?php echo esc_html__( 'Select image', 'frontblocks' ); ?>
1970 </button>
1971 <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;'; ?>">
1972 <?php echo esc_html__( 'Remove', 'frontblocks' ); ?>
1973 </button>
1974 </div>
1975 <p class="tw-text-xs tw-text-gray-500 tw-mt-2">
1976 <?php echo esc_html__( 'Upload an SVG, PNG or any image. Leave empty to use the default arrow icon.', 'frontblocks' ); ?>
1977 </p>
1978 </div>
1979 </div>
1980 <?php
1981 }
1982
1983 /**
1984 * Render toggle field for enable events.
1985 *
1986 * @return void
1987 */
1988 public function field_enable_events() {
1989 $options = get_option( 'frontblocks_settings', array() );
1990 $enabled = (bool) ( $options[ $this->option_enable_events ] ?? false );
1991 $events_type = sanitize_text_field( $options[ $this->option_events_type ] ?? 'cpt' );
1992 ?>
1993 <!-- Toggle - stays in horizontal layout with icon and text -->
1994 <label class="frbl-toggle">
1995 <input type="checkbox"
1996 id="<?php echo esc_attr( $this->option_enable_events ); ?>"
1997 name="frontblocks_settings[<?php echo esc_attr( $this->option_enable_events ); ?>]"
1998 value="1"
1999 <?php checked( true, $enabled ); ?>
2000 />
2001 <span></span>
2002 </label>
2003
2004 <!-- Select and description - will be moved below the card by JavaScript -->
2005 <div id="events-type-wrapper" class="tw:mt-4" style="<?php echo $enabled ? 'width: 100%; min-width: 100%; display: block;' : 'display: none;'; ?>">
2006 <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">
2007 <?php echo esc_html__( 'Event type', 'frontblocks' ); ?>
2008 </label>
2009 <select
2010 id="<?php echo esc_attr( $this->option_events_type ); ?>"
2011 name="frontblocks_settings[<?php echo esc_attr( $this->option_events_type ); ?>]"
2012 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"
2013 style="width: 100%; min-width: 100%; max-width: 100%; box-sizing: border-box;"
2014 >
2015 <option value="cpt" <?php selected( $events_type, 'cpt' ); ?>>
2016 <?php echo esc_html__( 'Custom Post Type (CPT)', 'frontblocks' ); ?>
2017 </option>
2018 <option value="posts" <?php selected( $events_type, 'posts' ); ?>>
2019 <?php echo esc_html__( 'Blog posts', 'frontblocks' ); ?>
2020 </option>
2021 </select>
2022 <p class="tw:text-xs tw:text-gray-500 tw:mt-2">
2023 <?php echo esc_html__( 'Choose whether events will be created in a dedicated CPT or in regular blog posts.', 'frontblocks' ); ?>
2024 </p>
2025 </div>
2026 <?php
2027 }
2028
2029 /**
2030 * Render toggle field for enable fluid typography.
2031 *
2032 * @return void
2033 */
2034 public function field_enable_fluid_typography() {
2035 $options = get_option( 'frontblocks_settings', array() );
2036 $enabled = (bool) ( $options[ $this->option_enable_fluid_typography ] ?? true );
2037 ?>
2038 <label class="frbl-toggle">
2039 <input type="checkbox"
2040 id="<?php echo esc_attr( $this->option_enable_fluid_typography ); ?>"
2041 name="frontblocks_settings[<?php echo esc_attr( $this->option_enable_fluid_typography ); ?>]"
2042 value="1"
2043 <?php checked( true, $enabled ); ?>
2044 />
2045 <span></span>
2046 </label>
2047 <?php
2048 }
2049
2050 /**
2051 * Render toggle field for enable maintenance mode.
2052 *
2053 * @return void
2054 */
2055 public function field_enable_maintenance() {
2056 $options = get_option( 'frontblocks_settings', array() );
2057 $enabled = (bool) ( $options[ $this->option_enable_maintenance ] ?? false );
2058 $title = (string) ( $options[ $this->option_maintenance_title ] ?? '' );
2059 $image_id = (int) ( $options[ $this->option_maintenance_image ] ?? 0 );
2060 $image_url = $image_id ? wp_get_attachment_image_url( $image_id, 'medium' ) : '';
2061 ?>
2062 <div class="frbl-maintenance-wrapper">
2063 <div class="tw:flex tw:items-center tw:justify-between tw:mb-4">
2064 <label for="<?php echo esc_attr( $this->option_enable_maintenance ); ?>" class="tw:text-base tw:font-medium tw:text-gray-900">
2065 <?php echo esc_html__( 'Enable Maintenance Mode', 'frontblocks' ); ?>
2066 </label>
2067 <label class="frbl-toggle">
2068 <input type="checkbox"
2069 id="<?php echo esc_attr( $this->option_enable_maintenance ); ?>"
2070 name="frontblocks_settings[<?php echo esc_attr( $this->option_enable_maintenance ); ?>]"
2071 value="1"
2072 <?php checked( true, $enabled ); ?>
2073 />
2074 <span></span>
2075 </label>
2076 </div>
2077
2078 <div id="maintenance-fields-wrapper" style="<?php echo $enabled ? '' : 'display: none;'; ?>">
2079 <div class="tw:p-4 tw:bg-gray-50 tw:rounded-lg tw:border tw:border-gray-200">
2080 <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">
2081 <?php echo esc_html__( 'Maintenance page title', 'frontblocks' ); ?>
2082 </label>
2083 <input
2084 type="text"
2085 id="<?php echo esc_attr( $this->option_maintenance_title ); ?>"
2086 name="frontblocks_settings[<?php echo esc_attr( $this->option_maintenance_title ); ?>]"
2087 value="<?php echo esc_attr( $title ); ?>"
2088 placeholder="<?php echo esc_attr__( 'We are currently performing maintenance', 'frontblocks' ); ?>"
2089 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"
2090 />
2091
2092 <label class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mt-4 tw:mb-2">
2093 <?php echo esc_html__( 'Background image', 'frontblocks' ); ?>
2094 </label>
2095 <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 ); ?>" />
2096 <div class="frbl-maintenance-image-preview tw:mb-2" style="<?php echo $image_url ? '' : 'display:none;'; ?>">
2097 <img src="<?php echo esc_url( $image_url ? $image_url : '' ); ?>" alt="" style="max-width: 200px; height: auto; border-radius: 8px; display: block;" />
2098 </div>
2099 <div class="tw:flex tw:gap-2">
2100 <button type="button" class="button frbl-maintenance-select-image">
2101 <?php echo esc_html__( 'Select image', 'frontblocks' ); ?>
2102 </button>
2103 <button type="button" class="button frbl-maintenance-remove-image" style="<?php echo $image_url ? '' : 'display:none;'; ?>">
2104 <?php echo esc_html__( 'Remove image', 'frontblocks' ); ?>
2105 </button>
2106 </div>
2107 <p class="tw:text-xs tw:text-gray-500 tw:mt-2 tw:mb-0">
2108 <?php echo esc_html__( 'Shown as the full-screen background while maintenance mode is active. Recommended size: 1920×1080px.', 'frontblocks' ); ?>
2109 </p>
2110 </div>
2111 </div>
2112 </div>
2113 <?php
2114 }
2115
2116 /**
2117 * Render toggle field and sub-settings for the cookie notice banner.
2118 *
2119 * @return void
2120 */
2121 public function field_enable_cookie_notice() {
2122 $options = get_option( 'frontblocks_settings', array() );
2123 $enabled = (bool) ( $options[ $this->option_enable_cookie_notice ] ?? false );
2124 $message = (string) ( $options[ $this->option_cookie_notice_message ] ?? '' );
2125 $accept_label = (string) ( $options[ $this->option_cookie_notice_accept_label ] ?? '' );
2126 $reject_label = (string) ( $options[ $this->option_cookie_notice_reject_label ] ?? '' );
2127 $policy_page_id = (int) ( $options[ $this->option_cookie_notice_policy_page_id ] ?? 0 );
2128 $layout = (string) ( $options[ $this->option_cookie_notice_layout ] ?? 'bar' );
2129 $position = (string) ( $options[ $this->option_cookie_notice_position ] ?? 'bottom-right' );
2130 $color = (string) ( $options[ $this->option_cookie_notice_color ] ?? '#687df9' );
2131 $expiration = (int) ( $options[ $this->option_cookie_notice_expiration_days ] ?? 365 );
2132 $gtm_id = (string) ( $options[ $this->option_cookie_notice_gtm_id ] ?? '' );
2133 $ga4_id = (string) ( $options[ $this->option_cookie_notice_ga4_id ] ?? '' );
2134 $accepted_count = (int) get_option( \FrontBlocks\Frontend\CookieNotice::STATS_OPTION_ACCEPTED, 0 );
2135 $rejected_count = (int) get_option( \FrontBlocks\Frontend\CookieNotice::STATS_OPTION_REJECTED, 0 );
2136 $total_count = $accepted_count + $rejected_count;
2137 $acceptance_pct = $total_count > 0 ? round( ( $accepted_count / $total_count ) * 100, 1 ) : 0;
2138 ?>
2139 <div class="frbl-cookie-notice-wrapper">
2140 <div class="tw:flex tw:items-center tw:justify-between tw:mb-4">
2141 <label for="<?php echo esc_attr( $this->option_enable_cookie_notice ); ?>" class="tw:text-base tw:font-medium tw:text-gray-900">
2142 <?php echo esc_html__( 'Enable Cookie Notice', 'frontblocks' ); ?>
2143 </label>
2144 <label class="frbl-toggle">
2145 <input type="checkbox"
2146 id="<?php echo esc_attr( $this->option_enable_cookie_notice ); ?>"
2147 name="frontblocks_settings[<?php echo esc_attr( $this->option_enable_cookie_notice ); ?>]"
2148 value="1"
2149 <?php checked( true, $enabled ); ?>
2150 />
2151 <span></span>
2152 </label>
2153 </div>
2154
2155 <div id="cookie-notice-fields-wrapper" style="<?php echo $enabled ? '' : 'display: none;'; ?>">
2156
2157 <?php if ( $total_count > 0 ) : ?>
2158 <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">
2159 <div class="tw:flex-1">
2160 <p class="tw:text-xs tw:text-gray-500 tw:mb-1"><?php echo esc_html__( 'Acceptance rate', 'frontblocks' ); ?></p>
2161 <p class="tw:text-2xl tw:font-bold tw:text-gray-900 tw:m-0"><?php echo esc_html( $acceptance_pct ); ?>%</p>
2162 </div>
2163 <div class="tw:flex-1">
2164 <p class="tw:text-xs tw:text-gray-500 tw:mb-1"><?php echo esc_html__( 'Accepted', 'frontblocks' ); ?></p>
2165 <p class="tw:text-2xl tw:font-bold tw:text-gray-900 tw:m-0"><?php echo esc_html( $accepted_count ); ?></p>
2166 </div>
2167 <div class="tw:flex-1">
2168 <p class="tw:text-xs tw:text-gray-500 tw:mb-1"><?php echo esc_html__( 'Rejected', 'frontblocks' ); ?></p>
2169 <p class="tw:text-2xl tw:font-bold tw:text-gray-900 tw:m-0"><?php echo esc_html( $rejected_count ); ?></p>
2170 </div>
2171 </div>
2172 <?php endif; ?>
2173
2174 <div class="tw:p-4 tw:bg-gray-50 tw:rounded-lg tw:border tw:border-gray-200 tw:mb-4">
2175 <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">
2176 <?php echo esc_html__( 'Message', 'frontblocks' ); ?>
2177 </label>
2178 <textarea
2179 id="<?php echo esc_attr( $this->option_cookie_notice_message ); ?>"
2180 name="frontblocks_settings[<?php echo esc_attr( $this->option_cookie_notice_message ); ?>]"
2181 rows="3"
2182 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' ); ?>"
2183 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"
2184 ><?php echo esc_textarea( $message ); ?></textarea>
2185
2186 <div class="tw:grid tw:grid-cols-2 tw:gap-4 tw:mt-4">
2187 <div>
2188 <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">
2189 <?php echo esc_html__( 'Accept button label', 'frontblocks' ); ?>
2190 </label>
2191 <input
2192 type="text"
2193 id="<?php echo esc_attr( $this->option_cookie_notice_accept_label ); ?>"
2194 name="frontblocks_settings[<?php echo esc_attr( $this->option_cookie_notice_accept_label ); ?>]"
2195 value="<?php echo esc_attr( $accept_label ); ?>"
2196 placeholder="<?php echo esc_attr__( 'Accept', 'frontblocks' ); ?>"
2197 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"
2198 />
2199 </div>
2200 <div>
2201 <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">
2202 <?php echo esc_html__( 'Reject button label', 'frontblocks' ); ?>
2203 </label>
2204 <input
2205 type="text"
2206 id="<?php echo esc_attr( $this->option_cookie_notice_reject_label ); ?>"
2207 name="frontblocks_settings[<?php echo esc_attr( $this->option_cookie_notice_reject_label ); ?>]"
2208 value="<?php echo esc_attr( $reject_label ); ?>"
2209 placeholder="<?php echo esc_attr__( 'Reject', 'frontblocks' ); ?>"
2210 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"
2211 />
2212 </div>
2213 </div>
2214
2215 <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">
2216 <?php echo esc_html__( 'Cookie policy page (optional)', 'frontblocks' ); ?>
2217 </label>
2218 <select
2219 id="<?php echo esc_attr( $this->option_cookie_notice_policy_page_id ); ?>"
2220 name="frontblocks_settings[<?php echo esc_attr( $this->option_cookie_notice_policy_page_id ); ?>]"
2221 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"
2222 >
2223 <option value=""><?php echo esc_html__( '— None —', 'frontblocks' ); ?></option>
2224 <?php
2225 $pages_limit = 300;
2226 $pages = get_pages(
2227 array(
2228 'sort_column' => 'post_title',
2229 'number' => $pages_limit,
2230 )
2231 );
2232
2233 // The saved page may fall outside the limited list above (e.g. sorted
2234 // past it alphabetically on a large site) — make sure it still shows up
2235 // and stays selected instead of silently disappearing from the dropdown.
2236 $selected_page_listed = 0 === $policy_page_id;
2237 foreach ( $pages as $page ) {
2238 if ( $policy_page_id === $page->ID ) {
2239 $selected_page_listed = true;
2240 break;
2241 }
2242 }
2243
2244 if ( ! $selected_page_listed ) {
2245 $selected_page = get_post( $policy_page_id );
2246 if ( $selected_page instanceof \WP_Post ) {
2247 array_unshift( $pages, $selected_page );
2248 }
2249 }
2250
2251 foreach ( $pages as $page ) {
2252 printf(
2253 '<option value="%1$d" %2$s>%3$s</option>',
2254 (int) $page->ID,
2255 selected( $policy_page_id, $page->ID, false ),
2256 esc_html( $page->post_title )
2257 );
2258 }
2259 ?>
2260 </select>
2261 <?php if ( count( $pages ) >= $pages_limit ) : ?>
2262 <p class="tw:text-xs tw:text-amber-600 tw:mt-2">
2263 <?php
2264 printf(
2265 /* translators: %d: number of pages shown in the dropdown. */
2266 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' ),
2267 (int) $pages_limit
2268 );
2269 ?>
2270 </p>
2271 <?php endif; ?>
2272 <p class="tw:text-xs tw:text-gray-500 tw:mt-2">
2273 <?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' ); ?>
2274 </p>
2275 </div>
2276
2277 <div class="tw:p-4 tw:bg-gray-50 tw:rounded-lg tw:border tw:border-gray-200 tw:mb-4">
2278 <div class="tw:grid tw:grid-cols-3 tw:gap-4">
2279 <div>
2280 <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">
2281 <?php echo esc_html__( 'Layout', 'frontblocks' ); ?>
2282 </label>
2283 <select
2284 id="<?php echo esc_attr( $this->option_cookie_notice_layout ); ?>"
2285 name="frontblocks_settings[<?php echo esc_attr( $this->option_cookie_notice_layout ); ?>]"
2286 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"
2287 >
2288 <option value="bar" <?php selected( $layout, 'bar' ); ?>><?php echo esc_html__( 'Full-width bar', 'frontblocks' ); ?></option>
2289 <option value="box" <?php selected( $layout, 'box' ); ?>><?php echo esc_html__( 'Boxed panel', 'frontblocks' ); ?></option>
2290 <option value="popup" <?php selected( $layout, 'popup' ); ?>><?php echo esc_html__( 'Centered popup', 'frontblocks' ); ?></option>
2291 </select>
2292 </div>
2293 <div id="cookie-notice-position-wrapper" style="<?php echo 'box' === $layout ? '' : 'display: none;'; ?>">
2294 <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">
2295 <?php echo esc_html__( 'Position', 'frontblocks' ); ?>
2296 </label>
2297 <select
2298 id="<?php echo esc_attr( $this->option_cookie_notice_position ); ?>"
2299 name="frontblocks_settings[<?php echo esc_attr( $this->option_cookie_notice_position ); ?>]"
2300 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"
2301 >
2302 <option value="bottom-right" <?php selected( $position, 'bottom-right' ); ?>><?php echo esc_html__( 'Bottom right', 'frontblocks' ); ?></option>
2303 <option value="bottom-left" <?php selected( $position, 'bottom-left' ); ?>><?php echo esc_html__( 'Bottom left', 'frontblocks' ); ?></option>
2304 </select>
2305 </div>
2306 <div>
2307 <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">
2308 <?php echo esc_html__( 'Accent color', 'frontblocks' ); ?>
2309 </label>
2310 <input
2311 type="color"
2312 id="<?php echo esc_attr( $this->option_cookie_notice_color ); ?>"
2313 name="frontblocks_settings[<?php echo esc_attr( $this->option_cookie_notice_color ); ?>]"
2314 value="<?php echo esc_attr( $color ); ?>"
2315 class="tw:h-10 tw:w-full tw:border tw:border-gray-300 tw:rounded-lg"
2316 />
2317 </div>
2318 </div>
2319
2320 <div class="tw:mt-4">
2321 <p class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mb-2">
2322 <?php echo esc_html__( 'Preview', 'frontblocks' ); ?>
2323 </p>
2324 <?php
2325 $preview_accent_text = \FrontBlocks\Frontend\CookieNotice::get_readable_text_color( $color );
2326 $preview_accent_link = \FrontBlocks\Frontend\CookieNotice::get_readable_on_white_color( $color );
2327 ?>
2328 <div id="frbl-cookie-notice-preview-stage" class="frbl-cookie-notice-preview-stage">
2329 <div
2330 id="frbl-cookie-notice-preview"
2331 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' ) : ''; ?>"
2332 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 ); ?>;"
2333 >
2334 <div class="frbl-cookie-notice__panel">
2335 <span id="frbl-cookie-notice-preview-icon" class="frbl-cookie-notice__icon">
2336 <?php echo \FrontBlocks\Frontend\CookieNotice::get_cookie_icon_svg(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static inline SVG, no dynamic data. ?>
2337 </span>
2338 <p class="frbl-cookie-notice__message">
2339 <?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' ) ); ?>
2340 </p>
2341 <div class="frbl-cookie-notice__actions">
2342 <button type="button" class="frbl-cookie-notice__button frbl-cookie-notice__button--reject" disabled>
2343 <?php echo esc_html( '' !== $reject_label ? $reject_label : __( 'Reject', 'frontblocks' ) ); ?>
2344 </button>
2345 <button type="button" class="frbl-cookie-notice__button frbl-cookie-notice__button--accept" disabled>
2346 <?php echo esc_html( '' !== $accept_label ? $accept_label : __( 'Accept', 'frontblocks' ) ); ?>
2347 </button>
2348 </div>
2349 </div>
2350 </div>
2351 </div>
2352 </div>
2353
2354 <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">
2355 <?php echo esc_html__( 'Cookie expiration (days)', 'frontblocks' ); ?>
2356 </label>
2357 <input
2358 type="number"
2359 min="1"
2360 max="730"
2361 id="<?php echo esc_attr( $this->option_cookie_notice_expiration_days ); ?>"
2362 name="frontblocks_settings[<?php echo esc_attr( $this->option_cookie_notice_expiration_days ); ?>]"
2363 value="<?php echo esc_attr( $expiration ); ?>"
2364 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"
2365 />
2366 </div>
2367
2368 <div class="tw:p-4 tw:bg-gray-50 tw:rounded-lg tw:border tw:border-gray-200">
2369 <p class="tw:text-sm tw:text-gray-600 tw:mt-0 tw:mb-4">
2370 <?php echo esc_html__( 'Scripts are only requested after a visitor accepts — never before.', 'frontblocks' ); ?>
2371 </p>
2372 <div class="tw:grid tw:grid-cols-2 tw:gap-4">
2373 <div>
2374 <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">
2375 <?php echo esc_html__( 'Google Tag Manager ID', 'frontblocks' ); ?>
2376 </label>
2377 <input
2378 type="text"
2379 id="<?php echo esc_attr( $this->option_cookie_notice_gtm_id ); ?>"
2380 name="frontblocks_settings[<?php echo esc_attr( $this->option_cookie_notice_gtm_id ); ?>]"
2381 value="<?php echo esc_attr( $gtm_id ); ?>"
2382 placeholder="GTM-XXXXXXX"
2383 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"
2384 />
2385 </div>
2386 <div>
2387 <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">
2388 <?php echo esc_html__( 'GA4 Measurement ID', 'frontblocks' ); ?>
2389 </label>
2390 <input
2391 type="text"
2392 id="<?php echo esc_attr( $this->option_cookie_notice_ga4_id ); ?>"
2393 name="frontblocks_settings[<?php echo esc_attr( $this->option_cookie_notice_ga4_id ); ?>]"
2394 value="<?php echo esc_attr( $ga4_id ); ?>"
2395 placeholder="G-XXXXXXXXXX"
2396 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"
2397 />
2398 </div>
2399 </div>
2400 </div>
2401 </div>
2402 </div>
2403 <?php
2404 }
2405
2406 /**
2407 * Render toggle field for enable popups (PRO feature).
2408 *
2409 * @return void
2410 */
2411 public function field_enable_popups() {
2412 $this->render_pro_toggle( $this->option_enable_popups );
2413 }
2414
2415 /**
2416 * Render toggle field for enable Gutenberg in products (PRO).
2417 *
2418 * @return void
2419 */
2420 public function field_enable_gutenberg() {
2421 $this->render_pro_toggle( $this->option_enable_gutenberg );
2422 }
2423
2424 /**
2425 * Render toggle field for enable Simple Prices Variable Products (PRO).
2426 *
2427 * @return void
2428 */
2429 public function field_enable_simple_prices_variable_products() {
2430 $this->render_pro_toggle( $this->option_enable_simple_prices_variable_products );
2431 }
2432
2433 /**
2434 * Render After Add to Cart Block field.
2435 *
2436 * @return void
2437 */
2438 public function field_enable_after_add_to_cart() {
2439 $this->render_pro_toggle( $this->option_enable_after_add_to_cart );
2440 }
2441
2442 /**
2443 * Render Deactivate Short Description field.
2444 *
2445 * @return void
2446 */
2447 public function field_deactivate_short_description() {
2448 $this->render_pro_toggle( $this->option_deactivate_short_description );
2449 }
2450
2451 /**
2452 * Render Move Content to Short Description field.
2453 *
2454 * @return void
2455 */
2456 public function field_move_content_to_short_description() {
2457 $this->render_pro_toggle( $this->option_move_content_to_short_description );
2458 }
2459
2460 /**
2461 * Render Disable Zoom in Product Images field.
2462 *
2463 * @return void
2464 */
2465 public function field_disable_zoom_images() {
2466 $this->render_pro_toggle( $this->option_disable_zoom_images );
2467 }
2468
2469 /**
2470 * Render Add Share Buttons in Product Page field.
2471 *
2472 * @return void
2473 */
2474 public function field_add_share_buttons() {
2475 $this->render_pro_toggle( $this->option_add_share_buttons );
2476 }
2477
2478 /**
2479 * Render Deactivate Product Tabs field.
2480 *
2481 * @return void
2482 */
2483 public function field_deactivate_product_tabs() {
2484 $this->render_pro_toggle( $this->option_deactivate_product_tabs );
2485 }
2486
2487 /**
2488 * Render Horizontal Product Form Layout field.
2489 *
2490 * @return void
2491 */
2492 public function field_horizontal_product_form() {
2493 $this->render_pro_toggle( $this->option_horizontal_product_form );
2494 }
2495
2496 /**
2497 * Render Variant Display Mode toggle field.
2498 *
2499 * @return void
2500 */
2501 public function field_enable_variant_display_mode() {
2502 $this->render_pro_toggle( $this->option_enable_variant_display_mode );
2503 }
2504
2505 /**
2506 * Render Disable Coupon Field in Cart field.
2507 *
2508 * @return void
2509 */
2510 public function field_disable_cart_coupon() {
2511 $this->render_pro_toggle( $this->option_disable_cart_coupon );
2512 }
2513
2514 /**
2515 * Render Disable Cross-Sells in Cart field.
2516 *
2517 * @return void
2518 */
2519 public function field_disable_cart_cross_sells() {
2520 $this->render_pro_toggle( $this->option_disable_cart_cross_sells );
2521 }
2522
2523 /**
2524 * Render Disable Coupon Field in Checkout field.
2525 *
2526 * @return void
2527 */
2528 public function field_disable_checkout_coupon() {
2529 $this->render_pro_toggle( $this->option_disable_checkout_coupon );
2530 }
2531
2532 /**
2533 * Render Disable Order Notes in Checkout field.
2534 *
2535 * @return void
2536 */
2537 public function field_disable_checkout_order_notes() {
2538 $this->render_pro_toggle( $this->option_disable_checkout_order_notes );
2539 }
2540
2541 /**
2542 * Render Disable Login Prompt in Checkout field.
2543 *
2544 * @return void
2545 */
2546 public function field_disable_checkout_login_prompt() {
2547 $this->render_pro_toggle( $this->option_disable_checkout_login_prompt );
2548 }
2549
2550 /**
2551 * Render Enable Full Page Scroll field.
2552 *
2553 * @return void
2554 */
2555 public function field_enable_fullpage_scroll() {
2556 $this->render_pro_toggle( $this->option_enable_fullpage_scroll );
2557 }
2558
2559 /**
2560 * Render Enable Language Banner field.
2561 *
2562 * @return void
2563 */
2564 public function field_enable_language_banner() {
2565 $this->render_pro_toggle( $this->option_enable_language_banner );
2566 }
2567
2568 /**
2569 * Render toggle field for checkout inline fields (PRO).
2570 *
2571 * @return void
2572 */
2573 public function field_checkout_inline() {
2574 $this->render_pro_toggle( $this->option_checkout_inline );
2575 }
2576
2577 /**
2578 * Custom Post Types section callback.
2579 *
2580 * @return void
2581 */
2582 public function section_custom_post_types_callback() {
2583 if ( ! frbl_is_pro_active() ) {
2584 echo '<div class="tw:bg-blue-50 tw:border-l-4 tw:border-blue-400 tw:p-4 tw:mb-4">';
2585 echo '<div class="tw:flex">';
2586 echo '<div class="tw:flex-shrink-0">';
2587 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>';
2588 echo '</div>';
2589 echo '<div class="tw:ml-3">';
2590 echo '<p class="tw:text-sm tw:text-blue-700">';
2591 printf(
2592 /* translators: %s: FrontBlocks PRO link */
2593 esc_html__( 'This feature requires %s. Upgrade to unlock advanced functionality.', 'frontblocks' ),
2594 '<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>'
2595 );
2596 echo '</p>';
2597 echo '</div>';
2598 echo '</div>';
2599 echo '</div>';
2600 } elseif ( ! $this->is_license_valid ) {
2601 echo '<div class="tw:bg-yellow-50 tw:border-l-4 tw:border-yellow-400 tw:p-4 tw:mb-4">';
2602 echo '<div class="tw:flex">';
2603 echo '<div class="tw:flex-shrink-0">';
2604 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>';
2605 echo '</div>';
2606 echo '<div class="tw:ml-3">';
2607 echo '<p class="tw:text-sm tw:text-yellow-700">';
2608 printf(
2609 /* translators: %s: License section link */
2610 esc_html__( 'License is not activated. Please activate your license in the %s section below to enable these features.', 'frontblocks' ),
2611 '<a href="#frontblocks_section_license" class="tw:font-medium tw:underline">' . esc_html__( 'License', 'frontblocks' ) . '</a>'
2612 );
2613 echo '</p>';
2614 echo '</div>';
2615 echo '</div>';
2616 echo '</div>';
2617 } else {
2618 ?>
2619 <p class="tw:text-sm tw:text-gray-600 tw:mt-0 tw:mb-4">
2620 <?php echo esc_html__( 'Create and manage custom post types with advanced configuration options.', 'frontblocks' ); ?>
2621 </p>
2622 <?php
2623 }
2624 }
2625
2626 /**
2627 * Render toggle field for enable custom post types.
2628 *
2629 * @return void
2630 */
2631 public function field_enable_custom_post_types() {
2632 $options = get_option( 'frontblocks_settings', array() );
2633 $enabled = (bool) ( $options[ $this->option_enable_custom_post_types ] ?? false );
2634 $is_enabled = $this->is_license_valid;
2635 $disabled = ! $is_enabled ? 'disabled' : '';
2636 ?>
2637 <div class="frbl-custom-post-types-wrapper">
2638 <div class="tw:flex tw:items-center tw:justify-between tw:mb-4">
2639 <label for="<?php echo esc_attr( $this->option_enable_custom_post_types ); ?>" class="tw:text-base tw:font-medium tw:text-gray-900">
2640 <?php echo esc_html__( 'Enable Custom Post Types Builder', 'frontblocks' ); ?>
2641 </label>
2642 <label class="frbl-toggle">
2643 <input type="checkbox"
2644 id="<?php echo esc_attr( $this->option_enable_custom_post_types ); ?>"
2645 name="frontblocks_settings[<?php echo esc_attr( $this->option_enable_custom_post_types ); ?>]"
2646 value="1"
2647 <?php checked( true, $enabled ); ?>
2648 <?php echo esc_attr( $disabled ); ?>
2649 />
2650 <span></span>
2651 </label>
2652 </div>
2653
2654 <?php if ( $is_enabled ) : ?>
2655 <div id="frbl-cpt-builder" class="frbl-cpt-builder" style="<?php echo $enabled ? '' : 'display: none;'; ?>">
2656 <div class="tw:mt-4 tw:p-4 tw:bg-gray-50 tw:rounded-lg tw:border tw:border-gray-200">
2657 <label for="frbl-cpt-name" class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mb-2">
2658 <?php echo esc_html__( 'Post Type Name', 'frontblocks' ); ?>
2659 </label>
2660 <div class="tw:flex tw:gap-2">
2661 <input
2662 type="text"
2663 id="frbl-cpt-name"
2664 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"
2665 placeholder="<?php echo esc_attr__( 'e.g., Portfolio, Team, Services', 'frontblocks' ); ?>"
2666 />
2667 <button
2668 type="button"
2669 id="frbl-create-cpt-btn"
2670 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"
2671 >
2672 <?php echo esc_html__( 'Create', 'frontblocks' ); ?>
2673 </button>
2674 </div>
2675 <p class="tw:text-xs tw:text-gray-500 tw:mt-2">
2676 <?php echo esc_html__( 'Enter a singular name for your custom post type (e.g., "Portfolio" will create "portfolio" post type).', 'frontblocks' ); ?>
2677 </p>
2678 </div>
2679
2680 <?php do_action( 'frontblocks_render_existing_cpts' ); ?>
2681 </div>
2682 <?php endif; ?>
2683 </div>
2684 <?php
2685 }
2686
2687 /**
2688 * Render PRO features section after the main form.
2689 * Shows when frbl_pro_blocks filter returns blocks and license is not active.
2690 *
2691 * @return void
2692 */
2693 /**
2694 * Default PRO blocks list — shown as promotional cards when PRO is not installed.
2695 * The frbl_pro_blocks filter lets the PRO plugin override this list.
2696 *
2697 * @return array<int, array<string, string>>
2698 */
2699 private function get_default_pro_blocks(): array {
2700 return array(
2701 array(
2702 'icon' => 'meta-fields',
2703 'title' => __( 'Dynamic Meta Fields', 'frontblocks' ),
2704 'desc' => __( 'Bind any paragraph or heading to a custom post meta field, editable from the block editor.', 'frontblocks' ),
2705 ),
2706 array(
2707 'icon' => 'user-text',
2708 'title' => __( 'User Data Block', 'frontblocks' ),
2709 'desc' => __( 'Display logged-in user data with placeholders like {nombre}, {email}, {username}.', 'frontblocks' ),
2710 ),
2711 array(
2712 'icon' => 'fullpage-scroll',
2713 'title' => __( 'Full Page Scroll', 'frontblocks' ),
2714 'desc' => __( 'Full-page scroll navigation between sections with smooth transitions.', 'frontblocks' ),
2715 ),
2716 array(
2717 'icon' => 'language-banner',
2718 'title' => __( 'Language Banner', 'frontblocks' ),
2719 'desc' => __( 'Detect visitor language and show a recommendation banner (WPML/Polylang).', 'frontblocks' ),
2720 ),
2721 array(
2722 'icon' => 'gutenberg',
2723 'title' => __( 'Gutenberg in Products', 'frontblocks' ),
2724 'desc' => __( 'Use the full block editor to build WooCommerce product descriptions.', 'frontblocks' ),
2725 ),
2726 array(
2727 'icon' => 'after-add-to-cart',
2728 'title' => __( 'After Add to Cart Block', 'frontblocks' ),
2729 'desc' => __( 'Insert custom block content right after the Add to Cart button.', 'frontblocks' ),
2730 ),
2731 array(
2732 'icon' => 'simple-prices',
2733 'title' => __( 'Simple Prices Variable Products', 'frontblocks' ),
2734 'desc' => __( 'Simplified price display for variable WooCommerce products.', 'frontblocks' ),
2735 ),
2736 array(
2737 'icon' => 'horizontal-form',
2738 'title' => __( 'Horizontal Product Form', 'frontblocks' ),
2739 'desc' => __( 'Switch the WooCommerce product form to a horizontal layout.', 'frontblocks' ),
2740 ),
2741 array(
2742 'icon' => 'deactivate-tabs',
2743 'title' => __( 'Deactivate Product Tabs', 'frontblocks' ),
2744 'desc' => __( 'Remove the default tabs from WooCommerce product pages.', 'frontblocks' ),
2745 ),
2746 array(
2747 'icon' => 'disable-zoom',
2748 'title' => __( 'Disable Product Image Zoom', 'frontblocks' ),
2749 'desc' => __( 'Remove zoom effect on WooCommerce product images.', 'frontblocks' ),
2750 ),
2751 array(
2752 'icon' => 'share-buttons',
2753 'title' => __( 'Share Buttons', 'frontblocks' ),
2754 'desc' => __( 'Add social share buttons to WooCommerce product pages.', 'frontblocks' ),
2755 ),
2756 array(
2757 'icon' => 'deactivate-description',
2758 'title' => __( 'Manage Short Description', 'frontblocks' ),
2759 'desc' => __( 'Deactivate or move the WooCommerce product short description.', 'frontblocks' ),
2760 ),
2761 array(
2762 'icon' => 'default',
2763 'title' => __( 'Custom Post Types Builder', 'frontblocks' ),
2764 'desc' => __( 'Create and manage custom post types directly from the admin panel.', 'frontblocks' ),
2765 ),
2766 array(
2767 'icon' => 'horizontal-form',
2768 'title' => __( 'Checkout Inline Fields', 'frontblocks' ),
2769 'desc' => __( 'Display address, email and phone fields side by side in the WooCommerce checkout.', 'frontblocks' ),
2770 ),
2771 array(
2772 'icon' => 'popups',
2773 'title' => __( 'Popups', 'frontblocks' ),
2774 'desc' => __( 'Create popups with the block editor and configure when and where they appear.', 'frontblocks' ),
2775 ),
2776 );
2777 }
2778
2779 /**
2780 * Render PRO features section after the main form.
2781 *
2782 * @return void
2783 */
2784 private function render_pro_section(): void {
2785 $pro_blocks = apply_filters( 'frbl_pro_blocks', $this->get_default_pro_blocks() );
2786 $license_valid = function_exists( 'frblp_is_license_valid' ) && frblp_is_license_valid();
2787
2788 if ( empty( $pro_blocks ) || $license_valid ) {
2789 return;
2790 }
2791
2792 ?>
2793 <div class="frbl-section-wrapper tw:mt-6">
2794 <div class="frbl-section-header">
2795 <h2 class="tw:text-2xl tw:font-bold tw:text-gray-900 tw:mb-0">
2796 <?php echo esc_html__( 'FrontBlocks PRO Features', 'frontblocks' ); ?>
2797 </h2>
2798 <div class="tw:text-sm tw:text-gray-600">
2799 <p class="tw:mt-0 tw:mb-4">
2800 <?php
2801 $anchor = frbl_is_pro_active()
2802 ? '<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>'
2803 : '<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>';
2804 printf(
2805 frbl_is_pro_active()
2806 /* translators: %s: license section link */
2807 ? esc_html__( 'Activate your license in the %s section below to unlock all features.', 'frontblocks' )
2808 /* translators: %s: FrontBlocks PRO link */
2809 : esc_html__( 'Unlock these features with %s.', 'frontblocks' ),
2810 $anchor // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
2811 );
2812 ?>
2813 </p>
2814 </div>
2815 </div>
2816
2817 <div class="frbl-features-grid">
2818 <?php foreach ( $pro_blocks as $block ) : ?>
2819 <?php UI::show_pro_info_card( $block['icon'], $block['title'], $block['desc'] ); ?>
2820 <?php endforeach; ?>
2821 </div>
2822
2823 <?php if ( ! frbl_is_pro_active() ) : ?>
2824 <div class="tw:mt-6 tw:text-center">
2825 <a
2826 href="https://close.technology/wordpress-plugins/frontblocks-pro/?utm_source=frontblocks&utm_medium=plugin&utm_campaign=settings-pro-cta"
2827 target="_blank"
2828 rel="noopener noreferrer"
2829 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"
2830 style="background-color: #ef4444; color: #fff;"
2831 onmouseover="this.style.backgroundColor='#dc2626'"
2832 onmouseout="this.style.backgroundColor='#ef4444'"
2833 >
2834 <?php echo esc_html__( 'Get FrontBlocks PRO', 'frontblocks' ); ?>
2835 </a>
2836 </div>
2837 <?php endif; ?>
2838 </div>
2839 <?php
2840 }
2841
2842 /**
2843 * Render license section (separate from main form).
2844 *
2845 * @return void
2846 */
2847 private function render_license_section() {
2848 global $frblp_license;
2849
2850 ?>
2851 <div class="tw:mt-6" id="frontblocks_section_license">
2852 <?php
2853 // Check if license instance exists.
2854 if ( ! $frblp_license ) {
2855 ?>
2856 <div class="tw:p-4 tw:rounded-lg tw:bg-red-50 tw:border tw:border-red-200">
2857 <p class="tw:text-sm tw:text-red-700">
2858 <?php echo esc_html__( 'License manager not initialized.', 'frontblocks' ); ?>
2859 </p>
2860 </div>
2861 <?php
2862 return;
2863 }
2864
2865 // Check if License class exists (requires FrontBlocks PRO).
2866 if ( ! class_exists( '\Closemarketing\WPLicenseManager\License' ) ) {
2867 ?>
2868 <div class="tw:p-4 tw:rounded-lg tw:bg-yellow-50 tw:border tw:border-yellow-200">
2869 <p class="tw:text-sm tw:text-yellow-700">
2870 <?php echo esc_html__( 'License management requires FrontBlocks PRO to be installed and active.', 'frontblocks' ); ?>
2871 </p>
2872 </div>
2873 <?php
2874 return;
2875 }
2876
2877 // Render license settings inline.
2878 $this->render_inline_license_settings( $frblp_license );
2879 ?>
2880 </div>
2881 <?php
2882 }
2883
2884 /**
2885 * Render inline license settings.
2886 *
2887 * @param \Closemarketing\WPLicenseManager\License $license License instance.
2888 * @return void
2889 */
2890 private function render_inline_license_settings( $license ) {
2891 // Get license data.
2892 $license_key = $license->get_option_value( 'apikey' );
2893 $is_active = $license->is_license_active();
2894 $license_status = get_option( 'frontblocks-pro_license_activated', 'Deactivated' );
2895
2896 ?>
2897 <div class="formscrm-license-wrapper">
2898 <!-- Main Card -->
2899 <div class="formscrm-card">
2900 <!-- Header -->
2901 <div class="formscrm-card-header">
2902 <h2><?php echo esc_html__( 'FrontBlocks PRO License', 'frontblocks' ); ?></h2>
2903 <p><?php echo esc_html__( 'Manage your license to receive automatic updates and support.', 'frontblocks' ); ?></p>
2904 </div>
2905
2906 <!-- License Status -->
2907 <div class="formscrm-form-group">
2908 <?php if ( $is_active ) : ?>
2909 <div class="formscrm-status-box formscrm-status-active">
2910 <span class="formscrm-status-icon">
2911 <svg class="formscrm-icon" fill="currentColor" viewBox="0 0 20 20">
2912 <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"/>
2913 </svg>
2914 </span>
2915 <span class="formscrm-status-text"><?php echo esc_html__( 'License Active', 'frontblocks' ); ?></span>
2916 </div>
2917 <?php else : ?>
2918 <div class="formscrm-status-box formscrm-status-inactive">
2919 <span class="formscrm-status-icon">
2920 <svg class="formscrm-icon" fill="currentColor" viewBox="0 0 20 20">
2921 <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"/>
2922 </svg>
2923 </span>
2924 <span class="formscrm-status-text"><?php echo esc_html__( 'License Inactive', 'frontblocks' ); ?></span>
2925 </div>
2926 <?php endif; ?>
2927 </div>
2928
2929 <!-- License Form -->
2930 <form method="post" action="options.php" class="formscrm-license-form">
2931 <?php settings_fields( 'frontblocks-pro_license' ); ?>
2932 <?php wp_nonce_field( 'Update_License_Options', 'license_nonce' ); ?>
2933
2934 <!-- License Key Field -->
2935 <div class="formscrm-form-group">
2936 <label class="formscrm-label" for="frontblocks-pro_license_apikey">
2937 <?php echo esc_html__( 'License Key', 'frontblocks' ); ?>
2938 </label>
2939 <div class="formscrm-input-group">
2940 <input
2941 type="text"
2942 id="frontblocks-pro_license_apikey"
2943 name="frontblocks-pro_license_apikey"
2944 value="<?php echo esc_attr( $license_key ); ?>"
2945 class="formscrm-input"
2946 placeholder="<?php echo esc_attr__( 'CTECH-XXXXX-XXXXX-XXXXX-XXXXX', 'frontblocks' ); ?>"
2947 <?php echo $is_active ? 'readonly' : ''; ?>
2948 />
2949 <?php if ( $is_active ) : ?>
2950 <label class="formscrm-deactivate-label">
2951 <input type="checkbox" name="frontblocks-pro_license_deactivate_checkbox" value="on" />
2952 <span><?php echo esc_html__( 'Deactivate', 'frontblocks' ); ?></span>
2953 </label>
2954 <?php endif; ?>
2955 </div>
2956 <p class="formscrm-help-text">
2957 <?php
2958 printf(
2959 /* translators: %s: Purchase URL */
2960 esc_html__( 'Enter your license key. You can find it in %s.', 'frontblocks' ),
2961 '<a href="https://close.technology/my-account/" target="_blank">' . esc_html__( 'your account', 'frontblocks' ) . '</a>'
2962 );
2963 ?>
2964 </p>
2965 </div>
2966
2967 <!-- License Status -->
2968 <div class="formscrm-form-group">
2969 <label class="formscrm-label"><?php echo esc_html__( 'License Status', 'frontblocks' ); ?></label>
2970 <div class="formscrm-status-box <?php echo $is_active ? 'formscrm-status-active' : 'formscrm-status-inactive'; ?>">
2971 <span class="formscrm-status-icon">
2972 <?php if ( $is_active ) : ?>
2973 <svg class="formscrm-icon" fill="currentColor" viewBox="0 0 20 20">
2974 <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"/>
2975 </svg>
2976 <?php else : ?>
2977 <svg class="formscrm-icon" fill="currentColor" viewBox="0 0 20 20">
2978 <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"/>
2979 </svg>
2980 <?php endif; ?>
2981 </span>
2982 <span class="formscrm-status-text">
2983 <?php echo $is_active ? esc_html__( 'Active', 'frontblocks' ) : esc_html__( 'Not Activated', 'frontblocks' ); ?>
2984 </span>
2985 </div>
2986 </div>
2987
2988 <!-- Submit Button -->
2989 <div class="formscrm-form-actions">
2990 <button type="submit" name="submit_license" class="formscrm-button formscrm-button-primary">
2991 <?php echo $is_active ? esc_html__( 'Update License', 'frontblocks' ) : esc_html__( 'Activate License', 'frontblocks' ); ?>
2992 </button>
2993 </div>
2994 </form>
2995 </div>
2996
2997 <!-- Sidebar Info -->
2998 <div class="formscrm-info-card">
2999 <h3><?php echo esc_html__( 'License Benefits', 'frontblocks' ); ?></h3>
3000 <p><?php echo esc_html__( 'An active license provides the following benefits:', 'frontblocks' ); ?></p>
3001
3002 <ul class="formscrm-benefits-list">
3003 <li><?php echo esc_html__( 'Automatic plugin updates', 'frontblocks' ); ?></li>
3004 <li><?php echo esc_html__( 'Access to new features', 'frontblocks' ); ?></li>
3005 <li><?php echo esc_html__( 'Priority support', 'frontblocks' ); ?></li>
3006 <li><?php echo esc_html__( 'Security patches', 'frontblocks' ); ?></li>
3007 </ul>
3008
3009 <hr style="margin: 20px 0; border: none; border-top: 1px solid #e2e8f0;">
3010
3011 <div style="font-size: 0.875rem; color: #64748b;">
3012 <p style="margin-bottom: 8px;">
3013 <strong><?php echo esc_html__( 'Need Help?', 'frontblocks' ); ?></strong>
3014 </p>
3015 <p style="margin-bottom: 8px;">
3016 <a href="https://close.technology/wordpress-plugins/frontblocks-pro/" target="_blank" style="color: #8b5cf6; text-decoration: none;">
3017 <?php echo esc_html__( 'Purchase License', 'frontblocks' ); ?>
3018 </a>
3019 </p>
3020 <p style="margin-bottom: 8px;">
3021 <a href="https://close.technology/my-account/" target="_blank" style="color: #8b5cf6; text-decoration: none;">
3022 <?php echo esc_html__( 'My Account', 'frontblocks' ); ?>
3023 </a>
3024 </p>
3025 <p>
3026 <a href="https://close.technology/support/" target="_blank" style="color: #8b5cf6; text-decoration: none;">
3027 <?php echo esc_html__( 'Support', 'frontblocks' ); ?>
3028 </a>
3029 </p>
3030 </div>
3031 </div>
3032 </div>
3033 <?php
3034 }
3035
3036 /**
3037 * Helper method to render PRO toggle fields.
3038 *
3039 * @param string $option_key Option key.
3040 * @return void
3041 */
3042 private function render_pro_toggle( $option_key ) {
3043 $options = get_option( 'frontblocks_settings', array() );
3044 $enabled = (bool) ( $options[ $option_key ] ?? false );
3045 $is_enabled = $this->is_license_valid;
3046 $disabled = ! $is_enabled ? 'disabled' : '';
3047 ?>
3048 <label class="frbl-toggle">
3049 <input type="checkbox"
3050 id="<?php echo esc_attr( $option_key ); ?>"
3051 name="frontblocks_settings[<?php echo esc_attr( $option_key ); ?>]"
3052 value="1"
3053 <?php checked( true, $enabled ); ?>
3054 <?php echo esc_attr( $disabled ); ?>
3055 />
3056 <span></span>
3057 </label>
3058 <?php
3059 }
3060
3061 /**
3062 * Sanitize settings array.
3063 *
3064 * @param array $value Raw value.
3065 * @return array
3066 */
3067 public function sanitize_settings( $value ) {
3068 // Nonce verification.
3069 $nonce = isset( $_POST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
3070 if ( empty( $nonce ) || ! wp_verify_nonce( $nonce, 'frontblocks_settings-options' ) ) {
3071 add_settings_error( 'frontblocks_settings', 'frontblocks_settings_nonce', esc_html__( 'Security check failed. Please try again.', 'frontblocks' ), 'error' );
3072
3073 return get_option( 'frontblocks_settings', array() );
3074 }
3075
3076 if ( ! is_array( $value ) ) {
3077 return array();
3078 }
3079
3080 // Get current options to preserve unchecked checkboxes.
3081 $current_options = get_option( 'frontblocks_settings', array() );
3082
3083 // Initialize sanitized array with current values.
3084 $sanitized = $current_options;
3085
3086 // List of all boolean options (checkboxes).
3087 $boolean_options = array(
3088 $this->option_enable_testimonials,
3089 $this->option_enable_reading_progress,
3090 $this->option_enable_back_button,
3091 $this->option_enable_scroll_top,
3092 $this->option_enable_events,
3093 $this->option_enable_fluid_typography,
3094 $this->option_enable_maintenance,
3095 $this->option_enable_cookie_notice,
3096 $this->option_enable_gutenberg,
3097 $this->option_enable_simple_prices_variable_products,
3098 $this->option_enable_after_add_to_cart,
3099 $this->option_deactivate_short_description,
3100 $this->option_move_content_to_short_description,
3101 $this->option_disable_zoom_images,
3102 $this->option_add_share_buttons,
3103 $this->option_deactivate_product_tabs,
3104 $this->option_horizontal_product_form,
3105 $this->option_enable_variant_display_mode,
3106 $this->option_enable_custom_post_types,
3107 $this->option_enable_fullpage_scroll,
3108 $this->option_enable_language_banner,
3109 $this->option_enable_popups,
3110 $this->option_checkout_inline,
3111 $this->option_disable_cart_coupon,
3112 $this->option_disable_cart_cross_sells,
3113 $this->option_disable_checkout_coupon,
3114 $this->option_disable_checkout_order_notes,
3115 $this->option_disable_checkout_login_prompt,
3116 );
3117
3118 // Initialize all boolean options to false (unchecked checkboxes are not submitted).
3119 foreach ( $boolean_options as $option ) {
3120 $sanitized[ $option ] = false;
3121 }
3122
3123 // Process submitted values.
3124 foreach ( $value as $key => $val ) {
3125 if ( in_array( $key, $boolean_options, true ) ) {
3126 $sanitized[ $key ] = (bool) $val;
3127 } elseif ( $this->option_events_type === $key ) {
3128 // Sanitize events type: only allow 'cpt' or 'posts'.
3129 $sanitized[ $key ] = in_array( $val, array( 'cpt', 'posts' ), true ) ? $val : 'cpt';
3130 } elseif ( $this->option_scroll_top_position === $key ) {
3131 $sanitized[ $key ] = in_array( $val, array( 'bottom-right', 'bottom-left' ), true ) ? $val : 'bottom-right';
3132 } elseif ( $this->option_scroll_top_icon_url === $key ) {
3133 $sanitized[ $key ] = esc_url_raw( $val );
3134 } elseif ( $this->option_maintenance_title === $key ) {
3135 $sanitized[ $key ] = sanitize_text_field( $val );
3136 } elseif ( $this->option_maintenance_image === $key ) {
3137 $sanitized[ $key ] = absint( $val );
3138 } elseif ( $this->option_cookie_notice_message === $key ) {
3139 $sanitized[ $key ] = sanitize_textarea_field( $val );
3140 } elseif ( in_array( $key, array( $this->option_cookie_notice_accept_label, $this->option_cookie_notice_reject_label ), true ) ) {
3141 $sanitized[ $key ] = sanitize_text_field( $val );
3142 } elseif ( $this->option_cookie_notice_policy_page_id === $key ) {
3143 $sanitized[ $key ] = absint( $val );
3144 } elseif ( $this->option_cookie_notice_layout === $key ) {
3145 $sanitized[ $key ] = in_array( $val, array( 'bar', 'box', 'popup' ), true ) ? $val : 'bar';
3146 } elseif ( $this->option_cookie_notice_position === $key ) {
3147 $sanitized[ $key ] = in_array( $val, array( 'bottom-right', 'bottom-left' ), true ) ? $val : 'bottom-right';
3148 } elseif ( $this->option_cookie_notice_color === $key ) {
3149 $hex_color = sanitize_hex_color( $val );
3150 $sanitized[ $key ] = $hex_color ? $hex_color : '#687df9';
3151 } elseif ( $this->option_cookie_notice_expiration_days === $key ) {
3152 $days = absint( $val );
3153 $sanitized[ $key ] = $days > 0 ? min( $days, 730 ) : 365;
3154 } elseif ( $this->option_cookie_notice_gtm_id === $key ) {
3155 $gtm_id = strtoupper( sanitize_text_field( $val ) );
3156 $sanitized[ $key ] = preg_match( '/^GTM-[A-Z0-9]+$/', $gtm_id ) ? $gtm_id : '';
3157 } elseif ( $this->option_cookie_notice_ga4_id === $key ) {
3158 $ga4_id = strtoupper( sanitize_text_field( $val ) );
3159 $sanitized[ $key ] = preg_match( '/^G-[A-Z0-9]+$/', $ga4_id ) ? $ga4_id : '';
3160 }
3161 }
3162
3163 // Ensure mutual exclusion: if both description options are enabled, keep only the last one changed.
3164 if ( ! empty( $sanitized[ $this->option_deactivate_short_description ] ) && ! empty( $sanitized[ $this->option_move_content_to_short_description ] ) ) {
3165 // Get current saved values to determine which one was just changed.
3166 $current_deactivate = ! empty( $current_options[ $this->option_deactivate_short_description ] );
3167 $current_move = ! empty( $current_options[ $this->option_move_content_to_short_description ] );
3168
3169 // If deactivate was already on, turn it off (move is the new one).
3170 if ( $current_deactivate ) {
3171 $sanitized[ $this->option_deactivate_short_description ] = false;
3172 } else {
3173 // Otherwise turn off move (deactivate is the new one).
3174 $sanitized[ $this->option_move_content_to_short_description ] = false;
3175 }
3176 }
3177
3178 do_action( 'frontblocks_sanitize_settings', $sanitized );
3179
3180 return $sanitized;
3181 }
3182 }
3183