This commit is contained in:
quasar 2025-05-30 13:39:38 +02:00
parent e1409cb8ca
commit 3883f0cef6
5 changed files with 128 additions and 81 deletions

View file

@ -1,11 +1,9 @@
```typescript
// job/frontend/src/App.tsx // job/frontend/src/App.tsx
import React from 'react'; import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import JobList from './components/JobList'; import JobSearch from './components/JobSearch';
import JobDetail from './components/JobDetail'; import JobDetail from './components/JobDetail';
// import Navbar from './components/Navbar'; // <-- SUPPRIMER CET IMPORT import Sidebar from './components/Sidebar';
import Sidebar from './components/Sidebar'; // Garde cet import
import { ThemeProvider, createTheme } from '@mui/material/styles'; import { ThemeProvider, createTheme } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline'; import CssBaseline from '@mui/material/CssBaseline';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
@ -35,7 +33,7 @@ const customTheme = createTheme({
button: { textTransform: 'none' }, button: { textTransform: 'none' },
}, },
components: { components: {
MuiAppBar: { // Optionnel : Si vous voulez garder une AppBar très minimale pour des actions spécifiques (sans navigation) MuiAppBar: {
styleOverrides: { styleOverrides: {
colorPrimary: { colorPrimary: {
backgroundColor: '#FFFFFF', color: '#212121', boxShadow: '0px 1px 4px rgba(0, 0, 0, 0.04)', backgroundColor: '#FFFFFF', color: '#212121', boxShadow: '0px 1px 4px rgba(0, 0, 0, 0.04)',
@ -94,28 +92,21 @@ const App: React.FC = () => {
<ThemeProvider theme={customTheme}> <ThemeProvider theme={customTheme}>
<CssBaseline /> <CssBaseline />
<Router> <Router>
<Box sx={{ display: 'flex', minHeight: '100vh', backgroundColor: 'background.default' }}> {/* Conteneur principal flex */} <Box sx={{ display: 'flex', minHeight: '100vh', backgroundColor: 'background.default' }}>
{/* Sidebar : elle prend sa largeur fixe */}
<Sidebar drawerWidth={drawerWidth} /> <Sidebar drawerWidth={drawerWidth} />
{/* Contenu principal : prend l'espace restant et a une marge à gauche */}
<Box <Box
component="main" component="main"
sx={{ sx={{
flexGrow: 1, // Prend l'espace restant flexGrow: 1,
p: { xs: 2, md: 4 }, // Padding général pour le contenu (réactif) p: { xs: 2, md: 4 },
width: { sm: `calc(100% - ${drawerWidth}px)` }, // Prend toute la largeur moins la sidebar sur desktop width: { sm: '100%' },
ml: { sm: `${drawerWidth}px` }, // Marge à gauche égale à la largeur de la sidebar sur desktop ml: { sm: drawerWidth },
// Pas besoin de mt (margin-top) car il n'y a plus de Navbar en haut.
// S'assure que le contenu peut défiler indépendamment si trop long
overflowY: 'auto', overflowY: 'auto',
backgroundColor: 'background.default', // Couleur de fond du contenu backgroundColor: 'background.default',
}} }}
> >
{/* Ici seront rendues vos routes de contenu */}
<Routes> <Routes>
<Route path="/" element={<JobList />} /> <Route path="/" element={<JobSearch />} />
<Route path="/jobs/:id" element={<JobDetail />} /> <Route path="/jobs/:id" element={<JobDetail />} />
</Routes> </Routes>
</Box> </Box>
@ -126,4 +117,3 @@ const App: React.FC = () => {
}; };
export default App; export default App;
```

View file

