PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.1.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.1.0
2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.10.0 All 48 releases
thinkrank / includes / admin / class-manager.php

class-manager.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 1.1.0, at includes/admin/class-manager.php

773 lines 24.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Admin Manager Class
4 *
5 * Handles WordPress admin interface integration
6 *
7 * @package ThinkRank\Admin
8 * @since 1.0.0
9 */
10
11 declare(strict_types=1);
12
13 namespace ThinkRank\Admin;
14
15 use ThinkRank\Core\Settings;
16 use ThinkRank\Core\Database;
17 use ThinkRank\Admin\Metabox_Manager;
18
19 // Prevent direct access
20 if (!defined('ABSPATH')) {
21 exit;
22 }
23
24 /**
25 * Admin Manager Class
26 *
27 * Single Responsibility: Manage WordPress admin interface
28 *
29 * @since 1.0.0
30 */
31 class Manager {
32
33 /**
34 * Settings instance
35 *
36 * @var Settings
37 */
38 private Settings $settings;
39
40 /**
41 * Database instance
42 *
43 * @var Database
44 */
45 private Database $database;
46
47 /**
48 * Metabox manager instance
49 *
50 * @var Metabox_Manager
51 */
52 private Metabox_Manager $metabox_manager;
53
54
55
56 /**
57 * Admin pages
58 *
59 * @var array
60 */
61 private array $pages = [];
62
63 /**
64 * Constructor
65 *
66 * @param Settings $settings Settings instance
67 * @param Database $database Database instance
68 */
69 public function __construct(Settings $settings = null, Database $database = null) {
70 $this->settings = $settings ?? new Settings();
71 $this->database = $database ?? new Database();
72 $this->metabox_manager = new Metabox_Manager($this->settings);
73 }
74
75 /**
76 * Initialize admin interface
77 *
78 * @return void
79 */
80 public function init(): void {
81 add_action('admin_menu', [$this, 'add_admin_menu']);
82 add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_scripts']);
83 add_action('admin_init', [$this, 'handle_admin_init']);
84 add_action('admin_notices', [$this, 'show_admin_notices']);
85
86 // AJAX handlers
87 add_action('wp_ajax_thinkrank_dismiss_notice', [$this, 'dismiss_notice']);
88
89 // Initialize metabox manager
90 $this->metabox_manager->init();
91 }
92
93 /**
94 * Add admin menu pages
95 *
96 * @return void
97 */
98 public function add_admin_menu(): void {
99 // Main menu page
100 $this->pages['dashboard'] = add_menu_page(
101 __('ThinkRank', 'thinkrank'),
102 __('ThinkRank', 'thinkrank'),
103 'manage_options',
104 'thinkrank',
105 [$this, 'render_dashboard_page'],
106 $this->get_menu_icon(),
107 30
108 );
109
110 // Dashboard submenu (same as main)
111 $this->pages['dashboard_sub'] = add_submenu_page(
112 'thinkrank',
113 __('Dashboard', 'thinkrank'),
114 __('Dashboard', 'thinkrank'),
115 'manage_options',
116 'thinkrank',
117 [$this, 'render_dashboard_page']
118 );
119
120 // Essential SEO page (React tabbed interface)
121 $this->pages['essential_seo'] = add_submenu_page(
122 'thinkrank',
123 __('Essential SEO', 'thinkrank'),
124 __('Essential SEO', 'thinkrank'),
125 'manage_options',
126 'thinkrank-essential-seo',
127 [$this, 'render_essential_seo_page']
128 );
129
130 // AI Tools page
131 $this->pages['ai_tools'] = add_submenu_page(
132 'thinkrank',
133 __('AI Tools', 'thinkrank'),
134 __('AI Tools', 'thinkrank'),
135 'edit_posts',
136 'thinkrank-ai-tools',
137 [$this, 'render_ai_tools_page']
138 );
139
140 // Settings page
141 $this->pages['settings'] = add_submenu_page(
142 'thinkrank',
143 __('Settings', 'thinkrank'),
144 __('Settings', 'thinkrank'),
145 'manage_options',
146 'thinkrank-settings',
147 [$this, 'render_settings_page']
148 );
149
150 // Usage Analytics page
151 $this->pages['analytics'] = add_submenu_page(
152 'thinkrank',
153 __('Usage Analytics', 'thinkrank'),
154 __('Usage Analytics', 'thinkrank'),
155 'edit_posts',
156 'thinkrank-analytics',
157 [$this, 'render_analytics_page']
158 );
159
160 // Hook for page-specific initialization
161 foreach ($this->pages as $page_hook) {
162 add_action("load-{$page_hook}", [$this, 'load_admin_page']);
163 }
164 }
165
166 /**
167 * Enqueue admin scripts and styles
168 *
169 * APPROACH: Manual enqueuing with disabled webpack code splitting
170 * - All dependencies bundled into main admin.js (677KB)
171 * - Chart.js separated into charts.js (138KB) for performance
172 * - No dynamic chunks - predictable loading order
173 *
174 * @param string $hook_suffix Current admin page hook
175 * @return void
176 */
177 public function enqueue_admin_scripts(string $hook_suffix): void {
178 // Only load on our admin pages
179 if (!in_array($hook_suffix, $this->pages, true)) {
180 return;
181 }
182
183 // Get asset files for cache busting
184 $admin_asset_file = THINKRANK_PLUGIN_DIR . 'assets/admin.asset.php';
185 $admin_asset_data = file_exists($admin_asset_file) ? include $admin_asset_file : [
186 'dependencies' => [],
187 'version' => THINKRANK_VERSION,
188 ];
189
190 // Enqueue Chart.js bundle only on pages that render charts (Analytics and Essential SEO Performance)
191 $charts_asset_file = THINKRANK_PLUGIN_DIR . 'assets/charts.asset.php';
192 $should_enqueue_charts = in_array($hook_suffix, [
193 $this->pages['analytics'] ?? '',
194 $this->pages['essential_seo'] ?? '',
195 ], true);
196
197 if ($should_enqueue_charts && file_exists($charts_asset_file)) {
198 $charts_asset_data = include $charts_asset_file;
199 wp_enqueue_script(
200 'thinkrank-charts',
201 THINKRANK_PLUGIN_URL . 'assets/charts.js',
202 $charts_asset_data['dependencies'],
203 $charts_asset_data['version'],
204 true
205 );
206 // Add charts as dependency for admin script to ensure registration before use
207 $admin_dependencies = array_merge($admin_asset_data['dependencies'], ['thinkrank-charts']);
208 } else {
209 // Do not load charts on pages that don't need it
210 $admin_dependencies = $admin_asset_data['dependencies'];
211 }
212
213 wp_enqueue_script(
214 'thinkrank-admin',
215 THINKRANK_PLUGIN_URL . 'assets/admin.js',
216 $admin_dependencies,
217 $admin_asset_data['version'],
218 true
219 );
220
221 // Add defer attribute for better performance
222 wp_script_add_data('thinkrank-admin', 'defer', true);
223
224 // Enqueue admin styles
225 wp_enqueue_style(
226 'thinkrank-admin',
227 THINKRANK_PLUGIN_URL . 'assets/admin.css',
228 ['wp-components'],
229 $admin_asset_data['version']
230 );
231
232 // Enqueue WordPress media library for MediaPicker component
233 wp_enqueue_media();
234
235 // Localize script with data
236 wp_localize_script('thinkrank-admin', 'thinkrankAdmin', [
237 'apiUrl' => rest_url('thinkrank/v1/'),
238 'nonce' => wp_create_nonce('wp_rest'),
239 'restNonce' => wp_create_nonce('wp_rest'),
240 'adminNonce' => wp_create_nonce('thinkrank_admin'),
241 'currentUser' => wp_get_current_user()->ID,
242 'capabilities' => $this->get_user_capabilities(),
243 'settings' => $this->get_admin_settings(),
244 'i18n' => $this->get_i18n_strings(),
245 'isAdmin' => current_user_can('manage_options'),
246 // Plugin version - directly available without API call
247 'version' => THINKRANK_VERSION,
248 // Site information for default values
249 'siteName' => get_bloginfo('name'),
250 'siteDescription' => get_bloginfo('description'),
251 'siteUrl' => home_url(),
252 'adminEmail' => get_option('admin_email'),
253 // Post types for Global SEO navigation
254 'postTypes' => $this->get_public_post_types(),
255 ]);
256
257 // Add welcome notice dismissal script
258 if (get_option('thinkrank_show_welcome') && !$this->has_api_key_configured()) {
259 wp_add_inline_script('thinkrank-admin', '
260 jQuery(document).ready(function($) {
261 $(".thinkrank-dismiss-welcome").on("click", function(e) {
262 e.preventDefault();
263
264 $.post(ajaxurl, {
265 action: "thinkrank_dismiss_notice",
266 notice_type: "welcome",
267 nonce: thinkrankAdmin.adminNonce
268 }, function(response) {
269 $(".thinkrank-welcome-notice").fadeOut();
270 });
271 });
272 });
273 ');
274 }
275 }
276
277 /**
278 * Handle admin initialization
279 *
280 * @return void
281 */
282 public function handle_admin_init(): void {
283 // Show welcome screen for new installations (only if no API key configured)
284 if (get_option('thinkrank_show_welcome') && !$this->has_api_key_configured()) {
285 add_action('admin_notices', [$this, 'show_welcome_notice']);
286 }
287
288 // Check for plugin updates
289 $this->check_plugin_updates();
290 }
291
292 /**
293 * Load admin page
294 *
295 * @return void
296 */
297 public function load_admin_page(): void {
298 // Add help tabs
299 $this->add_help_tabs();
300
301 // Add screen options
302 $this->add_screen_options();
303 }
304
305 /**
306 * Render dashboard page
307 *
308 * @return void
309 */
310 public function render_dashboard_page(): void {
311 $this->render_admin_page('dashboard', [
312 'title' => __('ThinkRank Dashboard', 'thinkrank'),
313 'description' => __('AI-powered SEO optimization for WordPress', 'thinkrank'),
314 ]);
315 }
316
317 /**
318 * Render Essential SEO page
319 *
320 * @return void
321 */
322 public function render_essential_seo_page(): void {
323 $this->render_admin_page('essential-seo', [
324 'title' => __('Essential SEO', 'thinkrank'),
325 'description' => __('Configure your site-wide SEO settings with AI-powered optimization', 'thinkrank'),
326 ]);
327 }
328
329 /**
330 * Render AI Tools page
331 *
332 * @return void
333 */
334 public function render_ai_tools_page(): void {
335 $this->render_admin_page('ai-tools', [
336 'title' => __('AI Tools', 'thinkrank'),
337 'description' => __('AI-powered content tools including Content Planner and Metadata Generator', 'thinkrank'),
338 ]);
339 }
340
341 /**
342 * Render settings page
343 *
344 * @return void
345 */
346 public function render_settings_page(): void {
347 $this->render_admin_page('settings', [
348 'title' => __('ThinkRank Settings', 'thinkrank'),
349 'description' => __('Configure your AI SEO settings', 'thinkrank'),
350 ]);
351 }
352
353
354
355 /**
356 * Render usage analytics page
357 *
358 * @return void
359 */
360 public function render_analytics_page(): void {
361 $this->render_admin_page('analytics', [
362 'title' => __('Usage Analytics', 'thinkrank'),
363 'description' => __('Track your AI usage, costs, and plugin performance analytics', 'thinkrank'),
364 ]);
365 }
366
367 /**
368 * Render admin page template
369 *
370 * @param string $page Page identifier
371 * @param array $data Page data
372 * @return void
373 */
374 private function render_admin_page(string $page, array $data): void {
375 ?>
376 <div class="wrap">
377 <div id="thinkrank-<?php echo esc_attr($page); ?>" class="thinkrank-admin-page">
378 <div class="thinkrank-loading">
379 <div class="spinner is-active"></div>
380 <p><?php esc_html_e('Loading ThinkRank...', 'thinkrank'); ?></p>
381 </div>
382 </div>
383 </div>
384 <?php
385 }
386
387 /**
388 * Add meta boxes to post edit screens
389 *
390 * @return void
391 */
392 public function add_meta_boxes(): void {
393 $post_types = get_post_types(['public' => true]);
394
395 foreach ($post_types as $post_type) {
396 add_meta_box(
397 'thinkrank-seo',
398 __('ThinkRank SEO', 'thinkrank'),
399 [$this, 'render_seo_meta_box'],
400 $post_type,
401 'normal',
402 'high'
403 );
404 }
405 }
406
407 /**
408 * Render SEO meta box
409 *
410 * @param \WP_Post $post Current post object
411 * @return void
412 */
413 public function render_seo_meta_box(\WP_Post $post): void {
414 wp_nonce_field('thinkrank_meta_box', 'thinkrank_meta_box_nonce');
415
416 echo '<div id="thinkrank-meta-box" data-post-id="' . esc_attr($post->ID) . '">';
417 echo '<div class="thinkrank-loading"><div class="spinner is-active"></div></div>';
418 echo '</div>';
419 }
420
421 /**
422 * Save meta box data
423 *
424 * @param int $post_id Post ID
425 * @return void
426 */
427 public function save_meta_boxes(int $post_id): void {
428 // Verify nonce
429 if (!isset($_POST['thinkrank_meta_box_nonce'])) {
430 return;
431 }
432
433 $nonce = sanitize_text_field(wp_unslash($_POST['thinkrank_meta_box_nonce']));
434 if (!wp_verify_nonce($nonce, 'thinkrank_meta_box')) {
435 return;
436 }
437
438 // Check permissions
439 if (!current_user_can('edit_post', $post_id)) {
440 return;
441 }
442
443 // Save meta data (handled by AJAX in React components)
444 do_action('thinkrank_save_post_meta', $post_id, $_POST);
445 }
446
447 /**
448 * Show admin notices
449 *
450 * @return void
451 */
452 public function show_admin_notices(): void {
453 // Implementation will be added in next iteration
454 }
455
456 /**
457 * Show welcome notice
458 *
459 * @return void
460 */
461 public function show_welcome_notice(): void {
462 ?>
463 <div class="notice notice-success is-dismissible thinkrank-welcome-notice">
464 <h3><?php esc_html_e('Welcome to ThinkRank!', 'thinkrank'); ?></h3>
465 <p><?php esc_html_e('Thank you for installing ThinkRank. Get started by configuring your AI settings.', 'thinkrank'); ?></p>
466 <p>
467 <a href="<?php echo esc_url(admin_url('admin.php?page=thinkrank-settings')); ?>" class="button button-primary">
468 <?php esc_html_e('Configure Settings', 'thinkrank'); ?>
469 </a>
470 <a href="#" class="button thinkrank-dismiss-welcome">
471 <?php esc_html_e('Dismiss', 'thinkrank'); ?>
472 </a>
473 </p>
474 </div>
475 <?php
476 }
477
478 /**
479 * Dismiss notice via AJAX
480 *
481 * @return void
482 */
483 public function dismiss_notice(): void {
484 check_ajax_referer('thinkrank_admin', 'nonce');
485
486 $notice_type = sanitize_key($_POST['notice_type'] ?? '');
487
488 if ($notice_type === 'welcome') {
489 delete_option('thinkrank_show_welcome');
490 }
491
492 wp_die();
493 }
494
495 /**
496 * Check if API key is configured
497 *
498 * @return bool True if at least one API key is configured
499 */
500 private function has_api_key_configured(): bool {
501 $settings = new \ThinkRank\Core\Settings();
502 $openai_key = $settings->get('openai_api_key');
503 $claude_key = $settings->get('claude_api_key');
504
505 return !empty($openai_key) || !empty($claude_key);
506 }
507
508 /**
509 * Get menu icon
510 *
511 * @return string Menu icon
512 */
513 private function get_menu_icon(): string {
514 return 'data:image/svg+xml;base64,' . base64_encode(
515 '<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
516 <g clipPath="url(#thinkrank-clip)">
517 <g filter="url(#thinkrank-shadow)">
518 <circle cx="14" cy="9.2" r="1.3" fill="#a7aaad"/>
519 </g>
520 <path d="M19.5 7v5.3c0 2.4 0 3.6-.5 4.5-.4.8-1 1.4-1.8 1.8-.9.5-2.1.5-4.5.5H7.3c-2.4 0-3.6 0-4.5-.5-.8-.4-1.4-1-1.8-1.8-.2-.3-.3-.7-.4-1.1.5-.4.9-.6 1.3-.7.5-.2.9-.2 1.4-.2.7.1 1.3.2 2 .3.7.1 1.4.2 2.1.1 1.5-.3 2.5-1 3.4-2 .5-.5.9-1 1.4-1.5.3-.3.6-.7.9-1 .3.1.6.2.9.2.9 0 1.7-.8 1.7-1.7 0-.2 0-.4-.1-.6.7-.4 1.4-.8 2.1-1.1.6-.3 1.2-.6 1.6-.8v.4zM12.5 0c2.4 0 3.6 0 4.5.5.8.4 1.4 1 1.8 1.8.4.7.5 1.6.5 3.1-.1 0-.2.1-.3.1-.5.2-1.1.5-1.8.8-.7.3-1.5.7-2.2 1.1-.3-.3-.7-.4-1.1-.4-.9 0-1.7.8-1.7 1.7 0 .3.1.6.3.9-.3.4-.6.7-.9 1-.5.6-.9 1.1-1.3 1.5-.9.9-1.8 1.5-3 1.7-.6.1-1.2.1-1.8 0-.6-.1-1.3-.3-2-.4-.6-.1-1.2-.1-1.8.1-.3.1-.7.3-1 .5 0-.6 0-1.4 0-2.3V7c0-2.4 0-3.6.5-4.5.4-.8 1-1.4 1.8-1.8C3.9 0 5.1 0 7.5 0h5zm-5.8 8.2c0-.1-.1-.1-.2 0l-.2.9c0 0 0 .1-.1.1l-.9.2c-.1 0-.1.1 0 .1l.9.2c0 0 .1 0 .1.1l.2.9c0 .1.1.1.2 0l.2-.9c0 0 0-.1.1-.1l.9-.2c.1 0 .1-.1 0-.1l-.9-.2c0 0-.1 0-.1-.1l-.2-.9zm8.8-.4c0 .1 0 .2 0 .3 0 .7-.6 1.3-1.3 1.3-.2 0-.4 0-.5-.1.1-.1.2-.2.3-.3.4-.4.9-.8 1.5-1.2zm-1.3-1c.3 0 .5.1.7.2-.6.4-1.1.8-1.5 1.2l-.1.1c-.1.1-.2.2-.3.3-.1-.2-.1-.4-.1-.6 0-.7.6-1.2 1.3-1.2zM8.6 2.5c-.1-.2-.4-.2-.4 0l-.4 1.4c0 .1-.1.1-.1.1L6.2 4.4c-.2.1-.2.4 0 .4l1.4.4c.1 0 .1.1.1.1l.4 1.4c.1.2.4.2.4 0l.4-1.4c0-.1.1-.1.1-.1l1.4-.4c.2-.1.2-.4 0-.4L8.9 4c-.1 0-.1-.1-.1-.1L8.6 2.5z" fill="#a7aaad"/>
521 </g>
522 <defs>
523 <filter id="thinkrank-shadow" x="11.2" y="7.2" width="5.6" height="5.6" filterUnits="userSpaceOnUse" colorInterpolationFilters="sRGB">
524 <feFlood floodOpacity="0" result="BackgroundImageFix"/>
525 <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
526 <feOffset dy="0.7"/>
527 <feGaussianBlur stdDeviation="0.7"/>
528 <feComposite in2="hardAlpha" operator="out"/>
529 <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
530 <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
531 <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
532 </filter>
533 <clipPath id="thinkrank-clip">
534 <rect width="20" height="20" rx="5.5" fill="white"/>
535 </clipPath>
536 </defs>
537 </svg>'
538 );
539 }
540
541 /**
542 * Get user capabilities for current user
543 *
544 * @return array User capabilities
545 */
546 private function get_user_capabilities(): array {
547 return [
548 'manage_settings' => current_user_can('manage_options'),
549 'view_analytics' => current_user_can('edit_posts'),
550
551 'use_ai_features' => current_user_can('edit_posts'),
552 ];
553 }
554
555 /**
556 * Get admin settings for JavaScript
557 *
558 * @return array Admin settings
559 */
560 private function get_admin_settings(): array {
561 return [
562
563 'ai_provider' => $this->settings->get('ai_provider', 'openai'),
564 'cache_duration' => $this->settings->get('cache_duration', 3600),
565 ];
566 }
567
568 /**
569 * Get internationalization strings
570 *
571 * @return array I18n strings
572 */
573 private function get_i18n_strings(): array {
574 return [
575 'loading' => __('Loading...', 'thinkrank'),
576 'error' => __('An error occurred', 'thinkrank'),
577 'success' => __('Success!', 'thinkrank'),
578 'confirm' => __('Are you sure?', 'thinkrank'),
579 'cancel' => __('Cancel', 'thinkrank'),
580 'save' => __('Save', 'thinkrank'),
581 ];
582 }
583
584 /**
585 * Get all public post types for SEO configuration
586 *
587 * @return array Post types data
588 */
589 private function get_public_post_types(): array {
590 // Get all public post types (both built-in and custom)
591 $post_types = get_post_types([
592 'public' => true
593 ], 'objects');
594
595 $post_types_data = [];
596
597 foreach ($post_types as $post_type) {
598 // Skip certain post types that shouldn't have SEO settings
599 $excluded_types = ['elementor_library', 'oceanwp_library', 'ae_global_templates'];
600 if (in_array($post_type->name, $excluded_types)) {
601 continue;
602 }
603
604 $post_types_data[] = [
605 'name' => $post_type->name,
606 'slug' => $post_type->name,
607 'label' => $post_type->label,
608 'singular_name' => $post_type->labels->singular_name ?? $post_type->label,
609 'plural_name' => $post_type->label,
610 'public' => $post_type->public,
611 'has_archive' => $post_type->has_archive,
612 'hierarchical' => $post_type->hierarchical,
613 ];
614 }
615
616 return $post_types_data;
617 }
618
619 /**
620 * Add help tabs
621 *
622 * @return void
623 */
624 private function add_help_tabs(): void {
625 $screen = get_current_screen();
626
627 $screen->add_help_tab([
628 'id' => 'thinkrank-overview',
629 'title' => __('Overview', 'thinkrank'),
630 'content' => '<p>' . __('ThinkRank.ai helps you optimize your content with AI-powered SEO suggestions.', 'thinkrank') . '</p>',
631 ]);
632
633 $screen->set_help_sidebar(
634 '<p><strong>' . __('For more information:', 'thinkrank') . '</strong></p>' .
635 '<p><a href="https://thinkrank.ai/docs" target="_blank">' . __('Documentation', 'thinkrank') . '</a></p>' .
636 '<p><a href="https://thinkrank.ai/support" target="_blank">' . __('Support', 'thinkrank') . '</a></p>'
637 );
638 }
639
640 /**
641 * Add screen options
642 *
643 * @return void
644 */
645 private function add_screen_options(): void {
646 // Screen options will be added as needed
647 }
648
649 /**
650 * Check for plugin updates
651 *
652 * @return void
653 */
654 private function check_plugin_updates(): void {
655 $current_version = get_option('thinkrank_version');
656
657 if (version_compare($current_version, THINKRANK_VERSION, '<')) {
658 // Handle plugin update
659 do_action('thinkrank_plugin_updated', $current_version, THINKRANK_VERSION);
660 update_option('thinkrank_version', THINKRANK_VERSION);
661 }
662 }
663
664 /**
665 * Get admin styles
666 *
667 * @return string CSS styles
668 */
669 private function get_admin_styles(): string {
670 return '
671 .thinkrank-loading {
672 display: flex;
673 flex-direction: column;
674 align-items: center;
675 justify-content: center;
676 padding: 40px 20px;
677 text-align: center;
678 }
679
680 .thinkrank-loading .spinner {
681 margin-bottom: 16px;
682 }
683
684 .thinkrank-loading p {
685 margin: 0;
686 color: #666;
687 font-size: 14px;
688 }
689
690 .thinkrank-app {
691 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
692 }
693
694 .thinkrank-header {
695 display: flex;
696 justify-content: space-between;
697 align-items: center;
698 padding: 20px 0;
699 border-bottom: 1px solid #ddd;
700 margin-bottom: 20px;
701 }
702
703 .thinkrank-header h1 {
704 margin: 0;
705 font-size: 24px;
706 font-weight: 600;
707 }
708
709 .thinkrank-header .version {
710 font-size: 12px;
711 color: #666;
712 font-weight: normal;
713 margin-left: 8px;
714 }
715
716 .thinkrank-header .tagline {
717 margin: 4px 0 0 0;
718 color: #666;
719 font-size: 14px;
720 }
721
722
723
724 .thinkrank-dashboard-stats {
725 display: grid;
726 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
727 gap: 20px;
728 margin-top: 20px;
729 }
730
731 .stat-card {
732 background: #f9f9f9;
733 padding: 20px;
734 border-radius: 8px;
735 text-align: center;
736 border: 1px solid #ddd;
737 }
738
739 .stat-card h3 {
740 margin: 0 0 10px 0;
741 font-size: 14px;
742 color: #666;
743 text-transform: uppercase;
744 letter-spacing: 0.5px;
745 }
746
747 .stat-number {
748 font-size: 32px;
749 font-weight: 700;
750 color: #0073aa;
751 margin: 0;
752 line-height: 1;
753 }
754
755 .stat-label {
756 margin: 4px 0 0 0;
757 font-size: 12px;
758 color: #666;
759 }
760
761 .thinkrank-error {
762 padding: 20px;
763 }
764
765 .thinkrank-error .notice {
766 margin: 0;
767 }
768 ';
769 }
770
771
772 }
773