Merge branch 'dev' into 'features/searchBar'

# Conflicts:
#   src/components/Event/Event.jsx
#   src/index.css
#   src/pages/Events/EventsPage.jsx
#   src/utils/getUser.js
#   src/utils/getXSRF.js
#   src/utils/login.js
#   src/utils/register.js
This commit is contained in:
JOSSERAND GIOVANNI p2405212 committed 2025-12-07 15:43:03 +00:00
commit c233d4cda3
49 files changed
+1146 -581

No files matched your search

+1
View File
@@ -0,0 +1 @@
VITE_API_URL=
+2
View File
@@ -25,3 +25,5 @@ dist-ssr
*storybook.log *storybook.log
storybook-static storybook-static
.env
+2
View File
@@ -8,7 +8,9 @@
"name": "sae-front", "name": "sae-front",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"chart.js": "^4.5.1",
"react": "^19.1.1", "react": "^19.1.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"
}, },
+8 -5
View File
@@ -9,10 +9,13 @@
"lint": "eslint .", "lint": "eslint .",
"preview": "vite preview", "preview": "vite preview",
"storybook": "storybook dev -p 6006", "storybook": "storybook dev -p 6006",
"build-storybook": "storybook build" "build-storybook": "storybook build",
"test": "vitest"
}, },
"dependencies": { "dependencies": {
"chart.js": "^4.5.1",
"react": "^19.1.1", "react": "^19.1.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"
}, },
@@ -26,17 +29,17 @@
"@types/react": "^19.1.16", "@types/react": "^19.1.16",
"@types/react-dom": "^19.1.9", "@types/react-dom": "^19.1.9",
"@vitejs/plugin-react": "^5.0.4", "@vitejs/plugin-react": "^5.0.4",
"@vitest/browser-playwright": "^4.0.8",
"@vitest/coverage-v8": "^4.0.8",
"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",
"eslint-plugin-storybook": "^10.0.6", "eslint-plugin-storybook": "^10.0.6",
"globals": "^16.4.0", "globals": "^16.4.0",
"playwright": "^1.56.1",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"storybook": "^10.0.6", "storybook": "^10.0.6",
"vite": "^7.1.7", "vite": "^7.1.7",
"vitest": "^4.0.8", "vitest": "^4.0.8"
"playwright": "^1.56.1",
"@vitest/browser-playwright": "^4.0.8",
"@vitest/coverage-v8": "^4.0.8"
} }
} }
+30
View File
@@ -8,9 +8,15 @@ importers:
.: .:
dependencies: dependencies:
chart.js:
specifier: ^4.5.1
version: 4.5.1
react: react:
specifier: ^19.1.1 specifier: ^19.1.1
version: 19.2.0 version: 19.2.0
react-chartjs-2:
specifier: ^5.3.1
version: 5.3.1(chart.js@4.5.1)(react@19.2.0)
react-dom: react-dom:
specifier: ^19.1.1 specifier: ^19.1.1
version: 19.2.0(react@19.2.0) version: 19.2.0(react@19.2.0)
@@ -436,6 +442,9 @@ packages:
'@jridgewell/trace-mapping@0.3.31': '@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@kurkle/color@0.3.4':
resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==}
'@mdx-js/react@3.1.1': '@mdx-js/react@3.1.1':
resolution: {integrity: sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==} resolution: {integrity: sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==}
peerDependencies: peerDependencies:
@@ -949,6 +958,10 @@ packages:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'} engines: {node: '>=10'}
chart.js@4.5.1:
resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==}
engines: {pnpm: '>=8'}
check-error@2.1.1: check-error@2.1.1:
resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
engines: {node: '>= 16'} engines: {node: '>= 16'}
@@ -1510,6 +1523,12 @@ packages:
queue-microtask@1.2.3: queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
react-chartjs-2@5.3.1:
resolution: {integrity: sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==}
peerDependencies:
chart.js: ^4.1.1
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-docgen-typescript@2.4.0: react-docgen-typescript@2.4.0:
resolution: {integrity: sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg==} resolution: {integrity: sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg==}
peerDependencies: peerDependencies:
@@ -2186,6 +2205,8 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2 '@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
'@kurkle/color@0.3.4': {}
'@mdx-js/react@3.1.1(@types/react@19.2.2)(react@19.2.0)': '@mdx-js/react@3.1.1(@types/react@19.2.2)(react@19.2.0)':
dependencies: dependencies:
'@types/mdx': 2.0.13 '@types/mdx': 2.0.13
@@ -2738,6 +2759,10 @@ snapshots:
ansi-styles: 4.3.0 ansi-styles: 4.3.0
supports-color: 7.2.0 supports-color: 7.2.0
chart.js@4.5.1:
dependencies:
'@kurkle/color': 0.3.4
check-error@2.1.1: {} check-error@2.1.1: {}
chromatic@12.2.0: {} chromatic@12.2.0: {}
@@ -3255,6 +3280,11 @@ snapshots:
queue-microtask@1.2.3: {} queue-microtask@1.2.3: {}
react-chartjs-2@5.3.1(chart.js@4.5.1)(react@19.2.0):
dependencies:
chart.js: 4.5.1
react: 19.2.0
react-docgen-typescript@2.4.0(typescript@5.9.3): react-docgen-typescript@2.4.0(typescript@5.9.3):
dependencies: dependencies:
typescript: 5.9.3 typescript: 5.9.3
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 4H7.2C6.0799 4 5.51984 4 5.09202 4.21799C4.71569 4.40974 4.40973 4.7157 4.21799 5.09202C4 5.51985 4 6.0799 4 7.2V16.8C4 17.9201 4 18.4802 4.21799 18.908C4.40973 19.2843 4.71569 19.5903 5.09202 19.782C5.51984 20 6.0799 20 7.2 20H16.8C17.9201 20 18.4802 20 18.908 19.782C19.2843 19.5903 19.5903 19.2843 19.782 18.908C20 18.4802 20 17.9201 20 16.8V12.5M15.5 5.5L18.3284 8.32843M10.7627 10.2373L17.411 3.58902C18.192 2.80797 19.4584 2.80797 20.2394 3.58902C21.0205 4.37007 21.0205 5.6364 20.2394 6.41745L13.3774 13.2794C12.6158 14.0411 12.235 14.4219 11.8012 14.7247C11.4162 14.9936 11.0009 15.2162 10.564 15.3882C10.0717 15.582 9.54378 15.6885 8.48793 15.9016L8 16L8.04745 15.6678C8.21536 14.4925 8.29932 13.9048 8.49029 13.3561C8.65975 12.8692 8.89125 12.4063 9.17906 11.9786C9.50341 11.4966 9.92319 11.0768 10.7627 10.2373Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+15 -12
View File
@@ -1,9 +1,12 @@
import styles from "./Event.module.css"; import styles from "./Event.module.css";
import {useState, useEffect} from "react"; import { useState, useEffect } from "react";
import {Link} from "react-router"; import { useNavigate } from "react-router";
import Button from "/src/components/ui/button/button.jsx" import Button from "/src/components/ui/button/button.jsx"
function Event({event}){ function Event({event}){
const navigate = useNavigate();
const [eventMenu, seteventMenu] = useState(false); const [eventMenu, seteventMenu] = useState(false);
const [windowSize, setWindowSize] = useState({ const [windowSize, setWindowSize] = useState({
width: null, width: null,
@@ -31,7 +34,7 @@ function Event({event}){
<div className={styles.eventContent}> <div className={styles.eventContent}>
<div className={`${styles.eventImageDiv}`}> <div className={`${styles.eventImageDiv}`}>
{event.image == null ? ( {event.image == null ? (
<img src="empty.png" className={styles.eventImage} alt="Pas de photo de l'événement" /> <img src="/empty.png" className={styles.eventImage} alt="Pas de photo de l'événement" />
) : ( ) : (
<img src={event.image + `.png`} alt="Photo de l'événement" /> <img src={event.image + `.png`} alt="Photo de l'événement" />
)} )}
@@ -57,21 +60,21 @@ function Event({event}){
</div> </div>
{windowSize.width > 768 ? ( {windowSize.width > 768 ? (
<div className={styles.eventButtons}> <div className={styles.eventButtons}>
<button <Button
className={`${styles.moreButton} ${eventMenu ? styles.moreButtonClicked : ""}`} variant={"transparent"}
onClick={()=>{seteventMenu(!eventMenu)}}> onClick={()=>{seteventMenu(!eventMenu)}}
className={`${eventMenu ? styles.moreButtonClicked : ""}`}
>
{">"} {">"}
</button> </Button>
</div> </div>
) : null} ) : null}
</div> </div>
<div className={styles.eventContainerBottom}> <div className={styles.eventContainerBottom}>
{(windowSize.width <= 768 || eventMenu) ? ( {(windowSize.width <= 768 || eventMenu) ? (
<Link
to={`/event/` + event.id} <Button onClick={() => navigate(`/event/` + event.id)}>Voir plus</Button>
className={styles.moreLink}>
<Button>En savoir plus →</Button>
</Link>
) : null} ) : null}
</div> </div>
</div> </div>
+3 -13
View File
@@ -3,6 +3,7 @@
flex-direction: column; flex-direction: column;
margin: 1rem 0; margin: 1rem 0;
width: 80%; width: 80%;
padding: 1rem 0.5rem;
} }
.eventContainerTop { .eventContainerTop {
@@ -46,11 +47,6 @@
font-size: 2rem; font-size: 2rem;
} }
.eventButtons { .eventButtons {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -64,9 +60,6 @@
white-space: nowrap; white-space: nowrap;
} }
.moreButton { .moreButton {
background: none; background: none;
border: none; border: none;
@@ -95,19 +88,16 @@
rotate: 90deg; rotate: 90deg;
} }
.eventMenuOpen { .eventMenuOpen {
display: flex; display: flex;
flex-direction: column;
margin-left: 1.5em;
} }
.eventMenuClose { .eventMenuClose {
display: none; display: none;
} }
@media screen and (max-width: 768px) { @media screen and (max-width: 768px) {
.eventImage { .eventImage {
width: 100%; width: 100%;
+2 -2
View File
@@ -18,7 +18,7 @@ function Header() {
setNotifications(notifData || []); setNotifications(notifData || []);
} }
loadNotifications(); //loadNotifications();
const handleClickOutside = (event) => { const handleClickOutside = (event) => {
if ( if (
notificationRef.current && notificationRef.current &&
@@ -70,7 +70,7 @@ function Header() {
{unreadNotification && <span className={styles.notificationBadge}></span>} {unreadNotification && <span className={styles.notificationBadge}></span>}
</button> </button>
<Link to="" className={styles.profileLink}> <Link to="/profile" className={styles.profileLink}>
<svg xmlns="http://www.w3.org/2000/svg" height="30px" viewBox="0 -960 960 960" width="auto" fill="currentFill"> <svg xmlns="http://www.w3.org/2000/svg" height="30px" viewBox="0 -960 960 960" width="auto" fill="currentFill">
<path d="M480-492.31q-57.75 0-98.87-41.12Q340-574.56 340-632.31q0-57.75 41.13-98.87 41.12-41.13 98.87-41.13 57.75 0 98.87 41.13Q620-690.06 620-632.31q0 57.75-41.13 98.88-41.12 41.12-98.87 41.12ZM180-187.69v-88.93q0-29.38 15.96-54.42 15.96-25.04 42.66-38.5 59.3-29.07 119.65-43.61 60.35-14.54 121.73-14.54t121.73 14.54q60.35 14.54 119.65 43.61 26.7 13.46 42.66 38.5Q780-306 780-276.62v88.93H180Z" /> <path d="M480-492.31q-57.75 0-98.87-41.12Q340-574.56 340-632.31q0-57.75 41.13-98.87 41.12-41.13 98.87-41.13 57.75 0 98.87 41.13Q620-690.06 620-632.31q0 57.75-41.13 98.88-41.12 41.12-98.87 41.12ZM180-187.69v-88.93q0-29.38 15.96-54.42 15.96-25.04 42.66-38.5 59.3-29.07 119.65-43.61 60.35-14.54 121.73-14.54t121.73 14.54q60.35 14.54 119.65 43.61 26.7 13.46 42.66 38.5Q780-306 780-276.62v88.93H180Z" />
</svg> </svg>
+29
View File
@@ -0,0 +1,29 @@
import React, {useEffect, useState} from "react";
import getEventById from "../../utils/event/getEventById.js";
import styles from "./Task.module.css";
import formatDate from "../../utils/date/formatDate.js";
const Task = ({ task }) => {
const [event, setEvent] = useState(null);
useEffect(() => {
(async () => {
const e = await getEventById(task.id);
setEvent(e);
}) ()
}, [])
return (
<div className={`${styles.task} glassCard`}>
<div className={styles.content}>
<div>
<h2> {event && 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;
+22
View File
@@ -0,0 +1,22 @@
.task {
width: 100%;
padding: 12px;
margin-bottom: 10px;
border-radius: 8px;
}
.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;
}
+21 -4
View File
@@ -1,8 +1,10 @@
import styles from "./background.module.css"; import styles from "./background.module.css";
import { useEffect, useRef } from "react"; import { useEffect, useRef, useState } from "react";
const Background = ({ children, className }) => { const Background = ({ children, className }) => {
const circlesRef = useRef([]);
const circlesRef = useRef([]);
const [theme, setTheme] = useState("light");
const getRandomTranslation = () => { const getRandomTranslation = () => {
const randomX = Math.floor(Math.random() * 100) - 30; const randomX = Math.floor(Math.random() * 100) - 30;
@@ -20,8 +22,23 @@ const Background = ({ children, className }) => {
}); });
}, []); }, []);
return ( useEffect(() => {
<div className={`${styles["background-container"]} ${className || ''}`}> const updateTheme = () => {
setTheme(localStorage.getItem("theme") || "light");
};
updateTheme(); // Initial load
window.addEventListener("theme-changed", updateTheme);
return () => {
window.removeEventListener("theme-changed", updateTheme);
};
}, []);
return (
<div className={`${styles.backgroundContainer} ${className || ''} ${theme === "light" ? styles.backgroundLight : styles.backgroundDark}`}>
<div <div
ref={(el) => (circlesRef.current[0] = el)} ref={(el) => (circlesRef.current[0] = el)}
className={`${styles["circle"]} ${styles["circle-1"]}`} className={`${styles["circle"]} ${styles["circle-1"]}`}
@@ -1,10 +1,9 @@
/* BACKGROUND ////////////////////////////////////////////////////////*/ /* BACKGROUND ////////////////////////////////////////////////////////*/
.background-container { .backgroundContainer {
z-index: 0; z-index: 0;
margin: 0; margin: 0;
padding: 0; padding: 0;
min-height: 100vh; min-height: 100vh;
background: #00F0FF;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
width: 100vw; width: 100vw;
@@ -13,6 +12,14 @@
align-items: center; align-items: center;
} }
.backgroundLight {
background: #00F0FF;
}
.backgroundDark {
background: #001C83;
}
.circle { .circle {
position: absolute; position: absolute;
border-radius: 50%; border-radius: 50%;
@@ -82,7 +89,6 @@
.circle-1 { .circle-1 {
width: 300px; width: 300px;
height: 300px; height: 300px;
background-color: #019AFF;
top: -5%; top: -5%;
left: -5%; left: -5%;
} }
@@ -90,7 +96,6 @@
.circle-2 { .circle-2 {
width: 175px; width: 175px;
height: 175px; height: 175px;
background-color: #91FCFF;
top: 58%; top: 58%;
left: 10%; left: 10%;
} }
@@ -98,7 +103,6 @@
.circle-3 { .circle-3 {
width: 140px; width: 140px;
height: 140px; height: 140px;
background-color: #91FCFF;
top: 29%; top: 29%;
right: 9%; right: 9%;
} }
@@ -106,7 +110,6 @@
.circle-4 { .circle-4 {
width: 250px; width: 250px;
height: 250px; height: 250px;
background-color: #019AFF;
bottom: 0; bottom: 0;
right: 0; right: 0;
} }
@@ -114,7 +117,6 @@
.circle-5 { .circle-5 {
width: 240px; width: 240px;
height: 240px; height: 240px;
background-color: #015AFF;
top: 2%; top: 2%;
right: 10%; right: 10%;
} }
+3 -32
View File
@@ -9,7 +9,6 @@ import { AuthContext } from "../../contexts/auth/AuthContext.js";
function LoginForm({ className }) { function LoginForm({ className }) {
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
let navigate = useNavigate(); let navigate = useNavigate();
const { login } = useContext(AuthContext); const { login } = useContext(AuthContext);
@@ -19,12 +18,8 @@ function LoginForm({ className }) {
navigate("/"); navigate("/");
} }
const toggleShowPassword = () => {
setShowPassword(!showPassword);
};
return ( return (
<div className={`${className} ${"glassCard"}`}> <div className={`${className} glassCard ${styles.inputContainer}`}>
<div className={styles.formgroup}> <div className={styles.formgroup}>
<Input <Input
type="email" type="email"
@@ -35,35 +30,11 @@ function LoginForm({ className }) {
/> />
</div> </div>
<div className={styles.formgroup}> <div className={styles.formgroup}>
<div style={{ position: 'relative', width: '100%' }}> <Input password={true} onChange={e => setPassword(e.target.value)} value={password} />
<Input
type={showPassword ? "text" : "password"}
value={password}
placeholder="Mot de passe"
onChange={e => setPassword(e.target.value)}
style={{ width: '100%' }}
/>
<button
type="button"
onClick={toggleShowPassword}
style={{
position: 'absolute',
width: '30px',
right: '10px',
top: '50%',
transform: 'translateY(-50%)',
background: 'none',
border: 'none',
cursor: 'pointer',
fontSize: '16px',
}}
>
{showPassword ? '👁️' : '🔒'}
</button>
</div>
</div> </div>
<div className={styles.logButton}> <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> <Button onClick={handleSubmit} variant={"default"}> Se connecter </Button>
</div> </div>
</div> </div>
@@ -18,14 +18,21 @@ input::placeholder{
font-size: 20px; font-size: 20px;
color: rgba(0,0,0,0.45); color: rgba(0,0,0,0.45);
} }
.formgroup{
margin-top:23px;
}
.logButton{ .logButton{
display: flex; display: flex;
justify-content: center; justify-content: center;
flex-direction: column;
align-items: center; align-items: center;
margin-top: 12px; margin-top: 12px;
} }
.formgroup{
margin-top:23px; .inputContainer {
padding: 15px;
} }
/* style scoped (module) */ /* style scoped (module) */
+2 -2
View File
@@ -1,6 +1,6 @@
import styles from "./button.module.css"; import styles from "./button.module.css";
const Button = ({ children, variant = "primary", onClick }) => { const Button = ({ children, variant = "primary", onClick, className }) => {
let btnStyle; let btnStyle;
if (variant === "primary") btnStyle = styles.primary; if (variant === "primary") btnStyle = styles.primary;
@@ -8,7 +8,7 @@ const Button = ({ children, variant = "primary", onClick }) => {
else if(variant === "transparent") btnStyle = styles.transparent; else if(variant === "transparent") btnStyle = styles.transparent;
else btnStyle = styles.default; else btnStyle = styles.default;
return <div className={`${styles.btn} ${btnStyle}`} onClick={onClick}> return <div className={`${styles.btn} ${btnStyle} ${className}`} onClick={onClick}>
{children} {children}
</div>; </div>;
}; };
+27 -2
View File
@@ -1,13 +1,38 @@
import styles from "./input.module.css"; import styles from "./input.module.css";
import { useState } from "react";
const TextInput = ({ placeholder, onChange, value, borderStyle, ...props }) => { const TextInput = ({ placeholder, onChange, value, borderStyle, password, className, ...props }) => {
const [showPassword, setShowPassword] = useState(false);
let inputBorderStyle = styles.square; let inputBorderStyle = styles.square;
if (borderStyle === "square") inputBorderStyle = styles.square; if (borderStyle === "square") inputBorderStyle = styles.square;
if(borderStyle === "rounded") inputBorderStyle = styles.rounded; if(borderStyle === "rounded") inputBorderStyle = styles.rounded;
return <input className={`${styles.input} ${inputBorderStyle}`} onChange={onChange} placeholder={placeholder} value={value} {...props} /> const toggleShowPassword = () => {
setShowPassword(!showPassword);
};
if(password) return (
<div className={styles.passwordContainer}>
<TextInput
type={showPassword ? "text" : "password"}
value={value}
placeholder="Mot de passe"
onChange={onChange}
className={styles.passwordInput}
/>
<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; export default TextInput;
+20
View File
@@ -30,3 +30,23 @@
border-radius: 12px; 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;
}
+23
View File
@@ -0,0 +1,23 @@
import styles from "./modal.module.css"
import Button from "../button/button.jsx";
const Modal = ({ open, onClose, children, title }) => {
if(!open) return null;
return <div className={styles.overlay}>
<div className={styles.modal}>
{title && <h2>{title}</h2>}
{children}
<div className={styles.modalFooter}> <Button onClick={onClose}>Fermer</Button> </div>
</div>
</div>
}
export default Modal;
+35
View File
@@ -0,0 +1,35 @@
.overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.25);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal {
padding: 20px 20px 20px 20px;
border-radius: 8px;
min-width: 300px;
max-width: 70%;
position: relative;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
width: fit-content;
border: 1px solid rgba(200, 200, 200, 0.3);
background: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
color: #000;
outline: none;
}
.modalFooter {
display: flex;
align-items: center;
justify-content: center;
margin-top: 20px;
}
@@ -0,0 +1,19 @@
import Button from "../button/button.jsx";
import React from "react";
export default function ThemeSwitcher() {
const switchTheme = () => {
const currentTheme = localStorage.getItem("theme");
let newTheme = currentTheme === "light" ? "dark" : "light";
if(currentTheme === null) newTheme = "dark";
localStorage.setItem("theme", newTheme);
window.dispatchEvent(new Event("theme-changed"));
};
return <Button variant={"default"} onClick={switchTheme}> Changer de thème </Button>
}
+2 -1
View File
@@ -2,6 +2,7 @@ import { useState, useEffect } from "react";
import loginFunction from "../../utils/login.js"; import loginFunction from "../../utils/login.js";
import getUser from "../../utils/getUser.js"; import getUser from "../../utils/getUser.js";
import { AuthContext } from "./AuthContext"; import { AuthContext } from "./AuthContext";
import logoutFunction from "../../utils/auth/logout.js"
export function AuthProvider({ children }) { export function AuthProvider({ children }) {
@@ -25,7 +26,7 @@ export function AuthProvider({ children }) {
}; };
const logout = async () => { const logout = async () => {
// TODO: Appeller fonction logout await logoutFunction();
setUser(null); setUser(null);
}; };
-1
View File
@@ -50,7 +50,6 @@ body {
.glassCard { .glassCard {
width: 100%; width: 100%;
padding: 10px;
height: fit-content; height: fit-content;
background: rgba(255, 255, 255, 0.65); background: rgba(255, 255, 255, 0.65);
backdrop-filter: blur(3px); backdrop-filter: blur(3px);
+10 -139
View File
@@ -1,4 +1,4 @@
import {useState} from "react"; import { useEffect, useState } from "react";
import Event from "../../components/Event/Event.jsx"; import Event from "../../components/Event/Event.jsx";
import styles from "./EventsPage.module.css"; import styles from "./EventsPage.module.css";
import ToolBar from "../../components/ToolBar/ToolBar.jsx"; import ToolBar from "../../components/ToolBar/ToolBar.jsx";
@@ -36,144 +36,15 @@ function EventsPage(){
volunteers_count: 3 volunteers_count: 3
}, },
] ]
}, }
{ */
id: 2,
name: "Festival de Musique d’Été", useEffect(() => {
description: "Un week-end de concerts en plein air avec des artistes locaux et internationaux.", (async () => {
image: null, const data = await getAllEvents();
tasks: [ setEvents(data.data);
{ }) ()
name: "Montage de la scène", }, []);
start: "2018-09-24 22:21:20",
end: "2018-09-24 22:21:20",
place: "Parc central",
description: "Installer la scène, les éclairages et le son.",
volunteers_count: 6
},
{
name: "Accueil du public",
start: "2018-09-24 22:21:20",
end: "2018-09-24 22:21:20",
place: "Entrée principale",
description: "Vérifier les billets et orienter les visiteurs.",
volunteers_count: 4
},
]
},
{
id: 3,
name: "Conférence Tech Innovante",
description: "Journée de conférences et workshops autour des nouvelles technologies.",
image: null,
tasks: [
{
name: "Préparation des salles",
start: "2018-09-24 22:21:20",
end: "2018-09-24 22:21:20",
place: "Campus Tech",
description: "Installer les chaises, projecteurs et matériel pour les conférences.",
volunteers_count: 4
},
{
name: "Accueil des intervenants",
start: "2018-09-24 22:21:20",
end: "2018-09-24 22:21:20",
place: "Hall principal",
description: "Assister les speakers et orienter vers les salles.",
volunteers_count: 2
},
{
name: "Gestion du buffet",
start: "2018-09-24 22:21:20",
end: "2018-09-24 22:21:20",
place: "Salle de restauration",
description: "Distribuer boissons et repas aux participants.",
volunteers_count: 3
},
]
},
{
id: 4,
name: "Atelier de Jardinage Urbain",
description: "Apprenez à cultiver vos plantes en ville et participez à la création de jardins partagés.",
image: null,
tasks: [
{
name: "Installation du matériel",
start: "2018-09-24 22:21:20",
end: "2018-09-24 22:21:20",
place: "Jardin partagé",
description: "Préparer outils et terreaux pour les participants.",
volunteers_count: 2
},
{
name: "Encadrement des participants",
start: "2018-09-24 22:21:20",
end: "2018-09-24 22:21:20",
place: "Jardin partagé",
description: "Aider et conseiller sur la plantation et l’entretien.",
volunteers_count: 3
},
]
},
{
id: 5,
name: "Collecte de Sang",
description: "Aidez à sauver des vies en participant à notre collecte de sang annuelle.",
image: null,
tasks: [
{
name: "Accueil et enregistrement",
start: "2020-09-24 22:21:20",
end: "2020-09-24 22:21:20",
place: "Salle municipale",
description: "Enregistrer les donneurs et expliquer le processus.",
volunteers_count: 4
},
{
name: "Préparation des salles",
start: "2018-09-24 22:21:20",
end: "2018-09-24 22:21:20",
place: "Salle municipale",
description: "Installer les lits, tables et matériel médical.",
volunteers_count: 3
},
{
name: "Assistance post-don",
start: "2018-09-24 22:21:20",
end: "2018-09-24 22:21:20",
place: "Salle municipale",
description: "Accompagner les donneurs après le don et proposer des rafraîchissements.",
volunteers_count: 2
},
{
name: "Accueil et enregistrement",
start: "2020-09-24 22:21:20",
end: "2020-09-24 22:21:20",
place: "Salle municipale",
description: "Enregistrer les donneurs et expliquer le processus.",
volunteers_count: 4
},
{
name: "Préparation des salles",
start: "2018-09-24 22:21:20",
end: "2018-09-24 22:21:20",
place: "Salle municipale",
description: "Installer les lits, tables et matériel médical.",
volunteers_count: 3
},
{
name: "Assistance post-don",
start: "2018-09-24 22:21:20",
end: "2018-09-24 22:21:20",
place: "Salle municipale",
description: "Accompagner les donneurs après le don et proposer des rafraîchissements.",
volunteers_count: 2
},
]
},
]);
return ( return (
+8 -9
View File
@@ -3,15 +3,14 @@ import LoginForm from "../../components/loginform/loginForm.jsx";
import Background from "../../components/background/background.jsx"; import Background from "../../components/background/background.jsx";
function LoginPage() { function LoginPage() {
return <Background> return (
<Background>
<div className={styles.container}> <div className={styles.container}>
<LoginForm /> <h1 className={styles.title}>Connexion</h1>
</div> <LoginForm />
</div>
</Background> </Background>
);
} }
export default LoginPage; export default LoginPage;
+13 -6
View File
@@ -1,16 +1,23 @@
.container { .container {
position: relative; position: absolute;
z-index: 2; z-index: 2;
display: flex; display: flex;
flex-direction: column;
width: 600px; width: 600px;
overflow: hidden;
max-height: 80%; max-height: 80%;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding: 20px; padding: 20px;
border-radius:12px; border-radius: 12px;
margin-left: 33.3%; left: 50%;
top: 50%;
transform: translate(-50%, -50%);
} }
.title {
font-size: 36px;
color: white;
text-align: center;
margin-bottom: 20px;
text-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
+39 -81
View File
@@ -1,105 +1,63 @@
import React, { useState } from "react"; import React, { useContext } from "react";
import styles from "./ProfilePage.module.css"; import styles from "./ProfilePage.module.css";
import { AuthContext } from "../../contexts/auth/AuthContext.js";
function Event({ title,date, tasks }) { import formatDate from "../../utils/date/formatDate.js";
const [isExpanded, setIsExpanded] = useState(false); import Task from "../../components/Task/Task";
import Button from "../../components/ui/button/button.jsx";
const toggleExpand = () => { import { useNavigate } from "react-router";
setIsExpanded(!isExpanded); import ThemeSwitcher from "../../components/ui/themeSwitcher/ThemeSwitcher.jsx";
};
return (
<div className={`${styles.event} glassCard`}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div style={{display:'flex', flexDirection:'column'}}>
<h3>{title}</h3>
<p style={{ fontSize: '0.8rem', color: '#666', margin: '0 0 0 0' }}>{date}</p>
</div>
<button
onClick={toggleExpand}
style={{
background: 'none',
border: 'none',
cursor: 'pointer',
fontSize: '1.2rem',
}}
>
{isExpanded ? '▼' : '▶'}
</button>
</div>
<ul
className={`${styles.eventTasks} ${isExpanded ? styles.expanded : ''}`}
>
{tasks.map((task, index) => (
<li key={index}>-- {task}</li>
))}
</ul>
</div>
);
}
function ProfilePage() { function ProfilePage() {
const { user, logout } = useContext(AuthContext);
const navigate = useNavigate();
const handleLogout = () => {
logout();
}
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={`${styles.profileboard} glassCard`}> <div className={`${styles.profileboard} glassCard`}>
<div className={styles.contentWrapper}> <div className={styles.contentWrapper}>
<div className={styles.profilePictureContainer}> <div className={styles.profilePictureContainer}>
<img <img
src="../src/assets/react.svg" src="/react.svg"
alt="Photo de profil" alt="Photo de profil"
className={styles.profilePicture} className={styles.profilePicture}
/> />
</div> </div>
<div className={styles.description}> <div className={styles.description}>
<h2>LEBLOND Malo</h2> <div className={styles.headerProfile}>
<h2> {user.name} {user.lastname} </h2>
<div className={styles.settingsBtns}>
<ThemeSwitcher />
<Button variant={"danger"} onClick={handleLogout}> Déconnexion </Button>
</div>
</div>
<div className={styles.topDescription}> <div className={styles.topDescription}>
<div className={styles.infoBlock}> <div className={styles.infoBlock}>
<p><strong>Role :</strong> Président</p> <p><strong>Role :</strong> {user.role}</p>
<p><strong>Membre depuis :</strong> 2019</p> <p><strong>Membre depuis :</strong> {formatDate(user.created_at)}</p>
</div> </div>
<div className={styles.infoBlock}> <div className={styles.infoBlock}>
<p><strong>Mail :</strong> mailpro@gmail.com</p> <p><strong>Mail :</strong> {user.email} </p>
<p><strong>Téléphone :</strong> 08 67 87 32 21</p> <p><strong>Téléphone :</strong> {user.phone}</p>
</div> </div>
<button href="/../UpdateProfile.jsx">M</button> <Button variant={"transparent"} onClick={() => navigate("/profile/update")}>
<img src={"/icons/pen.svg"} alt={"Modify btn"} className={styles.modifyIcon}/>
</Button>
</div> </div>
<h2>Vos Événements :</h2> <h2>Vos Événements :</h2>
<Event {user.tasks && user.tasks.length > 0 ? (
title="Titre event 1" user.tasks.map((task, index) => (
date="04/11/2025" <Task key={index} task={task} />
tasks={["Votre tâche 1", "Votre tâche 2", "Votre tâche 3"]} ))
/> ) : (
<Event <p>Aucune tâche pour le moment.</p>
title="Titre event 2" )}
date="04/11/2025"
tasks={["Votre tâche 1", "Votre tâche 2", "Votre tâche 3"]}
/>
<Event
title="Titre event 3"
date="04/11/2025"
tasks={["Votre tâche 1", "Votre tâche 2", "Votre tâche 3"]}
/>
<Event
title="Titre event 4"
date="04/11/2025"
tasks={["Votre tâche 1", "Votre tâche 2", "Votre tâche 3"]}
/>
<Event
title="Titre event 5"
date="04/11/2025"
tasks={["Votre tâche 1", "Votre tâche 2", "Votre tâche 3"]}
/>
<Event
title="Titre event 6"
date="04/11/2025"
tasks={["Votre tâche 1", "Votre tâche 2", "Votre tâche 3"]}
/>
<Event
title="Titre event 7"
date="04/11/2025"
tasks={["Votre tâche 1", "Votre tâche 2", "Votre tâche 3"]}
/>
</div> </div>
</div> </div>
</div> </div>
+24 -14
View File
@@ -7,7 +7,7 @@
.profileboard { .profileboard {
width: 100%; width: 100%;
padding: 15px; padding: 15px;
border-radius: 10px; border-radius: 20px;
box-sizing: border-box; box-sizing: border-box;
} }
@@ -51,13 +51,6 @@
border-radius: 5px; border-radius: 5px;
} }
.event {
width: 100%;
padding: 12px;
margin-bottom: 10px;
border-radius: 8px;
}
.eventTasks { .eventTasks {
max-height: 0; max-height: 0;
overflow: hidden; overflow: hidden;
@@ -73,12 +66,18 @@
} }
.description h2{ .description h2{
font-size:30px; font-size:30px;
margin-bottom: 2%;
} }
.profilePicture{ .profilePicture{
width:90%; width:90%;
} }
/* Tablette (768px et plus) */
.settingsBtns {
display: flex;
gap: 5px;
justify-content: right;
}
@media screen and (min-width: 768px) { @media screen and (min-width: 768px) {
.contentWrapper { .contentWrapper {
flex-direction: row; flex-direction: row;
@@ -112,7 +111,6 @@
} }
.description h2{ .description h2{
font-size:30px; font-size:30px;
margin-bottom: 2%;
} }
.profilePicture{ .profilePicture{
width:90%; width:90%;
@@ -124,6 +122,19 @@
} }
.modifyIcon {
height:50px;
width:50px;
}
.headerProfile {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
/* Desktop (1024px et plus) */ /* Desktop (1024px et plus) */
@media screen and (min-width: 1024px) { @media screen and (min-width: 1024px) {
.container { .container {
@@ -133,7 +144,7 @@
} }
.profileboard { .profileboard {
padding: 25px; padding: 25px;
} }
.profilePictureContainer { .profilePictureContainer {
@@ -153,6 +164,5 @@
} }
.description h2{ .description h2{
font-size:30px; font-size:30px;
margin-bottom: 2%;
} }
} }
+3 -28
View File
@@ -30,9 +30,9 @@ function RegisterPage() {
<div className={styles.container}> <div className={styles.container}>
<div className={"glassCard"}> <h1 className={styles.title}>Créer un compte</h1>
<h1>Créer un compte</h1> <div className={`glassCard ${styles.inputContainer}`}>
<div className={styles.formGroup}> <div className={styles.formGroup}>
<Input <Input
@@ -62,32 +62,7 @@ function RegisterPage() {
/> />
</div> </div>
<div className={styles.formGroup}> <div className={styles.formGroup}>
<div style={{ position: 'relative', width: '100%' }}> <Input password={true} onChange={e => setPassword(e.target.value)} value={password} />
<Input
type={showPassword ? "text" : "password"}
value={password}
placeholder="Mot de passe"
onChange={e => setPassword(e.target.value)}
style={{ width: '100%' }}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
style={{
position: 'absolute',
width: '30px',
right: '10px',
top: '50%',
transform: 'translateY(-50%)',
background: 'none',
border: 'none',
cursor: 'pointer',
fontSize: '16px',
}}
>
{showPassword ? '👁️' : '🔒'}
</button>
</div>
</div> </div>
<div className={styles.logButton}> <div className={styles.logButton}>
@@ -2,6 +2,7 @@
position: relative; position: relative;
z-index: 2; z-index: 2;
display: flex; display: flex;
flex-direction: column;
width: 30%; width: 30%;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@@ -9,6 +10,15 @@
border-radius:12px; border-radius:12px;
} }
.title {
font-size: 36px;
color: white;
text-align: center;
margin-bottom: 20px;
text-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
:global(.glassCard) { :global(.glassCard) {
--glass-bg: #ffffff9f; --glass-bg: #ffffff9f;
} }
@@ -26,4 +36,8 @@
h1 { h1 {
overflow: visible; overflow: visible;
text-align: center; text-align: center;
}
.inputContainer {
padding: 15px;
} }
+148
View File
@@ -0,0 +1,148 @@
import React, { useState } 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";
// Enregistrer les éléments nécessaires
Chart.register(ArcElement);
function StatPage() {
const [months, setMonths] = useState(3);
const [participationRate, setParticipationRate] = useState(89);
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;
+194
View File
@@ -0,0 +1,194 @@
.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;
}
}
+83
View File
@@ -0,0 +1,83 @@
import React, {useEffect, useState} from "react";
import styles from "./VolunteersPage.module.css";
import getUserById from "../../utils/users/getUserById.js";
import formatDate from "../../utils/date/formatDate.js";
import { useNavigate } from "react-router";
import Button from "../../components/ui/button/button.jsx";
export default function VolunteerCard({ id }) {
const [isExpanded, setIsExpanded] = useState(false);
const [user, setUser] = useState({});
const toggleExpand = () => {
setIsExpanded(!isExpanded);
};
useEffect(() => {
(async () => {
const data = await getUserById(id);
setUser(data);
}) ()
}, []);
const navigate = useNavigate();
return (
<div className={`${styles.profileboard} glassCard`}>
<div className={styles.header}>
<h2>{user?.name}</h2>
<Button
variant={"transparent"}
onClick={toggleExpand}
className={`${isExpanded ? styles.moreButtonClicked : ""}`}
>
{">"}
</Button>
</div>
<div className={styles.mainContent}>
{!isExpanded ? (
<div className={styles.basicInfo}>
<p><strong>Rôle :</strong> {user?.role}</p>
<p><strong>Membre depuis :</strong> {user?.created_at && formatDate(user.created_at)}</p>
</div>
) : (
<div className={styles.expandedContent}>
<div className={styles.profilePictureContainer}>
<img src={"/react.svg"} alt="profile" className={styles.profilePicture} />
</div>
<div className={styles.textInfo}>
<div className={styles.basicInfo}>
<p><strong>Rôle :</strong> {user?.role}</p>
<p><strong>Membre depuis :</strong> {user?.created_at && formatDate(user.created_at)}</p>
</div>
<div className={styles.contactInfo}>
<p><strong>Email :</strong> {user?.email}</p>
{user?.phone && <p><strong>Téléphone :</strong> {user.phone}</p>}
</div>
</div>
</div>
)}
</div>
{isExpanded && (
<div className={styles.bottomBar}>
<Button
onClick={() => navigate(`/profile/${id}`)}
variant="transparent"
>
Voir plus
</Button>
</div>
)}
</div>
);
}
+13 -96
View File
@@ -1,106 +1,23 @@
import { useState, useEffect } from "react";
import React, { useState } from "react";
import styles from "./VolunteersPage.module.css"; import styles from "./VolunteersPage.module.css";
import VolunteerCard from "./VolunteerCard.jsx";
import getAllUser from "../../utils/users/getAllUsers.js";
function Volunteer({ name, role, since, email, phone, photo }) {
const [isExpanded, setIsExpanded] = useState(false);
const toggleExpand = () => {
setIsExpanded(!isExpanded);
};
return (
<div className={`${styles.profileboard} glassCard`}>
<div className={styles.mainContent}>
{!isExpanded ? (
<div className={styles.basicInfo}>
<h2>{name}</h2>
<p><strong>Rôle :</strong> {role}</p>
<p><strong>Membre depuis :</strong> {since}</p>
</div>
) : (
<div className={styles.expandedContent}>
<div className={styles.profilePictureContainer}>
<img src={photo} alt={`${name}'s profile`} className={styles.profilePicture} />
</div>
<div className={styles.textInfo}>
<div className={styles.basicInfo}>
<h2>{name}</h2>
<p><strong>Rôle :</strong> {role}</p>
<p><strong>Membre depuis :</strong> {since}</p>
</div>
<div className={styles.contactInfo}>
<p><strong>Email :</strong> {email}</p>
<p><strong>Téléphone :</strong> {phone}</p>
</div>
</div>
</div>
)}
<button
className={`${styles.expandButton} glassCard`}
onClick={toggleExpand}
>
{isExpanded ? "▲" : "▼"}
</button>
{isExpanded && (
<div className="voirPlus">
<a href="#" className={styles.voirPlus}>VOIR PLUS</a>
</div>
)}
</div>
</div>
);
}
function VolunteerPage() { function VolunteerPage() {
const volunteers = [
{ const [users, setUsers] = useState([]);
name: "LEBLOND Malo",
role: "Président", useEffect(() => {
since: "2019", (async () => {
email: "malo.leblond@example.com", const data = await getAllUser();
phone: "08 67 87 32 21", setUsers(data);
photo: "../src/assets/react.svg", }) ()
}, }, [])
{
name: "DUPONT Jean",
role: "Bénévole",
since: "2020",
email: "jean.dupont@example.com",
phone: "06 12 34 56 78",
photo: "../src/assets/react.svg",
},
{
name: "MARTIN Sophie",
role: "Bénévole",
since: "2021",
email: "sophie.martin@example.com",
phone: "06 87 65 43 21",
photo: "../src/assets/react.svg",
},
{
name: "MARTIN Sophie",
role: "Bénévole",
since: "2021",
email: "sophie.martin@example.com",
phone: "06 87 65 43 21",
photo: "../src/assets/react.svg",
},
];
return ( return (
<div className={styles.container}> <div className={styles.container}>
{volunteers.map((volunteer, index) => ( {users.map((user, index) => <VolunteerCard key={index} id={user} /> )}
<Volunteer
key={index}
name={volunteer.name}
role={volunteer.role}
since={volunteer.since}
email={volunteer.email}
phone={volunteer.phone}
photo={volunteer.photo}
/>
))}
</div> </div>
); );
} }
+81 -89
View File
@@ -1,125 +1,117 @@
.container { .container {
width: 100%; width: 100%;
padding: 10px; padding: 10px;
box-sizing: border-box; margin-top: 50px;
margin-top: 50px; box-sizing: border-box;
} }
.profileboard { .profileboard {
width: 100%; width: 100%;
padding: 15px; padding: 15px;
border-radius: 10px; margin-top: 3%;
box-sizing: border-box; display: flex;
margin-top: 3%; flex-direction: column;
position: relative; gap: 10px;
box-sizing: border-box;
} }
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.header h2 {
margin: 0;
}
.mainContent { .mainContent {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; width: 100%;
align-items: flex-start; flex: 1;
width: 100%;
} }
.basicInfo { .basicInfo {
flex: 1; flex: 1;
} }
.expandedContent { .expandedContent {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: flex-start; width: 100%;
width: 100%;
}
.profilePictureContainer {
width: 120px;
height: 120px;
margin-right: 20px;
}
.profilePicture {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
} }
.textInfo { .textInfo {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;
} }
.contactInfo { .contactInfo {
margin-top: 10px; margin-top: 10px;
} }
.contactInfo p { .contactInfo p {
margin: 5px 0; margin: 5px 0;
}
.bottomBar {
display: flex;
justify-content: flex-end;
}
.profilePictureContainer {
width: 120px;
height: 120px;
margin-right: 20px;
}
.profilePicture {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
} }
.expandButton { .expandButton {
width: 30px; width: 30px;
height: 30px; height: 30px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
background: transparent; background: transparent;
border: none; border: none;
font-size: 16px; font-size: 16px;
margin-left: 10px;
position: absolute;
top: 10px;
right: 10px;
} }
.voirPlus { .moreButtonClicked {
position: absolute; rotate: 90deg;
bottom: 10px;
right: 10px;
color: #007bff;
text-decoration: none;
font-weight: bold;
cursor: pointer;
}
.voirPlus:hover {
text-decoration: underline;
} }
@media screen and (min-width: 768px) { @media screen and (min-width: 768px) {
.profilePictureContainer { .profilePictureContainer {
width: 150px; width: 150px;
height: 150px; height: 150px;
} }
.voirPlus{
text-align: right;
}
} }
@media screen and (min-width: 1024px) { @media screen and (min-width: 1024px) {
.container { .container {
max-width: 1200px; max-width: 1200px;
margin: 0 auto; margin: 50px auto;
padding: 20px; padding: 20px;
margin-top:50px; }
margin-bottom: 50px;
}
.profileboard { .profileboard {
padding: 25px; padding: 25px;
} }
.profilePictureContainer { .profilePictureContainer {
width: 200px; width: 200px;
height: auto; height: auto;
} }
} }
+3 -2
View File
@@ -2,6 +2,7 @@ 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";
@@ -25,8 +26,8 @@ const router = createBrowserRouter([
Component: HomePage, Component: HomePage,
}, },
{ {
path: "/login", path: "/stat",
Component: LoginPage, Component:StatPage,
}, },
{ {
path:"/profile", path:"/profile",
+49
View File
@@ -0,0 +1,49 @@
import Modal from "../components/ui/modal/modal.jsx";
import Button from "../components/ui/button/button.jsx";
import { useState } from "react";
export default {
title: 'UI/Modal',
component: Modal,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
open: {
control: 'boolean',
description: 'Variable qui gère si la modal est ouverte ou fermée.',
},
children: {
control: Object,
description: 'Contenu de la modal.',
},
title: {
control: 'text',
description: 'Titre de la modal',
},
onClose: {
action: 'changed',
description: 'Fonction appelée pour fermer la modal.',
},
},
};
const Template = (args) => {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<Button onClick={() => setIsOpen(true)}>Ouvrir</Button>
<Modal {...args} open={isOpen} onClose={() => setIsOpen(false)}>
{args.children}
</Modal>
</div>
);
};
export const Default = Template.bind({});
Default.args = {
title: "Hello",
children: <p>Lorem Ipsum...</p>,
};
+28
View File
@@ -0,0 +1,28 @@
import getXSRFToken from "../getXSRF.js";
export default async function logout() {
const csrfToken = await getXSRFToken();
try {
const response = await fetch(`http://${import.meta.env.VITE_API_URL}/api/users/logout`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-XSRF-TOKEN': csrfToken
},
});
console.log('Code HTTP :', response.status);
if (!response.ok) {
throw new Error(`Erreur HTTP ${response.status}`);
}
return { status: response.status };
} catch (error) {
return error;
}
}
+9
View File
@@ -0,0 +1,9 @@
export default function formatDate(d) {
const date = new Date(d);
const jj = String(date.getUTCDate()).padStart(2, '0');
const mm = String(date.getUTCMonth() + 1).padStart(2, '0');
const aaaa = date.getUTCFullYear();
return `${jj}/${mm}/${aaaa}`;
}
+9
View File
@@ -0,0 +1,9 @@
import { expect, test } from 'vitest';
import formatDate from './formatDate';
const date = "2025-11-28T11:26:45.000000Z"
const res = "28/11/2025"
test("Format Date", () => {
expect(formatDate(date)).toBe(res);
})
+23
View File
@@ -0,0 +1,23 @@
export default async function getAllEvents() {
try {
const res = await fetch(`http://${import.meta.env.VITE_API_URL}/api/events/`, {
method: 'GET',
credentials: 'include',
headers: {
'Accept': 'application/json'
}
});
if (!res.ok) {
throw new Error(`Erreur serveur : ${res.status}`);
}
const data = await res.json();
return data;
} catch (err) {
console.error('Erreur :', err);
return null;
}
}
+23
View File
@@ -0,0 +1,23 @@
export default async function GetEventById(id) {
try {
const res = await fetch(`http://${import.meta.env.VITE_API_URL}/api/events/${id}`, {
method: 'GET',
credentials: 'include',
headers: {
'Accept': 'application/json'
}
});
if (!res.ok) {
throw new Error(`Erreur serveur : ${res.status}`);
}
const data = await res.json();
return data;
} catch (err) {
console.error('Erreur :', err);
return null;
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
export default async function getUser() { export default async function getUser() {
try { try {
const res = await fetch('http://localhost:80/api/users/me', { const res = await fetch(`http://${import.meta.env.VITE_API_URL}/api/users/me`, {
method: 'GET', method: 'GET',
credentials: 'include', credentials: 'include',
headers: { headers: {
+1 -1
View File
@@ -1,5 +1,5 @@
export default async function getXSRFToken() { export default async function getXSRFToken() {
const response = await fetch('http://localhost:80/sanctum/csrf-cookie', { const response = await fetch(`http://${import.meta.env.VITE_API_URL}/sanctum/csrf-cookie`, {
method: 'GET', method: 'GET',
credentials: 'include' credentials: 'include'
}); });
+1 -1
View File
@@ -4,7 +4,7 @@ export default async function login(email, password) {
const csrfToken = await getXSRFToken(); const csrfToken = await getXSRFToken();
try { try {
const response = await fetch('http://localhost:80/api/users/login', { const response = await fetch(`http://${import.meta.env.VITE_API_URL}/api/users/login`, {
method: 'POST', method: 'POST',
credentials: 'include', credentials: 'include',
headers: { headers: {
+1 -1
View File
@@ -4,7 +4,7 @@ export default async function register(email, password, name, lastname) {
const csrfToken = await getXSRFToken(); const csrfToken = await getXSRFToken();
try { try {
const response = await fetch('http://localhost:80/api/users/register', { const response = await fetch(`http://${import.meta.env.VITE_API_URL}/api/users/register`, {
method: 'POST', method: 'POST',
credentials: 'include', credentials: 'include',
headers: { headers: {
+21
View File
@@ -0,0 +1,21 @@
export default async function getAllUser() {
try {
const res = await fetch(`http://${import.meta.env.VITE_API_URL}/api/users`, {
method: 'GET',
credentials: 'include',
headers: {
'Accept': 'application/json'
}
});
if (!res.ok) {
throw new Error(`Erreur serveur : ${res.status}`);
}
const data = await res.json();
return data;
} catch (err) {
console.error('Erreur :', err);
return null;
}
}
+20
View File
@@ -0,0 +1,20 @@
export default async function getUserById (id) {
try {
const res = await fetch(`http://${import.meta.env.VITE_API_URL}/api/users/${id}`, {
method: 'GET',
credentials: 'include',
headers: {
'Accept': 'application/json'
}
});
if (!res.ok) {
throw new Error(`Erreur serveur : ${res.status}`);
}
return await res.json();
} catch (err) {
console.error('Erreur :', err);
return null;
}
}
+39 -30
View File
@@ -1,39 +1,48 @@
// vite.config.js
/// <reference types="vitest/config" /> /// <reference types="vitest/config" />
import { defineConfig } from 'vite'; import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'
import { playwright } from '@vitest/browser-playwright'
// https://vite.dev/config/
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
import { playwright } from '@vitest/browser-playwright';
const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
// More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
base: '/', base: '/',
test: { test: {
projects: [{ projects: [
extends: true, {
plugins: [ extends: true,
// The plugin will run tests for the stories defined in your Storybook config plugins: [
// See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest storybookTest({
storybookTest({ configDir: path.join(dirname, '.storybook'),
configDir: path.join(dirname, '.storybook') }),
})], ],
test: { test: {
name: 'storybook', name: 'storybook',
browser: { browser: {
enabled: true, enabled: true,
headless: true, headless: true,
provider: playwright({}), provider: playwright({}),
instances: [{ instances: [{ browser: 'chromium' }],
browser: 'chromium' },
}] setupFiles: ['.storybook/vitest.setup.js'],
}, },
setupFiles: ['.storybook/vitest.setup.js'] },
} {
}] test: {
} name: 'unit',
}); include: [
'src/**/*.test.js',
'src/**/*.test.ts',
'utils/**/*.test.js',
],
environment: 'node',
},
},
],
},
})