205 lines
8.3 KiB
TypeScript
205 lines
8.3 KiB
TypeScript
import React from 'react';
|
|
import { ArrowUpIcon, ArrowDownIcon } from '../../icons';
|
|
|
|
interface ScoreData {
|
|
seo_score: number;
|
|
readability_score: number;
|
|
engagement_score: number;
|
|
overall_score: number;
|
|
metadata_completeness_score?: number; // Stage 3: Metadata completeness
|
|
word_count?: number;
|
|
has_meta_title?: boolean;
|
|
has_meta_description?: boolean;
|
|
has_primary_keyword?: boolean;
|
|
internal_links_count?: number;
|
|
has_cluster_mapping?: boolean; // Stage 3: Cluster mapping
|
|
has_taxonomy_mapping?: boolean; // Stage 3: Taxonomy mapping
|
|
has_attributes?: boolean; // Stage 3: Attributes
|
|
}
|
|
|
|
interface OptimizationScoresProps {
|
|
scores: ScoreData;
|
|
before?: ScoreData;
|
|
className?: string;
|
|
}
|
|
|
|
export const OptimizationScores: React.FC<OptimizationScoresProps> = ({
|
|
scores,
|
|
before,
|
|
className = '',
|
|
}) => {
|
|
const getScoreColor = (score: number) => {
|
|
if (score >= 80) return 'text-success-600 dark:text-success-400';
|
|
if (score >= 60) return 'text-warning-600 dark:text-warning-400';
|
|
return 'text-error-600 dark:text-error-400';
|
|
};
|
|
|
|
const getScoreBgColor = (score: number) => {
|
|
if (score >= 80) return 'bg-success-100 dark:bg-success-900';
|
|
if (score >= 60) return 'bg-warning-100 dark:bg-warning-900';
|
|
return 'bg-error-100 dark:bg-error-900';
|
|
};
|
|
|
|
const getChangeIcon = (current: number, previous?: number) => {
|
|
if (!previous) return null;
|
|
const diff = current - previous;
|
|
if (diff > 0) return <ArrowUpIcon className="w-4 h-4 text-success-600" />;
|
|
if (diff < 0) return <ArrowDownIcon className="w-4 h-4 text-error-600" />;
|
|
return <span className="w-4 h-4 text-gray-400">—</span>;
|
|
};
|
|
|
|
const getChangeText = (current: number, previous?: number) => {
|
|
if (!previous) return null;
|
|
const diff = current - previous;
|
|
if (diff > 0) return `+${diff.toFixed(1)}`;
|
|
if (diff < 0) return diff.toFixed(1);
|
|
return '0.0';
|
|
};
|
|
|
|
return (
|
|
<div className={`grid grid-cols-1 md:grid-cols-5 gap-4 ${className}`}>
|
|
{/* Overall Score */}
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">Overall</span>
|
|
{before && getChangeIcon(scores.overall_score, before.overall_score)}
|
|
</div>
|
|
<div className="flex items-baseline gap-2">
|
|
<span className={`text-2xl font-bold ${getScoreColor(scores.overall_score)}`}>
|
|
{scores.overall_score.toFixed(1)}
|
|
</span>
|
|
{before && (
|
|
<span className="text-xs text-gray-500">
|
|
{getChangeText(scores.overall_score, before.overall_score)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className={`mt-2 h-2 rounded-full ${getScoreBgColor(scores.overall_score)}`}>
|
|
<div
|
|
className={`h-2 rounded-full ${getScoreColor(scores.overall_score).replace('text-', 'bg-')}`}
|
|
style={{ width: `${scores.overall_score}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* SEO Score */}
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">SEO</span>
|
|
{before && getChangeIcon(scores.seo_score, before.seo_score)}
|
|
</div>
|
|
<div className="flex items-baseline gap-2">
|
|
<span className={`text-2xl font-bold ${getScoreColor(scores.seo_score)}`}>
|
|
{scores.seo_score.toFixed(1)}
|
|
</span>
|
|
{before && (
|
|
<span className="text-xs text-gray-500">
|
|
{getChangeText(scores.seo_score, before.seo_score)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className={`mt-2 h-2 rounded-full ${getScoreBgColor(scores.seo_score)}`}>
|
|
<div
|
|
className={`h-2 rounded-full ${getScoreColor(scores.seo_score).replace('text-', 'bg-')}`}
|
|
style={{ width: `${scores.seo_score}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Readability Score */}
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">Readability</span>
|
|
{before && getChangeIcon(scores.readability_score, before.readability_score)}
|
|
</div>
|
|
<div className="flex items-baseline gap-2">
|
|
<span className={`text-2xl font-bold ${getScoreColor(scores.readability_score)}`}>
|
|
{scores.readability_score.toFixed(1)}
|
|
</span>
|
|
{before && (
|
|
<span className="text-xs text-gray-500">
|
|
{getChangeText(scores.readability_score, before.readability_score)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className={`mt-2 h-2 rounded-full ${getScoreBgColor(scores.readability_score)}`}>
|
|
<div
|
|
className={`h-2 rounded-full ${getScoreColor(scores.readability_score).replace('text-', 'bg-')}`}
|
|
style={{ width: `${scores.readability_score}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Engagement Score */}
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">Engagement</span>
|
|
{before && getChangeIcon(scores.engagement_score, before.engagement_score)}
|
|
</div>
|
|
<div className="flex items-baseline gap-2">
|
|
<span className={`text-2xl font-bold ${getScoreColor(scores.engagement_score)}`}>
|
|
{scores.engagement_score.toFixed(1)}
|
|
</span>
|
|
{before && (
|
|
<span className="text-xs text-gray-500">
|
|
{getChangeText(scores.engagement_score, before.engagement_score)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className={`mt-2 h-2 rounded-full ${getScoreBgColor(scores.engagement_score)}`}>
|
|
<div
|
|
className={`h-2 rounded-full ${getScoreColor(scores.engagement_score).replace('text-', 'bg-')}`}
|
|
style={{ width: `${scores.engagement_score}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Metadata Completeness Score - Stage 3 */}
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">Metadata</span>
|
|
{before && scores.metadata_completeness_score !== undefined && getChangeIcon(
|
|
scores.metadata_completeness_score,
|
|
before.metadata_completeness_score
|
|
)}
|
|
</div>
|
|
<div className="flex items-baseline gap-2">
|
|
<span className={`text-2xl font-bold ${getScoreColor(scores.metadata_completeness_score || 0)}`}>
|
|
{(scores.metadata_completeness_score || 0).toFixed(1)}
|
|
</span>
|
|
{before && scores.metadata_completeness_score !== undefined && (
|
|
<span className="text-xs text-gray-500">
|
|
{getChangeText(scores.metadata_completeness_score, before.metadata_completeness_score)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className={`mt-2 h-2 rounded-full ${getScoreBgColor(scores.metadata_completeness_score || 0)}`}>
|
|
<div
|
|
className={`h-2 rounded-full ${getScoreColor(scores.metadata_completeness_score || 0).replace('text-', 'bg-')}`}
|
|
style={{ width: `${scores.metadata_completeness_score || 0}%` }}
|
|
/>
|
|
</div>
|
|
{/* Metadata indicators */}
|
|
<div className="mt-3 flex flex-wrap gap-2 text-xs">
|
|
{scores.has_cluster_mapping && (
|
|
<span className="px-2 py-0.5 bg-brand-100 dark:bg-brand-900 text-brand-800 dark:text-brand-200 rounded">
|
|
Cluster
|
|
</span>
|
|
)}
|
|
{scores.has_taxonomy_mapping && (
|
|
<span className="px-2 py-0.5 bg-info-100 dark:bg-info-900 text-info-800 dark:text-info-200 rounded">
|
|
Taxonomy
|
|
</span>
|
|
)}
|
|
{scores.has_attributes && (
|
|
<span className="px-2 py-0.5 bg-success-100 dark:bg-success-900 text-success-800 dark:text-success-200 rounded">
|
|
Attributes
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|