diff --git a/src/components/Background/Background.jsx b/src/components/Background/Background.tsx similarity index 56% rename from src/components/Background/Background.jsx rename to src/components/Background/Background.tsx index 88cd57d..a578004 100644 --- a/src/components/Background/Background.jsx +++ b/src/components/Background/Background.tsx @@ -1,13 +1,18 @@ import styles from "./Background.module.css"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState, ReactNode } from "react"; -const Background = ({ children, className }) => { +interface BackgroundProps { + children?: ReactNode; + className?: string; +} - const circlesRef = useRef([]); +const Background = ({ children, className }: BackgroundProps) => { + + const circlesRef = useRef<(HTMLDivElement | null)[]>([]); const [theme, setTheme] = useState("light"); - const getRandomTranslation = () => { - const randomX = Math.floor(Math.random() * 100) - 30; + const getRandomTranslation = (): { x: number; y: number } => { + const randomX = Math.floor(Math.random() * 100) - 30; const randomY = Math.floor(Math.random() * 100) - 30; return { x: randomX, y: randomY }; }; @@ -27,7 +32,7 @@ const Background = ({ children, className }) => { setTheme(localStorage.getItem("theme") || "light"); }; - updateTheme(); // Initial load + updateTheme(); window.addEventListener("theme-changed", updateTheme); @@ -40,29 +45,29 @@ const Background = ({ children, className }) => { return (
(circlesRef.current[0] = el)} + ref={(el) => { circlesRef.current[0] = el; }} className={`${styles["circle"]} ${styles["circle-1"]}`} - style={{ "--random-x": "0px", "--random-y": "0px" }} + style={{ "--random-x": "0px", "--random-y": "0px" } as React.CSSProperties} >
(circlesRef.current[1] = el)} + ref={(el) => { circlesRef.current[1] = el; }} className={`${styles["circle"]} ${styles["circle-2"]}`} - style={{ "--random-x": "0px", "--random-y": "0px" }} + style={{ "--random-x": "0px", "--random-y": "0px" } as React.CSSProperties} >
(circlesRef.current[2] = el)} + ref={(el) => { circlesRef.current[2] = el; }} className={`${styles["circle"]} ${styles["circle-3"]}`} - style={{ "--random-x": "0px", "--random-y": "0px" }} + style={{ "--random-x": "0px", "--random-y": "0px" } as React.CSSProperties} >
(circlesRef.current[3] = el)} + ref={(el) => { circlesRef.current[3] = el; }} className={`${styles["circle"]} ${styles["circle-4"]}`} - style={{ "--random-x": "0px", "--random-y": "0px" }} + style={{ "--random-x": "0px", "--random-y": "0px" } as React.CSSProperties} >
(circlesRef.current[4] = el)} + ref={(el) => { circlesRef.current[4] = el; }} className={`${styles["circle"]} ${styles["circle-5"]}`} - style={{ "--random-x": "0px", "--random-y": "0px" }} + style={{ "--random-x": "0px", "--random-y": "0px" } as React.CSSProperties} >
{children}
diff --git a/src/components/ExportCalendarBtn/ExportBtn.tsx b/src/components/ExportCalendarBtn/ExportBtn.tsx new file mode 100644 index 0000000..d563a66 --- /dev/null +++ b/src/components/ExportCalendarBtn/ExportBtn.tsx @@ -0,0 +1,21 @@ +import Button from "../ui/Button/Button"; +import { useContext } from "react"; +import { AuthContext, AuthContextType } from "../../contexts/Auth/AuthContext"; +import { APP_CONFIG } from "../../config/appConfig"; + +export default function ExportCalendarBtn() { + + const { user } = useContext(AuthContext) as AuthContextType; + const calendarUrl = `${import.meta.env.VITE_API_URL}/api/export/ics/${user?.id}`; + + return +} \ No newline at end of file diff --git a/src/components/Footer/Footer.jsx b/src/components/Footer/Footer.tsx similarity index 80% rename from src/components/Footer/Footer.jsx rename to src/components/Footer/Footer.tsx index 881943e..a25704a 100644 --- a/src/components/Footer/Footer.jsx +++ b/src/components/Footer/Footer.tsx @@ -1,13 +1,13 @@ import styles from "./Footer.module.css" -import {Link} from "react-router"; -import {useState} from "react"; +import { Link } from "react-router"; +import { ReactNode, useState } from "react"; import Modal from "../ui/Modal/Modal"; import Button from "../ui/Button/Button"; import scrollToTop from "../../utils/scrollToTop.js"; -export default function Footer(){ +export default function Footer(): ReactNode{ const currentYear = new Date().getFullYear(); - const [isCopyrightModal, setIsCopyrightModal] = useState(false); + const [isCopyrightModal, setIsCopyrightModal] = useState(false); return <> @@ -30,7 +30,7 @@ export default function Footer(){ Fiche RGPD - + Mentions légales @@ -48,11 +48,11 @@ export default function Footer(){ setIsCopyrightModal(false)}>
-

Quentin T'jampens

-

Antoine Ordonneau

-

Malo Leblond

+

Quentin T'JAMPENS

+

Antoine ORDONNEAU

+

Malo LEBLOND

- Giovanni Josserand ( + Giovanni JOSSERAND ( linktree )

@@ -60,4 +60,4 @@ export default function Footer(){
-} \ No newline at end of file +} diff --git a/src/components/Header/Header.jsx b/src/components/Header/Header.tsx similarity index 82% rename from src/components/Header/Header.jsx rename to src/components/Header/Header.tsx index bd4b821..41cba77 100644 --- a/src/components/Header/Header.jsx +++ b/src/components/Header/Header.tsx @@ -1,53 +1,49 @@ import { Link, NavLink } from "react-router"; -import {useContext, useEffect, useRef, useState} from "react"; +import { useContext, useEffect, useRef, useState } from "react"; import styles from "./Header.module.css" import getUserNotifications from "../../utils/notifications/getUserNotifications.js"; import deleteNotificationUser from "../../utils/notifications/deleteNotificationUser.js"; import initEcho from "../../utils/echo/initEcho.js" import readNotifications from "../../utils/notifications/readNotifications.js"; -import { userCreatedListener } from "../../utils/echo/listeners/userCreatedListener"; +import { adminNotificationsListener } from "../../utils/echo/listeners/adminNotificationsListener"; import { userNotificationsListener } from "../../utils/echo/listeners/userNotificationsListener.js"; -import NotificationCard from "../NotificationCard/NotificationCard.jsx"; +import NotificationCard from "../NotificationCard/NotificationCard"; 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"; +import Notification from "../../interfaces/notification.interface"; function Header() { - const { update, user } = useContext(AuthContext); - const { updateEvent } = useContext(EventContext); - const [notificationMenu, setnotificationMenu] = useState(false); - const [unreadNotification, setunreadNotification] = useState(false); - const [mobileMenu, setMobileMenu] = useState(false); - const [notifications, setNotifications] = useState([]); - const notificationRef = useRef(null); - const { updatePendingMembers } = useContext(PendingMembersContext); + const { update, user } = useContext(AuthContext)!; + const { updateEvent } = useContext(EventContext)!; + const { updatePendingMembers } = useContext(PendingMembersContext) as unknown as { updatePendingMembers: () => void }; + const [notificationMenu, setnotificationMenu] = useState(false); + const [unreadNotification, setunreadNotification] = useState(false); + const [mobileMenu, setMobileMenu] = useState(false); + const [notifications, setNotifications] = useState([]); + const notificationRef = useRef(null); useEffect(() => { + if (!user) return; updateEvent(); - let echoInstance = null; - let channelsToLeave = []; + let echoInstance: ReturnType | null = null; async function initializeHeader() { try { + const echo = initEcho(); + echoInstance = echo; + adminNotificationsListener(echo, user, setNotifications, setunreadNotification, updatePendingMembers); + userNotificationsListener(echo, user, setNotifications, setunreadNotification); + const notifData = await getUserNotifications(); - const data = notifData || []; + const data: Notification[] = notifData || []; setNotifications(data); const hasUnread = data.some(n => n.pivot && n.pivot.unread === 1); setunreadNotification(hasUnread); - - const echo = initEcho(); - echoInstance = echo; - - const activeChannels = [ - userCreatedListener(echo, user, setNotifications, setunreadNotification, updatePendingMembers), - userNotificationsListener(echo, user, setNotifications, setunreadNotification), - ]; - channelsToLeave = activeChannels.filter(name => name !== null); - } catch (err) { console.error("Erreur initialisation Header:", err); } @@ -55,22 +51,21 @@ function Header() { initializeHeader(); - const handleClickOutside = (event) => { + const handleClickOutside = (event: MouseEvent) => { if ( notificationRef.current && - !notificationRef.current.contains(event.target) && - !event.target.closest(`.${styles.bellBtn}`) + !notificationRef.current.contains(event.target as Node) && + !(event.target as Element).closest(`.${styles.bellBtn}`) ) { setnotificationMenu(false); } }; - document.addEventListener("mousedown", handleClickOutside); return () => { document.removeEventListener("mousedown", handleClickOutside); if (echoInstance) { - channelsToLeave.forEach(chan => echoInstance.leave(chan)); + echoInstance.disconnect(); } }; }, []); @@ -93,7 +88,7 @@ function Header() { } - async function deleteNotification(notificationId) { + async function deleteNotification(notificationId: number) { try { const result = await deleteNotificationUser(notificationId); if (result) { @@ -194,4 +189,4 @@ function Header() { ); } -export default Header; \ No newline at end of file +export default Header; diff --git a/src/components/NotificationCard/NotificationCard.jsx b/src/components/NotificationCard/NotificationCard.tsx similarity index 78% rename from src/components/NotificationCard/NotificationCard.jsx rename to src/components/NotificationCard/NotificationCard.tsx index 7eb6464..e2086aa 100644 --- a/src/components/NotificationCard/NotificationCard.jsx +++ b/src/components/NotificationCard/NotificationCard.tsx @@ -1,10 +1,14 @@ import formatDate from "../../utils/date/formatDate.js"; import formatTime from "../../utils/date/formatTime.js"; - import styles from "./NotificationCard.module.css" +import Notification from "../../interfaces/notification.interface"; +interface NotificationCardProps { + notifications: Notification[]; + deleteNotification: (id: number) => void; +} -function NotificationCard({notifications, deleteNotification}) { +function NotificationCard({ notifications, deleteNotification }: NotificationCardProps) { return ( notifications.map((notification, index) => (
@@ -31,4 +35,4 @@ function NotificationCard({notifications, deleteNotification}) { ); } -export default NotificationCard; \ No newline at end of file +export default NotificationCard; diff --git a/src/components/ToolBar/ToolBar.jsx b/src/components/ToolBar/ToolBar.tsx similarity index 58% rename from src/components/ToolBar/ToolBar.jsx rename to src/components/ToolBar/ToolBar.tsx index 548d567..2511d94 100644 --- a/src/components/ToolBar/ToolBar.jsx +++ b/src/components/ToolBar/ToolBar.tsx @@ -1,9 +1,20 @@ import styles from "./ToolBar.module.css" -import CreateEventBtn from "./tools/CreateEventBtn/CreateEventBtn.jsx"; +import CreateEventBtn from "./tools/CreateEventBtn/CreateEventBtn"; import SearchBtn from "./tools/SearchBtn"; import FilterBtn from "./tools/FilterBtn"; -function ToolBar({setFilters, filters, showCreate = true}) { +interface Filters { + alphabetical: "asc" | "desc"; + yearOrder: "asc" | "desc"; +} + +interface ToolBarProps { + setFilters: React.Dispatch>; + filters: Filters; + showCreate?: boolean; +} + +function ToolBar({ setFilters, filters, showCreate = true }: ToolBarProps) { return (
@@ -16,4 +27,4 @@ function ToolBar({setFilters, filters, showCreate = true}) { ); } -export default ToolBar; \ No newline at end of file +export default ToolBar; diff --git a/src/components/ToolBar/tools/CreateEventBtn/CreateEventBtn.jsx b/src/components/ToolBar/tools/CreateEventBtn/CreateEventBtn.tsx similarity index 83% rename from src/components/ToolBar/tools/CreateEventBtn/CreateEventBtn.jsx rename to src/components/ToolBar/tools/CreateEventBtn/CreateEventBtn.tsx index aa31499..c7cb2eb 100644 --- a/src/components/ToolBar/tools/CreateEventBtn/CreateEventBtn.jsx +++ b/src/components/ToolBar/tools/CreateEventBtn/CreateEventBtn.tsx @@ -4,19 +4,19 @@ import Modal from "../../../ui/Modal/Modal"; import Button from "../../../ui/Button/Button"; import TextInput from "../../../ui/Input/Input"; import { EventContext } from "../../../../contexts/Events/EventContext.js"; -import {AuthContext} from "../../../../contexts/Auth/AuthContext.ts"; +import { AuthContext } from "../../../../contexts/Auth/AuthContext"; export default function CreateEventBtn() { - const { addEvent } = useContext(EventContext); - const { user } = useContext(AuthContext); + const { addEvent } = useContext(EventContext)!; + const { user } = useContext(AuthContext)!; - const [isOpen, setIsOpen] = useState(false); - const [name, setName] = useState(""); - const [description, setDescription] = useState(""); - const [start, setStart] = useState(""); - const [end, setEnd] = useState(""); + const [isOpen, setIsOpen] = useState(false); + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [start, setStart] = useState(""); + const [end, setEnd] = useState(""); const handleCreateEvent = async () => { @@ -62,4 +62,4 @@ export default function CreateEventBtn() { -} \ No newline at end of file +} diff --git a/src/contexts/Auth/AuthContext.ts b/src/contexts/Auth/AuthContext.ts index b43a236..718bd7f 100644 --- a/src/contexts/Auth/AuthContext.ts +++ b/src/contexts/Auth/AuthContext.ts @@ -1,30 +1,12 @@ import { createContext } from "react"; - - -export interface Task { - id: number; - title?: string; - completed?: boolean; - [key: string]: unknown; -} - -export interface User { - id: number - name: string; - lastname: string; - role: string; - email: string; - phone: string | null; - created_at: string; - profile_photo_path?: string | null; - tasks: Task[]; -} +import User from "./../../interfaces/user.interface" export interface AuthContextType { user: User | null; loading: boolean; update: () => void; login: (email: string, password: string) => Promise<{ status: number }>; + logout: () => Promise; } diff --git a/src/contexts/Events/EventContext.ts b/src/contexts/Events/EventContext.ts index 166a883..14136d6 100644 --- a/src/contexts/Events/EventContext.ts +++ b/src/contexts/Events/EventContext.ts @@ -16,6 +16,8 @@ export type EventGroup = { export type EventContextType = { events: EventGroup[]; + updateEvent: () => void; + addEvent: (name: string, description: string, start: string, end: string) => Promise; }; export const EventContext = createContext(null); \ No newline at end of file diff --git a/src/interfaces/auth.interfaces.ts b/src/interfaces/auth.interfaces.ts new file mode 100644 index 0000000..765a46a --- /dev/null +++ b/src/interfaces/auth.interfaces.ts @@ -0,0 +1,6 @@ + +import type User from "./user.interface"; +export interface AuthContextType { + user: User | null; + update: () => void; +} diff --git a/src/interfaces/notification.interface.ts b/src/interfaces/notification.interface.ts new file mode 100644 index 0000000..b666fa7 --- /dev/null +++ b/src/interfaces/notification.interface.ts @@ -0,0 +1,6 @@ +export default interface Notification { + id: number; + content: string; + created_at: string | number; + pivot: { unread: number }; +} diff --git a/src/interfaces/taskType.interface.ts b/src/interfaces/task.interface.ts similarity index 73% rename from src/interfaces/taskType.interface.ts rename to src/interfaces/task.interface.ts index d116c3d..181d539 100644 --- a/src/interfaces/taskType.interface.ts +++ b/src/interfaces/task.interface.ts @@ -13,4 +13,10 @@ export default interface TaskType { user_id: number; task_id: number; }; +} +export default interface Task { + id: number; + title?: string; + completed?: boolean; + [key: string]: unknown; } \ No newline at end of file diff --git a/src/interfaces/user.interface.ts b/src/interfaces/user.interface.ts index de1f401..150286f 100644 --- a/src/interfaces/user.interface.ts +++ b/src/interfaces/user.interface.ts @@ -1,4 +1,4 @@ -import TaskType from "./taskType.interface"; +import TaskType from "./task.interface"; export default interface User { id: number; @@ -9,6 +9,9 @@ export default interface User { phone: string | null; created_at: string; isAdmin: boolean; + validate: number; tasks: TaskType[]; - profile_photo_path?: string | null; + profile_photo_path?: string | null, + email_notifications: number, + web_notifications: number; } \ No newline at end of file diff --git a/src/pages/Admin/components/RegisterMember/RegisterMember.tsx b/src/pages/Admin/components/RegisterMember/RegisterMember.tsx index 9905e68..3c33740 100644 --- a/src/pages/Admin/components/RegisterMember/RegisterMember.tsx +++ b/src/pages/Admin/components/RegisterMember/RegisterMember.tsx @@ -48,12 +48,18 @@ const RegisterMember: React.FC = () => { setOpen(true); } + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + handleCreate(); + } + }; + return

Enregister un bénévole

-
+
setName(e.target.value)} /> setLastName(e.target.value)} /> setEmail(e.target.value)} /> diff --git a/src/pages/Home/HomePage.jsx b/src/pages/Home/HomePage.jsx deleted file mode 100644 index 1e2e3d9..0000000 --- a/src/pages/Home/HomePage.jsx +++ /dev/null @@ -1,15 +0,0 @@ -import styles from "./HomePage.module.css"; -import Calendar from "./components/Calendar/Calendar.jsx"; -import EventList from "./components/EventList/EventList"; - -function HomePage() { - - return ( -
- - -
- ); -} - -export default HomePage; diff --git a/src/pages/Home/HomePage.tsx b/src/pages/Home/HomePage.tsx new file mode 100644 index 0000000..a34d095 --- /dev/null +++ b/src/pages/Home/HomePage.tsx @@ -0,0 +1,12 @@ +import styles from "./HomePage.module.css"; +import Calendar from "./components/Calendar/Calendar"; +import EventList from "./components/EventList/EventList"; +import { ReactNode } from "react"; + +export default function HomePage(): ReactNode { + + return
+ + +
+} diff --git a/src/pages/Home/components/Calendar/Calendar.jsx b/src/pages/Home/components/Calendar/Calendar.jsx index ba80de5..66f7a7b 100644 --- a/src/pages/Home/components/Calendar/Calendar.jsx +++ b/src/pages/Home/components/Calendar/Calendar.jsx @@ -1,9 +1,8 @@ import {useState, useMemo, useEffect, useContext} from "react"; import styles from "./Calendar.module.css"; import {formatDateLetterJS} from "../../../../utils/date/formatDateLetter.js"; -import Button from "../../../../components/ui/Button/Button"; import {AuthContext} from "../../../../contexts/Auth/AuthContext.ts"; -import ExportBtn from "./ExportBtn.tsx"; +import ExportBtn from "../../../../components/ExportCalendarBtn/ExportBtn.tsx"; function Calendar() { diff --git a/src/pages/Home/components/Calendar/ExportBtn.tsx b/src/pages/Home/components/Calendar/ExportBtn.tsx deleted file mode 100644 index c5192aa..0000000 --- a/src/pages/Home/components/Calendar/ExportBtn.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import Button from "../../../../components/ui/Button/Button"; -import styles from "../../HomePage.module.css" -import { useContext } from "react"; -import { AuthContext, AuthContextType } from "../../../../contexts/Auth/AuthContext"; -import { APP_CONFIG } from "../../../../config/appConfig"; - -export default function ExportBtn() { - - const { user } = useContext(AuthContext) as AuthContextType; - const calendarUrl = `${import.meta.env.VITE_API_URL}/api/export/ics/${user?.id}`; - - return -} \ No newline at end of file diff --git a/src/pages/LegalNotices/LegalNoticesPage.jsx b/src/pages/LegalNotices/LegalNoticesPage.tsx similarity index 99% rename from src/pages/LegalNotices/LegalNoticesPage.jsx rename to src/pages/LegalNotices/LegalNoticesPage.tsx index 4adf1a0..38aa86d 100644 --- a/src/pages/LegalNotices/LegalNoticesPage.jsx +++ b/src/pages/LegalNotices/LegalNoticesPage.tsx @@ -37,4 +37,4 @@ export default function legalNoticesPage() {
); -} \ No newline at end of file +} diff --git a/src/pages/PageNotFound/PageNotFoundPage.jsx b/src/pages/PageNotFound/PageNotFoundPage.tsx similarity index 61% rename from src/pages/PageNotFound/PageNotFoundPage.jsx rename to src/pages/PageNotFound/PageNotFoundPage.tsx index 5957cb5..afee5d0 100644 --- a/src/pages/PageNotFound/PageNotFoundPage.jsx +++ b/src/pages/PageNotFound/PageNotFoundPage.tsx @@ -1,4 +1,4 @@ -import Background from "../../components/Background/Background.jsx"; +import Background from "../../components/Background/Background"; import styles from "./PageNotFoundPage.module.css" import { useNavigate } from "react-router"; import { APP_CONFIG } from "../../config/appConfig.js"; @@ -12,14 +12,14 @@ export default function PageNotFoundPage() {

Page non trouvée

- Désolé, la page que vous recherchez n’existe pas ou n’est plus disponible.
+ Désolé, la page que vous recherchez n'existe pas ou n'est plus disponible.
Vous pouvez :
- - Vérifier l’URL pour une éventuelle erreur de saisie.
- - Revenir à l’accueil navigate("/")} className={styles.redirectSpan}>ici.
+ - Vérifier l'URL pour une éventuelle erreur de saisie.
+ - Revenir à l'accueil navigate("/")} className={styles.redirectSpan}>ici.

