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:
{msg.senderName}
{msg.timestamp}
{notif.title}
{notif.text}
{notif.time}
{jobNotif.title}
{jobNotif.text}
{jobNotif.time}
{toastNotif.message}