Merge branch 'refactor/ToolBar' into 'dev'

Refactor/toolbar

See merge request sae-but2/2025-26/gestion-benevoles-association/Frontend!80
This commit is contained in:
T'JAMPENS QUENTIN p2406187
2026-02-24 13:07:38 +00:00
10 changed files with 285 additions and 210 deletions
-56
View File
@@ -1,56 +0,0 @@
import styles from "./Filter.module.css";
import Button from "../ui/button/button.tsx"
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}`}
variant={"default"}
onClick={() => setAlphabeticalOrder("asc")}>
<img src={"sortByAlpha.svg"} alt="Tri alphabétique croissant (A à Z)"/>
</Button>
</abbr>
<abbr title="Tri alphabétique décroissant (Z à A)">
<Button className={`${styles.filterTag} glassCard ${filters.alphabetical !== "desc" ? "" : styles.active}`}
variant={"default"}
onClick={() => setAlphabeticalOrder("desc")}>
<img src={"sortByAntiAlpha.svg"} alt="Tri alphabétique décroissant (Z à A)"/>
</Button>
</abbr>
<abbr title="Tri chronologique décroissant (du plus récent au plus ancien)">
<Button className={`${styles.filterTag} glassCard ${filters.yearOrder !== "desc" ? "" : styles.active}`}
variant={"default"}
onClick={() => setYearOrder("desc")}>
<img src={"timerArrowUp.svg"} alt="Tri chronologique décroissant (du plus récent au plus ancien)"/>
</Button>
</abbr>
<abbr title="Tri chronologique croissant (du plus ancien au plus récent)">
<Button className={`${styles.filterTag} glassCard ${filters.yearOrder !== "asc" ? "" : styles.active}`}
variant={"default"}
onClick={() => setYearOrder("asc")}>
<img src={"timerArrowDown.svg"} alt="Tri chronologique croissant (du plus ancien au plus récent)"/>
</Button>
</abbr>
</div>
)
}
export default Filter;
+78
View File
@@ -0,0 +1,78 @@
import styles from "./Filter.module.css";
import Button from "../ui/button/button";
import React from "react";
type FiltersType = {
alphabetical: "asc" | "desc" | null;
yearOrder: "asc" | "desc" | null;
[key: string]: any;
};
type FilterProps = {
isFilterVisible: boolean;
filters: FiltersType;
setFilters: React.Dispatch<React.SetStateAction<FiltersType>>;
};
function Filter({ isFilterVisible, filters, setFilters }: FilterProps): any {
const setAlphabeticalOrder = (order: "asc" | "desc"): void => {
setFilters((prev) => ({
...prev,
alphabetical: prev.alphabetical === order ? null : order
}));
};
const setYearOrder = (order: "asc" | "desc"): void => {
setFilters((prev) => ({
...prev,
yearOrder: prev.yearOrder === order ? null : order
}));
};
return (
<div className={`${styles.filterContainer} ${isFilterVisible ? styles.filterOpen : styles.filterClose}`}>
<abbr title="Tri alphabétique croissant (A à Z)">
<Button
className={`${styles.filterTag} glassCard ${filters.alphabetical !== "asc" ? "" : styles.active}`}
variant={"default"}
onClick={() => setAlphabeticalOrder("asc")}
>
<img src={"sortByAlpha.svg"} alt="Tri alphabétique croissant (A à Z)" />
</Button>
</abbr>
<abbr title="Tri alphabétique décroissant (Z à A)">
<Button
className={`${styles.filterTag} glassCard ${filters.alphabetical !== "desc" ? "" : styles.active}`}
variant={"default"}
onClick={() => setAlphabeticalOrder("desc")}
>
<img src={"sortByAntiAlpha.svg"} alt="Tri alphabétique décroissant (Z à A)" />
</Button>
</abbr>
<abbr title="Tri chronologique décroissant (du plus récent au plus ancien)">
<Button
className={`${styles.filterTag} glassCard ${filters.yearOrder !== "desc" ? "" : styles.active}`}
variant={"default"}
onClick={() => setYearOrder("desc")}
>
<img src={"timerArrowUp.svg"} alt="Tri chronologique décroissant (du plus récent au plus ancien)" />
</Button>
</abbr>
<abbr title="Tri chronologique croissant (du plus ancien au plus récent)">
<Button
className={`${styles.filterTag} glassCard ${filters.yearOrder !== "asc" ? "" : styles.active}`}
variant={"default"}
onClick={() => setYearOrder("asc")}
>
<img src={"timerArrowDown.svg"} alt="Tri chronologique croissant (du plus ancien au plus récent)" />
</Button>
</abbr>
</div>
);
}
export default Filter;
-100
View File
@@ -1,100 +0,0 @@
import styles from "./SearchBar.module.css"
import {Link} from "react-router";
import {useEffect, useState} from "react";
import searchEvents from "../../utils/events/searchEvents.js";
import searchUsers from "../../utils/users/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()}>
<img src="search.svg" alt="Rechercher"/>
<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={`/events/` + 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;
@@ -1,6 +1,8 @@
.searchBarContainer{
position: fixed;
top: 4rem;
top: 20vh;
left: 50%;
transform: translateX(-50%);
z-index: 100;
width: 60%;
background: #fff;
+120
View File
@@ -0,0 +1,120 @@
import styles from "./SearchBar.module.css";
import { Link } from "react-router";
import { useEffect, useState } from "react";
import searchEvents from "../../utils/events/searchEvents.js";
import searchUsers from "../../utils/users/searchUsers.js";
import { useLocation } from "react-router";
function SearchBar(): any {
const [searchQuery, setSearchQuery] = useState<string>("");
const [searchResults, setSearchResults] = useState<any[]>([]);
const location: any = useLocation();
useEffect(() => {
const performSearch = async (query: string): Promise<void> => {
if (query.trim() === "") {
setSearchResults([]);
return;
}
let data: any;
if (location.pathname.includes("events")) {
data = await searchEvents(query);
} else if (location.pathname.includes("volunteers")) {
data = await searchUsers(query);
}
if (data) {
setSearchResults(data);
} else {
setSearchResults([]);
}
};
const handler: ReturnType<typeof setTimeout> = setTimeout(() => {
performSearch(searchQuery);
}, 300);
return () => {
clearTimeout(handler);
};
}, [searchQuery, location.pathname]);
return (
<div className={`${styles.searchBarContainer} glassCard`}>
<form
className={styles.searchBarForm}
onSubmit={(element: React.FormEvent<HTMLFormElement>) =>
element.preventDefault()
}
>
<img src="search.svg" alt="Rechercher" />
<input
type="text"
placeholder="Rechercher un événement"
value={searchQuery}
onChange={(element: React.ChangeEvent<HTMLInputElement>) =>
setSearchQuery(element.target.value)
}
/>
</form>
<div className={styles.lineContainer}>
{searchResults.length > 0 ? (
<div className={styles.line}></div>
) : null}
</div>
<div className={styles.elementList}>
{searchResults.map((result: any, i: number) =>
location.pathname.includes("events") ? (
<Link to={`/events/` + result.id} key={i}>
<div className={styles.searchedEvent}>
<div className={styles.eventImageDiv}>
{result.image == null ? (
<img
src="empty.png"
className={styles.eventImage}
alt="Pas de photo de l'événement"
/>
) : (
<img
src={result.image + `.png`}
alt="Photo de l'événement"
/>
)}
</div>
<p>{result.name}</p>
</div>
</Link>
) : location.pathname.includes("volunteers") ? (
<Link to={`/profile/` + result.id} key={i}>
<div className={styles.searchedEvent}>
<div className={styles.eventImageDiv}>
{result.image == null ? (
<img
src="empty.png"
className={styles.eventImage}
alt="Pas de photo de l'événement"
/>
) : (
<img
src={result.image + `.png`}
alt="Photo de l'événement"
/>
)}
</div>
<p>
{result.name} {result.lastname}
</p>
</div>
</Link>
) : null
)}
</div>
</div>
);
}
export default SearchBar;
+6 -49
View File
@@ -1,59 +1,16 @@
import styles from "./ToolBar.module.css"
import Filter from "../Filter/Filter.jsx";
import {useContext, useEffect, useState} from "react";
import SearchBar from "../SearchBar/SearchBar.jsx";
import CreateEventBtn from "../createEventBtn/CreateEventBtn.jsx";
import Button from "../ui/button/button.tsx";
import {AuthContext} from "../../contexts/auth/AuthContext.js";
import CreateEventBtn from "./tools/createEventBtn/CreateEventBtn.jsx";
import SearchBtn from "./tools/SearchBtn.tsx";
import FilterBtn from "./tools/FilterBtn.tsx";
function ToolBar({setFilters, filters, showCreate = true}) {
const { user } = useContext(AuthContext);
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}>
{showCreate && user.isAdmin ? (
<CreateEventBtn />
) : null}
<Button className={`${styles.searchButton} glassCard`} variant={"default"} onClick={() => setIsSearchVisible(true)}>
<img src="search.svg" alt="Rechercher"/>
</Button>
<div className={`${styles.filterContainer} ${isFilterVisible ? "glassCard" : ""}`}>
<Button className={`${isFilterVisible ? "" : styles.mobileFilter} ${styles.sortButton} glassCard`} variant={"default"} onClick={toggleFilters}>
<img src="filter.svg" alt="Filtrer"/>
</Button>
<div className={styles.filterMenu}>
<Filter isFilterVisible={isFilterVisible} filters={filters} setFilters={setFilters}/>
</div>
</div>
{showCreate && <CreateEventBtn />}
<SearchBtn />
<FilterBtn setFilters={setFilters} filters={filters} />
</div>
</div>
);
@@ -0,0 +1,32 @@
import styles from "../ToolBar.module.css"
import Button from "../../ui/button/button";
import { useState } from "react";
import Filter from "../../Filter/Filter";
interface Filters {
alphabetical: "asc" | "desc";
yearOrder: "asc" | "desc";
}
interface FilterBtnProps {
setFilters: any
filters: Filters
}
export default function FilterBtn({ setFilters, filters }: FilterBtnProps) {
const [isFilterVisible, setIsFilterVisible] = useState(false);
const toggleFilters = () => {
setIsFilterVisible(prev => !prev);
};
return <div className={`${styles.filterContainer} ${isFilterVisible ? "glassCard" : ""}`}>
<Button className={`${isFilterVisible ? "" : styles.mobileFilter} ${styles.sortButton} glassCard`} variant={"default"} onClick={toggleFilters}>
<img src="filter.svg" alt="Filtrer"/>
</Button>
<div className={styles.filterMenu}>
<Filter isFilterVisible={isFilterVisible} filters={filters} setFilters={setFilters}/>
</div>
</div>
}
@@ -0,0 +1,38 @@
import styles from "../ToolBar.module.css"
import Button from "../../ui/button/button";
import { useEffect, useState } from "react";
import SearchBar from "../../SearchBar/SearchBar";
export default function SearchBtn() {
const [isSearchVisible, setIsSearchVisible] = useState(false);
useEffect(() => {
if (isSearchVisible) {
document.documentElement.style.overflow = 'hidden';
} else {
document.documentElement.style.overflow = '';
}
return () => {
document.documentElement.style.overflow = '';
};
}, [isSearchVisible]);
return <>
{isSearchVisible ? (
<div className={styles.searchBarContainer}>
<div className={styles.searchBarOverlay} onClick={() => setIsSearchVisible(false)}></div>
<SearchBar/>
</div>
) : null}
<div>
<Button className={`${styles.searchButton} glassCard`} variant={"default"} onClick={() => setIsSearchVisible(true)}>
<img src="search.svg" alt="Rechercher"/>
</Button>
</div>
</>
}
@@ -1,14 +1,16 @@
import styles from "./createEventBtn.module.css"
import { useState, useContext } from "react";
import Modal from "../ui/modal/modal.tsx";
import Button from "../ui/button/button.tsx";
import TextInput from "../ui/input/input.tsx";
import { EventContext } from "../../contexts/events/EventContext.js";
import Modal from "../../../ui/modal/modal.tsx";
import Button from "../../../ui/button/button.tsx";
import TextInput from "../../../ui/input/input.tsx";
import { EventContext } from "../../../../contexts/events/EventContext.js";
import {AuthContext} from "../../../../contexts/auth/AuthContext.ts";
export default function CreateEventBtn() {
const { addEvent } = useContext(EventContext);
const { user } = useContext(AuthContext);
const [isOpen, setIsOpen] = useState(false);
const [name, setName] = useState("");
@@ -24,6 +26,8 @@ export default function CreateEventBtn() {
setIsOpen(false);
}
if(!user.isAdmin) return null;
return <>
<Button className={`${styles.createButton} glassCard`} variant={"default"} onClick={() => setIsOpen(true)}> + </Button>