Files
krow_talent_app/src/pages/Overview.jsx
2026-08-13 21:41:14 +05:30

198 lines
9.3 KiB
JavaScript

import React from 'react';
import { useNavigate } from 'react-router-dom';
import { motion } from 'framer-motion';
import { useJobPostings, useApplications } from '@/lib/krowHooks';
import KROWCandidateLeaderboardReal from '@/components/krow/KROWCandidateLeaderboard';
import { Briefcase, Users, Brain, Clock, TrendingUp, Award, ChevronRight } from 'lucide-react';
const METRICS = [
{ icon: Briefcase, label: 'Open Positions', bg: 'bg-[#0838E0]', glow: 'shadow-blue-600/20' },
{ icon: Users, label: 'Total Candidates', bg: 'bg-[#333F48]', glow: 'shadow-slate-600/20' },
{ icon: Brain, label: 'AI Screened', bg: 'bg-[#0838E0]', glow: 'shadow-blue-600/20' },
{ icon: Clock, label: 'Pending Screen', bg: 'bg-[#F9E547]', glow: 'shadow-yellow-400/30' },
{ icon: TrendingUp, label: 'Avg AI Score', bg: 'bg-[#333F48]', glow: 'shadow-slate-600/20' },
{ icon: Award, label: 'Hired', bg: 'bg-[#0838E0]', glow: 'shadow-blue-600/20' },
];
function MetricCard({ icon: Icon, value, label, bg, glow, index }) {
const isYellow = bg === 'bg-[#F9E547]';
return (
<motion.div
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.06, duration: 0.4, ease: 'easeOut' }}
whileHover={{ y: -4, transition: { duration: 0.2 } }}
className="glass-card border border-white/60 rounded-2xl p-4 shadow-sm hover:shadow-lg transition-shadow"
>
<div className={`w-10 h-10 rounded-xl ${bg} flex items-center justify-center shadow-lg ${glow} mb-3`}>
<Icon className={`w-5 h-5 ${isYellow ? 'text-[#0838E0]' : 'text-white'}`} />
</div>
<motion.div
key={value}
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ delay: index * 0.06 + 0.2, type: 'spring', stiffness: 200 }}
className="text-3xl font-heading font-bold gradient-text"
>
{value}
</motion.div>
<div className="text-[11px] text-[#9CA3AF] font-medium tracking-wide mt-0.5">{label}</div>
</motion.div>
);
}
export default function Overview() {
const navigate = useNavigate();
const { data: postings = [] } = useJobPostings();
const { data: applications = [] } = useApplications();
const values = [
postings.filter(p => p.status === 'active').length,
applications.length,
applications.filter(a => ['ai_screened', 'interview', 'hired', 'shortlisted'].includes(a.status)).length,
applications.filter(a => a.status === 'applied').length,
(() => { const s = applications.filter(a => a.ai_score > 0); return s.length ? Math.round(s.reduce((sum, a) => sum + a.ai_score, 0) / s.length) : 0; })(),
applications.filter(a => a.status === 'hired').length,
];
const topCandidates = [...applications.filter(a => a.ai_score > 0)].sort((a, b) => b.ai_score - a.ai_score).slice(0, 5);
const activePostings = postings.filter(p => p.status === 'active').slice(0, 5);
const statsForPosting = (postingId) => {
const apps = applications.filter(a => a.job_posting_id === postingId);
return {
applied: apps.length,
screened: apps.filter(a => a.status !== 'applied').length,
};
};
return (
<div className="space-y-8">
{/* Hero greeting */}
<motion.div
initial={{ opacity: 0, y: -12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="flex items-center justify-between flex-wrap gap-4"
>
<div className="min-w-0">
<h1 className="text-2xl sm:text-3xl font-heading font-bold text-[#111827]">
Good day. <span className="gradient-text">Let's hire smarter.</span>
</h1>
<p className="text-[13px] sm:text-[14px] text-[#6B7280] mt-1">AI-powered hiring · No quizzes · Real readiness</p>
</div>
</motion.div>
{/* Metrics Grid */}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
{METRICS.map((m, i) => (
<MetricCard key={m.label} icon={m.icon} value={values[i]} label={m.label} bg={m.bg} glow={m.glow} index={i} />
))}
</div>
{/* Top Candidates + Open Positions */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Top Candidates */}
<motion.div
initial={{ opacity: 0, x: -16 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.5 }}
className="glass-card border border-white/60 rounded-2xl p-6 shadow-sm"
>
<div className="flex items-center justify-between mb-5">
<h3 className="font-heading font-semibold text-[#111827] text-[16px]">Top Candidates</h3>
<button onClick={() => navigate('/candidates')} className="text-[12px] text-[#0838E0] hover:underline inline-flex items-center gap-1 font-medium">
View all <ChevronRight className="w-3 h-3" />
</button>
</div>
<div className="space-y-1">
{topCandidates.length === 0 && (
<div className="text-center py-10">
<div className="w-12 h-12 rounded-full bg-white/50 flex items-center justify-center mx-auto mb-3">
<Users className="w-5 h-5 text-[#9CA3AF]" />
</div>
<p className="text-[13px] text-[#9CA3AF]">No screened candidates yet</p>
</div>
)}
{topCandidates.map((app, idx) => (
<motion.div
key={app.id}
initial={{ opacity: 0, x: -8 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.6 + idx * 0.06 }}
className="flex items-center gap-3 p-2.5 rounded-xl hover:bg-white/50 transition-colors cursor-pointer"
onClick={() => navigate('/candidates')}
>
<span className="text-[13px] font-heading font-bold text-[#9CA3AF] w-7">{idx + 1}</span>
<div className="w-9 h-9 rounded-full bg-[#0838E0] flex items-center justify-center text-white text-[12px] font-semibold shrink-0">
{app.applicant_name?.split(' ').map(w => w[0]).slice(0, 2).join('').toUpperCase()}
</div>
<div className="flex-1 min-w-0">
<div className="text-[13px] font-medium text-[#111827] truncate">{app.applicant_name}</div>
<div className="text-[11px] text-[#6B7280] truncate">{app.job_title || 'Applicant'}</div>
</div>
<div className="flex items-center gap-2">
<div className="w-16 h-1.5 rounded-full bg-gray-100 overflow-hidden">
<div className="h-full bg-[#0838E0] rounded-full" style={{ width: `${app.ai_score}%` }} />
</div>
<span className="text-[14px] font-heading font-bold gradient-text w-7 text-right">{app.ai_score}</span>
</div>
</motion.div>
))}
</div>
</motion.div>
{/* Open Positions */}
<motion.div
initial={{ opacity: 0, x: 16 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.5 }}
className="glass-card border border-white/60 rounded-2xl p-6 shadow-sm"
>
<div className="flex items-center justify-between mb-5">
<h3 className="font-heading font-semibold text-[#111827] text-[16px]">Open Positions</h3>
<button onClick={() => navigate('/positions')} className="text-[12px] text-[#0838E0] hover:underline inline-flex items-center gap-1 font-medium">
View all <ChevronRight className="w-3 h-3" />
</button>
</div>
<div className="space-y-1">
{activePostings.length === 0 && (
<div className="text-center py-10">
<div className="w-12 h-12 rounded-full bg-white/50 flex items-center justify-center mx-auto mb-3">
<Briefcase className="w-5 h-5 text-[#9CA3AF]" />
</div>
<p className="text-[13px] text-[#9CA3AF]">No open positions</p>
</div>
)}
{activePostings.map((posting, idx) => {
const stats = statsForPosting(posting.id);
return (
<motion.div
key={posting.id}
initial={{ opacity: 0, x: 8 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.6 + idx * 0.06 }}
onClick={() => navigate(`/positions/${posting.id}`)}
className="flex items-center gap-3 p-2.5 rounded-xl hover:bg-white/50 transition-colors cursor-pointer group"
>
<span className="text-[13px] font-heading font-bold text-[#9CA3AF] w-7">{idx + 1}</span>
<div className="w-1 h-9 rounded-full bg-[#0838E0] shrink-0" />
<div className="flex-1 min-w-0">
<div className="text-[13px] font-medium text-[#111827] truncate">{posting.title}</div>
<div className="text-[11px] text-[#6B7280]">{stats.applied} applicants · {stats.screened} screened</div>
</div>
<ChevronRight className="w-4 h-4 text-[#D1D5DB] group-hover:text-[#0838E0] transition-colors" />
</motion.div>
);
})}
</div>
</motion.div>
</div>
{/* Candidate Pipeline */}
<KROWCandidateLeaderboardReal applications={applications} />
</div>
);
}