PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.0.2
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.0.2
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.0.2, at includes/admin/class-manager.php

736 lines 23.4 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 ]);
254
255 // Add welcome notice dismissal script
256 if (get_option('thinkrank_show_welcome') && !$this->has_api_key_configured()) {
257 wp_add_inline_script('thinkrank-admin', '
258 jQuery(document).ready(function($) {
259 $(".thinkrank-dismiss-welcome").on("click", function(e) {
260 e.preventDefault();
261
262 $.post(ajaxurl, {
263 action: "thinkrank_dismiss_notice",
264 notice_type: "welcome",
265 nonce: thinkrankAdmin.adminNonce
266 }, function(response) {
267 $(".thinkrank-welcome-notice").fadeOut();
268 });
269 });
270 });
271 ');
272 }
273 }
274
275 /**
276 * Handle admin initialization
277 *
278 * @return void
279 */
280 public function handle_admin_init(): void {
281 // Show welcome screen for new installations (only if no API key configured)
282 if (get_option('thinkrank_show_welcome') && !$this->has_api_key_configured()) {
283 add_action('admin_notices', [$this, 'show_welcome_notice']);
284 }
285
286 // Check for plugin updates
287 $this->check_plugin_updates();
288 }
289
290 /**
291 * Load admin page
292 *
293 * @return void
294 */
295 public function load_admin_page(): void {
296 // Add help tabs
297 $this->add_help_tabs();
298
299 // Add screen options
300 $this->add_screen_options();
301 }
302
303 /**
304 * Render dashboard page
305 *
306 * @return void
307 */
308 public function render_dashboard_page(): void {
309 $this->render_admin_page('dashboard', [
310 'title' => __('ThinkRank Dashboard', 'thinkrank'),
311 'description' => __('AI-powered SEO optimization for WordPress', 'thinkrank'),
312 ]);
313 }
314
315 /**
316 * Render Essential SEO page
317 *
318 * @return void
319 */
320 public function render_essential_seo_page(): void {
321 $this->render_admin_page('essential-seo', [
322 'title' => __('Essential SEO', 'thinkrank'),
323 'description' => __('Configure your site-wide SEO settings with AI-powered optimization', 'thinkrank'),
324 ]);
325 }
326
327 /**
328 * Render AI Tools page
329 *
330 * @return void
331 */
332 public function render_ai_tools_page(): void {
333 $this->render_admin_page('ai-tools', [
334 'title' => __('AI Tools', 'thinkrank'),
335 'description' => __('AI-powered content tools including Content Planner and Metadata Generator', 'thinkrank'),
336 ]);
337 }
338
339 /**
340 * Render settings page
341 *
342 * @return void
343 */
344 public function render_settings_page(): void {
345 $this->render_admin_page('settings', [
346 'title' => __('ThinkRank Settings', 'thinkrank'),
347 'description' => __('Configure your AI SEO settings', 'thinkrank'),
348 ]);
349 }
350
351
352
353 /**
354 * Render usage analytics page
355 *
356 * @return void
357 */
358 public function render_analytics_page(): void {
359 $this->render_admin_page('analytics', [
360 'title' => __('Usage Analytics', 'thinkrank'),
361 'description' => __('Track your AI usage, costs, and plugin performance analytics', 'thinkrank'),
362 ]);
363 }
364
365 /**
366 * Render admin page template
367 *
368 * @param string $page Page identifier
369 * @param array $data Page data
370 * @return void
371 */
372 private function render_admin_page(string $page, array $data): void {
373 ?>
374 <div class="wrap">
375 <div id="thinkrank-<?php echo esc_attr($page); ?>" class="thinkrank-admin-page">
376 <div class="thinkrank-loading">
377 <div class="spinner is-active"></div>
378 <p><?php esc_html_e('Loading ThinkRank...', 'thinkrank'); ?></p>
379 </div>
380 </div>
381 </div>
382 <?php
383 }
384
385 /**
386 * Add meta boxes to post edit screens
387 *
388 * @return void
389 */
390 public function add_meta_boxes(): void {
391 $post_types = get_post_types(['public' => true]);
392
393 foreach ($post_types as $post_type) {
394 add_meta_box(
395 'thinkrank-seo',
396 __('ThinkRank SEO', 'thinkrank'),
397 [$this, 'render_seo_meta_box'],
398 $post_type,
399 'normal',
400 'high'
401 );
402 }
403 }
404
405 /**
406 * Render SEO meta box
407 *
408 * @param \WP_Post $post Current post object
409 * @return void
410 */
411 public function render_seo_meta_box(\WP_Post $post): void {
412 wp_nonce_field('thinkrank_meta_box', 'thinkrank_meta_box_nonce');
413
414 echo '<div id="thinkrank-meta-box" data-post-id="' . esc_attr($post->ID) . '">';
415 echo '<div class="thinkrank-loading"><div class="spinner is-active"></div></div>';
416 echo '</div>';
417 }
418
419 /**
420 * Save meta box data
421 *
422 * @param int $post_id Post ID
423 * @return void
424 */
425 public function save_meta_boxes(int $post_id): void {
426 // Verify nonce
427 if (!isset($_POST['thinkrank_meta_box_nonce'])) {
428 return;
429 }
430
431 $nonce = sanitize_text_field(wp_unslash($_POST['thinkrank_meta_box_nonce']));
432 if (!wp_verify_nonce($nonce, 'thinkrank_meta_box')) {
433 return;
434 }
435
436 // Check permissions
437 if (!current_user_can('edit_post', $post_id)) {
438 return;
439 }
440
441 // Save meta data (handled by AJAX in React components)
442 do_action('thinkrank_save_post_meta', $post_id, $_POST);
443 }
444
445 /**
446 * Show admin notices
447 *
448 * @return void
449 */
450 public function show_admin_notices(): void {
451 // Implementation will be added in next iteration
452 }
453
454 /**
455 * Show welcome notice
456 *
457 * @return void
458 */
459 public function show_welcome_notice(): void {
460 ?>
461 <div class="notice notice-success is-dismissible thinkrank-welcome-notice">
462 <h3><?php esc_html_e('Welcome to ThinkRank!', 'thinkrank'); ?></h3>
463 <p><?php esc_html_e('Thank you for installing ThinkRank. Get started by configuring your AI settings.', 'thinkrank'); ?></p>
464 <p>
465 <a href="<?php echo esc_url(admin_url('admin.php?page=thinkrank-settings')); ?>" class="button button-primary">
466 <?php esc_html_e('Configure Settings', 'thinkrank'); ?>
467 </a>
468 <a href="#" class="button thinkrank-dismiss-welcome">
469 <?php esc_html_e('Dismiss', 'thinkrank'); ?>
470 </a>
471 </p>
472 </div>
473 <?php
474 }
475
476 /**
477 * Dismiss notice via AJAX
478 *
479 * @return void
480 */
481 public function dismiss_notice(): void {
482 check_ajax_referer('thinkrank_admin', 'nonce');
483
484 $notice_type = sanitize_key($_POST['notice_type'] ?? '');
485
486 if ($notice_type === 'welcome') {
487 delete_option('thinkrank_show_welcome');
488 }
489
490 wp_die();
491 }
492
493 /**
494 * Check if API key is configured
495 *
496 * @return bool True if at least one API key is configured
497 */
498 private function has_api_key_configured(): bool {
499 $settings = new \ThinkRank\Core\Settings();
500 $openai_key = $settings->get('openai_api_key');
501 $claude_key = $settings->get('claude_api_key');
502
503 return !empty($openai_key) || !empty($claude_key);
504 }
505
506 /**
507 * Get menu icon
508 *
509 * @return string Menu icon
510 */
511 private function get_menu_icon(): string {
512 return 'data:image/svg+xml;base64,' . base64_encode(
513 '<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
514 <g clipPath="url(#thinkrank-clip)">
515 <g filter="url(#thinkrank-shadow)">
516 <circle cx="14" cy="9.2" r="1.3" fill="#a7aaad"/>
517 </g>
518 <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"/>
519 </g>
520 <defs>
521 <filter id="thinkrank-shadow" x="11.2" y="7.2" width="5.6" height="5.6" filterUnits="userSpaceOnUse" colorInterpolationFilters="sRGB">
522 <feFlood floodOpacity="0" result="BackgroundImageFix"/>
523 <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"/>
524 <feOffset dy="0.7"/>
525 <feGaussianBlur stdDeviation="0.7"/>
526 <feComposite in2="hardAlpha" operator="out"/>
527 <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"/>
528 <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
529 <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
530 </filter>
531 <clipPath id="thinkrank-clip">
532 <rect width="20" height="20" rx="5.5" fill="white"/>
533 </clipPath>
534 </defs>
535 </svg>'
536 );
537 }
538
539 /**
540 * Get user capabilities for current user
541 *
542 * @return array User capabilities
543 */
544 private function get_user_capabilities(): array {
545 return [
546 'manage_settings' => current_user_can('manage_options'),
547 'view_analytics' => current_user_can('edit_posts'),
548
549 'use_ai_features' => current_user_can('edit_posts'),
550 ];
551 }
552
553 /**
554 * Get admin settings for JavaScript
555 *
556 * @return array Admin settings
557 */
558 private function get_admin_settings(): array {
559 return [
560
561 'ai_provider' => $this->settings->get('ai_provider', 'openai'),
562 'cache_duration' => $this->settings->get('cache_duration', 3600),
563 ];
564 }
565
566 /**
567 * Get internationalization strings
568 *
569 * @return array I18n strings
570 */
571 private function get_i18n_strings(): array {
572 return [
573 'loading' => __('Loading...', 'thinkrank'),
574 'error' => __('An error occurred', 'thinkrank'),
575 'success' => __('Success!', 'thinkrank'),
576 'confirm' => __('Are you sure?', 'thinkrank'),
577 'cancel' => __('Cancel', 'thinkrank'),
578 'save' => __('Save', 'thinkrank'),
579 ];
580 }
581
582 /**
583 * Add help tabs
584 *
585 * @return void
586 */
587 private function add_help_tabs(): void {
588 $screen = get_current_screen();
589
590 $screen->add_help_tab([
591 'id' => 'thinkrank-overview',
592 'title' => __('Overview', 'thinkrank'),
593 'content' => '<p>' . __('ThinkRank.ai helps you optimize your content with AI-powered SEO suggestions.', 'thinkrank') . '</p>',
594 ]);
595
596 $screen->set_help_sidebar(
597 '<p><strong>' . __('For more information:', 'thinkrank') . '</strong></p>' .
598 '<p><a href="https://thinkrank.ai/docs" target="_blank">' . __('Documentation', 'thinkrank') . '</a></p>' .
599 '<p><a href="https://thinkrank.ai/support" target="_blank">' . __('Support', 'thinkrank') . '</a></p>'
600 );
601 }
602
603 /**
604 * Add screen options
605 *
606 * @return void
607 */
608 private function add_screen_options(): void {
609 // Screen options will be added as needed
610 }
611
612 /**
613 * Check for plugin updates
614 *
615 * @return void
616 */
617 private function check_plugin_updates(): void {
618 $current_version = get_option('thinkrank_version');
619
620 if (version_compare($current_version, THINKRANK_VERSION, '<')) {
621 // Handle plugin update
622 do_action('thinkrank_plugin_updated', $current_version, THINKRANK_VERSION);
623 update_option('thinkrank_version', THINKRANK_VERSION);
624 }
625 }
626
627 /**
628 * Get admin styles
629 *
630 * @return string CSS styles
631 */
632 private function get_admin_styles(): string {
633 return '
634 .thinkrank-loading {
635 display: flex;
636 flex-direction: column;
637 align-items: center;
638 justify-content: center;
639 padding: 40px 20px;
640 text-align: center;
641 }
642
643 .thinkrank-loading .spinner {
644 margin-bottom: 16px;
645 }
646
647 .thinkrank-loading p {
648 margin: 0;
649 color: #666;
650 font-size: 14px;
651 }
652
653 .thinkrank-app {
654 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
655 }
656
657 .thinkrank-header {
658 display: flex;
659 justify-content: space-between;
660 align-items: center;
661 padding: 20px 0;
662 border-bottom: 1px solid #ddd;
663 margin-bottom: 20px;
664 }
665
666 .thinkrank-header h1 {
667 margin: 0;
668 font-size: 24px;
669 font-weight: 600;
670 }
671
672 .thinkrank-header .version {
673 font-size: 12px;
674 color: #666;
675 font-weight: normal;
676 margin-left: 8px;
677 }
678
679 .thinkrank-header .tagline {
680 margin: 4px 0 0 0;
681 color: #666;
682 font-size: 14px;
683 }
684
685
686
687 .thinkrank-dashboard-stats {
688 display: grid;
689 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
690 gap: 20px;
691 margin-top: 20px;
692 }
693
694 .stat-card {
695 background: #f9f9f9;
696 padding: 20px;
697 border-radius: 8px;
698 text-align: center;
699 border: 1px solid #ddd;
700 }
701
702 .stat-card h3 {
703 margin: 0 0 10px 0;
704 font-size: 14px;
705 color: #666;
706 text-transform: uppercase;
707 letter-spacing: 0.5px;
708 }
709
710 .stat-number {
711 font-size: 32px;
712 font-weight: 700;
713 color: #0073aa;
714 margin: 0;
715 line-height: 1;
716 }
717
718 .stat-label {
719 margin: 4px 0 0 0;
720 font-size: 12px;
721 color: #666;
722 }
723
724 .thinkrank-error {
725 padding: 20px;
726 }
727
728 .thinkrank-error .notice {
729 margin: 0;
730 }
731 ';
732 }
733
734
735 }
736