148 lines
5.6 KiB
TypeScript
148 lines
5.6 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import axios from 'axios';
|
|
import type { JobOffer, JobSearchResponse } from '../types'; // Importe les types définis
|
|
|
|
const API_BASE_URL = 'http://localhost:3000/api/jobs'; // L'URL de votre API backend
|
|
|
|
const JobSearch: React.FC = () => {
|
|
const [jobs, setJobs] = useState<JobOffer[]>([]);
|
|
const [total, setTotal] = useState<number>(0);
|
|
const [page, setPage] = useState<number>(1);
|
|
const [limit, setLimit] = useState<number>(10);
|
|
const [keyword, setKeyword] = useState<string>('');
|
|
const [location, setLocation] = useState<string>('');
|
|
const [contractType, setContractType] = useState<string>(''); // Nouvel état pour le type de contrat
|
|
const [sortBy, setSortBy] = useState<string>('publicationDate'); // Nouvel état pour le tri par colonne
|
|
const [sortOrder, setSortOrder] = useState<string>('desc'); // Nouvel état pour l'ordre de tri (asc/desc)
|
|
|
|
const [loading, setLoading] = useState<boolean>(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
const fetchJobs = async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const params = {
|
|
page,
|
|
limit,
|
|
keyword: keyword || undefined,
|
|
location: location || undefined,
|
|
contractType: contractType || undefined, // Ajout du paramètre contractType
|
|
sortBy: sortBy, // Ajout du paramètre sortBy
|
|
sortOrder: sortOrder, // Ajout du paramètre sortOrder
|
|
};
|
|
const response = await axios.get<JobSearchResponse>(API_BASE_URL, { params });
|
|
setJobs(response.data.jobs);
|
|
setTotal(response.data.total);
|
|
setPage(response.data.page);
|
|
setLimit(response.data.limit);
|
|
} catch (err) {
|
|
console.error('Error fetching jobs:', err);
|
|
setError('Failed to fetch job offers. Please try again later.');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchJobs();
|
|
}, [page, keyword, location, contractType, sortBy, sortOrder, limit]); // Mettre à jour les dépendances
|
|
|
|
const handleSearch = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setPage(1); // Réinitialise à la première page lors d'une nouvelle recherche
|
|
// La fonction fetchJobs sera appelée par useEffect car les états keyword ou location auront changé.
|
|
};
|
|
|
|
const totalPages = Math.ceil(total / limit);
|
|
|
|
return (
|
|
<div className="job-search-container">
|
|
<h1>Job Offers</h1>
|
|
|
|
<form onSubmit={handleSearch}>
|
|
<input
|
|
type="text"
|
|
placeholder="Keyword (e.g., développeur)"
|
|
value={keyword}
|
|
onChange={(e) => setKeyword(e.target.value)}
|
|
/>
|
|
<input
|
|
type="text"
|
|
placeholder="Location (e.g., Paris)"
|
|
value={location}
|
|
onChange={(e) => setLocation(e.target.value)}
|
|
/>
|
|
{/* Nouveau select pour le type de contrat */}
|
|
<select value={contractType} onChange={(e) => setContractType(e.target.value)}>
|
|
<option value="">All Contract Types</option>
|
|
<option value="CDI">CDI</option>
|
|
<option value="CDD">CDD</option>
|
|
<option value="MIS">Intérim / Mission</option>
|
|
<option value="SAI">Saisonnier</option>
|
|
<option value="APP">Apprentissage</option>
|
|
<option value="PRO">Professionnalisation</option>
|
|
</select>
|
|
|
|
{/* Nouveaux selects pour le tri */}
|
|
<select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
|
|
<option value="publicationDate">Sort by Date</option>
|
|
<option value="title">Sort by Title</option>
|
|
</select>
|
|
<select value={sortOrder} onChange={(e) => setSortOrder(e.target.value)}>
|
|
<option value="desc">Descending</option>
|
|
<option value="asc">Ascending</option>
|
|
</select>
|
|
|
|
<button type="submit">Search</button>
|
|
</form>
|
|
|
|
{/* ... (le reste de votre JSX pour l'affichage des résultats et la pagination reste inchangé) ... */}
|
|
|
|
{loading && <p className="info-message">Loading job offers...</p>}
|
|
{error && <p className="info-message" style={{ color: 'red' }}>Error: {error}</p>}
|
|
|
|
{!loading && !error && jobs.length === 0 && (
|
|
<p className="info-message">No job offers found for your search criteria.</p>
|
|
)}
|
|
|
|
{!loading && !error && jobs.length > 0 && (
|
|
<div>
|
|
<p className="info-message">Total offers: {total}</p>
|
|
<div className="job-grid">
|
|
{jobs.map((job) => (
|
|
<div key={job.id} className="job-card">
|
|
<h3>{job.title}</h3>
|
|
<p><strong>Company:</strong> {job.companyName || 'N/A'}</p>
|
|
<p><strong>Location:</strong> {job.locationLabel || job.cityName || 'N/A'}</p>
|
|
<p><strong>Contract:</strong> {job.contractLabel || job.contractType || 'N/A'}</p>
|
|
<p><strong>Published:</strong> {new Date(job.publicationDate).toLocaleDateString()}</p>
|
|
<p className="description">
|
|
{job.description?.substring(0, 150)}...
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="pagination-controls">
|
|
<button
|
|
onClick={() => setPage(prev => Math.max(1, prev - 1))}
|
|
disabled={page === 1}
|
|
>
|
|
Previous
|
|
</button>
|
|
<span>Page {page} of {totalPages}</span>
|
|
<button
|
|
onClick={() => setPage(prev => Math.min(totalPages, prev + 1))}
|
|
disabled={page === totalPages}
|
|
>
|
|
Next
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default JobSearch;
|