Compare commits

..

9 Commits

Author SHA1 Message Date
dzfelix 28f5babf22 fix meta anf first page 2024-08-27 17:01:29 +03:00
dzfelix 80f53e871d Merge branch 'refs/heads/develop' into blog 2024-08-27 16:54:31 +03:00
dzfelix f7fe427aae fix lock 2024-08-22 18:15:36 +03:00
dzfelix 5844bd9e7c Merge branch 'refs/heads/blog' into develop 2024-08-22 17:28:23 +03:00
norton81 c563818e91 Merge remote-tracking branch 'origin/blog' into develop 2024-08-21 09:44:07 +03:00
norton81 1461c4948e Merge branch 'master' into develop 2024-08-20 13:25:46 +03:00
SD f92810d320 feat: add expert profile 2024-08-17 03:51:27 +04:00
SD 6f5c3738b7 Merge branch 'master' into develop 2024-08-17 03:50:01 +04:00
Dasha 526e703d9a Merge pull request 'blog' (#1) from blog into develop
Reviewed-on: #1
2024-08-16 23:49:20 +00:00
47 changed files with 2716 additions and 461 deletions

731
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,15 @@ import { message } from 'antd';
import { ExpertData } from '../../../../../types/profile'; import { ExpertData } from '../../../../../types/profile';
import { AUTH_TOKEN_KEY } from '../../../../../constants/common'; import { AUTH_TOKEN_KEY } from '../../../../../constants/common';
import { useLocalStorage } from '../../../../../hooks/useLocalStorage'; 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 { ExpertProfile } from '../../../../../components/ExpertProfile';
import { Loader } from '../../../../../components/view/Loader'; 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 [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const [data, setData] = useState<ExpertData | undefined>(); const [data, setData] = useState<ExpertData | undefined>();
const [isFull, setIsFull] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
if (jwt) { if (jwt) {
setLoading(true); setLoading(true);
Promise.all([ Promise.all([
getUserData(locale, jwt),
getPersonalData(locale, jwt), getPersonalData(locale, jwt),
getEducation(locale, jwt), getEducation(locale, jwt),
getTags(locale, jwt), getTags(locale, jwt),
@ -27,13 +37,12 @@ export default function ExpertProfilePage({ params: { locale } }: { params: { lo
getSchedule(locale, jwt), getSchedule(locale, jwt),
getPayData(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('person', person);
console.log('education', education); console.log('education', education);
console.log('tags', tags);
console.log('practice', practice);
console.log('schedule', schedule); console.log('schedule', schedule);
console.log('payData', payData); setIsFull(profile.fillProgress === 'full');
setData({ setData({
person, person,
education, education,
@ -56,6 +65,7 @@ export default function ExpertProfilePage({ params: { locale } }: { params: { lo
<Loader isLoading={loading}> <Loader isLoading={loading}>
{data && ( {data && (
<ExpertProfile <ExpertProfile
isFull={isFull}
locale={locale} locale={locale}
data={data} data={data}
updateData={setData} updateData={setData}

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import type { Metadata } from 'next'; import type {Metadata, ResolvingMetadata} from 'next';
import { draftMode } from 'next/headers' import { draftMode } from 'next/headers'
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import {fetchBlogPost, fetchBlogPosts, Widget} from "../../../../lib/contentful/blogPosts"; import {fetchBlogPost, fetchBlogPosts, Widget} from "../../../../lib/contentful/blogPosts";
@ -23,7 +23,7 @@ export async function generateMetadata({ params }: BlogPostPageProps, parent: Re
return { return {
title: blogPost.title, title: blogPost.title,
// description: blogPost.metaDescription description: blogPost.metaDescription
} }
} }

View File

@ -112,7 +112,11 @@ export default async function ExpertItem({ params: { expertId = '', locale } }:
{expert?.publicCoachDetails?.trainings && expert.publicCoachDetails.trainings?.map(generateDescription)} {expert?.publicCoachDetails?.trainings && expert.publicCoachDetails.trainings?.map(generateDescription)}
{expert?.publicCoachDetails?.mbas && expert.publicCoachDetails.mbas?.map(generateDescription)} {expert?.publicCoachDetails?.mbas && expert.publicCoachDetails.mbas?.map(generateDescription)}
{expert?.publicCoachDetails?.experiences && expert.publicCoachDetails.experiences?.map(generateDescription)} {expert?.publicCoachDetails?.experiences && expert.publicCoachDetails.experiences?.map(generateDescription)}
<ExpertPractice expert={expert} locale={locale} /> <ExpertPractice
themes={expert?.publicCoachDetails?.themesGroups}
cases={expert?.publicCoachDetails?.practiceCases}
locale={locale}
/>
{/* <h2 className="title-h2">All Offers by this Expert</h2> {/* <h2 className="title-h2">All Offers by this Expert</h2>
<div className="offers-list"> <div className="offers-list">

View File

@ -12,7 +12,7 @@ import { validateImage } from '../../utils/account';
import { useProfileSettings } from '../../actions/hooks/useProfileSettings'; import { useProfileSettings } from '../../actions/hooks/useProfileSettings';
import { CustomInput } from '../view/CustomInput'; import { CustomInput } from '../view/CustomInput';
import { OutlinedButton } from '../view/OutlinedButton'; import { OutlinedButton } from '../view/OutlinedButton';
import { FilledYellowButton } from '../view/FilledButton'; import {FilledButton, FilledSquareButton, FilledYellowButton} from '../view/FilledButton';
import { DeleteAccountModal } from '../Modals/DeleteAccountModal'; import { DeleteAccountModal } from '../Modals/DeleteAccountModal';
import { Loader } from '../view/Loader'; import { Loader } from '../view/Loader';
@ -55,14 +55,14 @@ export const ProfileSettings: FC<ProfileSettingsProps> = ({ locale }) => {
languagesLinks: languagesLinks?.map(({ languageId }) => ({ languageId })) || [] languagesLinks: languagesLinks?.map(({ languageId }) => ({ languageId })) || []
}; };
// if (photo) { if (photo) {
// console.log(photo); console.log(photo);
// const formData = new FormData(); const formData = new FormData();
// formData.append('file', photo as FileType); formData.append('file', photo as FileType);
//
// newProfile.faceImage = photo; newProfile.faceImage = `[${(photo as File).arrayBuffer()}]`;
// newProfile.isFaceImageKeepExisting = false; newProfile.isFaceImageKeepExisting = false;
// } }
console.log(newProfile); console.log(newProfile);
@ -99,13 +99,6 @@ export const ProfileSettings: FC<ProfileSettingsProps> = ({ locale }) => {
return ( return (
<Loader isLoading={fetchLoading} refresh={fetchProfileSettings}> <Loader isLoading={fetchLoading} refresh={fetchProfileSettings}>
<Form form={form} className="form-settings"> <Form form={form} className="form-settings">
<div className="user-avatar">
<div className="user-avatar__edit" style={profileSettings?.faceImageUrl ? { backgroundImage: `url(${profileSettings.faceImageUrl})` } : undefined}>
<input className="" type="file" id="input-file" />
<label htmlFor="input-file" className="form-label" />
</div>
<div className="user-avatar__text">{i18nText('photoDesc', locale)}</div>
</div>
<ImgCrop <ImgCrop
modalTitle="Редактировать" modalTitle="Редактировать"
modalOk="Сохранить" modalOk="Сохранить"
@ -126,8 +119,17 @@ export const ProfileSettings: FC<ProfileSettingsProps> = ({ locale }) => {
multiple={false} multiple={false}
showUploadList={false} showUploadList={false}
> >
{photo && <img height={100} width={100} src={URL.createObjectURL(photo)} />} <div className="user-avatar">
<Button icon={<CameraOutlined />}>Click to Upload</Button> <div className="user-avatar__edit" style={photo
? { backgroundImage: `url(${URL.createObjectURL(photo)})` }
: profileSettings?.faceImageUrl ? { backgroundImage: `url(${profileSettings.faceImageUrl})`} : undefined }>
<FilledSquareButton
type="primary"
icon={<CameraOutlined style={{ fontSize: 28 }} />}
/>
</div>
<div className="user-avatar__text">{i18nText('photoDesc', locale)}</div>
</div>
</Upload> </Upload>
</ImgCrop> </ImgCrop>
<div className="form-fieldset"> <div className="form-fieldset">

View File

@ -1,43 +1,96 @@
'use client' 'use client'
import { useState } from 'react'; import React, { useState } from 'react';
import { message } from 'antd'; import {Alert, message} from 'antd';
import { EditOutlined } from '@ant-design/icons'; import Image from 'next/image';
import { i18nText } from '../../i18nKeys'; 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 { AUTH_TOKEN_KEY } from '../../constants/common';
import { useLocalStorage } from '../../hooks/useLocalStorage'; 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 { Loader } from '../view/Loader';
import { LinkButton } from '../view/LinkButton';
import { ExpertTags } from './content/ExpertTags'; import { ExpertTags } from './content/ExpertTags';
import { ExpertSchedule } from './content/ExpertSchedule'; import { ExpertSchedule } from './content/ExpertSchedule';
import { ExpertPayData } from './content/ExpertPayData'; import { ExpertPayData } from './content/ExpertPayData';
import { ExpertEducation } from './content/ExpertEducation'; import { ExpertEducation } from './content/ExpertEducation';
import { ExpertAbout } from './content/ExpertAbout';
type ExpertProfileProps = { type ExpertProfileProps = {
locale: string; locale: string;
data: ExpertData; data: ExpertData;
updateData: (data: ExpertData) => void; updateData: (data: ExpertData) => void;
isFull: boolean;
}; };
export const ExpertProfile = ({ locale, data, updateData }: ExpertProfileProps) => { type NewDataPartProps<T> = {
key: keyof ExpertData,
getNewData: (locale: string, token: string) => Promise<T>,
errorMessage?: string;
};
export const ExpertProfile = ({ locale, data, updateData, isFull }: ExpertProfileProps) => {
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, ''); const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
const [loading, setLoading] = useState<(keyof ExpertData)[]>([]); const [loading, setLoading] = useState<(keyof ExpertData)[]>([]);
function getNewPartData <T>({ key, getNewData, errorMessage = 'Не удалось получить данные' }: NewDataPartProps<T>) {
setLoading([key]);
getNewData(locale, jwt)
.then((newData) => {
updateData({
...data,
[key]: newData
});
})
.catch(() => message.error(errorMessage))
.finally(() => setLoading([]));
}
const updateExpert = (key: keyof ExpertData) => { const updateExpert = (key: keyof ExpertData) => {
switch (key) { switch (key) {
case 'tags': case 'tags':
setLoading([key]); getNewPartData<ExpertsTags>({
getTags(locale, jwt) key,
.then((tags) => { getNewData: getTags,
updateData({ errorMessage: 'Не удалось получить направления'
...data, });
tags break;
case 'practice':
getNewPartData<PracticeDTO>({
key,
getNewData: getPractice
});
break;
case 'education':
getNewPartData<EducationDTO>({
key,
getNewData: getEducation,
errorMessage: 'Не удалось получить информацию об образовании'
});
break;
case 'schedule':
getNewPartData<ScheduleDTO>({
key,
getNewData: getSchedule,
errorMessage: 'Не удалось получить расписание'
});
break;
case 'person':
getNewPartData<ProfileData>({
key,
getNewData: getPersonalData,
errorMessage: 'Не удалось получить информацию о пользователе'
});
break;
case 'payData':
getNewPartData<{ person6Data?: PayInfo }>({
key,
getNewData: getPayData,
errorMessage: 'Не удалось получить платежную информацию'
}); });
})
.catch(() => message.error('Не удалось обновить направления'))
.finally(() => setLoading([]));
break; break;
default: default:
break; break;
@ -52,36 +105,39 @@ export const ExpertProfile = ({ locale, data, updateData }: ExpertProfileProps)
<div className="coaching-info"> <div className="coaching-info">
<div className="coaching-profile"> <div className="coaching-profile">
<div className="coaching-profile__portrait"> <div className="coaching-profile__portrait">
<img src="/images/person.png" className="" alt="" /> <Image src={data?.person?.faceImageUrl || '/images/user-avatar.png'} width={216} height={216} alt="" />
</div> </div>
<div className="coaching-profile__inner"> <div className="coaching-profile__inner" style={{ flex: 1 }}>
<div className="coaching-profile__name"> <div className="coaching-profile__name">
David {`${data?.person?.username} ${data?.person?.surname || ''}`}
</div> </div>
</div> {!isFull && (
</div> <Alert
<div className="coaching-section__wrap"> message="Проверьте заполненность блоков"
<div className="coaching-section"> description={(
<div className="coaching-section__title"> <ul className="b-rules-list">
<h2 className="title-h2">{i18nText('aboutCoach', locale)}</h2> <li>о себе</li>
<h2 className="title-h2">person1 + person4</h2> <li>темы сессии</li>
<LinkButton <li>рабочее расписание</li>
type="link" <li>информация об образовании</li>
icon={<EditOutlined />} <li>платежная информация</li>
</ul>
)}
type="warning"
showIcon
/> />
</div> )}
<div className="card-profile__header__title">
{`12 ${i18nText('practiceHours', locale)}`}
</div>
<div className="card-profile__header__title ">
{`15 ${i18nText('supervisionCount', locale)}`}
</div>
<div className="base-text">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam aliquet, lectus nec viverra
malesuada, ligula sem tempor risus, non posuere urna diam a libero.
</div>
</div> </div>
</div> </div>
<Loader isLoading={loading.includes('practice') || loading.includes('person')}>
<ExpertAbout
locale={locale}
practice={data?.practice}
person={data?.person}
updateExpert={updateExpert}
/>
</Loader>
<Loader isLoading={loading.includes('tags')}> <Loader isLoading={loading.includes('tags')}>
<ExpertTags <ExpertTags
locale={locale} locale={locale}
@ -90,9 +146,21 @@ export const ExpertProfile = ({ locale, data, updateData }: ExpertProfileProps)
/> />
</Loader> </Loader>
<ExpertSchedule locale={locale} data={data?.schedule} /> <ExpertSchedule locale={locale} data={data?.schedule} />
<ExpertEducation locale={locale} data={data?.education} /> <Loader isLoading={loading.includes('education')}>
<ExpertPayData locale={locale} data={data?.payData?.person6Data} /> <ExpertEducation
locale={locale}
data={data?.education}
updateExpert={updateExpert}
/>
</Loader>
<Loader isLoading={loading.includes('payData')}>
<ExpertPayData
locale={locale}
data={data?.payData?.person6Data}
updateExpert={updateExpert}
/>
</Loader>
</div> </div>
</> </>
) );
}; };

View File

@ -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<boolean>(false);
const supervisionCount = practice?.person4Data?.supervisionPerYears && practice?.person4Data?.supervisionPerYearId
? practice.person4Data.supervisionPerYears.filter(({ id }) => id === practice.person4Data.supervisionPerYearId)
: [];
return (
<div className="coaching-section__wrap">
<div className="coaching-section">
<div className="coaching-section__title">
<h2 className="title-h2">{i18nText('aboutCoach', locale)}</h2>
<LinkButton
type="link"
icon={<EditOutlined />}
onClick={() => setShowEdit(true)}
/>
</div>
<div className="coaching-section__info">
<div className="coaching-section__practice">
{`${practice?.person4Data?.practiceHours || 0} ${i18nText('practiceHours', locale)} | ${supervisionCount.length > 0 ? supervisionCount[0].name : 0} ${i18nText('supervisionCount', locale)}`}
</div>
<div className="coaching-section__list">
{practice?.person4Data?.sessionCost && (
<div className="coaching-section__item">
<div>{i18nText('price', locale)}</div>
<div>{`${practice?.person4Data?.sessionCost}`}</div>
</div>
)}
{practice?.person4Data?.sessionDuration && (
<div className="coaching-section__item">
<div>{i18nText('duration', locale)}</div>
<div>{`${practice?.person4Data?.sessionDuration} ${locale === 'ru' ? 'мин' : 'min'}`}</div>
</div>
)}
</div>
<div className="coaching-section__lang">
<div>{i18nText('sessionLang', locale)}</div>
<div className="skills__list">
{person?.languagesLinks && person.languagesLinks?.length > 0 && person.languagesLinks
.map(({ language: { code, nativeSpelling } }) => <Tag key={code} className="skills__list__item">{nativeSpelling}</Tag>)}
</div>
</div>
<ExpertPractice
locale={locale}
themes={practice?.person4Data?.themesGroups}
cases={practice?.person4Data?.practiceCases}
/>
</div>
</div>
<EditExpertAboutModal
locale={locale}
open={showEdit}
practice={practice}
person={person}
handleCancel={() => setShowEdit(false)}
refreshPractice={() => updateExpert('practice')}
refreshPerson={() => updateExpert('person')}
/>
</div>
);
};

View File

@ -1,65 +1,154 @@
'use client'
import { EditOutlined } from '@ant-design/icons'; import { EditOutlined } from '@ant-design/icons';
import { EducationDTO } from '../../../types/education'; import { EducationDTO } from '../../../types/education';
import { i18nText } from '../../../i18nKeys'; import { i18nText } from '../../../i18nKeys';
import { LinkButton } from '../../view/LinkButton'; 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 = { type ExpertEducationProps = {
locale: string; locale: string;
data?: EducationDTO; data?: EducationDTO;
updateExpert: (key: keyof ExpertData) => void;
}; };
export const ExpertEducation = ({ locale, data }: ExpertEducationProps) => { export const ExpertEducation = ({ locale, data, updateExpert }: ExpertEducationProps) => {
const [showEdit, setShowEdit] = useState<boolean>(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 ( return (
<div className="coaching-section__wrap"> <div className="coaching-section__wrap">
<div className="coaching-section"> <div className="coaching-section">
<div className="coaching-section__title"> <div className="coaching-section__title">
<h2 className="title-h2">{i18nText('education', locale)}</h2> <h2 className="title-h2">{i18nText('skillsInfo', locale)}</h2>
<h2 className="title-h2">person2</h2>
<LinkButton <LinkButton
type="link" type="link"
icon={<EditOutlined />} icon={<EditOutlined />}
onClick={() => setShowEdit(true)}
/> />
</div> </div>
{data?.person2Data?.educations?.length > 0 && (
<div className="coaching-section__desc"> <div className="coaching-section__desc">
<h3 className="title-h3">Psychologist</h3> {data?.person2Data?.educations?.map(({ id, title, description, document }) => (
<div className="base-text"> <div key={id}>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam aliquet, lectus nec viverra <h3 className="title-h3">{title}</h3>
malesuada, ligula sem tempor risus, non posuere urna diam a libero. {description && <div className="base-text">{description}</div>}
</div> {document && (
<div className="sertific"> <div className="sertific">
<img src="/images/sertific.png" className="" alt="" /> <ExpertCertificate document={document} />
</div> </div>
)}
</div> </div>
))}
</div> </div>
)}
</div>
{data?.person2Data?.certificates?.length > 0 && (
<div className="coaching-section"> <div className="coaching-section">
<h2 className="title-h2">{i18nText('profCertification', locale)}</h2> <h2 className="title-h2">{i18nText('profCertification', locale)}</h2>
<div className="coaching-section__desc"> <div className="coaching-section__desc">
{data?.person2Data?.certificates?.map((cert) => (
<div key={cert.id}>
<div className="base-text"> <div className="base-text">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam aliquet, lectus nec viverra {`${getAssociationLevel(cert?.associationLevelId)} ${getAssociation(cert?.associationLevelId)}`}
malesuada, ligula sem tempor risus, non posuere urna diam a libero. </div>
</div> {cert.document && (
<div className="sertific">
<ExpertCertificate document={cert.document} />
</div>
)}
</div>
))}
</div> </div>
</div> </div>
)}
{data?.person2Data?.trainings?.length > 0 && (
<div className="coaching-section"> <div className="coaching-section">
<h2 className="title-h2"> <h2 className="title-h2">
{`${i18nText('trainings', locale)} | ${i18nText('seminars', locale)} | ${i18nText('courses', locale)}`} {`${i18nText('trainings', locale)} | ${i18nText('seminars', locale)} | ${i18nText('courses', locale)}`}
</h2> </h2>
<div className="coaching-section__desc"> <div className="coaching-section__desc">
<div className="base-text"> {data?.person2Data?.trainings?.map(({ id, title, description, document }) => (
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam aliquet, lectus nec viverra <div key={id}>
malesuada, ligula sem tempor risus, non posuere urna diam a libero. <h3 className="title-h3">{title}</h3>
</div> {description && <div className="base-text">{description}</div>}
{document && (
<div className="sertific">
<ExpertCertificate document={document} />
</div>
)}
</div>
))}
</div> </div>
</div> </div>
)}
{data?.person2Data?.mbas?.length > 0 && (
<div className="coaching-section"> <div className="coaching-section">
<h2 className="title-h2">{i18nText('mba', locale)}</h2> <h2 className="title-h2">{i18nText('mba', locale)}</h2>
<div className="coaching-section__desc"> <div className="coaching-section__desc">
<div className="base-text"> {data?.person2Data?.mbas?.map(({ id, title, description, document }) => (
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam aliquet, lectus nec viverra <div key={id}>
malesuada, ligula sem tempor risus, non posuere urna diam a libero. <h3 className="title-h3">{title}</h3>
{description && <div className="base-text">{description}</div>}
{document && (
<div className="sertific">
<ExpertCertificate document={document} />
</div>
)}
</div>
))}
</div> </div>
</div> </div>
)}
{data?.person2Data?.experiences?.length > 0 && (
<div className="coaching-section">
<h2 className="title-h2">{i18nText('mExperiences', locale)}</h2>
<div className="coaching-section__desc">
{data?.person2Data?.experiences?.map(({ id, title, description, document }) => (
<div key={id}>
<h3 className="title-h3">{title}</h3>
{description && <div className="base-text">{description}</div>}
{document && (
<div className="sertific">
<ExpertCertificate document={document} />
</div> </div>
)}
</div>
))}
</div>
</div>
)}
<EditExpertEducationModal
open={showEdit}
handleCancel={() => setShowEdit(false)}
locale={locale}
data={data}
refresh={() => updateExpert('education')}
/>
</div> </div>
); );
}; };

View File

@ -1,28 +1,65 @@
'use client'
import { useState } from 'react';
import { EditOutlined } from '@ant-design/icons'; import { EditOutlined } from '@ant-design/icons';
import { i18nText } from '../../../i18nKeys'; import { i18nText } from '../../../i18nKeys';
import { PayInfo } from '../../../types/profile'; import { ExpertData, PayInfo } from '../../../types/profile';
import { LinkButton } from '../../view/LinkButton'; import { LinkButton } from '../../view/LinkButton';
import { EditExpertPayDataModal } from '../../Modals/EditExpertPayDataModal';
type ExpertPayDataProps = { type ExpertPayDataProps = {
locale: string; 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<boolean>(false);
const hide = (str?: string) => {
const reg = new RegExp('(.)(?=.*....)', 'gi');
return str ? str.replace(reg, '*') : '';
}
return ( return (
<div className="coaching-section__wrap"> <div className="coaching-section__wrap">
<div className="coaching-section"> <div className="coaching-section">
<div className="coaching-section__title"> <div className="coaching-section__title">
<h2 className="title-h2">Card data - person6</h2> <h2 className="title-h2">{i18nText('payInfo', locale)}</h2>
<LinkButton <LinkButton
type="link" type="link"
icon={<EditOutlined />} icon={<EditOutlined />}
onClick={() => setShowEdit(true)}
/> />
</div> </div>
<div className="base-text"> <div className="base-text pay-data-list">
Card {data?.beneficiaryName && (
<div>
<div>{i18nText('beneficiaryName', locale)}</div>
<div>{data.beneficiaryName}</div>
</div>
)}
{data?.bicOrSwift && (
<div>
<div>{i18nText('bicOrSwift', locale)}</div>
<div>{hide(data.bicOrSwift)}</div>
</div>
)}
{data?.iban && (
<div>
<div>IBAN</div>
<div>{hide(data.iban)}</div>
</div>
)}
</div> </div>
</div> </div>
<EditExpertPayDataModal
locale={locale}
open={showEdit}
data={data}
handleCancel={() => setShowEdit(false)}
refresh={() => updateExpert('payData')}
/>
</div> </div>
); );
}; };

View File

@ -1,7 +1,11 @@
import { EditOutlined } from '@ant-design/icons'; import { EditOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import { ScheduleDTO } from '../../../types/schedule'; import { ScheduleDTO } from '../../../types/schedule';
import { i18nText } from '../../../i18nKeys'; import { i18nText } from '../../../i18nKeys';
import { LinkButton } from '../../view/LinkButton'; import { LinkButton } from '../../view/LinkButton';
import {useState} from "react";
import {Tag} from "antd";
import {getCurrentTime} from "../../../utils/time";
type ExpertScheduleProps = { type ExpertScheduleProps = {
locale: string; locale: string;
@ -9,18 +13,34 @@ type ExpertScheduleProps = {
}; };
export const ExpertSchedule = ({ locale, data }: ExpertScheduleProps) => { export const ExpertSchedule = ({ locale, data }: ExpertScheduleProps) => {
const [showEdit, setShowEdit] = useState<boolean>(false);
// person51
return ( return (
<div className="coaching-section__wrap"> <div className="coaching-section__wrap">
<div className="coaching-section"> <div className="coaching-section">
<div className="coaching-section__title"> <div className="coaching-section__title">
<h2 className="title-h2">Schedule - person51</h2> <h2 className="title-h2">{i18nText('schedule', locale)}</h2>
<LinkButton {/*<LinkButton
type="link" type="link"
icon={<EditOutlined />} icon={<EditOutlined />}
/> onClick={() => setShowEdit(true)}
/>*/}
</div> </div>
<div className="base-text"> <div className="b-schedule-list">
Schedule {data && data?.workingTimes?.map((date, index) => {
const { startDay, startTime, endDay, endTime } = getCurrentTime(date);
return (
<div key={`date_${index}`}>
<Tag className="skills__list__item">{i18nText(startDay, locale)}</Tag>
<div>{startTime}</div>
<span>-</span>
{startDay !== endDay && <Tag className="skills__list__item">{i18nText(endDay, locale)}</Tag>}
<div>{endTime}</div>
</div>
)
})}
</div> </div>
</div> </div>
</div> </div>

View File

@ -22,7 +22,7 @@ export const ExpertTags = ({ locale, data, updateExpert }: ExpertTagsProps) => {
<div className="coaching-section__wrap"> <div className="coaching-section__wrap">
<div className="coaching-section"> <div className="coaching-section">
<div className="coaching-section__title"> <div className="coaching-section__title">
<h2 className="title-h2">{i18nText('direction', locale)}</h2> <h2 className="title-h2">{i18nText('topics', locale)}</h2>
<LinkButton <LinkButton
type="link" type="link"
icon={<EditOutlined />} icon={<EditOutlined />}

View File

@ -4,7 +4,8 @@ import React, { FC } from 'react';
import Image from 'next/image'; import Image from 'next/image';
import { Tag, Image as AntdImage, Space } from 'antd'; import { Tag, Image as AntdImage, Space } from 'antd';
import { ZoomInOutlined, ZoomOutOutlined, StarFilled } from '@ant-design/icons'; 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 { Locale } from '../../types/locale';
import { CustomRate } from '../view/CustomRate'; import { CustomRate } from '../view/CustomRate';
import { i18nText } from '../../i18nKeys'; import { i18nText } from '../../i18nKeys';
@ -15,6 +16,12 @@ type ExpertDetailsProps = {
locale?: string; locale?: string;
}; };
type ExpertPracticeProps = {
cases?: Practice[];
themes?: ThemeGroup[];
locale?: string;
};
export const ExpertCard: FC<ExpertDetailsProps> = ({ expert, locale }) => { export const ExpertCard: FC<ExpertDetailsProps> = ({ expert, locale }) => {
const { publicCoachDetails } = expert || {}; const { publicCoachDetails } = expert || {};
@ -62,10 +69,10 @@ export const ExpertInformation: FC<ExpertDetailsProps> = ({ expert, locale }) =>
<div className="expert-info"> <div className="expert-info">
{/* <h2 className="title-h2">{}</h2> */} {/* <h2 className="title-h2">{}</h2> */}
<div className="skills__list"> <div className="skills__list">
{coachLanguages?.map((skill) => <Tag key={skill} className="skills__list__item">{skill}</Tag>)} {coachLanguages?.map((lang) => <Tag key={lang} className="skills__list__item">{lang}</Tag>)}
</div> </div>
</div> </div>
<p className="base-text"> {/* <p className="base-text">
Hello, my name is Marcelo. I am a Senior UX Designer with more than 6 years of experience working 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. 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 During my career, I have helped organizations solve complex problems using aesthetically pleasing
@ -79,7 +86,7 @@ export const ExpertInformation: FC<ExpertDetailsProps> = ({ expert, locale }) =>
Strategic thinking <br /><br /> Strategic thinking <br /><br />
Oh, and I also speak Spanish! Oh, and I also speak Spanish!
</p> </p> */}
<div className="skills__list"> <div className="skills__list">
{tags?.map((skill) => <Tag key={skill?.id} className="skills__list__item">{skill?.name}</Tag>)} {tags?.map((skill) => <Tag key={skill?.id} className="skills__list__item">{skill?.name}</Tag>)}
</div> </div>
@ -93,14 +100,12 @@ export const ExpertInformation: FC<ExpertDetailsProps> = ({ expert, locale }) =>
); );
}; };
export const ExpertPractice: FC<ExpertDetailsProps> = ({ expert, locale }) => { export const ExpertPractice: FC<ExpertPracticeProps> = ({ themes = [], cases = [], locale }) => {
const { publicCoachDetails: { practiceCases = [], themesGroups = [] } } = expert || {}; return cases?.length > 0 ? (
return practiceCases?.length > 0 ? (
<div> <div>
<h3 className="title-h3">{i18nText('successfulCase', locale)}</h3> <h3 className="title-h3">{i18nText('successfulCase', locale)}</h3>
{practiceCases?.map(({ id, description, themesGroupIds }) => { {cases?.map(({ id, description, themesGroupIds }) => {
const filtered = themesGroups?.filter(({ id }) => themesGroupIds?.includes(+id)); const filtered = themes ? themes.filter(({ id }) => themesGroupIds?.includes(+id)) : [];
return ( return (
<div key={id} className="case-list"> <div key={id} className="case-list">

View File

@ -162,18 +162,15 @@ export const ExpertsFilter = ({
), [filter, searchParams, searchData]); ), [filter, searchParams, searchData]);
const getLangList = () => { const getLangList = () => {
const reg = searchLang ? new RegExp(searchLang, 'ig') : ''; const langList = searchLang ? (languages || []).filter(({ code, nativeSpelling }) => code.indexOf(searchLang) !== -1 || nativeSpelling.indexOf(searchLang) !== -1) : languages;
const langList = reg ? (languages || []).filter(({ code, nativeSpelling }) => reg.test(code) || reg.test(nativeSpelling)) : languages;
return langList?.length return langList?.length
? getList('userLanguages', langList.map(({ code, nativeSpelling }) => ({ id: code, name: nativeSpelling }))) ? getList('userLanguages', langList.map(({ code, nativeSpelling }) => ({ id: code, name: nativeSpelling })))
: null; : null;
}; };
const getTagsList = () => { const getTagsList = () => {
const reg = searchTags ? new RegExp(searchTags, 'ig') : ''; if (searchTags) {
const tagsList = filteredTags.filter(({ name, group }) => name.indexOf(searchTags) !== -1 || group.indexOf(searchTags) !== -1);
if (reg) {
const tagsList = filteredTags.filter(({ name, group }) => reg.test(name) || reg.test(group));
return getList('themesTagIds', tagsList); return getList('themesTagIds', tagsList);
} }

View File

@ -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<EditExpertAboutModalProps> = ({
open,
handleCancel,
locale,
practice,
person,
refreshPerson,
refreshPractice
}) => {
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
const [loading, setLoading] = useState<boolean>(false);
const [form] = Form.useForm<FormPerson>();
const [practiceCases, setPracticeCases] = useState<PracticeCase[]>([]);
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 (
<Modal
className="b-modal"
open={open}
title={undefined}
onOk={undefined}
onCancel={handleCancel}
footer={false}
width={498}
closeIcon={<CloseOutlined style={{ fontSize: 20, color: '#000' }}/>}
>
<div className="b-modal__expert__content">
<div className="b-modal__expert__title">{i18nText('aboutCoach', locale)}</div>
<div className="b-modal__expert__inner">
<Form form={form} style={{ width: '100%', display: 'flex', gap: 16, flexDirection: 'column' }}>
<Form.Item
name="sessionLang"
noStyle
>
<CustomMultiSelect
label={i18nText('sessionLang', locale)}
options={person?.allLanguages?.map(({ id, nativeSpelling }) => ({ value: id, label: nativeSpelling })) || []}
/>
</Form.Item>
<Form.Item
name="sessionDuration"
noStyle
>
<CustomInput
size="small"
placeholder={i18nText('sessionDuration', locale)}
autoComplete="off"
addonAfter={locale === 'ru' ? 'мин' : 'min'}
/>
</Form.Item>
<Form.Item
name="sessionCost"
noStyle
>
<CustomSelect
label={i18nText('sessionCost', locale)}
options={practice?.person4Data?.sessionCosts?.map((cost) => ({ value: cost, label: cost })) || []}
/>
</Form.Item>
<Form.Item
name="practiceHours"
noStyle
>
<CustomInput
size="small"
placeholder={i18nText('experienceHours', locale)}
autoComplete="off"
addonAfter={locale === 'ru' ? 'часов' : 'hours'}
/>
</Form.Item>
<Form.Item
name="supervisionPerYearId"
noStyle
>
<CustomSelect
label={i18nText('supervisionCount', locale)}
options={practice?.person4Data?.supervisionPerYears?.map(({ id, name }) => ({ value: id, label: name })) || []}
/>
</Form.Item>
</Form>
</div>
<div className="b-practice-cases">
<div className="b-practice-case__header">
<div>{i18nText('successfulCase', locale)}</div>
<LinkButton
type="link"
icon={<PlusOutlined />}
onClick={addPracticeCase}
/>
</div>
{practiceCases.map(({ description, themesGroupIds }, index) => (
<div key={index} className="b-practice-case__item">
<div className="b-practice-case__content">
<div>
<CustomMultiSelect
value={themesGroupIds || []}
label={i18nText('topics', locale)}
options={practice?.person4Data?.themesGroups?.map(({ id, name }) => ({ value: id, label: name })) || []}
onChange={(val) => onChangePracticeThemes(val, index)}
/>
</div>
<div>
<Input.TextArea
value={description}
className="b-textarea"
rows={2}
placeholder={i18nText('description', locale)}
onChange={(e) => onChangePracticeDescription(e.target.value, index)}
/>
</div>
</div>
<LinkButton
type="link"
danger
icon={<DeleteOutlined />}
onClick={() => deletePracticeCase(index)}
/>
</div>
))}
</div>
<div className="b-modal__expert__button">
<Button
className="card-detail__apply"
onClick={onSave}
loading={loading}
>
{i18nText('save', locale)}
</Button>
</div>
</div>
</Modal>
);
};

View File

@ -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<EditExpertEducationModalProps> = ({
open,
handleCancel,
locale,
data,
refresh
}) => {
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
const [loading, setLoading] = useState<boolean>(false);
const [form] = Form.useForm<FormPerson>();
const [editedData, setEditedData] = useState<EducationData>(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: (
<CertificatesContent
certificates={editedData?.certificates}
update={(certificates) => setEditedData({ ...editedData, certificates })}
locale={locale}
associationLevels={data?.associationLevels}
associations={data?.associations}
/>
),
},
{
key: 'educations',
label: i18nText('education', locale),
children: (
<EducationsContent
educations={editedData?.educations}
update={(educations) => setEditedData({ ...editedData, educations })}
locale={locale}
/>
),
},
{
key: 'trainings',
label: `${i18nText('trainings', locale)} | ${i18nText('seminars', locale)} | ${i18nText('courses', locale)}`,
children: (
<TrainingsContent
trainings={editedData?.trainings}
update={(trainings) => setEditedData({ ...editedData, trainings })}
locale={locale}
/>
),
},
{
key: 'mbas',
label: i18nText('mba', locale),
children: (
<MbasContent
mbas={editedData?.mbas}
update={(mbas) => setEditedData({ ...editedData, mbas })}
locale={locale}
/>
),
},
{
key: 'experiences',
label: i18nText('mExperiences', locale),
children: (
<ExperiencesContent
experiences={editedData?.experiences}
update={(experiences) => setEditedData({ ...editedData, experiences })}
locale={locale}
/>
),
},
];
return (
<Modal
className="b-modal"
open={open}
title={undefined}
onOk={undefined}
onCancel={handleCancel}
footer={false}
width={498}
closeIcon={<CloseOutlined style={{ fontSize: 20, color: '#000' }}/>}
>
<div className="b-modal__expert__content">
<div className="b-modal__expert__title">{i18nText('skillsInfo', locale)}</div>
<div className="b-modal__expert__inner" style={{ paddingRight: 12 }}>
<Form form={form} style={{ width: '100%' }}>
<Collapse
ghost
expandIconPosition="end"
items={items}
/>
</Form>
</div>
<div className="b-modal__expert__button">
<Button
className="card-detail__apply"
onClick={onSave}
loading={loading}
>
{i18nText('save', locale)}
</Button>
</div>
</div>
</Modal>
);
};

View File

@ -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<EditExpertPayDataModalProps> = ({
open,
handleCancel,
locale,
data,
refresh
}) => {
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
const [loading, setLoading] = useState<boolean>(false);
const [form] = Form.useForm<PayInfo>();
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 (
<Modal
className="b-modal"
open={open}
title={undefined}
onOk={undefined}
onCancel={handleCancel}
footer={false}
width={498}
closeIcon={<CloseOutlined style={{ fontSize: 20, color: '#000' }}/>}
>
<div className="b-modal__expert__content">
<div className="b-modal__expert__title">{i18nText('payInfo', locale)}</div>
<div className="b-modal__expert__inner">
<Form form={form} style={{ width: '100%', display: 'flex', gap: 16, flexDirection: 'column' }}>
<Form.Item
name="beneficiaryName"
noStyle
>
<CustomInput
size="small"
placeholder={i18nText('beneficiaryName', locale)}
autoComplete="off"
/>
</Form.Item>
<Form.Item
name="bicOrSwift"
noStyle
>
<CustomInput
size="small"
placeholder={i18nText('bicOrSwift', locale)}
autoComplete="off"
/>
</Form.Item>
<Form.Item
name="iban"
noStyle
>
<CustomInput
size="small"
placeholder="IBAN"
autoComplete="off"
/>
</Form.Item>
</Form>
</div>
<div className="b-modal__expert__button">
<Button
className="card-detail__apply"
onClick={onSavePayData}
loading={loading}
>
{i18nText('save', locale)}
</Button>
</div>
</div>
</Modal>
);
};

View File

@ -69,7 +69,7 @@ export const EditExpertTagsModal: FC<EditExpertTagsModalProps> = ({
closeIcon={<CloseOutlined style={{ fontSize: 20, color: '#000' }}/>} closeIcon={<CloseOutlined style={{ fontSize: 20, color: '#000' }}/>}
> >
<div className="b-modal__expert__content"> <div className="b-modal__expert__content">
<div className="b-modal__expert__title">{i18nText('direction', locale)}</div> <div className="b-modal__expert__title">{i18nText('selectTopic', locale)}</div>
<div className="b-modal__expert__inner"> <div className="b-modal__expert__inner">
{data?.themesGroups && data.themesGroups.filter(({ isActive }) => isActive).map(({ id, name }) => ( {data?.themesGroups && data.themesGroups.filter(({ isActive }) => isActive).map(({ id, name }) => (
<div key={`group_${id}`}> <div key={`group_${id}`}>

View File

@ -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 (
<div className="b-edu-content">
<div className="b-edu-list">
{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 (
<div className="b-edu-list__item" key={`cert_${index}`}>
<div>
<CustomSelect
value={cAssociationId}
label={i18nText('association', locale)}
options={associations?.map(({ id, name }) => ({ value: id, label: name })) || []}
onChange={(val) => onChangeAssociation(val, index)}
style={{ maxWidth: 320, minWidth: 320 }}
/>
<CustomSelect
value={associationLevelId}
label={i18nText('level', locale)}
options={associationLevels && associationLevels.length > 0
? associationLevels
.filter(({ associationId }) => associationId === cAssociationId)
.map(({ id, name }) => ({ value: id, label: name }))
: []}
onChange={(val) => onChangeLevel(val, index)}
/>
{/*<Upload
fileList={tmpFile ? [tmpFile] : file ? [
{
uid: file.original?.id,
name: file.fileName,
status: 'done',
url: file.original?.url
}
] : undefined}
accept=".jpg,.jpeg,.png,.pdf"
beforeUpload={(file) => beforeUpload(file as UploadFile, index)}
multiple={false}
onRemove={() => onRemoveFile(index)}
>
<LinkButton type="link">{i18nText('addDiploma', locale)}</LinkButton>
</Upload>*/}
<Upload
fileList={file ? [
{
uid: file.original?.id,
name: file.fileName,
status: 'done',
url: file.original?.url
}
] : undefined}
accept=".jpg,.jpeg,.png,.pdf"
beforeUpload={beforeUpload}
multiple={false}
onRemove={() => 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)}
>
<LinkButton type="link">{i18nText('addDiploma', locale)}</LinkButton>
</Upload>
</div>
<LinkButton
type="link"
danger
icon={<DeleteOutlined />}
onClick={() => deleteCertificate(index)}
/>
</div>
)
})}
</div>
<OutlinedButton
type="link"
onClick={addCertificate}
>
{i18nText('addNew', locale)}
</OutlinedButton>
</div>
);
};

View File

@ -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 (
<div className="b-edu-content">
<div className="b-edu-list">
{educations?.map(({ title, description, document: file}, index) => (
<div className="b-edu-list__item" key={`edu_${index}`}>
<div>
<CustomInput
value={title}
placeholder={i18nText('university', locale)}
onChange={(e) => onChangeUniversity(e?.target?.value, index)}
/>
<CustomInput
value={description}
placeholder={i18nText('description', locale)}
onChange={(e) => onChangeDesc(e?.target?.value, index)}
/>
<Upload
fileList={file ? [
{
uid: file.original?.id,
name: file.fileName,
status: 'done',
url: file.original?.url
}
] : undefined}
accept=".jpg,.jpeg,.png,.pdf"
beforeUpload={beforeUpload}
multiple={false}
onRemove={() => 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)}
>
<LinkButton type="link">{i18nText('addDiploma', locale)}</LinkButton>
</Upload>
</div>
<LinkButton
type="link"
danger
icon={<DeleteOutlined />}
onClick={() => deleteEdu(index)}
/>
</div>
))}
</div>
<OutlinedButton
type="link"
onClick={addEdu}
>
{i18nText('addNew', locale)}
</OutlinedButton>
</div>
);
};

View File

@ -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 (
<div className="b-edu-content">
<div className="b-edu-list">
{experiences?.map(({ title, description}, index) => (
<div className="b-edu-list__item" key={`ex_${index}`}>
<div>
<CustomInput
value={title}
placeholder={i18nText('name', locale)}
onChange={(e) => onChangeName(e?.target?.value, index)}
/>
<CustomInput
value={description}
placeholder={i18nText('description', locale)}
onChange={(e) => onChangeDesc(e?.target?.value, index)}
/>
</div>
<LinkButton
type="link"
danger
icon={<DeleteOutlined />}
onClick={() => deleteExperience(index)}
/>
</div>
))}
</div>
<OutlinedButton
type="link"
onClick={addExperience}
>
{i18nText('addNew', locale)}
</OutlinedButton>
</div>
);
};

View File

@ -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 (
<div className="b-edu-content">
<div className="b-edu-list">
{mbas?.map(({ title, description, document: file}, index) => (
<div className="b-edu-list__item" key={`mba_${index}`}>
<div>
<CustomInput
value={title}
placeholder={i18nText('university', locale)}
onChange={(e) => onChangeName(e?.target?.value, index)}
/>
<CustomInput
value={description}
placeholder={i18nText('description', locale)}
onChange={(e) => onChangeDesc(e?.target?.value, index)}
/>
<Upload
fileList={file ? [
{
uid: file.original?.id,
name: file.fileName,
status: 'done',
url: file.original?.url
}
] : undefined}
accept=".jpg,.jpeg,.png,.pdf"
beforeUpload={beforeUpload}
multiple={false}
onRemove={() => 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)}
>
<LinkButton type="link">{i18nText('addDiploma', locale)}</LinkButton>
</Upload>
</div>
<LinkButton
type="link"
danger
icon={<DeleteOutlined />}
onClick={() => deleteMba(index)}
/>
</div>
))}
</div>
<OutlinedButton
type="link"
onClick={addMba}
>
{i18nText('addNew', locale)}
</OutlinedButton>
</div>
);
};

View File

@ -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 (
<div className="b-edu-content">
<div className="b-edu-list">
{trainings?.map(({ title, description, document: file}, index) => (
<div className="b-edu-list__item" key={`training_${index}`}>
<div>
<CustomInput
value={title}
placeholder={i18nText('name', locale)}
onChange={(e) => onChangeName(e?.target?.value, index)}
/>
<CustomInput
value={description}
placeholder={i18nText('description', locale)}
onChange={(e) => onChangeDesc(e?.target?.value, index)}
/>
<Upload
fileList={file ? [
{
uid: file.original?.id,
name: file.fileName,
status: 'done',
url: file.original?.url
}
] : undefined}
accept=".jpg,.jpeg,.png,.pdf"
beforeUpload={beforeUpload}
multiple={false}
onRemove={() => 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)}
>
<LinkButton type="link">{i18nText('addDiploma', locale)}</LinkButton>
</Upload>
</div>
<LinkButton
type="link"
danger
icon={<DeleteOutlined />}
onClick={() => deleteTrainings(index)}
/>
</div>
))}
</div>
<OutlinedButton
type="link"
onClick={addTrainings}
>
{i18nText('addNew', locale)}
</OutlinedButton>
</div>
);
};

View File

@ -12,3 +12,9 @@ export const FilledYellowButton = (props: any) => (
{props.children} {props.children}
</Button> </Button>
); );
export const FilledSquareButton = (props: any) => (
<Button className="b-button__filled_square" {...props}>
{props.children}
</Button>
);

View File

@ -2,7 +2,7 @@ import React from 'react';
import { Button } from 'antd'; import { Button } from 'antd';
export const LinkButton = (props: any) => ( export const LinkButton = (props: any) => (
<Button className="b-button__link" {...props}> <Button className={`b-button__link${props?.danger ? ' danger': ''}`} {...props}>
{props.children} {props.children}
</Button> </Button>
); );

View File

@ -105,6 +105,7 @@ export default {
signUp: 'Jetzt anmelden', signUp: 'Jetzt anmelden',
noData: 'Keine Daten', noData: 'Keine Daten',
notFound: 'Nicht gefunden', notFound: 'Nicht gefunden',
skillsInfo: 'Fähigkeiten-Infos',
trainings: 'Trainings', trainings: 'Trainings',
seminars: 'Seminare', seminars: 'Seminare',
courses: 'Kurse', courses: 'Kurse',
@ -112,7 +113,38 @@ export default {
aboutCoach: 'Über Coach', aboutCoach: 'Über Coach',
education: 'Bildung', education: 'Bildung',
coaching: 'Coaching', 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: { errors: {
invalidEmail: 'Die E-Mail-Adresse ist ungültig', invalidEmail: 'Die E-Mail-Adresse ist ungültig',
emptyEmail: 'Bitte geben Sie Ihre E-Mail ein', emptyEmail: 'Bitte geben Sie Ihre E-Mail ein',

View File

@ -110,8 +110,41 @@ export default {
courses: 'Courses', courses: 'Courses',
mba: 'MBA Information', mba: 'MBA Information',
aboutCoach: 'About Coach', aboutCoach: 'About Coach',
skillsInfo: 'Skills Info',
education: 'Education', education: 'Education',
coaching: 'Coaching', 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: { errors: {
invalidEmail: 'The email address is not valid', invalidEmail: 'The email address is not valid',
emptyEmail: 'Please enter your E-mail', emptyEmail: 'Please enter your E-mail',

View File

@ -105,6 +105,7 @@ export default {
signUp: 'Regístrate ahora', signUp: 'Regístrate ahora',
noData: 'Sin datos', noData: 'Sin datos',
notFound: 'No encontrado', notFound: 'No encontrado',
skillsInfo: 'Información',
trainings: 'Formación', trainings: 'Formación',
seminars: 'Seminarios', seminars: 'Seminarios',
courses: 'Cursos', courses: 'Cursos',
@ -112,7 +113,38 @@ export default {
aboutCoach: 'Sobre el coach', aboutCoach: 'Sobre el coach',
education: 'Educación', education: 'Educación',
coaching: 'Coaching', 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: { errors: {
invalidEmail: 'La dirección de correo electrónico no es válida', invalidEmail: 'La dirección de correo electrónico no es válida',
emptyEmail: 'Introduce tu correo electrónico', emptyEmail: 'Introduce tu correo electrónico',

View File

@ -105,6 +105,7 @@ export default {
signUp: 'Inscrivez-vous maintenant', signUp: 'Inscrivez-vous maintenant',
noData: 'Aucune donnée', noData: 'Aucune donnée',
notFound: 'Non trouvé', notFound: 'Non trouvé',
skillsInfo: 'Infos sur les compétences',
trainings: 'Formations', trainings: 'Formations',
seminars: 'Séminaires', seminars: 'Séminaires',
courses: 'Cours', courses: 'Cours',
@ -112,7 +113,38 @@ export default {
aboutCoach: 'À propos du coach', aboutCoach: 'À propos du coach',
education: 'Éducation', education: 'Éducation',
coaching: 'Coaching', 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: { errors: {
invalidEmail: 'L\'adresse e-mail n\'est pas valide', invalidEmail: 'L\'adresse e-mail n\'est pas valide',
emptyEmail: 'Veuillez saisir votre e-mail', emptyEmail: 'Veuillez saisir votre e-mail',

View File

@ -105,6 +105,7 @@ export default {
signUp: 'Iscriviti ora', signUp: 'Iscriviti ora',
noData: 'Nessun dato', noData: 'Nessun dato',
notFound: 'Non trovato', notFound: 'Non trovato',
skillsInfo: 'Info su competenze',
trainings: 'Training', trainings: 'Training',
seminars: 'Seminari', seminars: 'Seminari',
courses: 'Corsi', courses: 'Corsi',
@ -112,7 +113,38 @@ export default {
aboutCoach: 'Informazioni sul coach', aboutCoach: 'Informazioni sul coach',
education: 'Istruzione', education: 'Istruzione',
coaching: 'Coaching', 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: { errors: {
invalidEmail: 'L\'indirizzo e-mail non è valido', invalidEmail: 'L\'indirizzo e-mail non è valido',
emptyEmail: 'Inserisci l\'e-mail', emptyEmail: 'Inserisci l\'e-mail',

View File

@ -98,21 +98,53 @@ export default {
expertBackground: 'Профессиональный опыт эксперта', expertBackground: 'Профессиональный опыт эксперта',
profCertification: 'Профессиональная сертификация', profCertification: 'Профессиональная сертификация',
practiceHours: 'Часов практики', practiceHours: 'Часов практики',
supervisionCount: 'Часов супервизии в год', supervisionCount: 'Супервизий в год',
outOf: 'из', outOf: 'из',
schedule: 'Расписание', schedule: 'Расписание',
successfulCase: 'Успешные случаи из практики', successfulCase: 'Успешные случаи из практики',
signUp: 'Записаться сейчас', signUp: 'Записаться сейчас',
noData: 'Нет данных', noData: 'Нет данных',
notFound: 'Не найдено', notFound: 'Не найдено',
skillsInfo: 'Навыки',
trainings: 'Тренинги', trainings: 'Тренинги',
seminars: 'Семинары', seminars: 'Семинары',
courses: 'Курсы', courses: 'Курсы',
mba: 'Информация о MBA', mba: 'Информация о MBA',
experiences: 'Практический опыт',
aboutCoach: 'О коуче', aboutCoach: 'О коуче',
education: 'Образование', education: 'Образование',
coaching: 'Коучинг', 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: { errors: {
invalidEmail: 'Адрес электронной почты недействителен', invalidEmail: 'Адрес электронной почты недействителен',
emptyEmail: 'Пожалуйста, введите ваш E-mail', emptyEmail: 'Пожалуйста, введите ваш E-mail',

View File

@ -77,7 +77,7 @@ export async function fetchBlogPosts({ preview, category, page, sticky }: FetchB
const contentful = contentfulClient({ preview }) const contentful = contentfulClient({ preview })
const query = { const query = {
content_type: 'blogPost', content_type: 'blogPost',
select: ['fields.title', 'fields.excerpt', 'fields.author', 'fields.listImage', 'fields.author', 'fields.category', 'sys.createdAt', 'fields.slug'], select: ['fields.title', 'fields.excerpt', 'fields.author', 'fields.listImage', 'fields.author', 'fields.category', 'sys.createdAt', 'fields.slug', 'fields.metaDescription'],
order: ['sys.createdAt'], order: ['sys.createdAt'],
} }
if (category){ if (category){

35
src/styles/_edu.scss Normal file
View File

@ -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;
}
}
}
}

View File

@ -51,7 +51,8 @@
} }
&__inner { &__inner {
height: 60vh; height: auto;
max-height: 60vh;
overflow-y: auto; overflow-y: auto;
& > div { & > div {
@ -86,3 +87,13 @@
.ant-modal-mask { .ant-modal-mask {
background-color: rgba(0, 59, 70, 0.4) !important; 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;
}
}

View File

@ -1188,7 +1188,6 @@
height: 86px; height: 86px;
border-radius: 16px; border-radius: 16px;
border: 2px solid #FFF; border: 2px solid #FFF;
background: lightgray 50%;
box-shadow: 0 8px 16px 0 rgba(102, 165, 173, 0.32); box-shadow: 0 8px 16px 0 rgba(102, 165, 173, 0.32);
overflow: hidden; overflow: hidden;
@ -1210,6 +1209,7 @@
@include rem(18); @include rem(18);
font-weight: 600; font-weight: 600;
line-height: 150%; 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 { .title-h2 {
color: #003B46; color: #003B46;
@include rem(18); @include rem(18);
@ -1243,7 +1298,7 @@
margin-bottom: 0; margin-bottom: 0;
} }
&__desc { &__desc, &__desc > div {
border-radius: 16px; border-radius: 16px;
background: #EFFCFF; background: #EFFCFF;
padding: 16px; padding: 16px;
@ -1443,8 +1498,17 @@
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
} }
} }
} }
}
.pay-data-list {
display: flex;
flex-direction: column;
gap: 8px;
& > div {
display: flex;
gap: 16px;
}
} }

15
src/styles/_schedule.scss Normal file
View File

@ -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;
}
}

View File

@ -15,6 +15,8 @@
@import "_message.scss"; @import "_message.scss";
@import "_auth-modal.scss"; @import "_auth-modal.scss";
@import "_modal.scss"; @import "_modal.scss";
@import "_edu.scss";
@import "_schedule.scss";
@import "./view/style.scss"; @import "./view/style.scss";
@import "./sessions/style.scss"; @import "./sessions/style.scss";

View File

@ -17,6 +17,19 @@
padding: 4px 24px !important; 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 { &.danger {
background: #D93E5C !important; background: #D93E5C !important;
box-shadow: none !important; box-shadow: none !important;
@ -28,6 +41,10 @@
font-size: 15px !important; font-size: 15px !important;
height: auto !important; height: auto !important;
padding: 0 !important; padding: 0 !important;
&.danger {
color: #D93E5C !important;
}
} }
&__outlined { &__outlined {

View File

@ -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;
}

View File

@ -8,6 +8,14 @@
input { input {
background-color: transparent !important; 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 { &:focus, &:hover, &:focus-within {

View File

@ -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;
}
}
}

View File

@ -6,3 +6,5 @@
@import "_spin.scss"; @import "_spin.scss";
@import "_switch.scss"; @import "_switch.scss";
@import "_buttons.scss"; @import "_buttons.scss";
@import "_practice.scss";
@import "_collapse.scss";

View File

@ -9,6 +9,7 @@ export interface BlogPostFields {
title?: EntryFieldTypes.Symbol title?: EntryFieldTypes.Symbol
slug: EntryFieldTypes.Symbol slug: EntryFieldTypes.Symbol
excerpt: EntryFieldTypes.Symbol excerpt: EntryFieldTypes.Symbol
metaDescription: EntryFieldTypes.Symbol
listImage?: EntryFieldTypes.AssetLink listImage?: EntryFieldTypes.AssetLink
author?: AuthorSkeleton author?: AuthorSkeleton
category: BlogPostCategorySkeleton category: BlogPostCategorySkeleton
@ -28,6 +29,7 @@ export interface BlogPost {
author: Author | null author: Author | null
category: string category: string
createdAt: string createdAt: string
metaDescription: string
body: Array<WidgetMedia | WidgetParagraph> body: Array<WidgetMedia | WidgetParagraph>
} }

View File

@ -1,22 +1,23 @@
import { ExpertDocument } from './file'; import { ExpertDocument } from './file';
export type Details = { export type Details = {
id: number; id?: number;
userId?:number; userId?: number;
title?: string; title?: string;
description?: string; description?: string;
document?: ExpertDocument; document?: ExpertDocument | null;
}; };
export type Certificate = { export type Certificate = {
id: number; id?: number;
userId?: number; userId?: number;
associationLevelId?: number; associationLevelId?: number;
document?: ExpertDocument; associationId?: number;
document?: ExpertDocument | null;
}; };
export type Experience = { export type Experience = {
id: number, id?: number,
userId?: number, userId?: number,
title?: string, title?: string,
description?: string description?: string
@ -24,10 +25,10 @@ export type Experience = {
export type Association = { export type Association = {
id: number; id: number;
name?: string; name: string;
}; };
export type AssociationLevel = Association & { associationId?: number }; export type AssociationLevel = Association & { associationId: number };
export type EducationData = { export type EducationData = {
certificates?: Certificate[], certificates?: Certificate[],

View File

@ -6,7 +6,7 @@ export type Supervision = {
}; };
export type PracticeCase = { export type PracticeCase = {
id: number, id?: number,
userId?: number, userId?: number,
description?: string, description?: string,
themesGroupIds?: number[] themesGroupIds?: number[]
@ -18,12 +18,14 @@ export type PracticeData = {
sessionDuration?: number, sessionDuration?: number,
sessionCost?: number, sessionCost?: number,
practiceCases?: PracticeCase[] practiceCases?: PracticeCase[]
} };
export interface PracticeDTO { export type PracticePersonData = PracticeData & {
person4Data: PracticeData & {
themesGroups?: ExpertsThemesGroups[], themesGroups?: ExpertsThemesGroups[],
supervisionPerYears?: Supervision[], supervisionPerYears?: Supervision[],
sessionCosts?: number[] sessionCosts?: number[]
} };
export interface PracticeDTO {
person4Data: PracticePersonData
} }

View File

@ -15,7 +15,8 @@ export type ProfileData = {
hasExternalLogin?: boolean; hasExternalLogin?: boolean;
isTestMode?: boolean; isTestMode?: boolean;
phone?: string; 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 }; export type Profile = ProfileData & { id: number };

View File

@ -28,3 +28,19 @@ export const validateImage = (file: UploadFile, showMessage?: boolean): boolean
return isImage && isLt5M; 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;
};

62
src/utils/time.ts Normal file
View File

@ -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'}`
}
};