diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index 41cba77..5faeb33 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -5,7 +5,6 @@ import getUserNotifications from "../../utils/notifications/getUserNotifications import deleteNotificationUser from "../../utils/notifications/deleteNotificationUser.js"; import initEcho from "../../utils/echo/initEcho.js" import readNotifications from "../../utils/notifications/readNotifications.js"; -import { adminNotificationsListener } from "../../utils/echo/listeners/adminNotificationsListener"; import { userNotificationsListener } from "../../utils/echo/listeners/userNotificationsListener.js"; import NotificationCard from "../NotificationCard/NotificationCard"; import { EventContext } from "../../contexts/Events/EventContext.js"; @@ -35,14 +34,13 @@ function Header() { try { const echo = initEcho(); echoInstance = echo; - adminNotificationsListener(echo, user, setNotifications, setunreadNotification, updatePendingMembers); - userNotificationsListener(echo, user, setNotifications, setunreadNotification); + userNotificationsListener(echo, user, setNotifications, setunreadNotification, updatePendingMembers); const notifData = await getUserNotifications(); const data: Notification[] = notifData || []; setNotifications(data); - const hasUnread = data.some(n => n.pivot && n.pivot.unread === 1); + const hasUnread = data.some(n => n.read_at === null); setunreadNotification(hasUnread); } catch (err) { console.error("Erreur initialisation Header:", err); @@ -88,7 +86,7 @@ function Header() { } - async function deleteNotification(notificationId: number) { + async function deleteNotification(notificationId: string) { try { const result = await deleteNotificationUser(notificationId); if (result) { diff --git a/src/components/NotificationCard/NotificationCard.module.css b/src/components/NotificationCard/NotificationCard.module.css index 7fadd31..d0aee50 100644 --- a/src/components/NotificationCard/NotificationCard.module.css +++ b/src/components/NotificationCard/NotificationCard.module.css @@ -5,6 +5,7 @@ gap: 0.75rem; border-bottom: 1px solid rgba(0, 0, 0, 0.08); transition: background 0.2s ease; + margin: 0.5rem 0; } .cardHeader { diff --git a/src/components/NotificationCard/NotificationCard.tsx b/src/components/NotificationCard/NotificationCard.tsx index e2086aa..1878e33 100644 --- a/src/components/NotificationCard/NotificationCard.tsx +++ b/src/components/NotificationCard/NotificationCard.tsx @@ -5,13 +5,13 @@ import Notification from "../../interfaces/notification.interface"; interface NotificationCardProps { notifications: Notification[]; - deleteNotification: (id: number) => void; + deleteNotification: (id: string) => void; } function NotificationCard({ notifications, deleteNotification }: NotificationCardProps) { return ( notifications.map((notification, index) => ( -
+
Le {formatDate(notification.created_at)} à {formatTime(notification.created_at)} @@ -25,9 +25,9 @@ function NotificationCard({ notifications, deleteNotification }: NotificationCar
-
+

- {notification.content} + {notification.data.message}

diff --git a/src/interfaces/notification.interface.ts b/src/interfaces/notification.interface.ts index b666fa7..97debd4 100644 --- a/src/interfaces/notification.interface.ts +++ b/src/interfaces/notification.interface.ts @@ -1,6 +1,8 @@ export default interface Notification { - id: number; - content: string; - created_at: string | number; - pivot: { unread: number }; -} + id: string; + data: { + message: string; + }; + read_at: string | null; + created_at: string; +} \ No newline at end of file diff --git a/src/pages/ConfirmEmailChange/ConfirmEmailChangePage.module.css b/src/pages/ConfirmEmailChange/ConfirmEmailChangePage.module.css new file mode 100644 index 0000000..c99cebc --- /dev/null +++ b/src/pages/ConfirmEmailChange/ConfirmEmailChangePage.module.css @@ -0,0 +1,26 @@ +.title { + color: white; + font-size: xx-large; + margin-bottom: 20px; +} + +.content { + display: flex; + flex-direction: column; + z-index: 2; + padding: 10px; +} + +.container { + padding: 20px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 15px; + + p { + font-size: large; + white-space: pre-line; + } +} \ No newline at end of file diff --git a/src/pages/ConfirmEmailChange/ConfirmEmailChangePage.tsx b/src/pages/ConfirmEmailChange/ConfirmEmailChangePage.tsx new file mode 100644 index 0000000..aeca838 --- /dev/null +++ b/src/pages/ConfirmEmailChange/ConfirmEmailChangePage.tsx @@ -0,0 +1,41 @@ +import { useEffect, useState } from "react"; +import { useSearchParams, useNavigate } from "react-router"; +import confirmEmailChange from "../../utils/users/confirmEmailChange"; +import styles from "./ConfirmEmailChangePage.module.css"; +import Background from "../../components/Background/Background"; + +export default function ConfirmEmailChange() { + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + const [message, setMessage] = useState('Validation en cours...'); + const [success, setSuccess] = useState(null); + + useEffect(() => { + const token = searchParams.get('token'); + if (!token) { + setMessage('Token manquant.'); + setSuccess(false); + return; + } + + confirmEmailChange(token) + .then(() => { + setMessage('Votre adresse email a bien été mise à jour.\nVous allez être redirigé vers la page de connexion...'); + setSuccess(true); + setTimeout(() => navigate('/login'), 3000); + }) + .catch(() => { + setMessage('Token invalide ou expiré.'); + setSuccess(false); + }); + }, []); + + return +
+

Validation de votre adresse email

+
+

{message}

+
+
+
+} \ No newline at end of file diff --git a/src/pages/EventDetail/EventDetailPage.tsx b/src/pages/EventDetail/EventDetailPage.tsx index 65e9a64..7c3d37a 100644 --- a/src/pages/EventDetail/EventDetailPage.tsx +++ b/src/pages/EventDetail/EventDetailPage.tsx @@ -7,6 +7,7 @@ import { AuthContext } from "../../contexts/Auth/AuthContext"; import { EventDetailContext } from "../../contexts/EventDetail/EventDetailContext"; import CreateTaskBtn from "./components/CreateTaskBtn/CreateTaskBtn"; import DeleteEventBtn from "./components/DeleteEventBtn/DeleteEventBtn"; +import EditEventBtn from "./components/EditEventBtn/EditEventBtn"; function EventDetailPage() { const params = useParams(); @@ -42,6 +43,7 @@ function EventDetailPage() { {user?.isAdmin && (
+
diff --git a/src/pages/EventDetail/components/AssignUserBtn/AssignUserBtn.tsx b/src/pages/EventDetail/components/AssignUserBtn/AssignUserBtn.tsx index b44b5a5..95ae20c 100644 --- a/src/pages/EventDetail/components/AssignUserBtn/AssignUserBtn.tsx +++ b/src/pages/EventDetail/components/AssignUserBtn/AssignUserBtn.tsx @@ -55,19 +55,24 @@ export default function AssignUserBtn({ taskId, eventId }: AssignUserBtnProps): })(); }, []); - const isUserAssigned = (u: User): boolean => - u.tasks.some(t => t.id === taskId); + const [assignedIds, setAssignedIds] = useState>(new Set()); + + useEffect(() => { + setAssignedIds(new Set( + users.filter(u => u.tasks.some(t => t.id === taskId)).map(u => u.id) + )); + }, [users, taskId]); const handleAssign = async (selectedUser: User) => { + setAssignedIds(prev => new Set(prev).add(selectedUser.id)); await assignOtherUser(taskId as number, selectedUser.id); - updateEventInfo(eventId) - setOpen(false); + updateEventInfo(eventId); }; const handleUnassign = async (selectedUser: User) => { + setAssignedIds(prev => { const next = new Set(prev); next.delete(selectedUser.id); return next; }); await unAssignOtherUser(taskId as number, selectedUser.id); - updateEventInfo(eventId) - setOpen(false); + updateEventInfo(eventId); }; return <> @@ -75,18 +80,15 @@ export default function AssignUserBtn({ taskId, eventId }: AssignUserBtnProps): setOpen(false)} title={"Assigner un utilisateur à cette tâche"}>
- {loading ? : users.map((user: User, index) => ( -
-

- {user.name} {user.lastname} -

- - {isUserAssigned(user) ? ( - ) : ( - )} diff --git a/src/pages/EventDetail/components/EditEventBtn/EditEventBtn.tsx b/src/pages/EventDetail/components/EditEventBtn/EditEventBtn.tsx new file mode 100644 index 0000000..799c9cc --- /dev/null +++ b/src/pages/EventDetail/components/EditEventBtn/EditEventBtn.tsx @@ -0,0 +1,92 @@ +import styles from "../../EventDetailPage.module.css"; +import Button from "../../../../components/ui/Button/Button"; +import Modal from "../../../../components/ui/Modal/Modal"; +import TextInput from "../../../../components/ui/Input/Input"; +import { useContext, useState, useEffect } from "react"; +import { EventDetailContext } from "../../../../contexts/EventDetail/EventDetailContext"; +import updateEvent from "../../../../utils/events/updateEvent"; + +type EditEventBtnProps = { + id: string | number; +}; + +export default function EditEventBtn({ id }: EditEventBtnProps) { + const { eventInfo, updateEventInfo } = useContext(EventDetailContext) as any; + + const [open, setOpen] = useState(false); + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [start, setStart] = useState(""); + const [end, setEnd] = useState(""); + + useEffect(() => { + if (open && eventInfo) { + setName(eventInfo.name ?? ""); + setDescription(eventInfo.description ?? ""); + setStart(eventInfo.start ? eventInfo.start.slice(0, 16) : ""); + setEnd(eventInfo.end ? eventInfo.end.slice(0, 16) : ""); + } + }, [open, eventInfo]); + + const handleSubmit = async () => { + const res = await updateEvent(Number(id), name, description, start, end); + await updateEventInfo(id); + setOpen(false); + }; + + return ( + <> + + + setOpen(false)} title={"Modifier l'événement"}> +
+
+

Nom

+ setName(e.target.value)} + placeholder="Nom de l'événement" + /> +
+ +
+

Description

+ setDescription(e.target.value)} + placeholder="Description" + /> +
+ +
+

Début

+ setStart(e.target.value)} + /> +
+ +
+

Fin

+ setEnd(e.target.value)} + /> +
+ + +
+
+ + ); +} diff --git a/src/pages/EventDetail/components/EditTaskBtn/EditTaskBtn.tsx b/src/pages/EventDetail/components/EditTaskBtn/EditTaskBtn.tsx new file mode 100644 index 0000000..3da5cf4 --- /dev/null +++ b/src/pages/EventDetail/components/EditTaskBtn/EditTaskBtn.tsx @@ -0,0 +1,124 @@ +import { useContext, useEffect, useState } from "react"; +import { AuthContext } from "../../../../contexts/Auth/AuthContext"; +import { EventDetailContext } from "../../../../contexts/EventDetail/EventDetailContext"; +import Button from "../../../../components/ui/Button/Button"; +import Modal from "../../../../components/ui/Modal/Modal"; +import TextInput from "../../../../components/ui/Input/Input"; +import styles from "../../EventDetailPage.module.css"; +import updateTask from "../../../../utils/tasks/updateTask"; + +type EditTaskBtnProps = { + taskId: number; + eventId: number | string; +}; + +export default function EditTaskBtn({ taskId, eventId }: EditTaskBtnProps) { + const { user } = useContext(AuthContext) as any; + const { tasks, updateEventInfo } = useContext(EventDetailContext) as any; + const task = tasks?.find((t: any) => t.id === taskId); + + const [open, setOpen] = useState(false); + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [start, setStart] = useState(""); + const [end, setEnd] = useState(""); + const [location, setLocation] = useState(""); + const [maxParticipants, setMaxParticipants] = useState(""); + + useEffect(() => { + if (open && task) { + setName(task.name ?? ""); + setDescription(task.description ?? ""); + setStart(task.start ? task.start.slice(0, 16) : ""); + setEnd(task.end ? task.end.slice(0, 16) : ""); + setLocation(task.location ?? ""); + setMaxParticipants(task.max_participants?.toString() ?? ""); + } + }, [open, task]); + + if (!user?.isAdmin) return null; + + const handleSubmit = async () => { + await updateTask(taskId, { + name, + description, + start, + end, + location, + max_participants: Number(maxParticipants), + }); + await updateEventInfo(eventId); + setOpen(false); + }; + + return ( + <> + + + setOpen(false)} title={"Modifier la tâche"}> +
+
+

Nom

+ setName(e.target.value)} + placeholder="Nom de la tâche" + /> +
+ +
+

