diff --git a/src/components/Filter/Filter.jsx b/src/components/Filter/Filter.jsx deleted file mode 100644 index eb0bf87..0000000 --- a/src/components/Filter/Filter.jsx +++ /dev/null @@ -1,56 +0,0 @@ -import styles from "./Filter.module.css"; -import Button from "../ui/button/button.tsx" - -function Filter({isFilterVisible, filters, setFilters}){ - const setAlphabeticalOrder = (order) => { - setFilters(prev => ({ - ...prev, - alphabetical : prev.alphabetical === order ? null : order - })); - }; - - const setYearOrder = (order) => { - setFilters(prev => ({ - ...prev, - yearOrder: prev.yearOrder === order ? null : order - })); - }; - - return ( -
- - - - - - - - - - - - - - - -
- ) -} - -export default Filter; \ No newline at end of file diff --git a/src/components/Filter/Filter.tsx b/src/components/Filter/Filter.tsx new file mode 100644 index 0000000..d917928 --- /dev/null +++ b/src/components/Filter/Filter.tsx @@ -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>; +}; + +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 ( +
+ + + + + + + + + + + + + + + +
+ ); +} + +export default Filter; diff --git a/src/components/Header/Header.jsx b/src/components/Header/Header.jsx index 829c26e..314943b 100644 --- a/src/components/Header/Header.jsx +++ b/src/components/Header/Header.jsx @@ -10,6 +10,7 @@ import { userNotificationsListener } from "../../utils/echo/listeners/userNotifi import NotificationCard from "../NotificationCard/NotificationCard.jsx"; import { EventContext } from "../../contexts/events/EventContext.js"; import {AuthContext} from "../../contexts/auth/AuthContext.js"; +import { PendingMembersContext } from "../../contexts/pendingMembers/PendingMembersContext"; function Header() { @@ -20,6 +21,7 @@ function Header() { const [mobileMenu, setMobileMenu] = useState(false); const [notifications, setNotifications] = useState([]); const notificationRef = useRef(null); + const { updatePendingMembers } = useContext(PendingMembersContext); useEffect(() => { @@ -41,7 +43,7 @@ function Header() { echoInstance = echo; const activeChannels = [ - userCreatedListener(echo, user, setNotifications, setunreadNotification), + userCreatedListener(echo, user, setNotifications, setunreadNotification, updatePendingMembers), userNotificationsListener(echo, user, setNotifications, setunreadNotification), ]; channelsToLeave = activeChannels.filter(name => name !== null); @@ -86,7 +88,7 @@ function Header() { } } } catch (err) { - console.error('Erreur suppression notification :', err); + console.error('Erreur pour ouvrir/fermer le menu de notifications :', err); } } diff --git a/src/components/SearchBar/SearchBar.jsx b/src/components/SearchBar/SearchBar.jsx deleted file mode 100644 index a1dded2..0000000 --- a/src/components/SearchBar/SearchBar.jsx +++ /dev/null @@ -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 ( -
-
element.preventDefault()}> - Rechercher - setSearchQuery(element.target.value)} - /> -
- -
- {searchResults.length > 0 ? ( -
- ) : null} -
- -
- {searchResults.map((result, i) => ( - (location.pathname.includes("events")) ? ( - -
-
- {result.image == null ? ( - Pas de photo de l'événement - ) : ( - Photo de l'événement - )} -
-

{result.name}

-
- - ) : location.pathname.includes("volunteers") ? ( - -
-
- {result.image == null ? ( - Pas de photo de l'événement - ) : ( - Photo de l'événement - )} -
-

{result.name} {result.lastname}

-
- - ) : null - ))} -
-
- ); -} - -export default SearchBar; \ No newline at end of file diff --git a/src/components/SearchBar/SearchBar.module.css b/src/components/SearchBar/SearchBar.module.css index d35c808..c6a093d 100644 --- a/src/components/SearchBar/SearchBar.module.css +++ b/src/components/SearchBar/SearchBar.module.css @@ -1,6 +1,8 @@ .searchBarContainer{ position: fixed; - top: 4rem; + top: 20vh; + left: 50%; + transform: translateX(-50%); z-index: 100; width: 60%; background: #fff; diff --git a/src/components/SearchBar/SearchBar.tsx b/src/components/SearchBar/SearchBar.tsx new file mode 100644 index 0000000..d1ef4c6 --- /dev/null +++ b/src/components/SearchBar/SearchBar.tsx @@ -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(""); + const [searchResults, setSearchResults] = useState([]); + const location: any = useLocation(); + + useEffect(() => { + const performSearch = async (query: string): Promise => { + 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 = setTimeout(() => { + performSearch(searchQuery); + }, 300); + + return () => { + clearTimeout(handler); + }; + }, [searchQuery, location.pathname]); + + return ( +
+
) => + element.preventDefault() + } + > + Rechercher + ) => + setSearchQuery(element.target.value) + } + /> +
+ +
+ {searchResults.length > 0 ? ( +
+ ) : null} +
+ +
+ {searchResults.map((result: any, i: number) => + location.pathname.includes("events") ? ( + +
+
+ {result.image == null ? ( + Pas de photo de l'événement + ) : ( + Photo de l'événement + )} +
+

{result.name}

+
+ + ) : location.pathname.includes("volunteers") ? ( + +
+
+ {result.image == null ? ( + Pas de photo de l'événement + ) : ( + Photo de l'événement + )} +
+

+ {result.name} {result.lastname} +

+
+ + ) : null + )} +
+
+ ); +} + +export default SearchBar; diff --git a/src/components/Task/Task.jsx b/src/components/Task/Task.jsx deleted file mode 100644 index d14cabf..0000000 --- a/src/components/Task/Task.jsx +++ /dev/null @@ -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 ( -
-
-
-

{event && event.name} - {task.name}

-

{task.description}

-

{formatDate(task.start)}

-
-
-
- ); -} - -export default Task; \ No newline at end of file diff --git a/src/components/Task/Task.tsx b/src/components/Task/Task.tsx new file mode 100644 index 0000000..7c939b2 --- /dev/null +++ b/src/components/Task/Task.tsx @@ -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 = ({ task }) => { + const [event, setEvent] = useState(null); + + useEffect(() => { + const fetchEvent = async (): Promise => { + 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 ( +
+
+
+

+ {event?.name} - {task.name} +

+

{task.description}

+

+ {formatDate(task.start)} +

+
+
+
+ ); +}; + +export default Task; \ No newline at end of file diff --git a/src/components/ToolBar/ToolBar.jsx b/src/components/ToolBar/ToolBar.jsx index 874fa70..4461da7 100644 --- a/src/components/ToolBar/ToolBar.jsx +++ b/src/components/ToolBar/ToolBar.jsx @@ -1,59 +1,16 @@ import styles from "./ToolBar.module.css" -import Filter from "../Filter/Filter.jsx"; -import {useContext, useEffect, useState} from "react"; -import SearchBar from "../SearchBar/SearchBar.jsx"; -import CreateEventBtn from "../createEventBtn/CreateEventBtn.jsx"; -import Button from "../ui/button/button.tsx"; -import {AuthContext} from "../../contexts/auth/AuthContext.js"; +import CreateEventBtn from "./tools/createEventBtn/CreateEventBtn.jsx"; +import SearchBtn from "./tools/SearchBtn.tsx"; +import FilterBtn from "./tools/FilterBtn.tsx"; function ToolBar({setFilters, filters, showCreate = true}) { - const { user } = useContext(AuthContext); - const [isFilterVisible, setIsFilterVisible] = useState(false); - const [isSearchVisible, setIsSearchVisible] = useState(false); - const toggleFilters = () => { - setIsFilterVisible(prev => !prev); - }; - - - useEffect(() => { - if (isSearchVisible) { - document.documentElement.style.overflow = 'hidden'; - } else { - document.documentElement.style.overflow = ''; - } - - return () => { - document.documentElement.style.overflow = ''; - }; - }, [isSearchVisible]); - - return (
- {isSearchVisible ? ( -
-
setIsSearchVisible(false)}>
- -
- ) : null}
- {showCreate && user.isAdmin ? ( - - ) : null} - - - -
- -
- -
-
+ {showCreate && } + +
); diff --git a/src/components/ToolBar/tools/FilterBtn.tsx b/src/components/ToolBar/tools/FilterBtn.tsx new file mode 100644 index 0000000..0f87ae9 --- /dev/null +++ b/src/components/ToolBar/tools/FilterBtn.tsx @@ -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
+ +
+ +
+
+} \ No newline at end of file diff --git a/src/components/ToolBar/tools/SearchBtn.tsx b/src/components/ToolBar/tools/SearchBtn.tsx new file mode 100644 index 0000000..316db18 --- /dev/null +++ b/src/components/ToolBar/tools/SearchBtn.tsx @@ -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 ? ( +
+
setIsSearchVisible(false)}>
+ +
+ ) : null} + +
+ +
+ + +} \ No newline at end of file diff --git a/src/components/createEventBtn/CreateEventBtn.jsx b/src/components/ToolBar/tools/createEventBtn/CreateEventBtn.jsx similarity index 85% rename from src/components/createEventBtn/CreateEventBtn.jsx rename to src/components/ToolBar/tools/createEventBtn/CreateEventBtn.jsx index 019b836..11b96d6 100644 --- a/src/components/createEventBtn/CreateEventBtn.jsx +++ b/src/components/ToolBar/tools/createEventBtn/CreateEventBtn.jsx @@ -1,14 +1,16 @@ import styles from "./createEventBtn.module.css" import { useState, useContext } from "react"; -import Modal from "../ui/modal/modal.tsx"; -import Button from "../ui/button/button.tsx"; -import TextInput from "../ui/input/input.tsx"; -import { EventContext } from "../../contexts/events/EventContext.js"; +import Modal from "../../../ui/modal/modal.tsx"; +import Button from "../../../ui/button/button.tsx"; +import TextInput from "../../../ui/input/input.tsx"; +import { EventContext } from "../../../../contexts/events/EventContext.js"; +import {AuthContext} from "../../../../contexts/auth/AuthContext.ts"; export default function CreateEventBtn() { const { addEvent } = useContext(EventContext); + const { user } = useContext(AuthContext); const [isOpen, setIsOpen] = useState(false); const [name, setName] = useState(""); @@ -24,6 +26,8 @@ export default function CreateEventBtn() { setIsOpen(false); } + if(!user.isAdmin) return null; + return <> diff --git a/src/components/createEventBtn/createEventBtn.module.css b/src/components/ToolBar/tools/createEventBtn/createEventBtn.module.css similarity index 100% rename from src/components/createEventBtn/createEventBtn.module.css rename to src/components/ToolBar/tools/createEventBtn/createEventBtn.module.css diff --git a/src/components/ui/button/button.tsx b/src/components/ui/button/button.tsx index 1866c39..b57fc8c 100644 --- a/src/components/ui/button/button.tsx +++ b/src/components/ui/button/button.tsx @@ -7,7 +7,7 @@ interface ButtonProps { children: ReactNode; variant?: ButtonVariant; onClick?: MouseEventHandler; - className?: string; + className?: string | undefined; } const Button = ({ diff --git a/src/components/ui/loading/loading.module.css b/src/components/ui/loading/loading.module.css new file mode 100644 index 0000000..b12ba9f --- /dev/null +++ b/src/components/ui/loading/loading.module.css @@ -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); + } +} \ No newline at end of file diff --git a/src/components/ui/loading/loading.tsx b/src/components/ui/loading/loading.tsx new file mode 100644 index 0000000..536cc99 --- /dev/null +++ b/src/components/ui/loading/loading.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import styles from "./loading.module.css"; + +const Loading: React.FC = () => { + return ( +
+
+
+ ); +}; + +export default Loading; \ No newline at end of file diff --git a/src/contexts/pendingMembers/PendingMembersContext.js b/src/contexts/pendingMembers/PendingMembersContext.js new file mode 100644 index 0000000..1941409 --- /dev/null +++ b/src/contexts/pendingMembers/PendingMembersContext.js @@ -0,0 +1,3 @@ +import { createContext } from "react"; + +export const PendingMembersContext = createContext(null); \ No newline at end of file diff --git a/src/contexts/pendingMembers/PendingMembersProvider.jsx b/src/contexts/pendingMembers/PendingMembersProvider.jsx new file mode 100644 index 0000000..48ff64d --- /dev/null +++ b/src/contexts/pendingMembers/PendingMembersProvider.jsx @@ -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 ( + + {children} + + ); +} \ No newline at end of file diff --git a/src/main.jsx b/src/main.jsx index bd63d05..b742534 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -6,16 +6,19 @@ import router from './router'; import {AuthProvider} from "./contexts/auth/authProvider.jsx"; import {EventProvider} from "./contexts/events/EventProvider.jsx"; import {EventDetailProvider} from "./contexts/eventDetail/eventDetailProvider.jsx"; +import {PendingMembersProvider} from "./contexts/pendingMembers/PendingMembersProvider.jsx"; createRoot(document.getElementById('root')).render( - - - - - + + + + + + + , ) diff --git a/src/pages/Admin/components/pendingMembers/PendingMembers.jsx b/src/pages/Admin/components/pendingMembers/PendingMembers.jsx index 19b1a1a..f8280f6 100644 --- a/src/pages/Admin/components/pendingMembers/PendingMembers.jsx +++ b/src/pages/Admin/components/pendingMembers/PendingMembers.jsx @@ -1,11 +1,10 @@ -import React, {useState, useEffect} from "react"; +import React, {useState, useEffect, useContext} from "react"; import Button from "../../../../components/ui/button/button.tsx"; import styles from "./PendingMembers.module.css"; 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 deleteOtherUser from "../../../../utils/users/deleteOtherUser.js"; +import { PendingMembersContext } from "../../../../contexts/pendingMembers/PendingMembersContext"; function PendingMembers() { @@ -13,23 +12,10 @@ function PendingMembers() { const [open, setOpen] = useState(false); const [message, setMessage] = useState(""); const [title, setTitle] = useState(""); - - 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([]); - } + const { pendingMembers, updatePendingMembers } = useContext(PendingMembersContext); useEffect(() => { - fetchUsers(); + updatePendingMembers(); }, []); const handleValidate = async (id, name) => { @@ -38,14 +24,12 @@ function PendingMembers() { if(result.status === 200) { setTitle("Utilisateur accepté avec succès") setMessage(`L'utilisateur ${name} a été accepté avec succès`) - setOpen(true); } else { setTitle("Erreur") setMessage(`Une erreur est survenue durant l'acceptation de l'utilisateur ${name}`) - setOpen(true); } - - fetchUsers(); + setOpen(true); + updatePendingMembers(); } const handleRefuse = async (id, name) => { @@ -61,7 +45,7 @@ function PendingMembers() { setOpen(true); } - fetchUsers(); + updatePendingMembers(); } return ( diff --git a/src/pages/Profile/ProfilePage.module.css b/src/pages/Profile/ProfilePage.module.css index e354d42..7c2a179 100644 --- a/src/pages/Profile/ProfilePage.module.css +++ b/src/pages/Profile/ProfilePage.module.css @@ -7,11 +7,25 @@ } .profileboard { +<<<<<<< HEAD width: 100%; padding: 16px; border-radius: 20px; box-sizing: border-box; +======= + width: 100%; + padding: 16px; + border-radius: 20px; + box-sizing: border-box; } + +.settingImage { + display: flex; + justify-content: center; + align-items: center; +>>>>>>> dev +} + .contentWrapper { display: flex; flex-direction: column; @@ -77,12 +91,42 @@ margin-bottom: 16px; } +<<<<<<< HEAD .headerProfile h2 { font-size: 1.6rem; margin: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; +======= +.description h2 { + font-size: 1.6rem; + margin-bottom: 10px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.description button { + width: 44px; + height: 44px; + min-width: 44px; + min-height: 44px; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + background: transparent; + border: none; + cursor: pointer; +} + +.modifyIcon { + width: 22px; + height: 22px; + display: block; + margin: 0; +>>>>>>> dev } /* Conteneur des informations principales */ diff --git a/src/pages/Profile/ProfilePage.tsx b/src/pages/Profile/ProfilePage.tsx index 223b63c..fe8b82a 100644 --- a/src/pages/Profile/ProfilePage.tsx +++ b/src/pages/Profile/ProfilePage.tsx @@ -1,45 +1,91 @@ -import { useContext, useEffect, useState, useRef } from "react"; -import styles from "./ProfilePage.module.css"; -import { AuthContext } from "../../contexts/auth/AuthContext"; -import formatDate from "../../utils/date/formatDate"; import getXSRFToken from "../../utils/getXSRF.js"; import fetchWrapper from "../../utils/fetchWrapper"; +import {useContext, useEffect, useState} from "react"; +import styles from "./ProfilePage.module.css"; +import {AuthContext} from "../../contexts/auth/AuthContext"; +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"; interface TaskType { id: number; - title?: string; - completed?: boolean; - [key: string]: unknown; + 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 User { - name: string; - lastname: string; - role: string; - email: string; - phone: string | null; - created_at: string; - tasks: TaskType[]; - profile_photo_path?: string | null; + id: number; + name: string; + lastname: string; + role: string; + email: string; + phone: string | null; + created_at: string; + isAdmin: boolean; + tasks: TaskType[]; + profile_photo_path?: string | null; } interface AuthContextType { - user: User | null; - update: () => void; + user: User | null; + update: () => void; } function ProfilePage() { - const csrfToken = getXSRFToken(); + const navigate = useNavigate(); + const { id } = useParams(); const { user, update } = useContext(AuthContext) as AuthContextType; - /*const [profilePicture, setProfilePicture] = useState( - user?.profile_photo_path ? `${import.meta.env.VITE_API_URL}/storage/${user.profile_photo_path}`: null);*/ + const [profileUser, setProfileUser] = useState(null); const [profilePicture, setProfilePicture] = useState(null); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); const fileInputRef = useRef(null); - // Fonction pour déclencher l'input file + const isOwnProfile = !id || user?.id.toString() === id; + + // Charger le profil de l'utilisateur (soi-même ou un autre) + useEffect(() => { + (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]); + + // Fonctions pour la photo de profil (uniquement si c'est le profil de l'utilisateur connecté) const handleEditPictureClick = () => { if (fileInputRef.current) { fileInputRef.current.click(); @@ -47,80 +93,56 @@ function ProfilePage() { }; const handleFileChange = async (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (!file) return; + const file = e.target.files?.[0]; + if (!file) return; - const formData = new FormData(); - formData.append("photo", file); + const formData = new FormData(); + formData.append("photo", file); - try { - const response = await fetch(`${import.meta.env.VITE_API_URL}/api/profile-photo`, { - method: "POST", - headers: { - "X-XSRF-TOKEN": decodeURIComponent(await csrfToken), - }, - credentials: "include", - body: formData, -}); - - if (response.ok) { - const data = await response.json(); - setProfilePicture(`${import.meta.env.VITE_API_URL}/storage/${data?.profile_photo_path}`); - setSuccess("Photo de profil mise à jour avec succès !"); - setError(null); - await update(); - - /*if (response.ok) { - const data = await response.json(); - console.log("data:", data); - console.log("URL:", `/storage/${data?.profile_photo_path}`); - - setProfilePicture(`${import.meta.env.VITE_API_URL}/storage/${data?.profile_photo_path}`); - setSuccess("Photo de profil mise à jour avec succès !"); - setError(null); - update();*/ -} else { - const errorData = await response.json(); - setError(errorData?.message || "Erreur lors de l'upload de la photo."); - setSuccess(null); - } - } catch (err) { - setError("Erreur réseau. Vérifiez votre connexion."); - setSuccess(null); - } - - -}; - - // Fonction pour supprimer la photo de profil - const handleDeletePicture = async () => { try { - const token = localStorage.getItem("token"); - const response = await fetchWrapper("/api/profile-photo",null,"DELETE"); - if (response.status >= 200 && response.status < 300) { - setProfilePicture(null); - setSuccess("Photo de profil supprimée avec succès !"); + 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, + }); + + if (response.ok) { + const data = await response.json(); + setProfilePicture(`${import.meta.env.VITE_API_URL}/storage/${data.profile_photo_path}`); + setSuccess("Photo mise à jour !"); setError(null); - await update(); + await update(); } else { - const errorData =response.data; - setError(errorData.message || "Erreur lors de la suppression de la photo."); + const errorData = await response.json(); + setError(errorData.message || "Erreur lors de l'upload."); setSuccess(null); } } catch (err) { - setError("Erreur réseau. Vérifiez votre connexion."); + setError("Erreur réseau."); setSuccess(null); } }; -useEffect(() => { - console.log("user reçu:", user); - if (user?.profile_photo_path) { - setProfilePicture(`${import.meta.env.VITE_API_URL}/storage/${user.profile_photo_path}`); - } else if (user && !user.profile_photo_path && !profilePicture) { - setProfilePicture(null); - } -}, [user]); + const handleDeletePicture = async () => { + try { + const response = await fetchWrapper("/api/profile-photo", null, "DELETE"); + if (response.status >= 200 && response.status < 300) { + setProfilePicture(null); + setSuccess("Photo supprimée !"); + setError(null); + await update(); + } else { + const errorData = response.data; + setError(errorData.message || "Erreur lors de la suppression."); + setSuccess(null); + } + } catch (err) { + setError("Erreur réseau."); + setSuccess(null); + } + }; return (
@@ -128,27 +150,25 @@ useEffect(() => {
Photo de profil
-
- - {user?.profile_photo_path && ( - - )} -
+ {user?.profile_photo_path && ( + + )} +
+ )} + {

- {user?.name} {user?.lastname} + {profileUser?.name} {profileUser?.lastname}

+ {user?.isAdmin && profileUser && !isOwnProfile && ( + + )}
-

- Role : {user?.role} -

-

- Membre depuis :{" "} - {user && formatDate(user.created_at)} -

+

Role : {profileUser?.role}

+

Membre depuis : {profileUser && formatDate(profileUser.created_at)}

-

- Mail : {user?.email} -

-

- Téléphone :{" "} - {user?.phone ?? "Pas de numéro enregistré"} -

+

Mail : {profileUser?.email}

+

Téléphone : {profileUser?.phone ?? "Non renseigné"}

-
- {/* */} -
+ {isOwnProfile && ( +
+ +
+ )}

Tâches :

- {/* {user && user.tasks.length > 0 ? ( - user.tasks.map((task) => ( - - )) + {profileUser?.tasks.length ? ( + profileUser.tasks.map((task) => ) ) : ( -

Aucune tâche pour le moment.

- )} */} +

Aucune tâche.

+ )}
@@ -207,5 +220,4 @@ useEffect(() => { ); } -export default ProfilePage; - +export default ProfilePage; \ No newline at end of file diff --git a/src/pages/Profile/components/SettingsModal/SettingsModal.jsx b/src/pages/Profile/components/SettingsModal/SettingsModal.jsx deleted file mode 100644 index c5e0ff3..0000000 --- a/src/pages/Profile/components/SettingsModal/SettingsModal.jsx +++ /dev/null @@ -1,86 +0,0 @@ -import Modal from "../../../../components/ui/modal/modal.tsx"; -import Button from "../../../../components/ui/button/button.tsx"; -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.tsx"; -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 ( - <> - - - setOpen(false)}> -
- -
-

Compte

- - - -
-

Changer de Prénom

- setName(e.target.value)} type={"text"} /> -
- -
-

Changer de nom

- setLastName(e.target.value)} type={"text"} /> -
- -
-

Changer de numéro de téléphone

- setPhone(e.target.value)} type={"text"} /> -
- - - -
- -
-

