resolve conflict

This commit is contained in:
p2405951
2025-12-10 14:59:58 +01:00
84 changed files with 2825 additions and 972 deletions
+84
View File
@@ -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;
+127
View File
@@ -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;
}
}
+61
View File
@@ -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;
+85
View File
@@ -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;
}
}
+3 -3
View File
@@ -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>
+1 -1
View File
@@ -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 {
+103
View File
@@ -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;
}
+29
View File
@@ -0,0 +1,29 @@
import React, {useEffect, useState} from "react";
import getEventById from "../../utils/event/getEventById.js";
import styles from "./Task.module.css";
import formatDate from "../../utils/date/formatDate.js";
const Task = ({ task }) => {
const [event, setEvent] = useState(null);
useEffect(() => {
(async () => {
const e = await getEventById(task.id);
setEvent(e);
}) ()
}, [])
return (
<div className={`${styles.task} glassCard`}>
<div className={styles.content}>
<div>
<h2> {event && event.name} - {task.name}</h2>
<p className={styles.desc}> {task.description} </p>
<p className={styles.date}> {formatDate(task.start)} </p>
</div>
</div>
</div>
);
}
export default Task;
+22
View File
@@ -0,0 +1,22 @@
.task {
width: 100%;
padding: 12px;
margin-bottom: 10px;
border-radius: 8px;
}
.content {
display: flex;
justify-content: space-between;
flex-direction: column;
}
.desc {
font-size: 0.8rem;
color: #666;
margin: 0 0 0 0;
}
.date {
font-size: 0.8rem;
}
+66
View File
@@ -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;
+107
View File
@@ -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;
}
}
+21 -4
View File
@@ -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%;
}
-103
View File
@@ -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;
+45
View File
@@ -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) */
+2 -2
View File
@@ -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>;
};
+27 -2
View File
@@ -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;
+20
View File
@@ -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;
}
+31
View File
@@ -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;
+38
View File
@@ -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;
}