This commit is contained in:
quasar 2025-05-30 13:49:03 +02:00
parent 3883f0cef6
commit 08bb8deec5
3 changed files with 170 additions and 14 deletions

View file

@ -1,7 +1,7 @@
// job/frontend/src/App.tsx
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import JobSearch from './components/JobSearch';
import JobList from './components/JobList';
import JobDetail from './components/JobDetail';
import Sidebar from './components/Sidebar';
import { ThemeProvider, createTheme } from '@mui/material/styles';
@ -99,14 +99,16 @@ const App: React.FC = () => {
sx={{
flexGrow: 1,
p: { xs: 2, md: 4 },
width: { sm: '100%' },
ml: { sm: drawerWidth },
width: { sm: `calc(100% - ${drawerWidth}px)` },
ml: { sm: `${drawerWidth}px` },
overflowY: 'auto',
height: '100vh',
boxSizing: 'border-box',
backgroundColor: 'background.default',
}}
>
<Routes>
<Route path="/" element={<JobSearch />} />
<Route path="/" element={<JobList />} />
<Route path="/jobs/:id" element={<JobDetail />} />
</Routes>
</Box>

View file

@ -0,0 +1,156 @@
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 } 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<JobOffer[]>(API_BASE_URL);
setJobs(response.data);
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;

View file

@ -1,19 +1,17 @@
export interface JobOffer {
id: string;
title: string;
description: string;
publicationDate: string; // Ou Date, si vous traitez les dates côté frontend
romeCode?: string;
romeLabel?: string;
locationLabel?: string;
postalCode?: string;
departmentCode?: string;
cityName?: string;
companyName?: string;
locationLabel?: string;
cityName?: string;
contractType?: string;
contractLabel?: string;
urlOffre?: string; // <-- AJOUTEZ CETTE LIGNE
// Ajoutez d'autres champs si votre modèle Prisma JobOffer en contient
description: string;
publicationDate: string;
urlOffre?: string;
romeLabel?: string;
postalCode?: string;
departmentCode?: string;
}
export interface JobSearchResponse {