Merge branch 'refactor/profile-page' into 'dev'

Refactor/profile page

See merge request sae-but2/2025-26/gestion-benevoles-association/Frontend!83
This commit is contained in:
T'JAMPENS QUENTIN p2406187
2026-02-24 16:30:20 +00:00
13 changed files with 348 additions and 513 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ interface ButtonProps {
children: ReactNode; children: ReactNode;
variant?: ButtonVariant; variant?: ButtonVariant;
onClick?: MouseEventHandler<HTMLDivElement>; onClick?: MouseEventHandler<HTMLDivElement>;
className?: string; className?: string | undefined;
} }
const Button = ({ const Button = ({
+10 -7
View File
@@ -3,17 +3,20 @@
padding: 12px; padding: 12px;
box-sizing: border-box; box-sizing: border-box;
} }
.profileboard { .profileboard {
width: 100%; width: 100%;
padding: 16px; padding: 16px;
border-radius: 20px; border-radius: 20px;
box-sizing: border-box; box-sizing: border-box;
} }
.settingImage{
width:100%; .settingImage {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center;
} }
.contentWrapper { .contentWrapper {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -49,9 +52,9 @@
.description h2 { .description h2 {
font-size: 1.6rem; font-size: 1.6rem;
margin-bottom: 10px; margin-bottom: 10px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.description button { .description button {
+117 -143
View File
@@ -1,165 +1,139 @@
import { useContext, useEffect } from "react"; import {useContext, useEffect, useState} from "react";
import styles from "./ProfilePage.module.css"; import styles from "./ProfilePage.module.css";
import {AuthContext} from "../../contexts/auth/AuthContext";
import { AuthContext } from "../../contexts/auth/AuthContext";
import formatDate from "../../utils/date/formatDate"; import formatDate from "../../utils/date/formatDate";
import Task from "../../components/Task/Task";
import SettingsModal from "./components/SettingsModal/SettingsModal";
import {useParams} from "react-router";
import {useNavigate} from "react-router";
import getUserById from "../../utils/users/getUserById.js";
import ManageMember from "./components/manageMember/ManageMember.jsx";
//import Task from "../../components/Task/Task";
//import SettingsModal from "./components/SettingsModal/SettingsModal";
interface TaskType { interface TaskType {
id: number; id: number;
title?: string; title?: string;
completed?: boolean; completed?: boolean;
[key: string]: unknown;
[key: string]: unknown;
} }
interface User { interface User {
name: string; id: number;
lastname: string; name: string;
role: string; lastname: string;
email: string; role: string;
phone: string | null; email: string;
created_at: string; phone: string | null;
tasks: TaskType[]; created_at: string;
isAdmin: boolean;
tasks: TaskType[];
} }
interface AuthContextType { interface AuthContextType {
user: User | null; user: User;
update: () => void; update: () => void;
} }
function ProfilePage(){
const { user, update } = useContext(AuthContext) as AuthContextType;
useEffect(() => {
update();
}, []);
return (
<div className={styles.container}>
<div className={`${styles.profileboard} glassCard`}>
<div className={styles.contentWrapper}>
<div className={styles.profilePictureContainer}>
<img
src="/react.svg"
alt="Photo de profil"
className={styles.profilePicture}
/>
</div>
<div className={styles.description}>
<div className={styles.headerProfile}>
<h2>
{user?.name} {user?.lastname}
</h2>
</div>
<div className={styles.topDescription}>
<div className={`${styles.infoBlock} glassBorder`}>
<p>
<strong>Role :</strong> {user?.role}
</p>
<p>
<strong>Membre depuis :</strong>{" "}
{user && formatDate(user.created_at)}
</p>
</div>
<div className={`${styles.infoBlock} glassBorder`}>
<p>
<strong>Mail :</strong> {user?.email}
</p>
<p>
<strong>Téléphone :</strong>{" "}
{user?.phone ?? "Pas de numéro enregistré"}
</p>
</div>
<div className={styles.settingImage}>
{/* <SettingsModal />*/}
</div>
</div>
<h2>Tâches :</h2>
{/* {user && user.tasks.length > 0 ? (
user.tasks.map((task) => (
<Task key={task.id} task={task} />
))
) : (
<p>Aucune tâche pour le moment.</p>
)}*/}
</div>
</div>
</div>
</div>
);
}
export default ProfilePage;
/*import React, { useContext, useEffect } from "react";
import styles from "./ProfilePage.module.css";
import { AuthContext } from "../../contexts/auth/AuthContext.js";
import formatDate from "../../utils/date/formatDate.js";
import Task from "../../components/Task/Task";
import SettingsModal from "./components/SettingsModal/SettingsModal.jsx";
function ProfilePage() { function ProfilePage() {
const { user, update } = useContext(AuthContext); const navigate = useNavigate();
const {id} = useParams();
const {user, update} = useContext(AuthContext) as AuthContextType;
const [profileUser, setProfileUser] = useState<User | null>(null);
const isOwnProfile = !id || user.id.toString() === id;
useEffect(() => { useEffect(() => {
update();
}, [])
return ( (async () => {
<div className={styles.container}>
<div className={`${styles.profileboard} glassCard`}> if (!isOwnProfile) {
<div className={styles.contentWrapper}> console.log(id);
<div className={styles.profilePictureContainer}> const res = await getUserById(id);
<img
src="/react.svg" if (res.status === 404) {
alt="Photo de profil" navigate("/404");
className={styles.profilePicture} return;
/> }
</div>
<div className={styles.description}> if (res.status !== 200) {
<div className={styles.headerProfile}> console.error("Erreur lors du chargement du profil", res);
<h2> {user?.name} {user?.lastname} </h2> return;
}
setProfileUser(res.data);
} else {
update();
setProfileUser(user)
}
})()
}, [user]);
return (
<div className={styles.container}>
<div className={`${styles.profileboard} glassCard`}>
<div className={styles.contentWrapper}>
<div className={styles.profilePictureContainer}>
<img
src="/react.svg"
alt="Photo de profil"
className={styles.profilePicture}
/>
</div>
<div className={styles.description}>
<div className={styles.headerProfile}>
<h2>
{profileUser?.name} {profileUser?.lastname}
</h2>
{(user?.isAdmin && profileUser && !isOwnProfile) && (
<ManageMember userToManage={profileUser}/>
)}
</div>
<div className={styles.topDescription}>
<div className={`${styles.infoBlock} glassBorder`}>
<p>
<strong>Role :</strong> {profileUser?.role}
</p>
<p>
<strong>Membre depuis :</strong>{" "}
{profileUser && formatDate(profileUser.created_at)}
</p>
</div>
<div className={`${styles.infoBlock} glassBorder`}>
<p>
<strong>Mail :</strong> {profileUser?.email}
</p>
<p>
<strong>Téléphone :</strong>{" "}
{profileUser?.phone ?? "Pas de numéro enregistré"}
</p>
</div>
{isOwnProfile && (
<div className={styles.settingImage}>
<SettingsModal/>
</div>
)}
</div>
<h2>Tâches :</h2>
{profileUser && profileUser.tasks.length > 0 ? (
profileUser.tasks.map((task) => (
<Task key={task.id} task={task}/>
))
) : (
<p>Aucune tâche pour le moment.</p>
)}
</div>
</div>
</div> </div>
<div className={styles.topDescription}>
<div className={`${styles.infoBlock} glassBorder`}>
<p><strong>Role :</strong> {user?.role}</p>
<p><strong>Membre depuis :</strong> {user && formatDate(user.created_at)}</p>
</div>
<div className={`${styles.infoBlock} glassBorder`}>
<p><strong>Mail :</strong> {user?.email} </p>
<p><strong>Téléphone :</strong> {user?.phone === null ? "Pas de numéro enregistré" : user?.phone}</p>
</div>
<div className={styles.settingImage}>
<SettingsModal />
</div>
</div>
<h2>Tâches :</h2>
{user && user.tasks.length > 0 ? (
user.tasks.map((task, index) => (
<Task key={index} task={task} />
))
) : (
<p>Aucune tâche pour le moment.</p>
)}
</div>
</div> </div>
</div> );
</div>
);
} }
export default ProfilePage;*/ export default ProfilePage;
@@ -1,86 +0,0 @@
import Modal from "../../../../components/ui/modal/modal.tsx";
import Button from "../../../../components/ui/button/button.tsx";
import styles from "./SettingsModal.module.css"
import { useContext, useState } from "react";
import ThemeSwitcher from "../../../../components/ui/themeSwitcher/ThemeSwitcher.jsx";
import { AuthContext } from "../../../../contexts/auth/AuthContext.js";
import TextInput from "../../../../components/ui/input/input.tsx";
import updateUser from "../../../../utils/users/updateUser.js";
import deleteUser from "../../../../utils/users/deleteUser.js";
export default function SettingsModal() {
const { user, logout, update } = useContext(AuthContext);
const [open, setOpen] = useState(false);
const [isDelete, setIsDelete] = useState(false);
const [name, setName] = useState(user?.name);
const [lastname, setLastName] = useState(user?.lastname);
const [phone, setPhone] = useState(user?.phone);
const handleLogout = () => {
logout();
}
const handleSubmit = async () => {
await updateUser(name, lastname, phone);
update();
}
const handleDelete = async () => {
await deleteUser();
update();
}
return (
<>
<Button variant={"transparent"} onClick={() => setOpen(!open)}>
<img src={"/icons/settings-wheel.svg"} alt={"Modify btn"} className={styles.modifyIcon}/>
</Button>
<Modal title={"Paramètres"} open={open} onClose={() => setOpen(false)}>
<div className={styles.content}>
<div className={styles.section}>
<h3>Compte</h3>
<Button variant={"danger"} onClick={handleLogout}> Déconnexion </Button>
<Button variant={"danger"} onClick={() => setIsDelete(true)}> Supprimer le compte </Button>
<div className={styles.section}>
<h4>Changer de Prénom</h4>
<TextInput value={name} onChange={e => setName(e.target.value)} type={"text"} />
</div>
<div className={styles.section}>
<h4>Changer de nom</h4>
<TextInput value={lastname} onChange={e => setLastName(e.target.value)} type={"text"} />
</div>
<div className={styles.section}>
<h4>Changer de numéro de téléphone</h4>
<TextInput value={phone} onChange={e => setPhone(e.target.value)} type={"text"} />
</div>
<Button className={styles.submitBtn} onClick={handleSubmit}>Confirmer</Button>
</div>
<div className={styles.section}>
<h3>Apparence</h3>
<ThemeSwitcher />
</div>
</div>
</Modal>
<Modal title={"Validation"} open={isDelete} onClose={() => setIsDelete(false)}>
<div className={styles.section}>
<p className={styles.alignText}> Êtes vous vraiment sûr de vouloir supprimer votre compte ?</p>
<Button variant={"danger"} className={styles.submitBtn} onClick={handleDelete}>Confirmer</Button>
</div>
</Modal>
</>
)
}
@@ -1,73 +1,98 @@
.content { .content {
width: 100%; width: 100%;
padding: 16px; padding: 20px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 20px; gap: 28px;
} }
.modifyIcon { .modifyIcon {
height: 28px; height: 26px;
width: 28px; width: 26px;
transition: transform 0.2s ease, opacity 0.2s ease;
}
.modifyIcon:hover {
transform: rotate(15deg);
opacity: 0.8;
} }
.section { .section {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 16px;
margin-top: 10px; padding: 18px;
border-radius: 12px;
background: var(--glass-bg, rgba(255, 255, 255, 0.25));
backdrop-filter: blur(6px);
} }
.section h3 { .section h3 {
font-size: 1.1rem; font-size: 1.15rem;
margin-bottom: 5px; font-weight: 600;
} }
.section h4 { .section h4 {
font-size: 0.95rem; font-size: 0.9rem;
font-weight: 500; font-weight: 500;
} opacity: 0.85;
.submitBtn {
width: 100%;
margin-top: 15px;
} }
.section button { .section button {
width: 100%; width: 100%;
} }
@media (min-width: 600px) { .submitBtn {
width: 100%;
text-align: center;
margin-top: 10px;
}
.alignText {
text-align: center;
line-height: 1.5;
}
.section :global(input) {
width: 100%;
box-sizing: border-box;
}
@media (min-width: 640px) {
.content { .content {
padding: 24px; padding: 28px;
gap: 32px;
} }
.section h3 { .section {
font-size: 1.2rem; padding: 22px;
gap: 18px;
} }
.submitBtn { .submitBtn {
width: auto; width: auto;
align-self: center; align-self: center;
min-width: 200px; min-width: 220px;
} }
.section button { .section button {
width: fit-content; width: fit-content;
} }
} }
@media (min-width: 900px) { @media (min-width: 900px) {
.content { .content {
padding: 30px; padding: 36px;
gap: 30px; gap: 36px;
} }
.section { .section {
gap: 14px; padding: 26px;
border-radius: 16px;
} }
.modifyIcon { .modifyIcon {
height: 30px; height: 30px;
width: 30px; width: 30px;
} }
} }
@@ -0,0 +1,167 @@
import {useContext, useState, ChangeEvent} from "react";
import Modal from "../../../../components/ui/modal/modal";
import Button from "../../../../components/ui/button/button";
import styles from "./SettingsModal.module.css";
import ThemeSwitcher from "../../../../components/ui/themeSwitcher/ThemeSwitcher.jsx";
import {AuthContext} from "../../../../contexts/auth/AuthContext";
import TextInput from "../../../../components/ui/input/input";
import updateUser from "../../../../utils/users/updateUser.js";
import deleteUser from "../../../../utils/users/deleteUser.js";
interface TaskType {
id: number;
title?: string;
completed?: boolean;
[key: string]: unknown;
}
interface User {
id: number;
name: string;
lastname: string;
role: string;
email: string;
phone: string | null;
created_at: string;
isAdmin: boolean;
tasks: TaskType[];
}
interface AuthContextType {
user: User;
update: () => void;
logout: () => void;
}
export default function SettingsModal() {
const {user, logout, update} = useContext(AuthContext) as AuthContextType;
const [open, setOpen] = useState<boolean>(false);
const [isDelete, setIsDelete] = useState<boolean>(false);
const [name, setName] = useState<string>(user?.name ?? "");
const [lastname, setLastName] = useState<string>(user?.lastname ?? "");
const [phone, setPhone] = useState<string>(user?.phone ?? "");
const handleLogout = (): void => {
logout();
};
const handleSubmit = async (): Promise<void> => {
await updateUser(name, lastname, phone || null);
update();
setOpen(false);
};
const handleDelete = async (): Promise<void> => {
await deleteUser();
update();
};
const handleNameChange = (e: ChangeEvent<HTMLInputElement>): void => {
setName(e.target.value);
};
const handleLastnameChange = (e: ChangeEvent<HTMLInputElement>): void => {
setLastName(e.target.value);
};
const handlePhoneChange = (e: ChangeEvent<HTMLInputElement>): void => {
setPhone(e.target.value);
};
return (
<>
<Button variant="transparent" onClick={() => setOpen((prev) => !prev)}>
<img
src="/icons/settings-wheel.svg"
alt="Modify btn"
className={styles.modifyIcon}
/>
</Button>
<Modal
title="Paramètres"
open={open}
onClose={() => setOpen(false)}
>
<div className={styles.content}>
<div className={styles.section}>
<h3>Compte</h3>
<Button variant="danger" onClick={handleLogout}>
Déconnexion
</Button>
<Button
variant="danger"
onClick={() => setIsDelete(true)}
>
Supprimer le compte
</Button>
<div className={styles.section}>
<h4>Changer de Prénom</h4>
<TextInput
value={name}
onChange={handleNameChange}
type="text"
/>
</div>
<div className={styles.section}>
<h4>Changer de nom</h4>
<TextInput
value={lastname}
onChange={handleLastnameChange}
type="text"
/>
</div>
<div className={styles.section}>
<h4>Changer de numéro de téléphone</h4>
<TextInput
value={phone}
onChange={handlePhoneChange}
type="text"
/>
</div>
<Button
className={styles.submitBtn}
onClick={handleSubmit}
>
Confirmer
</Button>
</div>
<div className={styles.section}>
<h3>Apparence</h3>
<ThemeSwitcher/>
</div>
</div>
</Modal>
<Modal
title="Validation"
open={isDelete}
onClose={() => setIsDelete(false)}
>
<div className={styles.section}>
<p className={styles.alignText}>
Êtes vous vraiment sûr de vouloir supprimer votre compte ?
</p>
<Button
variant="danger"
className={styles.submitBtn}
onClick={handleDelete}
>
Confirmer
</Button>
</div>
</Modal>
</>
);
}
@@ -1,84 +0,0 @@
import styles from "./VolunteerProfilePage.module.css";
import { useParams, useNavigate } from "react-router";
import { useEffect, useState, useContext } from "react";
import getUserById from "../../utils/users/getUserById.js";
import formatDate from "../../utils/date/formatDate.js";
import Task from "../../components/Task/Task.jsx";
import ManageMember from "./components/manageMember/ManageMember.jsx";
import { AuthContext } from "../../contexts/auth/AuthContext.js";
export default function VolunteerProfilePage() {
const { id } = useParams();
const navigate = useNavigate();
const { user: currentUser } = useContext(AuthContext);
const [user, setUser] = useState(null);
useEffect(() => {
(async () => {
const res = await getUserById(id);
if (res.status === 404) {
navigate("/404");
return;
}
if (res.status !== 200) {
console.error("Erreur lors du chargement du profil", res);
return;
}
setUser(res.data);
})();
}, [id, navigate]);
if (!user) return null;
return (
<div className={styles.container}>
<div className={`${styles.profileboard} glassCard`}>
<div className={styles.contentWrapper}>
<div className={styles.profilePictureContainer}>
<img
src="/react.svg"
alt="Photo de profil"
className={styles.profilePicture}
/>
</div>
<div className={styles.description}>
<div className={styles.headerProfile}>
<h2>{user.name} {user.lastname}</h2>
{currentUser?.isAdmin && (
<ManageMember userToManage={user} />
)}
</div>
<div className={styles.topDescription}>
<div className={`${styles.infoBlock} glassBorder`}>
<p><strong>Role :</strong> {user.role}</p>
<p><strong>Membre depuis :</strong> {formatDate(user.created_at)}</p>
</div>
<div className={`${styles.infoBlock} glassBorder`}>
<p><strong>Mail :</strong> {user.email}</p>
<p>
<strong>Téléphone :</strong>{" "}
{user.phone ?? "Pas de numéro enregistré"}
</p>
</div>
</div>
<h2>Tâches :</h2>
{user.tasks?.length > 0 ? (
user.tasks.map((task, index) => (
<Task key={index} task={task} />
))
) : (
<p>Aucune tâche pour le moment.</p>
)}
</div>
</div>
</div>
</div>
);
}
@@ -1,163 +0,0 @@
.container {
width: 100%;
padding: 10px;
box-sizing: border-box;
}
.profileboard {
width: 100%;
padding: 15px;
border-radius: 20px;
box-sizing: border-box;
}
.contentWrapper {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
.profilePictureContainer {
width: 120px;
height: 120px;
margin-bottom: 15px;
}
.profilePicture {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
}
.description {
width: 100%;
text-align: left;
margin-left:4%;
}
.topDescription {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
margin-bottom: 15px;
}
.infoBlock {
width: 100%;
background: rgba(255, 255, 255, 0.1);
padding: 10px;
border-radius: 5px;
}
.eventTasks {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out, opacity 0.2s ease;
opacity: 0;
padding-left: 20px;
}
.eventTasks.expanded {
max-height: 500px;
opacity: 1;
margin-top: 10px;
}
.description h2{
font-size:30px;
}
.profilePicture{
width:90%;
}
.settingsBtns {
display: flex;
gap: 5px;
justify-content: right;
}
@media screen and (min-width: 768px) {
.contentWrapper {
flex-direction: row;
justify-content: flex-start;
align-items: flex-start;
}
.profilePictureContainer {
width: 150px;
height: 150px;
margin-right: 20px;
margin-bottom: 0;
}
.description {
width: calc(100% - 170px);
margin-left:4%;
}
.topDescription {
flex-direction: row;
justify-content: space-between;
}
.infoBlock {
width: 48%;
}
.event {
padding: 15px;
}
.description h2{
font-size:30px;
}
.profilePicture{
width:90%;
}
}
.description button{
width:50px;
height:50px;
}
.headerProfile {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
@media screen and (min-width: 1024px) {
.container {
max-width: 100%;
margin: 0 auto;
padding: 20px;
}
.profileboard {
padding: 25px;
}
.profilePictureContainer {
width: 30%;
height:auto;
}
.profilePicture{
width:90%;
}
.description {
width: calc(100% - 200px);
margin-left:4%;
}
.event {
padding: 18px;
}
.description h2{
font-size:30px;
}
}
+5 -6
View File
@@ -8,7 +8,6 @@ import ProfilePage from "./pages/Profile/ProfilePage";
import EventsPage from "./pages/Events/EventsPage.jsx"; import EventsPage from "./pages/Events/EventsPage.jsx";
import AdminPage from "./pages/Admin/AdminPage.jsx"; import AdminPage from "./pages/Admin/AdminPage.jsx";
import ValidationErrorPage from "./pages/waitValidationPage/ValidationErrorPage.jsx"; import ValidationErrorPage from "./pages/waitValidationPage/ValidationErrorPage.jsx";
import VolunteerProfilePage from "./pages/VolunteerProfile/VolunteerProfilePage.jsx";
import PageNotFound from "./pages/PageNotFound/PageNotFound.jsx"; import PageNotFound from "./pages/PageNotFound/PageNotFound.jsx";
import eventDetail from "./pages/eventDetail/eventDetail.jsx"; import eventDetail from "./pages/eventDetail/eventDetail.jsx";
import legalNotices from "./pages/LegalNotices/LegalNotices.jsx"; import legalNotices from "./pages/LegalNotices/LegalNotices.jsx";
@@ -47,10 +46,6 @@ const router = createBrowserRouter([
path: "/admin", path: "/admin",
Component:AdminPage, Component:AdminPage,
}, },
{
path:"/profile",
Component:ProfilePage,
},
{ {
path: "/events", path: "/events",
Component: EventsPage, Component: EventsPage,
@@ -59,9 +54,13 @@ const router = createBrowserRouter([
path: "/volunteers", path: "/volunteers",
Component: VolunteersPage, Component: VolunteersPage,
}, },
{
path:"/profile",
Component:ProfilePage,
},
{ {
path: "/profile/:id", path: "/profile/:id",
Component: VolunteerProfilePage, Component: ProfilePage,
}, },
{ {
path: "/events/:id", path: "/events/:id",