Initial commit
This commit is contained in:
		
							
								
								
									
										295
									
								
								frontend/src/pages/Jobs.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										295
									
								
								frontend/src/pages/Jobs.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,295 @@ | ||||
| import React, { useState } from 'react'; | ||||
| import { Link } from 'react-router-dom'; | ||||
| import { useQuery } from 'react-query'; | ||||
| import axios from 'axios'; | ||||
| import { useAuth } from '../contexts/AuthContext'; | ||||
| import { Search, MapPin, Clock, DollarSign, Briefcase, Plus } from 'lucide-react'; | ||||
|  | ||||
| const Jobs = () => { | ||||
|   const { user } = useAuth(); | ||||
|   const [filters, setFilters] = useState({ | ||||
|     search: '', | ||||
|     location: '', | ||||
|     employmentType: '', | ||||
|     experienceLevel: '' | ||||
|   }); | ||||
|  | ||||
|   const { data, isLoading, refetch } = useQuery(['jobs', filters], async () => { | ||||
|     const params = new URLSearchParams(); | ||||
|     Object.entries(filters).forEach(([key, value]) => { | ||||
|       if (value) params.append(key, value); | ||||
|     }); | ||||
|      | ||||
|     const response = await axios.get(`/api/jobs?${params.toString()}`); | ||||
|     return response.data; | ||||
|   }); | ||||
|  | ||||
|   const handleFilterChange = (e) => { | ||||
|     setFilters({ | ||||
|       ...filters, | ||||
|       [e.target.name]: e.target.value | ||||
|     }); | ||||
|   }; | ||||
|  | ||||
|   const handleSearch = (e) => { | ||||
|     e.preventDefault(); | ||||
|     refetch(); | ||||
|   }; | ||||
|  | ||||
|   const formatSalary = (min, max, currency = 'USD') => { | ||||
|     if (!min && !max) return 'Salary not specified'; | ||||
|     if (!min) return `Up to ${currency} ${max?.toLocaleString()}`; | ||||
|     if (!max) return `From ${currency} ${min?.toLocaleString()}`; | ||||
|     return `${currency} ${min?.toLocaleString()} - ${max?.toLocaleString()}`; | ||||
|   }; | ||||
|  | ||||
|   const getStatusColor = (status) => { | ||||
|     switch (status) { | ||||
|       case 'active': return 'bg-green-100 text-green-800'; | ||||
|       case 'paused': return 'bg-yellow-100 text-yellow-800'; | ||||
|       case 'closed': return 'bg-red-100 text-red-800'; | ||||
|       default: return 'bg-gray-100 text-gray-800'; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   if (isLoading) { | ||||
|     return ( | ||||
|       <div className="flex items-center justify-center h-64"> | ||||
|         <div className="animate-spin rounded-full h-32 w-32 border-b-2 border-primary-600"></div> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <div className="space-y-6"> | ||||
|       <div className="flex items-center justify-between"> | ||||
|         <div> | ||||
|           <h1 className="text-2xl font-bold text-gray-900">Job Postings</h1> | ||||
|           <p className="mt-1 text-sm text-gray-500"> | ||||
|             Find your next career opportunity | ||||
|           </p> | ||||
|         </div> | ||||
|         {/* Debug info */} | ||||
|         <div className="text-xs text-gray-500 mb-2"> | ||||
|           Debug: User role = {user?.role || 'undefined'} | ||||
|         </div> | ||||
|         {(user?.role === 'employer' || user?.role === 'recruiter') && ( | ||||
|           <Link | ||||
|             to="/jobs/create" | ||||
|             className="btn btn-primary" | ||||
|           > | ||||
|             <Plus className="h-4 w-4 mr-2" /> | ||||
|             Post a Job | ||||
|           </Link> | ||||
|         )} | ||||
|       </div> | ||||
|  | ||||
|       {/* Filters */} | ||||
|       <div className="bg-white shadow rounded-lg"> | ||||
|         <div className="px-4 py-5 sm:p-6"> | ||||
|           <form onSubmit={handleSearch} className="space-y-4"> | ||||
|             <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> | ||||
|               <div> | ||||
|                 <label htmlFor="search" className="block text-sm font-medium text-gray-700"> | ||||
|                   Search | ||||
|                 </label> | ||||
|                 <div className="mt-1 relative"> | ||||
|                   <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> | ||||
|                     <Search className="h-5 w-5 text-gray-400" /> | ||||
|                   </div> | ||||
|                   <input | ||||
|                     type="text" | ||||
|                     name="search" | ||||
|                     id="search" | ||||
|                     className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-primary-500 focus:border-primary-500 sm:text-sm" | ||||
|                     placeholder="Job title or keywords" | ||||
|                     value={filters.search} | ||||
|                     onChange={handleFilterChange} | ||||
|                   /> | ||||
|                 </div> | ||||
|               </div> | ||||
|  | ||||
|               <div> | ||||
|                 <label htmlFor="location" className="block text-sm font-medium text-gray-700"> | ||||
|                   Location | ||||
|                 </label> | ||||
|                 <div className="mt-1 relative"> | ||||
|                   <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> | ||||
|                     <MapPin className="h-5 w-5 text-gray-400" /> | ||||
|                   </div> | ||||
|                   <input | ||||
|                     type="text" | ||||
|                     name="location" | ||||
|                     id="location" | ||||
|                     className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-primary-500 focus:border-primary-500 sm:text-sm" | ||||
|                     placeholder="City, state, or remote" | ||||
|                     value={filters.location} | ||||
|                     onChange={handleFilterChange} | ||||
|                   /> | ||||
|                 </div> | ||||
|               </div> | ||||
|  | ||||
|               <div> | ||||
|                 <label htmlFor="employmentType" className="block text-sm font-medium text-gray-700"> | ||||
|                   Employment Type | ||||
|                 </label> | ||||
|                 <select | ||||
|                   id="employmentType" | ||||
|                   name="employmentType" | ||||
|                   className="mt-1 block w-full pl-3 pr-10 py-2 text-base border border-gray-300 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm rounded-md" | ||||
|                   value={filters.employmentType} | ||||
|                   onChange={handleFilterChange} | ||||
|                 > | ||||
|                   <option value="">All Types</option> | ||||
|                   <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 htmlFor="experienceLevel" className="block text-sm font-medium text-gray-700"> | ||||
|                   Experience Level | ||||
|                 </label> | ||||
|                 <select | ||||
|                   id="experienceLevel" | ||||
|                   name="experienceLevel" | ||||
|                   className="mt-1 block w-full pl-3 pr-10 py-2 text-base border border-gray-300 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm rounded-md" | ||||
|                   value={filters.experienceLevel} | ||||
|                   onChange={handleFilterChange} | ||||
|                 > | ||||
|                   <option value="">All Levels</option> | ||||
|                   <option value="entry">Entry Level</option> | ||||
|                   <option value="mid">Mid Level</option> | ||||
|                   <option value="senior">Senior Level</option> | ||||
|                   <option value="lead">Lead</option> | ||||
|                   <option value="executive">Executive</option> | ||||
|                 </select> | ||||
|               </div> | ||||
|             </div> | ||||
|  | ||||
|             <div className="flex justify-end"> | ||||
|               <button | ||||
|                 type="submit" | ||||
|                 className="btn btn-primary" | ||||
|               > | ||||
|                 Search Jobs | ||||
|               </button> | ||||
|             </div> | ||||
|           </form> | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
|       {/* Jobs List */} | ||||
|       <div className="space-y-4"> | ||||
|         {data?.jobs?.length > 0 ? ( | ||||
|           data.jobs.map((job) => ( | ||||
|             <div key={job.id} className="bg-white shadow rounded-lg"> | ||||
|               <div className="px-4 py-5 sm:p-6"> | ||||
|                 <div className="flex items-start justify-between"> | ||||
|                   <div className="flex-1"> | ||||
|                     <div className="flex items-center"> | ||||
|                       <h3 className="text-lg font-medium text-gray-900"> | ||||
|                         <Link to={`/jobs/${job.id}`} className="hover:text-primary-600"> | ||||
|                           {job.title} | ||||
|                         </Link> | ||||
|                       </h3> | ||||
|                       <span className={`ml-3 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusColor(job.status)}`}> | ||||
|                         {job.status} | ||||
|                       </span> | ||||
|                     </div> | ||||
|                     <div className="mt-1 flex items-center text-sm text-gray-500"> | ||||
|                       <Briefcase className="h-4 w-4 mr-1" /> | ||||
|                       {job.company_name} | ||||
|                     </div> | ||||
|                     <div className="mt-2 flex items-center text-sm text-gray-500 space-x-4"> | ||||
|                       <div className="flex items-center"> | ||||
|                         <MapPin className="h-4 w-4 mr-1" /> | ||||
|                         {job.location} | ||||
|                         {job.remote_allowed && <span className="ml-1">(Remote OK)</span>} | ||||
|                       </div> | ||||
|                       <div className="flex items-center"> | ||||
|                         <DollarSign className="h-4 w-4 mr-1" /> | ||||
|                         {formatSalary(job.salary_min, job.salary_max, job.currency)} | ||||
|                       </div> | ||||
|                       <div className="flex items-center"> | ||||
|                         <Clock className="h-4 w-4 mr-1" /> | ||||
|                         {new Date(job.created_at).toLocaleDateString()} | ||||
|                       </div> | ||||
|                     </div> | ||||
|                     <div className="mt-3"> | ||||
|                       <p className="text-sm text-gray-600 line-clamp-2"> | ||||
|                         {job.description} | ||||
|                       </p> | ||||
|                     </div> | ||||
|                     {job.skills_required && job.skills_required.length > 0 && ( | ||||
|                       <div className="mt-3"> | ||||
|                         <div className="flex flex-wrap gap-2"> | ||||
|                           {job.skills_required.slice(0, 5).map((skill, index) => ( | ||||
|                             <span | ||||
|                               key={index} | ||||
|                               className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary-100 text-primary-800" | ||||
|                             > | ||||
|                               {skill} | ||||
|                             </span> | ||||
|                           ))} | ||||
|                           {job.skills_required.length > 5 && ( | ||||
|                             <span className="text-xs text-gray-500"> | ||||
|                               +{job.skills_required.length - 5} more | ||||
|                             </span> | ||||
|                           )} | ||||
|                         </div> | ||||
|                       </div> | ||||
|                     )} | ||||
|                   </div> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           )) | ||||
|         ) : ( | ||||
|           <div className="text-center py-12"> | ||||
|             <Briefcase className="mx-auto h-12 w-12 text-gray-400" /> | ||||
|             <h3 className="mt-2 text-sm font-medium text-gray-900">No jobs found</h3> | ||||
|             <p className="mt-1 text-sm text-gray-500"> | ||||
|               Try adjusting your search criteria | ||||
|             </p> | ||||
|           </div> | ||||
|         )} | ||||
|       </div> | ||||
|  | ||||
|       {/* Pagination */} | ||||
|       {data?.pagination && data.pagination.pages > 1 && ( | ||||
|         <div className="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6"> | ||||
|           <div className="flex-1 flex justify-between sm:hidden"> | ||||
|             <button className="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"> | ||||
|               Previous | ||||
|             </button> | ||||
|             <button className="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"> | ||||
|               Next | ||||
|             </button> | ||||
|           </div> | ||||
|           <div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between"> | ||||
|             <div> | ||||
|               <p className="text-sm text-gray-700"> | ||||
|                 Showing{' '} | ||||
|                 <span className="font-medium"> | ||||
|                   {((data.pagination.page - 1) * data.pagination.limit) + 1} | ||||
|                 </span>{' '} | ||||
|                 to{' '} | ||||
|                 <span className="font-medium"> | ||||
|                   {Math.min(data.pagination.page * data.pagination.limit, data.pagination.total)} | ||||
|                 </span>{' '} | ||||
|                 of{' '} | ||||
|                 <span className="font-medium">{data.pagination.total}</span>{' '} | ||||
|                 results | ||||
|               </p> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|       )} | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default Jobs; | ||||
		Reference in New Issue
	
	Block a user