CV-Analyser / frontend /src /components /RankingTable.jsx
Adeen
Prepare for Hugging Face deployment
c7fb8cf
/**
* RankingTable.jsx
* Displays the ranked list of candidates with scores, badges, and selection status.
* Each row is clickable to show the full detail panel.
*/
import { Trophy, ChevronRight, CheckCircle, XCircle, TrendingUp } from 'lucide-react';
import ScoreRing from './ScoreRing';
function SkillBadge({ skill, variant = 'green' }) {
return (
<span className={`badge-${variant} mr-1 mb-1`}>{skill}</span>
);
}
function RankIcon({ rank }) {
if (rank === 1) return <span className="text-lg">🥇</span>;
if (rank === 2) return <span className="text-lg">🥈</span>;
if (rank === 3) return <span className="text-lg">🥉</span>;
return (
<span className="w-6 h-6 rounded-full bg-slate-800 border border-slate-700 flex items-center justify-center text-xs text-slate-400 font-semibold">
{rank}
</span>
);
}
export default function RankingTable({ candidates, selectedId, onSelectCandidate, jobTitle }) {
if (!candidates || candidates.length === 0) return null;
return (
<div className="space-y-4 animate-slide-up">
{/* Table header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold text-white flex items-center gap-2">
<Trophy size={20} className="text-amber-400" />
Ranked Candidates
</h2>
<p className="text-slate-400 text-sm mt-0.5">
{candidates.length} candidates screened for <span className="text-primary-300 font-medium">{jobTitle}</span>
</p>
</div>
<div className="flex items-center gap-3 text-xs text-slate-500">
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full bg-emerald-500 inline-block" /> Selected (≥50)</span>
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full bg-red-500 inline-block" /> Rejected</span>
</div>
</div>
{/* Desktop table */}
<div className="card overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-slate-800 bg-slate-900/80">
<th className="text-left px-5 py-3.5 text-xs font-semibold text-slate-500 uppercase tracking-wider w-12">Rank</th>
<th className="text-left px-5 py-3.5 text-xs font-semibold text-slate-500 uppercase tracking-wider">Candidate</th>
<th className="text-center px-4 py-3.5 text-xs font-semibold text-slate-500 uppercase tracking-wider">Score</th>
<th className="text-left px-4 py-3.5 text-xs font-semibold text-slate-500 uppercase tracking-wider">Skills Match</th>
<th className="text-center px-4 py-3.5 text-xs font-semibold text-slate-500 uppercase tracking-wider hidden md:table-cell">Exp Fit</th>
<th className="text-center px-4 py-3.5 text-xs font-semibold text-slate-500 uppercase tracking-wider hidden lg:table-cell">Edu Fit</th>
<th className="text-center px-4 py-3.5 text-xs font-semibold text-slate-500 uppercase tracking-wider">Status</th>
<th className="px-4 py-3.5 w-10"></th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800">
{candidates.map((c) => {
const isSelected = selectedId === c.rank;
const totalReq = c.matched_skills.length + c.missing_skills.length;
const skillPct = totalReq > 0
? Math.round((c.matched_skills.length / totalReq) * 100)
: 100;
return (
<tr
key={c.rank}
id={`candidate-row-${c.rank}`}
onClick={() => onSelectCandidate(c)}
className={`cursor-pointer transition-all duration-150 ${
isSelected
? 'bg-primary-900/20 border-l-2 border-l-primary-500'
: 'hover:bg-slate-800/40'
}`}
>
{/* Rank */}
<td className="px-5 py-4">
<div className="flex items-center justify-center">
<RankIcon rank={c.rank} />
</div>
</td>
{/* Candidate Info */}
<td className="px-5 py-4">
<div className="font-semibold text-slate-100">{c.name}</div>
<div className="text-xs text-slate-500 mt-0.5 truncate max-w-[180px]">{c.filename}</div>
</td>
{/* Score Ring */}
<td className="px-4 py-4 text-center">
<div className="flex flex-col items-center gap-1">
<ScoreRing score={c.final_score} size={52} />
<span className="text-xs text-slate-500">{c.match_percent}%</span>
</div>
</td>
{/* Skills Match */}
<td className="px-4 py-4">
<div className="flex items-center gap-2 mb-1.5">
<div className="flex-1 h-1.5 bg-slate-800 rounded-full overflow-hidden">
<div
className="h-full rounded-full transition-all duration-700"
style={{
width: `${skillPct}%`,
backgroundColor: skillPct >= 70 ? '#10b981' : skillPct >= 40 ? '#f59e0b' : '#ef4444'
}}
/>
</div>
<span className="text-xs text-slate-400 w-8 text-right">{skillPct}%</span>
</div>
<div className="flex flex-wrap">
{c.matched_skills.slice(0, 3).map(s => (
<SkillBadge key={s} skill={s} variant="green" />
))}
{c.matched_skills.length > 3 && (
<span className="text-xs text-slate-500 self-center">+{c.matched_skills.length - 3}</span>
)}
{c.matched_skills.length === 0 && (
<span className="text-xs text-slate-600 italic">None matched</span>
)}
</div>
</td>
{/* Experience Fit */}
<td className="px-4 py-4 text-center hidden md:table-cell">
<ScorePill score={c.experience_score} />
</td>
{/* Education Fit */}
<td className="px-4 py-4 text-center hidden lg:table-cell">
<div className="text-xs text-slate-400">{c.education_degree}</div>
<ScorePill score={c.education_score} />
</td>
{/* Status Badge */}
<td className="px-4 py-4 text-center">
{c.status === 'Selected' ? (
<span className="badge-green flex items-center gap-1 justify-center whitespace-nowrap">
<CheckCircle size={12} /> Selected
</span>
) : (
<span className="badge-red flex items-center gap-1 justify-center whitespace-nowrap">
<XCircle size={12} /> Rejected
</span>
)}
</td>
{/* Arrow */}
<td className="px-4 py-4">
<ChevronRight size={16} className={`transition-colors ${isSelected ? 'text-primary-400' : 'text-slate-700'}`} />
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
</div>
);
}
function ScorePill({ score }) {
const color = score >= 80 ? 'text-emerald-400' : score >= 50 ? 'text-amber-400' : 'text-red-400';
return <span className={`text-sm font-semibold ${color}`}>{score.toFixed(0)}</span>;
}