Initial commit
This commit is contained in:
246
src/components/krow/TalentPoolCard.jsx
Normal file
246
src/components/krow/TalentPoolCard.jsx
Normal file
@@ -0,0 +1,246 @@
|
||||
import React, { useState } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Mail, Briefcase, MapPin, Star, BadgeCheck, Award, ChevronDown } from 'lucide-react';
|
||||
import { Image } from '@/components/ui/image';
|
||||
import EmployerInsights from '@/components/krow/talent/EmployerInsights';
|
||||
import { getScoreBand, toFICO } from '@/lib/talentHome';
|
||||
|
||||
const PORTRAIT_PHOTOS = [
|
||||
'photo-1500648767731-7d3514ad8b31',
|
||||
'photo-1531123897727-8f129e1688ce',
|
||||
'photo-1573496359142-b8d87734a5a2',
|
||||
'photo-1487412720507-e7ab37603c6f',
|
||||
'photo-1502685104226-ee3a97a2f2c8',
|
||||
'photo-1544005313-94ddf0286df2',
|
||||
'photo-1494790108377-be9c29b29330',
|
||||
'photo-1438761681033-6461ffad8d80',
|
||||
'photo-1517841905240-472988babdf9',
|
||||
'photo-1463453091185-61582044d556',
|
||||
];
|
||||
|
||||
function getPortraitSeed(name) {
|
||||
let h = 0;
|
||||
for (let i = 0; i < (name || '').length; i++) h = (h * 31 + name.charCodeAt(i)) >>> 0;
|
||||
return PORTRAIT_PHOTOS[h % PORTRAIT_PHOTOS.length];
|
||||
}
|
||||
|
||||
function CareerGauge({ fico }) {
|
||||
const MIN = 300;
|
||||
const MAX = 850;
|
||||
const value = Math.max(MIN, Math.min(MAX, fico || MIN));
|
||||
const frac = (value - MIN) / (MAX - MIN);
|
||||
const cx = 55;
|
||||
const cy = 58;
|
||||
const r = 46;
|
||||
const theta = (180 - frac * 180) * (Math.PI / 180);
|
||||
const xv = cx + r * Math.cos(theta);
|
||||
const yv = cy - r * Math.sin(theta);
|
||||
const color = value > MIN ? '#0A39DF' : '#CBD5E1';
|
||||
return (
|
||||
<div className="flex flex-col items-center shrink-0">
|
||||
<span className="text-[8px] font-semibold text-[#8A94A4] tracking-[0.12em] mb-0.5">CAREER SCORE</span>
|
||||
<svg width="110" height="64" viewBox="0 0 110 64" className="overflow-visible">
|
||||
<path d={`M ${cx - r} ${cy} A ${r} ${r} 0 0 1 ${cx + r} ${cy}`} fill="none" stroke="#E1E4E8" strokeWidth="8" strokeLinecap="round" />
|
||||
{value > MIN && (
|
||||
<>
|
||||
<path d={`M ${cx - r} ${cy} A ${r} ${r} 0 0 1 ${xv} ${yv}`} fill="none" stroke={color} strokeWidth="8" strokeLinecap="round" />
|
||||
<circle cx={xv} cy={yv} r="5.5" fill={color} stroke="#fff" strokeWidth="1.5" />
|
||||
</>
|
||||
)}
|
||||
<text x={cx} y={cy - 6} textAnchor="middle" className="fill-[#0A39DF]" style={{ fontSize: 20, fontWeight: 700 }}>{value}</text>
|
||||
<text x={cx} y={cy + 9} textAnchor="middle" fill="#8A94A4" style={{ fontSize: 8, fontWeight: 600 }}>300-850</text>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const BADGE_LEVEL_STYLES = {
|
||||
bronze: 'bg-[#FEF3C7] text-[#92400E]',
|
||||
silver: 'bg-[#F3F4F6] text-[#4B5563]',
|
||||
gold: 'bg-[#FEF9E7] text-[#92400E]',
|
||||
platinum: 'bg-[#EFF6FF] text-[#1E40AF]',
|
||||
};
|
||||
|
||||
export default function TalentPoolCard({ profile, onContact, onView }) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
const score = profile.krow_score || 0;
|
||||
const band = getScoreBand(score);
|
||||
const fico = toFICO(score);
|
||||
const hasScore = score > 0;
|
||||
const skills = (profile.skills || []).slice(0, 4);
|
||||
const extraSkills = (profile.skills || []).length - skills.length;
|
||||
const badges = (profile.earned_badges || []).slice(0, 3);
|
||||
const extraBadges = (profile.earned_badges || []).length - badges.length;
|
||||
const capabilities = profile.capabilities || [];
|
||||
const endorsements = capabilities.filter((c) => c.status === 'verified' || c.status === 'pending').slice(0, 3);
|
||||
const extraEndorsements = capabilities.length - endorsements.length;
|
||||
|
||||
const toggle = (e) => {
|
||||
e.stopPropagation();
|
||||
setExpanded((v) => !v);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'bg-white border border-[#E5E7EB] rounded-xl transition-all overflow-hidden',
|
||||
expanded ? 'shadow-sm border-[#0A39DF]' : 'hover:shadow-sm hover:border-[#0A39DF]'
|
||||
)}
|
||||
>
|
||||
{/* Clickable header → opens full profile modal */}
|
||||
<div
|
||||
className="p-4 cursor-pointer"
|
||||
onClick={() => onView?.(profile)}
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
{/* Avatar */}
|
||||
<div className="w-14 h-14 rounded-full bg-[#E5E7EB] overflow-hidden shrink-0">
|
||||
<Image
|
||||
src={profile.selfie_url || `https://images.unsplash.com/${getPortraitSeed(profile.full_name)}?w=140&h=140&fit=crop&crop=faces`}
|
||||
alt={profile.full_name}
|
||||
className="w-full h-full object-cover"
|
||||
fittingType="fill"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<h3 className="font-semibold text-[#111827] text-[15px] truncate">{profile.full_name}</h3>
|
||||
<span
|
||||
className="px-2 py-0.5 rounded-full text-[10px] font-bold"
|
||||
style={hasScore
|
||||
? { backgroundColor: band.color, color: '#fff' }
|
||||
: { backgroundColor: '#E5E7EB', color: '#6B7280' }}
|
||||
>
|
||||
{hasScore ? band.label : 'No score yet'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Career Score gauge */}
|
||||
<CareerGauge fico={fico} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expanded details */}
|
||||
{expanded && (
|
||||
<div className="px-4 pb-4 space-y-3">
|
||||
{/* Quick meta line */}
|
||||
<div className="flex items-center gap-3 text-[11px] text-[#6B7280] flex-wrap">
|
||||
{profile.desired_position && (
|
||||
<span className="inline-flex items-center gap-1 font-medium text-[#0A39DF]">
|
||||
<Briefcase className="w-3 h-3" />
|
||||
{profile.desired_position}
|
||||
</span>
|
||||
)}
|
||||
{profile.experience_years > 0 && (
|
||||
<span>{profile.experience_years}yr exp</span>
|
||||
)}
|
||||
{profile.location && (
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<MapPin className="w-3 h-3" />
|
||||
{profile.location}
|
||||
</span>
|
||||
)}
|
||||
{(profile.client_rating > 0 || profile.supervisor_rating > 0) && (
|
||||
<span className="inline-flex items-center gap-1 text-[#F59E0B] font-medium">
|
||||
<Star className="w-3 h-3 fill-[#F59E0B]" />
|
||||
{((profile.client_rating + profile.supervisor_rating) / 2).toFixed(1)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{skills.length > 0 && (
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
{skills.map((s) => (
|
||||
<span key={s} className="px-2 py-0.5 rounded-full bg-[#EFF6FF] text-[#0A39DF] text-[10px] font-medium">{s}</span>
|
||||
))}
|
||||
{extraSkills > 0 && (
|
||||
<span className="px-2 py-0.5 rounded-full bg-[#F3F4F6] text-[#6B7280] text-[10px] font-medium">+{extraSkills}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(profile.availability || []).length > 0 && (
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
{(profile.availability || []).slice(0, 3).map((a) => (
|
||||
<span key={a} className="px-2 py-0.5 rounded-full bg-[#F3F4F6] text-[#374151] text-[10px]">{a}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Badges */}
|
||||
{badges.length > 0 && (
|
||||
<div className="pt-3 border-t border-[#F3F4F6]">
|
||||
<p className="text-[10px] font-bold text-[#9CA3AF] tracking-wider mb-2 flex items-center gap-1.5">
|
||||
<Award className="w-3 h-3" /> BADGES
|
||||
</p>
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
{badges.map((b, i) => (
|
||||
<span key={i} className={cn('inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-medium', BADGE_LEVEL_STYLES[b.level] || BADGE_LEVEL_STYLES.bronze)}>
|
||||
<Award className="w-2.5 h-2.5" />
|
||||
{b.name}
|
||||
</span>
|
||||
))}
|
||||
{extraBadges > 0 && (
|
||||
<span className="px-2 py-0.5 rounded-full bg-[#F3F4F6] text-[#6B7280] text-[10px] font-medium">+{extraBadges}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Endorsements */}
|
||||
{endorsements.length > 0 && (
|
||||
<div className="pt-3 border-t border-[#F3F4F6]">
|
||||
<p className="text-[10px] font-bold text-[#9CA3AF] tracking-wider mb-2 flex items-center gap-1.5">
|
||||
<BadgeCheck className="w-3 h-3" /> ENDORSEMENTS
|
||||
<span className="text-[#D1D5DB] font-semibold">({capabilities.length})</span>
|
||||
</p>
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
{endorsements.map((e, i) => (
|
||||
<span key={i} className={cn('inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-medium border', e.status === 'verified' ? 'bg-[#EFF6FF] text-[#0A39DF] border-[#DBEAFE]' : 'bg-[#F3F4F6] text-[#6B7280] border-[#E5E7EB]')}>
|
||||
<BadgeCheck className="w-2.5 h-2.5" />
|
||||
{e.skill}
|
||||
{e.level && <span className="opacity-70">· {e.level}</span>}
|
||||
</span>
|
||||
))}
|
||||
{extraEndorsements > 0 && (
|
||||
<span className="px-2 py-0.5 rounded-full bg-[#F3F4F6] text-[#6B7280] text-[10px] font-medium">+{extraEndorsements}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Employer insights */}
|
||||
<div className="pt-3 border-t border-[#F3F4F6]">
|
||||
<EmployerInsights profile={profile} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer: email + contact + expand toggle */}
|
||||
<div className="flex items-center gap-2 px-4 py-3 border-t border-[#F3F4F6]">
|
||||
<span className="text-[11px] text-[#9CA3AF] flex-1 truncate">{profile.email || 'No email'}</span>
|
||||
{onContact && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onContact(profile); }}
|
||||
className="inline-flex items-center gap-1.5 bg-[#0A39DF] hover:bg-[#082EB4] text-white text-[12px] font-medium px-3 py-1.5 rounded-lg transition-colors whitespace-nowrap"
|
||||
>
|
||||
<Mail className="w-3.5 h-3.5" />
|
||||
Contact
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={toggle}
|
||||
className="inline-flex items-center gap-1 border border-[#E5E7EB] hover:bg-[#F9FAFB] text-[#374151] text-[12px] font-medium px-2.5 py-1.5 rounded-lg transition-colors"
|
||||
aria-expanded={expanded}
|
||||
>
|
||||
<ChevronDown className={cn('w-3.5 h-3.5 transition-transform', expanded && 'rotate-180')} />
|
||||
{expanded ? 'Hide' : 'Details'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user