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 {
|
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>
|
||||||
|
|
|
@ -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
|
||||||
>
|
>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue