Files
krow_talent_app/src/components/krow/talent/IdentityCareerCard.jsx
2026-08-07 23:53:15 +05:30

259 lines
14 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useRef, useState } from 'react';
import html2canvas from 'html2canvas';
import { Wallet, Smartphone, Image as ImageIcon, ShieldCheck, Briefcase, Sparkles, QrCode, Download, Check, Info, RefreshCw } from 'lucide-react';
import { toast } from 'react-hot-toast';
import { Image } from '@/components/ui/image';
import { getTalentScore, getScoreBand, getGrowth } from '@/lib/talentHome';
const clamp = (n, min = 0, max = 100) => Math.max(min, Math.min(max, Math.round(Number(n) || 0)));
// Employee Satisfaction — composite of attendance, performance, client & supervisor ratings
export function getESAT(profile = {}) {
const att = clamp(profile.attendance_score ?? 100);
const perf = clamp(profile.performance_score);
const client = ((Number(profile.client_rating) || 0) / 5) * 100;
const sup = ((Number(profile.supervisor_rating) || 0) / 5) * 100;
const hasRatings = client > 0 || sup > 0;
if (!hasRatings) return clamp(att * 0.6 + perf * 0.4);
return clamp(att * 0.3 + perf * 0.25 + client * 0.225 + sup * 0.225);
}
// Display the career score on a FICO-style 300–850 scale ("like checking your credit score")
const toFICO = (s) => 300 + Math.round((clamp(s) / 100) * 550);
function buildVCard(profile) {
const name = profile.full_name || 'KROW Talent';
const [first, ...rest] = name.split(' ');
const last = rest.join(' ');
const lines = [
'BEGIN:VCARD', 'VERSION:3.0',
`N:${last};${first};;;`, `FN:${name}`,
profile.current_position ? `TITLE:${profile.current_position}` : '',
profile.phone ? `TEL;TYPE=CELL:${profile.phone}` : '',
profile.email ? `EMAIL:${profile.email}` : '',
profile.address ? `ADR:;;${profile.address};;;;` : '',
`URL:https://krow.app/me`,
`NOTE:KROW ID ${(profile.id || '').slice(-6).toUpperCase()} · Talent Score ${profile.krow_score || 0}`,
'END:VCARD',
].filter(Boolean);
return lines.join('\n');
}
function download(filename, content, type) {
const blob = new Blob([content], { type });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = filename; a.click();
URL.revokeObjectURL(url);
}
const BLUE = '#0055FF';
const TRACK = '#E1E5E9';
const ARC_R = 64;
const CX = 80;
const CY = 80;
function arcPoint(p) {
const ang = Math.PI - p * Math.PI;
return { x: CX + ARC_R * Math.cos(ang), y: CY - ARC_R * Math.sin(ang) };
}
function arcPath(startP, endP) {
const a = arcPoint(startP);
const b = arcPoint(endP);
const large = endP - startP > 0.5 ? 1 : 0;
return `M ${a.x} ${a.y} A ${ARC_R} ${ARC_R} 0 ${large} 1 ${b.x} ${b.y}`;
}
function Gauge({ fico }) {
const pct = Math.max(0, Math.min(1, (fico - 300) / 550));
const gap = 0.02;
const filledEnd = Math.max(gap, Math.min(1 - gap, pct));
const marker = arcPoint(filledEnd);
return (
<div className="relative mx-auto" style={{ width: CX * 2, height: CY + 18 }}>
<svg width={CX * 2} height={CY + 18} viewBox={`0 0 ${CX * 2} ${CY + 18}`}>
<path d={arcPath(gap, 1 - gap)} fill="none" stroke={TRACK} strokeWidth="12" strokeLinecap="round" />
{pct > gap && (
<path d={arcPath(gap, filledEnd)} fill="none" stroke={BLUE} strokeWidth="12" strokeLinecap="round" />
)}
<circle cx={marker.x} cy={marker.y} r="8" fill={BLUE} stroke="#fff" strokeWidth="3" />
</svg>
<div className="absolute inset-x-0 bottom-1 flex flex-col items-center">
<span className="text-[9px] uppercase tracking-wide text-[#A0AAB9] font-semibold">Career Score</span>
<span className="text-[28px] font-bold leading-none mt-0.5" style={{ color: BLUE }}>{fico}</span>
<span className="text-[10px] text-[#8690A1] font-medium mt-0.5">300-850</span>
</div>
</div>
);
}
function FactorPill({ label, value }) {
return (
<span className="inline-flex items-center gap-1.5 rounded-full bg-white/70 border border-[#E6ECF5] px-3 py-1.5 text-[11px] font-medium text-[#374151] backdrop-blur-sm">
<span className="text-[#9CA3AF]">{label}</span>
<span className="font-semibold text-[#0A39DF]">{value}</span>
</span>
);
}
function ReportRow({ icon: Icon, label, value, tone }) {
const valColor = tone === 'verified' ? 'text-[#10B981]' : tone === 'amber' ? 'text-[#D97706]' : 'text-[#0F172A]';
return (
<div className="flex items-center justify-between gap-3 py-2 border-b border-white/60 last:border-0">
<div className="flex items-center gap-2 text-[#6B7280]">
<Icon className="w-4 h-4" />
<span className="text-[12px] font-medium">{label}</span>
</div>
<span className={`inline-flex items-center gap-1 rounded-full bg-white/80 border border-[#E6ECF5] px-2.5 py-1 text-[11px] font-semibold ${valColor}`}>
{tone === 'verified' && <Check className="w-3 h-3" />}{value}
</span>
</div>
);
}
export default function IdentityCareerCard({ profile }) {
const stackRef = useRef(null);
const [busy, setBusy] = useState(false);
const score = getTalentScore(profile);
const band = getScoreBand(score);
const growth = getGrowth(profile);
const esat = getESAT(profile);
const fico = toFICO(score);
const shortId = (profile.id || '').slice(-6).toUpperCase();
const name = profile.full_name || 'KROW Talent';
const title = profile.current_position || profile.desired_position || 'Open to work';
const initials = name.split(' ').map(p => p[0]).slice(0, 2).join('').toUpperCase();
const experience = profile.experience || [];
const identityOk = !!profile.selfie_url;
const vcard = buildVCard(profile);
const qrSrc = `https://api.qrserver.com/v1/create-qr-code/?margin=0&size=160x160&data=${encodeURIComponent(vcard)}`;
const updated = new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
const addToContacts = () => {
download(`${name.replace(/\s+/g, '_')}.vcf`, vcard, 'text/vcard');
toast.success('Contact file downloaded — open it to add to your phone.');
};
const saveReport = async () => {
if (!stackRef.current) return;
setBusy(true);
try {
const canvas = await html2canvas(stackRef.current, { backgroundColor: null, scale: 2 });
canvas.toBlob((blob) => {
if (!blob) return;
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = `${name.replace(/\s+/g, '_')}_KROW_ID.png`; a.click();
URL.revokeObjectURL(url);
toast.success('Holographic ID saved to your downloads.');
});
} catch { toast.error('Could not generate ID image.'); }
finally { setBusy(false); }
};
const walletNotice = () => toast('Wallet passes need Apple/Google pass certificates. Use Add to Contacts for now.', { icon: 'ℹ️' });
return (
<div className="flex justify-center">
<div className="relative w-full max-w-md" style={{ perspective: '1400px' }}>
{/* Stacked depth layers (behind) */}
<div aria-hidden className="absolute inset-0 rounded-[28px] bg-white/50 border border-white/60 backdrop-blur-xl shadow-[0_20px_60px_-20px_rgba(10,57,223,0.35)]"
style={{ transform: 'translateY(18px) translateX(14px) rotateY(-10deg) rotateX(6deg)' }} />
<div aria-hidden className="absolute inset-0 rounded-[28px] bg-white/40 border border-white/70 backdrop-blur-xl shadow-[0_16px_50px_-18px_rgba(10,57,223,0.3)]"
style={{ transform: 'translateY(9px) translateX(7px) rotateY(-5deg) rotateX(3deg)' }} />
{/* Front holographic pass */}
<div ref={stackRef} className="relative rounded-[28px] border border-white/80 backdrop-blur-2xl shadow-[0_24px_70px_-24px_rgba(10,57,223,0.45)] overflow-hidden"
style={{ background: 'linear-gradient(160deg, rgba(255,255,255,0.92) 0%, rgba(244,247,255,0.88) 100%)' }}>
{/* holographic sheen */}
<div aria-hidden className="pointer-events-none absolute inset-0" style={{ background: 'linear-gradient(115deg, transparent 30%, rgba(10,57,223,0.05) 45%, rgba(249,229,71,0.06) 55%, transparent 70%)' }} />
<div className="relative p-5 text-center">
{/* Identity header */}
<div className="mx-auto w-16 h-16 rounded-full overflow-hidden border-4 border-white shadow-md bg-[#EFF6FF] flex items-center justify-center mb-2">
{profile.selfie_url ? (
<Image src={profile.selfie_url} alt={name} className="w-full h-full object-cover" fittingType="fill" />
) : (
<span className="text-[20px] font-bold text-[#0A39DF]">{initials}</span>
)}
</div>
<p className="text-[10px] font-semibold uppercase tracking-[0.12em] text-[#0A39DF]">KROW ID · {shortId}</p>
<h2 className="text-[20px] font-bold text-[#0F172A] tracking-tight mt-1">{name}</h2>
<p className="text-[12px] text-[#6B7280]">{title}</p>
<div className="mt-1.5 inline-flex items-center gap-1 text-[10px] text-[#9CA3AF]">
<ShieldCheck className="w-3 h-3" /> Career Credit Report · {updated}
</div>
{/* Career Score gauge */}
<div className="mt-4">
<Gauge fico={fico} />
<span className="inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-[12px] font-bold mt-2"
style={{ color: BLUE, background: '#F0F4F9' }}>
{band.label}
<Info className="w-3 h-3" style={{ color: BLUE }} />
</span>
<div className="inline-flex items-center gap-1.5 mt-2 text-[11px] text-[#7D8590]">
<RefreshCw className="w-3 h-3" />
Next update in 31 days
</div>
</div>
{/* Score factors */}
<div className="mt-4 text-left">
<p className="text-[10px] uppercase tracking-wide text-[#9CA3AF] mb-2">Score factors</p>
<div className="flex flex-wrap gap-1.5 justify-center">
<FactorPill label="Reliability" value={clamp(profile.reliability_score)} />
<FactorPill label="Attendance" value={clamp(profile.attendance_score ?? 100)} />
<FactorPill label="Performance" value={clamp(profile.performance_score)} />
<FactorPill label="Leadership" value={clamp(profile.leadership_potential)} />
<FactorPill label="AI Interview" value={clamp(profile.ai_interview_score)} />
<FactorPill label="XP Earned" value={Number(profile.xp) || 0} />
</div>
</div>
{/* Report rows */}
<div className="mt-4 text-left rounded-2xl bg-white/60 border border-white/70 p-3 backdrop-blur-sm">
<ReportRow icon={ShieldCheck} label="Identity" value={identityOk ? 'Verified' : 'Pending'} tone={identityOk ? 'verified' : 'amber'} />
<ReportRow icon={Briefcase} label="Job History" value={`${experience.length} ${experience.length === 1 ? 'role' : 'roles'}`} />
<ReportRow icon={Sparkles} label="ESAT" value={`${esat}/100`} tone={esat >= 75 ? 'verified' : esat >= 40 ? 'amber' : 'neutral'} />
</div>
{/* Job history detail */}
{experience.length > 0 && (
<div className="mt-3 text-left">
{experience.map((job, i) => (
<div key={i} className="flex items-center justify-between gap-3 py-1.5">
<div className="min-w-0">
<p className="text-[12px] font-medium text-[#0F172A] truncate">{job.role || 'Role'}</p>
<p className="text-[10px] text-[#9CA3AF]">{job.company || 'Company'}</p>
</div>
<span className="text-[10px] font-semibold text-[#0A39DF] whitespace-nowrap">{job.years ? `${job.years} yr${job.years === 1 ? '' : 's'}` : '—'}</span>
</div>
))}
</div>
)}
{/* Take your KROW ID */}
<div className="mt-4 text-left">
<p className="text-[10px] uppercase tracking-wide text-[#9CA3AF] mb-2 flex items-center gap-1.5"><Download className="w-3 h-3" /> Take your KROW ID</p>
<div className="flex items-center gap-3">
<div className="rounded-xl bg-white p-1.5 border border-[#E6ECF5] shadow-sm shrink-0">
<img src={qrSrc} alt="KROW Identity QR" width={64} height={64} className="block" />
</div>
<div className="flex-1 flex flex-col gap-1.5">
<div className="grid grid-cols-2 gap-1.5">
<button onClick={walletNotice} className="flex items-center justify-center gap-1 h-9 rounded-xl bg-[#0A39DF] text-white text-[10px] font-semibold hover:bg-[#082EB4] transition-colors"><Wallet className="w-3 h-3" /> Apple</button>
<button onClick={walletNotice} className="flex items-center justify-center gap-1 h-9 rounded-xl bg-white border border-[#E6ECF5] text-[#0F172A] text-[10px] font-semibold hover:border-[#0A39DF] transition-colors"><Wallet className="w-3 h-3 text-[#0A39DF]" /> Google</button>
</div>
<div className="grid grid-cols-2 gap-1.5">
<button onClick={addToContacts} className="flex items-center justify-center gap-1 h-9 rounded-xl bg-white border border-[#E6ECF5] text-[#0F172A] text-[10px] font-semibold hover:border-[#0A39DF] transition-colors"><Smartphone className="w-3 h-3 text-[#0A39DF]" /> Contacts</button>
<button onClick={saveReport} disabled={busy} className="flex items-center justify-center gap-1 h-9 rounded-xl bg-white border border-[#E6ECF5] text-[#0F172A] text-[10px] font-semibold hover:border-[#0A39DF] transition-colors disabled:opacity-60"><ImageIcon className="w-3 h-3 text-[#0A39DF]" /> {busy ? '…' : 'Save'}</button>
</div>
</div>
</div>
<p className="text-[10px] text-[#9CA3AF] mt-2 flex items-center gap-1 justify-center"><QrCode className="w-3 h-3" /> Scan to share your verified KROW Identity</p>
</div>
</div>
</div>
</div>
</div>
);
}