From f92810d320e8b703d7de4279d4c6b914a385286b Mon Sep 17 00:00:00 2001 From: SD Date: Sat, 17 Aug 2024 03:51:27 +0400 Subject: [PATCH] feat: add expert profile --- .../expert-profile/add-offer/page.tsx | 4 +- .../account/(account)/expert-profile/page.tsx | 20 +- src/app/[locale]/experts/[expertId]/page.tsx | 6 +- src/components/Account/ProfileSettings.tsx | 38 +-- .../ExpertProfile/ExpertProfile.tsx | 156 ++++++++--- .../ExpertProfile/content/ExpertAbout.tsx | 81 ++++++ .../ExpertProfile/content/ExpertEducation.tsx | 157 ++++++++--- .../ExpertProfile/content/ExpertPayData.tsx | 49 +++- .../ExpertProfile/content/ExpertSchedule.tsx | 30 ++- .../ExpertProfile/content/ExpertTags.tsx | 2 +- src/components/Experts/ExpertDetails.tsx | 25 +- src/components/Experts/Filter.tsx | 9 +- .../Modals/EditExpertAboutModal.tsx | 254 ++++++++++++++++++ .../Modals/EditExpertEducationModal.tsx | 152 +++++++++++ .../Modals/EditExpertPayDataModal.tsx | 118 ++++++++ src/components/Modals/EditExpertTagsModal.tsx | 2 +- .../educationModalContent/Certificates.tsx | 205 ++++++++++++++ .../educationModalContent/Educations.tsx | 166 ++++++++++++ .../educationModalContent/Experiences.tsx | 101 +++++++ .../Modals/educationModalContent/Mbas.tsx | 166 ++++++++++++ .../educationModalContent/Trainings.tsx | 166 ++++++++++++ src/components/view/FilledButton.tsx | 6 + src/components/view/LinkButton.tsx | 2 +- src/i18nKeys/de.ts | 34 ++- src/i18nKeys/en.ts | 33 +++ src/i18nKeys/es.ts | 34 ++- src/i18nKeys/fr.ts | 34 ++- src/i18nKeys/it.ts | 34 ++- src/i18nKeys/ru.ts | 36 ++- src/styles/_edu.scss | 35 +++ src/styles/_modal.scss | 13 +- src/styles/_pages.scss | 72 ++++- src/styles/_schedule.scss | 15 ++ src/styles/style.scss | 2 + src/styles/view/_buttons.scss | 17 ++ src/styles/view/_collapse.scss | 15 ++ src/styles/view/_input.scss | 8 + src/styles/view/_practice.scss | 29 ++ src/styles/view/style.scss | 2 + src/types/education.ts | 17 +- src/types/practice.ts | 16 +- src/types/profile.ts | 3 +- src/utils/account.ts | 16 ++ src/utils/time.ts | 62 +++++ 44 files changed, 2281 insertions(+), 161 deletions(-) create mode 100644 src/components/ExpertProfile/content/ExpertAbout.tsx create mode 100644 src/components/Modals/EditExpertAboutModal.tsx create mode 100644 src/components/Modals/EditExpertEducationModal.tsx create mode 100644 src/components/Modals/EditExpertPayDataModal.tsx create mode 100644 src/components/Modals/educationModalContent/Certificates.tsx create mode 100644 src/components/Modals/educationModalContent/Educations.tsx create mode 100644 src/components/Modals/educationModalContent/Experiences.tsx create mode 100644 src/components/Modals/educationModalContent/Mbas.tsx create mode 100644 src/components/Modals/educationModalContent/Trainings.tsx create mode 100644 src/styles/_edu.scss create mode 100644 src/styles/_schedule.scss create mode 100644 src/styles/view/_collapse.scss create mode 100644 src/styles/view/_practice.scss create mode 100644 src/utils/time.ts diff --git a/src/app/[locale]/account/(account)/expert-profile/add-offer/page.tsx b/src/app/[locale]/account/(account)/expert-profile/add-offer/page.tsx index e669b63..7ed015c 100644 --- a/src/app/[locale]/account/(account)/expert-profile/add-offer/page.tsx +++ b/src/app/[locale]/account/(account)/expert-profile/add-offer/page.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { Link } from '../../../../../../../navigation'; -import { CustomSelect } from '../../../../../../../components/view/CustomSelect'; +import { Link } from '../../../../../../navigation'; +import { CustomSelect } from '../../../../../../components/view/CustomSelect'; export default function AddOffer() { return ( diff --git a/src/app/[locale]/account/(account)/expert-profile/page.tsx b/src/app/[locale]/account/(account)/expert-profile/page.tsx index c9cd00d..3eaea64 100644 --- a/src/app/[locale]/account/(account)/expert-profile/page.tsx +++ b/src/app/[locale]/account/(account)/expert-profile/page.tsx @@ -6,7 +6,15 @@ import { message } from 'antd'; import { ExpertData } from '../../../../../types/profile'; import { AUTH_TOKEN_KEY } from '../../../../../constants/common'; import { useLocalStorage } from '../../../../../hooks/useLocalStorage'; -import { getEducation, getPersonalData, getTags, getPractice, getSchedule, getPayData } from '../../../../../actions/profile'; +import { + getEducation, + getPersonalData, + getTags, + getPractice, + getSchedule, + getPayData, + getUserData +} from '../../../../../actions/profile'; import { ExpertProfile } from '../../../../../components/ExpertProfile'; import { Loader } from '../../../../../components/view/Loader'; @@ -15,11 +23,13 @@ export default function ExpertProfilePage({ params: { locale } }: { params: { lo const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, ''); const [loading, setLoading] = useState(false); const [data, setData] = useState(); + const [isFull, setIsFull] = useState(false); useEffect(() => { if (jwt) { setLoading(true); Promise.all([ + getUserData(locale, jwt), getPersonalData(locale, jwt), getEducation(locale, jwt), getTags(locale, jwt), @@ -27,13 +37,12 @@ export default function ExpertProfilePage({ params: { locale } }: { params: { lo getSchedule(locale, jwt), getPayData(locale, jwt) ]) - .then(([person, education, tags, practice, schedule, payData]) => { + .then(([profile, person, education, tags, practice, schedule, payData]) => { + console.log('profile', profile); console.log('person', person); console.log('education', education); - console.log('tags', tags); - console.log('practice', practice); console.log('schedule', schedule); - console.log('payData', payData); + setIsFull(profile.fillProgress === 'full'); setData({ person, education, @@ -56,6 +65,7 @@ export default function ExpertProfilePage({ params: { locale } }: { params: { lo {data && ( + {/*

All Offers by this Expert

diff --git a/src/components/Account/ProfileSettings.tsx b/src/components/Account/ProfileSettings.tsx index 674ae2c..21da0a7 100644 --- a/src/components/Account/ProfileSettings.tsx +++ b/src/components/Account/ProfileSettings.tsx @@ -12,7 +12,7 @@ import { validateImage } from '../../utils/account'; import { useProfileSettings } from '../../actions/hooks/useProfileSettings'; import { CustomInput } from '../view/CustomInput'; import { OutlinedButton } from '../view/OutlinedButton'; -import { FilledYellowButton } from '../view/FilledButton'; +import {FilledButton, FilledSquareButton, FilledYellowButton} from '../view/FilledButton'; import { DeleteAccountModal } from '../Modals/DeleteAccountModal'; import { Loader } from '../view/Loader'; @@ -55,14 +55,14 @@ export const ProfileSettings: FC = ({ locale }) => { languagesLinks: languagesLinks?.map(({ languageId }) => ({ languageId })) || [] }; - // if (photo) { - // console.log(photo); - // const formData = new FormData(); - // formData.append('file', photo as FileType); - // - // newProfile.faceImage = photo; - // newProfile.isFaceImageKeepExisting = false; - // } + if (photo) { + console.log(photo); + const formData = new FormData(); + formData.append('file', photo as FileType); + + newProfile.faceImage = `[${(photo as File).arrayBuffer()}]`; + newProfile.isFaceImageKeepExisting = false; + } console.log(newProfile); @@ -99,13 +99,6 @@ export const ProfileSettings: FC = ({ locale }) => { return (
-
-
- -
-
{i18nText('photoDesc', locale)}
-
= ({ locale }) => { multiple={false} showUploadList={false} > - {photo && } - +
+
+ } + /> +
+
{i18nText('photoDesc', locale)}
+
diff --git a/src/components/ExpertProfile/ExpertProfile.tsx b/src/components/ExpertProfile/ExpertProfile.tsx index 669bf77..fa7e13a 100644 --- a/src/components/ExpertProfile/ExpertProfile.tsx +++ b/src/components/ExpertProfile/ExpertProfile.tsx @@ -1,43 +1,96 @@ 'use client' -import { useState } from 'react'; -import { message } from 'antd'; -import { EditOutlined } from '@ant-design/icons'; +import React, { useState } from 'react'; +import {Alert, message} from 'antd'; +import Image from 'next/image'; import { i18nText } from '../../i18nKeys'; -import { ExpertData } from '../../types/profile'; +import { ExpertData, PayInfo, ProfileData } from '../../types/profile'; +import { ExpertsTags } from '../../types/tags'; +import { PracticeDTO } from '../../types/practice'; +import { EducationDTO } from '../../types/education'; +import { ScheduleDTO } from '../../types/schedule'; import { AUTH_TOKEN_KEY } from '../../constants/common'; import { useLocalStorage } from '../../hooks/useLocalStorage'; -import { getTags } from '../../actions/profile'; +import { getTags, getPayData, getEducation, getPractice, getSchedule, getPersonalData } from '../../actions/profile'; import { Loader } from '../view/Loader'; -import { LinkButton } from '../view/LinkButton'; import { ExpertTags } from './content/ExpertTags'; import { ExpertSchedule } from './content/ExpertSchedule'; import { ExpertPayData } from './content/ExpertPayData'; import { ExpertEducation } from './content/ExpertEducation'; +import { ExpertAbout } from './content/ExpertAbout'; type ExpertProfileProps = { locale: string; data: ExpertData; updateData: (data: ExpertData) => void; + isFull: boolean; }; -export const ExpertProfile = ({ locale, data, updateData }: ExpertProfileProps) => { +type NewDataPartProps = { + key: keyof ExpertData, + getNewData: (locale: string, token: string) => Promise, + errorMessage?: string; +}; + +export const ExpertProfile = ({ locale, data, updateData, isFull }: ExpertProfileProps) => { const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, ''); const [loading, setLoading] = useState<(keyof ExpertData)[]>([]); + function getNewPartData ({ key, getNewData, errorMessage = 'Не удалось получить данные' }: NewDataPartProps) { + setLoading([key]); + getNewData(locale, jwt) + .then((newData) => { + updateData({ + ...data, + [key]: newData + }); + }) + .catch(() => message.error(errorMessage)) + .finally(() => setLoading([])); + } + const updateExpert = (key: keyof ExpertData) => { switch (key) { case 'tags': - setLoading([key]); - getTags(locale, jwt) - .then((tags) => { - updateData({ - ...data, - tags - }); - }) - .catch(() => message.error('Не удалось обновить направления')) - .finally(() => setLoading([])); + getNewPartData({ + key, + getNewData: getTags, + errorMessage: 'Не удалось получить направления' + }); + break; + case 'practice': + getNewPartData({ + key, + getNewData: getPractice + }); + break; + case 'education': + getNewPartData({ + key, + getNewData: getEducation, + errorMessage: 'Не удалось получить информацию об образовании' + }); + break; + case 'schedule': + getNewPartData({ + key, + getNewData: getSchedule, + errorMessage: 'Не удалось получить расписание' + }); + break; + case 'person': + getNewPartData({ + key, + getNewData: getPersonalData, + errorMessage: 'Не удалось получить информацию о пользователе' + }); + break; + case 'payData': + getNewPartData<{ person6Data?: PayInfo }>({ + key, + getNewData: getPayData, + errorMessage: 'Не удалось получить платежную информацию' + }); break; default: break; @@ -52,36 +105,39 @@ export const ExpertProfile = ({ locale, data, updateData }: ExpertProfileProps)
- +
-
+
- David + {`${data?.person?.username} ${data?.person?.surname || ''}`}
-
-
-
-
-
-

{i18nText('aboutCoach', locale)}

-

person1 + person4

- } + {!isFull && ( + +
  • о себе
  • +
  • темы сессии
  • +
  • рабочее расписание
  • +
  • информация об образовании
  • +
  • платежная информация
  • + + )} + type="warning" + showIcon /> -
    -
    - {`12 ${i18nText('practiceHours', locale)}`} -
    -
    - {`15 ${i18nText('supervisionCount', locale)}`} -
    -
    - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam aliquet, lectus nec viverra - malesuada, ligula sem tempor risus, non posuere urna diam a libero. -
    + )}
    + + + + - - + + + + + +
    - ) + ); }; diff --git a/src/components/ExpertProfile/content/ExpertAbout.tsx b/src/components/ExpertProfile/content/ExpertAbout.tsx new file mode 100644 index 0000000..0d1cdf5 --- /dev/null +++ b/src/components/ExpertProfile/content/ExpertAbout.tsx @@ -0,0 +1,81 @@ +'use client' + +import {useState} from "react"; +import {Tag} from "antd"; +import {EditOutlined} from "@ant-design/icons"; +import {LinkButton} from "../../view/LinkButton"; +import {ExpertData, ProfileData} from "../../../types/profile"; +import {i18nText} from "../../../i18nKeys/index"; +import {PracticeDTO} from "../../../types/practice"; +import {ExpertPractice} from "../../Experts/ExpertDetails"; +import {EditExpertAboutModal} from "../../Modals/EditExpertAboutModal"; + +type ExpertAboutProps = { + locale: string; + practice?: PracticeDTO; + person?: ProfileData; + updateExpert: (key: keyof ExpertData) => void; +}; + +export const ExpertAbout = ({ locale, updateExpert, practice, person }: ExpertAboutProps) => { + const [showEdit, setShowEdit] = useState(false); + + const supervisionCount = practice?.person4Data?.supervisionPerYears && practice?.person4Data?.supervisionPerYearId + ? practice.person4Data.supervisionPerYears.filter(({ id }) => id === practice.person4Data.supervisionPerYearId) + : []; + + return ( +
    +
    +
    +

    {i18nText('aboutCoach', locale)}

    + } + onClick={() => setShowEdit(true)} + /> +
    +
    +
    + {`${practice?.person4Data?.practiceHours || 0} ${i18nText('practiceHours', locale)} | ${supervisionCount.length > 0 ? supervisionCount[0].name : 0} ${i18nText('supervisionCount', locale)}`} +
    +
    + {practice?.person4Data?.sessionCost && ( +
    +
    {i18nText('price', locale)}
    +
    {`${practice?.person4Data?.sessionCost} €`}
    +
    + )} + {practice?.person4Data?.sessionDuration && ( +
    +
    {i18nText('duration', locale)}
    +
    {`${practice?.person4Data?.sessionDuration} ${locale === 'ru' ? 'мин' : 'min'}`}
    +
    + )} +
    +
    +
    {i18nText('sessionLang', locale)}
    +
    + {person?.languagesLinks && person.languagesLinks?.length > 0 && person.languagesLinks + .map(({ language: { code, nativeSpelling } }) => {nativeSpelling})} +
    +
    + +
    +
    + setShowEdit(false)} + refreshPractice={() => updateExpert('practice')} + refreshPerson={() => updateExpert('person')} + /> +
    + ); +}; diff --git a/src/components/ExpertProfile/content/ExpertEducation.tsx b/src/components/ExpertProfile/content/ExpertEducation.tsx index 8ddc1e6..210d2c8 100644 --- a/src/components/ExpertProfile/content/ExpertEducation.tsx +++ b/src/components/ExpertProfile/content/ExpertEducation.tsx @@ -1,65 +1,154 @@ +'use client' + import { EditOutlined } from '@ant-design/icons'; import { EducationDTO } from '../../../types/education'; import { i18nText } from '../../../i18nKeys'; import { LinkButton } from '../../view/LinkButton'; +import {ExpertCertificate} from "../../Experts/ExpertDetails"; +import {useState} from "react"; +import {ExpertData} from "../../../types/profile"; +import {EditExpertEducationModal} from "../../Modals/EditExpertEducationModal"; type ExpertEducationProps = { locale: string; data?: EducationDTO; + updateExpert: (key: keyof ExpertData) => void; }; -export const ExpertEducation = ({ locale, data }: ExpertEducationProps) => { +export const ExpertEducation = ({ locale, data, updateExpert }: ExpertEducationProps) => { + const [showEdit, setShowEdit] = useState(false); + + const getAssociationLevel = (accLevelId?: number) => { + if (accLevelId) { + const [cur] = (data?.associationLevels || []).filter(({ id }) => id === accLevelId) || []; + + return cur?.name || ''; + } + + return ''; + }; + + const getAssociation = (accLevelId?: number) => { + if (accLevelId) { + const [curLevel] = (data?.associationLevels || []).filter(({ id }) => id === accLevelId) || []; + if (curLevel) { + const [cur] = (data?.associations || []).filter(({ id }) => id === curLevel.associationId) || []; + return cur?.name || ''; + } + } + + return ''; + }; + return (
    -

    {i18nText('education', locale)}

    -

    person2

    +

    {i18nText('skillsInfo', locale)}

    } + onClick={() => setShowEdit(true)} />
    -
    -

    Psychologist

    -
    - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam aliquet, lectus nec viverra - malesuada, ligula sem tempor risus, non posuere urna diam a libero. + {data?.person2Data?.educations?.length > 0 && ( +
    + {data?.person2Data?.educations?.map(({ id, title, description, document }) => ( +
    +

    {title}

    + {description &&
    {description}
    } + {document && ( +
    + +
    + )} +
    + ))}
    -
    - + )} +
    + {data?.person2Data?.certificates?.length > 0 && ( +
    +

    {i18nText('profCertification', locale)}

    +
    + {data?.person2Data?.certificates?.map((cert) => ( +
    +
    + {`${getAssociationLevel(cert?.associationLevelId)} ${getAssociation(cert?.associationLevelId)}`} +
    + {cert.document && ( +
    + +
    + )} +
    + ))}
    -
    -
    -

    {i18nText('profCertification', locale)}

    -
    -
    - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam aliquet, lectus nec viverra - malesuada, ligula sem tempor risus, non posuere urna diam a libero. + )} + {data?.person2Data?.trainings?.length > 0 && ( +
    +

    + {`${i18nText('trainings', locale)} | ${i18nText('seminars', locale)} | ${i18nText('courses', locale)}`} +

    +
    + {data?.person2Data?.trainings?.map(({ id, title, description, document }) => ( +
    +

    {title}

    + {description &&
    {description}
    } + {document && ( +
    + +
    + )} +
    + ))}
    -
    -
    -

    - {`${i18nText('trainings', locale)} | ${i18nText('seminars', locale)} | ${i18nText('courses', locale)}`} -

    -
    -
    - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam aliquet, lectus nec viverra - malesuada, ligula sem tempor risus, non posuere urna diam a libero. + )} + {data?.person2Data?.mbas?.length > 0 && ( +
    +

    {i18nText('mba', locale)}

    +
    + {data?.person2Data?.mbas?.map(({ id, title, description, document }) => ( +
    +

    {title}

    + {description &&
    {description}
    } + {document && ( +
    + +
    + )} +
    + ))}
    -
    -
    -

    {i18nText('mba', locale)}

    -
    -
    - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam aliquet, lectus nec viverra - malesuada, ligula sem tempor risus, non posuere urna diam a libero. + )} + {data?.person2Data?.experiences?.length > 0 && ( +
    +

    {i18nText('mExperiences', locale)}

    +
    + {data?.person2Data?.experiences?.map(({ id, title, description, document }) => ( +
    +

    {title}

    + {description &&
    {description}
    } + {document && ( +
    + +
    + )} +
    + ))}
    -
    + )} + setShowEdit(false)} + locale={locale} + data={data} + refresh={() => updateExpert('education')} + />
    ); }; diff --git a/src/components/ExpertProfile/content/ExpertPayData.tsx b/src/components/ExpertProfile/content/ExpertPayData.tsx index 648aed2..6eeaf27 100644 --- a/src/components/ExpertProfile/content/ExpertPayData.tsx +++ b/src/components/ExpertProfile/content/ExpertPayData.tsx @@ -1,28 +1,65 @@ +'use client' + +import { useState } from 'react'; import { EditOutlined } from '@ant-design/icons'; import { i18nText } from '../../../i18nKeys'; -import { PayInfo } from '../../../types/profile'; +import { ExpertData, PayInfo } from '../../../types/profile'; import { LinkButton } from '../../view/LinkButton'; +import { EditExpertPayDataModal } from '../../Modals/EditExpertPayDataModal'; type ExpertPayDataProps = { locale: string; - data?: PayInfo + data?: PayInfo; + updateExpert: (key: keyof ExpertData) => void; }; -export const ExpertPayData = ({ locale, data }: ExpertPayDataProps) => { +export const ExpertPayData = ({ locale, data, updateExpert }: ExpertPayDataProps) => { + const [showEdit, setShowEdit] = useState(false); + + const hide = (str?: string) => { + const reg = new RegExp('(.)(?=.*....)', 'gi'); + return str ? str.replace(reg, '*') : ''; + } + return (
    -

    Card data - person6

    +

    {i18nText('payInfo', locale)}

    } + onClick={() => setShowEdit(true)} />
    -
    - Card +
    + {data?.beneficiaryName && ( +
    +
    {i18nText('beneficiaryName', locale)}
    +
    {data.beneficiaryName}
    +
    + )} + {data?.bicOrSwift && ( +
    +
    {i18nText('bicOrSwift', locale)}
    +
    {hide(data.bicOrSwift)}
    +
    + )} + {data?.iban && ( +
    +
    IBAN
    +
    {hide(data.iban)}
    +
    + )}
    + setShowEdit(false)} + refresh={() => updateExpert('payData')} + />
    ); }; diff --git a/src/components/ExpertProfile/content/ExpertSchedule.tsx b/src/components/ExpertProfile/content/ExpertSchedule.tsx index 907aa22..33d5285 100644 --- a/src/components/ExpertProfile/content/ExpertSchedule.tsx +++ b/src/components/ExpertProfile/content/ExpertSchedule.tsx @@ -1,7 +1,11 @@ import { EditOutlined } from '@ant-design/icons'; +import dayjs from 'dayjs'; import { ScheduleDTO } from '../../../types/schedule'; import { i18nText } from '../../../i18nKeys'; import { LinkButton } from '../../view/LinkButton'; +import {useState} from "react"; +import {Tag} from "antd"; +import {getCurrentTime} from "../../../utils/time"; type ExpertScheduleProps = { locale: string; @@ -9,18 +13,34 @@ type ExpertScheduleProps = { }; export const ExpertSchedule = ({ locale, data }: ExpertScheduleProps) => { + const [showEdit, setShowEdit] = useState(false); + // person51 + return (
    -

    Schedule - person51

    - {i18nText('schedule', locale)} + {/*} - /> + onClick={() => setShowEdit(true)} + />*/}
    -
    - Schedule +
    + {data && data?.workingTimes?.map((date, index) => { + const { startDay, startTime, endDay, endTime } = getCurrentTime(date); + + return ( +
    + {i18nText(startDay, locale)} +
    {startTime}
    + - + {startDay !== endDay && {i18nText(endDay, locale)}} +
    {endTime}
    +
    + ) + })}
    diff --git a/src/components/ExpertProfile/content/ExpertTags.tsx b/src/components/ExpertProfile/content/ExpertTags.tsx index 7441bed..1e9d27a 100644 --- a/src/components/ExpertProfile/content/ExpertTags.tsx +++ b/src/components/ExpertProfile/content/ExpertTags.tsx @@ -22,7 +22,7 @@ export const ExpertTags = ({ locale, data, updateExpert }: ExpertTagsProps) => {
    -

    {i18nText('direction', locale)}

    +

    {i18nText('topics', locale)}

    } diff --git a/src/components/Experts/ExpertDetails.tsx b/src/components/Experts/ExpertDetails.tsx index e2c942c..fec8476 100644 --- a/src/components/Experts/ExpertDetails.tsx +++ b/src/components/Experts/ExpertDetails.tsx @@ -4,7 +4,8 @@ import React, { FC } from 'react'; import Image from 'next/image'; import { Tag, Image as AntdImage, Space } from 'antd'; import { ZoomInOutlined, ZoomOutOutlined, StarFilled } from '@ant-design/icons'; -import { ExpertDetails, ExpertDocument } from '../../types/experts'; +import { ExpertDetails, Practice, ThemeGroup } from '../../types/experts'; +import { ExpertDocument } from '../../types/file'; import { Locale } from '../../types/locale'; import { CustomRate } from '../view/CustomRate'; import { i18nText } from '../../i18nKeys'; @@ -15,6 +16,12 @@ type ExpertDetailsProps = { locale?: string; }; +type ExpertPracticeProps = { + cases?: Practice[]; + themes?: ThemeGroup[]; + locale?: string; +}; + export const ExpertCard: FC = ({ expert, locale }) => { const { publicCoachDetails } = expert || {}; @@ -62,10 +69,10 @@ export const ExpertInformation: FC = ({ expert, locale }) =>
    {/*

    {}

    */}
    - {coachLanguages?.map((skill) => {skill})} + {coachLanguages?.map((lang) => {lang})}
    -

    + {/*

    Hello, my name is Marcelo. I am a Senior UX Designer with more than 6 years of experience working with the largest companies in the world such as Disney, Globant and currently IBM. During my career, I have helped organizations solve complex problems using aesthetically pleasing @@ -79,7 +86,7 @@ export const ExpertInformation: FC = ({ expert, locale }) => Strategic thinking

    Oh, and I also speak Spanish! -

    +

    */}
    {tags?.map((skill) => {skill?.name})}
    @@ -93,14 +100,12 @@ export const ExpertInformation: FC = ({ expert, locale }) => ); }; -export const ExpertPractice: FC = ({ expert, locale }) => { - const { publicCoachDetails: { practiceCases = [], themesGroups = [] } } = expert || {}; - - return practiceCases?.length > 0 ? ( +export const ExpertPractice: FC = ({ themes = [], cases = [], locale }) => { + return cases?.length > 0 ? (

    {i18nText('successfulCase', locale)}

    - {practiceCases?.map(({ id, description, themesGroupIds }) => { - const filtered = themesGroups?.filter(({ id }) => themesGroupIds?.includes(+id)); + {cases?.map(({ id, description, themesGroupIds }) => { + const filtered = themes ? themes.filter(({ id }) => themesGroupIds?.includes(+id)) : []; return (
    diff --git a/src/components/Experts/Filter.tsx b/src/components/Experts/Filter.tsx index 7a548ae..debb1d1 100644 --- a/src/components/Experts/Filter.tsx +++ b/src/components/Experts/Filter.tsx @@ -162,18 +162,15 @@ export const ExpertsFilter = ({ ), [filter, searchParams, searchData]); const getLangList = () => { - const reg = searchLang ? new RegExp(searchLang, 'ig') : ''; - const langList = reg ? (languages || []).filter(({ code, nativeSpelling }) => reg.test(code) || reg.test(nativeSpelling)) : languages; + const langList = searchLang ? (languages || []).filter(({ code, nativeSpelling }) => code.indexOf(searchLang) !== -1 || nativeSpelling.indexOf(searchLang) !== -1) : languages; return langList?.length ? getList('userLanguages', langList.map(({ code, nativeSpelling }) => ({ id: code, name: nativeSpelling }))) : null; }; const getTagsList = () => { - const reg = searchTags ? new RegExp(searchTags, 'ig') : ''; - - if (reg) { - const tagsList = filteredTags.filter(({ name, group }) => reg.test(name) || reg.test(group)); + if (searchTags) { + const tagsList = filteredTags.filter(({ name, group }) => name.indexOf(searchTags) !== -1 || group.indexOf(searchTags) !== -1); return getList('themesTagIds', tagsList); } diff --git a/src/components/Modals/EditExpertAboutModal.tsx b/src/components/Modals/EditExpertAboutModal.tsx new file mode 100644 index 0000000..a17c8a0 --- /dev/null +++ b/src/components/Modals/EditExpertAboutModal.tsx @@ -0,0 +1,254 @@ +'use client'; + +import React, { FC, useEffect, useState } from 'react'; +import { Modal, Button, message, Form, Input } from 'antd'; +import { CloseOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons'; +import { i18nText } from '../../i18nKeys'; +import { ProfileData, ProfileRequest } from '../../types/profile'; +import { PracticePersonData, PracticeDTO, PracticeData, PracticeCase } from '../../types/practice'; +import { AUTH_TOKEN_KEY } from '../../constants/common'; +import { useLocalStorage } from '../../hooks/useLocalStorage'; +import { setPersonData, setPractice } from '../../actions/profile'; +import { CustomInput } from '../view/CustomInput'; +import { CustomMultiSelect } from '../view/CustomMultiSelect'; +import { CustomSelect } from '../view/CustomSelect'; +import { LinkButton } from '../view/LinkButton'; + +type EditExpertAboutModalProps = { + open: boolean; + handleCancel: () => void; + locale: string; + practice?: PracticeDTO; + person?: ProfileData; + refreshPractice: () => void; + refreshPerson: () => void; +}; + +type FormPerson = PracticePersonData & { + sessionLang: number[]; +}; + +export const EditExpertAboutModal: FC = ({ + open, + handleCancel, + locale, + practice, + person, + refreshPerson, + refreshPractice +}) => { + const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, ''); + const [loading, setLoading] = useState(false); + const [form] = Form.useForm(); + const [practiceCases, setPracticeCases] = useState([]); + + useEffect(() => { + if (open) { + if (practice?.person4Data) { + form.setFieldsValue(practice.person4Data); + + setPracticeCases(practice.person4Data?.practiceCases || []); + } + if (person?.languagesLinks) { + form.setFieldValue('sessionLang', person.languagesLinks.map(({ languageId }) => languageId)) + } + } + }, [open, practice?.person4Data]); + + const addPracticeCase = () => { + setPracticeCases([ + ...practiceCases, + { + description: '', + themesGroupIds: [] + } + ]); + }; + + const deletePracticeCase = (index: number) => { + setPracticeCases([...practiceCases].filter((cases, i) => i !== index)); + }; + + const onChangePracticeDescription = (value: string, index: number) => { + setPracticeCases(practiceCases.map((cases, i) => { + if (i === index) { + return { + ...cases, + description: value + } + } + + return cases; + })); + }; + + const onChangePracticeThemes = (value: number[], index: number) => { + setPracticeCases(practiceCases.map((cases, i) => { + if (i === index) { + return { + ...cases, + themesGroupIds: value + } + } + + return cases; + })); + }; + + const onSave = () => { + form.validateFields().then((values) => { + const newPersonData: ProfileRequest = { + login: person?.login, + isPasswordKeepExisting: true, + username: person?.username, + surname: person?.surname, + isFaceImageKeepExisting: true, + phone: person?.phone, + languagesLinks: values?.sessionLang?.map((id) => ({ languageId: +id })) || [] + }; + + const newPracticeData: PracticeData = { + practiceHours: values?.practiceHours, + supervisionPerYearId: values?.supervisionPerYearId, + sessionDuration: values?.sessionDuration ? (isNaN(Number(values.sessionDuration)) ? 0 : Number(values.sessionDuration)) : 0, + sessionCost: values?.sessionCost ? (isNaN(Number(values.sessionCost)) ? 0 : Number(values.sessionCost)) : 0, + practiceCases: practiceCases ? practiceCases : practice?.person4Data?.practiceCases + } + + setLoading(true); + Promise.all([ + setPractice(locale, jwt, newPracticeData), + setPersonData(newPersonData, locale, jwt) + ]) + .then(() => { + handleCancel(); + refreshPractice(); + refreshPerson(); + }) + .catch(() => { + message.error('Не удалось сохранить данные'); + }) + .finally(() => { + setLoading(false); + }) + }) + }; + + return ( + } + > +
    +
    {i18nText('aboutCoach', locale)}
    +
    + + + ({ value: id, label: nativeSpelling })) || []} + /> + + + + + + ({ value: cost, label: cost })) || []} + /> + + + + + + ({ value: id, label: name })) || []} + /> + + +
    +
    +
    +
    {i18nText('successfulCase', locale)}
    + } + onClick={addPracticeCase} + /> +
    + {practiceCases.map(({ description, themesGroupIds }, index) => ( +
    +
    +
    + ({ value: id, label: name })) || []} + onChange={(val) => onChangePracticeThemes(val, index)} + /> +
    +
    + onChangePracticeDescription(e.target.value, index)} + /> +
    +
    + } + onClick={() => deletePracticeCase(index)} + /> +
    + ))} +
    +
    + +
    +
    +
    + ); +}; diff --git a/src/components/Modals/EditExpertEducationModal.tsx b/src/components/Modals/EditExpertEducationModal.tsx new file mode 100644 index 0000000..e2720e5 --- /dev/null +++ b/src/components/Modals/EditExpertEducationModal.tsx @@ -0,0 +1,152 @@ +'use client'; + +import React, { FC, useEffect, useState } from 'react'; +import {Modal, Button, message, Form, Collapse, GetProp, UploadProps} from 'antd'; +import type { CollapseProps } from 'antd'; +import { CloseOutlined } from '@ant-design/icons'; +import { i18nText } from '../../i18nKeys'; +import { PracticePersonData, PracticeDTO, PracticeData, PracticeCase } from '../../types/practice'; +import { AUTH_TOKEN_KEY } from '../../constants/common'; +import { useLocalStorage } from '../../hooks/useLocalStorage'; +import {setEducation} from '../../actions/profile'; +import {Certificate, Details, EducationData, EducationDTO, Experience} from "../../types/education"; +import {CertificatesContent} from "./educationModalContent/Certificates"; +import {EducationsContent} from "./educationModalContent/Educations"; +import {TrainingsContent} from "./educationModalContent/Trainings"; +import {MbasContent} from "./educationModalContent/Mbas"; +import {ExperiencesContent} from "./educationModalContent/Experiences"; + +type EditExpertEducationModalProps = { + open: boolean; + handleCancel: () => void; + locale: string; + data?: EducationDTO; + refresh: () => void; +}; + +type FormPerson = PracticePersonData & { + sessionLang: number[]; +}; + +export const EditExpertEducationModal: FC = ({ + open, + handleCancel, + locale, + data, + refresh +}) => { + const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, ''); + const [loading, setLoading] = useState(false); + const [form] = Form.useForm(); + const [editedData, setEditedData] = useState(data?.person2Data as EducationData); + + const onSave = () => { + setLoading(true); + setEducation(locale, jwt, editedData) + .then(() => { + handleCancel(); + refresh(); + }) + .catch(() => { + message.error('Не удалось сохранить образование'); + }) + .finally(() => { + setLoading(false); + }) + }; + + const items: CollapseProps['items'] = [ + { + key: 'certificates', + label: i18nText('profCertification', locale), + children: ( + setEditedData({ ...editedData, certificates })} + locale={locale} + associationLevels={data?.associationLevels} + associations={data?.associations} + /> + ), + }, + { + key: 'educations', + label: i18nText('education', locale), + children: ( + setEditedData({ ...editedData, educations })} + locale={locale} + /> + ), + }, + { + key: 'trainings', + label: `${i18nText('trainings', locale)} | ${i18nText('seminars', locale)} | ${i18nText('courses', locale)}`, + children: ( + setEditedData({ ...editedData, trainings })} + locale={locale} + /> + ), + }, + { + key: 'mbas', + label: i18nText('mba', locale), + children: ( + setEditedData({ ...editedData, mbas })} + locale={locale} + /> + ), + }, + { + key: 'experiences', + label: i18nText('mExperiences', locale), + children: ( + setEditedData({ ...editedData, experiences })} + locale={locale} + /> + ), + }, + ]; + + return ( + } + > +
    +
    {i18nText('skillsInfo', locale)}
    +
    +
    + + +
    +
    + +
    +
    +
    + ); +}; diff --git a/src/components/Modals/EditExpertPayDataModal.tsx b/src/components/Modals/EditExpertPayDataModal.tsx new file mode 100644 index 0000000..be226e7 --- /dev/null +++ b/src/components/Modals/EditExpertPayDataModal.tsx @@ -0,0 +1,118 @@ +'use client'; + +import React, { FC, useEffect, useState } from 'react'; +import { Modal, Button, message, Form } from 'antd'; +import { CloseOutlined } from '@ant-design/icons'; +import { i18nText } from '../../i18nKeys'; +import { PayInfo } from '../../types/profile'; +import { AUTH_TOKEN_KEY } from '../../constants/common'; +import { useLocalStorage } from '../../hooks/useLocalStorage'; +import { setPayData } from '../../actions/profile'; +import { CustomInput } from '../view/CustomInput'; + +type EditExpertPayDataModalProps = { + open: boolean; + handleCancel: () => void; + locale: string; + data?: PayInfo; + refresh: () => void; +}; + +export const EditExpertPayDataModal: FC = ({ + open, + handleCancel, + locale, + data, + refresh +}) => { + const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, ''); + const [loading, setLoading] = useState(false); + const [form] = Form.useForm(); + + useEffect(() => { + if (open) { + if (data) { + form.setFieldsValue(data); + } else { + form.resetFields(); + } + } + }, [open, data]); + + const onSavePayData = () => { + form.validateFields().then(({ beneficiaryName, bicOrSwift, iban }) => { + setLoading(true); + setPayData(locale, jwt, { beneficiaryName, bicOrSwift, iban }) + .then(() => { + handleCancel(); + refresh(); + }) + .catch(() => { + message.error('Не удалось сохранить платежную информацию'); + }) + .finally(() => { + setLoading(false); + }) + }) + }; + + return ( + } + > +
    +
    {i18nText('payInfo', locale)}
    +
    +
    + + + + + + + + + +
    +
    +
    + +
    +
    +
    + ); +}; diff --git a/src/components/Modals/EditExpertTagsModal.tsx b/src/components/Modals/EditExpertTagsModal.tsx index 9b1677a..2d56b98 100644 --- a/src/components/Modals/EditExpertTagsModal.tsx +++ b/src/components/Modals/EditExpertTagsModal.tsx @@ -69,7 +69,7 @@ export const EditExpertTagsModal: FC = ({ closeIcon={} >
    -
    {i18nText('direction', locale)}
    +
    {i18nText('selectTopic', locale)}
    {data?.themesGroups && data.themesGroups.filter(({ isActive }) => isActive).map(({ id, name }) => (
    diff --git a/src/components/Modals/educationModalContent/Certificates.tsx b/src/components/Modals/educationModalContent/Certificates.tsx new file mode 100644 index 0000000..b0acdc1 --- /dev/null +++ b/src/components/Modals/educationModalContent/Certificates.tsx @@ -0,0 +1,205 @@ +import { Upload, UploadFile } from 'antd'; +import { DeleteOutlined } from '@ant-design/icons'; +import { Association, AssociationLevel, Certificate } from '../../../types/education'; +import { CustomSelect } from '../../view/CustomSelect'; +import { LinkButton } from '../../view/LinkButton'; +import { OutlinedButton } from '../../view/OutlinedButton'; +import { i18nText } from '../../../i18nKeys'; +import { validateDoc } from '../../../utils/account'; +import { useLocalStorage } from '../../../hooks/useLocalStorage'; +import { AUTH_TOKEN_KEY } from '../../../constants/common'; + +type CertificatesContentProps = { + certificates?: Certificate[]; + update: (cert?: Certificate[]) => void; + associations?: Association[]; + associationLevels?: AssociationLevel[]; + locale: string; +}; + +export const CertificatesContent = ({ + certificates, + update, + associations, + associationLevels, + locale +}: CertificatesContentProps) => { + const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, ''); + + const addCertificate = () => { + const cert = { + associationLevelId: undefined, + document: null + }; + + update(certificates?.length > 0 + ? [ + ...certificates, + cert + ] + : [cert]); + }; + + const deleteCertificate = (index: number) => { + update([...certificates].filter((cert, i) => i !== index)); + }; + + const beforeUpload = (file: UploadFile) => { + const isValid = validateDoc(file); + + if (!isValid) { + return Upload.LIST_IGNORE; + } + + return true; + } + + const onRemoveFile = (index: number) => { + update(certificates?.map((cert, i) => { + if (i === index) { + return { + ...cert, + document: null, + } + } + + return cert; + })); + }; + + const onChangeAssociation = (val: number, index: number) => { + update(certificates?.map((cert, i) => { + if (i === index) { + return { + ...cert, + associationId: val, + associationLevelId: undefined + } + } + + return cert; + })); + }; + + const onChangeLevel = (val: number, index: number) => { + update(certificates?.map((cert, i) => { + if (i === index) { + return { + ...cert, + associationLevelId: val + } + } + + return cert; + })); + }; + + const onChange = (file: any, index: number) => { + if (file?.response) { + update([...certificates].map((cert, i) => { + if (i === index) { + return { + ...cert, + document: file?.response || null, + } + } + + return cert; + })); + } + }; + + return ( +
    +
    + {certificates?.map(({ associationId, associationLevelId, document: file }, index) => { + let cAssociationId = associationId; + + if (!cAssociationId) { + const [cAssLvl] = associationLevels ? associationLevels.filter(({ id }) => id === associationLevelId) : []; + + if (cAssLvl?.associationId) { + cAssociationId = associations ? associations.filter(({ id }) => id === cAssLvl.associationId)[0]?.id : undefined; + } + } + + return ( +
    +
    + ({ value: id, label: name })) || []} + onChange={(val) => onChangeAssociation(val, index)} + style={{ maxWidth: 320, minWidth: 320 }} + /> + 0 + ? associationLevels + .filter(({ associationId }) => associationId === cAssociationId) + .map(({ id, name }) => ({ value: id, label: name })) + : []} + onChange={(val) => onChangeLevel(val, index)} + /> + {/* beforeUpload(file as UploadFile, index)} + multiple={false} + onRemove={() => onRemoveFile(index)} + > + {i18nText('addDiploma', locale)} + */} + onRemoveFile(index)} + action="https://api.bbuddy.expert/api/home/uploadfile" + method="POST" + headers={{ + authorization: `Bearer ${jwt}`, + 'X-User-Language': locale, + 'X-Referrer-Channel': 'site', + }} + onChange={(obj) => onChange(obj.file, index)} + > + {i18nText('addDiploma', locale)} + +
    + } + onClick={() => deleteCertificate(index)} + /> +
    + ) + })} +
    + + {i18nText('addNew', locale)} + +
    + ); +}; diff --git a/src/components/Modals/educationModalContent/Educations.tsx b/src/components/Modals/educationModalContent/Educations.tsx new file mode 100644 index 0000000..33bae54 --- /dev/null +++ b/src/components/Modals/educationModalContent/Educations.tsx @@ -0,0 +1,166 @@ +import { DeleteOutlined } from '@ant-design/icons'; +import { CustomInput } from '../../view/CustomInput'; +import { LinkButton } from '../../view/LinkButton'; +import { OutlinedButton } from '../../view/OutlinedButton'; +import { Details } from '../../../types/education'; +import { i18nText } from '../../../i18nKeys'; +import { Upload, UploadFile } from 'antd'; +import { validateDoc } from '../../../utils/account'; +import { useLocalStorage } from '../../../hooks/useLocalStorage'; +import { AUTH_TOKEN_KEY } from '../../../constants/common'; + +type EducationsContentProps = { + educations?: Details[]; + update: (edu?: Details[]) => void; + locale: string; +}; + +export const EducationsContent = ({ + educations, + update, + locale +}: EducationsContentProps) => { + const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, ''); + + const addEdu = () => { + const edu = { + title: undefined, + description: undefined, + document: null + }; + + update(educations?.length > 0 + ? [ + ...educations, + edu + ] + : [edu]); + }; + + const deleteEdu = (index: number) => { + update([...educations].filter((ed, i) => i !== index)); + }; + + const beforeUpload = (file: UploadFile) => { + const isValid = validateDoc(file); + + if (!isValid) { + return Upload.LIST_IGNORE; + } + + return true; + } + + const onRemoveFile = (index: number) => { + update(educations?.map((edu, i) => { + if (i === index) { + return { + ...edu, + document: null, + } + } + + return edu; + })); + }; + + const onChange = (file: any, index: number) => { + if (file?.response) { + update([...educations].map((edu, i) => { + if (i === index) { + return { + ...edu, + document: file?.response || null, + } + } + + return edu; + })); + } + }; + + const onChangeUniversity = (val: string, index: number) => { + update(educations?.map((edu, i) => { + if (i === index) { + return { + ...edu, + title: val, + } + } + + return edu; + })); + }; + + const onChangeDesc = (val: string, index: number) => { + update(educations?.map((edu, i) => { + if (i === index) { + return { + ...edu, + description: val, + } + } + + return edu; + })); + }; + + return ( +
    +
    + {educations?.map(({ title, description, document: file}, index) => ( +
    +
    + onChangeUniversity(e?.target?.value, index)} + /> + onChangeDesc(e?.target?.value, index)} + /> + onRemoveFile(index)} + action="https://api.bbuddy.expert/api/home/uploadfile" + method="POST" + headers={{ + authorization: `Bearer ${jwt}`, + 'X-User-Language': locale, + 'X-Referrer-Channel': 'site', + }} + onChange={(obj) => onChange(obj.file, index)} + > + {i18nText('addDiploma', locale)} + +
    + } + onClick={() => deleteEdu(index)} + /> +
    + ))} +
    + + {i18nText('addNew', locale)} + +
    + ); +}; diff --git a/src/components/Modals/educationModalContent/Experiences.tsx b/src/components/Modals/educationModalContent/Experiences.tsx new file mode 100644 index 0000000..4a71b38 --- /dev/null +++ b/src/components/Modals/educationModalContent/Experiences.tsx @@ -0,0 +1,101 @@ +import { DeleteOutlined } from '@ant-design/icons'; +import { CustomInput } from '../../view/CustomInput'; +import { LinkButton } from '../../view/LinkButton'; +import { OutlinedButton } from '../../view/OutlinedButton'; +import { Experience } from '../../../types/education'; +import { i18nText } from '../../../i18nKeys'; +import {useLocalStorage} from "../../../hooks/useLocalStorage"; +import {AUTH_TOKEN_KEY} from "../../../constants/common"; + +type ExperiencesContentProps = { + experiences?: Experience[]; + update: (ex?: Experience[]) => void; + locale: string; +}; + +export const ExperiencesContent = ({ + experiences, + update, + locale +}: ExperiencesContentProps) => { + const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, ''); + + const addExperience = () => { + const ex = { + title: undefined, + description: undefined, + }; + + update(experiences?.length > 0 + ? [ + ...experiences, + ex + ] + : [ex]); + }; + + const deleteExperience = (index: number) => { + update([...experiences].filter((ex, i) => i !== index)); + }; + + const onChangeName = (val: string, index: number) => { + update(experiences?.map((ex, i) => { + if (i === index) { + return { + ...ex, + title: val, + } + } + + return ex; + })); + }; + + const onChangeDesc = (val: string, index: number) => { + update(experiences?.map((ex, i) => { + if (i === index) { + return { + ...ex, + description: val, + } + } + + return ex; + })); + }; + + return ( +
    +
    + {experiences?.map(({ title, description}, index) => ( +
    +
    + onChangeName(e?.target?.value, index)} + /> + onChangeDesc(e?.target?.value, index)} + /> +
    + } + onClick={() => deleteExperience(index)} + /> +
    + ))} +
    + + {i18nText('addNew', locale)} + +
    + ); +}; diff --git a/src/components/Modals/educationModalContent/Mbas.tsx b/src/components/Modals/educationModalContent/Mbas.tsx new file mode 100644 index 0000000..d119781 --- /dev/null +++ b/src/components/Modals/educationModalContent/Mbas.tsx @@ -0,0 +1,166 @@ +import { DeleteOutlined } from '@ant-design/icons'; +import { CustomInput } from '../../view/CustomInput'; +import { LinkButton } from '../../view/LinkButton'; +import { OutlinedButton } from '../../view/OutlinedButton'; +import { Details } from '../../../types/education'; +import { i18nText } from '../../../i18nKeys'; +import { Upload, UploadFile } from 'antd'; +import { validateDoc } from '../../../utils/account'; +import { useLocalStorage } from '../../../hooks/useLocalStorage'; +import { AUTH_TOKEN_KEY } from '../../../constants/common'; + +type MbasContentProps = { + mbas?: Details[]; + update: (mba?: Details[]) => void; + locale: string; +}; + +export const MbasContent = ({ + mbas, + update, + locale +}: MbasContentProps) => { + const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, ''); + + const addMba = () => { + const mba = { + title: undefined, + description: undefined, + document: null + }; + + update(mbas?.length > 0 + ? [ + ...mbas, + mba + ] + : [mba]); + }; + + const deleteMba = (index: number) => { + update([...mbas].filter((mba, i) => i !== index)); + }; + + const beforeUpload = (file: UploadFile) => { + const isValid = validateDoc(file); + + if (!isValid) { + return Upload.LIST_IGNORE; + } + + return true; + } + + const onRemoveFile = (index: number) => { + update(mbas?.map((mb, i) => { + if (i === index) { + return { + ...mb, + document: null, + } + } + + return mb; + })); + }; + + const onChange = (file: any, index: number) => { + if (file?.response) { + update([...mbas].map((mb, i) => { + if (i === index) { + return { + ...mb, + document: file?.response || null, + } + } + + return mb; + })); + } + }; + + const onChangeName = (val: string, index: number) => { + update(mbas?.map((mb, i) => { + if (i === index) { + return { + ...mb, + title: val, + } + } + + return mb; + })); + }; + + const onChangeDesc = (val: string, index: number) => { + update(mbas?.map((mb, i) => { + if (i === index) { + return { + ...mb, + description: val, + } + } + + return mb; + })); + }; + + return ( +
    +
    + {mbas?.map(({ title, description, document: file}, index) => ( +
    +
    + onChangeName(e?.target?.value, index)} + /> + onChangeDesc(e?.target?.value, index)} + /> + onRemoveFile(index)} + action="https://api.bbuddy.expert/api/home/uploadfile" + method="POST" + headers={{ + authorization: `Bearer ${jwt}`, + 'X-User-Language': locale, + 'X-Referrer-Channel': 'site', + }} + onChange={(obj) => onChange(obj.file, index)} + > + {i18nText('addDiploma', locale)} + +
    + } + onClick={() => deleteMba(index)} + /> +
    + ))} +
    + + {i18nText('addNew', locale)} + +
    + ); +}; diff --git a/src/components/Modals/educationModalContent/Trainings.tsx b/src/components/Modals/educationModalContent/Trainings.tsx new file mode 100644 index 0000000..9555784 --- /dev/null +++ b/src/components/Modals/educationModalContent/Trainings.tsx @@ -0,0 +1,166 @@ +import { DeleteOutlined } from '@ant-design/icons'; +import { CustomInput } from '../../view/CustomInput'; +import { LinkButton } from '../../view/LinkButton'; +import { OutlinedButton } from '../../view/OutlinedButton'; +import { Details } from '../../../types/education'; +import { i18nText } from '../../../i18nKeys'; +import { Upload, UploadFile } from 'antd'; +import { validateDoc } from '../../../utils/account'; +import { useLocalStorage } from '../../../hooks/useLocalStorage'; +import { AUTH_TOKEN_KEY } from '../../../constants/common'; + +type TrainingsContentProps = { + trainings?: Details[]; + update: (tr?: Details[]) => void; + locale: string; +}; + +export const TrainingsContent = ({ + trainings, + update, + locale +}: TrainingsContentProps) => { + const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, ''); + + const addTrainings = () => { + const training = { + title: undefined, + description: undefined, + document: null + }; + + update(trainings?.length > 0 + ? [ + ...trainings, + training + ] + : [training]); + }; + + const deleteTrainings = (index: number) => { + update([...trainings].filter((tr, i) => i !== index)); + }; + + const beforeUpload = (file: UploadFile) => { + const isValid = validateDoc(file); + + if (!isValid) { + return Upload.LIST_IGNORE; + } + + return true; + } + + const onRemoveFile = (index: number) => { + update(trainings?.map((tr, i) => { + if (i === index) { + return { + ...tr, + document: null, + } + } + + return tr; + })); + }; + + const onChange = (file: any, index: number) => { + if (file?.response) { + update([...trainings].map((tr, i) => { + if (i === index) { + return { + ...tr, + document: file?.response || null, + } + } + + return tr; + })); + } + }; + + const onChangeName = (val: string, index: number) => { + update(trainings?.map((tr, i) => { + if (i === index) { + return { + ...tr, + title: val, + } + } + + return tr; + })); + }; + + const onChangeDesc = (val: string, index: number) => { + update(trainings?.map((tr, i) => { + if (i === index) { + return { + ...tr, + description: val, + } + } + + return tr; + })); + }; + + return ( +
    +
    + {trainings?.map(({ title, description, document: file}, index) => ( +
    +
    + onChangeName(e?.target?.value, index)} + /> + onChangeDesc(e?.target?.value, index)} + /> + onRemoveFile(index)} + action="https://api.bbuddy.expert/api/home/uploadfile" + method="POST" + headers={{ + authorization: `Bearer ${jwt}`, + 'X-User-Language': locale, + 'X-Referrer-Channel': 'site', + }} + onChange={(obj) => onChange(obj.file, index)} + > + {i18nText('addDiploma', locale)} + +
    + } + onClick={() => deleteTrainings(index)} + /> +
    + ))} +
    + + {i18nText('addNew', locale)} + +
    + ); +}; diff --git a/src/components/view/FilledButton.tsx b/src/components/view/FilledButton.tsx index 3b875b6..f74a21f 100644 --- a/src/components/view/FilledButton.tsx +++ b/src/components/view/FilledButton.tsx @@ -12,3 +12,9 @@ export const FilledYellowButton = (props: any) => ( {props.children} ); + +export const FilledSquareButton = (props: any) => ( + +); diff --git a/src/components/view/LinkButton.tsx b/src/components/view/LinkButton.tsx index 6e06b0a..0a63f92 100644 --- a/src/components/view/LinkButton.tsx +++ b/src/components/view/LinkButton.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { Button } from 'antd'; export const LinkButton = (props: any) => ( - ); diff --git a/src/i18nKeys/de.ts b/src/i18nKeys/de.ts index f27812b..a12d82f 100644 --- a/src/i18nKeys/de.ts +++ b/src/i18nKeys/de.ts @@ -105,6 +105,7 @@ export default { signUp: 'Jetzt anmelden', noData: 'Keine Daten', notFound: 'Nicht gefunden', + skillsInfo: 'Fähigkeiten-Infos', trainings: 'Trainings', seminars: 'Seminare', courses: 'Kurse', @@ -112,7 +113,38 @@ export default { aboutCoach: 'Über Coach', education: 'Bildung', coaching: 'Coaching', - + experiences: 'Praktische Erfahrung', + payInfo: 'Zahlungsdaten', + sessionDuration: 'Sitzungsdauer', + experienceHours: 'Gesamtstunden praktischer Erfahrung', + topics: 'Themen', + selectTopic: 'Thema auswählen', + title: 'Titel', + description: 'Beschreibung', + sessionCost: 'Sitzungskosten in Euro', + yourTimezone: 'Deine Zeitzone', + workTime: 'Arbeitszeit', + startAt: 'Beginn um', + finishAt: 'Ende um', + addWorkingHours: 'Arbeitszeiten hinzufügen', + specialisation: 'Spezialisierung', + selectSpecialisation: 'Wählen Sie Ihre Spezialisierung, um fortzufahren', + fillWeeklySchedule: 'Trage Sachen in deinen Wochenplan ein', + beneficiaryName: 'Name des Empfängers', + bicOrSwift: 'BIC/Swift-Code', + association: 'Verband', + level: 'Stufe', + addDiploma: 'Zertifikat hinzufügen', + university: 'Institution', + sunday: 'So', + monday: 'Mo', + tuesday: 'Di', + wednesday: 'Mi', + thursday: 'Do', + friday: 'Fr', + saturday: 'Sa', + addNew: 'Neu hinzufügen', + mExperiences: 'Führungserfahrung', errors: { invalidEmail: 'Die E-Mail-Adresse ist ungültig', emptyEmail: 'Bitte geben Sie Ihre E-Mail ein', diff --git a/src/i18nKeys/en.ts b/src/i18nKeys/en.ts index 9857844..117a2b7 100644 --- a/src/i18nKeys/en.ts +++ b/src/i18nKeys/en.ts @@ -110,8 +110,41 @@ export default { courses: 'Courses', mba: 'MBA Information', aboutCoach: 'About Coach', + skillsInfo: 'Skills Info', education: 'Education', coaching: 'Coaching', + experiences: 'Practical experience', + payInfo: 'Payment Info', + sessionDuration: 'Session duration', + experienceHours: 'Total hours of practical experience', + topics: 'Topics', + selectTopic: 'Select Topic', + title: 'Title', + description: 'Description', + sessionCost: 'Session cost in euro', + yourTimezone: 'Your timezone', + workTime: 'Work time', + startAt: 'Start at', + finishAt: 'Finish at', + addWorkingHours: 'Add working hours', + specialisation: 'Specialisation', + selectSpecialisation: 'Select your specialisation to proceed', + fillWeeklySchedule: 'Fill up your weekly schedule', + beneficiaryName: 'Beneficiary Name', + bicOrSwift: 'BIC/Swift code', + association: 'Association', + level: 'Level', + addDiploma: 'Add Diploma', + university: 'Institution', + sunday: 'Su', + monday: 'Mo', + tuesday: 'Tu', + wednesday: 'We', + thursday: 'Th', + friday: 'Fr', + saturday: 'Sa', + addNew: 'Add New', + mExperiences: 'Managerial Experience', errors: { invalidEmail: 'The email address is not valid', emptyEmail: 'Please enter your E-mail', diff --git a/src/i18nKeys/es.ts b/src/i18nKeys/es.ts index f94d8a5..5f4a314 100644 --- a/src/i18nKeys/es.ts +++ b/src/i18nKeys/es.ts @@ -105,6 +105,7 @@ export default { signUp: 'Regístrate ahora', noData: 'Sin datos', notFound: 'No encontrado', + skillsInfo: 'Información', trainings: 'Formación', seminars: 'Seminarios', courses: 'Cursos', @@ -112,7 +113,38 @@ export default { aboutCoach: 'Sobre el coach', education: 'Educación', coaching: 'Coaching', - + experiences: 'Experiencia práctica', + payInfo: 'Información de pago', + sessionDuration: 'Duración de la sesión', + experienceHours: 'Total de horas de experiencia práctica', + topics: 'Temas', + selectTopic: 'Seleccione el tema', + title: 'Título', + description: 'Descripción', + sessionCost: 'Coste de la sesión en euros', + yourTimezone: 'Tu zona horaria', + workTime: 'Tiempo de trabajo', + startAt: 'Empieza a las', + finishAt: 'Termina a las', + addWorkingHours: 'Añadir horas de trabajo', + specialisation: 'Especialización', + selectSpecialisation: 'Selecciona tu especialización para continuar', + fillWeeklySchedule: 'Rellena tu agenda semanal', + beneficiaryName: 'Nombre del beneficiario', + bicOrSwift: 'Código BIC/Swift', + association: 'Asociación', + level: 'Nivel', + addDiploma: 'Añadir diploma', + university: 'Institución', + sunday: 'D', + monday: 'L', + tuesday: 'M', + wednesday: 'X', + thursday: 'J', + friday: 'V', + saturday: 'S', + addNew: 'Añadir nuevo', + mExperiences: 'Experiencia de dirección', errors: { invalidEmail: 'La dirección de correo electrónico no es válida', emptyEmail: 'Introduce tu correo electrónico', diff --git a/src/i18nKeys/fr.ts b/src/i18nKeys/fr.ts index 7978155..3502aef 100644 --- a/src/i18nKeys/fr.ts +++ b/src/i18nKeys/fr.ts @@ -105,6 +105,7 @@ export default { signUp: 'Inscrivez-vous maintenant', noData: 'Aucune donnée', notFound: 'Non trouvé', + skillsInfo: 'Infos sur les compétences', trainings: 'Formations', seminars: 'Séminaires', courses: 'Cours', @@ -112,7 +113,38 @@ export default { aboutCoach: 'À propos du coach', education: 'Éducation', coaching: 'Coaching', - + experiences: 'Expérience pratique', + payInfo: 'Infos sur le paiement', + sessionDuration: 'Durée de la session', + experienceHours: 'Heures totales d\'expérience pratique', + topics: 'Sujets', + selectTopic: 'Sélectionnez un sujet', + title: 'Titre', + description: 'Description', + sessionCost: 'Coût de la session en euros', + yourTimezone: 'Votre fuseau horaire', + workTime: 'Heures de travail', + startAt: 'Commencer à', + finishAt: 'Finir à', + addWorkingHours: 'Ajouter des heures de travail', + specialisation: 'Spécialisation', + selectSpecialisation: 'Sélectionnez votre spécialisation pour continuer', + fillWeeklySchedule: 'Remplissez votre emploi du temps hebdomadaire', + beneficiaryName: 'Nom du bénéficiaire', + bicOrSwift: 'Code BIC/Swift', + association: 'Association', + level: 'Niveau', + addDiploma: 'Ajouter un diplôme', + university: 'Institution', + sunday: 'Di', + monday: 'Lu', + tuesday: 'Ma', + wednesday: 'Me', + thursday: 'Je', + friday: 'Ve', + saturday: 'Sa', + addNew: 'Ajouter un nouveau', + mExperiences: 'Expérience en gestion', errors: { invalidEmail: 'L\'adresse e-mail n\'est pas valide', emptyEmail: 'Veuillez saisir votre e-mail', diff --git a/src/i18nKeys/it.ts b/src/i18nKeys/it.ts index 5520c2a..8a3743a 100644 --- a/src/i18nKeys/it.ts +++ b/src/i18nKeys/it.ts @@ -105,6 +105,7 @@ export default { signUp: 'Iscriviti ora', noData: 'Nessun dato', notFound: 'Non trovato', + skillsInfo: 'Info su competenze', trainings: 'Training', seminars: 'Seminari', courses: 'Corsi', @@ -112,7 +113,38 @@ export default { aboutCoach: 'Informazioni sul coach', education: 'Istruzione', coaching: 'Coaching', - + experiences: 'Esperienza pratica', + payInfo: 'Info pagamento', + sessionDuration: 'Durata della sessione', + experienceHours: 'Totale ore di esperienza pratica', + topics: 'Argomenti', + selectTopic: 'Seleziona l\'argomento', + title: 'Titolo', + description: 'Descrizione', + sessionCost: 'Costo della sessione in euro', + yourTimezone: 'Il tuo fuso orario', + workTime: 'Orario di lavoro', + startAt: 'Inizia alle', + finishAt: 'Termina alle', + addWorkingHours: 'Aggiungi ore lavorative', + specialisation: 'Specializzazione', + selectSpecialisation: 'Seleziona la tua specializzazione per continuare', + fillWeeklySchedule: 'Compila la tua agenda settimanale', + beneficiaryName: 'Nome del beneficiario', + bicOrSwift: 'BIC/codice Swift', + association: 'Associazione', + level: 'Livello', + addDiploma: 'Aggiungi diploma', + university: 'Istituto', + sunday: 'Do', + monday: 'Lu', + tuesday: 'Ma', + wednesday: 'Me', + thursday: 'Gi', + friday: 'Ve', + saturday: 'Sa', + addNew: 'Aggiungi nuovo', + mExperiences: 'Esperienza manageriale', errors: { invalidEmail: 'L\'indirizzo e-mail non è valido', emptyEmail: 'Inserisci l\'e-mail', diff --git a/src/i18nKeys/ru.ts b/src/i18nKeys/ru.ts index aa99952..de2a14b 100644 --- a/src/i18nKeys/ru.ts +++ b/src/i18nKeys/ru.ts @@ -98,21 +98,53 @@ export default { expertBackground: 'Профессиональный опыт эксперта', profCertification: 'Профессиональная сертификация', practiceHours: 'Часов практики', - supervisionCount: 'Часов супервизии в год', + supervisionCount: 'Супервизий в год', outOf: 'из', schedule: 'Расписание', successfulCase: 'Успешные случаи из практики', signUp: 'Записаться сейчас', noData: 'Нет данных', notFound: 'Не найдено', + skillsInfo: 'Навыки', trainings: 'Тренинги', seminars: 'Семинары', courses: 'Курсы', mba: 'Информация о MBA', + experiences: 'Практический опыт', aboutCoach: 'О коуче', education: 'Образование', coaching: 'Коучинг', - + payInfo: 'Платежная информация', + sessionDuration: 'Продолжительность сессии', + experienceHours: 'Общее количество часов практического опыта', + topics: 'Темы', + selectTopic: 'Выберите тему', + title: 'Название', + description: 'Описание', + sessionCost: 'Стоимость сессии в евро', + yourTimezone: 'Ваш часовой пояс', + workTime: 'Рабочее время', + startAt: 'Начало в', + finishAt: 'Завершение в', + addWorkingHours: 'Добавить рабочие часы', + specialisation: 'Специализация', + selectSpecialisation: 'Выберите свою специализацию для продолжения', + fillWeeklySchedule: 'Заполните свое недельное расписание', + beneficiaryName: 'Имя получателя', + bicOrSwift: 'BIC/Swift код', + association: 'Ассоциация', + level: 'Уровень', + addDiploma: 'Добавить диплом', + university: 'ВУЗ', + sunday: 'Вс', + monday: 'Пн', + tuesday: 'Вт', + wednesday: 'Ср', + thursday: 'Чт', + friday: 'Пт', + saturday: 'Сб', + addNew: 'Добавить', + mExperiences: 'Управленческий опыт', errors: { invalidEmail: 'Адрес электронной почты недействителен', emptyEmail: 'Пожалуйста, введите ваш E-mail', diff --git a/src/styles/_edu.scss b/src/styles/_edu.scss new file mode 100644 index 0000000..b3e65a0 --- /dev/null +++ b/src/styles/_edu.scss @@ -0,0 +1,35 @@ +.b-edu { + &-content { + display: flex; + flex-direction: column; + gap: 16px; + } + + &-list { + display: flex; + flex-direction: column; + gap: 12px; + width: 100%; + + &__item { + padding-top: 12px; + border-top: 1px solid #C4DFE6; + display: flex; + gap: 8px; + justify-content: space-between; + align-items: flex-start; + + &:first-child { + padding-top: 0; + border-top: none; + } + + & > div { + flex: 1; + display: flex; + flex-direction: column; + gap: 12px; + } + } + } +} diff --git a/src/styles/_modal.scss b/src/styles/_modal.scss index bf5423e..05be32d 100644 --- a/src/styles/_modal.scss +++ b/src/styles/_modal.scss @@ -51,7 +51,8 @@ } &__inner { - height: 60vh; + height: auto; + max-height: 60vh; overflow-y: auto; & > div { @@ -86,3 +87,13 @@ .ant-modal-mask { background-color: rgba(0, 59, 70, 0.4) !important; } + +.ant-upload-list-item-name { + max-width: 280px; +} + +.ant-upload-list-item { + &:hover { + background-color: transparent !important; + } +} diff --git a/src/styles/_pages.scss b/src/styles/_pages.scss index 49ceb51..394a403 100644 --- a/src/styles/_pages.scss +++ b/src/styles/_pages.scss @@ -1188,7 +1188,6 @@ height: 86px; border-radius: 16px; border: 2px solid #FFF; - background: lightgray 50%; box-shadow: 0 8px 16px 0 rgba(102, 165, 173, 0.32); overflow: hidden; @@ -1210,6 +1209,7 @@ @include rem(18); font-weight: 600; line-height: 150%; + margin-bottom: 16px; } } @@ -1233,6 +1233,61 @@ } } + &__info { + display: flex; + flex-direction: column; + gap: 8px; + + .title-h3 { + color: #003B46; + @include rem(16); + line-height: 18px; + } + + .case-list { + margin-top: 8px; + + p { + margin-top: 8px; + } + } + } + + &__practice { + color: #2C7873; + @include rem(16); + line-height: 18px; + } + + &__lang { + display: flex; + flex-direction: column; + gap: 8px; + + & > div { + color: #003B46; + @include rem(16); + line-height: 18px; + } + } + + &__list { + display: flex; + flex-direction: column; + gap: 8px; + + & > div { + display: flex; + gap: 12px; + } + } + + &__item { + color: #003B46; + @include rem(16); + line-height: 18px; + } + .title-h2 { color: #003B46; @include rem(18); @@ -1243,7 +1298,7 @@ margin-bottom: 0; } - &__desc { + &__desc, &__desc > div { border-radius: 16px; background: #EFFCFF; padding: 16px; @@ -1443,8 +1498,17 @@ top: 50%; transform: translateY(-50%); } - } } - +} + +.pay-data-list { + display: flex; + flex-direction: column; + gap: 8px; + + & > div { + display: flex; + gap: 16px; + } } diff --git a/src/styles/_schedule.scss b/src/styles/_schedule.scss new file mode 100644 index 0000000..4a3b9ec --- /dev/null +++ b/src/styles/_schedule.scss @@ -0,0 +1,15 @@ +.b-schedule-list { + display: flex; + flex-direction: column; + gap: 12px; + color: #003B46; + @include rem(16); + font-style: normal; + font-weight: 500; + line-height: 150%; + + & > div { + display: flex; + gap: 8px; + } +} diff --git a/src/styles/style.scss b/src/styles/style.scss index d00dbd2..cd8ed3f 100644 --- a/src/styles/style.scss +++ b/src/styles/style.scss @@ -15,6 +15,8 @@ @import "_message.scss"; @import "_auth-modal.scss"; @import "_modal.scss"; +@import "_edu.scss"; +@import "_schedule.scss"; @import "./view/style.scss"; @import "./sessions/style.scss"; diff --git a/src/styles/view/_buttons.scss b/src/styles/view/_buttons.scss index dfd9b8a..e5fc938 100644 --- a/src/styles/view/_buttons.scss +++ b/src/styles/view/_buttons.scss @@ -17,6 +17,19 @@ padding: 4px 24px !important; } + &_square { + width: 42px !important; + height: 42px !important; + background: #66A5AD !important; + font-size: 15px !important; + border-radius: 8px !important; + box-shadow: 0px 2px 4px 0px rgba(102, 165, 173, 0.32) !important; + position: absolute !important; + right: -8px !important; + bottom: -8px !important; + cursor: pointer; + } + &.danger { background: #D93E5C !important; box-shadow: none !important; @@ -28,6 +41,10 @@ font-size: 15px !important; height: auto !important; padding: 0 !important; + + &.danger { + color: #D93E5C !important; + } } &__outlined { diff --git a/src/styles/view/_collapse.scss b/src/styles/view/_collapse.scss new file mode 100644 index 0000000..491075c --- /dev/null +++ b/src/styles/view/_collapse.scss @@ -0,0 +1,15 @@ +.ant-collapse-header { + padding: 12px 0 !important; +} + +.ant-collapse-header-text { + color: #003B46; + @include rem(16); + font-style: normal; + font-weight: 600; + line-height: 133.333%; +} + +.ant-collapse-content-box { + padding: 12px 0 !important; +} diff --git a/src/styles/view/_input.scss b/src/styles/view/_input.scss index 3f6149d..392cbbf 100644 --- a/src/styles/view/_input.scss +++ b/src/styles/view/_input.scss @@ -8,6 +8,14 @@ input { background-color: transparent !important; + border-color: transparent !important; + box-shadow: none !important; + } + + .ant-input-group-addon { + background-color: transparent !important; + border-color: transparent !important; + box-shadow: none !important; } &:focus, &:hover, &:focus-within { diff --git a/src/styles/view/_practice.scss b/src/styles/view/_practice.scss new file mode 100644 index 0000000..921d855 --- /dev/null +++ b/src/styles/view/_practice.scss @@ -0,0 +1,29 @@ +.b-practice { + &-cases { + display: flex; + flex-direction: column; + gap: 16px; + } + + &-case { + &__header { + display: flex; + justify-content: space-between; + align-items: center; + } + + &__item { + display: flex; + justify-content: space-between; + gap: 8px; + align-items: flex-start; + } + + &__content { + display: flex; + flex-direction: column; + gap: 16px; + flex: 1; + } + } +} diff --git a/src/styles/view/style.scss b/src/styles/view/style.scss index 27a5921..42b0936 100644 --- a/src/styles/view/style.scss +++ b/src/styles/view/style.scss @@ -6,3 +6,5 @@ @import "_spin.scss"; @import "_switch.scss"; @import "_buttons.scss"; +@import "_practice.scss"; +@import "_collapse.scss"; diff --git a/src/types/education.ts b/src/types/education.ts index aa41a70..7cf99f7 100644 --- a/src/types/education.ts +++ b/src/types/education.ts @@ -1,22 +1,23 @@ import { ExpertDocument } from './file'; export type Details = { - id: number; - userId?:number; + id?: number; + userId?: number; title?: string; description?: string; - document?: ExpertDocument; + document?: ExpertDocument | null; }; export type Certificate = { - id: number; + id?: number; userId?: number; associationLevelId?: number; - document?: ExpertDocument; + associationId?: number; + document?: ExpertDocument | null; }; export type Experience = { - id: number, + id?: number, userId?: number, title?: string, description?: string @@ -24,10 +25,10 @@ export type Experience = { export type Association = { id: number; - name?: string; + name: string; }; -export type AssociationLevel = Association & { associationId?: number }; +export type AssociationLevel = Association & { associationId: number }; export type EducationData = { certificates?: Certificate[], diff --git a/src/types/practice.ts b/src/types/practice.ts index 438e125..1172a99 100644 --- a/src/types/practice.ts +++ b/src/types/practice.ts @@ -6,7 +6,7 @@ export type Supervision = { }; export type PracticeCase = { - id: number, + id?: number, userId?: number, description?: string, themesGroupIds?: number[] @@ -18,12 +18,14 @@ export type PracticeData = { sessionDuration?: number, sessionCost?: number, practiceCases?: PracticeCase[] -} +}; + +export type PracticePersonData = PracticeData & { + themesGroups?: ExpertsThemesGroups[], + supervisionPerYears?: Supervision[], + sessionCosts?: number[] +}; export interface PracticeDTO { - person4Data: PracticeData & { - themesGroups?: ExpertsThemesGroups[], - supervisionPerYears?: Supervision[], - sessionCosts?: number[] - } + person4Data: PracticePersonData } diff --git a/src/types/profile.ts b/src/types/profile.ts index 548398c..6da1047 100644 --- a/src/types/profile.ts +++ b/src/types/profile.ts @@ -15,7 +15,8 @@ export type ProfileData = { hasExternalLogin?: boolean; isTestMode?: boolean; phone?: string; - languagesLinks?: { language: { id: number, code: string, nativeSpelling: string }, languageId: number }[] + languagesLinks?: { language: { id: number, code: string, nativeSpelling: string }, languageId: number }[]; + allLanguages?: { id: number, code: string, nativeSpelling: string }[] } export type Profile = ProfileData & { id: number }; diff --git a/src/utils/account.ts b/src/utils/account.ts index e67e439..ca29021 100644 --- a/src/utils/account.ts +++ b/src/utils/account.ts @@ -28,3 +28,19 @@ export const validateImage = (file: UploadFile, showMessage?: boolean): boolean return isImage && isLt5M; }; + + +export const validateDoc = (file: UploadFile): boolean => { + const isDoc = file.type === 'image/jpg' || file.type === 'image/jpeg' + || file.type === 'image/png' || file.type === 'image/gif' || file.type === 'application/pdf'; + if (!isDoc) { + message.error('You can only upload JPG/PNG/PDF file'); + } + + const isLt5M = file.size / 1024 / 1024 <= 5; + if (!isLt5M) { + message.error('Image must smaller than 5MB'); + } + + return isDoc && isLt5M; +}; diff --git a/src/utils/time.ts b/src/utils/time.ts new file mode 100644 index 0000000..9657e3a --- /dev/null +++ b/src/utils/time.ts @@ -0,0 +1,62 @@ +import dayjs from 'dayjs'; +import { WorkingTime } from '../types/schedule'; + +const WEEK_DAY = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday']; +const MAX_DAY_TIME = 24 * 60; // min + +export const getCurrentTime = (data: WorkingTime) => { + let startDay = data.startDayOfWeekUtc; + let endDay = data.endDayOfWeekUtc; + const currentTimeZone = dayjs().format('Z'); + const startUtc = data.startTimeUtc / (1000 * 1000 * 60); + const endUtc = data.endTimeUtc / (1000 * 1000 * 60); + const matches = currentTimeZone.match(/(\+|-)([0-9]{2}):([0-9]{2})/); + const sign = matches[1]; + const h = matches[2]; + const m = matches[3]; + let startMin; + let endMin; + + if (sign === '+') { + startMin = startUtc + (Number(h) * 60) + Number(m); + endMin = endUtc + (Number(h) * 60) + Number(m); + // startMin = startUtc; + // endMin = endUtc; + } + + if (sign === '-') { + startMin = startUtc - (Number(h) * 60) - Number(m); + endMin = endUtc - (Number(h) * 60) - Number(m); + } + + if (startMin > MAX_DAY_TIME) { + startMin = startMin - MAX_DAY_TIME; + const ind = startDay ? WEEK_DAY.indexOf(startDay) + 1 : 0; + startDay = WEEK_DAY[ind >= WEEK_DAY.length ? 0 : ind]; + } + + if (endMin > MAX_DAY_TIME) { + endMin = endMin - MAX_DAY_TIME; + const ind = endDay ? WEEK_DAY.indexOf(endDay) + 1 : 0; + endDay = WEEK_DAY[ind >= WEEK_DAY.length ? 0 : ind]; + } + + if (startMin <= 0) { + startMin = MAX_DAY_TIME - startMin; + const ind = startDay ? WEEK_DAY.indexOf(startDay) - 1 : 0; + startDay = WEEK_DAY[ind < 0 ? WEEK_DAY.length - 1 : ind]; + } + + if (endMin <= 0) { + endMin = MAX_DAY_TIME - endMin; + const ind = endDay ? WEEK_DAY.indexOf(endDay) - 1 : 0; + endDay = WEEK_DAY[ind < 0 ? WEEK_DAY.length - 1 : ind]; + } + + return { + startDay, + startTime: `${(startMin - startMin % 60)/60}:${startMin % 60 || '00'}`, + endDay: endDay, + endTime: `${(endMin - endMin % 60)/60}:${endMin % 60 || '00'}` + } +};