Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
3d77b8a
Add authentication pages and update routing for login and account man…
Noemie-Koelblen Mar 26, 2026
1921d96
Add logout functionality and update account management UI
Noemie-Koelblen Mar 26, 2026
11a279b
Implement RequireAuth component for protected admin routes
Noemie-Koelblen Mar 26, 2026
8825fbe
Refactor authentication flow: update login functionality, remove unus…
Noemie-Koelblen Mar 26, 2026
a190f70
Remove sensitive environment variables and update .gitignore to inclu…
Noemie-Koelblen Mar 26, 2026
f2deb68
Refactor authentication routes and add URL utility function for clean…
Noemie-Koelblen Mar 26, 2026
6245f3e
Implement centralized authentication context and refactor authenticat…
Noemie-Koelblen Mar 27, 2026
3f31990
Refactor account page: implement user info fetching and add PasswordF…
Noemie-Koelblen Mar 27, 2026
33b16d7
Refactor password field component: add required prop support and upda…
Noemie-Koelblen Mar 27, 2026
16f93a4
Add password reset functionality: implement RequestResetPassword page…
Noemie-Koelblen Mar 27, 2026
10373be
Refactor and enhance components: update routing comments for clarity,…
Noemie-Koelblen Mar 27, 2026
64d520d
Add Snackbar and Alert components for error and success messages in a…
Noemie-Koelblen Mar 27, 2026
f995dc6
Refactor Alert components: change variant from 'filled' to 'outlined'…
Noemie-Koelblen Mar 27, 2026
96e0e85
Implement custom Snackbar components and enhance password field funct…
Noemie-Koelblen Mar 28, 2026
6e58772
Refactor authentication pages: move login, logout, request reset, and…
Noemie-Koelblen Mar 28, 2026
08f9faa
Add CustomDialog component and refactor AdminHeader to use it; enhanc…
Noemie-Koelblen Mar 28, 2026
b741704
Refactor authentication structure: move LogoutPage to 'auth' director…
Noemie-Koelblen Mar 28, 2026
9f4bc36
Refactor admin header and account menu: integrate IconButton for navi…
Noemie-Koelblen Mar 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .env

This file was deleted.

3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
VITE_API_URL = http://localhost:4000
VITE_LOGIN_ROUTE = /login
VITE_ENVIRONMENT = development # accept "development", "staging", "production"
3 changes: 0 additions & 3 deletions .env.production

This file was deleted.

3 changes: 0 additions & 3 deletions .env.staging

This file was deleted.

3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# Logs
logs
*.log
npm-debug.log*
Expand All @@ -23,3 +22,5 @@ dist-ssr
*.njsproj
*.sln
*.sw?

.env
76 changes: 51 additions & 25 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,63 @@
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";

import Home from "./pages/public/homePage";
import PublicLayout from "./layout/public/publicLayout";
import AdminLayout from "./layout/admin/adminLayout";
import Dashboard from "./pages/admin/dashboardPage";
import Account from "./pages/admin/accountPage";
import Login from "./pages/auth/loginPage";
import RequestResetPassword from "./pages/auth/requestResetPasswordPage";
import ResetPassword from "./pages/auth/resetPasswordPage";
import LogoutPage from "./pages/auth/logoutPage";
import RequireAuth from "./pages/admin/requireAuth";
import { LOGIN_ROUTE } from "./constants/apiConstants";
import { AuthProvider } from "./context/AuthContext";

export default function App() {
return (
<Router>
<Routes>
{/* FrontOffice routes */}
<Route
path="/*"
element={
<PublicLayout>
<Routes>
<Route path="/" element={<Home />} />
</Routes>
</PublicLayout>
}
/>
{/* Admin routes */}
<Route
path="/admin/*"
element={
<AdminLayout>
<Routes>
<Route path="/" element={<Dashboard />} />
</Routes>
</AdminLayout>
}
/>
</Routes>
<AuthProvider>
<Routes>
{/* Routes publiques */}
<Route
path="/*"
element={
<PublicLayout>
<Routes>
<Route path="/" element={<Home />} />
</Routes>
</PublicLayout>
}
/>

{/* Routes d'authentification */}
<Route path={`/${LOGIN_ROUTE}`} element={<Login />} />
<Route path="/logout" element={<LogoutPage />} />
<Route
path="/request-reset-password"
element={<RequestResetPassword />}
/>
<Route
path="/request-reset-password"
element={<RequestResetPassword />}
/>
<Route path="/reset-password" element={<ResetPassword />} />

{/* Routes admin */}
<Route
path="/admin/*"
element={
<RequireAuth>
<AdminLayout>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/account" element={<Account />} />
</Routes>
</AdminLayout>
</RequireAuth>
}
/>
</Routes>
</AuthProvider>
</Router>
);
}
8 changes: 5 additions & 3 deletions src/components/ResponsiveLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import Box, { type BoxProps } from "@mui/material/Box";
import Stack, { type StackProps } from "@mui/material/Stack";
import Paper, { type PaperProps } from "@mui/material/Paper";
import ImageList, { type ImageListProps } from "@mui/material/ImageList";
import type { ResponsiveLayoutProps } from "../types/responsiveComponents";
import type { ResponsiveLayoutProps } from "../types/responsiveTypes";
import { getResponsiveSx } from "../utils/responsiveUtils";

