Spaces:
Sleeping
Sleeping
| import React, { useState, useEffect } from 'react'; | |
| import { motion, AnimatePresence } from 'framer-motion'; | |
| import { supabase } from '../supabaseClient'; | |
| // --- Icons --- | |
| const PlusIcon = () => <svg style={{width:'20px', height:'20px', marginRight:'8px'}} viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z" clipRule="evenodd" /></svg>; | |
| const EditIcon = () => <svg style={{width:'16px', height:'16px'}} viewBox="0 0 20 20" fill="currentColor"><path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" /></svg>; | |
| const TrashIcon = () => <svg style={{width:'16px', height:'16px', color:'#EF4444'}} viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm4 0a1 1 0 012 0v6a1 1 0 11-2 0V8z" clipRule="evenodd" /></svg>; | |
| const SpinnerIcon = () => <motion.svg animate={{ rotate: 360 }} transition={{ duration: 1, repeat: Infinity, ease: "linear" }} style={{ width: '16px', height: '16px', marginRight: '8px' }} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></motion.svg>; | |
| export default function JobPosting({ onNavigate }) { | |
| const [jobs, setJobs] = useState([]); | |
| const [loading, setLoading] = useState(true); | |
| const [isSaving, setIsSaving] = useState(false); | |
| const [isModalOpen, setIsModalOpen] = useState(false); | |
| const [editingJob, setEditingJob] = useState(null); | |
| const [formData, setFormData] = useState({ | |
| title: '', | |
| department: '', | |
| location: '', | |
| job_type: 'Full-time', | |
| experience_level: '', | |
| salary_range: '', | |
| skills_required: '', | |
| deadline: '', | |
| description: '' | |
| }); | |
| useEffect(() => { | |
| fetchJobs(); | |
| }, []); | |
| const fetchJobs = async () => { | |
| setLoading(true); | |
| try { | |
| const { data: { user } } = await supabase.auth.getUser(); | |
| if(!user) return; | |
| // ✅ FIXED: Fetch company_id from 'user_roles' instead of 'profiles' | |
| const { data: roleData } = await supabase | |
| .from('user_roles') | |
| .select('company_id') | |
| .eq('user_id', user.id) | |
| .single(); | |
| if (roleData?.company_id) { | |
| const { data, error } = await supabase | |
| .from('jobs') | |
| .select('*') | |
| .eq('company_id', roleData.company_id) | |
| .order('created_at', { ascending: false }); | |
| if (data) setJobs(data); | |
| if (error) console.error("Error fetching jobs:", error.message); | |
| } | |
| } catch (error) { | |
| console.error("System error:", error.message); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| const handleSubmit = async (e) => { | |
| e.preventDefault(); | |
| setIsSaving(true); | |
| try { | |
| const { data: { user } } = await supabase.auth.getUser(); | |
| if (!user) throw new Error("User not found"); | |
| const skillsArray = formData.skills_required.split(',').map(s => s.trim()).filter(s => s); | |
| // ✅ FIXED: Fetch company_id from 'user_roles' instead of 'profiles' | |
| const { data: roleData } = await supabase | |
| .from('user_roles') | |
| .select('company_id') | |
| .eq('user_id', user.id) | |
| .single(); | |
| if (!roleData?.company_id) throw new Error("You are not linked to a company."); | |
| const payload = { | |
| title: formData.title, | |
| department: formData.department, | |
| location: formData.location, | |
| job_type: formData.job_type, | |
| experience_level: formData.experience_level, | |
| salary_range: formData.salary_range, | |
| skills_required: skillsArray, | |
| deadline: formData.deadline || null, | |
| description: formData.description, | |
| company_id: roleData.company_id, // ✅ Using correct ID | |
| status: 'Active' | |
| }; | |
| let error; | |
| if (editingJob) { | |
| const { error: updateError } = await supabase.from('jobs').update(payload).eq('id', editingJob.id); | |
| error = updateError; | |
| } else { | |
| const { error: insertError } = await supabase.from('jobs').insert(payload); | |
| error = insertError; | |
| } | |
| if (error) throw error; | |
| alert(editingJob ? "Job updated successfully!" : "Job posted successfully!"); | |
| fetchJobs(); | |
| closeModal(); | |
| } catch (error) { | |
| alert("Error saving job: " + error.message); | |
| } finally { | |
| setIsSaving(false); | |
| } | |
| }; | |
| const handleDelete = async (id) => { | |
| if (!window.confirm("Are you sure? This will delete the job posting.")) return; | |
| try { | |
| const { error } = await supabase.from('jobs').delete().eq('id', id); | |
| if (error) throw error; | |
| setJobs(jobs.filter(job => job.id !== id)); | |
| } catch (error) { | |
| alert("Error deleting job: " + error.message); | |
| } | |
| }; | |
| const openModal = (job = null) => { | |
| if (job) { | |
| setEditingJob(job); | |
| setFormData({ | |
| title: job.title, | |
| department: job.department || '', | |
| location: job.location || '', | |
| job_type: job.job_type || 'Full-time', | |
| experience_level: job.experience_level || '', | |
| salary_range: job.salary_range || '', | |
| skills_required: job.skills_required ? job.skills_required.join(', ') : '', | |
| deadline: job.deadline || '', | |
| description: job.description || '' | |
| }); | |
| } else { | |
| setEditingJob(null); | |
| setFormData({ | |
| title: '', department: '', location: '', | |
| job_type: 'Full-time', experience_level: '', salary_range: '', | |
| skills_required: '', deadline: '', description: '' | |
| }); | |
| } | |
| setIsModalOpen(true); | |
| }; | |
| const closeModal = () => setIsModalOpen(false); | |
| const inputStyle = { width: '100%', padding: '0.75rem', borderRadius: '0.5rem', border: '1px solid rgba(239, 68, 68, 0.3)', backgroundColor: 'rgba(255,255,255,0.05)', color: 'white', marginBottom: '1rem', boxSizing: 'border-box' }; | |
| const labelStyle = { display: 'block', color: '#d1d5db', marginBottom: '0.5rem', fontSize: '0.875rem' }; | |
| return ( | |
| <div> | |
| <style>{` | |
| select option { background-color: #111827; color: white; } | |
| input[type="date"]::-webkit-calendar-picker-indicator { filter: invert(1); cursor: pointer; } | |
| `}</style> | |
| <header style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem' }}> | |
| <h1 style={{ fontSize: '1.875rem', fontWeight: 'bold', margin: 0 }}>Job Postings</h1> | |
| <motion.button | |
| whileHover={{ scale: 1.05 }} | |
| whileTap={{ scale: 0.95 }} | |
| onClick={() => openModal()} | |
| style={{ | |
| backgroundColor: '#EF4444', | |
| color: 'white', | |
| padding: '0.75rem 1.5rem', | |
| borderRadius: '0.5rem', | |
| border: 'none', | |
| display: 'flex', | |
| alignItems: 'center', | |
| cursor: 'pointer', | |
| fontWeight: 'bold', | |
| marginRight: '11rem' | |
| }} | |
| > | |
| <PlusIcon /> Post New Job | |
| </motion.button> | |
| </header> | |
| {loading ? <div style={{color: '#d1d5db'}}>Loading jobs...</div> : ( | |
| <div style={{ display: 'grid', gap: '1.5rem' }}> | |
| {jobs.length === 0 && <p style={{color: '#666', textAlign: 'center', marginTop: '2rem'}}>No jobs posted yet.</p>} | |
| {jobs.map(job => ( | |
| <motion.div | |
| initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} key={job.id} | |
| style={{ backgroundColor: 'rgba(239, 68, 68, 0.05)', borderRadius: '1rem', padding: '1.5rem', border: '1px solid rgba(239, 68, 68, 0.2)', display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }} | |
| > | |
| <div style={{ flex: 1 }}> | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '0.5rem' }}> | |
| <h3 style={{ fontSize: '1.25rem', fontWeight: 'bold', margin: 0 }}>{job.title}</h3> | |
| <span style={{ fontSize: '0.75rem', padding: '0.1rem 0.5rem', borderRadius: '9999px', backgroundColor: job.status === 'Active' ? 'rgba(16, 185, 129, 0.2)' : 'rgba(239, 68, 68, 0.2)', color: job.status === 'Active' ? '#34D399' : '#EF4444' }}>{job.status || 'Active'}</span> | |
| </div> | |
| <div style={{ display: 'flex', gap: '1rem', color: '#d1d5db', fontSize: '0.9rem', marginBottom: '1rem', flexWrap: 'wrap' }}> | |
| <span>{job.department}</span><span>•</span><span>{job.job_type}</span><span>•</span><span>{job.location}</span> | |
| </div> | |
| </div> | |
| <div style={{ display: 'flex', gap: '0.5rem', marginLeft: '1rem' }}> | |
| <motion.button whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }} onClick={() => openModal(job)} style={{ background: 'rgba(255,255,255,0.1)', border: '1px solid rgba(255,255,255,0.2)', borderRadius: '0.5rem', color: 'white', cursor: 'pointer', padding: '0.5rem' }}><EditIcon /></motion.button> | |
| <motion.button whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }} onClick={() => handleDelete(job.id)} style={{ background: 'rgba(239, 68, 68, 0.1)', border: '1px solid rgba(239, 68, 68, 0.3)', borderRadius: '0.5rem', color: '#EF4444', cursor: 'pointer', padding: '0.5rem' }}><TrashIcon /></motion.button> | |
| </div> | |
| </motion.div> | |
| ))} | |
| </div> | |
| )} | |
| <AnimatePresence> | |
| {isModalOpen && ( | |
| <div style={{ position: 'fixed', inset: 0, backgroundColor: 'rgba(0,0,0,0.6)', backdropFilter: 'blur(5px)', display: 'flex', justifyContent: 'center', alignItems: 'center', zIndex: 100 }}> | |
| <motion.div initial={{ scale: 0.9, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.9, opacity: 0 }} style={{ backgroundColor: '#111827', width: '100%', maxWidth: '700px', padding: '2rem', borderRadius: '1rem', border: '1px solid rgba(239, 68, 68, 0.3)', maxHeight: '90vh', overflowY: 'auto' }}> | |
| <h2 style={{ fontSize: '1.5rem', fontWeight: 'bold', marginBottom: '1.5rem' }}>{editingJob ? 'Edit Job' : 'Post New Job'}</h2> | |
| <form onSubmit={handleSubmit}> | |
| <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}> | |
| <div><label style={labelStyle}>Job Title</label><input required type="text" value={formData.title} onChange={e => setFormData({...formData, title: e.target.value})} style={inputStyle} placeholder="e.g. Senior React Dev" /></div> | |
| <div><label style={labelStyle}>Department</label><input type="text" value={formData.department} onChange={e => setFormData({...formData, department: e.target.value})} style={inputStyle} placeholder="e.g. Engineering" /></div> | |
| </div> | |
| <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}> | |
| <div> | |
| <label style={labelStyle}>Job Type</label> | |
| <select value={formData.job_type} onChange={e => setFormData({...formData, job_type: e.target.value})} style={inputStyle}> | |
| <option value="Full-time">Full-time</option><option value="Part-time">Part-time</option><option value="Contract">Contract</option><option value="Internship">Internship</option> | |
| </select> | |
| </div> | |
| <div><label style={labelStyle}>Location</label><input type="text" value={formData.location} onChange={e => setFormData({...formData, location: e.target.value})} style={inputStyle} placeholder="e.g. Remote" /></div> | |
| </div> | |
| <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}> | |
| <div><label style={labelStyle}>Exp (Years)</label><input type="text" value={formData.experience_level} onChange={e => setFormData({...formData, experience_level: e.target.value})} style={inputStyle} placeholder="e.g. 3-5 years" /></div> | |
| <div> | |
| <label style={labelStyle}>Salary Range</label> | |
| <input type="text" value={formData.salary_range} onChange={e => setFormData({...formData, salary_range: e.target.value})} style={inputStyle} placeholder="e.g. $80k - $120k" /> | |
| </div> | |
| </div> | |
| <div style={{ marginBottom: '1rem' }}><label style={labelStyle}>Deadline</label><input type="date" value={formData.deadline} onChange={e => setFormData({...formData, deadline: e.target.value})} style={inputStyle} /></div> | |
| <div><label style={labelStyle}>Required Skills <span style={{color: '#6B7280'}}>(Comma separated)</span></label><input type="text" value={formData.skills_required} onChange={e => setFormData({...formData, skills_required: e.target.value})} style={inputStyle} placeholder="React, Node.js, SQL" /></div> | |
| <div><label style={labelStyle}>Job Description</label><textarea rows="5" value={formData.description} onChange={e => setFormData({...formData, description: e.target.value})} style={{...inputStyle, resize: 'vertical'}} placeholder="Enter detailed job description here..." /></div> | |
| <div style={{ display: 'flex', justifyContent: 'flex-end', gap: '1rem', marginTop: '1rem' }}> | |
| <button type="button" onClick={closeModal} style={{ padding: '0.75rem 1.5rem', borderRadius: '0.5rem', background: 'transparent', border: '1px solid #374151', color: 'white', cursor: 'pointer' }}>Cancel</button> | |
| <button type="submit" disabled={isSaving} style={{ padding: '0.75rem 1.5rem', borderRadius: '0.5rem', backgroundColor: '#EF4444', border: 'none', color: 'white', cursor: 'pointer', fontWeight: 'bold', display: 'flex', alignItems: 'center' }}> | |
| {isSaving && <SpinnerIcon />} {isSaving ? 'Saving...' : (editingJob ? 'Update Job' : 'Post Job')} | |
| </button> | |
| </div> | |
| </form> | |
| </motion.div> | |
| </div> | |
| )} | |
| </AnimatePresence> | |
| </div> | |
| ); | |
| } |