p2405951
2026-01-14 11:50:34 +01:00
63 changed files with 2356 additions and 337 deletions
+777 -4
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -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

+1 -1
View File
@@ -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>
+57 -9
View File
@@ -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>
</>
}
+58 -16
View File
@@ -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;
text-align: center; white-space: nowrap;
}
.section{
margin-top: 2rem;
display: flex;
flex-direction: column;
gap: 2rem;
} }
@media screen and (max-width: 480px) {
.footer{ @media screen and (max-width: 768px) {
padding: 1rem; .footerContent {
flex-direction: column-reverse;
gap: 1rem;
text-align: center;
} }
.footerContent{ .footerNav {
flex-direction: column; flex-wrap: wrap;
align-items: center; justify-content: center;
gap: 0.5rem; }
.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 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>
</ul> ))
) : (
<li>Aucune tâche associée.</li>
)}
</ul>
</div> </div>
))} ))}
</div> </div>
@@ -2,7 +2,7 @@
width: 100%; width: 100%;
} }
.rightBlock>h2 { .rightBlock > h2 {
font-size: 29px; font-size: 29px;
} }
@@ -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) { .eventCard > h3 {
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 {
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();
+39 -44
View File
@@ -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", <MenuItem value="1">1 mois</MenuItem>
fontSize: "19px", <MenuItem value="3">3 mois</MenuItem>
paddingBottom: "-2px", <MenuItem value="6">6 mois</MenuItem>
"& .MuiOutlinedInput-notchedOutline": { border: "none" }, <MenuItem value="12">12 mois</MenuItem>
"& .MuiSelect-select": { padding: "8px", paddingRight: "24px !important" }, </Select>
"& .MuiSvgIcon-root": { fontSize: "20px" }, </FormControl>
}}
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> </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)}>
+88
View File
@@ -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()}> Sinscrire </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);
}
}
+25
View File
@@ -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>
}
+5 -20
View File
@@ -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>
{/* 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> </div>
) : ( ) : (
<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>
}
+4
View File
@@ -0,0 +1,4 @@
export const APP_CONFIG = {
contactEmail: "contact@example.com",
contactPhone: "+33 01 02 03 04 05"
}
+2
View File
@@ -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>
+40
View File
@@ -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;
}
+2 -1
View File
@@ -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 à laccueil <span onClick={() => navigate("/")} className={styles.redirectSpan}>ici.</span> <br/> - Revenir à laccueil <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>
+7 -6
View File
@@ -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} />
)) ))
+143
View File
@@ -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>Linscription et la gestion des profils bénévoles</li>
<li>Lorganisation 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 lutilisation 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 didentification : 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 lutilisateur</li>
<li>Lintérêt légitime de lorganisme (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 lengagement 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 lorganisme</li>
<li>Aux prestataires techniques (hébergement, maintenance), strictement nécessaires</li>
</ul>
<p>Aucune donnée nest 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>Lhé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 daccès à vos données</li>
<li>Droit de rectification</li>
<li>Droit à leffacement (droit à loubli)</li>
<li>Droit à la limitation du traitement</li>
<li>Droit dopposition</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 lInformatique 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>
);
}
+15
View File
@@ -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>
+156 -43
View File
@@ -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" : "Sinscrire"}
</button>
</div>
</li>
))} ))}
</ul> </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> </div>
); );
} }
export default eventDetail; export default EventDetail;
+18 -40
View File
@@ -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);
} .modalContent {
to { display: flex;
opacity: 1; flex-direction: column;
transform: translateY(0); align-items: center;
} gap: 24px;
}
.manageEvent {
display: flex;
flex-direction: row;
gap: 10px;
} }
+10
View File
@@ -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,
},
] ]
} }
]) ])
+1 -1
View File
@@ -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: {
+1 -1
View File
@@ -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: {
+3
View File
@@ -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}`;
} }
+1 -1
View File
@@ -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: {
+38
View File
@@ -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 };
}
}
+28
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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: {
+1 -1
View File
@@ -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',
+21
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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',
+1 -1
View File
@@ -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: {
+1 -1
View File
@@ -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: {
+3
View File
@@ -0,0 +1,3 @@
export default function scrollToTop () {
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
};
+37
View File
@@ -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",
};
}
}
+49
View File
@@ -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;
}
}
+24
View File
@@ -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;
}
}
+37
View File
@@ -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",
};
}
}
+34
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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: {
+21
View File
@@ -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;
}
}
+33
View File
@@ -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" };
}
}
+1 -1
View File
@@ -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',
+1 -1
View File
@@ -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: {
+27
View File
@@ -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;
}
}