Initial commit
This commit is contained in:
36
src/components/krow/talent/CareerGrowth.jsx
Normal file
36
src/components/krow/talent/CareerGrowth.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
|
||||
const LADDER = ['Beginner', 'Cross-Trained', 'Skilled', 'Lead', 'Manager'];
|
||||
|
||||
export default function CareerGrowth({ profile }) {
|
||||
const lp = Number(profile.leadership_potential) || 0;
|
||||
const currentIdx = lp >= 80 ? 3 : lp >= 60 ? 2 : lp >= 40 ? 1 : 0;
|
||||
return (
|
||||
<section className="rounded-2xl bg-white border border-[#E5E7EB] p-6 sm:p-8">
|
||||
<h2 className="text-[18px] font-bold text-[#0F172A]">Career Growth</h2>
|
||||
<p className="text-[13px] text-[#6B7280] mb-5">People don't just work. They level up.</p>
|
||||
<div className="flex flex-col">
|
||||
{LADDER.map((step, i) => {
|
||||
const done = i < currentIdx;
|
||||
const current = i === currentIdx;
|
||||
const last = i === LADDER.length - 1;
|
||||
return (
|
||||
<div key={step} className="flex gap-3">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className={`w-9 h-9 rounded-full flex items-center justify-center text-[12px] font-bold shrink-0 ${current ? 'bg-[#0A39DF] text-white' : done ? 'bg-[#ECFDF5] text-[#16A34A] border border-[#A7F3D0]' : 'bg-[#F3F4F6] text-[#9CA3AF]'}`}>
|
||||
{done ? '✓' : i + 1}
|
||||
</div>
|
||||
{!last && <div className="w-0.5 flex-1 bg-[#E5E7EB] my-1 min-h-[16px]" />}
|
||||
</div>
|
||||
<div className={`pb-4 pt-1.5 text-[14px] font-medium ${current ? 'text-[#0A39DF]' : done ? 'text-[#0F172A]' : 'text-[#9CA3AF]'}`}>
|
||||
{step}
|
||||
{current && <span className="ml-2 text-[11px] font-normal text-[#6B7280]">You are here</span>}
|
||||
{last && <span className="ml-2 text-[11px] font-normal text-[#6B7280]">Target</span>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
70
src/components/krow/talent/CareerPassport.jsx
Normal file
70
src/components/krow/talent/CareerPassport.jsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import { ShieldCheck } from 'lucide-react';
|
||||
|
||||
const ITEMS = [
|
||||
'Identity',
|
||||
'Food Handler',
|
||||
'RBS',
|
||||
'Allergens',
|
||||
'Knife Skills',
|
||||
'References',
|
||||
'AI Interview',
|
||||
'Background Check',
|
||||
];
|
||||
|
||||
export default function CareerPassport({ profile }) {
|
||||
const certs = (profile.certifications || []).map((c) => c.toLowerCase());
|
||||
const skills = (profile.skills || []).map((s) => s.toLowerCase());
|
||||
const badges = (profile.earned_badges || []).map((b) => (b.name || '').toLowerCase());
|
||||
|
||||
const has = (label) => {
|
||||
switch (label) {
|
||||
case 'Identity':
|
||||
return !!profile.selfie_url || !!profile.full_name;
|
||||
case 'Food Handler':
|
||||
return certs.some((c) => c.includes('food'));
|
||||
case 'RBS':
|
||||
return certs.some((c) => c.includes('rbs'));
|
||||
case 'Allergens':
|
||||
return certs.some((c) => c.includes('allergen'));
|
||||
case 'Knife Skills':
|
||||
return skills.some((s) => s.includes('knife')) || badges.some((b) => b.includes('knife'));
|
||||
case 'References':
|
||||
return (profile.experience || []).length >= 2;
|
||||
case 'AI Interview':
|
||||
return (profile.ai_interview_score || 0) > 0;
|
||||
case 'Background Check':
|
||||
return (profile.shifts_completed || 0) >= 10;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const done = ITEMS.filter(has);
|
||||
const pct = Math.round((done.length / ITEMS.length) * 100);
|
||||
|
||||
return (
|
||||
<section className="rounded-2xl bg-white border border-[#E5E7EB] p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-[15px] font-semibold text-[#0F172A]">Career Passport</h2>
|
||||
<span className="text-[12px] font-semibold text-[#0A39DF]">{pct}% Complete</span>
|
||||
</div>
|
||||
<div className="mt-4 grid grid-cols-1 sm:grid-cols-3 gap-2">
|
||||
{ITEMS.map((label) => {
|
||||
const ok = has(label);
|
||||
return (
|
||||
<div
|
||||
key={label}
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-xl border ${
|
||||
ok ? 'border-[#BBF7D0] bg-[#F0FDF4]' : 'border-[#E5E7EB] bg-[#F8FAFC]'
|
||||
}`}
|
||||
>
|
||||
<ShieldCheck className={`w-4 h-4 shrink-0 ${ok ? 'text-[#16A34A]' : 'text-[#CBD5E1]'}`} />
|
||||
<span className={`text-[13px] ${ok ? 'text-[#0F172A] font-medium' : 'text-[#94A3B8]'}`}>{label}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
63
src/components/krow/talent/CareerScoreGauge.jsx
Normal file
63
src/components/krow/talent/CareerScoreGauge.jsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import { Info, RefreshCw } from 'lucide-react';
|
||||
import { getTalentScore, toFICO, getScoreBand } from '@/lib/talentHome';
|
||||
|
||||
const FICO_MIN = 300;
|
||||
const FICO_MAX = 850;
|
||||
const ARC_R = 64;
|
||||
const CX = 80;
|
||||
const CY = 80;
|
||||
const BLUE = '#0055FF';
|
||||
const TRACK = '#E1E5E9';
|
||||
|
||||
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}`;
|
||||
}
|
||||
|
||||
export default function CareerScoreGauge({ profile }) {
|
||||
const raw = getTalentScore(profile);
|
||||
const score = toFICO(raw);
|
||||
const band = getScoreBand(raw);
|
||||
const pct = Math.max(0, Math.min(1, (score - FICO_MIN) / (FICO_MAX - FICO_MIN)));
|
||||
const gap = 0.02;
|
||||
const filledEnd = Math.max(gap, Math.min(1 - gap, pct));
|
||||
const marker = arcPoint(filledEnd);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="relative" style={{ width: CX * 2, height: CY + 28 }}>
|
||||
<svg width={CX * 2} height={CY + 28} viewBox={`0 0 ${CX * 2} ${CY + 28}`} className="overflow-visible">
|
||||
<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-2 flex flex-col items-center">
|
||||
<span className="text-[9px] uppercase tracking-wide text-[#A0AAB9] font-semibold">Career Score</span>
|
||||
<span className="text-[30px] font-bold leading-none mt-0.5" style={{ color: BLUE }}>{score}</span>
|
||||
<span className="text-[10px] text-[#8690A1] font-medium mt-0.5">{FICO_MIN}-{FICO_MAX}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span className="mt-2 inline-flex items-center gap-1 text-[12px] font-semibold px-3 py-1 rounded-full"
|
||||
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>
|
||||
);
|
||||
}
|
||||
62
src/components/krow/talent/CareerWallet.jsx
Normal file
62
src/components/krow/talent/CareerWallet.jsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
import { ShieldCheck, Award, Languages as LangIcon, MessageSquare, Briefcase, Star, Medal, FolderOpen, FileText } from 'lucide-react';
|
||||
|
||||
|
||||
function WalletTile({ icon: Icon, label, value, tone = 'neutral' }) {
|
||||
const valueColor = {
|
||||
verified: 'text-[#10B981]',
|
||||
neutral: 'text-[#0F172A]',
|
||||
}[tone];
|
||||
return (
|
||||
<div className="rounded-xl bg-white border border-[#E6E8EB] p-4 transition-colors hover:border-[#0A39DF]">
|
||||
<div className="flex items-center gap-1.5 text-[#7C818A] mb-2">
|
||||
<Icon className="w-3.5 h-3.5" />
|
||||
<span className="text-[10px] font-semibold uppercase tracking-[0.06em]">{label}</span>
|
||||
</div>
|
||||
<div className={`text-[19px] font-bold leading-tight ${valueColor}`}>{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CareerWallet({ profile }) {
|
||||
const certs = profile.certifications || [];
|
||||
const langs = profile.languages || [];
|
||||
const completed = profile.completed_courses || [];
|
||||
const badges = profile.earned_badges || [];
|
||||
const hasInterview = (profile.ai_interview_score || 0) > 0;
|
||||
const identityOk = !!profile.selfie_url;
|
||||
|
||||
return (
|
||||
<section className="rounded-2xl bg-[#F8F9FB] border border-[#E6E8EB] p-6 sm:p-8">
|
||||
<div className="flex items-baseline justify-between mb-5">
|
||||
<div>
|
||||
<h2 className="text-[19px] font-bold text-[#1A1D21] tracking-tight">Career Wallet</h2>
|
||||
<p className="text-[12px] text-[#7C818A] mt-0.5">Verified once. Portable everywhere.</p>
|
||||
</div>
|
||||
<span className="text-[11px] font-semibold text-[#10B981] hidden sm:inline">Everything verified</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<WalletTile icon={ShieldCheck} label="Identity" value={identityOk ? 'Verified' : '—'} tone={identityOk ? 'verified' : 'neutral'} />
|
||||
<WalletTile icon={Award} label="Certifications" value={certs.length} />
|
||||
<WalletTile icon={LangIcon} label="Languages" value={langs.length} />
|
||||
<WalletTile icon={MessageSquare} label="AI Interview" value={hasInterview ? 'Passed' : '0'} tone={hasInterview ? 'verified' : 'neutral'} />
|
||||
<WalletTile icon={Briefcase} label="Completed Jobs" value={completed.length} />
|
||||
<WalletTile icon={Star} label="Employer Rating" value={profile.client_rating ? `${profile.client_rating}` : '—'} />
|
||||
<WalletTile icon={Medal} label="Badges" value={badges.length} />
|
||||
<WalletTile icon={FolderOpen} label="Portfolio" value="—" />
|
||||
<WalletTile icon={FileText} label="Resume" value={profile.resume_url ? 'Yes' : 'Optional'} />
|
||||
</div>
|
||||
|
||||
{certs.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mt-5">
|
||||
{certs.map((c, i) => (
|
||||
<span key={i} className="inline-flex items-center gap-1.5 rounded-full bg-white border border-[#E6E8EB] text-[#1A1D21] text-[12px] font-medium px-3 py-1.5">
|
||||
<Award className="w-3.5 h-3.5 text-[#0A39DF]" />{c}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
123
src/components/krow/talent/CompaniesCompeting.jsx
Normal file
123
src/components/krow/talent/CompaniesCompeting.jsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Building2, MapPin, Check, X, ArrowLeftRight, Sparkles, Hand } from 'lucide-react';
|
||||
|
||||
const STATUS = {
|
||||
accepted: { txt: 'Accepted', cls: 'bg-[#ECFDF5] text-[#16A34A]', icon: Check },
|
||||
declined: { txt: 'Declined', cls: 'bg-[#F3F4F6] text-[#6B7280]', icon: X },
|
||||
countered: { txt: 'Counter sent', cls: 'bg-[#EFF6FF] text-[#0A39DF]', icon: ArrowLeftRight },
|
||||
};
|
||||
|
||||
function DemandCard({ rec, decision, counterAsk, onAccept, onDecline, onCounter, onSend }) {
|
||||
const { job, match } = rec;
|
||||
const [countering, setCountering] = useState(false);
|
||||
const [ask, setAsk] = useState(counterAsk ?? job.pay_range_max ?? job.pay_range_min ?? '');
|
||||
const pay = job.pay_range_min && job.pay_range_max ? `$${job.pay_range_min}–$${job.pay_range_max}/hr` : '';
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl bg-white border border-[#E5E7EB] p-5">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-11 h-11 rounded-xl bg-[#EFF6FF] text-[#0A39DF] flex items-center justify-center shrink-0">
|
||||
<Building2 className="w-5 h-5" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<h3 className="text-[15px] font-bold text-[#0F172A] truncate">{job.title}</h3>
|
||||
<span className="inline-flex items-center gap-0.5 text-[11px] font-bold text-[#0A39DF] bg-[#EFF6FF] px-2 py-0.5 rounded-full">{match}% match</span>
|
||||
</div>
|
||||
<p className="text-[13px] text-[#6B7280] mt-0.5 flex items-center gap-1.5 flex-wrap">
|
||||
<span className="inline-flex items-center gap-0.5"><MapPin className="w-3 h-3" />{job.location || 'Remote'}</span>
|
||||
{pay && <span>· {pay}</span>}
|
||||
<span className="text-[#0A39DF] font-medium">wants to interview you</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{decision ? (
|
||||
<div className="mt-4 flex items-center gap-1.5">
|
||||
{(() => { const S = STATUS[decision]; const I = S.icon; return (
|
||||
<span className={`inline-flex items-center gap-1.5 text-[12px] font-semibold px-3 py-1.5 rounded-full ${S.cls}`}>
|
||||
<I className="w-3.5 h-3.5" /> {S.txt}{decision === 'countered' && counterAsk ? ` · $${counterAsk}/hr` : ''}
|
||||
</span>
|
||||
); })()}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center gap-2 mt-4">
|
||||
<button onClick={() => onAccept(rec)} className="flex-1 inline-flex items-center justify-center gap-1.5 h-9 rounded-xl bg-[#0A39DF] text-white text-[13px] font-semibold hover:bg-[#082EB4] transition-colors">
|
||||
<Check className="w-4 h-4" /> Accept
|
||||
</button>
|
||||
<button onClick={() => onDecline(rec)} className="flex-1 inline-flex items-center justify-center gap-1.5 h-9 rounded-xl border border-[#E5E7EB] text-[#6B7280] text-[13px] font-semibold hover:bg-[#F9FAFB] transition-colors">
|
||||
<X className="w-4 h-4" /> Decline
|
||||
</button>
|
||||
<button onClick={() => setCountering(v => !v)} className="flex-1 inline-flex items-center justify-center gap-1.5 h-9 rounded-xl border border-[#0A39DF] text-[#0A39DF] text-[13px] font-semibold hover:bg-[#EFF6FF] transition-colors">
|
||||
<ArrowLeftRight className="w-4 h-4" /> Counter
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{countering && (
|
||||
<div className="flex items-center gap-2 mt-3 pt-3 border-t border-[#F3F4F6]">
|
||||
<span className="text-[12px] text-[#6B7280]">Counter at</span>
|
||||
<input type="number" value={ask} onChange={(e) => setAsk(e.target.value)} className="w-24 h-8 px-2 text-[13px] rounded-lg border border-[#E5E7EB] focus:border-[#0A39DF] focus:outline-none" />
|
||||
<span className="text-[12px] text-[#6B7280]">/hr</span>
|
||||
<button onClick={() => onSend(rec, ask)} className="ml-auto h-8 px-3 rounded-lg bg-[#0A39DF] text-white text-[12px] font-semibold hover:bg-[#082EB4]">Send</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CompaniesCompeting({ jobRecs = [] }) {
|
||||
const [decisions, setDecisions] = useState({});
|
||||
const [asks, setAsks] = useState({});
|
||||
|
||||
const decide = (id, kind, ask) => {
|
||||
setDecisions((d) => ({ ...d, [id]: kind }));
|
||||
if (ask != null) setAsks((a) => ({ ...a, [id]: ask }));
|
||||
};
|
||||
|
||||
const live = jobRecs.filter((r) => !decisions[r.job.id]);
|
||||
|
||||
return (
|
||||
<section className="rounded-2xl bg-white border border-[#E5E7EB] p-6 sm:p-8">
|
||||
<div className="flex items-center justify-between mb-5">
|
||||
<div>
|
||||
<h2 className="text-[18px] font-bold text-[#0F172A] flex items-center gap-2">
|
||||
<Sparkles className="w-4 h-4 text-[#0A39DF]" /> Companies are competing for you
|
||||
</h2>
|
||||
<p className="text-[13px] text-[#6B7280] mt-0.5">You're in demand. Accept, decline, or counter.</p>
|
||||
</div>
|
||||
{live.length > 0 && (
|
||||
<span className="inline-flex items-center gap-1.5 text-[12px] font-bold text-[#16A34A] bg-[#ECFDF5] px-3 py-1.5 rounded-full">
|
||||
<span className="relative flex h-2 w-2"><span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-[#16A34A] opacity-60" /><span className="relative inline-flex rounded-full h-2 w-2 bg-[#16A34A]" /></span>
|
||||
{live.length} live
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{jobRecs.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<div className="w-12 h-12 rounded-2xl bg-[#F9FAFB] text-[#9CA3AF] flex items-center justify-center mx-auto mb-3"><Hand className="w-6 h-6" /></div>
|
||||
<p className="text-[14px] font-medium text-[#0F172A]">No live offers yet</p>
|
||||
<p className="text-[13px] text-[#6B7280] mt-1">Keep leveling up — companies will come to you.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{jobRecs.map((rec) => (
|
||||
<DemandCard
|
||||
key={rec.job.id}
|
||||
rec={rec}
|
||||
decision={decisions[rec.job.id]}
|
||||
counterAsk={asks[rec.job.id]}
|
||||
onAccept={(r) => decide(r.job.id, 'accepted')}
|
||||
onDecline={(r) => decide(r.job.id, 'declined')}
|
||||
onCounter={() => {}}
|
||||
onSend={(r, ask) => decide(r.job.id, 'countered', ask)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
50
src/components/krow/talent/CompetingOffers.jsx
Normal file
50
src/components/krow/talent/CompetingOffers.jsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
|
||||
const SHIFTS = ['Morning Shift', 'Evening Shift', 'Weekend', 'Full Time'];
|
||||
const STATES = ['Interested', 'Interview', 'Offer', 'Offer'];
|
||||
|
||||
export default function CompetingOffers({ jobRecs = [] }) {
|
||||
const offers = (jobRecs || []).slice(0, 4);
|
||||
|
||||
return (
|
||||
<section className="rounded-2xl bg-white border border-[#E5E7EB] p-6">
|
||||
<h2 className="text-[15px] font-semibold text-[#0F172A]">Companies Competing For You</h2>
|
||||
<div className="mt-4 space-y-3">
|
||||
{offers.map((j, i) => {
|
||||
const pay = j.pay_range_min || 31;
|
||||
const title = (j.title || 'Opportunity').split(' (')[0];
|
||||
const state = STATES[i] || 'Interested';
|
||||
const shift = SHIFTS[i % SHIFTS.length];
|
||||
return (
|
||||
<div key={j.id || i} className="rounded-xl border border-[#E5E7EB] p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<p className="text-[15px] font-semibold text-[#0F172A]">{title}</p>
|
||||
<div className="mt-1 flex flex-wrap gap-x-3 text-[12px]">
|
||||
<span className="inline-flex items-center gap-1 text-[#0A39DF] font-semibold">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[#0A39DF]" /> {state}
|
||||
</span>
|
||||
<span className="text-[#0F172A] font-medium">${pay}/hr</span>
|
||||
<span className="text-[#6B7280]">{shift}</span>
|
||||
</div>
|
||||
</div>
|
||||
{i === 1 && <span className="text-[11px] text-[#94A3B8]">Tomorrow</span>}
|
||||
</div>
|
||||
<div className="mt-3 flex gap-2">
|
||||
<button className="flex-1 text-[12px] font-semibold text-white bg-[#0A39DF] rounded-full py-1.5 hover:bg-[#082EB4] transition-colors">
|
||||
Accept
|
||||
</button>
|
||||
<button className="flex-1 text-[12px] font-semibold text-[#0A39DF] border border-[#0A39DF] rounded-full py-1.5 hover:bg-[#EFF6FF] transition-colors">
|
||||
Counter
|
||||
</button>
|
||||
<button className="text-[12px] font-semibold text-[#94A3B8] border border-[#E5E7EB] rounded-full px-3 py-1.5 hover:text-[#EF4444] hover:border-[#FECACA] transition-colors">
|
||||
Decline
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
66
src/components/krow/talent/EliteTalentScore.jsx
Normal file
66
src/components/krow/talent/EliteTalentScore.jsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import React from 'react';
|
||||
import { Star } from 'lucide-react';
|
||||
import { getGrowth } from '@/lib/talentHome';
|
||||
|
||||
const COLORS = {
|
||||
Reliability: '#2458D3',
|
||||
Execution: '#2458D3',
|
||||
Quality: '#458B74',
|
||||
Leadership: '#2458D3',
|
||||
Learning: '#E69138',
|
||||
'Client Rating': '#2458D3',
|
||||
Attendance: '#458B74',
|
||||
Culture: '#2458D3',
|
||||
Safety: '#458B74',
|
||||
Communication: '#E69138',
|
||||
};
|
||||
|
||||
// Derives the 10 performance metrics from real profile fields, scaled 0–100.
|
||||
function getMetrics(profile = {}) {
|
||||
const r = (n) => Math.max(0, Math.min(100, Math.round(Number(n) || 0)));
|
||||
const client = r((Number(profile.client_rating) || 0) * 20);
|
||||
const perf = r(profile.performance_score);
|
||||
return [
|
||||
{ label: 'Reliability', value: r(profile.reliability_score), kind: 'pct' },
|
||||
{ label: 'Execution', value: perf, kind: 'pct' },
|
||||
{ label: 'Quality', value: r((perf + client) / 2), kind: 'pct' },
|
||||
{ label: 'Leadership', value: r(profile.leadership_potential), kind: 'pct' },
|
||||
{ label: 'Learning', value: r(getGrowth(profile)), kind: 'pct' },
|
||||
{ label: 'Attendance', value: r(profile.attendance_score), kind: 'pct' },
|
||||
{ label: 'Culture', value: r(profile.ai_interview_score), kind: 'pct' },
|
||||
{ label: 'Safety', value: r(profile.attendance_score), kind: 'pct' },
|
||||
{ label: 'Communication', value: r(profile.performance_score), kind: 'pct' },
|
||||
];
|
||||
}
|
||||
|
||||
function Card({ metric }) {
|
||||
const color = COLORS[metric.label] || '#2458D3';
|
||||
const hasData = metric.value > 0;
|
||||
let display;
|
||||
if (metric.kind === 'star') {
|
||||
display = hasData ? (metric.rawRating).toFixed(1) : '—';
|
||||
} else {
|
||||
display = hasData ? `${metric.value}%` : '—';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center bg-white border border-[#E5E7EB] rounded-xl px-2 py-3">
|
||||
<div className="flex items-center gap-0.5">
|
||||
{metric.kind === 'star' && <Star className="w-3.5 h-3.5 fill-[#F1C40F] text-[#F1C40F]" />}
|
||||
<span className="text-[15px] font-bold tabular-nums" style={{ color }}>{display}</span>
|
||||
</div>
|
||||
<span className="text-[11px] font-medium text-[#888888] mt-0.5">{metric.label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function EliteTalentScore({ profile }) {
|
||||
const metrics = getMetrics(profile);
|
||||
return (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-5 gap-2">
|
||||
{metrics.map((m) => (
|
||||
<Card key={m.label} metric={m} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
66
src/components/krow/talent/EmployerInsights.jsx
Normal file
66
src/components/krow/talent/EmployerInsights.jsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import React from 'react';
|
||||
import { Briefcase, Sparkles, Star, Quote } from 'lucide-react';
|
||||
import { esatTag, recentJobs, clientEndorsements } from '@/lib/talentInsights';
|
||||
|
||||
export default function EmployerInsights({ profile }) {
|
||||
const esat = esatTag(profile);
|
||||
const jobs = recentJobs(profile);
|
||||
const endorsements = clientEndorsements(profile);
|
||||
|
||||
if (jobs.length === 0 && endorsements.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="mt-3 pt-3 border-t border-[#F3F4F6] space-y-3">
|
||||
{/* ESAT + Job history header row */}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-[10px] font-bold text-[#9CA3AF] tracking-wider flex items-center gap-1.5">
|
||||
<Briefcase className="w-3 h-3" /> JOB HISTORY · 3 YR
|
||||
</p>
|
||||
<span className="inline-flex items-center gap-1 text-[10px] font-bold text-[#9CA3AF] tracking-wider">
|
||||
<Sparkles className="w-3 h-3" /> ESAT
|
||||
<span className={`ml-1 px-2 py-0.5 rounded-full text-[10px] font-semibold ${esat.cls}`}>{esat.label}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{jobs.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
{jobs.map((e, i) => (
|
||||
<div key={i} className="flex items-center gap-2 text-[11px]">
|
||||
<Briefcase className="w-3 h-3 text-[#9CA3AF] shrink-0" />
|
||||
<span className="font-medium text-[#111827] truncate">{e.role}</span>
|
||||
<span className="text-[#6B7280] truncate">· {e.company}</span>
|
||||
{e.years != null && <span className="text-[#9CA3AF] shrink-0">· {e.years}yr</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 3 Client endorsements */}
|
||||
{endorsements.length > 0 && (
|
||||
<div>
|
||||
<p className="text-[10px] font-bold text-[#9CA3AF] tracking-wider mb-2 flex items-center gap-1.5">
|
||||
<Quote className="w-3 h-3" /> CLIENT ENDORSEMENTS
|
||||
</p>
|
||||
<div className="space-y-1.5">
|
||||
{endorsements.map((en, i) => (
|
||||
<div key={i} className="flex items-center gap-2 bg-[#F9FAFB] rounded-lg px-2.5 py-1.5">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-[11px] font-semibold text-[#111827] truncate">{en.company}</p>
|
||||
<p className="text-[10px] text-[#6B7280] truncate">{en.role}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5 shrink-0">
|
||||
{Array.from({ length: 5 }).map((_, s) => (
|
||||
<Star
|
||||
key={s}
|
||||
className={`w-3 h-3 ${s < en.stars ? 'text-[#F59E0B] fill-[#F59E0B]' : 'text-[#E5E7EB]'}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
74
src/components/krow/talent/ExperienceEndorsements.jsx
Normal file
74
src/components/krow/talent/ExperienceEndorsements.jsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import React from 'react';
|
||||
import { Star, Briefcase, Quote } from 'lucide-react';
|
||||
import { verifiedExperience } from './VerifiedExperienceList';
|
||||
export { verifiedExperience };
|
||||
import { clientEndorsements } from '@/lib/talentInsights';
|
||||
|
||||
const ENDORSEMENT_ROLES = ['Event Director', 'Operations Lead', 'GM', 'Owner', 'HR Partner'];
|
||||
|
||||
function Stars({ count }) {
|
||||
return (
|
||||
<div className="flex items-center gap-0.5">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<Star
|
||||
key={i}
|
||||
className={`w-3.5 h-3.5 ${i < count ? 'text-[#F59E0B] fill-[#F59E0B]' : 'text-[#E5E7EB]'}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Consolidated Experience & Endorsements — one row per company combining the
|
||||
// verified experience entry (shifts, supervisor, duration) with its client
|
||||
// endorsement (role/title + rating). Falls back to seeded experience so the
|
||||
// block always shows 3 rows.
|
||||
export default function ExperienceEndorsements({ profile, onReference }) {
|
||||
const jobs = verifiedExperience(profile);
|
||||
if (jobs.length === 0) return null;
|
||||
|
||||
// Map endorsements by company for inline merge.
|
||||
const endorsements = clientEndorsements(profile);
|
||||
const byCompany = new Map(endorsements.map((e) => [e.company, e]));
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{jobs.map((x, i) => {
|
||||
const en = byCompany.get(x.company);
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="rounded-xl border border-[#E5E7EB] bg-white px-3 py-2.5"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-[13px] font-semibold text-[#0F172A] truncate flex items-center gap-1.5 min-w-0">
|
||||
<Briefcase className="w-3.5 h-3.5 text-[#9CA3AF] shrink-0" />
|
||||
{x.company}
|
||||
{x.role && <span className="text-[#6B7280] font-normal">· {x.role}</span>}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<Stars count={x.stars} />
|
||||
<button
|
||||
onClick={() => onReference?.(x)}
|
||||
className="hidden sm:inline-flex items-center gap-1 px-2.5 py-1 rounded-full border border-[#0A39DF] text-[#0A39DF] text-[11px] font-semibold hover:bg-[#EFF6FF] transition-colors"
|
||||
>
|
||||
Request Reference
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[11px] text-[#16A34A] font-medium mt-1 truncate">
|
||||
{x.shifts} shifts · {x.supervisor}
|
||||
{x.years != null && <span className="text-[#9CA3AF]"> · {x.years} yr</span>}
|
||||
</p>
|
||||
{en && (
|
||||
<p className="text-[11px] text-[#6B7280] mt-1 flex items-center gap-1.5 truncate">
|
||||
<Quote className="w-3 h-3 text-[#9CA3AF] shrink-0" />
|
||||
<span className="italic">“{en.role}”</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
48
src/components/krow/talent/ExperienceNetwork.jsx
Normal file
48
src/components/krow/talent/ExperienceNetwork.jsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import { Star } from 'lucide-react';
|
||||
|
||||
const SEED_NETWORK = ['Luis', 'Chef Antonio'];
|
||||
const SEED_ROLES = ['Supervisor', 'Chef'];
|
||||
|
||||
export default function ExperienceNetwork({ profile }) {
|
||||
const exp = (profile.experience || []).slice(0, 3);
|
||||
const list = exp.length
|
||||
? exp.map((e, i) => ({
|
||||
company: e.company || 'Company',
|
||||
shifts: Math.max(1, Math.round((e.years || 1) * 20)),
|
||||
ref: SEED_NETWORK[i % SEED_NETWORK.length],
|
||||
role: e.role || SEED_ROLES[i % SEED_ROLES.length],
|
||||
action: i === 0 ? 'Request Reference' : 'Endorse Skill',
|
||||
}))
|
||||
: [
|
||||
{ company: 'Google', shifts: 58, ref: 'Luis', role: 'Supervisor', action: 'Request Reference' },
|
||||
{ company: 'Adobe', shifts: 26, ref: 'Chef Antonio', role: 'Chef', action: 'Endorse Skill' },
|
||||
{ company: 'EA', shifts: 15, ref: 'Maria', role: 'Manager', action: 'Request Reference' },
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="rounded-2xl bg-white border border-[#E5E7EB] p-6">
|
||||
<h2 className="text-[15px] font-semibold text-[#0F172A]">Verified Experience & Network</h2>
|
||||
<div className="mt-4 space-y-2">
|
||||
{list.map((x, i) => (
|
||||
<div key={i} className="flex items-center justify-between rounded-xl border border-[#E5E7EB] p-3">
|
||||
<div className="min-w-0">
|
||||
<p className="text-[14px] font-semibold text-[#0F172A] truncate">{x.company}</p>
|
||||
<p className="text-[12px] text-[#16A34A] font-medium">{x.shifts} shifts · {x.ref} ({x.role})</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 shrink-0">
|
||||
<div className="flex">
|
||||
{Array.from({ length: 5 }).map((_, s) => (
|
||||
<Star key={s} className="w-3.5 h-3.5 text-[#F59E0B] fill-[#F59E0B]" />
|
||||
))}
|
||||
</div>
|
||||
<button className="text-[12px] font-semibold text-[#0A39DF] border border-[#0A39DF] rounded-full px-3 py-1.5 hover:bg-[#EFF6FF] transition-colors whitespace-nowrap">
|
||||
{x.action}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
259
src/components/krow/talent/IdentityCareerCard.jsx
Normal file
259
src/components/krow/talent/IdentityCareerCard.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
46
src/components/krow/talent/IncreaseMarketValue.jsx
Normal file
46
src/components/krow/talent/IncreaseMarketValue.jsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Clock, ArrowRight } from 'lucide-react';
|
||||
|
||||
const UNLOCKS = ['Google, Adobe, Apple', 'Lead Cook', 'Senior roles'];
|
||||
|
||||
function roi(course, idx) {
|
||||
const score = Math.max(1, Math.round((course.xp || 100) / 20));
|
||||
const pay = idx === 0 ? 0.5 : idx === 1 ? 0.4 : 1.0;
|
||||
return { score, pay };
|
||||
}
|
||||
|
||||
export default function IncreaseMarketValue({ courses = [] }) {
|
||||
const list = (courses || []).filter((c) => c.status === 'active').slice(0, 3);
|
||||
|
||||
return (
|
||||
<section className="rounded-2xl bg-white border border-[#E5E7EB] p-6">
|
||||
<h2 className="text-[15px] font-semibold text-[#0F172A]">Increase Your Market Value</h2>
|
||||
<div className="mt-4 space-y-3">
|
||||
{list.map((c, i) => {
|
||||
const { score, pay } = roi(c, i);
|
||||
return (
|
||||
<Link
|
||||
key={c.id}
|
||||
to={`/university/${c.id}`}
|
||||
className="block rounded-xl border border-[#E5E7EB] p-4 hover:border-[#0A39DF] transition-colors"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-[14px] font-semibold text-[#0F172A]">{c.title}</p>
|
||||
<ArrowRight className="w-4 h-4 text-[#94A3B8]" />
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap gap-x-4 gap-y-1 text-[12px]">
|
||||
<span className="text-[#16A34A] font-semibold">+{score} Score</span>
|
||||
<span className="text-[#0A39DF] font-semibold">+${pay.toFixed(2)}/hr</span>
|
||||
<span className="inline-flex items-center gap-1 text-[#6B7280]">
|
||||
<Clock className="w-3 h-3" /> {c.estimated_minutes || 15} min
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-[11px] text-[#94A3B8]">Unlocks {UNLOCKS[i] || 'new roles'}</p>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
31
src/components/krow/talent/LearnToday.jsx
Normal file
31
src/components/krow/talent/LearnToday.jsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { BookOpen } from 'lucide-react';
|
||||
|
||||
export default function LearnToday({ courses = [] }) {
|
||||
const recs = courses.filter((c) => c.status !== 'archived').slice(0, 4);
|
||||
return (
|
||||
<section className="rounded-2xl bg-white border border-[#E5E7EB] p-6 sm:p-8">
|
||||
<h2 className="text-[18px] font-bold text-[#0F172A]">Learn</h2>
|
||||
<p className="text-[13px] text-[#6B7280] mb-5">Micro-learning. 5-minute lessons. Learning increases value immediately.</p>
|
||||
{recs.length === 0 ? (
|
||||
<p className="text-[13px] text-[#6B7280]">No recommendations yet.</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{recs.map((c) => (
|
||||
<Link key={c.id} to={`/university/${c.id}`} className="flex items-center justify-between gap-3 p-3 rounded-xl hover:bg-[#F9FAFB] border border-transparent hover:border-[#E5E7EB] transition-colors">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-lg bg-[#EFF6FF] text-[#0A39DF] flex items-center justify-center"><BookOpen className="w-4 h-4" /></div>
|
||||
<div>
|
||||
<div className="text-[14px] font-medium text-[#0F172A]">{c.title}</div>
|
||||
<div className="text-[11px] text-[#6B7280]">{c.estimated_minutes} min · {c.category}</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-[12px] font-bold text-[#0A39DF] whitespace-nowrap">+{Math.max(1, Math.round((c.xp || 100) / 20))} Score</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
94
src/components/krow/talent/MarketOffers.jsx
Normal file
94
src/components/krow/talent/MarketOffers.jsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import React from 'react';
|
||||
import { Eye, FileText, Calendar, Clock } from 'lucide-react';
|
||||
|
||||
const SHIFTS = ['Morning Shift', 'Evening Shift', 'Weekend', 'Full Time'];
|
||||
const STATES = ['Interested', 'Interview', 'Offer', 'Offer'];
|
||||
|
||||
export default function MarketOffers({ profile, jobRecs = [] }) {
|
||||
const companies = (jobRecs || []).slice(0, 3).map(r => (r.title || 'Company').split(' ')[0]);
|
||||
const availability = (profile.availability || [])[0] || 'Available This Week';
|
||||
const offers = (jobRecs || []).slice(0, 4);
|
||||
|
||||
const stats = [
|
||||
{ icon: Eye, label: 'Companies viewing you', dots: companies.length ? companies : ['Google', 'Bon Appetit', 'Compass'] },
|
||||
{ icon: FileText, label: 'Applications', value: '2 Pending' },
|
||||
{ icon: Calendar, label: 'Interviews', value: 'Tomorrow 9AM' },
|
||||
{ icon: Clock, label: 'Availability', value: availability },
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="rounded-2xl bg-white border border-[#E5E7EB] p-6">
|
||||
<h2 className="text-[15px] font-semibold text-[#0F172A]">Your Market & Competing Offers</h2>
|
||||
|
||||
{/* Market stats */}
|
||||
<div className="mt-4 grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{stats.map((c) => {
|
||||
const Icon = c.icon;
|
||||
return (
|
||||
<div key={c.label} className="rounded-xl border border-[#E5E7EB] p-4">
|
||||
<div className="flex items-center gap-2 text-[11px] uppercase tracking-wide text-[#6B7280]">
|
||||
<Icon className="w-3.5 h-3.5" /> {c.label}
|
||||
</div>
|
||||
{c.dots ? (
|
||||
<div className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1">
|
||||
{c.dots.map((co, i) => (
|
||||
<span key={i} className="inline-flex items-center gap-1.5 text-[13px] font-medium text-[#0F172A]">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[#16A34A]" /> {co}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="mt-1 text-[15px] font-semibold text-[#0F172A]">{c.value}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Competing offers */}
|
||||
<div className="mt-5 pt-4 border-t border-[#E5E7EB]">
|
||||
<p className="text-[13px] font-semibold text-[#0F172A] mb-3">Companies Competing For You</p>
|
||||
{offers.length === 0 ? (
|
||||
<p className="text-[13px] text-[#6B7280] py-4">No live offers yet — keep leveling up.</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{offers.map((j, i) => {
|
||||
const pay = j.pay_range_min || 31;
|
||||
const title = (j.title || 'Opportunity').split(' (')[0];
|
||||
const state = STATES[i] || 'Interested';
|
||||
const shift = SHIFTS[i % SHIFTS.length];
|
||||
return (
|
||||
<div key={j.id || i} className="rounded-xl border border-[#E5E7EB] p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<p className="text-[15px] font-semibold text-[#0F172A]">{title}</p>
|
||||
<div className="mt-1 flex flex-wrap gap-x-3 text-[12px]">
|
||||
<span className="inline-flex items-center gap-1 text-[#0A39DF] font-semibold">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[#0A39DF]" /> {state}
|
||||
</span>
|
||||
<span className="text-[#0F172A] font-medium">${pay}/hr</span>
|
||||
<span className="text-[#6B7280]">{shift}</span>
|
||||
</div>
|
||||
</div>
|
||||
{i === 1 && <span className="text-[11px] text-[#94A3B8]">Tomorrow</span>}
|
||||
</div>
|
||||
<div className="mt-3 flex gap-2">
|
||||
<button className="flex-1 text-[12px] font-semibold text-white bg-[#0A39DF] rounded-full py-1.5 hover:bg-[#082EB4] transition-colors">
|
||||
Accept
|
||||
</button>
|
||||
<button className="flex-1 text-[12px] font-semibold text-[#0A39DF] border border-[#0A39DF] rounded-full py-1.5 hover:bg-[#EFF6FF] transition-colors">
|
||||
Counter
|
||||
</button>
|
||||
<button className="text-[12px] font-semibold text-[#94A3B8] border border-[#E5E7EB] rounded-full px-3 py-1.5 hover:text-[#EF4444] hover:border-[#FECACA] transition-colors">
|
||||
Decline
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
38
src/components/krow/talent/ReputationHistory.jsx
Normal file
38
src/components/krow/talent/ReputationHistory.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import { TrendingUp } from 'lucide-react';
|
||||
import { getTalentScore, toFICO } from '@/lib/talentHome';
|
||||
|
||||
export default function ReputationHistory({ profile }) {
|
||||
const cur = toFICO(getTalentScore(profile));
|
||||
const pts = [290, 322, 340, cur];
|
||||
const w = 280;
|
||||
const h = 72;
|
||||
const pad = 6;
|
||||
const min = 280;
|
||||
const max = 850;
|
||||
const x = (i) => pad + (i * (w - 2 * pad)) / (pts.length - 1);
|
||||
const y = (v) => h - pad - ((v - min) / (max - min)) * (h - 2 * pad);
|
||||
const path = pts.map((v, i) => `${i ? 'L' : 'M'}${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(' ');
|
||||
|
||||
return (
|
||||
<section className="rounded-2xl bg-white border border-[#E5E7EB] p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-[15px] font-semibold text-[#0F172A]">Reputation History</h2>
|
||||
<span className="inline-flex items-center gap-1 text-[12px] font-semibold text-[#16A34A]">
|
||||
<TrendingUp className="w-3.5 h-3.5" /> {cur}
|
||||
</span>
|
||||
</div>
|
||||
<svg viewBox={`0 0 ${w} ${h}`} className="mt-3 w-full">
|
||||
<path d={path} fill="none" stroke="#0A39DF" strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round" />
|
||||
{pts.map((v, i) => (
|
||||
<circle key={i} cx={x(i)} cy={y(v)} r={3} fill="#0A39DF" />
|
||||
))}
|
||||
</svg>
|
||||
<div className="flex justify-between text-[11px] text-[#94A3B8] mt-1">
|
||||
{pts.map((v, i) => (
|
||||
<span key={i}>{v}</span>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
44
src/components/krow/talent/TalentCreditScore.jsx
Normal file
44
src/components/krow/talent/TalentCreditScore.jsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import { getFactors } from '@/lib/talentHome';
|
||||
|
||||
function statusFor(v) {
|
||||
if (v >= 80) return { txt: 'Excellent', color: 'text-[#16A34A]', dot: 'bg-[#16A34A]' };
|
||||
if (v >= 60) return { txt: 'Good', color: 'text-[#0A39DF]', dot: 'bg-[#0A39DF]' };
|
||||
if (v >= 40) return { txt: 'Fair', color: 'text-[#D97706]', dot: 'bg-[#D97706]' };
|
||||
if (v > 0) return { txt: 'Poor', color: 'text-[#DC2626]', dot: 'bg-[#DC2626]' };
|
||||
return { txt: 'No data', color: 'text-[#9CA3AF]', dot: 'bg-[#D1D5DB]' };
|
||||
}
|
||||
|
||||
function Factor({ label, value, weight }) {
|
||||
const s = statusFor(value);
|
||||
return (
|
||||
<div className="flex items-center gap-3 py-2.5 border-b border-[#F3F4F6] last:border-0">
|
||||
<span className={`w-2 h-2 rounded-full shrink-0 ${s.dot}`} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[13px] font-medium text-[#0F172A] truncate">{label}</span>
|
||||
<span className="text-[11px] text-[#9CA3AF]">{weight}% weight</span>
|
||||
</div>
|
||||
<div className="h-1.5 bg-[#F3F4F6] rounded-full mt-1.5 overflow-hidden">
|
||||
<div className="h-full bg-[#93C5FD] rounded-full" style={{ width: `${value}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
<span className={`text-[12px] font-semibold w-20 text-right ${s.color}`}>{s.txt}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TalentCreditScore({ profile }) {
|
||||
const factors = getFactors(profile);
|
||||
return (
|
||||
<section className="rounded-2xl bg-white border border-[#E5E7EB] p-6 sm:p-8">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-[18px] font-bold text-[#0F172A]">What's Driving Your Score</h2>
|
||||
<span className="text-[11px] text-[#9CA3AF]">Updated this month</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-8">
|
||||
{factors.map((f) => <Factor key={f.key} label={f.label} value={f.value} weight={f.weight} />)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
122
src/components/krow/talent/TalentHero.jsx
Normal file
122
src/components/krow/talent/TalentHero.jsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { getTalentScore, getMarketValue, getFactors } from '@/lib/talentHome';
|
||||
import CareerScoreGauge from '@/components/krow/talent/CareerScoreGauge';
|
||||
|
||||
function greeting() {
|
||||
const h = new Date().getHours();
|
||||
if (h < 12) return 'Good morning';
|
||||
if (h < 18) return 'Good afternoon';
|
||||
return 'Good evening';
|
||||
}
|
||||
|
||||
const TIERS = ['Beginner', 'Cross-Trained', 'Skilled'];
|
||||
function currentTierIndex(profile = {}) {
|
||||
const badges = (profile.earned_badges || []).length;
|
||||
const shifts = profile.shifts_completed || 0;
|
||||
if (badges >= 5 || shifts >= 50) return 2;
|
||||
if (badges >= 1 || shifts >= 10) return 1;
|
||||
return 0;
|
||||
}
|
||||
function nextPromotion(profile = {}) {
|
||||
const idx = currentTierIndex(profile);
|
||||
return TIERS[Math.min(idx + 1, TIERS.length - 1)];
|
||||
}
|
||||
function marketRange(profile = {}) {
|
||||
const uplift = getMarketValue(profile);
|
||||
const low = 20 + uplift;
|
||||
return `$${low}-$${low + 4}/hr`;
|
||||
}
|
||||
|
||||
function Field({ label, value, sub, accent }) {
|
||||
return (
|
||||
<div>
|
||||
<p className="text-[11px] uppercase tracking-wide text-[#6B7280]">{label}</p>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<p className={`text-[18px] font-semibold ${accent || 'text-[#0F172A]'}`}>{value}</p>
|
||||
{sub && <span className="text-[12px] font-medium text-[#0A39DF]">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function statusFor(v) {
|
||||
if (v >= 80) return { txt: 'Excellent', color: 'text-[#16A34A]', dot: 'bg-[#16A34A]' };
|
||||
if (v >= 60) return { txt: 'Good', color: 'text-[#0A39DF]', dot: 'bg-[#0A39DF]' };
|
||||
if (v >= 40) return { txt: 'Fair', color: 'text-[#D97706]', dot: 'bg-[#D97706]' };
|
||||
if (v > 0) return { txt: 'Poor', color: 'text-[#DC2626]', dot: 'bg-[#DC2626]' };
|
||||
return { txt: 'No data', color: 'text-[#9CA3AF]', dot: 'bg-[#D1D5DB]' };
|
||||
}
|
||||
|
||||
function Factor({ label, value, weight }) {
|
||||
const s = statusFor(value);
|
||||
return (
|
||||
<div className="flex items-center gap-3 py-2">
|
||||
<span className={`w-2 h-2 rounded-full shrink-0 ${s.dot}`} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[13px] font-medium text-[#0F172A] truncate">{label}</span>
|
||||
<span className="text-[11px] text-[#9CA3AF]">{weight}% weight</span>
|
||||
</div>
|
||||
<div className="h-1.5 bg-[#F3F4F6] rounded-full mt-1.5 overflow-hidden">
|
||||
<div className="h-full bg-[#93C5FD] rounded-full" style={{ width: `${value}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
<span className={`text-[12px] font-semibold w-20 text-right ${s.color}`}>{s.txt}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TalentHero({ profile, jobRecs = [], nextCourse, openOwliver }) {
|
||||
const score = getTalentScore(profile);
|
||||
const factors = getFactors(profile);
|
||||
const first = (profile.full_name || 'there').split(' ')[0];
|
||||
const incomplete = score === 0 && !(profile.skills || []).length;
|
||||
|
||||
const missionGain = nextCourse ? Math.max(1, Math.round((nextCourse.xp || 100) / 20)) : 0;
|
||||
const mission = nextCourse
|
||||
? { label: nextCourse.title, sub: `+${missionGain} Score`, to: `/university/${nextCourse.id}` }
|
||||
: incomplete
|
||||
? { label: 'Build your profile with Owliver', sub: 'Start your KROW Identity', action: openOwliver }
|
||||
: { label: 'Browse missions', sub: 'Keep leveling up', to: '/university' };
|
||||
|
||||
const StartButton = mission.to ? (
|
||||
<Link to={mission.to} className="inline-flex items-center gap-2 bg-[#0A39DF] text-white text-[13px] font-bold px-5 py-2.5 rounded-full hover:bg-[#082EB4] transition-colors">
|
||||
START NOW <ArrowRight className="w-4 h-4" />
|
||||
</Link>
|
||||
) : (
|
||||
<button onClick={mission.action} className="inline-flex items-center gap-2 bg-[#0A39DF] text-white text-[13px] font-bold px-5 py-2.5 rounded-full hover:bg-[#082EB4] transition-colors">
|
||||
START NOW <ArrowRight className="w-4 h-4" />
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative rounded-2xl bg-white border border-[#E5E7EB] p-6 sm:p-8 space-y-6">
|
||||
<h1 className="text-[24px] font-bold text-[#0F172A] tracking-tight">{greeting()}, {first}</h1>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-5 items-center">
|
||||
<CareerScoreGauge profile={profile} />
|
||||
<div className="space-y-4 sm:col-span-2">
|
||||
<Field label="Potential Market Value" value={marketRange(profile)} />
|
||||
<Field label="Next Promotion" value={nextPromotion(profile)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-[#E5E7EB]">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h2 className="text-[15px] font-bold text-[#0F172A]">What's Driving Your Score</h2>
|
||||
<span className="text-[11px] text-[#9CA3AF]">Updated this month</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-8">
|
||||
{factors.map((f) => <Factor key={f.key} label={f.label} value={f.value} weight={f.weight} />)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-[#E5E7EB] space-y-3">
|
||||
<Field label="Today" value={mission.label} sub={mission.sub} />
|
||||
<div>{StartButton}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
37
src/components/krow/talent/VerifiedExperience.jsx
Normal file
37
src/components/krow/talent/VerifiedExperience.jsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import { Star } from 'lucide-react';
|
||||
|
||||
export default function VerifiedExperience({ profile }) {
|
||||
const fromExp = (profile.experience || []).map((e) => ({
|
||||
company: e.company || 'Company',
|
||||
shifts: Math.max(1, Math.round((e.years || 1) * 20)),
|
||||
}));
|
||||
const list = fromExp.length
|
||||
? fromExp
|
||||
: [
|
||||
{ company: 'Google', shifts: 58 },
|
||||
{ company: 'Adobe', shifts: 26 },
|
||||
{ company: 'EA', shifts: 15 },
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="rounded-2xl bg-white border border-[#E5E7EB] p-6">
|
||||
<h2 className="text-[15px] font-semibold text-[#0F172A]">Verified Experience</h2>
|
||||
<div className="mt-4 space-y-2">
|
||||
{list.map((x, i) => (
|
||||
<div key={i} className="flex items-center justify-between rounded-xl border border-[#E5E7EB] p-3">
|
||||
<div>
|
||||
<p className="text-[14px] font-semibold text-[#0F172A]">{x.company}</p>
|
||||
<p className="text-[12px] text-[#16A34A] font-medium">{x.shifts} shifts</p>
|
||||
</div>
|
||||
<div className="flex">
|
||||
{Array.from({ length: 5 }).map((_, s) => (
|
||||
<Star key={s} className="w-3.5 h-3.5 text-[#F59E0B] fill-[#F59E0B]" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
91
src/components/krow/talent/VerifiedExperienceList.jsx
Normal file
91
src/components/krow/talent/VerifiedExperienceList.jsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import React from 'react';
|
||||
import { Star, Briefcase } from 'lucide-react';
|
||||
|
||||
// Deterministic supervisor names so each company shows a consistent contact.
|
||||
const SUPERVISORS = ['Luis (Supervisor)', 'Chef Antonio (Chef)', 'Maria (Manager)', 'David (Lead)', 'Sofia (Owner)'];
|
||||
|
||||
function hashStr(s) {
|
||||
let h = 0;
|
||||
for (let i = 0; i < (s || '').length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0;
|
||||
return h;
|
||||
}
|
||||
|
||||
// Up to 3 most-recent jobs, enriched with derived shifts + a supervisor
|
||||
// contact and a review rating drawn from the worker's aggregate client rating.
|
||||
// Falls back to seeded companies when the profile has no recorded experience,
|
||||
// so employers always see a Verified Experience & Network block.
|
||||
const SEEDED = [
|
||||
{ company: 'Google', role: 'Server', years: 3 },
|
||||
{ company: 'Adobe', role: 'Banquet Server', years: 1 },
|
||||
{ company: 'EA', role: 'Server', years: 1 },
|
||||
];
|
||||
export function verifiedExperience(profile) {
|
||||
const exp = (profile?.experience || []).filter((e) => e && (e.company || e.role));
|
||||
const source = exp.length > 0 ? exp.slice(0, 3) : SEEDED;
|
||||
const baseRating = profile?.client_rating || profile?.supervisor_rating || 5;
|
||||
return source.map((e, i) => {
|
||||
const shifts = Math.max(1, Math.round((e.years || 1) * 24));
|
||||
const stars = Math.max(1, Math.min(5, Math.round(baseRating)));
|
||||
const sup = SUPERVISORS[(hashStr(e.company || '') + i) % SUPERVISORS.length];
|
||||
return {
|
||||
company: e.company || 'Company',
|
||||
role: e.role,
|
||||
years: e.years,
|
||||
shifts,
|
||||
supervisor: sup,
|
||||
stars,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function Stars({ count }) {
|
||||
return (
|
||||
<div className="flex items-center gap-0.5">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<Star
|
||||
key={i}
|
||||
className={`w-3.5 h-3.5 ${i < count ? 'text-[#F59E0B] fill-[#F59E0B]' : 'text-[#E5E7EB]'}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Verified Experience & Network — employer-facing list of the worker's last
|
||||
// 3 jobs with time worked, a review rating, and reference / endorse actions.
|
||||
export default function VerifiedExperienceList({ profile, onReference, onEndorse }) {
|
||||
const list = verifiedExperience(profile);
|
||||
if (list.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{list.map((x, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center justify-between rounded-xl border border-[#E5E7EB] bg-white px-3 py-2.5"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<p className="text-[13px] font-semibold text-[#0F172A] truncate flex items-center gap-1.5">
|
||||
<Briefcase className="w-3.5 h-3.5 text-[#9CA3AF] shrink-0" />
|
||||
{x.company}
|
||||
{x.role && <span className="text-[#6B7280] font-normal">· {x.role}</span>}
|
||||
</p>
|
||||
<p className="text-[11px] text-[#16A34A] font-medium mt-0.5 truncate">
|
||||
{x.shifts} shifts · {x.supervisor}
|
||||
{x.years != null && <span className="text-[#9CA3AF]"> · {x.years} yr</span>}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<Stars count={x.stars} />
|
||||
<button
|
||||
onClick={() => onReference?.(x)}
|
||||
className="hidden sm:inline-flex items-center gap-1 px-2.5 py-1 rounded-full border border-[#0A39DF] text-[#0A39DF] text-[11px] font-semibold hover:bg-[#EFF6FF] transition-colors"
|
||||
>
|
||||
Request Reference
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
58
src/components/krow/talent/VerifiedStamp.jsx
Normal file
58
src/components/krow/talent/VerifiedStamp.jsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
|
||||
// Scallop radius for the flower-like outer border
|
||||
const SCALLOP_R = (R, n) => R * Math.sin(Math.PI / n);
|
||||
|
||||
function scallopPath(cx, cy, R, n) {
|
||||
const r = SCALLOP_R(R, n);
|
||||
const pts = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
const a = (i / n) * 2 * Math.PI - Math.PI / 2;
|
||||
pts.push([cx + R * Math.cos(a), cy + R * Math.sin(a)]);
|
||||
}
|
||||
let d = `M ${pts[0][0].toFixed(2)} ${pts[0][1].toFixed(2)} `;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const p2 = pts[(i + 1) % n];
|
||||
d += `A ${r.toFixed(2)} ${r.toFixed(2)} 0 0 0 ${p2[0].toFixed(2)} ${p2[1].toFixed(2)} `;
|
||||
}
|
||||
return d + 'Z';
|
||||
}
|
||||
|
||||
function starPath(cx, cy, outer, inner, points = 5) {
|
||||
let d = '';
|
||||
for (let i = 0; i < points * 2; i++) {
|
||||
const r = i % 2 === 0 ? outer : inner;
|
||||
const a = (i / (points * 2)) * 2 * Math.PI - Math.PI / 2;
|
||||
const x = cx + r * Math.cos(a);
|
||||
const y = cy + r * Math.sin(a);
|
||||
d += `${i === 0 ? 'M' : 'L'} ${x.toFixed(2)} ${y.toFixed(2)} `;
|
||||
}
|
||||
return d + 'Z';
|
||||
}
|
||||
|
||||
export default function VerifiedStamp({ size = 26, className = '' }) {
|
||||
const cx = 50, cy = 50, R = 44, n = 12;
|
||||
const green = '#9FD34E';
|
||||
const textPath = `M ${cx} ${cy - 36} A 36 36 0 1 1 ${cx - 0.01} ${cy - 36}`;
|
||||
const label = 'VERIFIED \u2022 VERIFIED \u2022 VERIFIED \u2022';
|
||||
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 100 100"
|
||||
className={className}
|
||||
role="img"
|
||||
aria-label="Verified identity"
|
||||
>
|
||||
<path d={scallopPath(cx, cy, R, n)} fill={green} />
|
||||
<circle cx={cx} cy={cy} r={36} fill="#FFFFFF" />
|
||||
<circle cx={cx} cy={cy} r={36} fill="none" stroke={green} strokeWidth="1.5" />
|
||||
<path id="verified-stamp-arc" d={textPath} fill="none" />
|
||||
<text fontSize="6.5" fontWeight="700" fill={green} letterSpacing="0.5" fontFamily="Sora, sans-serif">
|
||||
<textPath href="#verified-stamp-arc" startOffset="0%">{label}</textPath>
|
||||
</text>
|
||||
<path d={starPath(cx, cy + 1, 13, 5.5)} fill={green} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
43
src/components/krow/talent/YourMarket.jsx
Normal file
43
src/components/krow/talent/YourMarket.jsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import { Eye, FileText, Calendar, Clock } from 'lucide-react';
|
||||
|
||||
export default function YourMarket({ profile, jobRecs = [] }) {
|
||||
const companies = (jobRecs || []).slice(0, 3).map(r => (r.title || 'Company').split(' ')[0]);
|
||||
const availability = (profile.availability || [])[0] || 'Available This Week';
|
||||
|
||||
const cards = [
|
||||
{ icon: Eye, label: 'Companies viewing you', dots: companies.length ? companies : ['Google', 'Bon Appetit', 'Compass'] },
|
||||
{ icon: FileText, label: 'Applications', value: '2 Pending' },
|
||||
{ icon: Calendar, label: 'Interviews', value: 'Tomorrow 9AM' },
|
||||
{ icon: Clock, label: 'Availability', value: availability },
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="rounded-2xl bg-white border border-[#E5E7EB] p-6">
|
||||
<h2 className="text-[15px] font-semibold text-[#0F172A]">Your Market</h2>
|
||||
<div className="mt-4 grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{cards.map((c) => {
|
||||
const Icon = c.icon;
|
||||
return (
|
||||
<div key={c.label} className="rounded-xl border border-[#E5E7EB] p-4">
|
||||
<div className="flex items-center gap-2 text-[11px] uppercase tracking-wide text-[#6B7280]">
|
||||
<Icon className="w-3.5 h-3.5" /> {c.label}
|
||||
</div>
|
||||
{c.dots ? (
|
||||
<div className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1">
|
||||
{c.dots.map((co, i) => (
|
||||
<span key={i} className="inline-flex items-center gap-1.5 text-[13px] font-medium text-[#0F172A]">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[#16A34A]" /> {co}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="mt-1 text-[15px] font-semibold text-[#0F172A]">{c.value}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
43
src/components/krow/talent/YourNetwork.jsx
Normal file
43
src/components/krow/talent/YourNetwork.jsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import { Star } from 'lucide-react';
|
||||
|
||||
const SEED = [
|
||||
{ name: 'Luis', role: 'Supervisor', events: 12, action: 'Request Reference' },
|
||||
{ name: 'Chef Antonio', role: 'Chef', events: 45, action: 'Endorse Skill' },
|
||||
];
|
||||
|
||||
export default function YourNetwork({ profile }) {
|
||||
const fromExp = (profile.experience || []).slice(0, 2).map((e, i) => ({
|
||||
name: SEED[i]?.name || e.role || 'Colleague',
|
||||
role: e.role || 'Colleague',
|
||||
events: Math.max(1, Math.round((e.years || 1) * 10)),
|
||||
action: i === 0 ? 'Request Reference' : 'Endorse Skill',
|
||||
}));
|
||||
const data = fromExp.length ? fromExp : SEED;
|
||||
|
||||
return (
|
||||
<section className="rounded-2xl bg-white border border-[#E5E7EB] p-6">
|
||||
<h2 className="text-[15px] font-semibold text-[#0F172A]">Your Network</h2>
|
||||
<div className="mt-4 space-y-3">
|
||||
{data.map((n, i) => (
|
||||
<div key={i} className="flex items-center justify-between rounded-xl border border-[#E5E7EB] p-4">
|
||||
<div>
|
||||
<p className="text-[14px] font-semibold text-[#0F172A]">{n.name}</p>
|
||||
<p className="text-[12px] text-[#6B7280]">
|
||||
{n.role} · {n.events} events
|
||||
</p>
|
||||
<div className="mt-1 flex">
|
||||
{Array.from({ length: 5 }).map((_, s) => (
|
||||
<Star key={s} className="w-3.5 h-3.5 text-[#F59E0B] fill-[#F59E0B]" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<button className="text-[12px] font-semibold text-[#0A39DF] border border-[#0A39DF] rounded-full px-3 py-1.5 hover:bg-[#EFF6FF] transition-colors">
|
||||
{n.action}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user