PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.21
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.21
2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 2.6.4 2.6.3 2.5.23 All 138 releases
metasync / includes / class-metasync-admin-navigation.php

class-metasync-admin-navigation.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.21, at includes/class-metasync-admin-navigation.php

1,865 lines 95.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if (!defined('ABSPATH')) {
3 exit;
4 }
5
6 /**
7 * Admin navigation header, nav tabs, and admin-bar status indicator.
8 *
9 * Extracted from Metasync_Admin to keep the admin class focused on
10 * non-UI concerns. All header/nav rendering and admin-bar badge
11 * logic lives here.
12 *
13 * @package Metasync
14 * @subpackage Metasync/includes
15 */
16 class Metasync_Admin_Navigation
17 {
18 /** @var self|null */
19 private static $instance = null;
20
21 const CACHE_KEY = 'metasync_admin_bar_status';
22
23 /**
24 * Get singleton instance.
25 *
26 * @return self
27 */
28 public static function instance()
29 {
30 if (self::$instance === null) {
31 self::$instance = new self();
32 }
33 return self::$instance;
34 }
35
36 private function __construct() {}
37
38 /**
39 * Whether the Dashboard is hidden by the "Hide Dashboard" general setting
40 * (hide_dashboard_framework).
41 *
42 * When enabled this removes the Dashboard from the WordPress admin submenu
43 * and from the in-plugin SEO feature navigation, matching the setting's
44 * description "Hide the main dashboard from the WordPress admin menu".
45 *
46 * @return bool
47 */
48 public static function is_dashboard_hidden_by_framework()
49 {
50 $general_options = Metasync::get_option('general') ?? [];
51 return filter_var($general_options['hide_dashboard_framework'] ?? false, FILTER_VALIDATE_BOOLEAN);
52 }
53
54 // ------------------------------------------------------------------
55 // Logo resolution helper
56 // ------------------------------------------------------------------
57
58 /**
59 * Resolve which logo(s) to display based on whitelabel settings.
60 *
61 * @return array{show_logo: bool, use_dual: bool, url: string, light_url: string, dark_url: string, is_default: bool}
62 */
63 private function resolve_logo_data()
64 {
65 $wl_settings = Metasync::get_whitelabel_settings();
66 $is_whitelabel = !empty($wl_settings['is_whitelabel']);
67
68 $light_raw = Metasync::get_whitelabel_logo_light();
69 $dark_raw = Metasync::get_whitelabel_logo_dark();
70 $has_light = !empty($light_raw) && filter_var($light_raw, FILTER_VALIDATE_URL);
71 $has_dark = !empty($dark_raw) && filter_var($dark_raw, FILTER_VALIDATE_URL);
72 $use_dual = ($has_light && $has_dark && $light_raw !== $dark_raw);
73
74 $data = [
75 'show_logo' => false,
76 'use_dual' => false,
77 'url' => '',
78 'light_url' => '',
79 'dark_url' => '',
80 'is_default' => false,
81 ];
82
83 if ($use_dual) {
84 $data['show_logo'] = true;
85 $data['use_dual'] = true;
86 $data['light_url'] = $light_raw;
87 $data['dark_url'] = $dark_raw;
88 } else {
89 $single = $has_light ? $light_raw : ($has_dark ? $dark_raw : null);
90 if ($single) {
91 $data['show_logo'] = true;
92 $data['url'] = $single;
93 } elseif (!$is_whitelabel) {
94 $data['show_logo'] = true;
95 $data['url'] = plugin_dir_url(dirname(__FILE__)) . 'assets/images/searchatlas-logo.svg';
96 $data['is_default'] = true;
97 }
98 }
99
100 return $data;
101 }
102
103 // ------------------------------------------------------------------
104 // Header + Nav rendering
105 // ------------------------------------------------------------------
106
107 /**
108 * Render the standard header followed by the navigation tabs.
109 */
110 public function render_standard_header_nav($page_title = null, $current_page = null)
111 {
112 $this->render_static_header($page_title);
113 $this->render_static_navigation($current_page);
114 }
115
116 /**
117 * Render the page header (logo, connection badge, theme toggle).
118 */
119 public function render_static_header($page_title = null)
120 {
121 $effective_plugin_name = Metasync::get_effective_plugin_name();
122 $display_title = $page_title ?: $effective_plugin_name;
123 $logo = $this->resolve_logo_data();
124
125 $current_theme = get_option('metasync_theme', 'dark');
126 $general_settings = Metasync::get_option('general');
127 // derive the badge from confirmed heartbeat health, not from
128 // mere API-key presence (which stays set after Search Atlas revokes it).
129 $badge = Metasync_Heartbeat_Manager::instance()->get_connection_badge($general_settings);
130 ?>
131 <div class="metasync-header" data-current-theme="<?php echo esc_attr($current_theme); ?>">
132 <div class="metasync-header-left">
133 <?php if ($logo['show_logo'] && $logo['use_dual']): ?>
134 <div class="metasync-logo-container">
135 <img src="<?php echo esc_url($logo['light_url']); ?>" alt="Logo" class="metasync-logo metasync-logo-light" />
136 <img src="<?php echo esc_url($logo['dark_url']); ?>" alt="Logo" class="metasync-logo metasync-logo-dark" />
137 </div>
138 <?php elseif ($logo['show_logo'] && !empty($logo['url'])): ?>
139 <div class="metasync-logo-container">
140 <img src="<?php echo esc_url($logo['url']); ?>" alt="Logo" class="metasync-logo<?php echo !empty($logo['is_default']) ? ' metasync-logo-default' : ''; ?>" />
141 </div>
142 <?php endif; ?>
143 </div>
144 <div class="metasync-header-right">
145 <div class="metasync-status <?php echo esc_attr($badge['class']); ?>">
146 <span class="status-dot"></span>
147 <span class="status-text"><?php echo esc_html($badge['text']); ?></span>
148 </div>
149 <button type="button" class="metasync-theme-toggle" onclick="toggleMetasyncTheme()" title="Toggle theme">
150 <span class="theme-icon-light">&#9728;</span>
151 <span class="theme-icon-dark">&#9790;</span>
152 </button>
153 </div>
154 </div>
155 <?php
156 }
157
158 /**
159 * Render the horizontal navigation tabs.
160 */
161 public function render_static_navigation($current_page = null)
162 {
163 $general_options = Metasync::get_option('general') ?? [];
164 $whitelabel_settings = Metasync::get_whitelabel_settings();
165 $page_slug = Metasync_Admin::$page_slug;
166
167 $menu_items = [];
168 $menu_icons = [
169 'dashboard' => 'dashboard',
170 'seo_controls' => 'search',
171 'instant_index' => 'performance',
172 'google_console' => 'chart-area',
173 'compatibility' => 'admin-tools',
174 'sync_log' => 'list-view',
175 'redirections' => 'undo',
176 'robots_txt' => 'shield',
177 'xml_sitemap' => 'networking',
178 'custom_pages' => 'admin-page',
179 'report_issue' => 'sos',
180 'general' => 'admin-settings',
181 ];
182
183 if (empty($whitelabel_settings['hide_dashboard']) && !self::is_dashboard_hidden_by_framework()) {
184 $menu_items['dashboard'] = ['title' => 'Dashboard', 'slug_suffix' => '-dashboard'];
185 }
186
187 if (empty($whitelabel_settings['hide_indexation_control'])) {
188 $menu_items['seo_controls'] = ['title' => 'Indexation Control', 'slug_suffix' => '-seo-controls'];
189 }
190
191 if ($general_options['enable_googleinstantindex'] ?? false) {
192 $menu_items['instant_index'] = ['title' => 'Instant Indexing', 'slug_suffix' => '-instant-index'];
193 }
194
195 if ($general_options['enable_google_console'] ?? false) {
196 $menu_items['google_console'] = ['title' => 'Google Console', 'slug_suffix' => '-google-console'];
197 }
198
199 if (empty($whitelabel_settings['hide_compatibility'])) {
200 $menu_items['compatibility'] = ['title' => 'Compatibility', 'slug_suffix' => '-compatibility'];
201 }
202
203 if (empty($whitelabel_settings['hide_sync_log'])) {
204 $menu_items['sync_log'] = ['title' => 'Changes Log', 'slug_suffix' => '-sync-log'];
205 }
206
207 if (empty($whitelabel_settings['hide_redirections'])) {
208 $menu_items['redirections'] = ['title' => 'Redirections', 'slug_suffix' => '-redirections'];
209 }
210
211 if (empty($whitelabel_settings['hide_robots'])) {
212 $menu_items['robots_txt'] = ['title' => 'Robots.txt', 'slug_suffix' => '-robots-txt'];
213 }
214
215 $menu_items['xml_sitemap'] = ['title' => 'XML Sitemap', 'slug_suffix' => '-xml-sitemap'];
216 ?>
217 <div class="metasync-nav-wrapper">
218 <div class="metasync-nav-tabs">
219 <div class="metasync-nav-left">
220 <?php foreach ($menu_items as $key => $menu_item):
221 $is_active = ($current_page === $key);
222 $icon = $menu_icons[$key];
223 $page_url = '?page=' . $page_slug . $menu_item['slug_suffix'];
224 ?>
225 <a href="<?php echo esc_url($page_url); ?>" class="metasync-nav-tab <?php echo $is_active ? 'active' : ''; ?>">
226 <span class="tab-icon"><span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span></span>
227 <span class="tab-text"><?php echo esc_html($menu_item['title']); ?></span>
228 </a>
229 <?php endforeach; ?>
230 </div>
231 <div class="metasync-nav-right">
232 <a href="?page=<?php echo esc_attr( $page_slug ); ?>-custom-pages" class="metasync-nav-tab <?php echo $current_page === 'custom_pages' ? 'active' : ''; ?>" style="margin-right: 10px;">
233 <span class="tab-icon"><span class="dashicons dashicons-admin-page"></span></span>
234 <span class="tab-text">Custom Pages</span>
235 </a>
236 <a href="?page=<?php echo esc_attr( $page_slug ); ?>-report-issue" class="metasync-nav-tab <?php echo $current_page === 'report_issue' ? 'active' : ''; ?>">
237 <span class="tab-icon"><span class="dashicons dashicons-sos"></span></span>
238 <span class="tab-text">Report Issue</span>
239 </a>
240 <div class="metasync-simple-dropdown">
241 <button type="button" class="metasync-seo-btn" id="metasync-seo-btn" onclick="toggleSeoMenuPortal(event)" aria-expanded="false">
242 <span class="tab-icon"><span class="dashicons dashicons-search"></span></span>
243 <span class="tab-text">SEO</span>
244 <span class="dropdown-arrow"></span>
245 </button>
246 </div>
247 <div class="metasync-simple-dropdown">
248 <button type="button" class="metasync-settings-btn" id="metasync-settings-btn" onclick="toggleSettingsMenuPortal(event)" aria-expanded="false">
249 <span class="tab-icon"><span class="dashicons dashicons-admin-settings"></span></span>
250 <span class="tab-text">Settings</span>
251 <span class="dropdown-arrow"></span>
252 </button>
253 </div>
254 </div>
255 </div>
256 </div>
257
258 <script>
259 function toggleSeoMenuPortal(event) {
260 event.preventDefault();
261 event.stopPropagation();
262 var button = event.currentTarget;
263 var existingMenu = document.getElementById('metasync-seo-portal-menu');
264 if (existingMenu) {
265 existingMenu.remove();
266 button.classList.remove('active');
267 button.setAttribute('aria-expanded', 'false');
268 return;
269 }
270 var menu = document.createElement('div');
271 menu.id = 'metasync-seo-portal-menu';
272 menu.className = 'metasync-portal-menu';
273
274 var pageSlug = '<?php echo esc_js( $page_slug ); ?>';
275 var seoLinks = [
276 { href: '?page=' + pageSlug + '-seo-controls', text: 'Indexation Control' },
277 { href: '?page=' + pageSlug + '-xml-sitemap', text: 'XML Sitemap' },
278 { href: '?page=' + pageSlug + '-robots-txt', text: 'Robots.txt' },
279 { href: '?page=' + pageSlug + '-redirections', text: 'Redirections' },
280 ];
281 seoLinks.forEach(function(item) {
282 var link = document.createElement('a');
283 link.href = item.href;
284 link.className = 'metasync-portal-item';
285 link.textContent = item.text;
286 menu.appendChild(link);
287 });
288
289 var rect = button.getBoundingClientRect();
290 menu.style.position = 'fixed';
291 menu.style.top = (rect.bottom + 8) + 'px';
292 menu.style.right = (window.innerWidth - rect.right) + 'px';
293 menu.style.zIndex = '999999999';
294 document.body.appendChild(menu);
295 button.classList.add('active');
296 button.setAttribute('aria-expanded', 'true');
297 }
298
299 function toggleSettingsMenuPortal(event) {
300 event.preventDefault();
301 event.stopPropagation();
302 var button = event.currentTarget;
303 var existingMenu = document.getElementById('metasync-portal-menu');
304 if (existingMenu) {
305 existingMenu.remove();
306 button.classList.remove('active');
307 button.setAttribute('aria-expanded', 'false');
308 return;
309 }
310 var menu = document.createElement('div');
311 menu.id = 'metasync-portal-menu';
312 menu.className = 'metasync-portal-menu';
313
314 var hideAdvanced = <?php echo !empty($whitelabel_settings['hide_advanced']) ? 'true' : 'false'; ?>;
315 var showGeneral = <?php echo Metasync_Access_Control::user_can_access('hide_settings') ? 'true' : 'false'; ?>;
316
317 if (showGeneral) {
318 var generalLink = document.createElement('a');
319 generalLink.href = '?page=<?php echo esc_js( $page_slug ); ?>&tab=general';
320 generalLink.className = 'metasync-portal-item';
321 generalLink.textContent = 'General';
322 menu.appendChild(generalLink);
323 }
324
325 var whitelabelLink = document.createElement('a');
326 whitelabelLink.href = '?page=<?php echo esc_js( $page_slug ); ?>&tab=whitelabel';
327 whitelabelLink.className = 'metasync-portal-item';
328 whitelabelLink.textContent = 'White Label';
329 menu.appendChild(whitelabelLink);
330
331 if (!hideAdvanced) {
332 var advancedLink = document.createElement('a');
333 advancedLink.href = '?page=<?php echo esc_js( $page_slug ); ?>&tab=advanced';
334 advancedLink.className = 'metasync-portal-item';
335 advancedLink.textContent = 'Advanced';
336 menu.appendChild(advancedLink);
337 }
338
339
340 var rect = button.getBoundingClientRect();
341 menu.style.position = 'fixed';
342 menu.style.top = (rect.bottom + 8) + 'px';
343 menu.style.right = (window.innerWidth - rect.right) + 'px';
344 menu.style.zIndex = '999999999';
345 document.body.appendChild(menu);
346 button.classList.add('active');
347 button.setAttribute('aria-expanded', 'true');
348 }
349 document.addEventListener('click', function(event) {
350 var seoButton = document.getElementById('metasync-seo-btn');
351 var seoMenu = document.getElementById('metasync-seo-portal-menu');
352 if (seoMenu && seoButton && !seoButton.contains(event.target) && !seoMenu.contains(event.target)) {
353 seoMenu.remove();
354 seoButton.classList.remove('active');
355 seoButton.setAttribute('aria-expanded', 'false');
356 }
357
358 var button = document.getElementById('metasync-settings-btn');
359 var menu = document.getElementById('metasync-portal-menu');
360 if (menu && button && !button.contains(event.target) && !menu.contains(event.target)) {
361 menu.remove();
362 button.classList.remove('active');
363 button.setAttribute('aria-expanded', 'false');
364 }
365 });
366 </script>
367 <?php
368 }
369
370 // ------------------------------------------------------------------
371 // Menu registration + enhanced navigation (moved from Metasync_Admin)
372 // ------------------------------------------------------------------
373
374 /**
375 * Helper method to get available menu items based on configuration.
376 * Items are grouped into 'seo' (SEO Features) and 'plugin' (Plugin) categories.
377 */
378 public function get_available_menu_items()
379 {
380 $general_options = Metasync::get_option('general') ?? [];
381 $has_api_key = !empty($general_options['searchatlas_api_key'] ?? '');
382 $has_uuid = !empty($general_options['otto_pixel_uuid'] ?? '');
383 $is_fully_connected = Metasync_Heartbeat_Manager::instance()->is_heartbeat_connected($general_options);
384
385 $menu_items = [];
386
387 // === SEO FEATURES GROUP ===
388
389 // Dashboard (check access control + "Hide Dashboard" general setting)
390 if (Metasync_Access_Control::user_can_access('hide_dashboard') && !self::is_dashboard_hidden_by_framework()) {
391 $menu_items['dashboard'] = [
392 'title' => 'Dashboard',
393 'slug_suffix' => '-dashboard',
394 'callback' => 'create_admin_dashboard_iframe',
395 'internal_nav' => 'Dashboard',
396 'group' => 'seo'
397 ];
398 }
399
400 // Indexation Control (check access control)
401 if (Metasync_Access_Control::user_can_access('hide_indexation_control')) {
402 $menu_items['seo_controls'] = [
403 'title' => 'Indexation',
404 'slug_suffix' => '-seo-controls',
405 'callback' => 'create_admin_seo_controls_page',
406 'internal_nav' => 'Indexation Control',
407 'group' => 'seo'
408 ];
409 }
410
411 // Instant Indexing - setting now stored in seo_controls (moved from Settings to Indexation Control)
412 $seo_controls = Metasync::get_option('seo_controls');
413 if ($seo_controls['enable_googleinstantindex'] ?? false) {
414 $menu_items['instant_index'] = [
415 'title' => 'Instant Indexing',
416 'slug_suffix' => '-instant-index',
417 'callback' => 'create_admin_google_instant_index_page',
418 'internal_nav' => 'Instant Indexing',
419 'group' => 'seo'
420 ];
421 }
422
423 if ($general_options['enable_google_console'] ?? false) {
424 $menu_items['google_console'] = [
425 'title' => 'Google Console',
426 'slug_suffix' => '-google-console',
427 'callback' => 'create_admin_google_console_page',
428 'internal_nav' => 'Google Console',
429 'group' => 'seo'
430 ];
431 }
432
433 if ($general_options['enable_bing_console'] ?? false) {
434 $menu_items['bing_console'] = [
435 'title' => 'Bing Console',
436 'slug_suffix' => '-bing-console',
437 'callback' => 'create_admin_bing_console_page',
438 'internal_nav' => 'Bing Console',
439 'group' => 'seo'
440 ];
441 }
442
443 // Redirections page (check access control)
444 if (Metasync_Access_Control::user_can_access('hide_redirections')) {
445 $menu_items['redirections'] = [
446 'title' => 'Redirections',
447 'slug_suffix' => '-redirections',
448 'callback' => 'create_admin_redirections_page',
449 'internal_nav' => 'Redirections',
450 'group' => 'seo'
451 ];
452 }
453
454 // Robots.txt page (check access control)
455 if (Metasync_Access_Control::user_can_access('hide_robots')) {
456 $menu_items['robots_txt'] = [
457 'title' => 'Robots.txt',
458 'slug_suffix' => '-robots-txt',
459 'callback' => 'create_admin_robots_txt_page',
460 'internal_nav' => 'Robots.txt',
461 'group' => 'seo'
462 ];
463 }
464
465 // XML Sitemap page (check access control)
466 if (Metasync_Access_Control::user_can_access('hide_xml_sitemap')) {
467 $menu_items['xml_sitemap'] = [
468 'title' => 'XML Sitemap',
469 'slug_suffix' => '-xml-sitemap',
470 'callback' => 'create_admin_xml_sitemap_page',
471 'internal_nav' => 'XML Sitemap',
472 'group' => 'seo'
473 ];
474 }
475
476 // Schema Markup
477 $menu_items['schema_markup'] = [
478 'title' => 'Schema Markup',
479 'slug_suffix' => '-schema-markup',
480 'callback' => 'create_admin_schema_markup_page',
481 'internal_nav' => 'Schema Markup',
482 'group' => 'seo'
483 ];
484
485 // 404 Monitor (always available — SEO monitoring tool)
486 $menu_items['monitor_404'] = [
487 'title' => '404 Monitor',
488 'slug_suffix' => '-404-monitor',
489 'callback' => 'create_admin_404_monitor_page',
490 'internal_nav' => '404 Monitor',
491 'group' => 'seo'
492 ];
493
494 // Site Verification
495 $menu_items['site_verification'] = [
496 'title' => 'Site Verification',
497 'slug_suffix' => '-search-engine-verify',
498 'callback' => 'create_admin_search_engine_verification_page',
499 'internal_nav' => 'Site Verification',
500 'group' => 'seo'
501 ];
502
503 // SEO Health dashboard (always available)
504 $menu_items['seo_health'] = [
505 'title' => 'SEO Health',
506 'slug_suffix' => '-seo-health',
507 'callback' => 'create_admin_seo_health_page',
508 'internal_nav' => 'SEO Health',
509 'group' => 'seo'
510 ];
511
512 // Import SEO Data page (check access control)
513 if (Metasync_Access_Control::user_can_access('hide_import_seo')) {
514 $menu_items['import_seo'] = [
515 'title' => 'Import SEO Data',
516 'slug_suffix' => '-import-external',
517 'callback' => 'render_import_external_data_page',
518 'internal_nav' => 'Import SEO Data',
519 'group' => 'seo'
520 ];
521 }
522
523 // === PLUGIN GROUP ===
524
525 // Settings (check access control)
526 if (Metasync_Access_Control::user_can_access('hide_settings')) {
527 $menu_items['general'] = [
528 'title' => 'Settings',
529 'slug_suffix' => '',
530 'callback' => 'create_admin_settings_page',
531 'internal_nav' => 'General Settings',
532 'group' => 'plugin'
533 ];
534 }
535
536 // Local Business
537 $menu_items['local_business'] = [
538 'title' => 'Local Business',
539 'slug_suffix' => '-local-business',
540 'callback' => 'create_admin_local_business_page',
541 'internal_nav' => 'Local Business',
542 'group' => 'seo'
543 ];
544
545 // Breadcrumbs
546 $menu_items['breadcrumbs'] = [
547 'title' => 'Breadcrumbs',
548 'slug_suffix' => '-breadcrumbs',
549 'callback' => 'create_admin_breadcrumbs_page',
550 'internal_nav' => 'Breadcrumbs',
551 'group' => 'seo'
552 ];
553
554 // Code Snippets
555 $menu_items['code_snippets'] = [
556 'title' => 'Code Snippets',
557 'slug_suffix' => '-code-snippets',
558 'callback' => 'create_admin_code_snippets_page',
559 'internal_nav' => 'Code Snippets',
560 'group' => 'plugin'
561 ];
562
563 // Code Minification
564 $menu_items['code_minification'] = [
565 'title' => 'Code Minification',
566 'slug_suffix' => '-code-minification',
567 'callback' => 'create_admin_code_minification_page',
568 'internal_nav' => 'Code Minification',
569 'group' => 'plugin'
570 ];
571
572 // Media Optimization
573 $menu_items['media_optimization'] = [
574 'title' => 'Media Optimization',
575 'slug_suffix' => '-media-optimization',
576 'callback' => 'create_admin_media_optimization_page',
577 'internal_nav' => 'Media Optimization',
578 'group' => 'plugin'
579 ];
580
581 // Custom HTML Pages (check access control)
582 if (Metasync_Access_Control::user_can_access('hide_custom_pages')) {
583 $menu_items['custom_pages'] = [
584 'title' => 'Custom Pages',
585 'slug_suffix' => '-custom-pages',
586 'callback' => 'create_admin_custom_pages_page',
587 'internal_nav' => 'Custom HTML Pages',
588 'group' => 'plugin'
589 ];
590 }
591
592 // Compatibility page (check access control)
593 if (Metasync_Access_Control::user_can_access('hide_compatibility')) {
594 $menu_items['compatibility'] = [
595 'title' => 'Compatibility',
596 'slug_suffix' => '-compatibility',
597 'callback' => 'create_admin_compatibility_page',
598 'internal_nav' => 'Compatibility',
599 'group' => 'plugin'
600 ];
601 }
602
603 // Sync Log page (check access control)
604 if (Metasync_Access_Control::user_can_access('hide_sync_log')) {
605 $menu_items['sync_log'] = [
606 'title' => 'Changes Log',
607 'slug_suffix' => '-sync-log',
608 'callback' => 'create_admin_sync_log_page',
609 'internal_nav' => 'Changes Log',
610 'group' => 'plugin'
611 ];
612 }
613
614 // Bot Statistics
615 $menu_items['bot_statistics'] = [
616 'title' => 'Bot Statistics',
617 'slug_suffix' => '-bot-statistics',
618 'callback' => 'create_admin_bot_statistics_page',
619 'internal_nav' => 'Bot Statistics',
620 'group' => 'plugin'
621 ];
622
623 // Report Issue page (check access control)
624 if (Metasync_Access_Control::user_can_access('hide_report_issue')) {
625 $menu_items['report_issue'] = [
626 'title' => 'Report Issue',
627 'slug_suffix' => '-report-issue',
628 'callback' => 'create_admin_report_issue_page',
629 'internal_nav' => 'Report Issue',
630 'group' => 'plugin'
631 ];
632 }
633
634 return $menu_items;
635 }
636
637 /**
638 * Register all WordPress admin menu / submenu pages.
639 *
640 * @param Metasync_Admin $admin The admin instance whose callbacks WordPress will invoke.
641 */
642 public function add_plugin_settings_page($admin)
643 {
644 if (!Metasync::current_user_has_plugin_access()) {
645 return;
646 }
647
648 $data = Metasync::get_option('general');
649 $plugin_name = Metasync::get_effective_plugin_name();
650 $menu_name = $plugin_name;
651 $menu_title = $plugin_name;
652 // Sanitize so a URL-shaped legacy value still yields a valid WP menu slug
653 $menu_slug = !isset($data['white_label_plugin_menu_slug']) || $data['white_label_plugin_menu_slug'] == "" ? Metasync_Admin::$page_slug : sanitize_title($data['white_label_plugin_menu_slug']);
654 $menu_slug = $menu_slug === '' ? Metasync_Admin::$page_slug : $menu_slug;
655 $menu_icon = !isset($data['white_label_plugin_menu_icon']) || $data['white_label_plugin_menu_icon'] == "" ? 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzQiIHZpZXdCb3g9IjAgMCAzMiAzNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzE4NzlfMTcyNzcpIj4KPHBhdGggZD0iTTI5LjAyMTUgMi4xNjc2NkwzMC4wMTAxIDIuMTIyNDFMMjkuMTYyNyA4LjcyNzA1TDI5LjExNTcgOC44MTc0M0w1LjI5NTA0IDMzLjI0NTJMNC43MzAxMiAzMy4yOTA1TDQuMDcxMDQgMzMuOTY5MUgxLjk1MjYyTDIuMjM1MDggMzIuMjk1M0wyLjA5Mzg0IDMyLjM0MDZMMi4xODggMzEuNjYySDEuNjIzMDhDMS41NzYgMzEuNjYyIDEuNDgxODUgMzEuNjYyIDEuNDgxODUgMzEuNjE2N0MxLjQzNDc4IDMxLjU3MTYgMS4zODc3IDMxLjQ4MSAxLjM4NzcgMzEuNDM1OEwyLjUxNzU0IDI1LjEwMjdDMi41MTc1NCAyNS4wNTc1IDIuNTY0NiAyNS4wMTIyIDIuNTY0NiAyNS4wMTIyTDI2LjE5NjkgMC42Mjk2MDdDMjYuMjQ0IDAuNTg0MzY2IDI2LjI5MTEgMC41MzkxMjUgMjYuMzM4MSAwLjUzOTEyNUgyOC4zNjI1QzI4LjQwOTUgMC40OTM4ODQgMjguNDU2NiAwLjUzOTEyNSAyOC41MDM3IDAuNTg0MzY2QzI4LjU1MDcgMC42Mjk2MDcgMjguNTk3OSAwLjcyMDA3MSAyOC41OTc5IDAuNzY1MzExTDI4LjUwMzcgMS4zNTMzOUgyOS4xMTU3TDI5LjAyMTUgMi4xNjc2NlpNMS45MDU1NCAzMS4yMDk2SDIuMjgyMTZMMy4yMjM2OCAyNS43ODEySDMuMjcwNzZMNi44MDE1IDIyLjI1MjhMNi44NDg1MSAyMi4yOTgxTDIwLjIxODMgOC40NTU1N0wyNi45MDMxIDEuMzUzMzlIMjguMDMyOUwyOC4wNzk5IDAuOTkxNDk5SDI2LjQ3OTNMMi45ODgzIDI1LjIzODRMMS45MDU1NCAzMS4yMDk2Wk0yOC42OTE5IDguNTQ1OTVMMjkuNDQ1MiAyLjYyMDAxSDI4Ljk3NDVMMjguMzE1MyA3Ljc3Njk2TDI4LjI2ODMgNy44MjIyNEwyOC4xMjcxIDcuOTU3ODlMMjcuOTM4NyA5LjMxNTExTDI4LjY5MTkgOC41NDU5NVoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0zMS41NTQ0IDE4LjE5MzdDMzEuNzM0OSAxOC42NjYyIDMxLjgwMjggMTkuMjA2NCAzMS42ODk4IDE5Ljc2OUwyOS42NTgyIDMyLjEwMTNIMjkuMDkzOUwyOS4wMjYyIDMyLjQzODhIMjUuOTMzNkwyNi4wNjkxIDMxLjYwNjJIMjYuMDAxM0wyNi4wNjkxIDMxLjI2ODZIMjUuNzk4MUMyNS43NTMgMzEuMjY4NiAyNS43MzA1IDMxLjI2ODYgMjUuNzA3OSAzMS4yNDYxQzI1LjY4NTIgMzEuMjIzNyAyNS42ODUyIDMxLjE3ODYgMjUuNjg1MiAzMS4xNTZMMjYuMzYyNCAyNy4zOThIMTguNDE2N0wxNy40MjM0IDMyLjA3ODdIMTYuODM2NUwxNi43Njg3IDMyLjQxNjNIMTMuNjk4OEwxMy45MDE5IDMxLjU4MzZIMTMuODM0M0wxMy45MDE5IDMxLjI2ODZIMTMuNjMxQzEzLjYwODQgMzEuMjY4NiAxMy41NjMzIDMxLjI2ODYgMTMuNTQwOCAzMS4yNDYxQzEzLjU0MDggMzEuMjAxMSAxMy41MTgyIDMxLjE3ODYgMTMuNTQwOCAzMS4xMzM2TDE2LjM2MjQgMTguOTEzOEMxNi40OTc5IDE4LjM1MTIgMTYuNzQ2MiAxNy44MzM2IDE3LjEyOTkgMTcuMzM4NUMxNy40OTEyIDE2Ljg2NiAxNy45NDI2IDE2LjQ4MzUgMTguNDYxOCAxNi4yMTM0QzE4Ljk4MSAxNS45MjA3IDE5LjUwMDIgMTUuNzg1OCAyMC4wNDE5IDE1Ljc4NThIMjguNTk3MkMyOS4xMzkgMTUuNzg1OCAyOS42MTMxIDE1LjkyMDcgMzAuMDE5MyAxNi4yMTM0QzMwLjM1NzkgMTYuNDYwOSAzMC42Mjg5IDE2Ljc5ODUgMzAuODA5NSAxNy4xODFDMzEuMTI1NSAxNy40NTExIDMxLjM5NjMgMTcuNzg4NiAzMS41NTQ0IDE4LjE5MzdaTTI3LjQ5MTIgMjMuMzY5N0wyOC4wNTU1IDIwLjI2NDFDMjguMDMyOSAyMC4yNDE1IDI4LjAzMjkgMjAuMjE5MSAyOC4wMTA0IDIwLjIxOTFIMjcuODk3NUwyNy4zNzgzIDIzLjA5OTZDMjcuMzc4MyAyMy4xNDQ3IDI3LjMxMDYgMjMuMTg5NiAyNy4yNjU1IDIzLjE4OTZIMTkuMzQyMUwxOS4yOTcgMjMuMzY5N0gyNy40OTEyWk0xOS4wOTM5IDIzLjE4OTZIMTguODQ1NUwxOC44MjI5IDIzLjM2OTdIMTkuMDcxM0wxOS4wOTM5IDIzLjE4OTZaTTE5LjUwMDIgMjAuMjY0MUwxOC45MTMzIDIyLjk2NDZIMTkuMTYxNUwxOS43NDg0IDIwLjIxOTFIMTkuNTQ1M0MxOS41NDUzIDIwLjIxOTEgMTkuNTIyNyAyMC4yNDE1IDE5LjUwMDIgMjAuMjY0MVpNMjcuNjcxOCAyMC4yMTkxSDE5Ljk3NDJMMTkuMzg3MiAyMi45NjQ2SDI3LjE3NTFMMjcuNjcxOCAyMC4yMTkxWk0xMy43ODkgMzEuMDQzNkgxMy45NDdMMTUuMDk4NCAyNi4xMTUySDE1LjE2NkwxNi43MjM2IDE5LjI5NjRDMTYuODU5MSAxOC43NTYzIDE3LjEwNzMgMTguMjM4OCAxNy40Njg1IDE3Ljc2NjFDMTcuODI5NiAxNy4zMTYxIDE4LjI4MTIgMTYuOTMzNCAxOC43Nzc4IDE2LjY2MzNDMTkuMDI2MSAxNi41Mjg0IDE5LjI3NDUgMTYuNDM4NCAxOS41MjI3IDE2LjM0ODNMMTkuNTAwMiAxNi4zMDM0QzE5Ljc0ODQgMTYuMjEzNCAyMC4wMTkzIDE2LjE5MDggMjAuMjkwMyAxNi4xOTA4SDI4Ljg2ODJDMjkuMjI5NCAxNi4xOTA4IDI5LjU5MDUgMTYuMjU4MyAyOS44ODQgMTYuMzkzNEMyOS41MjI5IDE2LjEyMzMgMjkuMDkzOSAxNi4wMTA4IDI4LjU5NzIgMTYuMDEwOEgyMC4wNDE5QzE5LjU0NTMgMTYuMDEwOCAxOS4wNDg2IDE2LjE0NTkgMTguNTc0NyAxNi4zOTM0QzE4LjA3OCAxNi42NjMzIDE3LjY0OTEgMTcuMDIzNSAxNy4zMTA0IDE3LjQ5NkMxNi45NDkzIDE3Ljk0NjIgMTYuNzAxIDE4LjQ0MTIgMTYuNTg4MSAxOC45NTg5TDEzLjc4OSAzMS4wNDM2Wk0xNy4yMjAyIDMxLjg1MzdMMTguMTkxIDI3LjM5OEgxNy45NDI2TDE3LjAxNzEgMzEuNTgzNkgxNi45NDkzTDE2LjkwNDIgMzEuODUzN0gxNy4yMjAyWk0yNS45MzM2IDMxLjA0MzZIMjYuMTE0MkwyNi43Njg5IDI3LjM5OEgyNi41ODgzTDI1LjkzMzYgMzEuMDQzNlpNMzEuNDg2NyAxOS43NDY0QzMxLjU3NjkgMTkuMjA2NCAzMS41MDkzIDE4LjcxMTMgMzEuMzI4NyAxOC4yNjEyQzMxLjI4MzYgMTguMTQ4OCAzMS4yMTU4IDE4LjAzNjIgMzEuMTQ4MSAxNy45MjM2QzMxLjI4MzYgMTguMzUxMiAzMS4zMjg3IDE4LjgwMTQgMzEuMjM4MyAxOS4yOTY0TDMwLjA4NzIgMjYuMTE1MkwzMC4xNTQ4IDI2LjEzNzdMMjkuMjUxOSAzMS42MDYySDI5LjE4NDNMMjkuMTM5IDMxLjg3NjNIMjkuNDU1TDMxLjQ4NjcgMTkuNzQ2NFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0xNy42ODQ3IDIuNDI3MTdIMTcuNjYxOEMxNy44NjgyIDIuODk5MTIgMTcuOTE0IDMuNDM4NDkgMTcuODIyMiA0LjAwMDMzTDE3LjU5MjkgNS4zOTM3SDE3LjA0MjNMMTYuOTczNSA1LjczMDhIMTMuOTY4NEwxNC4xMjkxIDQuODk5MjhIMTQuMDM3M0wxNC4xMDYyIDQuNTg0NjRIMTMuODUzOEMxMy44MDggNC41ODQ2NCAxMy43ODUxIDQuNTYyMTcgMTMuNzYyIDQuNTM5NjlDMTMuNzM5MSA0LjUxNzIzIDEzLjczOTEgNC40OTQ3NSAxMy43MzkxIDQuNDQ5OEg1LjkxNjg2TDUuNTQ5ODcgNi4wOTAzOUgxMy41Nzg1QzE0LjEyOTEgNi4wOTAzOSAxNC42MzM3IDYuMjQ3NzEgMTUuMDQ2NyA2LjUzOTg3QzE1LjM2NzggNi43NDIxMyAxNS41OTcxIDcuMDU2NzcgMTUuODAzNiA3LjM3MTQxQzE1Ljg0OTUgNy40Mzg4OSAxNS44NzI0IDcuNDgzODIgMTUuOTE4NCA3LjU1MTEyQzE2LjIxNjYgNy43OTgzMiAxNi40Njg4IDguMTEyOTkgMTYuNjI5NSA4LjQ5NTE0QzE2LjgzNTkgOC45NjY5OCAxNi45MDQ4IDkuNTA2NDcgMTYuNzkwMSAxMC4wOTA3TDE2LjI4NTMgMTMuMTY5NkMxNi4xOTM1IDEzLjczMTUgMTUuOTQxMyAxNC4yNDg0IDE1LjU3NDMgMTQuNzQyOEMxNS4yMDcyIDE1LjIxNDcgMTQuNzQ4NCAxNS41OTY4IDE0LjIyMDggMTUuODg4OUMxNC4xNTIgMTUuOTExNSAxNC4wNjAyIDE1Ljk1NjQgMTMuOTkxNSAxNS45Nzg4QzEzLjg3NjcgMTYuMDY4OCAxMy43NjIgMTYuMTU4NyAxMy42MjQ1IDE2LjIyNkMxMy4wOTY4IDE2LjQ5NTcgMTIuNTY5MiAxNi42NTMxIDExLjk5NTcgMTYuNjUzMUgyLjYzNjYxQzIuMDg2MDcgMTYuNjUzMSAxLjU4MTQxIDE2LjQ5NTcgMS4xNjg1IDE2LjIyNkMwLjc1NTYxIDE1Ljk1NjQgMC40ODAzMzEgMTUuNTc0MyAwLjMxOTc1IDE1LjEyNDhDMC4xODIxMiAxNC43NjUyIDAuMTU5MTg3IDE0LjQwNTggMC4xODIxMTkgMTQuMDAxMkMwLjE4MjExOSAxMy45NTYxIDAuMTM2MjU0IDEzLjkzMzggMC4xMzYyNTQgMTMuODg4OEMtMC4wMjQzMjYxIDEzLjQxNjggLTAuMDQ3MjU4MSAxMi44Nzc1IDAuMDkwMzcyNSAxMi4zMTU2TDAuMzg4NTgzIDExLjAxMjJDMC4zODg1ODMgMTAuOTY3MyAwLjQ1NzM5OCAxMC45MjIzIDAuNTAzMjY0IDEwLjkyMjNIMy41NTQxN0MzLjU3NzExIDEwLjkyMjMgMy42MDAwNCAxMC45NDQ3IDMuNjIyOTkgMTAuOTY3M0MzLjY0NTkyIDEwLjk4OTYgMy42Njg4NyAxMS4wMzQ2IDMuNjQ1OTIgMTEuMDU3MUwzLjYwMDA0IDExLjMyNjlIMy44OTgyNUwzLjgwNjUgMTEuNzMxNEg0LjMxMTE2TDQuMjE5MzkgMTIuMjAzMkgxMi4yNzFDMTIuMjcxIDEyLjIwMzIgMTIuMjcxIDEyLjIwMzIgMTIuMjkzOSAxMi4xODA4QzEyLjI5MzkgMTIuMTU4MyAxMi4zMTY4IDEyLjE1ODMgMTIuMzE2OCAxMi4xNTgzTDEyLjU5MjEgMTAuNTYyN0gzLjk5MDAyQzMuNDM5NDggMTAuNTYyNyAyLjk1Nzc1IDEwLjQyNzggMi41Njc3OSAxMC4xMzU2QzIuMTU0ODggOS44NjU5IDEuODc5NjIgOS41MDY0NyAxLjcxOTA0IDkuMDM0NDZDMS42MDQzNCA4LjY5NzQxIDEuNTU4NDYgOC4zMzc4MSAxLjYwNDM0IDcuOTMzMjhDMS42MDQzNCA3Ljg4ODM1IDEuNTU4NDYgNy44NjU4IDEuNTU4NDYgNy44MjA4N0MxLjM5NzkgNy4zNDg4NiAxLjM3NDk3IDYuODA5NTYgMS41MTI2IDYuMjI1MjNMMi4yNDY2NSAzLjE0NjM0QzIuMzg0MjggMi41ODQ0OSAyLjYzNjYxIDIuMDY3NTggMy4wMDM2MyAxLjU3MzE2QzMuMzcwNjYgMS4xMDEyMiAzLjgyOTQ0IDAuNzE5MTUyIDQuMzU3MDQgMC40NDk0NzZDNC44ODQ2MyAwLjE1NzMxOSA1LjQxMjI0IDAgNS45NjI4MyAwSDE0LjcwMjZDMTUuMjMwMSAwIDE1LjcxMTggMC4xNTczMTkgMTYuMTI0OCAwLjQ0OTQ3NkMxNi40NDU5IDAuNjc0MjA2IDE2LjY3NTMgMC45NjYzOCAxNi44NTg4IDEuMzAzNDhDMTYuOTA0OCAxLjM0ODQzIDE2LjkyNzcgMS4zOTMzNyAxNi45NzM1IDEuNDYwOEMxNy4yNzE4IDEuNzMwNDggMTcuNTI0IDIuMDIyNjQgMTcuNjg0NyAyLjQyNzE3Wk0zLjY0NTkyIDEyLjU4NTRWMTIuNjA3OEgzLjg5ODI1TDMuOTIxMiAxMi40MjhIMy42Njg4N0wzLjY0NTkyIDEyLjU2MjhDMy42MjI5OSAxMi41NjI4IDMuNjIyOTkgMTIuNTg1NCAzLjY0NTkyIDEyLjU4NTRaTTQuOTk5MzMgNi41NjIzNUg1LjIwNTc4TDUuMjc0NjEgNi4zMTUxNEg0Ljk1MzQ1TDQuOTA3NTggNi40NDk5N0M0LjkwNzU4IDYuNDk0OTIgNC45MDc1OCA2LjUxNzQgNC45MzA1MSA2LjUzOTg3QzQuOTUzNDUgNi41NjIzNSA0Ljk3NjQgNi41NjIzNSA0Ljk5OTMzIDYuNTYyMzVaTTEuNzQxOTcgNi4yNzAxN0MxLjY1MDIzIDYuNjc0NyAxLjY1MDIzIDcuMDU2NzcgMS43MTkwNCA3LjM5Mzc5TDEuNzQxOTcgNy4yMzY1NUMxLjc2NDkyIDcuMDExODEgMS43ODc4NiA2LjgwOTU2IDEuODMzNzQgNi41ODQ4MUwyLjU0NDg0IDMuNTI4MzlDMi42ODI0OSAyLjk2NjU0IDIuOTM0ODIgMi40NDk2NSAzLjMwMTg1IDEuOTc3NjlDMy4zNDc3MSAxLjkzMjc0IDMuMzcwNjYgMS44ODc4IDMuMzkzNTkgMS44NDI4NUwzLjQ2MjQxIDEuOTEwMjhDMy44MDY1IDEuNDgzMjcgNC4xOTY0NiAxLjE0NjE2IDQuNjc4MTkgMC44OTg5NTNDNS4xODI4NCAwLjYyOTI2IDUuNjg3NSAwLjQ5NDQyMyA2LjIxNTA2IDAuNDk0NDIzSDE0Ljk3NzlDMTUuMTg0MyAwLjQ5NDQyMyAxNS4zOTA3IDAuNTE2OTA0IDE1LjU5NzEgMC41NjE4NUwxNS42MiAwLjQ5NDQyM0MxNS43MzQ5IDAuNTE2OTA0IDE1Ljg0OTUgMC41NjE4NSAxNS45NjQyIDAuNjA2Nzk2QzE1LjU5NzEgMC4zNTk1ODUgMTUuMTg0MyAwLjI0NzIxMSAxNC43MDI2IDAuMjQ3MjExSDUuOTYyODNDNS40NTgxMiAwLjI0NzIxMSA0Ljk1MzQ1IDAuMzU5NTg0IDQuNDcxNzQgMC42MjkyNkMzLjk2NzA3IDAuODk4OTUzIDMuNTU0MTcgMS4yNTg1NCAzLjE4NzE1IDEuNzA4MDFDMi44NDMwNSAyLjE3OTk1IDIuNTkwNzIgMi42NzQzOCAyLjQ3NjAzIDMuMTkxMjhMMS43NDE5NyA2LjI3MDE3Wk0xMi4yNzEgMTIuNDI4SDQuMTczNTNMNC4xMjc2NSAxMi42MDc4SDcuNzI5MVYxMi42NzUySDEyLjU2OTJDMTIuNTkyMSAxMi42NzUyIDEyLjYxNSAxMi42NTI3IDEyLjY2MSAxMi42MzAzQzEyLjY4MzkgMTIuNjA3OCAxMi43MDY4IDEyLjU2MjggMTIuNzA2OCAxMi41NDAzTDEyLjg0NDUgMTEuODIxMkwxMy4wNTEgMTAuNjc1QzEzLjA3MzkgMTAuNjMgMTMuMDUxIDEwLjYwNzcgMTMuMDI4MSAxMC41ODUxQzEzLjAwNSAxMC41NjI3IDEyLjk4MjEgMTAuNTYyNyAxMi45NTkyIDEwLjU2MjdIMTIuODQ0NUwxMi41MjMzIDEyLjIwMzJDMTIuNTIzMyAxMi4yNDgyIDEyLjUwMDQgMTIuMzE1NiAxMi40MzE3IDEyLjM2MDZDMTIuMzg1NyAxMi40MDU1IDEyLjMxNjggMTIuNDI4IDEyLjI3MSAxMi40MjhaTTQuMDM1OSAxMS45NTZIMy43NjA2MkwzLjcxNDc0IDEyLjIwMzJIMy45NjcwN0w0LjAzNTkgMTEuOTU2Wk0wLjI5NjgxNyAxMi4zNjA2QzAuMjA1MDY5IDEyLjc2NTEgMC4yMDUwNyAxMy4xNjk2IDAuMjczODg2IDEzLjUyOTJMMC4zMTk3NSAxMy4zMDQ0QzAuMzE5NzUgMTMuMTAyMSAwLjM0MjcgMTIuODk5OSAwLjQxMTUxNiAxMi42NzUyTDAuNzA5NzI3IDExLjMyNjlIMy4zNzA2NkwzLjM5MzU5IDExLjE0N0gwLjU5NTAyOUwwLjI5NjgxNyAxMi4zNjA2Wk0xNi41ODM1IDEwLjA0NThDMTYuNjc1MyA5LjUwNjQ3IDE2LjYwNjYgOS4wMTE5MSAxNi40MjMgOC41ODVDMTYuNDAwMSA4LjU0MDA3IDE2LjM3NzEgOC40OTUxNCAxNi4zNTQyIDguNDUwMjFDMTYuNDAwMSA4LjY1MjQ4IDE2LjQyMyA4Ljg1NDc0IDE2LjQyMyA5LjA3OTM5QzE2LjQyMyA5LjI1OTI3IDE2LjQyMyA5LjQzODk5IDE2LjM3NzEgOS42MTg3TDE1Ljg3MjQgMTIuNjk3NkMxNS44MjY2IDEyLjkyMjQgMTUuNzU3OCAxMy4xMjQ3IDE1LjY4ODkgMTMuMzI3TDE1LjY0MzEgMTMuNTk2N0MxNS41NTE0IDE0LjA2ODUgMTUuMzQ0OSAxNC41MTggMTUuMDQ2NyAxNC45NDUxQzE1LjE2MTQgMTQuODMyNyAxNS4yOTkgMTQuNzIwMyAxNS4zOTA3IDE0LjYwOEMxNS43MzQ5IDE0LjE1ODQgMTUuOTY0MiAxMy42NDE2IDE2LjA1NiAxMy4xMjQ3TDE2LjU4MzUgMTAuMDQ1OFpNMTMuNTc4NSA2LjMxNTE0SDUuNTAzOTlMNS40NTgxMiA2LjU2MjM1SDEwLjA5MTdWNi40OTQ5MkgxMy44NTM4QzE0LjI0MzcgNi40OTQ5MiAxNC41ODc5IDYuNTYyMzUgMTQuOTA5IDYuNzE5NjVDMTQuNTE5IDYuNDQ5OTcgMTQuMDgzMyA2LjMxNTE0IDEzLjU3ODUgNi4zMTUxNFpNNS4zMjA0NyA2LjA5MDM5TDUuNjg3NSA0LjQ0OThINS40NTgxMkM1LjQzNTE3IDQuNDQ5OCA1LjQxMjI0IDQuNDcyMjggNS4zODkyOSA0LjQ5NDc1QzUuMzQzNDIgNC41MTcyMyA1LjM0MzQyIDQuNTYyMTggNS4zMjA0NyA0LjU4NDY0TDQuOTk5MzMgNi4wOTAzOUg1LjMyMDQ3Wk0xNy41OTI5IDMuOTc3ODZDMTcuNjg0NyAzLjQzODQ5IDE3LjYzODcgMi45NDQwNyAxNy40NTUyIDIuNDk0NTlDMTcuNDU1MiAyLjQ3MjExIDE3LjQwOTQgMi40NDk2NSAxNy40MDk0IDIuNDA0NjhDMTcuNDc4MyAyLjc2NDI3IDE3LjUwMTEgMy4xNDYzNCAxNy40MzIzIDMuNTUwODVMMTcuMjAzIDQuODk5MjhIMTcuMTM0MUwxNy4wODgzIDUuMTY4OTdIMTcuNDA5NEwxNy41OTI5IDMuOTc3ODZaIiBmaWxsPSJ3aGl0ZSIvPgo8L2c+CjxkZWZzPgo8Y2xpcFBhdGggaWQ9ImNsaXAwXzE4NzlfMTcyNzciPgo8cmVjdCB3aWR0aD0iMzEuNzQ0OSIgaGVpZ2h0PSIzNCIgZmlsbD0id2hpdGUiLz4KPC9jbGlwUGF0aD4KPC9kZWZzPgo8L3N2Zz4K' : $data['white_label_plugin_menu_icon'];
656
657 // Use 'read' capability since actual access is controlled by current_user_has_plugin_access() check above
658 $menu_capability = 'read';
659
660 // Separator just before the plugin entry, after all other plugins (Yoast etc. ~99)
661 add_action('admin_menu', function() {
662 global $menu;
663 $menu['100'] = array('', 'read', 'separator-metasync-before', '', 'wp-menu-separator');
664 }, 5);
665
666 // Main menu page at position 100.1 — bottom of the menu after all standard items
667 add_menu_page(
668 $menu_name,
669 $menu_title,
670 $menu_capability,
671 $menu_slug,
672 array($admin, 'create_admin_settings_page'),
673 $menu_icon,
674 '100.1'
675 );
676
677 // Check connection status for submenu availability
678 $general_options = Metasync::get_option('general');
679 $has_api_key = !empty($general_options['searchatlas_api_key']);
680 $has_uuid = !empty($general_options['otto_pixel_uuid']);
681 $is_fully_connected = Metasync_Heartbeat_Manager::instance()->is_heartbeat_connected($general_options);
682
683 $seo_controls = Metasync::get_option('seo_controls');
684
685 // ── Connect slug ──────────────────────────────────────────────────
686 $connect_slug = $menu_slug . '-connect';
687
688 // Dashboard (check access control + "Hide Dashboard" general setting)
689 if (Metasync_Access_Control::user_can_access('hide_dashboard') && !self::is_dashboard_hidden_by_framework()) {
690 add_submenu_page($menu_slug, 'Dashboard', 'Dashboard', $menu_capability, $menu_slug . '-dashboard', array($admin, 'create_admin_dashboard_iframe'));
691 }
692
693 // Indexation Control
694 if (Metasync_Access_Control::user_can_access('hide_indexation_control')) {
695 add_submenu_page($menu_slug, 'Indexation Control', 'Indexation Control', $menu_capability, $menu_slug . '-seo-controls', array($admin, 'create_admin_seo_controls_page'));
696 }
697
698 // 404 Monitor
699 add_submenu_page($menu_slug, '404 Monitor', '404 Monitor', $menu_capability, $menu_slug . '-404-monitor', array($admin, 'create_admin_404_monitor_page'));
700
701 // Redirections
702 if (Metasync_Access_Control::user_can_access('hide_redirections')) {
703 add_submenu_page($menu_slug, 'Redirections', 'Redirections', $menu_capability, $menu_slug . '-redirections', array($admin, 'create_admin_redirections_page'));
704 }
705
706 // XML Sitemap
707 if (Metasync_Access_Control::user_can_access('hide_xml_sitemap')) {
708 add_submenu_page($menu_slug, 'XML Sitemap', 'XML Sitemap', $menu_capability, $menu_slug . '-xml-sitemap', array($admin, 'create_admin_xml_sitemap_page'));
709 }
710
711 // Robots.txt
712 if (Metasync_Access_Control::user_can_access('hide_robots')) {
713 add_submenu_page($menu_slug, 'Robots.txt', 'Robots.txt', $menu_capability, $menu_slug . '-robots-txt', array($admin, 'create_admin_robots_txt_page'));
714 }
715
716 // Site Verification
717 add_submenu_page($menu_slug, 'Site Verification', 'Site Verification', $menu_capability, $menu_slug . '-search-engine-verify', array($admin, 'create_admin_search_engine_verification_page'));
718
719 // Instant Indexing (conditional)
720 if ($seo_controls['enable_googleinstantindex'] ?? false) {
721 add_submenu_page($menu_slug, 'Instant Indexing', 'Instant Indexing', $menu_capability, $menu_slug . '-instant-index', array($admin, 'create_admin_google_instant_index_page'));
722 }
723
724 // Google Console (conditional)
725 if ($general_options['enable_google_console'] ?? false) {
726 add_submenu_page($menu_slug, 'Google Console', 'Google Console', $menu_capability, $menu_slug . '-google-console', array($admin, 'create_admin_google_console_page'));
727 }
728
729 // Bing Console (conditional)
730 if ($seo_controls['enable_binginstantindex'] ?? false) {
731 add_submenu_page($menu_slug, 'Bing Console', 'Bing Console', $menu_capability, $menu_slug . '-bing-console', array($admin, 'create_admin_bing_console_page'));
732 }
733
734 // Schema Markup
735 add_submenu_page($menu_slug, 'Schema Markup', 'Schema Markup', $menu_capability, $menu_slug . '-schema-markup', array($admin, 'create_admin_schema_markup_page'));
736
737 // Import SEO Data
738 if (Metasync_Access_Control::user_can_access('hide_import_seo')) {
739 add_submenu_page($menu_slug, 'Import SEO Data', 'Import SEO Data', $menu_capability, $menu_slug . '-import-external', array($admin, 'render_import_external_data_page'));
740 }
741
742 // Settings
743 if (Metasync_Access_Control::user_can_access('hide_settings')) {
744 add_submenu_page($menu_slug, 'Settings', 'Settings', $menu_capability, $menu_slug, array($admin, 'create_admin_settings_page'));
745 }
746
747 // Local Business
748 add_submenu_page($menu_slug, 'Local Business', 'Local Business', $menu_capability, $menu_slug . '-local-business', array($admin, 'create_admin_local_business_page'));
749
750 // Breadcrumbs
751 add_submenu_page($menu_slug, 'Breadcrumbs', 'Breadcrumbs', $menu_capability, $menu_slug . '-breadcrumbs', array($admin, 'create_admin_breadcrumbs_page'));
752
753 // Code Snippets
754 add_submenu_page($menu_slug, 'Code Snippets', 'Code Snippets', $menu_capability, $menu_slug . '-code-snippets', array($admin, 'create_admin_code_snippets_page'));
755
756 // Code Minification
757 add_submenu_page($menu_slug, 'Code Minification', 'Code Minification', $menu_capability, $menu_slug . '-code-minification', array($admin, 'create_admin_code_minification_page'));
758
759 // Media Optimization
760 add_submenu_page($menu_slug, 'Media Optimization', 'Media Optimization', $menu_capability, $menu_slug . '-media-optimization', array($admin, 'create_admin_media_optimization_page'));
761
762 // Custom Pages
763 if (Metasync_Access_Control::user_can_access('hide_custom_pages')) {
764 add_submenu_page($menu_slug, 'Custom Pages', 'Custom Pages', $menu_capability, $menu_slug . '-custom-pages', array($admin, 'create_admin_custom_pages_page'));
765 }
766
767 // Bot Statistics
768 add_submenu_page($menu_slug, 'Bot Statistics', 'Bot Statistics', $menu_capability, $menu_slug . '-bot-statistics', array($admin, 'create_admin_bot_statistics_page'));
769
770 // SEO Health dashboard
771 add_submenu_page($menu_slug, 'SEO Health', 'SEO Health', $menu_capability, $menu_slug . '-seo-health', array($admin, 'create_admin_seo_health_page'));
772
773 // Compatibility
774 if (Metasync_Access_Control::user_can_access('hide_compatibility')) {
775 add_submenu_page($menu_slug, 'Compatibility', 'Compatibility', $menu_capability, $menu_slug . '-compatibility', array($admin, 'create_admin_compatibility_page'));
776 }
777
778 // Changes Log
779 if (Metasync_Access_Control::user_can_access('hide_sync_log')) {
780 add_submenu_page($menu_slug, 'Changes Log', 'Changes Log', $menu_capability, $menu_slug . '-sync-log', array($admin, 'create_admin_sync_log_page'));
781 }
782
783 // Report Issue
784 if (Metasync_Access_Control::user_can_access('hide_report_issue')) {
785 add_submenu_page($menu_slug, 'Report Issue', 'Report Issue', $menu_capability, $menu_slug . '-report-issue', array($admin, 'create_admin_report_issue_page'));
786 }
787
788 // ── Connect CTA (shown when not authenticated) ────────────────────
789 if (!$is_fully_connected) {
790 $connect_label = sprintf('Connect to %s', Metasync::get_effective_plugin_name());
791 add_submenu_page($menu_slug, $connect_label, $connect_label, $menu_capability, $connect_slug, array($admin, 'create_admin_settings_page'));
792 }
793
794 // ── Hidden pages (no sidebar entry needed) ────────────────────────
795 add_submenu_page('', 'Setup Wizard', 'Setup Wizard', $menu_capability, $menu_slug . '-setup-wizard', array($admin->setup_wizard, 'render_wizard_page'));
796
797
798 // Rename auto-generated first submenu from plugin name to "Settings" and reorder
799 add_action('admin_menu', function() use ($menu_slug) {
800 global $submenu;
801 if (!isset($submenu[$menu_slug])) {
802 return;
803 }
804 // Remove the auto-duplicate of the main menu item (same slug as parent)
805 foreach ($submenu[$menu_slug] as $key => $item) {
806 if ($item[2] === $menu_slug) {
807 unset($submenu[$menu_slug][$key]);
808 break;
809 }
810 }
811 }, 999);
812
813 }
814
815 /**
816 * Render the grouped navigation menu with Dashboard + SEO/Plugin dropdowns.
817 */
818 public function render_navigation_menu($current_page = null)
819 {
820 $page_slug = Metasync_Admin::$page_slug;
821 $available_menu_items = $this->get_available_menu_items();
822
823 $menu_icons = [
824 'general' => 'admin-settings',
825 'dashboard' => 'dashboard',
826 'compatibility' => 'admin-tools',
827 'sync_log' => 'list-view',
828 'seo_controls' => 'search',
829 'instant_index' => 'performance',
830 'google_console' => 'chart-area',
831 'bing_console' => 'chart-bar',
832 'redirections' => 'undo',
833 'robots_txt' => 'shield',
834 'xml_sitemap' => 'networking',
835 'schema_markup' => 'tag',
836 'site_verification'=> 'yes-alt',
837 'import_seo' => 'download',
838 'custom_pages' => 'admin-page',
839 'code_minification'=> 'media-code',
840 'bot_statistics' => 'visibility',
841 'breadcrumbs' => 'menu',
842 'monitor_404' => 'warning',
843 'local_business' => 'building',
844 'code_snippets' => 'editor-code',
845 'report_issue' => 'sos',
846 'error_log' => 'warning',
847 'seo_health' => 'heart'
848
849 ];
850
851 $seo_items = [];
852 $plugin_items = [];
853
854 foreach ($available_menu_items as $key => $menu_item) {
855 $group = $menu_item['group'] ?? 'plugin';
856 if ($group === 'seo') {
857 $seo_items[$key] = $menu_item;
858 } else {
859 $plugin_items[$key] = $menu_item;
860 }
861 }
862 ?>
863 <!-- Plugin Navigation Menu with Dashboard + Grouped Dropdowns -->
864 <div class="metasync-nav-wrapper">
865 <div class="metasync-nav-tabs metasync-nav-grouped">
866 <?php
867 // Dashboard tab (standalone)
868 if (isset($seo_items['dashboard'])) {
869 $is_active = ($current_page === 'dashboard');
870 $icon = $menu_icons['dashboard'];
871 $page_url = '?page=' . $page_slug . $seo_items['dashboard']['slug_suffix'];
872 ?>
873 <a href="<?php echo esc_url($page_url); ?>" class="metasync-nav-tab <?php echo $is_active ? 'active' : ''; ?>">
874 <span class="tab-icon"><span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span></span>
875 <span class="tab-text"><?php echo esc_html($seo_items['dashboard']['title']); ?></span>
876 </a>
877 <?php
878 }
879 ?>
880
881 <!-- SEO Features Dropdown (excluding Dashboard) -->
882 <div class="metasync-nav-dropdown">
883 <?php
884 $has_active_seo = false;
885 foreach ($seo_items as $key => $menu_item) {
886 if ($key !== 'dashboard' && $current_page === $key) {
887 $has_active_seo = true;
888 break;
889 }
890 }
891 ?>
892 <button type="button" class="metasync-nav-dropdown-btn <?php echo $has_active_seo ? 'active' : ''; ?>" aria-haspopup="true" aria-expanded="false">
893 <span class="tab-icon"><span class="dashicons dashicons-search"></span></span>
894 <span class="tab-text">SEO</span>
895 <span class="dropdown-arrow"></span>
896 </button>
897 <div class="metasync-nav-dropdown-menu">
898 <?php
899 foreach ($seo_items as $key => $menu_item) {
900 if ($key === 'dashboard') {
901 continue;
902 }
903
904 $is_active = ($current_page === $key);
905 $icon = $menu_icons[$key] ?? 'admin-generic';
906 $page_url = '?page=' . $page_slug . $menu_item['slug_suffix'];
907 ?>
908 <a href="<?php echo esc_url($page_url); ?>" class="metasync-nav-dropdown-item <?php echo $is_active ? 'active' : ''; ?>">
909 <span class="tab-icon"><span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span></span>
910 <span class="tab-text"><?php echo esc_html($menu_item['title']); ?></span>
911 </a>
912 <?php
913 }
914 ?>
915 </div>
916 </div>
917
918 <!-- Plugin Dropdown -->
919 <div class="metasync-nav-dropdown">
920 <?php
921 $has_active_plugin = false;
922 foreach ($plugin_items as $key => $menu_item) {
923 if ($key !== 'report_issue' && $current_page === $key) {
924 $has_active_plugin = true;
925 break;
926 }
927 }
928 ?>
929 <button type="button" class="metasync-nav-dropdown-btn <?php echo $has_active_plugin ? 'active' : ''; ?>" aria-haspopup="true" aria-expanded="false">
930 <span class="tab-icon"><span class="dashicons dashicons-admin-settings"></span></span>
931 <span class="tab-text">Plugin</span>
932 <span class="dropdown-arrow"></span>
933 </button>
934 <div class="metasync-nav-dropdown-menu">
935 <?php
936 foreach ($plugin_items as $key => $menu_item) {
937 if ($key === 'report_issue') {
938 continue;
939 }
940
941 $is_active = ($current_page === $key);
942 $icon = $menu_icons[$key] ?? 'admin-generic';
943 $page_url = '?page=' . $page_slug . $menu_item['slug_suffix'];
944 ?>
945 <a href="<?php echo esc_url($page_url); ?>" class="metasync-nav-dropdown-item <?php echo $is_active ? 'active' : ''; ?>">
946 <span class="tab-icon"><span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span></span>
947 <span class="tab-text"><?php echo esc_html($menu_item['title']); ?></span>
948 </a>
949 <?php
950 }
951 ?>
952 </div>
953 </div>
954
955 <!-- Right side - Report Issue and Settings dropdown -->
956 <div class="metasync-nav-right">
957 <?php
958 if (isset($available_menu_items['report_issue'])) {
959 $is_active = ($current_page === 'report_issue');
960 $page_url = '?page=' . $page_slug . $available_menu_items['report_issue']['slug_suffix'];
961 ?>
962 <a href="<?php echo esc_url($page_url); ?>" class="metasync-nav-tab <?php echo $is_active ? 'active' : ''; ?>">
963 <span class="tab-icon"><span class="dashicons dashicons-sos"></span></span>
964 <span class="tab-text">Report Issue</span>
965 </a>
966 <?php } ?>
967 <div class="metasync-simple-dropdown">
968 <button type="button" class="metasync-settings-btn" id="metasync-settings-btn" onclick="toggleSettingsMenuPortal(event)" aria-expanded="false">
969 <span class="tab-icon"><span class="dashicons dashicons-admin-settings"></span></span>
970 <span class="tab-text">Settings</span>
971 <span class="dropdown-arrow"></span>
972 </button>
973 </div>
974 </div>
975 </div>
976 </div>
977
978 <script>
979 // Handle navigation dropdowns using portal pattern for reliable positioning
980 (function() {
981 var dropdowns = document.querySelectorAll('.metasync-nav-dropdown');
982 var activePortal = null;
983 var activeButton = null;
984 var activeDropdown = null;
985 var scrollHandler = null;
986 var resizeHandler = null;
987
988 function positionPortalMenu(button, portalMenu) {
989 var rect = button.getBoundingClientRect();
990 var menuRect = portalMenu.getBoundingClientRect();
991 var viewportWidth = window.innerWidth;
992 var viewportHeight = window.innerHeight;
993
994 var left = rect.left;
995 if (left + menuRect.width > viewportWidth - 10) {
996 left = Math.max(10, viewportWidth - menuRect.width - 10);
997 }
998
999 var top = rect.bottom + 8;
1000 if (top + menuRect.height > viewportHeight - 10 && rect.top > menuRect.height + 10) {
1001 top = rect.top - menuRect.height - 8;
1002 }
1003
1004 portalMenu.style.position = 'fixed';
1005 portalMenu.style.top = top + 'px';
1006 portalMenu.style.left = left + 'px';
1007 portalMenu.style.zIndex = '999999999';
1008 }
1009
1010 function closeActivePortal() {
1011 if (activePortal && activePortal.parentNode) {
1012 activePortal.parentNode.removeChild(activePortal);
1013 }
1014 if (activeButton) {
1015 activeButton.setAttribute('aria-expanded', 'false');
1016 }
1017 if (activeDropdown) {
1018 activeDropdown.classList.remove('active');
1019 }
1020 if (scrollHandler) {
1021 window.removeEventListener('scroll', scrollHandler, true);
1022 scrollHandler = null;
1023 }
1024 if (resizeHandler) {
1025 window.removeEventListener('resize', resizeHandler);
1026 resizeHandler = null;
1027 }
1028 activePortal = null;
1029 activeButton = null;
1030 activeDropdown = null;
1031 }
1032
1033 function openAsPortal(dropdown, button, menu) {
1034 closeActivePortal();
1035
1036 var portalMenu = menu.cloneNode(true);
1037 portalMenu.id = 'metasync-nav-portal-menu';
1038 portalMenu.style.opacity = '1';
1039 portalMenu.style.visibility = 'visible';
1040 portalMenu.style.transform = 'none';
1041
1042 document.body.appendChild(portalMenu);
1043
1044 positionPortalMenu(button, portalMenu);
1045
1046 activePortal = portalMenu;
1047 activeButton = button;
1048 activeDropdown = dropdown;
1049 dropdown.classList.add('active');
1050 button.setAttribute('aria-expanded', 'true');
1051
1052 scrollHandler = function() {
1053 if (activePortal && activeButton) {
1054 positionPortalMenu(activeButton, activePortal);
1055 }
1056 };
1057 resizeHandler = scrollHandler;
1058
1059 window.addEventListener('scroll', scrollHandler, true);
1060 window.addEventListener('resize', resizeHandler);
1061
1062 portalMenu.addEventListener('click', function(e) {
1063 var link = e.target.closest('a');
1064 if (link) {
1065 setTimeout(closeActivePortal, 0);
1066 }
1067 });
1068 }
1069
1070 dropdowns.forEach(function(dropdown) {
1071 var button = dropdown.querySelector('.metasync-nav-dropdown-btn');
1072 var menu = dropdown.querySelector('.metasync-nav-dropdown-menu');
1073
1074 if (!button || !menu) return;
1075
1076 button.addEventListener('click', function(e) {
1077 e.preventDefault();
1078 e.stopPropagation();
1079
1080 var isExpanded = button.getAttribute('aria-expanded') === 'true';
1081
1082 if (isExpanded) {
1083 closeActivePortal();
1084 } else {
1085 openAsPortal(dropdown, button, menu);
1086 }
1087 });
1088 });
1089
1090 document.addEventListener('click', function(e) {
1091 if (activePortal && activeButton) {
1092 if (!activePortal.contains(e.target) && !activeButton.contains(e.target)) {
1093 closeActivePortal();
1094 }
1095 }
1096 });
1097
1098 document.addEventListener('keydown', function(e) {
1099 if (e.key === 'Escape' && activePortal) {
1100 closeActivePortal();
1101 if (activeButton) {
1102 activeButton.focus();
1103 }
1104 }
1105 });
1106 })();
1107
1108 // Portal-style dropdown that bypasses stacking contexts
1109 function toggleSettingsMenuPortal(event) {
1110 event.preventDefault();
1111 event.stopPropagation();
1112
1113 var button = event.currentTarget;
1114 var existingMenu = document.getElementById('metasync-portal-menu');
1115
1116 if (existingMenu) {
1117 existingMenu.remove();
1118 button.classList.remove('active');
1119 button.setAttribute('aria-expanded', 'false');
1120 return;
1121 }
1122
1123 var menu = document.createElement('div');
1124 menu.id = 'metasync-portal-menu';
1125 menu.className = 'metasync-portal-menu';
1126
1127 var currentUrl = window.location.href;
1128 var isGeneralActive = currentUrl.indexOf('tab=general') > -1 || currentUrl.indexOf('tab=') === -1;
1129 var isAdvancedActive = currentUrl.indexOf('tab=advanced') > -1;
1130 var isWhitelabelActive = currentUrl.indexOf('tab=whitelabel') > -1;
1131
1132 menu.textContent = '';
1133
1134 var hideAdvanced = <?php
1135 echo !Metasync_Access_Control::user_can_access('hide_advanced') ? 'true' : 'false';
1136 ?>;
1137 var showGeneral = <?php
1138 echo Metasync_Access_Control::user_can_access('hide_settings') ? 'true' : 'false';
1139 ?>;
1140
1141 if (showGeneral) {
1142 const generalLink = document.createElement('a');
1143 generalLink.href = '?page=<?php echo esc_js( $page_slug ); ?>&tab=general';
1144 generalLink.className = 'metasync-portal-item' + (isGeneralActive ? ' active' : '');
1145 generalLink.textContent = 'General';
1146 menu.appendChild(generalLink);
1147 }
1148
1149 const whitelabelLink = document.createElement('a');
1150 whitelabelLink.href = '?page=<?php echo esc_js( $page_slug ); ?>&tab=whitelabel';
1151 whitelabelLink.className = 'metasync-portal-item' + (isWhitelabelActive ? ' active' : '');
1152 whitelabelLink.textContent = 'White label';
1153 menu.appendChild(whitelabelLink);
1154
1155 if (!hideAdvanced) {
1156 const advancedLink = document.createElement('a');
1157 advancedLink.href = '?page=<?php echo esc_js( $page_slug ); ?>&tab=advanced';
1158 advancedLink.className = 'metasync-portal-item' + (isAdvancedActive ? ' active' : '');
1159 advancedLink.textContent = 'Advanced';
1160 menu.appendChild(advancedLink);
1161 }
1162
1163
1164 var rect = button.getBoundingClientRect();
1165 menu.style.position = 'fixed';
1166 menu.style.top = (rect.bottom + 8) + 'px';
1167 menu.style.right = (window.innerWidth - rect.right) + 'px';
1168 menu.style.zIndex = '999999999';
1169
1170 document.body.appendChild(menu);
1171
1172 button.classList.add('active');
1173 button.setAttribute('aria-expanded', 'true');
1174 }
1175
1176 document.addEventListener('click', function(event) {
1177 var button = document.getElementById('metasync-settings-btn');
1178 var menu = document.getElementById('metasync-portal-menu');
1179
1180 if (menu && button && !button.contains(event.target) && !menu.contains(event.target)) {
1181 menu.remove();
1182 button.classList.remove('active');
1183 button.setAttribute('aria-expanded', 'false');
1184 }
1185 });
1186 </script>
1187 <?php
1188 }
1189
1190 /**
1191 * Render the plugin header with logo, theme toggle, and connection badge.
1192 */
1193 public function render_plugin_header($page_title = null)
1194 {
1195 $general_settings = Metasync::get_option('general');
1196
1197 $effective_plugin_name = Metasync::get_effective_plugin_name();
1198 $display_title = $page_title ?: $effective_plugin_name;
1199 $logo = $this->resolve_logo_data();
1200
1201 $searchatlas_api_key = isset($general_settings['searchatlas_api_key']) ? $general_settings['searchatlas_api_key'] : '';
1202 $otto_pixel_uuid = isset($general_settings['otto_pixel_uuid']) ? $general_settings['otto_pixel_uuid'] : '';
1203
1204 $is_integrated = Metasync_Heartbeat_Manager::instance()->is_heartbeat_connected($general_settings);
1205
1206 $current_theme = get_option('metasync_theme', 'dark');
1207 ?>
1208
1209 <!-- Plugin Header with Logo -->
1210 <div class="metasync-header" data-current-theme="<?php echo esc_attr($current_theme); ?>">
1211 <div class="metasync-header-left">
1212 <?php if ($logo['show_logo'] && $logo['use_dual']): ?>
1213 <div class="metasync-logo-container">
1214 <img src="<?php echo esc_url($logo['light_url']); ?>" alt="Logo" class="metasync-logo metasync-logo-light" />
1215 <img src="<?php echo esc_url($logo['dark_url']); ?>" alt="Logo" class="metasync-logo metasync-logo-dark" />
1216 </div>
1217 <?php elseif ($logo['show_logo'] && !empty($logo['url'])): ?>
1218 <div class="metasync-logo-container">
1219 <img src="<?php echo esc_url($logo['url']); ?>" alt="Logo" class="metasync-logo<?php echo !empty($logo['is_default']) ? ' metasync-logo-default' : ''; ?>" />
1220 </div>
1221 <?php endif; ?>
1222 </div>
1223
1224 <div class="metasync-header-right">
1225 <!-- Theme Toggle -->
1226 <div class="metasync-theme-toggle" role="group" aria-label="Theme Selector">
1227 <button class="metasync-theme-option <?php echo ($current_theme === 'light') ? 'active' : ''; ?>" data-theme="light" aria-label="Light Theme" type="button">
1228 <span class="metasync-theme-icon">&#9728;</span>
1229 <span class="theme-label">Light</span>
1230 </button>
1231 <button class="metasync-theme-option <?php echo ($current_theme === 'dark') ? 'active' : ''; ?>" data-theme="dark" aria-label="Dark Theme" type="button">
1232 <span class="metasync-theme-icon">&#9790;</span>
1233 <span class="theme-label">Dark</span>
1234 </button>
1235 </div>
1236
1237 <!-- Integration Status -->
1238 <?php
1239 if ($is_integrated && !empty($otto_pixel_uuid)) {
1240 $status_class_header = 'integrated';
1241 $status_title_header = 'Synced - Heartbeat API connectivity verified';
1242 $status_text_header = 'Synced';
1243 } elseif ($is_integrated && empty($otto_pixel_uuid)) {
1244 $status_class_header = 'warning';
1245 $status_title_header = 'Connected but OTTO UUID is missing — deploys will not work. Please reconnect.';
1246 $status_text_header = 'Warning';
1247 } else {
1248 $status_class_header = 'not-integrated';
1249 $status_title_header = 'Not Synced - Heartbeat API not responding or unreachable';
1250 $status_text_header = 'Not Synced';
1251 }
1252 ?>
1253 <div class="metasync-integration-status <?php echo $status_class_header; ?>"
1254 title="<?php echo esc_attr($status_title_header); ?>">
1255 <span class="status-indicator"></span>
1256 <span class="status-text"><?php echo esc_html($status_text_header); ?></span>
1257 </div>
1258 </div>
1259 </div>
1260
1261 <!-- Page Title Below Header -->
1262 <div class="metasync-page-title">
1263 <h1><?php echo esc_html($display_title); ?></h1>
1264 </div>
1265
1266 <?php
1267 }
1268
1269 // ------------------------------------------------------------------
1270 // Admin bar status indicator
1271 // ------------------------------------------------------------------
1272
1273 /**
1274 * Output inline CSS (and optional JS) for the admin-bar status node.
1275 */
1276 public function metasync_admin_bar_style()
1277 {
1278 if (!is_admin_bar_showing()) {
1279 return;
1280 }
1281
1282 # For backward compatibility, constant takes precedence.
1283 if (defined('METASYNC_SHOW_ADMIN_BAR_STATUS') && !METASYNC_SHOW_ADMIN_BAR_STATUS) {
1284 return;
1285 }
1286
1287
1288 # Check if admin bar status is enabled via setting
1289 $general_settings = Metasync::get_option('general');
1290 $show_admin_bar = $general_settings['show_admin_bar_status'] ?? true;
1291 if (!$show_admin_bar) {
1292 return;
1293 }
1294 ?>
1295 <style type="text/css">
1296 #wp-admin-bar-searchatlas-status .ab-item {
1297 font-weight: 500 !important;
1298 transition: all 0.2s ease !important;
1299 }
1300
1301 #wp-admin-bar-searchatlas-status:hover .ab-item {
1302 background-color: rgba(255, 255, 255, 0.1) !important;
1303 }
1304
1305 #wp-admin-bar-searchatlas-status.searchatlas-synced .ab-item {
1306 color: #46b450 !important; /* WordPress green for text */
1307 }
1308
1309 #wp-admin-bar-searchatlas-status.searchatlas-not-synced .ab-item {
1310 color: #dc3232 !important; /* WordPress red for text */
1311 }
1312
1313 #wp-admin-bar-searchatlas-status.searchatlas-warning .ab-item {
1314 color: #ffb900 !important; /* WordPress yellow for warning */
1315 }
1316
1317 /* Ensure emojis maintain their natural colors */
1318 #wp-admin-bar-searchatlas-status .ab-item {
1319 filter: none !important;
1320 }
1321
1322 /* Make status visible but subtle */
1323 #wp-admin-bar-searchatlas-status .ab-item {
1324 opacity: 0.9;
1325 }
1326
1327 #wp-admin-bar-searchatlas-status:hover .ab-item {
1328 opacity: 1;
1329 }
1330 </style>
1331
1332 <?php
1333 $page_slug = Metasync_Admin::$page_slug;
1334 $is_plugin_page = (
1335 (isset($_GET['page']) && strpos($_GET['page'], $page_slug) !== false) ||
1336 (isset($_GET['page']) && strpos($_GET['page'], 'searchatlas') !== false)
1337 );
1338 if ($is_plugin_page):
1339 ?>
1340 <script type="text/javascript">
1341 // Pass PHP variables to JavaScript
1342 window.MetasyncConfig = {
1343 pluginName: '<?php echo esc_js(Metasync::get_effective_plugin_name()); ?>',
1344 ottoName: '<?php echo esc_js(Metasync::get_whitelabel_otto_name()); ?>'
1345 };
1346
1347 jQuery(document).ready(function($) {
1348
1349 // Function to sync admin bar status
1350 function syncAdminBarStatus() {
1351 var pluginPageStatus = $('.metasync-integration-status .status-text').text();
1352 var adminBarItem = $('#wp-admin-bar-searchatlas-status .ab-item');
1353 var adminBarContainer = $('#wp-admin-bar-searchatlas-status');
1354 var pluginName = window.MetasyncConfig.pluginName;
1355
1356 if (pluginPageStatus && adminBarItem.length) {
1357 var allClasses = 'searchatlas-synced searchatlas-not-synced searchatlas-warning';
1358
1359 // Helper to update emoji in admin bar
1360 function updateAdminBarEmoji(targetEmoji, targetSvgCode) {
1361 var emojiImg = adminBarItem.find('img.emoji');
1362 if (emojiImg.length > 0) {
1363 emojiImg.attr('alt', targetEmoji);
1364 var currentSrc = emojiImg.attr('src');
1365 var updatedSrc = currentSrc.replace(/1f7e2\.svg|1f534\.svg|1f7e1\.svg/, targetSvgCode + '.svg');
1366 emojiImg.attr('src', updatedSrc);
1367 } else {
1368 var newHtml = adminBarItem.html().replace(/🟢|🔴|🟡/, targetEmoji);
1369 if (!newHtml.includes(targetEmoji) && newHtml.includes(pluginName)) {
1370 newHtml = newHtml.replace(pluginName, pluginName + ' ' + targetEmoji);
1371 }
1372 adminBarItem.html(newHtml);
1373 }
1374 }
1375
1376 if (pluginPageStatus.includes('Synced') && !pluginPageStatus.includes('Not Synced')) {
1377 // Update admin bar to synced (GREEN)
1378 updateAdminBarEmoji('🟢', '1f7e2');
1379 adminBarContainer.removeClass(allClasses).addClass('searchatlas-synced');
1380 var syncTitle = pluginName + ' - Synced (Heartbeat API connectivity verified)';
1381 adminBarContainer.attr('title', syncTitle);
1382 adminBarItem.attr('title', syncTitle);
1383
1384 } else if (pluginPageStatus.includes('Warning')) {
1385 // Update admin bar to warning (YELLOW)
1386 updateAdminBarEmoji('🟡', '1f7e1');
1387 adminBarContainer.removeClass(allClasses).addClass('searchatlas-warning');
1388 var warnTitle = pluginName + ' - Connected but OTTO UUID is missing — deploys will not work. Please reconnect.';
1389 adminBarContainer.attr('title', warnTitle);
1390 adminBarItem.attr('title', warnTitle);
1391
1392 } else if (pluginPageStatus.includes('Not Synced')) {
1393 // Update admin bar to not synced (RED)
1394 updateAdminBarEmoji('🔴', '1f534');
1395 adminBarContainer.removeClass(allClasses).addClass('searchatlas-not-synced');
1396 var notSyncTitle = pluginName + ' - Not Synced (Heartbeat API not responding or unreachable)';
1397 adminBarContainer.attr('title', notSyncTitle);
1398 adminBarItem.attr('title', notSyncTitle);
1399 }
1400 }
1401 }
1402
1403 // Sync when tabs are switched (for General/Advanced tabs)
1404 $(document).on('click', 'a[href*="tab="]', function() {
1405 setTimeout(syncAdminBarStatus, 200);
1406 });
1407
1408 // Also check every 5 seconds to keep it in sync
1409 setInterval(syncAdminBarStatus, 5000);
1410 });
1411 </script>
1412 <?php endif; ?>
1413 <?php
1414 }
1415
1416 /**
1417 * Add Search Atlas status indicator to WordPress admin bar.
1418 */
1419 public function add_searchatlas_admin_bar_status($wp_admin_bar)
1420 {
1421 if (!Metasync::current_user_has_plugin_access()) {
1422 return;
1423 }
1424
1425 # For backward compatibility, constant takes precedence.
1426 if (defined('METASYNC_SHOW_ADMIN_BAR_STATUS') && !METASYNC_SHOW_ADMIN_BAR_STATUS) {
1427 return;
1428 }
1429
1430 # Check if admin bar status is disabled via setting
1431 $general_settings = Metasync::get_option('general');
1432 if (!is_array($general_settings)) {
1433 $general_settings = [];
1434 }
1435 $show_admin_bar = $general_settings['show_admin_bar_status'] ?? true;
1436 if (!$show_admin_bar) {
1437 return;
1438 }
1439
1440 if (!is_admin() && !apply_filters('metasync_show_admin_bar_status_frontend', false)) {
1441 return;
1442 }
1443
1444 $node = get_transient(self::CACHE_KEY);
1445
1446 if ($node === false) {
1447 $is_synced = Metasync_Heartbeat_Manager::instance()->is_heartbeat_connected($general_settings);
1448
1449 $plugin_name = Metasync::get_effective_plugin_name();
1450
1451 if ($is_synced) {
1452 $otto_uuid = $general_settings['otto_pixel_uuid'] ?? '';
1453 if (!empty($otto_uuid)) {
1454 $status_emoji = '🟢'; // Green circle for synced
1455 $title = $plugin_name . ' - Synced (Heartbeat API connectivity verified)';
1456 $status_class = 'searchatlas-synced';
1457 } else {
1458 $status_emoji = '🟡'; // Yellow circle for warning
1459 $title = $plugin_name . ' - Connected but OTTO UUID is missing — deploys will not work. Please reconnect.';
1460 $status_class = 'searchatlas-warning';
1461 }
1462 } else {
1463 $status_emoji = '🔴'; // Red circle for not synced
1464 $title = $plugin_name . ' - Not Synced (Heartbeat API not responding or unreachable)';
1465 $status_class = 'searchatlas-not-synced';
1466 }
1467
1468 $admin_bar_title = $plugin_name . ' ' . $status_emoji;
1469
1470 $node = array(
1471 'title' => $admin_bar_title,
1472 'href' => admin_url('admin.php?page=' . Metasync_Admin::$page_slug),
1473 'meta_title' => $title,
1474 'meta_class' => $status_class,
1475 );
1476
1477 set_transient(self::CACHE_KEY, $node, apply_filters('metasync_admin_bar_status_cache_ttl', 5 * MINUTE_IN_SECONDS));
1478 }
1479
1480 $wp_admin_bar->add_node(array(
1481 'id' => 'searchatlas-status',
1482 'title' => $node['title'],
1483 'href' => $node['href'],
1484 'meta' => array(
1485 'title' => $node['meta_title'],
1486 'class' => $node['meta_class'],
1487 ),
1488 ));
1489 }
1490
1491 /**
1492 * Invalidate the admin bar status cache so the next page load recomputes status.
1493 */
1494 public static function invalidate_admin_bar_status_cache()
1495 {
1496 delete_transient(self::CACHE_KEY);
1497 }
1498
1499 // ------------------------------------------------------------------
1500 // Yoast-style 3-column layout helpers
1501 // ------------------------------------------------------------------
1502
1503 /**
1504 * Open the 3-column page layout.
1505 * Call this at the start of every admin page callback instead of
1506 * render_plugin_header() + render_navigation_menu().
1507 * Close with render_layout_close().
1508 *
1509 * @param string $page_title Human-readable page title.
1510 * @param string $current_page Key matching get_available_menu_items() (e.g. 'general').
1511 * @param string $description Optional subtitle shown below the title.
1512 */
1513 public function render_layout_open($page_title = '', $current_page = '', $description = '')
1514 {
1515 $theme = esc_attr(get_option('metasync_theme', 'dark'));
1516 $plugin_name = Metasync::get_effective_plugin_name();
1517 $general = Metasync::get_option('general') ?? [];
1518 $is_connected = Metasync_Heartbeat_Manager::instance()->is_heartbeat_connected($general);
1519 // badge reflects confirmed heartbeat health (connected / stale /
1520 // disconnected), not raw API-key presence.
1521 $badge = Metasync_Heartbeat_Manager::instance()->get_connection_badge($general);
1522 $logo = $this->resolve_logo_data();
1523
1524 $current_theme = get_option('metasync_theme', 'dark');
1525 ?>
1526 <div class="wrap metasync-dashboard-wrap" data-theme="<?php echo $theme; ?>">
1527
1528 <?php
1529 // Inject whitelabel color palette overrides
1530 $wl_settings = Metasync::get_whitelabel_settings();
1531 $wl_palette = isset($wl_settings['color_palette']) && is_array($wl_settings['color_palette']) ? $wl_settings['color_palette'] : array();
1532 if (!empty($wl_palette)) {
1533 echo '<style id="metasync-whitelabel-palette">';
1534 foreach (array('dark', 'light') as $t) {
1535 if (!empty($wl_palette[$t]) && is_array($wl_palette[$t])) {
1536 $selector = ($t === 'dark') ? ':root, [data-theme="dark"]' : '[data-theme="light"]';
1537 $vars = '';
1538 foreach ($wl_palette[$t] as $var_name => $color) {
1539 // Skip gradient partial keys — handled below
1540 if (strpos($var_name, 'gradient-') !== false) continue;
1541 if (preg_match('/^dashboard-[a-z-]+$/', $var_name) && preg_match('/^#[0-9a-fA-F]{3,8}$/', $color)) {
1542 $vars .= '--' . esc_attr($var_name) . ':' . esc_attr($color) . ';';
1543 }
1544 }
1545 // Build gradient overrides from paired from/to colors
1546 $gradient_pairs = array(
1547 'dashboard-gradient-primary' => array('dashboard-gradient-primary-from', 'dashboard-gradient-primary-to'),
1548 'dashboard-gradient-accent' => array('dashboard-gradient-accent-from', 'dashboard-gradient-accent-to'),
1549 );
1550 foreach ($gradient_pairs as $grad_var => $pair) {
1551 $from = isset($wl_palette[$t][$pair[0]]) ? $wl_palette[$t][$pair[0]] : '';
1552 $to = isset($wl_palette[$t][$pair[1]]) ? $wl_palette[$t][$pair[1]] : '';
1553 if ($from && $to && preg_match('/^#[0-9a-fA-F]{3,8}$/', $from) && preg_match('/^#[0-9a-fA-F]{3,8}$/', $to)) {
1554 $vars .= '--' . esc_attr($grad_var) . ':linear-gradient(135deg,' . esc_attr($from) . ' 0%,' . esc_attr($to) . ' 100%);';
1555 }
1556 }
1557 if ($vars) {
1558 echo $selector . '{' . $vars . '}';
1559 }
1560 }
1561 }
1562 echo '</style>';
1563 }
1564 ?>
1565
1566 <!-- Compact top header -->
1567 <div class="metasync-header-compact">
1568 <div style="display:flex;align-items:center;gap:10px;">
1569 <?php if ($logo['show_logo'] && $logo['use_dual']): ?>
1570 <img src="<?php echo esc_url($logo['light_url']); ?>" alt="<?php echo esc_attr($plugin_name); ?>" class="metasync-logo metasync-logo-light" style="height:28px;width:auto;">
1571 <img src="<?php echo esc_url($logo['dark_url']); ?>" alt="<?php echo esc_attr($plugin_name); ?>" class="metasync-logo metasync-logo-dark" style="height:28px;width:auto;">
1572 <?php elseif ($logo['show_logo'] && $logo['url']): ?>
1573 <img src="<?php echo esc_url($logo['url']); ?>" alt="<?php echo esc_attr($plugin_name); ?>" class="metasync-logo<?php echo !empty($logo['is_default']) ? ' metasync-logo-default' : ''; ?>" style="height:28px;width:auto;">
1574 <?php else: ?>
1575 <strong style="font-size:15px;color:var(--dashboard-text-primary);"><?php echo esc_html($plugin_name); ?></strong>
1576 <?php endif; ?>
1577 </div>
1578 <div class="metasync-header-compact-right">
1579 <div class="metasync-status <?php echo esc_attr($badge['class']); ?>">
1580 <span class="status-dot"></span>
1581 <span class="status-text"><?php echo esc_html($badge['text']); ?></span>
1582 </div>
1583 <button type="button" class="metasync-theme-toggle" onclick="toggleMetasyncTheme()" title="Toggle theme">
1584 <span class="theme-icon-light">&#9728;</span>
1585 <span class="theme-icon-dark">&#9790;</span>
1586 </button>
1587 </div>
1588 </div>
1589
1590 <!-- 3-column layout -->
1591 <div class="metasync-layout">
1592
1593 <!-- Left sidenav -->
1594 <aside class="metasync-layout-nav">
1595 <?php $this->render_sidenav($current_page); ?>
1596 </aside>
1597
1598 <!-- Main content -->
1599 <main class="metasync-layout-main">
1600 <?php
1601 // Anchor for WP core's notice relocation (common.js). Without it, core
1602 // moves admin notices after the first .wrap h1 only on DOM-ready, after
1603 // the server has already painted them at the top of #wpbody-content —
1604 // causing a flash above the header and layout shift. Providing
1605 // an explicit .wp-header-end inside this (FOUC-hidden) wrap makes core
1606 // relocate notices here deterministically, so they fade in with the page.
1607 ?>
1608 <hr class="wp-header-end">
1609 <?php if ($page_title || $description): ?>
1610 <div class="metasync-page-header">
1611 <?php if ($page_title): ?>
1612 <h1><?php echo esc_html($page_title); ?></h1>
1613 <?php endif; ?>
1614 <?php if ($description): ?>
1615 <p><?php echo esc_html($description); ?></p>
1616 <?php endif; ?>
1617 </div>
1618 <?php endif; ?>
1619 <?php
1620 // Note: render_layout_close() closes </main>, renders promo sidebar, closes </div>.metasync-layout and </div>.wrap
1621 }
1622
1623 /**
1624 * Close the 3-column layout opened by render_layout_open().
1625 *
1626 * @param bool $show_promo Whether to render the right promo sidebar. Default true.
1627 * Pass false for full-width pages (e.g. dashboard iframe).
1628 */
1629 public function render_layout_close($show_promo = true)
1630 {
1631 ?>
1632 </main><!-- /.metasync-layout-main -->
1633
1634 <?php if ($show_promo):
1635 $general = Metasync::get_option('general') ?? [];
1636 $is_connected = Metasync_Heartbeat_Manager::instance()->is_heartbeat_connected($general);
1637 ?>
1638 <!-- Right promo sidebar -->
1639 <aside class="metasync-layout-promo">
1640 <?php $this->render_promo_sidebar($is_connected); ?>
1641 </aside>
1642 <?php endif; ?>
1643
1644 </div><!-- /.metasync-layout -->
1645 </div><!-- /.metasync-dashboard-wrap -->
1646 <?php
1647 }
1648
1649 /**
1650 * Render the left sticky sidenav with grouped items.
1651 *
1652 * @param string $current_page Active page key.
1653 */
1654 public function render_sidenav($current_page = '')
1655 {
1656 $page_slug = Metasync_Admin::$page_slug;
1657 $menu_items = $this->get_available_menu_items();
1658
1659 // Dashicons names (without 'dashicons-' prefix) — monochrome, color set by CSS
1660 $icons = [
1661 'dashboard' => 'dashboard',
1662 'seo_controls' => 'search',
1663 'monitor_404' => 'warning',
1664 'redirections' => 'undo',
1665 'xml_sitemap' => 'networking',
1666 'robots_txt' => 'shield',
1667 'site_verification'=> 'yes-alt',
1668 'instant_index' => 'performance',
1669 'google_console' => 'chart-area',
1670 'bing_console' => 'chart-bar',
1671 'import_seo' => 'download',
1672 'general' => 'admin-settings',
1673 'schema_markup' => 'tag',
1674 'local_business' => 'building',
1675 'breadcrumbs' => 'menu',
1676 'code_snippets' => 'editor-code',
1677 'code_minification'=> 'media-code',
1678 'custom_pages' => 'admin-page',
1679 'compatibility' => 'admin-tools',
1680 'sync_log' => 'list-view',
1681 'bot_statistics' => 'visibility',
1682 'report_issue' => 'sos',
1683 'media_optimization'=> 'images-alt2',
1684 'seo_health' => 'heart'
1685 ];
1686
1687 $seo_items = [];
1688 $plugin_items = [];
1689 foreach ($menu_items as $key => $item) {
1690 if (($item['group'] ?? 'plugin') === 'seo') {
1691 $seo_items[$key] = $item;
1692 } else {
1693 $plugin_items[$key] = $item;
1694 }
1695 }
1696 ?>
1697 <nav class="metasync-sidenav">
1698
1699 <!-- SEO Features group -->
1700 <div class="metasync-sidenav-group">
1701 <div class="metasync-sidenav-group-title">SEO Features</div>
1702 <ul>
1703 <?php foreach ($seo_items as $key => $item):
1704 $is_active = ($current_page === $key);
1705 $icon = $icons[$key] ?? 'admin-generic';
1706 $url = esc_url(admin_url('admin.php?page=' . $page_slug . $item['slug_suffix']));
1707 ?>
1708 <li class="<?php echo $is_active ? 'metasync-sidenav-active' : ''; ?>">
1709 <a href="<?php echo $url; ?>">
1710 <span class="metasync-sidenav-icon"><span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span></span>
1711 <?php echo esc_html($item['title']); ?>
1712 </a>
1713 </li>
1714 <?php endforeach; ?>
1715 </ul>
1716 </div>
1717
1718 <!-- Plugin group -->
1719 <div class="metasync-sidenav-group">
1720 <div class="metasync-sidenav-group-title">Plugin</div>
1721 <ul>
1722 <?php foreach ($plugin_items as $key => $item):
1723 if ($key === 'report_issue') continue;
1724 $is_active = ($current_page === $key);
1725 $icon = $icons[$key] ?? 'admin-generic';
1726 $url = esc_url(admin_url('admin.php?page=' . $page_slug . $item['slug_suffix']));
1727 ?>
1728 <li class="<?php echo $is_active ? 'metasync-sidenav-active' : ''; ?>">
1729 <a href="<?php echo $url; ?>">
1730 <span class="metasync-sidenav-icon"><span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span></span>
1731 <?php echo esc_html($item['title']); ?>
1732 </a>
1733 </li>
1734 <?php if ($key === 'general'): ?>
1735 <li class="<?php echo $current_page === 'advanced_settings' ? 'metasync-sidenav-active' : ''; ?>" style="padding-left: 8px;">
1736 <a href="<?php echo esc_url(admin_url('admin.php?page=' . $page_slug . '&tab=advanced')); ?>">
1737 <span class="metasync-sidenav-icon"><span class="dashicons dashicons-admin-generic"></span></span>
1738 Advanced Settings
1739 </a>
1740 </li>
1741 <li class="<?php echo $current_page === 'whitelabel' ? 'metasync-sidenav-active' : ''; ?>" style="padding-left: 8px;">
1742 <a href="<?php echo esc_url(admin_url('admin.php?page=' . $page_slug . '&tab=whitelabel')); ?>">
1743 <span class="metasync-sidenav-icon"><span class="dashicons dashicons-tag"></span></span>
1744 Whitelabel
1745 </a>
1746 </li>
1747 <?php endif; ?>
1748 <?php endforeach; ?>
1749 </ul>
1750 </div>
1751
1752 <!-- Report Issue at the bottom -->
1753 <?php if (isset($menu_items['report_issue'])): ?>
1754 <div class="metasync-sidenav-group">
1755 <ul>
1756 <li class="<?php echo $current_page === 'report_issue' ? 'metasync-sidenav-active' : ''; ?>">
1757 <a href="<?php echo esc_url(admin_url('admin.php?page=' . $page_slug . $menu_items['report_issue']['slug_suffix'])); ?>">
1758 <span class="metasync-sidenav-icon"><span class="dashicons dashicons-sos"></span></span>
1759 Report Issue
1760 </a>
1761 </li>
1762 </ul>
1763 </div>
1764 <?php endif; ?>
1765
1766 </nav>
1767 <?php
1768 }
1769
1770 /**
1771 * Render the right promotional sidebar.
1772 *
1773 * @param bool $is_connected Whether the plugin is authenticated.
1774 */
1775 public function render_promo_sidebar($is_connected = false)
1776 {
1777 $plugin_name = Metasync::get_effective_plugin_name();
1778 $otto_name = Metasync::get_whitelabel_otto_name();
1779 $settings_url = esc_url(admin_url('admin.php?page=' . Metasync_Admin::$page_slug));
1780 $homepage = Metasync::HOMEPAGE_DOMAIN;
1781 $is_default_brand = ( $plugin_name === 'Search Atlas' );
1782 $whitelabel = Metasync::get_whitelabel_settings();
1783 $custom_links = isset($whitelabel['quick_links']) && is_array($whitelabel['quick_links'])
1784 ? array_filter($whitelabel['quick_links'], function($l) { return !empty($l['url']); })
1785 : [];
1786 ?>
1787
1788 <?php if (!$is_connected): ?>
1789 <!-- Connect CTA card -->
1790 <div class="metasync-promo-card metasync-promo-card--connect">
1791 <div class="metasync-promo-card-header">
1792 <div class="metasync-promo-card-icon">
1793 <span class="dashicons dashicons-admin-links"></span>
1794 </div>
1795 <div>
1796 <h3>Connect <?php echo esc_html($plugin_name); ?></h3>
1797 </div>
1798 </div>
1799 <p class="metasync-promo-tagline">Link your site to <?php echo esc_html($plugin_name); ?> to unlock <?php echo esc_html($otto_name); ?>, keyword data, and automated SEO.</p>
1800 <ul class="metasync-promo-benefits">
1801 <li><span class="promo-check">&#10003;</span> <?php echo esc_html($otto_name); ?> hands-free on-page SEO</li>
1802 <li><span class="promo-check">&#10003;</span> Real-time keyword tracking</li>
1803 <li><span class="promo-check">&#10003;</span> Automated schema markup</li>
1804 <li><span class="promo-check">&#10003;</span> Instant Google indexing</li>
1805 </ul>
1806 <a href="<?php echo $settings_url; ?>" class="metasync-promo-btn metasync-promo-btn--primary">
1807 Connect Now
1808 </a>
1809 </div>
1810 <?php else: ?>
1811 <!-- Connected feature highlights -->
1812 <div class="metasync-promo-card metasync-promo-card--accent">
1813 <div class="metasync-promo-card-header">
1814 <div class="metasync-promo-card-icon">
1815 <span class="dashicons dashicons-performance"></span>
1816 </div>
1817 <div>
1818 <h3><?php echo esc_html($otto_name); ?> Active</h3>
1819 </div>
1820 </div>
1821 <p class="metasync-promo-tagline">Your site is connected and <?php echo esc_html($otto_name); ?> is optimizing pages automatically.</p>
1822 <ul class="metasync-promo-benefits">
1823 <li><span class="promo-check">&#10003;</span> Schema markup auto-applied</li>
1824 <li><span class="promo-check">&#10003;</span> Meta titles &amp; descriptions optimized</li>
1825 <li><span class="promo-check">&#10003;</span> Internal linking suggestions active</li>
1826 </ul>
1827 <?php if ($is_default_brand): ?>
1828 <a href="<?php echo esc_url($homepage); ?>" target="_blank" rel="noopener" class="metasync-promo-btn metasync-promo-btn--outline">
1829 View <?php echo esc_html($plugin_name); ?> Dashboard
1830 </a>
1831 <?php elseif (!empty($whitelabel['domain'])): ?>
1832 <a href="<?php echo esc_url($whitelabel['domain']); ?>" target="_blank" rel="noopener" class="metasync-promo-btn metasync-promo-btn--outline">
1833 View <?php echo esc_html($plugin_name); ?> Dashboard
1834 </a>
1835 <?php endif; ?>
1836 </div>
1837 <?php endif; ?>
1838
1839 <?php
1840 // Quick Links: show default links for default brand, custom links if whitelabeled + provided, hide if whitelabeled + none
1841 $show_quick_links = $is_default_brand || !empty($custom_links);
1842 if ($show_quick_links):
1843 ?>
1844 <!-- Quick links card -->
1845 <div class="metasync-promo-card">
1846 <h3 style="margin:0 0 12px;font-size:13px;font-weight:700;color:var(--dashboard-text-primary);">Quick Links</h3>
1847 <ul class="metasync-promo-links">
1848 <?php if ($is_default_brand): ?>
1849 <li><a href="<?php echo esc_url($homepage . '/blog/'); ?>" target="_blank" rel="noopener"><span class="dashicons dashicons-rss"></span> SEO Blog</a></li>
1850 <li><a href="<?php echo esc_url($homepage . '/academy/'); ?>" target="_blank" rel="noopener"><span class="dashicons dashicons-welcome-learn-more"></span> SEO Academy</a></li>
1851 <li><a href="<?php echo esc_url(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-setup-wizard')); ?>"><span class="dashicons dashicons-admin-customizer"></span> Setup Wizard</a></li>
1852 <li><a href="<?php echo esc_url(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-report-issue')); ?>"><span class="dashicons dashicons-sos"></span> Report Issue</a></li>
1853 <?php else: ?>
1854 <?php foreach ($custom_links as $link): ?>
1855 <li><a href="<?php echo esc_url($link['url']); ?>" <?php echo !empty($link['external']) ? 'target="_blank" rel="noopener"' : ''; ?>><span class="dashicons dashicons-admin-links"></span> <?php echo esc_html($link['label'] ?: $link['url']); ?></a></li>
1856 <?php endforeach; ?>
1857 <?php endif; ?>
1858 </ul>
1859 </div>
1860 <?php endif; ?>
1861
1862 <?php
1863 }
1864 }
1865