Muhammed Sameer commited on
Commit
59f9574
·
1 Parent(s): cf06972

new feature implemented

Browse files
src/App.jsx CHANGED
@@ -13,6 +13,7 @@ import ApplicantProfile from './pages/ApplicantProfile'; // Make sure this fi
13
  import ApplicantATS from './pages/ApplicantATS'; // Make sure this file exists
14
  import ApplicantInterviews from './pages/ApplicantInterviews'; // Make sure this file exists
15
  import ApplicantMessages from './pages/ApplicantMessages'; // Make sure this file exists
 
16
 
17
  import { supabase } from './supabaseClient'; // Import at top
18
 
@@ -67,7 +68,7 @@ export default function App() {
67
  case 'login': return <LoginPage onNavigate={handleNavigate} />;
68
  case 'admin': return <AdminLogin onNavigate={handleNavigate} />;
69
  case 'applicant': return <AppliLogin onNavigate={handleNavigate} />;
70
-
71
  // --- APPLICANT ROUTES ---
72
  case 'applicant-jobs':
73
  return <ApplicantJobPage onNavigate={handleNavigate} />;
 
13
  import ApplicantATS from './pages/ApplicantATS'; // Make sure this file exists
14
  import ApplicantInterviews from './pages/ApplicantInterviews'; // Make sure this file exists
15
  import ApplicantMessages from './pages/ApplicantMessages'; // Make sure this file exists
16
+ import ResetPassword from './pages/ResetPassword';
17
 
18
  import { supabase } from './supabaseClient'; // Import at top
19
 
 
68
  case 'login': return <LoginPage onNavigate={handleNavigate} />;
69
  case 'admin': return <AdminLogin onNavigate={handleNavigate} />;
70
  case 'applicant': return <AppliLogin onNavigate={handleNavigate} />;
71
+ case 'reset-password': return <ResetPassword onNavigate={handleNavigate} />;
72
  // --- APPLICANT ROUTES ---
73
  case 'applicant-jobs':
74
  return <ApplicantJobPage onNavigate={handleNavigate} />;
src/components/Admin/AdminInterviewManagement.jsx CHANGED
@@ -186,9 +186,7 @@ export default function AdminInterviewManagement() {
186
  const [selectedApplicant, setSelectedApplicant] = useState(null);
187
  const [drawerCandidate, setDrawerCandidate] = useState(null);
188
 
189
- // 1. Fetch Data
190
- useEffect(() => { fetchData(); }, []);
191
-
192
  const fetchData = async () => {
193
  try {
194
  setLoading(true);
@@ -198,7 +196,7 @@ export default function AdminInterviewManagement() {
198
  .from('applications')
199
  .select(`
200
  id, user_id, job_id, created_at, status, experience, skills, match_score, resume_url,
201
- profiles ( id, full_name, email, avatar_url, phone, location, summary, headline, current_position, education, work_experience, projects, skills, technical_skills, resume_url ),
202
  jobs ( id, title ),
203
  interviews ( id, date, time, status, created_at )
204
  `)
@@ -272,7 +270,31 @@ export default function AdminInterviewManagement() {
272
  }
273
  };
274
 
 
 
 
 
 
275
  // 2. Updated Schedule Handler
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
  const handleScheduleConfirm = async (scheduleData) => {
277
  if (!selectedApplicant) return;
278
 
@@ -311,6 +333,15 @@ export default function AdminInterviewManagement() {
311
 
312
  if (dbError) throw dbError;
313
 
 
 
 
 
 
 
 
 
 
314
  if (selectedApplicant.email) {
315
  await supabase.functions.invoke('send-interview-email', {
316
  body: {
@@ -322,7 +353,7 @@ export default function AdminInterviewManagement() {
322
  });
323
  }
324
 
325
- alert("Interview Scheduled Successfully!");
326
  setIsScheduleModalOpen(false);
327
  fetchData();
328
 
@@ -411,7 +442,6 @@ export default function AdminInterviewManagement() {
411
  )}
412
  </AnimatePresence>
413
 
414
- {/* ✅ NEW FULL CHAT MODAL */}
415
  <AnimatePresence>
416
  {isMessageModalOpen && (
417
  <AdminChatModal
 
186
  const [selectedApplicant, setSelectedApplicant] = useState(null);
187
  const [drawerCandidate, setDrawerCandidate] = useState(null);
188
 
189
+ // FIXED: Marked this function as 'async' to resolve the Syntax Error
 
 
190
  const fetchData = async () => {
191
  try {
192
  setLoading(true);
 
196
  .from('applications')
197
  .select(`
198
  id, user_id, job_id, created_at, status, experience, skills, match_score, resume_url,
199
+ profiles ( id, full_name, email, avatar_url, phone, location, summary, headline, current_position, education, work_experience, experience_years, projects, skills, technical_skills, resume_url ),
200
  jobs ( id, title ),
201
  interviews ( id, date, time, status, created_at )
202
  `)
 
270
  }
271
  };
272
 
273
+ // 1. Fetch Data on Component Mount
274
+ useEffect(() => {
275
+ fetchData();
276
+ }, []);
277
+
278
  // 2. Updated Schedule Handler
279
+ // ✅ Helper: Send message to candidate
280
+ const sendMessageToCandidate = async (candidateUserId, message) => {
281
+ try {
282
+ const { data: { user: adminUser } } = await supabase.auth.getUser();
283
+ if (!adminUser) return;
284
+
285
+ const { error } = await supabase.from('messages').insert([{
286
+ sender_id: adminUser.id,
287
+ receiver_id: candidateUserId,
288
+ content: message,
289
+ is_read: false
290
+ }]);
291
+
292
+ if (error) console.error('Error sending message:', error);
293
+ } catch (err) {
294
+ console.error('Failed to send message:', err);
295
+ }
296
+ };
297
+
298
  const handleScheduleConfirm = async (scheduleData) => {
299
  if (!selectedApplicant) return;
300
 
 
333
 
334
  if (dbError) throw dbError;
335
 
336
+ // ✅ Send interview scheduled message to candidate
337
+ const interviewDatesTime = `${date} at ${time}`;
338
+ const modeInfo = mode === 'Online' ? `via ${details}` : `at ${details}`;
339
+ const jobContext = selectedApplicant.role ? ` for ${selectedApplicant.role}` : '';
340
+ await sendMessageToCandidate(
341
+ selectedApplicant.userId,
342
+ `📅 Great news! Your interview${jobContext} has been scheduled for ${interviewDatesTime} (${interviewType}) ${modeInfo}. Interviewer: ${interviewerName} (${interviewerRole}). Please confirm your availability.`
343
+ );
344
+
345
  if (selectedApplicant.email) {
346
  await supabase.functions.invoke('send-interview-email', {
347
  body: {
 
353
  });
354
  }
355
 
356
+ alert("Interview Scheduled Successfully and candidate notified!");
357
  setIsScheduleModalOpen(false);
358
  fetchData();
359
 
 
442
  )}
443
  </AnimatePresence>
444
 
 
445
  <AnimatePresence>
446
  {isMessageModalOpen && (
447
  <AdminChatModal
src/components/Admin/AdminSortingPage.jsx CHANGED
@@ -262,6 +262,25 @@ export default function AdminSortingPage() {
262
  fetchApplicants();
263
  }, []);
264
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
  // --- BULK ACTIONS ---
266
  const handleBulkReject = async () => {
267
  if (!confirm(`Are you sure you want to REJECT ${selectedIds.length} candidates?`)) return;
@@ -269,17 +288,27 @@ export default function AdminSortingPage() {
269
  try {
270
  const { error } = await supabase
271
  .from('applications')
272
- .update({ status: 'Rejected' }) // Update status to Rejected
273
  .in('id', selectedIds);
274
  if (error) throw error;
275
 
 
 
 
 
 
 
 
 
 
 
276
  // Update UI instantly
277
  setApplicants(prev => prev.map(app =>
278
  selectedIds.includes(app.id) ? { ...app, status: 'Rejected' } : app
279
  ));
280
 
281
- setSelectedIds([]); // Clear selection
282
- alert('Candidates Rejected.');
283
  } catch (error) {
284
  console.error('Error rejecting:', error.message);
285
  alert('Failed to reject.');
@@ -298,11 +327,21 @@ export default function AdminSortingPage() {
298
 
299
  if (error) throw error;
300
 
 
 
 
 
 
 
 
 
 
 
301
  setApplicants(prev => prev.map(app =>
302
  selectedIds.includes(app.id) ? { ...app, status: 'Accepted' } : app
303
  ));
304
  setSelectedIds([]);
305
- alert('Approved Successfully!');
306
  } catch (error) {
307
  console.error('Error approving:', error.message);
308
  alert('Failed to update.');
 
262
  fetchApplicants();
263
  }, []);
264
 
265
+ // --- HELPER: Send message to candidate ---
266
+ const sendMessageToCandidate = async (candidateUserId, message) => {
267
+ try {
268
+ const { data: { user: adminUser } } = await supabase.auth.getUser();
269
+ if (!adminUser) return;
270
+
271
+ const { error } = await supabase.from('messages').insert([{
272
+ sender_id: adminUser.id,
273
+ receiver_id: candidateUserId,
274
+ content: message,
275
+ is_read: false
276
+ }]);
277
+
278
+ if (error) console.error('Error sending message:', error);
279
+ } catch (err) {
280
+ console.error('Failed to send message:', err);
281
+ }
282
+ };
283
+
284
  // --- BULK ACTIONS ---
285
  const handleBulkReject = async () => {
286
  if (!confirm(`Are you sure you want to REJECT ${selectedIds.length} candidates?`)) return;
 
288
  try {
289
  const { error } = await supabase
290
  .from('applications')
291
+ .update({ status: 'Rejected' })
292
  .in('id', selectedIds);
293
  if (error) throw error;
294
 
295
+ // ✅ Send rejection message to each candidate
296
+ const rejectedApplicants = applicants.filter(a => selectedIds.includes(a.id));
297
+ for (const applicant of rejectedApplicants) {
298
+ const jobContext = applicant.jobTitle ? ` for ${applicant.jobTitle}` : '';
299
+ await sendMessageToCandidate(
300
+ applicant.userId,
301
+ `📧 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!`
302
+ );
303
+ }
304
+
305
  // Update UI instantly
306
  setApplicants(prev => prev.map(app =>
307
  selectedIds.includes(app.id) ? { ...app, status: 'Rejected' } : app
308
  ));
309
 
310
+ setSelectedIds([]);
311
+ alert('Candidates Rejected and notified.');
312
  } catch (error) {
313
  console.error('Error rejecting:', error.message);
314
  alert('Failed to reject.');
 
327
 
328
  if (error) throw error;
329
 
330
+ // ✅ Send acceptance message to each candidate
331
+ const approvedApplicants = applicants.filter(a => selectedIds.includes(a.id));
332
+ for (const applicant of approvedApplicants) {
333
+ const jobContext = applicant.jobTitle ? ` for ${applicant.jobTitle}` : '';
334
+ await sendMessageToCandidate(
335
+ applicant.userId,
336
+ `🎉 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.`
337
+ );
338
+ }
339
+
340
  setApplicants(prev => prev.map(app =>
341
  selectedIds.includes(app.id) ? { ...app, status: 'Accepted' } : app
342
  ));
343
  setSelectedIds([]);
344
+ alert('Approved Successfully and candidates notified!');
345
  } catch (error) {
346
  console.error('Error approving:', error.message);
347
  alert('Failed to update.');
src/components/Admin/AdminSummary.jsx CHANGED
@@ -23,7 +23,7 @@ const BellIcon = () => (
23
  </svg>
24
  );
25
 
26
- export default function AdminSummary({ onNavigate }) {
27
  const containerVariants = { hidden: { opacity: 0 }, visible: { opacity: 1, transition: { staggerChildren: 0.1 } } };
28
  const itemVariants = { hidden: { opacity: 0, y: 20 }, visible: { opacity: 1, y: 0 } };
29
 
@@ -36,6 +36,7 @@ export default function AdminSummary({ onNavigate }) {
36
  // ✅ Notification State
37
  const [notifications, setNotifications] = useState([]);
38
  const [showNotifications, setShowNotifications] = useState(false);
 
39
  const notifRef = useRef(null);
40
 
41
  // ✅ CLICK OUTSIDE LISTENER
@@ -54,6 +55,85 @@ export default function AdminSummary({ onNavigate }) {
54
  fetchDashboardData();
55
  }, []);
56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  const fetchDashboardData = async () => {
58
  try {
59
  setLoading(true);
@@ -103,6 +183,53 @@ export default function AdminSummary({ onNavigate }) {
103
 
104
  return (
105
  <motion.div variants={containerVariants} initial="hidden" animate="visible">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
 
107
  {/* ✅ HEADER SECTION */}
108
  <motion.header
@@ -172,11 +299,42 @@ export default function AdminSummary({ onNavigate }) {
172
  <div style={{ maxHeight: '300px', overflowY: 'auto' }}>
173
  {notifications.length > 0 ? (
174
  notifications.map(notif => (
175
- <div key={notif.id} style={{ padding: '1rem', borderBottom: '1px solid rgba(255,255,255,0.05)', display: 'flex', gap: '0.75rem', alignItems: 'start' }}>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  <div style={{ width: '8px', height: '8px', borderRadius: '50%', backgroundColor: notif.color, marginTop: '6px', flexShrink: 0 }}></div>
177
- <div>
178
- <p style={{ fontSize: '0.85rem', color: '#e2e8f0', margin: '0 0 0.25rem 0' }}>{notif.text}</p>
179
- <p style={{ fontSize: '0.75rem', color: '#94a3b8', margin: 0 }}>
 
 
 
180
  {new Date(notif.time).toLocaleDateString()} • {new Date(notif.time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
181
  </p>
182
  </div>
 
23
  </svg>
24
  );
25
 
26
+ export default function AdminSummary({ onNavigate, setActiveTab, selectedChatUserId, setSelectedChatUserId }) {
27
  const containerVariants = { hidden: { opacity: 0 }, visible: { opacity: 1, transition: { staggerChildren: 0.1 } } };
28
  const itemVariants = { hidden: { opacity: 0, y: 20 }, visible: { opacity: 1, y: 0 } };
29
 
 
36
  // ✅ Notification State
37
  const [notifications, setNotifications] = useState([]);
38
  const [showNotifications, setShowNotifications] = useState(false);
39
+ const [latestPopup, setLatestPopup] = useState(null); // ✅ Show only latest popup
40
  const notifRef = useRef(null);
41
 
42
  // ✅ CLICK OUTSIDE LISTENER
 
55
  fetchDashboardData();
56
  }, []);
57
 
58
+ // ✅ HELPER: Show popup notification (only latest)
59
+ const showPopup = (notif) => {
60
+ setLatestPopup(notif);
61
+
62
+ // Auto-dismiss after 4 seconds
63
+ setTimeout(() => {
64
+ setLatestPopup(null);
65
+ }, 4000);
66
+ };
67
+
68
+ // ✅ MESSAGE POLLING - Check for new messages every 5 seconds
69
+ useEffect(() => {
70
+ let pollInterval;
71
+ const startPolling = async () => {
72
+ const { data: { user } } = await supabase.auth.getUser();
73
+ console.log('📱 Polling started for user:', user?.id);
74
+ if (!user) return;
75
+
76
+ let lastMessageId = null;
77
+
78
+ pollInterval = setInterval(async () => {
79
+ try {
80
+ const { data: newMessages } = await supabase
81
+ .from('messages')
82
+ .select('id, sender_id, content, created_at')
83
+ .eq('receiver_id', user.id)
84
+ .order('id', { ascending: false })
85
+ .limit(1);
86
+
87
+ if (newMessages && newMessages.length > 0) {
88
+ const msg = newMessages[0];
89
+ console.log('📬 Checking messages:', msg.id, 'last:', lastMessageId);
90
+
91
+ // Only process if it's a new message
92
+ if (!lastMessageId || msg.id > lastMessageId) {
93
+ lastMessageId = msg.id;
94
+
95
+ // Get sender profile
96
+ const { data: senderProfile } = await supabase
97
+ .from('profiles')
98
+ .select('full_name')
99
+ .eq('id', msg.sender_id)
100
+ .single();
101
+
102
+ const senderName = senderProfile?.full_name || 'Candidate';
103
+
104
+ // Create and add notification
105
+ const newNotif = {
106
+ id: `msg-${msg.id}`,
107
+ type: 'New Message',
108
+ text: `📨 New message from ${senderName}`,
109
+ time: msg.created_at,
110
+ color: '#10b981',
111
+ preview: msg.content,
112
+ senderId: msg.sender_id // ✅ Store sender ID for navigation
113
+ };
114
+
115
+ console.log('🔔 Message notification:', newNotif);
116
+
117
+ setNotifications(prev => {
118
+ const exists = prev.some(n => n.id === newNotif.id);
119
+ if (exists) return prev;
120
+ return [newNotif, ...prev];
121
+ });
122
+
123
+ // ✅ SHOW POPUP
124
+ showPopup(newNotif);
125
+ }
126
+ }
127
+ } catch (err) {
128
+ console.error("Poll error:", err);
129
+ }
130
+ }, 3000); // Check every 3 seconds
131
+ };
132
+
133
+ startPolling();
134
+ return () => { if (pollInterval) clearInterval(pollInterval); };
135
+ }, [showPopup]);
136
+
137
  const fetchDashboardData = async () => {
138
  try {
139
  setLoading(true);
 
183
 
184
  return (
185
  <motion.div variants={containerVariants} initial="hidden" animate="visible">
186
+
187
+ {/* ✅ SINGLE SMALL POPUP NOTIFICATION */}
188
+ <AnimatePresence>
189
+ {latestPopup && (
190
+ <motion.div
191
+ key="popup"
192
+ initial={{ opacity: 0, y: -15, x: 20 }}
193
+ animate={{ opacity: 1, y: 0, x: 0 }}
194
+ exit={{ opacity: 0, y: -15, x: 20 }}
195
+ style={{
196
+ position: 'fixed',
197
+ top: '20px',
198
+ right: '20px',
199
+ background: 'rgba(15, 23, 42, 0.98)',
200
+ border: `2px solid ${latestPopup.color}`,
201
+ borderRadius: '10px',
202
+ padding: '0.75rem 1rem',
203
+ width: '280px',
204
+ zIndex: 9999,
205
+ backdropFilter: 'blur(10px)',
206
+ boxShadow: '0 8px 24px rgba(0,0,0,0.6)',
207
+ fontFamily: 'inherit'
208
+ }}
209
+ >
210
+ <p style={{
211
+ color: '#e2e8f0',
212
+ margin: '0 0 0.3rem 0',
213
+ fontWeight: '600',
214
+ fontSize: '0.88rem'
215
+ }}>
216
+ {latestPopup.text}
217
+ </p>
218
+ {latestPopup.preview && (
219
+ <p style={{
220
+ color: '#cbd5e1',
221
+ margin: '0',
222
+ fontSize: '0.78rem',
223
+ whiteSpace: 'nowrap',
224
+ overflow: 'hidden',
225
+ textOverflow: 'ellipsis'
226
+ }}>
227
+ {latestPopup.preview}
228
+ </p>
229
+ )}
230
+ </motion.div>
231
+ )}
232
+ </AnimatePresence>
233
 
234
  {/* ✅ HEADER SECTION */}
235
  <motion.header
 
299
  <div style={{ maxHeight: '300px', overflowY: 'auto' }}>
300
  {notifications.length > 0 ? (
301
  notifications.map(notif => (
302
+ <div
303
+ key={notif.id}
304
+ onClick={() => {
305
+ console.log('Notification clicked:', notif.type);
306
+
307
+ // ✅ Navigate based on notification type
308
+ if (notif.type === 'New Message' && notif.senderId) {
309
+ // For messages, set the selected chat user and go to messages tab
310
+ setSelectedChatUserId(notif.senderId);
311
+ }
312
+
313
+ setActiveTab('messages');
314
+ setShowNotifications(false);
315
+ // ✅ Remove notification after clicking
316
+ setNotifications(prev => prev.filter(n => n.id !== notif.id));
317
+ }}
318
+ style={{
319
+ padding: '1rem',
320
+ borderBottom: '1px solid rgba(255,255,255,0.05)',
321
+ display: 'flex',
322
+ gap: '0.75rem',
323
+ alignItems: 'start',
324
+ cursor: 'pointer',
325
+ transition: 'background 0.2s',
326
+ ':hover': { background: 'rgba(255,255,255,0.05)' }
327
+ }}
328
+ onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(255,255,255,0.08)'}
329
+ onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
330
+ >
331
  <div style={{ width: '8px', height: '8px', borderRadius: '50%', backgroundColor: notif.color, marginTop: '6px', flexShrink: 0 }}></div>
332
+ <div style={{ flex: 1 }}>
333
+ <p style={{ fontSize: '0.85rem', color: '#e2e8f0', margin: '0 0 0.25rem 0', fontWeight: '500' }}>{notif.text}</p>
334
+ {notif.preview && (
335
+ <p style={{ fontSize: '0.75rem', color: '#cbd5e1', margin: '0.25rem 0 0 0', fontStyle: 'italic' }}>{notif.preview}</p>
336
+ )}
337
+ <p style={{ fontSize: '0.75rem', color: '#94a3b8', margin: '0.25rem 0 0 0' }}>
338
  {new Date(notif.time).toLocaleDateString()} • {new Date(notif.time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
339
  </p>
340
  </div>
src/components/ApplicantLayout.jsx CHANGED
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect, useRef } from 'react'; // ✅ ADDED useRef
2
  import { motion, AnimatePresence } from 'framer-motion';
3
  import { supabase } from '../supabaseClient';
4
  import {
@@ -6,7 +6,6 @@ import {
6
  CalendarIcon, AtsCheckerIcon
7
  } from './Icons';
8
 
9
- // ✅ ADDED: Bell Icon SVG
10
  const BellIcon = () => (
11
  <svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
12
  <path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path>
@@ -16,16 +15,16 @@ const BellIcon = () => (
16
 
17
  export default function ApplicantLayout({ children, activePage, onNavigate }) {
18
 
19
- // ✅ FIX: Initialize state directly from LocalStorage.
20
- // This removes the "flicker" because it grabs the name instantly before the page paints.
21
  const [userName, setUserName] = useState(() => localStorage.getItem('applicant_name') || '');
22
 
23
- // ✅ ADDED: Notification States
24
  const [notifications, setNotifications] = useState([]);
25
  const [showNotifications, setShowNotifications] = useState(false);
26
  const notifRef = useRef(null);
27
 
28
- // ADDED: Close dropdown when clicking outside
 
 
 
29
  useEffect(() => {
30
  function handleClickOutside(event) {
31
  if (notifRef.current && !notifRef.current.contains(event.target)) {
@@ -49,11 +48,7 @@ export default function ApplicantLayout({ children, activePage, onNavigate }) {
49
 
50
  if (profile && profile.full_name) {
51
  const firstName = profile.full_name.split(' ')[0];
52
-
53
- // Update state
54
  setUserName(firstName);
55
-
56
- // ✅ Save to LocalStorage for next time (Instant load on refresh)
57
  localStorage.setItem('applicant_name', firstName);
58
  }
59
  }
@@ -64,7 +59,73 @@ export default function ApplicantLayout({ children, activePage, onNavigate }) {
64
  fetchUserName();
65
  }, []);
66
 
67
- // ADDED: Fetch notifications exclusively for the profile page
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  useEffect(() => {
69
  const fetchNotifications = async () => {
70
  if (activePage === 'applicant-profile') {
@@ -96,15 +157,37 @@ export default function ApplicantLayout({ children, activePage, onNavigate }) {
96
  }
97
  };
98
  fetchNotifications();
99
- }, [activePage]); // Runs when activePage changes
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
 
101
  const handleLogout = async () => {
102
  await supabase.auth.signOut();
103
- localStorage.removeItem('applicant_name'); // ✅ Clear cache so next user doesn't see your name
104
  onNavigate('login');
105
  };
106
 
107
- // Helper to check if a tab is active
108
  const isActive = (key) => activePage === key;
109
 
110
  const navItems = [
@@ -117,25 +200,14 @@ export default function ApplicantLayout({ children, activePage, onNavigate }) {
117
 
118
  return (
119
  <div style={{ height: '100vh', width: '100%', backgroundColor: '#020617', color: 'white', fontFamily: "'Montserrat', sans-serif", padding: '2rem', boxSizing: 'border-box', display: 'flex', flexDirection: 'column', position: 'relative' }}>
120
- <style>{` main::-webkit-scrollbar { width: 8px; } main::-webkit-scrollbar-track { background: rgba(255, 255, 255, 0.1); border-radius: 10px; } main::-webkit-scrollbar-thumb { background: #FBBF24; border-radius: 10px; } main::-webkit-scrollbar-thumb:hover { background: #FCD34D; } `}</style>
121
 
122
- {/* Background Blobs */}
123
- <div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, zIndex: 0, overflow: 'hidden', pointerEvents: 'none' }}>
124
- <div style={{ position: 'absolute', borderRadius: '50%', filter: 'blur(80px)', opacity: 0.3, width: '400px', height: '400px', backgroundColor: '#FBBF24', top: '-50px', left: '-100px' }}></div>
125
- <div style={{ position: 'absolute', borderRadius: '50%', filter: 'blur(80px)', opacity: 0.3, width: '400px', height: '400px', backgroundColor: '#F59E0B', bottom: '-80px', right: '-120px' }}></div>
126
- </div>
127
-
128
- {/* Header */}
129
  <header style={{ position: 'relative', zIndex: 1, display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem', flexShrink: 0 }}>
130
- {/* Name appears instantly now if cached */}
131
  <h1 style={{ fontSize: '1.875rem', fontWeight: 'bold' }}>
132
  {userName ? `Hi, ${userName} 👋` : 'Welcome 👋'}
133
  </h1>
134
 
135
- {/* ✅ ADDED: Group wrapper to place Bell and Logout together */}
136
  <div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
137
 
138
- {/* ✅ ADDED: Conditionally render Bell ONLY on profile page */}
139
  {activePage === 'applicant-profile' && (
140
  <div style={{ position: 'relative' }} ref={notifRef}>
141
  <motion.button
@@ -153,47 +225,87 @@ export default function ApplicantLayout({ children, activePage, onNavigate }) {
153
  }}
154
  >
155
  <BellIcon />
156
- {notifications.length > 0 && (
157
  <span style={{
158
- position: 'absolute', top: '0px', right: '0px',
159
- width: '12px', height: '12px',
160
- backgroundColor: '#FBBF24', // Yellow dot
 
 
 
161
  borderRadius: '50%',
162
- border: '2px solid #020617'
163
- }}></span>
 
 
 
 
 
 
 
 
164
  )}
165
  </motion.button>
166
 
167
- {/* Dropdown */}
168
  <AnimatePresence>
169
  {showNotifications && (
170
  <motion.div
171
- initial={{ opacity: 0, y: 10, scale: 0.95 }}
172
- animate={{ opacity: 1, y: 0, scale: 1 }}
173
- exit={{ opacity: 0, y: 10, scale: 0.95 }}
174
  style={{
175
- position: 'absolute', top: '55px', right: '0', width: '320px',
176
- backgroundColor: '#1e293b', border: '1px solid rgba(255,255,255,0.1)',
177
- borderRadius: '12px', boxShadow: '0 10px 25px rgba(0,0,0,0.5)', zIndex: 50, overflow: 'hidden'
 
 
 
 
 
 
 
178
  }}
179
  >
180
- <div style={{ padding: '1rem', borderBottom: '1px solid rgba(255,255,255,0.1)', fontWeight: 'bold', fontSize: '0.9rem' }}>
181
- Notifications
182
  </div>
183
- <div style={{ maxHeight: '300px', overflowY: 'auto' }}>
184
- {notifications.length > 0 ? (
185
- notifications.map(notif => (
186
- <div key={notif.id} style={{ padding: '1rem', borderBottom: '1px solid rgba(255,255,255,0.05)', display: 'flex', gap: '0.75rem', alignItems: 'start' }}>
187
- <div style={{ width: '8px', height: '8px', borderRadius: '50%', backgroundColor: notif.color, marginTop: '6px', flexShrink: 0 }}></div>
188
- <div>
189
- <p style={{ fontSize: '0.85rem', color: '#e2e8f0', margin: '0 0 0.25rem 0', fontWeight: 'bold' }}>{notif.title}</p>
190
- <p style={{ fontSize: '0.8rem', color: '#cbd5e1', margin: '0 0 0.25rem 0' }}>{notif.text}</p>
191
- <p style={{ fontSize: '0.7rem', color: '#94a3b8', margin: 0 }}>{new Date(notif.time).toLocaleDateString()}</p>
192
- </div>
193
- </div>
194
- ))
195
  ) : (
196
- <div style={{ padding: '2rem', textAlign: 'center', color: '#64748b', fontSize: '0.85rem' }}>No new notifications</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  )}
198
  </div>
199
  </motion.div>
@@ -214,9 +326,8 @@ export default function ApplicantLayout({ children, activePage, onNavigate }) {
214
  </div>
215
  </header>
216
 
217
- {/* Navigation Bar */}
218
  <div style={{ display: 'flex', justifyContent: 'center', width: '100%', flexShrink: 0, marginBottom: '2rem' }}>
219
- <nav style={{ position: 'relative', zIndex: 1, display: 'inline-flex', gap: '1rem', backgroundColor: 'rgba(255, 255, 255, 0.1)', borderRadius: '1rem', padding: '0.5rem', overflowX: 'auto' }}>
220
  {navItems.map(({ key, icon, label }) => {
221
  const active = isActive(key);
222
  return (
@@ -226,9 +337,19 @@ export default function ApplicantLayout({ children, activePage, onNavigate }) {
226
  style={{ position: 'relative', padding: '0.75rem 1.5rem', borderRadius: '0.5rem', cursor: 'pointer', display: 'flex', alignItems: 'center', color: active ? '#FCD34D' : '#d1d5db', fontWeight: active ? 'bold' : 'normal', zIndex: 1 }}
227
  >
228
  {icon}
229
- <span style={{ marginLeft: '0.5rem', textTransform: 'capitalize' }}>{label}</span>
 
230
  {active && (
231
- <motion.div layoutId="active-pill" style={{ position: 'absolute', inset: 0, backgroundColor: 'rgba(251, 191, 36, 0.2)', borderRadius: '0.5rem', zIndex: -1 }} transition={{ duration: 0.2 }} />
 
 
 
 
 
 
 
 
 
232
  )}
233
  </div>
234
  );
@@ -236,20 +357,9 @@ export default function ApplicantLayout({ children, activePage, onNavigate }) {
236
  </nav>
237
  </div>
238
 
239
- {/* Main Content */}
240
- <main style={{ position: 'relative', zIndex: 1, flex: 1, overflowY: 'auto', paddingRight: '1rem' }}>
241
- <AnimatePresence mode="wait">
242
- <motion.div
243
- key={activePage}
244
- initial={{ opacity: 0, y: 10 }}
245
- animate={{ opacity: 1, y: 0 }}
246
- exit={{ opacity: 0, y: -10 }}
247
- transition={{ duration: 0.2 }}
248
- >
249
- {children}
250
- </motion.div>
251
- </AnimatePresence>
252
  </main>
253
  </div>
254
  );
255
- };
 
1
+ import React, { useState, useEffect, useRef } from 'react';
2
  import { motion, AnimatePresence } from 'framer-motion';
3
  import { supabase } from '../supabaseClient';
4
  import {
 
6
  CalendarIcon, AtsCheckerIcon
7
  } from './Icons';
8
 
 
9
  const BellIcon = () => (
10
  <svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
11
  <path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path>
 
15
 
16
  export default function ApplicantLayout({ children, activePage, onNavigate }) {
17
 
 
 
18
  const [userName, setUserName] = useState(() => localStorage.getItem('applicant_name') || '');
19
 
 
20
  const [notifications, setNotifications] = useState([]);
21
  const [showNotifications, setShowNotifications] = useState(false);
22
  const notifRef = useRef(null);
23
 
24
+ // NEW: unread message badge state
25
+ const [unreadMessages, setUnreadMessages] = useState(0);
26
+ const [unreadMessagesList, setUnreadMessagesList] = useState([]);
27
+
28
  useEffect(() => {
29
  function handleClickOutside(event) {
30
  if (notifRef.current && !notifRef.current.contains(event.target)) {
 
48
 
49
  if (profile && profile.full_name) {
50
  const firstName = profile.full_name.split(' ')[0];
 
 
51
  setUserName(firstName);
 
 
52
  localStorage.setItem('applicant_name', firstName);
53
  }
54
  }
 
59
  fetchUserName();
60
  }, []);
61
 
62
+ // NEW: fetch unread messages count and details
63
+ useEffect(() => {
64
+
65
+ const fetchUnreadMessages = async () => {
66
+
67
+ const { data: { user } } = await supabase.auth.getUser();
68
+ if (!user) return;
69
+
70
+ const { data: messages } = await supabase
71
+ .from("messages")
72
+ .select("id, sender_id, content, created_at, profiles(full_name)")
73
+ .eq("receiver_id", user.id)
74
+ .eq("is_read", false)
75
+ .order("created_at", { ascending: false });
76
+
77
+ if (messages) {
78
+ setUnreadMessages(messages.length);
79
+ // Format messages with sender names and timestamps
80
+ const formattedMessages = messages.map(msg => ({
81
+ id: msg.id,
82
+ senderName: msg.profiles?.full_name || 'Someone',
83
+ content: msg.content,
84
+ timestamp: new Date(msg.created_at).toLocaleDateString() + ' • ' + new Date(msg.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
85
+ }));
86
+ setUnreadMessagesList(formattedMessages);
87
+ }
88
+ };
89
+
90
+ fetchUnreadMessages();
91
+
92
+ }, []);
93
+
94
+ // ⭐ NEW: realtime update for new messages
95
+ useEffect(() => {
96
+
97
+ const channel = supabase
98
+ .channel("messages-badge")
99
+ .on(
100
+ "postgres_changes",
101
+ { event: "INSERT", schema: "public", table: "messages" },
102
+ async (payload) => {
103
+ // Fetch the sender's name and add to the list
104
+ const { data: senderProfile } = await supabase
105
+ .from("profiles")
106
+ .select("full_name")
107
+ .eq("id", payload.new.sender_id)
108
+ .single();
109
+
110
+ const newMsg = {
111
+ id: payload.new.id,
112
+ senderName: senderProfile?.full_name || 'Someone',
113
+ content: payload.new.content,
114
+ timestamp: new Date(payload.new.created_at).toLocaleDateString() + ' • ' + new Date(payload.new.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
115
+ };
116
+
117
+ setUnreadMessagesList(prev => [newMsg, ...prev]);
118
+ setUnreadMessages(prev => prev + 1);
119
+ }
120
+ )
121
+ .subscribe();
122
+
123
+ return () => {
124
+ supabase.removeChannel(channel);
125
+ };
126
+
127
+ }, []);
128
+
129
  useEffect(() => {
130
  const fetchNotifications = async () => {
131
  if (activePage === 'applicant-profile') {
 
157
  }
158
  };
159
  fetchNotifications();
160
+ }, [activePage]);
161
+
162
+ // ⭐ NEW: mark messages read when user opens messages page
163
+ useEffect(() => {
164
+
165
+ const markMessagesRead = async () => {
166
+
167
+ if (activePage !== "applicant-messages") return;
168
+
169
+ const { data: { user } } = await supabase.auth.getUser();
170
+ if (!user) return;
171
+
172
+ await supabase
173
+ .from("messages")
174
+ .update({ is_read: true })
175
+ .eq("receiver_id", user.id);
176
+
177
+ setUnreadMessages(0);
178
+ setUnreadMessagesList([]);
179
+ };
180
+
181
+ markMessagesRead();
182
+
183
+ }, [activePage]);
184
 
185
  const handleLogout = async () => {
186
  await supabase.auth.signOut();
187
+ localStorage.removeItem('applicant_name');
188
  onNavigate('login');
189
  };
190
 
 
191
  const isActive = (key) => activePage === key;
192
 
193
  const navItems = [
 
200
 
201
  return (
202
  <div style={{ height: '100vh', width: '100%', backgroundColor: '#020617', color: 'white', fontFamily: "'Montserrat', sans-serif", padding: '2rem', boxSizing: 'border-box', display: 'flex', flexDirection: 'column', position: 'relative' }}>
 
203
 
 
 
 
 
 
 
 
204
  <header style={{ position: 'relative', zIndex: 1, display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem', flexShrink: 0 }}>
 
205
  <h1 style={{ fontSize: '1.875rem', fontWeight: 'bold' }}>
206
  {userName ? `Hi, ${userName} 👋` : 'Welcome 👋'}
207
  </h1>
208
 
 
209
  <div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
210
 
 
211
  {activePage === 'applicant-profile' && (
212
  <div style={{ position: 'relative' }} ref={notifRef}>
213
  <motion.button
 
225
  }}
226
  >
227
  <BellIcon />
228
+ {(notifications.length + unreadMessages) > 0 && (
229
  <span style={{
230
+ position: 'absolute',
231
+ top: '-5px',
232
+ right: '-5px',
233
+ width: '24px',
234
+ height: '24px',
235
+ backgroundColor: '#FBBF24',
236
  borderRadius: '50%',
237
+ border: '2px solid #020617',
238
+ display: 'flex',
239
+ alignItems: 'center',
240
+ justifyContent: 'center',
241
+ color: '#1a202c',
242
+ fontSize: '0.75rem',
243
+ fontWeight: 'bold'
244
+ }}>
245
+ {notifications.length + unreadMessages}
246
+ </span>
247
  )}
248
  </motion.button>
249
 
 
250
  <AnimatePresence>
251
  {showNotifications && (
252
  <motion.div
253
+ initial={{ opacity: 0, y: 10 }}
254
+ animate={{ opacity: 1, y: 0 }}
255
+ exit={{ opacity: 0, y: 10 }}
256
  style={{
257
+ position: 'absolute',
258
+ top: '55px',
259
+ right: '0',
260
+ width: '320px',
261
+ backgroundColor: '#1e293b',
262
+ border: '1px solid rgba(255,255,255,0.1)',
263
+ borderRadius: '12px',
264
+ zIndex: 50,
265
+ maxHeight: '300px',
266
+ overflowY: 'auto'
267
  }}
268
  >
269
+ <div style={{ padding: '1rem', fontWeight: 'bold', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>
270
+ Recent Notification ({notifications.length + unreadMessages})
271
  </div>
272
+ <div style={{ padding: '0.75rem' }}>
273
+ {notifications.length + unreadMessages === 0 ? (
274
+ <div style={{ padding: '1rem', textAlign: 'center', color: '#94a3b8', fontSize: '0.85rem' }}>
275
+ No new notifications
276
+ </div>
 
 
 
 
 
 
 
277
  ) : (
278
+ <>
279
+ {unreadMessagesList.map(msg => (
280
+ <div key={msg.id} style={{ padding: '0.75rem', borderBottom: '1px solid rgba(255,255,255,0.1)', display: 'flex', gap: '0.5rem', alignItems: 'flex-start' }}>
281
+ <span style={{ color: '#3b82f6', fontSize: '1rem' }}>📨</span>
282
+ <div style={{ flex: 1 }}>
283
+ <p style={{ margin: 0, fontSize: '0.85rem', color: '#e2e8f0', fontWeight: 500 }}>
284
+ {msg.senderName}
285
+ </p>
286
+ <p style={{ margin: '0.25rem 0 0 0', fontSize: '0.75rem', color: '#cbd5e1' }}>
287
+ {msg.timestamp}
288
+ </p>
289
+ </div>
290
+ </div>
291
+ ))}
292
+ {notifications.map(notif => (
293
+ <div key={notif.id} style={{ padding: '0.75rem', borderBottom: '1px solid rgba(255,255,255,0.1)', display: 'flex', gap: '0.5rem', alignItems: 'flex-start' }}>
294
+ <span style={{ color: notif.color, fontSize: '1rem' }}>•</span>
295
+ <div style={{ flex: 1 }}>
296
+ <p style={{ margin: 0, fontSize: '0.85rem', color: '#e2e8f0', fontWeight: 500 }}>
297
+ {notif.title}
298
+ </p>
299
+ <p style={{ margin: '0.25rem 0 0 0', fontSize: '0.75rem', color: '#cbd5e1' }}>
300
+ {notif.text}
301
+ </p>
302
+ <p style={{ margin: '0.25rem 0 0 0', fontSize: '0.7rem', color: '#64748b' }}>
303
+ {notif.time}
304
+ </p>
305
+ </div>
306
+ </div>
307
+ ))}
308
+ </>
309
  )}
310
  </div>
311
  </motion.div>
 
326
  </div>
327
  </header>
328
 
 
329
  <div style={{ display: 'flex', justifyContent: 'center', width: '100%', flexShrink: 0, marginBottom: '2rem' }}>
330
+ <nav style={{ position: 'relative', zIndex: 1, display: 'inline-flex', gap: '1rem', backgroundColor: 'rgba(255, 255, 255, 0.1)', borderRadius: '1rem', padding: '0.5rem' }}>
331
  {navItems.map(({ key, icon, label }) => {
332
  const active = isActive(key);
333
  return (
 
337
  style={{ position: 'relative', padding: '0.75rem 1.5rem', borderRadius: '0.5rem', cursor: 'pointer', display: 'flex', alignItems: 'center', color: active ? '#FCD34D' : '#d1d5db', fontWeight: active ? 'bold' : 'normal', zIndex: 1 }}
338
  >
339
  {icon}
340
+ <span style={{ marginLeft: '0.5rem' }}>{label}</span>
341
+
342
  {active && (
343
+ <motion.div
344
+ layoutId="active-pill"
345
+ style={{
346
+ position: 'absolute',
347
+ inset: 0,
348
+ backgroundColor: 'rgba(251, 191, 36, 0.2)',
349
+ borderRadius: '0.5rem',
350
+ zIndex: -1
351
+ }}
352
+ />
353
  )}
354
  </div>
355
  );
 
357
  </nav>
358
  </div>
359
 
360
+ <main style={{ position: 'relative', zIndex: 1, flex: 1, overflowY: 'auto' }}>
361
+ {children}
 
 
 
 
 
 
 
 
 
 
 
362
  </main>
363
  </div>
364
  );
365
+ }
src/components/ExperienceChart.jsx CHANGED
@@ -1,61 +1,77 @@
 
1
  import React, { useMemo } from 'react';
2
  import { motion } from 'framer-motion';
3
 
4
  const ExperienceChart = ({ applicants = [] }) => {
5
 
6
  const avgExperience = useMemo(() => {
7
- if (!applicants || applicants.length === 0) return "0.0";
8
-
9
- const totalExp = applicants.reduce((acc, curr) => {
10
- // ✅ FIX: Check all possible locations for the data
11
- // 1. 'curr.experience': If formatted by a parent component
12
- // 2. 'curr.profiles.experience_years': The actual DB column name
13
- // 3. 'curr.profiles.experience': A fallback
14
- const rawValue =
15
- curr.experience ??
16
- curr.profiles?.experience_years ??
17
- curr.profiles?.experience;
18
-
19
- // Handle null/undefined explicitly
20
- if (rawValue === null || rawValue === undefined) return acc;
21
-
22
- const strVal = String(rawValue).toLowerCase();
23
-
24
- // Filter out text that clearly indicates no experience
25
- if (strVal.includes('fresher') || strVal.includes('none') || strVal === '') {
26
- return acc;
27
- }
28
 
29
- // BETTER PARSING:
30
- // Matches "1.5", "2", "3.2 years", etc.
31
- const match = strVal.match(/(\d+(\.\d+)?)/);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
- // If match found, parse as Float (to keep the 1.5), otherwise 0
34
- const extractedNum = match ? parseFloat(match[0]) : 0;
35
 
36
- return acc + extractedNum;
37
- }, 0);
38
 
39
- // Calculate Average
40
- const avg = totalExp / applicants.length;
41
 
42
- // Return with 1 decimal place (e.g., "1.5")
43
- return isNaN(avg) ? "0.0" : avg.toFixed(1);
44
 
45
  }, [applicants]);
46
 
47
  return (
48
- <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
49
- <div style={{ position: 'relative', width: '160px', height: '160px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
50
- {/* Background Circle */}
51
- <svg viewBox="0 0 36 36" style={{ width: '100%', height: '100%', transform: 'rotate(-90deg)' }}>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  <path
53
  d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
54
  fill="none"
55
- stroke="rgba(255, 255, 255, 0.1)"
56
  strokeWidth="3"
57
  />
58
- {/* Animated Progress Circle */}
59
  <motion.path
60
  d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
61
  fill="none"
@@ -63,31 +79,60 @@ const ExperienceChart = ({ applicants = [] }) => {
63
  strokeWidth="3"
64
  strokeLinecap="round"
65
  initial={{ pathLength: 0 }}
66
- // Cap the visual meter at 10 years (dividing by 10)
67
- animate={{ pathLength: Math.min(parseFloat(avgExperience) / 10, 1) }}
 
68
  transition={{ duration: 1.5, ease: "easeOut" }}
69
  />
 
70
  </svg>
71
 
72
- {/* Center Text */}
73
- <div style={{ position: 'absolute', textAlign: 'center', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
 
 
 
 
 
 
74
  <motion.span
75
  initial={{ opacity: 0, scale: 0.5 }}
76
  animate={{ opacity: 1, scale: 1 }}
77
  transition={{ delay: 0.2 }}
78
- style={{ fontSize: '2.5rem', fontWeight: 'bold', color: 'white', lineHeight: '1' }}
 
 
 
 
 
79
  >
80
  {avgExperience}
81
  </motion.span>
82
- <span style={{ fontSize: '0.875rem', color: '#9ca3af', marginTop: '0.25rem' }}>Avg. Years</span>
 
 
 
 
 
 
 
 
83
  </div>
 
84
  </div>
85
 
86
- <p style={{ textAlign: 'center', fontSize: '0.85rem', color: '#6b7280', marginTop: '1rem' }}>
87
- Based on {applicants.length} candidate profiles
 
 
 
 
 
88
  </p>
 
89
  </div>
90
  );
91
  };
92
 
93
- export default ExperienceChart;
 
 
1
+
2
  import React, { useMemo } from 'react';
3
  import { motion } from 'framer-motion';
4
 
5
  const ExperienceChart = ({ applicants = [] }) => {
6
 
7
  const avgExperience = useMemo(() => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
+ console.log("📊 Applications received:", applicants);
10
+
11
+ if (!applicants || applicants.length === 0) {
12
+ return "0.0";
13
+ }
14
+
15
+ let totalExp = 0;
16
+
17
+ applicants.forEach((app, index) => {
18
+
19
+ const rawExp = app?.experience;
20
+
21
+ console.log(`🔍 Applicant [${index}] experience:`, rawExp);
22
+
23
+
24
+
25
+ const exp = parseFloat(rawExp);
26
+
27
+ if (!isNaN(exp)) {
28
+ totalExp += exp;
29
+ }
30
 
31
+ });
 
32
 
33
+ console.log("📈 Total Experience:", totalExp);
34
+ console.log("👥 Total Candidates:", applicants.length);
35
 
36
+ const avg = applicants.length > 0 ? totalExp / applicants.length : 0;
 
37
 
38
+ return avg.toFixed(1);
 
39
 
40
  }, [applicants]);
41
 
42
  return (
43
+ <div style={{
44
+ display: 'flex',
45
+ flexDirection: 'column',
46
+ alignItems: 'center',
47
+ justifyContent: 'center',
48
+ height: '100%'
49
+ }}>
50
+ <div style={{
51
+ position: 'relative',
52
+ width: '160px',
53
+ height: '160px',
54
+ display: 'flex',
55
+ alignItems: 'center',
56
+ justifyContent: 'center'
57
+ }}>
58
+
59
+ <svg
60
+ viewBox="0 0 36 36"
61
+ style={{
62
+ width: '100%',
63
+ height: '100%',
64
+ transform: 'rotate(-90deg)'
65
+ }}
66
+ >
67
+
68
  <path
69
  d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
70
  fill="none"
71
+ stroke="rgba(255,255,255,0.1)"
72
  strokeWidth="3"
73
  />
74
+
75
  <motion.path
76
  d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
77
  fill="none"
 
79
  strokeWidth="3"
80
  strokeLinecap="round"
81
  initial={{ pathLength: 0 }}
82
+ animate={{
83
+ pathLength: Math.min(parseFloat(avgExperience) / 10, 1)
84
+ }}
85
  transition={{ duration: 1.5, ease: "easeOut" }}
86
  />
87
+
88
  </svg>
89
 
90
+ <div style={{
91
+ position: 'absolute',
92
+ textAlign: 'center',
93
+ display: 'flex',
94
+ flexDirection: 'column',
95
+ alignItems: 'center'
96
+ }}>
97
+
98
  <motion.span
99
  initial={{ opacity: 0, scale: 0.5 }}
100
  animate={{ opacity: 1, scale: 1 }}
101
  transition={{ delay: 0.2 }}
102
+ style={{
103
+ fontSize: '2.5rem',
104
+ fontWeight: 'bold',
105
+ color: 'white',
106
+ lineHeight: '1'
107
+ }}
108
  >
109
  {avgExperience}
110
  </motion.span>
111
+
112
+ <span style={{
113
+ fontSize: '0.875rem',
114
+ color: '#9ca3af',
115
+ marginTop: '0.25rem'
116
+ }}>
117
+ Avg. Years
118
+ </span>
119
+
120
  </div>
121
+
122
  </div>
123
 
124
+ <p style={{
125
+ textAlign: 'center',
126
+ fontSize: '0.85rem',
127
+ color: '#6b7280',
128
+ marginTop: '1rem'
129
+ }}>
130
+ Based on application data
131
  </p>
132
+
133
  </div>
134
  );
135
  };
136
 
137
+ export default ExperienceChart;
138
+
src/components/JobListings.jsx CHANGED
@@ -45,6 +45,26 @@ export default function JobListings({ searchQuery, setSearchQuery, isSearching,
45
  }
46
  };
47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  // 3. Submit Application (With Verification Gatekeeper)
49
  const handleFinalSubmit = async (formData) => {
50
  if (!jobToApply) return;
@@ -62,7 +82,7 @@ export default function JobListings({ searchQuery, setSearchQuery, isSearching,
62
  // --- 🔒 GATEKEEPER CHECK: Verify Phone Status ---
63
  const { data: profile, error: profileError } = await supabase
64
  .from('profiles')
65
- .select('is_phone_verified')
66
  .eq('id', user.id)
67
  .single();
68
 
@@ -84,11 +104,15 @@ export default function JobListings({ searchQuery, setSearchQuery, isSearching,
84
  user_id: user.id,
85
  status: 'Pending',
86
  resume_url: formData.resume_url,
87
- cover_letter: formData.cover_letter
 
88
  }]);
89
 
90
  if (error) throw error;
91
 
 
 
 
92
  setAppliedJobIds(prev => new Set(prev).add(jobToApply.id));
93
  alert("Application submitted successfully!");
94
 
 
45
  }
46
  };
47
 
48
+ // ✅ Helper: Send welcome message to applicant
49
+ const sendApplicationConfirmationMessage = async (userId, jobTitle) => {
50
+ try {
51
+ const { data: { user: adminUser } } = await supabase.auth.getUser();
52
+ if (!adminUser) return;
53
+
54
+ const message = `Hello, Thank you for applying for the **${jobTitle}** position. We have received your application and our team is currently reviewing your profile. If your qualifications match our requirements, we will contact you shortly regarding the next steps in the selection process. We appreciate your interest in this opportunity.`;
55
+
56
+ const { error } = await supabase.from('messages').insert([{
57
+ sender_id: adminUser.id,
58
+ receiver_id: userId,
59
+ content: message
60
+ }]);
61
+
62
+ if (error) console.error('Error sending confirmation message:', error);
63
+ } catch (err) {
64
+ console.error('Failed to send confirmation message:', err);
65
+ }
66
+ };
67
+
68
  // 3. Submit Application (With Verification Gatekeeper)
69
  const handleFinalSubmit = async (formData) => {
70
  if (!jobToApply) return;
 
82
  // --- 🔒 GATEKEEPER CHECK: Verify Phone Status ---
83
  const { data: profile, error: profileError } = await supabase
84
  .from('profiles')
85
+ .select('is_phone_verified, experience_years')
86
  .eq('id', user.id)
87
  .single();
88
 
 
104
  user_id: user.id,
105
  status: 'Pending',
106
  resume_url: formData.resume_url,
107
+ cover_letter: formData.cover_letter,
108
+ experience: profile.experience // Include experience from profile
109
  }]);
110
 
111
  if (error) throw error;
112
 
113
+ // ✅ Send confirmation message to applicant
114
+ await sendApplicationConfirmationMessage(user.id, jobToApply.title);
115
+
116
  setAppliedJobIds(prev => new Set(prev).add(jobToApply.id));
117
  alert("Application submitted successfully!");
118
 
src/pages/Admindashboard.jsx CHANGED
@@ -14,15 +14,16 @@ import TalentClusters from '../components/Admin/TalentClusters';
14
  export default function AdminDashboard({ onNavigate }) {
15
  const [activeTab, setActiveTab] = useState('dashboard');
16
  const [isModalOpen, setIsModalOpen] = useState(false);
 
17
 
18
  const renderContent = () => {
19
  switch (activeTab) {
20
  case 'dashboard':
21
- return <AdminSummary onNavigate={onNavigate} setIsModalOpen={setIsModalOpen} />;
22
  case 'jobs':
23
  return <AdminSortingPage />;
24
  case 'messages':
25
- return <AdminInterviewManagement />;
26
  case 'job-management':
27
  return <JobPosting />;
28
  case 'clusters':
 
14
  export default function AdminDashboard({ onNavigate }) {
15
  const [activeTab, setActiveTab] = useState('dashboard');
16
  const [isModalOpen, setIsModalOpen] = useState(false);
17
+ const [selectedChatUserId, setSelectedChatUserId] = useState(null); // ✅ Track selected chat user
18
 
19
  const renderContent = () => {
20
  switch (activeTab) {
21
  case 'dashboard':
22
+ return <AdminSummary onNavigate={onNavigate} setIsModalOpen={setIsModalOpen} setActiveTab={setActiveTab} selectedChatUserId={selectedChatUserId} setSelectedChatUserId={setSelectedChatUserId} />;
23
  case 'jobs':
24
  return <AdminSortingPage />;
25
  case 'messages':
26
+ return <AdminInterviewManagement selectedChatUserId={selectedChatUserId} setSelectedChatUserId={setSelectedChatUserId} />;
27
  case 'job-management':
28
  return <JobPosting />;
29
  case 'clusters':
src/pages/AppliLogin.jsx CHANGED
@@ -68,11 +68,11 @@ export default function AppliLogin({ onNavigate }) {
68
  options: { data: { role: 'applicant' } }
69
  });
70
  if (error) throw error;
71
- setNotification({ type: 'success', message: 'Registration successful! Please login.' });
72
  setMode('login');
73
  } else if (mode === 'forgot') {
74
  const { error } = await supabase.auth.resetPasswordForEmail(email, {
75
- redirectTo: window.location.origin,
76
  });
77
  if (error) throw error;
78
  setNotification({ type: 'success', message: 'Password reset link sent! Check your email.' });
 
68
  options: { data: { role: 'applicant' } }
69
  });
70
  if (error) throw error;
71
+ setNotification({ type: 'success', message: 'Registration successful! Please confirm your Email and login.' });
72
  setMode('login');
73
  } else if (mode === 'forgot') {
74
  const { error } = await supabase.auth.resetPasswordForEmail(email, {
75
+ redirectTo: `${window.location.origin}/reset-password`,
76
  });
77
  if (error) throw error;
78
  setNotification({ type: 'success', message: 'Password reset link sent! Check your email.' });
src/pages/ApplicantJobPage.jsx CHANGED
@@ -33,6 +33,7 @@ export default function ApplicantJobPage({ onNavigate }) {
33
  const [searchQuery, setSearchQuery] = useState('');
34
  const [activeTab, setActiveTab] = useState('all');
35
  const [loading, setLoading] = useState(true);
 
36
 
37
  useEffect(() => {
38
  const fetchData = async () => {
@@ -156,8 +157,14 @@ export default function ApplicantJobPage({ onNavigate }) {
156
  job.company.toLowerCase().includes(lowerQuery)
157
  );
158
  }
 
 
 
 
 
 
159
  setFilteredJobs(result);
160
- }, [searchQuery, activeTab, allJobs, userProfile]);
161
 
162
  return (
163
  <ApplicantLayout activePage="applicant-jobs" onNavigate={onNavigate}>
@@ -167,8 +174,8 @@ export default function ApplicantJobPage({ onNavigate }) {
167
  {activeTab === 'recommended' ? 'Jobs For You' : 'All Opportunities'}
168
  </h2>
169
  <div style={{ backgroundColor: 'rgba(255,255,255,0.1)', padding: '0.25rem', borderRadius: '0.5rem', display: 'flex', gap: '0.5rem' }}>
170
- <button onClick={() => setActiveTab('all')} style={{ padding: '0.5rem 1rem', borderRadius: '0.3rem', border: 'none', cursor: 'pointer', fontWeight: 'bold', backgroundColor: activeTab === 'all' ? '#FBBF24' : 'transparent', color: activeTab === 'all' ? '#1a202c' : '#9ca3af' }}>All Jobs</button>
171
- <button onClick={() => setActiveTab('recommended')} style={{ padding: '0.5rem 1rem', borderRadius: '0.3rem', border: 'none', cursor: 'pointer', fontWeight: 'bold', backgroundColor: activeTab === 'recommended' ? '#FBBF24' : 'transparent', color: activeTab === 'recommended' ? '#1a202c' : '#9ca3af' }}>Recommended ✨</button>
172
  </div>
173
  </div>
174
 
@@ -194,7 +201,31 @@ export default function ApplicantJobPage({ onNavigate }) {
194
  <button onClick={() => onNavigate('applicant-profile')} style={{ marginTop: '1rem', padding: '0.5rem 1rem', backgroundColor: '#374151', color: 'white', border: 'none', borderRadius: '0.5rem', cursor: 'pointer' }}>Update Profile</button>
195
  </div>
196
  ) : (
197
- <JobListings searchQuery={searchQuery} setSearchQuery={setSearchQuery} isSearching={searchQuery.length > 0} filteredJobListings={filteredJobs} />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  )}
199
  </>
200
  )}
 
33
  const [searchQuery, setSearchQuery] = useState('');
34
  const [activeTab, setActiveTab] = useState('all');
35
  const [loading, setLoading] = useState(true);
36
+ const [showAllJobs, setShowAllJobs] = useState(false); // ✅ Track if showing all jobs
37
 
38
  useEffect(() => {
39
  const fetchData = async () => {
 
157
  job.company.toLowerCase().includes(lowerQuery)
158
  );
159
  }
160
+
161
+ // ✅ Limit to 4 jobs unless "See All" is clicked
162
+ if (!showAllJobs && !searchQuery) {
163
+ result = result.slice(0, 4);
164
+ }
165
+
166
  setFilteredJobs(result);
167
+ }, [searchQuery, activeTab, allJobs, userProfile, showAllJobs]);
168
 
169
  return (
170
  <ApplicantLayout activePage="applicant-jobs" onNavigate={onNavigate}>
 
174
  {activeTab === 'recommended' ? 'Jobs For You' : 'All Opportunities'}
175
  </h2>
176
  <div style={{ backgroundColor: 'rgba(255,255,255,0.1)', padding: '0.25rem', borderRadius: '0.5rem', display: 'flex', gap: '0.5rem' }}>
177
+ <button onClick={() => { setActiveTab('all'); setShowAllJobs(false); }} style={{ padding: '0.5rem 1rem', borderRadius: '0.3rem', border: 'none', cursor: 'pointer', fontWeight: 'bold', backgroundColor: activeTab === 'all' ? '#FBBF24' : 'transparent', color: activeTab === 'all' ? '#1a202c' : '#9ca3af' }}>All Jobs</button>
178
+ <button onClick={() => { setActiveTab('recommended'); setShowAllJobs(false); }} style={{ padding: '0.5rem 1rem', borderRadius: '0.3rem', border: 'none', cursor: 'pointer', fontWeight: 'bold', backgroundColor: activeTab === 'recommended' ? '#FBBF24' : 'transparent', color: activeTab === 'recommended' ? '#1a202c' : '#9ca3af' }}>Recommended ✨</button>
179
  </div>
180
  </div>
181
 
 
201
  <button onClick={() => onNavigate('applicant-profile')} style={{ marginTop: '1rem', padding: '0.5rem 1rem', backgroundColor: '#374151', color: 'white', border: 'none', borderRadius: '0.5rem', cursor: 'pointer' }}>Update Profile</button>
202
  </div>
203
  ) : (
204
+ <>
205
+ <JobListings searchQuery={searchQuery} setSearchQuery={setSearchQuery} isSearching={searchQuery.length > 0} filteredJobListings={filteredJobs} />
206
+
207
+ {/* ✅ See All Button */}
208
+ {!showAllJobs && !searchQuery && (
209
+ <div style={{ display: 'flex', justifyContent: 'center', marginTop: '2rem' }}>
210
+ <button
211
+ onClick={() => setShowAllJobs(true)}
212
+ style={{
213
+ padding: '0.75rem 2rem',
214
+ backgroundColor: '#FBBF24',
215
+ color: '#1a202c',
216
+ border: 'none',
217
+ borderRadius: '0.5rem',
218
+ cursor: 'pointer',
219
+ fontWeight: 'bold',
220
+ fontSize: '1rem',
221
+ transition: 'all 0.2s'
222
+ }}
223
+ >
224
+ See All Jobs
225
+ </button>
226
+ </div>
227
+ )}
228
+ </>
229
  )}
230
  </>
231
  )}
src/pages/ApplicantMessages.jsx CHANGED
@@ -1,15 +1,30 @@
1
- import React, { useState, useEffect } from 'react';
2
  import { motion, AnimatePresence } from 'framer-motion';
3
  import { supabase } from '../supabaseClient';
4
  import ApplicantLayout from '../components/ApplicantLayout';
5
  import PlaceholderContent from '../components/PlaceholderContent';
6
  import { ChatIcon, SearchIcon } from '../components/Icons';
7
 
8
- const SendIcon = () => <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>;
9
- const Avatar = ({ name }) => <div style={{ width: 45, height: 45, borderRadius: '50%', background: 'linear-gradient(135deg, #374151, #1f2937)', border: '2px solid rgba(255,255,255,0.1)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 'bold', color: 'white', flexShrink: 0 }}>{name ? name[0].toUpperCase() : 'U'}</div>;
 
 
 
 
10
 
11
- // Shared Styles to save lines
12
- const glassStyle = { background: 'rgba(15, 23, 42, 0.6)', backdropFilter: 'blur(16px)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: '1.25rem', display: 'flex', flexDirection: 'column', overflow: 'hidden' };
 
 
 
 
 
 
 
 
 
 
 
13
  const inputStyle = { width: '100%', padding: '0.8rem', background: 'transparent', border: 'none', color: 'white', outline: 'none', fontSize: '0.95rem' };
14
 
15
  export default function ApplicantMessages({ onNavigate }) {
@@ -17,31 +32,183 @@ export default function ApplicantMessages({ onNavigate }) {
17
  const [selected, setSelected] = useState(null);
18
  const [text, setText] = useState('');
19
  const [search, setSearch] = useState('');
 
 
 
20
 
 
21
  useEffect(() => {
22
- supabase.auth.getUser().then(({ data: { user } }) => {
23
- // Precise Mock Data (Replace with real DB fetch later)
24
- setThreads([{
25
- id: '1', name: 'HR Team - Iris', subj: 'Application Update: Software Engineer',
26
- last: 'Please let us know your availability.', unread: true, time: new Date().toISOString(),
27
- msgs: [
28
- { id: 1, text: 'Hi, your profile looks impressive!', time: '2 days ago', isMe: false },
29
- { id: 2, text: 'Thank you! Excited for the opportunity.', time: '1 day ago', isMe: true },
30
- { id: 3, text: 'Please let us know your availability for an interview.', time: '1 hour ago', isMe: false }
31
- ]
32
- }]);
33
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  }, []);
35
 
36
- const sendMsg = () => {
37
- if (!text.trim() || !selected) return;
38
- const upd = { ...selected, msgs: [...selected.msgs, { id: Date.now(), text, time: 'Just now', isMe: true }], last: text };
39
- setSelected(upd);
40
- setThreads(threads.map(t => t.id === upd.id ? upd : t));
 
 
 
 
 
 
 
 
 
 
 
 
41
  setText('');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  };
43
 
44
- const filtered = threads.filter(t => t.name.toLowerCase().includes(search.toLowerCase()) || t.subj.toLowerCase().includes(search.toLowerCase()));
45
 
46
  return (
47
  <ApplicantLayout activePage="applicant-messages" onNavigate={onNavigate}>
@@ -49,7 +216,7 @@ export default function ApplicantMessages({ onNavigate }) {
49
 
50
  <div style={{ display: 'flex', height: 'calc(100vh - 140px)', gap: '1.5rem', paddingBottom: '1rem' }}>
51
 
52
- {/* LEFT PANE: THREAD LIST */}
53
  <div style={{ ...glassStyle, width: '350px', flexShrink: 0 }}>
54
  <div style={{ padding: '1.5rem' }}>
55
  <h2 style={{ fontSize: '1.4rem', fontWeight: 'bold', marginBottom: '1rem' }}>Inbox</h2>
@@ -60,51 +227,105 @@ export default function ApplicantMessages({ onNavigate }) {
60
  </div>
61
 
62
  <div className="hide-scroll" style={{ flex: 1, overflowY: 'auto', padding: '0 0.75rem 1rem' }}>
63
- {filtered.map(t => {
64
- const isAct = selected?.id === t.id;
65
- return (
66
- <motion.div
67
- key={t.id} onClick={() => { setSelected(t); setThreads(threads.map(x => x.id === t.id ? { ...x, unread: false } : x)); }}
68
- whileHover={{ scale: 0.98 }} style={{ padding: '1.25rem', marginBottom: '0.5rem', borderRadius: '1rem', cursor: 'pointer', background: isAct ? 'rgba(251, 191, 36, 0.08)' : 'transparent', border: isAct ? '1px solid rgba(251,191,36,0.3)' : '1px solid transparent' }}
69
- >
70
- <div style={{ display: 'flex', justifyContent: 'space-between', color: isAct ? '#FCD34D' : 'white', fontWeight: 'bold' }}>
71
- {t.name} <span style={{ fontSize: '0.75rem', color: t.unread ? '#FCD34D' : '#64748b' }}>{new Date(t.time).toLocaleDateString([],{month:'short', day:'numeric'})}</span>
72
- </div>
73
- <div style={{ fontSize: '0.85rem', color: '#e2e8f0', margin: '0.2rem 0', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{t.subj}</div>
74
- <div style={{ fontSize: '0.8rem', color: '#94a3b8', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{t.last}</div>
75
- </motion.div>
76
- );
77
- })}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  </div>
79
  </div>
80
 
81
- {/* RIGHT PANE: ACTIVE CHAT */}
82
  <div style={{ ...glassStyle, flex: 1 }}>
83
  {selected ? (
84
  <>
85
- {/* Chat Header */}
86
  <div style={{ padding: '1.25rem 2rem', background: 'rgba(0,0,0,0.2)', borderBottom: '1px solid rgba(255,255,255,0.08)', display: 'flex', alignItems: 'center', gap: '1rem' }}>
87
- <Avatar name={selected.name} />
88
- <div><h3 style={{ margin: 0, color: 'white' }}>{selected.name}</h3><p style={{ margin: 0, fontSize: '0.85rem', color: '#10b981' }}>Active Now</p></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  </div>
90
 
91
- {/* Chat History */}
92
  <div className="hide-scroll" style={{ flex: 1, padding: '2rem', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
93
  {selected.msgs.map(m => (
94
  <motion.div key={m.id} initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} style={{ alignSelf: m.isMe ? 'flex-end' : 'flex-start', maxWidth: '70%' }}>
95
- <div style={{ background: m.isMe ? 'linear-gradient(135deg, #FBBF24, #F59E0B)' : 'rgba(255,255,255,0.06)', color: m.isMe ? '#020617' : 'white', padding: '1rem 1.25rem', borderRadius: '1.25rem', borderBottomRightRadius: m.isMe ? 4 : '1.25rem', borderBottomLeftRadius: m.isMe ? '1.25rem' : 4 }}>
 
 
 
 
 
96
  {m.text}
97
  </div>
98
  <div style={{ fontSize: '0.7rem', color: '#64748b', marginTop: 4, textAlign: m.isMe ? 'right' : 'left' }}>{m.time}</div>
99
  </motion.div>
100
  ))}
 
101
  </div>
102
 
103
- {/* Input Area */}
104
  <div style={{ padding: '1.5rem', background: 'rgba(0,0,0,0.3)', borderTop: '1px solid rgba(255,255,255,0.05)' }}>
105
  <div style={{ display: 'flex', gap: '1rem', background: 'rgba(255,255,255,0.03)', borderRadius: '1rem', padding: '0.5rem', border: '1px solid rgba(255,255,255,0.1)' }}>
106
- <textarea value={text} onChange={e => setText(e.target.value)} onKeyDown={e => { if(e.key === 'Enter' && !e.shiftKey){ e.preventDefault(); sendMsg(); }}} placeholder="Write a message..." style={{ ...inputStyle, resize: 'none', height: '45px' }} />
107
- <button onClick={sendMsg} disabled={!text.trim()} style={{ background: text.trim() ? '#FBBF24' : 'rgba(255,255,255,0.05)', color: text.trim() ? '#020617' : '#64748b', border: 'none', borderRadius: '50%', width: 45, height: 45, cursor: text.trim() ? 'pointer' : 'not-allowed' }}>
 
 
 
 
 
 
108
  <SendIcon />
109
  </button>
110
  </div>
@@ -112,7 +333,7 @@ export default function ApplicantMessages({ onNavigate }) {
112
  </>
113
  ) : (
114
  <div style={{ display: 'flex', flex: 1, alignItems: 'center', justifyContent: 'center' }}>
115
- <PlaceholderContent title="Your Messages" message="Select a conversation to start chatting." icon={<ChatIcon />} />
116
  </div>
117
  )}
118
  </div>
 
1
+ import React, { useState, useEffect, useRef } from 'react';
2
  import { motion, AnimatePresence } from 'framer-motion';
3
  import { supabase } from '../supabaseClient';
4
  import ApplicantLayout from '../components/ApplicantLayout';
5
  import PlaceholderContent from '../components/PlaceholderContent';
6
  import { ChatIcon, SearchIcon } from '../components/Icons';
7
 
8
+ // --- Inline UI Components for Chat ---
9
+ const SendIcon = () => (
10
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
11
+ <line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>
12
+ </svg>
13
+ );
14
 
15
+ const Avatar = ({ name }) => (
16
+ <div style={{
17
+ width: 45, height: 45, borderRadius: '50%',
18
+ background: 'linear-gradient(135deg, #374151, #1f2937)',
19
+ border: '2px solid rgba(255,255,255,0.1)',
20
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
21
+ fontWeight: 'bold', color: 'white', flexShrink: 0
22
+ }}>
23
+ {name ? name[0].toUpperCase() : 'U'}
24
+ </div>
25
+ );
26
+
27
+ const glassStyle = { background: 'rgba(15, 23, 42, 0.6)', backdropFilter: 'blur(16px)', border: '1px solid rgba(255, 255, 255, 0.08)', borderRadius: '1.25rem', display: 'flex', flexDirection: 'column', overflow: 'hidden' };
28
  const inputStyle = { width: '100%', padding: '0.8rem', background: 'transparent', border: 'none', color: 'white', outline: 'none', fontSize: '0.95rem' };
29
 
30
  export default function ApplicantMessages({ onNavigate }) {
 
32
  const [selected, setSelected] = useState(null);
33
  const [text, setText] = useState('');
34
  const [search, setSearch] = useState('');
35
+ const [userId, setUserId] = useState(null);
36
+ const [loading, setLoading] = useState(true);
37
+ const scrollRef = useRef(null);
38
 
39
+ // Auto-scroll to bottom of conversation
40
  useEffect(() => {
41
+ scrollRef.current?.scrollIntoView({ behavior: 'smooth' });
42
+ }, [selected?.msgs]);
43
+
44
+ // FETCH MESSAGES AND NAMES
45
+ const fetchMsgs = async (uid) => {
46
+ try {
47
+ console.log("Fetching messages for user:", uid);
48
+ const { data: messages, error } = await supabase
49
+ .from('messages')
50
+ .select('*')
51
+ .or(`receiver_id.eq.${uid},sender_id.eq.${uid}`)
52
+ .order('created_at', { ascending: true });
53
+
54
+ if (error) throw error;
55
+
56
+ const threadMap = {};
57
+ messages.forEach(m => {
58
+ const isMe = m.sender_id === uid;
59
+ const otherId = isMe ? m.receiver_id : m.sender_id;
60
+
61
+ if (!threadMap[otherId]) {
62
+ threadMap[otherId] = {
63
+ id: otherId,
64
+ name: 'Admin / HR',
65
+ subj: 'Application Update',
66
+ last: '',
67
+ unread: false,
68
+ time: m.created_at,
69
+ msgs: [],
70
+ companyName: '',
71
+ companyLogo: '',
72
+ companyId: null
73
+ };
74
+ }
75
+
76
+ threadMap[otherId].msgs.push({
77
+ id: m.id,
78
+ text: m.content,
79
+ time: new Date(m.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
80
+ isMe
81
+ });
82
+ threadMap[otherId].last = m.content;
83
+ threadMap[otherId].time = m.created_at;
84
+ if (!isMe && !m.is_read) threadMap[otherId].unread = true;
85
+ });
86
+
87
+ const threadList = Object.values(threadMap).sort((a, b) => new Date(b.time) - new Date(a.time));
88
+
89
+ // ✅ Fetch Admin Names and Company Info from 'user_roles' table
90
+ const otherUserIds = threadList.map(t => t.id);
91
+ if (otherUserIds.length > 0) {
92
+ const { data: rolesData } = await supabase
93
+ .from('user_roles')
94
+ .select('user_id, name, company_id')
95
+ .in('user_id', otherUserIds);
96
+
97
+ console.log("Roles Data:", rolesData);
98
+
99
+ if (rolesData && rolesData.length > 0) {
100
+ rolesData.forEach(role => {
101
+ const thread = threadList.find(t => t.id === role.user_id);
102
+ if (thread && role.name) thread.name = role.name;
103
+ if (thread) thread.companyId = role.company_id;
104
+ });
105
+
106
+ // ✅ Fetch Company Details including logo from storage
107
+ const companyIds = [...new Set(rolesData.map(r => r.company_id).filter(Boolean))];
108
+ console.log("Company IDs to fetch:", companyIds);
109
+
110
+ if (companyIds.length > 0) {
111
+ const { data: companiesData } = await supabase
112
+ .from('companies')
113
+ .select('id, name, logo_url')
114
+ .in('id', companyIds);
115
+
116
+ console.log("Companies Data:", companiesData);
117
+
118
+ if (companiesData) {
119
+ companiesData.forEach(company => {
120
+ threadList.forEach(thread => {
121
+ if (thread.companyId === company.id) {
122
+ thread.companyName = company.name;
123
+ thread.companyLogo = company.logo_url;
124
+ console.log(`Set company for thread ${thread.id}:`, company.name, company.logo_url);
125
+ }
126
+ });
127
+ });
128
+ }
129
+ }
130
+ } else {
131
+ console.log("No roles data found or rolesData is empty");
132
+ }
133
+ }
134
+
135
+ console.log("Final Thread List:", threadList);
136
+ setThreads(threadList);
137
+
138
+ // Refresh currently open chat window if data changed
139
+ if (selected) {
140
+ const updated = threadList.find(t => t.id === selected.id);
141
+ if (updated) setSelected(updated);
142
+ }
143
+ } catch (err) {
144
+ console.error("Message Fetch Error:", err);
145
+ } finally {
146
+ setLoading(false);
147
+ }
148
+ };
149
+
150
+ // ✅ REAL-TIME LISTENER
151
+ useEffect(() => {
152
+ let channel;
153
+ const init = async () => {
154
+ const { data: { user } } = await supabase.auth.getUser();
155
+ if (!user) return;
156
+ setUserId(user.id);
157
+ await fetchMsgs(user.id);
158
+
159
+ // Create a channel to listen for any new messages where THIS user is the receiver
160
+ channel = supabase.channel('applicant_inbox')
161
+ .on('postgres_changes', {
162
+ event: 'INSERT',
163
+ schema: 'public',
164
+ table: 'messages',
165
+ filter: `receiver_id=eq.${user.id}`
166
+ }, () => {
167
+ console.log("New message received from Admin!");
168
+ fetchMsgs(user.id);
169
+ })
170
+ .subscribe();
171
+ };
172
+
173
+ init();
174
+ return () => { if (channel) supabase.removeChannel(channel); };
175
  }, []);
176
 
177
+ const markAsRead = async (t) => {
178
+ setSelected(t);
179
+ if (t.unread && userId) {
180
+ // Optimistic UI Update
181
+ setThreads(threads.map(x => x.id === t.id ? { ...x, unread: false } : x));
182
+ // Database Update
183
+ await supabase.from('messages')
184
+ .update({ is_read: true })
185
+ .eq('receiver_id', userId)
186
+ .eq('sender_id', t.id)
187
+ .eq('is_read', false);
188
+ }
189
+ };
190
+
191
+ const sendMsg = async () => {
192
+ if (!text.trim() || !selected || !userId) return;
193
+ const messageText = text.trim();
194
  setText('');
195
+
196
+ // Send to Supabase
197
+ const { error } = await supabase.from('messages').insert([{
198
+ sender_id: userId,
199
+ receiver_id: selected.id,
200
+ content: messageText,
201
+ is_read: false
202
+ }]);
203
+
204
+ if (error) {
205
+ console.error("Send Error:", error);
206
+ } else {
207
+ fetchMsgs(userId); // Refresh to show my sent message
208
+ }
209
  };
210
 
211
+ const filtered = threads.filter(t => t.name.toLowerCase().includes(search.toLowerCase()));
212
 
213
  return (
214
  <ApplicantLayout activePage="applicant-messages" onNavigate={onNavigate}>
 
216
 
217
  <div style={{ display: 'flex', height: 'calc(100vh - 140px)', gap: '1.5rem', paddingBottom: '1rem' }}>
218
 
219
+ {/* --- CONVERSATIONS LIST --- */}
220
  <div style={{ ...glassStyle, width: '350px', flexShrink: 0 }}>
221
  <div style={{ padding: '1.5rem' }}>
222
  <h2 style={{ fontSize: '1.4rem', fontWeight: 'bold', marginBottom: '1rem' }}>Inbox</h2>
 
227
  </div>
228
 
229
  <div className="hide-scroll" style={{ flex: 1, overflowY: 'auto', padding: '0 0.75rem 1rem' }}>
230
+ {loading ? (
231
+ <div style={{ padding: '2rem', textAlign: 'center', color: '#64748b' }}>Loading...</div>
232
+ ) : filtered.length === 0 ? (
233
+ <div style={{ padding: '2rem', textAlign: 'center', color: '#64748b' }}>No messages found.</div>
234
+ ) : (
235
+ filtered.map(t => {
236
+ const isAct = selected?.id === t.id;
237
+ return (
238
+ <motion.div
239
+ key={t.id} onClick={() => markAsRead(t)}
240
+ whileHover={{ scale: 0.98 }}
241
+ style={{
242
+ padding: '1.25rem', marginBottom: '0.5rem', borderRadius: '1rem',
243
+ cursor: 'pointer', position: 'relative',
244
+ background: isAct ? 'rgba(251, 191, 36, 0.08)' : 'transparent',
245
+ border: isAct ? '1px solid rgba(251,191,36,0.3)' : '1px solid transparent',
246
+ display: 'flex', gap: '1rem'
247
+ }}
248
+ >
249
+ {t.unread && !isAct && <div style={{ position: 'absolute', top: '1.5rem', right: '1.25rem', width: '10px', height: '10px', backgroundColor: '#FBBF24', borderRadius: '50%' }}></div>}
250
+
251
+ {/* Company Logo */}
252
+ {t.companyLogo ? (
253
+ <img
254
+ src={t.companyLogo}
255
+ alt={t.companyName}
256
+ style={{ width: 48, height: 48, borderRadius: '0.5rem', objectFit: 'cover', flexShrink: 0, border: '1px solid rgba(255,255,255,0.2)' }}
257
+ />
258
+ ) : (
259
+ <Avatar name={t.companyName || t.name} />
260
+ )}
261
+
262
+ {/* Message Info */}
263
+ <div style={{ flex: 1, minWidth: 0 }}>
264
+ <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.25rem' }}>
265
+ <span style={{ fontWeight: 'bold', color: isAct ? '#FCD34D' : 'white', fontSize: '0.95rem' }}>{t.companyName || t.name}</span>
266
+ <span style={{ background: 'rgba(34, 197, 94, 0.2)', color: '#22c55e', padding: '0.15rem 0.5rem', borderRadius: '0.25rem', fontSize: '0.65rem', fontWeight: '600', flexShrink: 0 }}>HR</span>
267
+ <span style={{ fontSize: '0.7rem', color: t.unread ? '#FCD34D' : '#64748b', marginLeft: 'auto', flexShrink: 0 }}>{new Date(t.time).toLocaleDateString([],{month:'short', day:'numeric'})}</span>
268
+ </div>
269
+ <div style={{ fontSize: '0.8rem', color: '#e2e8f0', margin: '0.2rem 0', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{t.subj}</div>
270
+ <div style={{ fontSize: '0.75rem', color: '#94a3b8', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{t.last}</div>
271
+ </div>
272
+ </motion.div>
273
+ );
274
+ })
275
+ )}
276
  </div>
277
  </div>
278
 
279
+ {/* --- ACTIVE CONVERSATION --- */}
280
  <div style={{ ...glassStyle, flex: 1 }}>
281
  {selected ? (
282
  <>
 
283
  <div style={{ padding: '1.25rem 2rem', background: 'rgba(0,0,0,0.2)', borderBottom: '1px solid rgba(255,255,255,0.08)', display: 'flex', alignItems: 'center', gap: '1rem' }}>
284
+ {selected.companyLogo ? (
285
+ <img
286
+ src={selected.companyLogo}
287
+ alt={selected.companyName}
288
+ style={{ width: 45, height: 45, borderRadius: '0.5rem', objectFit: 'cover', border: '2px solid rgba(255,255,255,0.2)', flexShrink: 0 }}
289
+ />
290
+ ) : (
291
+ <Avatar name={selected.companyName || selected.name} />
292
+ )}
293
+ <div style={{ flex: 1 }}>
294
+ <div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
295
+ <h3 style={{ margin: 0, color: 'white' }}>{selected.companyName || selected.name}</h3>
296
+ <span style={{ background: 'rgba(34, 197, 94, 0.2)', color: '#22c55e', padding: '0.25rem 0.75rem', borderRadius: '0.5rem', fontSize: '0.75rem', fontWeight: '600' }}>HR</span>
297
+ </div>
298
+ <p style={{ margin: 0, fontSize: '0.85rem', color: '#10b981' }}>Active Now</p>
299
+ </div>
300
  </div>
301
 
 
302
  <div className="hide-scroll" style={{ flex: 1, padding: '2rem', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
303
  {selected.msgs.map(m => (
304
  <motion.div key={m.id} initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} style={{ alignSelf: m.isMe ? 'flex-end' : 'flex-start', maxWidth: '70%' }}>
305
+ <div style={{
306
+ background: m.isMe ? 'linear-gradient(135deg, #FBBF24, #F59E0B)' : 'rgba(255,255,255,0.06)',
307
+ color: m.isMe ? '#020617' : 'white',
308
+ padding: '1rem 1.25rem', borderRadius: '1.25rem',
309
+ borderBottomRightRadius: m.isMe ? 4 : '1.25rem', borderBottomLeftRadius: m.isMe ? '1.25rem' : 4
310
+ }}>
311
  {m.text}
312
  </div>
313
  <div style={{ fontSize: '0.7rem', color: '#64748b', marginTop: 4, textAlign: m.isMe ? 'right' : 'left' }}>{m.time}</div>
314
  </motion.div>
315
  ))}
316
+ <div ref={scrollRef} />
317
  </div>
318
 
 
319
  <div style={{ padding: '1.5rem', background: 'rgba(0,0,0,0.3)', borderTop: '1px solid rgba(255,255,255,0.05)' }}>
320
  <div style={{ display: 'flex', gap: '1rem', background: 'rgba(255,255,255,0.03)', borderRadius: '1rem', padding: '0.5rem', border: '1px solid rgba(255,255,255,0.1)' }}>
321
+ <textarea
322
+ value={text}
323
+ onChange={e => setText(e.target.value)}
324
+ onKeyDown={e => { if(e.key === 'Enter' && !e.shiftKey){ e.preventDefault(); sendMsg(); }}}
325
+ placeholder="Type your reply..."
326
+ style={{ ...inputStyle, resize: 'none', height: '45px', fontFamily: 'inherit' }}
327
+ />
328
+ <button onClick={sendMsg} disabled={!text.trim()} style={{ background: text.trim() ? '#FBBF24' : 'rgba(255,255,255,0.05)', color: text.trim() ? '#020617' : '#64748b', border: 'none', borderRadius: '50%', width: 45, height: 45, cursor: text.trim() ? 'pointer' : 'not-allowed', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
329
  <SendIcon />
330
  </button>
331
  </div>
 
333
  </>
334
  ) : (
335
  <div style={{ display: 'flex', flex: 1, alignItems: 'center', justifyContent: 'center' }}>
336
+ <PlaceholderContent title="Your Messages" message="Select an Admin to start chatting." icon={<ChatIcon />} />
337
  </div>
338
  )}
339
  </div>
src/pages/ResetPassword.jsx ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React, { useState } from "react";
3
+ import { supabase } from "../supabaseClient";
4
+
5
+ export default function ResetPassword() {
6
+
7
+ const [password, setPassword] = useState("");
8
+ const [loading, setLoading] = useState(false);
9
+
10
+ const handleUpdate = async (e) => {
11
+ e.preventDefault();
12
+ setLoading(true);
13
+
14
+ const { error } = await supabase.auth.updateUser({
15
+ password: password
16
+ });
17
+
18
+ if (error) {
19
+ alert(error.message);
20
+ } else {
21
+ alert("Password updated successfully!");
22
+ window.location.href = "/";
23
+ }
24
+
25
+ setLoading(false);
26
+ };
27
+
28
+ return (
29
+ <div style={{padding:"40px", color:"white"}}>
30
+
31
+ <h2>Reset Your Password</h2>
32
+
33
+ <form onSubmit={handleUpdate}>
34
+ <input
35
+ type="password"
36
+ placeholder="Enter new password"
37
+ value={password}
38
+ onChange={(e)=>setPassword(e.target.value)}
39
+ style={{padding:"10px", margin:"10px"}}
40
+ />
41
+
42
+ <button type="submit">
43
+ {loading ? "Updating..." : "Update Password"}
44
+ </button>
45
+
46
+ </form>
47
+
48
+ </div>
49
+ );
50
+ }
51
+