/**
* Composant générique pour appliquer un layout responsive à n'importe quel composant MUI
* @param Component Le composant cible (Box, Stack, Paper, etc.)
* @param props Les props du composant cible + marginY/paddingY/rowGap
* @param {ComponentProps} Component Le composant cible (Box, Stack, Paper, etc.)
* @param {number | string} props.marginY Marge verticale (peut être un nombre multiplié par 8px ou une string CSS)
* @param {number | string} props.paddingY Padding vertical (peut être un nombre multiplié par 8px ou une string CSS)
* @param {number | string} props.rowGap Espace entre les lignes (peut être un nombre multiplié par 8px ou une string CSS)
*/
export function ResponsiveLayout<ComponentProps extends { sx?: any }>(
Component: React.ElementType,
Expand Down
58 changes: 58 additions & 0 deletions src/components/accountMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { mdiAccount } from "@mdi/js";
import Icon from "@mdi/react";
import { Button, IconButton, Menu, MenuItem } from "@mui/material";
import CustomDialog from "./customDialog";
import { Link, useLocation } from "react-router";
import { useState } from "react";

export default function AccountMenu() {
const [logoutConfirm, setLogoutConfirm] = useState(false);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const handleCloseDialog = () => {
setLogoutConfirm(false);
setAnchorEl(null);
};
const location = useLocation();

return (
<>
<IconButton onClick={(e) => setAnchorEl(e.currentTarget)} color="inherit">
<Icon path={mdiAccount} size={1} />
</IconButton>
<Menu
id="account-menu"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={() => setAnchorEl(null)}
>
<MenuItem
component={Link}
to="/admin/account"
onClick={() => setAnchorEl(null)}
selected={location.pathname === "/admin/account"}
>
Mon compte
</MenuItem>
<MenuItem onClick={() => setLogoutConfirm(true)}>
Me déconnecter
</MenuItem>
</Menu>
<CustomDialog
open={logoutConfirm}
onClose={handleCloseDialog}
title="Voulez-vous vous déconnecter ?"
content="Vous devrez vous reconnecter pour accéder à nouveau à l'espace administrateur."
actions={
<>
<Button variant="text" onClick={handleCloseDialog}>
Annuler
</Button>
<Button component={Link} to="/logout" onClick={handleCloseDialog}>
Me déconnecter
</Button>
</>
}
/>
</>
);
}
44 changes: 44 additions & 0 deletions src/components/closableSnackbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { type SnackbarCloseReason } from "@mui/material";
import CustomSnackbar from "./customSnackBar";
import type { ClosableSnackbarProps } from "../types/baseComponent";

/**
* Composant de snackbar personnalisée avec possibilité de fermeture manuelle ou automatique.
* Permet d'afficher des messages de succès, d'erreur, d'avertissement ou d'information à l'utilisateur, avec une gestion intégrée de l'ouverture et de la fermeture du snackbar.
* Utilise le composant CustomSnackbar pour afficher le message avec le style approprié en fonction de la gravité du message.
* @param {boolean} props.open Indique si le snackbar est ouvert ou fermé.
* @param {function} props.setOpen Fonction pour mettre à jour l'état d'ouverture du snackbar.
* @param {string} props.message Le message à afficher dans le snackbar.
* @param {function} props.onClose Fonction optionnelle à appeler lors de la fermeture du snackbar.
* @param {"success" | "error" | "warning" | "info"} props.severity La gravité du message, qui détermine le style du snackbar.
* @param {number} props.autohideDuration Durée en millisecondes avant que le snackbar ne se ferme automatiquement. Par défaut, 6000 ms (6 secondes).
*/
export default function ClosableSnackbar({
open,
setOpen,
message,
onClose,
severity,
autohideDuration = 6000,
}: ClosableSnackbarProps) {
// Fonction de gestion de la fermeture du snackbar, qui peut être déclenchée manuellement ou automatiquement, et qui ignore les fermetures dues à un clic à l'extérieur du snackbar (clickaway).
const handleClose = (
_: React.SyntheticEvent | Event,
reason?: SnackbarCloseReason,
) => {
if (reason === "clickaway") {
return;
}
setOpen(false);
};

return (
<CustomSnackbar
open={open}
message={message}
severity={severity}
onClose={onClose || handleClose}
autohideDuration={autohideDuration}
/>
);
}
35 changes: 35 additions & 0 deletions src/components/customDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
} from "@mui/material";

