Merge branch 'dev' into 'features/searchBar'

Dev

See merge request sae-but2/2025-26/gestion-benevoles-association/Frontend!22
This commit is contained in:
JOSSERAND GIOVANNI p2405212
2025-11-30 13:59:04 +00:00
22 changed files with 962 additions and 244 deletions
@@ -1,47 +1,22 @@
/*import { useState } from "react";
import login from "../../utils/login.js";
import getUser from "../../utils/getUser.js";
import styles from "./form.module.css";
function LoginForm({ className }) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
async function handleSubmit(e) {
e.preventDefault();
await login(email, password);
getUser();
}
return (
<div className={`${className} ${"glassCard"}`}>
<div className={styles.formgroup}>
<input type="email" id="email" value={email} placeholder="Adresse mail" onChange={e => setEmail(e.target.value)} required/>
</div>
import { useState, useContext } from "react";
import styles from "./loginForm.module.css";
import Button from "../ui/button/button.jsx";
import Input from "../ui/input/input.jsx";
import { useNavigate } from "react-router";
import { AuthContext } from "../../contexts/auth/AuthContext.js";
<div className={styles.formgroup}>
<input type="password" id="password" value={password} placeholder="Mot de passe" onChange={e => setPassword(e.target.value)} required/>
<br/>
</div>
<div className={styles.logButton} >
<button className={styles.loginButton} onClick={e => handleSubmit(e)}>Se connecter</button>
</div>
</div>
);
}
export default LoginForm;*/
import { useState } from "react";
import login from "../../utils/login.js";
import getUser from "../../utils/getUser.js";
import styles from "./form.module.css";
function LoginForm({ className }) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
let navigate = useNavigate();
const { login } = useContext(AuthContext);
async function handleSubmit(e) {
e.preventDefault();
await login(email, password);
getUser();
navigate("/");
}
const toggleShowPassword = () => {
@@ -51,27 +26,22 @@ function LoginForm({ className }) {
return (
<div className={`${className} ${"glassCard"}`}>
<div className={styles.formgroup}>
<input
<Input
type="email"
id="email"
className={styles.input}
value={email}
placeholder="Adresse mail"
onChange={e => setEmail(e.target.value)}
required
style={{ width: '100%' }}
/>
</div>
<div className={styles.formgroup}>
<div style={{ position: 'relative', width: '100%' }}>
<input
<Input
type={showPassword ? "text" : "password"}
id="password"
className={styles.input}
value={password}
placeholder="Mot de passe"
onChange={e => setPassword(e.target.value)}
required
style={{ width: '100%', paddingRight: '40px' }}
style={{ width: '100%' }}
/>
<button
type="button"
@@ -92,8 +62,9 @@ function LoginForm({ className }) {
</button>
</div>
</div>
<div className={styles.logButton}>
<button className={styles.loginButton} onClick={handleSubmit}>Se connecter</button>
<Button onClick={handleSubmit} variant={"default"}> Se connecter </Button>
</div>
</div>
);
+3
View File
@@ -0,0 +1,3 @@
import { createContext } from "react";
export const AuthContext = createContext(null);
+37
View File
@@ -0,0 +1,37 @@
import { useState, useEffect } from "react";
import loginFunction from "../../utils/login.js";
import getUser from "../../utils/getUser.js";
import { AuthContext } from "./AuthContext";
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
(async () => {
const u = await getUser();
setUser(u);
setLoading(false);
})();
}, []);
const login = async (email, password) => {
const res = await loginFunction(email, password);
if (!res || res.status !== 200) throw new Error("Login error");
const u = await getUser();
setUser(u);
return res;
};
const logout = async () => {
// TODO: Appeller fonction logout
setUser(null);
};
return (
<AuthContext.Provider value={{ user, loading, login, logout }}>
{children}
</AuthContext.Provider>
);
}
+6 -5
View File
@@ -17,16 +17,17 @@
-moz-osx-font-smoothing: grayscale;
}
@media screen and (max-width: 768px) {
.content{
padding: 0.7rem;
z-index:1;
}
}
.content {
padding: 1.5rem;
z-index: 1;
}
a {
font-weight: 500;
color: #646cff;
+8
View File
@@ -2,10 +2,18 @@ import { Outlet } from "react-router";
import Footer from "./components/Footer/Footer.jsx"
import Header from "./components/Header/Header.jsx";
import Background from "./components/background/background.jsx";
import { Navigate } from "react-router";
import { useContext} from "react";
import { AuthContext } from "./contexts/auth/AuthContext.js";
function Layout(){
const { user, loading } = useContext(AuthContext);
if (loading) return <p>Loading</p>;
if (!user) return <Navigate to="/login" />;
return (
<div>
<Background>
+4 -1
View File
@@ -3,10 +3,13 @@ import { createRoot } from 'react-dom/client'
import './index.css'
import { RouterProvider } from "react-router";
import router from './router';
import { AuthProvider } from "./contexts/auth/authProvider.jsx";
createRoot(document.getElementById('root')).render(
<StrictMode>
<RouterProvider router={router} />
<AuthProvider>
<RouterProvider router={router} />
</AuthProvider>
</StrictMode>,
)
+7 -4
View File
@@ -1,12 +1,15 @@
import styles from "./LoginPage.module.css";
import LoginForm from "../../components/form/form.jsx";
import LoginForm from "../../components/loginform/loginForm.jsx";
import Background from "../../components/background/background.jsx";
function LoginPage() {
return (
return <Background>
<div className={styles.container}>
<LoginForm />
<LoginForm />
</div>
);
</Background>
}
export default LoginPage;
+1
View File
@@ -9,6 +9,7 @@
align-items: center;
padding: 20px;
border-radius:12px;
margin-left: 33.3%;
}
+110
View File
@@ -0,0 +1,110 @@
import React, { useState } from "react";
import styles from "./ProfilePage.module.css";
function Event({ title,date, tasks }) {
const [isExpanded, setIsExpanded] = useState(false);
const toggleExpand = () => {
setIsExpanded(!isExpanded);
};
return (
<div className={`${styles.event} glassCard`}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div style={{display:'flex', flexDirection:'column'}}>
<h3>{title}</h3>
<p style={{ fontSize: '0.8rem', color: '#666', margin: '0 0 0 0' }}>{date}</p>
</div>
<button
onClick={toggleExpand}
style={{
background: 'none',
border: 'none',
cursor: 'pointer',
fontSize: '1.2rem',
}}
>
{isExpanded ? '▼' : '▶'}
</button>
</div>
<ul
className={`${styles.eventTasks} ${isExpanded ? styles.expanded : ''}`}
>
{tasks.map((task, index) => (
<li key={index}>-- {task}</li>
))}
</ul>
</div>
);
}
function ProfilePage() {
return (
<div className={styles.container}>
<div className={`${styles.profileboard} glassCard`}>
<div className={styles.contentWrapper}>
<div className={styles.profilePictureContainer}>
<img
src="../src/assets/react.svg"
alt="Photo de profil"
className={styles.profilePicture}
/>
</div>
<div className={styles.description}>
<h2>LEBLOND Malo</h2>
<div className={styles.topDescription}>
<div className={styles.infoBlock}>
<p><strong>Role :</strong> Président</p>
<p><strong>Membre depuis :</strong> 2019</p>
</div>
<div className={styles.infoBlock}>
<p><strong>Mail :</strong> mailpro@gmail.com</p>
<p><strong>Téléphone :</strong> 08 67 87 32 21</p>
</div>
<button href="/../UpdateProfile.jsx">M</button>
</div>
<h2>Vos Événements :</h2>
<Event
title="Titre event 1"
date="04/11/2025"
tasks={["Votre tâche 1", "Votre tâche 2", "Votre tâche 3"]}
/>
<Event
title="Titre event 2"
date="04/11/2025"
tasks={["Votre tâche 1", "Votre tâche 2", "Votre tâche 3"]}
/>
<Event
title="Titre event 3"
date="04/11/2025"
tasks={["Votre tâche 1", "Votre tâche 2", "Votre tâche 3"]}
/>
<Event
title="Titre event 4"
date="04/11/2025"
tasks={["Votre tâche 1", "Votre tâche 2", "Votre tâche 3"]}
/>
<Event
title="Titre event 5"
date="04/11/2025"
tasks={["Votre tâche 1", "Votre tâche 2", "Votre tâche 3"]}
/>
<Event
title="Titre event 6"
date="04/11/2025"
tasks={["Votre tâche 1", "Votre tâche 2", "Votre tâche 3"]}
/>
<Event
title="Titre event 7"
date="04/11/2025"
tasks={["Votre tâche 1", "Votre tâche 2", "Votre tâche 3"]}
/>
</div>
</div>
</div>
</div>
);
}
export default ProfilePage;
+158
View File
@@ -0,0 +1,158 @@
.container {
width: 100%;
padding: 10px;
box-sizing: border-box;
}
.profileboard {
width: 100%;
padding: 15px;
border-radius: 10px;
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;
gap: 10px;
margin-bottom: 15px;
}
.infoBlock {
width: 100%;
background: rgba(255, 255, 255, 0.1);
padding: 10px;
border-radius: 5px;
}
.event {
width: 100%;
padding: 12px;
margin-bottom: 10px;
border-radius: 8px;
}
.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;
margin-bottom: 2%;
}
.profilePicture{
width:90%;
}
/* Tablette (768px et plus) */
@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;
margin-bottom: 2%;
}
.profilePicture{
width:90%;
}
}
.description button{
width:50px;
height:50px;
}
/* Desktop (1024px et plus) */
@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;
margin-bottom: 2%;
}
}
+105
View File
@@ -0,0 +1,105 @@
import styles from "./RegisterPage.module.css";
import Background from "../../components/background/background.jsx";
import Input from "../../components/ui/input/input.jsx";
import Button from "../../components/ui/button/button.jsx";
import { useState } from "react";
import register from "../../utils/register.js";
import { useNavigate } from "react-router";
function RegisterPage() {
const navigate = useNavigate();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [name, setName] = useState("");
const [lastName, setLastName] = useState("");
const [showPassword, setShowPassword] = useState(false);
async function handleSubmit(e) {
e.preventDefault();
const res = await register(email, password, name, lastName);
if (!res || res.status !== 200) throw new Error("Register error");
navigate("/login");
}
return <Background>
<div className={styles.container}>
<div className={"glassCard"}>
<h1>Créer un compte</h1>
<div className={styles.formGroup}>
<Input
value={name}
placeholder="Prénom"
onChange={e => setName(e.target.value)}
style={{ width: '100%' }}
/>
</div>
<div className={styles.formGroup}>
<Input
value={lastName}
placeholder="Nom"
onChange={e => setLastName(e.target.value)}
style={{ width: '100%' }}
/>
</div>
<div className={styles.formGroup}>
<Input
type="email"
value={email}
placeholder="Adresse mail"
onChange={e => setEmail(e.target.value)}
style={{ width: '100%' }}
/>
</div>
<div className={styles.formGroup}>
<div style={{ position: 'relative', width: '100%' }}>
<Input
type={showPassword ? "text" : "password"}
value={password}
placeholder="Mot de passe"
onChange={e => setPassword(e.target.value)}
style={{ width: '100%' }}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
style={{
position: 'absolute',
width: '30px',
right: '10px',
top: '50%',
transform: 'translateY(-50%)',
background: 'none',
border: 'none',
cursor: 'pointer',
fontSize: '16px',
}}
>
{showPassword ? '👁️' : '🔒'}
</button>
</div>
</div>
<div className={styles.logButton}>
<Button variant={"transparent"} onClick={() => navigate("/login")}>Vous avez déjà un compte ?</Button>
<Button variant={"default"} onClick={handleSubmit}> Se connecter </Button>
</div>
</div>
</div>
</Background>
}
export default RegisterPage;
@@ -0,0 +1,29 @@
.container {
position: relative;
z-index: 2;
display: flex;
width: 30%;
justify-content: center;
align-items: center;
padding: 20px;
border-radius:12px;
}
:global(.glassCard) {
--glass-bg: #ffffff9f;
}
.logButton{
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
margin-top: 12px;
}
.formGroup{
margin-top:23px;
}
h1 {
overflow: visible;
text-align: center;
}
+108
View File
@@ -0,0 +1,108 @@
import React, { useState } from "react";
import styles from "./VolunteersPage.module.css";
function Volunteer({ name, role, since, email, phone, photo }) {
const [isExpanded, setIsExpanded] = useState(false);
const toggleExpand = () => {
setIsExpanded(!isExpanded);
};
return (
<div className={`${styles.profileboard} glassCard`}>
<div className={styles.mainContent}>
{!isExpanded ? (
<div className={styles.basicInfo}>
<h2>{name}</h2>
<p><strong>Rôle :</strong> {role}</p>
<p><strong>Membre depuis :</strong> {since}</p>
</div>
) : (
<div className={styles.expandedContent}>
<div className={styles.profilePictureContainer}>
<img src={photo} alt={`${name}'s profile`} className={styles.profilePicture} />
</div>
<div className={styles.textInfo}>
<div className={styles.basicInfo}>
<h2>{name}</h2>
<p><strong>Rôle :</strong> {role}</p>
<p><strong>Membre depuis :</strong> {since}</p>
</div>
<div className={styles.contactInfo}>
<p><strong>Email :</strong> {email}</p>
<p><strong>Téléphone :</strong> {phone}</p>
</div>
</div>
</div>
)}
<button
className={`${styles.expandButton} glassCard`}
onClick={toggleExpand}
>
{isExpanded ? "▲" : "▼"}
</button>
{isExpanded && (
<div className="voirPlus">
<a href="#" className={styles.voirPlus}>VOIR PLUS</a>
</div>
)}
</div>
</div>
);
}
function VolunteerPage() {
const volunteers = [
{
name: "LEBLOND Malo",
role: "Président",
since: "2019",
email: "malo.leblond@example.com",
phone: "08 67 87 32 21",
photo: "../src/assets/react.svg",
},
{
name: "DUPONT Jean",
role: "Bénévole",
since: "2020",
email: "jean.dupont@example.com",
phone: "06 12 34 56 78",
photo: "../src/assets/react.svg",
},
{
name: "MARTIN Sophie",
role: "Bénévole",
since: "2021",
email: "sophie.martin@example.com",
phone: "06 87 65 43 21",
photo: "../src/assets/react.svg",
},
{
name: "MARTIN Sophie",
role: "Bénévole",
since: "2021",
email: "sophie.martin@example.com",
phone: "06 87 65 43 21",
photo: "../src/assets/react.svg",
},
];
return (
<div className={styles.container}>
{volunteers.map((volunteer, index) => (
<Volunteer
key={index}
name={volunteer.name}
role={volunteer.role}
since={volunteer.since}
email={volunteer.email}
phone={volunteer.phone}
photo={volunteer.photo}
/>
))}
</div>
);
}
export default VolunteerPage;
@@ -0,0 +1,125 @@
.container {
width: 100%;
padding: 10px;
box-sizing: border-box;
margin-top: 50px;
}
.profileboard {
width: 100%;
padding: 15px;
border-radius: 10px;
box-sizing: border-box;
margin-top: 3%;
position: relative;
}
.mainContent {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
width: 100%;
}
.basicInfo {
flex: 1;
}
.expandedContent {
display: flex;
flex-direction: row;
align-items: flex-start;
width: 100%;
}
.profilePictureContainer {
width: 120px;
height: 120px;
margin-right: 20px;
}
.profilePicture {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
}
.textInfo {
display: flex;
flex-direction: column;
flex: 1;
}
.contactInfo {
margin-top: 10px;
}
.contactInfo p {
margin: 5px 0;
}
.expandButton {
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
background: transparent;
border: none;
font-size: 16px;
margin-left: 10px;
position: absolute;
top: 10px;
right: 10px;
}
.voirPlus {
position: absolute;
bottom: 10px;
right: 10px;
color: #007bff;
text-decoration: none;
font-weight: bold;
cursor: pointer;
}
.voirPlus:hover {
text-decoration: underline;
}
@media screen and (min-width: 768px) {
.profilePictureContainer {
width: 150px;
height: 150px;
}
.voirPlus{
text-align: right;
}
}
@media screen and (min-width: 1024px) {
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
margin-top:50px;
margin-bottom: 50px;
}
.profileboard {
padding: 25px;
}
.profilePictureContainer {
width: 200px;
height: auto;
}
}
+19
View File
@@ -2,9 +2,20 @@ import { createBrowserRouter } from "react-router";
import Layout from "./layout.jsx";
import HomePage from "./pages/Home/HomePage";
import LoginPage from "./pages/Login/LoginPage";
import RegisterPage from "./pages/Register/RegisterPage.jsx";
import VolunteersPage from "./pages/Volunteers/VolunteersPage";
import ProfilePage from "./pages/Profile/ProfilePage";
import EventsPage from "./pages/Events/EventsPage.jsx";
const router = createBrowserRouter([
{
path: "/login",
Component: LoginPage,
},
{
path: "/register",
Component: RegisterPage,
},
{
path: "/",
Component: Layout,
@@ -17,9 +28,17 @@ const router = createBrowserRouter([
path: "/login",
Component: LoginPage,
},
{
path:"/profile",
Component:ProfilePage,
},
{
path: "/events",
Component: EventsPage,
},
{
path: "/volunteers",
Component: VolunteersPage,
}
]
}
+19 -10
View File
@@ -1,12 +1,21 @@
export default async function getUser() {
await fetch('http://localhost:8000/api/me', {
method: 'GET',
credentials: 'include',
headers: {
'Accept': 'application/json'
try {
const res = await fetch('http://localhost:80/api/users/me', {
method: 'GET',
credentials: 'include',
headers: {
'Accept': 'application/json'
}
});
if (!res.ok) {
throw new Error(`Erreur serveur : ${res.status}`);
}
})
.then(res => res.json())
.then(data => console.log('Utilisateur connecté :', data))
.catch(err => console.error('Erreur :', err));
}
const data = await res.json();
return data;
} catch (err) {
console.error('Erreur :', err);
return null;
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
export default async function getUserNotifications() {
try {
const response = await fetch(
'http://localhost:8000/api/users/1/notifications',
'http://localhost:80/api/users/1/notifications',
{
method: 'GET',
credentials: 'include',
+1 -1
View File
@@ -1,5 +1,5 @@
export default async function getXSRFToken() {
const response = await fetch('http://localhost:8000/sanctum/csrf-cookie', {
const response = await fetch('http://localhost:80/sanctum/csrf-cookie', {
method: 'GET',
credentials: 'include'
});
+25 -27
View File
@@ -3,31 +3,29 @@ import getXSRFToken from "./getXSRF.js";
export default async function login(email, password) {
const csrfToken = await getXSRFToken();
await fetch('http://localhost:8000/api/users/login', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': csrfToken,
'Accept': 'application/json'
},
body: JSON.stringify({
email: email,
password: password
})
})
.then(response => {
console.log('Code HTTP :', response.status);
if (!response.ok) {
throw new Error(`Erreur HTTP ${response.status}`);
}
return response.json();
})
.then(data => {
console.log('Connexion réussie ! Données utilisateur :', data);
})
.catch(error => {
console.error('Erreur lors de la connexion :', error.message);
try {
const response = await fetch('http://localhost:80/api/users/login', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': csrfToken,
'Accept': 'application/json'
},
body: JSON.stringify({ email, password })
});
}
console.log('Code HTTP :', response.status);
if (!response.ok) {
throw new Error(`Erreur HTTP ${response.status}`);
}
const data = await response.json();
console.log('Connexion réussie ! Données utilisateur :', data);
return { status: response.status, data };
} catch (error) {
return error;
}
}
+30
View File
@@ -0,0 +1,30 @@
import getXSRFToken from "./getXSRF.js";
export default async function register(email, password, name, lastname) {
const csrfToken = await getXSRFToken();
try {
const response = await fetch('http://localhost:80/api/users/register', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': csrfToken,
'Accept': 'application/json'
},
body: JSON.stringify({ email, password, name, lastname }),
});
console.log('Code HTTP :', response.status);
if (!response.ok) {
throw new Error(`Erreur HTTP ${response.status}`);
}
const data = await response.json();
return { status: response.status, data };
} catch (error) {
return error;
}
}