Spaces:
Sleeping
Sleeping
| 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."); | |
| }} | |
| /> | |
| )} | |
| </> | |
| ); | |
| }; |