export default function CustomDialog({
open,
onClose,
title,
content,
actions,
}: {
open: boolean;
onClose?: () => void;
content: string;
title?: string;
actions?: React.ReactNode;
}) {
return (
<Dialog
open={open}
onClose={onClose}
slotProps={{ paper: { elevation: 1 } }}
>
{title && <DialogTitle>{title}</DialogTitle>}
<DialogContent>
<DialogContentText>{content}</DialogContentText>
</DialogContent>
{actions && <DialogActions>{actions}</DialogActions>}
</Dialog>
);
}
28 changes: 28 additions & 0 deletions src/components/customSnackBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Alert, Snackbar } from "@mui/material";
import type { CustomSnackbarProps } from "../types/baseComponent";

/**
* Composant de snackbar personnalisée pour afficher des messages de succès, d'erreur, d'avertissement ou d'information à l'utilisateur.
* Utilise les composants Snackbar et Alert de Material-UI pour afficher le message avec le style approprié en fonction de la gravité du message.
* Permet de personnaliser le message, la gravité, la durée d'affichage et la fonction de fermeture du snackbar.
* @param {boolean} props.open Indique si le snackbar est ouvert ou fermé.
* @param {string} props.message Le message à afficher dans le snackbar.
* @param {"success" | "error" | "warning" | "info"} props.severity La gravité du message, qui détermine le style du snackbar.
* @param {function} props.onClose Fonction optionnelle à appeler lors de la fermeture du snackbar.
* @param {number} props.autohideDuration Durée en millisecondes avant que le snackbar ne se ferme automatiquement. Par défaut, 6000 ms (6 secondes).
*/
export default function CustomSnackbar({
open,
message,
severity,
onClose,
autohideDuration,
}: CustomSnackbarProps) {
return (
<Snackbar open={open} autoHideDuration={autohideDuration} onClose={onClose}>
<Alert severity={severity} onClose={onClose}>
{message}
</Alert>
</Snackbar>
);
}
90 changes: 90 additions & 0 deletions src/components/newPasswordFields.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import type { NewPasswordFieldsProps } from "../types/baseComponent";
import PasswordField from "./passwordField";

/**
* Composant pour les champs de saisie du nouveau mot de passe et de sa confirmation.
* Gère la validation du mot de passe et l'affichage des erreurs associées.
* @param props.newPassword Le mot de passe saisi par l'utilisateur.
* @param props.setNewPassword Fonction pour mettre à jour le mot de passe saisi.
* @param props.confirmPassword Le mot de passe de confirmation saisi par l'utilisateur.
* @param props.setConfirmPassword Fonction pour mettre à jour le mot de passe de confirmation.
* @param props.newPasswordError Message d'erreur lié au mot de passe saisi.
* @param props.setNewPasswordError Fonction pour mettre à jour le message d'erreur du mot de passe.
* @param props.confirmPasswordError Message d'erreur lié au mot de passe de confirmation.
* @param props.setConfirmPasswordError Fonction pour mettre à jour le message d'erreur du mot de passe de confirmation.
*/
export default function NewPasswordFields({
newPassword,
setNewPassword,
confirmPassword,
setConfirmPassword,
newPasswordError,
setNewPasswordError,
confirmPasswordError,
setConfirmPasswordError,
}: NewPasswordFieldsProps) {
// Fonction de validation du mot de passe selon les critères définis.
const validatePassword = (pwd: string) => {
if (!pwd) return "";
if (pwd.length < 20) {
return "Au moins 20 caractères.";
}
if (!/[A-Z]/.test(pwd)) {
return "Au moins une majuscule.";
}
if (!/[a-z]/.test(pwd)) {
return "Au moins une minuscule.";
}
if (!/[0-9]/.test(pwd)) {
return "Au moins un chiffre.";
}
if (!/[^A-Za-z0-9]/.test(pwd)) {
return "Au moins un caractère spécial.";
}
return "";
};

// Gestion du changement de valeur du champ de nouveau mot de passe, avec validation en temps réel.
const handleNewPasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setNewPassword(value);
const validationMsg = validatePassword(value);
setNewPasswordError(validationMsg);
if (value !== confirmPassword) {
setConfirmPasswordError("Les mots de passe doivent être identiques.");
} else {
setConfirmPasswordError("");
}
};

// Gestion du changement de valeur du champ de confirmation du mot de passe, avec validation en temps réel.
const handleConfirmPasswordChange = (
e: React.ChangeEvent<HTMLInputElement>,
) => {
setConfirmPassword(e.target.value);
if (newPassword && e.target.value !== newPassword) {
setConfirmPasswordError("Les mots de passe doivent être identiques.");
} else {
setConfirmPasswordError("");
}
};

return (
<>
<PasswordField
label="Nouveau mot de passe"
value={newPassword}
onChange={handleNewPasswordChange}
error={!!newPasswordError}
errorText={newPasswordError}
/>
<PasswordField
label="Confirmer le nouveau mot de passe"
value={confirmPassword}
onChange={handleConfirmPasswordChange}
error={!!confirmPasswordError}
errorText={confirmPasswordError}
/>
</>
);
}
Loading