From 3c3c6283f5cbc942ea7e0b1b273a30af3c5bf545 Mon Sep 17 00:00:00 2001 From: PaulEntourage <112417197+PaulEntourage@users.noreply.github.com> Date: Mon, 29 Jan 2024 17:37:13 +0100 Subject: [PATCH 01/21] [EN-6714] feat(LKO2-profile-details): profile details contact form (#194) --- README.md | 5 + assets/icons/avion-papier.svg | 3 + assets/icons/export-script.js | 50 ++++++ assets/icons/icons.ts | 147 ++++++++++++++++++ cypress/e2e/candidat.cy.js | 36 +++++ cypress/e2e/parcours-cv.cy.js | 2 +- cypress/fixtures/public-profile-res.json | 89 +++++++++++ package.json | 3 +- src/api/api.ts | 8 +- src/api/types.ts | 16 +- .../profile/HeaderProfile/HeaderProfile.tsx | 4 +- .../backoffice/profile/Profile.styles.tsx | 1 + src/components/backoffice/profile/Profile.tsx | 52 +++---- .../ProfileContactCard.styles.tsx | 41 +++++ .../ProfileContactCard/ProfileContactCard.tsx | 96 ++++++++++++ .../profile/ProfileContactCard/index.ts | 1 + .../ProfileHelpInformationCard.tsx | 6 +- .../ProfileProfessionalInformationCard.tsx | 5 +- .../profile/usIsProfileContacted.ts | 57 +++++++ .../backoffice/profile/useSelectedProfile.ts | 10 ++ .../schemas/formContactInternalMessage.ts | 28 ++++ .../partials/{index.js => index.ts} | 0 src/components/utils/Card/Card.styles.tsx | 4 +- .../utils/Card/ProfileCard.styles.ts | 2 +- src/components/utils/Card/ProfileCard.tsx | 2 +- .../utils/Colors/Colors.stories.tsx | 88 +++++++++++ src/components/utils/Colors/Colors.styles.tsx | 42 +++++ src/components/utils/Icons/Icons.stories.tsx | 22 +++ src/components/utils/Icons/Icons.styles.ts | 26 ++++ src/components/utils/Icons/Icons.tsx | 26 ++++ src/components/utils/Tag/Tag.styles.tsx | 1 + src/constants/tags.ts | 3 + src/pages/backoffice/profile/[userId].tsx | 14 +- src/use-cases/profiles/profiles.adapters.ts | 6 +- src/use-cases/profiles/profiles.saga.ts | 21 +++ src/use-cases/profiles/profiles.selectors.ts | 6 + src/use-cases/profiles/profiles.slice.ts | 7 + src/utils/Formatting.tsx | 7 + 38 files changed, 893 insertions(+), 44 deletions(-) create mode 100644 assets/icons/avion-papier.svg create mode 100644 assets/icons/export-script.js create mode 100644 assets/icons/icons.ts create mode 100644 cypress/fixtures/public-profile-res.json create mode 100644 src/components/backoffice/profile/ProfileContactCard/ProfileContactCard.styles.tsx create mode 100644 src/components/backoffice/profile/ProfileContactCard/ProfileContactCard.tsx create mode 100644 src/components/backoffice/profile/ProfileContactCard/index.ts create mode 100644 src/components/backoffice/profile/usIsProfileContacted.ts create mode 100644 src/components/forms/schemas/formContactInternalMessage.ts rename src/components/partials/{index.js => index.ts} (100%) create mode 100644 src/components/utils/Colors/Colors.stories.tsx create mode 100644 src/components/utils/Colors/Colors.styles.tsx create mode 100644 src/components/utils/Icons/Icons.stories.tsx create mode 100644 src/components/utils/Icons/Icons.styles.ts create mode 100644 src/components/utils/Icons/Icons.tsx diff --git a/README.md b/README.md index 324d91b82..11519b868 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,11 @@ Ces deux commandes sont lancées par les hooks de commit yarn storybook ``` +Don't forget to import icons into the storybook when adding a new one in "/assets/icons" +``` +yarn add-icons +``` + ### Tests #### Tests Unitaires - Jest diff --git a/assets/icons/avion-papier.svg b/assets/icons/avion-papier.svg new file mode 100644 index 000000000..f75099d0d --- /dev/null +++ b/assets/icons/avion-papier.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/export-script.js b/assets/icons/export-script.js new file mode 100644 index 000000000..e3ccab466 --- /dev/null +++ b/assets/icons/export-script.js @@ -0,0 +1,50 @@ +const fs = require('fs'); +const path = require('path'); + +// Get the current directory +const directoryPath = path.join(__dirname); + +// method to format component Name +const getComponentName = (fileName) => { + return fileName + .split('-') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join('') + .replace('.svg', '') +} + +// Read files in directory +fs.readdir(directoryPath, (err, files) => { + if (err) { + return console.log('Unable to scan directory: ' + err); + } + + // only keep svg files + files = files.filter((file) => { + return file.includes(".svg") + }); + + // get number of icons + const length = files.length; + + // Create a string with the import statements + let fullStrings = files.map((file) => { + return `import ${ + getComponentName(file) + } from './${file}';`; + }); + + // Create a string with all the imports, each on a new line + let fileNames = fullStrings.join('\n'); + + // Add a new line and export statement + fileNames += '\n\nexport {\n' + files.map(f => getComponentName(f)).join(',\n') + '\n};'; + + // Write the string to a new file + fs.writeFile(path.join(directoryPath, 'icons.ts'), fileNames, (err) => { + if (err) { + return console.log('Error writing file: ' + err); + } + console.log(`File list has been written to icons.ts (${length} icones)`); + }); +}); \ No newline at end of file diff --git a/assets/icons/icons.ts b/assets/icons/icons.ts new file mode 100644 index 000000000..2895749ea --- /dev/null +++ b/assets/icons/icons.ts @@ -0,0 +1,147 @@ +import Archive from './archive.svg'; +import AvionPapier from './avion-papier.svg'; +import Bolt from './bolt.svg'; +import Calendar from './calendar.svg'; +import Car from './car.svg'; +import CaretDown from './caret-down.svg'; +import Check from './check.svg'; +import ChevronDown from './chevron-down.svg'; +import ChevronLeft from './chevron-left.svg'; +import ChevronRight from './chevron-right.svg'; +import ChevronUp from './chevron-up.svg'; +import Close from './close.svg'; +import Copy from './copy.svg'; +import CreditCard from './credit-card.svg'; +import Document from './document.svg'; +import DoubleCarres from './double-carres.svg'; +import Download from './download.svg'; +import EditIcon from './editIcon.svg'; +import Email from './email.svg'; +import Entourage from './entourage.svg'; +import EyeClosed from './eye-closed.svg'; +import EyeHidden from './eye-hidden.svg'; +import EyeOpened from './eye-opened.svg'; +import EyeVisible from './eye-visible.svg'; +import Facebook from './facebook.svg'; +import FilterEmpty from './filter-empty.svg'; +import Filter from './filter.svg'; +import Gender from './gender.svg'; +import HeartEmpty from './heart-empty.svg'; +import Heart from './heart.svg'; +import History from './history.svg'; +import Home from './home.svg'; +import IlluCV from './illu-CV.svg'; +import IlluBulleQuestion from './illu-bulle-question.svg'; +import IlluCoeurMainsOuvertes from './illu-coeur-mains-ouvertes.svg'; +import IlluConversation from './illu-conversation.svg'; +import IlluMalette from './illu-malette.svg'; +import IlluPoigneeDeMain from './illu-poignee-de-main.svg'; +import IlluReseauxSociaux from './illu-reseaux-sociaux.svg'; +import Info from './info.svg'; +import Instagram from './instagram.svg'; +import Language from './language.svg'; +import Link from './link.svg'; +import LinkedIn from './linked-in.svg'; +import List from './list.svg'; +import Location from './location.svg'; +import LogOut from './log-out.svg'; +import LogoEntourage from './logo-entourage.svg'; +import Menu from './menu.svg'; +import More from './more.svg'; +import OrienterCarteSolidaire from './orienter-carte-solidaire.svg'; +import OrienterSablier from './orienter-sablier.svg'; +import Pencil from './pencil.svg'; +import Phone from './phone.svg'; +import PictoCreationOpportunite from './picto-creation-opportunite.svg'; +import PictoFaciliterIntegration from './picto-faciliter-integration.svg'; +import PictoRechercheCv from './picto-recherche-cv.svg'; +import PlusFilled from './plus-filled.svg'; +import Plus from './plus.svg'; +import Question from './question.svg'; +import QuoteLeft from './quote-left.svg'; +import QuoteRight from './quote-right.svg'; +import Search from './search.svg'; +import Settings from './settings.svg'; +import Share from './share.svg'; +import Star from './star.svg'; +import Trash from './trash.svg'; +import Twitter from './twitter.svg'; +import UserEmpty from './user-empty.svg'; +import User from './user.svg'; +import Whatsapp from './whatsapp.svg'; +import Youtube from './youtube.svg'; + +export { +Archive, +AvionPapier, +Bolt, +Calendar, +Car, +CaretDown, +Check, +ChevronDown, +ChevronLeft, +ChevronRight, +ChevronUp, +Close, +Copy, +CreditCard, +Document, +DoubleCarres, +Download, +EditIcon, +Email, +Entourage, +EyeClosed, +EyeHidden, +EyeOpened, +EyeVisible, +Facebook, +FilterEmpty, +Filter, +Gender, +HeartEmpty, +Heart, +History, +Home, +IlluCV, +IlluBulleQuestion, +IlluCoeurMainsOuvertes, +IlluConversation, +IlluMalette, +IlluPoigneeDeMain, +IlluReseauxSociaux, +Info, +Instagram, +Language, +Link, +LinkedIn, +List, +Location, +LogOut, +LogoEntourage, +Menu, +More, +OrienterCarteSolidaire, +OrienterSablier, +Pencil, +Phone, +PictoCreationOpportunite, +PictoFaciliterIntegration, +PictoRechercheCv, +PlusFilled, +Plus, +Question, +QuoteLeft, +QuoteRight, +Search, +Settings, +Share, +Star, +Trash, +Twitter, +UserEmpty, +User, +Whatsapp, +Youtube +}; \ No newline at end of file diff --git a/cypress/e2e/candidat.cy.js b/cypress/e2e/candidat.cy.js index 4c9439b77..0b28ee4f2 100644 --- a/cypress/e2e/candidat.cy.js +++ b/cypress/e2e/candidat.cy.js @@ -85,7 +85,43 @@ describe('Candidat', () => { cy.intercept('PUT', '/user/changePwd', {}).as('changePwd'); + cy.intercept('GET', '/user/profile/*', { + fixture: 'public-profile-res', + }).as('getUserProfile'); + + cy.intercept('/message/internal', {}).as('postInternalMessage'); + }); + + it('should open a user\'s public profile and contact him', () => { + cy.fixture('public-profile-res').then((userProfile) => { + cy.visit(`/backoffice/profile/${userProfile.id}`, { + onBeforeLoad: function async(window) { + window.localStorage.setItem('access-token', '1234'); + window.localStorage.setItem('release-version', 'v100'); + }, + }); + cy.url().should('include', userProfile.id); + }) + + cy.get('[data-testid="form-contact-internal-message-subject"]') + .scrollIntoView() + .type('test'); + + cy.get('[data-testid="form-contact-internal-message-message"]') + .scrollIntoView() + .type('test'); + + + cy.get('[data-testid="form-confirm-form-contact-internal-message"]') + .scrollIntoView() + .click(); + + + cy.get('[data-testid="profile-contact-form-confirm"]') + .should('contain', 'Votre message a été envoyé'); + }); + it('should open backoffice public offers', () => { cy.fixture('auth-current-candidat-res').then((user) => { cy.visit(`/backoffice/candidat/${user.id}/offres/public`, { diff --git a/cypress/e2e/parcours-cv.cy.js b/cypress/e2e/parcours-cv.cy.js index f4fb6c4bf..0fdc01a3a 100644 --- a/cypress/e2e/parcours-cv.cy.js +++ b/cypress/e2e/parcours-cv.cy.js @@ -19,7 +19,7 @@ describe('Parcours CV', () => { cy.intercept('POST', '/cv/count', {}).as('postCVCount'); - cy.intercept('POST', '/externalMessage', { + cy.intercept('POST', '/message/external', { fixture: 'post-external-message-res', }).as('postMessage'); diff --git a/cypress/fixtures/public-profile-res.json b/cypress/fixtures/public-profile-res.json new file mode 100644 index 000000000..c2f9613a2 --- /dev/null +++ b/cypress/fixtures/public-profile-res.json @@ -0,0 +1,89 @@ +{ + "id": "c67bcb92-fbd6-471a-8c96-0ecdbf37006d", + "firstName": "Aurélien", + "lastName": "Leprêtre", + "role": "Candidat", + "zone": "LILLE", + "currentJob": null, + "helpOffers": [], + "helpNeeds": [ + { + "id": "ad37f34c-5761-45f7-90a4-22e12e0ec0bf", + "name": "tips" + }, + { + "id": "92c6b301-ae60-4de8-aee6-b305dce20923", + "name": "interview" + }, + { + "id": "6edf899b-83bb-41b0-9dee-9a1981572e0a", + "name": "cv" + }, + { + "id": "d6921395-05f8-4dfe-ac7a-a88871284f2f", + "name": "event" + }, + { + "id": "2bfdf267-66c6-4f3d-82cd-c11e21e60291", + "name": "network" + } + ], + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", + "searchBusinessLines": [ + { + "id": "17a4b630-2354-41b2-8746-7cf856f7fbc8", + "name": "aev", + "order": 0, + "UserProfileSearchBusinessLine": { + "id": "57495e48-95f7-4f50-bb25-2ec994674957", + "UserProfileId": "8e2308b6-fe8e-4e68-b21f-4af8d946a503", + "BusinessLineI": "17a4b630-2354-41b2-8746-7cf856f7fbc8", + "createdAt": "2024-01-17T11:29:19.060Z", + "updatedAt": "2024-01-17T11:29:19.060Z" + } + }, + { + "id": "d9a58442-cbe8-4a7f-af70-932602121e67", + "name": "asp", + "order": 1, + "UserProfileSearchBusinessLine": { + "id": "b41fe3e4-efa4-4a10-843c-00d40e35a11c", + "UserProfileId": "8e2308b6-fe8e-4e68-b21f-4af8d946a503", + "BusinessLineI": "d9a58442-cbe8-4a7f-af70-932602121e67", + "createdAt": "2024-01-17T11:29:19.060Z", + "updatedAt": "2024-01-17T11:29:19.060Z" + } + } + ], + "networkBusinessLines": [], + "searchAmbitions": [ + { + "id": "45d0ddd3-0789-4113-a01a-0eaf681b0be9", + "name": "jardinier", + "prefix": "comme", + "order": 0, + "UserProfileSearchAmbition": { + "id": "18cfbb7c-c0cb-456f-a84e-73ad1ddca686", + "UserProfileId": "8e2308b6-fe8e-4e68-b21f-4af8d946a503", + "AmbitionId": "45d0ddd3-0789-4113-a01a-0eaf681b0be9", + "createdAt": "2024-01-17T11:29:19.071Z", + "updatedAt": "2024-01-17T11:29:19.071Z" + } + }, + { + "id": "7c940665-bb54-4420-9a28-376fe8ca263c", + "name": "Aide", + "prefix": "comme", + "order": 1, + "UserProfileSearchAmbition": { + "id": "9158715e-d615-48ce-b0bc-d94db6bef7a0", + "UserProfileId": "8e2308b6-fe8e-4e68-b21f-4af8d946a503", + "AmbitionId": "7c940665-bb54-4420-9a28-376fe8ca263c", + "createdAt": "2024-01-17T11:29:19.071Z", + "updatedAt": "2024-01-17T11:29:19.071Z" + } + } + ], + "lastSentMessage": "2024-01-24T13:16:50.332Z", + "lastReceivedMessage": "2024-01-24T13:16:50.332Z" +} \ No newline at end of file diff --git a/package.json b/package.json index 4b878193d..ea55eb779 100755 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "build-storybook": "storybook build", "chromatic": "chromatic --exit-zero-on-changes", "cypress:io": "npx cypress run --record --key $(grep CYPRESS_IO_KEY .env | cut -d '=' -f2)", - "cypress:local": "npx cypress open" + "cypress:local": "npx cypress open", + "add-icons": "node ./assets/icons/export-script.js" }, "repository": { "type": "git", diff --git a/src/api/api.ts b/src/api/api.ts index 5c8580a3d..c45875285 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -16,6 +16,7 @@ import { ContactNewsletter, ExternalMessage, ExternalOpportunityDto, + InternalMessage, OpportunityDto, OpportunityJoin, OpportunityUserEvent, @@ -489,7 +490,12 @@ export class APIHandler { /// // ////// // message / /// // ////// + postExternalMessage(params: ExternalMessage): Promise { - return this.post('/externalMessage', params); + return this.post('/message/external', params); + } + + postInternalMessage(params: InternalMessage): Promise { + return this.post('/message/internal', params); } } diff --git a/src/api/types.ts b/src/api/types.ts index 9a1f96ec4..bfcf10a8d 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -26,7 +26,7 @@ export const APIRoutes = { CONTACTS: 'contact', CVS: 'cv', ORGANIZATIONS: 'organization', - EXTERNAL_MESSAGES: 'externalMessage', + MESSAGE: 'message', } as const; export type APIRoute = (typeof APIRoutes)[keyof typeof APIRoutes]; @@ -90,6 +90,8 @@ export type UserProfile = { order: number; prefix: AmbitionsPrefixesType; }[]; + lastSendMessage: string; + lastReceivedMessage: string; }; export type User = { coach: User; @@ -510,6 +512,16 @@ export type ExternalMessage = { type: ExternalMessageContactType; }; +export type InternalMessage = { + addresseeUserId: string; + subject: string; + message: string; + // answered by the DB + senderUserId?: string; + createdAt?: string; + id?: string; +}; + export type PublicProfile = { id: string; firstName: string; @@ -533,4 +545,6 @@ export type PublicProfile = { order: number; prefix: AmbitionsPrefixesType; }[]; + lastSentMessage: string; + lastReceivedMessage: string; }; diff --git a/src/components/backoffice/profile/HeaderProfile/HeaderProfile.tsx b/src/components/backoffice/profile/HeaderProfile/HeaderProfile.tsx index dcdfeec8e..15baa341c 100644 --- a/src/components/backoffice/profile/HeaderProfile/HeaderProfile.tsx +++ b/src/components/backoffice/profile/HeaderProfile/HeaderProfile.tsx @@ -7,7 +7,7 @@ import { StyledHeaderProfileTextContainer, StyledMobileHeaderProfileTitlesContainer, } from '../../Backoffice.styles'; -import { useSelectedProfile } from '../useSelectedProfile'; +import { useSelectSelectedProfile } from '../useSelectedProfile'; import { ImgProfile, Section } from 'src/components/utils'; import { H1, H2, H5, H6 } from 'src/components/utils/Headings'; import { useIsDesktop } from 'src/hooks/utils'; @@ -16,7 +16,7 @@ import { StyledHeaderProfileDescriptionParagraphe } from './HeaderProfile.styles export const HeaderProfile = () => { const isDesktop = useIsDesktop(); const size = isDesktop ? 146 : 64; - const { selectedProfile: profile } = useSelectedProfile(); + const profile = useSelectSelectedProfile(); if (!profile) return null; return ( diff --git a/src/components/backoffice/profile/Profile.styles.tsx b/src/components/backoffice/profile/Profile.styles.tsx index 9777d10da..161609053 100644 --- a/src/components/backoffice/profile/Profile.styles.tsx +++ b/src/components/backoffice/profile/Profile.styles.tsx @@ -19,6 +19,7 @@ export const StyledProfileRightColumn = styled.div` gap: 40px; min-width: 400px; &.mobile { + min-width: unset; width: 100%; } `; diff --git a/src/components/backoffice/profile/Profile.tsx b/src/components/backoffice/profile/Profile.tsx index e0b2f0bbd..1d309cf56 100644 --- a/src/components/backoffice/profile/Profile.tsx +++ b/src/components/backoffice/profile/Profile.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { StyledProfileGrid, StyledProfileLayout } from '../Backoffice.styles'; import { LayoutBackOffice } from '../LayoutBackOffice'; -import { LoadingScreen } from '../LoadingScreen'; import { Section } from 'src/components/utils'; import { useIsDesktop } from 'src/hooks/utils'; import { HeaderProfile } from './HeaderProfile'; @@ -9,34 +8,35 @@ import { StyledProfileLeftColumn, StyledProfileRightColumn, } from './Profile.styles'; +import { ProfileContactCard } from './ProfileContactCard'; import { ProfileHelpInformationCard } from './ProfileHelpInformationCard'; import { ProfileProfessionalInformationCard } from './ProfileProfessionalInformationCard'; -import { useSelectedProfile } from './useSelectedProfile'; +import { useSelectSelectedProfile } from './useSelectedProfile'; export const Profile = () => { - const { selectedProfile } = useSelectedProfile(); + const selectedProfile = useSelectSelectedProfile(); const isDesktop = useIsDesktop(); - if (selectedProfile) { - return ( - - - -
- - - - - - - {/*
*/} - - -
-
-
- ); - } - return ; + + return ( + + + +
+ + + + + + + + + +
+
+
+ ); }; diff --git a/src/components/backoffice/profile/ProfileContactCard/ProfileContactCard.styles.tsx b/src/components/backoffice/profile/ProfileContactCard/ProfileContactCard.styles.tsx new file mode 100644 index 000000000..fb3464520 --- /dev/null +++ b/src/components/backoffice/profile/ProfileContactCard/ProfileContactCard.styles.tsx @@ -0,0 +1,41 @@ +import styled from 'styled-components'; +import { COLORS } from 'src/constants/styles'; + +export const StyledProfileContactForm = styled.div` + input, + textarea { + border: 1px solid #d8d8d8; + border-radius: 10px; + margin-top: 15px; + min-height: 50px; + padding: 15px; + } +`; + +export const StyledConfirmCheck = styled.div` + height: 16px; + width: 16px; + background-color: ${COLORS.yesGreen}; + border-radius: 50%; + display: inline-flex; + flex-direction: row; + align-items: center; + justify-content: center; + margin-right: 15px; + svg { + height: 10px; + width: 10px; + color: white; + } +`; + +export const StyledContactMessage = styled.div` + background-color: ${COLORS.hoverOrange}; + border-radius: 10px; + padding: 10px; + margin-bottom: 20px; + font-size: 13px; + svg { + margin-right: 10px; + } +`; diff --git a/src/components/backoffice/profile/ProfileContactCard/ProfileContactCard.tsx b/src/components/backoffice/profile/ProfileContactCard/ProfileContactCard.tsx new file mode 100644 index 000000000..adfc9da85 --- /dev/null +++ b/src/components/backoffice/profile/ProfileContactCard/ProfileContactCard.tsx @@ -0,0 +1,96 @@ +import React, { useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import UIkit from 'uikit'; +import { AvionPapier, Check } from 'assets/icons/icons'; +import { useIsProfileContacted } from '../usIsProfileContacted'; +import { useSelectSelectedProfile } from '../useSelectedProfile'; +import { FormWithValidation } from 'src/components/forms/FormWithValidation'; +import { formContactInternalMessage } from 'src/components/forms/schemas/formContactInternalMessage'; +import { Card } from 'src/components/utils'; +import { ReduxRequestEvents } from 'src/constants'; +import { GA_TAGS } from 'src/constants/tags'; +import { usePrevious } from 'src/hooks/utils'; +import { gaEvent } from 'src/lib/gtag'; +import { + postInternalMessageSelectors, + profilesActions, +} from 'src/use-cases/profiles'; +import { + StyledConfirmCheck, + StyledContactMessage, + StyledProfileContactForm, +} from './ProfileContactCard.styles'; + +export const ProfileContactCard = () => { + const selectedProfile = useSelectSelectedProfile(); + const [isFormSent, setIsFormSent] = useState(false); + const [loadingSending, setLoadingSending] = useState(false); + const dispatch = useDispatch(); + const postInternalMessageStatus = useSelector( + postInternalMessageSelectors.selectPostInternalMessageStatus + ); + const prevPostInternalMessageStatus = usePrevious(postInternalMessageStatus); + + useEffect(() => { + if (prevPostInternalMessageStatus === ReduxRequestEvents.REQUESTED) { + if (postInternalMessageStatus === ReduxRequestEvents.SUCCEEDED) { + UIkit.notification('Le message a bien été envoyé', 'success'); + setIsFormSent(true); + setLoadingSending(false); + } else if (postInternalMessageStatus === ReduxRequestEvents.FAILED) { + UIkit.notification('Une erreur est survenue', 'danger'); + setLoadingSending(false); + } + dispatch(profilesActions.postInternalMessageReset()); + } + }, [postInternalMessageStatus, prevPostInternalMessageStatus, dispatch]); + + const contactMessage = `Vous avez déjà contacté ${selectedProfile.firstName}`; + const contactedMessage = `Vous avez déjà été contacté par ${selectedProfile.firstName}`; + + const { existingContactMessage } = useIsProfileContacted( + selectedProfile, + contactMessage, + contactedMessage + ); + + return ( + + + {isFormSent ? ( +
+ + + + Votre message a été envoyé +
+ ) : ( + <> + {existingContactMessage && ( + + + {existingContactMessage} + + )} + { + gaEvent(GA_TAGS.PROFILE_DETAILS_CONTACT_SEND_CLIC); + dispatch( + profilesActions.postInternalMessageRequested({ + ...values, + addresseeUserId: selectedProfile?.id, + }) + ); + }} + noCompulsory + /> + + )} +
+
+ ); +}; diff --git a/src/components/backoffice/profile/ProfileContactCard/index.ts b/src/components/backoffice/profile/ProfileContactCard/index.ts new file mode 100644 index 000000000..323c5d9a6 --- /dev/null +++ b/src/components/backoffice/profile/ProfileContactCard/index.ts @@ -0,0 +1 @@ +export * from './ProfileContactCard'; diff --git a/src/components/backoffice/profile/ProfileHelpInformationCard/ProfileHelpInformationCard.tsx b/src/components/backoffice/profile/ProfileHelpInformationCard/ProfileHelpInformationCard.tsx index 05b3de61b..3ba918f77 100644 --- a/src/components/backoffice/profile/ProfileHelpInformationCard/ProfileHelpInformationCard.tsx +++ b/src/components/backoffice/profile/ProfileHelpInformationCard/ProfileHelpInformationCard.tsx @@ -1,7 +1,7 @@ import React from 'react'; import PlaceholderIllu from 'assets/icons/illu-coeur-mains-ouvertes.svg'; import { ProfilePlaceHolder } from '../ProfilePlaceholder'; -import { useSelectedProfile } from '../useSelectedProfile'; +import { useSelectSelectedProfile } from '../useSelectedProfile'; import { useHelpField } from 'src/components/backoffice/parametres/useUpdateProfile'; import { Card } from 'src/components/utils'; import { CANDIDATE_USER_ROLES } from 'src/constants/users'; @@ -9,10 +9,10 @@ import { isRoleIncluded } from 'src/utils'; import { ProfileHelpList } from './ProfileHelpList'; export const ProfileHelpInformationCard = () => { - const { selectedProfile } = useSelectedProfile(); + const selectedProfile = useSelectSelectedProfile(); const helpField = useHelpField(selectedProfile?.role); - if (!selectedProfile || !helpField) return null; + if (!helpField) return null; return ( { - const { selectedProfile } = useSelectedProfile(); + const selectedProfile = useSelectSelectedProfile(); const [hasData, setHasData] = useState(false); useEffect(() => { @@ -32,7 +32,6 @@ export const ProfileProfessionalInformationCard = () => { } }, [selectedProfile]); - if (!selectedProfile) return null; return ( {!hasData ? ( diff --git a/src/components/backoffice/profile/usIsProfileContacted.ts b/src/components/backoffice/profile/usIsProfileContacted.ts new file mode 100644 index 000000000..064828307 --- /dev/null +++ b/src/components/backoffice/profile/usIsProfileContacted.ts @@ -0,0 +1,57 @@ +import moment from 'moment'; +import { useEffect, useState } from 'react'; +import { PublicProfile } from 'src/api/types'; + +export const useIsProfileContacted = ( + selectedProfile: PublicProfile, + contactMessage?: string, + contactedMessage?: string +) => { + const [existingContactMessage, setExistingcontactMessage] = useState< + typeof contactMessage | typeof contactedMessage | null + >(null); + const [isContacted, setIsContacted] = useState(); + + useEffect(() => { + const { lastSentMessage, lastReceivedMessage } = selectedProfile; + + // Check if both timestamps are null + if (!lastSentMessage && !lastReceivedMessage) { + setExistingcontactMessage(null); + setIsContacted(false); + } + + // Check if at least one timestamp is defined + if (lastSentMessage || !lastReceivedMessage) { + setIsContacted(true); + } + + // if we have contact messages to return + if (!!lastSentMessage && !!lastReceivedMessage) { + // Check if only lastSentMessage is defined + if (lastSentMessage && !lastReceivedMessage) { + setExistingcontactMessage(contactMessage); + } + + // Check if only lastReceivedMessage is defined + if (!lastSentMessage && lastReceivedMessage) { + setExistingcontactMessage(contactedMessage); + } + + // Compare timestamps when both are defined + const a = moment(lastSentMessage); + const b = moment(lastReceivedMessage); + + if (a.isAfter(b)) { + setExistingcontactMessage(contactedMessage); + } else { + setExistingcontactMessage(contactMessage); + } + } + }, [selectedProfile, contactMessage, contactedMessage]); + + return { + isContacted, + existingContactMessage, + }; +}; diff --git a/src/components/backoffice/profile/useSelectedProfile.ts b/src/components/backoffice/profile/useSelectedProfile.ts index c837e0f97..a9c53391f 100644 --- a/src/components/backoffice/profile/useSelectedProfile.ts +++ b/src/components/backoffice/profile/useSelectedProfile.ts @@ -47,3 +47,13 @@ export function useSelectedProfile() { selectedProfile, }; } + +export function useSelectSelectedProfile() { + const selectedProfile = useSelector(selectSelectedProfile); + + if (!selectedProfile) { + throw new Error('No selected profile'); + } + + return selectedProfile; +} diff --git a/src/components/forms/schemas/formContactInternalMessage.ts b/src/components/forms/schemas/formContactInternalMessage.ts new file mode 100644 index 000000000..da54db4a7 --- /dev/null +++ b/src/components/forms/schemas/formContactInternalMessage.ts @@ -0,0 +1,28 @@ +import { FormSchema } from '../FormSchema'; + +export const formContactInternalMessage: FormSchema<{ + subject: string; + message: string; +}> = { + id: 'form-contact-internal-message', + fields: [ + { + id: 'subject', + name: 'subject', + component: 'text-input', + title: 'Objet du message :', + isRequired: true, + showLabel: true, + placeholder: ' ', + }, + { + id: 'message', + name: 'message', + component: 'textarea', + title: 'Votre message :', + isRequired: true, + showLabel: true, + placeholder: ' ', + }, + ], +}; diff --git a/src/components/partials/index.js b/src/components/partials/index.ts similarity index 100% rename from src/components/partials/index.js rename to src/components/partials/index.ts diff --git a/src/components/utils/Card/Card.styles.tsx b/src/components/utils/Card/Card.styles.tsx index f820900e6..835c1f110 100644 --- a/src/components/utils/Card/Card.styles.tsx +++ b/src/components/utils/Card/Card.styles.tsx @@ -36,8 +36,8 @@ export const StyledCardContent = styled.div` export const StyledCardTitleContainer = styled.div` border-bottom: ${COLORS.hoverOrange} solid 1px; - padding-right: 50px; - padding-left: 25px; + margin-right: 25px; + margin-left: 25px; padding-bottom: 25px; margin-bottom: 0px; diff --git a/src/components/utils/Card/ProfileCard.styles.ts b/src/components/utils/Card/ProfileCard.styles.ts index 857b09619..a9dcb0c9d 100644 --- a/src/components/utils/Card/ProfileCard.styles.ts +++ b/src/components/utils/Card/ProfileCard.styles.ts @@ -16,7 +16,7 @@ export const StyledProfileCardPictureContainer = styled.div` `; export const StyledProfileCardPicture = styled.div` - border-radius: 20px; + border-radius: 10px; overflow: hidden; position: relative; height: 250px; diff --git a/src/components/utils/Card/ProfileCard.tsx b/src/components/utils/Card/ProfileCard.tsx index 5718bc1c8..51ed7dab9 100644 --- a/src/components/utils/Card/ProfileCard.tsx +++ b/src/components/utils/Card/ProfileCard.tsx @@ -123,7 +123,7 @@ export function ProfileCard({ onClick={() => { gaEvent(GA_TAGS.PAGE_ANNUAIRE_CARTE_CLIC); push({ - pathname: '/backoffice/profile/[userId]', + pathname: `/backoffice/profile/[userId]`, query: { userId }, }); }} diff --git a/src/components/utils/Colors/Colors.stories.tsx b/src/components/utils/Colors/Colors.stories.tsx new file mode 100644 index 000000000..d38ba3bd0 --- /dev/null +++ b/src/components/utils/Colors/Colors.stories.tsx @@ -0,0 +1,88 @@ +import React from 'react'; + +import { COLORS } from 'src/constants/styles'; +import { + StyledColor, + StyledColorContainer, + StyledColorsContainer, + StyledStatusColor, +} from './Colors.styles'; + +const meta = { + title: 'Colors', + decorators: [ + (Story) => { + return ; + }, + ], +}; + +const DarkColors = [ + 'darkGrayFont', + 'darkGray', + 'black', + 'noRed', + 'darkOrange', + '#A0A0A0', +]; + +const ColorsTemplate = () => { + return ( +
+

Colors: click to copy

+

Main Colors

+ + {Object.keys(COLORS).map((colorKey) => { + if (colorKey === 'cvStatus') return null; + const isDarkColor = DarkColors.includes(colorKey); + return ( + { + navigator.clipboard.writeText(COLORS[colorKey]); + alert(`Copied: ${COLORS[colorKey]}`); // eslint-disable-line no-alert + }} + > + + {COLORS[colorKey]} + +
{colorKey}
+
+ ); + })} +
+

Status Colors (with borders)

+ + {Object.keys(COLORS.cvStatus).map((colorKey) => { + const isDarkColor = DarkColors.includes( + COLORS.cvStatus[colorKey].background + ); + return ( + { + navigator.clipboard.writeText( + COLORS.cvStatus[colorKey].background + ); + alert(`Copied: ${COLORS.cvStatus[colorKey].background}`); // eslint-disable-line no-alert + }} + > + + {COLORS.cvStatus[colorKey].background} + +
{colorKey}
+
+ ); + })} +
+
+ ); +}; + +export const Colors = { + render: ColorsTemplate, +}; + +export default meta; diff --git a/src/components/utils/Colors/Colors.styles.tsx b/src/components/utils/Colors/Colors.styles.tsx new file mode 100644 index 000000000..868f6420a --- /dev/null +++ b/src/components/utils/Colors/Colors.styles.tsx @@ -0,0 +1,42 @@ +import styled from 'styled-components'; +import { COLORS } from 'src/constants/styles'; + +export const StyledColorsContainer = styled.div` + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 40px; + justify-content: start; +`; + +export const StyledColorContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + padding-bottom: 10px; + &:hover { + cursor: pointer; + box-shadow: 0px 8px 8px 0px ${COLORS.gray}; + } +`; + +export const StyledColor = styled.div` + background-color: ${(props) => { + return props.color; + }}; + color: ${(props) => { + return props.isDarkColor ? COLORS.white : COLORS.black; + }}; + height: 100px; + width: 150px; + display: flex; + justify-content: center; + align-items: center; +`; + +export const StyledStatusColor = styled(StyledColor)` + border: 1px solid + ${(props) => { + return props.borderColor; + }}; +`; diff --git a/src/components/utils/Icons/Icons.stories.tsx b/src/components/utils/Icons/Icons.stories.tsx new file mode 100644 index 000000000..335f5b012 --- /dev/null +++ b/src/components/utils/Icons/Icons.stories.tsx @@ -0,0 +1,22 @@ +import React from 'react'; + +import { Icons } from './Icons'; + +const meta = { + title: 'Icons SVG', + decorators: [ + (Story) => { + return ; + }, + ], +}; + +const IconsTemplate = () => { + return ; +}; + +export const IconsSVG = { + render: IconsTemplate, +}; + +export default meta; diff --git a/src/components/utils/Icons/Icons.styles.ts b/src/components/utils/Icons/Icons.styles.ts new file mode 100644 index 000000000..f491997f0 --- /dev/null +++ b/src/components/utils/Icons/Icons.styles.ts @@ -0,0 +1,26 @@ +import styled from 'styled-components'; + +export const StyledIconsContainer = styled.div` + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 40px; + justify-content: space-between; +`; + +export const StyledIconContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 150px; + width: 150px; + text-align: center; + border: 1px solid #f5f5f5; + padding: 10px; + box-sizing: border-box; + svg { + height: 100%; + width: 100%; + } +`; diff --git a/src/components/utils/Icons/Icons.tsx b/src/components/utils/Icons/Icons.tsx new file mode 100644 index 000000000..3b4636edb --- /dev/null +++ b/src/components/utils/Icons/Icons.tsx @@ -0,0 +1,26 @@ +/* eslint-disable import/namespace */ +// this rule is disabled because "All" import sends an error + +import React from 'react'; +import * as All from 'assets/icons/icons'; +import { StyledIconsContainer, StyledIconContainer } from './Icons.styles'; + +export const Icons = () => { + const isComponent = (key) => + typeof All[key] === 'function' && /^\w+$/.test(key); + + const renderComponents = () => { + return Object.keys(All) + .filter(isComponent) + .map((key, index) => { + const Component = All[key]; + return ( + +

{key}

+ +
+ ); + }); + }; + return {renderComponents()}; +}; diff --git a/src/components/utils/Tag/Tag.styles.tsx b/src/components/utils/Tag/Tag.styles.tsx index 0e15a986f..10aacecb5 100644 --- a/src/components/utils/Tag/Tag.styles.tsx +++ b/src/components/utils/Tag/Tag.styles.tsx @@ -27,6 +27,7 @@ const sizeVariants = { export const StyledTag = styled.div` display: inline-flex; + padding: 3px 10px; border-radius: 30px; border: 1px solid; font-weight: 400; diff --git a/src/constants/tags.ts b/src/constants/tags.ts index 24e4a3eea..4feab0942 100644 --- a/src/constants/tags.ts +++ b/src/constants/tags.ts @@ -392,6 +392,9 @@ export const GA_TAGS = { PAGE_ANNUAIRE_CARTE_CLIC: { action: 'Page_Annuaire_Carte_Clic', }, + PROFILE_DETAILS_CONTACT_SEND_CLIC: { + action: 'Profile_Details_Contact_Send_Clic', + }, } as const; export const FB_TAGS = { diff --git a/src/pages/backoffice/profile/[userId].tsx b/src/pages/backoffice/profile/[userId].tsx index 19001b477..1cd27c371 100644 --- a/src/pages/backoffice/profile/[userId].tsx +++ b/src/pages/backoffice/profile/[userId].tsx @@ -1,8 +1,20 @@ import React from 'react'; +import { useSelector } from 'react-redux'; +import { LoadingScreen } from 'src/components/backoffice/LoadingScreen'; import { Profile } from 'src/components/backoffice/profile'; +import { useSelectedProfile } from 'src/components/backoffice/profile/useSelectedProfile'; +import { fetchSelectedProfileSelectors } from 'src/use-cases/profiles'; const PageProfile = () => { - return ; + const { selectedProfile } = useSelectedProfile(); + const isFetchProfileRequested = useSelector( + fetchSelectedProfileSelectors.selectIsFetchSelectedProfileRequested + ); + + if (selectedProfile && !isFetchProfileRequested) { + return ; + } + return ; }; export default PageProfile; diff --git a/src/use-cases/profiles/profiles.adapters.ts b/src/use-cases/profiles/profiles.adapters.ts index 00d1db3d8..671b2b1d8 100644 --- a/src/use-cases/profiles/profiles.adapters.ts +++ b/src/use-cases/profiles/profiles.adapters.ts @@ -1,4 +1,4 @@ -import { PublicProfile } from 'src/api/types'; +import { PublicProfile, InternalMessage } from 'src/api/types'; import { createRequestAdapter } from 'src/store/utils'; export const fetchProfilesAdapter = createRequestAdapter( @@ -13,3 +13,7 @@ export const fetchSelectedProfileAdapter = createRequestAdapter( }, PublicProfile >(); + +export const postInternalMessageAdapter = createRequestAdapter( + 'postInternalMessage' +).withPayloads(); diff --git a/src/use-cases/profiles/profiles.saga.ts b/src/use-cases/profiles/profiles.saga.ts index 0eea10761..ba713f5ef 100644 --- a/src/use-cases/profiles/profiles.saga.ts +++ b/src/use-cases/profiles/profiles.saga.ts @@ -15,6 +15,9 @@ const { fetchProfilesFailed, setProfilesRoleFilter, incrementProfilesOffset, + postInternalMessageRequested, + postInternalMessageSucceeded, + postInternalMessageFailed, } = slice.actions; function* fetchProfilesSagaRequested() { @@ -47,9 +50,27 @@ function* fetchSelectedProfileSaga( } } +function* postInternalMessageSaga( + action: ReturnType +) { + try { + const postInternalMessageResponse = yield* call(() => + Api.postInternalMessage(action.payload) + ); + yield* put(postInternalMessageSucceeded(postInternalMessageResponse.data)); + const putProfileResponse = yield* call(() => + Api.getPublicUserProfile(action.payload.addresseeUserId) + ); + yield* put(fetchSelectedProfileSucceeded(putProfileResponse.data)); + } catch { + yield* put(postInternalMessageFailed()); + } +} + export function* saga() { yield* takeLatest(fetchProfilesRequested, fetchProfilesSaga); yield* takeLatest(setProfilesRoleFilter, fetchProfilesSagaRequested); yield* takeLatest(incrementProfilesOffset, fetchProfilesSagaRequested); yield* takeLatest(fetchSelectedProfileRequested, fetchSelectedProfileSaga); + yield* takeLatest(postInternalMessageRequested, postInternalMessageSaga); } diff --git a/src/use-cases/profiles/profiles.selectors.ts b/src/use-cases/profiles/profiles.selectors.ts index 6a55f90e8..82b8699de 100644 --- a/src/use-cases/profiles/profiles.selectors.ts +++ b/src/use-cases/profiles/profiles.selectors.ts @@ -1,6 +1,7 @@ import { fetchProfilesAdapter, fetchSelectedProfileAdapter, + postInternalMessageAdapter, } from './profiles.adapters'; import { RootState } from './profiles.slice'; @@ -14,6 +15,11 @@ export const fetchSelectedProfileSelectors = (state) => state.profiles.fetchSelectedProfile ); +export const postInternalMessageSelectors = + postInternalMessageAdapter.getSelectors( + (state) => state.profiles.postInternalMessage + ); + export function selectProfiles(state: RootState) { return state.profiles.profiles; } diff --git a/src/use-cases/profiles/profiles.slice.ts b/src/use-cases/profiles/profiles.slice.ts index 2cd1052fb..db335147d 100644 --- a/src/use-cases/profiles/profiles.slice.ts +++ b/src/use-cases/profiles/profiles.slice.ts @@ -5,6 +5,7 @@ import { RequestState, SliceRootState } from 'src/store/utils'; import { fetchProfilesAdapter, fetchSelectedProfileAdapter, + postInternalMessageAdapter, } from './profiles.adapters'; const LIMIT = 25; @@ -12,6 +13,7 @@ const LIMIT = 25; export interface State { fetchProfiles: RequestState; fetchSelectedProfile: RequestState; + postInternalMessage: RequestState; profiles: PublicProfile[]; profilesFilters: { role?: UserRole[]; offset: number; limit: typeof LIMIT }; profilesHasFetchedAll: boolean; @@ -21,6 +23,7 @@ export interface State { const initialState: State = { fetchProfiles: fetchProfilesAdapter.getInitialState(), fetchSelectedProfile: fetchSelectedProfileAdapter.getInitialState(), + postInternalMessage: fetchProfilesAdapter.getInitialState(), profiles: [], profilesFilters: { offset: 0, limit: LIMIT }, profilesHasFetchedAll: false, @@ -48,6 +51,10 @@ export const slice = createSlice({ }, } ), + ...postInternalMessageAdapter.getReducers( + (state) => state.postInternalMessage, + {} + ), setProfilesRoleFilter(state, action: PayloadAction) { state.profilesFilters = { ...state.profilesFilters, diff --git a/src/utils/Formatting.tsx b/src/utils/Formatting.tsx index 05f8cbe18..673d1156c 100644 --- a/src/utils/Formatting.tsx +++ b/src/utils/Formatting.tsx @@ -78,3 +78,10 @@ export function buildContractLabel( return `${findConstantFromValue(contract, CONTRACTS)?.label}${dates}`; } + +export const limitChar = (string: string, limit: number) => { + if (string.length > limit) { + return `${string.slice(0, limit)}...`; + } + return string; +}; From 8276672aea3d04a92f1524e2eb749829c0a76d27 Mon Sep 17 00:00:00 2001 From: Gauthier Date: Tue, 30 Jan 2024 11:29:51 +0100 Subject: [PATCH 02/21] chore: github action improved (#181) --- .github/workflows/main.yml | 94 +- .husky/pre-push | 2 +- README.md | 259 +- cypress.config.js | 3 +- cypress/fixtures/opportunity-admin-res.json | 250 +- docs/use-cases.md | 2 +- package.json | 4 +- src/styles/dist/js/uikit-icons.js | 435 ++- src/styles/dist/js/uikit-icons.min.js | 272 +- stack.svg | 3283 +------------------ tsconfig.json | 10 +- 11 files changed, 865 insertions(+), 3749 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b209f8c2f..d7a975659 100755 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,24 +1,21 @@ -name: LinkedOut Frontend Test +name: LinkedOut [front-end] on: - [push] - # pull_request: - # branches: [master, develop] - # workflow_dispatch: + pull_request: {} # events by default (open, synchronized, reopened) in any branch triggers the workflow. env: - WEBINAR_URL: https://google.com - TOOLBOX_URL: https://google.com - TUTORIAL_CV: https://google.com - TUTORIAL_PP: https://google.com - TUTORIAL_VIDEO_FIRST_STEPS: https://google.com - TUTORIAL_VIDEO_CV: https://google.com - TUTORIAL_VIDEO_OFFERS: https://google.com - TUTORIAL_VIDEO_OFFERS_2: https://google.com - TUTORIAL_INTERVIEW_TRAINING: https://google.com - IRAISER_DONATION_LINK: https://google.com - SERVER_URL: http://localhost:3000 - API_URL: http://localhost:3002 + WEBINAR_URL: 'https://google.com' + TOOLBOX_URL: 'https://google.com' + TUTORIAL_CV: 'https://google.com' + TUTORIAL_PP: 'https://google.com' + TUTORIAL_VIDEO_FIRST_STEPS: 'https://google.com' + TUTORIAL_VIDEO_CV: 'https://google.com' + TUTORIAL_VIDEO_OFFERS: 'https://google.com' + TUTORIAL_VIDEO_OFFERS_2: 'https://google.com' + TUTORIAL_INTERVIEW_TRAINING: 'https://google.com' + IRAISER_DONATION_LINK: 'https://google.com' + SERVER_URL: 'http://localhost:3000' + API_URL: 'http://localhost:3002' PORT: 3000 AWSS3_URL: https://entourage-job-preprod.s3.amazonaws.com AWSS3_CDN_URL: https://d3s4t580ymtqme.cloudfront.net @@ -26,37 +23,58 @@ env: AWSS3_IMAGE_DIRECTORY: /images/ CYPRESS_IO_PROJECT_ID: ${{ secrets.CYPRESS_IO_PROJECT_ID }} PUSHER_API_KEY: ${{ secrets.PUSHER_API_KEY }} - ADRESSE_LOCAUX_PARIS: "174 Rue Championnet 75018, Paris" + ADRESSE_LOCAUX_PARIS: '174 Rue Championnet 75018, Paris' HEROKU_RELEASE_VERSION: 'v100' + NEXT_TELEMETRY_DISABLED: 1 jobs: test: - runs-on: ubuntu-latest + name: E2E on Chrome + runs-on: ubuntu-latest # use ubuntu-latest environment: 'entourage-job-front' container: - image: cypress/browsers:node16.17.0-chrome106 - options: --user 1001 + image: cypress/browsers:node16.17.0-chrome106 # use node16.17.0-chrome106 image from cypress/browsers (dockerhub) + options: --user 1001 # to grant user permission steps: - - name: checkout - uses: actions/checkout@v3 + # Move on the ref + - name: Checkout on PR + uses: actions/checkout@v3 # action to checkout on the ref - - name: cypress - uses: cypress-io/github-action@v4 + # Get node module from cache + - name: Cache Node Modules + id: yarn-cache-deps + uses: actions/cache@v3 # action to cache node modules with: - start: yarn dev - config: e2e.baseUrl=http://localhost:3000 - browser: chrome - command: npx cypress run --record --key ${{ secrets.CYPRESS_IO_KEY }} - wait-on: http://localhost:3000 + # npm cache files are stored in `/node_modules` on Linux/macOS + path: '**/node_modules' + key: linux-node-modules-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + linux-node-modules- + linux-node- - - run: yarn test:ts-check + # Run `yarn install` if cache no exist + - name: Yarn Install Deps + continue-on-error: true + run: | + yarn install --frozen-lockfile + if: steps.yarn-cache-deps.outputs.cache-hit != 'true' - - run: yarn test:eslint + # Run ts-check test then eslint test + - name: Run ts-check & eslint - Code Quality Test + run: | + yarn test:ts-check + yarn test:eslint - - name: set-up - uses: actions/setup-node@v3 - with: - node-version: '16.x' + # Run jest test + - name: Run Jest - Integration Test + run: yarn test:integ - - run: yarn test:inte - - run: echo No tests + # Run cypress test + - name: Run Cypress - End to End Test + uses: cypress-io/github-action@v4 # action to exec cypress-io + with: + start: yarn dev # run dev script to enable cypress execution + config: e2e.baseUrl=http://localhost:3000 + browser: chrome + command: npx cypress run --key ${{ secrets.CYPRESS_IO_KEY }} + wait-on: http://localhost:3000 diff --git a/.husky/pre-push b/.husky/pre-push index c60b2708b..4c694098a 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,4 +1,4 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -yarn test:inte +yarn test:integ diff --git a/README.md b/README.md index 11519b868..c46623098 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,89 @@ # LinkedOut Frontend +[![LinkedOut [front-end]](https://github.com/ReseauEntourage/entourage-job-front/actions/workflows/main.yml/badge.svg?branch=chore%2Fgithub-action)](https://github.com/ReseauEntourage/entourage-job-front/actions/workflows/main.yml) + +## Sommaire + - [Modules principaux & versions](#modules-principaux--versions) - [Architecture](#architecture) - [Configuration](#configuration) +- [Documentation](#documentation) +- [Tests](#tests) +- [Styles](#styles) +- [Pipeline](#pipeline) - [Déploiement](#déploiement) +- [Extra.](#extra.) - [Stack technique](#stack-technique) - Code - [Use cases](./docs/use-cases.md) - [Permissions des routes](./docs/routes-permissions.md) - - [Styles](#styles) ## Modules principaux & versions -> Node 16.x.x - -> React 17.0.2 - -> Next.js 12.1.0 - -> Webpack 4.46.0 - -> ESLint 7.32.0 - -> Babel 7.16.5 +| App | Version | +| ----------- | ------- | +| **Node** | 16.17.x | +| **NPM** | 8.15.x | +| **YARN** | 1.22.x | +| **Next.js** | 12.3.4 | +| **React** | 17.0.2 | +| **Webpack** | 4.46.0 | +| **esLint** | 7.32.0 | ## Architecture +- `entourage-job-front/` - `.github`: configuration de la CI avec **_Github Actions_** - `.husky` : scripts de hook de commit avec **_Husky_** -- `/assets` : fichiers de styles globaux avec **_UIkit_** -- `/public` : stockage des ressources non dynamique accessible publiquement tels que les images, le CSS ou les fonts -- `/src` - - `/components` : dossier contenant les composants **_React_** écrit avec les particularités de **_Next.js_** - - `/constants` : fichiers de constantes - - `/hooks` : hooks communs à plusieurs composants - - `/lib` : librairies pure JS (analytics ...) - - `/pages` : dossier contenant les composants **_React_** de rendu de pages - - `/styles` : feuilles CSS compilés à partir de **_UIkit_** + certains styles customs - - `/use-cases` : store redux séparé en modules - - `/utils` : fonctions utilitaires communes - - `Axios.js` : configuration **_Axios_** pour communiquer facilement avec l'API -- `.editorconfig` : configuration par défaut de la syntaxe du code de l'éditeur -- `.env` : à ajouter pour gérer les variables d'environnements ([cf. exemple](#fichier-env-minimal)) -- `.eslintignore` : configuration pour **_ESLint_** -- `.eslintrc.json` : configuration pour **_ESLint_** -- `next.config.js` : fichier de configuration pour **_Next.js_** -- `.prettierignore` : configuration pour **_Prettier_** -- `.prettierrc.json` : configuration pour **_Prettier_** -- `Procfile` : configuration des process **_Heroku_** à lancer après déploiement +- `assets/` : fichiers de styles globaux avec **_UIkit_** +- `public/` : stockage des ressources non dynamique accessible publiquement tels que les images, le CSS ou les fonts +- `src/` + - `components/` : dossier contenant les composants **_React_** écrit avec les particularités de **_Next.js_** + - `constants/` : fichiers de constantes + - `hooks/` : hooks communs à plusieurs composants + - `lib/` : librairies pure JS (analytics ...) + - `pages/` : dossier contenant les composants **_React_** de rendu de pages + - `styles/` : feuilles CSS compilés à partir de **_UIkit_** + certains styles customs + - `use-cases/` : store redux séparé en modules + - `utils/` : fonctions utilitaires communes +- `.editorconfig`: configuration par défaut de la syntaxe du code de l'éditeur +- `Makefile`: permet l'execution de la commande `make init` créant interactivement le fichier `.env` +- `.env.dist`: fichier de distribution +- `.eslintignore`: configuration pour **_ESLint_** +- `.eslintrc.json`: configuration pour **_ESLint_** +- `next.config.js`: fichier de configuration pour **_Next.js_** +- `.prettierignore`: configuration pour **_Prettier_** +- `.prettierrc.json`: configuration pour **_Prettier_** +- `Procfile`: configuration des process **_Heroku_** à lancer après déploiement - `server-next.js`: point d'entrée de lancement du serveur ## Configuration -> make init - to initialize the .env from .env.dist with the right values +Construire le fichier _.env_ contenant les variables d'environnement -### Avec Docker +``` +make init +``` -#### Build image et container +Vérifier que le [back-end LinkedOut](https://github.com/ReseauEntourage/entourage-job-back) fonctionne correctement ``` -docker-compose build +docker ps ``` -#### Lancer le projet en mode dev +[Yarn](https://classic.yarnpkg.com/lang/en/docs/install/#debian-stable) est requis pour installer le projet ``` -docker-compose up +npm install -g yarn ``` -### Sans Docker - -#### Installation des modules +Installation des dépendances - décrite dans le fichier _yarn-lock.json_ ``` -npm install +yarn install ``` -#### Lancement en en mode développement +### Lancement en mode développement ``` yarn dev @@ -84,174 +91,146 @@ yarn dev ### Lancement en mode production -(pour le moment sans Docker) +Pour le moment sans Docker ``` yarn build +``` + +Puis + +``` npm start ``` ### Prettier + Linter ``` -yarn lint -yarn format +yarn lint && yarn format ``` -avec docker, précéder chaque commande par "docker exec front ${cmd}" +Avec docker, précéder chaque commande par `docker exec front ${cmd}` Ces deux commandes sont lancées par les hooks de commit +Si un bug apparait lors de l'utilisation de git ajouter l'option suivante + +``` +--no-verify +``` + ### Storybook +Pour accéder à la page documenté des composants + ``` yarn storybook ``` Don't forget to import icons into the storybook when adding a new one in "/assets/icons" + ``` yarn add-icons ``` -### Tests +## Styles -#### Tests Unitaires - Jest +> Suppression progressive de la librairie **_UIkit_** -La commande pour lancer les tests une fois: +Les fichiers du thème globale, qui utilisent la librairie **_UIkit_**, se trouvent dans le dossier `/assets/custom` -``` -yarn test -``` +- `entourage.less` : style globale qui surcharge le thème par défaut de **_UIkit_** -La commande pour lancer les tests en mode watch: +- `entourage.print.less` : style utilisé pour le CV en version PDF + +- `/icons` : icônes en SVG rajoutés aux icônes **_UIkit_** + +Après avoir modifié les fichiers du thème, ou après avoir rajouté un icône, il faut recompiler les fichiers en CSS + +- Installer d'abord **_UIkit_** au sein de son propre module ``` -yarn test:watch +yarn uikit-install ``` -Si vous souhaitez obtenir le code coverage: +- Si le module est déjà installé, le mettre à jour ``` -npx jest --coverage +yarn uikit-update ``` -Un dossier coverage sera créé. Afin de pouvoir le consulter dans le navigateur: +- Ensuite, compiler les fichiers SCSS en CSS ``` -yarn posttest:cov +yarn uikit-compile ``` -#### tests End to End - Cypress +Les fichiers se retrouvent dans le dossier _/src/styles/dist_. -La commande suivante permet de lancer les tests Cypress: +## Tests -``` -npx run cypress -``` +### Tests Unitaires - Jest -Pour obtenir la vidéo des tests sur cypress.io, utilisez la commande suivante: +La commande pour lancer les tests une fois ``` -yarn cypress:io -``` - -### Fichier .env minimal - -```dotenv -ADMIN_CANDIDATES_HZ= -ADMIN_CANDIDATES_LILLE= -ADMIN_CANDIDATES_LORIENT= -ADMIN_CANDIDATES_LYON= -ADMIN_CANDIDATES_PARIS= -ADMIN_CANDIDATES_RENNES= -ADMIN_COMPANIES_HZ= -ADMIN_COMPANIES_LILLE= -ADMIN_COMPANIES_LORIENT= -ADMIN_COMPANIES_LYON= -ADMIN_COMPANIES_PARIS= -ADMIN_COMPANIES_RENNES= -ADRESSE_LOCAUX_PARIS= -API_URL= -AIRTABLE_LINK_BECOME_COACH= -ASSOCIATION_BROCHURE= -AWSS3_CDN_URL= -AWSS3_IMAGE_DIRECTORY= -AWSS3_URL= -CDN_URL= -CYPRESS_IO_ID= -CYPRESS_IO_KEY= -DONATION_LINK= -FB_APP_ID= -FB_DOMAIN_VERIFICATION= -FB_PIXEL_ID= -GA_TRACKING_ID= -GTM_TRACKING_ID= -HEROKU_APP_NAME= -HEROKU_RELEASE_VERSION= -IRAISER_DONATION_LINK= -LINKEDIN_PARTNER_ID= -MAILJET_CONTACT_EMAIL= -PUSHER_API_KEY= -SENTRY_AUTH_TOKEN= -SENTRY_DSN= -SERVER_URL= -SHOW_POPUP= -TARTEAUCITRON_UUID= -TOOLBOX_URL= -TUTORIAL_CV= -TUTORIAL_INTERVIEW_TRAINING= -TUTORIAL_PP= -TUTORIAL_VIDEO_CV= -TUTORIAL_VIDEO_FIRST_STEPS= -TUTORIAL_VIDEO_OFFERS= -TUTORIAL_VIDEO_OFFERS_2= +yarn test ``` -## Styles +La commande pour lancer les tests en mode watch -Les fichiers du thème globale, qui utilisent la librairie **_UIkit_**, se trouvent dans le dossier `/assets/custom` : +``` +yarn test:watch +``` -- `entourage.less` : style globale qui surcharge le thème par défaut de **_UIkit_** +Si vous souhaitez obtenir le code coverage -- `entourage.print.less` : style utilisé pour le CV en version PDF +``` +jest --coverage +``` -- `/icons` : icônes en SVG rajoutés aux icônes **_UIkit_** +Un dossier coverage sera créé. Afin de pouvoir le consulter dans le navigateur. -Après avoir modifié les fichiers du thème, ou après avoir rajouté un icône, il faut recompiler les fichiers en CSS : +### Tests End to End (E2E) - Cypress -- Installer d'abord **_UIkit_** au sein de son propre module : +La commande suivante permet de lancer les tests Cypress: ``` -yarn uikit-install +yarn cypress:local ``` -- Si le module est déjà installé, le mettre à jour ; +Pour obtenir la vidéo des tests sur cypress.io, utilisez la commande suivante: ``` -yarn uikit-update +yarn cypress:io ``` -- Ensuite, compiler les fichiers SCSS en CSS : +## Pipeline CI/CD -``` -yarn uikit-compile -``` +#### Pipeline CI + +Lire le fichier _.github/workflows/main.yml_ +Ce fichier constitue le workflow [Github Actions](https://docs.github.com/fr/actions) + +À chaque évènement de type: open, synchronized, reopened sur une PR executera le workflow. + +Et ici la documentation concernant l'action [Cypress.io](https://docs.cypress.io/guides/continuous-integration/github-actions). -Les fichiers transformés se retrouvent dans le dossier `/src/styles/dist`. +#### Pipeline CD + +> W.I.P. ## Déploiement Le déploiement se fait automatiquement grâce à **_Github Actions_** et **_Heroku_**. -Si un commit est poussé sur `develop`, l'application sera déployé sur la pre-production : **[https://entourage-job-front-preprod.herokuapp.com](https://entourage-job-front-preprod.herokuapp.com)** - -Si un commit est poussé sur `master`, l'application sera déployé sur la production : **[https://linkedout.fr](https://linkedout.fr)** +Si une branche, après PR et review, est mergée à `develop` alors l'application sera automatiquement déployée sur la pre-production: **[https://entourage-job-front-preprod.herokuapp.com](https://entourage-job-front-preprod.herokuapp.com)** -Comme il n'y a pas de tests, **_Github Actions_** n'est utilisé que pour déployer le projet sur **_Heroku_**. +Si une branche, après test en pre-production, est mergée à `master`, alors l'application sera automatiquement déployée sur la production: **[https://linkedout.fr](https://linkedout.fr)** -## +## Extra. -Régulièrement, cleaner le code en supprimant les commposants qui ne sont plus utilisés: +Régulièrement, lancer la commande ci-dessous, afin de cleaner le code en supprimant les dependances, imports, et exports qui ne sont plus utilisés ``` npx dead-exports @@ -259,4 +238,6 @@ npx dead-exports ## Stack technique +Dernier update: _14/12/2023_ + ![Stack technique LinkedOut](./stack.svg) diff --git a/cypress.config.js b/cypress.config.js index cb04e39b4..9bfbafc53 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -3,10 +3,11 @@ require('dotenv').config(); const { defineConfig } = require('cypress'); module.exports = defineConfig({ + video: false, projectId: process.env.CYPRESS_IO_PROJECT_ID, defaultCommandTimeout: 10000, pageLoadTimeout: 120000, - scrollBehavior: "center", + scrollBehavior: 'center', env: { adresseLocauxParis: `${process.env.ADRESSE_LOCAUX_PARIS}`, }, diff --git a/cypress/fixtures/opportunity-admin-res.json b/cypress/fixtures/opportunity-admin-res.json index 656621cf8..f52a32a14 100644 --- a/cypress/fixtures/opportunity-admin-res.json +++ b/cypress/fixtures/opportunity-admin-res.json @@ -1,128 +1,128 @@ { - "id": "84fe74bf-7998-4653-9150-906781bfdb91", - "title": "Chauffeur de taxi", - "isPublic": false, - "isValidated": true, - "isArchived": false, - "isExternal": false, - "link": null, - "externalOrigin": null, - "company": "Taxi Mania", - "recruiterName": "Delaroche", - "recruiterFirstName": "Jean", - "recruiterMail": "jeandelaroche34@gmail.com", - "contactMail": null, - "recruiterPosition": "Chauffeur professionnel", - "recruiterPhone": "+33628145475", - "date": "2022-06-16T15:00:42.540Z", - "address": "Rue Victor Hugo", - "description": "Conduire", - "companyDescription": "Présentation de l'entreprise", - "skills": null, - "prerequisites": null, - "department": "Rhône (69)", - "contract": "cdi", - "startOfContract": null, - "endOfContract": null, - "isPartTime": false, - "numberOfPositions": 1, - "message": "Bonjour Candidat", - "driversLicense": true, - "workingHours": "3j", - "salary": "1000", - "otherInfo": "", - "createdBy": "dc92c3a4-7c93-4f99-a660-f69138737617", - "createdAt": "2022-06-16T15:00:42.783Z", - "updatedAt": "2022-06-20T10:09:40.829Z", - "opportunityUsers": [ - { - "id": "bf8f4063-3704-451b-8b14-4edc0f41af9e", - "UserId": "3da510cc-abae-4c4d-a801-30ca0d403951", - "OpportunityId": "84fe74bf-7998-4653-9150-906781bfdb91", - "status": -1, - "bookmarked": false, - "archived": false, - "note": null, - "seen": false, - "recommended": false, - "user": { - "id": "3da510cc-abae-4c4d-a801-30ca0d403951", - "email": "emile@entourage.social", - "firstName": "Emile Sf", - "lastName": "Bex Sf", - "gender": 0, - "zone": "LYON" - } - }, - { - "id": "73f3bca2-a5fc-4ed8-b376-2659bb64f07c", - "UserId": "68ddd96b-db84-4c27-b6bb-19dea2fda867", - "OpportunityId": "84fe74bf-7998-4653-9150-906781bfdb91", - "status": -1, - "bookmarked": false, - "archived": false, - "note": null, - "seen": false, - "recommended": false, - "user": { - "id": "68ddd96b-db84-4c27-b6bb-19dea2fda867", - "email": "emile+@entourage.social", - "firstName": "Emile", - "lastName": "Bex", - "gender": 0, - "zone": "LYON" - } - }, - { - "id": "91c2eae8-7b57-4594-bc53-364a948bf32f", - "UserId": "fd463124-ac49-400a-942f-84faf7475b88", - "OpportunityId": "84fe74bf-7998-4653-9150-906781bfdb91", - "status": -1, - "bookmarked": false, - "archived": false, - "note": null, - "seen": false, - "recommended": false, - "user": { - "id": "fd463124-ac49-400a-942f-84faf7475b88", - "email": "emilebex@gmail.com", - "firstName": "Emile Test Démo", - "lastName": "Bex Démo", - "gender": 0, - "zone": "LYON" - } - }, - { - "id": "a0fc5770-9309-4149-9470-e032c873a51d", - "UserId": "85bc5a2f-b2ee-47b8-9346-f43601c1d4f2", - "OpportunityId": "84fe74bf-7998-4653-9150-906781bfdb91", - "status": 0, - "bookmarked": false, - "archived": false, - "note": null, - "seen": false, - "recommended": false, - "user": { - "id": "85bc5a2f-b2ee-47b8-9346-f43601c1d4f2", - "email": "a.b@gmail.com", - "firstName": "Alex Sf", - "lastName": "Bex Sf", - "gender": 0, - "zone": "LYON" - } + "id": "84fe74bf-7998-4653-9150-906781bfdb91", + "title": "Chauffeur de taxi", + "isPublic": false, + "isValidated": true, + "isArchived": false, + "isExternal": false, + "link": null, + "externalOrigin": null, + "company": "Taxi Mania", + "recruiterName": "Delaroche", + "recruiterFirstName": "Jean", + "recruiterMail": "jeandelaroche34@gmail.com", + "contactMail": null, + "recruiterPosition": "Chauffeur professionnel", + "recruiterPhone": "+33628145475", + "date": "2022-06-16T15:00:42.540Z", + "address": "Rue Victor Hugo", + "description": "Conduire", + "companyDescription": "Présentation de l'entreprise", + "skills": null, + "prerequisites": null, + "department": "Rhône (69)", + "contract": "cdi", + "startOfContract": null, + "endOfContract": null, + "isPartTime": false, + "numberOfPositions": 1, + "message": "Bonjour Candidat", + "driversLicense": true, + "workingHours": "3j", + "salary": "1000", + "otherInfo": "", + "createdBy": "dc92c3a4-7c93-4f99-a660-f69138737617", + "createdAt": "2022-06-16T15:00:42.783Z", + "updatedAt": "2022-06-20T10:09:40.829Z", + "opportunityUsers": [ + { + "id": "bf8f4063-3704-451b-8b14-4edc0f41af9e", + "UserId": "3da510cc-abae-4c4d-a801-30ca0d403951", + "OpportunityId": "84fe74bf-7998-4653-9150-906781bfdb91", + "status": -1, + "bookmarked": false, + "archived": false, + "note": null, + "seen": false, + "recommended": false, + "user": { + "id": "3da510cc-abae-4c4d-a801-30ca0d403951", + "email": "emile@entourage.social", + "firstName": "Emile Sf", + "lastName": "Bex Sf", + "gender": 0, + "zone": "LYON" } - ], - "businessLines": [ - { - "name": "asp", - "order": 0, - "OpportunityBusinessLine": { - "id": "5d9ec47b-be70-44f0-8822-6124ef78bf2f", - "OpportunityId": "84fe74bf-7998-4653-9150-906781bfdb91", - "BusinessLineId": "dd2e49c8-bc2a-4a58-8c88-814d91da4eb9", - "createdAt": "2022-06-20T10:09:40.852Z", - "updatedAt": "2022-06-20T10:09:40.852Z" - } + }, + { + "id": "73f3bca2-a5fc-4ed8-b376-2659bb64f07c", + "UserId": "68ddd96b-db84-4c27-b6bb-19dea2fda867", + "OpportunityId": "84fe74bf-7998-4653-9150-906781bfdb91", + "status": -1, + "bookmarked": false, + "archived": false, + "note": null, + "seen": false, + "recommended": false, + "user": { + "id": "68ddd96b-db84-4c27-b6bb-19dea2fda867", + "email": "emile+@entourage.social", + "firstName": "Emile", + "lastName": "Bex", + "gender": 0, + "zone": "LYON" + } + }, + { + "id": "91c2eae8-7b57-4594-bc53-364a948bf32f", + "UserId": "fd463124-ac49-400a-942f-84faf7475b88", + "OpportunityId": "84fe74bf-7998-4653-9150-906781bfdb91", + "status": -1, + "bookmarked": false, + "archived": false, + "note": null, + "seen": false, + "recommended": false, + "user": { + "id": "fd463124-ac49-400a-942f-84faf7475b88", + "email": "emilebex@gmail.com", + "firstName": "Emile Test Démo", + "lastName": "Bex Démo", + "gender": 0, + "zone": "LYON" + } + }, + { + "id": "a0fc5770-9309-4149-9470-e032c873a51d", + "UserId": "85bc5a2f-b2ee-47b8-9346-f43601c1d4f2", + "OpportunityId": "84fe74bf-7998-4653-9150-906781bfdb91", + "status": 0, + "bookmarked": false, + "archived": false, + "note": null, + "seen": false, + "recommended": false, + "user": { + "id": "85bc5a2f-b2ee-47b8-9346-f43601c1d4f2", + "email": "a.b@gmail.com", + "firstName": "Alex Sf", + "lastName": "Bex Sf", + "gender": 0, + "zone": "LYON" + } + } + ], + "businessLines": [ + { + "name": "asp", + "order": 0, + "OpportunityBusinessLine": { + "id": "5d9ec47b-be70-44f0-8822-6124ef78bf2f", + "OpportunityId": "84fe74bf-7998-4653-9150-906781bfdb91", + "BusinessLineId": "dd2e49c8-bc2a-4a58-8c88-814d91da4eb9", + "createdAt": "2022-06-20T10:09:40.852Z", + "updatedAt": "2022-06-20T10:09:40.852Z" } - ] - } \ No newline at end of file + } + ] +} diff --git a/docs/use-cases.md b/docs/use-cases.md index 9f7a7bc6a..86e89594a 100644 --- a/docs/use-cases.md +++ b/docs/use-cases.md @@ -292,4 +292,4 @@ export function DashboardPage() {
{user.name}
); } -``` +``` \ No newline at end of file diff --git a/package.json b/package.json index b02b2c828..bc7171aa6 100755 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "test": "run-s test:*", "test:ts-check": "tsc -p tsconfig.json --noEmit", "test:eslint": "npx eslint src --ext js,jsx,ts,tsx --fix --max-warnings=0", - "test:inte": "jest", + "test:integ": "jest", "test:e2e": "yarn cypress:io", "build": "next build", "uikit:install": "cd ./node_modules/uikit && yarn", @@ -34,7 +34,7 @@ }, "keywords": [], "author": "", - "license": "ISC", + "license": "MIT", "cacheDirectories": [ ".next/cache" ], diff --git a/src/styles/dist/js/uikit-icons.js b/src/styles/dist/js/uikit-icons.js index f162d79b4..943a53306 100644 --- a/src/styles/dist/js/uikit-icons.js +++ b/src/styles/dist/js/uikit-icons.js @@ -1,169 +1,282 @@ /*! UIkit 3.6.22 | https://www.getuikit.com | (c) 2014 - 2023 YOOtheme | MIT License */ (function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : - typeof define === 'function' && define.amd ? define('uikiticons', factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.UIkitIcons = factory()); -})(this, (function () { 'use strict'; - - function plugin(UIkit) { - - if (plugin.installed) { - return; - } - - UIkit.icon.add({ - "500px": "", - "album": "", - "arrow-down": "", - "arrow-left": "", - "arrow-right": "", - "arrow-up": "", - "ban": "", - "behance": "", - "bell": "", - "bold": "", - "bolt": "", - "bookmark": "", - "calendar": "", - "camera": "", - "cart": "", - "check": "", - "chevron-double-left": "", - "chevron-double-right": "", - "chevron-down": "", - "chevron-left": "", - "chevron-right": "", - "chevron-up": "", - "clock": "", - "close": "", - "cloud-download": "", - "cloud-upload": "", - "code": "", - "cog": "", - "comment": "", - "commenting": "", - "comments": "", - "copy": "", - "credit-card": "", - "database": "", - "desktop": "", - "discord": "", - "download": "", - "dribbble": "", - "etsy": "", - "expand": "", - "facebook": "", - "file-edit": "", - "file-pdf": "", - "file-text": "", - "file": "", - "flickr": "", - "folder": "", - "forward": "", - "foursquare": "", - "future": "", - "git-branch": "", - "git-fork": "", - "github-alt": "", - "github": "", - "gitter": "", - "google": "", - "grid": "", - "happy": "", - "hashtag": "", - "heart": "", - "history": "", - "home": "", - "image": "", - "info": "", - "instagram": "", - "italic": "", - "joomla": "", - "laptop": "", - "lifesaver": "", - "link": "", - "linkedin": "", - "list": "", - "location": "", - "lock": "", - "mail": "", - "menu": "", - "microphone": "", - "minus-circle": "", - "minus": "", - "more-vertical": "", - "more": "", - "move": "", - "nut": "", - "pagekit": "", - "paint-bucket": "", - "pencil": "", - "phone-landscape": "", - "phone": "", - "pinterest": "", - "play-circle": "", - "play": "", - "plus-circle": "", - "plus": "", - "print": "", - "pull": "", - "push": "", - "question": "", - "quote-right": "", - "receiver": "", - "reddit": "", - "refresh": "", - "reply": "", - "rss": "", - "search": "", - "server": "", - "settings": "", - "shrink": "", - "sign-in": "", - "sign-out": "", - "social": "", - "soundcloud": "", - "star": "", - "strikethrough": "", - "table": "", - "tablet-landscape": "", - "tablet": "", - "tag": "", - "thumbnails": "", - "tiktok": "", - "trash": "", - "triangle-down": "", - "triangle-left": "", - "triangle-right": "", - "triangle-up": "", - "tripadvisor": "", - "tumblr": "", - "tv": "", - "twitch": "", - "twitter": "", - "uikit": "", - "unlock": "", - "upload": "", - "user": "", - "users": "", - "video-camera": "", - "vimeo": "", - "warning": "", - "whatsapp": "", - "wordpress": "", - "world": "", - "xing": "", - "yelp": "", - "youtube": "" - }); + typeof exports === 'object' && typeof module !== 'undefined' + ? (module.exports = factory()) + : typeof define === 'function' && define.amd + ? define('uikiticons', factory) + : ((global = + typeof globalThis !== 'undefined' ? globalThis : global || self), + (global.UIkitIcons = factory())); +})(this, function () { + 'use strict'; + function plugin(UIkit) { + if (plugin.installed) { + return; } - if (typeof window !== 'undefined' && window.UIkit) { - window.UIkit.use(plugin); - } + UIkit.icon.add({ + '500px': + '', + album: + '', + 'arrow-down': + '', + 'arrow-left': + '', + 'arrow-right': + '', + 'arrow-up': + '', + ban: '', + behance: + '', + bell: '', + bold: '', + bolt: '', + bookmark: + '', + calendar: + '', + camera: + '', + cart: '', + check: + '', + 'chevron-double-left': + '', + 'chevron-double-right': + '', + 'chevron-down': + '', + 'chevron-left': + '', + 'chevron-right': + '', + 'chevron-up': + '', + clock: + '', + close: + '', + 'cloud-download': + '', + 'cloud-upload': + '', + code: '', + cog: '', + comment: + '', + commenting: + '', + comments: + '', + copy: '', + 'credit-card': + '', + database: + '', + desktop: + '', + discord: + '', + download: + '', + dribbble: + '', + etsy: '', + expand: + '', + facebook: + '', + 'file-edit': + '', + 'file-pdf': + '', + 'file-text': + '', + file: '', + flickr: + '', + folder: + '', + forward: + '', + foursquare: + '', + future: + '', + 'git-branch': + '', + 'git-fork': + '', + 'github-alt': + '', + github: + '', + gitter: + '', + google: + '', + grid: '', + happy: + '', + hashtag: + '', + heart: + '', + history: + '', + home: '', + image: + '', + info: '', + instagram: + '', + italic: + '', + joomla: + '', + laptop: + '', + lifesaver: + '', + link: '', + linkedin: + '', + list: '', + location: + '', + lock: '', + mail: '', + menu: '', + microphone: + '', + 'minus-circle': + '', + minus: + '', + 'more-vertical': + '', + more: '', + move: '', + nut: '', + pagekit: + '', + 'paint-bucket': + '', + pencil: + '', + 'phone-landscape': + '', + phone: + '', + pinterest: + '', + 'play-circle': + '', + play: '', + 'plus-circle': + '', + plus: '', + print: + '', + pull: '', + push: '', + question: + '', + 'quote-right': + '', + receiver: + '', + reddit: + '', + refresh: + '', + reply: + '', + rss: '', + search: + '', + server: + '', + settings: + '', + shrink: + '', + 'sign-in': + '', + 'sign-out': + '', + social: + '', + soundcloud: + '', + star: '', + strikethrough: + '', + table: + '', + 'tablet-landscape': + '', + tablet: + '', + tag: '', + thumbnails: + '', + tiktok: + '', + trash: + '', + 'triangle-down': + '', + 'triangle-left': + '', + 'triangle-right': + '', + 'triangle-up': + '', + tripadvisor: + '', + tumblr: + '', + tv: '', + twitch: + '', + twitter: + '', + uikit: + '', + unlock: + '', + upload: + '', + user: '', + users: + '', + 'video-camera': + '', + vimeo: + '', + warning: + '', + whatsapp: + '', + wordpress: + '', + world: + '', + xing: '', + yelp: '', + youtube: + '', + }); + } - return plugin; + if (typeof window !== 'undefined' && window.UIkit) { + window.UIkit.use(plugin); + } -})); + return plugin; +}); diff --git a/src/styles/dist/js/uikit-icons.min.js b/src/styles/dist/js/uikit-icons.min.js index 2f405894f..eba3f52a0 100644 --- a/src/styles/dist/js/uikit-icons.min.js +++ b/src/styles/dist/js/uikit-icons.min.js @@ -1,2 +1,272 @@ /*! UIkit 3.6.22 | https://www.getuikit.com | (c) 2014 - 2023 YOOtheme | MIT License */ -!function(t,i){"object"==typeof exports&&"undefined"!=typeof module?module.exports=i():"function"==typeof define&&define.amd?define("uikiticons",i):(t="undefined"!=typeof globalThis?globalThis:t||self).UIkitIcons=i()}(this,function(){"use strict";function i(t){i.installed||t.icon.add({"500px":'',album:'',"arrow-down":'',"arrow-left":'',"arrow-right":'',"arrow-up":'',ban:'',behance:'',bell:'',bold:'',bolt:'',bookmark:'',calendar:'',camera:'',cart:'',check:'',"chevron-double-left":'',"chevron-double-right":'',"chevron-down":'',"chevron-left":'',"chevron-right":'',"chevron-up":'',clock:'',close:'',"cloud-download":'',"cloud-upload":'',code:'',cog:'',comment:'',commenting:'',comments:'',copy:'',"credit-card":'',database:'',desktop:'',discord:'',download:'',dribbble:'',etsy:'',expand:'',facebook:'',"file-edit":'',"file-pdf":'',"file-text":'',file:'',flickr:'',folder:'',forward:'',foursquare:'',future:'',"git-branch":'',"git-fork":'',"github-alt":'',github:'',gitter:'',google:'',grid:'',happy:'',hashtag:'',heart:'',history:'',home:'',image:'',info:'',instagram:'',italic:'',joomla:'',laptop:'',lifesaver:'',link:'',linkedin:'',list:'',location:'',lock:'',mail:'',menu:'',microphone:'',"minus-circle":'',minus:'',"more-vertical":'',more:'',move:'',nut:'',pagekit:'',"paint-bucket":'',pencil:'',"phone-landscape":'',phone:'',pinterest:'',"play-circle":'',play:'',"plus-circle":'',plus:'',print:'',pull:'',push:'',question:'',"quote-right":'',receiver:'',reddit:'',refresh:'',reply:'',rss:'',search:'',server:'',settings:'',shrink:'',"sign-in":'',"sign-out":'',social:'',soundcloud:'',star:'',strikethrough:'',table:'',"tablet-landscape":'',tablet:'',tag:'',thumbnails:'',tiktok:'',trash:'',"triangle-down":'',"triangle-left":'',"triangle-right":'',"triangle-up":'',tripadvisor:'',tumblr:'',tv:'',twitch:'',twitter:'',uikit:'',unlock:'',upload:'',user:'',users:'',"video-camera":'',vimeo:'',warning:'',whatsapp:'',wordpress:'',world:'',xing:'',yelp:'',youtube:''})}return"undefined"!=typeof window&&window.UIkit&&window.UIkit.use(i),i}); +!(function (t, i) { + 'object' == typeof exports && 'undefined' != typeof module + ? (module.exports = i()) + : 'function' == typeof define && define.amd + ? define('uikiticons', i) + : ((t = + 'undefined' != typeof globalThis ? globalThis : t || self).UIkitIcons = + i()); +})(this, function () { + 'use strict'; + function i(t) { + i.installed || + t.icon.add({ + '500px': + '', + album: + '', + 'arrow-down': + '', + 'arrow-left': + '', + 'arrow-right': + '', + 'arrow-up': + '', + ban: '', + behance: + '', + bell: '', + bold: '', + bolt: '', + bookmark: + '', + calendar: + '', + camera: + '', + cart: '', + check: + '', + 'chevron-double-left': + '', + 'chevron-double-right': + '', + 'chevron-down': + '', + 'chevron-left': + '', + 'chevron-right': + '', + 'chevron-up': + '', + clock: + '', + close: + '', + 'cloud-download': + '', + 'cloud-upload': + '', + code: '', + cog: '', + comment: + '', + commenting: + '', + comments: + '', + copy: '', + 'credit-card': + '', + database: + '', + desktop: + '', + discord: + '', + download: + '', + dribbble: + '', + etsy: '', + expand: + '', + facebook: + '', + 'file-edit': + '', + 'file-pdf': + '', + 'file-text': + '', + file: '', + flickr: + '', + folder: + '', + forward: + '', + foursquare: + '', + future: + '', + 'git-branch': + '', + 'git-fork': + '', + 'github-alt': + '', + github: + '', + gitter: + '', + google: + '', + grid: '', + happy: + '', + hashtag: + '', + heart: + '', + history: + '', + home: '', + image: + '', + info: '', + instagram: + '', + italic: + '', + joomla: + '', + laptop: + '', + lifesaver: + '', + link: '', + linkedin: + '', + list: '', + location: + '', + lock: '', + mail: '', + menu: '', + microphone: + '', + 'minus-circle': + '', + minus: + '', + 'more-vertical': + '', + more: '', + move: '', + nut: '', + pagekit: + '', + 'paint-bucket': + '', + pencil: + '', + 'phone-landscape': + '', + phone: + '', + pinterest: + '', + 'play-circle': + '', + play: '', + 'plus-circle': + '', + plus: '', + print: + '', + pull: '', + push: '', + question: + '', + 'quote-right': + '', + receiver: + '', + reddit: + '', + refresh: + '', + reply: + '', + rss: '', + search: + '', + server: + '', + settings: + '', + shrink: + '', + 'sign-in': + '', + 'sign-out': + '', + social: + '', + soundcloud: + '', + star: '', + strikethrough: + '', + table: + '', + 'tablet-landscape': + '', + tablet: + '', + tag: '', + thumbnails: + '', + tiktok: + '', + trash: + '', + 'triangle-down': + '', + 'triangle-left': + '', + 'triangle-right': + '', + 'triangle-up': + '', + tripadvisor: + '', + tumblr: + '', + tv: '', + twitch: + '', + twitter: + '', + uikit: + '', + unlock: + '', + upload: + '', + user: '', + users: + '', + 'video-camera': + '', + vimeo: + '', + warning: + '', + whatsapp: + '', + wordpress: + '', + world: + '', + xing: '', + yelp: '', + youtube: + '', + }); + } + return 'undefined' != typeof window && window.UIkit && window.UIkit.use(i), i; +}); diff --git a/stack.svg b/stack.svg index 0214f5bf7..c999bc9a4 100644 --- a/stack.svg +++ b/stack.svg @@ -1,3279 +1,4 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + +
Worker
Worker
Logs et gestion de la
performance des applications
Logs et gestion de la...
Gestion d'environnement
Gestion d'environnement
Intégration continue
Intégration continue
Frontend
Frontend
ORM
ORM
API
API
SSR
SSR
Cache
Cache
Sockets
Sockets
Jobs
Jobs
BDD
BDD
Génération
d'image et de PDF

Génération...
Mails transactionnels
Mails transactionnels
Offres d'emploi
Messages
Contacts
Offres d'emploi...
Stockage
Stockage
Inscription
newsletter

Inscription...
SMS transactionnels
SMS transactionnels
Design system
Design system
Backend
Backend
Hébergement Cloud
Hébergement Cloud
Tests
Tests
Qualité du code
Qualité du code
Languages
Languages
Documentation
Documentat...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 2b3e996fa..9fa5e321a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,6 +17,14 @@ "baseUrl": "./", "noImplicitAny": false }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "src/components/utils/CarouselSwiper/swiper-augmentation.d.ts", ".storybook/**/*"], + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + "**/*.js", + "**/*.jsx", + "src/components/utils/CarouselSwiper/swiper-augmentation.d.ts", + ".storybook/**/*" + ], "exclude": ["node_modules", "./cypress.config.js"] } From 2c23d485f886b867636f2cb064f3ba5c461d4ba1 Mon Sep 17 00:00:00 2001 From: PaulEntourage <112417197+PaulEntourage@users.noreply.github.com> Date: Thu, 1 Feb 2024 18:03:49 +0100 Subject: [PATCH 03/21] [EN-6723] feat(lko2-dashboard): layout and profile card (#196) --- .../backoffice/Backoffice.styles.tsx | 6 +- .../backoffice/dashboard/Dashboard.styles.tsx | 25 +++++++ .../backoffice/dashboard/Dashboard.tsx | 34 +++++++++ .../DashboardProfileCard.styles.tsx | 53 ++++++++++++++ .../DashboardProfileCard.tsx | 69 +++++++++++++++++++ .../dashboard/DashboardProfileCard/index.ts | 1 + src/components/backoffice/dashboard/index.ts | 1 + .../ParametresLayout/ParametresLayout.tsx | 12 ++-- src/components/backoffice/profile/Profile.tsx | 13 ++-- src/constants/helps.tsx | 24 +++++++ src/constants/styles.ts | 1 + src/hooks/authentication/permissions.ts | 1 + src/pages/backoffice/dashboard.tsx | 13 ++++ 13 files changed, 239 insertions(+), 14 deletions(-) create mode 100644 src/components/backoffice/dashboard/Dashboard.styles.tsx create mode 100644 src/components/backoffice/dashboard/Dashboard.tsx create mode 100644 src/components/backoffice/dashboard/DashboardProfileCard/DashboardProfileCard.styles.tsx create mode 100644 src/components/backoffice/dashboard/DashboardProfileCard/DashboardProfileCard.tsx create mode 100644 src/components/backoffice/dashboard/DashboardProfileCard/index.ts create mode 100644 src/components/backoffice/dashboard/index.ts create mode 100644 src/pages/backoffice/dashboard.tsx diff --git a/src/components/backoffice/Backoffice.styles.tsx b/src/components/backoffice/Backoffice.styles.tsx index d74004bf5..528355726 100644 --- a/src/components/backoffice/Backoffice.styles.tsx +++ b/src/components/backoffice/Backoffice.styles.tsx @@ -1,11 +1,11 @@ import styled, { css } from 'styled-components'; import { COLORS } from 'src/constants/styles'; -export const StyledProfileLayout = styled.div` - background-color: #f3f3f3; +export const StyledBackofficeBackground = styled.div` + background-color: ${COLORS.lightGrayBackground}; `; -export const StyledProfileGrid = styled.div` +export const StyledBackofficeGrid = styled.div` display: flex; flex-direction: row; gap: 40px; diff --git a/src/components/backoffice/dashboard/Dashboard.styles.tsx b/src/components/backoffice/dashboard/Dashboard.styles.tsx new file mode 100644 index 000000000..c064ac0d4 --- /dev/null +++ b/src/components/backoffice/dashboard/Dashboard.styles.tsx @@ -0,0 +1,25 @@ +import styled from 'styled-components'; + +export const StyledDashboardLeftColumn = styled.div` + display: flex; + flex-direction: column; + flex-grow: 1; + gap: 40px; + min-width: 400px; + max-width: 400px; + &.mobile { + min-width: 100%; + max-width: 100%; + margin-bottom: 40px; + } +`; + +export const StyledParametresRightColumn = styled.div` + display: flex; + flex-direction: column; + flex-grow: 2; + gap: 40px; + &.mobile { + width: 100%; + } +`; diff --git a/src/components/backoffice/dashboard/Dashboard.tsx b/src/components/backoffice/dashboard/Dashboard.tsx new file mode 100644 index 000000000..eea35aebe --- /dev/null +++ b/src/components/backoffice/dashboard/Dashboard.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { + StyledBackofficeBackground, + StyledBackofficeGrid, +} from '../Backoffice.styles'; +import { Section } from 'src/components/utils'; +import { H1 } from 'src/components/utils/Headings'; +import { useIsDesktop } from 'src/hooks/utils'; +import { + StyledDashboardLeftColumn, + StyledParametresRightColumn, +} from './Dashboard.styles'; +import { DashboardProfileCard } from './DashboardProfileCard'; + +export const Dashboard = () => { + const isDesktop = useIsDesktop(); + return ( + +
+

+ {/*

+
*/} + + + + + + +
+
+ ); +}; diff --git a/src/components/backoffice/dashboard/DashboardProfileCard/DashboardProfileCard.styles.tsx b/src/components/backoffice/dashboard/DashboardProfileCard/DashboardProfileCard.styles.tsx new file mode 100644 index 000000000..bda7064a2 --- /dev/null +++ b/src/components/backoffice/dashboard/DashboardProfileCard/DashboardProfileCard.styles.tsx @@ -0,0 +1,53 @@ +import styled from 'styled-components'; + +export const StyledDashboardProfileCardPictureName = styled.div` + display: flex; + flex-direction: row; + align-items: center; + gap: 20px; + margin: 20px; + h5 { + margin-bottom: 0; + } + p { + margin: 0; + } +`; + +export const StyledDashboardProfileCardDescription = styled.div` + margin: 20px; + font-weight: 300; + font-size: 14px; + text-overflow: ellipsis; + overflow: hidden; + display: -webkit-box !important; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + white-space: normal; +`; + +export const StyledDashboardProfileCardHelps = styled.div` + margin: 0 20px 20px 20px; +`; + +export const StyledDashboardProfileCardhelpsTitle = styled.div` + width: 100%; + padding-bottom: 15px; + border-bottom: 1px solid #fddfd2; + margin-bottom: 30px; + font-size: 16px; +`; + +export const StyledDashboardProfileCardHelpList = styled.div` + > div { + margin-right: 10px; + margin-bottom: 10px; + } +`; + +export const StyledDashboardCTAContainer = styled.div` + margin: 0 20px 20px 20px; + display: flex; + flex-direction: row; + justify-content: center; +`; diff --git a/src/components/backoffice/dashboard/DashboardProfileCard/DashboardProfileCard.tsx b/src/components/backoffice/dashboard/DashboardProfileCard/DashboardProfileCard.tsx new file mode 100644 index 000000000..7a11e3318 --- /dev/null +++ b/src/components/backoffice/dashboard/DashboardProfileCard/DashboardProfileCard.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { useHelpField } from '../../parametres/useUpdateProfile'; +import { useContextualRole } from '../../useContextualRole'; +import { Button, Card, ImgProfile, Tag } from 'src/components/utils'; +import { H5 } from 'src/components/utils/Headings'; +import { ProfileCardHelps } from 'src/constants/helps'; +import { useAuthenticatedUser } from 'src/hooks/authentication/useAuthenticatedUser'; +import { + StyledDashboardCTAContainer, + StyledDashboardProfileCardDescription, + StyledDashboardProfileCardHelpList, + StyledDashboardProfileCardHelps, + StyledDashboardProfileCardhelpsTitle, + StyledDashboardProfileCardPictureName, +} from './DashboardProfileCard.styles'; + +export const DashboardProfileCard = () => { + const user = useAuthenticatedUser(); + const helpField = useHelpField(user.role); + const { contextualRole } = useContextualRole(user.role); + if (!helpField || !contextualRole) return null; + return ( + + + +
+
+ {user.userProfile.department &&

{user.userProfile.department}

} +
+
+ {user.userProfile.description && ( + + {user.userProfile.description} + + )} + {user.userProfile[helpField].length > 0 && ( + + + Mes coups de pouce + + + {user.userProfile[helpField].slice(0, 3).map((help, index) => { + const helpDetails = ProfileCardHelps.find( + (helpConstant) => helpConstant.value === help.name + ); + if (helpDetails) { + const tagContent = helpDetails.shortTitle[contextualRole]; + return ; + } + return null; + })} + {user.userProfile[helpField].length > 3 && ( + + )} + + + )} + + + +
+ ); +}; diff --git a/src/components/backoffice/dashboard/DashboardProfileCard/index.ts b/src/components/backoffice/dashboard/DashboardProfileCard/index.ts new file mode 100644 index 000000000..7b40486b5 --- /dev/null +++ b/src/components/backoffice/dashboard/DashboardProfileCard/index.ts @@ -0,0 +1 @@ +export * from './DashboardProfileCard'; diff --git a/src/components/backoffice/dashboard/index.ts b/src/components/backoffice/dashboard/index.ts new file mode 100644 index 000000000..19bd377d7 --- /dev/null +++ b/src/components/backoffice/dashboard/index.ts @@ -0,0 +1 @@ +export * from './Dashboard'; diff --git a/src/components/backoffice/parametres/ParametresLayout/ParametresLayout.tsx b/src/components/backoffice/parametres/ParametresLayout/ParametresLayout.tsx index 7d346232d..a331f1b31 100644 --- a/src/components/backoffice/parametres/ParametresLayout/ParametresLayout.tsx +++ b/src/components/backoffice/parametres/ParametresLayout/ParametresLayout.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { - StyledProfileGrid, - StyledProfileLayout, + StyledBackofficeGrid, + StyledBackofficeBackground, } from '../../Backoffice.styles'; import { useConfirmationToaster } from '../useConfirmationToaster'; import { Card, Section } from 'src/components/utils'; @@ -30,10 +30,10 @@ export const ParametresLayout = () => { useConfirmationToaster(); return ( - +
- + @@ -71,8 +71,8 @@ export const ParametresLayout = () => { )} - +
-
+ ); }; diff --git a/src/components/backoffice/profile/Profile.tsx b/src/components/backoffice/profile/Profile.tsx index 1d309cf56..5eb656672 100644 --- a/src/components/backoffice/profile/Profile.tsx +++ b/src/components/backoffice/profile/Profile.tsx @@ -1,5 +1,8 @@ import React from 'react'; -import { StyledProfileGrid, StyledProfileLayout } from '../Backoffice.styles'; +import { + StyledBackofficeGrid, + StyledBackofficeBackground, +} from '../Backoffice.styles'; import { LayoutBackOffice } from '../LayoutBackOffice'; import { Section } from 'src/components/utils'; import { useIsDesktop } from 'src/hooks/utils'; @@ -21,10 +24,10 @@ export const Profile = () => { - +
- + @@ -34,9 +37,9 @@ export const Profile = () => { > - +
-
+
); }; diff --git a/src/constants/helps.tsx b/src/constants/helps.tsx index c154c39e1..0813a30a3 100644 --- a/src/constants/helps.tsx +++ b/src/constants/helps.tsx @@ -10,31 +10,55 @@ import { FilterConstant } from './utils'; export const ProfileCardHelps: (FilterConstant & { icon: JSX.Element; + shortTitle: { + Candidat: string; + Coach: string; + }; })[] = [ { icon: , value: 'tips', label: 'Soutien', + shortTitle: { + Candidat: 'Demander un conseil', + Coach: 'Conseiller un(e) candidat(e)', + }, }, { icon: , value: 'interview', label: 'Entretien', + shortTitle: { + Candidat: 'Préparer un entretien', + Coach: 'Aider à préparer un entretien', + }, }, { icon: , value: 'cv', label: 'CV', + shortTitle: { + Candidat: 'Créer mon CV', + Coach: 'Aider à réaliser un CV', + }, }, { icon: , value: 'event', label: 'Événement', + shortTitle: { + Candidat: 'Rencontrer la communauté', + Coach: 'Rencontrer la communauté', + }, }, { icon: , value: 'network', label: 'Partage', + shortTitle: { + Candidat: 'Développer mon réseau', + Coach: 'Partager mon réseau', + }, }, ]; export const ParametresHelpCardTitles: { diff --git a/src/constants/styles.ts b/src/constants/styles.ts index 39dc16d5c..7be0a102d 100644 --- a/src/constants/styles.ts +++ b/src/constants/styles.ts @@ -18,6 +18,7 @@ export const HEIGHTS = { export const COLORS = { lightgray: '#F5F5F5', + lightGrayBackground: '#f3f3f3', gray: '#D9D9D9', darkGray: '#A0A0A0', darkGrayFont: '#6D6C6C', diff --git a/src/hooks/authentication/permissions.ts b/src/hooks/authentication/permissions.ts index 0c1fa608b..559e92ab8 100644 --- a/src/hooks/authentication/permissions.ts +++ b/src/hooks/authentication/permissions.ts @@ -6,6 +6,7 @@ export const authenticatedPermissions = [ '/backoffice/parametres', '/backoffice/profile/[userId]', '/backoffice/annuaire', + '/backoffice/dashboard', ], roles: '*', }, diff --git a/src/pages/backoffice/dashboard.tsx b/src/pages/backoffice/dashboard.tsx new file mode 100644 index 000000000..7e0c566a4 --- /dev/null +++ b/src/pages/backoffice/dashboard.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { LayoutBackOffice } from 'src/components/backoffice/LayoutBackOffice'; +import { Dashboard } from 'src/components/backoffice/dashboard'; + +const Parametres = () => { + return ( + + + + ); +}; + +export default Parametres; From ebfa4de8880ad8a22fedc74a22bc05c625e9ecb3 Mon Sep 17 00:00:00 2001 From: Emile Bex Date: Fri, 2 Feb 2024 11:55:27 +0100 Subject: [PATCH 04/21] chore(datadog): add datadog --- package.json | 1 + server-next.js | 2 + tracer.ts | 11 ++ yarn.lock | 327 +++++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 330 insertions(+), 11 deletions(-) create mode 100644 tracer.ts diff --git a/package.json b/package.json index bc7171aa6..4907eda02 100755 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "circular-dependency-plugin": "^5.2.2", "cookies-next": "^2.1.1", "cors": "^2.8.5", + "dd-trace": "^3.3.1", "dotenv": "^10.0.0", "express": "^4.17.1", "express-sslify": "^1.2.0", diff --git a/server-next.js b/server-next.js index 1e68d5216..fdf9ae427 100644 --- a/server-next.js +++ b/server-next.js @@ -1,3 +1,5 @@ +import './tracer'; + const next = require('next'); const cors = require('cors'); const express = require('express'); diff --git a/tracer.ts b/tracer.ts new file mode 100644 index 000000000..f6da95b05 --- /dev/null +++ b/tracer.ts @@ -0,0 +1,11 @@ +import tracer from 'dd-trace'; + +const ENV = `${process.env.NODE_ENV}`; + +if (ENV === 'production') { + tracer.init({ + version: process.env.HEROKU_RELEASE_VERSION, + }); +} + +export { tracer }; diff --git a/yarn.lock b/yarn.lock index c6653ea08..71ba65345 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2056,6 +2056,52 @@ debug "^3.1.0" lodash.once "^4.1.1" +"@datadog/native-appsec@7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@datadog/native-appsec/-/native-appsec-7.0.0.tgz#a380174dd49aef2d9bb613a0ec8ead6dc7822095" + integrity sha512-bywstWFW2hWxzPuS0+mFMVHHL0geulx5yQFtsjfszaH2LTAgk2D+Rt40MKbAoZ8q3tRw2dy6aYQ7svO3ca8jpA== + dependencies: + node-gyp-build "^3.9.0" + +"@datadog/native-iast-rewriter@2.2.2": + version "2.2.2" + resolved "https://registry.yarnpkg.com/@datadog/native-iast-rewriter/-/native-iast-rewriter-2.2.2.tgz#3f7feaf6be1af4c83ad063065b8ed509bbaf11cb" + integrity sha512-13ZBhJpjZ/tiV6rYfyAf/ITye9cyd3x12M/2NKhD4Ivev4N4uKBREAjpArOtzKtPXZ5b6oXwVV4ofT1SHoYyzA== + dependencies: + lru-cache "^7.14.0" + node-gyp-build "^4.5.0" + +"@datadog/native-iast-taint-tracking@1.6.4": + version "1.6.4" + resolved "https://registry.yarnpkg.com/@datadog/native-iast-taint-tracking/-/native-iast-taint-tracking-1.6.4.tgz#16c21ad7c36a53420c0d3c5a3720731809cc7e98" + integrity sha512-Owxk7hQ4Dxwv4zJAoMjRga0IvE6lhvxnNc8pJCHsemCWBXchjr/9bqg05Zy5JnMbKUWn4XuZeJD6RFZpRa8bfw== + dependencies: + node-gyp-build "^3.9.0" + +"@datadog/native-metrics@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@datadog/native-metrics/-/native-metrics-2.0.0.tgz#65bf03313ee419956361e097551db36173e85712" + integrity sha512-YklGVwUtmKGYqFf1MNZuOHvTYdKuR4+Af1XkWcMD8BwOAjxmd9Z+97328rCOY8TFUJzlGUPaXzB8j2qgG/BMwA== + dependencies: + node-addon-api "^6.1.0" + node-gyp-build "^3.9.0" + +"@datadog/pprof@5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@datadog/pprof/-/pprof-5.0.0.tgz#0c0aaf06def6d2bc4b2d353ec7b264dadbfbefab" + integrity sha512-vhNan4SBuNWLpexunDJQ+hNbRAgWdk2qy5Iyh7Nn94uSSHXigAJMAvu4jwMKKQKFfchtobOkWT8GQUWW3tgpFg== + dependencies: + delay "^5.0.0" + node-gyp-build "<4.0" + p-limit "^3.1.0" + pprof-format "^2.0.7" + source-map "^0.7.4" + +"@datadog/sketches-js@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@datadog/sketches-js/-/sketches-js-2.1.0.tgz#8c7e8028a5fc22ad102fa542b0a446c956830455" + integrity sha512-smLocSfrt3s53H/XSVP3/1kP42oqvrkjUPtyaFd1F79ux24oE31BKt+q0c6lsa6hOYrFzsIwyc5GXAI5JmfOew== + "@discoveryjs/json-ext@^0.5.3", "@discoveryjs/json-ext@^0.5.7": version "0.5.7" resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" @@ -2813,6 +2859,23 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@opentelemetry/api@^1.0.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.7.0.tgz#b139c81999c23e3c8d3c0a7234480e945920fc40" + integrity sha512-AdY5wvN0P2vXBi3b29hxZgSFvdhdxPB9+f0B6s//P9Q8nibRWeA3cHm8UmLpio9ABigkVHJ5NMPk+Mz8VCCyrw== + +"@opentelemetry/core@^1.14.0": + version "1.21.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.21.0.tgz#8c16faf16edf861b073c03c9d45977b3f4003ee1" + integrity sha512-KP+OIweb3wYoP7qTYL/j5IpOlu52uxBv5M4+QhSmmUfLyTgu1OIS71msK3chFo1D6Y61BIH3wMiMYRCxJCQctA== + dependencies: + "@opentelemetry/semantic-conventions" "1.21.0" + +"@opentelemetry/semantic-conventions@1.21.0": + version "1.21.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.21.0.tgz#83f7479c524ab523ac2df702ade30b9724476c72" + integrity sha512-lkC8kZYntxVKr7b8xmjCVUgE0a8xgDakPyDo9uSWavXPyYqLgYYGdEd2j8NxihRyb6UwpX3G/hFUF4/9q2V+/g== + "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" @@ -2833,6 +2896,59 @@ schema-utils "^3.0.0" source-map "^0.7.3" +"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" + integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== + +"@protobufjs/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== + +"@protobufjs/codegen@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" + integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== + +"@protobufjs/eventemitter@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" + integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q== + +"@protobufjs/fetch@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" + integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ== + dependencies: + "@protobufjs/aspromise" "^1.1.1" + "@protobufjs/inquire" "^1.1.0" + +"@protobufjs/float@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" + integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== + +"@protobufjs/inquire@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" + integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q== + +"@protobufjs/path@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" + integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA== + +"@protobufjs/pool@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" + integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw== + +"@protobufjs/utf8@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" + integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== + "@radix-ui/number@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/number/-/number-1.0.1.tgz#644161a3557f46ed38a042acf4a770e826021674" @@ -4718,6 +4834,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.13.0.tgz#0400d1e6ce87e9d3032c19eb6c58205b0d3f7850" integrity sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg== +"@types/node@>=13.7.0": + version "20.11.16" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.16.tgz#4411f79411514eb8e2926f036c86c9f0e4ec6708" + integrity sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ== + dependencies: + undici-types "~5.26.4" + "@types/node@^13.7.0": version "13.13.52" resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.52.tgz#03c13be70b9031baaed79481c0c0cfb0045e53f7" @@ -6502,6 +6625,11 @@ cjs-module-lexer@^1.0.0: resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== +cjs-module-lexer@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz#6c370ab19f8a3394e318fe682686ec0ac684d107" + integrity sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ== + classnames@^2.2.5, classnames@^2.3.0, classnames@^2.3.1: version "2.3.2" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924" @@ -6978,6 +7106,11 @@ crypto-random-string@^2.0.0: resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== +crypto-randomuuid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/crypto-randomuuid/-/crypto-randomuuid-1.0.0.tgz#acf583e5e085e867ae23e107ff70279024f9e9e7" + integrity sha512-/RC5F4l1SCqD/jazwUF6+t34Cd8zTSAGZ7rvvZu1whZUhD2a5MOGKjSGowoGcpj/cbVZk1ZODIooJEQQq3nNAA== + css-color-keywords@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05" @@ -7153,6 +7286,48 @@ dayjs@^1.10.4: resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2" integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ== +dc-polyfill@^0.1.2: + version "0.1.3" + resolved "https://registry.yarnpkg.com/dc-polyfill/-/dc-polyfill-0.1.3.tgz#fe9eefc86813439dd46d6f9ad9582ec079c39720" + integrity sha512-Wyk5n/5KUj3GfVKV2jtDbtChC/Ff9fjKsBcg4ZtYW1yQe3DXNHcGURvmoxhqQdfOQ9TwyMjnfyv1lyYcOkFkFA== + +dd-trace@^3.3.1: + version "3.47.0" + resolved "https://registry.yarnpkg.com/dd-trace/-/dd-trace-3.47.0.tgz#a6e79b1bbeb3c3593bec75b224fc07de5683a821" + integrity sha512-4YUJz+uFaNJy72k0sNjnwRKepkSAu8Pa7dYZcDTSv2MZ9S1c+hiiKWjH4VKaXL9fL0KBa4AvWc8/XJe8h/Fy7Q== + dependencies: + "@datadog/native-appsec" "7.0.0" + "@datadog/native-iast-rewriter" "2.2.2" + "@datadog/native-iast-taint-tracking" "1.6.4" + "@datadog/native-metrics" "^2.0.0" + "@datadog/pprof" "5.0.0" + "@datadog/sketches-js" "^2.1.0" + "@opentelemetry/api" "^1.0.0" + "@opentelemetry/core" "^1.14.0" + crypto-randomuuid "^1.0.0" + dc-polyfill "^0.1.2" + ignore "^5.2.4" + import-in-the-middle "^1.7.3" + int64-buffer "^0.1.9" + ipaddr.js "^2.1.0" + istanbul-lib-coverage "3.2.0" + jest-docblock "^29.7.0" + koalas "^1.0.2" + limiter "1.1.5" + lodash.sortby "^4.7.0" + lru-cache "^7.14.0" + methods "^1.1.2" + module-details-from-path "^1.0.3" + msgpack-lite "^0.1.26" + node-abort-controller "^3.1.1" + opentracing ">=0.12.1" + path-to-regexp "^0.1.2" + pprof-format "^2.0.7" + protobufjs "^7.2.5" + retry "^0.13.1" + semver "^7.5.4" + tlhunter-sorted-set "^0.1.0" + debug@2.6.9, debug@^2.1.3, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -7290,6 +7465,11 @@ del@^6.0.0: rimraf "^3.0.2" slash "^3.0.0" +delay@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/delay/-/delay-5.0.0.tgz#137045ef1b96e5071060dd5be60bf9334436bd1d" + integrity sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw== + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -8111,6 +8291,11 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== +event-lite@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/event-lite/-/event-lite-0.1.3.tgz#3dfe01144e808ac46448f0c19b4ab68e403a901d" + integrity sha512-8qz9nOz5VeD2z96elrEKD2U433+L3DWdUdDkOINLGOJvx1GsMBbMn0aCeu28y8/e85A6mCigBiFlYMnTBEGlSw== + event-loop-spinner@^2.0.0, event-loop-spinner@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/event-loop-spinner/-/event-loop-spinner-2.2.0.tgz#5b9bdf1759a5d9600576260ae770446a1a16c9b0" @@ -9207,7 +9392,7 @@ icss-utils@^5.0.0, icss-utils@^5.1.0: resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== -ieee754@^1.1.13, ieee754@^1.2.1: +ieee754@^1.1.13, ieee754@^1.1.8, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -9217,6 +9402,11 @@ ignore@^5.2.0: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== +ignore@^5.2.4: + version "5.3.1" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" + integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== + image-size@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/image-size/-/image-size-1.0.2.tgz#d778b6d0ab75b2737c1556dd631652eb963bc486" @@ -9242,6 +9432,16 @@ import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1: parent-module "^1.0.0" resolve-from "^4.0.0" +import-in-the-middle@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.7.3.tgz#ffa784cdd57a47d2b68d2e7dd33070ff06baee43" + integrity sha512-R2I11NRi0lI3jD2+qjqyVlVEahsejw7LDnYEbGb47QEFjczE3bZYsmWheCTQA+LFs2DzOQxR7Pms7naHW1V4bQ== + dependencies: + acorn "^8.8.2" + acorn-import-assertions "^1.9.0" + cjs-module-lexer "^1.2.2" + module-details-from-path "^1.0.3" + import-local@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" @@ -9290,6 +9490,11 @@ input-format@^0.3.8: dependencies: prop-types "^15.8.1" +int64-buffer@^0.1.9: + version "0.1.10" + resolved "https://registry.yarnpkg.com/int64-buffer/-/int64-buffer-0.1.10.tgz#277b228a87d95ad777d07c13832022406a473423" + integrity sha512-v7cSY1J8ydZ0GyjUHqF+1bshJ6cnEVLo9EnjB8p+4HDRPZc9N5jjmvUV7NvEsqQOKyH0pmIBFWXVQbiS0+OBbA== + internal-slot@^1.0.3, internal-slot@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.5.tgz#f2a2ee21f668f8627a4667f309dc0f4fb6674986" @@ -9316,6 +9521,11 @@ ipaddr.js@1.9.1: resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== +ipaddr.js@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.1.0.tgz#2119bc447ff8c257753b196fc5f1ce08a4cdf39f" + integrity sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ== + is-absolute-url@^3.0.0: version "3.0.3" resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698" @@ -9624,16 +9834,16 @@ is@^3.2.1: resolved "https://registry.yarnpkg.com/is/-/is-3.3.0.tgz#61cff6dd3c4193db94a3d62582072b44e5645d79" integrity sha512-nW24QBoPcFGGHJGUwnfpI7Yc5CdqWNdsyHQszVE/z2pKHXzh7FZ5GWhJqSyaQ9wMkQnsTx+kAI8bHlCX4tKdbg== +isarray@^1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + isarray@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== - isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -9649,7 +9859,7 @@ isstream@~0.1.2: resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== -istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: +istanbul-lib-coverage@3.2.0, istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== @@ -9806,6 +10016,13 @@ jest-docblock@^29.4.3: dependencies: detect-newline "^3.0.0" +jest-docblock@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.7.0.tgz#8fddb6adc3cdc955c93e2a87f61cfd350d5d119a" + integrity sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g== + dependencies: + detect-newline "^3.0.0" + jest-each@^29.4.3: version "29.4.3" resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.4.3.tgz#a434c199a2f6151c5e3dc80b2d54586bdaa72819" @@ -10370,6 +10587,11 @@ klona@^2.0.4, klona@^2.0.6: resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.6.tgz#85bffbf819c03b2f53270412420a4555ef882e22" integrity sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA== +koalas@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/koalas/-/koalas-1.0.2.tgz#318433f074235db78fae5661a02a8ca53ee295cd" + integrity sha512-RYhBbYaTTTHId3l6fnMZc3eGQNW6FVCqMG6AMwA5I1Mafr6AflaXeoi6x3xQuATRotGYRLk6+1ELZH4dstFNOA== + language-subtag-registry@~0.3.2: version "0.3.22" resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz#2e1500861b2e457eba7e7ae86877cbd08fa1fd1d" @@ -10451,6 +10673,11 @@ lilconfig@2.0.6: resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.6.tgz#32a384558bd58af3d4c6e077dd1ad1d397bc69d4" integrity sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg== +limiter@1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/limiter/-/limiter-1.1.5.tgz#8f92a25b3b16c6131293a0cc834b4a838a2aa7c2" + integrity sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA== + lines-and-columns@^1.1.6: version "1.2.4" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" @@ -10651,6 +10878,11 @@ lodash.size@^4.2.0: resolved "https://registry.yarnpkg.com/lodash.size/-/lodash.size-4.2.0.tgz#71fe75ed3eabdb2bcb73a1b0b4f51c392ee27b86" integrity sha512-wbu3SF1XC5ijqm0piNxw59yCbuUf2kaShumYBLWUrcCvwh6C8odz6SY/wGVzCWTQTFL/1Ygbvqg2eLtspUVVAQ== +lodash.sortby@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== + lodash.topairs@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.topairs/-/lodash.topairs-4.3.0.tgz#3b6deaa37d60fb116713c46c5f17ea190ec48d64" @@ -10694,6 +10926,11 @@ log-update@^4.0.0: slice-ansi "^4.0.0" wrap-ansi "^6.2.0" +long@^5.0.0: + version "5.2.3" + resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" + integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q== + loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -10727,6 +10964,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lru-cache@^7.14.0: + version "7.18.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" + integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== + "lru-cache@^9.1.1 || ^10.0.0": version "10.1.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.1.0.tgz#2098d41c2dc56500e6c88584aa656c84de7d0484" @@ -10856,7 +11098,7 @@ merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -methods@~1.1.2: +methods@^1.1.2, methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== @@ -11007,6 +11249,11 @@ mobile-detect@^1.4.5: resolved "https://registry.yarnpkg.com/mobile-detect/-/mobile-detect-1.4.5.tgz#da393c3c413ca1a9bcdd9ced653c38281c0fb6ad" integrity sha512-yc0LhH6tItlvfLBugVUEtgawwFU2sIe+cSdmRJJCTMZ5GEJyLxNyC/NIOAOGk67Fa8GNpOttO3Xz/1bHpXFD/g== +module-details-from-path@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/module-details-from-path/-/module-details-from-path-1.0.3.tgz#114c949673e2a8a35e9d35788527aa37b679da2b" + integrity sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A== + moment@^2.29.4: version "2.29.4" resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" @@ -11027,6 +11274,16 @@ ms@2.1.3, ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +msgpack-lite@^0.1.26: + version "0.1.26" + resolved "https://registry.yarnpkg.com/msgpack-lite/-/msgpack-lite-0.1.26.tgz#dd3c50b26f059f25e7edee3644418358e2a9ad89" + integrity sha512-SZ2IxeqZ1oRFGo0xFGbvBJWMp3yLIY9rlIJyxy8CGrwZn1f0ZK4r6jV/AM1r0FZMDUkWkglOk/eeKIL9g77Nxw== + dependencies: + event-lite "^0.1.1" + ieee754 "^1.1.8" + int64-buffer "^0.1.9" + isarray "^1.0.0" + nanoid@^3.3.4: version "3.3.4" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" @@ -11129,7 +11386,7 @@ node-abi@^3.3.0: dependencies: semver "^7.3.5" -node-abort-controller@^3.0.1: +node-abort-controller@^3.0.1, node-abort-controller@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548" integrity sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ== @@ -11170,6 +11427,16 @@ node-fetch@^2.6.7: dependencies: whatwg-url "^5.0.0" +node-gyp-build@<4.0, node-gyp-build@^3.9.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-3.9.0.tgz#53a350187dd4d5276750da21605d1cb681d09e25" + integrity sha512-zLcTg6P4AbcHPq465ZMFNXx7XpKKJh+7kkN699NiQWisR2uWYOWNWqRHAmbnmKiL4e9aLSlmy5U7rEMUXV59+A== + +node-gyp-build@^4.5.0: + version "4.8.0" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.0.tgz#3fee9c1731df4581a3f9ead74664369ff00d26dd" + integrity sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og== + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -11436,6 +11703,11 @@ open@^8.4.0: is-docker "^2.1.1" is-wsl "^2.2.0" +opentracing@>=0.12.1: + version "0.14.7" + resolved "https://registry.yarnpkg.com/opentracing/-/opentracing-0.14.7.tgz#25d472bd0296dc0b64d7b94cbc995219031428f5" + integrity sha512-vz9iS7MJ5+Bp1URw8Khvdyw1H/hGvzHWlKQ7eRrQojSCDL1/SrWfrY9QebLw97n2deyRtzHRC3MkQfVNUCo91Q== + optionator@^0.8.1: version "0.8.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" @@ -11688,7 +11960,7 @@ path-scurry@^1.10.1: lru-cache "^9.1.1 || ^10.0.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" -path-to-regexp@0.1.7: +path-to-regexp@0.1.7, path-to-regexp@^0.1.2: version "0.1.7" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== @@ -11931,6 +12203,11 @@ postcss@^8.4.5: picocolors "^1.0.0" source-map-js "^1.0.2" +pprof-format@^2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/pprof-format/-/pprof-format-2.0.7.tgz#526e4361f8b37d16b2ec4bb0696b5292de5046a4" + integrity sha512-1qWaGAzwMpaXJP9opRa23nPnt2Egi7RMNoNBptEE/XwHbcn4fC2b/4U4bKc5arkGkIh2ZabpF2bEb+c5GNHEKA== + prebuild-install@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45" @@ -12066,6 +12343,24 @@ prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2, object-assign "^4.1.1" react-is "^16.13.1" +protobufjs@^7.2.5: + version "7.2.6" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.2.6.tgz#4a0ccd79eb292717aacf07530a07e0ed20278215" + integrity sha512-dgJaEDDL6x8ASUZ1YqWciTRrdOuYNzoOf27oHNfdyvKqHr5i0FV7FSLU+aIeFjyFgVxrpTOtQUi0BLLBymZaBw== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/node" ">=13.7.0" + long "^5.0.0" + proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -12879,6 +13174,11 @@ restore-cursor@^3.1.0: onetime "^5.1.0" signal-exit "^3.0.2" +retry@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" @@ -13334,7 +13634,7 @@ source-map@^0.5.7: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== -source-map@^0.7.3: +source-map@^0.7.3, source-map@^0.7.4: version "0.7.4" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== @@ -13901,6 +14201,11 @@ tiny-invariant@^1.3.1: resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642" integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== +tlhunter-sorted-set@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/tlhunter-sorted-set/-/tlhunter-sorted-set-0.1.0.tgz#1c3eae28c0fa4dff97e9501d2e3c204b86406f4b" + integrity sha512-eGYW4bjf1DtrHzUYxYfAcSytpOkA44zsr7G2n3PV7yOUR23vmkGe3LL4R+1jL9OsXtbsFOwe8XtbCrabeaEFnw== + tmp@~0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" From 1d7770a9c667c36131f87ff0c00a808f62939ced Mon Sep 17 00:00:00 2001 From: Emile Bex Date: Fri, 2 Feb 2024 14:38:14 +0100 Subject: [PATCH 05/21] chore(datadog): change datadog to js --- server-next.js | 2 +- tracer.ts => tracer.js | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) rename tracer.ts => tracer.js (67%) diff --git a/server-next.js b/server-next.js index fdf9ae427..f608fe7a8 100644 --- a/server-next.js +++ b/server-next.js @@ -1,4 +1,4 @@ -import './tracer'; +require('./tracer'); const next = require('next'); const cors = require('cors'); diff --git a/tracer.ts b/tracer.js similarity index 67% rename from tracer.ts rename to tracer.js index f6da95b05..fabd21dd8 100644 --- a/tracer.ts +++ b/tracer.js @@ -1,4 +1,4 @@ -import tracer from 'dd-trace'; +const tracer = require('dd-trace'); const ENV = `${process.env.NODE_ENV}`; @@ -8,4 +8,6 @@ if (ENV === 'production') { }); } -export { tracer }; +module.exports = { + tracer, +}; From 4dd496a5157c644d49b4fcc34571b8bc6338059e Mon Sep 17 00:00:00 2001 From: Emile Bex Date: Mon, 5 Feb 2024 12:17:02 +0100 Subject: [PATCH 06/21] [EN-6614] feat(directory): add filters to directory (#197) * [EN-6614] feat(directory): started filters * [EN-6614] feat(directory): started filters * [EN-6614] feat(directory): typed filters and manage directory filters * [EN-6614] feat(directory): finished filters management --- src/api/types.ts | 15 +- .../backoffice/Backoffice.styles.tsx | 12 +- .../LoadingScreen/LoadingScreen.tsx | 2 +- .../AdminOpportunities/AdminOpportunities.tsx | 26 ++- .../AdminOpportunitiesFilters.types.ts | 24 --- .../MemberTab/OffersMemberTab.tsx | 7 +- .../admin/members/MemberList/MemberList.tsx | 8 +- .../OrganizationList/OrganizationList.tsx | 8 +- .../backoffice/candidate/CandidatHeader.tsx | 5 - .../CandidateOffersTab/CandidateOffersTab.tsx | 13 +- .../CandidateOpportunities.tsx | 30 ++-- .../CandidateOpportunitiesFilters.types.ts | 26 --- .../CandidateOpportunities/useTabsCount.ts | 7 +- .../backoffice/cv/CVEditPage/CVEditPage.tsx | 2 - .../CVEdit/CVEditCatchphrase.tsx | 2 +- .../DashboardProfileCard.tsx | 6 +- .../DirectoryContainer.styles.ts | 1 + .../DirectoryContainer/DirectoryContainer.tsx | 68 ++++++- .../directory/DirectoryItem/DirectoryItem.tsx | 4 +- .../backoffice/directory/useDirectory.ts | 8 +- .../directory/useDirectoryFilters.ts | 168 ++++++++++++++++++ .../useDirectoryFiltersQueryParams.ts | 19 ++ .../directory/useDirectoryRoleFilter.ts | 43 ----- .../OpportunitiesContainer.desktop.tsx | 15 +- .../OpportunitiesContainer.mobile.tsx | 6 +- .../OpportunitiesList/OpportunitiesList.tsx | 13 +- .../OpportunityDetails/OpportunityDetails.tsx | 9 +- .../ProfessionalInformationCard.tsx | 12 +- src/components/backoffice/profile/Profile.tsx | 37 ++-- .../ProfileHelpList/ProfileHelpList.tsx | 3 +- .../ProfileProfessionalInformationCard.tsx | 2 +- src/components/backoffice/useRole.ts | 10 -- src/components/filters/FiltersCheckboxes.tsx | 8 +- src/components/filters/FiltersDropdowns.tsx | 31 ++-- src/components/filters/FiltersSideBar.tsx | 8 +- src/components/filters/SearchBar.tsx | 37 +--- .../schemas/formAddExternalOpportunity.ts | 9 +- .../schemas/formEditExternalOpportunity.ts | 9 +- .../HeaderConnected/HeaderConnected.tsx | 2 - src/components/partials/CV/CVList/CVList.tsx | 16 +- .../utils/Button/ButtonMultiple.tsx | 1 - .../utils/CardList/CardList.stories.tsx | 2 +- .../utils/CardList/CardList.styles.ts | 10 +- src/components/utils/CardList/CardList.tsx | 4 +- .../utils/{ => Cards}/Card/Card.stories.tsx | 0 .../utils/{ => Cards}/Card/Card.styles.tsx | 20 +-- .../utils/{ => Cards}/Card/Card.tsx | 0 .../utils/{ => Cards}/Card/index.ts | 0 src/components/utils/Cards/Cards.styles.ts | 11 ++ .../ProfileCard}/ProfileCard.stories.tsx | 16 -- .../ProfileCard}/ProfileCard.styles.ts | 26 ++- .../ProfileCard}/ProfileCard.tsx | 35 ++-- .../utils/Cards/ProfileCard/index.ts | 1 + .../Inputs/SelectList/SelectList.stories.tsx | 2 +- src/components/utils/index.ts | 2 +- src/constants/departements.ts | 3 +- src/constants/helps.tsx | 28 +-- src/constants/index.ts | 35 +++- src/constants/tags.ts | 15 ++ src/constants/users.ts | 2 +- src/constants/utils.ts | 20 +++ src/hooks/useFilters.ts | 27 +-- src/pages/backoffice/admin/offres/index.tsx | 31 ++-- src/pages/backoffice/annuaire.tsx | 7 +- .../candidat/[candidateId]/offres/index.tsx | 15 +- src/pages/backoffice/profile/[userId].tsx | 20 ++- src/pages/reset/[id]/[token].tsx | 7 +- src/use-cases/profiles/profiles.saga.ts | 28 ++- src/use-cases/profiles/profiles.selectors.ts | 20 +++ src/use-cases/profiles/profiles.slice.ts | 88 ++++++++- src/utils/Filters.ts | 11 +- src/utils/Mutating.ts | 13 ++ 72 files changed, 775 insertions(+), 456 deletions(-) delete mode 100644 src/components/backoffice/admin/AdminOpportunities/AdminOpportunitiesFilters.types.ts delete mode 100644 src/components/backoffice/candidate/CandidateOpportunities/CandidateOpportunitiesFilters.types.ts create mode 100644 src/components/backoffice/directory/useDirectoryFilters.ts create mode 100644 src/components/backoffice/directory/useDirectoryFiltersQueryParams.ts delete mode 100644 src/components/backoffice/directory/useDirectoryRoleFilter.ts delete mode 100644 src/components/backoffice/useRole.ts rename src/components/utils/{ => Cards}/Card/Card.stories.tsx (100%) rename src/components/utils/{ => Cards}/Card/Card.styles.tsx (77%) rename src/components/utils/{ => Cards}/Card/Card.tsx (100%) rename src/components/utils/{ => Cards}/Card/index.ts (100%) create mode 100644 src/components/utils/Cards/Cards.styles.ts rename src/components/utils/{Card => Cards/ProfileCard}/ProfileCard.stories.tsx (82%) rename src/components/utils/{Card => Cards/ProfileCard}/ProfileCard.styles.ts (85%) rename src/components/utils/{Card => Cards/ProfileCard}/ProfileCard.tsx (94%) create mode 100644 src/components/utils/Cards/ProfileCard/index.ts diff --git a/src/api/types.ts b/src/api/types.ts index bfcf10a8d..178d04cff 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -1,15 +1,16 @@ import { - ExternalMessageContactType, - Contract as ContractValue, + AmbitionsPrefixesType, BusinessLineValue, - ExternalOfferOrigin, - HeardAboutValue, CandidateHelpWithValue, CompanyApproach, + Contract as ContractValue, + ExternalMessageContactType, + ExternalOfferOrigin, + HeardAboutValue, OfferStatus, - AmbitionsPrefixesType, } from 'src/constants'; import { AdminZone, Department } from 'src/constants/departements'; +import { HelpNames } from 'src/constants/helps'; import { AdminRole, Gender, UserRole } from 'src/constants/users'; export type SocialMedia = @@ -69,8 +70,6 @@ export type OrganizationDto = { zone: AdminZone; }; -export type HelpNames = 'tips' | 'interview' | 'cv' | 'network' | 'event'; - export type UserProfile = { currentJob: string; description: string; @@ -352,6 +351,7 @@ export type Event = { updatedAt: string; contract: Contract; }; + export interface OpportunityUser { OpportunityId: string; UserId: string; @@ -375,6 +375,7 @@ export interface OpportunityUser { salary: string; skills: Skill[]; } + export interface OpportunityWithOpportunityUsers extends Opportunity { opportunityUsers: OpportunityUser; } diff --git a/src/components/backoffice/Backoffice.styles.tsx b/src/components/backoffice/Backoffice.styles.tsx index 528355726..f78a237b6 100644 --- a/src/components/backoffice/Backoffice.styles.tsx +++ b/src/components/backoffice/Backoffice.styles.tsx @@ -2,7 +2,7 @@ import styled, { css } from 'styled-components'; import { COLORS } from 'src/constants/styles'; export const StyledBackofficeBackground = styled.div` - background-color: ${COLORS.lightGrayBackground}; + background-color: ${COLORS.lightgray}; `; export const StyledBackofficeGrid = styled.div` @@ -95,3 +95,13 @@ export const StyledHeaderProfileTextContainer = styled.div` line-height: 24px; } `; + +export const StyledNoResult = styled.div` + flex: 1; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + font-style: italic; + color: ${COLORS.darkGray}; +`; diff --git a/src/components/backoffice/LoadingScreen/LoadingScreen.tsx b/src/components/backoffice/LoadingScreen/LoadingScreen.tsx index 03c3e947a..a7767bae1 100644 --- a/src/components/backoffice/LoadingScreen/LoadingScreen.tsx +++ b/src/components/backoffice/LoadingScreen/LoadingScreen.tsx @@ -3,7 +3,7 @@ import styled from 'styled-components'; import { OverlayLoader } from 'src/components/utils/OverlayLoader'; export const StyledLoadingScreen = styled.div` - width: 100vw; + width: 100%; height: 100vh; position: relative; display: flex; diff --git a/src/components/backoffice/admin/AdminOpportunities/AdminOpportunities.tsx b/src/components/backoffice/admin/AdminOpportunities/AdminOpportunities.tsx index 0b1db52f1..498a37b7a 100644 --- a/src/components/backoffice/admin/AdminOpportunities/AdminOpportunities.tsx +++ b/src/components/backoffice/admin/AdminOpportunities/AdminOpportunities.tsx @@ -4,6 +4,7 @@ import React, { useRef, useState } from 'react'; import UIkit from 'uikit'; import useDeepCompareEffect from 'use-deep-compare-effect'; import PlusIcon from 'assets/icons/plus.svg'; +import { StyledNoResult } from '../../Backoffice.styles'; import { Api } from 'src/api'; import { Opportunity } from 'src/api/types'; import { OpportunitiesContainer } from 'src/components/backoffice/opportunities/OpportunitiesContainer'; @@ -20,6 +21,7 @@ import { Button, ButtonMultiple, Section } from 'src/components/utils'; import { OPPORTUNITY_FILTERS_DATA } from 'src/constants'; import { HEIGHTS } from 'src/constants/styles'; import { GA_TAGS } from 'src/constants/tags'; +import { FilterObject } from 'src/constants/utils'; import { useOpportunityId } from 'src/hooks/queryParams/useOpportunityId'; import { useQueryParamsOpportunities } from 'src/hooks/queryParams/useQueryParamsOpportunities'; import { useTag } from 'src/hooks/queryParams/useTag'; @@ -27,14 +29,15 @@ import { useBulkActions } from 'src/hooks/useBulkActions'; import { useAdminOpportunities } from 'src/hooks/useOpportunityList'; import { useIsDesktop, usePrevious } from 'src/hooks/utils'; import { AdminOffersTab } from './AdminOffersTab'; -import { AdminOpportunitiesFilters } from './AdminOpportunitiesFilters.types'; interface AdminOpportunitiesProps { search?: string; - filters: AdminOpportunitiesFilters; - setFilters?: (updatedFilters: AdminOpportunitiesFilters) => void; + filters: FilterObject; + setFilters?: ( + updatedFilters: FilterObject + ) => void; resetFilters?: () => void; - setSearch?: (updatedSearch: string) => void; + setSearch?: (updatedSearch?: string) => void; isMobile?: boolean; } @@ -46,9 +49,9 @@ const filtersAndTabsHeight = export const AdminOpportunities = ({ search, filters, - setFilters, - setSearch, - resetFilters, + setFilters = () => {}, + setSearch = () => {}, + resetFilters = () => {}, isMobile = false, }: AdminOpportunitiesProps) => { const { replace, push } = useRouter(); @@ -266,12 +269,9 @@ export const AdminOpportunities = ({ } - noContent={ -
- Aucun résultat. -
- } + noContent={Aucun résultat} /> )} diff --git a/src/components/backoffice/admin/AdminOpportunities/AdminOpportunitiesFilters.types.ts b/src/components/backoffice/admin/AdminOpportunities/AdminOpportunitiesFilters.types.ts deleted file mode 100644 index aea26e001..000000000 --- a/src/components/backoffice/admin/AdminOpportunities/AdminOpportunitiesFilters.types.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { BusinessLineValue, Contract, OfferStatus } from 'src/constants'; -import { AdminZone } from 'src/constants/departements'; - -export interface AdminOpportunitiesFilters { - businessLines: { - label: string; - value: BusinessLineValue; - prefix: string[]; - }[]; - status: { - label: string; - value: OfferStatus; - }[]; - contracts: { - label: string; - value: Contract; - end: boolean; - }[]; - department: { - value: string; - label: string; - zone: AdminZone; - }[]; -} diff --git a/src/components/backoffice/admin/members/MemberDetails/MemberTab/OffersMemberTab.tsx b/src/components/backoffice/admin/members/MemberDetails/MemberTab/OffersMemberTab.tsx index 3c0a2174f..8a12aff16 100644 --- a/src/components/backoffice/admin/members/MemberDetails/MemberTab/OffersMemberTab.tsx +++ b/src/components/backoffice/admin/members/MemberDetails/MemberTab/OffersMemberTab.tsx @@ -1,6 +1,7 @@ import { useRouter } from 'next/router'; import React, { useState } from 'react'; import useDeepCompareEffect from 'use-deep-compare-effect'; +import { StyledNoResult } from 'src/components/backoffice/Backoffice.styles'; import { OpportunitiesContainer } from 'src/components/backoffice/opportunities/OpportunitiesContainer'; import { AdminOpportunitiesList } from 'src/components/backoffice/opportunities/OpportunitiesContainer/OpportunitiesList/AdminOpportunitiesList'; import { AdminOpportunityDetailsContainer } from 'src/components/backoffice/opportunities/OpportunitiesContainer/OpportunityDetails/AdminOpportunityDetails/AdminOpportunityDetailsContainer'; @@ -89,11 +90,7 @@ export function OffersMemberTab({ candidateId }: OffersMemberTabProps) { fetchOpportunities={fetchOpportunities} /> } - noContent={ -
- Aucun résultat. -
- } + noContent={Aucun résultat} /> )} diff --git a/src/components/backoffice/admin/members/MemberList/MemberList.tsx b/src/components/backoffice/admin/members/MemberList/MemberList.tsx index ec9593aeb..29edaa0fa 100644 --- a/src/components/backoffice/admin/members/MemberList/MemberList.tsx +++ b/src/components/backoffice/admin/members/MemberList/MemberList.tsx @@ -13,6 +13,7 @@ import { BackToTop, Button, Section } from 'src/components/utils'; import { MEMBER_FILTERS_DATA } from 'src/constants'; import { GA_TAGS } from 'src/constants/tags'; import { CANDIDATE_USER_ROLES } from 'src/constants/users'; +import { FilterObject } from 'src/constants/utils'; import { useRole } from 'src/hooks/queryParams/useRole'; import { useBulkActions } from 'src/hooks/useBulkActions'; import { usePrevious } from 'src/hooks/utils'; @@ -21,13 +22,14 @@ import { mutateTypeFilterDependingOnRole, } from 'src/utils/Filters'; import { isRoleIncluded } from 'src/utils/Finding'; -import { AnyToFix } from 'src/utils/Types'; const LIMIT = 50; interface MemberListProps { - filters: AnyToFix; // to be typed - setFilters: (updatedFilters: AnyToFix) => void; + filters: FilterObject; + setFilters: ( + updatedFilters: FilterObject + ) => void; search?: string; setSearch: (search?: string) => void; resetFilters: () => void; diff --git a/src/components/backoffice/admin/organizations/OrganizationList/OrganizationList.tsx b/src/components/backoffice/admin/organizations/OrganizationList/OrganizationList.tsx index 674104675..7cc1b8825 100644 --- a/src/components/backoffice/admin/organizations/OrganizationList/OrganizationList.tsx +++ b/src/components/backoffice/admin/organizations/OrganizationList/OrganizationList.tsx @@ -10,14 +10,16 @@ import { SearchBar } from 'src/components/filters/SearchBar'; import { HeaderBackoffice } from 'src/components/headers/HeaderBackoffice'; import { Section, Button, BackToTop } from 'src/components/utils'; import { ORGANIZATION_FILTERS_DATA } from 'src/constants'; +import { FilterObject } from 'src/constants/utils'; import { filtersToQueryParams } from 'src/utils/Filters'; -import { AnyToFix } from 'src/utils/Types'; const LIMIT = 50; interface OrganizationListProps { - filters: AnyToFix; // to be typed - setFilters: (updatedFilters: AnyToFix) => void; + filters: FilterObject; + setFilters: ( + updatedFilters: FilterObject + ) => void; search?: string; setSearch: (search?: string) => void; resetFilters: () => void; diff --git a/src/components/backoffice/candidate/CandidatHeader.tsx b/src/components/backoffice/candidate/CandidatHeader.tsx index 4d020a0e4..8075eede1 100644 --- a/src/components/backoffice/candidate/CandidatHeader.tsx +++ b/src/components/backoffice/candidate/CandidatHeader.tsx @@ -22,9 +22,6 @@ export const CandidatHeader = ({ const [candidateCVUrl, setCandidateCVUrl] = useState(''); useEffect(() => { - if (!user) { - return; - } if (isRoleIncluded(COACH_USER_ROLES, user.role)) { const cand = user.coaches?.find( ({ candidat }) => @@ -45,8 +42,6 @@ export const CandidatHeader = ({ } }, [user, candidateId]); - if (!user) return null; - return ( diff --git a/src/components/backoffice/candidate/CandidateOpportunities/CandidateOffersTab/CandidateOffersTab.tsx b/src/components/backoffice/candidate/CandidateOpportunities/CandidateOffersTab/CandidateOffersTab.tsx index 7d4ebd5a0..0a0dfdbfd 100644 --- a/src/components/backoffice/candidate/CandidateOpportunities/CandidateOffersTab/CandidateOffersTab.tsx +++ b/src/components/backoffice/candidate/CandidateOpportunities/CandidateOffersTab/CandidateOffersTab.tsx @@ -7,17 +7,13 @@ import { formatPlural, tabs, } from 'src/components/backoffice/candidate/CandidateOpportunities/CandidateOffersTab/CandidateOffersTab.utils'; +import { FilterConstant } from 'src/constants/utils'; const uuidValue = uuid(); interface CandidateOffersTabProps { - activeStatus: { - value: number; - label: string; - color: string; - public: string; - }[]; - tabCounts: { + activeStatus: FilterConstant[]; + tabCounts?: { status: number; count: number; }[]; @@ -59,7 +55,8 @@ export const CandidateOffersTab = ({ const isActive = activeStatus.some((singleActiveStatus) => { return ( - singleActiveStatus && status.includes(singleActiveStatus.value) + singleActiveStatus && + status.includes(singleActiveStatus.value as number | string) ); }); diff --git a/src/components/backoffice/candidate/CandidateOpportunities/CandidateOpportunities.tsx b/src/components/backoffice/candidate/CandidateOpportunities/CandidateOpportunities.tsx index e32ffd4e7..6140c7720 100644 --- a/src/components/backoffice/candidate/CandidateOpportunities/CandidateOpportunities.tsx +++ b/src/components/backoffice/candidate/CandidateOpportunities/CandidateOpportunities.tsx @@ -3,6 +3,7 @@ import { useRouter } from 'next/router'; import React, { useState } from 'react'; import useDeepCompareEffect from 'use-deep-compare-effect'; import PlusIcon from 'assets/icons/plus.svg'; +import { StyledNoResult } from 'src/components/backoffice/Backoffice.styles'; import { CandidateOffersTab } from 'src/components/backoffice/candidate/CandidateOpportunities/CandidateOffersTab'; import { candidateSearchFilters, @@ -18,10 +19,10 @@ import { HeaderBackoffice } from 'src/components/headers/HeaderBackoffice'; import { openModal } from 'src/components/modals/Modal'; import { ModalExternalOffer } from 'src/components/modals/Modal/ModalGeneric/OfferModals/ModalOffer'; import { Button, Section } from 'src/components/utils'; - import { OPPORTUNITY_FILTERS_DATA } from 'src/constants'; import { HEIGHTS } from 'src/constants/styles'; import { CANDIDATE_USER_ROLES, USER_ROLES } from 'src/constants/users'; +import { FilterObject } from 'src/constants/utils'; import { useAuthenticatedUser } from 'src/hooks/authentication/useAuthenticatedUser'; import { useOpportunityId } from 'src/hooks/queryParams/useOpportunityId'; import { useOpportunityType } from 'src/hooks/queryParams/useOpportunityType'; @@ -30,16 +31,17 @@ import { useCandidateOpportunities } from 'src/hooks/useOpportunityList'; import { usePrevious } from 'src/hooks/utils'; import { getUserCandidateFromCoach, isRoleIncluded } from 'src/utils/Finding'; import { tabs } from './CandidateOffersTab/CandidateOffersTab.utils'; -import { CandidateOpportunitiesFilters } from './CandidateOpportunitiesFilters.types'; import { useTabsCount } from './useTabsCount'; import { useUpdateOpportunityStatus } from './useUpdateOpportunityStatus'; interface CandidateOpportunitiesProps { search?: string; - filters: CandidateOpportunitiesFilters; - setFilters?: (updatedFilters: CandidateOpportunitiesFilters) => void; + filters: FilterObject; + setFilters?: ( + updatedFilters: FilterObject + ) => void; resetFilters?: () => void; - setSearch?: (updatedSearch: string) => void; + setSearch?: (updatedSearch?: string) => void; candidateId: string; isMobile?: boolean; } @@ -47,9 +49,9 @@ interface CandidateOpportunitiesProps { export const CandidateOpportunities = ({ search, filters, - setFilters, - setSearch, - resetFilters, + setFilters = () => {}, + setSearch = () => {}, + resetFilters = () => {}, candidateId, isMobile = false, }: CandidateOpportunitiesProps) => { @@ -217,16 +219,11 @@ export const CandidateOpportunities = ({ {isPublic ? (
@@ -235,7 +232,6 @@ export const CandidateOpportunities = ({
- Aucun résultat. - + Aucun résultat ) : ( (); const fetchTabsCount = useCallback(async () => { const { data } = await Api.getOpportunitiesTabCountByCandidate(candidateId); diff --git a/src/components/backoffice/cv/CVEditPage/CVEditPage.tsx b/src/components/backoffice/cv/CVEditPage/CVEditPage.tsx index 61776889a..721b5d52f 100644 --- a/src/components/backoffice/cv/CVEditPage/CVEditPage.tsx +++ b/src/components/backoffice/cv/CVEditPage/CVEditPage.tsx @@ -308,8 +308,6 @@ export const CVEditPage = ({ candidateId, cv, setCV }: CVEditPageProps) => { [checkIfLastVersion, saveUserData] ); - if (!user) return null; - // aucun CV if (!cv) { return ( diff --git a/src/components/backoffice/cv/CVEditPage/CVFicheEdition/CVEdit/CVEditCatchphrase.tsx b/src/components/backoffice/cv/CVEditPage/CVFicheEdition/CVEdit/CVEditCatchphrase.tsx index b3585a76b..8c0e62239 100644 --- a/src/components/backoffice/cv/CVEditPage/CVFicheEdition/CVEdit/CVEditCatchphrase.tsx +++ b/src/components/backoffice/cv/CVEditPage/CVFicheEdition/CVEdit/CVEditCatchphrase.tsx @@ -10,7 +10,7 @@ export const CVEditCatchphrase = ({ onChange, }: { catchphrase: string; - onChange?: (arg1: { catchphrase: string }) => void; // to be typed + onChange?: (updatedCV: { catchphrase: string }) => void; }) => { return (
diff --git a/src/components/backoffice/dashboard/DashboardProfileCard/DashboardProfileCard.tsx b/src/components/backoffice/dashboard/DashboardProfileCard/DashboardProfileCard.tsx index 7a11e3318..2b99c7932 100644 --- a/src/components/backoffice/dashboard/DashboardProfileCard/DashboardProfileCard.tsx +++ b/src/components/backoffice/dashboard/DashboardProfileCard/DashboardProfileCard.tsx @@ -1,9 +1,9 @@ import React from 'react'; -import { useHelpField } from '../../parametres/useUpdateProfile'; import { useContextualRole } from '../../useContextualRole'; +import { useHelpField } from 'src/components/backoffice/parametres/useUpdateProfile'; import { Button, Card, ImgProfile, Tag } from 'src/components/utils'; import { H5 } from 'src/components/utils/Headings'; -import { ProfileCardHelps } from 'src/constants/helps'; +import { ProfileHelps } from 'src/constants/helps'; import { useAuthenticatedUser } from 'src/hooks/authentication/useAuthenticatedUser'; import { StyledDashboardCTAContainer, @@ -44,7 +44,7 @@ export const DashboardProfileCard = () => { {user.userProfile[helpField].slice(0, 3).map((help, index) => { - const helpDetails = ProfileCardHelps.find( + const helpDetails = ProfileHelps.find( (helpConstant) => helpConstant.value === help.name ); if (helpDetails) { diff --git a/src/components/backoffice/directory/DirectoryContainer/DirectoryContainer.styles.ts b/src/components/backoffice/directory/DirectoryContainer/DirectoryContainer.styles.ts index 046fb7a48..ab0455892 100644 --- a/src/components/backoffice/directory/DirectoryContainer/DirectoryContainer.styles.ts +++ b/src/components/backoffice/directory/DirectoryContainer/DirectoryContainer.styles.ts @@ -11,4 +11,5 @@ export const StyledDirectoryButtonContainer = styled.div` gap: 16px; margin-bottom: 30px; position: relative; + margin-top: ${({ isMobile }) => (isMobile ? 16 : 0)}px; `; diff --git a/src/components/backoffice/directory/DirectoryContainer/DirectoryContainer.tsx b/src/components/backoffice/directory/DirectoryContainer/DirectoryContainer.tsx index 27bb19a9a..8d8fdb986 100644 --- a/src/components/backoffice/directory/DirectoryContainer/DirectoryContainer.tsx +++ b/src/components/backoffice/directory/DirectoryContainer/DirectoryContainer.tsx @@ -1,13 +1,29 @@ import { useRouter } from 'next/router'; -import React from 'react'; +import React, { useMemo } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; import { DirectoryList } from '../DirectoryList'; +import { useDirectoryFiltersQueryParams } from '../useDirectoryFiltersQueryParams'; +import { SearchBar } from 'src/components/filters/SearchBar'; import { Button } from 'src/components/utils'; +import { BUSINESS_LINES, DirectoryFilters } from 'src/constants'; +import { DEPARTMENTS_FILTERS } from 'src/constants/departements'; +import { ProfileHelps } from 'src/constants/helps'; +import { GA_TAGS } from 'src/constants/tags'; import { CANDIDATE_USER_ROLES, COACH_USER_ROLES, USER_ROLES, } from 'src/constants/users'; -import { isRoleIncluded } from 'src/utils'; +import { useFilters } from 'src/hooks'; +import { useIsMobile } from 'src/hooks/utils'; +import { + profilesActions, + selectProfilesBusinessLinesFilters, + selectProfilesDepartmentsFilters, + selectProfilesHelpsFilters, + selectProfilesSearchFilter, +} from 'src/use-cases/profiles'; +import { findConstantFromValue, isRoleIncluded } from 'src/utils'; import { StyledDirectoryButtonContainer, StyledDirectoryContainer, @@ -19,10 +35,52 @@ const route = '/backoffice/annuaire'; export function DirectoryContainer() { const { push } = useRouter(); const roleFilter = useRoleFilter(); + const dispatch = useDispatch(); + + const isMobile = useIsMobile(); + const directoryFiltersParams = useDirectoryFiltersQueryParams(); + + const { setFilters, setSearch, resetFilters } = useFilters( + DirectoryFilters, + `/backoffice/annuaire`, + [], + GA_TAGS.PAGE_ANNUAIRE_SUPPRIMER_FILTRES_CLIC + ); + + const departmentsFilters = useSelector(selectProfilesDepartmentsFilters); + const helpsFilters = useSelector(selectProfilesHelpsFilters); + const businessLinesFilters = useSelector(selectProfilesBusinessLinesFilters); + const search = useSelector(selectProfilesSearchFilter); + + const filters = useMemo(() => { + return { + departments: departmentsFilters.map((department) => + findConstantFromValue(department, DEPARTMENTS_FILTERS) + ), + helps: helpsFilters.map((help) => + findConstantFromValue(help, ProfileHelps) + ), + businessLines: businessLinesFilters.map((businessLine) => + findConstantFromValue(businessLine, BUSINESS_LINES) + ), + }; + }, [departmentsFilters, helpsFilters, businessLinesFilters]); return ( - + { + dispatch(profilesActions.resetProfilesFilters()); + resetFilters(); + }} + search={search || undefined} + setSearch={setSearch} + setFilters={setFilters} + placeholder="Rechercher..." + /> +
); }; - -SearchBar.defaultProps = { - placeholder: 'Rechercher...', - startSearchEvent: undefined, - search: undefined, - smallSelectors: false, -}; diff --git a/src/components/forms/schemas/formAddExternalOpportunity.ts b/src/components/forms/schemas/formAddExternalOpportunity.ts index 36aa60566..3f6dd41e6 100644 --- a/src/components/forms/schemas/formAddExternalOpportunity.ts +++ b/src/components/forms/schemas/formAddExternalOpportunity.ts @@ -1,5 +1,5 @@ import moment from 'moment/moment'; -import { isAfter } from 'validator'; +import { isAfter, isEmail } from 'validator'; import { FormSchema } from '../FormSchema'; import { Api } from 'src/api'; import { @@ -106,6 +106,13 @@ export const formAddExternalOpportunityCandidate: FormSchema<{ component: 'text-input', type: 'email', title: 'Adresse mail du recruteur', + rules: [ + { + method: (fieldValue) => + !fieldValue || fieldValue.length === 0 || isEmail(fieldValue), + message: 'Invalide', + }, + ], }, { id: 'offerDetails', diff --git a/src/components/forms/schemas/formEditExternalOpportunity.ts b/src/components/forms/schemas/formEditExternalOpportunity.ts index bdac66952..ff2ae5f53 100644 --- a/src/components/forms/schemas/formEditExternalOpportunity.ts +++ b/src/components/forms/schemas/formEditExternalOpportunity.ts @@ -1,5 +1,5 @@ import moment from 'moment'; -import { isAfter } from 'validator'; +import { isAfter, isEmail } from 'validator'; import { FormSchema } from '../FormSchema'; import { BUSINESS_LINES, @@ -168,6 +168,13 @@ export const formEditExternalOpportunityAsAdmin: FormSchema<{ component: 'text-input', type: 'email', title: 'Adresse mail du recruteur', + rules: [ + { + method: (fieldValue) => + !fieldValue || fieldValue.length === 0 || isEmail(fieldValue), + message: 'Invalide', + }, + ], }, { id: 'offerDetails', diff --git a/src/components/headers/HeaderConnected/HeaderConnected.tsx b/src/components/headers/HeaderConnected/HeaderConnected.tsx index fcf087ae3..d6fa68fb8 100644 --- a/src/components/headers/HeaderConnected/HeaderConnected.tsx +++ b/src/components/headers/HeaderConnected/HeaderConnected.tsx @@ -32,8 +32,6 @@ export const HeaderConnected = () => { } }, [user, logout, prevUser, candidateId]); - if (!user) return null; - return ( void; // to be typed - setSearch?: (updatedSearch?: AnyToFix) => void; // to be typed + filters: FilterObject; + setFilters?: (updatedFilters: FilterObject) => void; + setSearch?: (updatedSearch?: string) => void; resetFilters?: () => void; } @@ -51,9 +52,9 @@ export const CVList = ({ nb, search, filters = {}, - setFilters, - setSearch, - resetFilters, + setFilters = () => {}, + setSearch = () => {}, + resetFilters = () => {}, }: CVListProps) => { const [cvs, setCVs] = useState(undefined); const [loading, setLoading] = useState(false); @@ -255,12 +256,9 @@ export const CVList = ({ diff --git a/src/components/utils/Button/ButtonMultiple.tsx b/src/components/utils/Button/ButtonMultiple.tsx index eb3e3acc6..fae31dfa6 100644 --- a/src/components/utils/Button/ButtonMultiple.tsx +++ b/src/components/utils/Button/ButtonMultiple.tsx @@ -30,7 +30,6 @@ interface ButtonMultipleProps { align?: 'left' | 'right'; buttons: { href?: string | { pathname: string; query: AnyToFix }; - newTab?: boolean; onClick?: () => void; toggle?: string; diff --git a/src/components/utils/CardList/CardList.stories.tsx b/src/components/utils/CardList/CardList.stories.tsx index 915f0ad0a..0a9119498 100644 --- a/src/components/utils/CardList/CardList.stories.tsx +++ b/src/components/utils/CardList/CardList.stories.tsx @@ -1,7 +1,7 @@ import { Meta, StoryObj } from '@storybook/react'; import React from 'react'; import { v4 as uuid } from 'uuid'; -import { ProfileCard } from '../Card/ProfileCard'; +import { ProfileCard } from 'src/components/utils/Cards/ProfileCard'; import { USER_ROLES } from 'src/constants/users'; import { CardList } from './CardList'; import { CardListItem } from './CardListItem'; diff --git a/src/components/utils/CardList/CardList.styles.ts b/src/components/utils/CardList/CardList.styles.ts index 441efb6f8..279da8fcf 100644 --- a/src/components/utils/CardList/CardList.styles.ts +++ b/src/components/utils/CardList/CardList.styles.ts @@ -1,5 +1,4 @@ import styled from 'styled-components'; -import { COLORS } from 'src/constants/styles'; export const StyledCardListContainer = styled.div` display: flex; @@ -11,18 +10,11 @@ export const StyledCardList = styled.ul` list-style-type: none; display: flex; flex-wrap: wrap; - gap: 32px; + gap: 56px; justify-content: center; padding: 0; `; -export const StyledCardListNoResult = styled.div` - flex: 1; - font-size: 14px; - font-style: italic; - color: ${COLORS.darkGray}; -`; - export const StyledCardListSpinnerContainer = styled.div` display: flex; justify-content: center; diff --git a/src/components/utils/CardList/CardList.tsx b/src/components/utils/CardList/CardList.tsx index 35f457e22..f371bc38d 100644 --- a/src/components/utils/CardList/CardList.tsx +++ b/src/components/utils/CardList/CardList.tsx @@ -1,9 +1,9 @@ import React from 'react'; import { Spinner } from '../Spinner'; +import { StyledNoResult } from 'src/components/backoffice/Backoffice.styles'; import { StyledCardList, StyledCardListContainer, - StyledCardListNoResult, StyledCardListSpinnerContainer, } from './CardList.styles'; @@ -26,7 +26,7 @@ export function CardList({ > {list.length > 0 && list} {list.length === 0 && !isLoading && ( - Aucun résultat. + Aucun résultat )} {isLoading && ( diff --git a/src/components/utils/Card/Card.stories.tsx b/src/components/utils/Cards/Card/Card.stories.tsx similarity index 100% rename from src/components/utils/Card/Card.stories.tsx rename to src/components/utils/Cards/Card/Card.stories.tsx diff --git a/src/components/utils/Card/Card.styles.tsx b/src/components/utils/Cards/Card/Card.styles.tsx similarity index 77% rename from src/components/utils/Card/Card.styles.tsx rename to src/components/utils/Cards/Card/Card.styles.tsx index 835c1f110..bbbcfe984 100644 --- a/src/components/utils/Card/Card.styles.tsx +++ b/src/components/utils/Cards/Card/Card.styles.tsx @@ -1,29 +1,15 @@ -import styled, { css } from 'styled-components'; +import styled from 'styled-components'; +import { StyledCardCommon } from '../Cards.styles'; import { COLORS } from 'src/constants/styles'; -export const StyledCard = styled.div` - background-color: #fff; - box-shadow: 0px 4px 4px 0px ${COLORS.lightgray}; +export const StyledCard = styled(StyledCardCommon)` border-radius: 20px; - position: relative; border: 1px solid ${COLORS.lightgray}; &.mobile { width: 100%; box-sizing: border-box; } - - ${({ onClick }) => - onClick - ? css` - cursor: pointer; - transition: box-shadow 0.2s ease-in-out; - - &:hover { - box-shadow: 0px 8px 8px 0px ${COLORS.gray}; - } - ` - : ''} `; export const StyledCardTopContainer = styled.div` diff --git a/src/components/utils/Card/Card.tsx b/src/components/utils/Cards/Card/Card.tsx similarity index 100% rename from src/components/utils/Card/Card.tsx rename to src/components/utils/Cards/Card/Card.tsx diff --git a/src/components/utils/Card/index.ts b/src/components/utils/Cards/Card/index.ts similarity index 100% rename from src/components/utils/Card/index.ts rename to src/components/utils/Cards/Card/index.ts diff --git a/src/components/utils/Cards/Cards.styles.ts b/src/components/utils/Cards/Cards.styles.ts new file mode 100644 index 000000000..975c7352b --- /dev/null +++ b/src/components/utils/Cards/Cards.styles.ts @@ -0,0 +1,11 @@ +import styled from 'styled-components'; +import { COLORS } from 'src/constants/styles'; + +export const StyledCardCommon = styled.div` + display: flex; + flex: 1; + flex-direction: column; + background-color: ${COLORS.white}; + box-shadow: 0 4px 8px 0 ${COLORS.lightgray}; + position: relative; +`; diff --git a/src/components/utils/Card/ProfileCard.stories.tsx b/src/components/utils/Cards/ProfileCard/ProfileCard.stories.tsx similarity index 82% rename from src/components/utils/Card/ProfileCard.stories.tsx rename to src/components/utils/Cards/ProfileCard/ProfileCard.stories.tsx index c9888a86d..130896db2 100644 --- a/src/components/utils/Card/ProfileCard.stories.tsx +++ b/src/components/utils/Cards/ProfileCard/ProfileCard.stories.tsx @@ -1,5 +1,4 @@ import { Meta, StoryObj } from '@storybook/react'; -import React from 'react'; import { v4 as uuid } from 'uuid'; import { USER_ROLES } from 'src/constants/users'; import { ProfileCard } from './ProfileCard'; @@ -7,21 +6,6 @@ import { ProfileCard } from './ProfileCard'; const meta = { title: 'Profile Card', component: ProfileCard, - decorators: [ - (Story) => { - return ( -
- -
- ); - }, - ], } satisfies Meta; export default meta; diff --git a/src/components/utils/Card/ProfileCard.styles.ts b/src/components/utils/Cards/ProfileCard/ProfileCard.styles.ts similarity index 85% rename from src/components/utils/Card/ProfileCard.styles.ts rename to src/components/utils/Cards/ProfileCard/ProfileCard.styles.ts index a9dcb0c9d..a756b643c 100644 --- a/src/components/utils/Card/ProfileCard.styles.ts +++ b/src/components/utils/Cards/ProfileCard/ProfileCard.styles.ts @@ -1,18 +1,24 @@ import styled from 'styled-components'; +import { StyledCardCommon } from '../Cards.styles'; import { COLORS } from 'src/constants/styles'; -export const StyledProfileCard = styled.div` - width: 300px; +export const StyledProfileCard = styled(StyledCardCommon)` + width: 295px; + border-radius: 10px; + border: 1px solid ${COLORS.gray}; + cursor: pointer; + transition: box-shadow 0.2s ease-in-out; - > * { - display: flex; - flex: 1; - flex-direction: column; + &:hover { + box-shadow: 0 8px 16px 0 ${COLORS.gray}; } `; export const StyledProfileCardPictureContainer = styled.div` position: relative; + margin-left: -1px; + margin-right: -1px; + margin-top: -1px; `; export const StyledProfileCardPicture = styled.div` @@ -83,6 +89,7 @@ export const StyledProfileCardEmptyJobContainer = styled.div` display: flex; flex-direction: column; margin-bottom: 12px; + > h4 { line-height: 24px; margin-bottom: 0; @@ -93,6 +100,13 @@ export const StyledProfileCardJobContainer = styled.div` display: flex; flex-direction: column; margin-bottom: 12px; + overflow-wrap: break-word; + text-overflow: ellipsis; + overflow: hidden; + display: -webkit-box !important; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + white-space: normal; > h4 { line-height: 24px; diff --git a/src/components/utils/Card/ProfileCard.tsx b/src/components/utils/Cards/ProfileCard/ProfileCard.tsx similarity index 94% rename from src/components/utils/Card/ProfileCard.tsx rename to src/components/utils/Cards/ProfileCard/ProfileCard.tsx index 51ed7dab9..1e5a16019 100644 --- a/src/components/utils/Card/ProfileCard.tsx +++ b/src/components/utils/Cards/ProfileCard/ProfileCard.tsx @@ -1,15 +1,15 @@ import _ from 'lodash'; -import { useRouter } from 'next/router'; +import Link from 'next/link'; import React, { useMemo } from 'react'; import HandsIcon from 'assets/icons/illu-coeur-mains-ouvertes.svg'; import CaseIcon from 'assets/icons/illu-malette.svg'; -import { HelpNames, UserCandidateWithUsers } from 'src/api/types'; +import { UserCandidateWithUsers } from 'src/api/types'; import { H3, H4, H5 } from 'src/components/utils/Headings'; import { Img } from 'src/components/utils/Img'; import { Tag } from 'src/components/utils/Tag'; import { BUSINESS_LINES, BusinessLineValue } from 'src/constants'; import { Department } from 'src/constants/departements'; -import { ProfileCardHelps } from 'src/constants/helps'; +import { HelpNames, ProfileHelps } from 'src/constants/helps'; import { COLORS } from 'src/constants/styles'; import { GA_TAGS } from 'src/constants/tags'; import { @@ -20,7 +20,6 @@ import { import { useImageFallback } from 'src/hooks/useImageFallback'; import { gaEvent } from 'src/lib/gtag'; import { findConstantFromValue, isRoleIncluded, sortByOrder } from 'src/utils'; -import { Card } from './Card'; import { StyledProfileCard, StyledProfileCardBusinessLines, @@ -99,8 +98,6 @@ export function ProfileCard({ userCandidate, job, }: ProfileCardProps) { - const { push } = useRouter(); - const { urlImg, fallbackToCVImage } = useImageFallback({ userId, role, @@ -118,16 +115,16 @@ export function ProfileCard({ ambitions && ambitions.length > 0 ? sortByOrder(ambitions) : null; return ( - - { - gaEvent(GA_TAGS.PAGE_ANNUAIRE_CARTE_CLIC); - push({ - pathname: `/backoffice/profile/[userId]`, - query: { userId }, - }); - }} - > + { + gaEvent(GA_TAGS.PAGE_ANNUAIRE_CARTE_CLIC); + }} + > + {urlImg ? ( @@ -244,7 +241,7 @@ export function ProfileCard({ {helps && helps.length > 0 ? ( helps.map(({ name }) => { - const help = findConstantFromValue(name, ProfileCardHelps); + const help = findConstantFromValue(name, ProfileHelps); return ( {help.icon} @@ -267,7 +264,7 @@ export function ProfileCard({ - - + + ); } diff --git a/src/components/utils/Cards/ProfileCard/index.ts b/src/components/utils/Cards/ProfileCard/index.ts new file mode 100644 index 000000000..d6fa6ff6c --- /dev/null +++ b/src/components/utils/Cards/ProfileCard/index.ts @@ -0,0 +1 @@ +export * from './ProfileCard'; diff --git a/src/components/utils/Inputs/SelectList/SelectList.stories.tsx b/src/components/utils/Inputs/SelectList/SelectList.stories.tsx index 11864eda9..56fb4d214 100644 --- a/src/components/utils/Inputs/SelectList/SelectList.stories.tsx +++ b/src/components/utils/Inputs/SelectList/SelectList.stories.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { StyledHelpModalSelectOption } from 'src/components/backoffice/parametres/ParametresLayout/ParametresHelpCard/ParametresHelpCard.styles'; -import { Card } from 'src/components/utils/Card'; +import { Card } from 'src/components/utils/Cards/Card'; import { H6 } from 'src/components/utils/Headings'; import { ParametresHelpCardContents } from 'src/constants/helps'; import { USER_ROLES } from 'src/constants/users'; diff --git a/src/components/utils/index.ts b/src/components/utils/index.ts index 53fabb854..c4aacd34f 100644 --- a/src/components/utils/index.ts +++ b/src/components/utils/index.ts @@ -1,7 +1,7 @@ export * from './AnimatedList'; export * from './Background'; export * from './ButtonIcon'; -export * from './Card'; +export * from './Cards/Card'; export * from './Tag'; export * from './Grid'; export * from './Carousel'; diff --git a/src/constants/departements.ts b/src/constants/departements.ts index eb7627dd4..74a5951c3 100644 --- a/src/constants/departements.ts +++ b/src/constants/departements.ts @@ -532,6 +532,7 @@ export const DEPARTMENTS = [ ] as const; export type Department = (typeof DEPARTMENTS)[number]['name']; +export type Region = (typeof DEPARTMENTS)[number]['region']; export const REGIONS_LABELS = { 'Île-de-France': 'Paris et sa région', 'Auvergne-Rhône-Alpes': 'Lyon et sa région', @@ -540,7 +541,7 @@ export const REGIONS_LABELS = { Lorient: 'Lorient', } as const; -export const REGIONS_FILTERS = _.sortBy( +export const REGIONS_FILTERS: FilterConstant[] = _.sortBy( Object.values( DEPARTMENTS.reduce((acc, curr) => { if (acc[curr.region]) { diff --git a/src/constants/helps.tsx b/src/constants/helps.tsx index 0813a30a3..a972d18d2 100644 --- a/src/constants/helps.tsx +++ b/src/constants/helps.tsx @@ -4,15 +4,15 @@ import ConversationIllu from 'assets/icons/illu-conversation.svg'; import MaletteIllu from 'assets/icons/illu-malette.svg'; import TipsIllu from 'assets/icons/illu-poignee-de-main.svg'; import RSIllu from 'assets/icons/illu-reseaux-sociaux.svg'; -import { HelpNames } from 'src/api/types'; import { USER_ROLES } from './users'; import { FilterConstant } from './utils'; -export const ProfileCardHelps: (FilterConstant & { +export type HelpNames = 'tips' | 'interview' | 'cv' | 'network' | 'event'; + +export const ProfileHelps: (FilterConstant & { icon: JSX.Element; shortTitle: { - Candidat: string; - Coach: string; + [K in typeof USER_ROLES.CANDIDATE | typeof USER_ROLES.COACH]: string; }; })[] = [ { @@ -20,8 +20,8 @@ export const ProfileCardHelps: (FilterConstant & { value: 'tips', label: 'Soutien', shortTitle: { - Candidat: 'Demander un conseil', - Coach: 'Conseiller un(e) candidat(e)', + [USER_ROLES.CANDIDATE]: 'Demander un conseil', + [USER_ROLES.COACH]: 'Conseiller un(e) candidat(e)', }, }, { @@ -29,8 +29,8 @@ export const ProfileCardHelps: (FilterConstant & { value: 'interview', label: 'Entretien', shortTitle: { - Candidat: 'Préparer un entretien', - Coach: 'Aider à préparer un entretien', + [USER_ROLES.CANDIDATE]: 'Préparer un entretien', + [USER_ROLES.COACH]: 'Aider à préparer un entretien', }, }, { @@ -38,8 +38,8 @@ export const ProfileCardHelps: (FilterConstant & { value: 'cv', label: 'CV', shortTitle: { - Candidat: 'Créer mon CV', - Coach: 'Aider à réaliser un CV', + [USER_ROLES.CANDIDATE]: 'Créer mon CV', + [USER_ROLES.COACH]: 'Aider à réaliser un CV', }, }, { @@ -47,8 +47,8 @@ export const ProfileCardHelps: (FilterConstant & { value: 'event', label: 'Événement', shortTitle: { - Candidat: 'Rencontrer la communauté', - Coach: 'Rencontrer la communauté', + [USER_ROLES.CANDIDATE]: 'Rencontrer la communauté', + [USER_ROLES.COACH]: 'Rencontrer la communauté', }, }, { @@ -56,8 +56,8 @@ export const ProfileCardHelps: (FilterConstant & { value: 'network', label: 'Partage', shortTitle: { - Candidat: 'Développer mon réseau', - Coach: 'Partager mon réseau', + [USER_ROLES.CANDIDATE]: 'Développer mon réseau', + [USER_ROLES.COACH]: 'Partager mon réseau', }, }, ]; diff --git a/src/constants/index.ts b/src/constants/index.ts index 405409784..bdc371cca 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -6,9 +6,9 @@ import { REGIONS_FILTERS, } from 'src/constants/departements'; import { GA_TAGS } from 'src/constants/tags'; - +import { ProfileHelps } from './helps'; import { GENDERS_FILTERS, USER_ROLES_FILTERS } from './users'; -import { FilterConstant } from './utils'; +import { Filter, FilterConstant } from './utils'; export type OfferStatus = -1 | 0 | 1 | 2 | 3 | 4; @@ -297,7 +297,7 @@ export const OFFER_ADMIN_FILTERS_DATA = [ { tag: 'archived', title: 'Offres archivées' }, ]; -export const CV_FILTERS_DATA = [ +export const CV_FILTERS_DATA: Filter[] = [ { key: 'employed', type: 'checkbox', @@ -338,7 +338,7 @@ export const CV_FILTERS_DATA = [ }, ]; -export const OPPORTUNITY_FILTERS_DATA = [ +export const OPPORTUNITY_FILTERS_DATA: Filter[] = [ { key: 'isPublic', constants: [ @@ -377,7 +377,7 @@ export const OPPORTUNITY_FILTERS_DATA = [ }, ]; -export const ORGANIZATION_FILTERS_DATA = [ +export const ORGANIZATION_FILTERS_DATA: Filter[] = [ { key: 'zone', constants: ADMIN_ZONES_FILTERS, @@ -386,7 +386,7 @@ export const ORGANIZATION_FILTERS_DATA = [ }, ]; -export const MEMBER_FILTERS_DATA = [ +export const MEMBER_FILTERS_DATA: Filter[] = [ { key: 'role', constants: USER_ROLES_FILTERS, @@ -444,9 +444,28 @@ export const MEMBER_FILTERS_DATA = [ title: 'Statut du CV', tag: GA_TAGS.BACKOFFICE_MEMBERS_FILTRE_STATUT_CV_CLIC, }, -] as const; +]; -export type MEMBER_FILTERS_CONSTANT = (typeof MEMBER_FILTERS_DATA)[number]; +export const DirectoryFilters: Filter[] = [ + { + key: 'departments', + constants: DEPARTMENTS_FILTERS, + title: 'Département', + tag: GA_TAGS.PAGE_ANNUAIRE_FILTRE_DEPARTEMENT_CLIC, + }, + { + key: 'helps', + constants: ProfileHelps, + title: "Type d'aide", + tag: GA_TAGS.PAGE_ANNUAIRE_FILTRE_AIDE_CLIC, + }, + { + key: 'businessLines', + constants: BUSINESS_LINES, + title: "Secteur d'activité", + tag: GA_TAGS.PAGE_ANNUAIRE_FILTRE_AIDE_CLIC, + }, +]; export type ExternalOfferOrigin = 'network' | 'internet' | 'counselor'; diff --git a/src/constants/tags.ts b/src/constants/tags.ts index 4feab0942..40b21c5ba 100644 --- a/src/constants/tags.ts +++ b/src/constants/tags.ts @@ -395,6 +395,21 @@ export const GA_TAGS = { PROFILE_DETAILS_CONTACT_SEND_CLIC: { action: 'Profile_Details_Contact_Send_Clic', }, + PAGE_ANNUAIRE_FILTRE_ROLE_CLIC: { + action: 'Page_Annuaire_Filtre_Role_Clic', + }, + PAGE_ANNUAIRE_FILTRE_DEPARTEMENT_CLIC: { + action: 'Page_Annuaire_Filtre_Departement_Clic', + }, + PAGE_ANNUAIRE_FILTRE_AIDE_CLIC: { + action: 'Page_Annuaire_Filtre_Aide_Clic', + }, + PAGE_ANNUAIRE_FILTRE_SECTEUR_CLIC: { + action: 'Page_Annuaire_Filtre_Secteur_Clic', + }, + PAGE_ANNUAIRE_SUPPRIMER_FILTRES_CLIC: { + action: 'Page_Annuaire_Supprimer_Filtres_Clic', + }, } as const; export const FB_TAGS = { diff --git a/src/constants/users.ts b/src/constants/users.ts index f3bc9006d..63d2e738d 100644 --- a/src/constants/users.ts +++ b/src/constants/users.ts @@ -65,4 +65,4 @@ export const GENDERS_FILTERS = [ label: 'Femme', value: GENDERS.FEMALE, }, -] as const; +]; diff --git a/src/constants/utils.ts b/src/constants/utils.ts index 83261d9f6..ac164612d 100644 --- a/src/constants/utils.ts +++ b/src/constants/utils.ts @@ -17,3 +17,23 @@ export const ActionsLabels = { export type FilterConstant< T extends string | number | boolean = string | number | boolean > = { value: T; label: string }; + +export interface Filter< + T extends string | number | boolean = string | number | boolean +> { + key: string; + constants: FilterConstant[]; + title: string; + tag?: { + action: string; + }; + mandatory?: boolean; + priority?: FilterConstant[]; + type?: 'checkbox'; + icon?: React.ReactNode; + disabled?: boolean; +} + +export type FilterObject = { + [K in T[number]['key']]: T[number]['constants']; +}; diff --git a/src/hooks/useFilters.ts b/src/hooks/useFilters.ts index 1741f403e..abf49032b 100644 --- a/src/hooks/useFilters.ts +++ b/src/hooks/useFilters.ts @@ -1,26 +1,29 @@ import _ from 'lodash'; import { useRouter } from 'next/router'; import { useCallback } from 'react'; +import { Filter } from 'src/constants/utils'; import { gaEvent } from 'src/lib/gtag'; import { filtersToQueryParams, getFiltersObjectsFromQueryParamsFront, } from 'src/utils/Filters'; -export function useFilters(filtersData, path, otherPathParams?, resetTag?) { +export function useFilters( + filtersData: Filter[], + path: string, + otherPathParams?: string[], + resetTag?: { action: string } +) { const { push, query: originalQuery } = useRouter(); - const // @ts-expect-error after enable TS strict mode. Please, try to fix it - { - search, - ...params - }: { - // @ts-expect-error after enable TS strict mode. Please, try to fix it - search?: string; - [key: string]: string | string[]; - } = otherPathParams - ? _.omit(originalQuery, otherPathParams) - : originalQuery; + const { + search: querySearch, + ...params + }: { + [key: string]: string | string[] | undefined; + } = otherPathParams ? _.omit(originalQuery, otherPathParams) : originalQuery; + + const search = querySearch as string | undefined; const otherParams = _.omit( params, diff --git a/src/pages/backoffice/admin/offres/index.tsx b/src/pages/backoffice/admin/offres/index.tsx index 7cc88b3de..b748b970e 100644 --- a/src/pages/backoffice/admin/offres/index.tsx +++ b/src/pages/backoffice/admin/offres/index.tsx @@ -3,7 +3,6 @@ import React, { useEffect, useState } from 'react'; import { LayoutBackOffice } from 'src/components/backoffice/LayoutBackOffice'; import { LoadingScreen } from 'src/components/backoffice/LoadingScreen'; import { AdminOpportunities } from 'src/components/backoffice/admin/AdminOpportunities'; -import { AdminOpportunitiesFilters } from 'src/components/backoffice/admin/AdminOpportunities/AdminOpportunitiesFilters.types'; import { Section } from 'src/components/utils'; import { OFFER_ADMIN_FILTERS_DATA, @@ -35,8 +34,6 @@ const AdminOpportunitiesPage = () => { GA_TAGS.BACKOFFICE_ADMIN_SUPPRIMER_FILTRES_CLIC ); - let content; - // redirect with default tag and departments useEffect(() => { if (user.role !== USER_ROLES.ADMIN) { @@ -97,26 +94,20 @@ const AdminOpportunitiesPage = () => { } }, [offerId, replace, restParams, tag, user, candidateId]); - if ( - // loading || - !user - ) { - content = ; - } else { - content = ( - - ); - } return (
- {!user || loadingDefaultFilters ? : content} + {loadingDefaultFilters ? ( + + ) : ( + + )}
); diff --git a/src/pages/backoffice/annuaire.tsx b/src/pages/backoffice/annuaire.tsx index ad0cde6f8..6f100cd70 100644 --- a/src/pages/backoffice/annuaire.tsx +++ b/src/pages/backoffice/annuaire.tsx @@ -1,14 +1,15 @@ import React from 'react'; import { useSelector } from 'react-redux'; import { LayoutBackOffice } from 'src/components/backoffice/LayoutBackOffice'; +import { LoadingScreen } from 'src/components/backoffice/LoadingScreen'; import { DirectoryContainer } from 'src/components/backoffice/directory/DirectoryContainer/DirectoryContainer'; -import { useDirectoryRoleFilter } from 'src/components/backoffice/directory/useDirectoryRoleFilter'; +import { useDirectoryFilters } from 'src/components/backoffice/directory/useDirectoryFilters'; import { HeaderBackoffice } from 'src/components/headers/HeaderBackoffice'; import { Section } from 'src/components/utils'; import { selectProfilesRoleFilter } from 'src/use-cases/profiles'; const Annuaire = () => { - useDirectoryRoleFilter(); + useDirectoryFilters(); const role = useSelector(selectProfilesRoleFilter); return ( @@ -20,7 +21,7 @@ const Annuaire = () => { "Découvrez les membres de la communauté et développez votre carnet d'adresse." } /> - {role && } + {role ? : }
); diff --git a/src/pages/backoffice/candidat/[candidateId]/offres/index.tsx b/src/pages/backoffice/candidat/[candidateId]/offres/index.tsx index 9c78eb132..e9d8457a8 100644 --- a/src/pages/backoffice/candidat/[candidateId]/offres/index.tsx +++ b/src/pages/backoffice/candidat/[candidateId]/offres/index.tsx @@ -7,7 +7,6 @@ import { Api } from 'src/api'; import { LayoutBackOffice } from 'src/components/backoffice/LayoutBackOffice'; import { LoadingScreen } from 'src/components/backoffice/LoadingScreen'; import { CandidateOpportunities } from 'src/components/backoffice/candidate/CandidateOpportunities'; -import { CandidateOpportunitiesFilters } from 'src/components/backoffice/candidate/CandidateOpportunities/CandidateOpportunitiesFilters.types'; import { OpportunityError } from 'src/components/backoffice/opportunities/OpportunityError'; import { Section } from 'src/components/utils'; import { OPPORTUNITY_FILTERS_DATA } from 'src/constants'; @@ -21,7 +20,7 @@ import { useOpportunityType } from 'src/hooks/queryParams/useOpportunityType'; import { useQueryParamsOpportunities } from 'src/hooks/queryParams/useQueryParamsOpportunities'; import { useFilters } from 'src/hooks/useFilters'; import { usePrevious } from 'src/hooks/utils'; -import { isRoleIncluded, getCandidateFromCoach } from 'src/utils/Finding'; +import { getCandidateFromCoach, isRoleIncluded } from 'src/utils/Finding'; // filters for the query const candidateQueryFilters = OPPORTUNITY_FILTERS_DATA.slice(1); @@ -168,10 +167,12 @@ const Opportunities = () => { // Et si on a pas d'autres paramètres qui indiquerait qu'on est pas sur un premier chargement de la page // Appliquer les filtres par défaut setCandidateDefaultsIfPublicTag(candidate.id, candidate.zone); - } else { + } else if (!hasLoadedDefaultFilters) { // Si on a déjà appliqué les filtres par défaut // Ou si on a d'autres paramètres donc ce n'est pas un premier chargement de la page setHasLoadedDefaultFilters(true); + } else { + setLoading(false); } } else { // Dernier cas pour la rétrocompatibilité. @@ -208,11 +209,7 @@ const Opportunities = () => { ]); let content; - if ( - loading || - !user || - (opportunityType === 'public' && !hasLoadedDefaultFilters) - ) { + if (loading || (opportunityType === 'public' && !hasLoadedDefaultFilters)) { content = ; } else if (hasError) { content = ; @@ -233,7 +230,7 @@ const Opportunities = () => { content = ( { fetchSelectedProfileSelectors.selectIsFetchSelectedProfileRequested ); - if (selectedProfile && !isFetchProfileRequested) { - return ; - } - return ; + return ( + + {selectedProfile && !isFetchProfileRequested ? ( + + ) : ( + + )} + + ); }; export default PageProfile; diff --git a/src/pages/reset/[id]/[token].tsx b/src/pages/reset/[id]/[token].tsx index f064a529e..e3cea65d8 100644 --- a/src/pages/reset/[id]/[token].tsx +++ b/src/pages/reset/[id]/[token].tsx @@ -14,6 +14,7 @@ interface ResetPasswordPageProps { token: string; isCreation: boolean; } + const ResetPasswordPage = ({ valid, id, @@ -60,7 +61,11 @@ const ResetPasswordPage = ({ ) : (
- +

Ce lien ne semble pas valide. Veuillez contacter l'équipe LinkedOut. diff --git a/src/use-cases/profiles/profiles.saga.ts b/src/use-cases/profiles/profiles.saga.ts index ba713f5ef..4b74c4660 100644 --- a/src/use-cases/profiles/profiles.saga.ts +++ b/src/use-cases/profiles/profiles.saga.ts @@ -13,14 +13,20 @@ const { fetchProfilesRequested, fetchProfilesSucceeded, fetchProfilesFailed, + setProfilesFilters, + setProfilesSearchFilter, setProfilesRoleFilter, + setProfilesHelpsFilter, + setProfilesBusinessLinesFilter, + setProfilesDepartmentsFilter, incrementProfilesOffset, + resetProfilesOffset, postInternalMessageRequested, postInternalMessageSucceeded, postInternalMessageFailed, } = slice.actions; -function* fetchProfilesSagaRequested() { +function* fetchProfilesNextPageSaga() { const hasFetchedAll = yield* select(selectProfilesHasFetchedAll); if (!hasFetchedAll) { @@ -28,6 +34,11 @@ function* fetchProfilesSagaRequested() { } } +function* fetchProfilesUpdatedFiltersSaga() { + yield* put(resetProfilesOffset()); + yield* put(fetchProfilesRequested()); +} + function* fetchProfilesSaga() { try { const filters = yield* select(selectProfilesFilters); @@ -69,8 +80,19 @@ function* postInternalMessageSaga( export function* saga() { yield* takeLatest(fetchProfilesRequested, fetchProfilesSaga); - yield* takeLatest(setProfilesRoleFilter, fetchProfilesSagaRequested); - yield* takeLatest(incrementProfilesOffset, fetchProfilesSagaRequested); + yield* takeLatest(setProfilesFilters, fetchProfilesUpdatedFiltersSaga); + yield* takeLatest(setProfilesSearchFilter, fetchProfilesUpdatedFiltersSaga); + yield* takeLatest(setProfilesRoleFilter, fetchProfilesUpdatedFiltersSaga); + yield* takeLatest(setProfilesHelpsFilter, fetchProfilesUpdatedFiltersSaga); + yield* takeLatest( + setProfilesBusinessLinesFilter, + fetchProfilesUpdatedFiltersSaga + ); + yield* takeLatest( + setProfilesDepartmentsFilter, + fetchProfilesUpdatedFiltersSaga + ); + yield* takeLatest(incrementProfilesOffset, fetchProfilesNextPageSaga); yield* takeLatest(fetchSelectedProfileRequested, fetchSelectedProfileSaga); yield* takeLatest(postInternalMessageRequested, postInternalMessageSaga); } diff --git a/src/use-cases/profiles/profiles.selectors.ts b/src/use-cases/profiles/profiles.selectors.ts index 82b8699de..2adaef0c0 100644 --- a/src/use-cases/profiles/profiles.selectors.ts +++ b/src/use-cases/profiles/profiles.selectors.ts @@ -24,14 +24,34 @@ export function selectProfiles(state: RootState) { return state.profiles.profiles; } +export function selectProfilesIsResetFilters(state: RootState) { + return state.profiles.profilesIsResetFilters; +} + export function selectProfilesFilters(state: RootState) { return state.profiles.profilesFilters; } +export function selectProfilesSearchFilter(state: RootState) { + return state.profiles.profilesFilters.search; +} + export function selectProfilesRoleFilter(state: RootState) { return state.profiles.profilesFilters.role; } +export function selectProfilesHelpsFilters(state: RootState) { + return state.profiles.profilesFilters.helps; +} + +export function selectProfilesDepartmentsFilters(state: RootState) { + return state.profiles.profilesFilters.departments; +} + +export function selectProfilesBusinessLinesFilters(state: RootState) { + return state.profiles.profilesFilters.businessLines; +} + export function selectProfilesHasFetchedAll(state: RootState) { return state.profiles.profilesHasFetchedAll; } diff --git a/src/use-cases/profiles/profiles.slice.ts b/src/use-cases/profiles/profiles.slice.ts index db335147d..395c8a32e 100644 --- a/src/use-cases/profiles/profiles.slice.ts +++ b/src/use-cases/profiles/profiles.slice.ts @@ -1,7 +1,11 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { PublicProfile } from 'src/api/types'; -import { UserRole } from 'src/constants/users'; +import { BusinessLineValue } from 'src/constants'; +import { Department } from 'src/constants/departements'; +import { HelpNames } from 'src/constants/helps'; +import { CANDIDATE_USER_ROLES, UserRole } from 'src/constants/users'; import { RequestState, SliceRootState } from 'src/store/utils'; +import { mutateToArray } from 'src/utils'; import { fetchProfilesAdapter, fetchSelectedProfileAdapter, @@ -15,8 +19,17 @@ export interface State { fetchSelectedProfile: RequestState; postInternalMessage: RequestState; profiles: PublicProfile[]; - profilesFilters: { role?: UserRole[]; offset: number; limit: typeof LIMIT }; + profilesFilters: { + role: UserRole[]; + offset: number; + limit: typeof LIMIT; + search: string | null; + helps: HelpNames[]; + departments: Department[]; + businessLines: BusinessLineValue[]; + }; profilesHasFetchedAll: boolean; + profilesIsResetFilters: boolean; selectedProfile: PublicProfile | null; } @@ -25,8 +38,17 @@ const initialState: State = { fetchSelectedProfile: fetchSelectedProfileAdapter.getInitialState(), postInternalMessage: fetchProfilesAdapter.getInitialState(), profiles: [], - profilesFilters: { offset: 0, limit: LIMIT }, + profilesFilters: { + role: CANDIDATE_USER_ROLES, + offset: 0, + limit: LIMIT, + search: null, + helps: [], + departments: [], + businessLines: [], + }, profilesHasFetchedAll: false, + profilesIsResetFilters: false, selectedProfile: null, }; @@ -41,6 +63,7 @@ export const slice = createSlice({ ? action.payload : [...state.profiles, ...action.payload]; state.profilesHasFetchedAll = action.payload.length < LIMIT; + state.profilesIsResetFilters = false; }, }), ...fetchSelectedProfileAdapter.getReducers( @@ -55,10 +78,66 @@ export const slice = createSlice({ (state) => state.postInternalMessage, {} ), + setProfilesFilters( + state, + action: PayloadAction<{ + role: UserRole[]; + search: string | null; + helps: HelpNames | HelpNames[]; + departments: Department | Department[]; + businessLines: BusinessLineValue | BusinessLineValue[]; + }> + ) { + state.profilesFilters = { + ...state.profilesFilters, + ...action.payload, + departments: mutateToArray(action.payload.departments), + businessLines: mutateToArray(action.payload.businessLines), + helps: mutateToArray(action.payload.helps), + }; + }, + setProfilesHelpsFilter( + state, + action: PayloadAction + ) { + state.profilesFilters = { + ...state.profilesFilters, + helps: mutateToArray(action.payload), + }; + }, + setProfilesBusinessLinesFilter( + state, + action: PayloadAction + ) { + state.profilesFilters = { + ...state.profilesFilters, + businessLines: mutateToArray(action.payload), + }; + }, + setProfilesDepartmentsFilter( + state, + action: PayloadAction + ) { + state.profilesFilters = { + ...state.profilesFilters, + departments: mutateToArray(action.payload), + }; + }, setProfilesRoleFilter(state, action: PayloadAction) { state.profilesFilters = { ...state.profilesFilters, role: action.payload, + }; + }, + setProfilesSearchFilter(state, action: PayloadAction) { + state.profilesFilters = { + ...state.profilesFilters, + search: action.payload, + }; + }, + resetProfilesOffset(state) { + state.profilesFilters = { + ...state.profilesFilters, offset: 0, }; state.profilesHasFetchedAll = false; @@ -72,6 +151,9 @@ export const slice = createSlice({ : state.profilesFilters.offset + LIMIT, }; }, + resetProfilesFilters(state) { + state.profilesIsResetFilters = true; + }, }, }); diff --git a/src/utils/Filters.ts b/src/utils/Filters.ts index e6ee4d264..a10a2b66b 100644 --- a/src/utils/Filters.ts +++ b/src/utils/Filters.ts @@ -1,21 +1,22 @@ import _ from 'lodash'; -import { MEMBER_FILTERS_DATA, MEMBER_FILTERS_CONSTANT } from 'src/constants'; +import { MEMBER_FILTERS_DATA } from 'src/constants'; import { CANDIDATE_USER_ROLES, COACH_USER_ROLES, UserRole, } from 'src/constants/users'; +import { Filter } from 'src/constants/utils'; import { isRoleIncluded } from './Finding'; const filterMemberTypeConstantsByRole = ( roles: typeof CANDIDATE_USER_ROLES | typeof COACH_USER_ROLES -): MEMBER_FILTERS_CONSTANT => { +): Filter => { return { ...MEMBER_FILTERS_DATA[0], constants: MEMBER_FILTERS_DATA[0].constants.filter(({ value }) => { - return isRoleIncluded(roles, value); - }) as MEMBER_FILTERS_CONSTANT['constants'], - } as MEMBER_FILTERS_CONSTANT; + return isRoleIncluded(roles, value as UserRole); + }), + }; }; export const mutateTypeFilterDependingOnRole = ( diff --git a/src/utils/Mutating.ts b/src/utils/Mutating.ts index af78ece81..2ff374926 100644 --- a/src/utils/Mutating.ts +++ b/src/utils/Mutating.ts @@ -1,6 +1,7 @@ import { Opportunity, OpportunityUser } from 'src/api/types'; import { OFFER_STATUS } from 'src/constants'; import { findOfferStatus } from 'src/utils/Finding'; +import { AnyCantFix } from './Types'; const getAlternateDefaultOfferStatus = ( offer: Opportunity = {} as Opportunity, @@ -22,3 +23,15 @@ export const mutateDefaultOfferStatus = (offer, opportunityUser) => { ...OFFER_STATUS.slice(1), ]; }; + +export function mutateToArray( + value: T | null | undefined +): T extends AnyCantFix[] | null | undefined ? T : T[] { + if (value === null || value === undefined) { + return value as T extends AnyCantFix[] | null | undefined ? T : never; + } + if (Array.isArray(value)) { + return value as T extends AnyCantFix[] | null | undefined ? T : never; + } + return [value] as T extends AnyCantFix[] | null | undefined ? never : T[]; +} From 23245962d66ca3ddd9537615d322287010824923 Mon Sep 17 00:00:00 2001 From: Emile Bex Date: Mon, 5 Feb 2024 17:33:38 +0100 Subject: [PATCH 07/21] [EN-6724] feat(availability): add availability card on dashboard (#199) * [EN-6723] feat(lko2-dashboard): layout and profile card * fix(directory): fix directory when click on external candidate * [EN-6724] feat(availability): add availability tag on profile card * [EN-6724] feat(availability): add availability tag profile * [EN-6724] feat(availability): add role to header * [EN-6724] feat(availability): refacto profile header * [EN-6724] feat(availability): add availability card on dashboard * [EN-6724] test(availability): fix test features with isAvailable field * [EN-6724] feat(availability): add ga tag on toggle availability --------- Co-authored-by: PaulEntourage --- .gitignore | 1 + cypress/e2e/candidat.cy.js | 8 +- cypress/fixtures/public-profile-res.json | 3 +- src/api/types.ts | 2 + .../backoffice/Backoffice.styles.tsx | 81 +-------- .../backoffice/LayoutBackOffice.tsx | 6 +- .../backoffice/dashboard/Dashboard.styles.tsx | 4 +- .../backoffice/dashboard/Dashboard.tsx | 12 ++ .../DashboardAvailabilityCard.tsx | 69 ++++++++ .../DashboardAvailabilityCard/index.ts | 1 + .../DashboardProfileCard.tsx | 2 + .../directory/DirectoryItem/DirectoryItem.tsx | 3 + .../directory/DirectoryList/DirectoryList.tsx | 1 + .../HeaderParametres.styles.tsx | 26 --- .../HeaderParametres/HeaderParametres.tsx | 158 ------------------ .../ParametresDescription.styles.tsx | 22 --- .../ParametresDescription.tsx | 42 ----- .../ParametresDescription/index.ts | 1 - .../HeaderParametres/index.ts | 1 - .../ParametresLayout.styles.tsx | 4 +- .../ParametresLayout/ParametresLayout.tsx | 15 +- .../parametres/useUpdateProfile.tsx | 8 +- .../backoffice/parametres/useUpdateUser.tsx | 8 +- .../HeaderProfile/HeaderProfile.styles.tsx | 5 - .../profile/HeaderProfile/HeaderProfile.tsx | 95 ----------- .../backoffice/profile/HeaderProfile/index.ts | 1 - .../backoffice/profile/Profile.styles.tsx | 4 +- src/components/backoffice/profile/Profile.tsx | 17 +- .../ProfileContactCard.styles.tsx | 4 +- .../ProfileContactCard/ProfileContactCard.tsx | 61 ++++--- .../ProfileHelpInformationCard.tsx | 1 + .../HeaderProfile/HeaderProfile.desktop.tsx | 113 +++++++++++++ .../HeaderProfile/HeaderProfile.mobile.tsx | 114 +++++++++++++ .../HeaderProfile/HeaderProfile.styles.tsx | 117 +++++++++++++ .../HeaderProfile/HeaderProfile.types.ts | 13 ++ .../ModalEditProfileDescription.tsx | 0 .../ModalEditProfileDescription/index.ts | 0 .../ProfileDescription.styles.tsx | 28 ++++ .../ProfileDescription/ProfileDescription.tsx | 49 ++++++ .../HeaderProfile/ProfileDescription/index.ts | 1 + src/components/headers/HeaderProfile/index.ts | 8 + .../headers/HeaderProfile/useHeaderProfile.ts | 32 ++++ .../HeaderProfile/useUploadProfileImage.ts | 20 +++ .../NewsletterPartial.styles.ts | 2 +- .../AvailabilityTag.stories.tsx | 22 +++ .../AvailabilityTag/AvailabilityTag.styles.ts | 23 +++ .../utils/AvailabilityTag/AvailabilityTag.tsx | 18 ++ src/components/utils/AvailabilityTag/index.ts | 1 + .../utils/CardList/CardList.stories.tsx | 4 + .../utils/Cards/Card/Card.styles.tsx | 8 +- src/components/utils/Cards/Card/Card.tsx | 9 - .../Cards/ProfileCard/ProfileCard.stories.tsx | 4 +- .../Cards/ProfileCard/ProfileCard.styles.ts | 9 + .../utils/Cards/ProfileCard/ProfileCard.tsx | 9 +- .../utils/ImgProfile/ImgProfile.tsx | 24 +-- .../ToggleWithModal.styles.tsx | 1 + .../ToggleWithModal/ToggleWithModal.tsx | 6 +- src/components/utils/Tag/Tag.styles.tsx | 1 + src/constants/tags.ts | 3 + src/hooks/useImageFallback.ts | 4 +- src/pages/login.tsx | 4 +- 61 files changed, 788 insertions(+), 525 deletions(-) create mode 100644 src/components/backoffice/dashboard/DashboardAvailabilityCard/DashboardAvailabilityCard.tsx create mode 100644 src/components/backoffice/dashboard/DashboardAvailabilityCard/index.ts delete mode 100644 src/components/backoffice/parametres/ParametresLayout/HeaderParametres/HeaderParametres.styles.tsx delete mode 100644 src/components/backoffice/parametres/ParametresLayout/HeaderParametres/HeaderParametres.tsx delete mode 100644 src/components/backoffice/parametres/ParametresLayout/HeaderParametres/ParametresDescription/ParametresDescription.styles.tsx delete mode 100644 src/components/backoffice/parametres/ParametresLayout/HeaderParametres/ParametresDescription/ParametresDescription.tsx delete mode 100644 src/components/backoffice/parametres/ParametresLayout/HeaderParametres/ParametresDescription/index.ts delete mode 100644 src/components/backoffice/parametres/ParametresLayout/HeaderParametres/index.ts delete mode 100644 src/components/backoffice/profile/HeaderProfile/HeaderProfile.styles.tsx delete mode 100644 src/components/backoffice/profile/HeaderProfile/HeaderProfile.tsx delete mode 100644 src/components/backoffice/profile/HeaderProfile/index.ts create mode 100644 src/components/headers/HeaderProfile/HeaderProfile.desktop.tsx create mode 100644 src/components/headers/HeaderProfile/HeaderProfile.mobile.tsx create mode 100644 src/components/headers/HeaderProfile/HeaderProfile.styles.tsx create mode 100644 src/components/headers/HeaderProfile/HeaderProfile.types.ts rename src/components/{backoffice/parametres/ParametresLayout/HeaderParametres/ParametresDescription => headers/HeaderProfile/ProfileDescription}/ModalEditProfileDescription/ModalEditProfileDescription.tsx (100%) rename src/components/{backoffice/parametres/ParametresLayout/HeaderParametres/ParametresDescription => headers/HeaderProfile/ProfileDescription}/ModalEditProfileDescription/index.ts (100%) create mode 100644 src/components/headers/HeaderProfile/ProfileDescription/ProfileDescription.styles.tsx create mode 100644 src/components/headers/HeaderProfile/ProfileDescription/ProfileDescription.tsx create mode 100644 src/components/headers/HeaderProfile/ProfileDescription/index.ts create mode 100644 src/components/headers/HeaderProfile/index.ts create mode 100644 src/components/headers/HeaderProfile/useHeaderProfile.ts create mode 100644 src/components/headers/HeaderProfile/useUploadProfileImage.ts create mode 100644 src/components/utils/AvailabilityTag/AvailabilityTag.stories.tsx create mode 100644 src/components/utils/AvailabilityTag/AvailabilityTag.styles.ts create mode 100644 src/components/utils/AvailabilityTag/AvailabilityTag.tsx create mode 100644 src/components/utils/AvailabilityTag/index.ts diff --git a/.gitignore b/.gitignore index 6d7e43e13..8446ea4c0 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ coverage/ /cypress/support /cypress/videos /tsconfig.tsbuildinfo +/storybook-static diff --git a/cypress/e2e/candidat.cy.js b/cypress/e2e/candidat.cy.js index 0b28ee4f2..b26c737e8 100644 --- a/cypress/e2e/candidat.cy.js +++ b/cypress/e2e/candidat.cy.js @@ -107,11 +107,11 @@ describe('Candidat', () => { cy.get('[data-testid="form-contact-internal-message-subject"]') .scrollIntoView() .type('test'); - + cy.get('[data-testid="form-contact-internal-message-message"]') .scrollIntoView() .type('test'); - + cy.get('[data-testid="form-confirm-form-contact-internal-message"]') .scrollIntoView() @@ -392,10 +392,10 @@ describe('Candidat', () => { cy.fixture('auth-current-candidat-res').then((user) => { cy.intercept('PUT', `/user/profile/${user.id}`, {fixture: "user-profile-candidate-description-modified"}).as('putUserProfile'); }) - cy.get(`[data-testid="parametres-description-placeholder"]`).scrollIntoView().click(); + cy.get(`[data-testid="profile-description-placeholder"]`).scrollIntoView().click(); cy.get(`[data-testid="form-profile-description-description"]`).scrollIntoView().type('hello'); cy.get(`[data-testid="form-confirm-form-profile-description"]`).scrollIntoView().click(); - cy.get(`[data-testid="parametres-description"]`).should('contain', "hello"); + cy.get(`[data-testid="profile-description"]`).should('contain', "hello"); // change profile picture cy.get(`[data-testid="profile-picture-upload-desktop"]`).selectFile('assets/image-fixture.jpg', {force: true}); diff --git a/cypress/fixtures/public-profile-res.json b/cypress/fixtures/public-profile-res.json index c2f9613a2..4fe3fdb9f 100644 --- a/cypress/fixtures/public-profile-res.json +++ b/cypress/fixtures/public-profile-res.json @@ -5,6 +5,7 @@ "role": "Candidat", "zone": "LILLE", "currentJob": null, + "isAvailable": true, "helpOffers": [], "helpNeeds": [ { @@ -86,4 +87,4 @@ ], "lastSentMessage": "2024-01-24T13:16:50.332Z", "lastReceivedMessage": "2024-01-24T13:16:50.332Z" -} \ No newline at end of file +} diff --git a/src/api/types.ts b/src/api/types.ts index 178d04cff..c328321c2 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -74,6 +74,7 @@ export type UserProfile = { currentJob: string; description: string; department: Department; + isAvailable: boolean; helpNeeds: { name: HelpNames }[]; helpOffers: { name: HelpNames }[]; networkBusinessLines: { @@ -531,6 +532,7 @@ export type PublicProfile = { department: Department; currentJob: string; description: string; + isAvailable: boolean; helpNeeds: { name: HelpNames }[]; helpOffers: { name: HelpNames }[]; networkBusinessLines: { diff --git a/src/components/backoffice/Backoffice.styles.tsx b/src/components/backoffice/Backoffice.styles.tsx index f78a237b6..853a0ffb2 100644 --- a/src/components/backoffice/Backoffice.styles.tsx +++ b/src/components/backoffice/Backoffice.styles.tsx @@ -1,4 +1,4 @@ -import styled, { css } from 'styled-components'; +import styled from 'styled-components'; import { COLORS } from 'src/constants/styles'; export const StyledBackofficeBackground = styled.div` @@ -11,6 +11,7 @@ export const StyledBackofficeGrid = styled.div` gap: 40px; align-items: flex-start; width: 100%; + &.mobile { margin-top: 30px; flex-direction: column; @@ -18,84 +19,6 @@ export const StyledBackofficeGrid = styled.div` } `; -export const StyledHeaderProfile = styled.div` - min-height: 275px; - background-color: white; - - &.mobile { - min-height: unset; - } -`; - -export const StyledHeaderProfilePictureContainer = styled.div` - display: flex; - justify-content: space-between; - flex-direction: column; - align-items: center; - margin-right: 50px; - position: relative; - .button-mock-image-input { - margin-top: 20px; - } - - &.mobile { - margin-right: 10px; - } -`; - -export const StyledHeaderProfilePicture = styled.div` - display: flex; - justify-content: center; - align-items: center; - width: 146px; - height: 146px; - border-radius: 50%; - background-color: ${COLORS.primaryOrange}; - overflow: hidden; - ${({ size }) => { - return css` - width: ${size}px; - height: ${size}px; - `; - }} -`; - -export const StyledHeaderProfileContent = styled.div` - display: flex; - flex-direction: row; - align-items: flex-start; -`; - -export const StyledMobileHeaderProfileTitlesContainer = styled.div` - display: flex; - flex-direction: column; - > h2 { - margin-bottom: 10px; - } - - > h6 { - margin-top: 0; - } - > h5, - > h6 { - margin-bottom: 0; - } -`; - -export const StyledHeaderProfileTextContainer = styled.div` - display: flex; - flex-direction: column; - - > h5, - > h6 { - margin-bottom: 0; - } - - > a { - line-height: 24px; - } -`; - export const StyledNoResult = styled.div` flex: 1; display: flex; diff --git a/src/components/backoffice/LayoutBackOffice.tsx b/src/components/backoffice/LayoutBackOffice.tsx index 56bbacf52..6dba73b46 100644 --- a/src/components/backoffice/LayoutBackOffice.tsx +++ b/src/components/backoffice/LayoutBackOffice.tsx @@ -3,7 +3,7 @@ import { Layout } from 'src/components/Layout'; export const LayoutBackOffice = ({ children, - title, + title = 'Espace personnel', }: { children: React.ReactNode; title?: string; @@ -14,7 +14,3 @@ export const LayoutBackOffice = ({ ); }; - -LayoutBackOffice.defaultProps = { - title: 'Espace perso', -}; diff --git a/src/components/backoffice/dashboard/Dashboard.styles.tsx b/src/components/backoffice/dashboard/Dashboard.styles.tsx index c064ac0d4..3cb434473 100644 --- a/src/components/backoffice/dashboard/Dashboard.styles.tsx +++ b/src/components/backoffice/dashboard/Dashboard.styles.tsx @@ -3,7 +3,7 @@ import styled from 'styled-components'; export const StyledDashboardLeftColumn = styled.div` display: flex; flex-direction: column; - flex-grow: 1; + flex: 1; gap: 40px; min-width: 400px; max-width: 400px; @@ -17,7 +17,7 @@ export const StyledDashboardLeftColumn = styled.div` export const StyledParametresRightColumn = styled.div` display: flex; flex-direction: column; - flex-grow: 2; + flex: 2; gap: 40px; &.mobile { width: 100%; diff --git a/src/components/backoffice/dashboard/Dashboard.tsx b/src/components/backoffice/dashboard/Dashboard.tsx index eea35aebe..4a1165826 100644 --- a/src/components/backoffice/dashboard/Dashboard.tsx +++ b/src/components/backoffice/dashboard/Dashboard.tsx @@ -5,15 +5,26 @@ import { } from '../Backoffice.styles'; import { Section } from 'src/components/utils'; import { H1 } from 'src/components/utils/Headings'; +import { CANDIDATE_USER_ROLES, USER_ROLES } from 'src/constants/users'; +import { useAuthenticatedUser } from 'src/hooks/authentication/useAuthenticatedUser'; import { useIsDesktop } from 'src/hooks/utils'; +import { isRoleIncluded } from 'src/utils'; import { StyledDashboardLeftColumn, StyledParametresRightColumn, } from './Dashboard.styles'; +import { DashboardAvailabilityCard } from './DashboardAvailabilityCard'; import { DashboardProfileCard } from './DashboardProfileCard'; export const Dashboard = () => { const isDesktop = useIsDesktop(); + const user = useAuthenticatedUser(); + + const shouldShowAllProfile = isRoleIncluded( + [...CANDIDATE_USER_ROLES, USER_ROLES.COACH], + user.role + ); + return (

@@ -23,6 +34,7 @@ export const Dashboard = () => { + {shouldShowAllProfile && } { + const user = useAuthenticatedUser(); + const dispatch = useDispatch(); + const updateProfileStatus = useSelector( + updateProfileSelectors.selectUpdateProfileStatus + ); + const prevUpdateProfileStatus = usePrevious(updateProfileStatus); + + const { updateUserProfile } = useUpdateProfile(user); + + useEffect(() => { + if (prevUpdateProfileStatus === ReduxRequestEvents.REQUESTED) { + if (updateProfileStatus === ReduxRequestEvents.SUCCEEDED) { + UIkit.notification( + `La modification de votre disponibilité a bien été enregistrée`, + 'success' + ); + } else if (updateProfileStatus === ReduxRequestEvents.FAILED) { + UIkit.notification( + `Une erreur est survenue lors de la modification de votre disponibilité`, + 'danger' + ); + } + dispatch(authenticationActions.updateProfileReset()); + } + }, [updateProfileStatus, prevUpdateProfileStatus, dispatch]); + + return ( + + { + updateUserProfile({ isAvailable }); + gaEvent(GA_TAGS.PAGE_DASHBOARD_DISPONIBILITE_CLIC); + }} + /> + + ); +}; diff --git a/src/components/backoffice/dashboard/DashboardAvailabilityCard/index.ts b/src/components/backoffice/dashboard/DashboardAvailabilityCard/index.ts new file mode 100644 index 000000000..1c0740fd6 --- /dev/null +++ b/src/components/backoffice/dashboard/DashboardAvailabilityCard/index.ts @@ -0,0 +1 @@ +export * from './DashboardAvailabilityCard'; diff --git a/src/components/backoffice/dashboard/DashboardProfileCard/DashboardProfileCard.tsx b/src/components/backoffice/dashboard/DashboardProfileCard/DashboardProfileCard.tsx index 2b99c7932..3f00d91f1 100644 --- a/src/components/backoffice/dashboard/DashboardProfileCard/DashboardProfileCard.tsx +++ b/src/components/backoffice/dashboard/DashboardProfileCard/DashboardProfileCard.tsx @@ -18,7 +18,9 @@ export const DashboardProfileCard = () => { const user = useAuthenticatedUser(); const helpField = useHelpField(user.role); const { contextualRole } = useContextualRole(user.role); + if (!helpField || !contextualRole) return null; + return ( diff --git a/src/components/backoffice/directory/DirectoryItem/DirectoryItem.tsx b/src/components/backoffice/directory/DirectoryItem/DirectoryItem.tsx index 00a973a50..9b660c66b 100644 --- a/src/components/backoffice/directory/DirectoryItem/DirectoryItem.tsx +++ b/src/components/backoffice/directory/DirectoryItem/DirectoryItem.tsx @@ -24,6 +24,7 @@ interface DirectoryItemProps { }[]; department: Department; job?: string; + isAvailable: boolean; } export function DirectoryItem({ @@ -36,6 +37,7 @@ export function DirectoryItem({ businessLines, ambitions, job, + isAvailable, }: DirectoryItemProps) { return ( @@ -49,6 +51,7 @@ export function DirectoryItem({ helps={helps} ambitions={ambitions} job={job} + isAvailable={isAvailable} /> ); diff --git a/src/components/backoffice/directory/DirectoryList/DirectoryList.tsx b/src/components/backoffice/directory/DirectoryList/DirectoryList.tsx index 5b8e139fe..019d8e16f 100644 --- a/src/components/backoffice/directory/DirectoryList/DirectoryList.tsx +++ b/src/components/backoffice/directory/DirectoryList/DirectoryList.tsx @@ -47,6 +47,7 @@ export function DirectoryList() { businessLines={businessLines} ambitions={profile.searchAmbitions} job={profile.currentJob} + isAvailable={profile.isAvailable} /> ); }); diff --git a/src/components/backoffice/parametres/ParametresLayout/HeaderParametres/HeaderParametres.styles.tsx b/src/components/backoffice/parametres/ParametresLayout/HeaderParametres/HeaderParametres.styles.tsx deleted file mode 100644 index afdcfb902..000000000 --- a/src/components/backoffice/parametres/ParametresLayout/HeaderParametres/HeaderParametres.styles.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import styled, { css } from 'styled-components'; -import { COLORS } from 'src/constants/styles'; - -export const StyledParametresPlaceholder = styled.a` - color: ${COLORS.primaryOrange}; - text-decoration: underline; - font-style: italic; - - &:hover { - text-decoration: underline; - } -`; - -export const StyledEditPictureIconContainer = styled.div` - position: absolute; - ${({ isMobile }) => { - return css` - bottom: ${isMobile ? 0 : '5%'}; - right: ${isMobile ? 0 : '5%'}; - `; - }} - border-radius: 50%; - background-color: white; - border: 1px solid ${COLORS.primaryOrange}; - padding: 5px; -`; diff --git a/src/components/backoffice/parametres/ParametresLayout/HeaderParametres/HeaderParametres.tsx b/src/components/backoffice/parametres/ParametresLayout/HeaderParametres/HeaderParametres.tsx deleted file mode 100644 index 2e1509535..000000000 --- a/src/components/backoffice/parametres/ParametresLayout/HeaderParametres/HeaderParametres.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import React, { useState } from 'react'; -import EditIcon from 'assets/icons/editIcon.svg'; -import { useOpenCorrespondingModal } from '../UserInformationCard/useOpenModal'; -import { Api } from 'src/api'; -import { - StyledHeaderProfile, - StyledHeaderProfileContent, - StyledMobileHeaderProfileTitlesContainer, - StyledHeaderProfilePicture, - StyledHeaderProfilePictureContainer, - StyledHeaderProfileTextContainer, -} from 'src/components/backoffice/Backoffice.styles'; -import { - ButtonIcon, - ButtonMock, - ImgProfile, - Section, -} from 'src/components/utils'; -import { H1, H2, H5, H6 } from 'src/components/utils/Headings'; -import { ImageInput } from 'src/components/utils/Inputs'; -import { Spinner } from 'src/components/utils/Spinner'; -import { COLORS } from 'src/constants/styles'; -import { USER_ROLES } from 'src/constants/users'; -import { useAuthenticatedUser } from 'src/hooks/authentication/useAuthenticatedUser'; -import { useIsDesktop } from 'src/hooks/utils'; -import { isRoleIncluded } from 'src/utils'; -import { - StyledEditPictureIconContainer, - StyledParametresPlaceholder, -} from './HeaderParametres.styles'; -import { ParametresDescription } from './ParametresDescription'; - -export const HeaderParametres = () => { - const isDesktop = useIsDesktop(); - const [imageUploading, setImageUploading] = useState(false); - const size = isDesktop ? 146 : 64; - const user = useAuthenticatedUser(); - - const { openCorrespondingModal } = useOpenCorrespondingModal(user); - - const shouldShowAllProfile = isRoleIncluded( - [USER_ROLES.COACH, USER_ROLES.CANDIDATE, USER_ROLES.CANDIDATE_EXTERNAL], - user.role - ); - - return ( - -
- - - - {imageUploading ? ( - - ) : ( - - )} - - {isDesktop ? ( - { - setImageUploading(true); - const formData = new FormData(); - formData.append('profileImage', profileImage); - - await Api.postProfileImage(user.id, formData); - setImageUploading(false); - }} - id="profile-picture-upload-desktop" - name="profile-picture-upload-desktop" - > - - Modifier - - - ) : ( - - { - setImageUploading(true); - const formData = new FormData(); - formData.append('profileImage', profileImage); - - await Api.postProfileImage(user.id, formData); - setImageUploading(false); - }} - id="profile-picture-upload-mobile" - name="profile-picture-upload-mobile" - > - } /> - - - )} - - {isDesktop ? ( - -

- {user.firstName} {user.lastName} - - } - color="black" - /> - {shouldShowAllProfile && ( - <> - {user.userProfile.department ? ( -

- ) : ( - - Ajouter votre département - - )} - - - )} - - ) : ( - -

- {user.firstName} {user.lastName} - - } - color="black" - /> - {shouldShowAllProfile && ( - <> - {user.userProfile.department ? ( -

- ) : ( - - Ajouter votre département - - )} - - )} - - )} - - {!isDesktop && shouldShowAllProfile && } -
-
- ); -}; diff --git a/src/components/backoffice/parametres/ParametresLayout/HeaderParametres/ParametresDescription/ParametresDescription.styles.tsx b/src/components/backoffice/parametres/ParametresLayout/HeaderParametres/ParametresDescription/ParametresDescription.styles.tsx deleted file mode 100644 index dbbcb8651..000000000 --- a/src/components/backoffice/parametres/ParametresLayout/HeaderParametres/ParametresDescription/ParametresDescription.styles.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import styled from 'styled-components'; -import { COLORS } from 'src/constants/styles'; - -export const StyledParametresDescriptionContainer = styled.div` - margin-top: 20px; -`; - -export const StyledParametresDescriptionParagraphe = styled.div` - white-space: pre-line; -`; - -export const StyledParametresDescriptionEditText = styled.a` - color: #979797; - text-decoration: underline; - font-style: italic; - margin-top: 20px; - display: block; - &:hover { - text-decoration: underline; - color: ${COLORS.darkGrayFont}; - } -`; diff --git a/src/components/backoffice/parametres/ParametresLayout/HeaderParametres/ParametresDescription/ParametresDescription.tsx b/src/components/backoffice/parametres/ParametresLayout/HeaderParametres/ParametresDescription/ParametresDescription.tsx deleted file mode 100644 index 8df4d7471..000000000 --- a/src/components/backoffice/parametres/ParametresLayout/HeaderParametres/ParametresDescription/ParametresDescription.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import { StyledParametresPlaceholder } from '../HeaderParametres.styles'; -import { openModal } from 'src/components/modals/Modal'; -import { useAuthenticatedUser } from 'src/hooks/authentication/useAuthenticatedUser'; -import { ModalEditProfileDescription } from './ModalEditProfileDescription'; -import { - StyledParametresDescriptionContainer, - StyledParametresDescriptionEditText, - StyledParametresDescriptionParagraphe, -} from './ParametresDescription.styles'; - -export const ParametresDescription = () => { - const user = useAuthenticatedUser(); - const { userProfile } = user; - - const openDescriptionModal = () => { - openModal(); - }; - - return ( - - {userProfile?.description ? ( - - {userProfile.description} -
- openDescriptionModal()} - > - Modifier la description - -
- ) : ( - openDescriptionModal()} - data-testid="parametres-description-placeholder" - > - Ajouter une description pour vous présenter à la communauté - - )} -
- ); -}; diff --git a/src/components/backoffice/parametres/ParametresLayout/HeaderParametres/ParametresDescription/index.ts b/src/components/backoffice/parametres/ParametresLayout/HeaderParametres/ParametresDescription/index.ts deleted file mode 100644 index 686a258c7..000000000 --- a/src/components/backoffice/parametres/ParametresLayout/HeaderParametres/ParametresDescription/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './ParametresDescription'; diff --git a/src/components/backoffice/parametres/ParametresLayout/HeaderParametres/index.ts b/src/components/backoffice/parametres/ParametresLayout/HeaderParametres/index.ts deleted file mode 100644 index 7dbe22705..000000000 --- a/src/components/backoffice/parametres/ParametresLayout/HeaderParametres/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './HeaderParametres'; diff --git a/src/components/backoffice/parametres/ParametresLayout/ParametresLayout.styles.tsx b/src/components/backoffice/parametres/ParametresLayout/ParametresLayout.styles.tsx index 85e209a8a..0362bf71e 100644 --- a/src/components/backoffice/parametres/ParametresLayout/ParametresLayout.styles.tsx +++ b/src/components/backoffice/parametres/ParametresLayout/ParametresLayout.styles.tsx @@ -3,7 +3,7 @@ import styled from 'styled-components'; export const StyledParametresLeftColumn = styled.div` display: flex; flex-direction: column; - flex-grow: 1; + flex: 1; gap: 40px; min-width: 400px; &.mobile { @@ -16,7 +16,7 @@ export const StyledParametresLeftColumn = styled.div` export const StyledParametresRightColumn = styled.div` display: flex; flex-direction: column; - flex-grow: 2; + flex: 2; gap: 40px; &.mobile { width: 100%; diff --git a/src/components/backoffice/parametres/ParametresLayout/ParametresLayout.tsx b/src/components/backoffice/parametres/ParametresLayout/ParametresLayout.tsx index a331f1b31..0bb7326e3 100644 --- a/src/components/backoffice/parametres/ParametresLayout/ParametresLayout.tsx +++ b/src/components/backoffice/parametres/ParametresLayout/ParametresLayout.tsx @@ -1,9 +1,10 @@ import React from 'react'; import { - StyledBackofficeGrid, StyledBackofficeBackground, + StyledBackofficeGrid, } from '../../Backoffice.styles'; import { useConfirmationToaster } from '../useConfirmationToaster'; +import { HeaderProfile } from 'src/components/headers/HeaderProfile'; import { Card, Section } from 'src/components/utils'; import { CANDIDATE_USER_ROLES, USER_ROLES } from 'src/constants/users'; import { useAuthenticatedUser } from 'src/hooks/authentication/useAuthenticatedUser'; @@ -11,7 +12,6 @@ import { useIsDesktop } from 'src/hooks/utils'; import { isRoleIncluded } from 'src/utils'; import { CVPreferences } from './CVPreferences'; import { ChangePasswordCard } from './ChangePasswordCard'; -import { HeaderParametres } from './HeaderParametres'; import { ParametresHelpCard } from './ParametresHelpCard'; import { StyledParametresLeftColumn, @@ -31,7 +31,16 @@ export const ParametresLayout = () => { return ( - +
{ return helpField; }; -export const useUpdateProfile = ( - user: UserWithUserCandidate, - onClose?: () => void -) => { +export const useUpdateProfile = (user: UserWithUserCandidate) => { const dispatch = useDispatch(); const [closeModal, setCloseModal] = useState(false); @@ -48,10 +45,9 @@ export const useUpdateProfile = ( useEffect(() => { if (updateProfileStatus === ReduxRequestEvents.SUCCEEDED) { - if (onClose) onClose(); setCloseModal(true); } - }, [updateProfileStatus, onClose]); + }, [updateProfileStatus]); const updateUserProfile = useCallback( (newProfileData: Partial): void => { diff --git a/src/components/backoffice/parametres/useUpdateUser.tsx b/src/components/backoffice/parametres/useUpdateUser.tsx index f756a3932..d8258c010 100644 --- a/src/components/backoffice/parametres/useUpdateUser.tsx +++ b/src/components/backoffice/parametres/useUpdateUser.tsx @@ -8,10 +8,7 @@ import { updateUserSelectors, } from 'src/use-cases/authentication'; -export const useUpdateUser = ( - user: UserWithUserCandidate, - onClose?: () => void -) => { +export const useUpdateUser = (user: UserWithUserCandidate) => { const dispatch = useDispatch(); const [closeModal, setCloseModal] = useState(false); @@ -22,10 +19,9 @@ export const useUpdateUser = ( useEffect(() => { if (updateUserStatus === ReduxRequestEvents.SUCCEEDED) { - if (onClose) onClose(); setCloseModal(true); } - }, [updateUserStatus, onClose]); + }, [updateUserStatus]); const updateUser = useCallback( (newUserData: Partial) => { diff --git a/src/components/backoffice/profile/HeaderProfile/HeaderProfile.styles.tsx b/src/components/backoffice/profile/HeaderProfile/HeaderProfile.styles.tsx deleted file mode 100644 index 9e2155168..000000000 --- a/src/components/backoffice/profile/HeaderProfile/HeaderProfile.styles.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import styled from 'styled-components'; - -export const StyledHeaderProfileDescriptionParagraphe = styled.p` - white-space: pre-line; -`; diff --git a/src/components/backoffice/profile/HeaderProfile/HeaderProfile.tsx b/src/components/backoffice/profile/HeaderProfile/HeaderProfile.tsx deleted file mode 100644 index 15baa341c..000000000 --- a/src/components/backoffice/profile/HeaderProfile/HeaderProfile.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import React from 'react'; -import { - StyledHeaderProfile, - StyledHeaderProfileContent, - StyledHeaderProfilePicture, - StyledHeaderProfilePictureContainer, - StyledHeaderProfileTextContainer, - StyledMobileHeaderProfileTitlesContainer, -} from '../../Backoffice.styles'; -import { useSelectSelectedProfile } from '../useSelectedProfile'; -import { ImgProfile, Section } from 'src/components/utils'; -import { H1, H2, H5, H6 } from 'src/components/utils/Headings'; -import { useIsDesktop } from 'src/hooks/utils'; -import { StyledHeaderProfileDescriptionParagraphe } from './HeaderProfile.styles'; - -export const HeaderProfile = () => { - const isDesktop = useIsDesktop(); - const size = isDesktop ? 146 : 64; - const profile = useSelectSelectedProfile(); - if (!profile) return null; - return ( - -
- - - - - - - {isDesktop ? ( - -

- {profile.firstName} {profile.lastName} - - } - color="black" - /> - {profile.department && ( -

- )} - - {profile.description} - - - ) : ( - -

- {profile.firstName} {profile.lastName} - - } - color="black" - /> - {profile.department && ( -

- )} - - )} - - {!isDesktop && ( - - {profile.description} - - )} -
-
- ); -}; diff --git a/src/components/backoffice/profile/HeaderProfile/index.ts b/src/components/backoffice/profile/HeaderProfile/index.ts deleted file mode 100644 index 86e0cc0ca..000000000 --- a/src/components/backoffice/profile/HeaderProfile/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './HeaderProfile'; diff --git a/src/components/backoffice/profile/Profile.styles.tsx b/src/components/backoffice/profile/Profile.styles.tsx index 161609053..71eb3a284 100644 --- a/src/components/backoffice/profile/Profile.styles.tsx +++ b/src/components/backoffice/profile/Profile.styles.tsx @@ -3,7 +3,7 @@ import styled from 'styled-components'; export const StyledProfileLeftColumn = styled.div` display: flex; flex-direction: column; - flex-grow: 3; + flex: 2; gap: 40px; &.mobile { min-width: unset; @@ -15,7 +15,7 @@ export const StyledProfileLeftColumn = styled.div` export const StyledProfileRightColumn = styled.div` display: flex; flex-direction: column; - flex-grow: 1; + flex: 1; gap: 40px; min-width: 400px; &.mobile { diff --git a/src/components/backoffice/profile/Profile.tsx b/src/components/backoffice/profile/Profile.tsx index 090270176..8af2ddccc 100644 --- a/src/components/backoffice/profile/Profile.tsx +++ b/src/components/backoffice/profile/Profile.tsx @@ -1,11 +1,11 @@ import React from 'react'; import { - StyledBackofficeGrid, StyledBackofficeBackground, + StyledBackofficeGrid, } from '../Backoffice.styles'; +import { HeaderProfile } from 'src/components/headers/HeaderProfile'; import { Section } from 'src/components/utils'; import { useIsDesktop } from 'src/hooks/utils'; -import { HeaderProfile } from './HeaderProfile'; import { StyledProfileLeftColumn, StyledProfileRightColumn, @@ -13,13 +13,24 @@ import { import { ProfileContactCard } from './ProfileContactCard'; import { ProfileHelpInformationCard } from './ProfileHelpInformationCard'; import { ProfileProfessionalInformationCard } from './ProfileProfessionalInformationCard'; +import { useSelectSelectedProfile } from './useSelectedProfile'; export const Profile = () => { const isDesktop = useIsDesktop(); + const selectedProfile = useSelectSelectedProfile(); return ( - +
diff --git a/src/components/backoffice/profile/ProfileContactCard/ProfileContactCard.styles.tsx b/src/components/backoffice/profile/ProfileContactCard/ProfileContactCard.styles.tsx index fb3464520..f976dde61 100644 --- a/src/components/backoffice/profile/ProfileContactCard/ProfileContactCard.styles.tsx +++ b/src/components/backoffice/profile/ProfileContactCard/ProfileContactCard.styles.tsx @@ -33,7 +33,9 @@ export const StyledContactMessage = styled.div` background-color: ${COLORS.hoverOrange}; border-radius: 10px; padding: 10px; - margin-bottom: 20px; + &:not(:last-child) { + margin-bottom: 20px; + } font-size: 13px; svg { margin-right: 10px; diff --git a/src/components/backoffice/profile/ProfileContactCard/ProfileContactCard.tsx b/src/components/backoffice/profile/ProfileContactCard/ProfileContactCard.tsx index adfc9da85..fc2cfb51b 100644 --- a/src/components/backoffice/profile/ProfileContactCard/ProfileContactCard.tsx +++ b/src/components/backoffice/profile/ProfileContactCard/ProfileContactCard.tsx @@ -60,35 +60,44 @@ export const ProfileContactCard = () => { isLoading={loadingSending} > - {isFormSent ? ( -
- - - - Votre message a été envoyé -
- ) : ( + {selectedProfile.isAvailable ? ( <> - {existingContactMessage && ( - - - {existingContactMessage} - + {isFormSent ? ( +
+ + + + Votre message a été envoyé +
+ ) : ( + <> + {existingContactMessage && ( + + + {existingContactMessage} + + )} + { + gaEvent(GA_TAGS.PROFILE_DETAILS_CONTACT_SEND_CLIC); + dispatch( + profilesActions.postInternalMessageRequested({ + ...values, + addresseeUserId: selectedProfile?.id, + }) + ); + }} + noCompulsory + /> + )} - { - gaEvent(GA_TAGS.PROFILE_DETAILS_CONTACT_SEND_CLIC); - dispatch( - profilesActions.postInternalMessageRequested({ - ...values, - addresseeUserId: selectedProfile?.id, - }) - ); - }} - noCompulsory - /> + ) : ( + + {selectedProfile.firstName} n'est pas disponible pour le moment + pour recevoir des demandes de contact + )}
diff --git a/src/components/backoffice/profile/ProfileHelpInformationCard/ProfileHelpInformationCard.tsx b/src/components/backoffice/profile/ProfileHelpInformationCard/ProfileHelpInformationCard.tsx index 1ee0cdf90..1dbd5e363 100644 --- a/src/components/backoffice/profile/ProfileHelpInformationCard/ProfileHelpInformationCard.tsx +++ b/src/components/backoffice/profile/ProfileHelpInformationCard/ProfileHelpInformationCard.tsx @@ -16,6 +16,7 @@ export const ProfileHelpInformationCard = () => { const { contextualRole } = useContextualRole(selectedProfile.role); if (!helpField) return null; + return ( { + const { + openCorrespondingModal, + imageUploading, + uploadProfileImage, + shouldShowAllProfile, + contextualRole, + } = useHeaderProfile(id, role); + + return ( + +
+ + + + {imageUploading ? ( + + ) : ( + + )} + + {isEditable && ( + + + Modifier + + + )} + + + + +

+ {firstName} {lastName} + + } + color="black" + /> + + + {shouldShowAllProfile && ( + + )} + + {shouldShowAllProfile && ( + <> + {department &&
} + {!department && isEditable && ( + + Ajouter votre département + + )} + + + )} + + +

+
+ ); +}; diff --git a/src/components/headers/HeaderProfile/HeaderProfile.mobile.tsx b/src/components/headers/HeaderProfile/HeaderProfile.mobile.tsx new file mode 100644 index 000000000..90b44494d --- /dev/null +++ b/src/components/headers/HeaderProfile/HeaderProfile.mobile.tsx @@ -0,0 +1,114 @@ +import React from 'react'; +import EditIcon from 'assets/icons/editIcon.svg'; +import { ButtonIcon, ImgProfile, Section, Tag } from 'src/components/utils'; +import { AvailabilityTag } from 'src/components/utils/AvailabilityTag/AvailabilityTag'; +import { H2, H6 } from 'src/components/utils/Headings'; +import { ImageInput } from 'src/components/utils/Inputs'; +import { Spinner } from 'src/components/utils/Spinner'; +import { COLORS } from 'src/constants/styles'; +import { USER_ROLES } from 'src/constants/users'; +import { + StyledEditPictureIconContainer, + StyledHeaderNameAndRoleMobile, + StyledHeaderProfile, + StyledHeaderProfileContent, + StyledHeaderProfileDescription, + StyledHeaderProfileInfoContainer, + StyledHeaderProfileNameContainer, + StyledHeaderProfilePicture, + StyledHeaderProfilePictureContainerMobile, + StyledProfilePlaceholder, +} from './HeaderProfile.styles'; +import { HeaderProfileProps } from './HeaderProfile.types'; +import { ProfileDescription } from './ProfileDescription'; +import { useHeaderProfile } from './useHeaderProfile'; + +const SIZE = 64; +export const HeaderProfileMobile = ({ + id, + firstName, + lastName, + role, + department, + description, + isAvailable, + isEditable, +}: HeaderProfileProps) => { + const { + openCorrespondingModal, + imageUploading, + uploadProfileImage, + shouldShowAllProfile, + contextualRole, + } = useHeaderProfile(id, role); + + return ( + +
+ + + + {imageUploading ? ( + + ) : ( + + )} + + {isEditable && ( + + + } /> + + + )} + + + + +

+ {firstName} {lastName} + + } + color="black" + /> + + + + {shouldShowAllProfile && ( + <> + {department &&
} + {!department && isEditable && ( + + Ajouter votre département + + )} + + )} + + + {shouldShowAllProfile && ( + + + + + )} +

+
+ ); +}; diff --git a/src/components/headers/HeaderProfile/HeaderProfile.styles.tsx b/src/components/headers/HeaderProfile/HeaderProfile.styles.tsx new file mode 100644 index 000000000..2d98ed831 --- /dev/null +++ b/src/components/headers/HeaderProfile/HeaderProfile.styles.tsx @@ -0,0 +1,117 @@ +import styled, { css } from 'styled-components'; +import { COLORS } from 'src/constants/styles'; + +export const StyledProfilePlaceholder = styled.a` + color: ${COLORS.primaryOrange}; + text-decoration: underline; + font-style: italic; + font-size: 14px; + + &:hover { + text-decoration: underline; + } +`; + +export const StyledEditPictureIconContainer = styled.div` + position: absolute; + bottom: 0; + right: 0; + border-radius: 50%; + background-color: white; + border: 1px solid ${COLORS.primaryOrange}; + padding: 5px; +`; + +export const StyledHeaderProfile = styled.div` + background-color: ${COLORS.white}; +`; + +export const StyledHeaderProfilePictureContainer = styled.div` + display: flex; + justify-content: space-between; + flex-direction: column; + align-items: center; + margin-right: 50px; + position: relative; + + .button-mock-image-input { + margin-top: 20px; + } +`; + +export const StyledHeaderProfilePictureContainerMobile = styled( + StyledHeaderProfilePictureContainer +)` + margin-right: 10px; +`; + +export const StyledHeaderProfilePicture = styled.div<{ size: number }>` + display: flex; + justify-content: center; + align-items: center; + border-radius: 50%; + background-color: ${COLORS.primaryOrange}; + overflow: hidden; + ${({ size }) => { + return css` + width: ${size}px; + height: ${size}px; + `; + }} +`; + +export const StyledHeaderProfileContent = styled.div` + display: flex; + flex-direction: row; + align-items: flex-start; +`; + +export const StyledHeaderProfileInfoContainer = styled.div` + display: flex; + flex: 1; + flex-direction: column; + + h1, + h2, + h3, + h5, + h6 { + margin-bottom: 0; + margin-top: 0; + } + + a { + line-height: 24px; + } +`; + +export const StyledHeaderProfileNameContainer = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: flex-start; + flex: 1; +`; + +export const StyledHeaderNameAndRole = styled.div` + display: flex; + flex-direction: row; + align-items: center; + margin-bottom: 20px; + + h1, + h2 { + margin-right: 16px; + } +`; + +export const StyledHeaderNameAndRoleMobile = styled(StyledHeaderNameAndRole)` + margin-bottom: 10px; +`; + +export const StyledHeaderProfileDescription = styled.div` + margin-top: 20px; + display: flex; + flex-direction: column; + align-items: flex-start; +`; diff --git a/src/components/headers/HeaderProfile/HeaderProfile.types.ts b/src/components/headers/HeaderProfile/HeaderProfile.types.ts new file mode 100644 index 000000000..e11fc85b0 --- /dev/null +++ b/src/components/headers/HeaderProfile/HeaderProfile.types.ts @@ -0,0 +1,13 @@ +import { Department } from 'src/constants/departements'; +import { UserRole } from 'src/constants/users'; + +export interface HeaderProfileProps { + id: string; + firstName: string; + lastName: string; + role: UserRole; + department: Department; + description: string; + isAvailable: boolean; + isEditable?: boolean; +} diff --git a/src/components/backoffice/parametres/ParametresLayout/HeaderParametres/ParametresDescription/ModalEditProfileDescription/ModalEditProfileDescription.tsx b/src/components/headers/HeaderProfile/ProfileDescription/ModalEditProfileDescription/ModalEditProfileDescription.tsx similarity index 100% rename from src/components/backoffice/parametres/ParametresLayout/HeaderParametres/ParametresDescription/ModalEditProfileDescription/ModalEditProfileDescription.tsx rename to src/components/headers/HeaderProfile/ProfileDescription/ModalEditProfileDescription/ModalEditProfileDescription.tsx diff --git a/src/components/backoffice/parametres/ParametresLayout/HeaderParametres/ParametresDescription/ModalEditProfileDescription/index.ts b/src/components/headers/HeaderProfile/ProfileDescription/ModalEditProfileDescription/index.ts similarity index 100% rename from src/components/backoffice/parametres/ParametresLayout/HeaderParametres/ParametresDescription/ModalEditProfileDescription/index.ts rename to src/components/headers/HeaderProfile/ProfileDescription/ModalEditProfileDescription/index.ts diff --git a/src/components/headers/HeaderProfile/ProfileDescription/ProfileDescription.styles.tsx b/src/components/headers/HeaderProfile/ProfileDescription/ProfileDescription.styles.tsx new file mode 100644 index 000000000..f2e5cf732 --- /dev/null +++ b/src/components/headers/HeaderProfile/ProfileDescription/ProfileDescription.styles.tsx @@ -0,0 +1,28 @@ +import styled from 'styled-components'; +import { COLORS } from 'src/constants/styles'; + +export const StyledDescriptionContainer = styled.div` + display: flex; + flex-direction: column; + margin-top: 20px; +`; + +export const StyledDescriptionParagraphe = styled.div` + white-space: pre-line; + color: ${COLORS.black}; + font-style: italic; + font-size: 16px; + line-height: 24px; +`; + +export const StyledDescriptionEditText = styled.a` + color: ${COLORS.darkGray}; + text-decoration: underline; + margin-top: 20px; + margin-left: 8px; + font-size: 14px; + &:hover { + text-decoration: underline; + color: ${COLORS.darkGrayFont}; + } +`; diff --git a/src/components/headers/HeaderProfile/ProfileDescription/ProfileDescription.tsx b/src/components/headers/HeaderProfile/ProfileDescription/ProfileDescription.tsx new file mode 100644 index 000000000..fc0f38e7f --- /dev/null +++ b/src/components/headers/HeaderProfile/ProfileDescription/ProfileDescription.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { StyledProfilePlaceholder } from '../HeaderProfile.styles'; +import { openModal } from 'src/components/modals/Modal'; +import { useAuthenticatedUser } from 'src/hooks/authentication/useAuthenticatedUser'; +import { ModalEditProfileDescription } from './ModalEditProfileDescription'; +import { + StyledDescriptionContainer, + StyledDescriptionEditText, + StyledDescriptionParagraphe, +} from './ProfileDescription.styles'; + +interface ProfileDescriptionProps { + isEditable?: boolean; + description: string; +} + +export const ProfileDescription = ({ + isEditable = false, + description, +}: ProfileDescriptionProps) => { + const user = useAuthenticatedUser(); + + const openDescriptionModal = () => { + openModal(); + }; + + return ( + + {description && ( + + “{description}” + {isEditable && ( + openDescriptionModal()}> + Modifier la description + + )} + + )} + {isEditable && !description && ( + openDescriptionModal()} + data-testid="profile-description-placeholder" + > + Ajouter une description pour vous présenter à la communauté + + )} + + ); +}; diff --git a/src/components/headers/HeaderProfile/ProfileDescription/index.ts b/src/components/headers/HeaderProfile/ProfileDescription/index.ts new file mode 100644 index 000000000..7d6a733e5 --- /dev/null +++ b/src/components/headers/HeaderProfile/ProfileDescription/index.ts @@ -0,0 +1 @@ +export * from './ProfileDescription'; diff --git a/src/components/headers/HeaderProfile/index.ts b/src/components/headers/HeaderProfile/index.ts new file mode 100644 index 000000000..ea5298c32 --- /dev/null +++ b/src/components/headers/HeaderProfile/index.ts @@ -0,0 +1,8 @@ +import { plateform } from 'src/utils/Device'; +import { HeaderProfileDesktop } from './HeaderProfile.desktop'; +import { HeaderProfileMobile } from './HeaderProfile.mobile'; + +export const HeaderProfile = plateform({ + Desktop: HeaderProfileDesktop, + Mobile: HeaderProfileMobile, +}); diff --git a/src/components/headers/HeaderProfile/useHeaderProfile.ts b/src/components/headers/HeaderProfile/useHeaderProfile.ts new file mode 100644 index 000000000..8844dca29 --- /dev/null +++ b/src/components/headers/HeaderProfile/useHeaderProfile.ts @@ -0,0 +1,32 @@ +import { useOpenCorrespondingModal } from 'src/components/backoffice/parametres/ParametresLayout/UserInformationCard/useOpenModal'; +import { useContextualRole } from 'src/components/backoffice/useContextualRole'; +import { + CANDIDATE_USER_ROLES, + USER_ROLES, + UserRole, +} from 'src/constants/users'; +import { useAuthenticatedUser } from 'src/hooks/authentication/useAuthenticatedUser'; +import { isRoleIncluded } from 'src/utils'; +import { useUploadProfileImage } from './useUploadProfileImage'; + +export function useHeaderProfile(userId: string, role: UserRole) { + const user = useAuthenticatedUser(); + const { openCorrespondingModal } = useOpenCorrespondingModal(user); + + const { imageUploading, uploadProfileImage } = useUploadProfileImage(userId); + + const shouldShowAllProfile = isRoleIncluded( + [...CANDIDATE_USER_ROLES, USER_ROLES.COACH], + role + ); + + const { contextualRole } = useContextualRole(role); + + return { + openCorrespondingModal, + imageUploading, + uploadProfileImage, + shouldShowAllProfile, + contextualRole, + }; +} diff --git a/src/components/headers/HeaderProfile/useUploadProfileImage.ts b/src/components/headers/HeaderProfile/useUploadProfileImage.ts new file mode 100644 index 000000000..20c5a766f --- /dev/null +++ b/src/components/headers/HeaderProfile/useUploadProfileImage.ts @@ -0,0 +1,20 @@ +import { useCallback, useState } from 'react'; +import { Api } from 'src/api'; + +export function useUploadProfileImage(userId: string) { + const [imageUploading, setImageUploading] = useState(false); + + const uploadProfileImage = useCallback( + async ({ profileImage }: { profileImage: Blob }) => { + setImageUploading(true); + const formData = new FormData(); + formData.append('profileImage', profileImage); + + await Api.postProfileImage(userId, formData); + setImageUploading(false); + }, + [userId] + ); + + return { imageUploading, uploadProfileImage }; +} diff --git a/src/components/partials/NewsletterPartial/NewsletterPartial.styles.ts b/src/components/partials/NewsletterPartial/NewsletterPartial.styles.ts index d9599a9aa..896b3cd65 100644 --- a/src/components/partials/NewsletterPartial/NewsletterPartial.styles.ts +++ b/src/components/partials/NewsletterPartial/NewsletterPartial.styles.ts @@ -33,7 +33,7 @@ export const StyledNLForm = styled.div` .input-label { display: flex; flex-direction: row; - flex-grow: 1; + flex: 1; margin-bottom: 16px; } } diff --git a/src/components/utils/AvailabilityTag/AvailabilityTag.stories.tsx b/src/components/utils/AvailabilityTag/AvailabilityTag.stories.tsx new file mode 100644 index 000000000..c28a5e5b3 --- /dev/null +++ b/src/components/utils/AvailabilityTag/AvailabilityTag.stories.tsx @@ -0,0 +1,22 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { AvailabilityTag } from '.'; + +const meta = { + title: 'Availability Tag', + component: AvailabilityTag, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const NotAvailable = { + args: { + isAvailable: false, + }, +} satisfies Story; + +export const Available = { + args: { + isAvailable: true, + }, +} satisfies Story; diff --git a/src/components/utils/AvailabilityTag/AvailabilityTag.styles.ts b/src/components/utils/AvailabilityTag/AvailabilityTag.styles.ts new file mode 100644 index 000000000..e97731fe8 --- /dev/null +++ b/src/components/utils/AvailabilityTag/AvailabilityTag.styles.ts @@ -0,0 +1,23 @@ +import styled from 'styled-components'; +import { COLORS } from 'src/constants/styles'; + +export const StyledAvailabilityTagContainer = styled.div` + display: flex; + align-items: center; + border-radius: 40px; + background-color: ${COLORS.white}; + padding: 2px 8px; + font-size: 12px; + border: 1px solid ${COLORS.gray}; + overflow-wrap: normal; +`; + +export const StyledAvailabilityTagDot = styled.div<{ isAvailable: boolean }>` + height: 10px; + width: 10px; + background-color: ${({ isAvailable }) => { + return isAvailable ? COLORS.yesGreen : COLORS.noRed; + }}; + border-radius: 50%; + margin-right: 8px; +`; diff --git a/src/components/utils/AvailabilityTag/AvailabilityTag.tsx b/src/components/utils/AvailabilityTag/AvailabilityTag.tsx new file mode 100644 index 000000000..1e15997cd --- /dev/null +++ b/src/components/utils/AvailabilityTag/AvailabilityTag.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { + StyledAvailabilityTagContainer, + StyledAvailabilityTagDot, +} from './AvailabilityTag.styles'; + +interface AvailabilityTagProps { + isAvailable: boolean; +} + +export function AvailabilityTag({ isAvailable }: AvailabilityTagProps) { + return ( + + + {isAvailable ? 'Disponible' : 'Indisponible'} + + ); +} diff --git a/src/components/utils/AvailabilityTag/index.ts b/src/components/utils/AvailabilityTag/index.ts new file mode 100644 index 000000000..d22bca44e --- /dev/null +++ b/src/components/utils/AvailabilityTag/index.ts @@ -0,0 +1 @@ +export * from './AvailabilityTag'; diff --git a/src/components/utils/CardList/CardList.stories.tsx b/src/components/utils/CardList/CardList.stories.tsx index 0a9119498..24b543ef3 100644 --- a/src/components/utils/CardList/CardList.stories.tsx +++ b/src/components/utils/CardList/CardList.stories.tsx @@ -38,6 +38,7 @@ const cards: ProfileCardProps[] = new Array(50) { name: 'ouvrier', order: 1 }, ], department: 'Paris (75)', + isAvailable: false, }, { userId: uuid(), @@ -51,6 +52,7 @@ const cards: ProfileCardProps[] = new Array(50) ], ambitions: [{ name: 'développeur', order: 0 }], department: 'Paris (75)', + isAvailable: true, }, ]) .reduce((acc, val) => [...acc, ...val], []); @@ -65,6 +67,7 @@ const list = cards.map( businessLines, ambitions, department, + isAvailable, }) => ( ) diff --git a/src/components/utils/Cards/Card/Card.styles.tsx b/src/components/utils/Cards/Card/Card.styles.tsx index bbbcfe984..245b6ffe7 100644 --- a/src/components/utils/Cards/Card/Card.styles.tsx +++ b/src/components/utils/Cards/Card/Card.styles.tsx @@ -25,18 +25,18 @@ export const StyledCardTitleContainer = styled.div` margin-right: 25px; margin-left: 25px; padding-bottom: 25px; - margin-bottom: 0px; + margin-bottom: 0; h5 { - margin-bottom: 0px; + margin-bottom: 0; } &.no-border { border-bottom: none; - margin-bottom: 0px; + margin-bottom: 0; > h5 { - margin-bottom: 0px; + margin-bottom: 0; } } `; diff --git a/src/components/utils/Cards/Card/Card.tsx b/src/components/utils/Cards/Card/Card.tsx index 6c662f9aa..a56d316b7 100644 --- a/src/components/utils/Cards/Card/Card.tsx +++ b/src/components/utils/Cards/Card/Card.tsx @@ -20,12 +20,7 @@ import { interface CardProps { children: React.ReactNode; - // badge?: React.ReactNode; - // style?: 'default' | 'primary' | 'secondary'; title?: React.ReactNode; - // body?: boolean; - // hover?: boolean; - // size?: 'small' | 'large' | 'default'; onClick?: () => void; editCallback?: () => void; isLoading?: boolean; @@ -37,10 +32,6 @@ interface CardProps { export const Card = ({ title, - // style = 'default', - // body = true, - // hover = false, - // size, onClick, children, editCallback, diff --git a/src/components/utils/Cards/ProfileCard/ProfileCard.stories.tsx b/src/components/utils/Cards/ProfileCard/ProfileCard.stories.tsx index 130896db2..065b77dc9 100644 --- a/src/components/utils/Cards/ProfileCard/ProfileCard.stories.tsx +++ b/src/components/utils/Cards/ProfileCard/ProfileCard.stories.tsx @@ -1,7 +1,7 @@ import { Meta, StoryObj } from '@storybook/react'; import { v4 as uuid } from 'uuid'; import { USER_ROLES } from 'src/constants/users'; -import { ProfileCard } from './ProfileCard'; +import { ProfileCard } from '.'; const meta = { title: 'Profile Card', @@ -18,6 +18,7 @@ export const Candidate = { lastName: 'Doe', role: USER_ROLES.CANDIDATE, helps: [{ name: 'network' }, { name: 'cv' }], + isAvailable: true, businessLines: [ { name: 'id', order: 0 }, { name: 'bat', order: 1 }, @@ -36,6 +37,7 @@ export const Coach = { firstName: 'John', lastName: 'Doe', role: USER_ROLES.COACH, + isAvailable: true, helps: [ { name: 'network' }, { name: 'cv' }, diff --git a/src/components/utils/Cards/ProfileCard/ProfileCard.styles.ts b/src/components/utils/Cards/ProfileCard/ProfileCard.styles.ts index a756b643c..8647a44b3 100644 --- a/src/components/utils/Cards/ProfileCard/ProfileCard.styles.ts +++ b/src/components/utils/Cards/ProfileCard/ProfileCard.styles.ts @@ -38,6 +38,15 @@ export const StyledProfileCardInfoContainer = styled.div` bottom: 24px; `; +export const StyledProfileCardAvailability = styled.div` + position: absolute; + display: flex; + align-items: center; + justify-content: center; + top: 10px; + right: 10px; +`; + export const StyledProfileCardName = styled.div` text-overflow: ellipsis; overflow: hidden; diff --git a/src/components/utils/Cards/ProfileCard/ProfileCard.tsx b/src/components/utils/Cards/ProfileCard/ProfileCard.tsx index 1e5a16019..ef328dbaf 100644 --- a/src/components/utils/Cards/ProfileCard/ProfileCard.tsx +++ b/src/components/utils/Cards/ProfileCard/ProfileCard.tsx @@ -4,6 +4,7 @@ import React, { useMemo } from 'react'; import HandsIcon from 'assets/icons/illu-coeur-mains-ouvertes.svg'; import CaseIcon from 'assets/icons/illu-malette.svg'; import { UserCandidateWithUsers } from 'src/api/types'; +import { AvailabilityTag } from 'src/components/utils/AvailabilityTag'; import { H3, H4, H5 } from 'src/components/utils/Headings'; import { Img } from 'src/components/utils/Img'; import { Tag } from 'src/components/utils/Tag'; @@ -22,6 +23,7 @@ import { gaEvent } from 'src/lib/gtag'; import { findConstantFromValue, isRoleIncluded, sortByOrder } from 'src/utils'; import { StyledProfileCard, + StyledProfileCardAvailability, StyledProfileCardBusinessLines, StyledProfileCardContent, StyledProfileCardDepartment, @@ -63,9 +65,10 @@ interface ProfileCardProps { userCandidate?: UserCandidateWithUsers; department?: Department; job?: string; + isAvailable: boolean; } -const getLabelsDependingOnRole = (role) => { +const getLabelsDependingOnRole = (role: UserRole) => { if (isRoleIncluded(CANDIDATE_USER_ROLES, role)) { return { businessLines: 'Je recherche un emploi dans\xa0:', @@ -97,6 +100,7 @@ export function ProfileCard({ ambitions, userCandidate, job, + isAvailable, }: ProfileCardProps) { const { urlImg, fallbackToCVImage } = useImageFallback({ userId, @@ -144,6 +148,9 @@ export function ProfileCard({ )} + + +

; + user: { + id: string; + firstName: string; + role: UserRole; + candidat?: UserCandidateWithUsers; + }; size?: number; } export const ImgProfile = ({ user, size = 40 }: ImgProfileProps) => { - const connectedUser = useAuthenticatedUser(); - - const myUser: Partial = user || connectedUser; const [hash, setHash] = useState(Date.now()); const { urlImg, fallbackToCVImage } = useImageFallback({ - userId: myUser.id, - role: myUser.role, - userCandidate: myUser.candidat, + userId: user.id, + role: user.role, + userCandidate: user.candidat, }); useEffect(() => { @@ -40,7 +42,7 @@ export const ImgProfile = ({ user, size = 40 }: ImgProfileProps) => { cover onError={fallbackToCVImage} src={`${urlImg}?${hash}`} - alt={`photo de ${myUser.firstName}`} + alt={`photo de ${user.firstName}`} id="parametres-profile-picture" />

@@ -49,7 +51,7 @@ export const ImgProfile = ({ user, size = 40 }: ImgProfileProps) => { className="uk-text-normal uk-text-uppercase" style={{ fontSize: size / 2, color: '#fff' }} > - {myUser.firstName?.substring(0, 1)} + {user.firstName?.substring(0, 1)} )} diff --git a/src/components/utils/Inputs/ToggleWithModal/ToggleWithModal.styles.tsx b/src/components/utils/Inputs/ToggleWithModal/ToggleWithModal.styles.tsx index dc7602612..2685bf4b9 100644 --- a/src/components/utils/Inputs/ToggleWithModal/ToggleWithModal.styles.tsx +++ b/src/components/utils/Inputs/ToggleWithModal/ToggleWithModal.styles.tsx @@ -78,4 +78,5 @@ export const StyledSlider = styled.span` export const StyledToggleLabel = styled.div` display: flex; flex-direction: column; + justify-content: center; `; diff --git a/src/components/utils/Inputs/ToggleWithModal/ToggleWithModal.tsx b/src/components/utils/Inputs/ToggleWithModal/ToggleWithModal.tsx index c60979f03..447f8976f 100644 --- a/src/components/utils/Inputs/ToggleWithModal/ToggleWithModal.tsx +++ b/src/components/utils/Inputs/ToggleWithModal/ToggleWithModal.tsx @@ -24,7 +24,7 @@ interface ToggleWithModalProps> { id: string; title: string; subtitle?: React.ReactNode; - modal: ModalType | React.ReactNode; + modal?: ModalType | React.ReactNode; onToggle: ( value: boolean, fields?: ExtractFormSchemaValidation, @@ -53,8 +53,8 @@ export const ToggleWithModal = >({ type="checkbox" checked={isToggled} onChange={async () => { - if (isToggled) { - await onToggle(false); + if (!modal || isToggled) { + await onToggle(!isToggled); // custom Modal (usually with redux) } else if (modal && React.isValidElement(modal)) { openModal(modal); diff --git a/src/components/utils/Tag/Tag.styles.tsx b/src/components/utils/Tag/Tag.styles.tsx index 10aacecb5..0c219ee4d 100644 --- a/src/components/utils/Tag/Tag.styles.tsx +++ b/src/components/utils/Tag/Tag.styles.tsx @@ -31,6 +31,7 @@ export const StyledTag = styled.div` border-radius: 30px; border: 1px solid; font-weight: 400; + overflow-wrap: normal; ${({ customStyle }) => styleVariants[customStyle]} ${({ size }) => sizeVariants[size]} `; diff --git a/src/constants/tags.ts b/src/constants/tags.ts index 40b21c5ba..58be90521 100644 --- a/src/constants/tags.ts +++ b/src/constants/tags.ts @@ -410,6 +410,9 @@ export const GA_TAGS = { PAGE_ANNUAIRE_SUPPRIMER_FILTRES_CLIC: { action: 'Page_Annuaire_Supprimer_Filtres_Clic', }, + PAGE_DASHBOARD_DISPONIBILITE_CLIC: { + action: 'Page_Dashboard_Disponibilite_Clic', + }, } as const; export const FB_TAGS = { diff --git a/src/hooks/useImageFallback.ts b/src/hooks/useImageFallback.ts index 788d4b5cb..8cfdf2fbe 100644 --- a/src/hooks/useImageFallback.ts +++ b/src/hooks/useImageFallback.ts @@ -8,8 +8,8 @@ export function useImageFallback({ role, userCandidate, }: { - userId: string | undefined; - role: UserRole | undefined; + userId: string; + role: UserRole; userCandidate?: UserCandidateWithUsers; }) { const [urlImg, setUrlImg] = useState(null); diff --git a/src/pages/login.tsx b/src/pages/login.tsx index a9ea6a512..32c63c1e1 100644 --- a/src/pages/login.tsx +++ b/src/pages/login.tsx @@ -65,7 +65,7 @@ const Login = () => {
-
+

Connexion

{ /> { openModal( Date: Tue, 6 Feb 2024 09:35:44 +0100 Subject: [PATCH 08/21] chore(chromatic): update chromatic version --- .github/workflows/storybook.yml | 10 +++++----- src/components/utils/CardList/CardList.stories.tsx | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/storybook.yml b/.github/workflows/storybook.yml index d1cd8c508..8fddae4a4 100644 --- a/.github/workflows/storybook.yml +++ b/.github/workflows/storybook.yml @@ -2,9 +2,9 @@ name: 'Storybook deploy' # Event for the workflow -on: +on: push: - branches: + branches: - develop # List of jobs @@ -14,11 +14,11 @@ jobs: steps: - uses: actions/checkout@v1 - uses: actions/setup-node@v1 - with: + with: node-version: 16 - name: Install dependencies run: yarn - name: Publish to Chromatic - uses: chromaui/action@v1 + uses: chromaui/action@latest with: - projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} \ No newline at end of file + projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} diff --git a/src/components/utils/CardList/CardList.stories.tsx b/src/components/utils/CardList/CardList.stories.tsx index 24b543ef3..002925309 100644 --- a/src/components/utils/CardList/CardList.stories.tsx +++ b/src/components/utils/CardList/CardList.stories.tsx @@ -21,7 +21,7 @@ type Story = StoryObj; type ProfileCardProps = React.ComponentProps; -const cards: ProfileCardProps[] = new Array(50) +const cards: ProfileCardProps[] = new Array(4) .fill([ { userId: uuid(), From 6f4809cad13c4c0460dce10889d1c3f9e6a65a62 Mon Sep 17 00:00:00 2001 From: Emile Bex Date: Thu, 8 Feb 2024 10:20:33 +0100 Subject: [PATCH 09/21] [EN-6614] fix(directory): remove filter management from redux + fix filter on mobile (#202) * [EN-6614] fix(directory): fix first request filters * [EN-6614] fix(directory): remove filter management from redux * [EN-6614] fix(directory): refacto to remove some hooks --- .eslintrc.json | 86 +++++++-- src/api/api.ts | 13 +- src/api/types.ts | 8 + .../backoffice/Backoffice.styles.tsx | 1 + .../DirectoryContainer.styles.ts | 3 - .../DirectoryContainer/DirectoryContainer.tsx | 127 ++++++------- .../DirectoryContainer/useRoleFilter.ts | 11 -- .../directory/DirectoryList/DirectoryList.tsx | 14 +- .../backoffice/directory/useDirectory.ts | 41 +++-- .../directory/useDirectoryFilters.ts | 168 ------------------ ...ryParams.ts => useDirectoryQueryParams.ts} | 20 ++- .../backoffice/directory/useDirectoryRole.ts | 11 ++ .../directory/useDirectoryRoleRedirection.ts | 46 +++++ .../backoffice/profile/useSelectedProfile.ts | 2 +- src/components/filters/FiltersDropdowns.tsx | 7 +- src/components/filters/FiltersOptions.tsx | 2 +- src/components/filters/SearchBar.tsx | 2 +- .../AvailabilityTag.stories.tsx | 18 +- src/constants/index.ts | 2 + src/pages/backoffice/annuaire.tsx | 10 +- src/use-cases/profiles/profiles.adapters.ts | 4 +- src/use-cases/profiles/profiles.saga.ts | 64 ++++--- src/use-cases/profiles/profiles.selectors.ts | 28 +-- src/use-cases/profiles/profiles.slice.ts | 117 ++---------- 24 files changed, 335 insertions(+), 470 deletions(-) delete mode 100644 src/components/backoffice/directory/DirectoryContainer/useRoleFilter.ts delete mode 100644 src/components/backoffice/directory/useDirectoryFilters.ts rename src/components/backoffice/directory/{useDirectoryFiltersQueryParams.ts => useDirectoryQueryParams.ts} (56%) create mode 100644 src/components/backoffice/directory/useDirectoryRole.ts create mode 100644 src/components/backoffice/directory/useDirectoryRoleRedirection.ts diff --git a/.eslintrc.json b/.eslintrc.json index 70c145b45..14a162a43 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -22,7 +22,8 @@ "plugin:@next/next/recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended", - "plugin:prettier/recommended" // should always be the last element in the array + "plugin:prettier/recommended" + // should always be the last element in the array ], "env": { "browser": true, @@ -35,9 +36,22 @@ "rules": { // Plain JavaScript Rules "arrow-body-style": 0, - - "no-console": [1, { "allow": ["warn", "error"] }], - "no-multiple-empty-lines": [2, { "max": 1 }], // prettier like + "no-console": [ + 1, + { + "allow": [ + "warn", + "error" + ] + } + ], + "no-multiple-empty-lines": [ + 2, + { + "max": 1 + } + ], + // prettier like // max-len is enought "object-curly-newline": 0, // disable due to TypeScript params. More infos here: https://kendaleiv.com/typescript-constructor-assignment-public-and-private-keywords @@ -46,7 +60,10 @@ "max-classes-per-file": 0, "camelcase": "off", "curly": "warn", - "strict": ["error", "global"], + "strict": [ + "error", + "global" + ], "prefer-promise-reject-errors": "off", "no-return-assign": "off", "no-case-declarations": "off", @@ -68,23 +85,31 @@ "react/forbid-prop-types": "warn", "react/jsx-props-no-spreading": "off", "react/jsx-wrap-multilines": "off", - // with TypeScript, no default props "react/require-default-props": 0, - "react/static-property-placement": ["warn", "static getter"], + "react/static-property-placement": [ + "warn", + "static getter" + ], "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "error", "jsx-a11y/aria-role": [ 2, { - "allowedInvalidRoles": ["Candidat"], + "allowedInvalidRoles": [ + "Candidat" + ], "ignoreNonDOM": true } ], "react/jsx-filename-extension": [ "warn", { - "extensions": [".js", ".jsx", ".tsx"] + "extensions": [ + ".js", + ".jsx", + ".tsx" + ] } ], "prettier/prettier": [ @@ -94,9 +119,14 @@ "usePrettierrc": true } ], - // TypeScript Rules - "@typescript-eslint/no-unused-vars": [1, { "ignoreRestSiblings": true }], + "@typescript-eslint/no-unused-vars": [ + 1, + { + "ignoreRestSiblings": true, + "argsIgnorePattern": "^(_action|_state)" + } + ], "@typescript-eslint/member-delimiter-style": [ 1, { @@ -117,13 +147,17 @@ "@typescript-eslint/explicit-function-return-type": 0, "@typescript-eslint/ban-ts-ignore": 0, "@typescript-eslint/interface-name-prefix": 0, - // Import Rules // TODO put ts: never and tsx: never when all files have been transformed "import/extensions": [ 2, "ignorePackages", - { "ts": "never", "tsx": "never", "js": "never", "jsx": "never" } + { + "ts": "never", + "tsx": "never", + "js": "never", + "jsx": "never" + } ], "import/prefer-default-export": 0, "import/no-default-export": 2, @@ -173,14 +207,19 @@ }, "settings": { "import/parsers": { - "@typescript-eslint/parser": [".ts", ".tsx"] + "@typescript-eslint/parser": [ + ".ts", + ".tsx" + ] }, "import/resolver": { "typescript": { "project": "./tsconfig.json" }, "node": { - "paths": ["."] + "paths": [ + "." + ] } } }, @@ -192,21 +231,30 @@ "overrides": [ { // storybook use both default export and named exports - "files": ["src/**/*.stories.tsx", "./.storybook/**/*"], + "files": [ + "src/**/*.stories.tsx", + "./.storybook/**/*" + ], "rules": { "import/no-default-export": 0 } }, { - "files": ["src/pages/**"], + "files": [ + "src/pages/**" + ], "rules": { "import/no-default-export": 0, "import/prefer-default-export": 2 } }, { - "files": ["./**/*.ts"], - "excludedFiles": ["./**/*.spec.ts"], + "files": [ + "./**/*.ts" + ], + "excludedFiles": [ + "./**/*.spec.ts" + ], "rules": { "@jambit/typed-redux-saga/use-typed-effects": "error", "@jambit/typed-redux-saga/delegate-effects": "error" diff --git a/src/api/api.ts b/src/api/api.ts index c45875285..9fb84fa4e 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -5,7 +5,6 @@ import axios, { } from 'axios'; import _ from 'lodash'; import { AdminZone } from 'src/constants/departements'; -import { UserRole } from 'src/constants/users'; import { addAxiosInterceptors } from './interceptor'; import { APIRoute, @@ -21,6 +20,7 @@ import { OpportunityJoin, OpportunityUserEvent, OrganizationDto, + ProfilesFilters, PutCandidate, Route, SocialMedia, @@ -189,11 +189,12 @@ export class APIHandler { return this.get(`/user/profile/${userId}`); } - getAllUsersProfiles(params: { - role?: UserRole[]; - offset: number; - limit: number; - }): Promise { + getAllUsersProfiles( + params: ProfilesFilters & { + offset: number; + limit: number; + } + ): Promise { return this.get('/user/profile', { params, }); diff --git a/src/api/types.ts b/src/api/types.ts index c328321c2..d75762dae 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -551,3 +551,11 @@ export type PublicProfile = { lastSentMessage: string; lastReceivedMessage: string; }; + +export type ProfilesFilters = { + role: UserRole[]; + search?: string; + helps: HelpNames | HelpNames[]; + departments: Department | Department[]; + businessLines: BusinessLineValue | BusinessLineValue[]; +}; diff --git a/src/components/backoffice/Backoffice.styles.tsx b/src/components/backoffice/Backoffice.styles.tsx index 853a0ffb2..05a306e13 100644 --- a/src/components/backoffice/Backoffice.styles.tsx +++ b/src/components/backoffice/Backoffice.styles.tsx @@ -27,4 +27,5 @@ export const StyledNoResult = styled.div` font-size: 14px; font-style: italic; color: ${COLORS.darkGray}; + margin-top: 16px; `; diff --git a/src/components/backoffice/directory/DirectoryContainer/DirectoryContainer.styles.ts b/src/components/backoffice/directory/DirectoryContainer/DirectoryContainer.styles.ts index ab0455892..cfe0a3d32 100644 --- a/src/components/backoffice/directory/DirectoryContainer/DirectoryContainer.styles.ts +++ b/src/components/backoffice/directory/DirectoryContainer/DirectoryContainer.styles.ts @@ -9,7 +9,4 @@ export const StyledDirectoryButtonContainer = styled.div` display: flex; flex-wrap: wrap; gap: 16px; - margin-bottom: 30px; - position: relative; - margin-top: ${({ isMobile }) => (isMobile ? 16 : 0)}px; `; diff --git a/src/components/backoffice/directory/DirectoryContainer/DirectoryContainer.tsx b/src/components/backoffice/directory/DirectoryContainer/DirectoryContainer.tsx index 8d8fdb986..fcf30d89e 100644 --- a/src/components/backoffice/directory/DirectoryContainer/DirectoryContainer.tsx +++ b/src/components/backoffice/directory/DirectoryContainer/DirectoryContainer.tsx @@ -1,10 +1,9 @@ import { useRouter } from 'next/router'; import React, { useMemo } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; import { DirectoryList } from '../DirectoryList'; -import { useDirectoryFiltersQueryParams } from '../useDirectoryFiltersQueryParams'; +import { useDirectoryQueryParams } from '../useDirectoryQueryParams'; import { SearchBar } from 'src/components/filters/SearchBar'; -import { Button } from 'src/components/utils'; +import { Button, Section } from 'src/components/utils'; import { BUSINESS_LINES, DirectoryFilters } from 'src/constants'; import { DEPARTMENTS_FILTERS } from 'src/constants/departements'; import { ProfileHelps } from 'src/constants/helps'; @@ -17,28 +16,24 @@ import { import { useFilters } from 'src/hooks'; import { useIsMobile } from 'src/hooks/utils'; import { - profilesActions, - selectProfilesBusinessLinesFilters, - selectProfilesDepartmentsFilters, - selectProfilesHelpsFilters, - selectProfilesSearchFilter, -} from 'src/use-cases/profiles'; -import { findConstantFromValue, isRoleIncluded } from 'src/utils'; + findConstantFromValue, + isRoleIncluded, + mutateToArray, +} from 'src/utils'; import { StyledDirectoryButtonContainer, StyledDirectoryContainer, } from './DirectoryContainer.styles'; -import { useRoleFilter } from './useRoleFilter'; const route = '/backoffice/annuaire'; export function DirectoryContainer() { const { push } = useRouter(); - const roleFilter = useRoleFilter(); - const dispatch = useDispatch(); - const isMobile = useIsMobile(); - const directoryFiltersParams = useDirectoryFiltersQueryParams(); + + const directoryFiltersParams = useDirectoryQueryParams(); + const { role, departments, helps, businessLines, search } = + directoryFiltersParams; const { setFilters, setSearch, resetFilters } = useFilters( DirectoryFilters, @@ -47,67 +42,73 @@ export function DirectoryContainer() { GA_TAGS.PAGE_ANNUAIRE_SUPPRIMER_FILTRES_CLIC ); - const departmentsFilters = useSelector(selectProfilesDepartmentsFilters); - const helpsFilters = useSelector(selectProfilesHelpsFilters); - const businessLinesFilters = useSelector(selectProfilesBusinessLinesFilters); - const search = useSelector(selectProfilesSearchFilter); - const filters = useMemo(() => { return { - departments: departmentsFilters.map((department) => + departments: mutateToArray(departments).map((department) => findConstantFromValue(department, DEPARTMENTS_FILTERS) ), - helps: helpsFilters.map((help) => + helps: mutateToArray(helps).map((help) => findConstantFromValue(help, ProfileHelps) ), - businessLines: businessLinesFilters.map((businessLine) => + businessLines: mutateToArray(businessLines).map((businessLine) => findConstantFromValue(businessLine, BUSINESS_LINES) ), }; - }, [departmentsFilters, helpsFilters, businessLinesFilters]); + }, [departments, helps, businessLines]); return ( - { - dispatch(profilesActions.resetProfilesFilters()); - resetFilters(); - }} - search={search || undefined} - setSearch={setSearch} - setFilters={setFilters} - placeholder="Rechercher..." - /> - - - - + search={search} + setSearch={setSearch} + setFilters={setFilters} + placeholder="Rechercher..." + additionalButtons={ + + + + + } + /> +
); diff --git a/src/components/backoffice/directory/DirectoryContainer/useRoleFilter.ts b/src/components/backoffice/directory/DirectoryContainer/useRoleFilter.ts deleted file mode 100644 index 0c173d650..000000000 --- a/src/components/backoffice/directory/DirectoryContainer/useRoleFilter.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useSelector } from 'react-redux'; -import { selectProfilesRoleFilter } from 'src/use-cases/profiles'; - -export function useRoleFilter() { - const roleFilter = useSelector(selectProfilesRoleFilter); - if (!roleFilter) { - throw new Error('No default role'); - } - - return roleFilter; -} diff --git a/src/components/backoffice/directory/DirectoryList/DirectoryList.tsx b/src/components/backoffice/directory/DirectoryList/DirectoryList.tsx index 019d8e16f..7872acba6 100644 --- a/src/components/backoffice/directory/DirectoryList/DirectoryList.tsx +++ b/src/components/backoffice/directory/DirectoryList/DirectoryList.tsx @@ -1,26 +1,16 @@ import React, { useMemo } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useSelector } from 'react-redux'; import { DirectoryItem } from '../DirectoryItem'; import { useDirectory } from '../useDirectory'; import { CardList } from 'src/components/utils/CardList'; import { CANDIDATE_USER_ROLES } from 'src/constants/users'; -import { useIsAtBottom } from 'src/hooks/useIsAtBottom'; -import { - fetchProfilesSelectors, - profilesActions, -} from 'src/use-cases/profiles'; +import { fetchProfilesSelectors } from 'src/use-cases/profiles'; import { isRoleIncluded } from 'src/utils'; import { StyledDirectoryListContainer } from './DirectoryList.styles'; export function DirectoryList() { const { profiles } = useDirectory(); - const dispatch = useDispatch(); - - useIsAtBottom(() => { - dispatch(profilesActions.incrementProfilesOffset()); - }); - const isFetchProfilesRequested = useSelector( fetchProfilesSelectors.selectIsFetchProfilesRequested ); diff --git a/src/components/backoffice/directory/useDirectory.ts b/src/components/backoffice/directory/useDirectory.ts index fd052b179..c174b5b09 100644 --- a/src/components/backoffice/directory/useDirectory.ts +++ b/src/components/backoffice/directory/useDirectory.ts @@ -1,33 +1,54 @@ +import _ from 'lodash'; import { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import UIkit from 'uikit'; -import { ReduxRequestEvents } from 'src/constants'; +import { useIsAtBottom } from 'src/hooks/useIsAtBottom'; import { usePrevious } from 'src/hooks/utils'; import { fetchProfilesSelectors, profilesActions, selectProfiles, } from 'src/use-cases/profiles'; +import { useDirectoryQueryParams } from './useDirectoryQueryParams'; +// Manage directory requests and filters export function useDirectory() { const dispatch = useDispatch(); - const fetchProfilesStatus = useSelector( - fetchProfilesSelectors.selectFetchProfilesStatus + const directoryFiltersParams = useDirectoryQueryParams(); + + const prevDirectoryFiltersParams = usePrevious(directoryFiltersParams); + + useEffect(() => { + if (!_.isEqual(prevDirectoryFiltersParams, directoryFiltersParams)) { + dispatch( + profilesActions.fetchProfilesWithFilters(directoryFiltersParams) + ); + } + }, [dispatch, directoryFiltersParams, prevDirectoryFiltersParams]); + + const isFetchProfileStatusFailed = useSelector( + fetchProfilesSelectors.selectIsFetchProfilesFailed ); - const prevIsFetchProfilesStatus = usePrevious(fetchProfilesStatus); const profiles = useSelector(selectProfiles); useEffect(() => { - if (prevIsFetchProfilesStatus === ReduxRequestEvents.REQUESTED) { - if (fetchProfilesStatus === ReduxRequestEvents.FAILED) { - UIkit.notification('Une erreur est survenue', 'danger'); - } + if (isFetchProfileStatusFailed) { + UIkit.notification('Une erreur est survenue', 'danger'); + } + }, [dispatch, isFetchProfileStatusFailed]); + useEffect(() => { + return () => { dispatch(profilesActions.fetchProfilesReset()); - } - }, [dispatch, fetchProfilesStatus, prevIsFetchProfilesStatus]); + }; + }, [dispatch]); + + // Manage offset and profiles request when scrolling to the bottom of the page + useIsAtBottom(() => { + dispatch(profilesActions.fetchProfilesNextPage(directoryFiltersParams)); + }); return { profiles, diff --git a/src/components/backoffice/directory/useDirectoryFilters.ts b/src/components/backoffice/directory/useDirectoryFilters.ts deleted file mode 100644 index e2855a489..000000000 --- a/src/components/backoffice/directory/useDirectoryFilters.ts +++ /dev/null @@ -1,168 +0,0 @@ -import _ from 'lodash'; -import { useRouter } from 'next/router'; -import { useEffect, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { CANDIDATE_USER_ROLES, USER_ROLES } from 'src/constants/users'; -import { useAuthenticatedUser } from 'src/hooks/authentication/useAuthenticatedUser'; -import { useRole } from 'src/hooks/queryParams/useRole'; -import { usePrevious } from 'src/hooks/utils'; -import { - profilesActions, - selectProfilesIsResetFilters, -} from 'src/use-cases/profiles'; -import { isRoleIncluded } from 'src/utils'; -import { useDirectoryFiltersQueryParams } from './useDirectoryFiltersQueryParams'; - -const route = '/backoffice/annuaire'; - -export function useDirectoryFilters() { - const { replace } = useRouter(); - const user = useAuthenticatedUser(); - - const isResetFilters = useSelector(selectProfilesIsResetFilters); - const prevIsResetFilters = usePrevious(isResetFilters); - - const [isFirstRequest, setIsFirstRequest] = useState(true); - - const dispatch = useDispatch(); - - const directoryFiltersParams = useDirectoryFiltersQueryParams(); - const { search, helps, businessLines, departments } = directoryFiltersParams; - - const role = useRole(); - const prevRole = usePrevious(role); - const prevSearch = usePrevious(search); - const prevHelps = usePrevious(helps); - const prevDepartments = usePrevious(departments); - const prevBusinessLines = usePrevious(businessLines); - - useEffect(() => { - if (!role) { - if (isRoleIncluded(CANDIDATE_USER_ROLES, user.role)) { - replace( - { - pathname: route, - query: { - ...directoryFiltersParams, - role: USER_ROLES.COACH, - }, - }, - undefined, - { shallow: true } - ); - } else { - replace( - { - pathname: route, - query: { - ...directoryFiltersParams, - role: CANDIDATE_USER_ROLES, - }, - }, - undefined, - { shallow: true } - ); - } - } - }, [ - replace, - businessLines, - departments, - helps, - role, - search, - user.role, - directoryFiltersParams, - ]); - - useEffect(() => { - if (isFirstRequest && role && role.length > 0) { - dispatch( - profilesActions.setProfilesFilters({ - role, - search, - helps, - businessLines, - departments, - }) - ); - setIsFirstRequest(false); - } - }, [ - dispatch, - businessLines, - departments, - helps, - isFirstRequest, - role, - search, - ]); - - useEffect(() => { - if (isResetFilters && !prevIsResetFilters) { - dispatch( - profilesActions.setProfilesFilters({ - role, - search: null, - helps: [], - businessLines: [], - departments: [], - }) - ); - } - }, [ - dispatch, - businessLines, - departments, - helps, - role, - search, - isFirstRequest, - isResetFilters, - prevIsResetFilters, - ]); - - useEffect(() => { - if (!isFirstRequest && !isResetFilters && !_.isEqual(role, prevRole)) { - dispatch(profilesActions.setProfilesRoleFilter(role)); - } - }, [dispatch, isFirstRequest, role, prevRole, isResetFilters]); - - useEffect(() => { - if (!isFirstRequest && !isResetFilters && !_.isEqual(search, prevSearch)) { - dispatch(profilesActions.setProfilesSearchFilter(search)); - } - }, [dispatch, isFirstRequest, isResetFilters, prevSearch, search]); - - useEffect(() => { - if (!isFirstRequest && !isResetFilters && !_.isEqual(helps, prevHelps)) { - dispatch(profilesActions.setProfilesHelpsFilter(helps)); - } - }, [dispatch, isFirstRequest, helps, prevHelps, isResetFilters]); - - useEffect(() => { - if ( - !isFirstRequest && - !isResetFilters && - !_.isEqual(departments, prevDepartments) - ) { - dispatch(profilesActions.setProfilesDepartmentsFilter(departments)); - } - }, [dispatch, isFirstRequest, departments, prevDepartments, isResetFilters]); - - useEffect(() => { - if ( - !isFirstRequest && - !isResetFilters && - !_.isEqual(businessLines, prevBusinessLines) - ) { - dispatch(profilesActions.setProfilesBusinessLinesFilter(businessLines)); - } - }, [ - dispatch, - isFirstRequest, - businessLines, - prevBusinessLines, - isResetFilters, - ]); -} diff --git a/src/components/backoffice/directory/useDirectoryFiltersQueryParams.ts b/src/components/backoffice/directory/useDirectoryQueryParams.ts similarity index 56% rename from src/components/backoffice/directory/useDirectoryFiltersQueryParams.ts rename to src/components/backoffice/directory/useDirectoryQueryParams.ts index 44061405b..fd88235a2 100644 --- a/src/components/backoffice/directory/useDirectoryFiltersQueryParams.ts +++ b/src/components/backoffice/directory/useDirectoryQueryParams.ts @@ -1,19 +1,33 @@ import { useRouter } from 'next/router'; +import { ProfilesFilters } from 'src/api/types'; import { BusinessLineValue } from 'src/constants'; import { Department } from 'src/constants/departements'; import { HelpNames } from 'src/constants/helps'; +import { useDirectoryRole } from './useDirectoryRole'; + +// Get the current query params for the directory filters +export function useDirectoryQueryParams() { + const role = useDirectoryRole(); -export function useDirectoryFiltersQueryParams() { const { query: { search, helps, businessLines, departments }, } = useRouter(); - return { - search: (search || null) as string | null, + const filters: ProfilesFilters = { + role, helps: (helps || []) as HelpNames | HelpNames[], businessLines: (businessLines || []) as | BusinessLineValue | BusinessLineValue[], departments: (departments || []) as Department | Department[], }; + + if (search) { + return { + ...filters, + search: search as string, + }; + } + + return filters; } diff --git a/src/components/backoffice/directory/useDirectoryRole.ts b/src/components/backoffice/directory/useDirectoryRole.ts new file mode 100644 index 000000000..62505a1ae --- /dev/null +++ b/src/components/backoffice/directory/useDirectoryRole.ts @@ -0,0 +1,11 @@ +import { useRole } from 'src/hooks/queryParams/useRole'; + +// Failsafe to maake sure that the role query param is set +export function useDirectoryRole() { + const roleFilter = useRole(); + if (!roleFilter) { + throw new Error('No default role'); + } + + return roleFilter; +} diff --git a/src/components/backoffice/directory/useDirectoryRoleRedirection.ts b/src/components/backoffice/directory/useDirectoryRoleRedirection.ts new file mode 100644 index 000000000..5bba703ae --- /dev/null +++ b/src/components/backoffice/directory/useDirectoryRoleRedirection.ts @@ -0,0 +1,46 @@ +import { useRouter } from 'next/router'; +import { useEffect } from 'react'; +import { CANDIDATE_USER_ROLES, USER_ROLES } from 'src/constants/users'; +import { useAuthenticatedUser } from 'src/hooks/authentication/useAuthenticatedUser'; +import { useRole } from 'src/hooks/queryParams/useRole'; +import { isRoleIncluded } from 'src/utils'; + +const route = '/backoffice/annuaire'; + +// Manage redirection to add the mandatory role query param +export function useDirectoryRoleRedirection() { + const { replace, query } = useRouter(); + const { role: userRole } = useAuthenticatedUser(); + + const role = useRole(); + + useEffect(() => { + if (!role) { + if (isRoleIncluded(CANDIDATE_USER_ROLES, userRole)) { + replace( + { + pathname: route, + query: { + ...query, + role: USER_ROLES.COACH, + }, + }, + undefined, + { shallow: true } + ); + } else { + replace( + { + pathname: route, + query: { + ...query, + role: CANDIDATE_USER_ROLES, + }, + }, + undefined, + { shallow: true } + ); + } + } + }, [replace, role, userRole, query]); +} diff --git a/src/components/backoffice/profile/useSelectedProfile.ts b/src/components/backoffice/profile/useSelectedProfile.ts index a9c53391f..179c2e4a5 100644 --- a/src/components/backoffice/profile/useSelectedProfile.ts +++ b/src/components/backoffice/profile/useSelectedProfile.ts @@ -28,7 +28,7 @@ export function useSelectedProfile() { if (userId && userId !== prevUserId) { dispatch( profilesActions.fetchSelectedProfileRequested({ - userId: userId as string, + userId, }) ); } diff --git a/src/components/filters/FiltersDropdowns.tsx b/src/components/filters/FiltersDropdowns.tsx index 63a71bbf3..5af3fd760 100644 --- a/src/components/filters/FiltersDropdowns.tsx +++ b/src/components/filters/FiltersDropdowns.tsx @@ -4,6 +4,7 @@ import { v4 as uuid } from 'uuid'; import CaretDownIcon from 'assets/icons/caret-down.svg'; import { Button, Tag } from 'src/components/utils'; import { Filter, FilterConstant, FilterObject } from 'src/constants/utils'; +import { useIsDesktop } from 'src/hooks/utils'; import { gaEvent } from 'src/lib/gtag'; const uuidValue1 = uuid(); @@ -28,6 +29,8 @@ export const FiltersDropdowns = ({ showSeparator, smallSelectors, }: FiltersDropdownProps) => { + const isDesktop = useIsDesktop(); + const renderFilters = useCallback( ( filterConstants: FilterConstant[], @@ -121,7 +124,9 @@ export const FiltersDropdowns = ({ style={{ opacity: disabled ? 0.6 : 1 }} > + )} + + + )} + {opportunities && opportunities.length > 0 && !isDataLoading && ( + <> +
+ + {opportunities.map((opportunity, i) => { + return ( + + + + {opportunity.title.toUpperCase()} + + + {opportunity.businessLines.map((businessLine, k) => { + return ( + + ); + })} + + + {buildContractLabel( + opportunity.contract, + opportunity.endOfContract, + opportunity.startOfContract + )}{' '} + - {opportunity.department} + + + + ); + })} + + + )} + {!isDataLoading && opportunities && opportunities.length === 0 && ( + <> + )} + + + + + ) : ( + + + + )} + + ); +}; diff --git a/src/components/backoffice/dashboard/DashboardOpportunitiesCard/index.ts b/src/components/backoffice/dashboard/DashboardOpportunitiesCard/index.ts new file mode 100644 index 000000000..00395ff9c --- /dev/null +++ b/src/components/backoffice/dashboard/DashboardOpportunitiesCard/index.ts @@ -0,0 +1 @@ +export * from './DashboardOpportunitiesCard'; diff --git a/src/components/backoffice/dashboard/DashboardOpportunitiesCard/useDashboardOpportunities.ts b/src/components/backoffice/dashboard/DashboardOpportunitiesCard/useDashboardOpportunities.ts new file mode 100644 index 000000000..a09435191 --- /dev/null +++ b/src/components/backoffice/dashboard/DashboardOpportunitiesCard/useDashboardOpportunities.ts @@ -0,0 +1,81 @@ +import { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import UIkit from 'uikit'; +import { + fetchOpportunitiesAsCandidateSelectors, + fetchOpportunitiesTabCountsSelectors, + opportunitiesActions, + fetchDashboardOpportunitiesSelectors, + selectDashboardOpportunities, + selectNumberOfOpportunitiesInProgress, +} from 'src/use-cases/opportunities'; + +export const useDashboardOpportunities = () => { + const dispatch = useDispatch(); + // opportunities + const opportunities = useSelector(selectDashboardOpportunities); + const isFetchDashboardOpportunitiesIdle = useSelector( + fetchDashboardOpportunitiesSelectors.selectIsFetchDashboardOpportunitiesIdle + ); + const isFetchDashboardOpportunitiesRequested = useSelector( + fetchDashboardOpportunitiesSelectors.selectIsFetchDashboardOpportunitiesRequested + ); + const isFetchOpportunitiesFailed = useSelector( + fetchOpportunitiesAsCandidateSelectors.selectIsFetchOpportunitiesAsCandidateFailed + ); + const isDataLoading = + isFetchDashboardOpportunitiesIdle || isFetchDashboardOpportunitiesRequested; + + // number of opportunities in progress + const numberOpportunitiesInProgess = useSelector( + selectNumberOfOpportunitiesInProgress + ); + const isFetchOpportunitiesTabCountsFailed = useSelector( + fetchOpportunitiesTabCountsSelectors.selectIsFetchOpportunitiesTabCountsFailed + ); + const isFetchOpportunitiesTabCountsIdle = useSelector( + fetchOpportunitiesTabCountsSelectors.selectIsFetchOpportunitiesTabCountsIdle + ); + + // fetch opportunities + useEffect(() => { + if (isFetchDashboardOpportunitiesIdle) { + dispatch(opportunitiesActions.fetchDashboardOpportunitiesRequested()); + } + }, [dispatch, isFetchDashboardOpportunitiesIdle]); + + // fetch tab counts + useEffect(() => { + if (isFetchOpportunitiesTabCountsIdle) { + dispatch(opportunitiesActions.fetchOpportunitiesTabCountsRequested()); + } + }, [dispatch, isFetchOpportunitiesTabCountsIdle]); + + // clean on unmount + useEffect(() => { + return () => { + dispatch(opportunitiesActions.fetchDashboardOpportunitiesReset()); + dispatch(opportunitiesActions.fetchOpportunitiesTabCountsReset()); + }; + }, [dispatch]); + + // notif on error for opportunitie fails + useEffect(() => { + if (isFetchOpportunitiesFailed) { + UIkit.notification('Une erreur est survenue', 'danger'); + } + }, [isFetchOpportunitiesFailed, dispatch]); + + // notif on error for tab counts fails + useEffect(() => { + if (isFetchOpportunitiesTabCountsFailed) { + UIkit.notification('Une erreur est survenue', 'danger'); + } + }, [isFetchOpportunitiesTabCountsFailed, dispatch]); + + return { + opportunities, + numberOpportunitiesInProgess, + isDataLoading, + }; +}; diff --git a/src/components/backoffice/dashboard/DashboardProfileCard/DashboardProfileCard.styles.tsx b/src/components/backoffice/dashboard/DashboardProfileCard/DashboardProfileCard.styles.tsx index bda7064a2..329b4ec83 100644 --- a/src/components/backoffice/dashboard/DashboardProfileCard/DashboardProfileCard.styles.tsx +++ b/src/components/backoffice/dashboard/DashboardProfileCard/DashboardProfileCard.styles.tsx @@ -1,4 +1,5 @@ import styled from 'styled-components'; +import { COLORS } from 'src/constants/styles'; export const StyledDashboardProfileCardPictureName = styled.div` display: flex; @@ -33,7 +34,7 @@ export const StyledDashboardProfileCardHelps = styled.div` export const StyledDashboardProfileCardhelpsTitle = styled.div` width: 100%; padding-bottom: 15px; - border-bottom: 1px solid #fddfd2; + border-bottom: 1px solid ${COLORS.hoverOrange}; margin-bottom: 30px; font-size: 16px; `; diff --git a/src/components/backoffice/parametres/ParametresLayout/UserInformationCard/LinkedUserInformationCard.tsx b/src/components/backoffice/parametres/ParametresLayout/UserInformationCard/LinkedUserInformationCard.tsx index 39495403a..1f775d234 100644 --- a/src/components/backoffice/parametres/ParametresLayout/UserInformationCard/LinkedUserInformationCard.tsx +++ b/src/components/backoffice/parametres/ParametresLayout/UserInformationCard/LinkedUserInformationCard.tsx @@ -28,7 +28,7 @@ export const LinkedUserInformationCard = ({ const assignUser = useCallback((userToAssign) => { if (isRoleIncluded(COACH_USER_ROLES, userToAssign.role)) { - const candidat: UserWithUserCandidate | UserWithUserCandidate[] = + const candidat: UserWithUserCandidate[] | null = getRelatedUser(userToAssign); if (candidat) { setLinkedUser(candidat); @@ -37,7 +37,8 @@ export const LinkedUserInformationCard = ({ } } if (isRoleIncluded(CANDIDATE_USER_ROLES, userToAssign.role)) { - const coach = getRelatedUser(userToAssign); + const coach: UserWithUserCandidate[] | null = + getRelatedUser(userToAssign); if (coach) { setLinkedUser(coach); } else { @@ -144,6 +145,7 @@ export const LinkedUserInformationCard = ({ {!isAdmin && isRoleIncluded(COACH_USER_ROLES, user.role) && + userCandidat && !singleLinkedUser.deletedAt && (
  • diff --git a/src/components/backoffice/profile/useSelectedProfile.ts b/src/components/backoffice/profile/useSelectedProfile.ts index 179c2e4a5..e0f66d061 100644 --- a/src/components/backoffice/profile/useSelectedProfile.ts +++ b/src/components/backoffice/profile/useSelectedProfile.ts @@ -1,9 +1,7 @@ import { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import UIkit from 'uikit'; -import { ReduxRequestEvents } from 'src/constants'; import { useUserId } from 'src/hooks/queryParams/useUserId'; -import { usePrevious } from 'src/hooks/utils'; import { profilesActions } from 'src/use-cases/profiles'; import { fetchSelectedProfileSelectors, @@ -12,36 +10,34 @@ import { export function useSelectedProfile() { const userId = useUserId(); - const prevUserId = usePrevious(userId); const dispatch = useDispatch(); - const fetchSelectedProfileStatus = useSelector( - fetchSelectedProfileSelectors.selectFetchSelectedProfileStatus + const isFetchSelectedProfileFailed = useSelector( + fetchSelectedProfileSelectors.selectIsFetchSelectedProfileFailed ); - const prevFetchSelectedProfileStatus = usePrevious( - fetchSelectedProfileStatus - ); - const selectedProfile = useSelector(selectSelectedProfile); useEffect(() => { - if (userId && userId !== prevUserId) { + if (userId) { dispatch( profilesActions.fetchSelectedProfileRequested({ userId, }) ); } - }, [dispatch, userId, prevUserId]); + }, [dispatch, userId]); useEffect(() => { - if (prevFetchSelectedProfileStatus === ReduxRequestEvents.REQUESTED) { - if (fetchSelectedProfileStatus === ReduxRequestEvents.FAILED) { - UIkit.notification('Une erreur est survenue', 'danger'); - } - dispatch(profilesActions.fetchSelectedProfileReset()); + if (isFetchSelectedProfileFailed) { + UIkit.notification('Une erreur est survenue', 'danger'); } - }, [dispatch, fetchSelectedProfileStatus, prevFetchSelectedProfileStatus]); + }, [dispatch, isFetchSelectedProfileFailed]); + + useEffect(() => { + return () => { + dispatch(profilesActions.fetchSelectedProfileReset()); + }; + }, [dispatch]); return { selectedProfile, diff --git a/src/components/utils/Inputs/CheckBox/useCheckBox.ts b/src/components/utils/Inputs/CheckBox/useCheckBox.ts index 86ccd1b18..aa709f545 100644 --- a/src/components/utils/Inputs/CheckBox/useCheckBox.ts +++ b/src/components/utils/Inputs/CheckBox/useCheckBox.ts @@ -1,8 +1,8 @@ import { useState, useCallback } from 'react'; export function useCheckBox( - callback: (args: T) => void, params: T, + callback?: (args: T) => void, defaultChecked = false ) { const [checked, setChecked] = useState(defaultChecked); diff --git a/src/hooks/queryParams/useOpportunityType.ts b/src/hooks/queryParams/useOpportunityType.ts index 7e9a66d2e..c078558e5 100644 --- a/src/hooks/queryParams/useOpportunityType.ts +++ b/src/hooks/queryParams/useOpportunityType.ts @@ -1,9 +1,10 @@ import { useRouter } from 'next/router'; +import { OpportunityType } from 'src/api/types'; export function useOpportunityType() { const { query: { type }, } = useRouter(); - return type as 'public' | 'private'; + return type as OpportunityType; } diff --git a/src/use-cases/authentication/authentication.selectors.ts b/src/use-cases/authentication/authentication.selectors.ts index 123553166..c3c638c3b 100644 --- a/src/use-cases/authentication/authentication.selectors.ts +++ b/src/use-cases/authentication/authentication.selectors.ts @@ -1,3 +1,10 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { UserCandidateWithUsers, UserProfile } from 'src/api/types'; +import { + getCandidateIdFromCoachOrCandidate, + getUserCandidateFromCoachOrCandidate, + mutateToArray, +} from 'src/utils'; import { fetchUserAdapter, loginAdapter, @@ -53,3 +60,57 @@ export function selectUserUpdateError(state: RootState) { export function selectProfileUpdateError(state: RootState) { return state.authentication.profileUpdateError; } + +// select candidate for the current user => doesn't work for external coach +export function selectCandidate( + state: RootState +): UserCandidateWithUsers | null { + if (state.authentication.user) { + let candidate = getUserCandidateFromCoachOrCandidate( + state.authentication.user + ); + if (Array.isArray(candidate)) { + [candidate] = candidate; + } + return candidate; + } + return null; +} + +// select candidateId for the current user => doesn't work for external coach +export function selectCandidateId(state: RootState): string | null { + if (state.authentication.user) { + let candidateId = getCandidateIdFromCoachOrCandidate( + state.authentication.user + ); + if (Array.isArray(candidateId)) { + [candidateId] = candidateId; + } + return candidateId; + } + return null; +} + +// select department and businesslines from the profile of the current user's candidate => doesn't work for external coach +export const selectCandidateProfileDefaultFiltersForDashboardOpportunities = + createSelector( + (state: RootState) => state.authentication.user, + (user) => { + let userCandidateProfile: UserProfile; + if (user) { + const candidate = getUserCandidateFromCoachOrCandidate(user); + if (Array.isArray(candidate) && candidate[0]?.candidat?.userProfile) { + userCandidateProfile = candidate[0]?.candidat?.userProfile; + } else { + userCandidateProfile = user?.userProfile; + } + return { + department: mutateToArray(userCandidateProfile.department), + businessLines: userCandidateProfile.searchBusinessLines.map( + (businessLine) => businessLine.name + ), + }; + } + return null; + } + ); diff --git a/src/use-cases/cv/cv.adapter.ts b/src/use-cases/cv/cv.adapter.ts new file mode 100644 index 000000000..5dabc2b35 --- /dev/null +++ b/src/use-cases/cv/cv.adapter.ts @@ -0,0 +1,7 @@ +import { CV } from 'src/api/types'; +import { createRequestAdapter } from 'src/store/utils'; + +export const fetchCVAdapter = createRequestAdapter('fetchCV').withPayloads< + string, // userId + CV +>(); diff --git a/src/use-cases/cv/cv.saga.ts b/src/use-cases/cv/cv.saga.ts new file mode 100644 index 000000000..398113c71 --- /dev/null +++ b/src/use-cases/cv/cv.saga.ts @@ -0,0 +1,20 @@ +import { call, put, takeLatest } from 'typed-redux-saga'; +import { Api } from 'src/api'; +import { slice } from './cv.slice'; + +const { fetchCVSucceeded, fetchCVFailed, fetchCVRequested } = slice.actions; + +function* fetchCVSagaRequested(action: ReturnType) { + const candidateId = action.payload; + + try { + const response = yield* call(() => Api.getCVByCandidateId(candidateId)); + yield* put(fetchCVSucceeded(response.data)); + } catch { + yield* put(fetchCVFailed()); + } +} + +export function* saga() { + yield* takeLatest(fetchCVRequested, fetchCVSagaRequested); +} diff --git a/src/use-cases/cv/cv.selectors.ts b/src/use-cases/cv/cv.selectors.ts new file mode 100644 index 000000000..67c58bb53 --- /dev/null +++ b/src/use-cases/cv/cv.selectors.ts @@ -0,0 +1,10 @@ +import { fetchCVAdapter } from './cv.adapter'; +import { RootState } from './cv.slice'; + +export const fetchCVSelectors = fetchCVAdapter.getSelectors( + (state) => state.cv.fetchCV +); + +export function selectCV(state: RootState) { + return state.cv.currentCv; +} diff --git a/src/use-cases/cv/cv.slice.ts b/src/use-cases/cv/cv.slice.ts new file mode 100644 index 000000000..8e48dea98 --- /dev/null +++ b/src/use-cases/cv/cv.slice.ts @@ -0,0 +1,28 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { CV } from 'src/api/types'; +import { RequestState, SliceRootState } from 'src/store/utils'; +import { fetchCVAdapter } from './cv.adapter'; + +export interface State { + fetchCV: RequestState; + currentCv: CV | null; +} + +const initialState: State = { + fetchCV: fetchCVAdapter.getInitialState(), + currentCv: null, +}; + +export const slice = createSlice({ + name: 'cv', + initialState, + reducers: { + ...fetchCVAdapter.getReducers((state) => state.fetchCV, { + fetchCVSucceeded(state, action) { + state.currentCv = action.payload; + }, + }), + }, +}); + +export type RootState = SliceRootState; diff --git a/src/use-cases/cv/index.ts b/src/use-cases/cv/index.ts new file mode 100644 index 000000000..7089b4c05 --- /dev/null +++ b/src/use-cases/cv/index.ts @@ -0,0 +1,20 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { Saga } from 'redux-saga'; +import { saga } from './cv.saga'; +import { slice } from './cv.slice'; + +export type UseCaseConfigItem = { + slice: ReturnType; + saga: Saga; +}; + +export type UseCaseConfigType = Record; + +export * from './cv.selectors'; + +export const cvActions = slice.actions; + +export const cvConfig = { + slice, + saga, +} as UseCaseConfigItem; diff --git a/src/use-cases/index.ts b/src/use-cases/index.ts index 33976102a..f023c386f 100644 --- a/src/use-cases/index.ts +++ b/src/use-cases/index.ts @@ -1,7 +1,11 @@ import { authenticationConfig, UseCaseConfigType } from './authentication'; +import { cvConfig } from './cv'; +import { opportunitiesConfig } from './opportunities'; import { profilesConfig } from './profiles'; export const useCasesConfig: UseCaseConfigType = { authenticationConfig, profilesConfig, + cvConfig, + opportunitiesConfig, }; diff --git a/src/use-cases/opportunities/index.ts b/src/use-cases/opportunities/index.ts new file mode 100644 index 000000000..194ba0ee1 --- /dev/null +++ b/src/use-cases/opportunities/index.ts @@ -0,0 +1,18 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { Saga } from 'redux-saga'; +import { saga } from './opportunities.saga'; +import { slice } from './opportunities.slice'; + +export type UseCaseConfigItem = { + slice: ReturnType; + saga: Saga; +}; + +export * from './opportunities.selectors'; + +export const opportunitiesActions = slice.actions; + +export const opportunitiesConfig = { + slice, + saga, +} as UseCaseConfigItem; diff --git a/src/use-cases/opportunities/opportunities.adapters.ts b/src/use-cases/opportunities/opportunities.adapters.ts new file mode 100644 index 000000000..cab081b54 --- /dev/null +++ b/src/use-cases/opportunities/opportunities.adapters.ts @@ -0,0 +1,24 @@ +import { + OpportunitiesFiltersForCandidate, + Opportunity, + OpportunityTabCount, +} from 'src/api/types'; +import { createRequestAdapter } from 'src/store/utils'; + +export const fetchOpportunitiesAsCandidateAdapter = createRequestAdapter( + 'fetchOpportunitiesAsCandidate' +).withPayloads< + OpportunitiesFiltersForCandidate & { + candidateId: string; + limit: number; + }, + Opportunity[] +>(); + +export const fetchDashboardOpportunitiesAdapter = createRequestAdapter( + 'fetchDashboardOpportunities' +).withPayloads(); + +export const fetchOpportunitiesTabCountsAdapter = createRequestAdapter( + 'fetchOpportunitiesTabCounts' +).withPayloads(); diff --git a/src/use-cases/opportunities/opportunities.saga.ts b/src/use-cases/opportunities/opportunities.saga.ts new file mode 100644 index 000000000..c112e8aa2 --- /dev/null +++ b/src/use-cases/opportunities/opportunities.saga.ts @@ -0,0 +1,108 @@ +import { call, put, select, takeLatest } from 'typed-redux-saga'; +import { + selectCandidateId, + selectCandidateProfileDefaultFiltersForDashboardOpportunities, +} from '../authentication'; +import { selectProfilesOffset } from '../profiles'; +import { Api } from 'src/api'; +import { mutateToArray } from 'src/utils'; +import { slice } from './opportunities.slice'; + +const { + fetchOpportunitiesAsCandidateSucceeded, + fetchOpportunitiesAsCandidateFailed, + fetchOpportunitiesAsCandidateRequested, + fetchOpportunitiesTabCountsSucceeded, + fetchOpportunitiesTabCountsFailed, + fetchOpportunitiesTabCountsRequested, + fetchOpportunitiesAsCandidateWithFilters, + resetOpportunitiesOffset, + fetchDashboardOpportunitiesSucceeded, + fetchDashboardOpportunitiesFailed, + fetchDashboardOpportunitiesRequested, +} = slice.actions; + +function* fetchOpportunitiesAsCandidateRequestedSaga( + action: ReturnType +) { + try { + const { candidateId, department, businessLines, ...restFilters } = + action.payload; + const offset = yield* select(selectProfilesOffset); + const response = yield* call(() => + Api.getAllCandidateOpportunities(candidateId, { + params: { + ...restFilters, + department: mutateToArray(department), + businessLines: mutateToArray(businessLines), + offset, + }, + }) + ); + yield* put(fetchOpportunitiesAsCandidateSucceeded(response.data.offers)); + } catch { + yield* put(fetchOpportunitiesAsCandidateFailed()); + } +} + +function* fetchOpportunitiesAsCandidateWithFiltersSaga( + action: ReturnType +) { + yield* put(resetOpportunitiesOffset()); + yield* put(fetchOpportunitiesAsCandidateRequested(action.payload)); +} + +function* fetchOpportunitiesTabCountsSaga() { + const candidateId = yield* select(selectCandidateId); + if (!candidateId) return null; + try { + const response = yield* call(() => + Api.getOpportunitiesTabCountByCandidate(candidateId) + ); + yield* put(fetchOpportunitiesTabCountsSucceeded(response.data)); + } catch { + yield* put(fetchOpportunitiesTabCountsFailed()); + } +} + +function* fetchDashboardOpportunitiesSaga() { + const candidateId = yield* select(selectCandidateId); + const defaultFilters = yield* select( + selectCandidateProfileDefaultFiltersForDashboardOpportunities + ); + if (!candidateId) return null; + try { + const response = yield* call(() => + Api.getAllCandidateOpportunities(candidateId, { + params: { + offset: 0, + limit: 3, + type: 'public', + ...defaultFilters, + }, + }) + ); + yield* put(fetchDashboardOpportunitiesSucceeded(response.data.offers)); + } catch { + yield* put(fetchDashboardOpportunitiesFailed()); + } +} + +export function* saga() { + yield* takeLatest( + fetchOpportunitiesAsCandidateRequested, + fetchOpportunitiesAsCandidateRequestedSaga + ); + yield* takeLatest( + fetchOpportunitiesTabCountsRequested, + fetchOpportunitiesTabCountsSaga + ); + yield* takeLatest( + fetchOpportunitiesAsCandidateWithFilters, + fetchOpportunitiesAsCandidateWithFiltersSaga + ); + yield* takeLatest( + fetchDashboardOpportunitiesRequested, + fetchDashboardOpportunitiesSaga + ); +} diff --git a/src/use-cases/opportunities/opportunities.selectors.ts b/src/use-cases/opportunities/opportunities.selectors.ts new file mode 100644 index 000000000..c85cb2687 --- /dev/null +++ b/src/use-cases/opportunities/opportunities.selectors.ts @@ -0,0 +1,43 @@ +import { + fetchDashboardOpportunitiesAdapter, + fetchOpportunitiesAsCandidateAdapter, + fetchOpportunitiesTabCountsAdapter, +} from './opportunities.adapters'; +import { RootState } from './opportunities.slice'; + +export const fetchOpportunitiesAsCandidateSelectors = + fetchOpportunitiesAsCandidateAdapter.getSelectors( + (state) => state.opportunities.fetchOpportunitiesAsCandidate + ); + +export const fetchOpportunitiesTabCountsSelectors = + fetchOpportunitiesTabCountsAdapter.getSelectors( + (state) => state.opportunities.fetchOpportunitiesTabCounts + ); + +export const fetchDashboardOpportunitiesSelectors = + fetchDashboardOpportunitiesAdapter.getSelectors( + (state) => state.opportunities.fetchDashboardOpportunities + ); + +export function selectOpportunities(state: RootState) { + return state.opportunities.opportunities; +} + +export function selectDashboardOpportunities(state: RootState) { + return state.opportunities.dashboardOpportunities; +} + +export function selectOpportunitiesHasFetchedAll(state: RootState) { + return state.opportunities.opportunitiesHasFetchedAll; +} + +export function selectOpportunitiesTabCounts(state: RootState) { + return state.opportunities.opportunitiesTabCounts; +} + +export function selectNumberOfOpportunitiesInProgress(state: RootState) { + return state.opportunities.opportunitiesTabCounts.find( + (tabCount) => tabCount.status === -1 + )?.count; +} diff --git a/src/use-cases/opportunities/opportunities.slice.ts b/src/use-cases/opportunities/opportunities.slice.ts new file mode 100644 index 000000000..e3bddde59 --- /dev/null +++ b/src/use-cases/opportunities/opportunities.slice.ts @@ -0,0 +1,96 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { + OpportunitiesFiltersForCandidate, + Opportunity, + OpportunityTabCount, +} from 'src/api/types'; +import { RequestState, SliceRootState } from 'src/store/utils'; +import { + fetchDashboardOpportunitiesAdapter, + fetchOpportunitiesAsCandidateAdapter, + fetchOpportunitiesTabCountsAdapter, +} from './opportunities.adapters'; + +export interface State { + fetchOpportunitiesAsCandidate: RequestState< + typeof fetchOpportunitiesAsCandidateAdapter + >; + fetchOpportunitiesTabCounts: RequestState< + typeof fetchOpportunitiesTabCountsAdapter + >; + fetchDashboardOpportunities: RequestState< + typeof fetchDashboardOpportunitiesAdapter + >; + opportunities: Opportunity[]; + dashboardOpportunities: Opportunity[]; + opportunitiesHasFetchedAll: boolean; + opportunitiesTabCounts: OpportunityTabCount[]; + opportunitiesOffset: number; +} + +const initialState: State = { + fetchOpportunitiesAsCandidate: + fetchOpportunitiesAsCandidateAdapter.getInitialState(), + fetchOpportunitiesTabCounts: + fetchOpportunitiesTabCountsAdapter.getInitialState(), + fetchDashboardOpportunities: + fetchDashboardOpportunitiesAdapter.getInitialState(), + opportunities: [], + opportunitiesHasFetchedAll: false, + opportunitiesTabCounts: [], + dashboardOpportunities: [], + opportunitiesOffset: 0, +}; + +export const slice = createSlice({ + name: 'opportunities', + initialState, + reducers: { + ...fetchOpportunitiesAsCandidateAdapter.getReducers( + (state) => state.fetchOpportunitiesAsCandidate, + { + fetchOpportunitiesAsCandidateSucceeded(state, action) { + state.opportunities = + state.opportunitiesOffset === 0 + ? action.payload + : [...state.opportunities, ...action.payload]; + }, + } + ), + ...fetchOpportunitiesTabCountsAdapter.getReducers( + (state) => state.fetchOpportunitiesTabCounts, + { + fetchOpportunitiesTabCountsSucceeded(state, action) { + state.opportunitiesTabCounts = action.payload; + }, + } + ), + ...fetchDashboardOpportunitiesAdapter.getReducers( + (state) => state.fetchDashboardOpportunities, + { + fetchDashboardOpportunitiesSucceeded(state, action) { + state.dashboardOpportunities = action.payload; + }, + } + ), + resetOpportunitiesOffset(state) { + state.opportunitiesOffset = 0; + state.opportunitiesHasFetchedAll = false; + state.opportunities = []; + }, + fetchOpportunitiesAsCandidateWithFilters( + _state, + _action: PayloadAction< + OpportunitiesFiltersForCandidate & { + candidateId: string; + limit: number; + } + > + ) {}, + setOpportunitiesHasFetchedAll(state, action: PayloadAction) { + state.opportunitiesHasFetchedAll = action.payload; + }, + }, +}); + +export type RootState = SliceRootState; diff --git a/src/utils/Finding.ts b/src/utils/Finding.ts index a9d99282e..92ffdd4d7 100644 --- a/src/utils/Finding.ts +++ b/src/utils/Finding.ts @@ -1,5 +1,9 @@ import _ from 'lodash'; -import { UserWithUserCandidate } from 'src/api/types'; +import { + UserCandidateWithUsers, + UserWithUserCandidate, + User, +} from 'src/api/types'; import { OFFER_STATUS } from 'src/constants'; import { CANDIDATE_USER_ROLES, @@ -76,20 +80,25 @@ export function getValueFromFormField( export function isRoleIncluded( superset: readonly UserRole[], subset: UserRole | UserRole[] -) { +): boolean { if (!Array.isArray(subset)) { return _.difference([subset], superset).length === 0; } return _.difference(subset, superset).length === 0; } -export function getUserCandidateFromCoachOrCandidate(member) { +export function getUserCandidateFromCoachOrCandidate( + member: UserWithUserCandidate +): UserCandidateWithUsers | UserCandidateWithUsers[] | null { if (member) { - if (isRoleIncluded(CANDIDATE_USER_ROLES, member.role)) { + if ( + isRoleIncluded(CANDIDATE_USER_ROLES, member.role) && + !!member.candidat + ) { return member.candidat; } - if (isRoleIncluded(COACH_USER_ROLES, member.role)) { + if (isRoleIncluded(COACH_USER_ROLES, member.role) && !!member.coaches) { return member.coaches; } } @@ -98,50 +107,58 @@ export function getUserCandidateFromCoachOrCandidate(member) { export function getRelatedUser( member: UserWithUserCandidate -): UserWithUserCandidate[] { +): UserWithUserCandidate[] | null { if (member) { if (member.candidat && member.candidat.coach) { return [member.candidat.coach]; } if (member.coaches && member.coaches.length > 0) { - // @ts-expect-error after enable TS strict mode. Please, try to fix it return member.coaches.map(({ candidat }) => { return candidat; }); } } - - // @ts-expect-error after enable TS strict mode. Please, try to fix it return null; } -export function getCoachFromCandidate(candidate) { +export function getCoachFromCandidate( + candidate: UserWithUserCandidate +): UserWithUserCandidate | null { if (candidate && isRoleIncluded(CANDIDATE_USER_ROLES, candidate.role)) { if (candidate.candidat && candidate.candidat.coach) { return candidate.candidat.coach; } } - return null; } -export function getUserCandidateFromCoach(coach, candidateId) { +export function getUserCandidateFromCoach( + coach: UserWithUserCandidate, + candidateId: string +): UserCandidateWithUsers | null { if (coach && isRoleIncluded(COACH_USER_ROLES, coach.role)) { if (coach.coaches && coach.coaches.length > 0) { - return coach.coaches.find(({ candidat }) => { - return candidat.id === candidateId; + const candidate = coach.coaches.find(({ candidat }) => { + return candidat?.id === candidateId; }); + if (candidate) { + return candidate; + } } } - return null; } -export function getCandidateFromCoach(coach, candidateId) { +export function getCandidateFromCoach( + coach: UserWithUserCandidate, + candidateId: string +): User | undefined { return getUserCandidateFromCoach(coach, candidateId)?.candidat; } -export function getCandidateIdFromCoachOrCandidate(member) { +export function getCandidateIdFromCoachOrCandidate( + member: UserWithUserCandidate +): string | string[] | null { if (member) { if (isRoleIncluded(CANDIDATE_USER_ROLES, member.role)) { return member.id; From 31eed813cdfefb082b3ce08decd73733d024cf16 Mon Sep 17 00:00:00 2001 From: Emile Bex Date: Tue, 13 Feb 2024 14:47:39 +0100 Subject: [PATCH 11/21] [EN-6721] fix(icons): fix icons not showing on firefox (#203) --- next.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/next.config.js b/next.config.js index 8025641c7..3b4eba3bd 100644 --- a/next.config.js +++ b/next.config.js @@ -128,6 +128,7 @@ module.exports = withLess({ { loader: '@svgr/webpack', options: { + icon: true, svgoConfig: { plugins: [ { From 74bb7100df07f1ef3472f05a79ca83ca42cd8687 Mon Sep 17 00:00:00 2001 From: Emile Bex Date: Wed, 14 Feb 2024 10:53:36 +0100 Subject: [PATCH 12/21] [EN-6735] fix(offers): remove sticky search bar for opportunities (#200) --- .../admin/AdminOpportunities/AdminOpportunities.tsx | 8 ++------ .../MemberDetails/MemberTab/OffersMemberTab.tsx | 4 ++-- .../CandidateOpportunities/CandidateOpportunities.tsx | 11 ++++------- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/components/backoffice/admin/AdminOpportunities/AdminOpportunities.tsx b/src/components/backoffice/admin/AdminOpportunities/AdminOpportunities.tsx index 498a37b7a..7856ebb3f 100644 --- a/src/components/backoffice/admin/AdminOpportunities/AdminOpportunities.tsx +++ b/src/components/backoffice/admin/AdminOpportunities/AdminOpportunities.tsx @@ -19,7 +19,6 @@ import { ModalEdit } from 'src/components/modals/Modal/ModalGeneric/ModalEdit'; import { PostAdminOpportunityModal } from 'src/components/modals/Modal/ModalGeneric/PostOpportunityModal'; import { Button, ButtonMultiple, Section } from 'src/components/utils'; import { OPPORTUNITY_FILTERS_DATA } from 'src/constants'; -import { HEIGHTS } from 'src/constants/styles'; import { GA_TAGS } from 'src/constants/tags'; import { FilterObject } from 'src/constants/utils'; import { useOpportunityId } from 'src/hooks/queryParams/useOpportunityId'; @@ -41,10 +40,7 @@ interface AdminOpportunitiesProps { isMobile?: boolean; } -const filtersAndTabsHeight = - HEIGHTS.TABS_HEIGHT_WITHOUT_NUMBERS + - HEIGHTS.SEARCH_BAR_HEIGHT + - HEIGHTS.SECTION_PADDING; +const filtersAndTabsHeight = 0; export const AdminOpportunities = ({ search, @@ -260,7 +256,7 @@ export const AdminOpportunities = ({ Créer -
    +
    } diff --git a/src/components/backoffice/candidate/CandidateOpportunities/CandidateOpportunities.tsx b/src/components/backoffice/candidate/CandidateOpportunities/CandidateOpportunities.tsx index 6140c7720..3e3b86c88 100644 --- a/src/components/backoffice/candidate/CandidateOpportunities/CandidateOpportunities.tsx +++ b/src/components/backoffice/candidate/CandidateOpportunities/CandidateOpportunities.tsx @@ -20,7 +20,6 @@ import { openModal } from 'src/components/modals/Modal'; import { ModalExternalOffer } from 'src/components/modals/Modal/ModalGeneric/OfferModals/ModalOffer'; import { Button, Section } from 'src/components/utils'; import { OPPORTUNITY_FILTERS_DATA } from 'src/constants'; -import { HEIGHTS } from 'src/constants/styles'; import { CANDIDATE_USER_ROLES, USER_ROLES } from 'src/constants/users'; import { FilterObject } from 'src/constants/utils'; import { useAuthenticatedUser } from 'src/hooks/authentication/useAuthenticatedUser'; @@ -66,9 +65,7 @@ export const CandidateOpportunities = ({ const isPublic = opportunityType === 'public'; - const contentHeight = isPublic - ? HEIGHTS.SEARCH_BAR_HEIGHT - : HEIGHTS.TABS_HEIGHT; + const filtersAndTabsHeight = 0; const [offset, setOffset] = useState(0); const [hasFetchedAll, setHasFetchedAll] = useState(false); @@ -217,7 +214,7 @@ export const CandidateOpportunities = ({ {isPublic ? ( -
    +
    ) : ( -
    +
    { await fetchOpportunities(true); From f067033076608d05aca45ef99366bb181a9992cc Mon Sep 17 00:00:00 2001 From: Emile Bex Date: Wed, 14 Feb 2024 11:21:58 +0100 Subject: [PATCH 13/21] fix(redux): fix logout management and profile loading on idle (#204) --- .../directory/DirectoryList/DirectoryList.tsx | 8 ++++- src/hooks/authentication/useAuthentication.ts | 32 ++++++++----------- .../authentication/authentication.saga.ts | 2 +- src/use-cases/profiles/profiles.saga.ts | 4 +-- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/components/backoffice/directory/DirectoryList/DirectoryList.tsx b/src/components/backoffice/directory/DirectoryList/DirectoryList.tsx index 7872acba6..7fa7cf0c0 100644 --- a/src/components/backoffice/directory/DirectoryList/DirectoryList.tsx +++ b/src/components/backoffice/directory/DirectoryList/DirectoryList.tsx @@ -11,10 +11,16 @@ import { StyledDirectoryListContainer } from './DirectoryList.styles'; export function DirectoryList() { const { profiles } = useDirectory(); + const isFetchProfilesIdle = useSelector( + fetchProfilesSelectors.selectIsFetchProfilesIdle + ); + const isFetchProfilesRequested = useSelector( fetchProfilesSelectors.selectIsFetchProfilesRequested ); + const isLoading = isFetchProfilesIdle || isFetchProfilesRequested; + const profileList = useMemo(() => { return profiles?.map((profile) => { const helps = isRoleIncluded(CANDIDATE_USER_ROLES, profile.role) @@ -45,7 +51,7 @@ export function DirectoryList() { return ( - + ); } diff --git a/src/hooks/authentication/useAuthentication.ts b/src/hooks/authentication/useAuthentication.ts index 3be67390f..57021222a 100644 --- a/src/hooks/authentication/useAuthentication.ts +++ b/src/hooks/authentication/useAuthentication.ts @@ -1,7 +1,6 @@ import { useRouter } from 'next/router'; import { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { usePrevious } from '../utils'; import { authenticationActions, fetchUserSelectors, @@ -22,19 +21,22 @@ export function useAuthentication() { logoutSelectors.selectIsLogoutSucceeded ); - const prevIsLogoutSucceeded = usePrevious(isLogoutSucceeded); - const isFetchUserFailed = useSelector( fetchUserSelectors.selectIsFetchUserFailed ); + const isFetchUserIdle = useSelector(fetchUserSelectors.selectIsFetchUserIdle); - const isAuthenticationPending = !isFetchUserSucceeded && !isFetchUserFailed; + const currentUser = useSelector(selectCurrentUser); + const isAuthenticationPending = !isFetchUserSucceeded && !isFetchUserFailed; + const { isUserAuthorized } = useRoutePermissions(); const isCurrentRouteReady = isUserAuthorized; + const isUserAuthenticated = !!currentUser; + const currentUserRole = currentUser?.role; useEffect(() => { @@ -44,19 +46,12 @@ export function useAuthentication() { }, [dispatch, isFetchUserIdle]); useEffect(() => { - if (isLogoutSucceeded) { - push('/login'); - dispatch(authenticationActions.logoutReset()); - } else if ( - !isAuthenticationPending && - !isUserAuthorized && - !prevIsLogoutSucceeded - ) { - if (currentUserRole) { + if (!isAuthenticationPending && !isUserAuthorized) { + if (isUserAuthenticated && currentUserRole) { replace(getDefaultUrl(currentUserRole)); } else { push( - asPath + asPath && !isLogoutSucceeded ? { pathname: '/login', query: { @@ -68,15 +63,14 @@ export function useAuthentication() { } } }, [ + asPath, currentUserRole, isAuthenticationPending, + isLogoutSucceeded, + isUserAuthenticated, isUserAuthorized, - replace, - asPath, push, - dispatch, - prevIsLogoutSucceeded, - isLogoutSucceeded, + replace, ]); return { diff --git a/src/use-cases/authentication/authentication.saga.ts b/src/use-cases/authentication/authentication.saga.ts index ee1a4f13b..526ebec32 100644 --- a/src/use-cases/authentication/authentication.saga.ts +++ b/src/use-cases/authentication/authentication.saga.ts @@ -184,7 +184,7 @@ export function* saga() { yield* takeLatest(loginRequested, loginRequestedSaga); yield* takeLatest(loginSucceeded, loginSucceededSaga); yield* takeLatest(logoutRequested, logoutRequestedSaga); - yield* takeLatest(logoutRequested, logoutSucceededSaga); + yield* takeLatest(logoutSucceeded, logoutSucceededSaga); yield* takeLatest(updateUserRequested, updateUserRequestedSaga); yield* takeLatest(updateProfileRequested, updateProfileRequestedSaga); yield* takeLatest(updateCandidateRequested, updateCandidateRequestedSaga); diff --git a/src/use-cases/profiles/profiles.saga.ts b/src/use-cases/profiles/profiles.saga.ts index 9a1586e7b..39514d419 100644 --- a/src/use-cases/profiles/profiles.saga.ts +++ b/src/use-cases/profiles/profiles.saga.ts @@ -1,4 +1,4 @@ -import { call, put, select, takeLatest } from 'typed-redux-saga'; +import { call, put, select, takeLatest, takeLeading } from 'typed-redux-saga'; import { Api } from 'src/api'; import { PROFILES_LIMIT } from 'src/constants'; import { mutateToArray } from 'src/utils'; @@ -97,7 +97,7 @@ function* postInternalMessageSaga( export function* saga() { yield* takeLatest(fetchProfilesWithFilters, fetchProfilesWithFiltersSaga); - yield* takeLatest(fetchProfilesNextPage, fetchProfilesNextPageSaga); + yield* takeLeading(fetchProfilesNextPage, fetchProfilesNextPageSaga); yield* takeLatest(fetchProfilesRequested, fetchProfilesRequestedSaga); yield* takeLatest(fetchSelectedProfileRequested, fetchSelectedProfileSaga); yield* takeLatest(postInternalMessageRequested, postInternalMessageSaga); From 1737f396d32e111de8bf51a9a9559d2d33b85f6e Mon Sep 17 00:00:00 2001 From: PaulEntourage <112417197+PaulEntourage@users.noreply.github.com> Date: Thu, 15 Feb 2024 11:39:21 +0100 Subject: [PATCH 14/21] [EN-6785] feat(lko2): wording and fixes (#206) --- .../backoffice/dashboard/Dashboard.styles.tsx | 4 +++ .../backoffice/dashboard/Dashboard.tsx | 5 +++- .../DashboardProfileCard.styles.tsx | 15 +++++++++-- .../DashboardProfileCard.tsx | 26 ++++++++++++++----- .../ProfessionalInformationCard.styles.tsx | 3 +++ .../profile/usIsProfileContacted.ts | 2 +- .../ToggleWithModal.styles.tsx | 2 +- src/constants/helps.tsx | 12 ++++----- 8 files changed, 51 insertions(+), 18 deletions(-) diff --git a/src/components/backoffice/dashboard/Dashboard.styles.tsx b/src/components/backoffice/dashboard/Dashboard.styles.tsx index 0876a0e9f..ca05db807 100644 --- a/src/components/backoffice/dashboard/Dashboard.styles.tsx +++ b/src/components/backoffice/dashboard/Dashboard.styles.tsx @@ -23,3 +23,7 @@ export const StyledDashboardRightColumn = styled.div` width: 100%; } `; + +export const StyledDashboardTitleContainer = styled.div` + padding-bottom: 20px; +`; diff --git a/src/components/backoffice/dashboard/Dashboard.tsx b/src/components/backoffice/dashboard/Dashboard.tsx index 09af97843..dc5c7e912 100644 --- a/src/components/backoffice/dashboard/Dashboard.tsx +++ b/src/components/backoffice/dashboard/Dashboard.tsx @@ -12,6 +12,7 @@ import { isRoleIncluded } from 'src/utils'; import { StyledDashboardLeftColumn, StyledDashboardRightColumn, + StyledDashboardTitleContainer, } from './Dashboard.styles'; import { DashboardAvailabilityCard } from './DashboardAvailabilityCard'; import { DashboardOpportunitiesCard } from './DashboardOpportunitiesCard'; @@ -29,7 +30,9 @@ export const Dashboard = () => { return (
    -

    + +

    + diff --git a/src/components/backoffice/dashboard/DashboardProfileCard/DashboardProfileCard.styles.tsx b/src/components/backoffice/dashboard/DashboardProfileCard/DashboardProfileCard.styles.tsx index 329b4ec83..3ab9039fe 100644 --- a/src/components/backoffice/dashboard/DashboardProfileCard/DashboardProfileCard.styles.tsx +++ b/src/components/backoffice/dashboard/DashboardProfileCard/DashboardProfileCard.styles.tsx @@ -28,14 +28,14 @@ export const StyledDashboardProfileCardDescription = styled.div` `; export const StyledDashboardProfileCardHelps = styled.div` - margin: 0 20px 20px 20px; + margin: 20px 20px 30px 20px; `; export const StyledDashboardProfileCardhelpsTitle = styled.div` width: 100%; padding-bottom: 15px; border-bottom: 1px solid ${COLORS.hoverOrange}; - margin-bottom: 30px; + margin-bottom: 20px; font-size: 16px; `; @@ -52,3 +52,14 @@ export const StyledDashboardCTAContainer = styled.div` flex-direction: row; justify-content: center; `; + +export const StyledDashboardProfileCardHelpListEmptyState = styled.div` + font-style: italic; + font-size: 14px; + display: flex; + align-items: center; + svg { + width: 50px; + margin-right: 15px; + } +`; diff --git a/src/components/backoffice/dashboard/DashboardProfileCard/DashboardProfileCard.tsx b/src/components/backoffice/dashboard/DashboardProfileCard/DashboardProfileCard.tsx index 3f00d91f1..2f6ea1244 100644 --- a/src/components/backoffice/dashboard/DashboardProfileCard/DashboardProfileCard.tsx +++ b/src/components/backoffice/dashboard/DashboardProfileCard/DashboardProfileCard.tsx @@ -1,14 +1,18 @@ import React from 'react'; +import { IlluBulleQuestion } from 'assets/icons/icons'; import { useContextualRole } from '../../useContextualRole'; import { useHelpField } from 'src/components/backoffice/parametres/useUpdateProfile'; import { Button, Card, ImgProfile, Tag } from 'src/components/utils'; import { H5 } from 'src/components/utils/Headings'; import { ProfileHelps } from 'src/constants/helps'; +import { CANDIDATE_USER_ROLES } from 'src/constants/users'; import { useAuthenticatedUser } from 'src/hooks/authentication/useAuthenticatedUser'; +import { isRoleIncluded } from 'src/utils'; import { StyledDashboardCTAContainer, StyledDashboardProfileCardDescription, StyledDashboardProfileCardHelpList, + StyledDashboardProfileCardHelpListEmptyState, StyledDashboardProfileCardHelps, StyledDashboardProfileCardhelpsTitle, StyledDashboardProfileCardPictureName, @@ -39,11 +43,14 @@ export const DashboardProfileCard = () => { {user.userProfile.description} )} - {user.userProfile[helpField].length > 0 && ( - - - Mes coups de pouce - + + + Mes{' '} + {isRoleIncluded(CANDIDATE_USER_ROLES, contextualRole) && + 'besoins de '}{' '} + coups de pouce + + {user.userProfile[helpField].length > 0 ? ( {user.userProfile[helpField].slice(0, 3).map((help, index) => { const helpDetails = ProfileHelps.find( @@ -59,8 +66,13 @@ export const DashboardProfileCard = () => { )} - - )} + ) : ( + + +
    Informations pas encore renseignées
    +
    + )} +
    + {isRoleIncluded(COACH_USER_ROLES, user.role) && ( + + )} + + + + ); +}; diff --git a/src/components/backoffice/dashboard/DashboardLinkedUserCard/index.ts b/src/components/backoffice/dashboard/DashboardLinkedUserCard/index.ts new file mode 100644 index 000000000..f5aa9e5a0 --- /dev/null +++ b/src/components/backoffice/dashboard/DashboardLinkedUserCard/index.ts @@ -0,0 +1 @@ +export * from './DashboardLinkedUserCard'; diff --git a/src/components/backoffice/parametres/ParametresLayout/UserInformationCard/LinkedUserInformationCard.tsx b/src/components/backoffice/parametres/ParametresLayout/UserInformationCard/LinkedUserInformationCard.tsx index 1f775d234..b5ca4b8ec 100644 --- a/src/components/backoffice/parametres/ParametresLayout/UserInformationCard/LinkedUserInformationCard.tsx +++ b/src/components/backoffice/parametres/ParametresLayout/UserInformationCard/LinkedUserInformationCard.tsx @@ -1,20 +1,17 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React from 'react'; +import { useSelector } from 'react-redux'; import EmailIcon from 'assets/icons/email.svg'; import HomeIcon from 'assets/icons/home.svg'; import LinkIcon from 'assets/icons/link.svg'; import PhoneIcon from 'assets/icons/phone.svg'; import UserIcon from 'assets/icons/user.svg'; import { CVPreferences } from '../CVPreferences'; -import { UserWithUserCandidate } from 'src/api/types'; import { Card, SimpleLink } from 'src/components/utils'; import { H5 } from 'src/components/utils/Headings'; -import { CANDIDATE_USER_ROLES, COACH_USER_ROLES } from 'src/constants/users'; +import { COACH_USER_ROLES } from 'src/constants/users'; import { useAuthenticatedUser } from 'src/hooks/authentication/useAuthenticatedUser'; -import { - getRelatedUser, - isRoleIncluded, - getUserCandidateFromCoach, -} from 'src/utils/Finding'; +import { selectLinkedUser } from 'src/use-cases/authentication'; +import { isRoleIncluded, getUserCandidateFromCoach } from 'src/utils/Finding'; import { StyledInformationsPersonnellesList } from './UserInformationCard.styles'; export const LinkedUserInformationCard = ({ @@ -23,33 +20,7 @@ export const LinkedUserInformationCard = ({ isAdmin?: boolean; }) => { const user = useAuthenticatedUser(); - - const [linkedUser, setLinkedUser] = useState(); - - const assignUser = useCallback((userToAssign) => { - if (isRoleIncluded(COACH_USER_ROLES, userToAssign.role)) { - const candidat: UserWithUserCandidate[] | null = - getRelatedUser(userToAssign); - if (candidat) { - setLinkedUser(candidat); - } else { - setLinkedUser(undefined); - } - } - if (isRoleIncluded(CANDIDATE_USER_ROLES, userToAssign.role)) { - const coach: UserWithUserCandidate[] | null = - getRelatedUser(userToAssign); - if (coach) { - setLinkedUser(coach); - } else { - setLinkedUser(undefined); - } - } - }, []); - - useEffect(() => { - assignUser(user); - }, [assignUser, user]); + const linkedUser = useSelector(selectLinkedUser); if (!linkedUser) { return ( @@ -62,119 +33,104 @@ export const LinkedUserInformationCard = ({ ); } + const userCandidat = getUserCandidateFromCoach(user, linkedUser.id); - return ( + // si membre lié ou non + const cardContent = ( <> - {linkedUser.map((singleLinkedUser) => { - const userCandidat = getUserCandidateFromCoach( - user, - singleLinkedUser.id - ); - - // si membre lié ou non - const cardContent = ( + +
  • + + {`${linkedUser.firstName} ${linkedUser.lastName}`} +
  • + {!linkedUser.deletedAt && ( <> - +
  • + + + + {linkedUser.email} + + +
  • + {linkedUser.phone ? (
  • - - {`${singleLinkedUser.firstName} ${singleLinkedUser.lastName}`} + + {linkedUser.phone} +
  • - {!singleLinkedUser.deletedAt && ( - <> -
  • - - - - {singleLinkedUser.email} - - -
  • - {singleLinkedUser.phone ? ( - -
  • - {singleLinkedUser.phone} -
  • -
    - ) : ( -
  • - - - Numéro de téléphone non renseigné - -
  • - )} - {isRoleIncluded(COACH_USER_ROLES, user.role) && - (singleLinkedUser.address ? ( -
  • - {singleLinkedUser.address} -
  • - ) : ( -
  • - {' '} - - Adresse postale non renseignée - -
  • - ))} - {isRoleIncluded(COACH_USER_ROLES, user.role) && - userCandidat && ( - -
  • - - - {userCandidat.url} - -
  • -
    - )} - - )} -
    - {!isAdmin && - isRoleIncluded(COACH_USER_ROLES, user.role) && - userCandidat && - !singleLinkedUser.deletedAt && ( - -
  • -
    -
  • - -
    - )} + ) : ( +
  • + + + Numéro de téléphone non renseigné + +
  • + )} + {isRoleIncluded(COACH_USER_ROLES, user.role) && + (linkedUser.address ? ( +
  • + {linkedUser.address} +
  • + ) : ( +
  • + {' '} + + Adresse postale non renseignée + +
  • + ))} + {isRoleIncluded(COACH_USER_ROLES, user.role) && userCandidat && ( + +
  • + + {userCandidat.url} +
  • +
    + )} - ); - - return ( - - {cardContent} - - ); - })} + )} +
    + {!isAdmin && + isRoleIncluded(COACH_USER_ROLES, user.role) && + userCandidat && + !linkedUser.deletedAt && ( + +
  • +
    +
  • + +
    + )} ); + + return ( + + {cardContent} + + ); }; diff --git a/src/pages/backoffice/annuaire.tsx b/src/pages/backoffice/annuaire.tsx index c7bf0c117..020dfd0fe 100644 --- a/src/pages/backoffice/annuaire.tsx +++ b/src/pages/backoffice/annuaire.tsx @@ -16,7 +16,7 @@ const Annuaire = () => {
    { + const { user } = state.authentication; + if (!user) return null; + if (isRoleIncluded(COACH_USER_ROLES, user.role)) { + const candidat: UserWithUserCandidate[] | null = getRelatedUser(user); + if (candidat) { + const [userToSend] = candidat; + return userToSend; + } + return null; + } + if (isRoleIncluded(CANDIDATE_USER_ROLES, user.role)) { + const coach: UserWithUserCandidate[] | null = getRelatedUser(user); + if (coach) { + const [userToSend] = coach; + return userToSend; + } + return null; + } + return null; +}; From 6056ec6fe6d0bad2dfb23023a64e8e202a77519d Mon Sep 17 00:00:00 2001 From: PaulEntourage <112417197+PaulEntourage@users.noreply.github.com> Date: Mon, 19 Feb 2024 17:18:25 +0100 Subject: [PATCH 16/21] [EN-6778] feat(lko2-dashboard): carte creation CV (#208) --- src/api/types.ts | 10 +- .../backoffice/dashboard/Dashboard.tsx | 8 +- .../DashboardOpportunitiesCard.tsx | 4 +- .../DashboardCVCreationStepCard.styles.tsx | 54 +++++++++ .../DashboardCVCreationStepCard.tsx | 114 ++++++++++++++++++ .../DashboardCVCreationStepCard/index.ts | 1 + .../DashboardStepsCard/DashboardStepsCard.tsx | 27 +++++ .../dashboard/DashboardStepsCard/index.ts | 1 + src/constants/index.ts | 2 +- .../authentication.selectors.ts | 22 +++- src/use-cases/cv/cv.adapter.ts | 2 +- src/use-cases/cv/cv.saga.ts | 7 +- src/use-cases/cv/cv.selectors.ts | 12 +- 13 files changed, 253 insertions(+), 11 deletions(-) create mode 100644 src/components/backoffice/dashboard/DashboardStepsCard/DashboardCVCreationStepCard/DashboardCVCreationStepCard.styles.tsx create mode 100644 src/components/backoffice/dashboard/DashboardStepsCard/DashboardCVCreationStepCard/DashboardCVCreationStepCard.tsx create mode 100644 src/components/backoffice/dashboard/DashboardStepsCard/DashboardCVCreationStepCard/index.ts create mode 100644 src/components/backoffice/dashboard/DashboardStepsCard/DashboardStepsCard.tsx create mode 100644 src/components/backoffice/dashboard/DashboardStepsCard/index.ts diff --git a/src/api/types.ts b/src/api/types.ts index e26a71971..a479469d3 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -149,6 +149,14 @@ export interface CVFormation { }[]; } +export type CVStatus = + | 'Draft' + | 'Published' + | 'New' + | 'Pending' + | 'Progress' + | 'Unknown'; + export interface CV { id?: string; version: string; @@ -206,7 +214,7 @@ export interface CV { }[]; formations?: CVFormation[]; experiences?: CVExperience[]; - status: string; + status: CVStatus; UserId: string; } diff --git a/src/components/backoffice/dashboard/Dashboard.tsx b/src/components/backoffice/dashboard/Dashboard.tsx index 943560324..766ee6c64 100644 --- a/src/components/backoffice/dashboard/Dashboard.tsx +++ b/src/components/backoffice/dashboard/Dashboard.tsx @@ -18,6 +18,7 @@ import { DashboardAvailabilityCard } from './DashboardAvailabilityCard'; import { DashboardLinkedUserCard } from './DashboardLinkedUserCard'; import { DashboardOpportunitiesCard } from './DashboardOpportunitiesCard'; import { DashboardProfileCard } from './DashboardProfileCard'; +import { DashboardStepsCard } from './DashboardStepsCard'; export const Dashboard = () => { const isDesktop = useIsDesktop(); @@ -46,7 +47,12 @@ export const Dashboard = () => { {!isRoleIncluded( [USER_ROLES.COACH_EXTERNAL, USER_ROLES.ADMIN], user.role - ) && } + ) && ( + <> + + + + )}
    diff --git a/src/components/backoffice/dashboard/DashboardOpportunitiesCard/DashboardOpportunitiesCard.tsx b/src/components/backoffice/dashboard/DashboardOpportunitiesCard/DashboardOpportunitiesCard.tsx index beaeacaeb..2073d31b6 100644 --- a/src/components/backoffice/dashboard/DashboardOpportunitiesCard/DashboardOpportunitiesCard.tsx +++ b/src/components/backoffice/dashboard/DashboardOpportunitiesCard/DashboardOpportunitiesCard.tsx @@ -13,9 +13,9 @@ import { USER_ROLES } from 'src/constants/users'; import { useAuthenticatedUser } from 'src/hooks/authentication/useAuthenticatedUser'; import { useIsDesktop } from 'src/hooks/utils'; import { - selectCandidate, selectCandidateId, selectCandidateProfileDefaultFiltersForDashboardOpportunities, + selectCandidateAsUser, } from 'src/use-cases/authentication'; import { findConstantFromValue, buildContractLabel } from 'src/utils'; import { @@ -37,7 +37,7 @@ export const DashboardOpportunitiesCard = () => { useDashboardOpportunities(); const { contextualRole } = useContextualRole(user.role); const isDesktop = useIsDesktop(); - const candidate = useSelector(selectCandidate); + const candidate = useSelector(selectCandidateAsUser); const candidateId = useSelector(selectCandidateId); const opportunitiesDefaultFilters = useSelector( selectCandidateProfileDefaultFiltersForDashboardOpportunities diff --git a/src/components/backoffice/dashboard/DashboardStepsCard/DashboardCVCreationStepCard/DashboardCVCreationStepCard.styles.tsx b/src/components/backoffice/dashboard/DashboardStepsCard/DashboardCVCreationStepCard/DashboardCVCreationStepCard.styles.tsx new file mode 100644 index 000000000..232dcb0b6 --- /dev/null +++ b/src/components/backoffice/dashboard/DashboardStepsCard/DashboardCVCreationStepCard/DashboardCVCreationStepCard.styles.tsx @@ -0,0 +1,54 @@ +import styled from 'styled-components'; +import { COLORS } from 'src/constants/styles'; + +export const StyledDashboardCVCreationStep = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 20px 0; +`; + +export const StyledDashboardCVCreationStepSubtitle = styled.div` + font-size: 14px; + font-weight: 400; + line-height: 21px; + text-align: center; +`; + +export const StyledDashboardCVCreationStepContent = styled.div` + width: 700px; + max-width: 100%; + display: flex; + flex-direction: row; + align-items: center; + gap: 32px; + margin: 30px 0; + svg { + min-width: 140px; + height: 140px; + width: 140px; + } +`; + +export const StyledDashboardCVCreationStepContentText = styled.div` + padding: 15px 0; + p { + font-size: 14px; + margin: 0; + } +`; + +export const StyledDashboardCVCreationStepCandidateName = styled.h3` + font-size: 20px; + font-weight: 700; + line-height: 30px; + text-align: left; + color: ${COLORS.primaryOrange}; + span { + font-weight: 400; + font-size: 14px; + font-style: italic; + line-height: 21px; + } +`; diff --git a/src/components/backoffice/dashboard/DashboardStepsCard/DashboardCVCreationStepCard/DashboardCVCreationStepCard.tsx b/src/components/backoffice/dashboard/DashboardStepsCard/DashboardCVCreationStepCard/DashboardCVCreationStepCard.tsx new file mode 100644 index 000000000..a885999b6 --- /dev/null +++ b/src/components/backoffice/dashboard/DashboardStepsCard/DashboardCVCreationStepCard/DashboardCVCreationStepCard.tsx @@ -0,0 +1,114 @@ +import React, { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { IlluCV } from 'assets/icons/icons'; +import { useContextualRole } from 'src/components/backoffice/useContextualRole'; +import { Button, Card } from 'src/components/utils'; +import { CV_STATUS } from 'src/constants'; +import { USER_ROLES } from 'src/constants/users'; +import { useAuthenticatedUser } from 'src/hooks/authentication/useAuthenticatedUser'; +import { useIsDesktop } from 'src/hooks/utils'; +import { selectCandidateAsUser } from 'src/use-cases/authentication'; +import { selectCurrentCVStatus } from 'src/use-cases/cv'; +import { + StyledDashboardCVCreationStep, + StyledDashboardCVCreationStepCandidateName, + StyledDashboardCVCreationStepContent, + StyledDashboardCVCreationStepContentText, + StyledDashboardCVCreationStepSubtitle, +} from './DashboardCVCreationStepCard.styles'; + +export const DashboardCVCreationStepCard = () => { + const user = useAuthenticatedUser(); + const candidate = useSelector(selectCandidateAsUser); + const { contextualRole } = useContextualRole(user.role); + const isDesktop = useIsDesktop(); + const CVStatus = useSelector(selectCurrentCVStatus); + + const textContent = useMemo( + () => ({ + title: { + [USER_ROLES.CANDIDATE]: { + [CV_STATUS.New.value]: 'Première étape - Créer votre CV', + [CV_STATUS.Progress.value]: 'Finalisez et soumettez votre CV', + [CV_STATUS.Pending.value]: + "Votre CV est en cours de validation par l'équipe Entourage Pro", + }, + [USER_ROLES.COACH]: { + [CV_STATUS.New + .value]: `Première étape - Créez le CV de ${candidate?.firstName}`, + [CV_STATUS.Progress + .value]: `Finalisez et soumettez le CV de ${candidate?.firstName}`, + [CV_STATUS.Pending + .value]: `Le CV de ${candidate?.firstName} est en cours de validation`, + }, + }, + subTitle: { + [USER_ROLES.CANDIDATE]: { + [CV_STATUS.New.value]: + 'Avant de postuler aux offres d’emploi, prenez le temps de réaliser votre CV', + [CV_STATUS.Progress.value]: + 'Avant de postuler aux offres d’emploi, prenez le temps de finaliser votre CV', + [CV_STATUS.Pending.value]: + "L'équipe Entourage Pro est entrain de relire votre CV", + }, + [USER_ROLES.COACH]: { + [CV_STATUS.New + .value]: `Avant de postuler aux offres d’emploi, prenez le temps de réaliser avec ${candidate?.firstName} son CV`, + [CV_STATUS.Progress + .value]: `Avant de postuler aux offres d’emploi, prenez le temps de finaliser avec ${candidate?.firstName} son CV`, + [CV_STATUS.Pending + .value]: `L'équipe Entourage Pro est entrain de relire le CV de ${candidate?.firstName}`, + }, + }, + CTA: { + [USER_ROLES.CANDIDATE]: { + [CV_STATUS.New.value]: 'Créer mon CV', + [CV_STATUS.Progress.value]: 'Finaliser mon CV', + [CV_STATUS.Pending.value]: 'Voir le CV', + }, + [USER_ROLES.COACH]: { + [CV_STATUS.New.value]: `Créez le cv de ${candidate?.firstName}`, + [CV_STATUS.Progress + .value]: `Finalisez le cv de ${candidate?.firstName}`, + [CV_STATUS.Pending.value]: 'Voir le CV', + }, + }, + }), + [candidate] + ); + if (!CVStatus || !candidate) return null; + return ( + + + + {textContent.subTitle[contextualRole][CVStatus]} + + + {isDesktop && ( +
    + +
    + )} + + + {candidate?.firstName?.toUpperCase()}{' '} + {candidate.lastName.toUpperCase()}{' '} + • {candidate.userProfile.department} + +

    + {USER_ROLES.CANDIDATE === contextualRole + ? 'L’objectif du CV Entourage Pro est de rendre visible et valoriser votre projet professionnel auprès des entreprises mais aussi vos qualités et votre parcours de vie.' + : 'L’objectif du CV Entourage Pro est de rendre visible et valoriser le projet professionnel du candidat auprès des entreprises mais aussi ses qualités et son parcours de vie.'} +

    +
    +
    + +
    +
    + ); +}; diff --git a/src/components/backoffice/dashboard/DashboardStepsCard/DashboardCVCreationStepCard/index.ts b/src/components/backoffice/dashboard/DashboardStepsCard/DashboardCVCreationStepCard/index.ts new file mode 100644 index 000000000..e9398f0f0 --- /dev/null +++ b/src/components/backoffice/dashboard/DashboardStepsCard/DashboardCVCreationStepCard/index.ts @@ -0,0 +1 @@ +export * from './DashboardCVCreationStepCard'; diff --git a/src/components/backoffice/dashboard/DashboardStepsCard/DashboardStepsCard.tsx b/src/components/backoffice/dashboard/DashboardStepsCard/DashboardStepsCard.tsx new file mode 100644 index 000000000..ab54b2320 --- /dev/null +++ b/src/components/backoffice/dashboard/DashboardStepsCard/DashboardStepsCard.tsx @@ -0,0 +1,27 @@ +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { selectCandidateAsUser } from 'src/use-cases/authentication'; +import { + cvActions, + fetchCVSelectors, + selectIsCurrentCVValidated, +} from 'src/use-cases/cv'; +import { DashboardCVCreationStepCard } from './DashboardCVCreationStepCard'; + +export const DashboardStepsCard = () => { + const dispatch = useDispatch(); + const candidate = useSelector(selectCandidateAsUser); + const isCurrentCVValidated = useSelector(selectIsCurrentCVValidated); + const isCVFetched = useSelector(fetchCVSelectors.selectIsFetchCVSucceeded); + + useEffect(() => { + if (candidate?.id) { + dispatch(cvActions.fetchCVRequested()); + } + }, [dispatch, candidate]); + + if (!isCurrentCVValidated && isCVFetched) { + return ; + } + return null; +}; diff --git a/src/components/backoffice/dashboard/DashboardStepsCard/index.ts b/src/components/backoffice/dashboard/DashboardStepsCard/index.ts new file mode 100644 index 000000000..4181e16c4 --- /dev/null +++ b/src/components/backoffice/dashboard/DashboardStepsCard/index.ts @@ -0,0 +1 @@ +export * from './DashboardStepsCard'; diff --git a/src/constants/index.ts b/src/constants/index.ts index 1258f8eb5..e33e85a60 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -206,7 +206,7 @@ export const CV_STATUS = { value: 'Unknown', style: '', }, -}; +} as const; export type AmbitionsPrefixesType = 'dans' | 'comme'; diff --git a/src/use-cases/authentication/authentication.selectors.ts b/src/use-cases/authentication/authentication.selectors.ts index a85e43965..b8a0e154a 100644 --- a/src/use-cases/authentication/authentication.selectors.ts +++ b/src/use-cases/authentication/authentication.selectors.ts @@ -2,6 +2,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { UserCandidateWithUsers, UserProfile, + User, UserWithUserCandidate, } from 'src/api/types'; import { CANDIDATE_USER_ROLES, COACH_USER_ROLES } from 'src/constants/users'; @@ -69,7 +70,7 @@ export function selectProfileUpdateError(state: RootState) { } // select candidate for the current user => doesn't work for external coach -export function selectCandidate( +export function selectUserCandidateWithUsers( state: RootState ): UserCandidateWithUsers | null { if (state.authentication.user) { @@ -84,6 +85,25 @@ export function selectCandidate( return null; } +// select candidate User for the current user => doesn't work for external coach +export function selectCandidateAsUser(state: RootState): User | null { + const { user } = state.authentication; + if (user) { + let candidate = getUserCandidateFromCoachOrCandidate(user); + if ( + isRoleIncluded(COACH_USER_ROLES, user.role) && + Array.isArray(candidate) + ) { + [candidate] = candidate; + if (candidate?.candidat) { + return candidate.candidat; + } + } + return user; + } + return null; +} + // select candidateId for the current user => doesn't work for external coach export function selectCandidateId(state: RootState): string | null { if (state.authentication.user) { diff --git a/src/use-cases/cv/cv.adapter.ts b/src/use-cases/cv/cv.adapter.ts index 5dabc2b35..204f6bad8 100644 --- a/src/use-cases/cv/cv.adapter.ts +++ b/src/use-cases/cv/cv.adapter.ts @@ -2,6 +2,6 @@ import { CV } from 'src/api/types'; import { createRequestAdapter } from 'src/store/utils'; export const fetchCVAdapter = createRequestAdapter('fetchCV').withPayloads< - string, // userId + void, CV >(); diff --git a/src/use-cases/cv/cv.saga.ts b/src/use-cases/cv/cv.saga.ts index 398113c71..4c6cfbeb5 100644 --- a/src/use-cases/cv/cv.saga.ts +++ b/src/use-cases/cv/cv.saga.ts @@ -1,11 +1,12 @@ -import { call, put, takeLatest } from 'typed-redux-saga'; +import { call, put, takeLatest, select } from 'typed-redux-saga'; import { Api } from 'src/api'; +import { selectCandidateId } from 'src/use-cases/authentication'; import { slice } from './cv.slice'; const { fetchCVSucceeded, fetchCVFailed, fetchCVRequested } = slice.actions; -function* fetchCVSagaRequested(action: ReturnType) { - const candidateId = action.payload; +function* fetchCVSagaRequested() { + const candidateId = yield* select(selectCandidateId); try { const response = yield* call(() => Api.getCVByCandidateId(candidateId)); diff --git a/src/use-cases/cv/cv.selectors.ts b/src/use-cases/cv/cv.selectors.ts index 67c58bb53..1e0f55f3e 100644 --- a/src/use-cases/cv/cv.selectors.ts +++ b/src/use-cases/cv/cv.selectors.ts @@ -1,3 +1,5 @@ +import { CVStatus } from 'src/api/types'; +import { CV_STATUS } from 'src/constants'; import { fetchCVAdapter } from './cv.adapter'; import { RootState } from './cv.slice'; @@ -5,6 +7,14 @@ export const fetchCVSelectors = fetchCVAdapter.getSelectors( (state) => state.cv.fetchCV ); -export function selectCV(state: RootState) { +export function selectCurrentCV(state: RootState) { return state.cv.currentCv; } + +export function selectIsCurrentCVValidated(state: RootState) { + return state.cv.currentCv?.status === CV_STATUS.Published.value; +} + +export function selectCurrentCVStatus(state: RootState): CVStatus | undefined { + return state.cv.currentCv?.status; +} From c36d391c80600c295f7a9fd770e2f13c47818f37 Mon Sep 17 00:00:00 2001 From: PaulEntourage <112417197+PaulEntourage@users.noreply.github.com> Date: Tue, 20 Feb 2024 12:38:01 +0100 Subject: [PATCH 17/21] [EN-6840] feat(lko2-dashboard): fix builds and feedbacks (#209) --- .../DashboardLinkedUserCard.tsx | 5 +- .../DashboardOpportunitiesCard.styles.tsx | 8 + .../DashboardOpportunitiesCard.tsx | 177 ++++++++++-------- .../DashboardCVCreationStepCard.styles.tsx | 2 - .../DashboardCVCreationStepCard.tsx | 2 +- src/constants/helps.tsx | 2 +- 6 files changed, 110 insertions(+), 86 deletions(-) diff --git a/src/components/backoffice/dashboard/DashboardLinkedUserCard/DashboardLinkedUserCard.tsx b/src/components/backoffice/dashboard/DashboardLinkedUserCard/DashboardLinkedUserCard.tsx index 3638e6545..b134bffad 100644 --- a/src/components/backoffice/dashboard/DashboardLinkedUserCard/DashboardLinkedUserCard.tsx +++ b/src/components/backoffice/dashboard/DashboardLinkedUserCard/DashboardLinkedUserCard.tsx @@ -17,7 +17,10 @@ import { export const DashboardLinkedUserCard = () => { const user = useAuthenticatedUser(); const linkedUser = useSelector(selectLinkedUser); - if (!linkedUser || isRoleIncluded([USER_ROLES.COACH_EXTERNAL, USER_ROLES.ADMIN], user.role)) { + if ( + !linkedUser || + isRoleIncluded([USER_ROLES.COACH_EXTERNAL, USER_ROLES.ADMIN], user.role) + ) { return null; } return ( diff --git a/src/components/backoffice/dashboard/DashboardOpportunitiesCard/DashboardOpportunitiesCard.styles.tsx b/src/components/backoffice/dashboard/DashboardOpportunitiesCard/DashboardOpportunitiesCard.styles.tsx index 95da8af29..b26c1e33e 100644 --- a/src/components/backoffice/dashboard/DashboardOpportunitiesCard/DashboardOpportunitiesCard.styles.tsx +++ b/src/components/backoffice/dashboard/DashboardOpportunitiesCard/DashboardOpportunitiesCard.styles.tsx @@ -63,6 +63,7 @@ export const StyledDashboardOpportunityItemBLs = styled.div` -webkit-line-clamp: 1; -webkit-box-orient: vertical; white-space: normal; + max-width: 200px; } `; @@ -78,3 +79,10 @@ export const StyledDashboardOpprtunityCTAOrSpinnerContainer = styled.div` margin-top: 20px; width: 100%; `; + +export const StyledDashboardOpportunitiesEmptyState = styled.div` + font-style: italic; + svg { + margin-right: 10px; + } +`; diff --git a/src/components/backoffice/dashboard/DashboardOpportunitiesCard/DashboardOpportunitiesCard.tsx b/src/components/backoffice/dashboard/DashboardOpportunitiesCard/DashboardOpportunitiesCard.tsx index 2073d31b6..27f8ec8d4 100644 --- a/src/components/backoffice/dashboard/DashboardOpportunitiesCard/DashboardOpportunitiesCard.tsx +++ b/src/components/backoffice/dashboard/DashboardOpportunitiesCard/DashboardOpportunitiesCard.tsx @@ -19,6 +19,7 @@ import { } from 'src/use-cases/authentication'; import { findConstantFromValue, buildContractLabel } from 'src/utils'; import { + StyledDashboardOpportunitiesEmptyState, StyledDashboardOpportunitiesListContainer, StyledDashboardOpportunityItem, StyledDashboardOpportunityItemBLs, @@ -33,7 +34,7 @@ const uuidValue = uuid(); export const DashboardOpportunitiesCard = () => { const user = useAuthenticatedUser(); - const { isDataLoading, numberOpportunitiesInProgess, opportunities } = + const { isDataLoading, opportunities, numberOpportunitiesInProgess } = useDashboardOpportunities(); const { contextualRole } = useContextualRole(user.role); const isDesktop = useIsDesktop(); @@ -52,92 +53,106 @@ export const DashboardOpportunitiesCard = () => { > {!isDataLoading ? ( <> - {numberOpportunitiesInProgess && ( - - -
    - - {contextualRole === USER_ROLES.COACH - ? `${candidate?.firstName} a` - : `Vous avez`} - - {' '} - {numberOpportunitiesInProgess} opportunité - {numberOpportunitiesInProgess > 0 && 's'} à traiter. - -
    - {isDesktop && ( - - )} -
    -
    - )} - {opportunities && opportunities.length > 0 && !isDataLoading && ( - <> -
    - - {opportunities.map((opportunity, i) => { - return ( - 0 && ( + + +
    + + {contextualRole === USER_ROLES.COACH + ? `${candidate?.firstName} a` + : `Vous avez`} + + {' '} + {numberOpportunitiesInProgess} opportunité + {numberOpportunitiesInProgess > 0 && 's'} à traiter. + +
    + {isDesktop && ( +