champs search
This commit is contained in:
parent
344fccac45
commit
7a4950ea83
2 changed files with 213 additions and 102 deletions
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Container,
|
||||
Typography,
|
||||
|
@ -10,6 +10,11 @@ import {
|
|||
Button,
|
||||
Box,
|
||||
Chip,
|
||||
TextField,
|
||||
Select,
|
||||
MenuItem,
|
||||
Paper,
|
||||
SelectChangeEvent,
|
||||
} from '@mui/material';
|
||||
import { Link } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
|
@ -21,25 +26,49 @@ const JobList: React.FC = () => {
|
|||
const [jobs, setJobs] = useState<JobOffer[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [searchTerm, setSearchTerm] = useState<string>('');
|
||||
const [locationTerm, setLocationTerm] = useState<string>('');
|
||||
const [contractType, setContractType] = useState<string>('');
|
||||
const [sortBy, setSortBy] = useState<string>('publicationDate');
|
||||
const [sortOrder, setSortOrder] = useState<string>('desc');
|
||||
|
||||
const fetchJobs = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const params = {
|
||||
keyword: searchTerm,
|
||||
location: locationTerm,
|
||||
contractType: contractType,
|
||||
sortBy: sortBy,
|
||||
sortOrder: sortOrder,
|
||||
};
|
||||
const response = await axios.get<JobSearchResponse>(API_BASE_URL, { params });
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
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();
|
||||
}, []);
|
||||
}, [searchTerm, locationTerm, contractType, sortBy, sortOrder]);
|
||||
|
||||
const handleSearchSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
fetchJobs();
|
||||
};
|
||||
|
||||
const handleSortChange = (event: React.ChangeEvent<{ value: unknown }>) => {
|
||||
const value = event.target.value as string;
|
||||
const [newSortBy, newSortOrder] = value.split(':');
|
||||
setSortBy(newSortBy);
|
||||
setSortOrder(newSortOrder);
|
||||
};
|
||||
|
||||
return (
|
||||
<Container maxWidth="lg" sx={{ mt: 2, mb: 4 }}>
|
||||
|
@ -47,6 +76,83 @@ const JobList: React.FC = () => {
|
|||
Découvrez les dernières offres d'emploi
|
||||
</Typography>
|
||||
|
||||
{/* Formulaire de recherche et filtres */}
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
p: 3,
|
||||
mb: 4,
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0px 4px 10px rgba(0, 0, 0, 0.05)',
|
||||
backgroundColor: 'background.paper',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="form"
|
||||
onSubmit={handleSearchSubmit}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 2,
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
label="Mots-clés"
|
||||
variant="outlined"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
sx={{ flexGrow: 1, minWidth: { xs: '100%', sm: '200px' } }}
|
||||
/>
|
||||
<TextField
|
||||
label="Localisation"
|
||||
variant="outlined"
|
||||
value={locationTerm}
|
||||
onChange={(e) => setLocationTerm(e.target.value)}
|
||||
sx={{ flexGrow: 1, minWidth: { xs: '100%', sm: '180px' } }}
|
||||
/>
|
||||
<Select
|
||||
value={contractType}
|
||||
onChange={(e) => setContractType(e.target.value as string)}
|
||||
displayEmpty
|
||||
sx={{ minWidth: { xs: '100%', sm: '160px' } }}
|
||||
>
|
||||
<MenuItem value="">Tous les contrats</MenuItem>
|
||||
<MenuItem value="CDI">CDI</MenuItem>
|
||||
<MenuItem value="CDD">CDD</MenuItem>
|
||||
<MenuItem value="INTERIM">Intérim</MenuItem>
|
||||
<MenuItem value="SAISONNIER">Saisonnier</MenuItem>
|
||||
<MenuItem value="STAGE">Stage</MenuItem>
|
||||
<MenuItem value="ALTERNANCE">Alternance</MenuItem>
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
value={`${sortBy}:${sortOrder}`}
|
||||
onChange={handleSortChange}
|
||||
displayEmpty
|
||||
sx={{ minWidth: { xs: '100%', sm: '220px' } }}
|
||||
>
|
||||
<MenuItem value="publicationDate:desc">Plus récent</MenuItem>
|
||||
<MenuItem value="publicationDate:asc">Plus ancien</MenuItem>
|
||||
<MenuItem value="title:asc">Titre (A-Z)</MenuItem>
|
||||
<MenuItem value="title:desc">Titre (Z-A)</MenuItem>
|
||||
</Select>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
minWidth: { xs: '100%', sm: '120px' },
|
||||
py: 1.5,
|
||||
}}
|
||||
>
|
||||
Rechercher
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{loading && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 4 }}>
|
||||
<CircularProgress />
|
||||
|
@ -69,82 +175,84 @@ const JobList: React.FC = () => {
|
|||
<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={{ height: '100%' }}>
|
||||
<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>
|
||||
<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,
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 3,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
mb: 2,
|
||||
flexGrow: 1,
|
||||
}}
|
||||
>
|
||||
Voir les détails
|
||||
</Button>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{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>
|
||||
</Box>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
Divider, // Séparateur visuel
|
||||
Box // Conteneur générique
|
||||
} from '@mui/material';
|
||||
import type { SxProps, Theme } from '@mui/material/styles';
|
||||
|
||||
// Importation des icônes
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
|
@ -26,21 +27,23 @@ interface SidebarProps {
|
|||
}
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({ drawerWidth }) => {
|
||||
const drawerStyles: SxProps<Theme> = {
|
||||
width: drawerWidth,
|
||||
flexShrink: 0,
|
||||
'& .MuiDrawer-paper': { // Style du "papier" (fond) de la sidebar
|
||||
width: drawerWidth,
|
||||
boxSizing: 'border-box',
|
||||
backgroundColor: 'background.paper', // Fond blanc du thème
|
||||
boxShadow: '0px 2px 8px rgba(0, 0, 0, 0.05)', // Ombre douce
|
||||
borderRight: 'none',
|
||||
overflowX: 'hidden', // Empêche le débordement horizontal
|
||||
borderRadius: '0 12px 12px 0', // Bords arrondis seulement à droite
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
sx={{
|
||||
width: drawerWidth,
|
||||
flexShrink: 0,
|
||||
'& .MuiDrawer-paper': { // Style du "papier" (fond) de la sidebar
|
||||
width: drawerWidth,
|
||||
boxSizing: 'border-box',
|
||||
backgroundColor: 'background.paper', // Fond blanc du thème
|
||||
boxShadow: '0px 2px 8px rgba(0, 0, 0, 0.05)', // Ombre douce
|
||||
borderRight: 'none',
|
||||
overflowX: 'hidden', // Empêche le débordement horizontal
|
||||
borderRadius: '0 12px 12px 0', // Bords arrondis seulement à droite
|
||||
},
|
||||
}}
|
||||
sx={drawerStyles}
|
||||
variant="permanent" // La sidebar est toujours visible
|
||||
anchor="left" // Positionnée à gauche
|
||||
>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue