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