Description

+ setDescription(e.target.value)} + placeholder="Description" + /> +
+ +
+

Lieu

+ setLocation(e.target.value)} + placeholder="Lieu" + /> +
+ +
+

Participants max

+ setMaxParticipants(e.target.value)} + placeholder="Participants max" + /> +
+ +
+

Début

+ setStart(e.target.value)} + /> +
+ +
+

Fin

+ setEnd(e.target.value)} + /> +
+ + +
+
+ + ); +} diff --git a/src/pages/EventDetail/components/EventTask/EventTask.tsx b/src/pages/EventDetail/components/EventTask/EventTask.tsx index 841227e..7166f27 100644 --- a/src/pages/EventDetail/components/EventTask/EventTask.tsx +++ b/src/pages/EventDetail/components/EventTask/EventTask.tsx @@ -10,6 +10,7 @@ import { AuthContext } from "../../../../contexts/Auth/AuthContext"; import { EventDetailContext } from "../../../../contexts/EventDetail/EventDetailContext"; import DeleteTaskBtn from "../DeleteTaskBtn/DeleteTaskBtn"; import AssignUserBtn from "../AssignUserBtn/AssignUserBtn"; +import EditTaskBtn from "../EditTaskBtn/EditTaskBtn"; type EventTaskProps = { taskId: number; @@ -135,6 +136,8 @@ export default function EventTask({ taskId, index }: EventTaskProps) { )} + + (user?.email_notifications ?? 0); const [webNotifications, setWebNotifications] = useState(user?.web_notifications ?? 0); + const [currentPassword, setCurrentPassword] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [newPasswordConfirm, setNewPasswordConfirm] = useState(''); + const [email, setEmail] = useState(''); + const handleLogout = (): void => { logout(); }; @@ -104,6 +111,27 @@ export default function SettingsModal() { } }; + const handlePasswordUpdate = async () => { + try { + await updateUserPassword(currentPassword, newPassword, newPasswordConfirm); + setCurrentPassword(''); + setNewPassword(''); + setNewPasswordConfirm(''); + } catch (err) { + console.error(err); + } + }; + + const handleEmailUpdate = async () => { + try { + await updateUserEmail(email); + setEmail(''); + update(); + } catch (err) { + console.error(err); + } + }; + return ( <> @@ -123,7 +151,7 @@ export default function SettingsModal() {

Modifier les informations du compte

-
+

Changer de Prénom

-
+

Changer de nom

-
+

Changer de numéro de téléphone

+
+

Changer l'adresse email

+
+

Nouvelle adresse email

+ setEmail(e.target.value)} type="email" /> +
+ +
+ + +
+

Changer le mot de passe

+
+

Mot de passe actuel

+ setCurrentPassword(e.target.value)} type="password" /> +
+
+

