PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.14
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.14
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.14, at includes/class-metasync-admin-navigation.php

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