Refactored key components to use react-i18next translations: - ErrorBoundary: error messages and labels - ProgressStepper: step labels - DimensionCard: health status labels, descriptions, benchmark text, action buttons - DashboardTabs: back button, footer, default title Added translation keys to es.json and en.json: - dashboard.defaultTitle - All health status and benchmark keys already existed Build verified successfully. Remaining 31 components to be refactored in subsequent commits. https://claude.ai/code/session_4f888c33-8937-4db8-8a9d-ddc9ac51a725
242 lines
7.8 KiB
TypeScript
242 lines
7.8 KiB
TypeScript
import React from 'react';
|
|
import { DimensionAnalysis } from '../types';
|
|
import { motion } from 'framer-motion';
|
|
import { AlertCircle, AlertTriangle, TrendingUp, CheckCircle, Zap } from 'lucide-react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import BadgePill from './BadgePill';
|
|
|
|
interface HealthStatus {
|
|
level: 'critical' | 'low' | 'medium' | 'good' | 'excellent';
|
|
label: string;
|
|
color: string;
|
|
textColor: string;
|
|
bgColor: string;
|
|
icon: React.ReactNode;
|
|
description: string;
|
|
}
|
|
|
|
const getHealthStatus = (score: number, t: (key: string) => string): HealthStatus => {
|
|
if (score >= 86) {
|
|
return {
|
|
level: 'excellent',
|
|
label: t('healthStatus.excellent'),
|
|
color: 'text-cyan-700',
|
|
textColor: 'text-cyan-700',
|
|
bgColor: 'bg-cyan-50',
|
|
icon: <CheckCircle size={20} className="text-cyan-600" />,
|
|
description: t('healthStatus.excellentDesc')
|
|
};
|
|
}
|
|
if (score >= 71) {
|
|
return {
|
|
level: 'good',
|
|
label: t('healthStatus.good'),
|
|
color: 'text-emerald-700',
|
|
textColor: 'text-emerald-700',
|
|
bgColor: 'bg-emerald-50',
|
|
icon: <TrendingUp size={20} className="text-emerald-600" />,
|
|
description: t('healthStatus.goodDesc')
|
|
};
|
|
}
|
|
if (score >= 51) {
|
|
return {
|
|
level: 'medium',
|
|
label: t('healthStatus.medium'),
|
|
color: 'text-amber-700',
|
|
textColor: 'text-amber-700',
|
|
bgColor: 'bg-amber-50',
|
|
icon: <AlertTriangle size={20} className="text-amber-600" />,
|
|
description: t('healthStatus.mediumDesc')
|
|
};
|
|
}
|
|
if (score >= 31) {
|
|
return {
|
|
level: 'low',
|
|
label: t('healthStatus.low'),
|
|
color: 'text-orange-700',
|
|
textColor: 'text-orange-700',
|
|
bgColor: 'bg-orange-50',
|
|
icon: <AlertTriangle size={20} className="text-orange-600" />,
|
|
description: t('healthStatus.lowDesc')
|
|
};
|
|
}
|
|
return {
|
|
level: 'critical',
|
|
label: t('healthStatus.critical'),
|
|
color: 'text-red-700',
|
|
textColor: 'text-red-700',
|
|
bgColor: 'bg-red-50',
|
|
icon: <AlertCircle size={20} className="text-red-600" />,
|
|
description: t('healthStatus.criticalDesc')
|
|
};
|
|
};
|
|
|
|
const getProgressBarColor = (score: number): string => {
|
|
if (score >= 86) return 'bg-cyan-500';
|
|
if (score >= 71) return 'bg-emerald-500';
|
|
if (score >= 51) return 'bg-amber-500';
|
|
if (score >= 31) return 'bg-orange-500';
|
|
return 'bg-red-500';
|
|
};
|
|
|
|
const ScoreIndicator: React.FC<{ score: number; benchmark?: number }> = ({ score, benchmark }) => {
|
|
const { t } = useTranslation();
|
|
const healthStatus = getHealthStatus(score, t);
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
{/* Main Score Display */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-baseline gap-2">
|
|
<span className="text-4xl font-bold text-slate-900">{score}</span>
|
|
<span className="text-lg text-slate-500">/100</span>
|
|
</div>
|
|
<BadgePill
|
|
label={healthStatus.label}
|
|
type={healthStatus.level === 'critical' ? 'critical' : healthStatus.level === 'low' ? 'warning' : 'info'}
|
|
size="md"
|
|
/>
|
|
</div>
|
|
|
|
{/* Progress Bar with Scale Reference */}
|
|
<div>
|
|
<div className="w-full bg-slate-200 rounded-full h-3">
|
|
<div
|
|
className={`${getProgressBarColor(score)} h-3 rounded-full transition-all duration-500`}
|
|
style={{ width: `${score}%` }}
|
|
/>
|
|
</div>
|
|
|
|
{/* Scale Reference */}
|
|
<div className="flex justify-between text-xs text-slate-500 mt-1">
|
|
<span>0</span>
|
|
<span>25</span>
|
|
<span>50</span>
|
|
<span>75</span>
|
|
<span>100</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Benchmark Comparison */}
|
|
{benchmark !== undefined && (
|
|
<div className="bg-slate-50 rounded-lg p-3 text-sm">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<span className="text-slate-600">{t('benchmark.title')}</span>
|
|
<span className="font-bold text-slate-900">{benchmark}/100</span>
|
|
</div>
|
|
<div className="text-xs text-slate-500">
|
|
{score > benchmark ? (
|
|
<span className="text-emerald-600 font-semibold">
|
|
{t('benchmark.aboveBenchmark', { points: score - benchmark })}
|
|
</span>
|
|
) : score === benchmark ? (
|
|
<span className="text-amber-600 font-semibold">
|
|
{t('benchmark.atBenchmark')}
|
|
</span>
|
|
) : (
|
|
<span className="text-orange-600 font-semibold">
|
|
{t('benchmark.belowBenchmark', { points: benchmark - score })}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Health Status Description */}
|
|
<div className={`${healthStatus.bgColor} rounded-lg p-3 flex items-start gap-2`}>
|
|
{healthStatus.icon}
|
|
<div>
|
|
<p className={`text-sm font-semibold ${healthStatus.textColor}`}>
|
|
{healthStatus.description}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const DimensionCard: React.FC<{ dimension: DimensionAnalysis }> = ({ dimension }) => {
|
|
const { t } = useTranslation();
|
|
const healthStatus = getHealthStatus(dimension.score, t);
|
|
|
|
return (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
className={`${healthStatus.bgColor} p-6 rounded-lg border-2 flex flex-col hover:shadow-lg transition-shadow`}
|
|
style={{
|
|
borderColor: healthStatus.color.replace('text-', '') + '-200'
|
|
}}
|
|
>
|
|
{/* Header */}
|
|
<div className="flex items-start justify-between mb-4">
|
|
<div className="flex-1">
|
|
<h3 className="font-bold text-lg text-slate-900">{dimension.title}</h3>
|
|
<p className="text-xs text-slate-500 mt-1">{dimension.name}</p>
|
|
</div>
|
|
{dimension.score >= 86 && (
|
|
<span className="text-2xl">⭐</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Score Indicator */}
|
|
<div className="mb-5">
|
|
<ScoreIndicator
|
|
score={dimension.score}
|
|
benchmark={dimension.percentile || 50}
|
|
/>
|
|
</div>
|
|
|
|
{/* Summary Description */}
|
|
<p className="text-sm text-slate-700 flex-grow mb-4 leading-relaxed">
|
|
{dimension.summary}
|
|
</p>
|
|
|
|
{/* KPI Display */}
|
|
{dimension.kpi && (
|
|
<div className="bg-white rounded-lg p-3 mb-4 border border-slate-200">
|
|
<p className="text-xs text-slate-500 uppercase font-semibold mb-1">
|
|
{dimension.kpi.label}
|
|
</p>
|
|
<div className="flex items-baseline gap-2">
|
|
<p className="text-2xl font-bold text-slate-900">{dimension.kpi.value}</p>
|
|
{dimension.kpi.change && (
|
|
<span className={`text-xs font-semibold px-2 py-1 rounded-full ${
|
|
dimension.kpi.changeType === 'positive'
|
|
? 'bg-emerald-100 text-emerald-700'
|
|
: 'bg-red-100 text-red-700'
|
|
}`}>
|
|
{dimension.kpi.change}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Action Button */}
|
|
<motion.button
|
|
whileHover={{ scale: 1.02 }}
|
|
whileTap={{ scale: 0.98 }}
|
|
className={`w-full py-2 px-4 rounded-lg font-semibold flex items-center justify-center gap-2 transition-colors ${
|
|
dimension.score < 51
|
|
? 'bg-red-500 text-white hover:bg-red-600'
|
|
: dimension.score < 71
|
|
? 'bg-amber-500 text-white hover:bg-amber-600'
|
|
: 'bg-slate-300 text-slate-600 cursor-default'
|
|
}`}
|
|
disabled={dimension.score >= 71}
|
|
>
|
|
<Zap size={16} />
|
|
{dimension.score < 51
|
|
? t('dimensionCard.viewCriticalActions')
|
|
: dimension.score < 71
|
|
? t('dimensionCard.exploreImprovements')
|
|
: t('dimensionCard.inGoodState')}
|
|
</motion.button>
|
|
</motion.div>
|
|
);
|
|
};
|
|
|
|
export default DimensionCard;
|