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,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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}