Nouveau mot de passe

+ setNewPassword(e.target.value)} type="password" /> +
+
+

Confirmer le nouveau mot de passe

+ setNewPasswordConfirm(e.target.value)} type="password" /> +
+ +
+

Apparence

@@ -166,7 +225,7 @@ export default function SettingsModal() {

Notifications

-
+

Notifications par email

-
+

Notifications sur le site

{ - 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/userNotificationsListener.js b/src/utils/echo/listeners/userNotificationsListener.js index f9ec96b..c518d7b 100644 --- a/src/utils/echo/listeners/userNotificationsListener.js +++ b/src/utils/echo/listeners/userNotificationsListener.js @@ -1,37 +1,23 @@ -export const userNotificationsListener = (echo, userData, setNotifications, setUnreadNotification) => { +export const userNotificationsListener = (echo, userData, setNotifications, setUnreadNotification, updatePendingMembers) => { - const channelName = `user.${userData.id}`; + const channelName = `App.Models.User.${userData.id}`; - const handleNotification = (id, content) => { + echo.private(channelName) + .notification((notification) => { + const newNotification = { + id: notification.id, + data: { message: notification.message }, + read_at: null, + created_at: new Date().toISOString(), + }; - const newNotification = { - id, - content, - created_at: new Date().toISOString(), - pivot: { unread: 1 } - }; + setNotifications(prev => [newNotification, ...prev]); + setUnreadNotification(true); - setNotifications(prev => [newNotification, ...prev]); - setUnreadNotification(true); - }; - - return echo.private(channelName) - .listen('.event.participation.cancelled', (event) => { - handleNotification(event.notificationId,`L'événement ${event.event.name} a été supprimé. Vous n'y participez donc plus.`); - }) - .listen(`.task.participation.cancelled`, (event) => { - 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) => { - 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) => { - 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}.`); + if (notification.type === 'App\\Notifications\\UserRegistered' && updatePendingMembers) { + updatePendingMembers(); + } }); + + return channelName; }; \ No newline at end of file diff --git a/src/utils/events/updateEvent.ts b/src/utils/events/updateEvent.ts new file mode 100644 index 0000000..a46b610 --- /dev/null +++ b/src/utils/events/updateEvent.ts @@ -0,0 +1,13 @@ +import fetchWrapper from "../fetchWrapper"; +import getXSRFToken from "../getXSRF"; + +export default async function updateEvent(id: number, name: string, description: string, start: string, end: string) { + const csrfToken = await getXSRFToken(); + + return fetchWrapper( + `/api/events/${id}/update`, + { name, description, start, end }, + "POST", + { "X-XSRF-TOKEN": csrfToken } + ); +} diff --git a/src/utils/notifications/deleteNotificationUser.ts b/src/utils/notifications/deleteNotificationUser.ts index 4322722..ce216e9 100644 --- a/src/utils/notifications/deleteNotificationUser.ts +++ b/src/utils/notifications/deleteNotificationUser.ts @@ -1,6 +1,6 @@ import getXSRFToken from "../getXSRF"; -export default async function deleteNotificationUser(notificationId:number) { +export default async function deleteNotificationUser(notificationId:string) { const csrfToken = await getXSRFToken(); try { diff --git a/src/utils/tasks/updateTask.ts b/src/utils/tasks/updateTask.ts new file mode 100644 index 0000000..ad18e82 --- /dev/null +++ b/src/utils/tasks/updateTask.ts @@ -0,0 +1,22 @@ +import fetchWrapper from "../fetchWrapper"; +import getXSRFToken from "../getXSRF"; + +interface UpdateTaskParams { + name: string; + description: string; + start: string; + end: string; + location: string; + max_participants: number; +} + +export default async function updateTask(id: number, params: UpdateTaskParams): Promise { + const csrfToken = await getXSRFToken(); + + return fetchWrapper( + `/api/events/tasks/${id}/update`, + params, + "POST", + { "X-XSRF-TOKEN": csrfToken } + ); +} diff --git a/src/utils/users/confirmEmailChange.ts b/src/utils/users/confirmEmailChange.ts new file mode 100644 index 0000000..285cc72 --- /dev/null +++ b/src/utils/users/confirmEmailChange.ts @@ -0,0 +1,12 @@ +import fetchWrapper from "../fetchWrapper"; +import getXSRFToken from "../getXSRF"; + +export default async function confirmEmailChange(token: string) { + const csrfToken = await getXSRFToken(); + return fetchWrapper( + "/api/users/update/email/confirm", + { token }, + "POST", + { "X-XSRF-TOKEN": csrfToken } + ); +} \ No newline at end of file diff --git a/src/utils/users/updateUserEmail.ts b/src/utils/users/updateUserEmail.ts new file mode 100644 index 0000000..e61364e --- /dev/null +++ b/src/utils/users/updateUserEmail.ts @@ -0,0 +1,12 @@ +import fetchWrapper from "../fetchWrapper"; +import getXSRFToken from "../getXSRF"; + +export default async function updateUserEmail(email: string) { + const csrfToken = await getXSRFToken(); + return fetchWrapper( + "/api/users/update/email/request", + { email }, + "POST", + { "X-XSRF-TOKEN": csrfToken } + ); +} \ No newline at end of file diff --git a/src/utils/users/updateUserPassword.ts b/src/utils/users/updateUserPassword.ts new file mode 100644 index 0000000..7d7ceb3 --- /dev/null +++ b/src/utils/users/updateUserPassword.ts @@ -0,0 +1,12 @@ +import fetchWrapper from "../fetchWrapper"; +import getXSRFToken from "../getXSRF"; + +export default async function updateUserPassword(current_password: string, new_password: string, new_password_confirmation: string) { + const csrfToken = await getXSRFToken(); + return fetchWrapper( + "/api/users/update/password", + { current_password, new_password, new_password_confirmation }, + "POST", + { "X-XSRF-TOKEN": csrfToken } + ); +} \ No newline at end of file