PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.0.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.0.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 / src / admin / components / seo / SEOScoreCalculator.js

SEOScoreCalculator.js in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 1.0.0, at src/admin/components/seo/SEOScoreCalculator.js

437 lines 17.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * SEO Score Calculator Component
3 *
4 * Main component for comprehensive SEO scoring and analysis
5 *
6 * @package ThinkRank
7 * @since 1.0.0
8 */
9
10 import { __ } from '@wordpress/i18n';
11 import { useState, useEffect, memo, useMemo, useCallback } from '@wordpress/element';
12 import { Button, Spinner, Notice, Modal, TabPanel } from '@wordpress/components';
13 import apiFetch from '@wordpress/api-fetch';
14
15 import ScoreBreakdown from './ScoreBreakdown';
16 import ImprovementSuggestions from './ImprovementSuggestions';
17 import SEOScoreSummary from './SEOScoreSummary';
18
19 /**
20 * SEO Score Calculator Component
21 *
22 * @param {Object} props Component props
23 * @param {number} props.postId WordPress post ID
24 * @param {string} props.targetKeyword Target keyword for analysis
25 * @param {string} props.postTitle Post title for analysis
26 * @param {string} props.metaDescription Meta description for analysis
27 * @param {Object} props.scoreData External score data (optional, for metabox integration)
28 * @param {Function} props.onRefresh External refresh callback (optional)
29 * @returns {JSX.Element} SEO Score Calculator component
30 */
31 const SEOScoreCalculator = ({
32 postId,
33 targetKeyword = '',
34 postTitle = '',
35 metaDescription = '',
36 scoreData: externalScoreData = null,
37 onRefresh: externalOnRefresh = null
38 }) => {
39 const [scoreData, setScoreData] = useState(null);
40 const [isCalculating, setIsCalculating] = useState(false);
41 const [error, setError] = useState(null);
42 const [lastCalculated, setLastCalculated] = useState(null);
43 const [modalState, setModalState] = useState({ isOpen: false, type: null, data: null });
44
45 /**
46 * Get live content from editor
47 */
48 const getLiveContent = () => {
49 let content = '';
50
51 // Try Block Editor first
52 if (typeof wp !== 'undefined' && wp.data && wp.data.select('core/editor')) {
53 try {
54 content = wp.data.select('core/editor').getEditedPostContent();
55 if (content) return content;
56 } catch (e) {
57 // Block editor not available
58 }
59 }
60
61 // Try TinyMCE (Classic Editor)
62 if (typeof tinymce !== 'undefined') {
63 const editor = tinymce.get('content');
64 if (editor && !editor.isHidden()) {
65 content = editor.getContent();
66 if (content) return content;
67 }
68 }
69
70 // Fallback to textarea
71 const contentTextarea = document.getElementById('content');
72 if (contentTextarea) {
73 content = contentTextarea.value || '';
74 }
75
76 return content;
77 };
78
79 /**
80 * Calculate SEO score - Memoized for performance
81 */
82 const calculateScore = useCallback(async (saveScore = true) => {
83 if (!postId) {
84 setError(__('No post ID provided', 'thinkrank'));
85 return;
86 }
87
88 setIsCalculating(true);
89 setError(null);
90
91 try {
92 const response = await apiFetch({
93 path: '/thinkrank/v1/seo-score/calculate',
94 method: 'POST',
95 data: {
96 post_id: postId,
97 target_keyword: targetKeyword,
98 save_score: saveScore,
99 live_content: getLiveContent()
100 }
101 });
102
103 if (response.success) {
104 setScoreData(response.data);
105 setLastCalculated(new Date());
106 } else {
107 setError(response.message || __('Failed to calculate SEO score', 'thinkrank'));
108 }
109 } catch (err) {
110 console.error('SEO Score calculation error:', err);
111 setError(err.message || __('An error occurred while calculating SEO score', 'thinkrank'));
112 } finally {
113 setIsCalculating(false);
114 }
115 }, [postId, targetKeyword, getLiveContent]);
116
117 /**
118 * Get latest score from history
119 */
120 const getLatestScore = async () => {
121 if (!postId) return;
122
123 try {
124 const response = await apiFetch({
125 path: `/thinkrank/v1/seo-score/get?post_id=${postId}`,
126 method: 'GET'
127 });
128
129 if (response.success && response.data) {
130 setScoreData(response.data);
131 setLastCalculated(new Date(response.data.calculated_at));
132 }
133 } catch (err) {
134 // Don't show error to user, just means no existing data
135 }
136 };
137
138 /**
139 * Handle applying a suggestion
140 */
141 const handleApplySuggestion = async (suggestion, index) => {
142 const suggestionText = typeof suggestion === 'string' ? suggestion : suggestion.text || String(suggestion);
143 const guidance = getSuggestionGuidance(suggestionText);
144
145 setModalState({
146 isOpen: true,
147 type: 'apply',
148 data: { suggestion: suggestionText, guidance, index }
149 });
150
151 return Promise.resolve();
152 };
153
154 /**
155 * Get guidance for applying a suggestion
156 */
157 const getSuggestionGuidance = (suggestion) => {
158 const lower = suggestion.toLowerCase();
159
160 if (lower.includes('meta description')) {
161 return __('💡 Scroll down to the meta description field and update it with compelling, keyword-rich content.', 'thinkrank');
162 }
163 if (lower.includes('title') || lower.includes('heading')) {
164 return __('💡 Update your post title or add relevant headings (H2, H3) to your content.', 'thinkrank');
165 }
166 if (lower.includes('keyword')) {
167 return __('💡 Review your content and naturally incorporate your target keyword in key locations.', 'thinkrank');
168 }
169 if (lower.includes('content') || lower.includes('words')) {
170 return __('💡 Expand your content with more detailed, valuable information for your readers.', 'thinkrank');
171 }
172 if (lower.includes('link')) {
173 return __('💡 Add relevant internal links to other pages on your site or quality external resources.', 'thinkrank');
174 }
175 if (lower.includes('image')) {
176 return __('💡 Add relevant images with descriptive alt text to enhance your content.', 'thinkrank');
177 }
178
179 return __('💡 Review the suggestion and make the recommended changes to improve your SEO.', 'thinkrank');
180 };
181
182
183
184 /**
185 * Handle dismissing a suggestion
186 */
187 const handleDismissSuggestion = (suggestion, index) => {
188 const suggestionText = typeof suggestion === 'string' ? suggestion : suggestion.text || String(suggestion);
189
190 setModalState({
191 isOpen: true,
192 type: 'dismiss',
193 data: { suggestion: suggestionText, index }
194 });
195
196 return false; // Don't dismiss immediately, wait for modal confirmation
197 };
198
199 /**
200 * Handle modal close
201 */
202 const handleModalClose = () => {
203 setModalState({ isOpen: false, type: null, data: null });
204 };
205
206 /**
207 * Handle dismiss confirmation
208 */
209 const handleDismissConfirm = () => {
210 const { data } = modalState;
211
212 // Store dismissed suggestion in localStorage for this session
213 const dismissed = JSON.parse(localStorage.getItem('thinkrank_dismissed_suggestions') || '[]');
214 dismissed.push({
215 suggestion: data.suggestion,
216 postId: postId,
217 timestamp: Date.now()
218 });
219 localStorage.setItem('thinkrank_dismissed_suggestions', JSON.stringify(dismissed));
220
221 // Force re-render by triggering a recalculation
222 calculateScore(false);
223
224 setModalState({ isOpen: false, type: null, data: null });
225 };
226
227 // Use external score data if provided, otherwise manage internal state
228 const effectiveScoreData = externalScoreData || scoreData;
229 const effectiveOnRefresh = externalOnRefresh || (() => calculateScore(true));
230
231 // Load latest score on component mount (only if no external data)
232 useEffect(() => {
233 if (!externalScoreData) {
234 getLatestScore();
235 }
236 }, [postId, externalScoreData]);
237
238 // Auto-calculate when target keyword changes (only if no external data)
239 useEffect(() => {
240 if (!externalScoreData && effectiveScoreData && targetKeyword !== effectiveScoreData.target_keyword) {
241 // Debounce the calculation
242 const timer = setTimeout(() => {
243 calculateScore(false); // Don't save auto-calculations
244 }, 1000);
245
246 return () => clearTimeout(timer);
247 }
248 }, [targetKeyword, externalScoreData]);
249
250 return (
251 <div className="thinkrank-seo-score-calculator">
252 {/* Header with Calculate Button */}
253 <div className="calculator-header">
254 <div className="header-content">
255 <h3>{__('SEO Score Analysis', 'thinkrank')}</h3>
256 <Button
257 variant="primary"
258 onClick={() => calculateScore(true)}
259 isBusy={isCalculating}
260 disabled={isCalculating}
261 >
262 {isCalculating ? __('Calculating...', 'thinkrank') : __('Calculate SEO Score', 'thinkrank')}
263 </Button>
264 </div>
265
266 {lastCalculated && (
267 <div className="last-calculated">
268 {__('Last calculated:', 'thinkrank')} {lastCalculated.toLocaleString()}
269 </div>
270 )}
271 </div>
272
273 {/* Loading State */}
274 {isCalculating && (
275 <div className="calculating-state">
276 <Spinner />
277 <p>{__('Analyzing your content for SEO optimization...', 'thinkrank')}</p>
278 </div>
279 )}
280
281 {/* Error State */}
282 {error && (
283 <Notice status="error" isDismissible onRemove={() => setError(null)}>
284 {error}
285 </Notice>
286 )}
287
288 {/* Results - Tabbed Interface */}
289 {effectiveScoreData && (
290 <div className="calculator-results">
291 <TabPanel
292 className="thinkrank-seo-results-tabs"
293 activeClass="is-active"
294 tabs={[
295 {
296 name: 'overview',
297 title: __('Overview', 'thinkrank'),
298 className: 'tab-overview',
299 },
300 {
301 name: 'factors',
302 title: __('Scoring Factors', 'thinkrank'),
303 className: 'tab-factors',
304 },
305 {
306 name: 'suggestions',
307 title: (
308 <span className="tab-title-with-badge">
309 {__('Suggestions', 'thinkrank')}
310 {effectiveScoreData.suggestions && effectiveScoreData.suggestions.length > 0 && (
311 <span className="suggestion-count-badge">
312 {effectiveScoreData.suggestions.length}
313 </span>
314 )}
315 </span>
316 ),
317 className: 'tab-suggestions',
318 },
319 ]}
320 initialTabName="overview"
321 >
322 {(tab) => (
323 <div className="tab-content">
324 {tab.name === 'overview' && (
325 <SEOScoreSummary
326 scoreData={effectiveScoreData}
327 targetKeyword={targetKeyword}
328 postTitle={postTitle}
329 metaDescription={metaDescription}
330 />
331 )}
332
333 {tab.name === 'factors' && (
334 <ScoreBreakdown
335 scoreData={effectiveScoreData}
336 showOverview={false}
337 onRefresh={null}
338 />
339 )}
340
341 {tab.name === 'suggestions' && (
342 <>
343 {effectiveScoreData.suggestions && effectiveScoreData.suggestions.length > 0 ? (
344 <ImprovementSuggestions
345 suggestions={effectiveScoreData.suggestions}
346 onApplySuggestion={handleApplySuggestion}
347 onDismissSuggestion={handleDismissSuggestion}
348 />
349 ) : (
350 <div className="no-suggestions-message">
351 <div className="success-icon">�
352 </div>
353 <h4>{__('Excellent Work!', 'thinkrank')}</h4>
354 <p>{__('No immediate SEO improvements needed. Your content is well optimized!', 'thinkrank')}</p>
355 </div>
356 )}
357 </>
358 )}
359 </div>
360 )}
361 </TabPanel>
362 </div>
363 )}
364
365 {/* Empty State */}
366 {!effectiveScoreData && !isCalculating && !error && (
367 <div className="empty-state">
368 <div className="empty-state-content">
369 <div className="empty-state-icon">📊</div>
370 <h4>{__('No SEO Analysis Yet', 'thinkrank')}</h4>
371 <p>{__('Click "Calculate SEO Score" to analyze your content and get detailed optimization recommendations.', 'thinkrank')}</p>
372 </div>
373 </div>
374 )}
375
376 {/* Information Modal */}
377 {modalState.isOpen && (
378 <Modal
379 title={modalState.type === 'apply' ? __('SEO Improvement Guidance', 'thinkrank') : __('Dismiss SEO Suggestion', 'thinkrank')}
380 onRequestClose={handleModalClose}
381 className="thinkrank-suggestion-modal"
382 >
383 <div className="modal-content">
384 {modalState.type === 'apply' ? (
385 <>
386 <blockquote className="suggestion-quote">
387 "{modalState.data?.suggestion}"
388 </blockquote>
389 <div className="guidance-section">
390 <p>{modalState.data?.guidance}</p>
391 </div>
392 <p>{__('Use this guidance to manually improve your content. The suggestions are recommendations to help you optimize your SEO.', 'thinkrank')}</p>
393 </>
394 ) : (
395 <>
396 <p><strong>{__('Dismiss this suggestion?', 'thinkrank')}</strong></p>
397 <blockquote className="suggestion-quote">
398 "{modalState.data?.suggestion}"
399 </blockquote>
400 <p>{__('You can always recalculate your SEO score to see suggestions again.', 'thinkrank')}</p>
401 </>
402 )}
403 </div>
404
405 <div className="modal-actions">
406 {modalState.type === 'apply' ? (
407 <Button
408 variant="primary"
409 onClick={handleModalClose}
410 >
411 {__('Got it', 'thinkrank')}
412 </Button>
413 ) : (
414 <>
415 <Button
416 variant="primary"
417 onClick={handleDismissConfirm}
418 >
419 {__('Dismiss', 'thinkrank')}
420 </Button>
421 <Button
422 variant="secondary"
423 onClick={handleModalClose}
424 >
425 {__('Cancel', 'thinkrank')}
426 </Button>
427 </>
428 )}
429 </div>
430 </Modal>
431 )}
432 </div>
433 );
434 };
435
436 export default SEOScoreCalculator;
437