iris_backend / src /components /Admin /AdminSortingPage.jsx
Muhammed Sameer
new feature implemented
59f9574
import React, { useState, useMemo, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { supabase } from '../../supabaseClient';
import CandidateDrawer from '../CandidateDrawer';
// ✅ IMPORT ICONS FROM YOUR SEPARATE FILE
import {
FilterIcon, ScoringIcon, ClearIcon, ViewIcon,
ChevronDownIcon, SearchIcon, ChevronLeftIcon,
ChevronRightIcon, CheckSquareIcon, MailIcon, LoaderIcon, RoundCheckbox
} from '../../components/Icons';
// --- REUSABLE BUTTON COMPONENT ---
const BulkActionButton = ({ Icon, label, color, onClick }) => {
const [hover, setHover] = useState(false);
return (
<motion.button
onClick={onClick}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
layout
style={{
display: 'flex', alignItems: 'center',
backgroundColor: hover ? color : 'rgba(255,255,255,0.05)',
border: `1px solid ${hover ? color : 'rgba(255,255,255,0.2)'}`,
borderRadius: '20px', padding: '0.5rem',
height: '40px', minWidth: '40px',
cursor: 'pointer', color: hover ? 'white' : '#94a3b8',
boxShadow: hover ? `0 0 15px ${color}66` : 'none',
justifyContent: 'center', outline: 'none'
}}
transition={{ type: 'spring', stiffness: 500, damping: 30 }}
>
<Icon />
<AnimatePresence>
{hover && (
<motion.span
initial={{ width: 0, opacity: 0, marginLeft: 0 }}
animate={{ width: 'auto', opacity: 1, marginLeft: 8 }}
exit={{ width: 0, opacity: 0, marginLeft: 0 }}
style={{ overflow: 'hidden', whiteSpace: 'nowrap', fontWeight: '600', fontSize: '0.85rem' }}
>
{label}
</motion.span>
)}
</AnimatePresence>
</motion.button>
);
};
// --- FILTER PANEL COMPONENT ---
const FilterPanel = ({ filters, setFilters, jobOptions = [] }) => {
const languages = ["JavaScript", "TypeScript", "Python", "Java", "C++", "C#", "React", "Go", "Rust", "Swift", "Kotlin", "PHP"];
const toggleItem = (category, item) => {
const current = filters[category] || [];
const updated = current.includes(item) ? current.filter(i => i !== item) : [...current, item];
setFilters({ ...filters, [category]: updated });
};
return (
<div style={{ padding: '0 1.5rem 1.5rem 1.5rem', color: '#e2e8f0' }}>
<div style={{ height: '1px', backgroundColor: 'rgba(239, 68, 68, 0.2)', marginBottom: '1.5rem' }}></div>
{/* Status & Score Group */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '2rem', marginBottom: '2rem' }}>
<div>
<h4 style={{ fontSize: '0.85rem', color: '#94a3b8', marginBottom: '0.75rem', fontWeight: '600' }}>Status</h4>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
{['All', 'Pending', 'Accepted', 'Rejected'].map(status => (
<label key={status} style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '0.9rem', cursor: 'pointer' }}>
<div style={{ marginRight: '8px' }}>
<RoundCheckbox
checked={filters.status === status}
onChange={() => setFilters({ ...filters, status })}
/>
</div>
{status}
</label>
))}
</div>
</div>
<div>
<h4 style={{ fontSize: '0.85rem', color: '#94a3b8', marginBottom: '0.75rem', fontWeight: '600' }}>Min. Match Score</h4>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<input type="range" min="0" max="100" value={filters.minScore} onChange={(e) => setFilters({ ...filters, minScore: parseInt(e.target.value) })} style={{ width: '100%', accentColor: '#EF4444', height: '4px', background: 'rgba(255,255,255,0.1)', borderRadius: '2px' }} />
<span style={{ fontWeight: 'bold', color: '#EF4444', minWidth: '3rem' }}>{filters.minScore}%</span>
</div>
</div>
</div>
{/* List Group */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '2rem' }}>
<div>
<h4 style={{ fontSize: '0.85rem', color: '#94a3b8', marginBottom: '0.75rem', fontWeight: '600' }}>Languages</h4>
<div className="hide-scrollbar" style={{ height: '150px', overflowY: 'auto', backgroundColor: 'rgba(0,0,0,0.2)', borderRadius: '8px', padding: '0.5rem', border: '1px solid rgba(255,255,255,0.05)' }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0.5rem' }}>
{languages.map(lang => (
<label key={lang} style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '0.85rem', cursor: 'pointer', padding: '4px', borderRadius: '4px', backgroundColor: (filters.languages || []).includes(lang) ? 'rgba(239, 68, 68, 0.1)' : 'transparent' }}>
<RoundCheckbox
checked={(filters.languages || []).includes(lang)}
onChange={() => toggleItem('languages', lang)}
/>
<span style={{ color: (filters.languages || []).includes(lang) ? '#EF4444' : 'inherit', marginLeft: '8px' }}>{lang}</span>
</label>
))}
</div>
</div>
</div>
<div>
<h4 style={{ fontSize: '0.85rem', color: '#94a3b8', marginBottom: '0.75rem', fontWeight: '600' }}>Job Positions</h4>
<div className="hide-scrollbar" style={{ height: '150px', overflowY: 'auto', backgroundColor: 'rgba(0,0,0,0.2)', borderRadius: '8px', padding: '0.5rem', border: '1px solid rgba(255,255,255,0.05)' }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: '0.5rem' }}>
{jobOptions.length > 0 ? (
jobOptions.map(pos => (
<label key={pos} style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '0.85rem', cursor: 'pointer', padding: '4px', borderRadius: '4px', backgroundColor: (filters.positions || []).includes(pos) ? 'rgba(239, 68, 68, 0.1)' : 'transparent' }}>
<RoundCheckbox
checked={(filters.positions || []).includes(pos)}
onChange={() => toggleItem('positions', pos)}
/>
<span style={{ color: (filters.positions || []).includes(pos) ? '#EF4444' : 'inherit', marginLeft: '8px' }}>{pos}</span>
</label>
))
) : (
<span style={{ color: '#64748b', fontSize: '0.8rem', padding: '0.5rem' }}>No applicants found.</span>
)}
</div>
</div>
</div>
</div>
</div>
);
};
// --- SCORING PANEL COMPONENT ---
const ScoringPanel = ({ config, setConfig, onReset, onClose }) => {
const handleChange = (key, value) => setConfig({ ...config, [key]: parseInt(value) });
// Internal slider for this component
const ConfigSlider = ({ label, value, min, max, onChangeKey }) => (
<div style={{ marginBottom: '1rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.25rem', fontSize: '0.85rem' }}>
<span style={{ color: '#e2e8f0' }}>{label}</span>
<span style={{ fontWeight: 'bold', color: '#EF4444' }}>{value}</span>
</div>
<input type="range" min={min} max={max} value={value} onChange={(e) => handleChange(onChangeKey, e.target.value)} style={{ width: '100%', accentColor: '#EF4444', height: '4px', background: 'rgba(255,255,255,0.1)', borderRadius: '2px' }} />
</div>
);
return (
<div style={{ padding: '0 1.5rem 1.5rem 1.5rem' }}>
<div style={{ height: '1px', backgroundColor: 'rgba(239, 68, 68, 0.2)', marginBottom: '1.5rem' }}></div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '2rem' }}>
<div>
<ConfigSlider label="Skills Weight" value={config.skillsWeight} min={1} max={10} onChangeKey="skillsWeight" />
<ConfigSlider label="Experience Weight" value={config.experienceWeight} min={1} max={10} onChangeKey="experienceWeight" />
</div>
<div>
<ConfigSlider label="Certification Bonus" value={config.certBonus} min={1} max={10} onChangeKey="certBonus" />
<ConfigSlider label="Projects Weight" value={config.projectsWeight} min={1} max={10} onChangeKey="projectsWeight" />
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '1rem' }}>
<button onClick={onReset} style={{ background: 'transparent', border: '1px solid rgba(255,255,255,0.2)', color: '#94a3b8', padding: '0.4rem 0.8rem', borderRadius: '4px', cursor: 'pointer', fontSize: '0.8rem' }}>Reset Default</button>
<button onClick={onClose} style={{ backgroundColor: '#EF4444', border: 'none', color: 'white', padding: '0.4rem 1.2rem', borderRadius: '4px', cursor: 'pointer', fontSize: '0.8rem', fontWeight: 'bold' }}>Apply</button>
</div>
</div>
);
};
// --- MAIN PAGE COMPONENT ---
export default function AdminSortingPage() {
const [applicants, setApplicants] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [openPanel, setOpenPanel] = useState(null);
const [searchQuery, setSearchQuery] = useState('');
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [drawerCandidate, setDrawerCandidate] = useState(null);
// Pagination & Selection
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 5;
const [selectedIds, setSelectedIds] = useState([]);
// Filters
const initialFilters = { sortBy: 'Match Score', status: 'All', minScore: 0, languages: [], positions: [] };
const [filters, setFilters] = useState(initialFilters);
const [availableJobs, setAvailableJobs] = useState([]);
// Scoring
const defaultScoring = { skillsWeight: 2, experienceWeight: 5, certBonus: 3, projectsWeight: 3 };
const [scoringConfig, setScoringConfig] = useState(defaultScoring);
//candidate overview
const handleViewCandidate = (candidate) => {
setDrawerCandidate(candidate);
setIsDrawerOpen(true);
};
// --- DATA FETCHING (Supabase) ---
useEffect(() => {
const fetchApplicants = async () => {
setIsLoading(true);
try {
const { data, error } = await supabase
.from('applications')
.select(`
id,
resume_url,
created_at,
status,
match_score,
skills,
skills_match,
technical_skills_match,
work_experience_match,
education_match,
certifications_match,
project_match,
profiles ( id, full_name, email, avatar_url, experience_years, languages, resume_url ),
jobs ( id, title )
`);
if (error) throw error;
const formattedData = data.map(app => ({
id: app.id,
userId: app.profiles?.id,
jobId: app.jobs?.id,
name: app.profiles?.full_name || 'Unknown Candidate',
email: app.profiles?.email || 'No Email',
img: app.profiles?.avatar_url || `https://ui-avatars.com/api/?name=${encodeURIComponent(app.profiles?.full_name || 'User')}&background=random`,
jobTitle: app.jobs?.title || app.profiles?.job_title || 'Applicant',
experience: parseInt(app.profiles?.experience_years) || 0,
skills: app.skills || [],
status: app.status,
resumeUrl: app.resume_url || app.profiles?.resume_url,
dbScore: app.match_score || 0,
scores: {
skills: app.skills_match || 0,
technical: app.technical_skills_match || 0,
experience: app.work_experience_match || 0,
certifications: app.certifications_match || 0,
languages: 0,
education: app.education_match || 0,
projects: app.project_match || 0
}
}));
setApplicants(formattedData);
// Extract unique job titles
const uniqueTitles = [...new Set(formattedData.map(app => app.jobTitle).filter(Boolean))];
setAvailableJobs(uniqueTitles);
} catch (error) {
console.error('Error fetching applicants:', error.message);
} finally {
setIsLoading(false);
}
};
fetchApplicants();
}, []);
// --- HELPER: Send message to candidate ---
const sendMessageToCandidate = async (candidateUserId, message) => {
try {
const { data: { user: adminUser } } = await supabase.auth.getUser();
if (!adminUser) return;
const { error } = await supabase.from('messages').insert([{
sender_id: adminUser.id,
receiver_id: candidateUserId,
content: message,
is_read: false
}]);
if (error) console.error('Error sending message:', error);
} catch (err) {
console.error('Failed to send message:', err);
}
};
// --- BULK ACTIONS ---
const handleBulkReject = async () => {
if (!confirm(`Are you sure you want to REJECT ${selectedIds.length} candidates?`)) return;
try {
const { error } = await supabase
.from('applications')
.update({ status: 'Rejected' })
.in('id', selectedIds);
if (error) throw error;
// ✅ Send rejection message to each candidate
const rejectedApplicants = applicants.filter(a => selectedIds.includes(a.id));
for (const applicant of rejectedApplicants) {
const jobContext = applicant.jobTitle ? ` for ${applicant.jobTitle}` : '';
await sendMessageToCandidate(
applicant.userId,
`📧 We regret to inform you that your application${jobContext} has been rejected. We appreciate your interest and wish you the best of luck in your career. Feel free to apply again in the future!`
);
}
// Update UI instantly
setApplicants(prev => prev.map(app =>
selectedIds.includes(app.id) ? { ...app, status: 'Rejected' } : app
));
setSelectedIds([]);
alert('Candidates Rejected and notified.');
} catch (error) {
console.error('Error rejecting:', error.message);
alert('Failed to reject.');
}
};
const handleBulkApprove = async () => {
if (!confirm(`Are you sure you want to approve ${selectedIds.length} candidates?`)) return;
try {
const { error } = await supabase
.from('applications')
.update({ status: 'Accepted' })
.in('id', selectedIds);
if (error) throw error;
// ✅ Send acceptance message to each candidate
const approvedApplicants = applicants.filter(a => selectedIds.includes(a.id));
for (const applicant of approvedApplicants) {
const jobContext = applicant.jobTitle ? ` for ${applicant.jobTitle}` : '';
await sendMessageToCandidate(
applicant.userId,
`🎉 Congratulations! Your application${jobContext} has been accepted. We are excited about the possibility of working with you. Our team will be in touch soon to schedule an interview.`
);
}
setApplicants(prev => prev.map(app =>
selectedIds.includes(app.id) ? { ...app, status: 'Accepted' } : app
));
setSelectedIds([]);
alert('Approved Successfully and candidates notified!');
} catch (error) {
console.error('Error approving:', error.message);
alert('Failed to update.');
}
};
const handleBulkEmail = () => {
const emails = applicants.filter(a => selectedIds.includes(a.id)).map(a => a.email).join(',');
window.location.href = `mailto:?bcc=${emails}&subject=Interview Update`;
};
// --- SORTING & FILTERING ---
const filteredApplicants = useMemo(() => {
return applicants.map(app => {
// Dynamic Score Calculation
const { skillsWeight, experienceWeight, certBonus, projectsWeight } = scoringConfig;
// Using a weighted average formula
// Denominator: weights + fixed education(2)
const totalWeight = skillsWeight + experienceWeight + projectsWeight + certBonus;
const dynamicScore = Math.round(
(Math.max(app.scores.skills, app.scores.technical) * skillsWeight +
app.scores.experience * experienceWeight +
app.scores.education * 2 + // Education as a 2x base
app.scores.projects * projectsWeight + // Projects now dynamic
app.scores.certifications * certBonus) / (totalWeight + 2)
);
// If calculation leads to 0 but DB has a score, and weights are default, show DB score
const finalScore = (dynamicScore === 0 && app.dbScore > 0) ? app.dbScore : dynamicScore;
return { ...app, score: finalScore };
}).filter(app => {
const matchesSearch = app.name.toLowerCase().includes(searchQuery.toLowerCase()) || (app.jobTitle || '').toLowerCase().includes(searchQuery.toLowerCase());
const matchesStatus = filters.status === 'All' || app.status === filters.status;
const matchesScore = (app.score || 0) >= filters.minScore;
const matchesLang = (filters.languages || []).length === 0 || (filters.languages || []).some(l => (app.skills || []).includes(l));
const matchesPos = (filters.positions || []).length === 0 || (filters.positions || []).includes(app.jobTitle);
return matchesSearch && matchesStatus && matchesScore && matchesLang && matchesPos;
}).sort((a, b) => {
if (filters.sortBy === 'Match Score') return (b.score || 0) - (a.score || 0);
if (filters.sortBy === 'Experience') return (b.experience || 0) - (a.experience || 0);
if (filters.sortBy === 'Name') return a.name.localeCompare(b.name);
return 0;
});
}, [searchQuery, filters, applicants, scoringConfig]);
// --- PAGINATION & SELECTION UTILS ---
const totalPages = Math.ceil(filteredApplicants.length / itemsPerPage);
const paginatedApplicants = filteredApplicants.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage);
const toggleSelectAll = () => {
if (selectedIds.length === paginatedApplicants.length && paginatedApplicants.length > 0) {
setSelectedIds([]);
} else {
setSelectedIds(paginatedApplicants.map(a => a.id));
}
};
const toggleSelectRow = (id) => {
if (selectedIds.includes(id)) setSelectedIds(selectedIds.filter(sid => sid !== id));
else setSelectedIds([...selectedIds, id]);
};
const togglePanel = (panelName) => {
if (openPanel === panelName) setOpenPanel(null);
else setOpenPanel(panelName);
};
return (
<div style={{ paddingBottom: '4rem' }}>
<style>{`.hide-scrollbar::-webkit-scrollbar { display: none; } .hide-scrollbar { -ms-overflow-style: none; scrollbar-width: none; } @keyframes spin { 100% { transform: rotate(360deg); } }`}</style>
<header style={{ marginBottom: '2rem' }}>
<h1 style={{ fontSize: '1.875rem', fontWeight: 'bold' }}>CV Sorting</h1>
</header>
{/* Controls Bar */}
<div style={{ backgroundColor: 'rgba(239, 68, 68, 0.05)', border: '1px solid rgba(239, 68, 68, 0.2)', borderRadius: '1rem', overflow: 'hidden', marginBottom: '2rem' }}>
<div style={{ padding: '1.5rem 1.5rem 0.5rem 1.5rem', display: 'flex', gap: '1rem', alignItems: 'center', flexWrap: 'wrap' }}>
<div style={{ position: 'relative', flexGrow: 1 }}>
<div style={{ position: 'absolute', left: '12px', top: '50%', transform: 'translateY(-50%)', color: '#94a3b8' }}><SearchIcon /></div>
<input type="text" placeholder="Search..." value={searchQuery} onChange={e => setSearchQuery(e.target.value)} style={{ width: '100%', padding: '0.75rem 0.75rem 0.75rem 2.5rem', borderRadius: '0.5rem', border: '1px solid rgba(239, 68, 68, 0.3)', backgroundColor: 'rgba(255,255,255,0.05)', color: 'white' }} />
</div>
<motion.button onClick={() => togglePanel('filter')} whileHover={{ scale: 1.02 }} style={{ backgroundColor: openPanel === 'filter' ? '#EF4444' : 'rgba(255,255,255,0.1)', color: 'white', padding: '0.75rem 1.2rem', borderRadius: '0.5rem', border: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px', fontWeight: '600' }}>
<FilterIcon /> Filters <ChevronDownIcon isOpen={openPanel === 'filter'} />
</motion.button>
<motion.button onClick={() => togglePanel('scoring')} whileHover={{ scale: 1.02 }} style={{ backgroundColor: openPanel === 'scoring' ? '#EF4444' : 'rgba(255,255,255,0.1)', color: 'white', padding: '0.75rem 1.2rem', borderRadius: '0.5rem', border: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px', fontWeight: '600' }}>
<ScoringIcon /> Scoring <ChevronDownIcon isOpen={openPanel === 'scoring'} />
</motion.button>
<motion.button onClick={() => { setSearchQuery(''); setFilters({ sortBy: 'Match Score', status: 'All', minScore: 0, languages: [], positions: [] }); }} whileHover={{ scale: 1.02 }} style={{ backgroundColor: 'rgba(255,255,255,0.1)', color: 'white', padding: '0.75rem 1rem', borderRadius: '0.5rem', border: '1px solid rgba(255,255,255,0.2)', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '6px' }}>
<ClearIcon /> Clear
</motion.button>
</div>
<div style={{ padding: '0 1.5rem 1.5rem 1.5rem', display: 'flex', alignItems: 'center', gap: '1rem' }}>
<span style={{ fontSize: '0.85rem', fontWeight: 'bold', color: '#94a3b8', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Sort By</span>
<div style={{ display: 'flex', gap: '0.5rem' }}>
{['Match Score', 'Experience', 'Name', 'Date'].map(opt => (
<button key={opt} onClick={() => setFilters({ ...filters, sortBy: opt })} style={{ padding: '0.4rem 1rem', borderRadius: '6px', fontSize: '0.85rem', cursor: 'pointer', backgroundColor: filters.sortBy === opt ? '#EF4444' : 'transparent', color: 'white', border: filters.sortBy === opt ? '1px solid #EF4444' : '1px solid rgba(255,255,255,0.2)', fontWeight: filters.sortBy === opt ? '600' : 'normal', transition: 'all 0.2s' }}>{opt}</button>
))}
</div>
</div>
<AnimatePresence>
{openPanel === 'filter' && <motion.div initial={{ height: 0, opacity: 0 }} animate={{ height: 'auto', opacity: 1 }} exit={{ height: 0, opacity: 0 }} style={{ overflow: 'hidden', backgroundColor: 'rgba(0,0,0,0.2)' }}><FilterPanel filters={filters} setFilters={setFilters} jobOptions={availableJobs} /></motion.div>}
{openPanel === 'scoring' && <motion.div initial={{ height: 0, opacity: 0 }} animate={{ height: 'auto', opacity: 1 }} exit={{ height: 0, opacity: 0 }} style={{ overflow: 'hidden', backgroundColor: 'rgba(0,0,0,0.2)' }}><ScoringPanel config={scoringConfig} setConfig={setScoringConfig} onReset={() => setScoringConfig(defaultScoring)} onClose={() => setOpenPanel(null)} /></motion.div>}
</AnimatePresence>
</div>
{/* Results Table */}
<div style={{ backgroundColor: 'rgba(239, 68, 68, 0.05)', border: '1px solid rgba(239, 68, 68, 0.2)', borderRadius: '1rem', padding: '1.5rem', overflow: 'hidden', position: 'relative', minHeight: '300px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1.5rem' }}>
<h2 style={{ fontSize: '1.5rem', fontWeight: 'bold' }}>Applications <span style={{ fontSize: '0.9rem', color: '#94a3b8', fontWeight: 'normal' }}>({filteredApplicants.length})</span></h2>
<AnimatePresence>
{selectedIds.length > 0 && (
<motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: 20 }} style={{ display: 'flex', gap: '1rem', alignItems: 'center', backgroundColor: 'rgba(239, 68, 68, 0.1)', padding: '0.5rem 1rem', borderRadius: '12px', border: '1px solid rgba(239, 68, 68, 0.2)' }}>
<span style={{ fontSize: '0.85rem', color: '#fff', fontWeight: 'bold' }}>{selectedIds.length} Selected</span>
<BulkActionButton
Icon={CheckSquareIcon}
label="Accept"
color="#10b981"
onClick={handleBulkApprove}
/>
<BulkActionButton
Icon={ClearIcon}
label="Reject"
color="#ef4444"
onClick={handleBulkReject}
/>
<BulkActionButton Icon={MailIcon} label="Email" color="#3b82f6" onClick={handleBulkEmail} />
</motion.div>
)}
</AnimatePresence>
</div>
{isLoading ? (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '200px', color: '#EF4444' }}>
<LoaderIcon />
</div>
) : (
<>
<div className="hide-scrollbar" style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'separate', borderSpacing: '0 8px', minWidth: '800px' }}>
<thead>
<tr style={{ color: '#d1ddeb', textAlign: 'left', fontSize: '0.9rem' }}>
<th style={{ padding: '0 0 1rem 1rem', width: '40px' }}>
<RoundCheckbox
checked={paginatedApplicants.length > 0 && selectedIds.length === paginatedApplicants.length}
onChange={toggleSelectAll}
/>
</th>
<th style={{ padding: '0 1rem 1rem 0' }}>Applicant</th>
<th style={{ padding: '0 1rem 1rem 1rem' }}>Experience</th>
<th style={{ padding: '0 1rem 1rem 1rem' }}>Job Title</th>
<th style={{ padding: '0 1rem 1rem 1rem' }}>Score</th>
<th style={{ padding: '0 1rem 1rem 1rem' }}>Status</th>
<th style={{ padding: '0 1rem 1rem 1rem', textAlign: 'right' }}>Action</th>
</tr>
</thead>
<tbody>
<AnimatePresence>
{paginatedApplicants.map((app) => {
const isSelected = selectedIds.includes(app.id);
return (
<motion.tr
key={app.id}
initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }}
whileHover={{ scale: 1.005, backgroundColor: 'rgba(255,255,255,0.05)' }}
style={{ backgroundColor: isSelected ? 'rgba(239, 68, 68, 0.1)' : 'rgba(255,255,255,0.02)', borderRadius: '8px', cursor: 'pointer', border: isSelected ? '1px solid rgba(239, 68, 68, 0.3)' : '1px solid transparent' }}
onClick={() => toggleSelectRow(app.id)}
>
<td style={{ padding: '1rem', borderTopLeftRadius: '8px', borderBottomLeftRadius: '8px' }}>
<RoundCheckbox
checked={isSelected}
onChange={(e) => { e.stopPropagation(); toggleSelectRow(app.id); }}
/>
</td>
<td style={{ padding: '1rem 1rem 1rem 0' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<img src={app.img} alt={app.name} style={{ width: '40px', height: '40px', borderRadius: '50%', objectFit: 'cover' }} />
<div><p style={{ fontWeight: 'bold', color: 'white' }}>{app.name}</p><p style={{ fontSize: '0.8rem', color: '#9ca3af' }}>{app.email}</p></div>
</div>
</td>
<td style={{ padding: '1rem', color: '#d1d5db' }}>{app.experience > 0 ? `${app.experience} years` : 'Fresher'}</td>
<td style={{ padding: '1rem', color: '#d1d5db' }}>{app.jobTitle}</td>
<td style={{ padding: '1rem' }}><span style={{ fontWeight: 'bold', color: (app.score || 0) > 80 ? '#34d399' : '#fbbf24' }}>{app.score || 0}</span></td>
<td style={{ padding: '1rem' }}><span style={{ fontSize: '0.75rem', padding: '4px 8px', borderRadius: '4px', backgroundColor: app.status === 'Accepted' ? 'rgba(52, 211, 153, 0.2)' : app.status === 'Rejected' ? 'rgba(239, 68, 68, 0.2)' : 'rgba(251, 191, 36, 0.2)', color: app.status === 'Accepted' ? '#34d399' : app.status === 'Rejected' ? '#ef4444' : '#fbbf24' }}>{app.status}</span></td>
<td style={{ padding: '1rem', textAlign: 'right', borderTopRightRadius: '8px', borderBottomRightRadius: '8px' }}>
<button
onClick={(e) => { e.stopPropagation(); handleViewCandidate(app); }}
style={{ background: 'none', border: 'none', color: '#6b7280', cursor: 'pointer' }}
title="View Details"
>
<ViewIcon />
</button>
</td>
</motion.tr>
);
})}
</AnimatePresence>
</tbody>
</table>
</div>
{totalPages > 1 && (
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '1.5rem', paddingTop: '1rem', borderTop: '1px solid rgba(255,255,255,0.1)' }}>
<span style={{ fontSize: '0.85rem', color: '#94a3b8' }}>Showing {((currentPage - 1) * itemsPerPage) + 1}-{Math.min(currentPage * itemsPerPage, filteredApplicants.length)} of {filteredApplicants.length}</span>
<div style={{ display: 'flex', gap: '0.5rem' }}>
<button disabled={currentPage === 1} onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))} style={{ padding: '0.5rem', borderRadius: '6px', background: 'rgba(255,255,255,0.1)', border: 'none', color: currentPage === 1 ? '#525252' : 'white', cursor: currentPage === 1 ? 'not-allowed' : 'pointer' }}><ChevronLeftIcon /></button>
<span style={{ padding: '0.5rem 1rem', background: '#EF4444', borderRadius: '6px', fontSize: '0.85rem', color: 'white', fontWeight: 'bold' }}>{currentPage}</span>
<button disabled={currentPage === totalPages} onClick={() => setCurrentPage(prev => Math.min(prev + 1, totalPages))} style={{ padding: '0.5rem', borderRadius: '6px', background: 'rgba(255,255,255,0.1)', border: 'none', color: currentPage === totalPages ? '#525252' : 'white', cursor: currentPage === totalPages ? 'not-allowed' : 'pointer' }}><ChevronRightIcon /></button>
</div>
</div>
)}
</>
)}
</div>
{/* Render the Drawer specifically for the sorting page */}
<AnimatePresence>
{isDrawerOpen && (
<CandidateDrawer
isOpen={isDrawerOpen}
onClose={() => setIsDrawerOpen(false)}
candidate={drawerCandidate}
/>
)}
</AnimatePresence>
</div>
);
}