Apparence

- -
-
-
- - - setIsDelete(false)}> -
-

Êtes vous vraiment sûr de vouloir supprimer votre compte ?

- -
-
- - ) -} \ No newline at end of file diff --git a/src/pages/Profile/components/SettingsModal/SettingsModal.module.css b/src/pages/Profile/components/SettingsModal/SettingsModal.module.css index d6b0b12..7808db3 100644 --- a/src/pages/Profile/components/SettingsModal/SettingsModal.module.css +++ b/src/pages/Profile/components/SettingsModal/SettingsModal.module.css @@ -1,73 +1,98 @@ .content { width: 100%; - padding: 16px; + padding: 20px; display: flex; flex-direction: column; - gap: 20px; + gap: 28px; } .modifyIcon { - height: 28px; - width: 28px; + height: 26px; + width: 26px; + transition: transform 0.2s ease, opacity 0.2s ease; +} + +.modifyIcon:hover { + transform: rotate(15deg); + opacity: 0.8; } .section { display: flex; flex-direction: column; - gap: 12px; - margin-top: 10px; + gap: 16px; + padding: 18px; + border-radius: 12px; + background: var(--glass-bg, rgba(255, 255, 255, 0.25)); + backdrop-filter: blur(6px); } .section h3 { - font-size: 1.1rem; - margin-bottom: 5px; + font-size: 1.15rem; + font-weight: 600; } .section h4 { - font-size: 0.95rem; + font-size: 0.9rem; font-weight: 500; -} - -.submitBtn { - width: 100%; - margin-top: 15px; + opacity: 0.85; } .section button { 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 { - padding: 24px; + padding: 28px; + gap: 32px; } - .section h3 { - font-size: 1.2rem; + .section { + padding: 22px; + gap: 18px; } .submitBtn { width: auto; align-self: center; - min-width: 200px; + min-width: 220px; } .section button { width: fit-content; } } + @media (min-width: 900px) { .content { - padding: 30px; - gap: 30px; + padding: 36px; + gap: 36px; } .section { - gap: 14px; + padding: 26px; + border-radius: 16px; } .modifyIcon { height: 30px; width: 30px; } -} +} \ No newline at end of file diff --git a/src/pages/Profile/components/SettingsModal/SettingsModal.tsx b/src/pages/Profile/components/SettingsModal/SettingsModal.tsx index 452d668..4c2e7b1 100644 --- a/src/pages/Profile/components/SettingsModal/SettingsModal.tsx +++ b/src/pages/Profile/components/SettingsModal/SettingsModal.tsx @@ -1,3 +1,4 @@ +<<<<<<< HEAD /*import Modal from "../../../../components/ui/modal/modal.jsx"; import Button from "../../../../components/ui/button/button.jsx"; import styles from "./SettingsModal.module.css" @@ -51,29 +52,171 @@ export default function SettingsModal() {

Changer de Prénom

setName(e.target.value)} type={"text"} /> +======= +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(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 = (): void => { + logout(); + }; + + const handleSubmit = async (): Promise => { + await updateUser(name, lastname, phone || null); + update(); + setOpen(false); + }; + + const handleDelete = async (): Promise => { + await deleteUser(); + update(); + }; + + const handleNameChange = (e: ChangeEvent): void => { + setName(e.target.value); + }; + + const handleLastnameChange = (e: ChangeEvent): void => { + setLastName(e.target.value); + }; + + const handlePhoneChange = (e: ChangeEvent): void => { + setPhone(e.target.value); + }; + + + return ( + <> + + + setOpen(false)} + > +
+
+

Compte

+ + + + + +
+

Changer de Prénom

+ +>>>>>>> dev

Changer de nom

+<<<<<<< HEAD setLastName(e.target.value)} type={"text"} /> +======= + +>>>>>>> dev

Changer de numéro de téléphone

+<<<<<<< HEAD setPhone(e.target.value)} type={"text"} />
+======= + +
+ + +>>>>>>> dev

Apparence

+<<<<<<< HEAD +======= + +>>>>>>> dev
+<<<<<<< HEAD setIsDelete(false)}>
@@ -185,3 +328,26 @@ export default function SettingsModal() { ); } +======= + setIsDelete(false)} + > +
+

+ Êtes vous vraiment sûr de vouloir supprimer votre compte ? +

+ +
+
+ + ); +} +>>>>>>> dev diff --git a/src/pages/VolunteerProfile/components/manageMember/ManageMember.jsx b/src/pages/Profile/components/manageMember/ManageMember.jsx similarity index 100% rename from src/pages/VolunteerProfile/components/manageMember/ManageMember.jsx rename to src/pages/Profile/components/manageMember/ManageMember.jsx diff --git a/src/pages/VolunteerProfile/components/manageMember/deactivateMember/deactivateMemberBtn.jsx b/src/pages/Profile/components/manageMember/deactivateMember/deactivateMemberBtn.jsx similarity index 100% rename from src/pages/VolunteerProfile/components/manageMember/deactivateMember/deactivateMemberBtn.jsx rename to src/pages/Profile/components/manageMember/deactivateMember/deactivateMemberBtn.jsx diff --git a/src/pages/VolunteerProfile/components/manageMember/manageMember.module.css b/src/pages/Profile/components/manageMember/manageMember.module.css similarity index 100% rename from src/pages/VolunteerProfile/components/manageMember/manageMember.module.css rename to src/pages/Profile/components/manageMember/manageMember.module.css diff --git a/src/pages/VolunteerProfile/components/manageMember/modifyrole/ModifyRole.jsx b/src/pages/Profile/components/manageMember/modifyrole/ModifyRole.jsx similarity index 100% rename from src/pages/VolunteerProfile/components/manageMember/modifyrole/ModifyRole.jsx rename to src/pages/Profile/components/manageMember/modifyrole/ModifyRole.jsx diff --git a/src/pages/VolunteerProfile/VolunteerProfilePage.jsx b/src/pages/VolunteerProfile/VolunteerProfilePage.jsx deleted file mode 100644 index 763b7f7..0000000 --- a/src/pages/VolunteerProfile/VolunteerProfilePage.jsx +++ /dev/null @@ -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 ( -
-
-
-
- Photo de profil -
- -
-
-

{user.name} {user.lastname}

- {currentUser?.isAdmin && ( - - )} -
- -
-
-

Role : {user.role}

-

Membre depuis : {formatDate(user.created_at)}

-
-
-

Mail : {user.email}

-

- Téléphone :{" "} - {user.phone ?? "Pas de numéro enregistré"} -

-
-
- -

Tâches :

- {user.tasks?.length > 0 ? ( - user.tasks.map((task, index) => ( - - )) - ) : ( -

Aucune tâche pour le moment.

- )} -
-
-
-
- ); -} diff --git a/src/pages/VolunteerProfile/VolunteerProfilePage.module.css b/src/pages/VolunteerProfile/VolunteerProfilePage.module.css deleted file mode 100644 index 834af59..0000000 --- a/src/pages/VolunteerProfile/VolunteerProfilePage.module.css +++ /dev/null @@ -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; - } -} diff --git a/src/pages/Volunteers/VolunteerCard.jsx b/src/pages/Volunteers/VolunteerCard.jsx deleted file mode 100644 index f69f6df..0000000 --- a/src/pages/Volunteers/VolunteerCard.jsx +++ /dev/null @@ -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.tsx"; - -export default function VolunteerCard({ user }) { - - const [isExpanded, setIsExpanded] = useState(false); - - const toggleExpand = () => { - setIsExpanded(!isExpanded); - }; - - const navigate = useNavigate(); - - return ( -
setIsExpanded(!isExpanded))}> - -
-

- {user?.name}{" "} - {user?.lastname} -

- - -
- -
- {!isExpanded ? ( -
-

Rôle : {user?.role}

-

Membre depuis : {user?.created_at && formatDate(user.created_at)}

-
- ) : ( -
-
- profile -
- -
-
-

Rôle : {user?.role}

-

Membre depuis : {user?.created_at && formatDate(user.created_at)}

-
- -
-

Email : {user?.email}

- {user?.phone &&

Téléphone : {user.phone}

} -
-
-
- )} -
- - {isExpanded && ( -
- -
- )} - -
- ); - - - -} \ No newline at end of file diff --git a/src/pages/Volunteers/VolunteersPage.jsx b/src/pages/Volunteers/VolunteersPage.jsx deleted file mode 100644 index 169275a..0000000 --- a/src/pages/Volunteers/VolunteersPage.jsx +++ /dev/null @@ -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 ( -
- -
- {filter(users, filters, "user").map((user, index) => ( - - ))} -
-
- ); -} - -export default VolunteerPage; diff --git a/src/pages/Volunteers/VolunteersPage.module.css b/src/pages/Volunteers/VolunteersPage.module.css index 6ef455b..b953cb5 100644 --- a/src/pages/Volunteers/VolunteersPage.module.css +++ b/src/pages/Volunteers/VolunteersPage.module.css @@ -3,33 +3,18 @@ position: relative; 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 { - display: flex; - flex-direction: column; - align-items: center; - gap: 10px; + display: grid; + gap: 16px; + width: 100%; + grid-template-columns: 1fr; } + +.usersList > * { + min-width: 0; +} + .profileboard { width: 100%; 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) { .container { padding: 20px; } .usersList { gap: 20px; + grid-template-columns: repeat(3, 1fr); + padding-right: 5em; + padding-left: 5em; } .profileboard { max-width: 700px; diff --git a/src/pages/Volunteers/VolunteersPage.tsx b/src/pages/Volunteers/VolunteersPage.tsx new file mode 100644 index 0000000..ae43fbc --- /dev/null +++ b/src/pages/Volunteers/VolunteersPage.tsx @@ -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({ + alphabetical: "asc", + yearOrder: "desc", + }); + + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchUsers = async (): Promise => { + 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 => { + 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 ; + + return ( +
+ + +
+ {filter(users, filters, "user").map((user: User) => ( + + ))} +
+ +
+ ); +}; + +export default VolunteerPage; diff --git a/src/pages/Volunteers/components/VolunteersCard/VolunteerCard.tsx b/src/pages/Volunteers/components/VolunteersCard/VolunteerCard.tsx new file mode 100644 index 0000000..a3d5f01 --- /dev/null +++ b/src/pages/Volunteers/components/VolunteersCard/VolunteerCard.tsx @@ -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 ( +
+
+ profile +

{user.name} {user.lastname}

+
+
+

{user.email}

+ +
+
+ ); +} diff --git a/src/pages/Volunteers/components/VolunteersCard/VolunteersCard.module.css b/src/pages/Volunteers/components/VolunteersCard/VolunteersCard.module.css new file mode 100644 index 0000000..4ed1ccf --- /dev/null +++ b/src/pages/Volunteers/components/VolunteersCard/VolunteersCard.module.css @@ -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; +} diff --git a/src/pages/Volunteers/interfaces/task.interface.ts b/src/pages/Volunteers/interfaces/task.interface.ts new file mode 100644 index 0000000..b6206c9 --- /dev/null +++ b/src/pages/Volunteers/interfaces/task.interface.ts @@ -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; + } +} \ No newline at end of file diff --git a/src/pages/Volunteers/interfaces/user.interface.ts b/src/pages/Volunteers/interfaces/user.interface.ts new file mode 100644 index 0000000..e22a1cd --- /dev/null +++ b/src/pages/Volunteers/interfaces/user.interface.ts @@ -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[]; +} \ No newline at end of file diff --git a/src/router.js b/src/router.js index b9137b0..662f8f8 100644 --- a/src/router.js +++ b/src/router.js @@ -3,12 +3,11 @@ import Layout from "./layout.jsx"; import HomePage from "./pages/Home/HomePage"; import LoginPage from "./pages/Login/LoginPage"; 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 EventsPage from "./pages/Events/EventsPage.jsx"; import AdminPage from "./pages/Admin/AdminPage.jsx"; import ValidationErrorPage from "./pages/waitValidationPage/ValidationErrorPage.jsx"; -import VolunteerProfilePage from "./pages/VolunteerProfile/VolunteerProfilePage.jsx"; import PageNotFound from "./pages/PageNotFound/PageNotFound.jsx"; import eventDetail from "./pages/eventDetail/eventDetail.jsx"; import legalNotices from "./pages/LegalNotices/LegalNotices.jsx"; @@ -47,10 +46,6 @@ const router = createBrowserRouter([ path: "/admin", Component:AdminPage, }, - { - path:"/profile", - Component:ProfilePage, - }, { path: "/events", Component: EventsPage, @@ -59,9 +54,13 @@ const router = createBrowserRouter([ path: "/volunteers", Component: VolunteersPage, }, + { + path:"/profile", + Component:ProfilePage, + }, { path: "/profile/:id", - Component: VolunteerProfilePage, + Component: ProfilePage, }, { path: "/events/:id", diff --git a/src/stories/Loading.stories.jsx b/src/stories/Loading.stories.jsx new file mode 100644 index 0000000..6986aa8 --- /dev/null +++ b/src/stories/Loading.stories.jsx @@ -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 = {}; \ No newline at end of file diff --git a/src/utils/echo/initEcho.js b/src/utils/echo/initEcho.js index 9d27b37..4e59d4b 100644 --- a/src/utils/echo/initEcho.js +++ b/src/utils/echo/initEcho.js @@ -14,8 +14,31 @@ export default function initEcho() { encrypted: import.meta.env.VITE_ENCRYPTED === 'true', cluster: import.meta.env.VITE_CLUSTER, 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; return echoInstance; } diff --git a/src/utils/echo/listeners/userCreatedListener.js b/src/utils/echo/listeners/userCreatedListener.js index e2bb017..a4a36c4 100644 --- a/src/utils/echo/listeners/userCreatedListener.js +++ b/src/utils/echo/listeners/userCreatedListener.js @@ -1,4 +1,4 @@ -export const userCreatedListener = (echo, userData, setNotifications, setunreadNotification) => { +export const userCreatedListener = (echo, userData, setNotifications, setunreadNotification, updatePendingMembers) => { if (userData.isAdmin) { const channelName = "users.registration"; echo.private(channelName) @@ -11,6 +11,7 @@ export const userCreatedListener = (echo, userData, setNotifications, setunreadN }; setNotifications(prev => [newNotification, ...prev]); setunreadNotification(true); + updatePendingMembers(); }); return channelName; }