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

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