maj du frontend
This commit is contained in:
parent
0585ff56fd
commit
1c8d960b52
8 changed files with 281 additions and 26 deletions
|
@ -71,4 +71,31 @@ export const searchLocalJobOffers = async (req: Request, res: Response) => {
|
|||
console.error('Error searching job offers:', error);
|
||||
res.status(500).json({ error: 'Failed to search job offers' });
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const getJobOfferById = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { id } = req.params; // Récupère l'ID depuis les paramètres de l'URL
|
||||
|
||||
if (!id) {
|
||||
res.status(400).json({ error: 'Job offer ID is required.' });
|
||||
return;
|
||||
}
|
||||
|
||||
const job = await prisma.jobOffer.findUnique({
|
||||
where: { id: id },
|
||||
});
|
||||
|
||||
if (!job) {
|
||||
res.status(404).json({ error: 'Job offer not found.' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json(job);
|
||||
} catch (error) {
|
||||
console.error('Error fetching job offer by ID:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch job offer details.' });
|
||||
} finally {
|
||||
await prisma.$disconnect(); // Assurez-vous de déconnecter Prisma
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import express from 'express';
|
||||
import { searchLocalJobOffers } from '../controllers/jobSearchController';
|
||||
import { Router } from 'express';
|
||||
import { searchLocalJobOffers, getJobOfferById } from '../controllers/jobSearchController';
|
||||
|
||||
const router = express.Router();
|
||||
const router = Router();
|
||||
|
||||
// Route pour la recherche d'offres (existante)
|
||||
router.get('/api/jobs', searchLocalJobOffers);
|
||||
|
||||
// Nouvelle route pour récupérer une offre par ID
|
||||
router.get('/api/jobs/:id', getJobOfferById);
|
||||
|
||||
export default router;
|
||||
|
|
87
frontend/package-lock.json
generated
87
frontend/package-lock.json
generated
|
@ -10,13 +10,15 @@
|
|||
"dependencies": {
|
||||
"axios": "^1.6.8",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
"react-dom": "^19.1.0",
|
||||
"react-router-dom": "^7.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.25.0",
|
||||
"@types/axios": "^0.14.0",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
|
@ -1409,6 +1411,13 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/history": {
|
||||
"version": "4.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz",
|
||||
"integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
|
@ -1436,6 +1445,29 @@
|
|||
"@types/react": "^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-router": {
|
||||
"version": "5.1.20",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz",
|
||||
"integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/history": "^4.7.11",
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-router-dom": {
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz",
|
||||
"integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/history": "^4.7.11",
|
||||
"@types/react": "*",
|
||||
"@types/react-router": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz",
|
||||
|
@ -1937,6 +1969,15 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
|
@ -3144,6 +3185,44 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.6.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.1.tgz",
|
||||
"integrity": "sha512-hPJXXxHJZEsPFNVbtATH7+MMX43UDeOauz+EAU4cgqTn7ojdI9qQORqS8Z0qmDlL1TclO/6jLRYUEtbWidtdHQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "^1.0.1",
|
||||
"set-cookie-parser": "^2.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "7.6.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.1.tgz",
|
||||
"integrity": "sha512-vxU7ei//UfPYQ3iZvHuO1D/5fX3/JOqhNTbRR+WjSBWxf9bIvpWK+ftjmdfJHzPOuMQKe2fiEdG+dZX6E8uUpA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-router": "7.6.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-from": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||
|
@ -3245,6 +3324,12 @@
|
|||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
||||
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
|
|
|
@ -9,16 +9,18 @@
|
|||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"dependencies": {
|
||||
"axios": "^1.6.8",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
"react-dom": "^19.1.0",
|
||||
"react-router-dom": "^7.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.25.0",
|
||||
"@types/axios": "^0.14.0",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
import React from 'react';
|
||||
import JobSearch from './components/JobSearch'; // Importe le nouveau composant
|
||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; // Importe les composants de routage
|
||||
import JobSearch from './components/JobSearch';
|
||||
import JobDetail from './components/JobDetail'; // Importe le nouveau composant de détail
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="App">
|
||||
<JobSearch /> {/* Utilise le composant de recherche d'emploi */}
|
||||
</div>
|
||||
<Router> {/* Enveloppe toute l'application dans le routeur */}
|
||||
<div className="App">
|
||||
<Routes> {/* Définit les routes */}
|
||||
<Route path="/" element={<JobSearch />} /> {/* Route pour la recherche d'offres */}
|
||||
<Route path="/jobs/:id" element={<JobDetail />} /> {/* Route pour le détail d'une offre */}
|
||||
</Routes>
|
||||
</div>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
122
frontend/src/components/JobDetail.tsx
Normal file
122
frontend/src/components/JobDetail.tsx
Normal file
|
@ -0,0 +1,122 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, Link } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import type { JobOffer } from '../types'; // Use type-only import
|
||||
|
||||
const API_BASE_URL = 'http://localhost:3000/api/jobs';
|
||||
|
||||
const JobDetail: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [job, setJob] = useState<JobOffer | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchJobDetail = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await axios.get<JobOffer>(`${API_BASE_URL}/${id}`);
|
||||
setJob(response.data);
|
||||
} catch (err) {
|
||||
console.error('Error fetching job detail:', err);
|
||||
if (axios.isAxiosError(err) && err.response?.status === 404) {
|
||||
setError('Job offer not found.');
|
||||
} else {
|
||||
setError('Failed to load job offer details. Please try again.');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (id) {
|
||||
fetchJobDetail();
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
const handleApplyClick = () => {
|
||||
// Logique pour trouver la meilleure URL de candidature
|
||||
const applyUrl = job?.urlOffre || job?.contact?.urlPostulation || job?.origineOffre?.urlOrigine;
|
||||
|
||||
if (applyUrl) {
|
||||
window.open(applyUrl, '_blank'); // Ouvre l'URL dans un nouvel onglet
|
||||
} else {
|
||||
alert("No application URL available for this job offer.");
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <p className="info-message">Loading job details...</p>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="job-detail-container">
|
||||
<p className="info-message" style={{ color: 'red' }}>Error: {error}</p>
|
||||
<Link to="/" style={{ display: 'block', textAlign: 'center', marginTop: '20px' }}>Back to Search</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!job) {
|
||||
return (
|
||||
<div className="job-detail-container">
|
||||
<p className="info-message">No job details available.</p>
|
||||
<Link to="/" style={{ display: 'block', textAlign: 'center', marginTop: '20px' }}>Back to Search</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="job-detail-container" style={{ padding: '20px', maxWidth: '800px', margin: '20px auto', backgroundColor: '#fff', borderRadius: '8px', boxShadow: '0 4px 8px rgba(0,0,0,0.1)' }}>
|
||||
<Link to="/" style={{ textDecoration: 'none', color: '#3498db', fontWeight: 'bold', marginBottom: '20px', display: 'inline-block' }}>
|
||||
← Back to Job Search
|
||||
</Link>
|
||||
<h1 style={{ color: '#2c3e50', marginBottom: '10px' }}>{job.title}</h1>
|
||||
<p style={{ fontSize: '1.1em', color: '#555', marginBottom: '15px' }}>
|
||||
<strong>Company:</strong> {job.companyName || 'N/A'}
|
||||
</p>
|
||||
<p style={{ fontSize: '1.1em', color: '#555', marginBottom: '15px' }}>
|
||||
<strong>Location:</strong> {job.locationLabel || job.cityName || 'N/A'}
|
||||
</p>
|
||||
<p style={{ fontSize: '1.1em', color: '#555', marginBottom: '15px' }}>
|
||||
<strong>Contract:</strong> {job.contractLabel || job.contractType || 'N/A'}
|
||||
</p>
|
||||
<p style={{ fontSize: '1.1em', color: '#555', marginBottom: '15px' }}>
|
||||
<strong>Published:</strong> {new Date(job.publicationDate).toLocaleDateString()}
|
||||
</p>
|
||||
{job.romeLabel && <p style={{ fontSize: '1.1em', color: '#555', marginBottom: '15px' }}><strong>ROME:</strong> {job.romeLabel}</p>}
|
||||
{job.postalCode && <p style={{ fontSize: '1.1em', color: '#555', marginBottom: '15px' }}><strong>Postal Code:</strong> {job.postalCode}</p>}
|
||||
{job.departmentCode && <p style={{ fontSize: '1.1em', color: '#555', marginBottom: '15px' }}><strong>Department Code:</strong> {job.departmentCode}</p>}
|
||||
|
||||
<h2 style={{ color: '#2c3e50', marginTop: '30px', marginBottom: '10px' }}>Description</h2>
|
||||
<p style={{ lineHeight: '1.6', whiteSpace: 'pre-wrap' }}>{job.description}</p>
|
||||
|
||||
{/* Bouton Postuler */}
|
||||
{(job.urlOffre || job.contact?.urlPostulation || job.origineOffre?.urlOrigine) && (
|
||||
<button
|
||||
onClick={handleApplyClick}
|
||||
style={{
|
||||
backgroundColor: '#28a745', // Vert pour le bouton "Postuler"
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '5px',
|
||||
padding: '12px 25px',
|
||||
fontSize: '1.1em',
|
||||
cursor: 'pointer',
|
||||
marginTop: '30px',
|
||||
display: 'block',
|
||||
width: 'fit-content',
|
||||
margin: '30px auto 0 auto',
|
||||
transition: 'background-color 0.3s ease',
|
||||
}}
|
||||
>
|
||||
Apply Now
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default JobDetail;
|
|
@ -1,8 +1,9 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import { Link } from 'react-router-dom';
|
||||
import type { JobOffer, JobSearchResponse } from '../types'; // Importe les types définis
|
||||
|
||||
const API_BASE_URL = 'http://localhost:3000/api/jobs'; // L'URL de votre API backend
|
||||
const API_BASE_URL = 'http://localhost:3000/api/jobs'; // L'URL de notre API backend
|
||||
|
||||
const JobSearch: React.FC = () => {
|
||||
const [jobs, setJobs] = useState<JobOffer[]>([]);
|
||||
|
@ -97,8 +98,6 @@ const JobSearch: React.FC = () => {
|
|||
<button type="submit">Search</button>
|
||||
</form>
|
||||
|
||||
{/* ... (le reste de votre JSX pour l'affichage des résultats et la pagination reste inchangé) ... */}
|
||||
|
||||
{loading && <p className="info-message">Loading job offers...</p>}
|
||||
{error && <p className="info-message" style={{ color: 'red' }}>Error: {error}</p>}
|
||||
|
||||
|
@ -111,16 +110,19 @@ const JobSearch: React.FC = () => {
|
|||
<p className="info-message">Total offers: {total}</p>
|
||||
<div className="job-grid">
|
||||
{jobs.map((job) => (
|
||||
<div key={job.id} className="job-card">
|
||||
<h3>{job.title}</h3>
|
||||
<p><strong>Company:</strong> {job.companyName || 'N/A'}</p>
|
||||
<p><strong>Location:</strong> {job.locationLabel || job.cityName || 'N/A'}</p>
|
||||
<p><strong>Contract:</strong> {job.contractLabel || job.contractType || 'N/A'}</p>
|
||||
<p><strong>Published:</strong> {new Date(job.publicationDate).toLocaleDateString()}</p>
|
||||
<p className="description">
|
||||
{job.description?.substring(0, 150)}...
|
||||
</p>
|
||||
</div>
|
||||
// Enveloppe la carte de l'offre avec un lien
|
||||
<Link to={`/jobs/${job.id}`} key={job.id} style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<div className="job-card">
|
||||
<h3>{job.title}</h3>
|
||||
<p><strong>Company:</strong> {job.companyName || 'N/A'}</p>
|
||||
<p><strong>Location:</strong> {job.locationLabel || job.cityName || 'N/A'}</p>
|
||||
<p><strong>Contract:</strong> {job.contractLabel || job.contractType || 'N/A'}</p>
|
||||
<p><strong>Published:</strong> {new Date(job.publicationDate).toLocaleDateString()}</p>
|
||||
<p className="description">
|
||||
{job.description?.substring(0, 150)}...
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -12,6 +12,13 @@ export interface JobOffer {
|
|||
companyName?: string;
|
||||
contractType?: string;
|
||||
contractLabel?: string;
|
||||
urlOffre?: string; // <-- Assurez-vous que ce champ existe
|
||||
contact?: { // Si vous avez ingéré les infos de contact
|
||||
urlPostulation?: string;
|
||||
};
|
||||
origineOffre?: { // Si vous avez ingéré les infos d'origine
|
||||
urlOrigine?: string;
|
||||
};
|
||||
// Ajoutez d'autres champs si votre modèle Prisma JobOffer en contient
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue