From 2f2c2b730507a3cbdc1b1221ece1f1e22ef3a9d4 Mon Sep 17 00:00:00 2001 From: Guillaume Cauchois Date: Fri, 16 Aug 2024 11:34:56 +0200 Subject: [PATCH 1/7] [EN-6995] Dropdown - Add new version without UIKit --- src/components/utils/Dropdown.tsx | 4 ++ .../utils/Dropdown/Dropdown.context.ts | 7 +++ .../utils/Dropdown/Dropdown.styles.ts | 41 ++++++++++++++++ src/components/utils/Dropdown/Dropdown.tsx | 47 +++++++++++++++++++ .../utils/Dropdown/DropdownItem.tsx | 23 +++++++++ .../utils/Dropdown/DropdownItemSeparator.tsx | 6 +++ .../utils/Dropdown/DropdownMenu.tsx | 25 ++++++++++ .../utils/Dropdown/DropdownToggle.tsx | 12 +++++ src/hooks/utils/index.ts | 1 + src/hooks/utils/useClickOutside.ts | 20 ++++++++ 10 files changed, 186 insertions(+) create mode 100644 src/components/utils/Dropdown/Dropdown.context.ts create mode 100644 src/components/utils/Dropdown/Dropdown.styles.ts create mode 100644 src/components/utils/Dropdown/Dropdown.tsx create mode 100644 src/components/utils/Dropdown/DropdownItem.tsx create mode 100644 src/components/utils/Dropdown/DropdownItemSeparator.tsx create mode 100644 src/components/utils/Dropdown/DropdownMenu.tsx create mode 100644 src/components/utils/Dropdown/DropdownToggle.tsx create mode 100644 src/hooks/utils/useClickOutside.ts diff --git a/src/components/utils/Dropdown.tsx b/src/components/utils/Dropdown.tsx index 898aa2f81..b410eac30 100644 --- a/src/components/utils/Dropdown.tsx +++ b/src/components/utils/Dropdown.tsx @@ -9,6 +9,10 @@ interface DropdownProps { boundaryId?: string; } +/** + * Dropdown component (using UIKit) + * @deprecated This component is deprecated. Please use the Dropdown component in the ./Dropdown folder + */ export const Dropdown = ({ id = 'dropdown', children, diff --git a/src/components/utils/Dropdown/Dropdown.context.ts b/src/components/utils/Dropdown/Dropdown.context.ts new file mode 100644 index 000000000..6375bbf66 --- /dev/null +++ b/src/components/utils/Dropdown/Dropdown.context.ts @@ -0,0 +1,7 @@ +import React from 'react'; + +export const DropdownContext = React.createContext({ + isOpen: false, + toggleDropdown: () => {}, + closeDropdown: () => {}, +}); diff --git a/src/components/utils/Dropdown/Dropdown.styles.ts b/src/components/utils/Dropdown/Dropdown.styles.ts new file mode 100644 index 000000000..cc9d9469e --- /dev/null +++ b/src/components/utils/Dropdown/Dropdown.styles.ts @@ -0,0 +1,41 @@ +import styled from 'styled-components'; +import { COLORS } from 'src/constants/styles'; + +export const StyledDropdown = styled.div` + position: relative; + display: inline-block; +`; + +export const StyledDropdownMenu = styled.div` + display: flex; + flex-direction: column; + gap: 15px; + position: absolute; + top: calc(100% + 10px); + background-color: white; + border: 1px solid #ececec; + border-radius: 20px; + box-shadow: 0 4px 4px 0px #0000000d; + padding: 25px; + z-index: 1000; + min-width: 200px; + ${({ openDirection }) => { + return openDirection === 'right' ? 'left: 0;' : ''; + }} + ${({ openDirection }) => { + return openDirection === 'left' ? 'right: 0;' : ''; + }} +`; + +export const StyledDropdownMenuItem = styled.div` + color: #484848; + font-size: 16px; + cursor: pointer; + :hover { + color: ${COLORS.primaryBlue}; + } +`; + +export const StyledDropdownMenuItemSeparator = styled.div` + border-top: 1px solid #ececec; +`; diff --git a/src/components/utils/Dropdown/Dropdown.tsx b/src/components/utils/Dropdown/Dropdown.tsx new file mode 100644 index 000000000..e12b35c1b --- /dev/null +++ b/src/components/utils/Dropdown/Dropdown.tsx @@ -0,0 +1,47 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { DropdownContext } from './Dropdown.context'; +import { StyledDropdown } from './Dropdown.styles'; +import { DropdownItem } from './DropdownItem'; +import { DropdownItemSeparator } from './DropdownItemSeparator'; +import { DropdownMenu } from './DropdownMenu'; +import { DropdownToggle } from './DropdownToggle'; + +type DropdownProps = { + children: React.ReactNode; +}; + +const Dropdown = ({ children }: DropdownProps) => { + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + + const toggleDropdown = () => setIsOpen((prev) => !prev); + const closeDropdown = () => setIsOpen(false); + + const handleClickOutside = (event) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { + closeDropdown(); + } + }; + + useEffect(() => { + document.addEventListener('mousedown', handleClickOutside); + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + {children} + + ); +}; + +Dropdown.Toggle = DropdownToggle; +Dropdown.Menu = DropdownMenu; +Dropdown.ItemSeparator = DropdownItemSeparator; +Dropdown.Item = DropdownItem; + +export { Dropdown }; diff --git a/src/components/utils/Dropdown/DropdownItem.tsx b/src/components/utils/Dropdown/DropdownItem.tsx new file mode 100644 index 000000000..b107cb78a --- /dev/null +++ b/src/components/utils/Dropdown/DropdownItem.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { DropdownContext } from './Dropdown.context'; +import { StyledDropdownMenuItem } from './Dropdown.styles'; + +type DropdownItemProps = { + children: React.ReactNode; + onClick?: () => void; +}; + +export const DropdownItem = ({ children, onClick }: DropdownItemProps) => { + const { closeDropdown } = React.useContext(DropdownContext); + + const handleClick = () => { + if (onClick) onClick(); + closeDropdown(); + }; + + return ( + + {children} + + ); +}; diff --git a/src/components/utils/Dropdown/DropdownItemSeparator.tsx b/src/components/utils/Dropdown/DropdownItemSeparator.tsx new file mode 100644 index 000000000..dab8aa051 --- /dev/null +++ b/src/components/utils/Dropdown/DropdownItemSeparator.tsx @@ -0,0 +1,6 @@ +import React from 'react'; +import { StyledDropdownMenuItemSeparator } from './Dropdown.styles'; + +export const DropdownItemSeparator = () => { + return ; +}; diff --git a/src/components/utils/Dropdown/DropdownMenu.tsx b/src/components/utils/Dropdown/DropdownMenu.tsx new file mode 100644 index 000000000..fce0fadc3 --- /dev/null +++ b/src/components/utils/Dropdown/DropdownMenu.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { DropdownContext } from './Dropdown.context'; +import { StyledDropdownMenu } from './Dropdown.styles'; + +type DropdownMenuProps = { + children: React.ReactNode; + openDirection?: 'left' | 'right'; +}; + +export const DropdownMenu = ({ + children, + openDirection = 'right', +}: DropdownMenuProps) => { + const { isOpen } = React.useContext(DropdownContext); + + return ( + <> + {isOpen && ( + + {children} + + )} + + ); +}; diff --git a/src/components/utils/Dropdown/DropdownToggle.tsx b/src/components/utils/Dropdown/DropdownToggle.tsx new file mode 100644 index 000000000..2d662c6fc --- /dev/null +++ b/src/components/utils/Dropdown/DropdownToggle.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { DropdownContext } from './Dropdown.context'; + +type DropdownToggleProps = { + children: React.ReactNode; +}; + +export const DropdownToggle = ({ children }: DropdownToggleProps) => { + const { toggleDropdown } = React.useContext(DropdownContext); + + return
{children}
; +}; diff --git a/src/hooks/utils/index.ts b/src/hooks/utils/index.ts index 4e966ea2d..abae0f686 100644 --- a/src/hooks/utils/index.ts +++ b/src/hooks/utils/index.ts @@ -4,3 +4,4 @@ export * from 'src/hooks/utils/usePlatforms'; export * from 'src/hooks/utils/useSSRDataContext'; export * from 'src/hooks/utils/useOnScroll'; export * from 'src/hooks/utils/useResetForm'; +export * from 'src/hooks/utils/useClickOutside'; diff --git a/src/hooks/utils/useClickOutside.ts b/src/hooks/utils/useClickOutside.ts new file mode 100644 index 000000000..5870b9baf --- /dev/null +++ b/src/hooks/utils/useClickOutside.ts @@ -0,0 +1,20 @@ +import { useEffect } from 'react'; + +export const useClickOutside = ( + ref: React.RefObject, + handler: () => void +) => { + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (ref.current && !ref.current.contains(event.target as Node)) { + handler(); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [ref, handler]); +}; From 7910cc2d7039d11bcbda4ca0ec466494fbfbaf49 Mon Sep 17 00:00:00 2001 From: Guillaume Cauchois Date: Fri, 16 Aug 2024 11:48:27 +0200 Subject: [PATCH 2/7] [EN-6995] Api - postProfileUserAbuse --- src/api/api.ts | 8 ++++++++ src/api/types.ts | 5 +++++ 2 files changed, 13 insertions(+) diff --git a/src/api/api.ts b/src/api/api.ts index 2a0fc363d..46d10ff4e 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -32,6 +32,7 @@ import { UserDto, UserProfile, UserRegistrationDto, + UserReportDto, UserWithUserCandidate, } from './types'; @@ -294,6 +295,13 @@ export class APIHandler { return this.put(`/user/profile/${userId}`, userProfile); } + postProfileUserAbuse( + userId: string, + userReportDto: UserReportDto + ): Promise { + return this.post(`/user/profile/${userId}/abuse`, userReportDto); + } + // delete deleteUser(userId: string): Promise { diff --git a/src/api/types.ts b/src/api/types.ts index 98aa6d4c1..226a133e1 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -113,6 +113,11 @@ export type UserProfile = { hasExternalCv: boolean; }; +export type UserReportDto = { + reason: string; + comment: string; +}; + export interface WhatsappJoinUrl { name: string; qrCodePath: string; From bc774a7bbd21a103160b113e125c79195f3b9ac5 Mon Sep 17 00:00:00 2001 From: Guillaume Cauchois Date: Fri, 16 Aug 2024 11:56:35 +0200 Subject: [PATCH 3/7] [EN-6995] Add - ProfileReportUserModal --- .../ProfileReportUserModal.tsx | 48 +++++++++++++++++++ .../profile/useOnReportUserFormSubmit.ts | 46 ++++++++++++++++++ .../forms/schemas/formReportUser.ts | 26 ++++++++++ src/constants/users.ts | 8 ++++ 4 files changed, 128 insertions(+) create mode 100644 src/components/backoffice/profile/ProfileReportUserModal/ProfileReportUserModal.tsx create mode 100644 src/components/backoffice/profile/useOnReportUserFormSubmit.ts create mode 100644 src/components/forms/schemas/formReportUser.ts diff --git a/src/components/backoffice/profile/ProfileReportUserModal/ProfileReportUserModal.tsx b/src/components/backoffice/profile/ProfileReportUserModal/ProfileReportUserModal.tsx new file mode 100644 index 000000000..08c9b98b4 --- /dev/null +++ b/src/components/backoffice/profile/ProfileReportUserModal/ProfileReportUserModal.tsx @@ -0,0 +1,48 @@ +import React, { useCallback, useMemo } from 'react'; +import { useOnReportUserFormSubmit } from '../useOnReportUserFormSubmit'; +import { Api } from 'src/api'; +import { UserReportDto } from 'src/api/types'; +import { formReportUser } from 'src/components/forms/schemas/formReportUser'; +import { ModalEdit } from 'src/components/modals/Modal/ModalGeneric/ModalEdit'; +import { Actions } from 'src/constants/utils'; + +type ProfileReportUserModalProps = { + userId: string; +}; + +export const ProfileReportUserModal = ({ + userId, +}: ProfileReportUserModalProps) => { + const { onSubmit } = useOnReportUserFormSubmit( + async (userReportDto: UserReportDto) => { + return Api.postProfileUserAbuse(userId, userReportDto); + }, + Actions.CREATE + ); + + const handleReportUserSubmit = useCallback( + async (fields, closeModal) => { + await onSubmit(fields, closeModal); + }, + [onSubmit] + ); + + const updateUserModalProps = useMemo(() => { + return { + formId: 'id', + formSchema: formReportUser, + title: 'Signaler un utilisateur', + description: + 'Vous pouvez signaler un utilisateur si vous pensez qu’il ne respecte pas les règles de la plateforme.', + submitText: 'Envoyer', + cancelText: 'Annuler', + onSubmit: handleReportUserSubmit, + defaultValues: { + reason: '', + comment: '', + }, + }; + }, [handleReportUserSubmit]); + + return ; +}; diff --git a/src/components/backoffice/profile/useOnReportUserFormSubmit.ts b/src/components/backoffice/profile/useOnReportUserFormSubmit.ts new file mode 100644 index 000000000..b4dc1e22f --- /dev/null +++ b/src/components/backoffice/profile/useOnReportUserFormSubmit.ts @@ -0,0 +1,46 @@ +import { AxiosResponse } from 'axios'; +import { useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import { UserReportDto } from 'src/api/types'; +import { ExtractFormSchemaValidation } from 'src/components/forms/FormSchema'; +import { formReportUser } from 'src/components/forms/schemas/formReportUser'; +import { Action, ActionsLabels } from 'src/constants/utils'; +import { notificationsActions } from 'src/use-cases/notifications'; + +export function useOnReportUserFormSubmit( + apiCall: (organization: UserReportDto) => Promise, + action: Action +) { + const dispatch = useDispatch(); + const onSubmit = useCallback( + async ( + fields: ExtractFormSchemaValidation, + closeModal + ) => { + try { + const { data } = await apiCall(fields); + closeModal(); + dispatch( + notificationsActions.addNotification({ + type: 'success', + message: `Le signalement a bien été ${ActionsLabels[action].VERB}e`, + }) + ); + return data; + } catch (error) { + console.error(error); + dispatch( + notificationsActions.addNotification({ + type: 'danger', + message: `Une erreur s'est produite lors de la ${ActionsLabels[action].NAME} du signalement`, + }) + ); + } + }, + [action, apiCall, dispatch] + ); + + return { + onSubmit, + }; +} diff --git a/src/components/forms/schemas/formReportUser.ts b/src/components/forms/schemas/formReportUser.ts new file mode 100644 index 000000000..2f58fd1f1 --- /dev/null +++ b/src/components/forms/schemas/formReportUser.ts @@ -0,0 +1,26 @@ +import { FormSchema } from '../FormSchema'; +import { USER_REPORT_REASONS } from 'src/constants/users'; + +export const formReportUser: FormSchema<{ + reason: string; + comment: string; +}> = { + id: 'form-add-organization', + fields: [ + { + id: 'reason', + name: 'reason', + component: 'select-simple', + title: 'Raison du signalement *', + options: USER_REPORT_REASONS, + isRequired: true, + }, + { + id: 'comment', + name: 'comment', + component: 'text-input', + title: 'Commentaire', + isRequired: false, + }, + ], +}; diff --git a/src/constants/users.ts b/src/constants/users.ts index 7bb00c257..72b7075f6 100644 --- a/src/constants/users.ts +++ b/src/constants/users.ts @@ -98,3 +98,11 @@ export const GENDERS_FILTERS = [ value: GENDERS.OTHER, }, */ ]; + +export const USER_REPORT_REASONS = [ + { value: 'SPAM', label: 'Spam' }, + { value: 'FRAUD', label: 'Arnaque' }, + { value: 'INSULTS', label: 'Propos déplacés' }, + { value: 'IN_DANGER', label: 'Mise en danger' }, + { value: 'OTHER', label: 'Autre' }, +]; From da08b046a37d681c2aba266191752816d6a0a17e Mon Sep 17 00:00:00 2001 From: Guillaume Cauchois Date: Fri, 16 Aug 2024 12:01:17 +0200 Subject: [PATCH 4/7] [EN-6995] [HeaderProfile] Add - UserActionsMenu --- .../HeaderProfile/HeaderProfile.desktop.tsx | 11 +++-- .../HeaderProfile/HeaderProfile.mobile.tsx | 7 ++- .../HeaderProfile/HeaderProfile.styles.tsx | 5 +++ .../utils/UserActions/UserActions.styles.ts | 15 +++++++ .../utils/UserActions/UserActions.tsx | 44 +++++++++++++++++++ .../utils/UserActions/UserActions.types.ts | 4 ++ 6 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 src/components/utils/UserActions/UserActions.styles.ts create mode 100644 src/components/utils/UserActions/UserActions.tsx create mode 100644 src/components/utils/UserActions/UserActions.types.ts diff --git a/src/components/headers/HeaderProfile/HeaderProfile.desktop.tsx b/src/components/headers/HeaderProfile/HeaderProfile.desktop.tsx index 291724f87..4072087e3 100644 --- a/src/components/headers/HeaderProfile/HeaderProfile.desktop.tsx +++ b/src/components/headers/HeaderProfile/HeaderProfile.desktop.tsx @@ -13,9 +13,11 @@ import { AvailabilityTag } from 'src/components/utils/AvailabilityTag/Availabili import { H1, H5 } from 'src/components/utils/Headings'; import { ImageInput } from 'src/components/utils/Inputs'; import { Spinner } from 'src/components/utils/Spinner'; +import { UserActions } from 'src/components/utils/UserActions/UserActions'; import { COLORS } from 'src/constants/styles'; import { USER_ROLES } from 'src/constants/users'; import { + StyledHeaderAvailibilityAndUserActions, StyledHeaderNameAndRole, StyledHeaderProfile, StyledHeaderProfileContent, @@ -158,9 +160,12 @@ export const HeaderProfileDesktop = ({ style="secondary" /> - {shouldShowAllProfile && ( - - )} + + {shouldShowAllProfile && ( + + )} + + {shouldShowAllProfile && ( <> diff --git a/src/components/headers/HeaderProfile/HeaderProfile.mobile.tsx b/src/components/headers/HeaderProfile/HeaderProfile.mobile.tsx index bd28add8e..9fe13a89b 100644 --- a/src/components/headers/HeaderProfile/HeaderProfile.mobile.tsx +++ b/src/components/headers/HeaderProfile/HeaderProfile.mobile.tsx @@ -14,10 +14,12 @@ import { AvailabilityTag } from 'src/components/utils/AvailabilityTag/Availabili import { H2, H6 } from 'src/components/utils/Headings'; import { ImageInput } from 'src/components/utils/Inputs'; import { Spinner } from 'src/components/utils/Spinner'; +import { UserActions } from 'src/components/utils/UserActions/UserActions'; import { COLORS } from 'src/constants/styles'; import { USER_ROLES } from 'src/constants/users'; import { StyledEditPictureIconContainer, + StyledHeaderAvailibilityAndUserActions, StyledHeaderNameAndRoleMobile, StyledHeaderProfile, StyledHeaderProfileContent, @@ -134,7 +136,10 @@ export const HeaderProfileMobile = ({ {shouldShowAllProfile && ( - + + + + { + openModal(); + }, + }, + ]; + + return ( + <> + + + + } + onClick={() => {}} + /> + + + + {actions.map((action) => ( + + {action.name} + + ))} + + + + ); +} diff --git a/src/components/utils/UserActions/UserActions.types.ts b/src/components/utils/UserActions/UserActions.types.ts new file mode 100644 index 000000000..592718a12 --- /dev/null +++ b/src/components/utils/UserActions/UserActions.types.ts @@ -0,0 +1,4 @@ +export interface UserActionsProps { + userId: string; + openDirection?: 'left' | 'right'; +} From 7869019561a9bb0eb36beb76ccbe7c6406c392ce Mon Sep 17 00:00:00 2001 From: Guillaume Cauchois Date: Mon, 26 Aug 2024 17:32:31 +0200 Subject: [PATCH 5/7] [EN-6995] Fix - Make comment mandatory --- src/components/forms/schemas/formReportUser.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/forms/schemas/formReportUser.ts b/src/components/forms/schemas/formReportUser.ts index 2f58fd1f1..1776fd9e9 100644 --- a/src/components/forms/schemas/formReportUser.ts +++ b/src/components/forms/schemas/formReportUser.ts @@ -19,8 +19,8 @@ export const formReportUser: FormSchema<{ id: 'comment', name: 'comment', component: 'text-input', - title: 'Commentaire', - isRequired: false, + title: 'Commentaire *', + isRequired: true, }, ], }; From fca3090981d5f4b7fba036259d1640dcc89eafbe Mon Sep 17 00:00:00 2001 From: Guillaume Cauchois Date: Fri, 30 Aug 2024 16:34:16 +0200 Subject: [PATCH 6/7] [EN-6995] update - api route for profile reporting --- src/api/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/api.ts b/src/api/api.ts index 46d10ff4e..a4ef19d43 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -299,7 +299,7 @@ export class APIHandler { userId: string, userReportDto: UserReportDto ): Promise { - return this.post(`/user/profile/${userId}/abuse`, userReportDto); + return this.post(`/user/profile/${userId}/report`, userReportDto); } // delete From aa2d2e62e5ca728e0244c87f9828f03ddfb8acb0 Mon Sep 17 00:00:00 2001 From: Guillaume Cauchois Date: Fri, 30 Aug 2024 16:59:40 +0200 Subject: [PATCH 7/7] [EN-6995] Fix - interface ProfileReportUserModalProps --- .../profile/ProfileReportUserModal/ProfileReportUserModal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/backoffice/profile/ProfileReportUserModal/ProfileReportUserModal.tsx b/src/components/backoffice/profile/ProfileReportUserModal/ProfileReportUserModal.tsx index 08c9b98b4..951e1ced4 100644 --- a/src/components/backoffice/profile/ProfileReportUserModal/ProfileReportUserModal.tsx +++ b/src/components/backoffice/profile/ProfileReportUserModal/ProfileReportUserModal.tsx @@ -6,9 +6,9 @@ import { formReportUser } from 'src/components/forms/schemas/formReportUser'; import { ModalEdit } from 'src/components/modals/Modal/ModalGeneric/ModalEdit'; import { Actions } from 'src/constants/utils'; -type ProfileReportUserModalProps = { +interface ProfileReportUserModalProps { userId: string; -}; +} export const ProfileReportUserModal = ({ userId,