champs search

This commit is contained in:
quasar 2025-05-30 13:59:52 +02:00
parent 344fccac45
commit 7a4950ea83
2 changed files with 213 additions and 102 deletions

View file

@ -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>

View file

@ -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
>