Initial commit

This commit is contained in:
2026-08-07 23:53:15 +05:30
commit 90f14a73b2
222 changed files with 35000 additions and 0 deletions

View File

@@ -0,0 +1,259 @@
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>
);
}