Merge branch 'dev' into fixcss/event

merge dev into fixcss
This commit is contained in:
ORDONNEAU Antoine
2026-03-26 12:18:38 +01:00
32 changed files with 330 additions and 255 deletions
@@ -1,13 +1,18 @@
import styles from "./Background.module.css"; 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 [theme, setTheme] = useState("light");
const getRandomTranslation = () => { const getRandomTranslation = (): { x: number; y: number } => {
const randomX = Math.floor(Math.random() * 100) - 30; const randomX = Math.floor(Math.random() * 100) - 30;
const randomY = Math.floor(Math.random() * 100) - 30; const randomY = Math.floor(Math.random() * 100) - 30;
return { x: randomX, y: randomY }; return { x: randomX, y: randomY };
}; };
@@ -27,7 +32,7 @@ const Background = ({ children, className }) => {
setTheme(localStorage.getItem("theme") || "light"); setTheme(localStorage.getItem("theme") || "light");
}; };
updateTheme(); // Initial load updateTheme();
window.addEventListener("theme-changed", updateTheme); window.addEventListener("theme-changed", updateTheme);
@@ -40,29 +45,29 @@ const Background = ({ children, className }) => {
return ( return (
<div className={`${styles.backgroundContainer} ${className || ''} ${theme === "light" ? styles.backgroundLight : styles.backgroundDark}`}> <div className={`${styles.backgroundContainer} ${className || ''} ${theme === "light" ? styles.backgroundLight : styles.backgroundDark}`}>
<div <div
ref={(el) => (circlesRef.current[0] = el)} ref={(el) => { circlesRef.current[0] = el; }}
className={`${styles["circle"]} ${styles["circle-1"]}`} className={`${styles["circle"]} ${styles["circle-1"]}`}
style={{ "--random-x": "0px", "--random-y": "0px" }} style={{ "--random-x": "0px", "--random-y": "0px" } as React.CSSProperties}
></div> ></div>
<div <div
ref={(el) => (circlesRef.current[1] = el)} ref={(el) => { circlesRef.current[1] = el; }}
className={`${styles["circle"]} ${styles["circle-2"]}`} className={`${styles["circle"]} ${styles["circle-2"]}`}
style={{ "--random-x": "0px", "--random-y": "0px" }} style={{ "--random-x": "0px", "--random-y": "0px" } as React.CSSProperties}
></div> ></div>
<div <div
ref={(el) => (circlesRef.current[2] = el)} ref={(el) => { circlesRef.current[2] = el; }}
className={`${styles["circle"]} ${styles["circle-3"]}`} className={`${styles["circle"]} ${styles["circle-3"]}`}
style={{ "--random-x": "0px", "--random-y": "0px" }} style={{ "--random-x": "0px", "--random-y": "0px" } as React.CSSProperties}
></div> ></div>
<div <div
ref={(el) => (circlesRef.current[3] = el)} ref={(el) => { circlesRef.current[3] = el; }}
className={`${styles["circle"]} ${styles["circle-4"]}`} className={`${styles["circle"]} ${styles["circle-4"]}`}
style={{ "--random-x": "0px", "--random-y": "0px" }} style={{ "--random-x": "0px", "--random-y": "0px" } as React.CSSProperties}
></div> ></div>
<div <div
ref={(el) => (circlesRef.current[4] = el)} ref={(el) => { circlesRef.current[4] = el; }}
className={`${styles["circle"]} ${styles["circle-5"]}`} className={`${styles["circle"]} ${styles["circle-5"]}`}
style={{ "--random-x": "0px", "--random-y": "0px" }} style={{ "--random-x": "0px", "--random-y": "0px" } as React.CSSProperties}
></div> ></div>
{children} {children}
</div> </div>
@@ -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 <Button
variant="default"
onClick={() => {
window.open(
`${APP_CONFIG.newCalendarURL}${calendarUrl}`
);
}}
>
Exporter le calendrier
</Button>
}
@@ -1,13 +1,13 @@
import styles from "./Footer.module.css" import styles from "./Footer.module.css"
import {Link} from "react-router"; import { Link } from "react-router";
import {useState} from "react"; import { ReactNode, useState } from "react";
import Modal from "../ui/Modal/Modal"; import Modal from "../ui/Modal/Modal";
import Button from "../ui/Button/Button"; import Button from "../ui/Button/Button";
import scrollToTop from "../../utils/scrollToTop.js"; import scrollToTop from "../../utils/scrollToTop.js";
export default function Footer(){ export default function Footer(): ReactNode{
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
const [isCopyrightModal, setIsCopyrightModal] = useState(false); const [isCopyrightModal, setIsCopyrightModal] = useState<boolean>(false);
return <> return <>
@@ -30,7 +30,7 @@ export default function Footer(){
Fiche RGPD Fiche RGPD
</Link> </Link>
<span className={styles.separator}></span> <span className={styles.separator}></span>
<Link to="/legalNoticesPage" className={styles.link} onClick={scrollToTop}> <Link to="/legalNotices" className={styles.link} onClick={scrollToTop}>
Mentions légales Mentions légales
</Link> </Link>
<span className={styles.separator}></span> <span className={styles.separator}></span>
@@ -48,11 +48,11 @@ export default function Footer(){
<Modal title={"Réalisé par"} open={isCopyrightModal} onClose={() => setIsCopyrightModal(false)}> <Modal title={"Réalisé par"} open={isCopyrightModal} onClose={() => setIsCopyrightModal(false)}>
<section className={styles.section}> <section className={styles.section}>
<div className={styles.inputContainer}> <div className={styles.inputContainer}>
<p>Quentin T'jampens</p> <p>Quentin T'JAMPENS</p>
<p>Antoine Ordonneau</p> <p>Antoine ORDONNEAU</p>
<p>Malo Leblond</p> <p>Malo LEBLOND</p>
<p> <p>
Giovanni Josserand ( Giovanni JOSSERAND (
<Link to={"https://linktree.josserand.ovh/"}>linktree</Link> <Link to={"https://linktree.josserand.ovh/"}>linktree</Link>
) )
</p> </p>
@@ -60,4 +60,4 @@ export default function Footer(){
</section> </section>
</Modal> </Modal>
</> </>
} }
@@ -1,53 +1,49 @@
import { Link, NavLink } from "react-router"; 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 styles from "./Header.module.css"
import getUserNotifications from "../../utils/notifications/getUserNotifications.js"; import getUserNotifications from "../../utils/notifications/getUserNotifications.js";
import deleteNotificationUser from "../../utils/notifications/deleteNotificationUser.js"; import deleteNotificationUser from "../../utils/notifications/deleteNotificationUser.js";
import initEcho from "../../utils/echo/initEcho.js" import initEcho from "../../utils/echo/initEcho.js"
import readNotifications from "../../utils/notifications/readNotifications.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 { 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 { 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 { PendingMembersContext } from "../../contexts/PendingMembers/PendingMembersContext";
import Notification from "../../interfaces/notification.interface";
function Header() { function Header() {
const { update, user } = useContext(AuthContext); const { update, user } = useContext(AuthContext)!;
const { updateEvent } = useContext(EventContext); const { updateEvent } = useContext(EventContext)!;
const [notificationMenu, setnotificationMenu] = useState(false); const { updatePendingMembers } = useContext(PendingMembersContext) as unknown as { updatePendingMembers: () => void };
const [unreadNotification, setunreadNotification] = useState(false); const [notificationMenu, setnotificationMenu] = useState<boolean>(false);
const [mobileMenu, setMobileMenu] = useState(false); const [unreadNotification, setunreadNotification] = useState<boolean>(false);
const [notifications, setNotifications] = useState([]); const [mobileMenu, setMobileMenu] = useState<boolean>(false);
const notificationRef = useRef(null); const [notifications, setNotifications] = useState<Notification[]>([]);
const { updatePendingMembers } = useContext(PendingMembersContext); const notificationRef = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
if (!user) return;
updateEvent(); updateEvent();
let echoInstance = null; let echoInstance: ReturnType<typeof initEcho> | null = null;
let channelsToLeave = [];
async function initializeHeader() { async function initializeHeader() {
try { try {
const echo = initEcho();
echoInstance = echo;
adminNotificationsListener(echo, user, setNotifications, setunreadNotification, updatePendingMembers);
userNotificationsListener(echo, user, setNotifications, setunreadNotification);
const notifData = await getUserNotifications(); const notifData = await getUserNotifications();
const data = notifData || []; const data: Notification[] = notifData || [];
setNotifications(data); setNotifications(data);
const hasUnread = data.some(n => n.pivot && n.pivot.unread === 1); const hasUnread = data.some(n => n.pivot && n.pivot.unread === 1);
setunreadNotification(hasUnread); 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) { } catch (err) {
console.error("Erreur initialisation Header:", err); console.error("Erreur initialisation Header:", err);
} }
@@ -55,22 +51,21 @@ function Header() {
initializeHeader(); initializeHeader();
const handleClickOutside = (event) => { const handleClickOutside = (event: MouseEvent) => {
if ( if (
notificationRef.current && notificationRef.current &&
!notificationRef.current.contains(event.target) && !notificationRef.current.contains(event.target as Node) &&
!event.target.closest(`.${styles.bellBtn}`) !(event.target as Element).closest(`.${styles.bellBtn}`)
) { ) {
setnotificationMenu(false); setnotificationMenu(false);
} }
}; };
document.addEventListener("mousedown", handleClickOutside); document.addEventListener("mousedown", handleClickOutside);
return () => { return () => {
document.removeEventListener("mousedown", handleClickOutside); document.removeEventListener("mousedown", handleClickOutside);
if (echoInstance) { 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 { try {
const result = await deleteNotificationUser(notificationId); const result = await deleteNotificationUser(notificationId);
if (result) { if (result) {
@@ -194,4 +189,4 @@ function Header() {
); );
} }
export default Header; export default Header;
@@ -1,10 +1,14 @@
import formatDate from "../../utils/date/formatDate.js"; import formatDate from "../../utils/date/formatDate.js";
import formatTime from "../../utils/date/formatTime.js"; import formatTime from "../../utils/date/formatTime.js";
import styles from "./NotificationCard.module.css" 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 ( return (
notifications.map((notification, index) => ( notifications.map((notification, index) => (
<div className={`${styles.notificationCard} glassBorder`}key={index}> <div className={`${styles.notificationCard} glassBorder`}key={index}>
@@ -31,4 +35,4 @@ function NotificationCard({notifications, deleteNotification}) {
); );
} }
export default NotificationCard; export default NotificationCard;
@@ -1,9 +1,20 @@
import styles from "./ToolBar.module.css" 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 SearchBtn from "./tools/SearchBtn";
import FilterBtn from "./tools/FilterBtn"; import FilterBtn from "./tools/FilterBtn";
function ToolBar({setFilters, filters, showCreate = true}) { interface Filters {
alphabetical: "asc" | "desc";
yearOrder: "asc" | "desc";
}
interface ToolBarProps {
setFilters: React.Dispatch<React.SetStateAction<Filters>>;
filters: Filters;
showCreate?: boolean;
}
function ToolBar({ setFilters, filters, showCreate = true }: ToolBarProps) {
return ( return (
<div> <div>
@@ -16,4 +27,4 @@ function ToolBar({setFilters, filters, showCreate = true}) {
); );
} }
export default ToolBar; export default ToolBar;
@@ -4,19 +4,19 @@ import Modal from "../../../ui/Modal/Modal";
import Button from "../../../ui/Button/Button"; import Button from "../../../ui/Button/Button";
import TextInput from "../../../ui/Input/Input"; import TextInput from "../../../ui/Input/Input";
import { EventContext } from "../../../../contexts/Events/EventContext.js"; import { EventContext } from "../../../../contexts/Events/EventContext.js";
import {AuthContext} from "../../../../contexts/Auth/AuthContext.ts"; import { AuthContext } from "../../../../contexts/Auth/AuthContext";
export default function CreateEventBtn() { export default function CreateEventBtn() {
const { addEvent } = useContext(EventContext); const { addEvent } = useContext(EventContext)!;
const { user } = useContext(AuthContext); const { user } = useContext(AuthContext)!;
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState<boolean>(false);
const [name, setName] = useState(""); const [name, setName] = useState<string>("");
const [description, setDescription] = useState(""); const [description, setDescription] = useState<string>("");
const [start, setStart] = useState(""); const [start, setStart] = useState<string>("");
const [end, setEnd] = useState(""); const [end, setEnd] = useState<string>("");
const handleCreateEvent = async () => { const handleCreateEvent = async () => {
@@ -62,4 +62,4 @@ export default function CreateEventBtn() {
</section> </section>
</Modal> </Modal>
</> </>
} }
+2 -20
View File
@@ -1,30 +1,12 @@
import { createContext } from "react"; import { createContext } from "react";
import User from "./../../interfaces/user.interface"
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[];
}
export interface AuthContextType { export interface AuthContextType {
user: User | null; user: User | null;
loading: boolean; loading: boolean;
update: () => void; update: () => void;
login: (email: string, password: string) => Promise<{ status: number }>; login: (email: string, password: string) => Promise<{ status: number }>;
logout: () => Promise<void>;
} }
+2
View File
@@ -16,6 +16,8 @@ export type EventGroup = {
export type EventContextType = { export type EventContextType = {
events: EventGroup[]; events: EventGroup[];
updateEvent: () => void;
addEvent: (name: string, description: string, start: string, end: string) => Promise<void>;
}; };
export const EventContext = createContext<EventContextType | null>(null); export const EventContext = createContext<EventContextType | null>(null);
+6
View File
@@ -0,0 +1,6 @@
import type User from "./user.interface";
export interface AuthContextType {
user: User | null;
update: () => void;
}
+6
View File
@@ -0,0 +1,6 @@
export default interface Notification {
id: number;
content: string;
created_at: string | number;
pivot: { unread: number };
}
@@ -13,4 +13,10 @@ export default interface TaskType {
user_id: number; user_id: number;
task_id: number; task_id: number;
}; };
}
export default interface Task {
id: number;
title?: string;
completed?: boolean;
[key: string]: unknown;
} }
+5 -2
View File
@@ -1,4 +1,4 @@
import TaskType from "./taskType.interface"; import TaskType from "./task.interface";
export default interface User { export default interface User {
id: number; id: number;
@@ -9,6 +9,9 @@ export default interface User {
phone: string | null; phone: string | null;
created_at: string; created_at: string;
isAdmin: boolean; isAdmin: boolean;
validate: number;
tasks: TaskType[]; tasks: TaskType[];
profile_photo_path?: string | null; profile_photo_path?: string | null,
email_notifications: number,
web_notifications: number;
} }
@@ -48,12 +48,18 @@ const RegisterMember: React.FC = () => {
setOpen(true); setOpen(true);
} }
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
handleCreate();
}
};
return <div className={`${styles.leftBlock} glassCard`}> return <div className={`${styles.leftBlock} glassCard`}>
<div className={styles.registerMemberHeader}> <div className={styles.registerMemberHeader}>
<h2>Enregister un bénévole</h2> <h2>Enregister un bénévole</h2>
</div> </div>
<div className={styles.registerForm}> <div className={styles.registerForm} onKeyDown={handleKeyDown}>
<TextInput placeholder={"Prenom"} type={"text"} value={name} onChange={e => setName(e.target.value)} /> <TextInput placeholder={"Prenom"} type={"text"} value={name} onChange={e => setName(e.target.value)} />
<TextInput placeholder={"Nom"} type={"text"} value={lastname} onChange={e => setLastName(e.target.value)} /> <TextInput placeholder={"Nom"} type={"text"} value={lastname} onChange={e => setLastName(e.target.value)} />
<TextInput placeholder={"Adresse Email"} type={"email"} value={email} onChange={e => setEmail(e.target.value)} /> <TextInput placeholder={"Adresse Email"} type={"email"} value={email} onChange={e => setEmail(e.target.value)} />
-15
View File
@@ -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 (
<div className={styles.allContent}>
<Calendar />
<EventList />
</div>
);
}
export default HomePage;
+12
View File
@@ -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 <div className={styles.allContent}>
<Calendar/>
<EventList/>
</div>
}
@@ -1,9 +1,8 @@
import {useState, useMemo, useEffect, useContext} from "react"; import {useState, useMemo, useEffect, useContext} from "react";
import styles from "./Calendar.module.css"; import styles from "./Calendar.module.css";
import {formatDateLetterJS} from "../../../../utils/date/formatDateLetter.js"; import {formatDateLetterJS} from "../../../../utils/date/formatDateLetter.js";
import Button from "../../../../components/ui/Button/Button";
import {AuthContext} from "../../../../contexts/Auth/AuthContext.ts"; import {AuthContext} from "../../../../contexts/Auth/AuthContext.ts";
import ExportBtn from "./ExportBtn.tsx"; import ExportBtn from "../../../../components/ExportCalendarBtn/ExportBtn.tsx";
function function
Calendar() { Calendar() {
@@ -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 <Button className={styles.exportbtn}
onClick={() => {
window.open(
`${APP_CONFIG.newCalendarURL}${calendarUrl}`
);
}}
>
Exporter
</Button>
}
@@ -37,4 +37,4 @@ export default function legalNoticesPage() {
</section> </section>
</div> </div>
); );
} }
@@ -1,4 +1,4 @@
import Background from "../../components/Background/Background.jsx"; import Background from "../../components/Background/Background";
import styles from "./PageNotFoundPage.module.css" import styles from "./PageNotFoundPage.module.css"
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import { APP_CONFIG } from "../../config/appConfig.js"; import { APP_CONFIG } from "../../config/appConfig.js";
@@ -12,14 +12,14 @@ export default function PageNotFoundPage() {
<h1 className={styles.title}> Page non trouvée </h1> <h1 className={styles.title}> Page non trouvée </h1>
<div className={`${styles.container} glassCard`}> <div className={`${styles.container} glassCard`}>
<p> <p>
Désolé, la page que vous recherchez nexiste pas ou nest plus disponible. <br/> Désolé, la page que vous recherchez n'existe pas ou n'est plus disponible. <br/>
Vous pouvez : <br/> Vous pouvez : <br/>
- Vérifier lURL pour une éventuelle erreur de saisie. <br/> - Vérifier l'URL pour une éventuelle erreur de saisie. <br/>
- Revenir à laccueil <span onClick={() => navigate("/")} className={styles.redirectSpan}>ici.</span> <br/> - Revenir à l'accueil <span onClick={() => navigate("/")} className={styles.redirectSpan}>ici.</span> <br/>
</p> </p>
<p> Contact : {APP_CONFIG.contactEmail} </p> <p> Contact : {APP_CONFIG.contactEmail} </p>
</div> </div>
</div> </div>
</Background> </Background>
} }
+6 -38
View File
@@ -7,46 +7,15 @@ import SettingsModal from "./components/SettingsModal/SettingsModal";
import {useParams} from "react-router"; import {useParams} from "react-router";
import {useNavigate} from "react-router"; import {useNavigate} from "react-router";
import {useRef} from "react"; import {useRef} from "react";
import getUserById from "../../utils/users/getUserById"; import getUserById from "../../utils/users/getUserById.js";
import ManageMember from "./components/ManageMember/ManageMember.js"; import ManageMember from "./components/ManageMember/ManageMember";
import uploadProfilePhoto from "../../utils/users/uploadProfilePhoto"; import uploadProfilePhoto from "../../utils/users/uploadProfilePhoto";
import deleteProfilePhoto from "../../utils/users/deleteProfilePhoto.js"; import deleteProfilePhoto from "../../utils/users/deleteProfilePhoto";
import type { AuthContextType } from "../../interfaces/AuthInterfaces";
interface TaskType { import type TaskType from "../../interfaces/task.interface";
id: number; import type User from "../../interfaces/user.interface";
name: string;
description: string;
location: string;
start: string;
end: string;
max_participants: number;
events_id: number;
created_at: string;
updated_at: string;
pivot: {
user_id: number;
task_id: number;
};
}
interface User {
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() { function ProfilePage() {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -60,7 +29,6 @@ function ProfilePage() {
useEffect(() => { useEffect(() => {
(async () => { (async () => {
console.log(JSON.stringify(import.meta.env.VITE_API_URL));
if (!isOwnProfile && id) { if (!isOwnProfile && id) {
const res = await getUserById(Number(id)); const res = await getUserById(Number(id));
if (res.status === 404) { if (res.status === 404) {
@@ -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 Modal from "../../../../components/ui/Modal/Modal";
import Button from "../../../../components/ui/Button/Button"; import Button from "../../../../components/ui/Button/Button";
import styles from "./SettingsModal.module.css"; import styles from "./SettingsModal.module.css";
@@ -7,6 +7,11 @@ import {AuthContext} from "../../../../contexts/Auth/AuthContext";
import TextInput from "../../../../components/ui/Input/Input"; import TextInput from "../../../../components/ui/Input/Input";
import updateUser from "../../../../utils/users/updateUser.js"; import updateUser from "../../../../utils/users/updateUser.js";
import deleteUser from "../../../../utils/users/deleteUser.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 { interface TaskType {
id: number; id: number;
@@ -25,7 +30,9 @@ interface User {
phone: string | null; phone: string | null;
created_at: string; created_at: string;
isAdmin: boolean; isAdmin: boolean;
tasks: TaskType[]; tasks: TaskType[],
email_notifications: number,
web_notifications: number;
} }
interface AuthContextType { interface AuthContextType {
@@ -35,6 +42,7 @@ interface AuthContextType {
} }
export default function SettingsModal() { export default function SettingsModal() {
const navigate = useNavigate();
const {user, logout, update} = useContext(AuthContext) as unknown as AuthContextType; const {user, logout, update} = useContext(AuthContext) as unknown as AuthContextType;
const [open, setOpen] = useState<boolean>(false); const [open, setOpen] = useState<boolean>(false);
@@ -44,6 +52,9 @@ export default function SettingsModal() {
const [lastname, setLastName] = useState<string>(user?.lastname ?? ""); const [lastname, setLastName] = useState<string>(user?.lastname ?? "");
const [phone, setPhone] = useState<string>(user?.phone ?? ""); const [phone, setPhone] = useState<string>(user?.phone ?? "");
const [emailNotifications, setEmailNotifications] = useState<number>(user?.email_notifications ?? 0);
const [webNotifications, setWebNotifications] = useState<number>(user?.web_notifications ?? 0);
const handleLogout = (): void => { const handleLogout = (): void => {
logout(); logout();
}; };
@@ -56,6 +67,8 @@ export default function SettingsModal() {
const handleDelete = async (): Promise<void> => { const handleDelete = async (): Promise<void> => {
await deleteUser(); await deleteUser();
logout();
navigate('/');
update(); update();
}; };
@@ -71,6 +84,26 @@ export default function SettingsModal() {
setPhone(e.target.value); 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 ( return (
<> <>
@@ -130,6 +163,34 @@ export default function SettingsModal() {
<ThemeSwitcher/> <ThemeSwitcher/>
</div> </div>
<div className={`glassBorder ${styles.section}`}>
<h3>Notifications</h3>
<div className={styles.test}>
<h4>Notifications par email</h4>
<input
type="checkbox"
checked={emailNotifications === 1}
onChange={handleEmailToggle}
/>
</div>
<div className={styles.test}>
<h4>Notifications sur le site</h4>
<input
type="checkbox"
checked={webNotifications === 1}
onChange={handleWebToggle}
/>
</div>
</div>
<div className={`glassBorder ${styles.section}`}>
<h3>Divers</h3>
<ExportCalendarBtn />
</div>
<div className={`glassBorder ${styles.section}`}> <div className={`glassBorder ${styles.section}`}>
<h3>Sécurité du compte</h3> <h3>Sécurité du compte</h3>
@@ -12,8 +12,8 @@ export default function RGPDPage() {
Le présent site web est un outil de gestion des bénévoles permettant notamment : Le présent site web est un outil de gestion des bénévoles permettant notamment :
</p> </p>
<ul> <ul>
<li>Linscription et la gestion des profils bénévoles</li> <li>L'inscription et la gestion des profils bénévoles</li>
<li>Lorganisation dévénements ou de missions</li> <li>L'organisation d'événements ou de missions</li>
<li>La communication avec les bénévoles</li> <li>La communication avec les bénévoles</li>
<li>Le suivi des participations</li> <li>Le suivi des participations</li>
</ul> </ul>
@@ -33,15 +33,15 @@ export default function RGPDPage() {
<h2 className={styles.h2}>2. Responsable du traitement</h2> <h2 className={styles.h2}>2. Responsable du traitement</h2>
<p> <p>
Le responsable du traitement des données personnelles est Monsieur FRÈRE Robin Le responsable du traitement des données personnelles est Monsieur FRÈRE Robin
<br />Il est responsable de la collecte et de lutilisation des données personnelles conformément au Règlement Général sur la Protection des Données (RGPD UE 2016/679). <br />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).
</p> </p>
</section> </section>
<section className={styles.section}> <section className={styles.section}>
<h2 className={styles.h2}>3. Données personnelles collectées</h2> <h2 className={styles.h2}>3. Données personnelles collectées</h2>
<p>Les données susceptibles dêtre collectées sont notamment :</p> <p>Les données susceptibles d'être collectées sont notamment :</p>
<ul> <ul>
<li>Les données didentification : nom, prénom, adresse email et numéro de téléphone.</li> <li>Les données d'identification : nom, prénom, adresse email et numéro de téléphone.</li>
<li>Les données techniques : adresse IP, données de connexion (logs) et type de navigateur et appareil.</li> <li>Les données techniques : adresse IP, données de connexion (logs) et type de navigateur et appareil.</li>
</ul> </ul>
@@ -63,8 +63,8 @@ export default function RGPDPage() {
<h2 className={styles.h2}>5. Base légale du traitement</h2> <h2 className={styles.h2}>5. Base légale du traitement</h2>
<p>Les traitements reposent sur :</p> <p>Les traitements reposent sur :</p>
<ul> <ul>
<li>Le consentement de lutilisateur</li> <li>Le consentement de l'utilisateur</li>
<li>Lintérêt légitime de lorganisme (gestion des bénévoles)</li> <li>L'intérêt légitime de l'organisme (gestion des bénévoles)</li>
<li>Une obligation légale, le cas échéant</li> <li>Une obligation légale, le cas échéant</li>
</ul> </ul>
</section> </section>
@@ -73,7 +73,7 @@ export default function RGPDPage() {
<h2 className={styles.h2}>6. Durée de conservation</h2> <h2 className={styles.h2}>6. Durée de conservation</h2>
<p>Les données sont conservées :</p> <p>Les données sont conservées :</p>
<ul> <ul>
<li>Pendant la durée de lengagement bénévole</li> <li>Pendant la durée de l'engagement bénévole</li>
<li>Puis archivées ou supprimées au plus tard 3 ans après la dernière activité ou contact</li> <li>Puis archivées ou supprimées au plus tard 3 ans après la dernière activité ou contact</li>
</ul> </ul>
<p>Les logs techniques sont conservés pour une durée maximale de 2 mois.</p> <p>Les logs techniques sont conservés pour une durée maximale de 2 mois.</p>
@@ -83,10 +83,10 @@ export default function RGPDPage() {
<h2 className={styles.h2}>7. Destinataires des données</h2> <h2 className={styles.h2}>7. Destinataires des données</h2>
<p>Les données sont accessibles uniquement :</p> <p>Les données sont accessibles uniquement :</p>
<ul> <ul>
<li>Aux personnes habilitées de lorganisme</li> <li>Aux personnes habilitées de l'organisme</li>
<li>Aux prestataires techniques (hébergement, maintenance), strictement nécessaires</li> <li>Aux prestataires techniques (hébergement, maintenance), strictement nécessaires</li>
</ul> </ul>
<p>Aucune donnée nest vendue ou cédée à des tiers.</p> <p>Aucune donnée n'est vendue ou cédée à des tiers.</p>
</section> </section>
<section className={styles.section}> <section className={styles.section}>
@@ -96,7 +96,7 @@ export default function RGPDPage() {
<li>Siège social : 2 rue Kellermann - 59100 Roubaix - France</li> <li>Siège social : 2 rue Kellermann - 59100 Roubaix - France</li>
<li>Site web : <Link to={"www.ovh.com"}>www.ovh.com</Link></li> <li>Site web : <Link to={"www.ovh.com"}>www.ovh.com</Link></li>
</ul> </ul>
<p>Lhébergeur garantit un niveau de sécurité conforme au RGPD.</p> <p>L'hébergeur garantit un niveau de sécurité conforme au RGPD.</p>
</section> </section>
<section className={styles.section}> <section className={styles.section}>
@@ -113,11 +113,11 @@ export default function RGPDPage() {
<h2 className={styles.h2}>10. Droits des utilisateurs</h2> <h2 className={styles.h2}>10. Droits des utilisateurs</h2>
<p>Conformément au RGPD, vous disposez des droits suivants :</p> <p>Conformément au RGPD, vous disposez des droits suivants :</p>
<ul> <ul>
<li>Droit daccès à vos données</li> <li>Droit d'accès à vos données</li>
<li>Droit de rectification</li> <li>Droit de rectification</li>
<li>Droit à leffacement (droit à loubli)</li> <li>Droit à l'effacement (droit à l'oubli)</li>
<li>Droit à la limitation du traitement</li> <li>Droit à la limitation du traitement</li>
<li>Droit dopposition</li> <li>Droit d'opposition</li>
<li>Droit à la portabilité des données</li> <li>Droit à la portabilité des données</li>
</ul> </ul>
</section> </section>
@@ -126,7 +126,7 @@ export default function RGPDPage() {
<h2 className={styles.h2}>11. Réclamation</h2> <h2 className={styles.h2}>11. Réclamation</h2>
<p> <p>
Si vous estimez que vos droits ne sont pas respectés, vous pouvez introduire une réclamation auprès de la : Si vous estimez que vos droits ne sont pas respectés, vous pouvez introduire une réclamation auprès de la :
<br /><strong>CNIL</strong> Commission Nationale de lInformatique et des Libertés <br /><strong>CNIL</strong> Commission Nationale de l'Informatique et des Libertés
<br />Site : <a href="https://www.cnil.fr" target="_blank" rel="noreferrer">https://www.cnil.fr</a> <br />Site : <a href="https://www.cnil.fr" target="_blank" rel="noreferrer">https://www.cnil.fr</a>
</p> </p>
</section> </section>
@@ -140,4 +140,4 @@ export default function RGPDPage() {
</section> </section>
</div> </div>
); );
} }
@@ -1,5 +1,5 @@
import styles from "./WaitValidationPage.module.css" 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 Button from "../../components/ui/Button/Button";
import { useContext } from "react"; import { useContext } from "react";
import { AuthContext } from "../../contexts/Auth/AuthContext.js"; import { AuthContext } from "../../contexts/Auth/AuthContext.js";
@@ -7,7 +7,7 @@ import { Navigate, useNavigate, Link } from "react-router";
export default function WaitValidationPage() { export default function WaitValidationPage() {
const { logout, user } = useContext(AuthContext); const { logout, user } = useContext(AuthContext)!;
const navigate = useNavigate(); const navigate = useNavigate();
const handleLogout = async () => { const handleLogout = async () => {
@@ -38,4 +38,4 @@ export default function WaitValidationPage() {
</div> </div>
</div> </div>
</Background> </Background>
} }
+2 -2
View File
@@ -1,6 +1,6 @@
import { createBrowserRouter } from "react-router"; import { createBrowserRouter } from "react-router";
import Layout from "./layout.jsx"; 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 LoginPage from "./pages/Login/LoginPage";
import RegisterPage from "./pages/Register/RegisterPage.jsx"; import RegisterPage from "./pages/Register/RegisterPage.jsx";
import VolunteersPage from "./pages/Volunteers/VolunteersPage"; import VolunteersPage from "./pages/Volunteers/VolunteersPage";
@@ -10,7 +10,7 @@ import AdminPage from "./pages/Admin/AdminPage.jsx";
import WaitValidationPage from "./pages/WaitValidation/WaitValidationPage.jsx"; import WaitValidationPage from "./pages/WaitValidation/WaitValidationPage.jsx";
import PageNotFoundPage from "./pages/PageNotFound/PageNotFoundPage.jsx"; import PageNotFoundPage from "./pages/PageNotFound/PageNotFoundPage.jsx";
import eventDetail from "./pages/EventDetail/EventDetailPage.tsx"; 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"; import RGPDPage from "./pages/RGPD/RGPDPage.jsx";
const router = createBrowserRouter([ const router = createBrowserRouter([
+3 -1
View File
@@ -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 date = d instanceof Date ? d : new Date(d);
const jj = String(date.getUTCDate()).padStart(2, "0"); const jj = String(date.getUTCDate()).padStart(2, "0");
+1 -1
View File
@@ -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"; if (!d) return "Heure inconnue";
const date = d instanceof Date ? d : new Date(d); const date = d instanceof Date ? d : new Date(d);
@@ -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;
};
@@ -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;
};
@@ -1,46 +1,37 @@
export const userNotificationsListener = (echo, userData, setNotifications, setunreadNotification) => { export const userNotificationsListener = (echo, userData, setNotifications, setUnreadNotification) => {
const channelName = `user.${userData.id}`; 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) return echo.private(channelName)
.listen('.event.participation.cancelled', (event) => { .listen('.event.participation.cancelled', (event) => {
const newNotification = { handleNotification(event.notificationId,`L'événement ${event.event.name} a été supprimé. Vous n'y participez donc plus.`);
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) => { .listen(`.task.participation.cancelled`, (event) => {
const newNotification = { 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.`);
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) => { .listen('.volunteer.assigned.to.task', (event) => {
const newNotification = { handleNotification(event.notificationId,`Vous avez été assigné à la tâche ${event.task.name} de l'événement ${event.event.name}.`);
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) => { .listen('.volunteer.unassigned.to.task', (event) => {
const newNotification = { handleNotification(event.notificationId,`Vous avez été désassigné de la tâche ${event.task.name} de l'événement ${event.event.name}.`);
id: Date.now(), })
content: `Vous avez été désassigné de la tâche ${event.task.name} de l'événement ${event.event.name}`, .listen('.volunteer.role.updated', (event) => {
created_at: Date.now(), handleNotification(event.notificationId, `Votre rôle a changé pour : ${event.role} `);
pivot: { unread: 1 } })
}; .listen('.task.date.updated', (event) => {
setNotifications(prev => [newNotification, ...prev]); 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}.`);
setunreadNotification(true);
}); });
}; };
@@ -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 }
);
}
+13
View File
@@ -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 }
);
}