refactor: move frontend in apps/frontend
This commit is contained in:
1 parent
dc39113335
commit
31304c931d
225 files changed
+67
-65
No files matched your search
@@ -0,0 +1,123 @@
|
||||
.backgroundContainer {
|
||||
z-index: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.backgroundLight {
|
||||
background: #00F0FF;
|
||||
}
|
||||
|
||||
.backgroundDark {
|
||||
background: #001C83;
|
||||
}
|
||||
|
||||
.circle {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
opacity: 0.8;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.circle-1 {
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
background-color: #019AFF;
|
||||
top: -10%;
|
||||
left: -10%;
|
||||
}
|
||||
|
||||
.circle-2 {
|
||||
width: 250px;
|
||||
height: 250px;
|
||||
background-color: #91FCFF;
|
||||
top: 51%;
|
||||
left: 10%;
|
||||
}
|
||||
|
||||
.circle-3 {
|
||||
width: 280px;
|
||||
height: 280px;
|
||||
background-color: #91FCFF;
|
||||
top: 28%;
|
||||
right: 9%;
|
||||
}
|
||||
|
||||
.circle-4 {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
background-color: #019AFF;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.circle-5 {
|
||||
width: 240px;
|
||||
height: 240px;
|
||||
background-color: #015AFF;
|
||||
top: 2%;
|
||||
right: 2%;
|
||||
}
|
||||
|
||||
.circle-1,.circle-2, .circle-3, .circle-4, .circle-5 {
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
.circle {
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
50% {
|
||||
transform: translate(var(--random-x), var(--random-y));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width:600px) {
|
||||
|
||||
|
||||
.circle-1 {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
top: -5%;
|
||||
left: -5%;
|
||||
}
|
||||
|
||||
.circle-2 {
|
||||
width: 175px;
|
||||
height: 175px;
|
||||
top: 58%;
|
||||
left: 10%;
|
||||
}
|
||||
|
||||
.circle-3 {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
top: 29%;
|
||||
right: 9%;
|
||||
}
|
||||
|
||||
.circle-4 {
|
||||
width: 250px;
|
||||
height: 250px;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.circle-5 {
|
||||
width: 240px;
|
||||
height: 240px;
|
||||
top: 2%;
|
||||
right: 10%;
|
||||
}
|
||||
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
import styles from "./Background.module.css";
|
||||
import { useEffect, useRef, useState, ReactNode } from "react";
|
||||
|
||||
interface BackgroundProps {
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Background = ({ children, className }: BackgroundProps) => {
|
||||
|
||||
const circlesRef = useRef<(HTMLDivElement | null)[]>([]);
|
||||
const [theme, setTheme] = useState("light");
|
||||
|
||||
const getRandomTranslation = (): { x: number; y: number } => {
|
||||
const randomX = Math.floor(Math.random() * 100) - 30;
|
||||
const randomY = Math.floor(Math.random() * 100) - 30;
|
||||
return { x: randomX, y: randomY };
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
circlesRef.current.forEach((circle) => {
|
||||
if (circle) {
|
||||
const { x, y } = getRandomTranslation();
|
||||
circle.style.setProperty('--random-x', `${x}px`);
|
||||
circle.style.setProperty('--random-y', `${y}px`);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const updateTheme = () => {
|
||||
const t = localStorage.getItem("theme") || "light";
|
||||
setTheme(t);
|
||||
document.body.classList.toggle("dark", t === "dark");
|
||||
};
|
||||
|
||||
updateTheme();
|
||||
|
||||
window.addEventListener("theme-changed", updateTheme);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("theme-changed", updateTheme);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
return (
|
||||
<div className={`${styles.backgroundContainer} ${className || ''} ${theme === "light" ? styles.backgroundLight : styles.backgroundDark}`}>
|
||||
<div
|
||||
ref={(el) => { circlesRef.current[0] = el; }}
|
||||
className={`${styles["circle"]} ${styles["circle-1"]}`}
|
||||
style={{ "--random-x": "0px", "--random-y": "0px" } as React.CSSProperties}
|
||||
></div>
|
||||
<div
|
||||
ref={(el) => { circlesRef.current[1] = el; }}
|
||||
className={`${styles["circle"]} ${styles["circle-2"]}`}
|
||||
style={{ "--random-x": "0px", "--random-y": "0px" } as React.CSSProperties}
|
||||
></div>
|
||||
<div
|
||||
ref={(el) => { circlesRef.current[2] = el; }}
|
||||
className={`${styles["circle"]} ${styles["circle-3"]}`}
|
||||
style={{ "--random-x": "0px", "--random-y": "0px" } as React.CSSProperties}
|
||||
></div>
|
||||
<div
|
||||
ref={(el) => { circlesRef.current[3] = el; }}
|
||||
className={`${styles["circle"]} ${styles["circle-4"]}`}
|
||||
style={{ "--random-x": "0px", "--random-y": "0px" } as React.CSSProperties}
|
||||
></div>
|
||||
<div
|
||||
ref={(el) => { circlesRef.current[4] = el; }}
|
||||
className={`${styles["circle"]} ${styles["circle-5"]}`}
|
||||
style={{ "--random-x": "0px", "--random-y": "0px" } as React.CSSProperties}
|
||||
></div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Background;
|
||||
@@ -0,0 +1,21 @@
|
||||
import Button from "../ui/Button/Button";
|
||||
import { useContext } from "react";
|
||||
import { AuthContext, AuthContextType } from "../../contexts/Auth/AuthContext";
|
||||
import { APP_CONFIG } from "../../config/appConfig";
|
||||
|
||||
export default function ExportCalendarBtn() {
|
||||
|
||||
const { user } = useContext(AuthContext) as AuthContextType;
|
||||
const calendarUrl = `${import.meta.env.VITE_API_URL}/api/export/ics/${user?.id}`;
|
||||
|
||||
return <Button
|
||||
variant="default"
|
||||
onClick={() => {
|
||||
window.open(
|
||||
`${APP_CONFIG.newCalendarURL}${calendarUrl}`
|
||||
);
|
||||
}}
|
||||
>
|
||||
Exporter le calendrier
|
||||
</Button>
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
.filterContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
overflow: hidden;
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
transition: max-height 0.5s ease, opacity 0.8s ease, transform 0.5s ease;
|
||||
}
|
||||
|
||||
.filterOpen {
|
||||
max-height: 200px;
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.filterClose{
|
||||
max-height: 200px;
|
||||
opacity: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.filterTag {
|
||||
border: none;
|
||||
background: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
}
|
||||
|
||||
|
||||
.filterTag:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.filterTag:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
.active {
|
||||
border: 2px solid #019AFF;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px){
|
||||
.filterContainer {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filterOpen{
|
||||
display: flex;
|
||||
}
|
||||
.filterClose{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.filterMenu {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
abbr {
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
import styles from "./Filter.module.css";
|
||||
import Button from "../ui/Button/Button";
|
||||
import React from "react";
|
||||
|
||||
type FiltersType = {
|
||||
alphabetical: "asc" | "desc" | null;
|
||||
yearOrder: "asc" | "desc" | null;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
type FilterProps = {
|
||||
isFilterVisible: boolean;
|
||||
filters: FiltersType;
|
||||
setFilters: React.Dispatch<React.SetStateAction<FiltersType>>;
|
||||
};
|
||||
|
||||
function Filter({ isFilterVisible, filters, setFilters }: FilterProps): any {
|
||||
|
||||
const setAlphabeticalOrder = (order: "asc" | "desc"): void => {
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
alphabetical: prev.alphabetical === order ? null : order
|
||||
}));
|
||||
};
|
||||
|
||||
const setYearOrder = (order: "asc" | "desc"): void => {
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
yearOrder: prev.yearOrder === order ? null : order
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${styles.filterContainer} ${isFilterVisible ? styles.filterOpen : styles.filterClose}`}>
|
||||
<abbr title="Tri alphabétique croissant (A à Z)">
|
||||
<Button
|
||||
className={`${styles.filterTag} glassCard ${filters.alphabetical !== "asc" ? "" : styles.active}`}
|
||||
variant={"default"}
|
||||
onClick={() => setAlphabeticalOrder("asc")}
|
||||
>
|
||||
<img src={"sortByAlpha.svg"} alt="Tri alphabétique croissant (A à Z)" />
|
||||
</Button>
|
||||
</abbr>
|
||||
|
||||
<abbr title="Tri alphabétique décroissant (Z à A)">
|
||||
<Button
|
||||
className={`${styles.filterTag} glassCard ${filters.alphabetical !== "desc" ? "" : styles.active}`}
|
||||
variant={"default"}
|
||||
onClick={() => setAlphabeticalOrder("desc")}
|
||||
>
|
||||
<img src={"sortByAntiAlpha.svg"} alt="Tri alphabétique décroissant (Z à A)" />
|
||||
</Button>
|
||||
</abbr>
|
||||
|
||||
<abbr title="Tri chronologique décroissant (du plus récent au plus ancien)">
|
||||
<Button
|
||||
className={`${styles.filterTag} glassCard ${filters.yearOrder !== "desc" ? "" : styles.active}`}
|
||||
variant={"default"}
|
||||
onClick={() => setYearOrder("desc")}
|
||||
>
|
||||
<img src={"timerArrowUp.svg"} alt="Tri chronologique décroissant (du plus récent au plus ancien)" />
|
||||
</Button>
|
||||
</abbr>
|
||||
|
||||
<abbr title="Tri chronologique croissant (du plus ancien au plus récent)">
|
||||
<Button
|
||||
className={`${styles.filterTag} glassCard ${filters.yearOrder !== "asc" ? "" : styles.active}`}
|
||||
variant={"default"}
|
||||
onClick={() => setYearOrder("asc")}
|
||||
>
|
||||
<img src={"timerArrowDown.svg"} alt="Tri chronologique croissant (du plus ancien au plus récent)" />
|
||||
</Button>
|
||||
</abbr>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Filter;
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
.footer {
|
||||
width: 100%;
|
||||
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%;
|
||||
}
|
||||
|
||||
.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;
|
||||
font-size: 0.9rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.section{
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.footerContent {
|
||||
flex-direction: column-reverse;
|
||||
gap: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footerNav {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.link {
|
||||
margin: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.separator {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import styles from "./Footer.module.css"
|
||||
import { Link } from "react-router";
|
||||
import { ReactNode, useState } from "react";
|
||||
import Modal from "../ui/Modal/Modal";
|
||||
import Button from "../ui/Button/Button";
|
||||
import scrollToTop from "../../utils/scrollToTop.js";
|
||||
|
||||
export default function Footer(): ReactNode{
|
||||
const currentYear = new Date().getFullYear();
|
||||
const [isCopyrightModal, setIsCopyrightModal] = useState<boolean>(false);
|
||||
|
||||
|
||||
return <>
|
||||
<footer className={`${styles.footer} glassCard`}>
|
||||
<div className={styles.footerContent}>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</>
|
||||
}
|
||||
+248
@@ -0,0 +1,248 @@
|
||||
.headerContainer {
|
||||
position: relative;
|
||||
height: fit-content;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
|
||||
.header{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.8rem;
|
||||
height: fit-content;
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.profileImg{
|
||||
width:40px;
|
||||
height:40px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
|
||||
.activeHeaderContent {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.inactiveHeaderContent{
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.inactiveMobileMenu{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.activeMobileMenu{
|
||||
height: fit-content;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.headerLeftContent {
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.headerRightContent {
|
||||
width: fit-content;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 4rem;
|
||||
width: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logoLink {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
||||
.link {
|
||||
color: #000000;
|
||||
width: fit-content;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
|
||||
.link:hover{
|
||||
border-bottom: 2px solid #019AFF;
|
||||
|
||||
}
|
||||
|
||||
.activeLink {
|
||||
width: fit-content;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
color: #019AFF;
|
||||
border-bottom: 2px solid #019AFF;
|
||||
}
|
||||
|
||||
.bellBtn, .profileLink {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: transform 0.5s ease;
|
||||
background: none;
|
||||
padding: 7px;
|
||||
border-radius: 50%;
|
||||
width: 55px;
|
||||
height: 55px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.notificationsImg {
|
||||
stroke: black;
|
||||
stroke-width: 3.5;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.bellBtn:focus {
|
||||
outline: none;
|
||||
}
|
||||
.bellBtn:focus-visible {
|
||||
outline: 2px solid black;
|
||||
}
|
||||
|
||||
.activeBellBtn {
|
||||
transform: translateY(-4px);
|
||||
background: rgba(191, 191, 191, 0.5);
|
||||
}
|
||||
|
||||
|
||||
.bellBtn:hover {
|
||||
transform: translateY(-4px);
|
||||
background: rgba(191, 191, 191, 0.5);
|
||||
}
|
||||
|
||||
|
||||
.profileLink:hover {
|
||||
transform: translateY(-4px);
|
||||
background: rgba(191, 191, 191, 0.5);
|
||||
}
|
||||
|
||||
|
||||
.profileImg {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.notificationDiv {
|
||||
position: absolute;
|
||||
width: auto;
|
||||
top: 105%;
|
||||
right: 2rem;
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
z-index: 0;
|
||||
max-height: 60vh;
|
||||
overflow: scroll;
|
||||
overflow-x: hidden;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.activeNotificationDiv {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.notification{
|
||||
padding: 0.5em 1em;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.notificationBadge {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 9px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background-color: red;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
|
||||
.closeBtn:focus {
|
||||
outline: none;
|
||||
}
|
||||
.closeBtn:focus-visible {
|
||||
outline: 2px solid black;
|
||||
}
|
||||
|
||||
.burgerBtn {
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
width: 30px;
|
||||
height: 22px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
z-index: 10;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.burgerBar {
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background-color: #000000;
|
||||
border-radius: 2px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.activeBurgerBtn .burgerBar:nth-child(1) {
|
||||
transform: translateY(10px) rotate(45deg);
|
||||
}
|
||||
.activeBurgerBtn .burgerBar:nth-child(2) {
|
||||
opacity: 0;
|
||||
}
|
||||
.activeBurgerBtn .burgerBar:nth-child(3) {
|
||||
transform: translateY(-9px) rotate(-45deg);
|
||||
}
|
||||
|
||||
|
||||
.navLinks{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.navLinks {
|
||||
display: none;
|
||||
}
|
||||
.burgerBtn {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
import { Link, NavLink } from "react-router";
|
||||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
import styles from "./Header.module.css"
|
||||
import getUserNotifications from "../../utils/notifications/getUserNotifications.js";
|
||||
import deleteNotificationUser from "../../utils/notifications/deleteNotificationUser.js";
|
||||
import initEcho from "../../utils/echo/initEcho.js"
|
||||
import readNotifications from "../../utils/notifications/readNotifications.js";
|
||||
import { userNotificationsListener } from "../../utils/echo/listeners/userNotificationsListener.js";
|
||||
import NotificationCard from "../NotificationCard/NotificationCard";
|
||||
import { EventContext } from "../../contexts/Events/EventContext.js";
|
||||
import { AuthContext } from "../../contexts/Auth/AuthContext.js";
|
||||
import { PendingMembersContext } from "../../contexts/PendingMembers/PendingMembersContext";
|
||||
import Notification from "../../interfaces/notification.interface";
|
||||
|
||||
|
||||
function Header() {
|
||||
|
||||
const { update, user } = useContext(AuthContext)!;
|
||||
const { updateEvent } = useContext(EventContext)!;
|
||||
const { updatePendingMembers } = useContext(PendingMembersContext) as unknown as { updatePendingMembers: () => void };
|
||||
const [notificationMenu, setnotificationMenu] = useState<boolean>(false);
|
||||
const [unreadNotification, setunreadNotification] = useState<boolean>(false);
|
||||
const [mobileMenu, setMobileMenu] = useState<boolean>(false);
|
||||
const [notifications, setNotifications] = useState<Notification[]>([]);
|
||||
const notificationRef = useRef<HTMLDivElement>(null);
|
||||
const profileImageUrl = user?.profile_photo_path
|
||||
? `${import.meta.env.VITE_API_URL}/storage/${user.profile_photo_path}`
|
||||
: "/person.svg";
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
|
||||
updateEvent();
|
||||
|
||||
let echoInstance: ReturnType<typeof initEcho> | null = null;
|
||||
|
||||
async function initializeHeader() {
|
||||
try {
|
||||
const echo = initEcho();
|
||||
echoInstance = echo;
|
||||
userNotificationsListener(echo, user, setNotifications, setunreadNotification, updatePendingMembers);
|
||||
|
||||
const notifData = await getUserNotifications();
|
||||
|
||||
const data: Notification[] = notifData || [];
|
||||
setNotifications(data);
|
||||
const hasUnread = data.some(n => n.read_at === null);
|
||||
setunreadNotification(hasUnread);
|
||||
} catch (err) {
|
||||
console.error("Erreur initialisation Header:", err);
|
||||
}
|
||||
}
|
||||
|
||||
initializeHeader();
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
notificationRef.current &&
|
||||
!notificationRef.current.contains(event.target as Node) &&
|
||||
!(event.target as Element).closest(`.${styles.bellBtn}`)
|
||||
) {
|
||||
setnotificationMenu(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
if (echoInstance) {
|
||||
echoInstance.disconnect();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
const toggleNotificationMenu = async () => {
|
||||
try {
|
||||
setnotificationMenu(!notificationMenu);
|
||||
if (unreadNotification) {
|
||||
const result = await readNotifications();
|
||||
if (result) {
|
||||
setunreadNotification(false);
|
||||
update();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Erreur pour ouvrir/fermer le menu de notifications :', err);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function deleteNotification(notificationId: string) {
|
||||
try {
|
||||
const result = await deleteNotificationUser(notificationId);
|
||||
if (result) {
|
||||
setNotifications(prev => prev.filter(n => n.id !== notificationId));
|
||||
update();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Erreur suppression notification :', err);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className={styles.headerContainer}>
|
||||
<header className={`${styles.header} glassCard`}>
|
||||
<nav className={`${mobileMenu ? styles.inactiveHeaderContent : styles.activeHeaderContent}`}>
|
||||
<div className={styles.headerLeftContent}>
|
||||
<NavLink className={styles.logoLink} to="/">
|
||||
<img src="/logo.png" alt="logo" className={styles.logo} />
|
||||
</NavLink>
|
||||
<div className={styles.navLinks}>
|
||||
<NavLink to="/" className={({ isActive }) => isActive ? styles.activeLink : styles.link}>
|
||||
Accueil
|
||||
</NavLink>
|
||||
<NavLink to="/events" className={({ isActive }) => isActive ? styles.activeLink : styles.link}>
|
||||
Evènements
|
||||
</NavLink>
|
||||
<NavLink to="/volunteers" className={({ isActive }) => isActive ? styles.activeLink : styles.link}>
|
||||
Bénévoles
|
||||
</NavLink>
|
||||
{user?.isAdmin && (
|
||||
<NavLink to="/admin" className={({ isActive }) => isActive ? styles.activeLink : styles.link}>
|
||||
Administration
|
||||
</NavLink>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.headerRightContent}>
|
||||
<button
|
||||
className={`${styles.bellBtn} ${notificationMenu ? styles.activeBellBtn : ""}`}
|
||||
onClick={() => { toggleNotificationMenu(); }}
|
||||
>
|
||||
<img className={styles.notificationsImg} src="/bell.svg" alt="notifications" />
|
||||
{unreadNotification && <span className={styles.notificationBadge}></span>}
|
||||
</button>
|
||||
|
||||
<Link to="/profile" className={styles.profileLink}>
|
||||
<img
|
||||
className={styles.profileImg}
|
||||
src={profileImageUrl}
|
||||
alt="profile"
|
||||
/>
|
||||
</Link>
|
||||
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<nav className={`${mobileMenu ? styles.activeMobileMenu : styles.inactiveMobileMenu}`}>
|
||||
<NavLink to="/" className={({ isActive }) => isActive ? styles.activeLink : styles.link}>
|
||||
Accueil
|
||||
</NavLink>
|
||||
<NavLink to="/events" className={({ isActive }) => isActive ? styles.activeLink : styles.link}>
|
||||
Evènements
|
||||
</NavLink>
|
||||
<NavLink to="/volunteers" className={({ isActive }) => isActive ? styles.activeLink : styles.link}>
|
||||
Bénévoles
|
||||
</NavLink>
|
||||
{user?.isAdmin && (
|
||||
<NavLink to="/admin" className={({ isActive }) => isActive ? styles.activeLink : styles.link}>
|
||||
Administration
|
||||
</NavLink>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
|
||||
<button
|
||||
className={`${styles.burgerBtn} ${mobileMenu ? styles.activeBurgerBtn : ""}`}
|
||||
onClick={() => setMobileMenu(!mobileMenu)}
|
||||
>
|
||||
<div className={styles.burgerBar}></div>
|
||||
<div className={styles.burgerBar}></div>
|
||||
<div className={styles.burgerBar}></div>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
|
||||
|
||||
|
||||
<div
|
||||
ref={notificationRef}
|
||||
className={`${styles.notificationDiv} glassCard ${notificationMenu ? styles.activeNotificationDiv : ""}`}
|
||||
>
|
||||
{notifications.length === 0 ? (
|
||||
<p className={styles.notification}>Vous n'avez aucune notification !</p>
|
||||
) : (
|
||||
<NotificationCard notifications={notifications} deleteNotification={deleteNotification} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Header;
|
||||
@@ -0,0 +1,45 @@
|
||||
.notificationCard {
|
||||
padding: 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||
transition: background 0.2s ease;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.cardHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.timeTag {
|
||||
color: #019AFF;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.5;
|
||||
color: #2c3e50;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.closeIconButton {
|
||||
background: none;
|
||||
border: none;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
font-size: 1.1rem;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.closeIconButton:hover {
|
||||
color: #cc0000;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import formatDate from "../../utils/date/formatDate.js";
|
||||
import formatTime from "../../utils/date/formatTime.js";
|
||||
import styles from "./NotificationCard.module.css"
|
||||
import Notification from "../../interfaces/notification.interface";
|
||||
|
||||
interface NotificationCardProps {
|
||||
notifications: Notification[];
|
||||
deleteNotification: (id: string) => void;
|
||||
}
|
||||
|
||||
function NotificationCard({ notifications, deleteNotification }: NotificationCardProps) {
|
||||
return (
|
||||
notifications.map((notification, index) => (
|
||||
<div className={`${styles.notificationCard} glassBorder`} key={index}>
|
||||
<div className={styles.cardHeader}>
|
||||
<span className={styles.timeTag}>
|
||||
Le {formatDate(notification.created_at)} à {formatTime(notification.created_at)}
|
||||
</span>
|
||||
<button
|
||||
className={styles.closeIconButton}
|
||||
onClick={() => deleteNotification(notification.id)}
|
||||
title="Supprimer"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className={styles.message}>
|
||||
{notification.data.message}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
export default NotificationCard;
|
||||
@@ -0,0 +1,87 @@
|
||||
.searchBarContainer{
|
||||
position: fixed;
|
||||
top: 20vh;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 100;
|
||||
width: 60%;
|
||||
background: #fff;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.searchBarForm{
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.elementList {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.eventImageDiv {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 1rem;
|
||||
width: auto;
|
||||
height: 2rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.searchedEvent{
|
||||
display: flex;
|
||||
padding: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.searchedEvent:hover {
|
||||
background: #afafaf;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.searchedEvent p {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
}
|
||||
|
||||
|
||||
.lineContainer{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.line{
|
||||
height: 0;
|
||||
width: 96%;
|
||||
border: #606060 solid 1px;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.eventImage {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.searchBarForm input{
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
.searchBarForm input:focus{
|
||||
outline: none;
|
||||
}
|
||||
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
.searchBarContainer{
|
||||
width: 80%;
|
||||
}
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
import styles from "./SearchBar.module.css";
|
||||
import { Link } from "react-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import searchEvents from "../../utils/events/searchEvents";
|
||||
import searchUsers from "../../utils/users/searchUsers";
|
||||
import { useLocation } from "react-router";
|
||||
|
||||
function SearchBar(): any {
|
||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||
const [searchResults, setSearchResults] = useState<any[]>([]);
|
||||
const location: any = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
const performSearch = async (query: string): Promise<void> => {
|
||||
if (query.trim() === "") {
|
||||
setSearchResults([]);
|
||||
return;
|
||||
}
|
||||
|
||||
let data: any;
|
||||
|
||||
if (location.pathname.includes("events")) {
|
||||
data = await searchEvents(query);
|
||||
} else if (location.pathname.includes("volunteers")) {
|
||||
data = await searchUsers(query);
|
||||
}
|
||||
|
||||
if (data) {
|
||||
setSearchResults(data);
|
||||
} else {
|
||||
setSearchResults([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handler: ReturnType<typeof setTimeout> = setTimeout(() => {
|
||||
performSearch(searchQuery);
|
||||
}, 300);
|
||||
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
}, [searchQuery, location.pathname]);
|
||||
|
||||
return (
|
||||
<div className={`${styles.searchBarContainer} glassCard`}>
|
||||
<form
|
||||
className={styles.searchBarForm}
|
||||
onSubmit={(element: React.FormEvent<HTMLFormElement>) =>
|
||||
element.preventDefault()
|
||||
}
|
||||
>
|
||||
<img src="search.svg" alt="Rechercher" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Rechercher un événement"
|
||||
value={searchQuery}
|
||||
onChange={(element: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setSearchQuery(element.target.value)
|
||||
}
|
||||
/>
|
||||
</form>
|
||||
|
||||
<div className={styles.lineContainer}>
|
||||
{searchResults.length > 0 ? (
|
||||
<div className={styles.line}></div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className={styles.elementList}>
|
||||
{searchResults.map((result: any, i: number) =>
|
||||
location.pathname.includes("events") ? (
|
||||
<Link to={`/events/` + result.id} key={i}>
|
||||
<div className={styles.searchedEvent}>
|
||||
<div className={styles.eventImageDiv}>
|
||||
{result.image == null ? (
|
||||
<img
|
||||
src="empty.png"
|
||||
className={styles.eventImage}
|
||||
alt="Pas de photo de l'événement"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={result.image + `.png`}
|
||||
alt="Photo de l'événement"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p>{result.name}</p>
|
||||
</div>
|
||||
</Link>
|
||||
) : location.pathname.includes("volunteers") ? (
|
||||
<Link to={`/profile/` + result.id} key={i}>
|
||||
<div className={styles.searchedEvent}>
|
||||
<div className={styles.eventImageDiv}>
|
||||
{result.image == null ? (
|
||||
<img
|
||||
src="empty.png"
|
||||
className={styles.eventImage}
|
||||
alt="Pas de photo de l'événement"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={result.image + `.png`}
|
||||
alt="Photo de l'événement"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p>
|
||||
{result.name} {result.lastname}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
) : null
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SearchBar;
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
.task {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 0.8rem;
|
||||
color: #666;
|
||||
margin: 0 0 0 0;
|
||||
}
|
||||
|
||||
.date {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
Executable
+71
@@ -0,0 +1,71 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import getEventById from "../../utils/events/getEventById";
|
||||
import styles from "./Task.module.css";
|
||||
import formatDate from "../../utils/date/formatDate";
|
||||
|
||||
interface TaskType {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
location: string;
|
||||
start: string;
|
||||
end: string;
|
||||
max_participants: number;
|
||||
events_id: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
pivot: {
|
||||
user_id: number;
|
||||
task_id: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface EventType {
|
||||
id: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
location?: string;
|
||||
start?: string;
|
||||
end?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
interface TaskProps {
|
||||
task: TaskType;
|
||||
}
|
||||
|
||||
const Task: React.FC<TaskProps> = ({ task }) => {
|
||||
const [event, setEvent] = useState<EventType | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchEvent = async (): Promise<void> => {
|
||||
try {
|
||||
const e = await getEventById(task.events_id);
|
||||
setEvent(e.data as EventType);
|
||||
} catch (error) {
|
||||
console.error("Erreur lors du chargement de l'événement", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchEvent();
|
||||
}, [task.events_id]);
|
||||
|
||||
return (
|
||||
<div className={`${styles.task} glassBorder`}>
|
||||
<div className={styles.content}>
|
||||
<div>
|
||||
<h2>
|
||||
{event?.name} - {task.name}
|
||||
</h2>
|
||||
<p className={styles.desc}>{task.description}</p>
|
||||
<p className={styles.date}>
|
||||
{formatDate(task.start)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Task;
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
.eventsButtons {
|
||||
overflow: visible;
|
||||
position: fixed;
|
||||
top: 10rem;
|
||||
right: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
gap: 1rem;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
|
||||
.filterContainer{
|
||||
padding: 0;
|
||||
border-radius: 30px;
|
||||
overflow: visible;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filterMenu{
|
||||
padding: 5px 5px 5px 5px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.createButton, .searchButton, .sortButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
border-radius: 50% !important;
|
||||
width: 55px !important;
|
||||
height: 55px !important;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.createButton:hover, .searchButton:hover, .mobileFilter:hover {
|
||||
transform: translateY(-5px);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
.searchBarOverlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 100;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
.searchBarContainer{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
|
||||
.sortButton:focus, .searchButton:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.createButton,
|
||||
.searchButton,
|
||||
.sortButton {
|
||||
flex: 0 0 55px;
|
||||
width: 55px !important;
|
||||
height: 55px !important;
|
||||
}
|
||||
|
||||
|
||||
.eventsButtons {
|
||||
flex-direction: row;
|
||||
position: relative;
|
||||
top: 0;
|
||||
right: 0;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
margin: 6px 0 16px 0;
|
||||
}
|
||||
|
||||
.filterContainer{
|
||||
width: fit-content;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.filterMenu{
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.createButton:hover, .searchButton:hover, .mobileFilter:hover {
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import styles from "./ToolBar.module.css"
|
||||
import CreateEventBtn from "./tools/CreateEventBtn/CreateEventBtn";
|
||||
import SearchBtn from "./tools/SearchBtn";
|
||||
import FilterBtn from "./tools/FilterBtn";
|
||||
|
||||
interface Filters {
|
||||
alphabetical: "asc" | "desc";
|
||||
yearOrder: "asc" | "desc";
|
||||
}
|
||||
|
||||
interface ToolBarProps {
|
||||
setFilters: React.Dispatch<React.SetStateAction<Filters>>;
|
||||
filters: Filters;
|
||||
showCreate?: boolean;
|
||||
}
|
||||
|
||||
function ToolBar({ setFilters, filters, showCreate = true }: ToolBarProps) {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.eventsButtons}>
|
||||
{showCreate && <CreateEventBtn />}
|
||||
<SearchBtn />
|
||||
<FilterBtn setFilters={setFilters} filters={filters} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ToolBar;
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
.createButton {
|
||||
font-size: 1.5rem;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.createButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
border-radius: 50% !important;
|
||||
width: 55px !important;
|
||||
height: 55px !important;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.createButton:hover {
|
||||
transform: translateY(-5px);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.inputContainer {
|
||||
margin: 0.5em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.create {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.dateInputContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
.inputContainer h3{
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
|
||||
.dateInputContainer {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
import styles from "./CreateEventBtn.module.css"
|
||||
import { useState, useContext } from "react";
|
||||
import Modal from "../../../ui/Modal/Modal";
|
||||
import Button from "../../../ui/Button/Button";
|
||||
import TextInput from "../../../ui/Input/Input";
|
||||
import { EventContext } from "../../../../contexts/Events/EventContext.js";
|
||||
import { AuthContext } from "../../../../contexts/Auth/AuthContext";
|
||||
|
||||
|
||||
export default function CreateEventBtn() {
|
||||
|
||||
const { addEvent } = useContext(EventContext)!;
|
||||
const { user } = useContext(AuthContext)!;
|
||||
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const [name, setName] = useState<string>("");
|
||||
const [description, setDescription] = useState<string>("");
|
||||
const [start, setStart] = useState<string>("");
|
||||
const [end, setEnd] = useState<string>("");
|
||||
|
||||
const handleCreateEvent = async () => {
|
||||
|
||||
if (name !== "" && description !== "" && start !== "" && end !== "") {
|
||||
await addEvent(name, description, start, end);
|
||||
}
|
||||
setIsOpen(false);
|
||||
}
|
||||
|
||||
if(!user?.isAdmin) return null;
|
||||
|
||||
return <>
|
||||
<Button className={`${styles.createButton} glassCard`} variant={"default"} onClick={() => setIsOpen(true)}> + </Button>
|
||||
|
||||
<Modal title={"Créer un événement"} open={isOpen} onClose={() => setIsOpen(false)}>
|
||||
|
||||
<section className={styles.section}>
|
||||
<div className={styles.inputContainer}>
|
||||
<h3>Titre</h3>
|
||||
<TextInput className={styles.input} placeholder={"Nom de l'événement"} value={name} onChange={e => setName(e.target.value)} />
|
||||
</div>
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<h3>Description</h3>
|
||||
<TextInput className={styles.input} placeholder={"Description de l'événement"} value={description} onChange={e => setDescription(e.target.value)} />
|
||||
</div>
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<h3>Date</h3>
|
||||
<div className={styles.dateInputContainer}>
|
||||
<div className={styles.dateInput}>
|
||||
<p>Début</p>
|
||||
<TextInput type={"datetime-local"} value={start} onChange={e => setStart(e.target.value)} />
|
||||
</div>
|
||||
<div className={styles.dateInput}>
|
||||
<p>Fin</p>
|
||||
<TextInput type={"datetime-local"} value={end} onChange={e => setEnd(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button onClick={handleCreateEvent} className={styles.create}>Créer</Button>
|
||||
</section>
|
||||
</Modal>
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import styles from "../ToolBar.module.css"
|
||||
import Button from "../../ui/Button/Button";
|
||||
import { useState } from "react";
|
||||
import Filter from "../../Filter/Filter";
|
||||
|
||||
interface Filters {
|
||||
alphabetical: "asc" | "desc";
|
||||
yearOrder: "asc" | "desc";
|
||||
}
|
||||
|
||||
interface FilterBtnProps {
|
||||
setFilters: any
|
||||
filters: Filters
|
||||
}
|
||||
|
||||
export default function FilterBtn({ setFilters, filters }: FilterBtnProps) {
|
||||
|
||||
const [isFilterVisible, setIsFilterVisible] = useState(false);
|
||||
|
||||
const toggleFilters = () => {
|
||||
setIsFilterVisible(prev => !prev);
|
||||
};
|
||||
|
||||
return <div className={`${styles.filterContainer} ${isFilterVisible ? "glassCard" : ""}`}>
|
||||
<Button className={`${isFilterVisible ? "" : styles.mobileFilter} ${styles.sortButton} glassCard`} variant={"default"} onClick={toggleFilters}>
|
||||
<img src="filter.svg" alt="Filtrer"/>
|
||||
</Button>
|
||||
<div className={styles.filterMenu}>
|
||||
<Filter isFilterVisible={isFilterVisible} filters={filters} setFilters={setFilters}/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import styles from "../ToolBar.module.css"
|
||||
import Button from "../../ui/Button/Button";
|
||||
import { useEffect, useState } from "react";
|
||||
import SearchBar from "../../SearchBar/SearchBar";
|
||||
|
||||
export default function SearchBtn() {
|
||||
|
||||
const [isSearchVisible, setIsSearchVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSearchVisible) {
|
||||
document.documentElement.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.documentElement.style.overflow = '';
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.documentElement.style.overflow = '';
|
||||
};
|
||||
}, [isSearchVisible]);
|
||||
|
||||
return <>
|
||||
|
||||
{isSearchVisible ? (
|
||||
<div className={styles.searchBarContainer}>
|
||||
<div className={styles.searchBarOverlay} onClick={() => setIsSearchVisible(false)}></div>
|
||||
<SearchBar/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div>
|
||||
<Button className={`${styles.searchButton} glassCard`} variant={"default"} onClick={() => setIsSearchVisible(true)}>
|
||||
<img src="search.svg" alt="Rechercher"/>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
.btn {
|
||||
padding: 0.2rem 0.8rem 0.2rem 0.8rem;
|
||||
width: fit-content;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
cursor: pointer;
|
||||
filter: brightness(0.95);
|
||||
}
|
||||
|
||||
.primary {
|
||||
color: white;
|
||||
background-color: #019AFF;
|
||||
}
|
||||
|
||||
.danger {
|
||||
color: white;
|
||||
background-color: #FF073A;
|
||||
}
|
||||
|
||||
.transparent {
|
||||
color: #019AFF;
|
||||
}
|
||||
|
||||
.default {
|
||||
color: black;
|
||||
background: rgba(255, 255, 255, 0.65);
|
||||
backdrop-filter: blur(3px);
|
||||
-webkit-backdrop-filter: blur(3px);
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
box-shadow:
|
||||
0 8px 32px rgba(0, 0, 0, 0.1),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.5),
|
||||
inset 0 -1px 0 rgba(255, 255, 255, 0.1),
|
||||
inset 0 0 8px 4px rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { ReactNode, MouseEventHandler } from "react";
|
||||
import styles from "./Button.module.css";
|
||||
|
||||
type ButtonVariant = "primary" | "danger" | "transparent" | "default";
|
||||
|
||||
interface ButtonProps {
|
||||
children: ReactNode;
|
||||
variant?: ButtonVariant;
|
||||
onClick?: MouseEventHandler<HTMLDivElement>;
|
||||
className?: string | undefined;
|
||||
}
|
||||
|
||||
const Button = ({
|
||||
children,
|
||||
variant = "primary",
|
||||
onClick,
|
||||
className = "",
|
||||
}: ButtonProps) => {
|
||||
let btnStyle: string;
|
||||
|
||||
if (variant === "primary") { // @ts-ignore
|
||||
btnStyle = styles.primary;
|
||||
}
|
||||
else if (variant === "danger") { // @ts-ignore
|
||||
btnStyle = styles.danger;
|
||||
}
|
||||
else if (variant === "transparent") { // @ts-ignore
|
||||
btnStyle = styles.transparent;
|
||||
}
|
||||
else { // @ts-ignore
|
||||
btnStyle = styles.default;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.btn} ${btnStyle} ${className}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Button;
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
.input {
|
||||
border: 1px solid rgba(200, 200, 200, 0.3);
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
color: #000;
|
||||
font-size: 16px;
|
||||
outline: none;
|
||||
transition: all 0.3s ease;
|
||||
width: fit-content;
|
||||
padding: 8px 10px 8px 10px;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.input::placeholder {
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
border-color: rgba(200, 200, 200, 0.6);
|
||||
box-shadow: 0 4px 30px rgba(255, 255, 255, 0.2);
|
||||
background: rgba(255, 255, 255, 0.38);
|
||||
}
|
||||
|
||||
.rounded {
|
||||
border-radius: 25px;
|
||||
}
|
||||
|
||||
.square {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.passwordContainer {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.passwordInput {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.passwordBtn {
|
||||
position: absolute;
|
||||
width: 30px;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import styles from "./Input.module.css";
|
||||
import { useState, ChangeEvent, InputHTMLAttributes } from "react";
|
||||
|
||||
interface TextInputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
onChange?: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
borderStyle?: "square" | "rounded";
|
||||
password?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const TextInput = ({
|
||||
placeholder = "",
|
||||
onChange,
|
||||
value = "",
|
||||
borderStyle = "square",
|
||||
password = false,
|
||||
className = "",
|
||||
...props
|
||||
}: TextInputProps) => {
|
||||
const [showPassword, setShowPassword] = useState<boolean>(false);
|
||||
|
||||
let inputBorderStyle = styles.square;
|
||||
if (borderStyle === "rounded") inputBorderStyle = styles.rounded;
|
||||
|
||||
const toggleShowPassword = () => {
|
||||
setShowPassword((prev) => !prev);
|
||||
};
|
||||
|
||||
if (password) {
|
||||
return (
|
||||
<div className={styles.passwordContainer}>
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={onChange}
|
||||
className={`${styles.input} ${styles.passwordInput} ${inputBorderStyle} ${className}`}
|
||||
{...props}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleShowPassword}
|
||||
className={styles.passwordBtn}
|
||||
>
|
||||
{showPassword ? "👁️" : "🔒"}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
className={`${styles.input} ${inputBorderStyle} ${className}`}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TextInput;
|
||||
@@ -0,0 +1,25 @@
|
||||
.wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
|
||||
border: 5px solid #e0e0e0;
|
||||
border-top: 5px solid #3498db;
|
||||
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import React from "react";
|
||||
import styles from "./Loading.module.css";
|
||||
|
||||
const Loading: React.FC = () => {
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.spinner} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loading;
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
.overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
align-items: flex-start;
|
||||
overflow-y: auto;
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.modal {
|
||||
border-radius: 20px;
|
||||
min-width: 300px;
|
||||
max-width: 70%;
|
||||
max-height: none;
|
||||
position: relative;
|
||||
width: clamp(300px, 50vw, 600px);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.modalFooter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { createPortal } from "react-dom";
|
||||
import styles from "./Modal.module.css";
|
||||
import Button from "../Button/Button";
|
||||
import { ReactNode, MouseEvent } from "react";
|
||||
|
||||
interface ModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
children: ReactNode;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const Modal = ({ open, onClose, children, title }: ModalProps) => {
|
||||
if (!open) return null;
|
||||
|
||||
const handleOverlayClick = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleModalClick = (e: MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
return createPortal(
|
||||
<div className={styles.overlay} onClick={handleOverlayClick}>
|
||||
<div className={`glassCard ${styles.modal}`} onClick={handleModalClick}>
|
||||
{title && <h2>{title}</h2>}
|
||||
{children}
|
||||
<div className={styles.modalFooter}>
|
||||
<Button variant={"default"} onClick={onClose}>Fermer</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
|
||||
export default Modal;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
.themeBtn {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.themeIcon {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
import Button from "../button/button.jsx";
|
||||
import { useState, useEffect } from "react";
|
||||
import styles from "./ThemeSwitcher.module.css";
|
||||
|
||||
|
||||
export default function ThemeSwitcher() {
|
||||
|
||||
const [theme, setTheme] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const t = localStorage.getItem("theme");
|
||||
if(t) setTheme(t);
|
||||
else setTheme("light");
|
||||
}, []);
|
||||
|
||||
const switchTheme = () => {
|
||||
|
||||
let newTheme = theme === "light" ? "dark" : "light";
|
||||
|
||||
if(theme === null || theme === "") newTheme = "dark";
|
||||
|
||||
localStorage.setItem("theme", newTheme);
|
||||
setTheme(newTheme);
|
||||
|
||||
window.dispatchEvent(new Event("theme-changed"));
|
||||
};
|
||||
|
||||
return <Button variant={"default"} onClick={switchTheme} className={styles.themeBtn}>
|
||||
<img src={`/icons/theme/${theme}.svg`} alt={`${theme} icon`} className={styles.themeIcon} />
|
||||
<p>Changer de thème </p>
|
||||
</Button>
|
||||
}
|
||||
}*/
|
||||
|
||||
import Button from "../Button/Button";
|
||||
import { useState, useEffect } from "react";
|
||||
import styles from "./ThemeSwitcher.module.css";
|
||||
|
||||
type Theme = "light" | "dark";
|
||||
|
||||
export default function ThemeSwitcher() {
|
||||
const [theme, setTheme] = useState<Theme | "">("");
|
||||
|
||||
useEffect(() => {
|
||||
const savedTheme = localStorage.getItem("theme") as Theme | null;
|
||||
if (savedTheme) {
|
||||
setTheme(savedTheme);
|
||||
} else {
|
||||
setTheme("light");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const switchTheme = () => {
|
||||
const newTheme: Theme = theme === "light" ? "dark" : "light";
|
||||
localStorage.setItem("theme", newTheme);
|
||||
setTheme(newTheme);
|
||||
window.dispatchEvent(new Event("theme-changed"));
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={switchTheme}
|
||||
className={styles.themeBtn}
|
||||
>
|
||||
<img
|
||||
src={`/icons/theme/${theme}.svg`}
|
||||
alt={`${theme} icon`}
|
||||
className={styles.themeIcon}
|
||||
/>
|
||||
<p>Changer de thème</p>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
export const APP_CONFIG = {
|
||||
contactEmail: "contact@example.com",
|
||||
contactPhone: "+33 01 02 03 04 05",
|
||||
newCalendarURL: "https://calendar.google.com/calendar/u/0/r/settings/addbyurl?cid="
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { createContext } from "react";
|
||||
import User from "../../interfaces/user.interface"
|
||||
|
||||
export interface AuthContextType {
|
||||
user: User | null;
|
||||
loading: boolean;
|
||||
update: () => void;
|
||||
login: (email: string, password: string) => Promise<{ status: number }>;
|
||||
logout: () => Promise<void>;
|
||||
}
|
||||
|
||||
|
||||
export const AuthContext = createContext<AuthContextType | null>(null);
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import loginFunction from "../../utils/auth/login.ts";
|
||||
import getUser from "../../utils/getUser.ts";
|
||||
import { AuthContext } from "./AuthContext.ts";
|
||||
import logoutFunction from "../../utils/auth/logout.ts"
|
||||
|
||||
|
||||
export function AuthProvider({ children }) {
|
||||
const [user, setUser] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const u = await getUser();
|
||||
if(u.status === 200) setUser(u.data);
|
||||
setLoading(false);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const login = async (email, password) => {
|
||||
const res = await loginFunction(email, password);
|
||||
const u = res.status === 200 ? await getUser() : null;
|
||||
if (u) setUser(u.data);
|
||||
return res;
|
||||
};
|
||||
|
||||
|
||||
const logout = async () => {
|
||||
await logoutFunction();
|
||||
setUser(null);
|
||||
};
|
||||
|
||||
const update = async () => {
|
||||
const u = await getUser();
|
||||
setUser(u.data);
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ user, loading, login, logout, update }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { createContext } from "react";
|
||||
|
||||
export const EventDetailContext = createContext(null);
|
||||
@@ -0,0 +1,28 @@
|
||||
import {useState} from 'react';
|
||||
import { EventDetailContext } from "./EventDetailContext.js"
|
||||
import getEventById from "../../utils/events/getEventById.ts";
|
||||
import deleteTaskFunction from "../../utils/tasks/deleteTask.ts";
|
||||
|
||||
export function EventDetailProvider({ children }) {
|
||||
|
||||
const [tasks, setTasks] = useState([]);
|
||||
const [eventInfo, setEventInfo] = useState([]);
|
||||
|
||||
|
||||
const updateEventInfo = async (id) => {
|
||||
const e = await getEventById(id);
|
||||
setEventInfo(e.data);
|
||||
setTasks(e.data.tasks)
|
||||
}
|
||||
|
||||
const deleteTask = async (id, eventId) => {
|
||||
await deleteTaskFunction(id);
|
||||
await updateEventInfo(eventId);
|
||||
}
|
||||
|
||||
return (
|
||||
<EventDetailContext.Provider value={{ tasks, eventInfo, updateEventInfo, deleteTask }}>
|
||||
{children}
|
||||
</EventDetailContext.Provider>
|
||||
);
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { createContext } from "react";
|
||||
|
||||
export type Task = {
|
||||
name: string;
|
||||
description: string;
|
||||
start: string | Date;
|
||||
end: string | Date;
|
||||
};
|
||||
|
||||
export type EventGroup = {
|
||||
date: string;
|
||||
name: string;
|
||||
start: string | Date;
|
||||
tasks: Task[];
|
||||
};
|
||||
|
||||
export type EventContextType = {
|
||||
events: EventGroup[];
|
||||
updateEvent: () => void;
|
||||
addEvent: (name: string, description: string, start: string, end: string) => Promise<void>;
|
||||
removeEvent:(eventId: number) => void;
|
||||
};
|
||||
|
||||
export const EventContext = createContext<EventContextType | null>(null);
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { useState, ReactNode } from 'react';
|
||||
import { EventContext, EventGroup, EventContextType } from "./EventContext";
|
||||
import getAllEvents from "../../utils/events/getAllEvents";
|
||||
import createEvent from "../../utils/events/createEvent";
|
||||
import deleteEvent from "../../utils/events/deleteEvent";
|
||||
|
||||
interface EventProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function EventProvider({ children }: EventProviderProps) {
|
||||
const [events, setEvents] = useState<EventGroup[]>([]);
|
||||
|
||||
const updateEvent = async () => {
|
||||
const result = await getAllEvents();
|
||||
setEvents(result.data.data);
|
||||
};
|
||||
|
||||
const addEvent = async (name: string, description: string, start: string, end: string) => {
|
||||
await createEvent(name, description, start, end);
|
||||
await updateEvent();
|
||||
};
|
||||
|
||||
const removeEvent = async (eventId: number) => {
|
||||
await deleteEvent(eventId);
|
||||
await updateEvent();
|
||||
};
|
||||
|
||||
const contextValue: EventContextType = {
|
||||
events,
|
||||
updateEvent,
|
||||
addEvent,
|
||||
removeEvent,
|
||||
};
|
||||
|
||||
return (
|
||||
<EventContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</EventContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { createContext } from "react";
|
||||
|
||||
export const PendingMembersContext = createContext(null);
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useState } from "react";
|
||||
import { PendingMembersContext } from "./PendingMembersContext.js";
|
||||
|
||||
import getUserToValidate from "../../utils/users/getUserToValidate.ts";
|
||||
import getUserById from "../../utils/users/getUserById.ts";
|
||||
|
||||
export function PendingMembersProvider({ children }) {
|
||||
|
||||
const [pendingMembers, setPendingMembers] = useState([]);
|
||||
|
||||
const updatePendingMembers = async () => {
|
||||
const result = await getUserToValidate();
|
||||
const userIds = result.data;
|
||||
|
||||
if (!userIds || userIds.length === 0) {
|
||||
setPendingMembers([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const users = await Promise.all(
|
||||
userIds.map(id => getUserById(id))
|
||||
);
|
||||
|
||||
setPendingMembers(users.map(u => u.data));
|
||||
};
|
||||
|
||||
return (
|
||||
<PendingMembersContext.Provider
|
||||
value={{
|
||||
pendingMembers,
|
||||
updatePendingMembers
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</PendingMembersContext.Provider>
|
||||
);
|
||||
}
|
||||
Executable
+115
@@ -0,0 +1,115 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
scrollbar-color: #b5b5b5 transparent;
|
||||
}
|
||||
|
||||
html {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
--glass-back: rgba(255, 255, 255, 0.54);
|
||||
--glass-border: rgba(255, 255, 255, 0.2);
|
||||
--glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.1),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.3),
|
||||
inset 0 -1px 0 rgba(255, 255, 255, 0.05),
|
||||
inset 0 0 8px 4px rgba(255, 255, 255, 0.2);
|
||||
|
||||
--glass-border-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);
|
||||
--glass-border-back: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
width:100%;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
body.dark {
|
||||
--glass-back: rgba(160, 160, 160, 0.54);
|
||||
--glass-border: rgba(160, 160, 160, 0.2);
|
||||
--glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.1),
|
||||
inset 0 1px 0 rgba(160, 160, 160, 0.3),
|
||||
inset 0 -1px 0 rgba(160, 160, 160, 0.05),
|
||||
inset 0 0 8px 4px rgba(160, 160, 160, 0.2);
|
||||
|
||||
--glass-border-shadow:
|
||||
inset 1.25px 1.25px 1px rgba(180, 180, 180, 0.6),
|
||||
inset -1.25px -1.25px 1px rgba(180, 180, 180, 0.6),
|
||||
inset -0.25px 0.25px 1px rgba(0, 0, 0, 0.4),
|
||||
inset 0.25px -0.25px 1px rgba(0, 0, 0, 0.4);
|
||||
--glass-border-back: rgba(160, 160, 160, 0.25);
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 1.5rem;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
.glassCard {
|
||||
padding: 10px 20px 10px 20px;
|
||||
background: var(--glass-back);
|
||||
backdrop-filter: blur(3px);
|
||||
-webkit-backdrop-filter: blur(3px);
|
||||
border-radius: 20px;
|
||||
border: 1px solid var(--glass-border);
|
||||
box-shadow: var(--glass-shadow);
|
||||
}
|
||||
|
||||
.glassBorder{
|
||||
padding: 10px 20px 10px 20px;
|
||||
border-radius: 20px;
|
||||
border: none;
|
||||
background: var(--glass-border-back);
|
||||
box-shadow: var(--glass-border-shadow);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.content{
|
||||
padding: 0.7rem;
|
||||
}
|
||||
}
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
|
||||
import type User from "./user.interface";
|
||||
export interface AuthContextType {
|
||||
user: User | null;
|
||||
update: () => void;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export default interface Notification {
|
||||
id: string;
|
||||
data: {
|
||||
message: string;
|
||||
};
|
||||
read_at: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export default interface Response<T> {
|
||||
status: number;
|
||||
data: T;
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
export default interface TaskType {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
location: string;
|
||||
start: string;
|
||||
end: string;
|
||||
max_participants: number;
|
||||
events_id: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
pivot: {
|
||||
user_id: number;
|
||||
task_id: number;
|
||||
};
|
||||
}
|
||||
export default interface Task {
|
||||
id: number;
|
||||
title?: string;
|
||||
completed?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import TaskType from "./task.interface";
|
||||
|
||||
export default interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
lastname: string;
|
||||
role: string;
|
||||
email: string;
|
||||
phone: string | null;
|
||||
created_at: string;
|
||||
isAdmin: boolean;
|
||||
validate: number;
|
||||
tasks: TaskType[];
|
||||
profile_photo_path?: string | null,
|
||||
email_notifications: number,
|
||||
web_notifications: number;
|
||||
}
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
import { Outlet } from "react-router";
|
||||
import Footer from "./components/Footer/Footer.tsx"
|
||||
import Header from "./components/Header/Header.tsx";
|
||||
import Background from "./components/Background/Background.tsx";
|
||||
import { Navigate } from "react-router";
|
||||
import { useContext} from "react";
|
||||
import { AuthContext } from "./contexts/Auth/AuthContext.ts";
|
||||
import Loading from "./components/ui/Loading/Loading.tsx";
|
||||
|
||||
|
||||
function Layout(){
|
||||
|
||||
const { user, loading } = useContext(AuthContext);
|
||||
|
||||
if (loading) return <Background>
|
||||
<Loading />
|
||||
</Background>;
|
||||
if (!user) return <Navigate to="/login" />;
|
||||
if(user && user.validate === 0) return <Navigate to="/error/validation" />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Background>
|
||||
<div className="content">
|
||||
<Header />
|
||||
<Outlet />
|
||||
<Footer />
|
||||
</div>
|
||||
</Background>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Layout;
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
import {StrictMode} from 'react'
|
||||
import {createRoot} from 'react-dom/client'
|
||||
import './index.css'
|
||||
import {RouterProvider} from "react-router";
|
||||
import router from './router.js';
|
||||
import {AuthProvider} from "./contexts/Auth/AuthProvider.jsx";
|
||||
import {EventProvider} from "./contexts/Events/EventProvider.tsx";
|
||||
import {EventDetailProvider} from "./contexts/EventDetail/EventDetailProvider.jsx";
|
||||
import {PendingMembersProvider} from "./contexts/PendingMembers/PendingMembersProvider.jsx";
|
||||
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<AuthProvider>
|
||||
<PendingMembersProvider>
|
||||
<EventProvider>
|
||||
<EventDetailProvider>
|
||||
<RouterProvider router={router}/>
|
||||
</EventDetailProvider>
|
||||
</EventProvider>
|
||||
</PendingMembersProvider>
|
||||
</AuthProvider>
|
||||
</StrictMode>,
|
||||
)
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
gap: 20px;
|
||||
}
|
||||
.rightSection {
|
||||
width: 60%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 760px) {
|
||||
.container {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-top:25px;
|
||||
}
|
||||
|
||||
.rightSection {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
margin-top:0;
|
||||
margin-bottom:25px;
|
||||
}
|
||||
}
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
import { useContext } from "react";
|
||||
import styles from "./AdminPage.module.css";
|
||||
import IncompleteEvents from "./components/IncompleteEvent/IncompleteEvent";
|
||||
import PendingMembers from "./components/PendingMembers/PendingMembers";
|
||||
import { AuthContext } from "../../contexts/Auth/AuthContext";
|
||||
import user from "../../interfaces/user.interface"
|
||||
import { useNavigate } from "react-router";
|
||||
import RegisterMember from "./components/RegisterMember/RegisterMember";
|
||||
|
||||
|
||||
|
||||
function AdminPage() {
|
||||
const authContext = useContext(AuthContext);
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (!authContext?.user?.isAdmin) {
|
||||
navigate("/");
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<RegisterMember />
|
||||
<div className={styles.rightSection}>
|
||||
<IncompleteEvents />
|
||||
<PendingMembers />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AdminPage;
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import React, { useContext } from "react";
|
||||
import styles from "./IncompleteEvent.module.css";
|
||||
import { EventContext } from "../../../../contexts/Events/EventContext.ts";
|
||||
|
||||
function IncompleteEvent() {
|
||||
|
||||
const { events } = useContext(EventContext);
|
||||
|
||||
return (
|
||||
<div className={`${styles.rightBlock} glassCard`}>
|
||||
<h2>Derniers événements</h2>
|
||||
<div className={styles.eventsContainer}>
|
||||
{Array.isArray(events) && events.length > 0 ? (
|
||||
events.map((event) => (
|
||||
<div key={event.id} className={`glassBorder ${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?.length ? (
|
||||
event.tasks.map((task, taskIndex) => (
|
||||
<li key={taskIndex}>{task.name}</li>
|
||||
))
|
||||
) : (
|
||||
<li>Aucune tâche associée.</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
))) : (
|
||||
<p>Aucun événement n'existe pour le moment</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default IncompleteEvent;
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
.rightBlock {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rightBlock > h2 {
|
||||
font-size: 29px;
|
||||
}
|
||||
|
||||
.eventsContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
margin-top: 5px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
padding-bottom: 10px;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.eventCard {
|
||||
min-width: calc((100% - 20px) / 3);
|
||||
max-width: calc((100% - 20px) / 3);
|
||||
min-height: 250px;
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
animation: slideInEvent 1s ease forwards;
|
||||
flex-shrink: 0;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.eventCard ul {
|
||||
align-items: center;
|
||||
margin-left: 5%;
|
||||
}
|
||||
|
||||
.eventCard ul li {
|
||||
text-align: left;
|
||||
list-style-type: circle;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 760px) {
|
||||
.eventCard {
|
||||
min-width: 100%;
|
||||
min-height: 200px;
|
||||
max-height: fit-content;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.eventCard h3 {
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.eventsContainer {
|
||||
flex-direction: column;
|
||||
max-height: 300px;
|
||||
margin-top: 15px;
|
||||
height: fit-content;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.rightBlock > h2 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.emptyEvent {
|
||||
min-width: 100%;
|
||||
min-height: 200px;
|
||||
max-height: fit-content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import React, {useState, useEffect, useContext} from "react";
|
||||
import Button from "../../../../components/ui/Button/Button.tsx";
|
||||
import styles from "./PendingMembers.module.css";
|
||||
import Modal from "../../../../components/ui/Modal/Modal.tsx";
|
||||
import validateUser from "../../../../utils/users/validateUser.ts";
|
||||
import deleteOtherUser from "../../../../utils/users/deleteOtherUser.ts";
|
||||
import { PendingMembersContext } from "../../../../contexts/PendingMembers/PendingMembersContext.js";
|
||||
|
||||
function PendingMembers() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [message, setMessage] = useState("");
|
||||
const [title, setTitle] = useState("");
|
||||
const { pendingMembers, updatePendingMembers } = useContext(PendingMembersContext);
|
||||
|
||||
useEffect(() => {
|
||||
updatePendingMembers();
|
||||
}, []);
|
||||
|
||||
const handleValidate = async (id, name) => {
|
||||
const result = await validateUser(id);
|
||||
|
||||
if(result.status === 200) {
|
||||
setTitle("Utilisateur accepté avec succès")
|
||||
setMessage(`L'utilisateur ${name} a été accepté avec succès`)
|
||||
} else {
|
||||
setTitle("Erreur")
|
||||
setMessage(`Une erreur est survenue durant l'acceptation de l'utilisateur ${name}`)
|
||||
}
|
||||
setOpen(true);
|
||||
updatePendingMembers();
|
||||
}
|
||||
|
||||
const handleRefuse = async (id, name) => {
|
||||
const result = await deleteOtherUser(id);
|
||||
|
||||
if(result.status === 200) {
|
||||
setTitle("Utilisateur refusé avec succès")
|
||||
setMessage(`L'utilisateur ${name} a été refusé avec succès`)
|
||||
setOpen(true);
|
||||
} else {
|
||||
setTitle("Erreur")
|
||||
setMessage(`Une erreur est survenue durant le refus de l'utilisateur ${name}`)
|
||||
setOpen(true);
|
||||
}
|
||||
|
||||
updatePendingMembers();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${styles.rightBlock} glassCard`}>
|
||||
<h2>Membres en attente de validation</h2>
|
||||
|
||||
<div className={styles.membersContainer}>
|
||||
|
||||
{pendingMembers.length === 0 && <p>Aucun membre en attente</p>}
|
||||
|
||||
{pendingMembers.map((member, index) => (
|
||||
|
||||
<div key={index} className={`glassBorder ${styles.memberCard}`}>
|
||||
|
||||
<p>{member.name} {member.lastname}</p>
|
||||
|
||||
<div className={styles.buttonGroup}>
|
||||
<Button variant="primary" onClick={() => handleValidate(member.id, `${member.name} ${member.lastname}`)}>Valider</Button>
|
||||
<Button variant="danger" onClick={() => handleRefuse(member.id, `${member.name} ${member.lastname}`)}>Refuser</Button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
))}
|
||||
|
||||
</div>
|
||||
|
||||
<Modal open={open} onClose={() => setOpen(false)} title={title}>
|
||||
{message}
|
||||
</Modal>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PendingMembers;
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
.rightBlock {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.rightBlock > h2 {
|
||||
font-size: 29px;
|
||||
}
|
||||
|
||||
.membersContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
max-height: 185px;
|
||||
overflow-y: auto;
|
||||
padding-right: 5px;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.memberCard {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
flex-shrink: 0;
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
animation: slideInMember 0.5s ease forwards;
|
||||
}
|
||||
|
||||
.memberCard:nth-child(1) { animation-delay: 0s; }
|
||||
.memberCard:nth-child(2) { animation-delay: 0.1s; }
|
||||
.memberCard:nth-child(3) { animation-delay: 0.2s; }
|
||||
|
||||
.memberCard p {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.buttonGroup {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: right;
|
||||
}
|
||||
|
||||
@keyframes slideInMember {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@media screen and (max-width: 760px) {
|
||||
.membersContainer {
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
.rightBlock > h2{
|
||||
font-size: 18px;
|
||||
}
|
||||
.memberCard p{
|
||||
font-size: 15px;
|
||||
max-width: 150px;
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 761px) {
|
||||
.memberCard p{
|
||||
white-space: normal;
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
.leftBlock {
|
||||
width: 100%;
|
||||
max-width: 720px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-height: calc(100vh - 260px);
|
||||
box-sizing: border-box;
|
||||
padding: 20px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.registerMemberHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.registerForm {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.registerForm input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.registerForm button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.modalContent {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.leftBlock {
|
||||
width: 70%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.leftBlock {
|
||||
width: 40%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import React, { useState } from "react"
|
||||
import styles from "./RegisterMember.module.css"
|
||||
import TextInput from "../../../../components/ui/Input/Input"
|
||||
import Button from "../../../../components/ui/Button/Button";
|
||||
import Response from "../../../../interfaces/response.interface";
|
||||
import ResponseData from "./responseData.inteface";
|
||||
import adminCreateUser from "../../../../utils/users/adminCreateUser";
|
||||
import Modal from "../../../../components/ui/Modal/Modal";
|
||||
|
||||
|
||||
const RegisterMember: React.FC = () => {
|
||||
|
||||
const [name, setName] = useState<string>("");
|
||||
const [lastname, setLastName] = useState<string>("");
|
||||
const [email, setEmail] = useState<string>("");
|
||||
const [phone, setPhone] = useState<string>("");
|
||||
const [password, setPassword] = useState<string>("");
|
||||
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const [title, setTitle] = useState<string>("");
|
||||
const [message, setMessage] = useState<string>("");
|
||||
|
||||
|
||||
const handleCreate = async (): Promise<void> => {
|
||||
|
||||
if(name !== "" && lastname !== "" && email !== "" && phone !== "" ) {
|
||||
const response: Response<ResponseData> = await adminCreateUser(email, name, lastname, phone);
|
||||
if(response.status === 200) {
|
||||
setPassword(response.data.password);
|
||||
setTitle(`Utilisateur enregistré avec succès`);
|
||||
setMessage(`L'utilisateur ${name} ${lastname} a été enregistré avec le mot de passe: ${response.data.password}`);
|
||||
setOpen(true);
|
||||
} else {
|
||||
setTitle(`Erreur`);
|
||||
setMessage(`Une erreur est survenue lors de l'enregistrement de ${name} ${lastname}`);
|
||||
setOpen(true);
|
||||
}
|
||||
} else {
|
||||
setTitle("Erreur");
|
||||
setMessage("Un des champs n'a pas été rempli");
|
||||
setOpen(true);
|
||||
}
|
||||
}
|
||||
|
||||
const showPassword = (): void => {
|
||||
setTitle(`Mot de passe de ${name} ${lastname}`);
|
||||
setMessage(password);
|
||||
setOpen(true);
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleCreate();
|
||||
}
|
||||
};
|
||||
|
||||
return <div className={`${styles.leftBlock} glassCard`}>
|
||||
<div className={styles.registerMemberHeader}>
|
||||
<h2>Enregister un bénévole</h2>
|
||||
</div>
|
||||
|
||||
<div className={styles.registerForm} onKeyDown={handleKeyDown}>
|
||||
<TextInput placeholder={"Prenom"} type={"text"} value={name} onChange={e => setName(e.target.value)} />
|
||||
<TextInput placeholder={"Nom"} type={"text"} value={lastname} onChange={e => setLastName(e.target.value)} />
|
||||
<TextInput placeholder={"Adresse Email"} type={"email"} value={email} onChange={e => setEmail(e.target.value)} />
|
||||
<TextInput placeholder={"Numéro de téléphone"} type={"tel"} value={phone} onChange={e => setPhone(e.target.value)} />
|
||||
|
||||
{password !== "" && <Button onClick={showPassword}> Afficher le mot de passe </Button>}
|
||||
|
||||
<Button variant={"default"} onClick={handleCreate}>Enregistrer</Button>
|
||||
</div>
|
||||
|
||||
<Modal open={open} onClose={() => setOpen(false)} title={title}>
|
||||
<p className={styles.modalContent}>{message}</p>
|
||||
</Modal>
|
||||
|
||||
</div>
|
||||
}
|
||||
|
||||
export default RegisterMember
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export default interface ResponseData {
|
||||
message: string;
|
||||
password: string;
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
.title {
|
||||
color: white;
|
||||
font-size: xx-large;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 2;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
|
||||
p {
|
||||
font-size: large;
|
||||
white-space: pre-line;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSearchParams, useNavigate } from "react-router";
|
||||
import confirmEmailChange from "../../utils/users/confirmEmailChange";
|
||||
import styles from "./ConfirmEmailChangePage.module.css";
|
||||
import Background from "../../components/Background/Background";
|
||||
|
||||
export default function ConfirmEmailChange() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const [message, setMessage] = useState<string>('Validation en cours...');
|
||||
const [success, setSuccess] = useState<boolean | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const token = searchParams.get('token');
|
||||
if (!token) {
|
||||
setMessage('Token manquant.');
|
||||
setSuccess(false);
|
||||
return;
|
||||
}
|
||||
|
||||
confirmEmailChange(token)
|
||||
.then(() => {
|
||||
setMessage('Votre adresse email a bien été mise à jour.\nVous allez être redirigé vers la page de connexion...');
|
||||
setSuccess(true);
|
||||
setTimeout(() => navigate('/login'), 3000);
|
||||
})
|
||||
.catch(() => {
|
||||
setMessage('Token invalide ou expiré.');
|
||||
setSuccess(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return <Background>
|
||||
<div className={styles.content}>
|
||||
<h1 className={styles.title}> Validation de votre adresse email </h1>
|
||||
<div className={`${styles.container} glassCard`}>
|
||||
<p>{message}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Background>
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
.allContent{
|
||||
margin: 16px 0 16px 0;
|
||||
align-self: center;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
|
||||
.textImage {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.eventImage {
|
||||
width: 40%;
|
||||
aspect-ratio: 16 / 9;
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.eventInfo {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.eventInfo p {
|
||||
white-space: normal;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.eventImage {
|
||||
width: 40%;
|
||||
height: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.registerButton:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.tasks {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.eventContent {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.manageBtn {
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.modalContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.manageEvent {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
|
||||
.eventContent {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.textImage {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.manageEvent {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.eventImage {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.manageEvent{
|
||||
margin: 16px 0 16px 0;
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
import {useEffect, useContext, useRef} from "react";
|
||||
import styles from "./EventDetailPage.module.css";
|
||||
import { useParams } from "react-router";
|
||||
import { formatDateLetter } from "../../utils/date/formatDateLetter";
|
||||
import EventTask from "./components/EventTask/EventTask";
|
||||
import { AuthContext } from "../../contexts/Auth/AuthContext";
|
||||
import { EventDetailContext } from "../../contexts/EventDetail/EventDetailContext";
|
||||
import CreateTaskBtn from "./components/CreateTaskBtn/CreateTaskBtn";
|
||||
import DeleteEventBtn from "./components/DeleteEventBtn/DeleteEventBtn";
|
||||
import EditEventBtn from "./components/EditEventBtn/EditEventBtn";
|
||||
import uploadEventPhoto from "../../utils/events/uploadEventPhoto";
|
||||
|
||||
function EventDetailPage() {
|
||||
const params = useParams();
|
||||
const id = params.id as string;
|
||||
|
||||
const { user } = useContext(AuthContext) as any;
|
||||
const { tasks, updateEventInfo, eventInfo } = useContext(EventDetailContext) as any;
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const imageUrl = eventInfo?.event_photo_path
|
||||
? `${import.meta.env.VITE_API_URL}/storage/${eventInfo.event_photo_path}`
|
||||
: "/empty.png";
|
||||
|
||||
console.log(eventInfo);
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
updateEventInfo(id);
|
||||
}
|
||||
}, []);
|
||||
const handleImageClick = () => {
|
||||
if (user?.isAdmin && fileInputRef.current) {
|
||||
fileInputRef.current?.click();
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileChange = async (e: any) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
await uploadEventPhoto(Number(id), file);
|
||||
updateEventInfo(id);
|
||||
};
|
||||
return (
|
||||
<div className={styles.allContent}>
|
||||
<div className={`glassCard`}>
|
||||
<div className={styles.textImage}>
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt="Photo événement"
|
||||
className={styles.eventImage}
|
||||
onClick={handleImageClick}
|
||||
style={{ cursor: user?.isAdmin ? "pointer" : "default" }}
|
||||
/>
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
style={{ display: "none" }}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
|
||||
<div className={styles.eventInfo}>
|
||||
<h2>{eventInfo?.name}</h2>
|
||||
|
||||
<div className={styles.eventContent}>
|
||||
<div>
|
||||
<p>{eventInfo && formatDateLetter(eventInfo.start)}</p>
|
||||
<p>{eventInfo?.description}</p>
|
||||
</div>
|
||||
|
||||
{user?.isAdmin && (
|
||||
<div className={styles.manageEvent}>
|
||||
<EditEventBtn id={id} />
|
||||
<DeleteEventBtn id={id} />
|
||||
<CreateTaskBtn id={id} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Tâches</h2>
|
||||
|
||||
<div className={styles.tasks}>
|
||||
{tasks?.map((task: any, index: number) => (
|
||||
<EventTask key={task.id} taskId={task.id} index={index} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EventDetailPage;
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
.usersList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.userCard {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.userName {
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
import {ReactNode, useContext, useEffect, useState} from "react";
|
||||
import Button from "../../../../components/ui/Button/Button";
|
||||
import Modal from "../../../../components/ui/Modal/Modal";
|
||||
import User from "../../../Volunteers/interfaces/user.interface";
|
||||
import getAllUser from "../../../../utils/users/getAllUsers";
|
||||
import Loading from "../../../../components/ui/Loading/Loading";
|
||||
import getUserById from "../../../../utils/users/getUserById";
|
||||
import styles from "./AssignUserBtn.module.css";
|
||||
import {AuthContext} from "../../../../contexts/Auth/AuthContext";
|
||||
import assignOtherUser from "../../../../utils/tasks/assignOtherUser";
|
||||
import unAssignOtherUser from "../../../../utils/tasks/unAssignOtherUser";
|
||||
import {EventDetailContext} from "../../../../contexts/EventDetail/EventDetailContext";
|
||||
|
||||
interface AssignUserBtnProps {
|
||||
taskId: string | number;
|
||||
eventId: string | number;
|
||||
}
|
||||
|
||||
export default function AssignUserBtn({ taskId, eventId }: AssignUserBtnProps): ReactNode {
|
||||
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const [users, setUsers] = useState<Array<User>>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
|
||||
const { user } = useContext(AuthContext) as any;
|
||||
const { updateEventInfo } = useContext(EventDetailContext) as any;
|
||||
|
||||
if (!user?.isAdmin) return null;
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setLoading(true);
|
||||
|
||||
const result = await getAllUser();
|
||||
|
||||
if (result.status !== 200 || !Array.isArray(result.data)) {
|
||||
console.error("Erreur lors du chargement des utilisateurs", result);
|
||||
return;
|
||||
}
|
||||
|
||||
const usersData = await Promise.all(
|
||||
result.data.map(async (id: number): Promise<User | null> => {
|
||||
const res = await getUserById(id);
|
||||
|
||||
if (res.status === 200) {
|
||||
return res.data as User;
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
);
|
||||
|
||||
setUsers(usersData.filter((user): user is User => user !== null));
|
||||
setLoading(false);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const [assignedIds, setAssignedIds] = useState<Set<number>>(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
setAssignedIds(new Set(
|
||||
users.filter(u => u.tasks.some(t => t.id === taskId)).map(u => u.id)
|
||||
));
|
||||
}, [users, taskId]);
|
||||
|
||||
const handleAssign = async (selectedUser: User) => {
|
||||
setAssignedIds(prev => new Set(prev).add(selectedUser.id));
|
||||
await assignOtherUser(taskId as number, selectedUser.id);
|
||||
updateEventInfo(eventId);
|
||||
};
|
||||
|
||||
const handleUnassign = async (selectedUser: User) => {
|
||||
setAssignedIds(prev => { const next = new Set(prev); next.delete(selectedUser.id); return next; });
|
||||
await unAssignOtherUser(taskId as number, selectedUser.id);
|
||||
updateEventInfo(eventId);
|
||||
};
|
||||
|
||||
return <>
|
||||
<Button variant={"default"} onClick={() => setOpen(true)}>Assigner</Button>
|
||||
|
||||
<Modal open={open} onClose={() => setOpen(false)} title={"Assigner un utilisateur à cette tâche"}>
|
||||
<div className={styles.usersList}>
|
||||
{loading ? <Loading /> : users.map((u: User) => (
|
||||
<div key={u.id} className={`${styles.userCard} glassBorder`}>
|
||||
<p className={styles.userName}>{u.name} {u.lastname}</p>
|
||||
{assignedIds.has(u.id) ? (
|
||||
<Button variant={"danger"} onClick={() => handleUnassign(u)}>
|
||||
Désassigner
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={() => handleAssign(u)}>
|
||||
Assigner
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
import styles from "../../EventDetailPage.module.css";
|
||||
import Button from "../../../../components/ui/Button/Button.tsx";
|
||||
import {useContext, useState} from "react";
|
||||
import TextInput from "../../../../components/ui/Input/Input.tsx";
|
||||
import Modal from "../../../../components/ui/Modal/Modal.tsx";
|
||||
import createTaskApi from "../../../../utils/tasks/createTask.ts";
|
||||
import {EventDetailContext} from "../../../../contexts/EventDetail/EventDetailContext.js";
|
||||
|
||||
export default function CreateTaskBtn({ id }) {
|
||||
|
||||
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("");
|
||||
|
||||
const { updateEventInfo } = useContext(EventDetailContext)
|
||||
|
||||
const createTask = async () => {
|
||||
const data = await createTaskApi({
|
||||
eventId: id,
|
||||
name: taskName,
|
||||
description: taskDescription,
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
location: taskLocation,
|
||||
maxParticipants,
|
||||
});
|
||||
|
||||
if (data?.status === 200) {
|
||||
setOpenTaskModal(false);
|
||||
setTaskName("");
|
||||
setTaskDescription("");
|
||||
setTaskLocation("");
|
||||
setMaxParticipants("");
|
||||
setStartTime("");
|
||||
setEndTime("");
|
||||
|
||||
updateEventInfo(id)
|
||||
} else {
|
||||
console.log("⚠️ Échec de la création de la task");
|
||||
}
|
||||
};
|
||||
|
||||
return <>
|
||||
|
||||
<Button variant={"default"} className={styles.manageBtn} onClick={() => setOpenTaskModal(true)}>Créer tâche</Button>
|
||||
|
||||
<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 de participants max</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="primary" onClick={createTask}> Créer la tâche </Button>
|
||||
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
</>
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
import styles from "../../EventDetailPage.module.css";
|
||||
import Button from "../../../../components/ui/Button/Button";
|
||||
import { useContext, useState } from "react";
|
||||
import TextInput from "../../../../components/ui/Input/Input";
|
||||
import Modal from "../../../../components/ui/Modal/Modal";
|
||||
import createTaskApi from "../../../../utils/tasks/createTask";
|
||||
import { EventDetailContext } from "../../../../contexts/EventDetail/EventDetailContext";
|
||||
|
||||
type CreateTaskBtnProps = {
|
||||
id: string | number;
|
||||
};
|
||||
|
||||
export default function CreateTaskBtn({ id }: CreateTaskBtnProps) {
|
||||
const [openTaskModal, setOpenTaskModal] = useState(false);
|
||||
|
||||
const [taskName, setTaskName] = useState<string>("");
|
||||
const [taskDescription, setTaskDescription] = useState<string>("");
|
||||
const [taskLocation, setTaskLocation] = useState<string>("");
|
||||
const [maxParticipants, setMaxParticipants] = useState<string>("");
|
||||
const [startTime, setStartTime] = useState<string>("");
|
||||
const [endTime, setEndTime] = useState<string>("");
|
||||
|
||||
const { updateEventInfo } = useContext(EventDetailContext) as any;
|
||||
|
||||
const createTask = async () => {
|
||||
const data = await createTaskApi({
|
||||
eventId: id,
|
||||
name: taskName,
|
||||
description: taskDescription,
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
location: taskLocation,
|
||||
maxParticipants,
|
||||
});
|
||||
|
||||
if (data?.status === 200) {
|
||||
setOpenTaskModal(false);
|
||||
setTaskName("");
|
||||
setTaskDescription("");
|
||||
setTaskLocation("");
|
||||
setMaxParticipants("");
|
||||
setStartTime("");
|
||||
setEndTime("");
|
||||
|
||||
updateEventInfo(id);
|
||||
} else {
|
||||
console.log("⚠️ Échec de la création de la task");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant={"default"}
|
||||
className={styles.manageBtn}
|
||||
onClick={() => setOpenTaskModal(true)}
|
||||
>
|
||||
Créer tâche
|
||||
</Button>
|
||||
|
||||
<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: any) => setTaskName(e.target.value)}
|
||||
placeholder="Nom de la tâche"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Description</p>
|
||||
<TextInput
|
||||
value={taskDescription}
|
||||
onChange={(e: any) => setTaskDescription(e.target.value)}
|
||||
placeholder="Description de la tâche"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Lieu</p>
|
||||
<TextInput
|
||||
value={taskLocation}
|
||||
onChange={(e: any) => setTaskLocation(e.target.value)}
|
||||
placeholder="Lieu"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Nombre maximum de participants</p>
|
||||
<TextInput
|
||||
type="number"
|
||||
value={maxParticipants}
|
||||
onChange={(e: any) => setMaxParticipants(e.target.value)}
|
||||
placeholder="Participants max"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Heure de début</p>
|
||||
<TextInput
|
||||
type="datetime-local"
|
||||
value={startTime}
|
||||
onChange={(e: any) => setStartTime(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Heure de fin</p>
|
||||
<TextInput
|
||||
type="datetime-local"
|
||||
value={endTime}
|
||||
onChange={(e: any) => setEndTime(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button variant="default" onClick={createTask}>
|
||||
Créer la tâche
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import styles from "../../EventDetailPage.module.css";
|
||||
import Button from "../../../../components/ui/Button/Button";
|
||||
import Modal from "../../../../components/ui/Modal/Modal";
|
||||
import { useContext, useState } from "react";
|
||||
import { EventDetailContext } from "../../../../contexts/EventDetail/EventDetailContext";
|
||||
import { EventContext } from "../../../../contexts/Events/EventContext";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
type DeleteEventBtnProps = {
|
||||
id: string | number;
|
||||
};
|
||||
|
||||
export default function DeleteEventBtn({ id }: DeleteEventBtnProps) {
|
||||
const { eventInfo } = useContext(EventDetailContext) as any;
|
||||
const { removeEvent } = useContext(EventContext) as any;
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleDelete = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const delEvent = async () => {
|
||||
const result = await removeEvent(id);
|
||||
|
||||
if (result.status === 200 || result.status === 204) {
|
||||
setOpen(false);
|
||||
navigate("/events");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant={"danger"}
|
||||
className={styles.manageBtn}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
Supprimer
|
||||
</Button>
|
||||
|
||||
<Modal open={open} onClose={() => setOpen(false)} title={"Suppression"}>
|
||||
<div className={styles.modalContent}>
|
||||
<p>
|
||||
Etes-vous sur de vouloir supprimer l'événement {eventInfo?.name} ?
|
||||
</p>
|
||||
|
||||
<Button variant={"primary"} onClick={delEvent}>
|
||||
Confirmer
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import { useContext, useState } from "react";
|
||||
import { AuthContext } from "../../../../contexts/Auth/AuthContext";
|
||||
import { EventDetailContext } from "../../../../contexts/EventDetail/EventDetailContext";
|
||||
import Button from "../../../../components/ui/Button/Button";
|
||||
import Modal from "../../../../components/ui/Modal/Modal";
|
||||
import styles from "../../EventDetailPage.module.css";
|
||||
|
||||
type DeleteTaskBtnProps = {
|
||||
taskId: string | number;
|
||||
eventId: string | number;
|
||||
taskName: string;
|
||||
};
|
||||
|
||||
export default function DeleteTaskBtn({ taskId, eventId, taskName }: DeleteTaskBtnProps) {
|
||||
|
||||
const { user } = useContext(AuthContext) as any;
|
||||
const { deleteTask } = useContext(EventDetailContext) as any;
|
||||
|
||||
const [openModal, setOpenModal] = useState<boolean>(false);
|
||||
|
||||
if (!user?.isAdmin) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant={"danger"} onClick={() => setOpenModal(true)}>
|
||||
Supprimer
|
||||
</Button>
|
||||
|
||||
<Modal open={openModal} onClose={() => setOpenModal(false)} title={"Suppression"}>
|
||||
<div className={styles.modalContent}>
|
||||
<p className={styles.modalContent}>
|
||||
Etes-vous sur de vouloir supprimer la tâche {taskName} ?
|
||||
</p>
|
||||
|
||||
<Button
|
||||
variant={"danger"}
|
||||
onClick={() => {
|
||||
deleteTask(taskId, eventId);
|
||||
setOpenModal(false);
|
||||
}}
|
||||
>
|
||||
Confirmer
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
import styles from "../../EventDetailPage.module.css";
|
||||
import Button from "../../../../components/ui/Button/Button";
|
||||
import Modal from "../../../../components/ui/Modal/Modal";
|
||||
import TextInput from "../../../../components/ui/Input/Input";
|
||||
import { useContext, useState, useEffect } from "react";
|
||||
import { EventDetailContext } from "../../../../contexts/EventDetail/EventDetailContext";
|
||||
import updateEvent from "../../../../utils/events/updateEvent";
|
||||
|
||||
type EditEventBtnProps = {
|
||||
id: string | number;
|
||||
};
|
||||
|
||||
export default function EditEventBtn({ id }: EditEventBtnProps) {
|
||||
const { eventInfo, updateEventInfo } = useContext(EventDetailContext) as any;
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [name, setName] = useState<string>("");
|
||||
const [description, setDescription] = useState<string>("");
|
||||
const [start, setStart] = useState<string>("");
|
||||
const [end, setEnd] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
if (open && eventInfo) {
|
||||
setName(eventInfo.name ?? "");
|
||||
setDescription(eventInfo.description ?? "");
|
||||
setStart(eventInfo.start ? eventInfo.start.slice(0, 16) : "");
|
||||
setEnd(eventInfo.end ? eventInfo.end.slice(0, 16) : "");
|
||||
}
|
||||
}, [open, eventInfo]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const res = await updateEvent(Number(id), name, description, start, end);
|
||||
await updateEventInfo(id);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant={"default"}
|
||||
className={styles.manageBtn}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
Modifier
|
||||
</Button>
|
||||
|
||||
<Modal open={open} onClose={() => setOpen(false)} title={"Modifier l'événement"}>
|
||||
<div className={styles.modalContent}>
|
||||
<div>
|
||||
<p>Nom</p>
|
||||
<TextInput
|
||||
value={name}
|
||||
onChange={(e: any) => setName(e.target.value)}
|
||||
placeholder="Nom de l'événement"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Description</p>
|
||||
<TextInput
|
||||
value={description}
|
||||
onChange={(e: any) => setDescription(e.target.value)}
|
||||
placeholder="Description"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Début</p>
|
||||
<TextInput
|
||||
type="datetime-local"
|
||||
value={start}
|
||||
onChange={(e: any) => setStart(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Fin</p>
|
||||
<TextInput
|
||||
type="datetime-local"
|
||||
value={end}
|
||||
onChange={(e: any) => setEnd(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button variant="default" onClick={handleSubmit}>
|
||||
Confirmer
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { AuthContext } from "../../../../contexts/Auth/AuthContext";
|
||||
import { EventDetailContext } from "../../../../contexts/EventDetail/EventDetailContext";
|
||||
import Button from "../../../../components/ui/Button/Button";
|
||||
import Modal from "../../../../components/ui/Modal/Modal";
|
||||
import TextInput from "../../../../components/ui/Input/Input";
|
||||
import styles from "../../EventDetailPage.module.css";
|
||||
import updateTask from "../../../../utils/tasks/updateTask";
|
||||
|
||||
type EditTaskBtnProps = {
|
||||
taskId: number;
|
||||
eventId: number | string;
|
||||
};
|
||||
|
||||
export default function EditTaskBtn({ taskId, eventId }: EditTaskBtnProps) {
|
||||
const { user } = useContext(AuthContext) as any;
|
||||
const { tasks, updateEventInfo } = useContext(EventDetailContext) as any;
|
||||
const task = tasks?.find((t: any) => t.id === taskId);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [name, setName] = useState<string>("");
|
||||
const [description, setDescription] = useState<string>("");
|
||||
const [start, setStart] = useState<string>("");
|
||||
const [end, setEnd] = useState<string>("");
|
||||
const [location, setLocation] = useState<string>("");
|
||||
const [maxParticipants, setMaxParticipants] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
if (open && task) {
|
||||
setName(task.name ?? "");
|
||||
setDescription(task.description ?? "");
|
||||
setStart(task.start ? task.start.slice(0, 16) : "");
|
||||
setEnd(task.end ? task.end.slice(0, 16) : "");
|
||||
setLocation(task.location ?? "");
|
||||
setMaxParticipants(task.max_participants?.toString() ?? "");
|
||||
}
|
||||
}, [open, task]);
|
||||
|
||||
if (!user?.isAdmin) return null;
|
||||
|
||||
const handleSubmit = async () => {
|
||||
await updateTask(taskId, {
|
||||
name,
|
||||
description,
|
||||
start,
|
||||
end,
|
||||
location,
|
||||
max_participants: Number(maxParticipants),
|
||||
});
|
||||
await updateEventInfo(eventId);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant={"default"} onClick={() => setOpen(true)}>
|
||||
Modifier
|
||||
</Button>
|
||||
|
||||
<Modal open={open} onClose={() => setOpen(false)} title={"Modifier la tâche"}>
|
||||
<div className={styles.modalContent}>
|
||||
<div>
|
||||
<p>Nom</p>
|
||||
<TextInput
|
||||
value={name}
|
||||
onChange={(e: any) => setName(e.target.value)}
|
||||
placeholder="Nom de la tâche"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Description</p>
|
||||
<TextInput
|
||||
value={description}
|
||||
onChange={(e: any) => setDescription(e.target.value)}
|
||||
placeholder="Description"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Lieu</p>
|
||||
<TextInput
|
||||
value={location}
|
||||
onChange={(e: any) => setLocation(e.target.value)}
|
||||
placeholder="Lieu"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Participants max</p>
|
||||
<TextInput
|
||||
type="number"
|
||||
value={maxParticipants}
|
||||
onChange={(e: any) => setMaxParticipants(e.target.value)}
|
||||
placeholder="Participants max"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Début</p>
|
||||
<TextInput
|
||||
type="datetime-local"
|
||||
value={start}
|
||||
onChange={(e: any) => setStart(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Fin</p>
|
||||
<TextInput
|
||||
type="datetime-local"
|
||||
value={end}
|
||||
onChange={(e: any) => setEnd(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button variant="default" onClick={handleSubmit}>
|
||||
Confirmer
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
.task {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.taskDate {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.manageTaskBtn {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin: 16px 0 16px 0;
|
||||
animation: fadeInUp 0.6s ease-out forwards;
|
||||
animation-delay: calc(var(--i) * 0.1s);
|
||||
}
|
||||
|
||||
.participants {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.participantsHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.participantsLabel {
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.participantsBadge {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
padding: 2px 10px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.badgeOpen {
|
||||
background: var(--glass-back);
|
||||
color: #00F0FF;
|
||||
}
|
||||
|
||||
.badgeAlmost {
|
||||
background: var(--glass-back);
|
||||
color: #019AFF;
|
||||
}
|
||||
|
||||
.badgeFull {
|
||||
background: var(--glass-back);
|
||||
color: #001C83;
|
||||
}
|
||||
|
||||
.progressBar {
|
||||
width: 100%;
|
||||
max-width: 200px;
|
||||
height: 5px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progressFill {
|
||||
height: 100%;
|
||||
border-radius: 999px;
|
||||
transition: width 0.4s ease;
|
||||
}
|
||||
|
||||
.fillOpen { background: #00F0FF; }
|
||||
.fillAlmost { background: #019AFF; }
|
||||
.fillFull { background: #001C83; }
|
||||
|
||||
.modalContent {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
|
||||
.task {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import styles from "./EventTask.module.css";
|
||||
import { formatDateLetter } from "../../../../utils/date/formatDateLetter";
|
||||
import Button from "../../../../components/ui/Button/Button";
|
||||
import assign from "../../../../utils/tasks/assign";
|
||||
import unAssign from "../../../../utils/tasks/unAssign";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import isAssigned from "../../../../utils/tasks/isAssigned";
|
||||
import Modal from "../../../../components/ui/Modal/Modal";
|
||||
import { AuthContext } from "../../../../contexts/Auth/AuthContext";
|
||||
import { EventDetailContext } from "../../../../contexts/EventDetail/EventDetailContext";
|
||||
import DeleteTaskBtn from "../DeleteTaskBtn/DeleteTaskBtn";
|
||||
import AssignUserBtn from "../AssignUserBtn/AssignUserBtn";
|
||||
import EditTaskBtn from "../EditTaskBtn/EditTaskBtn";
|
||||
|
||||
type EventTaskProps = {
|
||||
taskId: number;
|
||||
index: number;
|
||||
};
|
||||
|
||||
export default function EventTask({ taskId, index }: EventTaskProps) {
|
||||
|
||||
const { update } = useContext(AuthContext) as any;
|
||||
const { tasks, eventInfo, updateEventInfo } = useContext(EventDetailContext) as any;
|
||||
const task = tasks?.find((t: any) => t.id === taskId);
|
||||
const eventId = eventInfo?.id;
|
||||
|
||||
const [assigned, setAssigned] = useState<boolean>(false);
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const [title, setTitle] = useState<string>("");
|
||||
const [message, setMessage] = useState<string>("");
|
||||
|
||||
async function checkAssign() {
|
||||
const data = await isAssigned(task.id);
|
||||
setAssigned(data?.data.assigned);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
checkAssign();
|
||||
}, []);
|
||||
|
||||
const handleRegisterBtn = async () => {
|
||||
const res = await assign(task.id);
|
||||
|
||||
switch (res.status) {
|
||||
case 200:
|
||||
update();
|
||||
await updateEventInfo(eventId);
|
||||
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:
|
||||
update();
|
||||
await updateEventInfo(eventId);
|
||||
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 } as any} key={index} className={`${styles.content} glassBorder`}>
|
||||
<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 className={styles.participants}>
|
||||
<div className={styles.participantsHeader}>
|
||||
<span className={styles.participantsLabel}>Participants</span>
|
||||
<span className={`${styles.participantsBadge} ${
|
||||
task.users_count >= task.max_participants
|
||||
? styles.badgeFull
|
||||
: task.users_count / task.max_participants >= 0.75
|
||||
? styles.badgeAlmost
|
||||
: styles.badgeOpen
|
||||
}`}>
|
||||
{task.users_count} / {task.max_participants}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.progressBar}>
|
||||
<div
|
||||
className={`${styles.progressFill} ${
|
||||
task.users_count >= task.max_participants
|
||||
? styles.fillFull
|
||||
: task.users_count / task.max_participants >= 0.75
|
||||
? styles.fillAlmost
|
||||
: styles.fillOpen
|
||||
}`}
|
||||
style={{ width: `${Math.min((task.users_count / task.max_participants) * 100, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.manageTaskBtn}>
|
||||
{assigned ? (
|
||||
<Button variant={"danger"} onClick={() => handleUnassign()}>
|
||||
Se désinscrire
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={() => handleRegisterBtn()}>
|
||||
S’inscrire
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<EditTaskBtn taskId={task.id} eventId={eventId} />
|
||||
|
||||
<DeleteTaskBtn
|
||||
eventId={eventId}
|
||||
taskId={task.id}
|
||||
taskName={task.name}
|
||||
/>
|
||||
|
||||
<AssignUserBtn taskId={task.id} eventId={eventId} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal open={open} onClose={() => setOpen(false)} title={title}>
|
||||
<p className={styles.modalContent}>{message}</p>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
.eventsContainer {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
margin: 16px 0 16px 0;
|
||||
}
|
||||
|
||||
.eventsList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.eventCard {
|
||||
width: 100%;
|
||||
max-width: 360px;
|
||||
padding: 12px 10px;
|
||||
border-radius: 12px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #ffffffcc;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
.eventCard h3 {
|
||||
margin: 0 0 5px 0;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.eventCard p {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: #333;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.eventImage {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
.eventsContainer {
|
||||
}
|
||||
|
||||
.eventCard {
|
||||
max-width: 700px;
|
||||
padding: 20px;
|
||||
}
|
||||
.eventCard h3 {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
.eventCard p {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.eventImage {
|
||||
height: 180px;
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { useState, useContext } from "react";
|
||||
import Event from "./components/Event/Event";
|
||||
import styles from "./EventsPage.module.css";
|
||||
import ToolBar from "../../components/ToolBar/ToolBar";
|
||||
import filter from "../../utils/filter";
|
||||
import { EventContext, EventGroup, EventContextType } from "../../contexts/Events/EventContext";
|
||||
|
||||
interface FilterOptions {
|
||||
alphabetical: "asc" | "desc";
|
||||
yearOrder: "asc" | "desc";
|
||||
}
|
||||
|
||||
function EventsPage() {
|
||||
const eventContext = useContext(EventContext);
|
||||
if (!eventContext) {
|
||||
throw new Error("EventsPage must be used within an EventContext.Provider");
|
||||
}
|
||||
const { events } = eventContext;
|
||||
|
||||
const [filters, setFilters] = useState<FilterOptions>({
|
||||
alphabetical: "asc",
|
||||
yearOrder: "desc",
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.eventsContainer}>
|
||||
<ToolBar setFilters={setFilters} filters={filters} />
|
||||
<div className={styles.eventsList}>
|
||||
{Array.isArray(events) && events.length > 0 ? (
|
||||
filter(events, filters, "event").map((event: EventGroup, index: number) => (
|
||||
<Event key={index} event={event} />
|
||||
))
|
||||
) : (
|
||||
<p>Aucun événement n'existe pour le moment</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EventsPage;
|
||||
@@ -0,0 +1,37 @@
|
||||
import styles from "./Event.module.css";
|
||||
import { useNavigate } from "react-router";
|
||||
import Button from "/apps/frontend/src/components/ui/Button/Button"
|
||||
|
||||
function Event({event}){
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleNavigate = () => navigate(`/events/` + event.id);
|
||||
|
||||
const imageUrl = event.event_photo_path
|
||||
? `${import.meta.env.VITE_API_URL}/storage/${event.event_photo_path}`
|
||||
: "/empty.png";
|
||||
console.log(imageUrl);
|
||||
return (
|
||||
<div className={`${styles.eventContainer} glassCard`} onClick={handleNavigate}>
|
||||
<div className={styles.eventContainerTop}>
|
||||
<div className={styles.eventContent}>
|
||||
<div className={styles.eventImageDiv}>
|
||||
<img
|
||||
src={imageUrl}
|
||||
className={styles.eventImage}
|
||||
alt="Photo de l'événement"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.eventInfos}>
|
||||
<p className={styles.eventTitle}>{event.name}</p>
|
||||
<p>{event.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={handleNavigate} className={styles.openBtn}>Voir plus</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Event;
|
||||
@@ -0,0 +1,140 @@
|
||||
.eventContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.eventContainer:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.eventContainerTop {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.eventContainerBottom{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.moreLink {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.eventImageDiv {
|
||||
width: 10rem;
|
||||
height: 7rem;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
border-radius: 20px;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.eventImage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.eventContent {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.eventTitle {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.eventButtons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
width: fit-content;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.eventButtons a {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.moreButton {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #019AFF;
|
||||
font-size: 1.5rem;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
width: 55px;
|
||||
height: 55px;
|
||||
}
|
||||
|
||||
.moreButton:hover {
|
||||
font-size: 2rem;
|
||||
background: rgba(191, 191, 191, 0.5);
|
||||
}
|
||||
|
||||
.moreButton:focus {
|
||||
outline: none;
|
||||
}
|
||||
.moreButton:focus-visible {
|
||||
outline: 2px solid black;
|
||||
}
|
||||
|
||||
.moreButtonClicked {
|
||||
rotate: 90deg;
|
||||
}
|
||||
|
||||
.eventMenuOpen {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 1.5em;
|
||||
}
|
||||
|
||||
.eventMenuClose {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.openBtn {
|
||||
align-self: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.eventImage {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.eventContainerTop {
|
||||
flex-direction: column;
|
||||
justify-content: initial;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.eventContainer{
|
||||
padding: 0;
|
||||
margin: 16px 0 16px 0;
|
||||
}
|
||||
|
||||
.eventImageDiv {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.eventContent {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.eventInfos, .eventContainerBottom {
|
||||
padding: 10px 20px 10px 20px;
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
.allContent{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
gap: 80px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.allContent{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
}
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
import styles from "./HomePage.module.css";
|
||||
import Calendar from "./components/Calendar/Calendar";
|
||||
import EventList from "./components/EventList/EventList";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export default function HomePage(): ReactNode {
|
||||
|
||||
return <div className={styles.allContent}>
|
||||
<Calendar/>
|
||||
<EventList/>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import {useState, useMemo, useEffect, useContext} from "react";
|
||||
import styles from "./Calendar.module.css";
|
||||
import {formatDateLetterJS} from "../../../../utils/date/formatDateLetter.ts";
|
||||
import {AuthContext} from "../../../../contexts/Auth/AuthContext.ts";
|
||||
import ExportBtn from "../../../../components/ExportCalendarBtn/ExportBtn.tsx";
|
||||
|
||||
function
|
||||
Calendar() {
|
||||
const { user } = useContext(AuthContext);
|
||||
|
||||
const monthNames = [
|
||||
"Janvier", "Février", "Mars", "Avril", "Mai", "Juin",
|
||||
"Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"
|
||||
];
|
||||
|
||||
const today = new Date();
|
||||
|
||||
const [currentMonth, setCurrentMonth] = useState(today.getMonth());
|
||||
const [currentYear, setCurrentYear] = useState(today.getFullYear());
|
||||
const [selectedDay, setSelectedDay] = useState(null);
|
||||
|
||||
const eventsByDate = useMemo(() => {
|
||||
if (!user?.tasks) return {};
|
||||
return user.tasks.reduce((eventsByDate, task) => {
|
||||
if (!task?.start) return eventsByDate;
|
||||
const dateKey = task.start.split(" ")[0];
|
||||
if (!eventsByDate[dateKey]) eventsByDate[dateKey] = [];
|
||||
eventsByDate[dateKey].push(task);
|
||||
return eventsByDate;
|
||||
}, {});
|
||||
}, [user]);
|
||||
|
||||
useEffect(() => {
|
||||
const key = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`;
|
||||
|
||||
setSelectedDay({
|
||||
date: key,
|
||||
events: eventsByDate[key] || []
|
||||
});
|
||||
}, [eventsByDate]);
|
||||
|
||||
const daysInMonth = new Date(currentYear, currentMonth + 1, 0).getDate();
|
||||
const firstDay = new Date(currentYear, currentMonth, 1).getDay();
|
||||
const startDay = (firstDay + 6) % 7;
|
||||
|
||||
const days = [];
|
||||
for (let i = 0; i < startDay; i++) days.push(null);
|
||||
for (let i = 1; i <= daysInMonth; i++) days.push(i);
|
||||
|
||||
const selectDay = (year, month, day) => {
|
||||
const key = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
||||
setSelectedDay({
|
||||
date: key,
|
||||
events: eventsByDate[key] || []
|
||||
});
|
||||
};
|
||||
|
||||
const handleDayClick = (day) => {
|
||||
if (!day) return;
|
||||
selectDay(currentYear, currentMonth, day);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${styles.glassCard} glassCard`}>
|
||||
<div className={styles.calendarContainer}>
|
||||
|
||||
<div className={styles.calendarHeader}>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (currentMonth === 0) {
|
||||
setCurrentMonth(11);
|
||||
setCurrentYear(y => y - 1);
|
||||
} else {
|
||||
setCurrentMonth(m => m - 1);
|
||||
}
|
||||
}}
|
||||
>
|
||||
◀
|
||||
</button>
|
||||
<p>{monthNames[currentMonth]} {currentYear}</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (currentMonth === 11) {
|
||||
setCurrentMonth(0);
|
||||
setCurrentYear(y => y + 1);
|
||||
} else {
|
||||
setCurrentMonth(m => m + 1);
|
||||
}
|
||||
}}
|
||||
>
|
||||
▶
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<table className={styles.calendar}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Lun</th>
|
||||
<th>Mar</th>
|
||||
<th>Mer</th>
|
||||
<th>Jeu</th>
|
||||
<th>Ven</th>
|
||||
<th>Sam</th>
|
||||
<th>Dim</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Array.from({length: Math.ceil(days.length / 7)}).map((_, w) => (
|
||||
<tr key={w}>
|
||||
{days.slice(w * 7, w * 7 + 7).map((day, i) => {
|
||||
if (!day) return <td key={i}></td>;
|
||||
|
||||
const key = `${currentYear}-${String(currentMonth + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
||||
const hasEvent = (eventsByDate[key]?.length ?? 0) > 0;
|
||||
|
||||
return (
|
||||
<td
|
||||
key={i}
|
||||
className={[
|
||||
hasEvent ? styles.hasEvent : "",
|
||||
selectedDay?.date === key ? styles.selected : ""
|
||||
].join(" ")}
|
||||
onClick={() => handleDayClick(day)}
|
||||
>
|
||||
{day}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{selectedDay && (
|
||||
<div className={`${styles.eventBox} glassBorder`}>
|
||||
<h3>Événements du {formatDateLetterJS(selectedDay.date)}</h3>
|
||||
|
||||
{selectedDay.events.length > 0 ? (
|
||||
<ul>
|
||||
{selectedDay.events.map(event => (
|
||||
<li key={event.id}>
|
||||
<strong>{event.name}</strong><br/>
|
||||
- {event.description}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p>Aucun événement ce jour-là.</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.exportbox}>
|
||||
<ExportBtn />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Calendar;
|
||||
@@ -0,0 +1,155 @@
|
||||
.calendarContainer {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.calendarHeader {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.calendarHeader p {
|
||||
width: auto;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.calendarHeader button {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
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);
|
||||
font-size: 15px;
|
||||
color: #019AFF;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.calendar {
|
||||
border-collapse: collapse;
|
||||
width: 80%;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.calendar th, .calendar td {
|
||||
border: none;
|
||||
padding: 10px;
|
||||
width: 14.2%;
|
||||
line-height: 130%;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.today {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.hasEvent {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hasEvent::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 10px;
|
||||
width: 20px;
|
||||
height: 2px;
|
||||
background-color: #007bff;
|
||||
transform: translateX(-50%);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.hasEvent.selected::before {
|
||||
width: 25px;
|
||||
height: 2px;
|
||||
bottom: 10px;
|
||||
}
|
||||
|
||||
.selected {
|
||||
position: relative;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.selected::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
transform: translate(-50%, -50%);
|
||||
border-radius: 38%;
|
||||
pointer-events: 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);
|
||||
}
|
||||
|
||||
.eventBox {
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.eventBox ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.glassCard {
|
||||
width: 45%;
|
||||
max-height: 90vh;
|
||||
margin: 16px 0 16px 0;
|
||||
height: fit-content;
|
||||
overflow: scroll;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.glassCard::-webkit-scrollbar{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.exportbox{
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.exportbtn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.glassCard {
|
||||
width: 100%;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
max-height: 450px;
|
||||
}
|
||||
|
||||
.calendarContainer {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import styles from "./EventList.module.css";
|
||||
import { formatDateLetter } from "../../../../utils/date/formatDateLetter";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
type Task = {
|
||||
name: string;
|
||||
description: string;
|
||||
start: string | Date;
|
||||
end: string | Date;
|
||||
};
|
||||
|
||||
type EventGroup = {
|
||||
id: number;
|
||||
date: string;
|
||||
name: string;
|
||||
start: string | Date;
|
||||
tasks: Task[];
|
||||
};
|
||||
|
||||
type EventItemProps = {
|
||||
eventGroup: EventGroup;
|
||||
};
|
||||
|
||||
export default function EventItem({ eventGroup }: EventItemProps) {
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div
|
||||
key={eventGroup.date}
|
||||
className={`${styles.eventGroup} glassBorder`}
|
||||
onClick={() => navigate(`/events/${eventGroup.id}`)}
|
||||
>
|
||||
<div className={styles.eventTitle}>
|
||||
<p className={styles.title}>{eventGroup.name}</p>
|
||||
<p>{formatDateLetter(eventGroup.start)}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
.glassCard {
|
||||
width: 45%;
|
||||
max-height: 90vh;
|
||||
margin: 16px 0 16px 0;
|
||||
height: fit-content;
|
||||
overflow: scroll;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.glassCard::-webkit-scrollbar{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.allContent{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
gap: 76px;
|
||||
}
|
||||
|
||||
.eventList {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.eventDetails {
|
||||
margin-top: 10px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.dateSelected{
|
||||
font-size: 100px;
|
||||
}
|
||||
|
||||
h2{
|
||||
font-size: 30px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.eventGroup{
|
||||
margin: 16px 0 16px 0;
|
||||
}
|
||||
|
||||
.eventGroup:hover{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.eventTitle {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.glassCard {
|
||||
width: 100%;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
max-height: 450px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useMemo, useContext } from "react";
|
||||
import styles from "./EventList.module.css";
|
||||
import EventItem from "./EventItem";
|
||||
import { EventContext } from "../../../../contexts/Events/EventContext";
|
||||
|
||||
type Task = {
|
||||
name: string;
|
||||
description: string;
|
||||
start: string | Date;
|
||||
end: string | Date;
|
||||
};
|
||||
|
||||
type EventGroup = {
|
||||
date: string;
|
||||
name: string;
|
||||
start: string | Date;
|
||||
tasks: Task[];
|
||||
};
|
||||
|
||||
type EventContextType = {
|
||||
events?: EventGroup[];
|
||||
};
|
||||
|
||||
function EventList() {
|
||||
const { events = [] } = useContext(EventContext) as EventContextType;
|
||||
|
||||
const sortedEvents = useMemo<EventGroup[]>(() => {
|
||||
if (!Array.isArray(events)) return [];
|
||||
|
||||
return [...events].sort(
|
||||
(a, b) =>
|
||||
new Date(a.start).getTime() - new Date(b.start).getTime()
|
||||
);
|
||||
}, [events]);
|
||||
|
||||
return (
|
||||
<div className={`${styles.glassCard} glassCard`}>
|
||||
<h2>Liste des événements à venir</h2>
|
||||
|
||||
{sortedEvents.length > 0 ? (
|
||||
<div className={styles.eventList}>
|
||||
{sortedEvents.map((eventGroup, index) => (
|
||||
<EventItem eventGroup={eventGroup} key={index} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p>Aucun événement planifié pour l'instant.</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EventList;
|
||||
@@ -0,0 +1,16 @@
|
||||
.h2 {
|
||||
justify-content: left;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.section ul {
|
||||
padding : 0 2em;
|
||||
}
|
||||
|
||||
|
||||
.legalNotices{
|
||||
margin: 16px 2em;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import styles from "./LegalNoticesPage.module.css";
|
||||
import {Link} from "react-router";
|
||||
|
||||
export default function legalNoticesPage() {
|
||||
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>
|
||||
);
|
||||
}
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
import styles from "./LoginPage.module.css";
|
||||
import LoginForm from "./components/LoginForm/LoginForm.tsx";
|
||||
import Background from "../../components/Background/Background.tsx";
|
||||
import { useEffect, useContext } from "react";
|
||||
import { AuthContext } from "../../contexts/Auth/AuthContext.ts";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
function LoginPage() {
|
||||
|
||||
const { user, loading } = useContext(AuthContext)
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
console.log("loading" + loading);
|
||||
console.log("user" + user);
|
||||
|
||||
if (!loading && user) {
|
||||
navigate("/");
|
||||
}
|
||||
}, [loading, navigate, user]);
|
||||
|
||||
return (
|
||||
<Background>
|
||||
<div className={styles.container}>
|
||||
<h1 className={styles.title}>Connexion</h1>
|
||||
<LoginForm />
|
||||
</div>
|
||||
</Background>
|
||||
);
|
||||
}
|
||||
|
||||
export default LoginPage;
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/*.container {
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
touch-action: manipulation;
|
||||
width: 90%;
|
||||
max-width: 380px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 14px;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}*/
|
||||
.container {
|
||||
position: relative; /* au lieu d'absolute */
|
||||
z-index: 9999;
|
||||
touch-action: manipulation;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 90%;
|
||||
max-width: 420px; /* RegisterPage : 420px / LoginPage : 380px */
|
||||
padding: 20px;
|
||||
border-radius: 14px;
|
||||
|
||||
margin: auto; /* centre dans le parent flex */
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.8rem;
|
||||
color: white;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
@media (min-width: 600px) {
|
||||
.container {
|
||||
max-width: 460px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.container {
|
||||
max-width: 600px;
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 2.6rem;
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
import styles from "./LoginPage.module.css";
|
||||
import LoginForm from "./components/LoginForm/LoginForm";
|
||||
import Background from "../../components/Background/Background.js";
|
||||
import { useEffect, useContext } from "react";
|
||||
import { AuthContext, AuthContextType } from "../../contexts/Auth/AuthContext";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
function LoginPage() {
|
||||
const { user, loading } = useContext(AuthContext) as AuthContextType;
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && user) {
|
||||
navigate("/");
|
||||
}
|
||||
}, [loading, user, navigate]);
|
||||
|
||||
return (
|
||||
<Background className="">
|
||||
<div className={styles.container}>
|
||||
<h1 className={styles.title}>Connexion</h1>
|
||||
<LoginForm />
|
||||
</div>
|
||||
</Background>
|
||||
);
|
||||
}
|
||||
|
||||
export default LoginPage;
|
||||
@@ -0,0 +1,68 @@
|
||||
:global(.glassCard) {
|
||||
--glass-bg: #ffffff9f;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
padding: 0 10px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 19px;
|
||||
color: #111;
|
||||
background: var(--glass-bg);
|
||||
box-shadow: 0 2px 6px rgba(91, 90, 90, 0.06) inset;
|
||||
}
|
||||
|
||||
input::placeholder{
|
||||
font-size: 20px;
|
||||
color: rgba(0,0,0,0.45);
|
||||
}
|
||||
|
||||
.formgroup{
|
||||
margin-top:23px;
|
||||
}
|
||||
|
||||
.logButton{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.inputContainer {
|
||||
padding: 15px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.loginButton {
|
||||
display: inline-block;
|
||||
height: 50px;
|
||||
width:90%;
|
||||
border-radius: 16px;
|
||||
margin-bottom:3%;
|
||||
margin-top:3%;
|
||||
background-color: rgba(255, 255, 255, 0.335);
|
||||
color: #000000;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background-color 160ms ease, transform 120ms ease, box-shadow 160ms ease;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.loginButton:hover {
|
||||
background-color: #54585e;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 14px rgba(43,108,176,0.18);
|
||||
}
|
||||
|
||||
.loginButton:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 3px 8px rgba(43,108,176,0.12);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useState, useContext, FormEvent } from "react";
|
||||
import styles from "./LoginForm.module.css";
|
||||
import Button from "../../../../components/ui/Button/Button";
|
||||
import Input from "../../../../components/ui/Input/Input";
|
||||
import Modal from "../../../../components/ui/Modal/Modal";
|
||||
import { useNavigate } from "react-router";
|
||||
import { AuthContext, AuthContextType } from "../../../../contexts/Auth/AuthContext";
|
||||
|
||||
interface LoginFormProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function LoginForm({ className }: LoginFormProps) {
|
||||
const navigate = useNavigate();
|
||||
const authContext = useContext(AuthContext) as AuthContextType;
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { login } = authContext;
|
||||
|
||||
async function handleSubmit(e?: FormEvent) {
|
||||
if (e) e.preventDefault();
|
||||
const res = await login(email, password);
|
||||
if (res.status === 401 || res.status === 422) setOpen(true);
|
||||
else navigate("/");
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className={`${className} glassCard ${styles.inputContainer}`} onKeyDown={handleKeyDown}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className={styles.formgroup}>
|
||||
<Input
|
||||
type="email"
|
||||
value={email}
|
||||
placeholder="Adresse mail"
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.formgroup}>
|
||||
<Input
|
||||
password={true}
|
||||
value={password}
|
||||
placeholder="Mot de passe"
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.logButton}>
|
||||
<Button variant={"transparent"} onClick={() => navigate("/register")}>Vous n'avez pas de compte ?</Button>
|
||||
<Button onClick={handleSubmit} variant={"default"}>Se connecter</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<Modal title={"Impossible de se connecter"} open={open} onClose={() => setOpen(false)}>
|
||||
<p>L'Email ou le mot de passe est incorrect</p>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LoginForm;
|
||||
@@ -0,0 +1,33 @@
|
||||
.title {
|
||||
color: white;
|
||||
font-size: xx-large;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 2;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
|
||||
p {
|
||||
font-size: large;
|
||||
}
|
||||
}
|
||||
.redirectSpan {
|
||||
color: blue;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.redirectSpan:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import Background from "../../components/Background/Background";
|
||||
import styles from "./PageNotFoundPage.module.css"
|
||||
import { useNavigate } from "react-router";
|
||||
import { APP_CONFIG } from "../../config/appConfig.js";
|
||||
|
||||
export default function PageNotFoundPage() {
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
return <Background>
|
||||
<div className={styles.content}>
|
||||
<h1 className={styles.title}> Page non trouvée </h1>
|
||||
<div className={`${styles.container} glassCard`}>
|
||||
<p>
|
||||
Désolé, la page que vous recherchez n'existe pas ou n'est plus disponible. <br/>
|
||||
Vous pouvez : <br/>
|
||||
- Vérifier l'URL pour une éventuelle erreur de saisie. <br/>
|
||||
- Revenir à l'accueil <span onClick={() => navigate("/")} className={styles.redirectSpan}>ici.</span> <br/>
|
||||
|
||||
</p>
|
||||
<p> Contact : {APP_CONFIG.contactEmail} </p>
|
||||
</div>
|
||||
</div>
|
||||
</Background>
|
||||
}
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
box-sizing: border-box;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.profileboard {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
border-radius: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.settingAccount {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.contentWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.profilePictureContainer {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
margin-bottom: 0;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.profilePicture {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.editPictureButton {
|
||||
margin-top: 16px;
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border: none;
|
||||
border-radius: 24px;
|
||||
cursor: pointer;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: #e74c3c;
|
||||
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15);
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(5px);
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
width: auto;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.editPictureButton:hover {
|
||||
background: rgba(255, 255, 255, 1);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.2);
|
||||
color: #c0392b;
|
||||
}
|
||||
|
||||
.description {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
.headerProfile {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.description h2 {
|
||||
font-size: 1.6rem;
|
||||
margin-bottom: 10px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.description button {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modifyIcon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
display: block;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.topDescription {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.infoBlock {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.settingAcount {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.profilePictureContainer {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.pictureOverlay {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 2px;
|
||||
border-radius: 0 0 90% 90%;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
.profilePictureContainer:hover .pictureOverlay {
|
||||
opacity: 1;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.contentWrapper {
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.profilePictureContainer {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.description {
|
||||
width: calc(100% - 180px);
|
||||
}
|
||||
|
||||
.topDescription {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.infoBlock {
|
||||
flex: 1;
|
||||
min-width: calc(50% - 8px);
|
||||
max-width: calc(50% - 8px);
|
||||
}
|
||||
|
||||
.editPictureButton {
|
||||
margin: 16px auto 0 auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.headerProfile h2 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.profileboard {
|
||||
padding: 28px;
|
||||
}
|
||||
|
||||
.profilePictureContainer {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
.description {
|
||||
width: calc(100% - 220px);
|
||||
}
|
||||
|
||||
.headerProfile h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
import {useContext, useEffect, useState} from "react";
|
||||
import styles from "./ProfilePage.module.css";
|
||||
import {AuthContext} from "../../contexts/Auth/AuthContext";
|
||||
import formatDate from "../../utils/date/formatDate";
|
||||
import Task from "../../components/Task/Task";
|
||||
import SettingsModal from "./components/SettingsModal/SettingsModal";
|
||||
import {useParams} from "react-router";
|
||||
import {useNavigate} from "react-router";
|
||||
import {useRef} from "react";
|
||||
import getUserById from "../../utils/users/getUserById.js";
|
||||
import ManageMember from "./components/ManageMember/ManageMember";
|
||||
import uploadProfilePhoto from "../../utils/users/uploadProfilePhoto";
|
||||
import deleteProfilePhoto from "../../utils/users/deleteProfilePhoto";
|
||||
import type { AuthContextType } from "../../interfaces/auth.interface";
|
||||
import type TaskType from "../../interfaces/task.interface";
|
||||
import type User from "../../interfaces/user.interface";
|
||||
|
||||
|
||||
|
||||
function ProfilePage() {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams();
|
||||
const { user, update } = useContext(AuthContext) as AuthContextType;
|
||||
const [profileUser, setProfileUser] = useState<User | null>(null);
|
||||
const [profilePicture, setProfilePicture] = useState<string | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const isOwnProfile = !id || user?.id.toString() === id;
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!isOwnProfile && id) {
|
||||
const res = await getUserById(Number(id));
|
||||
if (res.status === 404) {
|
||||
navigate("/404");
|
||||
return;
|
||||
}
|
||||
if (res.status !== 200) {
|
||||
console.log("Erreur lors du chargement du profil", res);
|
||||
return;
|
||||
}
|
||||
setProfileUser(res.data);
|
||||
} else {
|
||||
setProfileUser(user);
|
||||
if (user?.profile_photo_path) {
|
||||
setProfilePicture(`${import.meta.env.VITE_API_URL}/storage/${user.profile_photo_path}`);
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, [user, id]);
|
||||
|
||||
const handleEditPictureClick = () => {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.click();
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
const res = await uploadProfilePhoto(file);
|
||||
if (res.ok) {
|
||||
setProfilePicture(`${import.meta.env.VITE_API_URL}/storage/${res.data.profile_photo_path}`);
|
||||
console.log("Photo mise à jour")
|
||||
await update();
|
||||
} else {
|
||||
console.log(res.data?.message || res.error || "Erreur lors de l'upload.");
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("Erreur réseau.");
|
||||
}
|
||||
};
|
||||
const handleDeletePicture = async () => {
|
||||
|
||||
await deleteProfilePhoto();
|
||||
setProfilePicture(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={`${styles.profileboard} glassCard`}>
|
||||
<div className={styles.contentWrapper}>
|
||||
<div className={styles.profilePictureContainer} onClick={isOwnProfile ? handleEditPictureClick : undefined} style={isOwnProfile ? { cursor: "pointer" } : {}}>
|
||||
<img
|
||||
src={isOwnProfile ? (profilePicture || "/react.svg") : "/react.svg"}
|
||||
alt="Photo de profil"
|
||||
className={styles.profilePicture}
|
||||
/>
|
||||
{isOwnProfile && (
|
||||
<div className={styles.pictureOverlay}>
|
||||
Modifier
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<input type="file" ref={fileInputRef} onChange={handleFileChange} accept="image/*" style={{ display: "none" }}/>
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileChange}
|
||||
accept="image/*"
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
|
||||
<div className={styles.description}>
|
||||
<div className={styles.headerProfile}>
|
||||
<h2>
|
||||
{profileUser?.name} {profileUser?.lastname}
|
||||
</h2>
|
||||
{user?.isAdmin && profileUser && !isOwnProfile && (
|
||||
<ManageMember userToManage={profileUser} />
|
||||
)}
|
||||
{isOwnProfile && (
|
||||
<div className={styles.settingAccount}>
|
||||
<SettingsModal />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.topDescription}>
|
||||
<div className={`${styles.infoBlock} glassBorder`}>
|
||||
<p><strong>Role :</strong> {profileUser?.role}</p>
|
||||
<p><strong>Membre depuis :</strong> {profileUser && formatDate(profileUser.created_at)}</p>
|
||||
</div>
|
||||
|
||||
<div className={`${styles.infoBlock} glassBorder`}>
|
||||
<p><strong>Mail :</strong> {profileUser?.email}</p>
|
||||
<p><strong>Téléphone :</strong> {profileUser?.phone ?? "Non renseigné"}</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<h2>Tâches :</h2>
|
||||
{profileUser?.tasks.length ? (
|
||||
profileUser.tasks.map((task) => <Task key={task.id} task={task} />)
|
||||
) : (
|
||||
<p>Aucune tâche.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProfilePage;
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
import Button from "../../../../../components/ui/Button/Button.tsx";
|
||||
import Modal from "../../../../../components/ui/Modal/Modal.tsx";
|
||||
import { useState } from "react";
|
||||
import deactivateUser from "../../../../../utils/users/deactivateUser.ts"
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
export default function DeactivateMemberBtn({ name, id, ...props }) {
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const deactivateMember = async () => {
|
||||
const result = await deactivateUser(id);
|
||||
|
||||
if (result.status === 200 || result.status === 204) {
|
||||
setOpen(false);
|
||||
navigate("/volunteers");
|
||||
}
|
||||
}
|
||||
|
||||
return <div {...props}>
|
||||
<Button variant={"danger"} onClick={() => setOpen(true)}>Désactiver</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 désactiver le compte {name} ?</p>
|
||||
<Button variant={"danger"} onClick={deactivateMember}>Désactiver</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import {ReactNode, useState} from "react";
|
||||
import Button from "../../../../../components/ui/Button/Button";
|
||||
import Modal from "../../../../../components/ui/Modal/Modal";
|
||||
import { useNavigate } from "react-router";
|
||||
import deleteOtherUser from "../../../../../utils/users/deleteOtherUser";
|
||||
|
||||
export default function DeleteMemberBtn({name, id, className}: { name: string, id: number, className?: any }): ReactNode {
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const deleteMember = async () => {
|
||||
const result = await deleteOtherUser(id);
|
||||
|
||||
if (result.status === 200 || result.status === 204) {
|
||||
setOpen(false);
|
||||
navigate("/volunteers");
|
||||
}
|
||||
}
|
||||
|
||||
return <div className={className}>
|
||||
<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={deleteMember}>Supprimer</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
.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%;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #1d1d1f;
|
||||
background: rgb(255, 255, 255, 0.75);
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
}
|
||||
|
||||
.modalContent {
|
||||
text-align: center;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import Button from "../../../../components/ui/Button/Button";
|
||||
import Modal from "../../../../components/ui/Modal/Modal";
|
||||
import {useContext, useState} from "react";
|
||||
import styles from "./ManageMember.module.css"
|
||||
import DeactivateMemberBtn from "./DeactivateMember/DeactivateMemberBtn.jsx";
|
||||
import ModifyRole from "./ModifyRole/ModifyRole.jsx";
|
||||
import {AuthContext, AuthContextType} from "../../../../contexts/Auth/AuthContext.js";
|
||||
import DeleteMemberBtn from "./DeleteMember/DeleteMemberBtn";
|
||||
import User from "../../../../interfaces/user.interface";
|
||||
|
||||
|
||||
export default function ManageMember({userToManage}: { userToManage: User }) {
|
||||
|
||||
const {user} = useContext(AuthContext) as AuthContextType;
|
||||
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 ${userToManage.name}`}>
|
||||
|
||||
{(user?.id != userToManage.id) ? (
|
||||
<div className={`${styles.manageSection} glassBorder`}>
|
||||
<h3>Désactiver le compte</h3>
|
||||
<DeactivateMemberBtn name={userToManage.name} id={userToManage.id}
|
||||
className={styles.manageBlockContent}/>
|
||||
<h3> Supprimer le compte</h3>
|
||||
<DeleteMemberBtn name={userToManage.name} id={userToManage.id}
|
||||
className={styles.manageBlockContent}/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className={`${styles.manageSection} glassBorder`}>
|
||||
<h3>Modifier le rôle</h3>
|
||||
<ModifyRole user={userToManage}/>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import styles from "../ManageMember.module.css";
|
||||
import Button from "../../../../../components/ui/Button/Button.tsx";
|
||||
import modifyRole from "../../../../../utils/users/modifyRole.ts";
|
||||
import { useState, useEffect } from "react";
|
||||
import getRoles from "../../../../../utils/getRoles.ts";
|
||||
import Modal from "../../../../../components/ui/Modal/Modal.tsx";
|
||||
|
||||
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.data);
|
||||
//TODO: Fermer les modal + actualiser les infos lors du changement de role
|
||||
});
|
||||
}, [])
|
||||
|
||||
|
||||
const handleRoleUpdate = async () => {
|
||||
const res = await modifyRole(selectedRole, user.id);
|
||||
|
||||
if (res.status === 200) {
|
||||
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} glassBorder`}
|
||||
>
|
||||
{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>
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
.content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.divNotif {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.checkboxLabel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkboxLabel input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 6px;
|
||||
border: 2px solid #019AFF;
|
||||
background-color: transparent;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
}
|
||||
.checkboxLabel input:checked + .checkmark {
|
||||
background-color: #019AFF;
|
||||
}
|
||||
|
||||
.checkmark::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.checkboxLabel input:checked + .checkmark::after {
|
||||
display: block;
|
||||
left: 8px;
|
||||
top: 4px;
|
||||
width: 6px;
|
||||
height: 12px;
|
||||
border: solid white;
|
||||
border-width: 0 3px 3px 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.modifyIcon {
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.modifyIcon:hover {
|
||||
transform: rotate(15deg);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.section h3 {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.section h4 {
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.section button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.submitBtn {
|
||||
align-self: center;
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.alignText {
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.section :global(input) {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.submitBtn {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.section button {
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
+298
@@ -0,0 +1,298 @@
|
||||
import {useContext, useState, ChangeEvent, useEffect} from "react";
|
||||
import Modal from "../../../../components/ui/Modal/Modal";
|
||||
import Button from "../../../../components/ui/Button/Button";
|
||||
import styles from "./SettingsModal.module.css";
|
||||
import ThemeSwitcher from "../../../../components/ui/ThemeSwitcher/ThemeSwitcher";
|
||||
import {AuthContext} from "../../../../contexts/Auth/AuthContext";
|
||||
import TextInput from "../../../../components/ui/Input/Input";
|
||||
import updateUser from "../../../../utils/users/updateUser.js";
|
||||
import deleteUser from "../../../../utils/users/deleteUser.js";
|
||||
import ExportCalendarBtn from "../../../../components/ExportCalendarBtn/ExportBtn";
|
||||
import { useNavigate } from "react-router";
|
||||
import toggleEmailNotifications from "../../../../utils/users/toggleEmailNotifications";
|
||||
import toggleWebNotifications from "../../../../utils/users/toggleWebNotifications";
|
||||
import updateUserPassword from "../../../../utils/users/updateUserPassword";
|
||||
import updateUserEmail from "../../../../utils/users/updateUserEmail";
|
||||
|
||||
|
||||
interface TaskType {
|
||||
id: number;
|
||||
title?: string;
|
||||
completed?: boolean;
|
||||
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
lastname: string;
|
||||
role: string;
|
||||
email: string;
|
||||
phone: string | null;
|
||||
created_at: string;
|
||||
isAdmin: boolean;
|
||||
tasks: TaskType[],
|
||||
email_notifications: number,
|
||||
web_notifications: number;
|
||||
}
|
||||
|
||||
interface AuthContextType {
|
||||
user: User;
|
||||
update: () => void;
|
||||
logout: () => void;
|
||||
}
|
||||
|
||||
export default function SettingsModal() {
|
||||
const navigate = useNavigate();
|
||||
const {user, logout, update} = useContext(AuthContext) as unknown as AuthContextType;
|
||||
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const [isDelete, setIsDelete] = useState<boolean>(false);
|
||||
|
||||
const [name, setName] = useState<string>(user?.name ?? "");
|
||||
const [lastname, setLastName] = useState<string>(user?.lastname ?? "");
|
||||
const [phone, setPhone] = useState<string>(user?.phone ?? "");
|
||||
|
||||
const [emailNotifications, setEmailNotifications] = useState<number>(user?.email_notifications ?? 0);
|
||||
const [webNotifications, setWebNotifications] = useState<number>(user?.web_notifications ?? 0);
|
||||
|
||||
const [currentPassword, setCurrentPassword] = useState<string>('');
|
||||
const [newPassword, setNewPassword] = useState<string>('');
|
||||
const [newPasswordConfirm, setNewPasswordConfirm] = useState<string>('');
|
||||
const [email, setEmail] = useState<string>('');
|
||||
|
||||
const handleLogout = (): void => {
|
||||
logout();
|
||||
};
|
||||
|
||||
const handleSubmit = async (): Promise<void> => {
|
||||
await updateUser(name, lastname, phone || null);
|
||||
update();
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleDelete = async (): Promise<void> => {
|
||||
await deleteUser();
|
||||
logout();
|
||||
navigate('/');
|
||||
update();
|
||||
};
|
||||
|
||||
const handleNameChange = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||
setName(e.target.value);
|
||||
};
|
||||
|
||||
const handleLastnameChange = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||
setLastName(e.target.value);
|
||||
};
|
||||
|
||||
const handlePhoneChange = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||
setPhone(e.target.value);
|
||||
};
|
||||
|
||||
const handleEmailToggle = async () => {
|
||||
try {
|
||||
await toggleEmailNotifications(user.id);
|
||||
setEmailNotifications(prev => prev === 0 ? 1 : 0);
|
||||
update();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleWebToggle = async () => {
|
||||
try {
|
||||
await toggleWebNotifications(user.id);
|
||||
setWebNotifications(prev => prev === 0 ? 1 : 0);
|
||||
update();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePasswordUpdate = async () => {
|
||||
try {
|
||||
await updateUserPassword(currentPassword, newPassword, newPasswordConfirm);
|
||||
setCurrentPassword('');
|
||||
setNewPassword('');
|
||||
setNewPasswordConfirm('');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEmailUpdate = async () => {
|
||||
try {
|
||||
await updateUserEmail(email);
|
||||
setEmail('');
|
||||
update();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant="transparent" onClick={() => setOpen((prev) => !prev)}>
|
||||
<img
|
||||
src="/icons/settings-wheel.svg"
|
||||
alt="Modify btn"
|
||||
className={styles.modifyIcon}
|
||||
/>
|
||||
</Button>
|
||||
|
||||
<Modal
|
||||
title="Paramètres"
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
>
|
||||
<div className={styles.content}>
|
||||
<div className={`glassBorder ${styles.section}`}>
|
||||
<h3>Modifier les informations du compte</h3>
|
||||
<div>
|
||||
<h4>Changer de Prénom</h4>
|
||||
<TextInput
|
||||
value={name}
|
||||
onChange={handleNameChange}
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4>Changer de nom</h4>
|
||||
<TextInput
|
||||
value={lastname}
|
||||
onChange={handleLastnameChange}
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4>Changer de numéro de téléphone</h4>
|
||||
<TextInput
|
||||
value={phone}
|
||||
onChange={handlePhoneChange}
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className={styles.submitBtn}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Confirmer
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={`glassBorder ${styles.section}`}>
|
||||
<h3>Changer l'adresse email</h3>
|
||||
<div>
|
||||
<h4>Nouvelle adresse email</h4>
|
||||
<TextInput value={email} onChange={e => setEmail(e.target.value)} type="email" />
|
||||
</div>
|
||||
<Button className={styles.submitBtn} onClick={handleEmailUpdate}>
|
||||
Confirmer
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
<div className={`glassBorder ${styles.section}`}>
|
||||
<h3>Changer le mot de passe</h3>
|
||||
<div>
|
||||
<h4>Mot de passe actuel</h4>
|
||||
<TextInput value={currentPassword} onChange={e => setCurrentPassword(e.target.value)} type="password" />
|
||||
</div>
|
||||
<div>
|
||||
<h4>Nouveau mot de passe</h4>
|
||||
<TextInput value={newPassword} onChange={e => setNewPassword(e.target.value)} type="password" />
|
||||
</div>
|
||||
<div>
|
||||
<h4>Confirmer le nouveau mot de passe</h4>
|
||||
<TextInput value={newPasswordConfirm} onChange={e => setNewPasswordConfirm(e.target.value)} type="password" />
|
||||
</div>
|
||||
<Button className={styles.submitBtn} onClick={handlePasswordUpdate}>
|
||||
Confirmer
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={`glassBorder ${styles.section}`}>
|
||||
<h3>Apparence</h3>
|
||||
<ThemeSwitcher/>
|
||||
</div>
|
||||
|
||||
<div className={`glassBorder ${styles.section}`}>
|
||||
<h3>Notifications</h3>
|
||||
|
||||
<div className={styles.divNotif}>
|
||||
<h4>Notifications par email</h4>
|
||||
|
||||
<label className={styles.checkboxLabel}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={emailNotifications === 1}
|
||||
onChange={handleEmailToggle}
|
||||
/>
|
||||
<span className={styles.checkmark}></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className={styles.divNotif}>
|
||||
<h4>Notifications sur le site</h4>
|
||||
|
||||
<label className={styles.checkboxLabel}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={webNotifications === 1}
|
||||
onChange={handleWebToggle}
|
||||
/>
|
||||
<span className={styles.checkmark}></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className={`glassBorder ${styles.section}`}>
|
||||
<h3>Divers</h3>
|
||||
<ExportCalendarBtn />
|
||||
</div>
|
||||
|
||||
<div className={`glassBorder ${styles.section}`}>
|
||||
<h3>Sécurité du compte</h3>
|
||||
|
||||
<Button variant="danger" onClick={handleLogout}>
|
||||
Déconnexion
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={() => setIsDelete(true)}
|
||||
>
|
||||
Supprimer le compte
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="Validation"
|
||||
open={isDelete}
|
||||
onClose={() => setIsDelete(false)}
|
||||
>
|
||||
<div className={styles.section}>
|
||||
<p className={styles.alignText}>
|
||||
Êtes vous vraiment sûr de vouloir supprimer votre compte ?
|
||||
</p>
|
||||
<Button
|
||||
variant="danger"
|
||||
className={styles.submitBtn}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
Confirmer
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Loaded 100 of 225 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user