156 lines
No EOL
4.8 KiB
TypeScript
156 lines
No EOL
4.8 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import {
|
|
Container,
|
|
Typography,
|
|
CircularProgress,
|
|
Alert,
|
|
Grid,
|
|
Card,
|
|
CardContent,
|
|
Button,
|
|
Box,
|
|
Chip,
|
|
} from '@mui/material';
|
|
import { Link } from 'react-router-dom';
|
|
import axios from 'axios';
|
|
import type { JobOffer, JobSearchResponse } from '../types';
|
|
|
|
const API_BASE_URL = 'http://localhost:3000/api/jobs';
|
|
|
|
const JobList: React.FC = () => {
|
|
const [jobs, setJobs] = useState<JobOffer[]>([]);
|
|
const [loading, setLoading] = useState<boolean>(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
const fetchJobs = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const response = await axios.get<JobSearchResponse>(API_BASE_URL);
|
|
setJobs(response.data.jobs);
|
|
setError(null);
|
|
} catch (err) {
|
|
console.error("Erreur lors de la récupération des offres:", err);
|
|
setError("Impossible de charger les offres pour le moment. Veuillez réessayer plus tard.");
|
|
setJobs([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchJobs();
|
|
}, []);
|
|
|
|
return (
|
|
<Container maxWidth="lg" sx={{ mt: 2, mb: 4 }}>
|
|
<Typography variant="h4" component="h1" gutterBottom sx={{ mb: 4, fontWeight: 600 }}>
|
|
Découvrez les dernières offres d'emploi
|
|
</Typography>
|
|
|
|
{loading && (
|
|
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 4 }}>
|
|
<CircularProgress />
|
|
</Box>
|
|
)}
|
|
|
|
{error && (
|
|
<Alert severity="error" sx={{ mt: 4 }}>
|
|
{error}
|
|
</Alert>
|
|
)}
|
|
|
|
{!loading && !error && jobs.length === 0 && (
|
|
<Alert severity="info" sx={{ mt: 4 }}>
|
|
Aucune offre d'emploi disponible pour le moment.
|
|
</Alert>
|
|
)}
|
|
|
|
{!loading && !error && jobs.length > 0 && (
|
|
<Grid container spacing={3}>
|
|
{jobs.map((job) => (
|
|
<Grid item xs={12} sm={6} md={4} key={job.id}>
|
|
<Card
|
|
sx={{
|
|
height: '100%',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
transition: 'transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out',
|
|
'&:hover': {
|
|
transform: 'translateY(-4px)',
|
|
boxShadow: '0px 4px 20px rgba(0, 0, 0, 0.1)',
|
|
},
|
|
}}
|
|
>
|
|
<CardContent sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column' }}>
|
|
<Typography variant="h6" component="h2" sx={{ mb: 1, fontWeight: 600 }}>
|
|
{job.title}
|
|
</Typography>
|
|
|
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
|
{job.companyName || 'Entreprise non spécifiée'}
|
|
</Typography>
|
|
|
|
<Box sx={{ mb: 2, display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
|
{job.contractType && (
|
|
<Chip
|
|
label={job.contractType}
|
|
size="small"
|
|
sx={{
|
|
backgroundColor: 'primary.light',
|
|
color: 'primary.contrastText',
|
|
}}
|
|
/>
|
|
)}
|
|
{job.locationLabel && (
|
|
<Chip
|
|
label={job.locationLabel}
|
|
size="small"
|
|
sx={{
|
|
backgroundColor: 'secondary.light',
|
|
color: 'secondary.contrastText',
|
|
}}
|
|
/>
|
|
)}
|
|
</Box>
|
|
|
|
<Typography
|
|
variant="body2"
|
|
sx={{
|
|
overflow: 'hidden',
|
|
textOverflow: 'ellipsis',
|
|
display: '-webkit-box',
|
|
WebkitLineClamp: 3,
|
|
WebkitBoxOrient: 'vertical',
|
|
mb: 2,
|
|
flexGrow: 1,
|
|
}}
|
|
>
|
|
{job.description}
|
|
</Typography>
|
|
|
|
<Box sx={{ mt: 'auto' }}>
|
|
<Button
|
|
component={Link}
|
|
to={`/jobs/${job.id}`}
|
|
variant="contained"
|
|
color="secondary"
|
|
fullWidth
|
|
sx={{
|
|
mt: 2,
|
|
py: 1,
|
|
}}
|
|
>
|
|
Voir les détails
|
|
</Button>
|
|
</Box>
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
))}
|
|
</Grid>
|
|
)}
|
|
</Container>
|
|
);
|
|
};
|
|
|
|
export default JobList;
|