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 { import {
Container, Container,
Typography, Typography,
@ -10,6 +10,11 @@ import {
Button, Button,
Box, Box,
Chip, Chip,
TextField,
Select,
MenuItem,
Paper,
SelectChangeEvent,
} from '@mui/material'; } from '@mui/material';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import axios from 'axios'; import axios from 'axios';
@ -21,25 +26,49 @@ const JobList: React.FC = () => {
const [jobs, setJobs] = useState<JobOffer[]>([]); const [jobs, setJobs] = useState<JobOffer[]>([]);
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null); 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(() => { 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(); 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 ( return (
<Container maxWidth="lg" sx={{ mt: 2, mb: 4 }}> <Container maxWidth="lg" sx={{ mt: 2, mb: 4 }}>
@ -47,6 +76,83 @@ const JobList: React.FC = () => {
Découvrez les dernières offres d'emploi Découvrez les dernières offres d'emploi
</Typography> </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 && ( {loading && (
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 4 }}> <Box sx={{ display: 'flex', justifyContent: 'center', mt: 4 }}>
<CircularProgress /> <CircularProgress />
@ -69,82 +175,84 @@ const JobList: React.FC = () => {
<Grid container spacing={3}> <Grid container spacing={3}>
{jobs.map((job) => ( {jobs.map((job) => (
<Grid item xs={12} sm={6} md={4} key={job.id}> <Grid item xs={12} sm={6} md={4} key={job.id}>
<Card <Box sx={{ height: '100%' }}>
sx={{ <Card
height: '100%', sx={{
display: 'flex', height: '100%',
flexDirection: 'column', display: 'flex',
transition: 'transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out', flexDirection: 'column',
'&:hover': { transition: 'transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out',
transform: 'translateY(-4px)', '&:hover': {
boxShadow: '0px 4px 20px rgba(0, 0, 0, 0.1)', 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 }}> <CardContent sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column' }}>
{job.title} <Typography variant="h6" component="h2" sx={{ mb: 1, fontWeight: 600 }}>
</Typography> {job.title}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}> <Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
{job.companyName || 'Entreprise non spécifiée'} {job.companyName || 'Entreprise non spécifiée'}
</Typography> </Typography>
<Box sx={{ mb: 2, display: 'flex', gap: 1, flexWrap: 'wrap' }}> <Box sx={{ mb: 2, display: 'flex', gap: 1, flexWrap: 'wrap' }}>
{job.contractType && ( {job.contractType && (
<Chip <Chip
label={job.contractType} label={job.contractType}
size="small" size="small"
sx={{ sx={{
backgroundColor: 'primary.light', backgroundColor: 'primary.light',
color: 'primary.contrastText', color: 'primary.contrastText',
}} }}
/> />
)} )}
{job.locationLabel && ( {job.locationLabel && (
<Chip <Chip
label={job.locationLabel} label={job.locationLabel}
size="small" size="small"
sx={{ sx={{
backgroundColor: 'secondary.light', backgroundColor: 'secondary.light',
color: 'secondary.contrastText', color: 'secondary.contrastText',
}} }}
/> />
)} )}
</Box> </Box>
<Typography <Typography
variant="body2" 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={{ sx={{
mt: 2, overflow: 'hidden',
py: 1, textOverflow: 'ellipsis',
display: '-webkit-box',
WebkitLineClamp: 3,
WebkitBoxOrient: 'vertical',
mb: 2,
flexGrow: 1,
}} }}
> >
Voir les détails {job.description}
</Button> </Typography>
</Box>
</CardContent> <Box sx={{ mt: 'auto' }}>
</Card> <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>
))} ))}
</Grid> </Grid>

View file

@ -13,6 +13,7 @@ import {
Divider, // Séparateur visuel Divider, // Séparateur visuel
Box // Conteneur générique Box // Conteneur générique
} from '@mui/material'; } from '@mui/material';
import type { SxProps, Theme } from '@mui/material/styles';
// Importation des icônes // Importation des icônes
import SearchIcon from '@mui/icons-material/Search'; import SearchIcon from '@mui/icons-material/Search';
@ -26,21 +27,23 @@ interface SidebarProps {
} }
const Sidebar: React.FC<SidebarProps> = ({ drawerWidth }) => { 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 ( return (
<Drawer <Drawer
sx={{ sx={drawerStyles}
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
},
}}
variant="permanent" // La sidebar est toujours visible variant="permanent" // La sidebar est toujours visible
anchor="left" // Positionnée à gauche anchor="left" // Positionnée à gauche
> >