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,201 @@
import React, { useState } from 'react';
import { cn } from '@/lib/utils';
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from '@/components/ui/dropdown-menu';
import { MessageSquare, Phone, Calendar, X, Trash2, ChevronDown, UserCheck, Briefcase } from 'lucide-react';
import { Image } from '@/components/ui/image';
import { getScoreBand } from '@/lib/talentHome';
import CandidateExpandedDetails from '@/components/krow/CandidateExpandedDetails';
const STATUS_STYLES = {
hired: { badge: 'bg-[#E8F0FE] text-[#1A56DB]', label: 'Hired', card: 'bg-white border-[#E5E7EB]' },
ai_screened: { badge: 'bg-[#E8F0FE] text-[#1A56DB]', label: 'AI Screened', card: 'bg-white border-[#E5E7EB]' },
shortlisted: { badge: 'bg-[#E8F0FE] text-[#1A56DB]', label: 'Shortlisted', card: 'bg-white border-[#E5E7EB]' },
interview: { badge: 'bg-[#E8F0FE] text-[#1A56DB]', label: 'Interviewing', card: 'bg-white border-[#E5E7EB]' },
applied: { badge: 'bg-[#F3F4F6] text-[#6B7280]', label: 'Applied', card: 'bg-white border-[#E5E7EB]' },
rejected: { badge: 'bg-[#FEE2E2] text-[#991B1B]', label: 'Rejected', card: 'bg-white border-[#E5E7EB]' },
};
function getInitials(name) {
return name?.split(' ').map(w => w[0]).slice(0, 2).join('').toUpperCase() || '??';
}
const PORTRAIT_PHOTOS = [
'photo-1500648767731-7d3514ad8b31',
'photo-1531123897727-8f129e1688ce',
'photo-1573496359142-b8d87734a5a2',
'photo-1487412720507-e7ab37603c6f',
'photo-1502685104226-ee3a97a2f2c8',
'photo-1544005313-94ddf0286df2',
'photo-1494790108377-be9c29b29330',
'photo-1438761681033-6461ffad8d80',
'photo-1517841905240-472988babdf9',
'photo-1463453091185-61582044d556',
];
function getPortraitSeed(name) {
let h = 0;
for (let i = 0; i < (name || '').length; i++) h = (h * 31 + name.charCodeAt(i)) >>> 0;
return PORTRAIT_PHOTOS[h % PORTRAIT_PHOTOS.length];
}
function getScoreColor(score) {
if (score > 0) return '#0A39DF';
return '#EF4444';
}
function ScoreRing({ score }) {
const color = getScoreColor(score);
const radius = 24;
const circumference = 2 * Math.PI * radius;
const offset = circumference - (score / 100) * circumference;
return (
<div className="relative w-14 h-14 flex items-center justify-center shrink-0">
<svg className="w-14 h-14 -rotate-90" viewBox="0 0 56 56">
<circle cx="28" cy="28" r={radius} fill="none" stroke="#F3F4F6" strokeWidth="4" />
<circle
cx="28" cy="28" r={radius} fill="none" stroke={color} strokeWidth="4"
strokeDasharray={circumference} strokeDashoffset={offset} strokeLinecap="round"
/>
</svg>
<span className="absolute text-[13px] font-bold text-[#111827]">{score}</span>
</div>
);
}
export default function CandidateCard({ application, jobTitle, rank, onAction, onMessage, onCall, onSchedule, onDecline, onDelete, onHire }) {
const position = jobTitle || application.job_title || '';
const status = application.status || 'applied';
const style = STATUS_STYLES[status] || STATUS_STYLES.applied;
const score = application.ai_score || 0;
const canTakeAction = status !== 'hired' && status !== 'rejected';
const [expanded, setExpanded] = useState(false);
return (
<div className={cn('border rounded-xl p-4 transition-all hover:shadow-sm', style.card)}>
<div
className="flex items-center gap-4 cursor-pointer"
onClick={() => setExpanded(!expanded)}
>
{/* Rank */}
<div className="flex flex-col items-center justify-center gap-0.5 shrink-0 w-8">
<span className="text-[18px] font-bold text-[#0A39DF]">{rank}</span>
</div>
{/* Avatar */}
<div className="w-11 h-11 rounded-full bg-[#E5E7EB] flex items-center justify-center text-white font-semibold text-[13px] shrink-0 overflow-hidden">
<Image
src={application.selfie_url || `https://images.unsplash.com/${getPortraitSeed(application.applicant_name)}?w=120&h=120&fit=crop&crop=faces`}
alt={application.applicant_name}
className="w-full h-full object-cover"
fittingType="fill"
/>
</div>
{/* Info */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<h3 className="font-semibold text-[#111827] text-[14px] truncate">{application.applicant_name}</h3>
<span className={cn('px-2 py-0.5 rounded-full text-[10px] font-medium', style.badge)}>{style.label}</span>
{score > 0 && (() => {
const band = getScoreBand(score);
return (
<span
className="px-2 py-0.5 rounded-full text-[10px] font-bold text-white"
style={{ backgroundColor: band.color }}
>
{band.label}
</span>
);
})()}
</div>
<div className="flex items-center gap-3 mt-1 text-[11px] text-[#6B7280] flex-wrap">
{position && (
<span className="inline-flex items-center gap-1 font-medium text-[#0A39DF]">
<Briefcase className="w-3 h-3" />
{position}
</span>
)}
{application.email && (
<span>{application.email}</span>
)}
{application.phone && (
<span>{application.phone}</span>
)}
{application.years_experience != null && (
<span>{application.years_experience}yr exp</span>
)}
</div>
{application.ai_match_label && (
<div className="mt-1.5">
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-[#0A39DF] text-white text-[10px] font-bold">
+ {application.ai_match_label}
</span>
</div>
)}
</div>
{/* Score */}
{status !== 'applied' && <ScoreRing score={score} />}
{/* Actions */}
<div className="flex items-center gap-2 shrink-0" onClick={(e) => e.stopPropagation()}>
{onAction && canTakeAction && (
<button
onClick={() => onAction(application)}
className="px-3 py-1.5 rounded-lg bg-[#0A39DF] hover:bg-[#082EB4] text-white text-[12px] font-medium transition-colors whitespace-nowrap"
>
{status === 'interview' ? 'AI Interview' : 'Interview'}
</button>
)}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="inline-flex items-center gap-1 px-3 py-1.5 rounded-lg border border-[#E5E7EB] bg-white hover:bg-[#F9FAFB] text-[#374151] text-[12px] font-medium transition-colors">
Actions
<ChevronDown className="w-3.5 h-3.5" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
{onMessage && (
<DropdownMenuItem onClick={() => onMessage(application)} className="cursor-pointer py-2">
<MessageSquare className="w-4 h-4 mr-2.5 text-[#0A39DF]" />
<span className="text-[#374151]">Message</span>
</DropdownMenuItem>
)}
{onCall && (
<DropdownMenuItem onClick={() => onCall(application)} className="cursor-pointer py-2">
<Phone className="w-4 h-4 mr-2.5 text-[#059669]" />
<span className="text-[#059669]">Call</span>
</DropdownMenuItem>
)}
{onSchedule && (
<DropdownMenuItem onClick={() => onSchedule(application)} className="cursor-pointer py-2">
<Calendar className="w-4 h-4 mr-2.5 text-[#0A39DF]" />
<span className="text-[#0A39DF]">Set up interview</span>
</DropdownMenuItem>
)}
{onHire && (
<DropdownMenuItem onClick={() => onHire(application)} className="cursor-pointer py-2">
<UserCheck className="w-4 h-4 mr-2.5 text-[#059669]" />
<span className="text-[#059669]">Hire candidate</span>
</DropdownMenuItem>
)}
{onDecline && (
<DropdownMenuItem onClick={() => onDecline(application)} className="cursor-pointer py-2">
<X className="w-4 h-4 mr-2.5 text-[#DC2626]" />
<span className="text-[#DC2626]">Decline candidate</span>
</DropdownMenuItem>
)}
{onDelete && (
<DropdownMenuItem onClick={() => onDelete(application)} className="cursor-pointer py-2">
<Trash2 className="w-4 h-4 mr-2.5 text-[#DC2626]" />
<span className="text-[#DC2626]">Delete candidate</span>
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
{expanded && <CandidateExpandedDetails application={application} />}
</div>
);
}