d0n384 commited on
Commit
4e3e1fc
·
1 Parent(s): 411b8bd

Inserted ProfileOverlay

Browse files
src/components/Admin/AdminInterviewManagement.jsx CHANGED
@@ -1,12 +1,12 @@
1
  import React, { useState, useEffect } from 'react';
2
  import { motion, AnimatePresence } from 'framer-motion';
3
- import { supabase } from '../../supabaseClient';
4
  import CandidateDrawer from '../CandidateDrawer';
5
  import ScheduleInterviewModal from '../ScheduleInterviewModal'; // <--- IMPORT NEW MODAL
6
 
7
  // --- ICONS ---
8
- const SmallCalendarIcon = () => ( <svg style={{ width: '24px', height: '24px', color: 'rgba(255,255,255,0.7)' }} viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clipRule="evenodd" /></svg> );
9
- const ChevronRightIcon = () => ( <svg style={{ width: '16px', height: '16px', marginLeft: '4px' }} viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" /></svg> );
10
 
11
  // --- Simple Message Modal (Kept Inline) ---
12
  const MessageModal = ({ isOpen, onClose, onSend }) => {
@@ -37,7 +37,7 @@ export default function AdminInterviewManagement() {
37
  const [isScheduleModalOpen, setIsScheduleModalOpen] = useState(false); // <--- Updated Name
38
  const [isMessageModalOpen, setIsMessageModalOpen] = useState(false);
39
  const [isDrawerOpen, setIsDrawerOpen] = useState(false);
40
-
41
  const [selectedApplicant, setSelectedApplicant] = useState(null);
42
  const [drawerCandidate, setDrawerCandidate] = useState(null);
43
 
@@ -47,13 +47,15 @@ export default function AdminInterviewManagement() {
47
  const fetchData = async () => {
48
  try {
49
  setLoading(true);
 
 
50
  const { data, error } = await supabase
51
  .from('applications')
52
  .select(`
53
- id, created_at, status, experience, skills, match_score,
54
- profiles ( id, full_name, email, avatar_url ),
55
  jobs ( title ),
56
- interviews ( id, date, time, status )
57
  `)
58
  .order('created_at', { ascending: false });
59
 
@@ -62,27 +64,75 @@ export default function AdminInterviewManagement() {
62
  const categorized = { interviews: [], accepted: [], rejected: [] };
63
 
64
  data.forEach(app => {
65
- const interviewData = app.interviews && app.interviews.length > 0 ? app.interviews[0] : null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  const formattedApp = {
67
  ...app,
 
 
 
 
 
 
 
 
 
 
 
 
68
  name: app.profiles?.full_name || 'Unknown User',
69
- email: app.profiles?.email,
70
  role: app.jobs?.title || 'Unknown Role',
71
  avatar: app.profiles?.avatar_url,
72
- experience: (app.experience === '0' || app.experience === 0) ? 'Fresher' : (app.experience ? `${app.experience} years` : 'N/A'),
73
- skills: Array.isArray(app.skills) ? app.skills : (app.skills ? [app.skills] : []),
 
 
 
 
 
74
  interviewId: interviewData?.id,
75
  date: interviewData ? interviewData.date : 'Not Scheduled',
76
  time: interviewData ? interviewData.time : '',
77
  };
78
 
79
- if (interviewData) categorized.interviews.push(formattedApp);
80
- else if (app.status === 'Accepted' || app.status === 'Approved') categorized.accepted.push(formattedApp);
81
- else if (app.status === 'Rejected') categorized.rejected.push(formattedApp);
 
 
 
 
 
 
 
 
82
  });
 
83
  setApplicants(categorized);
84
- } catch (error) { console.error('Error fetching data:', error); }
85
- finally { setLoading(false); }
 
 
 
86
  };
87
 
88
  // 2. Updated Schedule Handler (Receives Object from Modal)
@@ -94,10 +144,10 @@ export default function AdminInterviewManagement() {
94
 
95
  try {
96
  const scheduledTimeISO = new Date(`${date}T${time}:00`).toISOString();
97
-
98
  const interviewPayload = {
99
  application_id: selectedApplicant.id,
100
- scheduled_time: scheduledTimeISO,
101
  date, time,
102
  status: 'Scheduled',
103
  interview_type: interviewType,
@@ -110,8 +160,21 @@ export default function AdminInterviewManagement() {
110
  duration_mins: 45 // Default
111
  };
112
 
113
- // Database Insert
114
- const { error: dbError } = await supabase.from('interviews').insert([interviewPayload]);
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  if (dbError) throw dbError;
116
 
117
  // Email Notification
@@ -148,7 +211,7 @@ export default function AdminInterviewManagement() {
148
  return (
149
  <div>
150
  <header style={{ marginBottom: '2rem' }}><h1 style={{ fontSize: '1.875rem', fontWeight: 'bold' }}>Interview Management</h1></header>
151
-
152
  {/* Tabs */}
153
  <div style={{ display: 'flex', justifyContent: 'center', width: '100%', marginBottom: '2rem' }}>
154
  <nav style={{ position: 'relative', display: 'inline-flex', gap: '1rem', backgroundColor: 'rgba(255, 255, 255, 0.1)', borderRadius: '1rem', padding: '0.5rem' }}>
@@ -162,20 +225,17 @@ export default function AdminInterviewManagement() {
162
 
163
  <AnimatePresence mode="wait">
164
  <motion.div key={activeSubTab} initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -10 }}>
165
- {loading ? ( <div style={{textAlign: 'center', color: '#888', padding: '2rem'}}>Loading...</div> ) : applicants[activeSubTab].length === 0 ? ( <div style={{textAlign: 'center', color: '#666', padding: '2rem'}}>No candidates found.</div> ) : (
166
  <div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: '1.5rem' }}>
167
  {applicants[activeSubTab].map((applicant, index) => (
168
  <div key={applicant.id || index} style={{ backgroundColor: 'rgba(239, 68, 68, 0.05)', border: '1px solid rgba(239, 68, 68, 0.2)', borderRadius: '1rem', padding: '1.5rem', display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: '1rem' }}>
169
-
170
  {/* INFO SECTION */}
171
- <div style={{display: 'flex', alignItems: 'flex-start', gap: '1rem'}}>
172
  <img src={applicant.avatar || `https://ui-avatars.com/api/?name=${encodeURIComponent(applicant.name)}&background=random`} alt={applicant.name} style={{ width: '48px', height: '48px', borderRadius: '50%', objectFit: 'cover' }} />
173
  <div>
174
  <h3 style={{ fontSize: '1.25rem', fontWeight: 'bold' }}>{applicant.name}</h3>
175
  <p style={{ color: '#d1d5db', fontSize: '0.9rem' }}>{applicant.role} • {applicant.experience}</p>
176
- <div style={{ display: 'flex', gap: '6px', marginTop: '8px', flexWrap: 'wrap' }}>
177
- {applicant.skills.map((skill, i) => ( <span key={i} style={{ fontSize: '0.75rem', backgroundColor: 'rgba(255,255,255,0.1)', color: '#e5e7eb', padding: '2px 8px', borderRadius: '4px' }}>{skill}</span> ))}
178
- </div>
179
  {activeSubTab === 'interviews' && applicant.time && (
180
  <div style={{ marginTop: '12px', fontSize: '0.85rem', color: '#9ca3af', display: 'flex', alignItems: 'center', gap: '6px' }}>
181
  <SmallCalendarIcon /> <span>Interview: <span style={{ color: 'white' }}>{applicant.date} at {applicant.time}</span></span>
@@ -186,20 +246,20 @@ export default function AdminInterviewManagement() {
186
 
187
  {/* BUTTONS SECTION */}
188
  <div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem', minWidth: '170px' }}>
189
- {activeSubTab === 'interviews' && ( <>
190
  <motion.button onClick={() => openScheduleModal(applicant)} whileHover={{ scale: 1.02 }} style={secondaryButtonStyle}>Reschedule</motion.button>
191
  <motion.button onClick={() => openMessageModal(applicant)} whileHover={{ scale: 1.02 }} style={secondaryButtonStyle}>Message</motion.button>
192
  <motion.button onClick={() => openDrawer(applicant)} whileHover={{ scale: 1.02 }} style={primaryButtonStyle}>View CV <ChevronRightIcon /></motion.button>
193
- </> )}
194
- {activeSubTab === 'accepted' && ( <>
195
  <motion.button onClick={() => openScheduleModal(applicant)} whileHover={{ scale: 1.02 }} style={secondaryButtonStyle}>Schedule Interview</motion.button>
196
  <motion.button onClick={() => openMessageModal(applicant)} whileHover={{ scale: 1.02 }} style={secondaryButtonStyle}>Send Message</motion.button>
197
  <motion.button onClick={() => openDrawer(applicant)} whileHover={{ scale: 1.02 }} style={primaryButtonStyle}>View CV <ChevronRightIcon /></motion.button>
198
- </> )}
199
- {activeSubTab === 'rejected' && ( <>
200
  <motion.button onClick={() => openMessageModal(applicant)} whileHover={{ scale: 1.02 }} style={secondaryButtonStyle}>Send Message</motion.button>
201
  <motion.button onClick={() => openDrawer(applicant)} whileHover={{ scale: 1.02 }} style={primaryButtonStyle}>View CV <ChevronRightIcon /></motion.button>
202
- </> )}
203
  </div>
204
  </div>
205
  ))}
@@ -211,11 +271,11 @@ export default function AdminInterviewManagement() {
211
  {/* --- MOUNT NEW MODAL --- */}
212
  <AnimatePresence>
213
  {isScheduleModalOpen && (
214
- <ScheduleInterviewModal
215
- isOpen={isScheduleModalOpen}
216
- onClose={() => setIsScheduleModalOpen(false)}
217
- onConfirm={handleScheduleConfirm}
218
- candidateName={selectedApplicant?.name}
219
  />
220
  )}
221
  </AnimatePresence>
 
1
  import React, { useState, useEffect } from 'react';
2
  import { motion, AnimatePresence } from 'framer-motion';
3
+ import { supabase } from '../../supabaseClient';
4
  import CandidateDrawer from '../CandidateDrawer';
5
  import ScheduleInterviewModal from '../ScheduleInterviewModal'; // <--- IMPORT NEW MODAL
6
 
7
  // --- ICONS ---
8
+ const SmallCalendarIcon = () => (<svg style={{ width: '24px', height: '24px', color: 'rgba(255,255,255,0.7)' }} viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clipRule="evenodd" /></svg>);
9
+ const ChevronRightIcon = () => (<svg style={{ width: '16px', height: '16px', marginLeft: '4px' }} viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" /></svg>);
10
 
11
  // --- Simple Message Modal (Kept Inline) ---
12
  const MessageModal = ({ isOpen, onClose, onSend }) => {
 
37
  const [isScheduleModalOpen, setIsScheduleModalOpen] = useState(false); // <--- Updated Name
38
  const [isMessageModalOpen, setIsMessageModalOpen] = useState(false);
39
  const [isDrawerOpen, setIsDrawerOpen] = useState(false);
40
+
41
  const [selectedApplicant, setSelectedApplicant] = useState(null);
42
  const [drawerCandidate, setDrawerCandidate] = useState(null);
43
 
 
47
  const fetchData = async () => {
48
  try {
49
  setLoading(true);
50
+
51
+ // Join applications with profiles, jobs, and CHECK interviews table
52
  const { data, error } = await supabase
53
  .from('applications')
54
  .select(`
55
+ id, created_at, status, experience, skills, match_score, resume_url,
56
+ profiles ( id, full_name, email, avatar_url, phone, location, summary, headline, current_position, education, work_experience, projects, skills, technical_skills ),
57
  jobs ( title ),
58
+ interviews ( id, date, time, status, created_at )
59
  `)
60
  .order('created_at', { ascending: false });
61
 
 
64
  const categorized = { interviews: [], accepted: [], rejected: [] };
65
 
66
  data.forEach(app => {
67
+ // Check if an interview row exists for this application (Get the LATEST one)
68
+ const interviews = app.interviews || [];
69
+ interviews.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
70
+ const interviewData = interviews.length > 0 ? interviews[0] : null;
71
+
72
+ // Combine skills and technical_skills
73
+ const profileSkills = Array.isArray(app.profiles?.skills) ? app.profiles.skills : [];
74
+ const profileTechSkills = Array.isArray(app.profiles?.technical_skills) ? app.profiles.technical_skills : [];
75
+ // Handle comma-separated strings if they were stored as text in legacy rows
76
+ const parsedTechSkills = (typeof app.profiles?.technical_skills === 'string')
77
+ ? app.profiles.technical_skills.split(',').map(s => s.trim())
78
+ : profileTechSkills;
79
+
80
+ const combinedSkills = [...new Set([...profileSkills, ...parsedTechSkills])];
81
+
82
+ // Fallback to application skills if profiles skills are empty
83
+ const finalSkills = combinedSkills.length > 0
84
+ ? combinedSkills
85
+ : (Array.isArray(app.skills) ? app.skills : (app.skills ? [app.skills] : []));
86
+
87
+ // Format the object for UI (Handle '0' values correctly)
88
  const formattedApp = {
89
  ...app,
90
+ // Mapped fields for FullProfileOverlay
91
+ full_name: app.profiles?.full_name,
92
+ email: app.profiles?.email,
93
+ phone: app.profiles?.phone,
94
+ location: app.profiles?.location,
95
+ summary: app.profiles?.summary,
96
+ headline: app.profiles?.headline,
97
+ current_position: app.profiles?.current_position,
98
+ education: app.profiles?.education,
99
+ work_experience: app.profiles?.work_experience,
100
+ projects: app.profiles?.projects,
101
+
102
  name: app.profiles?.full_name || 'Unknown User',
 
103
  role: app.jobs?.title || 'Unknown Role',
104
  avatar: app.profiles?.avatar_url,
105
+ resumeUrl: app.resume_url, // Map resume_url to camelCase
106
+ // FIX: Handle 0 experience
107
+ experience: (app.experience === '0' || app.experience === 0)
108
+ ? 'Fresher'
109
+ : (app.experience ? `${app.experience} years` : 'N/A'),
110
+ skills: finalSkills,
111
+ // Use interview details if available
112
  interviewId: interviewData?.id,
113
  date: interviewData ? interviewData.date : 'Not Scheduled',
114
  time: interviewData ? interviewData.time : '',
115
  };
116
 
117
+ // --- SORTING LOGIC ---
118
+ if (interviewData) {
119
+ // HAS interview -> Interviews Tab
120
+ categorized.interviews.push(formattedApp);
121
+ } else if (app.status === 'Accepted' || app.status === 'Approved') {
122
+ // Approved but NO interview -> Accepted Tab
123
+ categorized.accepted.push(formattedApp);
124
+ } else if (app.status === 'Rejected') {
125
+ // Rejected -> Rejected Tab
126
+ categorized.rejected.push(formattedApp);
127
+ }
128
  });
129
+
130
  setApplicants(categorized);
131
+ } catch (error) {
132
+ console.error('Error fetching data:', error);
133
+ } finally {
134
+ setLoading(false);
135
+ }
136
  };
137
 
138
  // 2. Updated Schedule Handler (Receives Object from Modal)
 
144
 
145
  try {
146
  const scheduledTimeISO = new Date(`${date}T${time}:00`).toISOString();
147
+
148
  const interviewPayload = {
149
  application_id: selectedApplicant.id,
150
+ scheduled_time: scheduledTimeISO,
151
  date, time,
152
  status: 'Scheduled',
153
  interview_type: interviewType,
 
160
  duration_mins: 45 // Default
161
  };
162
 
163
+ // Database Operation: Update if exists, otherwise Insert
164
+ let dbError;
165
+ if (selectedApplicant.interviewId) {
166
+ const { error } = await supabase
167
+ .from('interviews')
168
+ .update(interviewPayload)
169
+ .eq('id', selectedApplicant.interviewId);
170
+ dbError = error;
171
+ } else {
172
+ const { error } = await supabase
173
+ .from('interviews')
174
+ .insert([interviewPayload]);
175
+ dbError = error;
176
+ }
177
+
178
  if (dbError) throw dbError;
179
 
180
  // Email Notification
 
211
  return (
212
  <div>
213
  <header style={{ marginBottom: '2rem' }}><h1 style={{ fontSize: '1.875rem', fontWeight: 'bold' }}>Interview Management</h1></header>
214
+
215
  {/* Tabs */}
216
  <div style={{ display: 'flex', justifyContent: 'center', width: '100%', marginBottom: '2rem' }}>
217
  <nav style={{ position: 'relative', display: 'inline-flex', gap: '1rem', backgroundColor: 'rgba(255, 255, 255, 0.1)', borderRadius: '1rem', padding: '0.5rem' }}>
 
225
 
226
  <AnimatePresence mode="wait">
227
  <motion.div key={activeSubTab} initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -10 }}>
228
+ {loading ? (<div style={{ textAlign: 'center', color: '#888', padding: '2rem' }}>Loading...</div>) : applicants[activeSubTab].length === 0 ? (<div style={{ textAlign: 'center', color: '#666', padding: '2rem' }}>No candidates found.</div>) : (
229
  <div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: '1.5rem' }}>
230
  {applicants[activeSubTab].map((applicant, index) => (
231
  <div key={applicant.id || index} style={{ backgroundColor: 'rgba(239, 68, 68, 0.05)', border: '1px solid rgba(239, 68, 68, 0.2)', borderRadius: '1rem', padding: '1.5rem', display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: '1rem' }}>
232
+
233
  {/* INFO SECTION */}
234
+ <div style={{ display: 'flex', alignItems: 'flex-start', gap: '1rem' }}>
235
  <img src={applicant.avatar || `https://ui-avatars.com/api/?name=${encodeURIComponent(applicant.name)}&background=random`} alt={applicant.name} style={{ width: '48px', height: '48px', borderRadius: '50%', objectFit: 'cover' }} />
236
  <div>
237
  <h3 style={{ fontSize: '1.25rem', fontWeight: 'bold' }}>{applicant.name}</h3>
238
  <p style={{ color: '#d1d5db', fontSize: '0.9rem' }}>{applicant.role} • {applicant.experience}</p>
 
 
 
239
  {activeSubTab === 'interviews' && applicant.time && (
240
  <div style={{ marginTop: '12px', fontSize: '0.85rem', color: '#9ca3af', display: 'flex', alignItems: 'center', gap: '6px' }}>
241
  <SmallCalendarIcon /> <span>Interview: <span style={{ color: 'white' }}>{applicant.date} at {applicant.time}</span></span>
 
246
 
247
  {/* BUTTONS SECTION */}
248
  <div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem', minWidth: '170px' }}>
249
+ {activeSubTab === 'interviews' && (<>
250
  <motion.button onClick={() => openScheduleModal(applicant)} whileHover={{ scale: 1.02 }} style={secondaryButtonStyle}>Reschedule</motion.button>
251
  <motion.button onClick={() => openMessageModal(applicant)} whileHover={{ scale: 1.02 }} style={secondaryButtonStyle}>Message</motion.button>
252
  <motion.button onClick={() => openDrawer(applicant)} whileHover={{ scale: 1.02 }} style={primaryButtonStyle}>View CV <ChevronRightIcon /></motion.button>
253
+ </>)}
254
+ {activeSubTab === 'accepted' && (<>
255
  <motion.button onClick={() => openScheduleModal(applicant)} whileHover={{ scale: 1.02 }} style={secondaryButtonStyle}>Schedule Interview</motion.button>
256
  <motion.button onClick={() => openMessageModal(applicant)} whileHover={{ scale: 1.02 }} style={secondaryButtonStyle}>Send Message</motion.button>
257
  <motion.button onClick={() => openDrawer(applicant)} whileHover={{ scale: 1.02 }} style={primaryButtonStyle}>View CV <ChevronRightIcon /></motion.button>
258
+ </>)}
259
+ {activeSubTab === 'rejected' && (<>
260
  <motion.button onClick={() => openMessageModal(applicant)} whileHover={{ scale: 1.02 }} style={secondaryButtonStyle}>Send Message</motion.button>
261
  <motion.button onClick={() => openDrawer(applicant)} whileHover={{ scale: 1.02 }} style={primaryButtonStyle}>View CV <ChevronRightIcon /></motion.button>
262
+ </>)}
263
  </div>
264
  </div>
265
  ))}
 
271
  {/* --- MOUNT NEW MODAL --- */}
272
  <AnimatePresence>
273
  {isScheduleModalOpen && (
274
+ <ScheduleInterviewModal
275
+ isOpen={isScheduleModalOpen}
276
+ onClose={() => setIsScheduleModalOpen(false)}
277
+ onConfirm={handleScheduleConfirm}
278
+ candidateName={selectedApplicant?.name}
279
  />
280
  )}
281
  </AnimatePresence>
src/components/CandidateDrawer.jsx CHANGED
@@ -1,8 +1,41 @@
1
- import React from 'react';
2
  import { motion, AnimatePresence } from 'framer-motion';
3
  import { X, ExternalLink, ThumbsUp, ThumbsDown, BrainCircuit } from 'lucide-react';
 
 
 
 
4
 
5
  const CandidateDrawer = ({ isOpen, onClose, candidate }) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  if (!candidate) return null;
7
 
8
  return (
@@ -37,7 +70,7 @@ const CandidateDrawer = ({ isOpen, onClose, candidate }) => {
37
  height: '100%',
38
  width: '100%',
39
  maxWidth: '500px',
40
-
41
  // === 🎨 CSS-ONLY BACKGROUND (No Image Needed) ===
42
  backgroundColor: '#0f172a', // Base Dark Slate Color
43
  backgroundImage: `
@@ -48,7 +81,7 @@ const CandidateDrawer = ({ isOpen, onClose, candidate }) => {
48
  // 1. Blue Glow (Top Left)
49
  // 2. Red Glow (Bottom Right)
50
  // 3. Subtle Diagonal Sheen
51
-
52
  borderLeft: '1px solid rgba(255,255,255,0.1)',
53
  zIndex: 50,
54
  overflowY: 'auto',
@@ -57,7 +90,7 @@ const CandidateDrawer = ({ isOpen, onClose, candidate }) => {
57
  >
58
  {/* Content Container */}
59
  <div style={{ padding: '2rem' }}>
60
-
61
  {/* Header */}
62
  <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start', marginBottom: '2rem' }}>
63
  <div>
@@ -76,11 +109,11 @@ const CandidateDrawer = ({ isOpen, onClose, candidate }) => {
76
  <span style={{ fontWeight: 'bold', color: 'white' }}>{candidate.matchScore || 0}%</span>
77
  </div>
78
  <div style={{ width: '100%', height: '6px', backgroundColor: 'rgba(255,255,255,0.1)', borderRadius: '3px' }}>
79
- <motion.div
80
- initial={{ width: 0 }}
81
- animate={{ width: `${candidate.matchScore || 0}%` }}
82
  transition={{ delay: 0.2, duration: 1 }}
83
- style={{ height: '100%', backgroundColor: '#EF4444', borderRadius: '3px', boxShadow: '0 0 10px rgba(239,68,68,0.5)' }}
84
  />
85
  </div>
86
  </div>
@@ -90,20 +123,20 @@ const CandidateDrawer = ({ isOpen, onClose, candidate }) => {
90
  <h3 style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '1.1rem', fontWeight: '600', color: 'white', marginBottom: '1rem' }}>
91
  <BrainCircuit size={18} color="#EF4444" /> AI Insights
92
  </h3>
93
-
94
  <div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
95
- {candidate.insights?.strengths?.map((str, i) => (
96
- <div key={`str-${i}`} style={{ display: 'flex', gap: '10px', fontSize: '0.9rem', color: '#cbd5e1' }}>
97
- <ThumbsUp size={16} color="#34d399" style={{ marginTop: '3px', flexShrink: 0 }} />
98
- <span>{str}</span>
99
- </div>
100
- ))}
101
- {candidate.insights?.weaknesses?.map((wk, i) => (
102
- <div key={`wk-${i}`} style={{ display: 'flex', gap: '10px', fontSize: '0.9rem', color: '#cbd5e1' }}>
103
- <ThumbsDown size={16} color="#fb7185" style={{ marginTop: '3px', flexShrink: 0 }} />
104
- <span>{wk}</span>
105
- </div>
106
- ))}
107
  </div>
108
  </div>
109
 
@@ -120,29 +153,44 @@ const CandidateDrawer = ({ isOpen, onClose, candidate }) => {
120
  <h3 style={{ fontSize: '1.1rem', fontWeight: '600', color: 'white', marginBottom: '0.75rem' }}>Portfolio & Projects</h3>
121
  <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.75rem' }}>
122
  {candidate.projects?.map((project, i) => (
123
- <a key={i} href="#" style={{ textDecoration: 'none', display: 'flex', alignItems: 'center', gap: '6px', fontSize: '0.85rem', color: 'white', backgroundColor: 'rgba(255,255,255,0.1)', padding: '0.4rem 0.8rem', borderRadius: '6px', border: '1px solid rgba(255,255,255,0.1)', transition: 'background-color 0.2s' }}>
124
- {project} <ExternalLink size={12} />
125
- </a>
126
  ))}
127
  </div>
128
  </div>
129
 
130
- {/* Footer Actions */}
131
- <div style={{ marginTop: '3rem', paddingTop: '1.5rem', borderTop: '1px solid rgba(255,255,255,0.1)', display: 'flex', gap: '1rem' }}>
132
- <button style={{ flex: 1, padding: '0.75rem', borderRadius: '0.5rem', backgroundColor: '#EF4444', color: 'white', border: 'none', fontWeight: '600', cursor: 'pointer', boxShadow: '0 4px 12px rgba(239, 68, 68, 0.3)' }}>
133
- Full Profile
134
- </button>
135
- <button style={{ flex: 1, padding: '0.75rem', borderRadius: '0.5rem', backgroundColor: 'transparent', color: 'white', border: '1px solid rgba(255,255,255,0.2)', fontWeight: '600', cursor: 'pointer' }}>
136
- Download CV
137
- </button>
138
- </div>
 
 
 
 
 
 
139
 
140
  </div>
141
  </motion.div>
 
 
 
 
 
 
 
 
 
142
  </>
143
  )}
144
  </AnimatePresence>
145
  );
146
  };
147
 
148
- export default CandidateDrawer;
 
1
+ import React, { useState } from 'react';
2
  import { motion, AnimatePresence } from 'framer-motion';
3
  import { X, ExternalLink, ThumbsUp, ThumbsDown, BrainCircuit } from 'lucide-react';
4
+ import FullProfileOverlay from './FullProfileOverlay';
5
+
6
+ import { supabase } from '../supabaseClient';
7
+
8
 
9
  const CandidateDrawer = ({ isOpen, onClose, candidate }) => {
10
+ const [showFullProfile, setShowFullProfile] = useState(false);
11
+
12
+ const handleDownloadCV = async () => {
13
+ if (!candidate.resumeUrl) {
14
+ alert("No resume available.");
15
+ return;
16
+ }
17
+
18
+ try {
19
+ // Check if it's a full URL or a storage path
20
+ if (candidate.resumeUrl.startsWith('http')) {
21
+ saveAs(candidate.resumeUrl, `${candidate.name}_Resume.pdf`);
22
+ } else {
23
+ // Assume it's a Supabase Storage path
24
+ const { data, error } = await supabase
25
+ .storage
26
+ .from('resume') // Ensure this bucket name matches your setup
27
+ .download(candidate.resumeUrl);
28
+
29
+ if (error) throw error;
30
+
31
+ saveAs(data, `${candidate.name}_Resume.pdf`);
32
+ }
33
+ } catch (error) {
34
+ console.error("Download failed:", error);
35
+ alert("Failed to download resume.");
36
+ }
37
+ };
38
+
39
  if (!candidate) return null;
40
 
41
  return (
 
70
  height: '100%',
71
  width: '100%',
72
  maxWidth: '500px',
73
+
74
  // === 🎨 CSS-ONLY BACKGROUND (No Image Needed) ===
75
  backgroundColor: '#0f172a', // Base Dark Slate Color
76
  backgroundImage: `
 
81
  // 1. Blue Glow (Top Left)
82
  // 2. Red Glow (Bottom Right)
83
  // 3. Subtle Diagonal Sheen
84
+
85
  borderLeft: '1px solid rgba(255,255,255,0.1)',
86
  zIndex: 50,
87
  overflowY: 'auto',
 
90
  >
91
  {/* Content Container */}
92
  <div style={{ padding: '2rem' }}>
93
+
94
  {/* Header */}
95
  <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start', marginBottom: '2rem' }}>
96
  <div>
 
109
  <span style={{ fontWeight: 'bold', color: 'white' }}>{candidate.matchScore || 0}%</span>
110
  </div>
111
  <div style={{ width: '100%', height: '6px', backgroundColor: 'rgba(255,255,255,0.1)', borderRadius: '3px' }}>
112
+ <motion.div
113
+ initial={{ width: 0 }}
114
+ animate={{ width: `${candidate.matchScore || 0}%` }}
115
  transition={{ delay: 0.2, duration: 1 }}
116
+ style={{ height: '100%', backgroundColor: '#EF4444', borderRadius: '3px', boxShadow: '0 0 10px rgba(239,68,68,0.5)' }}
117
  />
118
  </div>
119
  </div>
 
123
  <h3 style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '1.1rem', fontWeight: '600', color: 'white', marginBottom: '1rem' }}>
124
  <BrainCircuit size={18} color="#EF4444" /> AI Insights
125
  </h3>
126
+
127
  <div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
128
+ {candidate.insights?.strengths?.map((str, i) => (
129
+ <div key={`str-${i}`} style={{ display: 'flex', gap: '10px', fontSize: '0.9rem', color: '#cbd5e1' }}>
130
+ <ThumbsUp size={16} color="#34d399" style={{ marginTop: '3px', flexShrink: 0 }} />
131
+ <span>{str}</span>
132
+ </div>
133
+ ))}
134
+ {candidate.insights?.weaknesses?.map((wk, i) => (
135
+ <div key={`wk-${i}`} style={{ display: 'flex', gap: '10px', fontSize: '0.9rem', color: '#cbd5e1' }}>
136
+ <ThumbsDown size={16} color="#fb7185" style={{ marginTop: '3px', flexShrink: 0 }} />
137
+ <span>{wk}</span>
138
+ </div>
139
+ ))}
140
  </div>
141
  </div>
142
 
 
153
  <h3 style={{ fontSize: '1.1rem', fontWeight: '600', color: 'white', marginBottom: '0.75rem' }}>Portfolio & Projects</h3>
154
  <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.75rem' }}>
155
  {candidate.projects?.map((project, i) => (
156
+ <div key={i} style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '0.85rem', color: 'white', backgroundColor: 'rgba(255,255,255,0.1)', padding: '0.4rem 0.8rem', borderRadius: '6px', border: '1px solid rgba(255,255,255,0.1)', cursor: 'default' }}>
157
+ {typeof project === 'object' ? (project.title || project.name) : project} <ExternalLink size={12} />
158
+ </div>
159
  ))}
160
  </div>
161
  </div>
162
 
163
+ {/* Footer Actions */}
164
+ <div style={{ marginTop: '3rem', paddingTop: '1.5rem', borderTop: '1px solid rgba(255,255,255,0.1)', display: 'flex', gap: '1rem' }}>
165
+ <button
166
+ onClick={() => setShowFullProfile(true)}
167
+ style={{ flex: 1, padding: '0.75rem', borderRadius: '0.5rem', backgroundColor: '#EF4444', color: 'white', border: 'none', fontWeight: '600', cursor: 'pointer', boxShadow: '0 4px 12px rgba(239, 68, 68, 0.3)' }}
168
+ >
169
+ Full Profile
170
+ </button>
171
+ <button
172
+ onClick={handleDownloadCV}
173
+ style={{ flex: 1, padding: '0.75rem', borderRadius: '0.5rem', backgroundColor: 'transparent', color: 'white', border: '1px solid rgba(255,255,255,0.2)', fontWeight: '600', cursor: 'pointer' }}
174
+ >
175
+ Download CV
176
+ </button>
177
+ </div>
178
 
179
  </div>
180
  </motion.div>
181
+
182
+ <AnimatePresence>
183
+ {showFullProfile && (
184
+ <FullProfileOverlay
185
+ candidate={candidate}
186
+ onClose={() => setShowFullProfile(false)}
187
+ />
188
+ )}
189
+ </AnimatePresence>
190
  </>
191
  )}
192
  </AnimatePresence>
193
  );
194
  };
195
 
196
+ export default CandidateDrawer
src/components/FullProfileOverlay.jsx ADDED
@@ -0,0 +1,297 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { X, Mail, Phone, MapPin, Briefcase, GraduationCap, Award, User } from 'lucide-react';
4
+
5
+ const FullProfileOverlay = ({ candidate, onClose }) => {
6
+ if (!candidate) return null;
7
+
8
+ // Helper to safely parse skills (array or CSV string)
9
+ const parseSkills = (data) => {
10
+ if (Array.isArray(data)) return data;
11
+ if (typeof data === 'string') return data.split(',').map(s => s.trim()).filter(Boolean);
12
+ return [];
13
+ };
14
+
15
+ // Robustly gather skills from multiple possible sources
16
+ const rawSkills = parseSkills(candidate.skills);
17
+ const rawTechSkills = parseSkills(candidate.technical_skills);
18
+ const profileSkills = parseSkills(candidate.profiles?.skills);
19
+ const profileTechSkills = parseSkills(candidate.profiles?.technical_skills);
20
+
21
+ // Merge unique skills
22
+ const allSkills = [...new Set([
23
+ ...rawSkills,
24
+ ...rawTechSkills,
25
+ ...profileSkills,
26
+ ...profileTechSkills
27
+ ])];
28
+
29
+ // Mapping Real PostgreSQL Data (profiles table) to UI variables
30
+ const fullCandidate = {
31
+ ...candidate, // Keep any existing IDs or metadata
32
+
33
+ // Basic Info from Database
34
+ name: candidate.full_name || candidate.name || 'No Name Provided',
35
+ role: candidate.current_position || candidate.role || 'Applicant',
36
+ email: candidate.email || 'No email available',
37
+ phone: candidate.phone || 'No phone provided',
38
+ location: candidate.location || 'Remote',
39
+ avatar: candidate.avatar_url || candidate.avatar || `https://ui-avatars.com/api/?name=${encodeURIComponent(candidate.full_name || 'User')}`,
40
+
41
+ // Long-form Text
42
+ about: candidate.summary || candidate.headline || "No summary available for this profile.",
43
+
44
+ // Skills (Consolidated)
45
+ skills: allSkills,
46
+
47
+ // JSONB Mapping (ensures .map() won't crash if data is empty)
48
+ education: Array.isArray(candidate.education)
49
+ ? candidate.education
50
+ : [],
51
+
52
+ experience_details: Array.isArray(candidate.work_experience)
53
+ ? candidate.work_experience
54
+ : [],
55
+
56
+ projects: Array.isArray(candidate.projects)
57
+ ? candidate.projects
58
+ : []
59
+ };
60
+
61
+ return (
62
+ <motion.div
63
+ initial={{ opacity: 0 }}
64
+ animate={{ opacity: 1 }}
65
+ exit={{ opacity: 0 }}
66
+ style={{
67
+ position: 'fixed',
68
+ inset: 0,
69
+ zIndex: 60, // Higher than Drawer (50)
70
+ display: 'flex',
71
+ alignItems: 'center',
72
+ justifyContent: 'center',
73
+ backgroundColor: 'rgba(0, 0, 0, 0.7)',
74
+ backdropFilter: 'blur(8px)',
75
+ padding: '2rem'
76
+ }}
77
+ >
78
+ <motion.div
79
+ initial={{ scale: 0.9, opacity: 0, y: 20 }}
80
+ animate={{ scale: 1, opacity: 1, y: 0 }}
81
+ exit={{ scale: 0.9, opacity: 0, y: 20 }}
82
+ transition={{ type: 'spring', damping: 25, stiffness: 300 }}
83
+ style={{
84
+ width: '100%',
85
+ maxWidth: '900px',
86
+ maxHeight: '90vh',
87
+ overflowY: 'auto',
88
+ backgroundColor: '#0f172a',
89
+ backgroundImage: `
90
+ radial-gradient(at 0% 0%, rgba(56, 189, 248, 0.15) 0px, transparent 50%),
91
+ radial-gradient(at 100% 100%, rgba(239, 68, 68, 0.15) 0px, transparent 50%),
92
+ linear-gradient(135deg, rgba(255,255,255,0.03) 0%, transparent 100%)
93
+ `,
94
+ borderRadius: '1.5rem',
95
+ border: '1px solid rgba(255,255,255,0.1)',
96
+ boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.5)',
97
+ color: 'white',
98
+ display: 'flex',
99
+ flexDirection: 'column'
100
+ }}
101
+ >
102
+ {/* === Header === */}
103
+ <div style={{
104
+ padding: '2rem',
105
+ borderBottom: '1px solid rgba(255,255,255,0.1)',
106
+ display: 'flex',
107
+ justifyContent: 'space-between',
108
+ alignItems: 'start',
109
+ position: 'sticky',
110
+ top: 0,
111
+ backgroundColor: 'rgba(15, 23, 42, 0.95)',
112
+ backdropFilter: 'blur(4px)',
113
+ zIndex: 10
114
+ }}>
115
+ <div style={{ display: 'flex', gap: '1.5rem', alignItems: 'center' }}>
116
+ <div style={{
117
+ width: '80px',
118
+ height: '80px',
119
+ borderRadius: '50%',
120
+ overflow: 'hidden',
121
+ border: '3px solid rgba(255,255,255,0.1)',
122
+ boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)'
123
+ }}>
124
+ <img
125
+ src={fullCandidate.avatar || `https://ui-avatars.com/api/?name=${encodeURIComponent(fullCandidate.name)}&background=random`}
126
+ alt={fullCandidate.name}
127
+ style={{ width: '100%', height: '100%', objectFit: 'cover' }}
128
+ />
129
+ </div>
130
+ <div>
131
+ <h2 style={{ fontSize: '2rem', fontWeight: 'bold', margin: 0, lineHeight: 1.2 }}>{fullCandidate.name}</h2>
132
+ <p style={{ color: '#94a3b8', fontSize: '1.1rem', margin: '0.25rem 0 0 0' }}>{fullCandidate.role}</p>
133
+ <div style={{ display: 'flex', gap: '1rem', marginTop: '0.75rem', fontSize: '0.9rem', color: '#cbd5e1' }}>
134
+ <span style={{ display: 'flex', alignItems: 'center', gap: '6px' }}><Mail size={14} /> {fullCandidate.email}</span>
135
+ <span style={{ display: 'flex', alignItems: 'center', gap: '6px' }}><Phone size={14} /> {fullCandidate.phone}</span>
136
+ <span style={{ display: 'flex', alignItems: 'center', gap: '6px' }}><MapPin size={14} /> {fullCandidate.location}</span>
137
+ </div>
138
+ </div>
139
+ </div>
140
+
141
+ <button
142
+ onClick={onClose}
143
+ style={{
144
+ padding: '0.75rem',
145
+ borderRadius: '0.75rem',
146
+ backgroundColor: 'rgba(255,255,255,0.05)',
147
+ border: '1px solid rgba(255,255,255,0.1)',
148
+ color: '#94a3b8',
149
+ cursor: 'pointer',
150
+ transition: 'all 0.2s',
151
+ display: 'flex', alignItems: 'center', justifyContent: 'center'
152
+ }}
153
+ onMouseOver={(e) => { e.currentTarget.style.backgroundColor = 'rgba(239, 68, 68, 0.2)'; e.currentTarget.style.color = 'white'; }}
154
+ onMouseOut={(e) => { e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.05)'; e.currentTarget.style.color = '#94a3b8'; }}
155
+ >
156
+ <X size={24} />
157
+ </button>
158
+ </div>
159
+
160
+ {/* === Content Body === */}
161
+ <div style={{ padding: '2rem', display: 'grid', gridTemplateColumns: 'minmax(300px, 1fr) 2fr', gap: '3rem' }}>
162
+
163
+ {/* Left Column */}
164
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
165
+ {/* About */}
166
+ <section>
167
+ <h3 style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '1.1rem', fontWeight: '600', color: 'white', marginBottom: '1rem' }}>
168
+ <User size={18} color="#38bdf8" /> About
169
+ </h3>
170
+ <p style={{ color: '#94a3b8', lineHeight: '1.7', fontSize: '0.95rem' }}>
171
+ {fullCandidate.about}
172
+ </p>
173
+ </section>
174
+
175
+ {/* Skills */}
176
+ <section>
177
+ <h3 style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '1.1rem', fontWeight: '600', color: 'white', marginBottom: '1rem' }}>
178
+ <Award size={18} color="#c084fc" /> Skills
179
+ </h3>
180
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem' }}>
181
+ {fullCandidate.skills?.map((skill, i) => (
182
+ <span key={i} style={{
183
+ fontSize: '0.85rem',
184
+ backgroundColor: 'rgba(56, 189, 248, 0.1)',
185
+ color: '#38bdf8',
186
+ padding: '0.4rem 0.8rem',
187
+ borderRadius: '6px',
188
+ border: '1px solid rgba(56, 189, 248, 0.2)'
189
+ }}>
190
+ {skill}
191
+ </span>
192
+ ))}
193
+ </div>
194
+ </section>
195
+
196
+ {/* Projects Pointers */}
197
+ <section>
198
+ <h3 style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '1.1rem', fontWeight: '600', color: 'white', marginBottom: '1rem' }}>
199
+ <Briefcase size={18} color="#34d399" /> Key Projects
200
+ </h3>
201
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
202
+ {fullCandidate.projects?.map((project, i) => (
203
+ <div key={i} style={{ padding: '0.75rem', backgroundColor: 'rgba(255,255,255,0.03)', borderRadius: '0.5rem', border: '1px solid rgba(255,255,255,0.05)' }}>
204
+ <div style={{ color: '#e2e8f0', fontSize: '0.95rem', fontWeight: '600' }}>
205
+ {typeof project === 'object' ? (project.title || project.name) : project}
206
+ </div>
207
+ {typeof project === 'object' && project.description && (
208
+ <div style={{ color: '#94a3b8', fontSize: '0.85rem', marginTop: '0.25rem' }}>
209
+ {project.description}
210
+ </div>
211
+ )}
212
+ </div>
213
+ ))}
214
+ </div>
215
+ </section>
216
+ </div>
217
+
218
+ {/* Right Column */}
219
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
220
+
221
+ {/* Experience Timeline */}
222
+ <section>
223
+ <h3 style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '1.1rem', fontWeight: '600', color: 'white', marginBottom: '1.5rem' }}>
224
+ <Briefcase size={18} color="#fcd34d" /> Experience
225
+ </h3>
226
+
227
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem', position: 'relative', paddingLeft: '1rem' }}>
228
+ {/* Vertical Line */}
229
+ <div style={{ position: 'absolute', left: 0, top: '10px', bottom: '10px', width: '2px', backgroundColor: 'rgba(255,255,255,0.1)' }} />
230
+
231
+ {fullCandidate.experience_details.map((exp, i) => (
232
+ <div key={i} style={{ position: 'relative', paddingLeft: '1.5rem' }}>
233
+ {/* Dot */}
234
+ <div style={{ position: 'absolute', left: '-5px', top: '6px', width: '12px', height: '12px', borderRadius: '50%', backgroundColor: '#fcd34d', boxShadow: '0 0 10px rgba(252, 211, 77, 0.5)' }} />
235
+
236
+ <h4 style={{ color: 'white', fontWeight: '600', fontSize: '1.05rem', marginBottom: '0.25rem' }}>{exp.role || exp.job_title}</h4>
237
+ <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.9rem', color: '#94a3b8', marginBottom: '0.5rem' }}>
238
+ <span>{exp.company}</span>
239
+ <span>{exp.duration || exp.years}</span>
240
+ </div>
241
+ <p style={{ color: '#cbd5e1', fontSize: '0.95rem', lineHeight: '1.6' }}>{exp.description}</p>
242
+ </div>
243
+ ))}
244
+ </div>
245
+ </section>
246
+
247
+ {/* Education */}
248
+ <section>
249
+ <h3 style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '1.1rem', fontWeight: '600', color: 'white', marginBottom: '1rem' }}>
250
+ <GraduationCap size={18} color="#f472b6" /> Education
251
+ </h3>
252
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
253
+ {fullCandidate.education.map((edu, i) => (
254
+ <div key={i} style={{ padding: '1rem', backgroundColor: 'rgba(255,255,255,0.03)', borderRadius: '0.75rem', border: '1px solid rgba(255,255,255,0.05)' }}>
255
+ <h4 style={{ color: 'white', fontWeight: '600', fontSize: '1rem', marginBottom: '0.25rem' }}>{edu.degree || edu.course}</h4>
256
+ <p style={{ color: '#94a3b8', fontSize: '0.9rem' }}>{edu.school || edu.institution}</p>
257
+ <p style={{ color: '#64748b', fontSize: '0.85rem', marginTop: '0.5rem' }}>{edu.year}</p>
258
+ </div>
259
+ ))}
260
+ </div>
261
+ </section>
262
+
263
+ </div>
264
+ </div>
265
+
266
+ {/* === Footer === */}
267
+ <div style={{
268
+ padding: '1.5rem 2rem',
269
+ borderTop: '1px solid rgba(255,255,255,0.1)',
270
+ backgroundColor: 'rgba(15, 23, 42, 0.5)',
271
+ display: 'flex',
272
+ justifyContent: 'flex-end',
273
+ gap: '1rem'
274
+ }}>
275
+ <button
276
+ onClick={onClose}
277
+ style={{
278
+ padding: '0.75rem 1.5rem',
279
+ borderRadius: '0.5rem',
280
+ border: '1px solid rgba(255,255,255,0.2)',
281
+ backgroundColor: 'transparent',
282
+ color: 'white',
283
+ fontWeight: '600',
284
+ cursor: 'pointer'
285
+ }}
286
+ >
287
+ Close
288
+ </button>
289
+
290
+ </div>
291
+
292
+ </motion.div>
293
+ </motion.div>
294
+ );
295
+ };
296
+
297
+ export default FullProfileOverlay;