sidebar
This commit is contained in:
parent
e1409cb8ca
commit
3883f0cef6
5 changed files with 128 additions and 81 deletions
|
@ -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;
|
||||||
```
|
|
|
@ -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' }}>
|
||||||
|
|
|
@ -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;
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue