Merge branch 'dev' into 'features/export'
# Conflicts: # src/pages/Home/components/Calendar/calendar.jsx
This commit is contained in:
Vendored
-7
@@ -1,7 +0,0 @@
|
|||||||
/// <reference types="vite/client" />
|
|
||||||
|
|
||||||
declare module "*.module.css" {
|
|
||||||
const classes: { readonly [key: string]: string };
|
|
||||||
export default classes;
|
|
||||||
}
|
|
||||||
// permet à TS de reconnaître les fichiers modules.css
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import styles from "./Filter.module.css";
|
|
||||||
import Button from "../ui/button/button.jsx"
|
|
||||||
|
|
||||||
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;
|
|
||||||
@@ -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;
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import styles from "./Footer.module.css"
|
import styles from "./Footer.module.css"
|
||||||
import {Link} from "react-router";
|
import {Link} from "react-router";
|
||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import Modal from "../ui/modal/modal.jsx";
|
import Modal from "../ui/modal/modal.tsx";
|
||||||
import Button from "../ui/button/button.jsx";
|
import Button from "../ui/button/button.tsx";
|
||||||
import scrollToTop from "../../utils/scrollToTop.js";
|
import scrollToTop from "../../utils/scrollToTop.js";
|
||||||
|
|
||||||
export default function Footer(){
|
export default function Footer(){
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { userNotificationsListener } from "../../utils/echo/listeners/userNotifi
|
|||||||
import NotificationCard from "../NotificationCard/NotificationCard.jsx";
|
import NotificationCard from "../NotificationCard/NotificationCard.jsx";
|
||||||
import { EventContext } from "../../contexts/events/EventContext.js";
|
import { EventContext } from "../../contexts/events/EventContext.js";
|
||||||
import {AuthContext} from "../../contexts/auth/AuthContext.js";
|
import {AuthContext} from "../../contexts/auth/AuthContext.js";
|
||||||
|
import { PendingMembersContext } from "../../contexts/pendingMembers/PendingMembersContext";
|
||||||
|
|
||||||
function Header() {
|
function Header() {
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ function Header() {
|
|||||||
const [mobileMenu, setMobileMenu] = useState(false);
|
const [mobileMenu, setMobileMenu] = useState(false);
|
||||||
const [notifications, setNotifications] = useState([]);
|
const [notifications, setNotifications] = useState([]);
|
||||||
const notificationRef = useRef(null);
|
const notificationRef = useRef(null);
|
||||||
|
const { updatePendingMembers } = useContext(PendingMembersContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
||||||
@@ -41,7 +43,7 @@ function Header() {
|
|||||||
echoInstance = echo;
|
echoInstance = echo;
|
||||||
|
|
||||||
const activeChannels = [
|
const activeChannels = [
|
||||||
userCreatedListener(echo, user, setNotifications, setunreadNotification),
|
userCreatedListener(echo, user, setNotifications, setunreadNotification, updatePendingMembers),
|
||||||
userNotificationsListener(echo, user, setNotifications, setunreadNotification),
|
userNotificationsListener(echo, user, setNotifications, setunreadNotification),
|
||||||
];
|
];
|
||||||
channelsToLeave = activeChannels.filter(name => name !== null);
|
channelsToLeave = activeChannels.filter(name => name !== null);
|
||||||
@@ -86,7 +88,7 @@ function Header() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Erreur suppression notification :', err);
|
console.error('Erreur pour ouvrir/fermer le menu de notifications :', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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{
|
.searchBarContainer{
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 4rem;
|
top: 20vh;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
width: 60%;
|
width: 60%;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import React, {useEffect, useState} from "react";
|
|
||||||
import getEventById from "../../utils/events/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.data);
|
|
||||||
}) ()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
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,71 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import getEventById from "../../utils/events/getEventById.js";
|
||||||
|
import styles from "./Task.module.css";
|
||||||
|
import formatDate from "../../utils/date/formatDate";
|
||||||
|
|
||||||
|
interface TaskType {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
location: string;
|
||||||
|
start: string;
|
||||||
|
end: string;
|
||||||
|
max_participants: number;
|
||||||
|
events_id: number;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
pivot: {
|
||||||
|
user_id: number;
|
||||||
|
task_id: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EventType {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
location?: string;
|
||||||
|
start?: string;
|
||||||
|
end?: string;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TaskProps {
|
||||||
|
task: TaskType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Task: React.FC<TaskProps> = ({ task }) => {
|
||||||
|
const [event, setEvent] = useState<EventType | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchEvent = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const e = await getEventById(task.events_id);
|
||||||
|
setEvent(e.data as EventType);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors du chargement de l'événement", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchEvent();
|
||||||
|
}, [task.events_id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${styles.task} glassCard`}>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div>
|
||||||
|
<h2>
|
||||||
|
{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;
|
||||||
@@ -1,59 +1,16 @@
|
|||||||
import styles from "./ToolBar.module.css"
|
import styles from "./ToolBar.module.css"
|
||||||
import Filter from "../Filter/Filter.jsx";
|
import CreateEventBtn from "./tools/createEventBtn/CreateEventBtn.jsx";
|
||||||
import {useContext, useEffect, useState} from "react";
|
import SearchBtn from "./tools/SearchBtn.tsx";
|
||||||
import SearchBar from "../SearchBar/SearchBar.jsx";
|
import FilterBtn from "./tools/FilterBtn.tsx";
|
||||||
import CreateEventBtn from "../createEventBtn/CreateEventBtn.jsx";
|
|
||||||
import Button from "../ui/button/button.jsx";
|
|
||||||
import {AuthContext} from "../../contexts/auth/AuthContext.js";
|
|
||||||
|
|
||||||
function ToolBar({setFilters, filters, showCreate = true}) {
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{isSearchVisible ? (
|
|
||||||
<div className={styles.searchBarContainer}>
|
|
||||||
<div className={styles.searchBarOverlay} onClick={() => setIsSearchVisible(false)}></div>
|
|
||||||
<SearchBar/>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<div className={styles.eventsButtons}>
|
<div className={styles.eventsButtons}>
|
||||||
{showCreate && user.isAdmin ? (
|
{showCreate && <CreateEventBtn />}
|
||||||
<CreateEventBtn />
|
<SearchBtn />
|
||||||
) : null}
|
<FilterBtn setFilters={setFilters} filters={filters} />
|
||||||
|
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
|
</>
|
||||||
|
}
|
||||||
+8
-4
@@ -1,14 +1,16 @@
|
|||||||
import styles from "./createEventBtn.module.css"
|
import styles from "./createEventBtn.module.css"
|
||||||
import { useState, useContext } from "react";
|
import { useState, useContext } from "react";
|
||||||
import Modal from "../ui/modal/modal.jsx";
|
import Modal from "../../../ui/modal/modal.tsx";
|
||||||
import Button from "../ui/button/button.jsx";
|
import Button from "../../../ui/button/button.tsx";
|
||||||
import TextInput from "../ui/input/input.jsx";
|
import TextInput from "../../../ui/input/input.tsx";
|
||||||
import { EventContext } from "../../contexts/events/EventContext.js";
|
import { EventContext } from "../../../../contexts/events/EventContext.js";
|
||||||
|
import {AuthContext} from "../../../../contexts/auth/AuthContext.ts";
|
||||||
|
|
||||||
|
|
||||||
export default function CreateEventBtn() {
|
export default function CreateEventBtn() {
|
||||||
|
|
||||||
const { addEvent } = useContext(EventContext);
|
const { addEvent } = useContext(EventContext);
|
||||||
|
const { user } = useContext(AuthContext);
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
@@ -24,6 +26,8 @@ export default function CreateEventBtn() {
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!user.isAdmin) return null;
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Button className={`${styles.createButton} glassCard`} variant={"default"} onClick={() => setIsOpen(true)}> + </Button>
|
<Button className={`${styles.createButton} glassCard`} variant={"default"} onClick={() => setIsOpen(true)}> + </Button>
|
||||||
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import styles from "./button.module.css";
|
|
||||||
|
|
||||||
const Button = ({ children, variant = "primary", onClick, className }) => {
|
|
||||||
let btnStyle;
|
|
||||||
|
|
||||||
if (variant === "primary") btnStyle = styles.primary;
|
|
||||||
else if (variant === "danger") btnStyle = styles.danger;
|
|
||||||
else if(variant === "transparent") btnStyle = styles.transparent;
|
|
||||||
else btnStyle = styles.default;
|
|
||||||
|
|
||||||
return <div className={`${styles.btn} ${btnStyle} ${className}`} onClick={onClick}>
|
|
||||||
{children}
|
|
||||||
</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Button;
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { ReactNode, MouseEventHandler } from "react";
|
||||||
|
import styles from "./button.module.css";
|
||||||
|
|
||||||
|
type ButtonVariant = "primary" | "danger" | "transparent" | "default";
|
||||||
|
|
||||||
|
interface ButtonProps {
|
||||||
|
children: ReactNode;
|
||||||
|
variant?: ButtonVariant;
|
||||||
|
onClick?: MouseEventHandler<HTMLDivElement>;
|
||||||
|
className?: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = ({
|
||||||
|
children,
|
||||||
|
variant = "primary",
|
||||||
|
onClick,
|
||||||
|
className = "",
|
||||||
|
}: ButtonProps) => {
|
||||||
|
let btnStyle: string;
|
||||||
|
|
||||||
|
if (variant === "primary") { // @ts-ignore
|
||||||
|
btnStyle = styles.primary;
|
||||||
|
}
|
||||||
|
else if (variant === "danger") { // @ts-ignore
|
||||||
|
btnStyle = styles.danger;
|
||||||
|
}
|
||||||
|
else if (variant === "transparent") { // @ts-ignore
|
||||||
|
btnStyle = styles.transparent;
|
||||||
|
}
|
||||||
|
else { // @ts-ignore
|
||||||
|
btnStyle = styles.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${styles.btn} ${btnStyle} ${className}`}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Button;
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import styles from "./input.module.css";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
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;
|
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import styles from "./input.module.css";
|
||||||
|
import { useState, ChangeEvent, InputHTMLAttributes } from "react";
|
||||||
|
|
||||||
|
interface TextInputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||||
|
placeholder?: string;
|
||||||
|
value?: string;
|
||||||
|
onChange?: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
borderStyle?: "square" | "rounded";
|
||||||
|
password?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TextInput = ({
|
||||||
|
placeholder,
|
||||||
|
onChange,
|
||||||
|
value,
|
||||||
|
borderStyle = "square",
|
||||||
|
password,
|
||||||
|
className = "",
|
||||||
|
...props
|
||||||
|
}: TextInputProps) => {
|
||||||
|
const [showPassword, setShowPassword] = useState<boolean>(false);
|
||||||
|
|
||||||
|
let inputBorderStyle = styles.square;
|
||||||
|
if (borderStyle === "rounded") inputBorderStyle = styles.rounded;
|
||||||
|
|
||||||
|
const toggleShowPassword = () => {
|
||||||
|
setShowPassword((prev) => !prev);
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
|
||||||
|
border: 5px solid #e0e0e0;
|
||||||
|
border-top: 5px solid #3498db;
|
||||||
|
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import React from "react";
|
||||||
|
import styles from "./loading.module.css";
|
||||||
|
|
||||||
|
const Loading: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div className={styles.wrapper}>
|
||||||
|
<div className={styles.spinner} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Loading;
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
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,71 @@
|
|||||||
|
/*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;*/
|
||||||
|
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
import styles from "./modal.module.css";
|
||||||
|
import Button from "../button/button";
|
||||||
|
import { ReactNode, MouseEvent } from "react";
|
||||||
|
|
||||||
|
interface ModalProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
children: ReactNode;
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Modal = ({ open, onClose, children, title }: ModalProps) => {
|
||||||
|
if (!open) return null;
|
||||||
|
|
||||||
|
const handleOverlayClick = () => {
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleModalClick = (e: MouseEvent<HTMLDivElement>) => {
|
||||||
|
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;
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import Button from "../button/button.jsx";
|
import Button from "../button/button.tsx";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import styles from "./ThemeSwitcher.module.css";
|
import styles from "./ThemeSwitcher.module.css";
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/*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>
|
||||||
|
}*/
|
||||||
|
|
||||||
|
import Button from "../button/button";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import styles from "./ThemeSwitcher.module.css";
|
||||||
|
|
||||||
|
type Theme = "light" | "dark";
|
||||||
|
|
||||||
|
export default function ThemeSwitcher() {
|
||||||
|
const [theme, setTheme] = useState<Theme | "">("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const savedTheme = localStorage.getItem("theme") as Theme | null;
|
||||||
|
if (savedTheme) {
|
||||||
|
setTheme(savedTheme);
|
||||||
|
} else {
|
||||||
|
setTheme("light");
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const switchTheme = () => {
|
||||||
|
const newTheme: Theme = theme === "light" ? "dark" : "light";
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ export interface User {
|
|||||||
email: string;
|
email: string;
|
||||||
phone: string | null;
|
phone: string | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
profile_photo_path?: string | null;
|
||||||
tasks: Task[];
|
tasks: Task[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { createContext } from "react";
|
||||||
|
|
||||||
|
export const PendingMembersContext = createContext(null);
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { PendingMembersContext } from "./PendingMembersContext";
|
||||||
|
|
||||||
|
import getUserToValidate from "../../utils/users/getUserToValidate";
|
||||||
|
import getUserById from "../../utils/users/getUserById";
|
||||||
|
|
||||||
|
export function PendingMembersProvider({ children }) {
|
||||||
|
|
||||||
|
const [pendingMembers, setPendingMembers] = useState([]);
|
||||||
|
|
||||||
|
const updatePendingMembers = async () => {
|
||||||
|
const result = await getUserToValidate();
|
||||||
|
const userIds = result.data;
|
||||||
|
|
||||||
|
if (!userIds || userIds.length === 0) {
|
||||||
|
setPendingMembers([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = await Promise.all(
|
||||||
|
userIds.map(id => getUserById(id))
|
||||||
|
);
|
||||||
|
|
||||||
|
setPendingMembers(users.map(u => u.data));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PendingMembersContext.Provider
|
||||||
|
value={{
|
||||||
|
pendingMembers,
|
||||||
|
updatePendingMembers
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</PendingMembersContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export default interface Response<T> {
|
||||||
|
status: number;
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
+8
-5
@@ -6,16 +6,19 @@ import router from './router';
|
|||||||
import {AuthProvider} from "./contexts/auth/authProvider.jsx";
|
import {AuthProvider} from "./contexts/auth/authProvider.jsx";
|
||||||
import {EventProvider} from "./contexts/events/EventProvider.jsx";
|
import {EventProvider} from "./contexts/events/EventProvider.jsx";
|
||||||
import {EventDetailProvider} from "./contexts/eventDetail/eventDetailProvider.jsx";
|
import {EventDetailProvider} from "./contexts/eventDetail/eventDetailProvider.jsx";
|
||||||
|
import {PendingMembersProvider} from "./contexts/pendingMembers/PendingMembersProvider.jsx";
|
||||||
|
|
||||||
|
|
||||||
createRoot(document.getElementById('root')).render(
|
createRoot(document.getElementById('root')).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<EventProvider>
|
<PendingMembersProvider>
|
||||||
<EventDetailProvider>
|
<EventProvider>
|
||||||
<RouterProvider router={router}/>
|
<EventDetailProvider>
|
||||||
</EventDetailProvider>
|
<RouterProvider router={router}/>
|
||||||
</EventProvider>
|
</EventDetailProvider>
|
||||||
|
</EventProvider>
|
||||||
|
</PendingMembersProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useState, useContext } from "react";
|
import { useContext } from "react";
|
||||||
import styles from "./AdminPage.module.css";
|
import styles from "./AdminPage.module.css";
|
||||||
import ParticipationChart from "./components/chart/ParticipationChart.jsx";
|
|
||||||
import IncompleteEvents from "./components/IncompleteEvent/IncompleteEvent.jsx";
|
import IncompleteEvents from "./components/IncompleteEvent/IncompleteEvent.jsx";
|
||||||
import PendingMembers from "./components/pendingMembers/PendingMembers.jsx";
|
import PendingMembers from "./components/pendingMembers/PendingMembers.jsx";
|
||||||
import { AuthContext } from "../../contexts/auth/AuthContext.js";
|
import { AuthContext } from "../../contexts/auth/AuthContext.js";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
|
import RegisterMember from "./components/registerMember/registerMember.tsx";
|
||||||
|
|
||||||
|
|
||||||
function AdminPage() {
|
function AdminPage() {
|
||||||
@@ -12,18 +12,11 @@ function AdminPage() {
|
|||||||
const { user } = useContext(AuthContext);
|
const { user } = useContext(AuthContext);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [selected, setSelected] = useState("3");
|
|
||||||
const [participationRate] = useState(89);
|
|
||||||
|
|
||||||
if(!user.isAdmin) navigate("/");
|
if(!user.isAdmin) navigate("/");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<ParticipationChart
|
<RegisterMember />
|
||||||
selected={selected}
|
|
||||||
setSelected={setSelected}
|
|
||||||
participationRate={participationRate}
|
|
||||||
/>
|
|
||||||
<div className={styles.rightSection}>
|
<div className={styles.rightSection}>
|
||||||
<IncompleteEvents />
|
<IncompleteEvents />
|
||||||
<PendingMembers />
|
<PendingMembers />
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import React, {useState, useEffect} from "react";
|
import React, {useState, useEffect, useContext} from "react";
|
||||||
import Button from "../../../../components/ui/button/button.jsx";
|
import Button from "../../../../components/ui/button/button.tsx";
|
||||||
import styles from "./PendingMembers.module.css";
|
import styles from "./PendingMembers.module.css";
|
||||||
import Modal from "../../../../components/ui/modal/modal.jsx";
|
import Modal from "../../../../components/ui/modal/modal.tsx";
|
||||||
import getUserToValidate from "../../../../utils/users/getUserToValidate.js";
|
|
||||||
import getUserById from "../../../../utils/users/getUserById.js";
|
|
||||||
import validateUser from "../../../../utils/users/validateUser.js";
|
import validateUser from "../../../../utils/users/validateUser.js";
|
||||||
import deleteOtherUser from "../../../../utils/users/deleteOtherUser.js";
|
import deleteOtherUser from "../../../../utils/users/deleteOtherUser.js";
|
||||||
|
import { PendingMembersContext } from "../../../../contexts/pendingMembers/PendingMembersContext";
|
||||||
|
|
||||||
function PendingMembers() {
|
function PendingMembers() {
|
||||||
|
|
||||||
@@ -13,23 +12,10 @@ function PendingMembers() {
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [message, setMessage] = useState("");
|
const [message, setMessage] = useState("");
|
||||||
const [title, setTitle] = useState("");
|
const [title, setTitle] = useState("");
|
||||||
|
const { pendingMembers, updatePendingMembers } = useContext(PendingMembersContext);
|
||||||
const [pendingMembers, setPendingMembers] = useState([]);
|
|
||||||
|
|
||||||
const fetchUsers = async () => {
|
|
||||||
|
|
||||||
const result = await getUserToValidate();
|
|
||||||
const userIds = result.data;
|
|
||||||
|
|
||||||
if (userIds && userIds.length > 0) {
|
|
||||||
const users = await Promise.all(userIds.map(id => getUserById(id)));
|
|
||||||
const usersData = users.map(u => u.data);
|
|
||||||
setPendingMembers(usersData);
|
|
||||||
} else setPendingMembers([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchUsers();
|
updatePendingMembers();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleValidate = async (id, name) => {
|
const handleValidate = async (id, name) => {
|
||||||
@@ -38,14 +24,12 @@ function PendingMembers() {
|
|||||||
if(result.status === 200) {
|
if(result.status === 200) {
|
||||||
setTitle("Utilisateur accepté avec succès")
|
setTitle("Utilisateur accepté avec succès")
|
||||||
setMessage(`L'utilisateur ${name} a été accepté avec succès`)
|
setMessage(`L'utilisateur ${name} a été accepté avec succès`)
|
||||||
setOpen(true);
|
|
||||||
} else {
|
} else {
|
||||||
setTitle("Erreur")
|
setTitle("Erreur")
|
||||||
setMessage(`Une erreur est survenue durant l'acceptation de l'utilisateur ${name}`)
|
setMessage(`Une erreur est survenue durant l'acceptation de l'utilisateur ${name}`)
|
||||||
setOpen(true);
|
|
||||||
}
|
}
|
||||||
|
setOpen(true);
|
||||||
fetchUsers();
|
updatePendingMembers();
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRefuse = async (id, name) => {
|
const handleRefuse = async (id, name) => {
|
||||||
@@ -61,7 +45,7 @@ function PendingMembers() {
|
|||||||
setOpen(true);
|
setOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchUsers();
|
updatePendingMembers();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
.leftBlock {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 720px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
min-height: calc(100vh - 260px);
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registerMemberHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registerForm {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registerForm input {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registerForm button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalContent {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.leftBlock {
|
||||||
|
width: 70%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.leftBlock {
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import React, { useState } from "react"
|
||||||
|
import styles from "./registerMember.module.css"
|
||||||
|
import TextInput from "../../../../components/ui/input/input"
|
||||||
|
import Button from "../../../../components/ui/button/button";
|
||||||
|
import Response from "../../../../interfaces/response.interface";
|
||||||
|
import ResponseData from "./responseData.inteface";
|
||||||
|
import adminCreateUser from "../../../../utils/users/adminCreateUser";
|
||||||
|
import Modal from "../../../../components/ui/modal/modal";
|
||||||
|
|
||||||
|
|
||||||
|
const RegisterMember: React.FC = () => {
|
||||||
|
|
||||||
|
const [name, setName] = useState<string>("");
|
||||||
|
const [lastname, setLastName] = useState<string>("");
|
||||||
|
const [email, setEmail] = useState<string>("");
|
||||||
|
const [phone, setPhone] = useState<string>("");
|
||||||
|
const [password, setPassword] = useState<string>("");
|
||||||
|
|
||||||
|
const [open, setOpen] = useState<boolean>(false);
|
||||||
|
const [title, setTitle] = useState<string>("");
|
||||||
|
const [message, setMessage] = useState<string>("");
|
||||||
|
|
||||||
|
|
||||||
|
const handleCreate = async (): Promise<void> => {
|
||||||
|
|
||||||
|
if(name !== "" && lastname !== "" && email !== "" && phone !== "" ) {
|
||||||
|
const response: Response<ResponseData> = await adminCreateUser(email, name, lastname, phone);
|
||||||
|
if(response.status === 200) {
|
||||||
|
setPassword(response.data.password);
|
||||||
|
setTitle(`Utilisateur enregistré avec succès`);
|
||||||
|
setMessage(`L'utilisateur ${name} ${lastname} a été enregistré avec le mot de passe: ${response.data.password}`);
|
||||||
|
setOpen(true);
|
||||||
|
} else {
|
||||||
|
setTitle(`Erreur`);
|
||||||
|
setMessage(`Une erreur est survenue lors de l'enregistrement de ${name} ${lastname}`);
|
||||||
|
setOpen(true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setTitle("Erreur");
|
||||||
|
setMessage("Un des champs n'a pas été rempli");
|
||||||
|
setOpen(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const showPassword = (): void => {
|
||||||
|
setTitle(`Mot de passe de ${name} ${lastname}`);
|
||||||
|
setMessage(password);
|
||||||
|
setOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className={`${styles.leftBlock} glassCard`}>
|
||||||
|
<div className={styles.registerMemberHeader}>
|
||||||
|
<h2>Enregister un bénévole</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.registerForm}>
|
||||||
|
<TextInput placeholder={"Prenom"} type={"text"} value={name} onChange={e => setName(e.target.value)} />
|
||||||
|
<TextInput placeholder={"Nom"} type={"text"} value={lastname} onChange={e => setLastName(e.target.value)} />
|
||||||
|
<TextInput placeholder={"Adresse Email"} type={"email"} value={email} onChange={e => setEmail(e.target.value)} />
|
||||||
|
<TextInput placeholder={"Numéro de téléphone"} type={"tel"} value={phone} onChange={e => setPhone(e.target.value)} />
|
||||||
|
|
||||||
|
{password !== "" && <Button onClick={showPassword}> Afficher le mot de passe </Button>}
|
||||||
|
|
||||||
|
<Button variant={"default"} onClick={handleCreate}>Enregistrer</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal open={open} onClose={() => setOpen(false)} title={title}>
|
||||||
|
<p className={styles.modalContent}>{message}</p>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RegisterMember
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export default interface ResponseData {
|
||||||
|
message: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import styles from "./Event.module.css";
|
import styles from "./Event.module.css";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import Button from "/src/components/ui/button/button.jsx"
|
import Button from "/src/components/ui/button/button"
|
||||||
|
|
||||||
function Event({event}){
|
function Event({event}){
|
||||||
|
|
||||||
|
|||||||
@@ -4,32 +4,31 @@ import EventItem from "./eventItem.jsx";
|
|||||||
import { EventContext } from "../../../../contexts/events/EventContext.js";
|
import { EventContext } from "../../../../contexts/events/EventContext.js";
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
|
|
||||||
|
|
||||||
function EventList() {
|
function EventList() {
|
||||||
|
// Utilise une valeur par défaut pour `events` si elle est `undefined`
|
||||||
const { events } = useContext(EventContext);
|
const { events = [] } = useContext(EventContext);
|
||||||
|
|
||||||
const sortedEvents = useMemo(() => {
|
const sortedEvents = useMemo(() => {
|
||||||
|
// Vérifie explicitement que `events` est un tableau avant de trier
|
||||||
|
if (!Array.isArray(events)) return [];
|
||||||
return [...events].sort(
|
return [...events].sort(
|
||||||
(a, b) => new Date(a.start) - new Date(b.start)
|
(a, b) => new Date(a.start) - new Date(b.start)
|
||||||
);
|
);
|
||||||
}, [events]);
|
}, [events]);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.glassCard} glassCard`}>
|
<div className={`${styles.glassCard} glassCard`}>
|
||||||
<h2>Liste des événements à venir</h2>
|
<h2>Liste des événements à venir</h2>
|
||||||
{sortedEvents.length > 0 ? (
|
{sortedEvents.length > 0 ? (
|
||||||
<div className={styles.eventList}>
|
<div className={styles.eventList}>
|
||||||
{sortedEvents.map((eventGroup, index) => (
|
{sortedEvents.map((eventGroup, index) => (
|
||||||
<EventItem eventGroup={eventGroup} key={index} />
|
<EventItem eventGroup={eventGroup} key={index} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p>Aucun événement planifié pour l'instant.</p>
|
<p>Aucun événement planifié pour l'instant.</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useState, useContext } from "react";
|
import { useState, useContext } from "react";
|
||||||
import styles from "./loginForm.module.css";
|
import styles from "./loginForm.module.css";
|
||||||
import Button from "../../../../components/ui/button/button.jsx";
|
import Button from "../../../../components/ui/button/button.tsx";
|
||||||
import Input from "../../../../components/ui/input/input.jsx";
|
import Input from "../../../../components/ui/input/input.tsx";
|
||||||
import Modal from "../../../../components/ui/modal/modal.jsx";
|
import Modal from "../../../../components/ui/modal/modal.tsx";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import { AuthContext } from "../../../../contexts/auth/AuthContext.js";
|
import { AuthContext } from "../../../../contexts/auth/AuthContext.js";
|
||||||
|
|
||||||
|
|||||||
@@ -1,49 +1,87 @@
|
|||||||
|
/* Conteneur principal */
|
||||||
.container {
|
.container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profileboard {
|
.profileboard {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.settingImage{
|
|
||||||
width:100%;
|
.settingImage {
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.contentWrapper {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
}
|
||||||
|
|
||||||
|
.contentWrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profilePictureContainer {
|
.profilePictureContainer {
|
||||||
width: 120px;
|
width: 120px;
|
||||||
height: 120px;
|
height: 120px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 0;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profilePicture {
|
.profilePicture {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editPictureButton {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-top: 12px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border: none;
|
||||||
|
border-radius: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #e74c3c;
|
||||||
|
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
white-space: nowrap;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: auto;
|
||||||
|
min-width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editPictureButton:hover {
|
||||||
|
background: rgba(255, 255, 255, 1);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.2);
|
||||||
|
color: #c0392b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.headerProfile {
|
.headerProfile {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.description h2 {
|
.description h2 {
|
||||||
@@ -75,84 +113,91 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Conteneur des informations principales */
|
||||||
.topDescription {
|
.topDescription {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 16px;
|
||||||
margin-bottom: 20px;
|
width: 100%;
|
||||||
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Bloc d'informations */
|
||||||
.infoBlock {
|
.infoBlock {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.eventTasks {
|
/* Conteneur pour les paramètres */
|
||||||
max-height: 0;
|
.settingImage {
|
||||||
overflow: hidden;
|
width: 100%;
|
||||||
transition: max-height 0.3s ease, opacity 0.2s ease;
|
display: flex;
|
||||||
opacity: 0;
|
justify-content: center;
|
||||||
padding-left: 16px;
|
margin-top: 16px;
|
||||||
}
|
|
||||||
|
|
||||||
.eventTasks.expanded {
|
|
||||||
max-height: 500px;
|
|
||||||
opacity: 1;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Adaptation pour tablette et PC */
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.contentWrapper {
|
.contentWrapper {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
.profilePictureContainer {
|
.profilePictureContainer {
|
||||||
width: 150px;
|
width: 150px;
|
||||||
height: 150px;
|
height: 150px;
|
||||||
margin-right: 24px;
|
margin-right: 0;
|
||||||
margin-bottom: 0;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
width: calc(100% - 180px);
|
width: calc(100% - 180px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.topDescription {
|
.topDescription {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
.infoBlock {
|
.infoBlock {
|
||||||
width: 48%;
|
flex: 1;
|
||||||
}
|
min-width: calc(50% - 8px);
|
||||||
|
max-width: calc(50% - 8px);
|
||||||
|
}
|
||||||
|
|
||||||
.description h2 {
|
.editPictureButton {
|
||||||
font-size: 1.8rem;
|
margin: 16px auto 0 auto;
|
||||||
}
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerProfile h2 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Adaptation pour grand écran */
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
.container {
|
.container {
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
padding: 24px;
|
||||||
padding: 24px;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.profileboard {
|
.profileboard {
|
||||||
padding: 28px;
|
padding: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profilePictureContainer {
|
.profilePictureContainer {
|
||||||
width: 180px;
|
width: 180px;
|
||||||
height: 180px;
|
height: 180px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
width: calc(100% - 220px);
|
width: calc(100% - 220px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.description h2 {
|
.headerProfile h2 {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+159
-111
@@ -1,42 +1,138 @@
|
|||||||
import { useContext, useEffect } from "react";
|
import getXSRFToken from "../../utils/getXSRF.js";
|
||||||
|
// import fetchWrapper from "../../utils/fetchWrapper";
|
||||||
|
import {useContext, useEffect, useState} from "react";
|
||||||
import styles from "./ProfilePage.module.css";
|
import styles from "./ProfilePage.module.css";
|
||||||
|
import {AuthContext} from "../../contexts/auth/AuthContext";
|
||||||
import { AuthContext } from "../../contexts/auth/AuthContext";
|
|
||||||
import formatDate from "../../utils/date/formatDate";
|
import formatDate from "../../utils/date/formatDate";
|
||||||
|
import Task from "../../components/Task/Task";
|
||||||
|
import SettingsModal from "./components/SettingsModal/SettingsModal";
|
||||||
|
import {useParams} from "react-router";
|
||||||
|
import {useNavigate} from "react-router";
|
||||||
|
import {useRef} from "react";
|
||||||
|
import getUserById from "../../utils/users/getUserById.js";
|
||||||
|
import ManageMember from "./components/manageMember/ManageMember.jsx";
|
||||||
|
|
||||||
|
import uploadProfilePhoto from "../../utils/users/uploadProfilePhoto";
|
||||||
|
import deleteProfilePhoto from "../../utils/users/deleteProfilePhoto.js";
|
||||||
|
|
||||||
//import Task from "../../components/Task/Task";
|
|
||||||
//import SettingsModal from "./components/SettingsModal/SettingsModal";
|
|
||||||
|
|
||||||
interface TaskType {
|
interface TaskType {
|
||||||
id: number;
|
id: number;
|
||||||
title?: string;
|
name: string;
|
||||||
completed?: boolean;
|
description: string;
|
||||||
[key: string]: unknown;
|
location: string;
|
||||||
|
start: string;
|
||||||
|
end: string;
|
||||||
|
max_participants: number;
|
||||||
|
events_id: number;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
pivot: {
|
||||||
|
user_id: number;
|
||||||
|
task_id: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
name: string;
|
id: number;
|
||||||
lastname: string;
|
name: string;
|
||||||
role: string;
|
lastname: string;
|
||||||
email: string;
|
role: string;
|
||||||
phone: string | null;
|
email: string;
|
||||||
created_at: string;
|
phone: string | null;
|
||||||
tasks: TaskType[];
|
created_at: string;
|
||||||
|
isAdmin: boolean;
|
||||||
|
tasks: TaskType[];
|
||||||
|
profile_photo_path?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AuthContextType {
|
interface AuthContextType {
|
||||||
user: User | null;
|
user: User | null;
|
||||||
update: () => void;
|
update: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ProfilePage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
function ProfilePage(){
|
const { id } = useParams();
|
||||||
const { user, update } = useContext(AuthContext) as AuthContextType;
|
const { user, update } = useContext(AuthContext) as AuthContextType;
|
||||||
|
const [profileUser, setProfileUser] = useState<User | null>(null);
|
||||||
|
const [profilePicture, setProfilePicture] = useState<string | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [success, setSuccess] = useState<string | null>(null);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const isOwnProfile = !id || user?.id.toString() === id;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
update();
|
(async () => {
|
||||||
}, []);
|
if (!isOwnProfile && id) {
|
||||||
|
const res = await getUserById(id);
|
||||||
|
if (res.status === 404) {
|
||||||
|
navigate("/404");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (res.status !== 200) {
|
||||||
|
console.error("Erreur lors du chargement du profil", res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setProfileUser(res.data);
|
||||||
|
} else {
|
||||||
|
update();
|
||||||
|
setProfileUser(user);
|
||||||
|
if (user?.profile_photo_path) {
|
||||||
|
setProfilePicture(`${import.meta.env.VITE_API_URL}/storage/${user.profile_photo_path}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [user, id]);
|
||||||
|
|
||||||
|
const handleEditPictureClick = () => {
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remplacé : handleFileChange extrait vers utils/users/uploadProfilePhoto
|
||||||
|
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await uploadProfilePhoto(file);
|
||||||
|
if (res.ok) {
|
||||||
|
setProfilePicture(`${import.meta.env.VITE_API_URL}/storage/${res.data.profile_photo_path}`);
|
||||||
|
setSuccess("Photo mise à jour !");
|
||||||
|
setError(null);
|
||||||
|
await update();
|
||||||
|
} else {
|
||||||
|
setError(res.data?.message || res.error || "Erreur lors de l'upload.");
|
||||||
|
setSuccess(null);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError("Erreur réseau.");
|
||||||
|
setSuccess(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remplacé : handleDeletePicture extrait vers utils/users/deleteProfilePhoto
|
||||||
|
const handleDeletePicture = async () => {
|
||||||
|
try {
|
||||||
|
const res = await deleteProfilePhoto();
|
||||||
|
if (res.status >= 200 && res.status < 300) {
|
||||||
|
setProfilePicture(null);
|
||||||
|
setSuccess("Photo supprimée !");
|
||||||
|
setError(null);
|
||||||
|
await update();
|
||||||
|
} else {
|
||||||
|
setError(res.data?.message || "Erreur lors de la suppression.");
|
||||||
|
setSuccess(null);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError("Erreur réseau.");
|
||||||
|
setSuccess(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
@@ -44,54 +140,69 @@ function ProfilePage(){
|
|||||||
<div className={styles.contentWrapper}>
|
<div className={styles.contentWrapper}>
|
||||||
<div className={styles.profilePictureContainer}>
|
<div className={styles.profilePictureContainer}>
|
||||||
<img
|
<img
|
||||||
src="/react.svg"
|
src={isOwnProfile ? (profilePicture || "/react.svg") : "/react.svg"}
|
||||||
alt="Photo de profil"
|
alt="Photo de profil"
|
||||||
className={styles.profilePicture}
|
className={styles.profilePicture}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isOwnProfile && (
|
||||||
|
<div className={styles.pictureButtons}>
|
||||||
|
<button onClick={handleEditPictureClick} className={styles.editPictureButton}>
|
||||||
|
Modifier la photo
|
||||||
|
</button>
|
||||||
|
{user?.profile_photo_path && (
|
||||||
|
<button onClick={handleDeletePicture} className={styles.deletePictureButton}>
|
||||||
|
Supprimer
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
accept="image/*"
|
||||||
|
style={{ display: "none" }}
|
||||||
|
/>
|
||||||
|
{error && <p className={styles.errorMessage}>{error}</p>}
|
||||||
|
{success && <p className={styles.successMessage}>{success}</p>}
|
||||||
|
|
||||||
<div className={styles.description}>
|
<div className={styles.description}>
|
||||||
<div className={styles.headerProfile}>
|
<div className={styles.headerProfile}>
|
||||||
<h2>
|
<h2>
|
||||||
{user?.name} {user?.lastname}
|
{profileUser?.name} {profileUser?.lastname}
|
||||||
</h2>
|
</h2>
|
||||||
|
{user?.isAdmin && profileUser && !isOwnProfile && (
|
||||||
|
<ManageMember userToManage={profileUser} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.topDescription}>
|
<div className={styles.topDescription}>
|
||||||
<div className={`${styles.infoBlock} glassBorder`}>
|
<div className={`${styles.infoBlock} glassBorder`}>
|
||||||
<p>
|
<p><strong>Role :</strong> {profileUser?.role}</p>
|
||||||
<strong>Role :</strong> {user?.role}
|
<p><strong>Membre depuis :</strong> {profileUser && formatDate(profileUser.created_at)}</p>
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Membre depuis :</strong>{" "}
|
|
||||||
{user && formatDate(user.created_at)}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={`${styles.infoBlock} glassBorder`}>
|
<div className={`${styles.infoBlock} glassBorder`}>
|
||||||
<p>
|
<p><strong>Mail :</strong> {profileUser?.email}</p>
|
||||||
<strong>Mail :</strong> {user?.email}
|
<p><strong>Téléphone :</strong> {profileUser?.phone ?? "Non renseigné"}</p>
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Téléphone :</strong>{" "}
|
|
||||||
{user?.phone ?? "Pas de numéro enregistré"}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.settingImage}>
|
{isOwnProfile && (
|
||||||
{/* <SettingsModal />*/}
|
<div className={styles.settingImage}>
|
||||||
</div>
|
<SettingsModal />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>Tâches :</h2>
|
<h2>Tâches :</h2>
|
||||||
|
{profileUser?.tasks.length ? (
|
||||||
{/* {user && user.tasks.length > 0 ? (
|
profileUser.tasks.map((task) => <Task key={task.id} task={task} />)
|
||||||
user.tasks.map((task) => (
|
|
||||||
<Task key={task.id} task={task} />
|
|
||||||
))
|
|
||||||
) : (
|
) : (
|
||||||
<p>Aucune tâche pour le moment.</p>
|
<p>Aucune tâche.</p>
|
||||||
)}*/}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -100,66 +211,3 @@ function ProfilePage(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default ProfilePage;
|
export default ProfilePage;
|
||||||
|
|
||||||
|
|
||||||
/*import React, { useContext, useEffect } 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, update } = useContext(AuthContext);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
update();
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
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} glassBorder`}>
|
|
||||||
<p><strong>Role :</strong> {user?.role}</p>
|
|
||||||
<p><strong>Membre depuis :</strong> {user && formatDate(user.created_at)}</p>
|
|
||||||
</div>
|
|
||||||
<div className={`${styles.infoBlock} glassBorder`}>
|
|
||||||
<p><strong>Mail :</strong> {user?.email} </p>
|
|
||||||
<p><strong>Téléphone :</strong> {user?.phone === null ? "Pas de numéro enregistré" : user?.phone}</p>
|
|
||||||
</div>
|
|
||||||
<div className={styles.settingImage}>
|
|
||||||
<SettingsModal />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>Tâches :</h2>
|
|
||||||
{user && 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;*/
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
import Modal from "../../../../components/ui/modal/modal.jsx";
|
|
||||||
import Button from "../../../../components/ui/button/button.jsx";
|
|
||||||
import styles from "./SettingsModal.module.css"
|
|
||||||
import { useContext, useState } from "react";
|
|
||||||
import ThemeSwitcher from "../../../../components/ui/themeSwitcher/ThemeSwitcher.jsx";
|
|
||||||
import { AuthContext } from "../../../../contexts/auth/AuthContext.js";
|
|
||||||
import TextInput from "../../../../components/ui/input/input.jsx";
|
|
||||||
import updateUser from "../../../../utils/users/updateUser.js";
|
|
||||||
import deleteUser from "../../../../utils/users/deleteUser.js";
|
|
||||||
|
|
||||||
|
|
||||||
export default function SettingsModal() {
|
|
||||||
|
|
||||||
const { user, logout, update } = useContext(AuthContext);
|
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const [isDelete, setIsDelete] = 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDelete = async () => {
|
|
||||||
await deleteUser();
|
|
||||||
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>
|
|
||||||
<Button variant={"danger"} onClick={() => setIsDelete(true)}> Supprimer le compte </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>
|
|
||||||
|
|
||||||
|
|
||||||
<Modal title={"Validation"} open={isDelete} onClose={() => setIsDelete(false)}>
|
|
||||||
<div className={styles.section}>
|
|
||||||
<p className={styles.alignText}> Êtes vous vraiment sûr de vouloir supprimer votre compte ?</p>
|
|
||||||
<Button variant={"danger"} className={styles.submitBtn} onClick={handleDelete}>Confirmer</Button>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,69 +1,94 @@
|
|||||||
.content {
|
.content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 16px;
|
padding: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 20px;
|
gap: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modifyIcon {
|
.modifyIcon {
|
||||||
height: 28px;
|
height: 26px;
|
||||||
width: 28px;
|
width: 26px;
|
||||||
|
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modifyIcon:hover {
|
||||||
|
transform: rotate(15deg);
|
||||||
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section {
|
.section {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 16px;
|
||||||
margin-top: 10px;
|
padding: 18px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--glass-bg, rgba(255, 255, 255, 0.25));
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.section h3 {
|
.section h3 {
|
||||||
font-size: 1.1rem;
|
font-size: 1.15rem;
|
||||||
margin-bottom: 5px;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section h4 {
|
.section h4 {
|
||||||
font-size: 0.95rem;
|
font-size: 0.9rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
opacity: 0.85;
|
||||||
|
|
||||||
.submitBtn {
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 15px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.section button {
|
.section button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 600px) {
|
.submitBtn {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alignText {
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section :global(input) {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
.content {
|
.content {
|
||||||
padding: 24px;
|
padding: 28px;
|
||||||
|
gap: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section h3 {
|
.section {
|
||||||
font-size: 1.2rem;
|
padding: 22px;
|
||||||
|
gap: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.submitBtn {
|
.submitBtn {
|
||||||
width: auto;
|
width: auto;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
min-width: 200px;
|
min-width: 220px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section button {
|
.section button {
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 900px) {
|
@media (min-width: 900px) {
|
||||||
.content {
|
.content {
|
||||||
padding: 30px;
|
padding: 36px;
|
||||||
gap: 30px;
|
gap: 36px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section {
|
.section {
|
||||||
gap: 14px;
|
padding: 26px;
|
||||||
|
border-radius: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modifyIcon {
|
.modifyIcon {
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
import {useContext, useState, ChangeEvent} from "react";
|
||||||
|
import Modal from "../../../../components/ui/modal/modal";
|
||||||
|
import Button from "../../../../components/ui/button/button";
|
||||||
|
import styles from "./SettingsModal.module.css";
|
||||||
|
import ThemeSwitcher from "../../../../components/ui/themeSwitcher/ThemeSwitcher.jsx";
|
||||||
|
import {AuthContext} from "../../../../contexts/auth/AuthContext";
|
||||||
|
import TextInput from "../../../../components/ui/input/input";
|
||||||
|
import updateUser from "../../../../utils/users/updateUser.js";
|
||||||
|
import deleteUser from "../../../../utils/users/deleteUser.js";
|
||||||
|
|
||||||
|
interface TaskType {
|
||||||
|
id: number;
|
||||||
|
title?: string;
|
||||||
|
completed?: boolean;
|
||||||
|
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
lastname: string;
|
||||||
|
role: string;
|
||||||
|
email: string;
|
||||||
|
phone: string | null;
|
||||||
|
created_at: string;
|
||||||
|
isAdmin: boolean;
|
||||||
|
tasks: TaskType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AuthContextType {
|
||||||
|
user: User;
|
||||||
|
update: () => void;
|
||||||
|
logout: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SettingsModal() {
|
||||||
|
const {user, logout, update} = useContext(AuthContext) as AuthContextType;
|
||||||
|
|
||||||
|
const [open, setOpen] = useState<boolean>(false);
|
||||||
|
const [isDelete, setIsDelete] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [name, setName] = useState<string>(user?.name ?? "");
|
||||||
|
const [lastname, setLastName] = useState<string>(user?.lastname ?? "");
|
||||||
|
const [phone, setPhone] = useState<string>(user?.phone ?? "");
|
||||||
|
|
||||||
|
const handleLogout = (): void => {
|
||||||
|
logout();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (): Promise<void> => {
|
||||||
|
await updateUser(name, lastname, phone || null);
|
||||||
|
update();
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (): Promise<void> => {
|
||||||
|
await deleteUser();
|
||||||
|
update();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNameChange = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||||
|
setName(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLastnameChange = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||||
|
setLastName(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePhoneChange = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||||
|
setPhone(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button variant="transparent" onClick={() => setOpen((prev) => !prev)}>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
onClick={() => setIsDelete(true)}
|
||||||
|
>
|
||||||
|
Supprimer le compte
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h4>Changer de Prénom</h4>
|
||||||
|
<TextInput
|
||||||
|
value={name}
|
||||||
|
onChange={handleNameChange}
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h4>Changer de nom</h4>
|
||||||
|
<TextInput
|
||||||
|
value={lastname}
|
||||||
|
onChange={handleLastnameChange}
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h4>Changer de numéro de téléphone</h4>
|
||||||
|
<TextInput
|
||||||
|
value={phone}
|
||||||
|
onChange={handlePhoneChange}
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className={styles.submitBtn}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
>
|
||||||
|
Confirmer
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h3>Apparence</h3>
|
||||||
|
<ThemeSwitcher/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title="Validation"
|
||||||
|
open={isDelete}
|
||||||
|
onClose={() => setIsDelete(false)}
|
||||||
|
>
|
||||||
|
<div className={styles.section}>
|
||||||
|
<p className={styles.alignText}>
|
||||||
|
Êtes vous vraiment sûr de vouloir supprimer votre compte ?
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
className={styles.submitBtn}
|
||||||
|
onClick={handleDelete}
|
||||||
|
>
|
||||||
|
Confirmer
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
import Button from "../../../../components/ui/button/button.jsx";
|
import Button from "../../../../components/ui/button/button.tsx";
|
||||||
import Modal from "../../../../components/ui/modal/modal.jsx";
|
import Modal from "../../../../components/ui/modal/modal.tsx";
|
||||||
import {useContext, useState} from "react";
|
import {useContext, useState} from "react";
|
||||||
import styles from "./manageMember.module.css"
|
import styles from "./manageMember.module.css"
|
||||||
import DeactivateMemberBtn from "./deactivateMember/deactivateMemberBtn.jsx";
|
import DeactivateMemberBtn from "./deactivateMember/deactivateMemberBtn.jsx";
|
||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
import Button from "../../../../../components/ui/button/button.jsx";
|
import Button from "../../../../../components/ui/button/button.tsx";
|
||||||
import Modal from "../../../../../components/ui/modal/modal.jsx";
|
import Modal from "../../../../../components/ui/modal/modal.tsx";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import deactivateUser from "../../../../../utils/users/deactivateUser.js"
|
import deactivateUser from "../../../../../utils/users/deactivateUser.js"
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
import styles from "../manageMember.module.css";
|
import styles from "../manageMember.module.css";
|
||||||
import Button from "../../../../../components/ui/button/button.jsx";
|
import Button from "../../../../../components/ui/button/button.tsx";
|
||||||
import modifyRole from "../../../../../utils/users/modifyRole.js";
|
import modifyRole from "../../../../../utils/users/modifyRole.js";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import getRoles from "../../../../../utils/getRoles.js";
|
import getRoles from "../../../../../utils/getRoles.js";
|
||||||
import Modal from "../../../../../components/ui/modal/modal.jsx";
|
import Modal from "../../../../../components/ui/modal/modal.tsx";
|
||||||
|
|
||||||
export default function ModifyRole({ user }) {
|
export default function ModifyRole({ user }) {
|
||||||
|
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import styles from "./RegisterPage.module.css";
|
import styles from "./RegisterPage.module.css";
|
||||||
import Background from "../../components/background/background.jsx";
|
import Background from "../../components/background/background.jsx";
|
||||||
import Input from "../../components/ui/input/input.jsx";
|
import Input from "../../components/ui/input/input.tsx";
|
||||||
import Button from "../../components/ui/button/button.jsx";
|
import Button from "../../components/ui/button/button.tsx";
|
||||||
import {useContext, useEffect, useState} from "react";
|
import {useContext, useEffect, useState} from "react";
|
||||||
import register from "../../utils/register.js";
|
import register from "../../utils/register.js";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import { AuthContext } from "../../contexts/auth/AuthContext.js";
|
import { AuthContext } from "../../contexts/auth/AuthContext.js";
|
||||||
import Modal from "../../components/ui/modal/modal.jsx";
|
import Modal from "../../components/ui/modal/modal.tsx";
|
||||||
|
|
||||||
|
|
||||||
function RegisterPage() {
|
function RegisterPage() {
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
import styles from "./VolunteerProfilePage.module.css";
|
|
||||||
import { useParams, useNavigate } from "react-router";
|
|
||||||
import { useEffect, useState, useContext } from "react";
|
|
||||||
import getUserById from "../../utils/users/getUserById.js";
|
|
||||||
import formatDate from "../../utils/date/formatDate.js";
|
|
||||||
import Task from "../../components/Task/Task.jsx";
|
|
||||||
import ManageMember from "./components/manageMember/ManageMember.jsx";
|
|
||||||
import { AuthContext } from "../../contexts/auth/AuthContext.js";
|
|
||||||
|
|
||||||
export default function VolunteerProfilePage() {
|
|
||||||
const { id } = useParams();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { user: currentUser } = useContext(AuthContext);
|
|
||||||
|
|
||||||
const [user, setUser] = useState(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
const res = await getUserById(id);
|
|
||||||
|
|
||||||
if (res.status === 404) {
|
|
||||||
navigate("/404");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res.status !== 200) {
|
|
||||||
console.error("Erreur lors du chargement du profil", res);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setUser(res.data);
|
|
||||||
})();
|
|
||||||
}, [id, navigate]);
|
|
||||||
|
|
||||||
if (!user) return null;
|
|
||||||
|
|
||||||
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>
|
|
||||||
{currentUser?.isAdmin && (
|
|
||||||
<ManageMember userToManage={user} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.topDescription}>
|
|
||||||
<div className={`${styles.infoBlock} glassBorder`}>
|
|
||||||
<p><strong>Role :</strong> {user.role}</p>
|
|
||||||
<p><strong>Membre depuis :</strong> {formatDate(user.created_at)}</p>
|
|
||||||
</div>
|
|
||||||
<div className={`${styles.infoBlock} glassBorder`}>
|
|
||||||
<p><strong>Mail :</strong> {user.email}</p>
|
|
||||||
<p>
|
|
||||||
<strong>Téléphone :</strong>{" "}
|
|
||||||
{user.phone ?? "Pas de numéro enregistré"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>Tâches :</h2>
|
|
||||||
{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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
.container {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profileboard {
|
|
||||||
width: 100%;
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 20px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contentWrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profilePictureContainer {
|
|
||||||
width: 120px;
|
|
||||||
height: 120px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profilePicture {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border-radius: 50%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
margin-left:4%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topDescription {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.infoBlock {
|
|
||||||
width: 100%;
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.eventTasks {
|
|
||||||
max-height: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: max-height 0.3s ease-out, opacity 0.2s ease;
|
|
||||||
opacity: 0;
|
|
||||||
padding-left: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.eventTasks.expanded {
|
|
||||||
max-height: 500px;
|
|
||||||
opacity: 1;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
.description h2{
|
|
||||||
font-size:30px;
|
|
||||||
|
|
||||||
}
|
|
||||||
.profilePicture{
|
|
||||||
width:90%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settingsBtns {
|
|
||||||
display: flex;
|
|
||||||
gap: 5px;
|
|
||||||
justify-content: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
|
||||||
.contentWrapper {
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profilePictureContainer {
|
|
||||||
width: 150px;
|
|
||||||
height: 150px;
|
|
||||||
margin-right: 20px;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
width: calc(100% - 170px);
|
|
||||||
margin-left:4%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topDescription {
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.infoBlock {
|
|
||||||
width: 48%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.event {
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
.description h2{
|
|
||||||
font-size:30px;
|
|
||||||
}
|
|
||||||
.profilePicture{
|
|
||||||
width:90%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.description button{
|
|
||||||
width:50px;
|
|
||||||
height:50px;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.headerProfile {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (min-width: 1024px) {
|
|
||||||
.container {
|
|
||||||
max-width: 100%;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profileboard {
|
|
||||||
padding: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profilePictureContainer {
|
|
||||||
width: 30%;
|
|
||||||
height:auto;
|
|
||||||
}
|
|
||||||
.profilePicture{
|
|
||||||
width:90%;
|
|
||||||
}
|
|
||||||
.description {
|
|
||||||
width: calc(100% - 200px);
|
|
||||||
margin-left:4%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.event {
|
|
||||||
padding: 18px;
|
|
||||||
}
|
|
||||||
.description h2{
|
|
||||||
font-size:30px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
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`} onClick={(() => setIsExpanded(!isExpanded))}>
|
|
||||||
|
|
||||||
<div className={styles.header}>
|
|
||||||
<h2 className={styles.userName}>
|
|
||||||
<span className={styles.firstName}>{user?.name}</span>{" "}
|
|
||||||
<span className={styles.lastName}>{user?.lastname}</span>
|
|
||||||
</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>
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
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 result = await getAllUser();
|
|
||||||
|
|
||||||
if (result.status !== 200 || !Array.isArray(result.data)) {
|
|
||||||
console.error("Erreur lors du chargement des utilisateurs", result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const usersData = await Promise.all(
|
|
||||||
result.data.map(async (id) => {
|
|
||||||
const res = await getUserById(id);
|
|
||||||
|
|
||||||
if (res.status === 200) {
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
console.warn(`Utilisateur ${id} non récupéré`, res.status);
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
setUsers(usersData.filter(Boolean));
|
|
||||||
})();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.container}>
|
|
||||||
<ToolBar setFilters={setFilters} filters={filters} showCreate={false} />
|
|
||||||
<div className={styles.usersList}>
|
|
||||||
{filter(users, filters, "user").map((user, index) => (
|
|
||||||
<VolunteerCard key={user.id ?? index} user={user} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default VolunteerPage;
|
|
||||||
@@ -3,33 +3,18 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
.userName {
|
|
||||||
display: flex;
|
|
||||||
gap: 5px;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 1rem;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.firstName {
|
|
||||||
white-space: nowrap;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lastName {
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
flex-shrink: 1;
|
|
||||||
max-width: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.usersList {
|
.usersList {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-direction: column;
|
gap: 16px;
|
||||||
align-items: center;
|
width: 100%;
|
||||||
gap: 10px;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.usersList > * {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.profileboard {
|
.profileboard {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 380px;
|
max-width: 380px;
|
||||||
@@ -140,12 +125,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
opacity: 1;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 1024px) {
|
@media screen and (min-width: 1024px) {
|
||||||
.container {
|
.container {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
.usersList {
|
.usersList {
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
padding-right: 5em;
|
||||||
|
padding-left: 5em;
|
||||||
}
|
}
|
||||||
.profileboard {
|
.profileboard {
|
||||||
max-width: 700px;
|
max-width: 700px;
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import styles from "./VolunteersPage.module.css";
|
||||||
|
import VolunteerCard from "./components/VolunteersCard/VolunteerCard";
|
||||||
|
import getAllUser from "../../utils/users/getAllUsers";
|
||||||
|
import ToolBar from "../../components/ToolBar/ToolBar";
|
||||||
|
import filter from "../../utils/filter";
|
||||||
|
import getUserById from "../../utils/users/getUserById";
|
||||||
|
import User from "./interfaces/user.interface";
|
||||||
|
import Loading from "../../components/ui/loading/loading";
|
||||||
|
|
||||||
|
interface Filters {
|
||||||
|
alphabetical: "asc" | "desc";
|
||||||
|
yearOrder: "asc" | "desc";
|
||||||
|
}
|
||||||
|
|
||||||
|
const VolunteerPage: React.FC = () => {
|
||||||
|
const [filters, setFilters] = useState<Filters>({
|
||||||
|
alphabetical: "asc",
|
||||||
|
yearOrder: "desc",
|
||||||
|
});
|
||||||
|
|
||||||
|
const [users, setUsers] = useState<User[]>([]);
|
||||||
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchUsers = async (): Promise<void> => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await getAllUser();
|
||||||
|
|
||||||
|
if (result.status !== 200 || !Array.isArray(result.data)) {
|
||||||
|
console.error("Erreur lors du chargement des utilisateurs", result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const usersData = await Promise.all(
|
||||||
|
result.data.map(async (id: number): Promise<User | null> => {
|
||||||
|
const res = await getUserById(id);
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
return res.data as User;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn(`Utilisateur ${id} non récupéré`, res.status);
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
setUsers(usersData.filter((user): user is User => user !== null));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur réseau", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchUsers();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if(loading) return <Loading />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<ToolBar
|
||||||
|
setFilters={setFilters}
|
||||||
|
filters={filters}
|
||||||
|
showCreate={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.usersList}>
|
||||||
|
{filter(users, filters, "user").map((user: User) => (
|
||||||
|
<VolunteerCard key={user.id} user={user} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VolunteerPage;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import styles from "./VolunteersCard.module.css";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
import Button from "../../../../components/ui/button/button";
|
||||||
|
import User from "../../interfaces/user.interface"
|
||||||
|
|
||||||
|
|
||||||
|
interface VolunteersCardProps {
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function VolunteerCard({ user }: VolunteersCardProps) {
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleNavigate = (): void => { navigate(`/profile/${user.id}`) }
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`glassCard ${styles.volunteerCard}`}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<img src={"/react.svg"} alt="profile" className={styles.profilePicture} />
|
||||||
|
<p> {user.name} {user.lastname} </p>
|
||||||
|
</div>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<p> {user.email} </p>
|
||||||
|
<Button variant={"primary"} onClick={handleNavigate}>Voir</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
.volunteerCard {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header p {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content p {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content button {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profilePicture {
|
||||||
|
background-color: #1d1d1f;
|
||||||
|
border-radius: 20px;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
export default interface Task {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
location: string;
|
||||||
|
start: string;
|
||||||
|
end: string;
|
||||||
|
max_participants: number;
|
||||||
|
events_id: number;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
pivot: {
|
||||||
|
user_id: number;
|
||||||
|
task_id: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import Task from "./task.interface";
|
||||||
|
|
||||||
|
export default interface User {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
lastname: string;
|
||||||
|
email: string;
|
||||||
|
role: string;
|
||||||
|
isAdmin: boolean;
|
||||||
|
phone: string | null;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
verified_at: string | null;
|
||||||
|
validate: number;
|
||||||
|
tasks: Task[];
|
||||||
|
}
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
import styles from "./eventTask.module.css";
|
import styles from "./eventTask.module.css";
|
||||||
import { formatDateLetter } from "../../../../utils/date/formatDateLetter.js";
|
import { formatDateLetter } from "../../../../utils/date/formatDateLetter.js";
|
||||||
import Button from "../../../../components/ui/button/button.jsx";
|
import Button from "../../../../components/ui/button/button.tsx";
|
||||||
import assign from "../../../../utils/tasks/assign.js";
|
import assign from "../../../../utils/tasks/assign.js";
|
||||||
import unAssign from "../../../../utils/tasks/unAssign.js";
|
import unAssign from "../../../../utils/tasks/unAssign.js";
|
||||||
import { useContext, useEffect, useState } from "react";
|
import { useContext, useEffect, useState } from "react";
|
||||||
import isAssigned from "../../../../utils/tasks/isAssigned.js";
|
import isAssigned from "../../../../utils/tasks/isAssigned.js";
|
||||||
import Modal from "../../../../components/ui/modal/modal.jsx";
|
import Modal from "../../../../components/ui/modal/modal.tsx";
|
||||||
import { AuthContext } from "../../../../contexts/auth/AuthContext.js";
|
import { AuthContext } from "../../../../contexts/auth/AuthContext.js";
|
||||||
import DeleteTaskBtn from "../deleteTaskBtn/deleteTaskBtn.jsx";
|
import DeleteTaskBtn from "../deleteTaskBtn/deleteTaskBtn.jsx";
|
||||||
|
|
||||||
export default function EventTask({ task, index, eventId }) {
|
export default function EventTask({ task, index, eventId }) {
|
||||||
|
|
||||||
const { user } = useContext(AuthContext)
|
const { update } = useContext(AuthContext)
|
||||||
|
|
||||||
const [assigned, setAssigned] = useState(false);
|
const [assigned, setAssigned] = useState(false);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@@ -33,6 +33,7 @@ export default function EventTask({ task, index, eventId }) {
|
|||||||
|
|
||||||
switch (res.status) {
|
switch (res.status) {
|
||||||
case 200:
|
case 200:
|
||||||
|
update()
|
||||||
setTitle("Inscription réussie")
|
setTitle("Inscription réussie")
|
||||||
setMessage(`Vous êtes maintenant inscrit à la tâche ${task.name}`);
|
setMessage(`Vous êtes maintenant inscrit à la tâche ${task.name}`);
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
@@ -58,6 +59,7 @@ export default function EventTask({ task, index, eventId }) {
|
|||||||
|
|
||||||
switch (res.status) {
|
switch (res.status) {
|
||||||
case 200:
|
case 200:
|
||||||
|
update()
|
||||||
setTitle("Désinscription réussie")
|
setTitle("Désinscription réussie")
|
||||||
setMessage(`Vous n'êtes plus inscrit à la tâche ${task.name}`);
|
setMessage(`Vous n'êtes plus inscrit à la tâche ${task.name}`);
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import styles from "../../eventDetail.module.css";
|
import styles from "../../eventDetail.module.css";
|
||||||
import Button from "../../../../components/ui/button/button.jsx";
|
import Button from "../../../../components/ui/button/button.tsx";
|
||||||
import {useContext, useState} from "react";
|
import {useContext, useState} from "react";
|
||||||
import TextInput from "../../../../components/ui/input/input.jsx";
|
import TextInput from "../../../../components/ui/input/input.tsx";
|
||||||
import Modal from "../../../../components/ui/modal/modal.jsx";
|
import Modal from "../../../../components/ui/modal/modal.tsx";
|
||||||
import createTaskApi from "../../../../utils/tasks/createTask.js";
|
import createTaskApi from "../../../../utils/tasks/createTask.js";
|
||||||
import {EventDetailContext} from "../../../../contexts/eventDetail/eventDetail.js";
|
import {EventDetailContext} from "../../../../contexts/eventDetail/eventDetail.js";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import styles from "../../eventDetail.module.css";
|
import styles from "../../eventDetail.module.css";
|
||||||
import Button from "../../../../components/ui/button/button.jsx";
|
import Button from "../../../../components/ui/button/button.tsx";
|
||||||
import Modal from "../../../../components/ui/modal/modal.jsx";
|
import Modal from "../../../../components/ui/modal/modal.tsx";
|
||||||
import { useContext, useState } from "react";
|
import { useContext, useState } from "react";
|
||||||
import { EventDetailContext } from "../../../../contexts/eventDetail/eventDetail.js";
|
import { EventDetailContext } from "../../../../contexts/eventDetail/eventDetail.js";
|
||||||
import { EventContext } from "../../../../contexts/events/EventContext.js";
|
import { EventContext } from "../../../../contexts/events/EventContext.js";
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import { AuthContext } from "../../../../contexts/auth/AuthContext.js";
|
import { AuthContext } from "../../../../contexts/auth/AuthContext.js";
|
||||||
import { EventDetailContext } from "../../../../contexts/eventDetail/eventDetail.js";
|
import { EventDetailContext } from "../../../../contexts/eventDetail/eventDetail.js";
|
||||||
import Button from "../../../../components/ui/button/button.jsx";
|
import Button from "../../../../components/ui/button/button.tsx";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Modal from "../../../../components/ui/modal/modal.jsx";
|
import Modal from "../../../../components/ui/modal/modal.tsx";
|
||||||
import styles from "../../eventDetail.module.css";
|
import styles from "../../eventDetail.module.css";
|
||||||
|
|
||||||
export default function DeleteTaskBtn({ taskId, eventId, taskName }) {
|
export default function DeleteTaskBtn({ taskId, eventId, taskName }) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import styles from "./validationErrorPage.module.css"
|
import styles from "./validationErrorPage.module.css"
|
||||||
import Background from "../../components/background/background.jsx";
|
import Background from "../../components/background/background.jsx";
|
||||||
import Button from "../../components/ui/button/button.jsx";
|
import Button from "../../components/ui/button/button.tsx";
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import { AuthContext } from "../../contexts/auth/AuthContext.js";
|
import { AuthContext } from "../../contexts/auth/AuthContext.js";
|
||||||
import { Navigate, useNavigate, Link } from "react-router";
|
import { Navigate, useNavigate, Link } from "react-router";
|
||||||
|
|||||||
+6
-7
@@ -3,12 +3,11 @@ import Layout from "./layout.jsx";
|
|||||||
import HomePage from "./pages/Home/HomePage";
|
import HomePage from "./pages/Home/HomePage";
|
||||||
import LoginPage from "./pages/Login/LoginPage";
|
import LoginPage from "./pages/Login/LoginPage";
|
||||||
import RegisterPage from "./pages/Register/RegisterPage.jsx";
|
import RegisterPage from "./pages/Register/RegisterPage.jsx";
|
||||||
import VolunteersPage from "./pages/Volunteers/VolunteersPage";
|
import VolunteersPage from "./pages/Volunteers/VolunteersPage.tsx";
|
||||||
import ProfilePage from "./pages/Profile/ProfilePage";
|
import ProfilePage from "./pages/Profile/ProfilePage";
|
||||||
import EventsPage from "./pages/Events/EventsPage.jsx";
|
import EventsPage from "./pages/Events/EventsPage.jsx";
|
||||||
import AdminPage from "./pages/Admin/AdminPage.jsx";
|
import AdminPage from "./pages/Admin/AdminPage.jsx";
|
||||||
import ValidationErrorPage from "./pages/waitValidationPage/ValidationErrorPage.jsx";
|
import ValidationErrorPage from "./pages/waitValidationPage/ValidationErrorPage.jsx";
|
||||||
import VolunteerProfilePage from "./pages/VolunteerProfile/VolunteerProfilePage.jsx";
|
|
||||||
import PageNotFound from "./pages/PageNotFound/PageNotFound.jsx";
|
import PageNotFound from "./pages/PageNotFound/PageNotFound.jsx";
|
||||||
import eventDetail from "./pages/eventDetail/eventDetail.jsx";
|
import eventDetail from "./pages/eventDetail/eventDetail.jsx";
|
||||||
import legalNotices from "./pages/LegalNotices/LegalNotices.jsx";
|
import legalNotices from "./pages/LegalNotices/LegalNotices.jsx";
|
||||||
@@ -47,10 +46,6 @@ const router = createBrowserRouter([
|
|||||||
path: "/admin",
|
path: "/admin",
|
||||||
Component:AdminPage,
|
Component:AdminPage,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path:"/profile",
|
|
||||||
Component:ProfilePage,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "/events",
|
path: "/events",
|
||||||
Component: EventsPage,
|
Component: EventsPage,
|
||||||
@@ -59,9 +54,13 @@ const router = createBrowserRouter([
|
|||||||
path: "/volunteers",
|
path: "/volunteers",
|
||||||
Component: VolunteersPage,
|
Component: VolunteersPage,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path:"/profile",
|
||||||
|
Component:ProfilePage,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/profile/:id",
|
path: "/profile/:id",
|
||||||
Component: VolunteerProfilePage,
|
Component: ProfilePage,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/events/:id",
|
path: "/events/:id",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { fn } from 'storybook/test';
|
import { fn } from 'storybook/test';
|
||||||
|
|
||||||
import Button from '../components/ui/button/button.jsx';
|
import Button from '../components/ui/button/button.tsx';
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import TextInput from "../components/ui/input/input.jsx";
|
import TextInput from "../components/ui/input/input.tsx";
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import Loading from "../components/ui/loading/loading.tsx";
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'UI/Loading',
|
||||||
|
component: Loading,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Primary = {};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import Modal from "../components/ui/modal/modal.jsx";
|
import Modal from "../components/ui/modal/modal.tsx";
|
||||||
import Button from "../components/ui/button/button.jsx";
|
import Button from "../components/ui/button/button.tsx";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -14,8 +14,31 @@ export default function initEcho() {
|
|||||||
encrypted: import.meta.env.VITE_ENCRYPTED === 'true',
|
encrypted: import.meta.env.VITE_ENCRYPTED === 'true',
|
||||||
cluster: import.meta.env.VITE_CLUSTER,
|
cluster: import.meta.env.VITE_CLUSTER,
|
||||||
enabledTransports: ['ws', 'wss'],
|
enabledTransports: ['ws', 'wss'],
|
||||||
|
authorizer: (channel) => {
|
||||||
|
return {
|
||||||
|
authorize: (socketId, callback) => {
|
||||||
|
fetch(`${import.meta.env.VITE_API_URL}/broadcasting/auth`, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
socket_id: socketId,
|
||||||
|
channel_name: channel.name,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => callback(null, data))
|
||||||
|
.catch(err => callback(err, null));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
window.Echo = echoInstance;
|
window.Echo = echoInstance;
|
||||||
return echoInstance;
|
return echoInstance;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export const userCreatedListener = (echo, userData, setNotifications, setunreadNotification) => {
|
export const userCreatedListener = (echo, userData, setNotifications, setunreadNotification, updatePendingMembers) => {
|
||||||
if (userData.isAdmin) {
|
if (userData.isAdmin) {
|
||||||
const channelName = "users.registration";
|
const channelName = "users.registration";
|
||||||
echo.private(channelName)
|
echo.private(channelName)
|
||||||
@@ -11,6 +11,7 @@ export const userCreatedListener = (echo, userData, setNotifications, setunreadN
|
|||||||
};
|
};
|
||||||
setNotifications(prev => [newNotification, ...prev]);
|
setNotifications(prev => [newNotification, ...prev]);
|
||||||
setunreadNotification(true);
|
setunreadNotification(true);
|
||||||
|
updatePendingMembers();
|
||||||
});
|
});
|
||||||
return channelName;
|
return channelName;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
export default async function fetchWrapper(
|
|
||||||
path,
|
|
||||||
data = null,
|
|
||||||
method = "GET",
|
|
||||||
headers = {}
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const res = await fetch(
|
|
||||||
`${import.meta.env.VITE_API_URL}${path}`,
|
|
||||||
{
|
|
||||||
method,
|
|
||||||
credentials: "include",
|
|
||||||
headers: {
|
|
||||||
"Accept": "application/json",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
...headers,
|
|
||||||
},
|
|
||||||
body: data ? JSON.stringify(data) : null,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
let responseData = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
responseData = await res.json();
|
|
||||||
} catch (_) {
|
|
||||||
responseData = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: res.status,
|
|
||||||
data: responseData,
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.error("❌ Erreur réseau :", err);
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: 0,
|
|
||||||
data: {
|
|
||||||
message: "Network error",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
/*export default async function fetchWrapper(path,data = null,method = "GET",headers = {}) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`${import.meta.env.VITE_API_URL}${path}`,
|
||||||
|
{
|
||||||
|
method,
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...headers,
|
||||||
|
},
|
||||||
|
body: data ? JSON.stringify(data) : null,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let responseData = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
responseData = await res.json();
|
||||||
|
} catch (_) {
|
||||||
|
responseData = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: res.status,
|
||||||
|
data: responseData,
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ Erreur réseau :", err);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 0,
|
||||||
|
data: {
|
||||||
|
message: "Network error",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
interface FetchResponse<T = any> {
|
||||||
|
status: number;
|
||||||
|
data: T | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function fetchWrapper(
|
||||||
|
path: string,
|
||||||
|
data: any = null,
|
||||||
|
method: string = "GET",
|
||||||
|
headers: Record<string, string> = {}
|
||||||
|
): Promise<FetchResponse> {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`${import.meta.env.VITE_API_URL}${path}`,
|
||||||
|
{
|
||||||
|
method,
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...headers,
|
||||||
|
},
|
||||||
|
body: data ? JSON.stringify(data) : null,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let responseData = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
responseData = await res.json();
|
||||||
|
} catch (_) {
|
||||||
|
responseData = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: res.status,
|
||||||
|
data: responseData,
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ Erreur réseau :", err);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 0,
|
||||||
|
data: {
|
||||||
|
message: "Network error",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
export default async function getXSRFToken() {
|
|
||||||
const response = await fetch(`${import.meta.env.VITE_API_URL}/sanctum/csrf-cookie`, {
|
|
||||||
method: 'GET',
|
|
||||||
credentials: 'include'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok && response.status !== 204) {
|
|
||||||
throw new Error(`Error : ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const name = 'XSRF-TOKEN';
|
|
||||||
const value = `; ${document.cookie}`;
|
|
||||||
const parts = value.split(`; ${name}=`);
|
|
||||||
if (parts.length === 2) {
|
|
||||||
return decodeURIComponent(parts.pop().split(';').shift());
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('Error: Invalid XSRF-TOKEN');
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/*export default async function getXSRFToken() {
|
||||||
|
const response = await fetch(`${import.meta.env.VITE_API_URL}/sanctum/csrf-cookie`, {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok && response.status !== 204) {
|
||||||
|
throw new Error(`Error : ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = 'XSRF-TOKEN';
|
||||||
|
const value = `; ${document.cookie}`;
|
||||||
|
const parts = value.split(`; ${name}=`);
|
||||||
|
if (parts.length === 2) {
|
||||||
|
return decodeURIComponent(parts.pop().split(';').shift());
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Error: Invalid XSRF-TOKEN');
|
||||||
|
}*/
|
||||||
|
|
||||||
|
export default async function getXSRFToken(): Promise<string> {
|
||||||
|
const response = await fetch(`${import.meta.env.VITE_API_URL}/sanctum/csrf-cookie`, {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok && response.status !== 204) {
|
||||||
|
throw new Error(`Error : ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = 'XSRF-TOKEN';
|
||||||
|
const value = `; ${document.cookie}`;
|
||||||
|
const parts = value.split(`; ${name}=`);
|
||||||
|
if (parts.length === 2) {
|
||||||
|
return decodeURIComponent(parts.pop()!.split(';').shift()!);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Error: Invalid XSRF-TOKEN');
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import fetchWrapper from "../fetchWrapper.js";
|
||||||
|
import getXSRFToken from "../getXSRF.js";
|
||||||
|
import Response from "../../interfaces/response.interface";
|
||||||
|
|
||||||
|
export default async function adminCreateUser(email: string, name: string, lastname: string, phone: string): Promise<Response> {
|
||||||
|
const csrfToken = await getXSRFToken();
|
||||||
|
|
||||||
|
return fetchWrapper(
|
||||||
|
"/api/users/create",
|
||||||
|
{ email, name, lastname, phone },
|
||||||
|
"POST",
|
||||||
|
{ "X-XSRF-TOKEN": csrfToken }
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import fetchWrapper from "../fetchWrapper";
|
||||||
|
|
||||||
|
export default async function deleteProfilePhoto(): Promise<{ status: number; ok: boolean; data?: any; error?: string }> {
|
||||||
|
try {
|
||||||
|
const response = await fetchWrapper("/api/profile-photo", null, "DELETE");
|
||||||
|
return { status: response.status || 0, ok: response.status >= 200 && response.status < 300, data: response.data };
|
||||||
|
} catch (err: any) {
|
||||||
|
return { status: 0, ok: false, error: err?.message || "Network error" };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import fetchWrapper from "../fetchWrapper.js";
|
|
||||||
import getXSRFToken from "../getXSRF.js";
|
|
||||||
|
|
||||||
export default async function deleteUser() {
|
|
||||||
const csrfToken = await getXSRFToken();
|
|
||||||
|
|
||||||
return fetchWrapper(
|
|
||||||
"/api/users",
|
|
||||||
null,
|
|
||||||
"DELETE",
|
|
||||||
{ "X-XSRF-TOKEN": csrfToken }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import fetchWrapper from "../fetchWrapper";
|
||||||
|
import getXSRFToken from "../getXSRF";
|
||||||
|
|
||||||
|
export default async function deleteUser(): Promise<{status: number;data: any;}> {
|
||||||
|
const csrfToken: string = await getXSRFToken();
|
||||||
|
|
||||||
|
return fetchWrapper(
|
||||||
|
"/api/users",
|
||||||
|
null,
|
||||||
|
"DELETE",
|
||||||
|
{ "X-XSRF-TOKEN": csrfToken }
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import uploadProfilePhoto from "./uploadProfilePhoto";
|
||||||
|
|
||||||
|
export default async function replaceProfilePhoto(file: File) {
|
||||||
|
return uploadProfilePhoto(file);
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import fetchWrapper from "../fetchWrapper.js";
|
|
||||||
import getXSRFToken from "../getXSRF.js";
|
|
||||||
|
|
||||||
export default async function updateUser(name, lastname, phone) {
|
|
||||||
const csrfToken = await getXSRFToken();
|
|
||||||
|
|
||||||
return fetchWrapper(
|
|
||||||
"/api/users/update",
|
|
||||||
{ name, lastname, phone },
|
|
||||||
"POST",
|
|
||||||
{ "X-XSRF-TOKEN": csrfToken }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/*import fetchWrapper from "../fetchWrapper.js";
|
||||||
|
import getXSRFToken from "../getXSRF.js";
|
||||||
|
|
||||||
|
export default async function updateUser(name, lastname, phone) {
|
||||||
|
const csrfToken = await getXSRFToken();
|
||||||
|
|
||||||
|
return fetchWrapper(
|
||||||
|
"/api/users/update",
|
||||||
|
{ name, lastname, phone },
|
||||||
|
"POST",
|
||||||
|
{ "X-XSRF-TOKEN": csrfToken }
|
||||||
|
);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
import fetchWrapper from "../fetchWrapper";
|
||||||
|
import getXSRFToken from "../getXSRF";
|
||||||
|
|
||||||
|
interface UserUpdateData {
|
||||||
|
name: string;
|
||||||
|
lastname: string;
|
||||||
|
phone: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RequestHeaders {
|
||||||
|
"X-XSRF-TOKEN": string;
|
||||||
|
}
|
||||||
|
export default async function updateUser(name: string,lastname: string,phone: string | null): Promise<Response> {
|
||||||
|
const csrfToken: string = await getXSRFToken();
|
||||||
|
const userData: UserUpdateData = {name,lastname,phone,};
|
||||||
|
const headers: RequestHeaders = {"X-XSRF-TOKEN": csrfToken,};
|
||||||
|
return fetchWrapper("/api/users/update",userData,"POST",headers);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import getXSRFToken from "../getXSRF.js";
|
||||||
|
|
||||||
|
export default async function uploadProfilePhoto(file: File): Promise<{ status: number; ok: boolean; data?: any; error?: string }> {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("photo", file);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const csrfToken = await getXSRFToken();
|
||||||
|
const response = await fetch(`${import.meta.env.VITE_API_URL}/api/profile-photo`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "X-XSRF-TOKEN": decodeURIComponent(csrfToken) },
|
||||||
|
credentials: "include",
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const status = response.status;
|
||||||
|
let data = null;
|
||||||
|
try {
|
||||||
|
data = await response.json();
|
||||||
|
} catch (e) {
|
||||||
|
// no json
|
||||||
|
}
|
||||||
|
|
||||||
|
return { status, ok: response.ok, data };
|
||||||
|
} catch (err: any) {
|
||||||
|
return { status: 0, ok: false, error: err?.message || "Network error" };
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+6
@@ -0,0 +1,6 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
declare module "*.module.css" {
|
||||||
|
const classes: { [key: string]: string };
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
+7
-50
@@ -1,49 +1,4 @@
|
|||||||
{
|
{
|
||||||
// Visit https://aka.ms/tsconfig to read more about this file
|
|
||||||
/*"compilerOptions": {*/
|
|
||||||
// File Layout
|
|
||||||
// "rootDir": "./src",
|
|
||||||
// "outDir": "./dist",
|
|
||||||
|
|
||||||
// Environment Settings
|
|
||||||
// See also https://aka.ms/tsconfig/module
|
|
||||||
/*"module": "nodenext",
|
|
||||||
"target": "esnext",
|
|
||||||
"types": [],*/
|
|
||||||
// For nodejs:
|
|
||||||
// "lib": ["esnext"],
|
|
||||||
// "types": ["node"],
|
|
||||||
// and npm install -D @types/node
|
|
||||||
|
|
||||||
// Other Outputs
|
|
||||||
/*"sourceMap": true,
|
|
||||||
"declaration": true,
|
|
||||||
"declarationMap": true,*/
|
|
||||||
|
|
||||||
// Stricter Typechecking Options
|
|
||||||
/*"noUncheckedIndexedAccess": true,
|
|
||||||
"exactOptionalPropertyTypes": true,*/
|
|
||||||
|
|
||||||
// Style Options
|
|
||||||
// "noImplicitReturns": true,
|
|
||||||
// "noImplicitOverride": true,
|
|
||||||
// "noUnusedLocals": true,
|
|
||||||
// "noUnusedParameters": true,
|
|
||||||
// "noFallthroughCasesInSwitch": true,
|
|
||||||
// "noPropertyAccessFromIndexSignature": true,
|
|
||||||
|
|
||||||
// Recommended Options
|
|
||||||
/* "strict": true,
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
"verbatimModuleSyntax": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"noUncheckedSideEffectImports": true,
|
|
||||||
"moduleDetection": "force",
|
|
||||||
"skipLibCheck": true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{*/
|
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||||
@@ -52,11 +7,13 @@
|
|||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
|
"noUncheckedIndexedAccess": false,
|
||||||
"noUncheckedIndexedAccess": true,
|
|
||||||
"exactOptionalPropertyTypes": true,
|
"exactOptionalPropertyTypes": true,
|
||||||
|
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true,
|
||||||
}
|
"allowJs": true,
|
||||||
|
"esModuleInterop": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
// "exclude": ["**/*.js"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user