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}>
|
||||
{(windowSize.width <= 768 || eventMenu) ? (
|
||||
|
||||
<Button onClick={() => navigate(`/event/` + event.id)}>Voir plus</Button>
|
||||
<Button onClick={() => navigate(`/events/` + event.id)}>Voir plus</Button>
|
||||
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,64 @@
|
||||
import styles from "./Footer.module.css"
|
||||
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(){
|
||||
return (
|
||||
<footer className={`${styles.footer} ${"glassCard"}`} >
|
||||
export default function Footer(){
|
||||
const currentYear = new Date().getFullYear();
|
||||
const [isCopyrightModal, setIsCopyrightModal] = useState(false);
|
||||
|
||||
|
||||
return <>
|
||||
<footer className={`${styles.footer} glassCard`}>
|
||||
<div className={styles.footerContent}>
|
||||
<Link to="" className={styles.link}>Contact</Link>
|
||||
<Link to="" className={styles.link}>RGPD</Link>
|
||||
<Link to="" className={styles.link}>Mentions légales</Link>
|
||||
<Button
|
||||
variant={"transparent"}
|
||||
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>
|
||||
</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%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: fit-content;
|
||||
padding: 2rem;
|
||||
padding: 1.5rem 2rem;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.footerContent {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.footerNav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.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;
|
||||
width: fit-content;
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.section{
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
.footer{
|
||||
padding: 1rem;
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.footerContent {
|
||||
flex-direction: column-reverse;
|
||||
gap: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footerContent{
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
.footerNav {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.link {
|
||||
margin: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.separator {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,54 @@
|
||||
import React from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import styles from "./IncompleteEvent.module.css";
|
||||
import getAllEvents from "../../utils/events/getAllEvents";
|
||||
|
||||
function IncompleteEvent() {
|
||||
const incompleteEvents = [
|
||||
{ title: "Événement A", 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]"] },
|
||||
{ title: "Événement B", tasks: ["Réservation de la salle", "Vérification du matériel"] },
|
||||
{ title: "Événement C", tasks: ["Communication sur les réseaux", "Enormément de comm", "Toujours + de comm"] },
|
||||
{ title: "Événement D", tasks: ["Communication sur les réseaux", "Enormément de comm", "Toujours + de comm"] },
|
||||
{ title: "Événement E", 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]"] },
|
||||
{ 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]"] },
|
||||
{ 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]"] },
|
||||
];
|
||||
const [events, setEvents] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchEvents = async () => {
|
||||
const data = await getAllEvents();
|
||||
setEvents(data?.data || data || []);
|
||||
setLoading(false);
|
||||
};
|
||||
fetchEvents();
|
||||
}, []);
|
||||
|
||||
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 (
|
||||
<div className={`${styles.rightBlock} glassCard`}>
|
||||
<h2>Événements incomplets</h2>
|
||||
<h2>Derniers événements</h2>
|
||||
<div className={styles.eventsContainer}>
|
||||
{incompleteEvents.map((event, index) => (
|
||||
<div key={index} className={`glassBorder ${styles.eventCard}`}>
|
||||
<h3>{event.title}</h3>
|
||||
{events.map((event) => (
|
||||
<div key={event.id} className={`glassCard ${styles.eventCard}`}>
|
||||
<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}>
|
||||
{event.tasks.map((task, taskIndex) => (
|
||||
<li key={taskIndex}>{task}</li>
|
||||
))}
|
||||
</ul>
|
||||
{event.tasks?.length ? (
|
||||
event.tasks.map((task, taskIndex) => (
|
||||
<li key={taskIndex}>{task.name}</li>
|
||||
))
|
||||
) : (
|
||||
<li>Aucune tâche associée.</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rightBlock>h2 {
|
||||
.rightBlock > h2 {
|
||||
font-size: 29px;
|
||||
}
|
||||
|
||||
@@ -17,20 +17,6 @@
|
||||
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 {
|
||||
min-width: calc((100% - 20px) / 3);
|
||||
max-width: calc((100% - 20px) / 3);
|
||||
@@ -38,63 +24,69 @@ display: none;
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
text-align: center;
|
||||
padding: 10px 10px 200px 10px;
|
||||
box-sizing: border-box;
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
animation: slideInEvent 1s ease forwards;
|
||||
flex-shrink: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.eventCard:nth-child(1) {
|
||||
animation-delay: 0s;
|
||||
.eventCard ul {
|
||||
align-items: center;
|
||||
margin-left: 8%;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.eventCard:nth-child(2) {
|
||||
animation-delay: 0.1s;
|
||||
.eventCard ul li {
|
||||
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;
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Adaptation pour mobile */
|
||||
@media screen and (max-width: 760px) {
|
||||
.eventCard {
|
||||
min-width: 100%;
|
||||
min-height: 200px;
|
||||
max-height: fit-content;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.eventCard h3 {
|
||||
@@ -102,18 +94,21 @@ display: none;
|
||||
}
|
||||
|
||||
.eventsContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
max-height: 300px;
|
||||
margin-top: 15px;
|
||||
height: fit-content;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 0;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.rightBlock>h2 {
|
||||
.rightBlock > h2 {
|
||||
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 styles from "./PendingMembers.module.css";
|
||||
|
||||
function pendingMembers() {
|
||||
const pendingMembers = [
|
||||
{ name: "Antoine Ordonneau" },
|
||||
{ name: "Giovanni Josserand" },
|
||||
{ name: "Quentin T'Jampens" },
|
||||
{ name: "Quentin T'Jampens" },
|
||||
{ name: "Quentin T'Jampens" },
|
||||
{ name: "Quentin T'Jampens" },
|
||||
{ name: "Quentin T'Jampens" }
|
||||
];
|
||||
import getUserToValidate from "../../utils/users/getUserToValidate";
|
||||
import getUserById from "../../utils/users/getUserById";
|
||||
import validateUser from "../../utils/users/validateUser";
|
||||
import deleteUser from "../../utils/users/deleteUser";
|
||||
|
||||
const handleValidate = (name) => {
|
||||
console.log(`Valider ${name}`);
|
||||
function PendingMembers() {
|
||||
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) => {
|
||||
console.log(`Refuser ${name}`);
|
||||
const handleValidate = async (id) => {
|
||||
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 (
|
||||
<div className={`${styles.rightBlock} glassCard`}>
|
||||
<h2>Membres en attente de validation</h2>
|
||||
{error && <p style={{ color: "red" }}>{error}</p>}
|
||||
|
||||
<div className={styles.membersContainer}>
|
||||
{pendingMembers.map((member, index) => (
|
||||
<div key={index} className={`glassBorder ${styles.memberCard}`}>
|
||||
<p>{member.name}</p>
|
||||
{pendingMembers.length === 0 && <p>Aucun membre en attente</p>}
|
||||
|
||||
{pendingMembers.map((member) => (
|
||||
<div key={member.id} className={`glassCard ${styles.memberCard}`}>
|
||||
<p>{member.name} {member.lastname}</p>
|
||||
<div className={styles.buttonGroup}>
|
||||
<Button onClick={() => handleValidate(member.name)} variant="primary">
|
||||
Valider
|
||||
<Button
|
||||
onClick={() => handleValidate(member.id)}
|
||||
variant="primary"
|
||||
disabled={actionLoading === member.id}
|
||||
>
|
||||
{actionLoading === member.id ? "En cours..." : "Valider"}
|
||||
</Button>
|
||||
<Button onClick={() => handleReject(member.name)} variant="danger">
|
||||
Refuser
|
||||
<Button
|
||||
onClick={() => handleReject(member.id)}
|
||||
variant="danger"
|
||||
disabled={actionLoading === member.id}
|
||||
>
|
||||
{actionLoading === member.id ? "En cours..." : "Refuser"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -43,4 +106,4 @@ function pendingMembers() {
|
||||
);
|
||||
}
|
||||
|
||||
export default pendingMembers;
|
||||
export default PendingMembers;
|
||||
|
||||
@@ -18,10 +18,24 @@
|
||||
}
|
||||
|
||||
|
||||
.membersContainer::-webkit-scrollbar {
|
||||
display: none;
|
||||
/*.membersContainer::-webkit-scrollbar {
|
||||
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 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@@ -14,9 +14,9 @@ export default function SettingsModal() {
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const [name, setName] = useState(user.name);
|
||||
const [lastname, setLastName] = useState(user.lastname);
|
||||
const [phone, setPhone] = useState(user.phone);
|
||||
const [name, setName] = useState(user?.name);
|
||||
const [lastname, setLastName] = useState(user?.lastname);
|
||||
const [phone, setPhone] = useState(user?.phone);
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
|
||||
@@ -1,14 +1,32 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Doughnut } from "react-chartjs-2";
|
||||
import { Chart, ArcElement } from "chart.js";
|
||||
import { MenuItem, Select, FormControl } from "@mui/material";
|
||||
import styles from "./ParticipationChart.module.css";
|
||||
import getAllEvents from "../../utils/events/getAllEvents";
|
||||
|
||||
Chart.register(ArcElement);
|
||||
|
||||
function ParticipationChart() {
|
||||
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 = {
|
||||
labels: ["Progress", "Background"],
|
||||
@@ -17,9 +35,7 @@ function ParticipationChart() {
|
||||
data: [participationRate, 100 - participationRate],
|
||||
backgroundColor: ["#1e60f9ff", "#ffffffff"],
|
||||
borderWidth: 0,
|
||||
weight: 10,
|
||||
cutout: "93%",
|
||||
circumference: 360,
|
||||
rotation: -90,
|
||||
},
|
||||
],
|
||||
@@ -31,56 +47,35 @@ function ParticipationChart() {
|
||||
legend: { display: false },
|
||||
tooltip: { enabled: false },
|
||||
},
|
||||
animation: {
|
||||
animateScale: true,
|
||||
animateRotate: true,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${styles.leftBlock} glassCard`}>
|
||||
<div className={styles.participationHeader}>
|
||||
<div className={`${styles.partiText} glassBorder`}>
|
||||
<label htmlFor={styles.months}>Taux de participation depuis :</label>
|
||||
<div className={styles.formControl}>
|
||||
<FormControl className={styles.formDesign}>
|
||||
<Select
|
||||
value={selected}
|
||||
onChange={(e) => setSelected(e.target.value)}
|
||||
sx={{
|
||||
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="3" sx={{ fontSize: "18px" }}>3 mois</MenuItem>
|
||||
<MenuItem value="6" sx={{ fontSize: "18px" }}>6 mois</MenuItem>
|
||||
<MenuItem value="12" sx={{ fontSize: "18px" }}>12 mois</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
<div className={`${styles.partiText} glassCard`}>
|
||||
<label>Taux de participation depuis :</label>
|
||||
|
||||
<FormControl className={styles.formDesign}>
|
||||
<Select
|
||||
value={selected}
|
||||
onChange={(e) => setSelected(e.target.value)}
|
||||
className={styles.selectRoot}
|
||||
>
|
||||
<MenuItem value="1">1 mois</MenuItem>
|
||||
<MenuItem value="3">3 mois</MenuItem>
|
||||
<MenuItem value="6">6 mois</MenuItem>
|
||||
<MenuItem value="12">12 mois</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.chartContainer}>
|
||||
<div className={styles.chartWrapper}>
|
||||
<Doughnut data={chartData} options={chartOptions} />
|
||||
<div className={styles.chartPercentage}>{participationRate}%</div>
|
||||
<div className={styles.chartPercentage}>
|
||||
{eventsCount} events
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,14 +3,18 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: calc(100vh - 260px);
|
||||
margin-bottom: auto;
|
||||
height: 100%;
|
||||
min-height: calc(100vh - 260px);
|
||||
box-sizing: border-box;
|
||||
padding: 20px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.participationHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.partiText {
|
||||
@@ -26,14 +30,15 @@
|
||||
|
||||
.partiText label {
|
||||
font-size: 20px;
|
||||
text-align: right;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chartContainer {
|
||||
max-width: 80%;
|
||||
width: 400px;
|
||||
width: 80%;
|
||||
max-width: 400px;
|
||||
height: auto;
|
||||
aspect-ratio: 1/1;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
animation: slideEvent2 2s;
|
||||
@@ -58,12 +63,70 @@
|
||||
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 {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
@@ -75,45 +138,52 @@
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.formControl {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.formDesign {
|
||||
margin: 0 auto;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 760px) {
|
||||
.partiText {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
.leftBlock {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
min-height: auto;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chartContainer {
|
||||
width: 90%;
|
||||
max-width: 300px;
|
||||
height: auto;
|
||||
aspect-ratio: 1/1;
|
||||
}
|
||||
|
||||
.partiText label {
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.leftBlock {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: fit-content;
|
||||
margin-bottom: auto;
|
||||
margin-top: 30px;
|
||||
}
|
||||
.chartContainer{
|
||||
height:fit-content;
|
||||
.chartPercentage {
|
||||
font-size: 48px;
|
||||
}
|
||||
}
|
||||
/* 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 <>
|
||||
{/* TODO : faire le responsive du menu de création d'événement */}
|
||||
<Button className={`${styles.createButton} glassCard`} variant={"default"} onClick={() => setIsOpen(true)}> + </Button>
|
||||
|
||||
<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 styles from "./eventList.module.css";
|
||||
import { formatDateLetter } from "../../utils/date/formatDateLetter.js";
|
||||
import EventItem from "./eventItem";
|
||||
|
||||
|
||||
function EventList({ events }) {
|
||||
@@ -16,26 +16,11 @@ function EventList({ events }) {
|
||||
<div className={`${styles.glassCard} glassCard`}>
|
||||
<h2>Liste des événements à venir</h2>
|
||||
{sortedEvents.length > 0 ? (
|
||||
<ul className={styles.eventList}>
|
||||
{sortedEvents.map((eventGroup) => (
|
||||
<li key={eventGroup.date} className={`${styles.eventGroup} glassBorder`}>
|
||||
<div className={styles.eventTitle}>
|
||||
<p className={styles.title}>{eventGroup.name}</p>
|
||||
<p>{formatDateLetter(eventGroup.start)}</p>
|
||||
</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>
|
||||
|
||||
<div className={styles.eventList}>
|
||||
{sortedEvents.map((eventGroup, index) => (
|
||||
<EventItem eventGroup={eventGroup} key={index} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : (
|
||||
<p>Aucun événement planifié pour l'instant.</p>
|
||||
)}
|
||||
|
||||
@@ -54,6 +54,10 @@ h2{
|
||||
padding: 10px 20px 10px 20px;
|
||||
}
|
||||
|
||||
.eventGroup:hover{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.eventTitle {
|
||||
display: flex;
|
||||
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(
|
||||
<StrictMode>
|
||||
{/* TODO: refaire l'arborescence du frontend */}
|
||||
{/* TODO: refaire le docker du backend */}
|
||||
<AuthProvider>
|
||||
<RouterProvider router={router} />
|
||||
</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 styles from "./PageNotFound.module.css"
|
||||
import { useNavigate } from "react-router";
|
||||
import { APP_CONFIG } from "../../config/appConfig.js";
|
||||
|
||||
export default function PageNotFound() {
|
||||
|
||||
@@ -17,7 +18,7 @@ export default function PageNotFound() {
|
||||
- Revenir à l’accueil <span onClick={() => navigate("/")} className={styles.redirectSpan}>ici.</span> <br/>
|
||||
|
||||
</p>
|
||||
<p> Contact : contact@example.com </p>
|
||||
<p> Contact : {APP_CONFIG.contactEmail} </p>
|
||||
</div>
|
||||
</div>
|
||||
</Background>
|
||||
|
||||
@@ -8,6 +8,7 @@ import SettingsModal from "../../components/SettingsModal/SettingsModal.jsx";
|
||||
function ProfilePage() {
|
||||
|
||||
const { user } = useContext(AuthContext);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={`${styles.profileboard} glassCard`}>
|
||||
@@ -21,22 +22,22 @@ function ProfilePage() {
|
||||
</div>
|
||||
<div className={styles.description}>
|
||||
<div className={styles.headerProfile}>
|
||||
<h2> {user.name} {user.lastname} </h2>
|
||||
<h2> {user?.name} {user?.lastname} </h2>
|
||||
</div>
|
||||
<div className={styles.topDescription}>
|
||||
<div className={`${styles.infoBlock} glassBorder`}>
|
||||
<p><strong>Role :</strong> {user.role}</p>
|
||||
<p><strong>Membre depuis :</strong> {formatDate(user.created_at)}</p>
|
||||
<p><strong>Role :</strong> {user?.role}</p>
|
||||
<p><strong>Membre depuis :</strong> {user && formatDate(user.created_at)}</p>
|
||||
</div>
|
||||
<div className={`${styles.infoBlock} glassBorder`}>
|
||||
<p><strong>Mail :</strong> {user.email} </p>
|
||||
<p><strong>Téléphone :</strong> {user.phone === null ? "Pas de numéro enregistré" : user.phone}</p>
|
||||
<p><strong>Mail :</strong> {user?.email} </p>
|
||||
<p><strong>Téléphone :</strong> {user?.phone === null ? "Pas de numéro enregistré" : user?.phone}</p>
|
||||
</div>
|
||||
<SettingsModal />
|
||||
</div>
|
||||
|
||||
<h2>Tâches :</h2>
|
||||
{user.tasks && user.tasks.length > 0 ? (
|
||||
{user && user.tasks.length > 0 ? (
|
||||
user.tasks.map((task, index) => (
|
||||
<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 { useParams } from "react-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState, useContext } from "react";
|
||||
import getUserById from "../../utils/users/getUserById.js";
|
||||
import formatDate from "../../utils/date/formatDate.js";
|
||||
import Task from "../../components/Task/Task.jsx";
|
||||
import { useNavigate } from "react-router";
|
||||
import ManageMember from "../../components/manageMember/ManageMember.jsx";
|
||||
import { AuthContext } from "../../contexts/auth/AuthContext.js";
|
||||
|
||||
export default function VolunteerProfilePage() {
|
||||
|
||||
@@ -13,6 +15,8 @@ export default function VolunteerProfilePage() {
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { user: currentUser } = useContext(AuthContext);
|
||||
|
||||
const [user, setUser] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -41,13 +45,14 @@ export default function VolunteerProfilePage() {
|
||||
<div className={styles.description}>
|
||||
<div className={styles.headerProfile}>
|
||||
<h2> {user.name} {user.lastname} </h2>
|
||||
{currentUser?.isAdmin && <ManageMember user={user} />}
|
||||
</div>
|
||||
<div className={styles.topDescription}>
|
||||
<div className={styles.infoBlock}>
|
||||
<div className={`${styles.infoBlock} glassBorder`}>
|
||||
<p><strong>Role :</strong> {user.role}</p>
|
||||
<p><strong>Membre depuis :</strong> {formatDate(user.created_at)}</p>
|
||||
</div>
|
||||
<div className={styles.infoBlock}>
|
||||
<div className={`${styles.infoBlock} glassBorder`}>
|
||||
<p><strong>Mail :</strong> {user.email} </p>
|
||||
<p><strong>Téléphone :</strong> {user.phone === null ? "Pas de numéro enregistré" : user.phone}</p>
|
||||
</div>
|
||||
|
||||
@@ -1,70 +1,183 @@
|
||||
import {useState} from "react";
|
||||
import { useState, useEffect, useContext } from "react";
|
||||
import styles from "./eventDetail.module.css";
|
||||
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 id = params.id;
|
||||
const { user } = useContext(AuthContext)
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [tasks, setTasks] = useState([
|
||||
{ text: "Préparer les slides", registered: false },
|
||||
{ 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 [open, setOpen] = useState(false);
|
||||
const [event, setEvent] = useState(null);
|
||||
|
||||
const event = {
|
||||
title: "Conférence React 2025",
|
||||
date: "27 Décembre 2025",
|
||||
location: "Paris, France",
|
||||
description: "azertyuiop azertyuiop azertyuiop azertyuiop azertyuiop azertyuiop azertyuiop azertyuiop azertyuiop azertyuiop azertyuiop azertyuiop"
|
||||
const [openTaskModal, setOpenTaskModal] = useState(false);
|
||||
const [taskName, setTaskName] = useState("");
|
||||
const [taskDescription, setTaskDescription] = useState("");
|
||||
const [taskLocation, setTaskLocation] = useState("");
|
||||
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 (
|
||||
<div className={styles.allContent}>
|
||||
<div className={`glassCard`}>
|
||||
<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}>
|
||||
<h2>{event.title}</h2>
|
||||
<p>Date: {event.date}</p>
|
||||
<p>Lieu: {event.location}</p>
|
||||
<p>Description: {event.description}</p>
|
||||
<h2>{event?.name}</h2>
|
||||
<div className={styles.eventContent}>
|
||||
<div>
|
||||
<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>
|
||||
|
||||
<h2>Tâches</h2>
|
||||
<ul className={styles.tasks}>
|
||||
{tasks.map((task, index) => (
|
||||
<li style={{"--i": index}} 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>
|
||||
<div className={styles.tasks}>
|
||||
{event?.tasks?.map((task, index) => (
|
||||
<EventTask task={task} key={index} />
|
||||
))}
|
||||
</ul>
|
||||
</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;
|
||||
}
|
||||
|
||||
.registerButton {
|
||||
margin-left: auto;
|
||||
padding: 5px 10px;
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
background: #019AFF;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.registerButton:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.tasksButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.tasks {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tasks li {
|
||||
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);
|
||||
.eventContent {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
.manageBtn {
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.modalContent {
|
||||
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 PageNotFound from "./pages/PageNotFound/PageNotFound.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([
|
||||
{
|
||||
@@ -65,6 +67,14 @@ const router = createBrowserRouter([
|
||||
path: "/events/:id",
|
||||
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();
|
||||
|
||||
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',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
|
||||
@@ -5,7 +5,7 @@ export default async function logout() {
|
||||
const csrfToken = await getXSRFToken();
|
||||
|
||||
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',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
|
||||
|
||||
export function formatDateLetter(d) {
|
||||
if (!d) return "Date inconnue";
|
||||
|
||||
@@ -26,4 +28,5 @@ export function formatDateLetterJS(d) {
|
||||
];
|
||||
const [year, month, day] = d.split('-');
|
||||
return `${day} ${monthNames[parseInt(month) - 1]} ${year}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ export default async function createEvent(name, description, start, end) {
|
||||
const csrfToken = await getXSRFToken();
|
||||
|
||||
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',
|
||||
credentials: 'include',
|
||||
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() {
|
||||
|
||||
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',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export default async function GetEventById(id) {
|
||||
|
||||
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',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
|
||||
@@ -2,7 +2,7 @@ export default async function searchEvents(query) {
|
||||
try {
|
||||
const encodedQuery = encodeURIComponent(query);
|
||||
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',
|
||||
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() {
|
||||
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',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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',
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ export default async function deleteNotificationUser(notificationId) {
|
||||
const csrfToken = await getXSRFToken();
|
||||
|
||||
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',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export default async function getUserNotifications() {
|
||||
try {
|
||||
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',
|
||||
credentials: 'include',
|
||||
|
||||
@@ -5,7 +5,7 @@ export default async function readNotifications() {
|
||||
const csrfToken = await getXSRFToken();
|
||||
|
||||
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',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
|
||||
@@ -4,7 +4,7 @@ export default async function register(email, password, name, lastname, phone) {
|
||||
const csrfToken = await getXSRFToken();
|
||||
|
||||
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',
|
||||
credentials: 'include',
|
||||
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() {
|
||||
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',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export default async function getUserById(id) {
|
||||
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',
|
||||
credentials: 'include',
|
||||
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 {
|
||||
const encodedQuery = encodeURIComponent(query);
|
||||
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',
|
||||
credentials: 'include',
|
||||
|
||||
@@ -5,7 +5,7 @@ export default async function updateUser(name, lastname, phone) {
|
||||
const csrfToken = await getXSRFToken();
|
||||
|
||||
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',
|
||||
credentials: 'include',
|
||||
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