Merge branch 'dev' into features/notifications
# Conflicts: # package-lock.json # package.json # src/components/Header/Header.jsx
This commit is contained in:
commit
6da4880f0d
42 files changed
+2465
-1652
No files matched your search
+2
-2
@@ -1,3 +1,5 @@
|
|||||||
|
.env
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
@@ -25,5 +27,3 @@ dist-ssr
|
|||||||
|
|
||||||
*storybook.log
|
*storybook.log
|
||||||
storybook-static
|
storybook-static
|
||||||
|
|
||||||
.env
|
|
||||||
Generated
+908
-918
File diff suppressed because it is too large.
Load diff
+7
-1
@@ -13,11 +13,16 @@
|
|||||||
"test": "vitest"
|
"test": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@laravel/echo-react": "^2.2.6",
|
"@emotion/react": "^11.14.0",
|
||||||
|
"@emotion/styled": "^11.14.1",
|
||||||
|
"@floating-ui/react": "^0.27.16",
|
||||||
|
"@mui/material": "^7.3.6",
|
||||||
|
"bootstrap": "^5.3.8",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"laravel-echo": "^2.2.7",
|
"laravel-echo": "^2.2.7",
|
||||||
"pusher-js": "^8.4.0",
|
"pusher-js": "^8.4.0",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
|
"react-bootstrap": "^2.10.10",
|
||||||
"react-chartjs-2": "^5.3.1",
|
"react-chartjs-2": "^5.3.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-router": "^7.9.4",
|
"react-router": "^7.9.4",
|
||||||
@@ -35,6 +40,7 @@
|
|||||||
"@vitejs/plugin-react": "^5.0.4",
|
"@vitejs/plugin-react": "^5.0.4",
|
||||||
"@vitest/browser-playwright": "^4.0.8",
|
"@vitest/browser-playwright": "^4.0.8",
|
||||||
"@vitest/coverage-v8": "^4.0.8",
|
"@vitest/coverage-v8": "^4.0.8",
|
||||||
|
"baseline-browser-mapping": "^2.9.7",
|
||||||
"eslint": "^9.36.0",
|
"eslint": "^9.36.0",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.22",
|
"eslint-plugin-react-refresh": "^0.4.22",
|
||||||
|
|||||||
Generated
+34
@@ -23,6 +23,9 @@ importers:
|
|||||||
react-router:
|
react-router:
|
||||||
specifier: ^7.9.4
|
specifier: ^7.9.4
|
||||||
version: 7.9.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
version: 7.9.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
|
react-router-dom:
|
||||||
|
specifier: ^7.10.1
|
||||||
|
version: 7.10.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@chromatic-com/storybook':
|
'@chromatic-com/storybook':
|
||||||
specifier: ^4.1.2
|
specifier: ^4.1.2
|
||||||
@@ -1553,6 +1556,23 @@ packages:
|
|||||||
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
|
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
react-router-dom@7.10.1:
|
||||||
|
resolution: {integrity: sha512-JNBANI6ChGVjA5bwsUIwJk7LHKmqB4JYnYfzFwyp2t12Izva11elds2jx7Yfoup2zssedntwU0oZ5DEmk5Sdaw==}
|
||||||
|
engines: {node: '>=20.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=18'
|
||||||
|
react-dom: '>=18'
|
||||||
|
|
||||||
|
react-router@7.10.1:
|
||||||
|
resolution: {integrity: sha512-gHL89dRa3kwlUYtRQ+m8NmxGI6CgqN+k4XyGjwcFoQwwCWF6xXpOCUlDovkXClS0d0XJN/5q7kc5W3kiFEd0Yw==}
|
||||||
|
engines: {node: '>=20.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=18'
|
||||||
|
react-dom: '>=18'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
react-dom:
|
||||||
|
optional: true
|
||||||
|
|
||||||
react-router@7.9.4:
|
react-router@7.9.4:
|
||||||
resolution: {integrity: sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA==}
|
resolution: {integrity: sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
@@ -3315,6 +3335,20 @@ snapshots:
|
|||||||
|
|
||||||
react-refresh@0.17.0: {}
|
react-refresh@0.17.0: {}
|
||||||
|
|
||||||
|
react-router-dom@7.10.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||||
|
dependencies:
|
||||||
|
react: 19.2.0
|
||||||
|
react-dom: 19.2.0(react@19.2.0)
|
||||||
|
react-router: 7.10.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
|
|
||||||
|
react-router@7.10.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||||
|
dependencies:
|
||||||
|
cookie: 1.0.2
|
||||||
|
react: 19.2.0
|
||||||
|
set-cookie-parser: 2.7.1
|
||||||
|
optionalDependencies:
|
||||||
|
react-dom: 19.2.0(react@19.2.0)
|
||||||
|
|
||||||
react-router@7.9.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
react-router@7.9.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
cookie: 1.0.2
|
cookie: 1.0.2
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Link, NavLink } from "react-router";
|
import { Link, NavLink } from "react-router";
|
||||||
import {useContext, useEffect, useRef, useState} from "react";
|
import {useContext, useEffect, useRef, useState} from "react";
|
||||||
|
|
||||||
import {AuthContext} from "../../contexts/auth/AuthContext.js";
|
import {AuthContext} from "../../contexts/auth/AuthContext.js";
|
||||||
|
|
||||||
import styles from "./Header.module.css"
|
import styles from "./Header.module.css"
|
||||||
@@ -13,11 +14,8 @@ import { userCreatedListener } from "../../utils/echo/listeners/userCreatedListe
|
|||||||
import { userNotificationsListener } from "../../utils/echo/listeners/userNotificationsListener.js";
|
import { userNotificationsListener } from "../../utils/echo/listeners/userNotificationsListener.js";
|
||||||
import NotificationCard from "../NotificationCard/NotificationCard.jsx";
|
import NotificationCard from "../NotificationCard/NotificationCard.jsx";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function Header() {
|
function Header() {
|
||||||
const { update } = useContext(AuthContext);
|
const { update, user } = useContext(AuthContext);
|
||||||
const [notificationMenu, setnotificationMenu] = useState(false);
|
const [notificationMenu, setnotificationMenu] = useState(false);
|
||||||
const [unreadNotification, setunreadNotification] = useState(false);
|
const [unreadNotification, setunreadNotification] = useState(false);
|
||||||
const [mobileMenu, setMobileMenu] = useState(false);
|
const [mobileMenu, setMobileMenu] = useState(false);
|
||||||
@@ -115,7 +113,7 @@ function Header() {
|
|||||||
<nav className={`${mobileMenu ? styles.inactiveHeaderContent : styles.activeHeaderContent}`}>
|
<nav className={`${mobileMenu ? styles.inactiveHeaderContent : styles.activeHeaderContent}`}>
|
||||||
<div className={styles.headerLeftContent}>
|
<div className={styles.headerLeftContent}>
|
||||||
<NavLink className={styles.logoLink} to="/">
|
<NavLink className={styles.logoLink} to="/">
|
||||||
<img src="logo.png" alt="logo" className={styles.logo} />
|
<img src="/logo.png" alt="logo" className={styles.logo} />
|
||||||
</NavLink>
|
</NavLink>
|
||||||
<div className={styles.navLinks}>
|
<div className={styles.navLinks}>
|
||||||
<NavLink to="/" className={({ isActive }) => isActive ? styles.activeLink : styles.link}>
|
<NavLink to="/" className={({ isActive }) => isActive ? styles.activeLink : styles.link}>
|
||||||
@@ -127,6 +125,11 @@ function Header() {
|
|||||||
<NavLink to="/volunteers" className={({ isActive }) => isActive ? styles.activeLink : styles.link}>
|
<NavLink to="/volunteers" className={({ isActive }) => isActive ? styles.activeLink : styles.link}>
|
||||||
Bénévoles
|
Bénévoles
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
{user?.isAdmin && (
|
||||||
|
<NavLink to="/admin" className={({ isActive }) => isActive ? styles.activeLink : styles.link}>
|
||||||
|
Administration
|
||||||
|
</NavLink>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -135,12 +138,12 @@ function Header() {
|
|||||||
className={`${styles.bellBtn} ${notificationMenu ? styles.activeBellBtn : ""}`}
|
className={`${styles.bellBtn} ${notificationMenu ? styles.activeBellBtn : ""}`}
|
||||||
onClick={() => {toggleNotificationMenu();}}
|
onClick={() => {toggleNotificationMenu();}}
|
||||||
>
|
>
|
||||||
<img className={styles.notificationsImg} src="bell.svg" alt="notifications"/>
|
<img className={styles.notificationsImg} src="/bell.svg" alt="notifications"/>
|
||||||
{unreadNotification && <span className={styles.notificationBadge}></span>}
|
{unreadNotification && <span className={styles.notificationBadge}></span>}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Link to="/profile" className={styles.profileLink}>
|
<Link to="/profile" className={styles.profileLink}>
|
||||||
<img className={styles.profileImg} src="person.svg" alt="profile"/>
|
<img className={styles.profileImg} src="/person.svg" alt="profile"/>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -155,6 +158,11 @@ function Header() {
|
|||||||
<NavLink to="/volunteers" className={({ isActive }) => isActive ? styles.activeLink : styles.link}>
|
<NavLink to="/volunteers" className={({ isActive }) => isActive ? styles.activeLink : styles.link}>
|
||||||
Bénévoles
|
Bénévoles
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
{user?.isAdmin && (
|
||||||
|
<NavLink to="/admin" className={({ isActive }) => isActive ? styles.activeLink : styles.link}>
|
||||||
|
Administration
|
||||||
|
</NavLink>
|
||||||
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import React from "react";
|
||||||
|
import styles from "./IncompleteEvent.module.css";
|
||||||
|
|
||||||
|
function IncompleteEvent() {
|
||||||
|
const incompleteEvents = [
|
||||||
|
{ title: "Événement A", tasks: ["Communication sur les réseaux", "Enormément de comm", "Toujours + de comm,Communication sur les réseaux",
|
||||||
|
"Enormément de comm", "Toujours + de comm","Communication sur les réseaux", "Enormément de comm", "Toujours + de comm","Communication sur les réseaux", "Enormément de comm", "Toujours + de comm]"] },
|
||||||
|
{ title: "Événement B", tasks: ["Réservation de la salle", "Vérification du matériel"] },
|
||||||
|
{ title: "Événement C", tasks: ["Communication sur les réseaux", "Enormément de comm", "Toujours + de comm"] },
|
||||||
|
{ title: "Événement D", tasks: ["Communication sur les réseaux", "Enormément de comm", "Toujours + de comm"] },
|
||||||
|
{ title: "Événement E", tasks: ["Communication sur les réseaux", "Enormément de comm", "Toujours + de comm,Communication sur les réseaux",
|
||||||
|
"Enormément de comm", "Toujours + de comm","Communication sur les réseaux", "Enormément de comm", "Toujours + de comm","Communication sur les réseaux", "Enormément de comm", "Toujours + de comm]"] },
|
||||||
|
{ title: "Événement F", tasks: ["Communication sur les réseaux", "Enormément de comm", "Toujours + de comm,Communication sur les réseaux",
|
||||||
|
"Enormément de comm", "Toujours + de comm","Communication sur les réseaux", "Enormément de comm", "Toujours + de comm","Communication sur les réseaux", "Enormément de comm", "Toujours + de comm]"] },
|
||||||
|
{ title: "Événement G", tasks: ["Communication sur les réseaux", "Enormément de comm", "Toujours + de comm,Communication sur les réseaux",
|
||||||
|
"Enormément de comm", "Toujours + de comm","Communication sur les réseaux", "Enormément de comm", "Toujours + de comm","Communication sur les réseaux", "Enormément de comm", "Toujours + de comm]"] },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${styles.rightBlock} glassCard`}>
|
||||||
|
<h2>Événements incomplets</h2>
|
||||||
|
<div className={styles.eventsContainer}>
|
||||||
|
{incompleteEvents.map((event, index) => (
|
||||||
|
<div key={index} className={`glassCard ${styles.eventCard}`}>
|
||||||
|
<h3>{event.title}</h3>
|
||||||
|
<ul className={styles.tasksList}>
|
||||||
|
{event.tasks.map((task, taskIndex) => (
|
||||||
|
<li key={taskIndex}>{task}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IncompleteEvent;
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
.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 ul {
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 8%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eventCard ul li {
|
||||||
|
text-align: left;
|
||||||
|
list-style-type: circle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eventsContainer::-webkit-scrollbar {
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eventsContainer::-webkit-scrollbar-track {
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eventsContainer::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(30, 96, 249, 0.5);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eventsContainer::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(30, 96, 249, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
padding: 10px 10px 200px 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(20px);
|
||||||
|
animation: slideInEvent 1s ease forwards;
|
||||||
|
|
||||||
|
flex-shrink: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eventCard>h3 {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eventCard h3 {
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eventsContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
max-height: 300px;
|
||||||
|
margin-top: 15px;
|
||||||
|
height: fit-content;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-bottom: 0;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rightBlock>h2 {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Button from "../ui/button/button";
|
||||||
|
import styles from "./PendingMembers.module.css";
|
||||||
|
|
||||||
|
function pendingMembers() {
|
||||||
|
const pendingMembers = [
|
||||||
|
{ name: "Antoine Ordonneau" },
|
||||||
|
{ name: "Giovanni Josserand" },
|
||||||
|
{ name: "Quentin T'Jampens" },
|
||||||
|
{ name: "Quentin T'Jampens" },
|
||||||
|
{ name: "Quentin T'Jampens" },
|
||||||
|
{ name: "Quentin T'Jampens" },
|
||||||
|
{ name: "Quentin T'Jampens" }
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleValidate = (name) => {
|
||||||
|
console.log(`Valider ${name}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReject = (name) => {
|
||||||
|
console.log(`Refuser ${name}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${styles.rightBlock} glassCard`}>
|
||||||
|
<h2>Membres en attente de validation</h2>
|
||||||
|
<div className={styles.membersContainer}>
|
||||||
|
{pendingMembers.map((member, index) => (
|
||||||
|
<div key={index} className={`glassCard ${styles.memberCard}`}>
|
||||||
|
<p>{member.name}</p>
|
||||||
|
<div className={styles.buttonGroup}>
|
||||||
|
<Button onClick={() => handleValidate(member.name)} variant="primary">
|
||||||
|
Valider
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => handleReject(member.name)} variant="danger">
|
||||||
|
Refuser
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default pendingMembers;
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.membersContainer::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.membersContainer::-webkit-scrollbar-track {
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.membersContainer::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 0, 0, 0.5);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.membersContainer::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(125, 249, 30, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.memberCard {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
/* BACKGROUND ////////////////////////////////////////////////////////*/
|
|
||||||
.backgroundContainer {
|
.backgroundContainer {
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { Doughnut } from "react-chartjs-2";
|
||||||
|
import { Chart, ArcElement } from "chart.js";
|
||||||
|
import { MenuItem, Select, FormControl } from "@mui/material";
|
||||||
|
import styles from "./ParticipationChart.module.css";
|
||||||
|
|
||||||
|
Chart.register(ArcElement);
|
||||||
|
|
||||||
|
function ParticipationChart() {
|
||||||
|
const [selected, setSelected] = useState("3");
|
||||||
|
const [participationRate] = useState(78);
|
||||||
|
|
||||||
|
const chartData = {
|
||||||
|
labels: ["Progress", "Background"],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
data: [participationRate, 100 - participationRate],
|
||||||
|
backgroundColor: ["#1e60f9ff", "#ffffffff"],
|
||||||
|
borderWidth: 0,
|
||||||
|
weight: 10,
|
||||||
|
cutout: "93%",
|
||||||
|
circumference: 360,
|
||||||
|
rotation: -90,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const chartOptions = {
|
||||||
|
responsive: true,
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false },
|
||||||
|
tooltip: { enabled: false },
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
animateScale: true,
|
||||||
|
animateRotate: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${styles.leftBlock} glassCard`}>
|
||||||
|
<div className={styles.participationHeader}>
|
||||||
|
<div className={`${styles.partiText} glassCard`}>
|
||||||
|
<label htmlFor={styles.months}>Taux de participation depuis :</label>
|
||||||
|
<div className={styles.formControl}>
|
||||||
|
<FormControl className={styles.formDesign}>
|
||||||
|
<Select
|
||||||
|
value={selected}
|
||||||
|
onChange={(e) => setSelected(e.target.value)}
|
||||||
|
sx={{
|
||||||
|
width: "100px",
|
||||||
|
height: "35px",
|
||||||
|
fontSize: "19px",
|
||||||
|
paddingBottom: "-2px",
|
||||||
|
"& .MuiOutlinedInput-notchedOutline": { border: "none" },
|
||||||
|
"& .MuiSelect-select": { padding: "8px", paddingRight: "24px !important" },
|
||||||
|
"& .MuiSvgIcon-root": { fontSize: "20px" },
|
||||||
|
}}
|
||||||
|
MenuProps={{
|
||||||
|
PaperProps: {
|
||||||
|
sx: {
|
||||||
|
className: "glassCard",
|
||||||
|
borderRadius: "7px",
|
||||||
|
width: "100px",
|
||||||
|
maxWidth: "100px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
displayEmpty
|
||||||
|
>
|
||||||
|
<MenuItem value="1" sx={{ fontSize: "18px" }}>1 mois</MenuItem>
|
||||||
|
<MenuItem value="3" sx={{ fontSize: "18px" }}>3 mois</MenuItem>
|
||||||
|
<MenuItem value="6" sx={{ fontSize: "18px" }}>6 mois</MenuItem>
|
||||||
|
<MenuItem value="12" sx={{ fontSize: "18px" }}>12 mois</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.chartContainer}>
|
||||||
|
<div className={styles.chartWrapper}>
|
||||||
|
<Doughnut data={chartData} options={chartOptions} />
|
||||||
|
<div className={styles.chartPercentage}>{participationRate}%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ParticipationChart;
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
.leftBlock {
|
||||||
|
width: 40%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
height: calc(100vh - 260px);
|
||||||
|
margin-bottom: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.participationHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partiText {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 2%;
|
||||||
|
margin-top: 2%;
|
||||||
|
animation: slideInEvent 2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partiText label {
|
||||||
|
font-size: 20px;
|
||||||
|
text-align: right;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chartContainer {
|
||||||
|
max-width: 80%;
|
||||||
|
width: 400px;
|
||||||
|
height: auto;
|
||||||
|
margin: 0 auto;
|
||||||
|
position: relative;
|
||||||
|
animation: slideEvent2 2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chartWrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chartPercentage {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
text-align: center;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
font-size: 60px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideEvent2 {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInEvent {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.formControl {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formDesign {
|
||||||
|
margin: 0 auto;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 760px) {
|
||||||
|
.partiText {
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partiText label {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftBlock {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
height: fit-content;
|
||||||
|
margin-bottom: auto;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
.chartContainer{
|
||||||
|
height:fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -11,10 +11,15 @@ export default function CreateEventBtn() {
|
|||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
|
const [start, setStart] = useState("");
|
||||||
|
const [end, setEnd] = useState("");
|
||||||
|
|
||||||
const handleCreateEvent = async () => {
|
const handleCreateEvent = async () => {
|
||||||
|
|
||||||
if(name !== "" || description !== "") await createEvent(name, description);
|
if(name !== "" || description !== "" || start !== "" || end !== "") {
|
||||||
|
await createEvent(name, description, start, end);
|
||||||
|
console.log(start);
|
||||||
|
}
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,16 +31,29 @@ export default function CreateEventBtn() {
|
|||||||
<section className={styles.section}>
|
<section className={styles.section}>
|
||||||
<div className={styles.inputContainer}>
|
<div className={styles.inputContainer}>
|
||||||
<h2>Titre</h2>
|
<h2>Titre</h2>
|
||||||
<TextInput placeholder={"Nom de l'événement"} value={name} onChange={e => setName(e.target.value)} />
|
<TextInput className={styles.input} placeholder={"Nom de l'événement"} value={name} onChange={e => setName(e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.inputContainer}>
|
<div className={styles.inputContainer}>
|
||||||
<h2>Description</h2>
|
<h2>Description</h2>
|
||||||
<TextInput placeholder={"Description de l'événement"} value={description} onChange={e => setDescription(e.target.value)} />
|
<TextInput className={styles.input} placeholder={"Description de l'événement"} value={description} onChange={e => setDescription(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.inputContainer}>
|
||||||
|
<h2>Date</h2>
|
||||||
|
<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>
|
</div>
|
||||||
|
|
||||||
<Button onClick={handleCreateEvent} className={styles.create}>Créer</Button>
|
<Button onClick={handleCreateEvent} className={styles.create}>Créer</Button>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -26,6 +26,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
margin: 1em;
|
margin: 1em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
width: 70%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.create {
|
.create {
|
||||||
@@ -39,3 +47,13 @@
|
|||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dateInputContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dateInput {
|
||||||
|
margin: 1em;
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import { useState, useMemo, useEffect } from "react";
|
||||||
|
import styles from "./calendar.module.css";
|
||||||
|
import { formatDateLetterJS } from "../../utils/date/formatDateLetter.js";
|
||||||
|
|
||||||
|
function Calendar({ events = [] }) {
|
||||||
|
|
||||||
|
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] = useState(today.getFullYear());
|
||||||
|
const [selectedDay, setSelectedDay] = useState(null);
|
||||||
|
|
||||||
|
const eventsByDate = useMemo(() => {
|
||||||
|
return events.reduce((acc, ev) => {
|
||||||
|
if (!ev?.start) return acc;
|
||||||
|
|
||||||
|
const dateKey = ev.start.split(" ")[0]; // YYYY-MM-DD
|
||||||
|
if (!acc[dateKey]) acc[dateKey] = [];
|
||||||
|
acc[dateKey].push(ev);
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}, [events]);
|
||||||
|
|
||||||
|
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}>
|
||||||
|
<div className={styles.calendarContainer}>
|
||||||
|
|
||||||
|
<div className={styles.calendarHeader}>
|
||||||
|
<button onClick={() => setCurrentMonth(m => m === 0 ? 11 : m - 1)}>◀</button>
|
||||||
|
<p>{monthNames[currentMonth]} {currentYear}</p>
|
||||||
|
<button onClick={() => setCurrentMonth(m => m === 11 ? 0 : 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}>
|
||||||
|
<h3>Événements du {formatDateLetterJS(selectedDay.date)}</h3>
|
||||||
|
|
||||||
|
{selectedDay.events.length > 0 ? (
|
||||||
|
<ul>
|
||||||
|
{selectedDay.events.map(ev => (
|
||||||
|
<li key={ev.id}>
|
||||||
|
<strong>{ev.name}</strong><br />
|
||||||
|
{ev.description}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<p>Aucun événement ce jour-là.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Calendar;
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
.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%;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-top: 16px;
|
||||||
|
border: none;
|
||||||
|
background: rgb(255, 255, 255, 0.25);
|
||||||
|
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 ul {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eventGroup{
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-top: 16px;
|
||||||
|
border: none;
|
||||||
|
background: rgb(255, 255, 255, 0.25);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glassCard {
|
||||||
|
width: 45%;
|
||||||
|
max-height: 450px;
|
||||||
|
margin: 16px 0 16px 0;
|
||||||
|
padding: 10px 20px 10px 20px;
|
||||||
|
height: fit-content;
|
||||||
|
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);
|
||||||
|
overflow: scroll;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.glassCard::-webkit-scrollbar{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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,47 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import styles from "./eventList.module.css";
|
||||||
|
import { formatDateLetter } from "../../utils/date/formatDateLetter.js";
|
||||||
|
|
||||||
|
|
||||||
|
function EventList({ events }) {
|
||||||
|
|
||||||
|
const sortedEvents = useMemo(() => {
|
||||||
|
return [...events].sort(
|
||||||
|
(a, b) => new Date(a.start) - new Date(b.start)
|
||||||
|
);
|
||||||
|
}, [events]);
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.glassCard}>
|
||||||
|
<h2>Liste des événements à venir</h2>
|
||||||
|
{sortedEvents.length > 0 ? (
|
||||||
|
<ul className={styles.eventList}>
|
||||||
|
{sortedEvents.map((eventGroup, index) => (
|
||||||
|
|
||||||
|
<li key={index} className={styles.eventGroup}>
|
||||||
|
<div className={styles.eventTitle}>
|
||||||
|
<p className={styles.title}>{eventGroup.name}</p>
|
||||||
|
<p>{formatDateLetter(eventGroup.start)} - {formatDateLetter(eventGroup.end)}</p>
|
||||||
|
</div>
|
||||||
|
<ul className={styles.eventDetails}>
|
||||||
|
{eventGroup.tasks.map((task, i) => (
|
||||||
|
<li key={i} className={styles.eventTasks}>
|
||||||
|
<p>- {task.name} - {task.description}</p>
|
||||||
|
<p>{formatDateLetter(task.start)} - {formatDateLetter(task.end)}</p>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<p>Aucun événement planifié pour l'instant.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EventList;
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
.glassCard {
|
||||||
|
width: 45%;
|
||||||
|
max-height: 450px;
|
||||||
|
margin: 16px 0 16px 0;
|
||||||
|
padding: 10px 20px 10px 20px;
|
||||||
|
height: fit-content;
|
||||||
|
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);
|
||||||
|
overflow: scroll;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
padding: 10px 20px 10px 20px;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow:
|
||||||
|
inset 1.25px 1.25px 1px rgba(255, 255, 255, 1),
|
||||||
|
inset -1.25px -1.25px 1px rgba(255, 255, 255, 1),
|
||||||
|
inset -0.25px 0.25px 1px rgba(0, 0, 0, 0.2),
|
||||||
|
inset 0.25px -0.25px 1px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eventTitle {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eventTasks {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.glassCard {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
max-height: 450px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
input {
|
input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
padding: 0px 10px;
|
padding: 0 10px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
font-size: 19px;
|
font-size: 19px;
|
||||||
@@ -35,7 +35,6 @@ input::placeholder{
|
|||||||
padding: 15px;
|
padding: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* style scoped (module) */
|
|
||||||
.loginButton {
|
.loginButton {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
|
|||||||
+5
-26
@@ -52,36 +52,15 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.glassCard {
|
|
||||||
width: 100%;
|
|
||||||
height: fit-content;
|
|
||||||
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);
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.glassBorder{
|
.glassCard {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
margin-top: 16px;
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
border: none;
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
background: rgb(255, 255, 255, 0.25);
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 3.2em;
|
font-size: 3.2em;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ function Layout(){
|
|||||||
|
|
||||||
if (loading) return <p>Loading</p>;
|
if (loading) return <p>Loading</p>;
|
||||||
if (!user) return <Navigate to="/login" />;
|
if (!user) return <Navigate to="/login" />;
|
||||||
|
if(user && user.validate === 0) return <Navigate to="/error/validation" />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { useState, useContext } from "react";
|
||||||
|
import styles from "./AdminPage.module.css";
|
||||||
|
import ParticipationChart from "../../components/chart/ParticipationChart.jsx";
|
||||||
|
import IncompleteEvents from "../../components/IncompleteEvent/IncompleteEvent.jsx";
|
||||||
|
import PendingMembers from "../../components/PendingMembers/PendingMembers.jsx";
|
||||||
|
import { AuthContext } from "../../contexts/auth/AuthContext.js";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
|
||||||
|
|
||||||
|
function AdminPage() {
|
||||||
|
|
||||||
|
const { user } = useContext(AuthContext);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [selected, setSelected] = useState("3");
|
||||||
|
const [participationRate] = useState(89);
|
||||||
|
|
||||||
|
if(!user.isAdmin) navigate("/");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<ParticipationChart
|
||||||
|
selected={selected}
|
||||||
|
setSelected={setSelected}
|
||||||
|
participationRate={participationRate}
|
||||||
|
/>
|
||||||
|
<div className={styles.rightSection}>
|
||||||
|
<IncompleteEvents />
|
||||||
|
<PendingMembers />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AdminPage;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rightSection {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
-168
@@ -1,179 +1,24 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { useMemo } from "react";
|
|
||||||
import styles from "./HomePage.module.css";
|
import styles from "./HomePage.module.css";
|
||||||
|
import Calendar from "../../components/homePage/calendar.jsx";
|
||||||
|
import EventList from "../../components/homePage/eventList.jsx";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import getAllEvents from "../../utils/events/getAllEvents.js";
|
||||||
|
|
||||||
function HomePage() {
|
function HomePage() {
|
||||||
const events = {
|
|
||||||
"2025-11-07": ["Réunion de projet", "Anniversaire de Marie"],
|
|
||||||
"2025-11-10": ["Cours de React", "Rendez-vous médecin"],
|
|
||||||
"2025-11-14": ["Sortie cinéma"],
|
|
||||||
};
|
|
||||||
|
|
||||||
const monthNames = [
|
const [events, setEvents] = useState([]);
|
||||||
"Janvier", "Février", "Mars", "Avril", "Mai", "Juin",
|
|
||||||
"Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"
|
|
||||||
];
|
|
||||||
|
|
||||||
const today = new Date();
|
useEffect(() => {
|
||||||
const [currentMonth, setCurrentMonth] = useState(today.getMonth());
|
(async () => {
|
||||||
const [currentYear, setCurrentYear] = useState(today.getFullYear());
|
const data = await getAllEvents();
|
||||||
const [selectedDay, setSelectedDay] = useState(() => {
|
setEvents(data.data);
|
||||||
const year = today.getFullYear();
|
}) ()
|
||||||
const month = String(today.getMonth() + 1).padStart(2, "0");
|
}, [])
|
||||||
const day = String(today.getDate()).padStart(2, "0");
|
|
||||||
const key = `${year}-${month}-${day}`;
|
|
||||||
return { date: key, events: events[key] || [] };
|
|
||||||
});
|
|
||||||
|
|
||||||
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 handlePrevMonth = () => {
|
|
||||||
let newMonth = currentMonth - 1;
|
|
||||||
let newYear = currentYear;
|
|
||||||
|
|
||||||
if (newMonth < 0) {
|
|
||||||
newMonth = 11;
|
|
||||||
newYear--;
|
|
||||||
}
|
|
||||||
|
|
||||||
setCurrentMonth(newMonth);
|
|
||||||
setCurrentYear(newYear);
|
|
||||||
|
|
||||||
const key = `${newYear}-${String(newMonth + 1).padStart(2, "0")}-01`;
|
|
||||||
setSelectedDay({ date: key, events: events[key] || [] });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNextMonth = () => {
|
|
||||||
let newMonth = currentMonth + 1;
|
|
||||||
let newYear = currentYear;
|
|
||||||
|
|
||||||
if (newMonth > 11) {
|
|
||||||
newMonth = 0;
|
|
||||||
newYear++;
|
|
||||||
}
|
|
||||||
|
|
||||||
setCurrentMonth(newMonth);
|
|
||||||
setCurrentYear(newYear);
|
|
||||||
|
|
||||||
const key = `${newYear}-${String(newMonth + 1).padStart(2, "0")}-01`;
|
|
||||||
setSelectedDay({ date: key, events: events[key] || [] });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDayClick = (day) => {
|
|
||||||
if (!day) return;
|
|
||||||
const key = `${currentYear}-${String(currentMonth + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
|
||||||
setSelectedDay({ date: key, events: events[key] || [] });
|
|
||||||
};
|
|
||||||
|
|
||||||
const sortedEvents = useMemo(() => {
|
|
||||||
return Object.entries(events)
|
|
||||||
.map(([dateKey, eventList]) => ({ date: dateKey, events: eventList }))
|
|
||||||
.sort((a, b) => new Date(a.date) - new Date(b.date));
|
|
||||||
}, [events]);
|
|
||||||
|
|
||||||
const formatDate = (dateString) => {
|
|
||||||
const [year, month, day] = dateString.split('-');
|
|
||||||
return `${day} ${monthNames[parseInt(month) - 1]} ${year}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.allContent}>
|
<div className={styles.allContent}>
|
||||||
<div className={styles.glassCard}>
|
<Calendar events={events} />
|
||||||
<div className={styles.calendarContainer}>
|
<EventList events={events} />
|
||||||
<div className={styles.calendarHeader}>
|
|
||||||
<button onClick={handlePrevMonth}>◀</button>
|
|
||||||
<p>{monthNames[currentMonth]} {currentYear}</p>
|
|
||||||
<button onClick={handleNextMonth}>▶</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((_, weekIndex) => (
|
|
||||||
<tr key={weekIndex}>
|
|
||||||
{days.slice(weekIndex * 7, weekIndex * 7 + 7).map((day, index) => {
|
|
||||||
const key = `${currentYear}-${String(currentMonth + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
|
||||||
const hasEvent = events[key];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<td
|
|
||||||
key={index}
|
|
||||||
className={[
|
|
||||||
day === today.getDate() &&
|
|
||||||
currentMonth === today.getMonth() &&
|
|
||||||
currentYear === today.getFullYear()
|
|
||||||
? styles.today : "",
|
|
||||||
hasEvent ? styles.hasEvent : "",
|
|
||||||
selectedDay?.date === key ? styles.selected : ""
|
|
||||||
].join(" ")}
|
|
||||||
onClick={() => handleDayClick(day)}
|
|
||||||
>
|
|
||||||
{day || ""}
|
|
||||||
</td>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
{selectedDay && (
|
|
||||||
<div className={styles.eventBox}>
|
|
||||||
<h3>Événements du {formatDate(selectedDay.date)}</h3>
|
|
||||||
|
|
||||||
{selectedDay.events.length > 0 ? (
|
|
||||||
<ul>
|
|
||||||
{selectedDay.events.map((ev, i) => (
|
|
||||||
<li key={i}>{ev}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
) : (
|
|
||||||
<p>Aucun événement ce jour-là.</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={styles.glassCard}>
|
|
||||||
<div className={styles.eventListContainer}>
|
|
||||||
<h2>Liste des Événements à Venir</h2>
|
|
||||||
{sortedEvents.length > 0 ? (
|
|
||||||
<ul className={styles.eventList}>
|
|
||||||
{sortedEvents.map((eventGroup) => (
|
|
||||||
<li key={eventGroup.date} className={styles.eventGroup}>
|
|
||||||
<div className={styles.eventDate}>
|
|
||||||
<strong>{formatDate(eventGroup.date)}</strong>
|
|
||||||
</div>
|
|
||||||
<ul className={styles.eventDetails}>
|
|
||||||
{eventGroup.events.map((event, i) => (
|
|
||||||
<li key={i}>- {event}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
) : (
|
|
||||||
<p>Aucun événement planifié pour l'instant.</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,173 +1,14 @@
|
|||||||
.calendarContainer {
|
|
||||||
padding: 20px;
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
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 {
|
|
||||||
border-radius: 20px;
|
|
||||||
padding: 15px;
|
|
||||||
margin-top: 16px;
|
|
||||||
border: none;
|
|
||||||
background: rgb(255, 255, 255, 0.25);
|
|
||||||
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 ul {
|
|
||||||
list-style-type: disc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.eventGroup{
|
|
||||||
border-radius: 20px;
|
|
||||||
padding: 15px;
|
|
||||||
margin-top: 16px;
|
|
||||||
border: none;
|
|
||||||
background: rgb(255, 255, 255, 0.25);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
.glassCard {
|
|
||||||
width: 45%;
|
|
||||||
margin: 16px;
|
|
||||||
padding: 10px 20px 10px 20px;
|
|
||||||
height: fit-content;
|
|
||||||
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);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.allContent{
|
.allContent{
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.glassCard {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0px;
|
|
||||||
margin-top: 16px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.allContent{
|
.allContent{
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
gap: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,11 +10,16 @@ function LoginPage() {
|
|||||||
const { user, loading } = useContext(AuthContext)
|
const { user, loading } = useContext(AuthContext)
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
console.log("loading" + loading);
|
||||||
|
console.log("user" + user);
|
||||||
|
|
||||||
if (!loading && user) {
|
if (!loading && user) {
|
||||||
navigate("/");
|
navigate("/");
|
||||||
}
|
}
|
||||||
}, [loading, user]);
|
}, [loading, navigate, user]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Background>
|
<Background>
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import Background from "../../components/background/background.jsx";
|
||||||
|
import styles from "./PageNotFound.module.css"
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
|
||||||
|
export default function PageNotFound() {
|
||||||
|
|
||||||
|
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 : contact@example.com </p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Background>
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -8,7 +8,6 @@ import SettingsModal from "../../components/SettingsModal/SettingsModal.jsx";
|
|||||||
function ProfilePage() {
|
function ProfilePage() {
|
||||||
|
|
||||||
const { user } = useContext(AuthContext);
|
const { user } = useContext(AuthContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={`${styles.profileboard} glassCard`}>
|
<div className={`${styles.profileboard} glassCard`}>
|
||||||
@@ -31,12 +30,12 @@ function ProfilePage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.infoBlock}>
|
<div className={styles.infoBlock}>
|
||||||
<p><strong>Mail :</strong> {user.email} </p>
|
<p><strong>Mail :</strong> {user.email} </p>
|
||||||
<p><strong>Téléphone :</strong> {user.phone}</p>
|
<p><strong>Téléphone :</strong> {user.phone === null ? "Pas de numéro enregistré" : user.phone}</p>
|
||||||
</div>
|
</div>
|
||||||
<SettingsModal />
|
<SettingsModal />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>Vos Événements :</h2>
|
<h2>Tâches :</h2>
|
||||||
{user.tasks && user.tasks.length > 0 ? (
|
{user.tasks && user.tasks.length > 0 ? (
|
||||||
user.tasks.map((task, index) => (
|
user.tasks.map((task, index) => (
|
||||||
<Task key={index} task={task} />
|
<Task key={index} task={task} />
|
||||||
|
|||||||
@@ -131,7 +131,6 @@
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Desktop (1024px et plus) */
|
|
||||||
@media screen and (min-width: 1024px) {
|
@media screen and (min-width: 1024px) {
|
||||||
.container {
|
.container {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|||||||
@@ -1,157 +0,0 @@
|
|||||||
import React, { useState, useEffect, useContext } from "react";
|
|
||||||
import { Doughnut } from "react-chartjs-2";
|
|
||||||
import { Chart, ArcElement } from "chart.js";
|
|
||||||
import styles from "./StatPage.module.css";
|
|
||||||
import Button from "./../../components/ui/button/button";
|
|
||||||
import { AuthContext } from "../../contexts/auth/AuthContext.js";
|
|
||||||
import { useNavigate } from "react-router";
|
|
||||||
|
|
||||||
// Enregistrer les éléments nécessaires
|
|
||||||
Chart.register(ArcElement);
|
|
||||||
|
|
||||||
function StatPage() {
|
|
||||||
|
|
||||||
const { user } = useContext(AuthContext);
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const [months, setMonths] = useState(3);
|
|
||||||
const [participationRate, setParticipationRate] = useState(89);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if(!user.isAdmin) navigate("/")
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const incompleteEvents = [
|
|
||||||
{ title: "Événement A", tasks: "Préparation des flyers" },
|
|
||||||
{ title: "Événement B", tasks: "Réservation de la salle" },
|
|
||||||
{ title: "Événement C", tasks: "Communication sur les réseaux" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const pendingMembers = [
|
|
||||||
{ name: "Antoine Ordonneau" },
|
|
||||||
{ name: "Giovanni Josserand" },
|
|
||||||
{ name: "Quentin T'Jampens" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const handleValidate = (name) => {
|
|
||||||
console.log(`Valider ${name}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleReject = (name) => {
|
|
||||||
console.log(`Refuser ${name}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const chartData = {
|
|
||||||
labels: ["Progress", "Background"],
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
data: [participationRate, 100 - participationRate],
|
|
||||||
backgroundColor: ["#1e60f9ff", "#ffffffff"],
|
|
||||||
borderWidth: 0,
|
|
||||||
weight:10,
|
|
||||||
cutout: "90%",
|
|
||||||
circumference: 360,
|
|
||||||
rotation: -90,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const chartOptions = {
|
|
||||||
responsive: true,
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
animation: {
|
|
||||||
animateScale: true,
|
|
||||||
animateRotate: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.container}>
|
|
||||||
<div className={`${styles.leftBlock} glassCard`}>
|
|
||||||
<div className={styles.participationHeader}>
|
|
||||||
<div className={styles.partiText}>
|
|
||||||
<label htmlFor="months">Taux de participation </label>
|
|
||||||
<label htmlFor="months2">depuis : </label>
|
|
||||||
|
|
||||||
<select
|
|
||||||
id="months"
|
|
||||||
value={months}
|
|
||||||
onChange={(e) => setMonths(e.target.value)}
|
|
||||||
className={styles.monthsDropdown}
|
|
||||||
>
|
|
||||||
<option value="1">1 mois</option>
|
|
||||||
<option value="3">3 mois</option>
|
|
||||||
<option value="6">6 mois</option>
|
|
||||||
<option value="12">12 mois</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div className={styles.chartContainer}>
|
|
||||||
<div style={{ position: "relative", width: "100%", height: "100%" }}>
|
|
||||||
<Doughnut data={chartData} options={chartOptions} />
|
|
||||||
<div style={{
|
|
||||||
position: "absolute",
|
|
||||||
top: "50%",
|
|
||||||
left: "50%",
|
|
||||||
textAlign:"center",
|
|
||||||
transform: "translate(-50%, -50%)",
|
|
||||||
fontSize: "60px",
|
|
||||||
fontWeight: "bold",
|
|
||||||
color: "#333",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{participationRate}%
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={styles.rightSection}>
|
|
||||||
<div className={`${styles.rightBlock} glassCard`}>
|
|
||||||
<h2>Événements incomplets</h2>
|
|
||||||
<div className={styles.eventsContainer}>
|
|
||||||
{incompleteEvents.map((event, index) => (
|
|
||||||
<div key={index} className={`glassCard ${styles.eventCard}`}>
|
|
||||||
<h3>{event.title}</h3>
|
|
||||||
<p>{event.tasks}</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={`${styles.rightBlock} glassCard`}>
|
|
||||||
<h2>Membres en attente de validation</h2>
|
|
||||||
<div className={styles.membersContainer}>
|
|
||||||
{pendingMembers.map((member, index) => (
|
|
||||||
<div key={index} className={`glassCard ${styles.memberCard}`}>
|
|
||||||
<p>{member.name}</p>
|
|
||||||
<div className={styles.buttonGroup}>
|
|
||||||
<Button
|
|
||||||
onClick={() => handleValidate(member.name)}
|
|
||||||
variant="primary"
|
|
||||||
>
|
|
||||||
Valider
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => handleReject(member.name)}
|
|
||||||
variant="danger"
|
|
||||||
>
|
|
||||||
Refuser
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default StatPage;
|
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
width: 100%;
|
|
||||||
padding: 20px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
/* style a montrer a giovanni prcq il est + bo */
|
|
||||||
/*.glassCard {
|
|
||||||
background: rgba(255, 255, 255, 0.2);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 15px;
|
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
||||||
}*/
|
|
||||||
|
|
||||||
.block {
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 15px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leftBlock {
|
|
||||||
width: 40%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
height:fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.participationHeader {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.partiText{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.participationHeader label{
|
|
||||||
font-size:20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.monthsDropdown {
|
|
||||||
width:25%;
|
|
||||||
border-radius: 5px;
|
|
||||||
opacity: 1;
|
|
||||||
background-color: transparent;
|
|
||||||
border:none;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.chartContainer {
|
|
||||||
|
|
||||||
max-width: 80%;
|
|
||||||
margin: 0 auto;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.circularProgress {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progressBackground {
|
|
||||||
opacity: 0.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progressBar {
|
|
||||||
transition: stroke-dasharray 0.5s ease;
|
|
||||||
stroke-linecap: round;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progressText {
|
|
||||||
font-weight: bold;
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.rightSection {
|
|
||||||
width: 60%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rightBlock {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.rightBlock>h2{
|
|
||||||
font-size:29px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.eventsContainer {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.eventCard {
|
|
||||||
flex: 1;
|
|
||||||
padding: 10px;
|
|
||||||
text-align: center;
|
|
||||||
padding-bottom:200px;
|
|
||||||
}
|
|
||||||
.eventCard>h3{
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
.membersContainer {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.memberCard {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
.memberCard p{
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.buttonGroup {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
justify-content: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@media screen and (max-width: 760px) {
|
|
||||||
.container {
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leftBlock {
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
height:fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rightSection {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.eventsContainer {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.eventCard {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.donutChart {
|
|
||||||
width: 120px;
|
|
||||||
height: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.memberCard {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 10px;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttonGroup {
|
|
||||||
width: 100%;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.validateButton,
|
|
||||||
.rejectButton {
|
|
||||||
flex: 1;
|
|
||||||
text-align: center;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import styles from "./VolunteerProfilePage.module.css"
|
||||||
|
import { useParams } from "react-router";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import getUserById from "../../utils/users/getUserById.js";
|
||||||
|
import formatDate from "../../utils/date/formatDate.js";
|
||||||
|
import Task from "../../components/Task/Task.jsx";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
|
||||||
|
export default function VolunteerProfilePage() {
|
||||||
|
|
||||||
|
const params = useParams();
|
||||||
|
const id = params.id;
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [user, setUser] = useState({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const result = await getUserById(id);
|
||||||
|
if (result?.error === 'not_found') {
|
||||||
|
navigate("/404");
|
||||||
|
} else {
|
||||||
|
setUser(result);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return <div className={styles.container}>
|
||||||
|
<div className={`${styles.profileboard} glassCard`}>
|
||||||
|
<div className={styles.contentWrapper}>
|
||||||
|
<div className={styles.profilePictureContainer}>
|
||||||
|
<img
|
||||||
|
src="/react.svg"
|
||||||
|
alt="Photo de profil"
|
||||||
|
className={styles.profilePicture}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.description}>
|
||||||
|
<div className={styles.headerProfile}>
|
||||||
|
<h2> {user.name} {user.lastname} </h2>
|
||||||
|
</div>
|
||||||
|
<div className={styles.topDescription}>
|
||||||
|
<div className={styles.infoBlock}>
|
||||||
|
<p><strong>Role :</strong> {user.role}</p>
|
||||||
|
<p><strong>Membre depuis :</strong> {formatDate(user.created_at)}</p>
|
||||||
|
</div>
|
||||||
|
<div className={styles.infoBlock}>
|
||||||
|
<p><strong>Mail :</strong> {user.email} </p>
|
||||||
|
<p><strong>Téléphone :</strong> {user.phone === null ? "Pas de numéro enregistré" : user.phone}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Tâches :</h2>
|
||||||
|
{user.tasks && user.tasks.length > 0 ? (
|
||||||
|
user.tasks.map((task, index) => (
|
||||||
|
<Task key={index} task={task} />
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p>Aucune tâche pour le moment.</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profileboard {
|
||||||
|
width: 100%;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentWrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profilePictureContainer {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profilePicture {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
margin-left:4%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topDescription {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoBlock {
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eventTasks {
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 0.3s ease-out, opacity 0.2s ease;
|
||||||
|
opacity: 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eventTasks.expanded {
|
||||||
|
max-height: 500px;
|
||||||
|
opacity: 1;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.description h2{
|
||||||
|
font-size:30px;
|
||||||
|
|
||||||
|
}
|
||||||
|
.profilePicture{
|
||||||
|
width:90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settingsBtns {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
justify-content: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
.contentWrapper {
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profilePictureContainer {
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
margin-right: 20px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
width: calc(100% - 170px);
|
||||||
|
margin-left:4%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topDescription {
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoBlock {
|
||||||
|
width: 48%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
.description h2{
|
||||||
|
font-size:30px;
|
||||||
|
}
|
||||||
|
.profilePicture{
|
||||||
|
width:90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.description button{
|
||||||
|
width:50px;
|
||||||
|
height:50px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerProfile {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1024px) {
|
||||||
|
.container {
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profileboard {
|
||||||
|
padding: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profilePictureContainer {
|
||||||
|
width: 30%;
|
||||||
|
height:auto;
|
||||||
|
}
|
||||||
|
.profilePicture{
|
||||||
|
width:90%;
|
||||||
|
}
|
||||||
|
.description {
|
||||||
|
width: calc(100% - 200px);
|
||||||
|
margin-left:4%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event {
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
.description h2{
|
||||||
|
font-size:30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import styles from "./validationErrorPage.module.css"
|
||||||
|
import Background from "../../components/background/background.jsx";
|
||||||
|
import Button from "../../components/ui/button/button.jsx";
|
||||||
|
import { useContext } from "react";
|
||||||
|
import { AuthContext } from "../../contexts/auth/AuthContext.js";
|
||||||
|
import { Navigate, useNavigate } from "react-router";
|
||||||
|
|
||||||
|
export default function ValidationErrorPage() {
|
||||||
|
|
||||||
|
const { logout, user } = useContext(AuthContext);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
await logout();
|
||||||
|
navigate("/login");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(user && user.validate === 1) return <Navigate to="/" />;
|
||||||
|
|
||||||
|
return <Background>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<h1 className={styles.title}> Votre compte n'a pas été validé </h1>
|
||||||
|
<div className={`${styles.container} glassCard`}>
|
||||||
|
<p> Votre compte n'a pas encore été validé, veuillez attendre la vérification de votre compte par un membre autorisé. </p>
|
||||||
|
<p> Contact : contact@example.com </p>
|
||||||
|
<Button variant={"danger"} onClick={handleLogout}> Déconnexion </Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Background>
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
-2
@@ -2,11 +2,14 @@ import { createBrowserRouter } from "react-router";
|
|||||||
import Layout from "./layout.jsx";
|
import Layout from "./layout.jsx";
|
||||||
import HomePage from "./pages/Home/HomePage";
|
import HomePage from "./pages/Home/HomePage";
|
||||||
import LoginPage from "./pages/Login/LoginPage";
|
import LoginPage from "./pages/Login/LoginPage";
|
||||||
import StatPage from "./pages/Stat/StatPage";
|
|
||||||
import RegisterPage from "./pages/Register/RegisterPage.jsx";
|
import RegisterPage from "./pages/Register/RegisterPage.jsx";
|
||||||
import VolunteersPage from "./pages/Volunteers/VolunteersPage";
|
import VolunteersPage from "./pages/Volunteers/VolunteersPage";
|
||||||
import ProfilePage from "./pages/Profile/ProfilePage";
|
import ProfilePage from "./pages/Profile/ProfilePage";
|
||||||
import EventsPage from "./pages/Events/EventsPage.jsx";
|
import EventsPage from "./pages/Events/EventsPage.jsx";
|
||||||
|
import AdminPage from "./pages/Admin/AdminPage.jsx";
|
||||||
|
import ValidationErrorPage from "./pages/waitValidationPage/ValidationErrorPage.jsx";
|
||||||
|
import VolunteerProfilePage from "./pages/VolunteerProfile/VolunteerProfilePage.jsx";
|
||||||
|
import PageNotFound from "./pages/PageNotFound/PageNotFound.jsx";
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@@ -17,6 +20,18 @@ const router = createBrowserRouter([
|
|||||||
path: "/register",
|
path: "/register",
|
||||||
Component: RegisterPage,
|
Component: RegisterPage,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/error/validation",
|
||||||
|
Component: ValidationErrorPage
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/*",
|
||||||
|
Component: PageNotFound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/404",
|
||||||
|
Component: PageNotFound,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
Component: Layout,
|
Component: Layout,
|
||||||
@@ -27,7 +42,7 @@ const router = createBrowserRouter([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/admin",
|
path: "/admin",
|
||||||
Component:StatPage,
|
Component:AdminPage,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path:"/profile",
|
path:"/profile",
|
||||||
@@ -40,6 +55,10 @@ const router = createBrowserRouter([
|
|||||||
{
|
{
|
||||||
path: "/volunteers",
|
path: "/volunteers",
|
||||||
Component: VolunteersPage,
|
Component: VolunteersPage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/profile/:id",
|
||||||
|
Component: VolunteerProfilePage,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import ThemeSwitcher from "../components/ui/themeSwitcher/ThemeSwitcher.jsx";
|
||||||
|
import Background from "../components/background/background.jsx";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'UI/ThemeSwitcher',
|
||||||
|
component: ThemeSwitcher,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const ThemeSwitcherRender = () => {
|
||||||
|
return <Background>
|
||||||
|
<ThemeSwitcher />
|
||||||
|
</Background>
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Default = () => <ThemeSwitcherRender />;
|
||||||
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
export function formatDateLetter(d) {
|
||||||
|
const monthNames = [
|
||||||
|
"Janvier", "Février", "Mars", "Avril", "Mai", "Juin",
|
||||||
|
"Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"
|
||||||
|
];
|
||||||
|
|
||||||
|
const [datePart, timePart] = d.split(' ');
|
||||||
|
const [year, month, day] = datePart.split('-');
|
||||||
|
const [hours, minutes] = timePart.split(':');
|
||||||
|
|
||||||
|
return `${day} ${monthNames[parseInt(month, 10) - 1]} ${year} à ${hours}:${minutes}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function formatDateLetterJS(d) {
|
||||||
|
const monthNames = [
|
||||||
|
"Janvier", "Février", "Mars", "Avril", "Mai", "Juin",
|
||||||
|
"Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"
|
||||||
|
];
|
||||||
|
const [year, month, day] = d.split('-');
|
||||||
|
return `${day} ${monthNames[parseInt(month) - 1]} ${year}`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { expect, test } from 'vitest';
|
||||||
|
import formatDateLetter from './formatDateLetter';
|
||||||
|
|
||||||
|
const date = "2025-11-07"
|
||||||
|
const res = "07 Novembre 2025"
|
||||||
|
|
||||||
|
test("Format Date Letter", () => {
|
||||||
|
expect(formatDateLetter(date)).toBe(res);
|
||||||
|
})
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import getXSRFToken from "../getXSRF.js";
|
import getXSRFToken from "../getXSRF.js";
|
||||||
|
|
||||||
export default async function createEvent(name, description) {
|
export default async function createEvent(name, description, start, end) {
|
||||||
const csrfToken = await getXSRFToken();
|
const csrfToken = await getXSRFToken();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -12,7 +12,7 @@ export default async function createEvent(name, description) {
|
|||||||
'X-XSRF-TOKEN': csrfToken,
|
'X-XSRF-TOKEN': csrfToken,
|
||||||
'Accept': 'application/json'
|
'Accept': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ name, description }),
|
body: JSON.stringify({ name, description, start, end }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|||||||
@@ -9,12 +9,15 @@ export default async function getUserById (id) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
if (res.status === 404) {
|
||||||
|
return { error: 'not_found' };
|
||||||
|
}
|
||||||
throw new Error(`Erreur serveur : ${res.status}`);
|
throw new Error(`Erreur serveur : ${res.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await res.json();
|
return await res.json();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Erreur :', err);
|
console.error('Erreur :', err);
|
||||||
return null;
|
return { error: 'server_error' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user