/**
* SEO Score Breakdown Component
*
* Displays detailed breakdown of the 10-factor SEO scoring system
*
* @package ThinkRank
* @since 1.0.0
*/
import { __ } from '@wordpress/i18n';
import { useState, useEffect } from '@wordpress/element';
import { Card, CardBody, CardHeader, ProgressBar, Button, Flex, FlexItem } from '@wordpress/components';
import { chevronDown, chevronUp } from '@wordpress/icons';
/**
* Score Breakdown Component
*
* @param {Object} props Component props
* @param {Object} props.scoreData Complete scoring data from API
* @param {Function} props.onRefresh Callback for refreshing scores (optional)
* @param {boolean} props.showOverview Whether to show the overview card (default: true)
* @returns {JSX.Element} Score breakdown component
*/
const ScoreBreakdown = ({ scoreData, onRefresh = null, showOverview = true }) => {
const [expandedSections, setExpandedSections] = useState({});
const [isRefreshing, setIsRefreshing] = useState(false);
// 2025 SEO Algorithm Factors (Google Q1 2025 Update)
// Based on First Page Sage research and latest Google algorithm changes
const scoreFactors = {
satisfying_content: {
label: __('Satisfying Content', 'thinkrank'),
description: __('#1 ranking factor - Consistent publication of satisfying content (23%)', 'thinkrank'),
maxScore: 23,
icon: '🎯'
},
title_optimization: {
label: __('Title Optimization', 'thinkrank'),
description: __('Keyword in meta title with looser matching requirements (14%)', 'thinkrank'),
maxScore: 14,
icon: '📝'
},
niche_expertise: {
label: __('Niche Expertise', 'thinkrank'),
description: __('Hub & spoke content clusters demonstrating expertise (13%)', 'thinkrank'),
maxScore: 13,
icon: '🧠'
},
searcher_engagement: {
label: __('Searcher Engagement', 'thinkrank'),
description: __('Dwell time, bounce rate, pages per session metrics (12%)', 'thinkrank'),
maxScore: 12,
icon: '👥'
},
backlink_authority: {
label: __('Backlink Authority', 'thinkrank'),
description: __('Quality backlinks - declining but still important (13%)', 'thinkrank'),
maxScore: 13,
icon: '🔗'
},
content_freshness: {
label: __('Content Freshness', 'thinkrank'),
description: __('Quarterly content updates and maintenance (6%)', 'thinkrank'),
maxScore: 6,
icon: '🔄'
},
mobile_experience: {
label: __('Mobile Experience Score', 'thinkrank'),
description: __('NEW 2025: Mobile Experience Score (MES) replacing mobile-first (5%)', 'thinkrank'),
maxScore: 5,
icon: '📱'
},
trustworthiness: {
label: __('Trustworthiness', 'thinkrank'),
description: __('E-A-T signals and brand authority indicators (5%)', 'thinkrank'),
maxScore: 5,
icon: '🛡️'
},
link_diversity: {
label: __('Link Distribution Diversity', 'thinkrank'),
description: __('Diverse link profile and natural link patterns (4%)', 'thinkrank'),
maxScore: 4,
icon: '🌐'
},
core_web_vitals: {
label: __('Core Web Vitals 2.0', 'thinkrank'),
description: __('Updated 2025 performance metrics and user experience (3%)', 'thinkrank'),
maxScore: 3,
icon: '⚡'
},
site_security: {
label: __('Site Security', 'thinkrank'),
description: __('HTTPS, security headers, and trust signals (1%)', 'thinkrank'),
maxScore: 1,
icon: '🔒'
},
internal_linking: {
label: __('Internal Linking', 'thinkrank'),
description: __('Strategic internal link structure and content clusters (1%)', 'thinkrank'),
maxScore: 1,
icon: '🔗'
}
};
/**
* Toggle expanded state for a section
*/
const toggleSection = (factorKey) => {
setExpandedSections(prev => ({
...prev,
[factorKey]: !prev[factorKey]
}));
};
/**
* Get score color class based on percentage
*/
const getScoreColor = (score, maxScore) => {
const percentage = (score / maxScore) * 100;
if (percentage >= 90) return 'score-excellent';
if (percentage >= 70) return 'score-good';
if (percentage >= 50) return 'score-fair';
return 'score-poor';
};
/**
* Handle refresh scores
*/
const handleRefresh = async () => {
if (!onRefresh) return;
setIsRefreshing(true);
try {
await onRefresh();
} finally {
setIsRefreshing(false);
}
};
/**
* Convert technical details to human-readable format
*/
const formatFactorDetails = (factorKey, details) => {
if (!details || typeof details !== 'object') return null;
const formatters = {
satisfying_content: (d) => [
`Word count: ${d.word_count || 0} words (saved content)`,
`Content satisfaction: ${d.intent_satisfaction || 0}%`,
`Content uniqueness: ${d.uniqueness_score || 0}%`,
`Content depth: ${d.content_depth || 'Basic'}`
],
title_optimization: (d) => [
`Title length: ${d.title_length || 0} characters`,
`Keyword in title: ${d.keyword_in_title ? 'Yes' : 'No'}`,
`Title optimization: ${d.title_score || 0}%`
],
niche_expertise: (d) => [
`Topic coverage: ${d.topic_coverage || 'Basic'}`,
`Content depth: ${d.content_depth || 'Standard'}`,
`Expertise signals: ${d.expertise_score || 0}%`
],
searcher_engagement: (d) => [
`Content engagement: ${d.engagement_score || 0}%`,
`Reading time: ${d.estimated_reading_time || 'Unknown'}`,
`User experience: ${d.ux_score || 'Good'}`
],
backlink_authority: (d) => [
`Authority score: ${d.authority_score || 0}%`,
`Link quality: ${d.link_quality || 'Unknown'}`,
`Domain authority: ${d.domain_authority || 'Not assessed'}`
],
content_freshness: (d) => [
`Freshness status: ${d.freshness_status || 'Current'}`,
`Last updated: ${d.last_updated ? new Date(d.last_updated).toLocaleDateString() : 'Unknown'}`
],
mobile_experience: (d) => [
`Mobile score: ${d.mes_score || 'Good'}`,
`Mobile friendly: ${d.mobile_friendly ? 'Yes' : 'No'}`,
`Page speed: ${d.page_speed || 'Not tested'}`
],
technical_factors: (d) => [
`Meta description: ${d.meta_description_length || 0} characters`,
`Schema markup: ${d.schema_present ? 'Present' : 'Missing'}`,
`Technical score: ${d.technical_score || 0}%`
]
};
const formatter = formatters[factorKey];
return formatter ? formatter(details) : null;
};
if (!scoreData || !scoreData.score_breakdown) {
return (
{__('No scoring data available. Run an SEO analysis to see detailed breakdown.', 'thinkrank')}
{overall_score >= 80 && __('Excellent SEO optimization!', 'thinkrank')} {overall_score >= 60 && overall_score < 80 && __('Good SEO with room for improvement', 'thinkrank')} {overall_score >= 40 && overall_score < 60 && __('Fair SEO - needs attention', 'thinkrank')} {overall_score < 40 && __('Poor SEO - requires significant work', 'thinkrank')}
{calculated_at && ({__('Last updated:', 'thinkrank')} {new Date(calculated_at).toLocaleString()}
)}