Contact : {APP_CONFIG.contactEmail}

-} \ No newline at end of file +} diff --git a/src/pages/Profile/ProfilePage.tsx b/src/pages/Profile/ProfilePage.tsx index ef09a69..5844adb 100644 --- a/src/pages/Profile/ProfilePage.tsx +++ b/src/pages/Profile/ProfilePage.tsx @@ -7,46 +7,15 @@ 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"; -import ManageMember from "./components/ManageMember/ManageMember.js"; +import getUserById from "../../utils/users/getUserById.js"; +import ManageMember from "./components/ManageMember/ManageMember"; import uploadProfilePhoto from "../../utils/users/uploadProfilePhoto"; -import deleteProfilePhoto from "../../utils/users/deleteProfilePhoto.js"; - -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; - }; -} +import deleteProfilePhoto from "../../utils/users/deleteProfilePhoto"; +import type { AuthContextType } from "../../interfaces/AuthInterfaces"; +import type TaskType from "../../interfaces/task.interface"; +import type User from "../../interfaces/user.interface"; -interface User { - 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; -} function ProfilePage() { const navigate = useNavigate(); @@ -60,7 +29,6 @@ function ProfilePage() { useEffect(() => { (async () => { - console.log(JSON.stringify(import.meta.env.VITE_API_URL)); if (!isOwnProfile && id) { const res = await getUserById(Number(id)); if (res.status === 404) { diff --git a/src/pages/Profile/components/SettingsModal/SettingsModal.tsx b/src/pages/Profile/components/SettingsModal/SettingsModal.tsx index 7d39830..c354310 100644 --- a/src/pages/Profile/components/SettingsModal/SettingsModal.tsx +++ b/src/pages/Profile/components/SettingsModal/SettingsModal.tsx @@ -1,4 +1,4 @@ -import {useContext, useState, ChangeEvent} from "react"; +import {useContext, useState, ChangeEvent, useEffect} from "react"; import Modal from "../../../../components/ui/Modal/Modal"; import Button from "../../../../components/ui/Button/Button"; import styles from "./SettingsModal.module.css"; @@ -7,6 +7,11 @@ 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"; +import ExportCalendarBtn from "../../../../components/ExportCalendarBtn/ExportBtn"; +import { useNavigate } from "react-router-dom"; +import toggleEmailNotifications from "../../../../utils/users/toggleEmailNotifications"; +import toggleWebNotifications from "../../../../utils/users/toggleWebNotifications"; + interface TaskType { id: number; @@ -25,7 +30,9 @@ interface User { phone: string | null; created_at: string; isAdmin: boolean; - tasks: TaskType[]; + tasks: TaskType[], + email_notifications: number, + web_notifications: number; } interface AuthContextType { @@ -35,6 +42,7 @@ interface AuthContextType { } export default function SettingsModal() { + const navigate = useNavigate(); const {user, logout, update} = useContext(AuthContext) as unknown as AuthContextType; const [open, setOpen] = useState(false); @@ -44,6 +52,9 @@ export default function SettingsModal() { const [lastname, setLastName] = useState(user?.lastname ?? ""); const [phone, setPhone] = useState(user?.phone ?? ""); + const [emailNotifications, setEmailNotifications] = useState(user?.email_notifications ?? 0); + const [webNotifications, setWebNotifications] = useState(user?.web_notifications ?? 0); + const handleLogout = (): void => { logout(); }; @@ -56,6 +67,8 @@ export default function SettingsModal() { const handleDelete = async (): Promise => { await deleteUser(); + logout(); + navigate('/'); update(); }; @@ -71,6 +84,26 @@ export default function SettingsModal() { setPhone(e.target.value); }; + const handleEmailToggle = async () => { + try { + await toggleEmailNotifications(user.id); + setEmailNotifications(prev => prev === 0 ? 1 : 0); + update(); + } catch (err) { + console.error(err); + } + }; + + const handleWebToggle = async () => { + try { + await toggleWebNotifications(user.id); + setWebNotifications(prev => prev === 0 ? 1 : 0); + update(); + } catch (err) { + console.error(err); + } + }; + return ( <> @@ -130,6 +163,34 @@ export default function SettingsModal() {
+
+

Notifications

+ +
+

Notifications par email

+ +
+ +
+

Notifications sur le site

+ +
+
+ + +
+

Divers

+ +
+

Sécurité du compte

diff --git a/src/pages/RGPD/RGPDPage.jsx b/src/pages/RGPD/RGPDPage.tsx similarity index 82% rename from src/pages/RGPD/RGPDPage.jsx rename to src/pages/RGPD/RGPDPage.tsx index 69d5dd5..e0a81bb 100644 --- a/src/pages/RGPD/RGPDPage.jsx +++ b/src/pages/RGPD/RGPDPage.tsx @@ -12,8 +12,8 @@ export default function RGPDPage() { Le présent site web est un outil de gestion des bénévoles permettant notamment :

    -
  • L’inscription et la gestion des profils bénévoles
  • -
  • L’organisation d’événements ou de missions
  • +
  • L'inscription et la gestion des profils bénévoles
  • +
  • L'organisation d'événements ou de missions
  • La communication avec les bénévoles
  • Le suivi des participations
@@ -33,15 +33,15 @@ export default function RGPDPage() {

2. Responsable du traitement

Le responsable du traitement des données personnelles est Monsieur FRÈRE Robin -
Il est responsable de la collecte et de l’utilisation des données personnelles conformément au Règlement Général sur la Protection des Données (RGPD – UE 2016/679). +
Il est responsable de la collecte et de l'utilisation des données personnelles conformément au Règlement Général sur la Protection des Données (RGPD – UE 2016/679).

3. Données personnelles collectées

-

Les données susceptibles d’être collectées sont notamment :

+

Les données susceptibles d'être collectées sont notamment :

    -
  • Les données d’identification : nom, prénom, adresse email et numéro de téléphone.
  • +
  • Les données d'identification : nom, prénom, adresse email et numéro de téléphone.
  • Les données techniques : adresse IP, données de connexion (logs) et type de navigateur et appareil.
@@ -63,8 +63,8 @@ export default function RGPDPage() {

5. Base légale du traitement

Les traitements reposent sur :

    -
  • Le consentement de l’utilisateur
  • -
  • L’intérêt légitime de l’organisme (gestion des bénévoles)
  • +
  • Le consentement de l'utilisateur
  • +
  • L'intérêt légitime de l'organisme (gestion des bénévoles)
  • Une obligation légale, le cas échéant
@@ -73,7 +73,7 @@ export default function RGPDPage() {

6. Durée de conservation

Les données sont conservées :

    -
  • Pendant la durée de l’engagement bénévole
  • +
  • Pendant la durée de l'engagement bénévole
  • Puis archivées ou supprimées au plus tard 3 ans après la dernière activité ou contact

Les logs techniques sont conservés pour une durée maximale de 2 mois.

@@ -83,10 +83,10 @@ export default function RGPDPage() {

7. Destinataires des données

Les données sont accessibles uniquement :

    -
  • Aux personnes habilitées de l’organisme
  • +
  • Aux personnes habilitées de l'organisme
  • Aux prestataires techniques (hébergement, maintenance), strictement nécessaires
-

Aucune donnée n’est vendue ou cédée à des tiers.

+

Aucune donnée n'est vendue ou cédée à des tiers.

@@ -96,7 +96,7 @@ export default function RGPDPage() {
  • Siège social : 2 rue Kellermann - 59100 Roubaix - France
  • Site web : www.ovh.com
  • -

    L’hébergeur garantit un niveau de sécurité conforme au RGPD.

    +

    L'hébergeur garantit un niveau de sécurité conforme au RGPD.

    @@ -113,11 +113,11 @@ export default function RGPDPage() {

    10. Droits des utilisateurs

    Conformément au RGPD, vous disposez des droits suivants :

      -
    • Droit d’accès à vos données
    • +
    • Droit d'accès à vos données
    • Droit de rectification
    • -
    • Droit à l’effacement (droit à l’oubli)
    • +
    • Droit à l'effacement (droit à l'oubli)
    • Droit à la limitation du traitement
    • -
    • Droit d’opposition
    • +
    • Droit d'opposition
    • Droit à la portabilité des données
    @@ -126,7 +126,7 @@ export default function RGPDPage() {

    11. Réclamation

    Si vous estimez que vos droits ne sont pas respectés, vous pouvez introduire une réclamation auprès de la : -
    CNIL – Commission Nationale de l’Informatique et des Libertés +
    CNIL – Commission Nationale de l'Informatique et des Libertés
    Site : https://www.cnil.fr

    @@ -140,4 +140,4 @@ export default function RGPDPage() {
    ); -} \ No newline at end of file +} diff --git a/src/pages/WaitValidation/WaitValidationPage.jsx b/src/pages/WaitValidation/WaitValidationPage.tsx similarity index 92% rename from src/pages/WaitValidation/WaitValidationPage.jsx rename to src/pages/WaitValidation/WaitValidationPage.tsx index 2972391..1480cb7 100644 --- a/src/pages/WaitValidation/WaitValidationPage.jsx +++ b/src/pages/WaitValidation/WaitValidationPage.tsx @@ -1,5 +1,5 @@ import styles from "./WaitValidationPage.module.css" -import Background from "../../components/Background/Background.jsx"; +import Background from "../../components/Background/Background"; import Button from "../../components/ui/Button/Button"; import { useContext } from "react"; import { AuthContext } from "../../contexts/Auth/AuthContext.js"; @@ -7,7 +7,7 @@ import { Navigate, useNavigate, Link } from "react-router"; export default function WaitValidationPage() { - const { logout, user } = useContext(AuthContext); + const { logout, user } = useContext(AuthContext)!; const navigate = useNavigate(); const handleLogout = async () => { @@ -38,4 +38,4 @@ export default function WaitValidationPage() {
    -} \ No newline at end of file +} diff --git a/src/router.js b/src/router.js index 11c3bb8..e17bf44 100644 --- a/src/router.js +++ b/src/router.js @@ -1,6 +1,6 @@ import { createBrowserRouter } from "react-router"; import Layout from "./layout.jsx"; -import HomePage from "./pages/Home/HomePage"; +import HomePage from "./pages/Home/HomePage.tsx"; import LoginPage from "./pages/Login/LoginPage"; import RegisterPage from "./pages/Register/RegisterPage.jsx"; import VolunteersPage from "./pages/Volunteers/VolunteersPage"; @@ -10,7 +10,7 @@ import AdminPage from "./pages/Admin/AdminPage.jsx"; import WaitValidationPage from "./pages/WaitValidation/WaitValidationPage.jsx"; import PageNotFoundPage from "./pages/PageNotFound/PageNotFoundPage.jsx"; import eventDetail from "./pages/EventDetail/EventDetailPage.tsx"; -import legalNotices from "./pages/LegalNotices/LegalNoticesPage.jsx"; +import legalNotices from "./pages/LegalNotices/LegalNoticesPage.tsx"; import RGPDPage from "./pages/RGPD/RGPDPage.jsx"; const router = createBrowserRouter([ diff --git a/src/utils/date/formatDate.ts b/src/utils/date/formatDate.ts index 5a14daa..8f4bf1b 100644 --- a/src/utils/date/formatDate.ts +++ b/src/utils/date/formatDate.ts @@ -1,4 +1,6 @@ -export default function formatDate(d: string | Date): string { +export default function formatDate(d: string | Date | number | null | undefined): string { + if (!d) return "Date inconnue"; + const date = d instanceof Date ? d : new Date(d); const jj = String(date.getUTCDate()).padStart(2, "0"); diff --git a/src/utils/date/formatTime.ts b/src/utils/date/formatTime.ts index 93311b9..561d03a 100644 --- a/src/utils/date/formatTime.ts +++ b/src/utils/date/formatTime.ts @@ -1,5 +1,5 @@ -export default function formatTime(d: string | Date | null | undefined): string { +export default function formatTime(d: string | Date | number | null | undefined): string { if (!d) return "Heure inconnue"; const date = d instanceof Date ? d : new Date(d); diff --git a/src/utils/echo/listeners/adminNotificationsListener.js b/src/utils/echo/listeners/adminNotificationsListener.js new file mode 100644 index 0000000..5210122 --- /dev/null +++ b/src/utils/echo/listeners/adminNotificationsListener.js @@ -0,0 +1,24 @@ +export const adminNotificationsListener = (echo, userData, setNotifications, setUnreadNotification, updatePendingMembers) => { + if (userData.isAdmin) { + const channelName = "users.admin"; + + const handleNotification = (id, content) => { + const newNotification = { + id, + content, + created_at: new Date().toISOString(), + pivot: { unread: 1 } + }; + setNotifications(prev => [newNotification, ...prev]); + setUnreadNotification(true); + }; + + echo.private(channelName) + .listen(".users.registration", (event) => { + handleNotification(event.notificationId, `Nouvelle demande d'inscription : ${event.user.name} ${event.user.lastname}`); + updatePendingMembers(); + }); + return channelName; + } + return null; +}; \ No newline at end of file diff --git a/src/utils/echo/listeners/userCreatedListener.js b/src/utils/echo/listeners/userCreatedListener.js deleted file mode 100644 index a4a36c4..0000000 --- a/src/utils/echo/listeners/userCreatedListener.js +++ /dev/null @@ -1,19 +0,0 @@ -export const userCreatedListener = (echo, userData, setNotifications, setunreadNotification, updatePendingMembers) => { - if (userData.isAdmin) { - const channelName = "users.registration"; - echo.private(channelName) - .listen(".users.registration", (event) => { - const newNotification = { - id: Date.now(), - content: `Nouvelle demande d'inscription : ${event.user.name} ${event.user.lastname}`, - created_at: Date.now(), - pivot: { unread: 1 } - }; - setNotifications(prev => [newNotification, ...prev]); - setunreadNotification(true); - updatePendingMembers(); - }); - return channelName; - } - return null; -}; \ No newline at end of file diff --git a/src/utils/echo/listeners/userNotificationsListener.js b/src/utils/echo/listeners/userNotificationsListener.js index 7d94483..f9ec96b 100644 --- a/src/utils/echo/listeners/userNotificationsListener.js +++ b/src/utils/echo/listeners/userNotificationsListener.js @@ -1,46 +1,37 @@ -export const userNotificationsListener = (echo, userData, setNotifications, setunreadNotification) => { +export const userNotificationsListener = (echo, userData, setNotifications, setUnreadNotification) => { const channelName = `user.${userData.id}`; + const handleNotification = (id, content) => { + + const newNotification = { + id, + content, + created_at: new Date().toISOString(), + pivot: { unread: 1 } + }; + + setNotifications(prev => [newNotification, ...prev]); + setUnreadNotification(true); + }; + return echo.private(channelName) .listen('.event.participation.cancelled', (event) => { - const newNotification = { - id: Date.now(), - content: `L'événement ${event.event.name} a été supprimé ! Vous n'y participez donc plus.`, - created_at: Date.now(), - pivot: { unread: 1 } - }; - setNotifications(prev => [newNotification, ...prev]); - setunreadNotification(true); + handleNotification(event.notificationId,`L'événement ${event.event.name} a été supprimé. Vous n'y participez donc plus.`); }) .listen(`.task.participation.cancelled`, (event) => { - const newNotification = { - id: Date.now(), - content: `La tâche ${event.task.name} de l'événement ${event.event.name} a été supprimé ! Vous n'y participez donc plus.`, - created_at: Date.now(), - pivot: { unread: 1 } - }; - setNotifications(prev => [newNotification, ...prev]); - setunreadNotification(true); + handleNotification(event.notificationId,`La tâche ${event.task.name} de l'événement ${event.event.name} a été supprimée. Vous n'y participez donc plus.`); }) .listen('.volunteer.assigned.to.task', (event) => { - const newNotification = { - id: Date.now(), - content: `Vous avez été assigné à la tâche ${event.task.name} de l'événement ${event.event.name}`, - created_at: Date.now(), - pivot: { unread: 1 } - }; - setNotifications(prev => [newNotification, ...prev]); - setunreadNotification(true); + handleNotification(event.notificationId,`Vous avez été assigné à la tâche ${event.task.name} de l'événement ${event.event.name}.`); }) .listen('.volunteer.unassigned.to.task', (event) => { - const newNotification = { - id: Date.now(), - content: `Vous avez été désassigné de la tâche ${event.task.name} de l'événement ${event.event.name}`, - created_at: Date.now(), - pivot: { unread: 1 } - }; - setNotifications(prev => [newNotification, ...prev]); - setunreadNotification(true); + handleNotification(event.notificationId,`Vous avez été désassigné de la tâche ${event.task.name} de l'événement ${event.event.name}.`); + }) + .listen('.volunteer.role.updated', (event) => { + handleNotification(event.notificationId, `Votre rôle a changé pour : ${event.role} `); + }) + .listen('.task.date.updated', (event) => { + handleNotification(event.notificationId,`La date de la tâche ${event.task.name} de l'événement ${event.event.name} à été mis à jour. Cette tâche aura maintenant lieu du ${event.start} au ${event.end}.`); }); }; \ No newline at end of file diff --git a/src/utils/users/toggleEmailNotifications.ts b/src/utils/users/toggleEmailNotifications.ts new file mode 100644 index 0000000..1fa8744 --- /dev/null +++ b/src/utils/users/toggleEmailNotifications.ts @@ -0,0 +1,13 @@ +import fetchWrapper from "../fetchWrapper"; +import getXSRFToken from "../getXSRF"; + +export default async function toggleEmailNotifications(id: number) { + const csrfToken = await getXSRFToken(); + + return fetchWrapper( + `/api/users/${id}/email-notifications`, + {}, + "PATCH", + { "X-XSRF-TOKEN": csrfToken } + ); +} \ No newline at end of file diff --git a/src/utils/users/toggleWebNotifications.ts b/src/utils/users/toggleWebNotifications.ts new file mode 100644 index 0000000..a068c0b --- /dev/null +++ b/src/utils/users/toggleWebNotifications.ts @@ -0,0 +1,13 @@ +import fetchWrapper from "../fetchWrapper"; +import getXSRFToken from "../getXSRF"; + +export default async function toggleWebNotifications(id: number) { + const csrfToken = await getXSRFToken(); + + return fetchWrapper( + `/api/users/${id}/web-notifications`, + {}, + "PATCH", + { "X-XSRF-TOKEN": csrfToken } + ); +} \ No newline at end of file