Merge branch 'dev' of https://iutbg-gitlab.iutbourg.univ-lyon1.fr/sae-but2/2025-26/gestion-benevoles-association/frontend into dev
This commit is contained in:
Generated
+777
-4
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 -8 72 72" id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:#010002;}</style></defs><title>tools</title><path class="cls-1" d="M13.32,44.31a4.5,4.5,0,0,0,6.36,6.36L34.44,35.91l-6.36-6.36Z"/><polygon class="cls-1" points="50.2 16.96 56.63 13.63 60 7.13 56.86 3.99 50.36 7.36 47.03 13.78 39.21 21.61 42.38 24.79 50.2 16.96"/><path class="cls-1" d="M51.61,34,51,34a8.9,8.9,0,0,0-3.11.58L29.45,16.09A8.9,8.9,0,0,0,30,13l-.06-.6A8.9,8.9,0,0,0,17.19,4.89l6,6a3,3,0,0,1,.68,1.08A3,3,0,0,1,21,16a2.92,2.92,0,0,1-1-.21,3.15,3.15,0,0,1-1.08-.67l-6-6a8.9,8.9,0,0,0,7.49,12.78L21,22a8.9,8.9,0,0,0,3.11-.58L42.6,39.84A8.9,8.9,0,0,0,42,43l.06.6A9,9,0,0,0,51,51.94a8.72,8.72,0,0,0,3.85-.9l-6-6A3.08,3.08,0,0,1,48.21,44,3,3,0,0,1,51,40a2.92,2.92,0,0,1,1,.21,2.72,2.72,0,0,1,1.08.67l6,6A8.9,8.9,0,0,0,51.61,34Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1009 B |
@@ -73,7 +73,7 @@ function Event({event}){
|
|||||||
<div className={styles.eventContainerBottom}>
|
<div className={styles.eventContainerBottom}>
|
||||||
{(windowSize.width <= 768 || eventMenu) ? (
|
{(windowSize.width <= 768 || eventMenu) ? (
|
||||||
|
|
||||||
<Button onClick={() => navigate(`/event/` + event.id)}>Voir plus</Button>
|
<Button onClick={() => navigate(`/events/` + event.id)}>Voir plus</Button>
|
||||||
|
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,16 +1,64 @@
|
|||||||
import styles from "./Footer.module.css"
|
import styles from "./Footer.module.css"
|
||||||
import {Link} from "react-router";
|
import {Link} from "react-router";
|
||||||
|
import {useState} from "react";
|
||||||
|
import Modal from "../ui/modal/modal.jsx";
|
||||||
|
import Button from "../ui/button/button.jsx";
|
||||||
|
import scrollToTop from "../../utils/scrollToTop.js";
|
||||||
|
|
||||||
function Footer(){
|
export default function Footer(){
|
||||||
return (
|
const currentYear = new Date().getFullYear();
|
||||||
<footer className={`${styles.footer} ${"glassCard"}`} >
|
const [isCopyrightModal, setIsCopyrightModal] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<footer className={`${styles.footer} glassCard`}>
|
||||||
<div className={styles.footerContent}>
|
<div className={styles.footerContent}>
|
||||||
<Link to="" className={styles.link}>Contact</Link>
|
<Button
|
||||||
<Link to="" className={styles.link}>RGPD</Link>
|
variant={"transparent"}
|
||||||
<Link to="" className={styles.link}>Mentions légales</Link>
|
className={styles.link}
|
||||||
|
onClick={() => setIsCopyrightModal(true)}
|
||||||
|
>
|
||||||
|
Copyright © {currentYear}. Tous droits réservés.
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className={styles.copyright}>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav className={styles.footerNav}>
|
||||||
|
<Link to="/RGPD" className={styles.link} onClick={scrollToTop}>
|
||||||
|
Fiche RGPD
|
||||||
|
</Link>
|
||||||
|
<span className={styles.separator}></span>
|
||||||
|
<Link to="/legalNotices" className={styles.link} onClick={scrollToTop}>
|
||||||
|
Mentions légales
|
||||||
|
</Link>
|
||||||
|
<span className={styles.separator}></span>
|
||||||
|
<Link to={"https://comite.beaupont.fr/contactez-nous"} className={styles.link}>
|
||||||
|
Contact
|
||||||
|
</Link>
|
||||||
|
<span className={styles.separator}></span>
|
||||||
|
<Link to="/" className={styles.link} onClick={scrollToTop}>
|
||||||
|
Accueil
|
||||||
|
</Link>
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Footer;
|
{/* TODO : mettre nos noms dans un fichier de configuration */}
|
||||||
|
<Modal title={"Réalisé par"} open={isCopyrightModal} onClose={() => setIsCopyrightModal(false)}>
|
||||||
|
<section className={styles.section}>
|
||||||
|
<div className={styles.inputContainer}>
|
||||||
|
<p>Quentin T'jampens</p>
|
||||||
|
<p>Antoine Ordonneau</p>
|
||||||
|
<p>Malo Leblond</p>
|
||||||
|
<p>
|
||||||
|
Giovanni Josserand (
|
||||||
|
<Link to={"https://linktree.josserand.ovh/"}>linktree</Link>
|
||||||
|
)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
}
|
||||||
@@ -1,34 +1,76 @@
|
|||||||
.footer {
|
.footer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
padding: 1.5rem 2rem;
|
||||||
align-items: center;
|
margin-top: auto;
|
||||||
height: fit-content;
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.footerContent {
|
.footerContent {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: center;
|
}
|
||||||
gap: 2rem;
|
|
||||||
|
.footerNav {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
|
color: black !important;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link:hover {
|
||||||
|
color: #019AFF !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
width: 1px;
|
||||||
|
height: 16px;
|
||||||
|
background-color: black;
|
||||||
|
margin: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyright {
|
||||||
color: black;
|
color: black;
|
||||||
width: fit-content;
|
font-size: 0.9rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section{
|
||||||
|
margin-top: 2rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.footerContent {
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
gap: 1rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.footerNav {
|
||||||
|
flex-wrap: wrap;
|
||||||
@media screen and (max-width: 480px) {
|
justify-content: center;
|
||||||
.footer{
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.footerContent{
|
.link {
|
||||||
flex-direction: column;
|
margin: 0.5rem 1rem;
|
||||||
align-items: center;
|
}
|
||||||
gap: 0.5rem;
|
|
||||||
|
.separator {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,32 +1,53 @@
|
|||||||
import React from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import styles from "./IncompleteEvent.module.css";
|
import styles from "./IncompleteEvent.module.css";
|
||||||
|
import getAllEvents from "../../utils/events/getAllEvents";
|
||||||
|
|
||||||
function IncompleteEvent() {
|
function IncompleteEvent() {
|
||||||
const incompleteEvents = [
|
const [events, setEvents] = useState([]);
|
||||||
{ title: "Événement A", tasks: ["Communication sur les réseaux", "Enormément de comm", "Toujours + de comm,Communication sur les réseaux",
|
const [loading, setLoading] = useState(true);
|
||||||
"Enormément de comm", "Toujours + de comm","Communication sur les réseaux", "Enormément de comm", "Toujours + de comm","Communication sur les réseaux", "Enormément de comm", "Toujours + de comm]"] },
|
|
||||||
{ title: "Événement B", tasks: ["Réservation de la salle", "Vérification du matériel"] },
|
useEffect(() => {
|
||||||
{ title: "Événement C", tasks: ["Communication sur les réseaux", "Enormément de comm", "Toujours + de comm"] },
|
const fetchEvents = async () => {
|
||||||
{ title: "Événement D", tasks: ["Communication sur les réseaux", "Enormément de comm", "Toujours + de comm"] },
|
const data = await getAllEvents();
|
||||||
{ title: "Événement E", tasks: ["Communication sur les réseaux", "Enormément de comm", "Toujours + de comm,Communication sur les réseaux",
|
setEvents(data?.data || data || []);
|
||||||
"Enormément de comm", "Toujours + de comm","Communication sur les réseaux", "Enormément de comm", "Toujours + de comm","Communication sur les réseaux", "Enormément de comm", "Toujours + de comm]"] },
|
setLoading(false);
|
||||||
{ title: "Événement F", tasks: ["Communication sur les réseaux", "Enormément de comm", "Toujours + de comm,Communication sur les réseaux",
|
};
|
||||||
"Enormément de comm", "Toujours + de comm","Communication sur les réseaux", "Enormément de comm", "Toujours + de comm","Communication sur les réseaux", "Enormément de comm", "Toujours + de comm]"] },
|
fetchEvents();
|
||||||
{ title: "Événement G", tasks: ["Communication sur les réseaux", "Enormément de comm", "Toujours + de comm,Communication sur les réseaux",
|
}, []);
|
||||||
"Enormément de comm", "Toujours + de comm","Communication sur les réseaux", "Enormément de comm", "Toujours + de comm","Communication sur les réseaux", "Enormément de comm", "Toujours + de comm]"] },
|
|
||||||
];
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className={`${styles.rightBlock} glassCard`}>
|
||||||
|
<h2>Derniers événements</h2>
|
||||||
|
<div className={styles.eventsContainer}>
|
||||||
|
<div className={`${styles.emptyEvent} glassCard`}>
|
||||||
|
<p>Chargement des événements...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.rightBlock} glassCard`}>
|
<div className={`${styles.rightBlock} glassCard`}>
|
||||||
<h2>Événements incomplets</h2>
|
<h2>Derniers événements</h2>
|
||||||
<div className={styles.eventsContainer}>
|
<div className={styles.eventsContainer}>
|
||||||
{incompleteEvents.map((event, index) => (
|
{events.map((event) => (
|
||||||
<div key={index} className={`glassBorder ${styles.eventCard}`}>
|
<div key={event.id} className={`glassCard ${styles.eventCard}`}>
|
||||||
<h3>{event.title}</h3>
|
<h3>{event.name}</h3>
|
||||||
|
<p className={styles.eventDate}>
|
||||||
|
{new Date(event.start).toLocaleDateString("fr-FR")} - {new Date(event.end).toLocaleDateString("fr-FR")}
|
||||||
|
</p>
|
||||||
|
<p className={styles.eventDescription}>{event.description}</p>
|
||||||
|
<h4>Tâches associées :</h4>
|
||||||
<ul className={styles.tasksList}>
|
<ul className={styles.tasksList}>
|
||||||
{event.tasks.map((task, taskIndex) => (
|
{event.tasks?.length ? (
|
||||||
<li key={taskIndex}>{task}</li>
|
event.tasks.map((task, taskIndex) => (
|
||||||
))}
|
<li key={taskIndex}>{task.name}</li>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<li>Aucune tâche associée.</li>
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -17,20 +17,6 @@
|
|||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
}
|
}
|
||||||
|
|
||||||
.eventCard ul {
|
|
||||||
align-items: center;
|
|
||||||
margin-left: 8%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.eventCard ul li {
|
|
||||||
text-align: left;
|
|
||||||
list-style-type: circle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.eventsContainer::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.eventCard {
|
.eventCard {
|
||||||
min-width: calc((100% - 20px) / 3);
|
min-width: calc((100% - 20px) / 3);
|
||||||
max-width: calc((100% - 20px) / 3);
|
max-width: calc((100% - 20px) / 3);
|
||||||
@@ -38,63 +24,69 @@ display: none;
|
|||||||
max-height: 250px;
|
max-height: 250px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 10px 10px 200px 10px;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(20px);
|
transform: translateX(20px);
|
||||||
animation: slideInEvent 1s ease forwards;
|
animation: slideInEvent 1s ease forwards;
|
||||||
flex-shrink: 1;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.eventCard:nth-child(1) {
|
.eventCard ul {
|
||||||
animation-delay: 0s;
|
align-items: center;
|
||||||
|
margin-left: 8%;
|
||||||
|
padding-left: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.eventCard:nth-child(2) {
|
.eventCard ul li {
|
||||||
animation-delay: 0.1s;
|
text-align: left;
|
||||||
}
|
list-style-type: circle;
|
||||||
|
|
||||||
.eventCard:nth-child(3) {
|
|
||||||
animation-delay: 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.eventCard:nth-child(4) {
|
|
||||||
animation-delay: 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.eventCard:nth-child(5) {
|
|
||||||
animation-delay: 0.4s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.eventCard:nth-child(6) {
|
|
||||||
animation-delay: 0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.eventCard::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.eventCard > h3 {
|
.eventCard > h3 {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Animation des cartes avec délais */
|
||||||
|
.eventCard:nth-child(1) { animation-delay: 0s; }
|
||||||
|
.eventCard:nth-child(2) { animation-delay: 0.1s; }
|
||||||
|
.eventCard:nth-child(3) { animation-delay: 0.2s; }
|
||||||
|
.eventCard:nth-child(4) { animation-delay: 0.3s; }
|
||||||
|
.eventCard:nth-child(5) { animation-delay: 0.4s; }
|
||||||
|
.eventCard:nth-child(6) { animation-delay: 0.5s; }
|
||||||
|
|
||||||
|
.emptyEvent {
|
||||||
|
min-width: calc((100% - 20px) / 3);
|
||||||
|
max-width: calc((100% - 20px) / 3);
|
||||||
|
min-height: 250px;
|
||||||
|
max-height: 250px;
|
||||||
|
padding: 10px;
|
||||||
|
text-align: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
opacity: 1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes slideInEvent {
|
@keyframes slideInEvent {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(20px);
|
transform: translateX(20px);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Adaptation pour mobile */
|
||||||
@media screen and (max-width: 760px) {
|
@media screen and (max-width: 760px) {
|
||||||
.eventCard {
|
.eventCard {
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
max-height: fit-content;
|
max-height: fit-content;
|
||||||
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.eventCard h3 {
|
.eventCard h3 {
|
||||||
@@ -102,18 +94,21 @@ display: none;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.eventsContainer {
|
.eventsContainer {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
scroll-behavior: smooth;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.rightBlock > h2 {
|
.rightBlock > h2 {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.emptyEvent {
|
||||||
|
min-width: 100%;
|
||||||
|
min-height: 200px;
|
||||||
|
max-height: fit-content;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,39 +1,102 @@
|
|||||||
import React from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import Button from "../ui/button/button";
|
import Button from "../ui/button/button";
|
||||||
import styles from "./PendingMembers.module.css";
|
import styles from "./PendingMembers.module.css";
|
||||||
|
|
||||||
function pendingMembers() {
|
import getUserToValidate from "../../utils/users/getUserToValidate";
|
||||||
const pendingMembers = [
|
import getUserById from "../../utils/users/getUserById";
|
||||||
{ name: "Antoine Ordonneau" },
|
import validateUser from "../../utils/users/validateUser";
|
||||||
{ name: "Giovanni Josserand" },
|
import deleteUser from "../../utils/users/deleteUser";
|
||||||
{ name: "Quentin T'Jampens" },
|
|
||||||
{ name: "Quentin T'Jampens" },
|
|
||||||
{ name: "Quentin T'Jampens" },
|
|
||||||
{ name: "Quentin T'Jampens" },
|
|
||||||
{ name: "Quentin T'Jampens" }
|
|
||||||
];
|
|
||||||
|
|
||||||
const handleValidate = (name) => {
|
function PendingMembers() {
|
||||||
console.log(`Valider ${name}`);
|
const [pendingMembers, setPendingMembers] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [actionLoading, setActionLoading] = useState(null);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchPendingMembers = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const userIds = await getUserToValidate();
|
||||||
|
if (userIds && userIds.length > 0) {
|
||||||
|
const users = await Promise.all(userIds.map((id) => getUserById(id)));
|
||||||
|
setPendingMembers(users.filter(Boolean));
|
||||||
|
} else {
|
||||||
|
setPendingMembers([]);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
fetchPendingMembers();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const removeMember = (id) => {
|
||||||
|
setPendingMembers(prevMembers =>
|
||||||
|
prevMembers.filter(member => member.id !== id)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReject = (name) => {
|
const handleValidate = async (id) => {
|
||||||
console.log(`Refuser ${name}`);
|
setActionLoading(id);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const result = await validateUser(id);
|
||||||
|
if (result.success) {
|
||||||
|
removeMember(id);
|
||||||
|
} else {
|
||||||
|
setError(result.message || "Échec de la validation.");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError("Erreur lors de la validation.");
|
||||||
|
} finally {
|
||||||
|
setActionLoading(null);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleReject = async (id) => {
|
||||||
|
setActionLoading(id);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const result = await deleteUser(id);
|
||||||
|
if (result.success) {
|
||||||
|
removeMember(id);
|
||||||
|
} else {
|
||||||
|
setError(result.message || "Échec de la suppression.");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError("Erreur lors de la suppression.");
|
||||||
|
} finally {
|
||||||
|
setActionLoading(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div>Chargement des membres en attente...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.rightBlock} glassCard`}>
|
<div className={`${styles.rightBlock} glassCard`}>
|
||||||
<h2>Membres en attente de validation</h2>
|
<h2>Membres en attente de validation</h2>
|
||||||
|
{error && <p style={{ color: "red" }}>{error}</p>}
|
||||||
|
|
||||||
<div className={styles.membersContainer}>
|
<div className={styles.membersContainer}>
|
||||||
{pendingMembers.map((member, index) => (
|
{pendingMembers.length === 0 && <p>Aucun membre en attente</p>}
|
||||||
<div key={index} className={`glassBorder ${styles.memberCard}`}>
|
|
||||||
<p>{member.name}</p>
|
{pendingMembers.map((member) => (
|
||||||
|
<div key={member.id} className={`glassCard ${styles.memberCard}`}>
|
||||||
|
<p>{member.name} {member.lastname}</p>
|
||||||
<div className={styles.buttonGroup}>
|
<div className={styles.buttonGroup}>
|
||||||
<Button onClick={() => handleValidate(member.name)} variant="primary">
|
<Button
|
||||||
Valider
|
onClick={() => handleValidate(member.id)}
|
||||||
|
variant="primary"
|
||||||
|
disabled={actionLoading === member.id}
|
||||||
|
>
|
||||||
|
{actionLoading === member.id ? "En cours..." : "Valider"}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => handleReject(member.name)} variant="danger">
|
<Button
|
||||||
Refuser
|
onClick={() => handleReject(member.id)}
|
||||||
|
variant="danger"
|
||||||
|
disabled={actionLoading === member.id}
|
||||||
|
>
|
||||||
|
{actionLoading === member.id ? "En cours..." : "Refuser"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -43,4 +106,4 @@ function pendingMembers() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default pendingMembers;
|
export default PendingMembers;
|
||||||
|
|||||||
@@ -18,10 +18,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.membersContainer::-webkit-scrollbar {
|
/*.membersContainer::-webkit-scrollbar {
|
||||||
display: none;
|
width: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.membersContainer::-webkit-scrollbar-track {
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.membersContainer::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 0, 0, 0.5);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.membersContainer::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(125, 249, 30, 0.7);
|
||||||
|
}*/
|
||||||
|
|
||||||
.memberCard {
|
.memberCard {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ export default function SettingsModal() {
|
|||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const [name, setName] = useState(user.name);
|
const [name, setName] = useState(user?.name);
|
||||||
const [lastname, setLastName] = useState(user.lastname);
|
const [lastname, setLastName] = useState(user?.lastname);
|
||||||
const [phone, setPhone] = useState(user.phone);
|
const [phone, setPhone] = useState(user?.phone);
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
logout();
|
logout();
|
||||||
|
|||||||
@@ -1,14 +1,32 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Doughnut } from "react-chartjs-2";
|
import { Doughnut } from "react-chartjs-2";
|
||||||
import { Chart, ArcElement } from "chart.js";
|
import { Chart, ArcElement } from "chart.js";
|
||||||
import { MenuItem, Select, FormControl } from "@mui/material";
|
import { MenuItem, Select, FormControl } from "@mui/material";
|
||||||
import styles from "./ParticipationChart.module.css";
|
import styles from "./ParticipationChart.module.css";
|
||||||
|
import getAllEvents from "../../utils/events/getAllEvents";
|
||||||
|
|
||||||
Chart.register(ArcElement);
|
Chart.register(ArcElement);
|
||||||
|
|
||||||
function ParticipationChart() {
|
function ParticipationChart() {
|
||||||
const [selected, setSelected] = useState("3");
|
const [selected, setSelected] = useState("3");
|
||||||
const [participationRate] = useState(78);
|
|
||||||
|
const [eventsCount, setEventsCount] = useState(0);
|
||||||
|
const [participationRate, setParticipationRate] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchEvents = async () => {
|
||||||
|
const res = await getAllEvents();
|
||||||
|
|
||||||
|
const eventsArray = res?.data ?? [];
|
||||||
|
|
||||||
|
setEventsCount(eventsArray.length);
|
||||||
|
|
||||||
|
const rate = Math.min(eventsArray.length, 100);
|
||||||
|
setParticipationRate(rate);
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchEvents();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const chartData = {
|
const chartData = {
|
||||||
labels: ["Progress", "Background"],
|
labels: ["Progress", "Background"],
|
||||||
@@ -17,9 +35,7 @@ function ParticipationChart() {
|
|||||||
data: [participationRate, 100 - participationRate],
|
data: [participationRate, 100 - participationRate],
|
||||||
backgroundColor: ["#1e60f9ff", "#ffffffff"],
|
backgroundColor: ["#1e60f9ff", "#ffffffff"],
|
||||||
borderWidth: 0,
|
borderWidth: 0,
|
||||||
weight: 10,
|
|
||||||
cutout: "93%",
|
cutout: "93%",
|
||||||
circumference: 360,
|
|
||||||
rotation: -90,
|
rotation: -90,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -31,56 +47,35 @@ function ParticipationChart() {
|
|||||||
legend: { display: false },
|
legend: { display: false },
|
||||||
tooltip: { enabled: false },
|
tooltip: { enabled: false },
|
||||||
},
|
},
|
||||||
animation: {
|
|
||||||
animateScale: true,
|
|
||||||
animateRotate: true,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.leftBlock} glassCard`}>
|
<div className={`${styles.leftBlock} glassCard`}>
|
||||||
<div className={styles.participationHeader}>
|
<div className={styles.participationHeader}>
|
||||||
<div className={`${styles.partiText} glassBorder`}>
|
<div className={`${styles.partiText} glassCard`}>
|
||||||
<label htmlFor={styles.months}>Taux de participation depuis :</label>
|
<label>Taux de participation depuis :</label>
|
||||||
<div className={styles.formControl}>
|
|
||||||
<FormControl className={styles.formDesign}>
|
<FormControl className={styles.formDesign}>
|
||||||
<Select
|
<Select
|
||||||
value={selected}
|
value={selected}
|
||||||
onChange={(e) => setSelected(e.target.value)}
|
onChange={(e) => setSelected(e.target.value)}
|
||||||
sx={{
|
className={styles.selectRoot}
|
||||||
width: "100px",
|
|
||||||
height: "35px",
|
|
||||||
fontSize: "19px",
|
|
||||||
paddingBottom: "-2px",
|
|
||||||
"& .MuiOutlinedInput-notchedOutline": { border: "none" },
|
|
||||||
"& .MuiSelect-select": { padding: "8px", paddingRight: "24px !important" },
|
|
||||||
"& .MuiSvgIcon-root": { fontSize: "20px" },
|
|
||||||
}}
|
|
||||||
MenuProps={{
|
|
||||||
PaperProps: {
|
|
||||||
sx: {
|
|
||||||
className: "glassCard",
|
|
||||||
borderRadius: "7px",
|
|
||||||
width: "100px",
|
|
||||||
maxWidth: "100px",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
displayEmpty
|
|
||||||
>
|
>
|
||||||
<MenuItem value="1" sx={{ fontSize: "18px" }}>1 mois</MenuItem>
|
<MenuItem value="1">1 mois</MenuItem>
|
||||||
<MenuItem value="3" sx={{ fontSize: "18px" }}>3 mois</MenuItem>
|
<MenuItem value="3">3 mois</MenuItem>
|
||||||
<MenuItem value="6" sx={{ fontSize: "18px" }}>6 mois</MenuItem>
|
<MenuItem value="6">6 mois</MenuItem>
|
||||||
<MenuItem value="12" sx={{ fontSize: "18px" }}>12 mois</MenuItem>
|
<MenuItem value="12">12 mois</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className={styles.chartContainer}>
|
<div className={styles.chartContainer}>
|
||||||
<div className={styles.chartWrapper}>
|
<div className={styles.chartWrapper}>
|
||||||
<Doughnut data={chartData} options={chartOptions} />
|
<Doughnut data={chartData} options={chartOptions} />
|
||||||
<div className={styles.chartPercentage}>{participationRate}%</div>
|
<div className={styles.chartPercentage}>
|
||||||
|
{eventsCount} events
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,14 +3,18 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: calc(100vh - 260px);
|
height: 100%;
|
||||||
margin-bottom: auto;
|
min-height: calc(100vh - 260px);
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.participationHeader {
|
.participationHeader {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.partiText {
|
.partiText {
|
||||||
@@ -26,14 +30,15 @@
|
|||||||
|
|
||||||
.partiText label {
|
.partiText label {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
text-align: right;
|
text-align: center;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chartContainer {
|
.chartContainer {
|
||||||
max-width: 80%;
|
width: 80%;
|
||||||
width: 400px;
|
max-width: 400px;
|
||||||
height: auto;
|
height: auto;
|
||||||
|
aspect-ratio: 1/1;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
animation: slideEvent2 2s;
|
animation: slideEvent2 2s;
|
||||||
@@ -58,12 +63,70 @@
|
|||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.formControl {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formDesign {
|
||||||
|
margin: 0 auto;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectRoot {
|
||||||
|
width: 100px;
|
||||||
|
height: 35px;
|
||||||
|
font-size: 19px;
|
||||||
|
border:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectRoot .MuiOutlinedInput-notchedOutline {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectRoot .MuiSelect-select {
|
||||||
|
padding: 8px;
|
||||||
|
padding-right: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectRoot .MuiSvgIcon-root {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectRoot .MuiMenu-paper {
|
||||||
|
border-radius: 7px;
|
||||||
|
width: 100px;
|
||||||
|
max-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectRoot .MuiMenuItem-root {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectMenuPaper {
|
||||||
|
border-radius: 7px;
|
||||||
|
width: 100px;
|
||||||
|
max-width: 120px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectMenuItem {
|
||||||
|
font-size: 18px;
|
||||||
|
min-height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@keyframes slideEvent2 {
|
@keyframes slideEvent2 {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(-20px);
|
transform: translateX(-20px);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
@@ -75,45 +138,52 @@
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(20px);
|
transform: translateX(20px);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.formControl {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.formDesign {
|
|
||||||
margin: 0 auto;
|
|
||||||
width: fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 760px) {
|
@media screen and (max-width: 760px) {
|
||||||
.partiText {
|
.leftBlock {
|
||||||
flex-direction: column;
|
width: 100%;
|
||||||
justify-content: center;
|
height: auto;
|
||||||
|
min-height: auto;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chartContainer {
|
||||||
|
width: 90%;
|
||||||
|
max-width: 300px;
|
||||||
|
height: auto;
|
||||||
|
aspect-ratio: 1/1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.partiText label {
|
.partiText label {
|
||||||
text-align: center;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.leftBlock {
|
.chartPercentage {
|
||||||
width: 100%;
|
font-size: 48px;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
height: fit-content;
|
|
||||||
margin-bottom: auto;
|
|
||||||
margin-top: 30px;
|
|
||||||
}
|
}
|
||||||
.chartContainer{
|
}
|
||||||
height:fit-content;
|
/* scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(100, 100, 100, 0.3);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(100, 100, 100, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export default function CreateEventBtn() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
|
{/* TODO : faire le responsive du menu de création d'événement */}
|
||||||
<Button className={`${styles.createButton} glassCard`} variant={"default"} onClick={() => setIsOpen(true)}> + </Button>
|
<Button className={`${styles.createButton} glassCard`} variant={"default"} onClick={() => setIsOpen(true)}> + </Button>
|
||||||
|
|
||||||
<Modal title={"Créer un événement"} open={isOpen} onClose={() => setIsOpen(false)}>
|
<Modal title={"Créer un événement"} open={isOpen} onClose={() => setIsOpen(false)}>
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import styles from "./eventTask.module.css";
|
||||||
|
import { formatDateLetter } from "../../utils/date/formatDateLetter.js";
|
||||||
|
import Button from "../ui/button/button.jsx";
|
||||||
|
import assign from "../../utils/tasks/assign.js";
|
||||||
|
import unAssign from "../../utils/tasks/unAssign.js";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import isAssigned from "../../utils/tasks/isAssigned.js";
|
||||||
|
import Modal from "../ui/modal/modal.jsx";
|
||||||
|
|
||||||
|
export default function EventTask({ task, index }) {
|
||||||
|
|
||||||
|
const [assigned, setAssigned] = useState(false);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [title, setTitle] = useState("");
|
||||||
|
const [message, setMessage] = useState("");
|
||||||
|
|
||||||
|
async function checkAssign() {
|
||||||
|
const data = await isAssigned(task.id);
|
||||||
|
setAssigned(data?.assigned);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkAssign();
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleRegisterBtn = async () => {
|
||||||
|
const res = await assign(task.id);
|
||||||
|
|
||||||
|
switch (res.status) {
|
||||||
|
case 200:
|
||||||
|
setTitle("Inscription réussie")
|
||||||
|
setMessage(`Vous êtes maintenant inscrit à la tâche ${task.name}`);
|
||||||
|
setOpen(true);
|
||||||
|
setAssigned(true);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 409:
|
||||||
|
setTitle("Erreur lors de l'inscription")
|
||||||
|
setMessage("Vous êtes déjà inscrit à cette tâche")
|
||||||
|
setOpen(true);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 403:
|
||||||
|
setTitle("Erreur lors de l'inscription")
|
||||||
|
setMessage("Le nombre maximum d'inscription pour cette tâche à été atteint")
|
||||||
|
setOpen(true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUnassign = async () => {
|
||||||
|
const res = await unAssign(task.id);
|
||||||
|
|
||||||
|
switch (res.status) {
|
||||||
|
case 200:
|
||||||
|
setTitle("Désinscription réussie")
|
||||||
|
setMessage(`Vous n'êtes plus inscrit à la tâche ${task.name}`);
|
||||||
|
setOpen(true);
|
||||||
|
setAssigned(false);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 409:
|
||||||
|
setTitle("Erreur lors de la désinscription")
|
||||||
|
setMessage(`Vous n'êtes pas inscrit à cette tâche`);
|
||||||
|
setOpen(true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return <div style={{"--i": index}} key={index} className={styles.content}>
|
||||||
|
<div className={styles.task}>
|
||||||
|
<div>
|
||||||
|
<span><strong>{task.name}</strong></span>
|
||||||
|
<p className={styles.taskDate}> {formatDateLetter(task.start)} - {formatDateLetter(task.end)} </p>
|
||||||
|
<p>{task.description}</p>
|
||||||
|
</div>
|
||||||
|
{assigned ?
|
||||||
|
<Button variant={"danger"} onClick={() => handleUnassign()}> Se désinscrire </Button>
|
||||||
|
:
|
||||||
|
<Button onClick={() => handleRegisterBtn()}> S’inscrire </Button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal open={open} onClose={setOpen} title={title}>
|
||||||
|
<p className={styles.modalContent}>{message}</p>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
.task {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskDate {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
margin: 16px 0px 16px 0px;
|
||||||
|
padding: 10px 20px 10px 20px;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow:
|
||||||
|
inset 1.25px 1.25px 1px rgba(255, 255, 255, 1),
|
||||||
|
inset -1.25px -1.25px 1px rgba(255, 255, 255, 1),
|
||||||
|
inset -0.25px 0.25px 1px rgba(0, 0, 0, 0.2),
|
||||||
|
inset 0.25px -0.25px 1px rgba(0, 0, 0, 0.2);
|
||||||
|
opacity: 0;
|
||||||
|
animation: fadeInUp 0.6s ease-out forwards;
|
||||||
|
animation-delay: calc(var(--i) * 0.1s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalContent {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import styles from "./eventList.module.css";
|
||||||
|
import { formatDateLetter } from "../../utils/date/formatDateLetter.js";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export default function EventItem({ eventGroup }) {
|
||||||
|
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
|
||||||
|
return <div key={eventGroup.date} className={`${styles.eventGroup} glassBorder`} onClick={() => setIsVisible(!isVisible)}>
|
||||||
|
<div className={styles.eventTitle}>
|
||||||
|
<p className={styles.title}>{eventGroup.name}</p>
|
||||||
|
<p>{formatDateLetter(eventGroup.start)}</p>
|
||||||
|
</div>
|
||||||
|
{isVisible &&
|
||||||
|
<ul className={styles.eventDetails}>
|
||||||
|
{eventGroup.tasks.map((task, i) => (
|
||||||
|
<li key={i} className={styles.eventTasks}>
|
||||||
|
<p>- {task.name} - {task.description}</p>
|
||||||
|
<p>{formatDateLetter(task.start)} - {formatDateLetter(task.end)}</p>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import styles from "./eventList.module.css";
|
import styles from "./eventList.module.css";
|
||||||
import { formatDateLetter } from "../../utils/date/formatDateLetter.js";
|
import EventItem from "./eventItem";
|
||||||
|
|
||||||
|
|
||||||
function EventList({ events }) {
|
function EventList({ events }) {
|
||||||
@@ -16,26 +16,11 @@ function EventList({ events }) {
|
|||||||
<div className={`${styles.glassCard} glassCard`}>
|
<div className={`${styles.glassCard} glassCard`}>
|
||||||
<h2>Liste des événements à venir</h2>
|
<h2>Liste des événements à venir</h2>
|
||||||
{sortedEvents.length > 0 ? (
|
{sortedEvents.length > 0 ? (
|
||||||
<ul className={styles.eventList}>
|
<div className={styles.eventList}>
|
||||||
{sortedEvents.map((eventGroup) => (
|
{sortedEvents.map((eventGroup, index) => (
|
||||||
<li key={eventGroup.date} className={`${styles.eventGroup} glassBorder`}>
|
<EventItem eventGroup={eventGroup} key={index} />
|
||||||
<div className={styles.eventTitle}>
|
))}
|
||||||
<p className={styles.title}>{eventGroup.name}</p>
|
|
||||||
<p>{formatDateLetter(eventGroup.start)}</p>
|
|
||||||
</div>
|
</div>
|
||||||
{/* TODO: Faire une liste déroulante pour ne plus afficher les tache */}
|
|
||||||
<ul className={styles.eventDetails}>
|
|
||||||
{eventGroup.tasks.map((task, i) => (
|
|
||||||
<li key={i} className={styles.eventTasks}>
|
|
||||||
<p>- {task.name} - {task.description}</p>
|
|
||||||
<p>{formatDateLetter(task.start)} - {formatDateLetter(task.end)}</p>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
) : (
|
) : (
|
||||||
<p>Aucun événement planifié pour l'instant.</p>
|
<p>Aucun événement planifié pour l'instant.</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ h2{
|
|||||||
padding: 10px 20px 10px 20px;
|
padding: 10px 20px 10px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.eventGroup:hover{
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.eventTitle {
|
.eventTitle {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import Button from "../ui/button/button.jsx";
|
||||||
|
import Modal from "../ui/modal/modal.jsx";
|
||||||
|
import { useState } from "react";
|
||||||
|
import styles from "./manageMember.module.css"
|
||||||
|
import DeleteMemberBtn from "./deleteMember/deleteMemberBtn.jsx";
|
||||||
|
import ModifyRole from "./modifyrole/ModifyRole.jsx";
|
||||||
|
|
||||||
|
export default function ManageMember({ user }) {
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<Button onClick={() => setOpen(true)} variant={"default"} className={styles.manageBtn}>
|
||||||
|
<img src="/icons/tools.svg" alt="tools icon" className={styles.icon} />
|
||||||
|
<p>Gérer</p>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Modal open={open} onClose={() => setOpen(false)} title={`Gestion de l'utilisateur ${user.name}`}>
|
||||||
|
|
||||||
|
<div className={styles.manageSection}>
|
||||||
|
<h3>Supprimer le compte</h3>
|
||||||
|
<DeleteMemberBtn name={user.name} id={user.id} className={styles.manageBlockContent} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.manageSection}>
|
||||||
|
<h3>Modifier le rôle</h3>
|
||||||
|
<ModifyRole user={user} />
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import Button from "../../ui/button/button.jsx";
|
||||||
|
import Modal from "../../ui/modal/modal.jsx";
|
||||||
|
import { useState } from "react";
|
||||||
|
import deleteUser from "../../../utils/users/deleteUser.js"
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
|
||||||
|
export default function DeleteMemberBtn({ name, id, ...props }) {
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const delMember = async () => {
|
||||||
|
const result = await deleteUser(id);
|
||||||
|
|
||||||
|
if (result.status === 200 || result.status === 204) {
|
||||||
|
setOpen(false);
|
||||||
|
navigate("/volunteers");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div {...props}>
|
||||||
|
<Button variant={"danger"} onClick={() => setOpen(true)}>Supprimer</Button>
|
||||||
|
|
||||||
|
<Modal open={open} onClose={() => setOpen(false)} title={"Supprimer"}>
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 15 }}>
|
||||||
|
<p>Etes-vous sur de vouloir supprimer le compte {name} ?</p>
|
||||||
|
<Button variant={"danger"} onClick={delMember}>Supprimer</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
.icon {
|
||||||
|
width: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manageBtn {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manageSection {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manageBlockContent {
|
||||||
|
margin: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modifyRole {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select {
|
||||||
|
width: 70%;
|
||||||
|
padding: 10px 14px;
|
||||||
|
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1d1d1f;
|
||||||
|
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #d2d2d7;
|
||||||
|
border-radius: 10px;
|
||||||
|
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalContent {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import styles from "../manageMember.module.css";
|
||||||
|
import Button from "../../ui/button/button.jsx";
|
||||||
|
import modifyRole from "../../../utils/users/modifyRole.js";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import getRoles from "../../../utils/getRoles.js";
|
||||||
|
import Modal from "../../ui/modal/modal.jsx";
|
||||||
|
|
||||||
|
export default function ModifyRole({ user }) {
|
||||||
|
|
||||||
|
const [roles, setRoles] = useState([]);
|
||||||
|
const [selectedRole, setSelectedRole] = useState(3);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [title, setTitle] = useState("");
|
||||||
|
const [description, setDescription] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getRoles().then(data => {
|
||||||
|
if (data) setRoles(data);
|
||||||
|
});
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
|
||||||
|
const handleRoleUpdate = async () => {
|
||||||
|
const res = await modifyRole(selectedRole, user.id);
|
||||||
|
|
||||||
|
if (res.success) {
|
||||||
|
setDescription("Rôle modifié avec succès");
|
||||||
|
setTitle("Modification")
|
||||||
|
setOpen(true);
|
||||||
|
} else {
|
||||||
|
setDescription("Erreur lors de la modification du rôle");
|
||||||
|
setTitle("Erreur")
|
||||||
|
setOpen(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return <div className={`${styles.manageBlockContent} ${styles.modifyRole}`}>
|
||||||
|
<select
|
||||||
|
value={selectedRole}
|
||||||
|
onChange={(e) => setSelectedRole(e.target.value)}
|
||||||
|
className={styles.select}
|
||||||
|
>
|
||||||
|
{roles.map((role, index) => (
|
||||||
|
<option key={index} value={index+1}>
|
||||||
|
{role}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<Button onClick={handleRoleUpdate} variant={"default"}>
|
||||||
|
Enregistrer
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Modal open={open} onClose={() => setOpen(false)} title={title}>
|
||||||
|
<p className={styles.modalContent}>{description}</p>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export const APP_CONFIG = {
|
||||||
|
contactEmail: "contact@example.com",
|
||||||
|
contactPhone: "+33 01 02 03 04 05"
|
||||||
|
}
|
||||||
@@ -8,6 +8,8 @@ import { AuthProvider } from "./contexts/auth/authProvider.jsx";
|
|||||||
|
|
||||||
createRoot(document.getElementById('root')).render(
|
createRoot(document.getElementById('root')).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
{/* TODO: refaire l'arborescence du frontend */}
|
||||||
|
{/* TODO: refaire le docker du backend */}
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import styles from "./LegalNotices.module.css";
|
||||||
|
import {Link} from "react-router";
|
||||||
|
|
||||||
|
export default function legalNotices() {
|
||||||
|
return (
|
||||||
|
<div className={`${styles.legalNotices} glassCard`}>
|
||||||
|
<h1>Mentions Légales</h1>
|
||||||
|
|
||||||
|
<section className={styles.section}>
|
||||||
|
<h2 className={styles.h2}>1. Édition de l'application</h2>
|
||||||
|
<p>
|
||||||
|
L'édition et la direction de la publication de l'application sont assurées par Monsieur Robin Frère
|
||||||
|
<br />Contact : <Link to={"https://comite.beaupont.fr/contactez-nous"}>https://comite.beaupont.fr/contactez-nous</Link>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className={styles.section}>
|
||||||
|
<h2 className={styles.h2}>2. Conception et Développement</h2>
|
||||||
|
<p>L'application a été conçue et développée par :</p>
|
||||||
|
<ul>
|
||||||
|
<li>Quentin T'jampens</li>
|
||||||
|
<li>Antoine Ordonneau</li>
|
||||||
|
<li>Malo Leblond</li>
|
||||||
|
<li>Giovanni Josserand</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className={styles.section}>
|
||||||
|
<h2 className={styles.h2}>3. Hébergement</h2>
|
||||||
|
<p>
|
||||||
|
L'application est hébergée par la société OVH.
|
||||||
|
<ul>
|
||||||
|
<li>Siège social : 2 rue Kellermann - 59100 Roubaix - France</li>
|
||||||
|
<li>Site web : <Link to={"www.ovh.com"}>www.ovh.com</Link></li>
|
||||||
|
</ul>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
.h2 {
|
||||||
|
justify-content: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section ul {
|
||||||
|
padding : 0 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.legalNotices{
|
||||||
|
margin: 16px 2em;
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import Background from "../../components/background/background.jsx";
|
import Background from "../../components/background/background.jsx";
|
||||||
import styles from "./PageNotFound.module.css"
|
import styles from "./PageNotFound.module.css"
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
|
import { APP_CONFIG } from "../../config/appConfig.js";
|
||||||
|
|
||||||
export default function PageNotFound() {
|
export default function PageNotFound() {
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@ export default function PageNotFound() {
|
|||||||
- Revenir à l’accueil <span onClick={() => navigate("/")} className={styles.redirectSpan}>ici.</span> <br/>
|
- Revenir à l’accueil <span onClick={() => navigate("/")} className={styles.redirectSpan}>ici.</span> <br/>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
<p> Contact : contact@example.com </p>
|
<p> Contact : {APP_CONFIG.contactEmail} </p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Background>
|
</Background>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import SettingsModal from "../../components/SettingsModal/SettingsModal.jsx";
|
|||||||
function ProfilePage() {
|
function ProfilePage() {
|
||||||
|
|
||||||
const { user } = useContext(AuthContext);
|
const { user } = useContext(AuthContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={`${styles.profileboard} glassCard`}>
|
<div className={`${styles.profileboard} glassCard`}>
|
||||||
@@ -21,22 +22,22 @@ function ProfilePage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.description}>
|
<div className={styles.description}>
|
||||||
<div className={styles.headerProfile}>
|
<div className={styles.headerProfile}>
|
||||||
<h2> {user.name} {user.lastname} </h2>
|
<h2> {user?.name} {user?.lastname} </h2>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.topDescription}>
|
<div className={styles.topDescription}>
|
||||||
<div className={`${styles.infoBlock} glassBorder`}>
|
<div className={`${styles.infoBlock} glassBorder`}>
|
||||||
<p><strong>Role :</strong> {user.role}</p>
|
<p><strong>Role :</strong> {user?.role}</p>
|
||||||
<p><strong>Membre depuis :</strong> {formatDate(user.created_at)}</p>
|
<p><strong>Membre depuis :</strong> {user && formatDate(user.created_at)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={`${styles.infoBlock} glassBorder`}>
|
<div className={`${styles.infoBlock} glassBorder`}>
|
||||||
<p><strong>Mail :</strong> {user.email} </p>
|
<p><strong>Mail :</strong> {user?.email} </p>
|
||||||
<p><strong>Téléphone :</strong> {user.phone === null ? "Pas de numéro enregistré" : user.phone}</p>
|
<p><strong>Téléphone :</strong> {user?.phone === null ? "Pas de numéro enregistré" : user?.phone}</p>
|
||||||
</div>
|
</div>
|
||||||
<SettingsModal />
|
<SettingsModal />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>Tâches :</h2>
|
<h2>Tâches :</h2>
|
||||||
{user.tasks && user.tasks.length > 0 ? (
|
{user && user.tasks.length > 0 ? (
|
||||||
user.tasks.map((task, index) => (
|
user.tasks.map((task, index) => (
|
||||||
<Task key={index} task={task} />
|
<Task key={index} task={task} />
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
import styles from "./RGPD.module.css";
|
||||||
|
import {Link} from "react-router";
|
||||||
|
|
||||||
|
export default function RGPD() {
|
||||||
|
return (
|
||||||
|
<div className={`${styles.RGPD} glassCard`}>
|
||||||
|
<h1> Fiche d'informatino RGPD </h1>
|
||||||
|
|
||||||
|
<section className={styles.section}>
|
||||||
|
<h2 className={styles.h2}> 1. Présentation du site </h2>
|
||||||
|
<p>
|
||||||
|
Le présent site web est un outil de gestion des bénévoles permettant notamment :
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>L’inscription et la gestion des profils bénévoles</li>
|
||||||
|
<li>L’organisation d’événements ou de missions</li>
|
||||||
|
<li>La communication avec les bénévoles</li>
|
||||||
|
<li>Le suivi des participations</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Le site est édité par :
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>Nom de la structure : Comité des fêtes de Beaupont</li>
|
||||||
|
<li>Forme juridique : Association loi 1901</li>
|
||||||
|
<li>Adresse : Beaupont 01270</li>
|
||||||
|
<li>Contact : <Link to="/contactez-nous">Formulaire de contact</Link></li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className={styles.section}>
|
||||||
|
<h2 className={styles.h2}>2. Responsable du traitement</h2>
|
||||||
|
<p>
|
||||||
|
Le responsable du traitement des données personnelles est Monsieur FRÈRE Robin
|
||||||
|
<br />Il est responsable de la collecte et de l’utilisation des données personnelles conformément au Règlement Général sur la Protection des Données (RGPD – UE 2016/679).
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className={styles.section}>
|
||||||
|
<h2 className={styles.h2}>3. Données personnelles collectées</h2>
|
||||||
|
<p>Les données susceptibles d’être collectées sont notamment :</p>
|
||||||
|
<ul>
|
||||||
|
<li>Les données d’identification : nom, prénom, adresse email et numéro de téléphone.</li>
|
||||||
|
<li>Les données techniques : adresse IP, données de connexion (logs) et type de navigateur et appareil.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className={styles.section}>
|
||||||
|
<h2 className={styles.h2}>4. Finalités du traitement</h2>
|
||||||
|
<p>Les données personnelles sont collectées pour les finalités suivantes :</p>
|
||||||
|
<ul>
|
||||||
|
<li>Gestion des comptes bénévoles</li>
|
||||||
|
<li>Organisation et suivi des activités / tâches</li>
|
||||||
|
<li>Communication interne (notifications)</li>
|
||||||
|
<li>Gestion administrative et statistique</li>
|
||||||
|
<li>Sécurité et bon fonctionnement du site</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className={styles.section}>
|
||||||
|
<h2 className={styles.h2}>5. Base légale du traitement</h2>
|
||||||
|
<p>Les traitements reposent sur :</p>
|
||||||
|
<ul>
|
||||||
|
<li>Le consentement de l’utilisateur</li>
|
||||||
|
<li>L’intérêt légitime de l’organisme (gestion des bénévoles)</li>
|
||||||
|
<li>Une obligation légale, le cas échéant</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className={styles.section}>
|
||||||
|
<h2 className={styles.h2}>6. Durée de conservation</h2>
|
||||||
|
<p>Les données sont conservées :</p>
|
||||||
|
<ul>
|
||||||
|
<li>Pendant la durée de l’engagement bénévole</li>
|
||||||
|
<li>Puis archivées ou supprimées au plus tard 3 ans après la dernière activité ou contact</li>
|
||||||
|
</ul>
|
||||||
|
<p>Les logs techniques sont conservés pour une durée maximale de 2 mois.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className={styles.section}>
|
||||||
|
<h2 className={styles.h2}>7. Destinataires des données</h2>
|
||||||
|
<p>Les données sont accessibles uniquement :</p>
|
||||||
|
<ul>
|
||||||
|
<li>Aux personnes habilitées de l’organisme</li>
|
||||||
|
<li>Aux prestataires techniques (hébergement, maintenance), strictement nécessaires</li>
|
||||||
|
</ul>
|
||||||
|
<p>Aucune donnée n’est vendue ou cédée à des tiers.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className={styles.section}>
|
||||||
|
<h2 className={styles.h2}>8. Hébergement des données</h2>
|
||||||
|
<p>Les données sont hébergées par la société OVH :</p>
|
||||||
|
<ul>
|
||||||
|
<li>Siège social : 2 rue Kellermann - 59100 Roubaix - France</li>
|
||||||
|
<li>Site web : <Link to={"www.ovh.com"}>www.ovh.com</Link></li>
|
||||||
|
</ul>
|
||||||
|
<p>L’hébergeur garantit un niveau de sécurité conforme au RGPD.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className={styles.section}>
|
||||||
|
<h2 className={styles.h2}>9. Sécurité des données</h2>
|
||||||
|
<p>Des mesures techniques et organisationnelles sont mises en place :</p>
|
||||||
|
<ul>
|
||||||
|
<li>Accès sécurisé par identifiant et mot de passe</li>
|
||||||
|
<li>Sauvegardes régulières</li>
|
||||||
|
<li>Accès limité aux personnes autorisées</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className={styles.section}>
|
||||||
|
<h2 className={styles.h2}>10. Droits des utilisateurs</h2>
|
||||||
|
<p>Conformément au RGPD, vous disposez des droits suivants :</p>
|
||||||
|
<ul>
|
||||||
|
<li>Droit d’accès à vos données</li>
|
||||||
|
<li>Droit de rectification</li>
|
||||||
|
<li>Droit à l’effacement (droit à l’oubli)</li>
|
||||||
|
<li>Droit à la limitation du traitement</li>
|
||||||
|
<li>Droit d’opposition</li>
|
||||||
|
<li>Droit à la portabilité des données</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className={styles.section}>
|
||||||
|
<h2 className={styles.h2}>11. Réclamation</h2>
|
||||||
|
<p>
|
||||||
|
Si vous estimez que vos droits ne sont pas respectés, vous pouvez introduire une réclamation auprès de la :
|
||||||
|
<br /><strong>CNIL</strong> – Commission Nationale de l’Informatique et des Libertés
|
||||||
|
<br />Site : <a href="https://www.cnil.fr" target="_blank" rel="noreferrer">https://www.cnil.fr</a>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className={styles.section}>
|
||||||
|
<h2 className={styles.h2}>12. Mise à jour</h2>
|
||||||
|
<p>
|
||||||
|
La présente fiche RGPD peut être mise à jour à tout moment afin de rester conforme à la réglementation en vigueur.
|
||||||
|
<br /><em>Dernière mise à jour : 09/01/2026</em>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
.h2 {
|
||||||
|
justify-content: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section ul {
|
||||||
|
padding : 0 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.RGPD{
|
||||||
|
margin: 16px 2em;
|
||||||
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import styles from "./VolunteerProfilePage.module.css"
|
import styles from "./VolunteerProfilePage.module.css"
|
||||||
import { useParams } from "react-router";
|
import { useParams } from "react-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useContext } from "react";
|
||||||
import getUserById from "../../utils/users/getUserById.js";
|
import getUserById from "../../utils/users/getUserById.js";
|
||||||
import formatDate from "../../utils/date/formatDate.js";
|
import formatDate from "../../utils/date/formatDate.js";
|
||||||
import Task from "../../components/Task/Task.jsx";
|
import Task from "../../components/Task/Task.jsx";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
|
import ManageMember from "../../components/manageMember/ManageMember.jsx";
|
||||||
|
import { AuthContext } from "../../contexts/auth/AuthContext.js";
|
||||||
|
|
||||||
export default function VolunteerProfilePage() {
|
export default function VolunteerProfilePage() {
|
||||||
|
|
||||||
@@ -13,6 +15,8 @@ export default function VolunteerProfilePage() {
|
|||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { user: currentUser } = useContext(AuthContext);
|
||||||
|
|
||||||
const [user, setUser] = useState({});
|
const [user, setUser] = useState({});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -41,13 +45,14 @@ export default function VolunteerProfilePage() {
|
|||||||
<div className={styles.description}>
|
<div className={styles.description}>
|
||||||
<div className={styles.headerProfile}>
|
<div className={styles.headerProfile}>
|
||||||
<h2> {user.name} {user.lastname} </h2>
|
<h2> {user.name} {user.lastname} </h2>
|
||||||
|
{currentUser?.isAdmin && <ManageMember user={user} />}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.topDescription}>
|
<div className={styles.topDescription}>
|
||||||
<div className={styles.infoBlock}>
|
<div className={`${styles.infoBlock} glassBorder`}>
|
||||||
<p><strong>Role :</strong> {user.role}</p>
|
<p><strong>Role :</strong> {user.role}</p>
|
||||||
<p><strong>Membre depuis :</strong> {formatDate(user.created_at)}</p>
|
<p><strong>Membre depuis :</strong> {formatDate(user.created_at)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.infoBlock}>
|
<div className={`${styles.infoBlock} glassBorder`}>
|
||||||
<p><strong>Mail :</strong> {user.email} </p>
|
<p><strong>Mail :</strong> {user.email} </p>
|
||||||
<p><strong>Téléphone :</strong> {user.phone === null ? "Pas de numéro enregistré" : user.phone}</p>
|
<p><strong>Téléphone :</strong> {user.phone === null ? "Pas de numéro enregistré" : user.phone}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,70 +1,183 @@
|
|||||||
import {useState} from "react";
|
import { useState, useEffect, useContext } from "react";
|
||||||
import styles from "./eventDetail.module.css";
|
import styles from "./eventDetail.module.css";
|
||||||
import { useParams } from "react-router";
|
import { useParams } from "react-router";
|
||||||
|
import getEventById from "../../utils/events/getEventById.js";
|
||||||
|
import { formatDateLetter } from "../../utils/date/formatDateLetter.js";
|
||||||
|
import EventTask from "../../components/eventTask/EventTask.jsx";
|
||||||
|
import Button from "../../components/ui/button/button.jsx";
|
||||||
|
import Modal from "../../components/ui/modal/modal.jsx";
|
||||||
|
import { AuthContext } from "../../contexts/auth/AuthContext.js";
|
||||||
|
import deleteEvent from "../../utils/events/deleteEvent.js";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
import TextInput from "../../components/ui/input/input.jsx";
|
||||||
|
import createTaskApi from "../../utils/tasks/createTask.js";
|
||||||
|
|
||||||
|
|
||||||
function eventDetail() {
|
function EventDetail() {
|
||||||
|
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const id = params.id;
|
const id = params.id;
|
||||||
|
const { user } = useContext(AuthContext)
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [tasks, setTasks] = useState([
|
const [open, setOpen] = useState(false);
|
||||||
{ text: "Préparer les slides", registered: false },
|
const [event, setEvent] = useState(null);
|
||||||
{ text: "Réserver la salle", registered: false },
|
|
||||||
{ text: "Envoyer les invitations", registered: false },
|
|
||||||
{ text: "Tester la configuration technique", registered: false },
|
|
||||||
{ text: "Organiser le catering", registered: false },
|
|
||||||
]);
|
|
||||||
|
|
||||||
const event = {
|
const [openTaskModal, setOpenTaskModal] = useState(false);
|
||||||
title: "Conférence React 2025",
|
const [taskName, setTaskName] = useState("");
|
||||||
date: "27 Décembre 2025",
|
const [taskDescription, setTaskDescription] = useState("");
|
||||||
location: "Paris, France",
|
const [taskLocation, setTaskLocation] = useState("");
|
||||||
description: "azertyuiop azertyuiop azertyuiop azertyuiop azertyuiop azertyuiop azertyuiop azertyuiop azertyuiop azertyuiop azertyuiop azertyuiop"
|
const [maxParticipants, setMaxParticipants] = useState("");
|
||||||
|
const [startTime, setStartTime] = useState("");
|
||||||
|
const [endTime, setEndTime] = useState("");
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const e = await getEventById(id);
|
||||||
|
setEvent(e);
|
||||||
|
}) ()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
setOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const delEvent = async () => {
|
||||||
|
const result = await deleteEvent(id);
|
||||||
|
|
||||||
|
if (result.status === 200 || result.status === 204) {
|
||||||
|
setOpen(false);
|
||||||
|
navigate("/events");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createTask = async () => {
|
||||||
|
const success = await createTaskApi({
|
||||||
|
eventId: id,
|
||||||
|
name: taskName,
|
||||||
|
description: taskDescription,
|
||||||
|
start: startTime,
|
||||||
|
end: endTime,
|
||||||
|
location: taskLocation,
|
||||||
|
maxParticipants,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
setOpenTaskModal(false);
|
||||||
|
setTaskName("");
|
||||||
|
setTaskDescription("");
|
||||||
|
setTaskLocation("");
|
||||||
|
setMaxParticipants("");
|
||||||
|
setStartTime("");
|
||||||
|
setEndTime("");
|
||||||
|
} else {
|
||||||
|
console.log("⚠️ Échec de la création de la task");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleRegister = (index) => {
|
|
||||||
setTasks((prevTasks) =>
|
|
||||||
prevTasks.map((task, i) =>
|
|
||||||
i === index
|
|
||||||
? { ...task, registered: !task.registered }
|
|
||||||
: task
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.allContent}>
|
<div className={styles.allContent}>
|
||||||
<div className={`glassCard`}>
|
<div className={`glassCard`}>
|
||||||
<div className={styles.textImage}>
|
<div className={styles.textImage}>
|
||||||
<img src="/public/icons/theme/dark.svg" alt="Illustration événement" className={styles.eventImage}/>
|
<img src="/icons/theme/dark.svg" alt="Illustration événement" className={styles.eventImage}/>
|
||||||
<div className={styles.eventInfo}>
|
<div className={styles.eventInfo}>
|
||||||
<h2>{event.title}</h2>
|
<h2>{event?.name}</h2>
|
||||||
<p>Date: {event.date}</p>
|
<div className={styles.eventContent}>
|
||||||
<p>Lieu: {event.location}</p>
|
<div>
|
||||||
<p>Description: {event.description}</p>
|
<p>{event && formatDateLetter(event.start)}</p>
|
||||||
|
<p>{event?.description}</p>
|
||||||
|
</div>
|
||||||
|
{user?.isAdmin && <div className={styles.manageEvent}>
|
||||||
|
<Button variant={"danger"} className={styles.manageBtn} onClick={handleDelete}>Supprimer</Button>
|
||||||
|
<Button variant={"default"} className={styles.manageBtn} onClick={() => setOpenTaskModal(true)}>Créer tâche</Button>
|
||||||
|
</div>}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>Tâches</h2>
|
<h2>Tâches</h2>
|
||||||
<ul className={styles.tasks}>
|
<div className={styles.tasks}>
|
||||||
{tasks.map((task, index) => (
|
{event?.tasks?.map((task, index) => (
|
||||||
<li style={{"--i": index}} key={index}>
|
<EventTask task={task} key={index} />
|
||||||
<div className={styles.tasksButton}>
|
|
||||||
<span><strong>{task.text}</strong></span>
|
|
||||||
<button
|
|
||||||
className={styles.registerButton}
|
|
||||||
onClick={() => toggleRegister(index)}
|
|
||||||
>
|
|
||||||
{task.registered ? "Se désinscrire" : "S’inscrire"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
))}
|
))}
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Modal open={open} onClose={() => setOpen(false)} title={"Suppression"}>
|
||||||
|
<div className={styles.modalContent}>
|
||||||
|
<p>Etes-vous sur de vouloir supprimer l'événement {event?.name} ?</p>
|
||||||
|
|
||||||
|
<Button variant={"default"} onClick={delEvent}>Confirmer</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal open={openTaskModal} onClose={() => setOpenTaskModal(false)} title={"Créer tâche"}>
|
||||||
|
<div className={styles.modalContent}>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p>Nom de la tâche</p>
|
||||||
|
<TextInput
|
||||||
|
value={taskName}
|
||||||
|
onChange={(e) => setTaskName(e.target.value)}
|
||||||
|
placeholder="Nom de la tâche"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p>Description</p>
|
||||||
|
<TextInput
|
||||||
|
value={taskDescription}
|
||||||
|
onChange={(e) => setTaskDescription(e.target.value)}
|
||||||
|
placeholder="Description de la tâche"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p>Lieu</p>
|
||||||
|
<TextInput
|
||||||
|
value={taskLocation}
|
||||||
|
onChange={(e) => setTaskLocation(e.target.value)}
|
||||||
|
placeholder="Lieu"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p>Nombre maximum de participants</p>
|
||||||
|
<TextInput
|
||||||
|
type="number"
|
||||||
|
value={maxParticipants}
|
||||||
|
onChange={(e) => setMaxParticipants(e.target.value)}
|
||||||
|
placeholder="Participants max"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p>Heure de début</p>
|
||||||
|
<TextInput
|
||||||
|
type="datetime-local"
|
||||||
|
value={startTime}
|
||||||
|
onChange={(e) => setStartTime(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p>Heure de fin</p>
|
||||||
|
<TextInput
|
||||||
|
type="datetime-local"
|
||||||
|
value={endTime}
|
||||||
|
onChange={(e) => setEndTime(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button variant="default" onClick={createTask}> Créer la tâche </Button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default eventDetail;
|
export default EventDetail;
|
||||||
@@ -38,55 +38,33 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.registerButton {
|
|
||||||
margin-left: auto;
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: 12px;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
background: #019AFF;
|
|
||||||
color: white;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.registerButton:hover {
|
.registerButton:hover {
|
||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tasksButton {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tasks {
|
.tasks {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tasks li {
|
.eventContent {
|
||||||
margin: 16px 0px 16px 0px;
|
display: flex;
|
||||||
padding: 10px 20px 10px 20px;
|
justify-content: space-between;
|
||||||
border-radius: 20px;
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
box-shadow:
|
|
||||||
inset 1.25px 1.25px 1px rgba(255, 255, 255, 1),
|
|
||||||
inset -1.25px -1.25px 1px rgba(255, 255, 255, 1),
|
|
||||||
inset -0.25px 0.25px 1px rgba(0, 0, 0, 0.2),
|
|
||||||
inset 0.25px -0.25px 1px rgba(0, 0, 0, 0.2);
|
|
||||||
opacity: 0;
|
|
||||||
animation: fadeInUp 0.6s ease-out forwards;
|
|
||||||
animation-delay: calc(var(--i) * 0.1s);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeInUp {
|
.manageBtn {
|
||||||
from {
|
height: fit-content;
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(10px);
|
|
||||||
}
|
}
|
||||||
to {
|
|
||||||
opacity: 1;
|
.modalContent {
|
||||||
transform: translateY(0);
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.manageEvent {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,8 @@ import ValidationErrorPage from "./pages/waitValidationPage/ValidationErrorPage.
|
|||||||
import VolunteerProfilePage from "./pages/VolunteerProfile/VolunteerProfilePage.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 RGPD from "./pages/RGPD/RGPD.jsx";
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@@ -65,6 +67,14 @@ const router = createBrowserRouter([
|
|||||||
path: "/events/:id",
|
path: "/events/:id",
|
||||||
Component: eventDetail,
|
Component: eventDetail,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/legalNotices",
|
||||||
|
Component: legalNotices,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/RGPD",
|
||||||
|
Component: RGPD,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export default async function login(email, password) {
|
|||||||
const csrfToken = await getXSRFToken();
|
const csrfToken = await getXSRFToken();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`http://${import.meta.env.VITE_API_URL}/api/users/login`, {
|
const response = await fetch(`${import.meta.env.VITE_API_URL}/api/users/login`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export default async function logout() {
|
|||||||
const csrfToken = await getXSRFToken();
|
const csrfToken = await getXSRFToken();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`http://${import.meta.env.VITE_API_URL}/api/users/logout`, {
|
const response = await fetch(`${import.meta.env.VITE_API_URL}/api/users/logout`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
|
||||||
|
|
||||||
export function formatDateLetter(d) {
|
export function formatDateLetter(d) {
|
||||||
if (!d) return "Date inconnue";
|
if (!d) return "Date inconnue";
|
||||||
|
|
||||||
@@ -27,3 +29,4 @@ export function formatDateLetterJS(d) {
|
|||||||
const [year, month, day] = d.split('-');
|
const [year, month, day] = d.split('-');
|
||||||
return `${day} ${monthNames[parseInt(month) - 1]} ${year}`;
|
return `${day} ${monthNames[parseInt(month) - 1]} ${year}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export default async function createEvent(name, description, start, end) {
|
|||||||
const csrfToken = await getXSRFToken();
|
const csrfToken = await getXSRFToken();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`http://${import.meta.env.VITE_API_URL}/api/events/create`, {
|
const response = await fetch(`${import.meta.env.VITE_API_URL}/api/events/create`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import getXSRFToken from "../getXSRF.js";
|
||||||
|
|
||||||
|
export default async function deleteEvent(eventId) {
|
||||||
|
const csrfToken = await getXSRFToken();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${import.meta.env.VITE_API_URL}/api/events/${eventId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-XSRF-TOKEN': csrfToken,
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: eventId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 422) {
|
||||||
|
return {
|
||||||
|
status: 422,
|
||||||
|
errors: data.errors
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Erreur HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { status: response.status, data };
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
return { status: "error", error };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import getAllEvents from "../../utils/events/getAllEvents";
|
||||||
|
|
||||||
|
function EventsCounter() {
|
||||||
|
const [eventsCount, setEventsCount] = useState(0);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchEvents = async () => {
|
||||||
|
try {
|
||||||
|
const events = await getAllEvents();
|
||||||
|
if (Array.isArray(events)) {
|
||||||
|
setEventsCount(events.length);
|
||||||
|
} else {
|
||||||
|
setEventsCount(0);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setEventsCount(0);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchEvents();
|
||||||
|
}, []);
|
||||||
|
if (loading) return <p>Chargement...</p>;
|
||||||
|
return <p>Nombre d’événements : {eventsCount}</p>;
|
||||||
|
}
|
||||||
|
export default EventsCounter;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
export default async function getAllEvents() {
|
export default async function getAllEvents() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`http://${import.meta.env.VITE_API_URL}/api/events/`, {
|
const res = await fetch(`${import.meta.env.VITE_API_URL}/api/events/`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export default async function GetEventById(id) {
|
export default async function GetEventById(id) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`http://${import.meta.env.VITE_API_URL}/api/events/${id}`, {
|
const res = await fetch(`${import.meta.env.VITE_API_URL}/api/events/${id}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ export default async function searchEvents(query) {
|
|||||||
try {
|
try {
|
||||||
const encodedQuery = encodeURIComponent(query);
|
const encodedQuery = encodeURIComponent(query);
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`http://${import.meta.env.VITE_API_URL}/api/events/search?query=${encodedQuery}`,
|
`${import.meta.env.VITE_API_URL}/api/events/search?query=${encodedQuery}`,
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
export default async function getRoles() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${import.meta.env.VITE_API_URL}/api/roles`, {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`Erreur serveur : ${res.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Erreur :', err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
export default async function getUser() {
|
export default async function getUser() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`http://${import.meta.env.VITE_API_URL}/api/users/me`, {
|
const res = await fetch(`${import.meta.env.VITE_API_URL}/api/users/me`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export default async function getXSRFToken() {
|
export default async function getXSRFToken() {
|
||||||
const response = await fetch(`http://${import.meta.env.VITE_API_URL}/sanctum/csrf-cookie`, {
|
const response = await fetch(`${import.meta.env.VITE_API_URL}/sanctum/csrf-cookie`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export default async function deleteNotificationUser(notificationId) {
|
|||||||
const csrfToken = await getXSRFToken();
|
const csrfToken = await getXSRFToken();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`http://${import.meta.env.VITE_API_URL}/api/users/delete/notification/${notificationId}`, {
|
const res = await fetch(`${import.meta.env.VITE_API_URL}/api/users/delete/notification/${notificationId}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export default async function getUserNotifications() {
|
export default async function getUserNotifications() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`http://${import.meta.env.VITE_API_URL}/api/users/notifications`,
|
`${import.meta.env.VITE_API_URL}/api/users/notifications`,
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export default async function readNotifications() {
|
|||||||
const csrfToken = await getXSRFToken();
|
const csrfToken = await getXSRFToken();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`http://${import.meta.env.VITE_API_URL}/api/users/notifications/read`, {
|
const res = await fetch(`${import.meta.env.VITE_API_URL}/api/users/notifications/read`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export default async function register(email, password, name, lastname, phone) {
|
|||||||
const csrfToken = await getXSRFToken();
|
const csrfToken = await getXSRFToken();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`http://${import.meta.env.VITE_API_URL}/api/users/register`, {
|
const response = await fetch(`${import.meta.env.VITE_API_URL}/api/users/register`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function scrollToTop () {
|
||||||
|
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
|
||||||
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import getXSRFToken from "../getXSRF.js";
|
||||||
|
|
||||||
|
export default async function assign(taskId) {
|
||||||
|
const csrfToken = await getXSRFToken();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`${import.meta.env.VITE_API_URL}/api/events/task/assign`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-XSRF-TOKEN': csrfToken,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
task_id: taskId
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: res.status,
|
||||||
|
message: data?.message ?? null,
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ Erreur réseau :", err);
|
||||||
|
return {
|
||||||
|
status: 0,
|
||||||
|
message: "Network error",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import getXSRFToken from "../getXSRF.js";
|
||||||
|
|
||||||
|
export default async function createTask({
|
||||||
|
eventId,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
location,
|
||||||
|
maxParticipants,
|
||||||
|
}) {
|
||||||
|
const csrfToken = await getXSRFToken();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`${import.meta.env.VITE_API_URL}/api/events/task`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-XSRF-TOKEN": csrfToken,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
event_id: eventId,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
location,
|
||||||
|
max_participants: maxParticipants,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const errorData = await res.json();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.log("❌ Erreur réseau :", err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
export default async function isAssigned(taskId) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`${import.meta.env.VITE_API_URL}/api/events/tasks/${taskId}/assigned`,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`Erreur serveur : ${res.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await res.json();
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Erreur :', err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import getXSRFToken from "../getXSRF.js";
|
||||||
|
|
||||||
|
export default async function unAssign(taskId) {
|
||||||
|
const csrfToken = await getXSRFToken();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`${import.meta.env.VITE_API_URL}/api/events/task/unassign`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-XSRF-TOKEN': csrfToken,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
task_id: taskId
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: res.status,
|
||||||
|
message: data?.message ?? null,
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ Erreur réseau :", err);
|
||||||
|
return {
|
||||||
|
status: 0,
|
||||||
|
message: "Network error",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import getXSRFToken from "../getXSRF.js";
|
||||||
|
|
||||||
|
export default async function deleteUser(userId) {
|
||||||
|
const csrfToken = await getXSRFToken();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${import.meta.env.VITE_API_URL}/api/users/${userId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-XSRF-TOKEN': csrfToken,
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: userId
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return {
|
||||||
|
status: response.status,
|
||||||
|
errors: data.errors || data.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { status: response.status, data };
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
return { status: 500, error };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
export default async function getAllUser() {
|
export default async function getAllUser() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`http://${import.meta.env.VITE_API_URL}/api/users`, {
|
const res = await fetch(`${import.meta.env.VITE_API_URL}/api/users`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export default async function getUserById(id) {
|
export default async function getUserById(id) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`http://${import.meta.env.VITE_API_URL}/api/users/${id}`, {
|
const res = await fetch(`${import.meta.env.VITE_API_URL}/api/users/${id}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
export default async function getUserToValidate() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${import.meta.env.VITE_API_URL}/api/users/invalid`, {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`Erreur serveur : ${res.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Erreur :', err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import getXSRFToken from "../getXSRF.js";
|
||||||
|
|
||||||
|
|
||||||
|
export default async function modifyRole(role, userId) {
|
||||||
|
try {
|
||||||
|
const csrfToken = await getXSRFToken();
|
||||||
|
|
||||||
|
const res = await fetch(
|
||||||
|
`${import.meta.env.VITE_API_URL}/api/users/${userId}/role`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'X-XSRF-TOKEN': csrfToken,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ role }),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
return { success: false, error: data.message ?? "Erreur serveur" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, data };
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return { success: false, error: "Erreur réseau" };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ export default async function searchUsers(query) {
|
|||||||
try {
|
try {
|
||||||
const encodedQuery = encodeURIComponent(query);
|
const encodedQuery = encodeURIComponent(query);
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`http://localhost:80/api/users/search?query=${encodedQuery}`,
|
`${import.meta.env.VITE_API_URL}/api/users/search?query=${encodedQuery}`,
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export default async function updateUser(name, lastname, phone) {
|
|||||||
const csrfToken = await getXSRFToken();
|
const csrfToken = await getXSRFToken();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`http://${import.meta.env.VITE_API_URL}/api/users/update`, {
|
const res = await fetch(`${import.meta.env.VITE_API_URL}/api/users/update`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import getXSRFToken from "../getXSRF.js";
|
||||||
|
|
||||||
|
export default async function validateUser(userId) {
|
||||||
|
const csrfToken = await getXSRFToken();
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`${import.meta.env.VITE_API_URL}/api/users/${userId}/validate`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
'X-XSRF-TOKEN': csrfToken,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!res.ok) {
|
||||||
|
const errorData = await res.json();
|
||||||
|
console.error("Erreur backend:", errorData);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Erreur réseau:", err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user