iris_backend / src /components /JobListings.jsx
sameer2026's picture
Fix messaging UI alignment, unsend functionality, and applicant notification bugs
b66d14f
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { supabase } from '../supabaseClient';
import { SearchIcon } from './Icons';
import JobDetail from './JobDetail';
import ApplyModel from './ApplyModel';
import JobCard from './JobCard';
import VerificationModal from './VerificationModal'; // βœ… Import the new modal
export default function JobListings({ searchQuery, setSearchQuery, isSearching, filteredJobListings }) {
const [selectedJob, setSelectedJob] = useState(null);
const [appliedJobIds, setAppliedJobIds] = useState(new Set());
const [applying, setApplying] = useState(null);
// State for the Apply Modal
const [jobToApply, setJobToApply] = useState(null);
// βœ… State for Verification Modal
const [showVerificationModal, setShowVerificationModal] = useState(false);
// 1. Check Existing Applications on Load
useEffect(() => {
const fetchApplications = async () => {
const { data: { user } } = await supabase.auth.getUser();
if (user) {
const { data } = await supabase
.from('applications')
.select('job_id')
.eq('user_id', user.id);
if (data) {
setAppliedJobIds(new Set(data.map(app => app.job_id)));
}
}
};
fetchApplications();
}, []);
// 2. Open Apply Modal
const initiateApply = (jobId) => {
const job = filteredJobListings.find(j => j.id === jobId);
if (job) {
setJobToApply(job);
}
};
// βœ… Helper: Send welcome message to applicant
const sendApplicationConfirmationMessage = async (userId, jobTitle) => {
try {
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.`;
// Insert message using applicant's ID for both sender and receiver
// (RLS prevents applicants from inserting messages on behalf of an Admin)
const { error } = await supabase.from('messages').insert([{
sender_id: userId,
receiver_id: userId,
content: message,
is_read: false
}]);
if (error) console.error('Error sending confirmation message:', error);
} catch (err) {
console.error('Failed to send confirmation message:', err);
}
};
// 3. Submit Application (With Verification Gatekeeper)
const handleFinalSubmit = async (formData) => {
if (!jobToApply) return;
setApplying(jobToApply.id);
try {
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
alert("Please log in to apply.");
return;
}
// --- πŸ”’ GATEKEEPER CHECK: Verify Phone Status ---
const { data: profile, error: profileError } = await supabase
.from('profiles')
.select('is_phone_verified, experience_years')
.eq('id', user.id)
.single();
if (profileError) throw profileError;
// If NOT verified, stop the application and show modal
/** if (!profile.is_phone_verified) {
setApplying(null); // Stop loading spinner
setJobToApply(null); // Close application form
setShowVerificationModal(true); // Open Verification Modal
return; // πŸ›‘ Stop execution here
} **/
// --- βœ… IF VERIFIED: Proceed with Application ---
const { error } = await supabase
.from('applications')
.insert([{
job_id: jobToApply.id,
user_id: user.id,
status: 'Pending',
resume_url: formData.resume_url,
cover_letter: formData.cover_letter,
experience: profile.experience // Include experience from profile
}]);
if (error) throw error;
// βœ… Send confirmation message to applicant
await sendApplicationConfirmationMessage(user.id, jobToApply.title);
setAppliedJobIds(prev => new Set(prev).add(jobToApply.id));
alert("Application submitted successfully!");
setJobToApply(null);
} catch (error) {
console.error("Error applying:", error.message);
alert(`Could not apply: ${error.message}`);
} finally {
// Only stop spinner if we didn't switch to verification modal
if (!showVerificationModal) setApplying(null);
}
};
// 4. Withdraw Application
const handleWithdraw = async (jobId) => {
if (!confirm("Are you sure you want to withdraw this application?")) {
return;
}
try {
const { data: { user } } = await supabase.auth.getUser();
if (!user) return;
const { error } = await supabase
.from('applications')
.delete()
.eq('job_id', jobId)
.eq('user_id', user.id);
if (error) throw error;
setAppliedJobIds(prev => {
const newSet = new Set(prev);
newSet.delete(jobId);
return newSet;
});
} catch (error) {
console.error("Error withdrawing:", error.message);
alert("Failed to withdraw application.");
}
};
return (
<>
{/* Search Bar */}
<div style={{ backgroundColor: 'rgba(251, 191, 36, 0.1)', border: '1px solid rgba(251, 191, 36, 0.3)', borderRadius: '1rem', padding: '1.5rem', marginBottom: '2rem' }}>
<h2 style={{ fontSize: '1.5rem', fontWeight: 'bold' }}>Open Positions</h2>
<p style={{ color: '#d1d5db', marginBottom: '1rem' }}>Browse through our current job openings</p>
<div style={{ position: 'relative' }}>
<motion.div animate={{ scale: isSearching ? 1.1 : 1, rotate: isSearching ? 5 : 0 }} transition={{ type: 'spring', stiffness: 400, damping: 15 }} style={{ position: 'absolute', left: '0.75rem', top: '0', bottom: '0', display: 'grid', placeItems: 'center' }}><SearchIcon /></motion.div>
<input type="text" value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} placeholder="Search by job title..." style={{ width: '100%', padding: '0.75rem 1rem 0.75rem 2.5rem', borderRadius: '0.5rem', border: '1px solid rgba(251, 191, 36, 0.3)', backgroundColor: 'rgba(255,255,255,0.1)', color: 'white' }} />
</div>
</div>
{/* Job Grid */}
<motion.main layout style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: '2rem' }}>
<AnimatePresence>
{filteredJobListings.length > 0 ? (
filteredJobListings.map((job) => (
<motion.div key={job.id} layout initial={{ opacity: 0, scale: 0.8 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.8 }} transition={{ duration: 0.2 }}>
<JobCard
{...job}
onViewDetails={() => setSelectedJob(job)}
onApply={() => initiateApply(job.id)}
onWithdraw={() => handleWithdraw(job.id)}
isApplied={appliedJobIds.has(job.id)}
isApplying={applying === job.id}
/>
</motion.div>
))
) : (
<motion.p initial={{ opacity: 0 }} animate={{ opacity: 1 }} style={{ color: '#d1d5db' }}>No jobs found.</motion.p>
)}
</AnimatePresence>
</motion.main>
{/* Job Detail Modal */}
{selectedJob && (
<JobDetail
job={selectedJob}
onClose={() => setSelectedJob(null)}
onApply={() => initiateApply(selectedJob.id)}
isApplied={appliedJobIds.has(selectedJob.id)}
isApplying={applying === selectedJob.id}
/>
)}
{/* Apply Form Modal */}
{jobToApply && (
<ApplyModel
job={jobToApply}
isSubmitting={applying === jobToApply.id}
onClose={() => setJobToApply(null)}
onSubmit={handleFinalSubmit}
/>
)}
{/* βœ… OTP Verification Modal */}
{showVerificationModal && (
<VerificationModal
onClose={() => setShowVerificationModal(false)}
onVerified={() => {
setShowVerificationModal(false);
alert("Phone verified successfully! Please click Apply again.");
}}
/>
)}
</>
);
};