diff --git a/.env.exemple b/.env.exemple index 3452b5c..5136f91 100644 --- a/.env.exemple +++ b/.env.exemple @@ -1 +1,11 @@ -VITE_API_URL= \ No newline at end of file +VITE_API_URL= + +BROADCASTER = reverb +REVERB_KEY = local +REVERB_HOST = 127.0.0.1 +REVERB_PORT = 8080 +FORCE_TLS = false +DISABLE_STATS = true +ENCRYPTED = false +CLUSTER = mt1 +ENABLED_TRANSPORTS = [ws, wss] \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0fc3e15..861965b 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,4 @@ dist-ssr *.sw? *storybook.log -storybook-static - -.env \ No newline at end of file +storybook-static \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index fb4813e..4e66827 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,8 @@ "@mui/material": "^7.3.6", "bootstrap": "^5.3.8", "chart.js": "^4.5.1", + "laravel-echo": "^2.2.7", + "pusher-js": "^8.4.0", "react": "^19.1.1", "react-bootstrap": "^2.10.10", "react-chartjs-2": "^5.3.1", @@ -1333,6 +1335,13 @@ "linux" ] }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT", + "peer": true + }, "node_modules/@standard-schema/spec": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", @@ -2819,6 +2828,30 @@ "node": ">=14" } }, + "node_modules/engine.io-client": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz", + "integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/error-ex": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", @@ -3736,6 +3769,19 @@ "node": ">=6" } }, + "node_modules/laravel-echo": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/laravel-echo/-/laravel-echo-2.2.7.tgz", + "integrity": "sha512-MgD3ZFXqH5OOVdRjxNHPyQ0ijRr5+nLr7MtyF2XP+kRfhl+Qaa7qVzbtCn1HMgXuTn4SWH6ivn4qWVLlvRl8kg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "pusher-js": "*", + "socket.io-client": "*" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -4370,6 +4416,15 @@ "node": ">=6" } }, + "node_modules/pusher-js": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-8.4.0.tgz", + "integrity": "sha512-wp3HqIIUc1GRyu1XrP6m2dgyE9MoCsXVsWNlohj0rjSkLf+a0jLvEyVubdg58oMk7bhjBWnFClgp8jfAa6Ak4Q==", + "license": "MIT", + "dependencies": { + "tweetnacl": "^1.0.3" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -4798,6 +4853,36 @@ "dev": true, "license": "MIT" }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz", + "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -5225,6 +5310,12 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "license": "Unlicense" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -5683,7 +5774,6 @@ "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -5701,6 +5791,15 @@ } } }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "peer": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index a5d1069..02da846 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,8 @@ "@mui/material": "^7.3.6", "bootstrap": "^5.3.8", "chart.js": "^4.5.1", + "laravel-echo": "^2.2.7", + "pusher-js": "^8.4.0", "react": "^19.1.1", "react-bootstrap": "^2.10.10", "react-chartjs-2": "^5.3.1", diff --git a/src/components/Header/Header.jsx b/src/components/Header/Header.jsx index 5b4300f..cbb531e 100644 --- a/src/components/Header/Header.jsx +++ b/src/components/Header/Header.jsx @@ -1,26 +1,59 @@ -import styles from "./Header.module.css" import { Link, NavLink } from "react-router"; import {useContext, useEffect, useRef, useState} from "react"; -import getUserNotifications from "../../utils/getUserNotifications.js"; + import {AuthContext} from "../../contexts/auth/AuthContext.js"; +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 getUser from "../../utils/getUser.js"; +import { userCreatedListener } from "../../utils/echo/listeners/userCreatedListener"; +import { userNotificationsListener } from "../../utils/echo/listeners/userNotificationsListener.js"; +import NotificationCard from "../NotificationCard/NotificationCard.jsx"; + function Header() { + const { update, user } = useContext(AuthContext); const [notificationMenu, setnotificationMenu] = useState(false); - const [unreadNotification, setunreadNotification] = useState(true); + const [unreadNotification, setunreadNotification] = useState(false); const [mobileMenu, setMobileMenu] = useState(false); - const [notifications, setNotifications] = useState([{"content" : "ok"}, {"content" : "pourquoi pas antoine"}]); + const [notifications, setNotifications] = useState([]); const notificationRef = useRef(null); - const { user } = useContext(AuthContext); - - useEffect(() => { - async function loadNotifications() { - const notifData = await getUserNotifications(); - setNotifications(notifData || []); + let echoInstance = null; + let channelsToLeave = []; + + async function initializeHeader() { + try { + const [userData, notifData] = await Promise.all([ + getUser(), + getUserNotifications() + ]); + + const data = 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, userData, setNotifications, setunreadNotification), + userNotificationsListener(echo, userData, setNotifications, setunreadNotification), + ]; + channelsToLeave = activeChannels.filter(name => name !== null); + + } catch (err) { + console.error("Erreur initialisation Header:", err); + } } - //loadNotifications(); + initializeHeader(); + const handleClickOutside = (event) => { if ( notificationRef.current && @@ -32,11 +65,48 @@ function Header() { }; document.addEventListener("mousedown", handleClickOutside); + return () => { document.removeEventListener("mousedown", handleClickOutside); + if (echoInstance) { + channelsToLeave.forEach(chan => echoInstance.leave(chan)); + } }; }, []); + + + const toggleNotificationMenu = async () => { + try { + setnotificationMenu(!notificationMenu); + if (unreadNotification) { + const result = await readNotifications(); + if (result) { + setunreadNotification(false); + update(); + } + } + } catch (err) { + console.error('Erreur suppression notification :', err); + } + } + + + async function deleteNotification(notificationId) { + try { + const result = await deleteNotificationUser(notificationId); + if (result) { + setNotifications(prev => prev.filter(n => n.id !== notificationId)); + update(); + } + } catch (err) { + console.error('Erreur suppression notification :', err); + } + } + + + + return (
@@ -66,10 +136,7 @@ function Header() {
-
- )) + )}
diff --git a/src/components/Header/Header.module.css b/src/components/Header/Header.module.css index eac6e90..952529d 100644 --- a/src/components/Header/Header.module.css +++ b/src/components/Header/Header.module.css @@ -150,13 +150,16 @@ .notificationDiv { position: absolute; - width: 250px; + width: 300px; top: 105%; right: 2rem; opacity: 0; transform: translateY(-10px); transition: opacity 0.3s ease, transform 0.3s ease; z-index: 0; + max-height: 60vh; + overflow: scroll; + scrollbar-width: none; } .activeNotificationDiv { @@ -183,16 +186,6 @@ border-radius: 50%; } -.closeBtn { - display: flex; - align-items: center; - justify-content: center; - border: none; - cursor: pointer; - background: none; - margin: 0; - width: fit-content; -} .closeBtn:focus { outline: none; diff --git a/src/components/IncompleteEvent/IncompleteEvent.module.css b/src/components/IncompleteEvent/IncompleteEvent.module.css index 0140936..966a2e5 100644 --- a/src/components/IncompleteEvent/IncompleteEvent.module.css +++ b/src/components/IncompleteEvent/IncompleteEvent.module.css @@ -37,17 +37,13 @@ display: none; min-height: 250px; max-height: 250px; overflow-y: auto; - padding: 10px; text-align: center; - padding-bottom: 200px; + padding: 10px 10px 200px 10px; box-sizing: border-box; opacity: 0; transform: translateX(20px); animation: slideInEvent 1s ease forwards; flex-shrink: 1; - padding: 10px; - text-align: center; - box-sizing: border-box; } .eventCard:nth-child(1) { @@ -99,7 +95,6 @@ display: none; min-width: 100%; min-height: 200px; max-height: fit-content; - padding-bottom: 0px; } .eventCard h3 { @@ -114,7 +109,7 @@ display: none; margin-top: 15px; height: fit-content; overflow-y: auto; - padding-bottom: 0px; + padding-bottom: 0; scroll-behavior: smooth; } diff --git a/src/components/NotificationCard/NotificationCard.jsx b/src/components/NotificationCard/NotificationCard.jsx new file mode 100644 index 0000000..50774b4 --- /dev/null +++ b/src/components/NotificationCard/NotificationCard.jsx @@ -0,0 +1,34 @@ +import formatDate from "../../utils/date/formatDate.js"; +import formatTime from "../../utils/date/formatTime.js"; + +import styles from "./NotificationCard.module.css" + + +function NotificationCard({notifications, deleteNotification}) { + return ( + notifications.map((notification, index) => ( +
+
+ + Le {formatDate(notification.created_at)} à {formatTime(notification.created_at)} + + +
+ +
+

+ {notification.content} +

+
+
+ )) + ); +} + +export default NotificationCard; \ No newline at end of file diff --git a/src/components/NotificationCard/NotificationCard.module.css b/src/components/NotificationCard/NotificationCard.module.css new file mode 100644 index 0000000..db01e1a --- /dev/null +++ b/src/components/NotificationCard/NotificationCard.module.css @@ -0,0 +1,46 @@ +.notificationCard { + padding: 1.25rem; + display: flex; + flex-direction: column; + gap: 0.75rem; + border-bottom: 1px solid rgba(0, 0, 0, 0.08); + transition: background 0.2s ease; +} + +.cardHeader { + display: flex; + justify-content: space-between; + align-items: center; +} + +.timeTag { + font-size: 0.9rem; + font-weight: 500; + color: #019AFF; + letter-spacing: 0.2px; +} + +.message { + margin: 0; + font-size: 1.1rem; + line-height: 1.5; + color: #2c3e50; + font-weight: 400; +} + +.closeIconButton { + background: none; + border: none; + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + font-size: 1.1rem; + color: #2c3e50; +} + +.closeIconButton:hover { + color: #cc0000; +} \ No newline at end of file diff --git a/src/components/SearchBar/SearchBar.jsx b/src/components/SearchBar/SearchBar.jsx index d39f095..6635d98 100644 --- a/src/components/SearchBar/SearchBar.jsx +++ b/src/components/SearchBar/SearchBar.jsx @@ -1,8 +1,8 @@ import styles from "./SearchBar.module.css" import {Link} from "react-router"; import {useEffect, useState} from "react"; -import searchEvents from "../../utils/searchEvents.js"; -import searchUsers from "../../utils/searchUsers.js"; +import searchEvents from "../../utils/events/searchEvents.js"; +import searchUsers from "../../utils/users/searchUsers.js"; import { useLocation } from "react-router"; diff --git a/src/components/Task/Task.jsx b/src/components/Task/Task.jsx index 740aeda..1be1990 100644 --- a/src/components/Task/Task.jsx +++ b/src/components/Task/Task.jsx @@ -1,5 +1,5 @@ import React, {useEffect, useState} from "react"; -import getEventById from "../../utils/event/getEventById.js"; +import getEventById from "../../utils/events/getEventById.js"; import styles from "./Task.module.css"; import formatDate from "../../utils/date/formatDate.js"; diff --git a/src/components/background/background.module.css b/src/components/background/background.module.css index c0e7211..43504cd 100644 --- a/src/components/background/background.module.css +++ b/src/components/background/background.module.css @@ -1,4 +1,3 @@ -/* BACKGROUND ////////////////////////////////////////////////////////*/ .backgroundContainer { z-index: 0; margin: 0; diff --git a/src/components/chart/ParticipationChart.jsx b/src/components/chart/ParticipationChart.jsx index 6af6d0d..0a401ef 100644 --- a/src/components/chart/ParticipationChart.jsx +++ b/src/components/chart/ParticipationChart.jsx @@ -6,9 +6,9 @@ import styles from "./ParticipationChart.module.css"; Chart.register(ArcElement); -function participationChart() { +function ParticipationChart() { const [selected, setSelected] = useState("3"); - const [participationRate, setParticipationRate] = useState(78); + const [participationRate] = useState(78); const chartData = { labels: ["Progress", "Background"], @@ -87,4 +87,4 @@ function participationChart() { ); } -export default participationChart; +export default ParticipationChart; diff --git a/src/components/chart/ParticipationChart.module.css b/src/components/chart/ParticipationChart.module.css index 7fa3515..00c9652 100644 --- a/src/components/chart/ParticipationChart.module.css +++ b/src/components/chart/ParticipationChart.module.css @@ -4,7 +4,6 @@ flex-direction: column; align-items: center; height: calc(100vh - 260px); - /* Exemple: calc(100vh - 60px) */ margin-bottom: auto; } diff --git a/src/components/createEventBtn/CreateEventBtn.jsx b/src/components/createEventBtn/CreateEventBtn.jsx index 7e99ec4..fb60a1e 100644 --- a/src/components/createEventBtn/CreateEventBtn.jsx +++ b/src/components/createEventBtn/CreateEventBtn.jsx @@ -3,7 +3,7 @@ import { useState } from "react"; import Modal from "../ui/modal/modal.jsx"; import Button from "../ui/button/button.jsx"; import TextInput from "../ui/input/input.jsx"; -import createEvent from "../../utils/event/createEvent.js" +import createEvent from "../../utils/events/createEvent.js" export default function CreateEventBtn() { diff --git a/src/components/homePage/calendar.jsx b/src/components/homePage/calendar.jsx index 9b53629..faab865 100644 --- a/src/components/homePage/calendar.jsx +++ b/src/components/homePage/calendar.jsx @@ -12,7 +12,7 @@ function Calendar({ events = [] }) { const today = new Date(); const [currentMonth, setCurrentMonth] = useState(today.getMonth()); - const [currentYear, setCurrentYear] = useState(today.getFullYear()); + const [currentYear] = useState(today.getFullYear()); const [selectedDay, setSelectedDay] = useState(null); const eventsByDate = useMemo(() => { diff --git a/src/components/homePage/calendar.module.css b/src/components/homePage/calendar.module.css index ef2f189..9bbfda8 100644 --- a/src/components/homePage/calendar.module.css +++ b/src/components/homePage/calendar.module.css @@ -109,6 +109,13 @@ border-radius: 20px; padding: 15px; margin-top: 16px; + border: none; + background: rgb(255, 255, 255, 0.25); + box-shadow: + inset 1.25px 1.25px 1px rgba(255, 255, 255, 1), + inset -1.25px -1.25px 1px rgba(255, 255, 255, 1), + inset -0.25px 0.25px 1px rgba(0, 0, 0, 0.2), + inset 0.25px -0.25px 1px rgba(0, 0, 0, 0.2); } .eventBox ul { @@ -131,10 +138,20 @@ .glassCard { width: 45%; max-height: 100%; - margin: 16px 0px 16px 0px; + margin: 16px 0 16px 0; padding: 10px 20px 10px 20px; height: fit-content; + background: rgba(255, 255, 255, 0.65); + backdrop-filter: blur(3px); + -webkit-backdrop-filter: blur(3px); + border-radius: 20px; + border: 1px solid rgba(255, 255, 255, 0.3); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1), + inset 0 1px 0 rgba(255, 255, 255, 0.5), + inset 0 -1px 0 rgba(255, 255, 255, 0.1), + inset 0 0 8px 4px rgba(255, 255, 255, 0.4); overflow: scroll; + } .glassCard::-webkit-scrollbar{ diff --git a/src/components/homePage/eventList.module.css b/src/components/homePage/eventList.module.css index cd2b3d6..6bb0b67 100644 --- a/src/components/homePage/eventList.module.css +++ b/src/components/homePage/eventList.module.css @@ -1,10 +1,20 @@ .glassCard { width: 45%; max-height: 100%; - margin: 16px 0px 16px 0px; + margin: 16px 0 16px 0; padding: 10px 20px 10px 20px; height: fit-content; + background: rgba(255, 255, 255, 0.65); + backdrop-filter: blur(3px); + -webkit-backdrop-filter: blur(3px); + border-radius: 20px; + border: 1px solid rgba(255, 255, 255, 0.3); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1), + inset 0 1px 0 rgba(255, 255, 255, 0.5), + inset 0 -1px 0 rgba(255, 255, 255, 0.1), + inset 0 0 8px 4px rgba(255, 255, 255, 0.4); overflow: scroll; + } .glassCard::-webkit-scrollbar{ @@ -42,6 +52,14 @@ h2{ .eventGroup{ margin: 16px 0px 16px 0px; padding: 10px 20px 10px 20px; + border-radius: 20px; + border: none; + background: transparent; + box-shadow: + inset 1.25px 1.25px 1px rgba(255, 255, 255, 1), + inset -1.25px -1.25px 1px rgba(255, 255, 255, 1), + inset -0.25px 0.25px 1px rgba(0, 0, 0, 0.2), + inset 0.25px -0.25px 1px rgba(0, 0, 0, 0.2); } .eventTitle { diff --git a/src/components/loginform/loginForm.module.css b/src/components/loginform/loginForm.module.css index 45caff4..b726fa7 100644 --- a/src/components/loginform/loginForm.module.css +++ b/src/components/loginform/loginForm.module.css @@ -5,7 +5,7 @@ input { width: 100%; height: 60px; - padding: 0px 10px; + padding: 0 10px; border: none; border-radius: 10px; font-size: 19px; @@ -35,7 +35,6 @@ input::placeholder{ padding: 15px; } -/* style scoped (module) */ .loginButton { display: inline-block; height: 50px; diff --git a/src/layout.jsx b/src/layout.jsx index 3e477a4..4fa0991 100644 --- a/src/layout.jsx +++ b/src/layout.jsx @@ -9,11 +9,11 @@ import { AuthContext } from "./contexts/auth/AuthContext.js"; function Layout(){ - /*const { user, loading } = useContext(AuthContext); + const { user, loading } = useContext(AuthContext); - if (loading) return

Loading

; + if (loading) return

Loading

; if (!user) return ; - if(user && user.validate === 0) return ;*/ + if(user && user.validate === 0) return ; return (
diff --git a/src/pages/Admin/AdminPage.jsx b/src/pages/Admin/AdminPage.jsx index 7682bb1..54f9478 100644 --- a/src/pages/Admin/AdminPage.jsx +++ b/src/pages/Admin/AdminPage.jsx @@ -12,24 +12,24 @@ function AdminPage() { const { user } = useContext(AuthContext); const navigate = useNavigate(); - const [selected, setSelected] = useState("3"); - const [participationRate, setParticipationRate] = useState(89); + const [selected, setSelected] = useState("3"); + const [participationRate] = useState(89); - /*if(!user.isAdmin) navigate("/");*/ + if(!user.isAdmin) navigate("/"); - return ( -
- -
- - -
-
- ); + return ( +
+ +
+ + +
+
+ ); } export default AdminPage; diff --git a/src/pages/Events/EventsPage.jsx b/src/pages/Events/EventsPage.jsx index 19fa549..4c56864 100644 --- a/src/pages/Events/EventsPage.jsx +++ b/src/pages/Events/EventsPage.jsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; import Event from "../../components/Event/Event.jsx"; import styles from "./EventsPage.module.css"; import ToolBar from "../../components/ToolBar/ToolBar.jsx"; -import getAllEvents from "../../utils/event/getAllEvents.js"; +import getAllEvents from "../../utils/events/getAllEvents.js"; import filter from "../../utils/filter.js"; diff --git a/src/pages/Home/HomePage.jsx b/src/pages/Home/HomePage.jsx index 02bde02..e1c9849 100644 --- a/src/pages/Home/HomePage.jsx +++ b/src/pages/Home/HomePage.jsx @@ -2,7 +2,7 @@ import styles from "./HomePage.module.css"; import Calendar from "../../components/homePage/calendar.jsx"; import EventList from "../../components/homePage/eventList.jsx"; import { useState, useEffect } from "react"; -import getAllEvents from "../../utils/event/getAllEvents.js"; +import getAllEvents from "../../utils/events/getAllEvents.js"; function HomePage() { diff --git a/src/pages/Home/HomePage.module.css b/src/pages/Home/HomePage.module.css index 8d3602d..abf6145 100644 --- a/src/pages/Home/HomePage.module.css +++ b/src/pages/Home/HomePage.module.css @@ -9,6 +9,6 @@ .allContent{ display: flex; flex-direction: column; - gap: 0px; + gap: 0; } } \ No newline at end of file diff --git a/src/pages/Login/LoginPage.jsx b/src/pages/Login/LoginPage.jsx index 79af77a..65d1d12 100644 --- a/src/pages/Login/LoginPage.jsx +++ b/src/pages/Login/LoginPage.jsx @@ -10,11 +10,16 @@ function LoginPage() { const { user, loading } = useContext(AuthContext) const navigate = useNavigate(); + + useEffect(() => { + console.log("loading" + loading); + console.log("user" + user); + if (!loading && user) { navigate("/"); } - }, [loading, user]); + }, [loading, navigate, user]); return ( diff --git a/src/pages/Profile/ProfilePage.module.css b/src/pages/Profile/ProfilePage.module.css index 1e7913f..9b20a37 100644 --- a/src/pages/Profile/ProfilePage.module.css +++ b/src/pages/Profile/ProfilePage.module.css @@ -131,7 +131,6 @@ margin-bottom: 20px; } -/* Desktop (1024px et plus) */ @media screen and (min-width: 1024px) { .container { max-width: 100%; diff --git a/src/pages/VolunteerProfile/VolunteerProfilePage.module.css b/src/pages/VolunteerProfile/VolunteerProfilePage.module.css index e9861dc..834af59 100644 --- a/src/pages/VolunteerProfile/VolunteerProfilePage.module.css +++ b/src/pages/VolunteerProfile/VolunteerProfilePage.module.css @@ -131,7 +131,6 @@ margin-bottom: 20px; } -/* Desktop (1024px et plus) */ @media screen and (min-width: 1024px) { .container { max-width: 100%; diff --git a/src/utils/date/formatTime.js b/src/utils/date/formatTime.js new file mode 100644 index 0000000..6012db2 --- /dev/null +++ b/src/utils/date/formatTime.js @@ -0,0 +1,8 @@ +export default function formatTime(d) { + const date = new Date(d); + + const hh = String(date.getUTCHours()).padStart(2, '0'); + const min = String(date.getUTCMinutes()).padStart(2, '0'); + + return `${hh}:${min}`; +} \ No newline at end of file diff --git a/src/utils/echo/initEcho.js b/src/utils/echo/initEcho.js new file mode 100644 index 0000000..9d27b37 --- /dev/null +++ b/src/utils/echo/initEcho.js @@ -0,0 +1,21 @@ +import Echo from "laravel-echo"; +import Pusher from "pusher-js"; + +window.Pusher = Pusher; + +export default function initEcho() { + const echoInstance = new Echo({ + broadcaster: import.meta.env.VITE_BROADCASTER, + key: import.meta.env.VITE_REVERB_KEY, + wsHost: import.meta.env.VITE_REVERB_HOST, + wsPort: Number(import.meta.env.VITE_REVERB_PORT), + forceTLS: import.meta.env.VITE_FORCE_TLS === 'true', + disableStats: import.meta.env.VITE_DISABLE_STATS === 'true', + encrypted: import.meta.env.VITE_ENCRYPTED === 'true', + cluster: import.meta.env.VITE_CLUSTER, + enabledTransports: ['ws', 'wss'], + }); + + window.Echo = echoInstance; + return echoInstance; +} diff --git a/src/utils/echo/listeners/userCreatedListener.js b/src/utils/echo/listeners/userCreatedListener.js new file mode 100644 index 0000000..dddab10 --- /dev/null +++ b/src/utils/echo/listeners/userCreatedListener.js @@ -0,0 +1,18 @@ +export const userCreatedListener = (echo, userData, setNotifications, setunreadNotification) => { + if (userData.isAdmin || userData.role === "Gérant") { + 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); + }); + 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 new file mode 100644 index 0000000..6b07beb --- /dev/null +++ b/src/utils/echo/listeners/userNotificationsListener.js @@ -0,0 +1,47 @@ +export const userNotificationsListener = (echo, userData, setNotifications, setunreadNotification) => { + if (!userData?.id) return null; + + const channelName = `user.${userData.id}`; + + 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); + }) + .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); + }) + .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); + }) + .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); + }); +}; \ No newline at end of file diff --git a/src/utils/event/createEvent.js b/src/utils/events/createEvent.js similarity index 100% rename from src/utils/event/createEvent.js rename to src/utils/events/createEvent.js diff --git a/src/utils/event/getAllEvents.js b/src/utils/events/getAllEvents.js similarity index 100% rename from src/utils/event/getAllEvents.js rename to src/utils/events/getAllEvents.js diff --git a/src/utils/event/getEventById.js b/src/utils/events/getEventById.js similarity index 100% rename from src/utils/event/getEventById.js rename to src/utils/events/getEventById.js diff --git a/src/utils/searchEvents.js b/src/utils/events/searchEvents.js similarity index 100% rename from src/utils/searchEvents.js rename to src/utils/events/searchEvents.js diff --git a/src/utils/filter.js b/src/utils/filter.js index 1495a74..0ed30d7 100644 --- a/src/utils/filter.js +++ b/src/utils/filter.js @@ -4,9 +4,9 @@ function filter(elements, filters, type) { const dB = new Date(dateB); if (filters.yearOrder === "asc") { - return dA - dB; // plus ancien -> plus récent + return dA - dB; } else { - return dB - dA; // plus récent -> plus ancien + return dB - dA; } }; diff --git a/src/utils/notifications/deleteNotificationUser.js b/src/utils/notifications/deleteNotificationUser.js new file mode 100644 index 0000000..804c1dd --- /dev/null +++ b/src/utils/notifications/deleteNotificationUser.js @@ -0,0 +1,26 @@ +import getXSRFToken from "../getXSRF.js"; + +export default async function deleteNotificationUser(notificationId) { + const csrfToken = await getXSRFToken(); + + try { + const res = await fetch(`http://${import.meta.env.VITE_API_URL}/api/users/delete/notification/${notificationId}`, { + method: 'DELETE', + credentials: 'include', + headers: { + 'Accept': 'application/json', + 'X-XSRF-TOKEN': csrfToken + } + }); + + if (!res.ok) { + throw new Error(`Erreur serveur : ${res.status}`); + } + + const data = await res.json(); + return data; + } catch (err) { + console.error('Erreur :', err); + return null; + } +} diff --git a/src/utils/getUserNotifications.js b/src/utils/notifications/getUserNotifications.js similarity index 87% rename from src/utils/getUserNotifications.js rename to src/utils/notifications/getUserNotifications.js index 1b93395..491471f 100644 --- a/src/utils/getUserNotifications.js +++ b/src/utils/notifications/getUserNotifications.js @@ -1,7 +1,7 @@ export default async function getUserNotifications() { try { const response = await fetch( - 'http://localhost:80/api/users/1/notifications', + `http://${import.meta.env.VITE_API_URL}/api/users/notifications`, { method: 'GET', credentials: 'include', diff --git a/src/utils/notifications/readNotifications.js b/src/utils/notifications/readNotifications.js new file mode 100644 index 0000000..3f504a6 --- /dev/null +++ b/src/utils/notifications/readNotifications.js @@ -0,0 +1,27 @@ +import getXSRFToken from "../getXSRF.js"; + +export default async function readNotifications() { + + const csrfToken = await getXSRFToken(); + + try { + const res = await fetch(`http://${import.meta.env.VITE_API_URL}/api/users/notifications/read`, { + method: 'POST', + credentials: 'include', + headers: { + 'Accept': 'application/json', + 'X-XSRF-TOKEN': csrfToken, + 'Content-Type': 'application/json', + }, + }); + + if (!res.ok) { + throw new Error(`Erreur serveur : ${res.status}`); + } + + return await res.json(); + } catch (err) { + console.error('Erreur :', err); + return null; + } +} \ No newline at end of file diff --git a/src/utils/searchUsers.js b/src/utils/users/searchUsers.js similarity index 100% rename from src/utils/searchUsers.js rename to src/utils/users/searchUsers.js