resolve conflict
This commit is contained in:
commit
f2492c253c
84 files changed
+2825
-972
No files matched your search
@@ -1,3 +0,0 @@
|
||||
<svg width="60px" height="60px" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M34.325 52.5C33.8855 53.2577 33.2546 53.8866 32.4956 54.3238C31.7365 54.761 30.8759 54.9911 30 54.9911C29.1241 54.9911 28.2635 54.761 27.5044 54.3238C26.7454 53.8866 26.1145 53.2577 25.675 52.5M45 20C45 16.0218 43.4196 12.2064 40.6066 9.3934C37.7936 6.58035 33.9782 5 30 5C26.0218 5 22.2064 6.58035 19.3934 9.3934C16.5804 12.2064 15 16.0218 15 20C15 37.5 7.5 42.5 7.5 42.5H52.5C52.5 42.5 45 37.5 45 20Z" stroke="#1E1E1E" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 605 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 222 B |
Binary file not shown.
|
Before Width: | Height: | Size: 98 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><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.93H180Zm60-60h480v-28.93q0-12.15-7.04-22.5-7.04-10.34-19.11-16.88-51.7-25.46-105.42-38.58Q534.7-367.69 480-367.69q-54.7 0-108.43 13.11-53.72 13.12-105.42 38.58-12.07 6.54-19.11 16.88-7.04 10.35-7.04 22.5v28.93Zm240-304.62q33 0 56.5-23.5t23.5-56.5q0-33-23.5-56.5t-56.5-23.5q-33 0-56.5 23.5t-23.5 56.5q0 33 23.5 56.5t56.5 23.5Zm0-80Zm0 384.62Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 844 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 4.0 KiB |
@@ -0,0 +1,84 @@
|
||||
import styles from "./Event.module.css";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import Button from "/src/components/ui/button/button.jsx"
|
||||
|
||||
function Event({event}){
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [eventMenu, seteventMenu] = useState(false);
|
||||
const [windowSize, setWindowSize] = useState({
|
||||
width: null,
|
||||
height: null,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
function handleResize() {
|
||||
setWindowSize({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
}
|
||||
window.addEventListener("resize", handleResize);
|
||||
handleResize();
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className={`${styles.eventContainer} glassCard`}>
|
||||
<div className={styles.eventContainerTop}>
|
||||
<div className={styles.eventContent}>
|
||||
<div className={`${styles.eventImageDiv}`}>
|
||||
{event.image == null ? (
|
||||
<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" />
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.eventInfos}>
|
||||
<p className={styles.eventTitle}>{event.name}</p>
|
||||
<p>{event.description}</p>
|
||||
{event.tasks != null ? (
|
||||
<div className={eventMenu ? styles.eventMenuOpen : styles.eventMenuClose}>
|
||||
<ul>
|
||||
{event.tasks
|
||||
.slice(0,5)
|
||||
.map((task, index) => (
|
||||
<li key={index}> - {task.name}</li>
|
||||
))}
|
||||
{event.tasks.length > 5 ? (
|
||||
<li>...</li>
|
||||
) : null}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{windowSize.width > 768 ? (
|
||||
<div className={styles.eventButtons}>
|
||||
<Button
|
||||
variant={"transparent"}
|
||||
onClick={()=>{seteventMenu(!eventMenu)}}
|
||||
className={`${eventMenu ? styles.moreButtonClicked : ""}`}
|
||||
>
|
||||
{">"}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className={styles.eventContainerBottom}>
|
||||
{(windowSize.width <= 768 || eventMenu) ? (
|
||||
|
||||
<Button onClick={() => navigate(`/event/` + event.id)}>Voir plus</Button>
|
||||
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Event;
|
||||
@@ -0,0 +1,127 @@
|
||||
.eventContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 1rem 0;
|
||||
width: 80%;
|
||||
padding: 1rem 0.5rem;
|
||||
}
|
||||
|
||||
.eventContainerTop {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.eventContainerBottom{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.moreLink {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.eventImageDiv {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 1rem;
|
||||
width: auto;
|
||||
height: 6rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.eventImage {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
|
||||
.eventContent {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.eventTitle {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.eventButtons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
width: fit-content;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.eventButtons a {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.moreButton {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #019AFF;
|
||||
font-size: 1.5rem;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
width: 55px;
|
||||
height: 55px;
|
||||
}
|
||||
|
||||
.moreButton:hover {
|
||||
font-size: 2rem;
|
||||
background: rgba(191, 191, 191, 0.5);
|
||||
}
|
||||
|
||||
.moreButton:focus {
|
||||
outline: none;
|
||||
}
|
||||
.moreButton:focus-visible {
|
||||
outline: 2px solid black;
|
||||
}
|
||||
|
||||
.moreButtonClicked {
|
||||
rotate: 90deg;
|
||||
}
|
||||
|
||||
.eventMenuOpen {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 1.5em;
|
||||
}
|
||||
|
||||
.eventMenuClose {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.eventImage {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.eventContainer{
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.eventImageDiv {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.eventContent {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.eventInfos, .eventContainerBottom {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import styles from "./Filter.module.css";
|
||||
|
||||
function Filter({isFilterVisible, filters, setFilters}){
|
||||
const setAlphabeticalOrder = (order) => {
|
||||
setFilters(prev => ({
|
||||
...prev,
|
||||
alphabetical : prev.alphabetical === order ? null : order
|
||||
}));
|
||||
};
|
||||
|
||||
const setYearOrder = (order) => {
|
||||
setFilters(prev => ({
|
||||
...prev,
|
||||
yearOrder: prev.yearOrder === order ? null : order
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${styles.filterContainer} ${isFilterVisible ? styles.filterOpen : styles.filterClose}`}>
|
||||
<abbr title="Tri alphabétique croissant (A à Z)">
|
||||
<button className={`${styles.filterTag} glassCard ${filters.alphabetical !== "asc" ? "" : styles.active}`}
|
||||
onClick={() => setAlphabeticalOrder("asc")}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor">
|
||||
<path d="m80-280 150-400h86l150 400h-82l-34-96H196l-32 96H80Zm140-164h104l-48-150h-6l-50 150Zm328 164v-76l202-252H556v-72h282v76L638-352h202v72H548ZM360-760l120-120 120 120H360ZM480-80 360-200h240L480-80Z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</abbr>
|
||||
|
||||
<abbr title="Tri alphabétique décroissant (Z à A)">
|
||||
<button className={`${styles.filterTag} glassCard ${filters.alphabetical !== "desc" ? "" : styles.active}`}
|
||||
onClick={() => setAlphabeticalOrder("desc")}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor">
|
||||
<path d="M 480,-880 360,-760 H 600 Z M 360,-200 480,-80 600,-200 Z" />
|
||||
<path d="M 71.679924,-674.40094 H 398.50264 v 60.67705 l -208.59362,252.3436 h 214.5832 v 75.7812 H 65.690344 v -60.67705 L 274.28397,-598.61973 H 71.679924 Z" />
|
||||
<path d="M 769.04932,-356.43238 H 612.27858 l -24.73957,70.83329 H 486.75782 l 144.01033,-388.80185 h 119.53118 l 144.01033,388.80185 H 793.52847 Z m -131.77076,-72.13537 h 106.51035 l -53.12496,-154.68741 z" />
|
||||
</svg>
|
||||
</button>
|
||||
</abbr>
|
||||
|
||||
<abbr title="Tri chronologique décroissant (du plus récent au plus ancien)">
|
||||
<button className={`${styles.filterTag} glassCard ${filters.yearOrder !== "desc" ? "" : styles.active}`}
|
||||
onClick={() => setYearOrder("desc")}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor">
|
||||
<path d="M340-160q-125 0-212.5-87.5T40-460q0-125 87.5-212.5T340-760q52 0 98 16.5t84 45.5l42-42 56 56-42 42q29 38 45.5 84.5T640-460q0 125-87.5 212.5T340-160Zm400 0v-488l-44 44-56-56 140-140 140 140-57 56-43-43v487h-80ZM240-800v-80h200v80H240Zm100 560q92 0 156-64t64-156q0-92-64-156t-156-64q-92 0-156 64t-64 156q0 92 64 156t156 64Zm-40-180h80v-200h-80v200Zm40-40Z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</abbr>
|
||||
|
||||
<abbr title="Tri chronologique croissant (du plus ancien au plus récent)">
|
||||
<button className={`${styles.filterTag} glassCard ${filters.yearOrder !== "asc" ? "" : styles.active}`}
|
||||
onClick={() => setYearOrder("asc")}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor">
|
||||
<path d="M340-160q-125 0-212.5-87.5T40-460q0-125 87.5-212.5T340-760q52 0 98 16.5t84 45.5l42-42 56 56-42 42q29 38 45.5 84.5T640-460q0 125-87.5 212.5T340-160Zm440 0L640-300l56-56 44 44v-488h80v487l43-43 57 56-140 140ZM240-800v-80h200v80H240Zm100 560q92 0 156-64t64-156q0-92-64-156t-156-64q-92 0-156 64t-64 156q0 92 64 156t156 64Zm-40-180h80v-200h-80v200Zm40-40Z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</abbr>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Filter;
|
||||
@@ -0,0 +1,85 @@
|
||||
.filterContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
overflow: hidden;
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
transition: max-height 0.5s ease, opacity 0.8s ease, transform 0.5s ease;
|
||||
}
|
||||
|
||||
.filterOpen {
|
||||
max-height: 200px;
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.filterClose{
|
||||
max-height: 200px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
.filterTag {
|
||||
border: none;
|
||||
background: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
}
|
||||
|
||||
|
||||
.filterTag:focus {
|
||||
outline: none;
|
||||
}
|
||||
.filterTag:focus-visible {
|
||||
outline: 2px solid black;
|
||||
}
|
||||
|
||||
.filterTag:hover {
|
||||
cursor: pointer;
|
||||
svg{
|
||||
fill: #019AFF;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.filterTag svg {
|
||||
transform: scale(0.7);
|
||||
color: black;
|
||||
}
|
||||
|
||||
|
||||
.active {
|
||||
border: 1px solid #019AFF;
|
||||
}
|
||||
|
||||
.active svg {
|
||||
fill: #019AFF;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px){
|
||||
.filterContainer {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filterOpen{
|
||||
display: flex;
|
||||
}
|
||||
.filterClose{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.filterMenu {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
abbr {
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ function Header() {
|
||||
setNotifications(notifData || []);
|
||||
}
|
||||
|
||||
loadNotifications();
|
||||
//loadNotifications();
|
||||
const handleClickOutside = (event) => {
|
||||
if (
|
||||
notificationRef.current &&
|
||||
@@ -41,7 +41,7 @@ function Header() {
|
||||
<nav className={`${mobileMenu ? styles.inactiveHeaderContent : styles.activeHeaderContent}`}>
|
||||
<div className={styles.headerLeftContent}>
|
||||
<NavLink className={styles.logoLink} to="/">
|
||||
<img src="/src/assets/logo.png" alt="logo" className={styles.logo} />
|
||||
<img src="logo.png" alt="logo" className={styles.logo} />
|
||||
</NavLink>
|
||||
<div className={styles.navLinks}>
|
||||
<NavLink to="/" className={({ isActive }) => isActive ? styles.activeLink : styles.link}>
|
||||
@@ -70,7 +70,7 @@ function Header() {
|
||||
{unreadNotification && <span className={styles.notificationBadge}></span>}
|
||||
</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">
|
||||
<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>
|
||||
|
||||
@@ -179,7 +179,7 @@ svg {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
z-index: 1;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.activeNotificationDiv {
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import styles from "./SearchBar.module.css"
|
||||
import {Link} from "react-router";
|
||||
import {useEffect, useState} from "react";
|
||||
import searchEvents from "../../utils/searchEvents.js";
|
||||
import searchUsers from "../../utils/searchUsers.js";
|
||||
import { useLocation } from "react-router";
|
||||
|
||||
|
||||
function SearchBar() {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [searchResults, setSearchResults] = useState([]);
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
const performSearch = async (query) => {
|
||||
if (query.trim() === '') {
|
||||
setSearchResults([]);
|
||||
return;
|
||||
}
|
||||
|
||||
let data;
|
||||
|
||||
if (location.pathname.includes("events")) {
|
||||
data = await searchEvents(query);
|
||||
} else if (location.pathname.includes("volunteers")) {
|
||||
data = await searchUsers(query);
|
||||
}
|
||||
|
||||
|
||||
if (data) {
|
||||
setSearchResults(data);
|
||||
} else {
|
||||
setSearchResults([]);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handler = setTimeout(() => {
|
||||
performSearch(searchQuery);
|
||||
}, 300);
|
||||
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
}, [searchQuery]);
|
||||
|
||||
return (
|
||||
<div className={`${styles.searchBarContainer} glassCard`}>
|
||||
<form className={styles.searchBarForm} onSubmit={(element) => element.preventDefault()}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24px" viewBox="0 -960 960 960" fill="#currentColor">
|
||||
<path
|
||||
d="M784-120 532-372q-30 24-69 38t-83 14q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l252 252-56 56ZM380-400q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z"/>
|
||||
</svg>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Rechercher un événement"
|
||||
value={searchQuery}
|
||||
onChange={(element) => setSearchQuery(element.target.value)}
|
||||
/>
|
||||
</form>
|
||||
|
||||
<div className={styles.lineContainer}>
|
||||
{searchResults.length > 0 ? (
|
||||
<div className={styles.line}></div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className={styles.elementList}>
|
||||
{searchResults.map((result, i) => (
|
||||
(location.pathname.includes("events")) ? (
|
||||
<Link to={`/event/` + result.id}>
|
||||
<div className={styles.searchedEvent} key={i}>
|
||||
<div className={`${styles.eventImageDiv}`}>
|
||||
{result.image == null ? (
|
||||
<img src="empty.png" className={styles.eventImage} alt="Pas de photo de l'événement" />
|
||||
) : (
|
||||
<img src={result.image + `.png`} alt="Photo de l'événement" />
|
||||
)}
|
||||
</div>
|
||||
<p>{result.name}</p>
|
||||
</div>
|
||||
</Link>
|
||||
) : location.pathname.includes("volunteers") ? (
|
||||
<Link to={`/profile/` + result.id}>
|
||||
<div className={styles.searchedEvent} key={i}>
|
||||
<div className={`${styles.eventImageDiv}`}>
|
||||
{result.image == null ? (
|
||||
<img src="empty.png" className={styles.eventImage} alt="Pas de photo de l'événement" />
|
||||
) : (
|
||||
<img src={result.image + `.png`} alt="Photo de l'événement" />
|
||||
)}
|
||||
</div>
|
||||
<p>{result.name} {result.lastname}</p>
|
||||
</div>
|
||||
</Link>
|
||||
) : null
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SearchBar;
|
||||
@@ -0,0 +1,86 @@
|
||||
.searchBarContainer{
|
||||
position: fixed;
|
||||
top: 4rem;
|
||||
z-index: 100;
|
||||
width: 60%;
|
||||
background: #fff;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.searchBarForm{
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.elementList {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.eventImageDiv {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 1rem;
|
||||
width: auto;
|
||||
height: 2rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.searchedEvent{
|
||||
display: flex;
|
||||
padding: 0.5rem;
|
||||
border-radius: 15px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.searchedEvent:hover {
|
||||
background: #afafaf;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.searchedEvent p {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
}
|
||||
|
||||
|
||||
.lineContainer{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.line{
|
||||
height: 0;
|
||||
width: 96%;
|
||||
border: #606060 solid 1px;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.eventImage {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.searchBarForm input{
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
.searchBarForm input:focus{
|
||||
outline: none;
|
||||
}
|
||||
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
.searchBarContainer{
|
||||
width: 80%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import Modal from "../ui/modal/modal.jsx";
|
||||
import Button from "../ui/button/button.jsx";
|
||||
import styles from "./SettingsModal.module.css"
|
||||
import { useContext, useState } from "react";
|
||||
import ThemeSwitcher from "../ui/themeSwitcher/ThemeSwitcher.jsx";
|
||||
import { AuthContext } from "../../contexts/auth/AuthContext.js";
|
||||
import TextInput from "../ui/input/input.jsx";
|
||||
import updateUser from "../../utils/users/updateUser.js";
|
||||
|
||||
|
||||
export default function SettingsModal() {
|
||||
|
||||
const { user, logout, update } = useContext(AuthContext);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const [name, setName] = useState(user.name);
|
||||
const [lastname, setLastName] = useState(user.lastname);
|
||||
const [phone, setPhone] = useState(user.phone);
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
await updateUser(name, lastname, phone);
|
||||
update();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant={"transparent"} onClick={() => setOpen(!open)}>
|
||||
<img src={"/icons/settings-wheel.svg"} alt={"Modify btn"} className={styles.modifyIcon}/>
|
||||
</Button>
|
||||
|
||||
<Modal title={"Paramètres"} open={open} onClose={() => setOpen(false)}>
|
||||
<div className={styles.content}>
|
||||
|
||||
<div className={styles.section}>
|
||||
<h3>Compte</h3>
|
||||
<Button variant={"danger"} onClick={handleLogout}> Déconnexion </Button>
|
||||
|
||||
<div className={styles.section}>
|
||||
<h4>Changer de Prénom</h4>
|
||||
<TextInput value={name} onChange={e => setName(e.target.value)} type={"text"} />
|
||||
</div>
|
||||
|
||||
<div className={styles.section}>
|
||||
<h4>Changer de nom</h4>
|
||||
<TextInput value={lastname} onChange={e => setLastName(e.target.value)} type={"text"} />
|
||||
</div>
|
||||
|
||||
<div className={styles.section}>
|
||||
<h4>Changer de numéro de téléphone</h4>
|
||||
<TextInput value={phone} onChange={e => setPhone(e.target.value)} type={"text"} />
|
||||
</div>
|
||||
|
||||
<Button className={styles.submitBtn} onClick={handleSubmit}>Confirmer</Button>
|
||||
|
||||
</div>
|
||||
|
||||
<div className={styles.section}>
|
||||
<h3>Apparence</h3>
|
||||
<ThemeSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
.content {
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modifyIcon {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.submitBtn {
|
||||
align-self: center;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import styles from "./ToolBar.module.css"
|
||||
import {Link} from "react-router";
|
||||
import Filter from "../Filter/Filter.jsx";
|
||||
import {useEffect, useState} from "react";
|
||||
import SearchBar from "../SearchBar/SearchBar.jsx";
|
||||
|
||||
|
||||
function ToolBar({setFilters, filters}) {
|
||||
const [isFilterVisible, setIsFilterVisible] = useState(false);
|
||||
const [isSearchVisible, setIsSearchVisible] = useState(false);
|
||||
const toggleFilters = () => {
|
||||
setIsFilterVisible(prev => !prev);
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (isSearchVisible) {
|
||||
document.documentElement.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.documentElement.style.overflow = '';
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.documentElement.style.overflow = '';
|
||||
};
|
||||
}, [isSearchVisible]);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isSearchVisible ? (
|
||||
<div className={styles.searchBarContainer}>
|
||||
<div className={styles.searchBarOverlay} onClick={() => setIsSearchVisible(false)}></div>
|
||||
<SearchBar/>
|
||||
</div>
|
||||
) : null}
|
||||
<div className={styles.eventsButtons}>
|
||||
<Link
|
||||
to="/event/create"
|
||||
className={`${styles.createButton} glassCard`}>
|
||||
+
|
||||
</Link>
|
||||
|
||||
<button className={`${styles.searchButton} glassCard`} onClick={() => setIsSearchVisible(true)}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="#currentColor">
|
||||
<path d="M784-120 532-372q-30 24-69 38t-83 14q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l252 252-56 56ZM380-400q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div className={`${styles.filterContainer} ${isFilterVisible ? "glassCard" : ""}`}>
|
||||
<button className={`${isFilterVisible ? "" : styles.mobileFilter} ${styles.sortButton} glassCard`} onClick={toggleFilters}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor">
|
||||
<path d="M440-160q-17 0-28.5-11.5T400-200v-240L168-736q-15-20-4.5-42t36.5-22h560q26 0 36.5 22t-4.5 42L560-440v240q0 17-11.5 28.5T520-160h-80Zm40-308 198-252H282l198 252Zm0 0Z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div className={styles.filterMenu}>
|
||||
<Filter isFilterVisible={isFilterVisible} filters={filters} setFilters={setFilters}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ToolBar;
|
||||
@@ -0,0 +1,107 @@
|
||||
.eventsButtons {
|
||||
overflow: visible;
|
||||
position: fixed;
|
||||
top: 10rem;
|
||||
right: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.filterContainer{
|
||||
padding: 0;
|
||||
border-radius: 30px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.filterMenu{
|
||||
padding: 5px 5px 5px 5px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.createButton, .searchButton, .sortButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
border-radius: 50% !important;
|
||||
width: 55px !important;
|
||||
height: 55px !important;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.createButton {
|
||||
font-size: 2rem;
|
||||
color: #019AFF;
|
||||
}
|
||||
|
||||
.createButton:hover, .searchButton:hover, .mobileFilter:hover {
|
||||
transform: translateY(-5px);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
.searchBarOverlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 100;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
.searchBarContainer{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.sortButton svg, .searchButton svg {
|
||||
transform: scale(0.5);
|
||||
fill: #019AFF;
|
||||
}
|
||||
|
||||
|
||||
.sortButton:focus, .searchButton:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.createButton,
|
||||
.searchButton,
|
||||
.sortButton {
|
||||
flex: 0 0 55px;
|
||||
width: 55px !important;
|
||||
height: 55px !important;
|
||||
}
|
||||
|
||||
|
||||
.eventsButtons {
|
||||
flex-direction: row;
|
||||
position: relative;
|
||||
top: 0;
|
||||
right: 0;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.filterContainer{
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.filterMenu{
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.createButton:hover, .searchButton:hover, .mobileFilter:hover {
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import styles from "./background.module.css";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
const Background = ({ children, className }) => {
|
||||
const circlesRef = useRef([]);
|
||||
|
||||
const circlesRef = useRef([]);
|
||||
const [theme, setTheme] = useState("light");
|
||||
|
||||
const getRandomTranslation = () => {
|
||||
const randomX = Math.floor(Math.random() * 100) - 30;
|
||||
@@ -20,8 +22,23 @@ const Background = ({ children, className }) => {
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={`${styles["background-container"]} ${className || ''}`}>
|
||||
useEffect(() => {
|
||||
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
|
||||
ref={(el) => (circlesRef.current[0] = el)}
|
||||
className={`${styles["circle"]} ${styles["circle-1"]}`}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
/* BACKGROUND ////////////////////////////////////////////////////////*/
|
||||
.background-container {
|
||||
.backgroundContainer {
|
||||
z-index: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-height: 100vh;
|
||||
background: #00F0FF;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
@@ -13,6 +12,14 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.backgroundLight {
|
||||
background: #00F0FF;
|
||||
}
|
||||
|
||||
.backgroundDark {
|
||||
background: #001C83;
|
||||
}
|
||||
|
||||
.circle {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
@@ -82,7 +89,6 @@
|
||||
.circle-1 {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
background-color: #019AFF;
|
||||
top: -5%;
|
||||
left: -5%;
|
||||
}
|
||||
@@ -90,7 +96,6 @@
|
||||
.circle-2 {
|
||||
width: 175px;
|
||||
height: 175px;
|
||||
background-color: #91FCFF;
|
||||
top: 58%;
|
||||
left: 10%;
|
||||
}
|
||||
@@ -98,7 +103,6 @@
|
||||
.circle-3 {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
background-color: #91FCFF;
|
||||
top: 29%;
|
||||
right: 9%;
|
||||
}
|
||||
@@ -106,7 +110,6 @@
|
||||
.circle-4 {
|
||||
width: 250px;
|
||||
height: 250px;
|
||||
background-color: #019AFF;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
@@ -114,7 +117,6 @@
|
||||
.circle-5 {
|
||||
width: 240px;
|
||||
height: 240px;
|
||||
background-color: #015AFF;
|
||||
top: 2%;
|
||||
right: 10%;
|
||||
}
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
/*import { useState } from "react";
|
||||
import login from "../../utils/login.js";
|
||||
import getUser from "../../utils/getUser.js";
|
||||
import styles from "./form.module.css";
|
||||
function LoginForm({ className }) {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
async function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
await login(email, password);
|
||||
getUser();
|
||||
}
|
||||
return (
|
||||
<div className={`${className} ${"glassCard"}`}>
|
||||
<div className={styles.formgroup}>
|
||||
<input type="email" id="email" value={email} placeholder="Adresse mail" onChange={e => setEmail(e.target.value)} required/>
|
||||
</div>
|
||||
|
||||
<div className={styles.formgroup}>
|
||||
<input type="password" id="password" value={password} placeholder="Mot de passe" onChange={e => setPassword(e.target.value)} required/>
|
||||
<br/>
|
||||
</div>
|
||||
<div className={styles.logButton} >
|
||||
<button className={styles.loginButton} onClick={e => handleSubmit(e)}>Se connecter</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default LoginForm;*/
|
||||
|
||||
import { useState } from "react";
|
||||
import login from "../../utils/login.js";
|
||||
import getUser from "../../utils/getUser.js";
|
||||
import styles from "./form.module.css";
|
||||
|
||||
function LoginForm({ className }) {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
async function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
await login(email, password);
|
||||
getUser();
|
||||
}
|
||||
|
||||
const toggleShowPassword = () => {
|
||||
setShowPassword(!showPassword);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${className} ${"glassCard"}`}>
|
||||
<div className={styles.formgroup}>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
className={styles.input}
|
||||
value={email}
|
||||
placeholder="Adresse mail"
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.formgroup}>
|
||||
<div style={{ position: 'relative', width: '100%' }}>
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
id="password"
|
||||
className={styles.input}
|
||||
value={password}
|
||||
placeholder="Mot de passe"
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
required
|
||||
style={{ width: '100%', paddingRight: '40px' }}
|
||||
/>
|
||||
<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 className={styles.logButton}>
|
||||
<button className={styles.loginButton} onClick={handleSubmit}>Se connecter</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LoginForm;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useState, useContext } from "react";
|
||||
import styles from "./loginForm.module.css";
|
||||
import Button from "../ui/button/button.jsx";
|
||||
import Input from "../ui/input/input.jsx";
|
||||
import { useNavigate } from "react-router";
|
||||
import { AuthContext } from "../../contexts/auth/AuthContext.js";
|
||||
|
||||
|
||||
function LoginForm({ className }) {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
let navigate = useNavigate();
|
||||
const { login } = useContext(AuthContext);
|
||||
|
||||
async function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
await login(email, password);
|
||||
navigate("/");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${className} glassCard ${styles.inputContainer}`}>
|
||||
<div className={styles.formgroup}>
|
||||
<Input
|
||||
type="email"
|
||||
value={email}
|
||||
placeholder="Adresse mail"
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.formgroup}>
|
||||
<Input password={true} onChange={e => setPassword(e.target.value)} value={password} placeholder={"Mot de passe"} />
|
||||
</div>
|
||||
|
||||
<div className={styles.logButton}>
|
||||
<Button variant={"transparent"} onClick={() => navigate("/register")}>Vous n'avez pas de compte ?</Button>
|
||||
<Button onClick={handleSubmit} variant={"default"}> Se connecter </Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LoginForm;
|
||||
|
||||
@@ -18,14 +18,21 @@ input::placeholder{
|
||||
font-size: 20px;
|
||||
color: rgba(0,0,0,0.45);
|
||||
}
|
||||
|
||||
.formgroup{
|
||||
margin-top:23px;
|
||||
}
|
||||
|
||||
.logButton{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.formgroup{
|
||||
margin-top:23px;
|
||||
|
||||
.inputContainer {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
/* style scoped (module) */
|
||||
@@ -1,6 +1,6 @@
|
||||
import styles from "./button.module.css";
|
||||
|
||||
const Button = ({ children, variant = "primary", onClick }) => {
|
||||
const Button = ({ children, variant = "primary", onClick, className }) => {
|
||||
let btnStyle;
|
||||
|
||||
if (variant === "primary") btnStyle = styles.primary;
|
||||
@@ -8,7 +8,7 @@ const Button = ({ children, variant = "primary", onClick }) => {
|
||||
else if(variant === "transparent") btnStyle = styles.transparent;
|
||||
else btnStyle = styles.default;
|
||||
|
||||
return <div className={`${styles.btn} ${btnStyle}`} onClick={onClick}>
|
||||
return <div className={`${styles.btn} ${btnStyle} ${className}`} onClick={onClick}>
|
||||
{children}
|
||||
</div>;
|
||||
};
|
||||
|
||||
@@ -1,13 +1,38 @@
|
||||
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;
|
||||
if (borderStyle === "square") inputBorderStyle = styles.square;
|
||||
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={placeholder}
|
||||
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;
|
||||
@@ -30,3 +30,23 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { createPortal } from "react-dom";
|
||||
import styles from "./modal.module.css";
|
||||
import Button from "../button/button.jsx";
|
||||
|
||||
const Modal = ({ open, onClose, children, title }) => {
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const handleOverlayClick = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleModalClick = (e) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
return createPortal(
|
||||
<div className={styles.overlay} onClick={handleOverlayClick}>
|
||||
<div className={styles.modal} onClick={handleModalClick}>
|
||||
{title && <h2>{title}</h2>}
|
||||
{children}
|
||||
<div className={styles.modalFooter}>
|
||||
<Button onClick={onClose}>Fermer</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
|
||||
export default Modal;
|
||||
@@ -0,0 +1,38 @@
|
||||
.overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
align-items: flex-start;
|
||||
overflow-y: auto;
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.modal {
|
||||
padding: 20px 20px 20px 20px;
|
||||
border-radius: 8px;
|
||||
min-width: 300px;
|
||||
max-width: 70%;
|
||||
max-height: none;
|
||||
position: relative;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
||||
width: clamp(300px, 50vw, 600px);
|
||||
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,32 @@
|
||||
import Button from "../button/button.jsx";
|
||||
import { useState, useEffect } from "react";
|
||||
import styles from "./ThemeSwitcher.module.css";
|
||||
|
||||
|
||||
export default function ThemeSwitcher() {
|
||||
|
||||
const [theme, setTheme] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const t = localStorage.getItem("theme");
|
||||
if(t) setTheme(t);
|
||||
else setTheme("light");
|
||||
}, []);
|
||||
|
||||
const switchTheme = () => {
|
||||
|
||||
let newTheme = theme === "light" ? "dark" : "light";
|
||||
|
||||
if(theme === null || theme === "") newTheme = "dark";
|
||||
|
||||
localStorage.setItem("theme", newTheme);
|
||||
setTheme(newTheme);
|
||||
|
||||
window.dispatchEvent(new Event("theme-changed"));
|
||||
};
|
||||
|
||||
return <Button variant={"default"} onClick={switchTheme} className={styles.themeBtn}>
|
||||
<img src={`/icons/theme/${theme}.svg`} alt={`${theme} icon`} className={styles.themeIcon} />
|
||||
<p>Changer de thème </p>
|
||||
</Button>
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
.themeBtn {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.themeIcon {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { createContext } from "react";
|
||||
|
||||
export const AuthContext = createContext(null);
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import loginFunction from "../../utils/login.js";
|
||||
import getUser from "../../utils/getUser.js";
|
||||
import { AuthContext } from "./AuthContext";
|
||||
import logoutFunction from "../../utils/auth/logout.js"
|
||||
|
||||
|
||||
export function AuthProvider({ children }) {
|
||||
const [user, setUser] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const u = await getUser();
|
||||
setUser(u);
|
||||
setLoading(false);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const login = async (email, password) => {
|
||||
const res = await loginFunction(email, password);
|
||||
if (!res || res.status !== 200) throw new Error("Login error");
|
||||
const u = await getUser();
|
||||
setUser(u);
|
||||
return res;
|
||||
};
|
||||
|
||||
const logout = async () => {
|
||||
await logoutFunction();
|
||||
setUser(null);
|
||||
};
|
||||
|
||||
const update = async () => {
|
||||
const u = await getUser();
|
||||
setUser(u);
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ user, loading, login, logout, update }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
+9
-5
@@ -1,8 +1,10 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
@@ -17,16 +19,19 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.content{
|
||||
padding: 0.7rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.content {
|
||||
padding: 1.5rem;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
@@ -45,7 +50,6 @@ body {
|
||||
|
||||
/*.glassCard {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
height: fit-content;
|
||||
background: rgba(255, 255, 255, 0.65);
|
||||
backdrop-filter: blur(3px);
|
||||
|
||||
@@ -2,10 +2,18 @@ import { Outlet } from "react-router";
|
||||
import Footer from "./components/Footer/Footer.jsx"
|
||||
import Header from "./components/Header/Header.jsx";
|
||||
import Background from "./components/background/background.jsx";
|
||||
import { Navigate } from "react-router";
|
||||
import { useContext} from "react";
|
||||
import { AuthContext } from "./contexts/auth/AuthContext.js";
|
||||
|
||||
|
||||
function Layout(){
|
||||
|
||||
const { user, loading } = useContext(AuthContext);
|
||||
|
||||
if (loading) return <p>Loading</p>;
|
||||
if (!user) return <Navigate to="/login" />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Background>
|
||||
|
||||
+4
-1
@@ -3,10 +3,13 @@ import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import { RouterProvider } from "react-router";
|
||||
import router from './router';
|
||||
import { AuthProvider } from "./contexts/auth/authProvider.jsx";
|
||||
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<RouterProvider router={router} />
|
||||
<AuthProvider>
|
||||
<RouterProvider router={router} />
|
||||
</AuthProvider>
|
||||
</StrictMode>,
|
||||
)
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Event from "../../components/Event/Event.jsx";
|
||||
import styles from "./EventsPage.module.css";
|
||||
import ToolBar from "../../components/ToolBar/ToolBar.jsx";
|
||||
import getAllEvents from "../../utils/event/getAllEvents.js";
|
||||
import filter from "../../utils/filter.js";
|
||||
|
||||
|
||||
function EventsPage(){
|
||||
const [filters, setFilters] = useState({
|
||||
alphabetical: "asc",
|
||||
yearOrder: "desc",
|
||||
});
|
||||
|
||||
|
||||
const [events, setEvents] = useState([]);
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const data = await getAllEvents();
|
||||
setEvents(data.data);
|
||||
}) ()
|
||||
}, []);
|
||||
|
||||
|
||||
return (
|
||||
<div className={styles.eventsContainer}>
|
||||
<ToolBar setFilters={setFilters} filters={filters}/>
|
||||
<div className={styles.eventsList}>
|
||||
{filter(events, filters, "event").map((event, index) => (
|
||||
<Event key={index} event={event} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EventsPage;
|
||||
@@ -0,0 +1,11 @@
|
||||
.eventsContainer {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
.eventsList{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
+173
-3
@@ -1,11 +1,181 @@
|
||||
import { useState } from "react";
|
||||
import { useMemo } from "react";
|
||||
import styles from "./HomePage.module.css";
|
||||
|
||||
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"],
|
||||
};
|
||||
|
||||
function HomePage(){
|
||||
const monthNames = [
|
||||
"Janvier", "Février", "Mars", "Avril", "Mai", "Juin",
|
||||
"Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"
|
||||
];
|
||||
|
||||
const today = new Date();
|
||||
const [currentMonth, setCurrentMonth] = useState(today.getMonth());
|
||||
const [currentYear, setCurrentYear] = useState(today.getFullYear());
|
||||
const [selectedDay, setSelectedDay] = useState(() => {
|
||||
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 (
|
||||
<div>
|
||||
<div className={styles.allContent}>
|
||||
<div className={styles.glassCard}>
|
||||
<div className={styles.calendarContainer}>
|
||||
<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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default HomePage;
|
||||
@@ -1,3 +1,173 @@
|
||||
.title {
|
||||
color: red;
|
||||
.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{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.glassCard {
|
||||
width: 100%;
|
||||
margin: 0px;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
||||
.allContent{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,29 @@
|
||||
import styles from "./LoginPage.module.css";
|
||||
import LoginForm from "../../components/form/form.jsx";
|
||||
import LoginForm from "../../components/loginform/loginForm.jsx";
|
||||
import Background from "../../components/background/background.jsx";
|
||||
import { useEffect, useContext } from "react";
|
||||
import { AuthContext } from "../../contexts/auth/AuthContext.js";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
function LoginPage() {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<LoginForm />
|
||||
</div>
|
||||
);
|
||||
|
||||
const { user, loading } = useContext(AuthContext)
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && user) {
|
||||
navigate("/");
|
||||
}
|
||||
}, [loading, user]);
|
||||
|
||||
return (
|
||||
<Background>
|
||||
<div className={styles.container}>
|
||||
<h1 className={styles.title}>Connexion</h1>
|
||||
<LoginForm />
|
||||
</div>
|
||||
</Background>
|
||||
);
|
||||
}
|
||||
|
||||
export default LoginPage;
|
||||
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
.container {
|
||||
position: relative;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 600px;
|
||||
overflow: hidden;
|
||||
max-height: 80%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
border-radius:12px;
|
||||
border-radius: 12px;
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import React, { useContext } from "react";
|
||||
import styles from "./ProfilePage.module.css";
|
||||
import { AuthContext } from "../../contexts/auth/AuthContext.js";
|
||||
import formatDate from "../../utils/date/formatDate.js";
|
||||
import Task from "../../components/Task/Task";
|
||||
import SettingsModal from "../../components/SettingsModal/SettingsModal.jsx";
|
||||
|
||||
function ProfilePage() {
|
||||
|
||||
const { user } = useContext(AuthContext);
|
||||
|
||||
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}</p>
|
||||
</div>
|
||||
<SettingsModal />
|
||||
</div>
|
||||
|
||||
<h2>Vos Événements :</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>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProfilePage;
|
||||
@@ -0,0 +1,164 @@
|
||||
.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;
|
||||
}
|
||||
|
||||
/* Desktop (1024px et plus) */
|
||||
@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,134 @@
|
||||
import styles from "./RegisterPage.module.css";
|
||||
import Background from "../../components/background/background.jsx";
|
||||
import Input from "../../components/ui/input/input.jsx";
|
||||
import Button from "../../components/ui/button/button.jsx";
|
||||
import {useContext, useEffect, useState} from "react";
|
||||
import register from "../../utils/register.js";
|
||||
import { useNavigate } from "react-router";
|
||||
import { AuthContext } from "../../contexts/auth/AuthContext.js";
|
||||
import Modal from "../../components/ui/modal/modal.jsx";
|
||||
|
||||
|
||||
function RegisterPage() {
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [email, setEmail] = useState("");
|
||||
const [phone, setPhone] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [name, setName] = useState("");
|
||||
const [lastName, setLastName] = useState("");
|
||||
const [open, setOpen] = useState(false);
|
||||
const [title, setTitle] = useState("");
|
||||
const [message, setMessage] = useState("");
|
||||
|
||||
const { user, loading } = useContext(AuthContext)
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && user) {
|
||||
navigate("/");
|
||||
}
|
||||
}, [loading, user]);
|
||||
|
||||
const handleModal = (t, m) => {
|
||||
setTitle(t)
|
||||
setMessage(m)
|
||||
setOpen(true);
|
||||
}
|
||||
|
||||
async function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if(!(password === confirmPassword)) handleModal("Mot de passe invalide", "Les mots de passes ne conrrespondent pas");
|
||||
else if(password.length < 8) handleModal("Mot de passe invalide", "Le mot de passe doit faire plus de 8 charactères");
|
||||
else {
|
||||
|
||||
const res = await register(email, password, name, lastName, phone);
|
||||
|
||||
if (res.status === 422) {
|
||||
if (res.errors.email) handleModal("Adresse Email invalide", "Le formet de votre adresse Email n'est pas valide");
|
||||
if (res.errors.phone) handleModal("Numéro de téléphone invalide", "Le formet de votre numéro de téléphone n'est pas valide");
|
||||
} else navigate("/login");
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const handlePhone = (e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "" || /^[0-9 +]+$/.test(value)) {
|
||||
setPhone(value);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return <Background>
|
||||
|
||||
<div className={styles.container}>
|
||||
|
||||
<h1 className={styles.title}>Créer un compte</h1>
|
||||
|
||||
<div className={`glassCard ${styles.inputContainer}`}>
|
||||
|
||||
<div className={styles.formGroup}>
|
||||
<Input
|
||||
value={name}
|
||||
placeholder="Prénom"
|
||||
onChange={e => setName(e.target.value)}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.formGroup}>
|
||||
<Input
|
||||
value={lastName}
|
||||
placeholder="Nom"
|
||||
onChange={e => setLastName(e.target.value)}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.formGroup}>
|
||||
<Input
|
||||
type="email"
|
||||
value={email}
|
||||
placeholder="Adresse mail"
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.formGroup}>
|
||||
<Input
|
||||
value={phone}
|
||||
placeholder="Numéro de téléphone"
|
||||
onChange={handlePhone}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.formGroup}>
|
||||
<Input password={true} onChange={e => setPassword(e.target.value)} value={password} placeholder={"Mot de passe"}/>
|
||||
</div>
|
||||
|
||||
<div className={styles.formGroup}>
|
||||
<Input password={true} onChange={e => setConfirmPassword(e.target.value)} value={confirmPassword} placeholder={"Confirmer le mot de passe"} />
|
||||
</div>
|
||||
|
||||
<div className={styles.logButton}>
|
||||
<Button variant={"transparent"} onClick={() => navigate("/login")}>Vous avez déjà un compte ?</Button>
|
||||
<Button variant={"default"} onClick={handleSubmit}> Se connecter </Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal open={open} onClose={() => setOpen(false)} title={title}>
|
||||
<p>{message}</p>
|
||||
</Modal>
|
||||
|
||||
</Background>
|
||||
}
|
||||
|
||||
export default RegisterPage;
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
.container {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 30%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
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) {
|
||||
--glass-bg: #ffffff9f;
|
||||
}
|
||||
.logButton{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.formGroup{
|
||||
margin-top:23px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
overflow: visible;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.inputContainer {
|
||||
padding: 15px;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useState } from "react";
|
||||
import styles from "./StatPage.module.css";
|
||||
// Importez les composants avec une majuscule
|
||||
import ParticipationChart from "../../components/chart/ParticipationChart.jsx";
|
||||
import IncompleteEvents from "../../components/IncompleteEvent/IncompleteEvent.jsx";
|
||||
import PendingMembers from "../../components/PendingMembers/PendingMembers.jsx";
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import React, {useState} from "react";
|
||||
import styles from "./VolunteersPage.module.css";
|
||||
import formatDate from "../../utils/date/formatDate.js";
|
||||
import { useNavigate } from "react-router";
|
||||
import Button from "../../components/ui/button/button.jsx";
|
||||
|
||||
export default function VolunteerCard({ user }) {
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const toggleExpand = () => {
|
||||
setIsExpanded(!isExpanded);
|
||||
};
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className={`${styles.profileboard} glassCard`}>
|
||||
|
||||
<div className={styles.header}>
|
||||
<h2>{user?.name} {user?.lastname}</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/${user.id}`)}
|
||||
variant="primary"
|
||||
>
|
||||
Voir plus
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import styles from "./VolunteersPage.module.css";
|
||||
import VolunteerCard from "./VolunteerCard.jsx";
|
||||
import getAllUser from "../../utils/users/getAllUsers.js";
|
||||
import ToolBar from "../../components/ToolBar/ToolBar.jsx";
|
||||
import filter from "../../utils/filter.js";
|
||||
import getUserById from "../../utils/users/getUserById.js";
|
||||
|
||||
|
||||
function VolunteerPage() {
|
||||
const [filters, setFilters] = useState({
|
||||
alphabetical: "asc",
|
||||
yearOrder: "desc",
|
||||
});
|
||||
const [users, setUsers] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const ids = await getAllUser();
|
||||
const usersData = await Promise.all(ids.map(id => getUserById(id)));
|
||||
setUsers(usersData);
|
||||
})()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<ToolBar setFilters={setFilters} filters={filters}/>
|
||||
<div className={styles.usersList}>
|
||||
{filter(users, filters, "user").map((user, index) => (
|
||||
<VolunteerCard key={index} user={user} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default VolunteerPage;
|
||||
@@ -0,0 +1,119 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.profileboard {
|
||||
width: 80%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
box-sizing: border-box;
|
||||
margin: 1rem 0;
|
||||
padding: 1rem 0.5rem;
|
||||
}
|
||||
|
||||
.usersList{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.header h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
.mainContent {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.basicInfo {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.expandedContent {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.textInfo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.contactInfo {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.contactInfo p {
|
||||
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 {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: none;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.moreButtonClicked {
|
||||
rotate: 90deg;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.profilePictureContainer {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
.container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.profileboard {
|
||||
padding: 25px;
|
||||
}
|
||||
|
||||
.profilePictureContainer {
|
||||
width: 200px;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
+27
-7
@@ -3,9 +3,21 @@ import Layout from "./layout.jsx";
|
||||
import HomePage from "./pages/Home/HomePage";
|
||||
import LoginPage from "./pages/Login/LoginPage";
|
||||
import StatPage from "./pages/Stat/StatPage";
|
||||
import testPage from "./pages/Stat/test.jsx"
|
||||
import RegisterPage from "./pages/Register/RegisterPage.jsx";
|
||||
import VolunteersPage from "./pages/Volunteers/VolunteersPage";
|
||||
import ProfilePage from "./pages/Profile/ProfilePage";
|
||||
import EventsPage from "./pages/Events/EventsPage.jsx";
|
||||
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: "/login",
|
||||
Component: LoginPage,
|
||||
},
|
||||
{
|
||||
path: "/register",
|
||||
Component: RegisterPage,
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
Component: Layout,
|
||||
@@ -15,17 +27,25 @@ const router = createBrowserRouter([
|
||||
Component: HomePage,
|
||||
},
|
||||
{
|
||||
path: "/login",
|
||||
Component: LoginPage,
|
||||
path: "/admin",
|
||||
Component:StatPage,
|
||||
},
|
||||
{
|
||||
path:"/profile",
|
||||
Component:ProfilePage,
|
||||
},
|
||||
{
|
||||
path: "/events",
|
||||
Component: EventsPage,
|
||||
},
|
||||
{
|
||||
path: "/volunteers",
|
||||
Component: VolunteersPage,
|
||||
},
|
||||
{
|
||||
path: "/stat",
|
||||
Component:StatPage,
|
||||
},
|
||||
{
|
||||
path: "/test",
|
||||
Component:testPage,
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
@@ -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>,
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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}`;
|
||||
}
|
||||
@@ -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);
|
||||
})
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
function filter(elements, filters, type) {
|
||||
const compareDate = (dateA, dateB) => {
|
||||
const dA = new Date(dateA);
|
||||
const dB = new Date(dateB);
|
||||
|
||||
if (filters.yearOrder === "asc") {
|
||||
return dA - dB; // plus ancien -> plus récent
|
||||
} else {
|
||||
return dB - dA; // plus récent -> plus ancien
|
||||
}
|
||||
};
|
||||
|
||||
return [...elements].sort((a, b) => {
|
||||
let timeComparison;
|
||||
|
||||
if (filters.yearOrder !== null) {
|
||||
if (type === "event") {
|
||||
timeComparison = compareDate(a.start, b.start);
|
||||
}
|
||||
|
||||
else if (type === "user") {
|
||||
timeComparison = compareDate(a.created_at, b.created_at);
|
||||
}
|
||||
|
||||
if (timeComparison !== 0 && timeComparison !== undefined) {
|
||||
return timeComparison;
|
||||
}
|
||||
}
|
||||
|
||||
if (filters.alphabetical !== null) {
|
||||
return filters.alphabetical === "asc"
|
||||
? a.name.localeCompare(b.name)
|
||||
: b.name.localeCompare(a.name);
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
export default filter;
|
||||
+19
-10
@@ -1,12 +1,21 @@
|
||||
export default async function getUser() {
|
||||
await fetch('http://localhost:8000/api/me', {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
try {
|
||||
const res = await fetch(`http://${import.meta.env.VITE_API_URL}/api/users/me`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Erreur serveur : ${res.status}`);
|
||||
}
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => console.log('Utilisateur connecté :', data))
|
||||
.catch(err => console.error('Erreur :', err));
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
return data;
|
||||
} catch (err) {
|
||||
console.error('Erreur :', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
export default async function getUserNotifications() {
|
||||
try {
|
||||
const response = await fetch(
|
||||
'http://localhost:8000/api/users/1/notifications',
|
||||
'http://localhost:80/api/users/1/notifications',
|
||||
{
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export default async function getXSRFToken() {
|
||||
const response = await fetch('http://localhost:8000/sanctum/csrf-cookie', {
|
||||
const response = await fetch(`http://${import.meta.env.VITE_API_URL}/sanctum/csrf-cookie`, {
|
||||
method: 'GET',
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
+25
-27
@@ -3,31 +3,29 @@ import getXSRFToken from "./getXSRF.js";
|
||||
export default async function login(email, password) {
|
||||
const csrfToken = await getXSRFToken();
|
||||
|
||||
|
||||
await fetch('http://localhost:8000/api/users/login', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-XSRF-TOKEN': csrfToken,
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: email,
|
||||
password: password
|
||||
})
|
||||
})
|
||||
.then(response => {
|
||||
console.log('Code HTTP :', response.status);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Erreur HTTP ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log('Connexion réussie ! Données utilisateur :', data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Erreur lors de la connexion :', error.message);
|
||||
try {
|
||||
const response = await fetch(`http://${import.meta.env.VITE_API_URL}/api/users/login`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-XSRF-TOKEN': csrfToken,
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ email, password })
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Code HTTP :', response.status);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Erreur HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Connexion réussie ! Données utilisateur :', data);
|
||||
|
||||
return { status: response.status, data };
|
||||
} catch (error) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import getXSRFToken from "./getXSRF.js";
|
||||
|
||||
export default async function register(email, password, name, lastname, phone) {
|
||||
const csrfToken = await getXSRFToken();
|
||||
|
||||
try {
|
||||
const response = await fetch(`http://${import.meta.env.VITE_API_URL}/api/users/register`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-XSRF-TOKEN': csrfToken,
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ email, password, name, lastname, phone }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 422) {
|
||||
return {
|
||||
status: 422,
|
||||
errors: data.errors
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Erreur HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return { status: response.status, data };
|
||||
|
||||
} catch (error) {
|
||||
return { status: "error", error };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
export default async function searchEvents(query) {
|
||||
try {
|
||||
const encodedQuery = encodeURIComponent(query);
|
||||
const response = await fetch(
|
||||
`http://${import.meta.env.VITE_API_URL}/api/events/search?query=${encodedQuery}`,
|
||||
{
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
}
|
||||
);
|
||||
const data = await response.json();
|
||||
return data.hits.map(hit => hit.document);
|
||||
|
||||
} catch (err) {
|
||||
console.error("Erreur :", err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
export default async function searchUsers(query) {
|
||||
try {
|
||||
const encodedQuery = encodeURIComponent(query);
|
||||
const response = await fetch(
|
||||
`http://localhost:80/api/users/search?query=${encodedQuery}`,
|
||||
{
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
}
|
||||
);
|
||||
const data = await response.json();
|
||||
return data.hits.map(hit => hit.document);
|
||||
|
||||
} catch (err) {
|
||||
console.error("Erreur :", err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import getXSRFToken from "../getXSRF.js";
|
||||
|
||||
export default async function updateUser(name, lastname, phone) {
|
||||
|
||||
const csrfToken = await getXSRFToken();
|
||||
|
||||
try {
|
||||
const res = await fetch(`http://${import.meta.env.VITE_API_URL}/api/users/update`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'X-XSRF-TOKEN': csrfToken,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: name,
|
||||
phone: phone,
|
||||
lastname: lastname,
|
||||
})
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Erreur serveur : ${res.status}`);
|
||||
}
|
||||
|
||||
return await res.json();
|
||||
} catch (err) {
|
||||
console.error('Erreur :', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user