import React, { useState, useEffect, useRef } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { supabase } from '../supabaseClient'; import { LogoutIcon, BriefcaseIcon, UserCircleIcon, ChatIcon, CalendarIcon, AtsCheckerIcon } from './Icons'; const BellIcon = () => ( ); export default function ApplicantLayout({ children, activePage, onNavigate }) { const [userName, setUserName] = useState(() => localStorage.getItem('applicant_name') || ''); const [notifications, setNotifications] = useState([]); const [showNotifications, setShowNotifications] = useState(false); const [hasSeenNotifs, setHasSeenNotifs] = useState(false); const notifRef = useRef(null); const [unreadMessages, setUnreadMessages] = useState(0); const [unreadMessagesList, setUnreadMessagesList] = useState([]); // ⭐ NEW: recent jobs notifications const [newJobs, setNewJobs] = useState([]); // ⭐ NEW: toast notification state const [toastNotif, setToastNotif] = useState(null); const triggerToast = (title, message, icon) => { setToastNotif({ id: Date.now(), title, message, icon }); setTimeout(() => setToastNotif(null), 5000); // Hide after 5 seconds }; useEffect(() => { function handleClickOutside(event) { if (notifRef.current && !notifRef.current.contains(event.target)) { setShowNotifications(false); } } document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [notifRef]); useEffect(() => { const fetchUserName = async () => { try { const { data: { user } } = await supabase.auth.getUser(); if (user) { const { data: profile } = await supabase .from('profiles') .select('full_name') .eq('id', user.id) .maybeSingle(); if (profile && profile.full_name) { const firstName = profile.full_name.split(' ')[0]; setUserName(firstName); localStorage.setItem('applicant_name', firstName); } } } catch (error) { console.error("Error fetching name:", error); } }; fetchUserName(); }, []); // ⭐ NEW: fetch unread messages count and details useEffect(() => { const fetchUnreadMessages = async () => { const { data: { user } } = await supabase.auth.getUser(); if (!user) return; const { data: messages } = await supabase .from("messages") .select("id, sender_id, content, created_at") .eq("receiver_id", user.id) .eq("is_read", false) .order("created_at", { ascending: false }); if (messages && messages.length > 0) { setUnreadMessages(messages.length); // Fetch sender names from user_roles/companies const senderIds = [...new Set(messages.map(m => m.sender_id))]; const { data: rolesData } = await supabase .from("user_roles") .select("user_id, name, company_id") .in("user_id", senderIds); let companiesData = []; if (rolesData) { const companyIds = [...new Set(rolesData.map(r => r.company_id).filter(Boolean))]; if (companyIds.length > 0) { const { data: comp } = await supabase.from("companies").select("id, name").in("id", companyIds); if (comp) companiesData = comp; } } const formattedMessages = messages.map(msg => { let senderName = 'Admin / HR'; const role = rolesData?.find(r => r.user_id === msg.sender_id); if (role) { senderName = role.name || 'HR'; if (role.company_id) { const comp = companiesData.find(c => c.id === role.company_id); if (comp) senderName = comp.name; } } return { id: msg.id, senderName, content: msg.content, timestamp: new Date(msg.created_at).toLocaleDateString() + ' • ' + new Date(msg.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }; }); setUnreadMessagesList(formattedMessages); } }; fetchUnreadMessages(); const fetchRecentJobs = async () => { const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); const { data: recentJobs } = await supabase .from('jobs') .select('id, title, company_id, created_at, companies ( name )') .eq('status', 'Active') .gte('created_at', oneDayAgo) .order('created_at', { ascending: false }) .limit(3); if (recentJobs) { const jobNotifs = recentJobs.map(job => ({ id: `job-${job.id}`, title: 'New Job Posted', text: `${job.companies?.name || 'A company'} is hiring: ${job.title}`, time: new Date(job.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), color: '#8b5cf6' // Purple color for jobs })); setNewJobs(jobNotifs); } }; fetchRecentJobs(); }, []); // ⭐ NEW: realtime update for new messages useEffect(() => { let channel, jobsChannel, appsChannel; const initRealtime = async () => { const { data: { user } } = await supabase.auth.getUser(); if (!user) return; channel = supabase .channel("messages-badge") .on( "postgres_changes", { event: "INSERT", schema: "public", table: "messages", filter: `receiver_id=eq.${user.id}` }, async (payload) => { console.log("=== MESSAGES PAYLOAD ===", payload); // Ignore applicant's own manual replies to the system thread if (payload.new.sender_id === user.id) { if (!payload.new.content || !payload.new.content.startsWith("Hello, Thank you for applying")) { return; } } // Fetch the sender's name from user_roles/companies let senderName = 'Admin / HR'; const { data: roleData } = await supabase .from("user_roles") .select("name, company_id") .eq("user_id", payload.new.sender_id) .maybeSingle(); if (roleData) { senderName = roleData.name || 'HR'; if (roleData.company_id) { const { data: company } = await supabase.from('companies').select('name').eq('id', roleData.company_id).maybeSingle(); if (company) senderName = company.name; } } const newMsg = { id: payload.new.id, senderName, content: payload.new.content, timestamp: new Date(payload.new.created_at).toLocaleDateString() + ' • ' + new Date(payload.new.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }; setUnreadMessagesList(prev => [newMsg, ...prev]); setUnreadMessages(prev => prev + 1); // Trigger realtime toast popup triggerToast( 'New Message Received', `${senderName} sent you a message`, '📨' ); } ) .subscribe(); const jobsChannel = supabase .channel("jobs-badge") .on( "postgres_changes", { event: "INSERT", schema: "public", table: "jobs", filter: "status=eq.Active" }, async (payload) => { console.log("=== JOBS PAYLOAD ===", payload); let companyName = 'A company'; if (payload.new.company_id) { const { data: comp } = await supabase .from("companies") .select("name") .eq("id", payload.new.company_id) .maybeSingle(); if (comp) companyName = comp.name; } const newJobNotif = { id: `job-${payload.new.id}`, title: 'New Job Posted', text: `${companyName} is hiring: ${payload.new.title}`, time: new Date(payload.new.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), color: '#8b5cf6' }; setNewJobs(prev => [newJobNotif, ...prev]); // Trigger realtime toast popup triggerToast( 'New Job Posted', `${companyName} is hiring: ${payload.new.title}`, '💼' ); } ) .subscribe(); const appsChannel = supabase .channel("interviews-badge-applicant") // unique channel name .on( "postgres_changes", { event: "*", schema: "public", table: "interviews" }, async (payload) => { console.log("=== INTERVIEWS PAYLOAD ===", payload); const { data: { user } } = await supabase.auth.getUser(); if (!user || !payload.new) return; // Interviews don't have user_id on them directly. They have application_id. // We must fetch the application to verify if it belongs to this user. const { data: appData } = await supabase .from('applications') .select('user_id, job_id') .eq('id', payload.new.application_id) .maybeSingle(); if (!appData || appData.user_id !== user.id) { return; // Not this user's interview } // For an insert OR an update where status is Scheduled if (payload.new.status === 'Scheduled') { // If it's an UPDATE, optionally ensure it actually CHANGED to Scheduled if (payload.eventType === 'UPDATE') { if (payload.old && payload.old.status === 'Scheduled') { return; } } console.log(">>> TRIGGERING INTERVIEW NOTIFICATION! <<<"); const { data: job } = await supabase .from('jobs') .select('title') .eq('id', appData.job_id) .maybeSingle(); const jobTitle = job?.title || 'a job'; let timeLabel = ''; if (payload.new.scheduled_time) { const dateObj = new Date(payload.new.scheduled_time); timeLabel = ` on ${dateObj.toLocaleDateString()} at ${dateObj.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`; } triggerToast( 'Interview Scheduled! 📅', `You have been invited to interview for ${jobTitle}${timeLabel}`, '🎉' ); setHasSeenNotifs(false); const newNotif = { id: Date.now(), title: `Interview Scheduled`, text: `You have an upcoming interview for ${jobTitle}.`, time: new Date().toISOString(), color: '#3B82F6' }; setNotifications(prev => [newNotif, ...prev]); } } ) .subscribe(); }; initRealtime(); return () => { if (channel) supabase.removeChannel(channel); if (jobsChannel) supabase.removeChannel(jobsChannel); if (appsChannel) supabase.removeChannel(appsChannel); }; }, []); useEffect(() => { const fetchNotifications = async () => { if (activePage === 'applicant-profile') { try { const { data: { user } } = await supabase.auth.getUser(); if (user) { const { data: apps } = await supabase .from('applications') .select('id, status, jobs(title), updated_at') .eq('user_id', user.id) .in('status', ['Accepted', 'Rejected', 'Interviewing']) .order('updated_at', { ascending: false }) .limit(5); if (apps) { const notifs = apps.map(app => ({ id: app.id, title: `Application ${app.status}`, text: `Your application for ${app.jobs?.title} is now ${app.status}.`, time: app.updated_at, color: app.status === 'Accepted' ? '#10b981' : app.status === 'Rejected' ? '#ef4444' : '#FBBF24' })); setNotifications(notifs); } } } catch (error) { console.error("Error fetching notifications:", error); } } }; fetchNotifications(); }, [activePage]); // ⭐ NEW: specific timing interview notifications useEffect(() => { let interviewInterval; const checkInterviews = async () => { try { const { data: { user } } = await supabase.auth.getUser(); if (!user) return; const { data: ints } = await supabase .from('interviews') .select('id, scheduled_time, created_at, applications!inner( jobs(title) )') .eq('applications.user_id', user.id) .eq('status', 'Scheduled'); if (ints && ints.length > 0) { const now = new Date(); ints.forEach(intObj => { if (!intObj.scheduled_time) return; const interviewDate = new Date(intObj.scheduled_time); if (isNaN(interviewDate.getTime())) return; const diffMs = interviewDate - now; const diffHours = diffMs / (1000 * 60 * 60); const ONE_WEEK = 7 * 24; const TWO_DAYS = 2 * 24; const TWENTY_FOUR_HOURS = 24; const FIVE_HOURS = 5; const windowHours = 0.5; let triggered = false; let timeLabel = ''; if (diffHours > 0) { if (Math.abs(diffHours - ONE_WEEK) <= windowHours) { triggered = true; timeLabel = 'in 1 week'; } else if (Math.abs(diffHours - TWO_DAYS) <= windowHours) { triggered = true; timeLabel = 'in 2 days'; } else if (Math.abs(diffHours - TWENTY_FOUR_HOURS) <= windowHours) { triggered = true; timeLabel = 'in 24 hours'; } else if (Math.abs(diffHours - FIVE_HOURS) <= windowHours) { triggered = true; timeLabel = 'in 5 hours'; } } // Just scheduled within last 30 minutes const updatedDate = new Date(intObj.created_at); const updatedDiffHours = (now - updatedDate) / (1000 * 60 * 60); if (updatedDiffHours <= windowHours) { triggered = true; timeLabel = 'recently scheduled'; } if (triggered) { const jobTitle = intObj.applications?.jobs?.title || 'a job'; triggerToast( 'Interview Reminder! 📅', `You have an interview ${timeLabel} for: ${jobTitle}`, '⏰' ); } }); } } catch (error) { console.error("Error checking interviews:", error); } }; // Check immediately on load checkInterviews(); // Check every 30 minutes (30 * 60 * 1000 ms) interviewInterval = setInterval(checkInterviews, 1800000); return () => clearInterval(interviewInterval); }, []); // ⭐ NEW: mark messages read when user opens messages page useEffect(() => { const markMessagesRead = async () => { if (activePage !== "applicant-messages") return; const { data: { user } } = await supabase.auth.getUser(); if (!user) return; await supabase .from("messages") .update({ is_read: true }) .eq("receiver_id", user.id); setUnreadMessages(0); setUnreadMessagesList([]); }; markMessagesRead(); if (activePage === "applicant-jobs") { setNewJobs([]); // Clear job notifications when jobs page is opened } }, [activePage]); const handleLogout = async () => { await supabase.auth.signOut(); localStorage.removeItem('applicant_name'); onNavigate('login'); }; const isActive = (key) => activePage === key; const navItems = [ { key: 'applicant-jobs', icon: , label: 'Job Listings' }, { key: 'applicant-profile', icon: , label: 'Profile' }, { key: 'applicant-interviews', icon: , label: 'Interviews' }, { key: 'applicant-ats', icon: , label: 'ATS Checker' }, { key: 'applicant-messages', icon: , label: 'Messages' }, ]; return (

{userName ? `Hi, ${userName} 👋` : 'Welcome 👋'}

{activePage === 'applicant-profile' && (
{ setShowNotifications(!showNotifications); if (!showNotifications) setHasSeenNotifs(true); }} style={{ background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '50%', width: '45px', height: '45px', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#FCD34D', cursor: 'pointer', position: 'relative', padding: 0 }} > {!hasSeenNotifs && (notifications.length + unreadMessages + newJobs.length) > 0 && ( {notifications.length + unreadMessages + newJobs.length} )} {showNotifications && (
Recent Notification ({notifications.length + unreadMessages + newJobs.length})
{notifications.length + unreadMessages + newJobs.length === 0 ? (
No new notifications
) : ( <> {unreadMessagesList.map(msg => (
📨

{msg.senderName}

{msg.timestamp}

))} {notifications.map(notif => (

{notif.title}

{notif.text}

{notif.time}

))} {newJobs.map(jobNotif => (
💼

{jobNotif.title}

{jobNotif.text}

{jobNotif.time}

))} )}
)}
)} Logout
{children}
{/* ⭐ NEW: Realtime Toast Notification Popup */} {toastNotif && ( {toastNotif.icon}

{toastNotif.title}

{toastNotif.message}

)}
); }