@ -7,8 +7,11 @@ import {
Button, // Bouton "Postuler" Button, // Bouton "Postuler"
CircularProgress, // Indicateur de chargement CircularProgress, // Indicateur de chargement
Paper, // Pour encadrer le contenu de l'offre Paper, // Pour encadrer le contenu de l'offre
IconButton,
} from '@mui/material'; } from '@mui/material';
import ArrowBackIcon from '@mui/icons-material/ArrowBack'; // Icône pour le bouton retour import ArrowBackIcon from '@mui/icons-material/ArrowBack'; // Icône pour le bouton retour
import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
import FavoriteIcon from '@mui/icons-material/Favorite';
import type { JobOffer } from '../types'; // Use type-only import import type { JobOffer } from '../types'; // Use type-only import
@ -19,6 +22,7 @@ const JobDetail: React.FC = () => {
const [job, setJob] = useState<JobOffer | null>(null); const [job, setJob] = useState<JobOffer | null>(null);
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 [isFavorite, setIsFavorite] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
const fetchJobDetail = async () => { const fetchJobDetail = async () => {
@ -27,12 +31,15 @@ const JobDetail: React.FC = () => {
try { try {
const response = await axios.get<JobOffer>(`${API_BASE_URL}/${id}`); const response = await axios.get<JobOffer>(`${API_BASE_URL}/${id}`);
setJob(response.data); setJob(response.data);
// Vérifie si l'offre est dans les favoris
const favorites = JSON.parse(localStorage.getItem('jobFavorites') || '[]');
setIsFavorite(favorites.includes(response.data.id));
} catch (err) { } catch (err) {
console.error('Error fetching job detail:', err); console.error('Erreur lors de la récupération des détails de l\'offre:', err);
if (axios.isAxiosError(err) && err.response?.status === 404) { if (axios.isAxiosError(err) && err.response?.status === 404) {
setError('Job offer not found.'); setError('Offre d\'emploi non trouvée.');
} else { } else {
setError('Failed to load job offer details. Please try again.'); setError('Impossible de charger les détails de l\'offre. Veuillez réessayer.');
} }
} finally { } finally {
setLoading(false); setLoading(false);
@ -46,7 +53,7 @@ const JobDetail: React.FC = () => {
const handleApplyClick = () => { const handleApplyClick = () => {
if (job?.urlOffre) { if (job?.urlOffre) {
window.open(job.urlOffre, '_blank'); // Ouvre l'URL dans un nouvel onglet window.open(job.urlOffre, '_blank', 'noopener,noreferrer');
} else { } else {
// Utilisation d'un alert MUI ou d'un SnackBar serait mieux ici, // Utilisation d'un alert MUI ou d'un SnackBar serait mieux ici,
// mais pour l'instant, gardons l'alert JS pour la simplicité. // mais pour l'instant, gardons l'alert JS pour la simplicité.
@ -54,6 +61,21 @@ const JobDetail: React.FC = () => {
} }
}; };
const handleToggleFavorite = () => {
if (!job) return;
let favorites = JSON.parse(localStorage.getItem('jobFavorites') || '[]');
if (isFavorite) {
// Supprimer des favoris
favorites = favorites.filter((favId: string) => favId !== job.id);
} else {
// Ajouter aux favoris
favorites.push(job.id);
}
localStorage.setItem('jobFavorites', JSON.stringify(favorites));
setIsFavorite(!isFavorite);
};
if (loading) { if (loading) {
return ( return (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '80vh' }}> <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '80vh' }}>
@ -109,8 +131,28 @@ const JobDetail: React.FC = () => {
borderRadius: '12px', // Bords arrondis borderRadius: '12px', // Bords arrondis
boxShadow: '0px 4px 10px rgba(0, 0, 0, 0.05)', // Ombre douce boxShadow: '0px 4px 10px rgba(0, 0, 0, 0.05)', // Ombre douce
backgroundColor: 'background.paper', // Fond blanc du Paper backgroundColor: 'background.paper', // Fond blanc du Paper
position: 'relative',
}}> }}>
<Typography variant="h4" component="h1" gutterBottom sx={{ color: 'text.primary' }}> {/* Bouton Favori */}
<IconButton
aria-label={isFavorite ? "Retirer des favoris" : "Ajouter aux favoris"}
onClick={handleToggleFavorite}
sx={{
position: 'absolute',
top: 16,
right: 16,
color: isFavorite ? 'secondary.main' : 'text.secondary',
transition: 'transform 0.2s ease-in-out',
'&:hover': {
transform: 'scale(1.1)',
color: isFavorite ? 'secondary.dark' : 'secondary.main',
},
}}
>
{isFavorite ? <FavoriteIcon /> : <FavoriteBorderIcon />}
</IconButton>
<Typography variant="h4" component="h1" gutterBottom sx={{ color: 'text.primary', pr: 6 }}>
{job.title} {job.title}
</Typography> </Typography>
<Typography variant="body1" sx={{ mb: 1.5, color: 'text.secondary' }}> <Typography variant="body1" sx={{ mb: 1.5, color: 'text.secondary' }}>

View file

@ -1,33 +0,0 @@
// job/frontend/src/components/Navbar.tsx
import React from 'react';
import { Link } from 'react-router-dom';
import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
const Navbar: React.FC = () => {
return (
<AppBar position="static">
<Toolbar>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
<Link to="/" style={{ textDecoration: 'none', color: 'inherit' }}>
JobFinder
</Link>
</Typography>
<Button color="inherit" component={Link} to="/">
Rechercher
</Button>
{/* FUTURE: Ajoutez d'autres boutons ici pour Favoris, Connexion, etc. */}
{/* <Button color="inherit" component={Link} to="/favorites">
Mes Favoris
</Button> */}
{/* <Button color="inherit" component={Link} to="/login">
Connexion
</Button> */}
</Toolbar>
</AppBar>
);
};
export default Navbar;

View file

@ -36,58 +36,107 @@ const Sidebar: React.FC<SidebarProps> = ({ drawerWidth }) => {
boxSizing: 'border-box', boxSizing: 'border-box',
backgroundColor: 'background.paper', // Fond blanc du thème backgroundColor: 'background.paper', // Fond blanc du thème
boxShadow: '0px 2px 8px rgba(0, 0, 0, 0.05)', // Ombre douce boxShadow: '0px 2px 8px rgba(0, 0, 0, 0.05)', // Ombre douce
borderRadius: '0 8px 8px 0', // Bords arrondis seulement à droite borderRight: 'none',
overflowX: 'hidden', // Empêche le débordement horizontal 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
> >
{/* Un Toolbar pour s'aligner avec la Navbar en haut */} {/* Section du haut de la Sidebar (similaire à Notion) */}
{/* Peut être utilisé pour un logo ou un titre dans la sidebar si vous enlevez le titre de la Navbar */} <Box sx={{ p: 2, display: 'flex', alignItems: 'center', mb: 1 }}>
<Toolbar sx={{ <Typography variant="h6" sx={{ fontWeight: 600, color: 'text.primary' }}>
backgroundColor: 'primary.main', // Assurez-vous que cette couleur correspond à la Navbar ou est transparente JobIA
minHeight: { xs: '56px', sm: '64px' }, // Hauteur de la Navbar
}}>
{/* Laissez vide ou ajoutez un logo/titre de l'application ici pour une disposition Notion-like */}
<Typography variant="h6" noWrap component="div" sx={{ color: 'primary.contrastText', display: 'flex', alignItems: 'center' }}>
<img src="/logo-jobfinder.png" alt="JobFinder Logo" style={{ height: '30px', marginRight: '10px' }} /> {/* Si vous avez un logo */}
JobFinder
</Typography> </Typography>
</Toolbar> </Box>
<Divider /> {/* Ligne de séparation */} <Divider sx={{ mb: 1 }} />
<Box sx={{ overflow: 'auto' }}> {/* Permet le défilement si le contenu dépasse */} <Box sx={{ overflow: 'auto' }}> {/* Permet le défilement si le contenu dépasse */}
<List> <List sx={{ px: 1 }}>
{/* Lien vers la page de recherche */} {/* Lien vers la page de recherche */}
<ListItem disablePadding> <ListItem disablePadding>
<ListItemButton component={RouterLink} to="/"> <ListItemButton
component={RouterLink}
to="/"
sx={{
borderRadius: '6px',
mb: 0.5,
'&:hover': {
backgroundColor: 'rgba(0, 0, 0, 0.04)',
},
}}
>
<ListItemIcon> <ListItemIcon>
<SearchIcon sx={{ color: 'text.secondary' }} /> {/* Icône de recherche */} <SearchIcon sx={{ color: 'text.secondary' }} /> {/* Icône de recherche */}
</ListItemIcon> </ListItemIcon>
<ListItemText primary="Rechercher" sx={{ color: 'text.primary' }} /> <ListItemText
primary="Rechercher"
sx={{
color: 'text.primary',
'& .MuiTypography-root': {
fontWeight: 500,
},
}}
/>
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
{/* FUTURS LIENS DE LA PHASE 3 */} {/* FUTURS LIENS DE LA PHASE 3 */}
{/* <ListItem disablePadding> {/* <ListItem disablePadding>
<ListItemButton component={RouterLink} to="/favorites"> <ListItemButton
component={RouterLink}
to="/favorites"
sx={{
borderRadius: '6px',
mb: 0.5,
'&:hover': {
backgroundColor: 'rgba(0, 0, 0, 0.04)',
},
}}
>
<ListItemIcon> <ListItemIcon>
<StarIcon sx={{ color: 'text.secondary' }} /> <StarIcon sx={{ color: 'text.secondary' }} />
</ListItemIcon> </ListItemIcon>
<ListItemText primary="Mes Favoris" sx={{ color: 'text.primary' }} /> <ListItemText
primary="Mes Favoris"
sx={{
color: 'text.primary',
'& .MuiTypography-root': {
fontWeight: 500,
},
}}
/>
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
<ListItem disablePadding> <ListItem disablePadding>
<ListItemButton component={RouterLink} to="/account"> <ListItemButton
component={RouterLink}
to="/account"
sx={{
borderRadius: '6px',
mb: 0.5,
'&:hover': {
backgroundColor: 'rgba(0, 0, 0, 0.04)',
},
}}
>
<ListItemIcon> <ListItemIcon>
<AccountCircleIcon sx={{ color: 'text.secondary' }} /> <AccountCircleIcon sx={{ color: 'text.secondary' }} />
</ListItemIcon> </ListItemIcon>
<ListItemText primary="Mon Compte" sx={{ color: 'text.primary' }} /> <ListItemText
primary="Mon Compte"
sx={{
color: 'text.primary',
'& .MuiTypography-root': {
fontWeight: 500,
},
}}
/>
</ListItemButton> </ListItemButton>
</ListItem> */} </ListItem> */}
</List> </List>
<Divider /> <Divider sx={{ mt: 1 }} />
{/* Vous pouvez ajouter d'autres sections de liens ici */} {/* Vous pouvez ajouter d'autres sections de liens ici */}
</Box> </Box>
</Drawer> </Drawer>

View file

@ -19,7 +19,6 @@
"strict": true, "strict": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true "noUncheckedSideEffectImports": true
}, },