feat: add experts profile

This commit is contained in:
SD 2024-08-09 12:54:33 +04:00
parent abf04b4c5b
commit ff74e5ba49
45 changed files with 1175 additions and 364 deletions

View File

@ -79,7 +79,7 @@
}
},
"Experts": {
"title": "Find an expert",
"title": "Einen Experten finden",
"filter": {
"price": "Price from {from}€ to {to}€",
"duration": "Duration from {from}min to {to}min",

View File

@ -79,7 +79,7 @@
}
},
"Experts": {
"title": "Find an expert",
"title": "Encontrar un experto",
"filter": {
"price": "Price from {from}€ to {to}€",
"duration": "Duration from {from}min to {to}min",

View File

@ -79,7 +79,7 @@
}
},
"Experts": {
"title": "Find an expert",
"title": "Trouver un expert",
"filter": {
"price": "Price from {from}€ to {to}€",
"duration": "Duration from {from}min to {to}min",

View File

@ -79,7 +79,7 @@
}
},
"Experts": {
"title": "Find an expert",
"title": "Trova un esperto",
"filter": {
"price": "Price from {from}€ to {to}€",
"duration": "Duration from {from}min to {to}min",

View File

@ -2,7 +2,7 @@ import { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios';
import { apiClient } from '../lib/apiClient';
type RequiredConfigParams<D = any> = Required<Pick<AxiosRequestConfig, 'url' | 'method'>> & Pick<AxiosRequestConfig<D>, 'data'>;
export type PageRequestConfig<D = any> = RequiredConfigParams<D> & { locale?: string, token?: string };
export type PageRequestConfig<D = any> = RequiredConfigParams<D> & Partial<Pick<AxiosRequestConfig, 'headers'>> & { locale?: string, token?: string };
export const apiRequest = async <T = any, K = any>(
baseParams: PageRequestConfig<T>,
@ -15,29 +15,30 @@ export const apiRequest = async <T = any, K = any>(
headers: {
'X-User-Language': baseParams?.locale || 'en',
'X-Referrer-Channel': 'site',
...(baseParams?.token ? { Authorization: `Bearer ${baseParams.token}` } : {})
...(baseParams?.token ? { Authorization: `Bearer ${baseParams.token}` } : {}),
...(baseParams.headers || {})
}
};
const response: AxiosResponse<K> = await apiClient.request<any, AxiosResponse<K>, T>(config as AxiosRequestConfig<T>);
return response.data;
} catch (err) {
const {
response: {
status: responseCode = null,
statusText = '',
data: { message = '', status: errorKey = '' } = {},
} = {},
code: statusCode = '',
} = err as AxiosError;
throw new Error(
JSON.stringify({
statusCode,
statusMessage: message || statusText,
responseCode,
errorKey,
}),
);
// const {
// response: {
// status: responseCode = null,
// statusText = '',
// data: { message = '', status: errorKey = '' } = {},
// } = {},
// code: statusCode = '',
// } = err as AxiosError;
//
// throw new Error(
// JSON.stringify({
// statusCode,
// statusMessage: message || statusText,
// responseCode,
// errorKey,
// }),
// );
}
};

View File

@ -1,18 +1,17 @@
'use client'
import { useCallback, useEffect, useState } from 'react';
import { Profile } from '../../types/profile';
import { getPersonalData } from '../profile';
import { ProfileData, ProfileRequest } from '../../types/profile';
import { getPersonalData, setPersonData } from '../profile';
import { useLocalStorage } from '../../hooks/useLocalStorage';
import { AUTH_TOKEN_KEY } from '../../constants/common';
export const useProfileSettings = (locale: string) => {
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
const [profileSettings, setProfileSettings] = useState<Profile | undefined>();
const [profileSettings, setProfileSettings] = useState<ProfileData | undefined>();
const [fetchLoading, setFetchLoading] = useState<boolean>(false);
const [saveLoading, setSaveLoading] = useState<boolean>(false);
useEffect(() => {
const fetchProfileSettings = () => {
if (jwt) {
getPersonalData(locale, jwt)
.then((data) => {
@ -25,16 +24,14 @@ export const useProfileSettings = (locale: string) => {
setFetchLoading(false);
});
}
}, []);
};
const save = useCallback(() => {
}, []);
const save = useCallback((data: ProfileRequest) => setPersonData(data, locale, jwt), []);
return {
fetchLoading,
fetchProfileSettings,
save,
saveLoading,
profileSettings
};
};

View File

@ -1,7 +1,25 @@
import { Profile } from '../types/profile';
import { PayInfo, Profile, ProfileRequest, ProfileData } from '../types/profile';
import { ExpertsTags } from '../types/tags';
import { EducationData, EducationDTO } from '../types/education';
import { PracticeData, PracticeDTO } from '../types/practice';
import { ScheduleDTO } from '../types/schedule';
import { apiRequest } from './helpers';
export const setPersonData = (data: { login: string, password: string, role: string, languagesLinks: any[] }, locale: string, token: string): Promise<{ userData: Profile }> => apiRequest({
export const getUserData = (locale: string, token: string): Promise<Profile> => apiRequest({
url: '/home/userdata',
method: 'post',
locale,
token
});
export const getPersonalData = (locale: string, token: string): Promise<ProfileData> => apiRequest({
url: '/home/person1',
method: 'post',
locale,
token
});
export const setPersonData = (data: ProfileRequest, locale: string, token: string): Promise<{ userData: Profile }> => apiRequest({
url: '/home/applyperson1',
method: 'post',
data,
@ -9,9 +27,77 @@ export const setPersonData = (data: { login: string, password: string, role: str
token
});
export const getPersonalData = (locale: string, token: string): Promise<Profile> => apiRequest({
url: '/home/userdata',
export const getEducation = (locale: string, token: string): Promise<EducationDTO> => apiRequest({
url: '/home/person2',
method: 'post',
locale,
token
});
export const setEducation = (locale: string, token: string, data: EducationData): Promise<EducationData> => apiRequest({
url: '/home/applyperson2',
method: 'post',
data,
locale,
token
});
export const getTags = (locale: string, token: string): Promise<ExpertsTags> => apiRequest({
url: '/home/person3',
method: 'post',
locale,
token
});
export const setTags = (locale: string, token: string, data: ExpertsTags): Promise<ExpertsTags> => apiRequest({
url: '/home/applyperson3',
method: 'post',
data,
locale,
token
});
export const getPractice = (locale: string, token: string): Promise<PracticeDTO> => apiRequest({
url: '/home/person4',
method: 'post',
locale,
token
});
export const setPractice = (locale: string, token: string, data: PracticeData): Promise<PracticeDTO> => apiRequest({
url: '/home/applyperson4',
method: 'post',
data,
locale,
token
});
export const getSchedule = (locale: string, token: string): Promise<ScheduleDTO> => apiRequest({
url: '/home/person51',
method: 'post',
locale,
token
});
export const setSchedule = (locale: string, token: string, data: ScheduleDTO): Promise<ScheduleDTO> => apiRequest({
url: '/home/applyperson51',
method: 'post',
data,
locale,
token
});
export const getPayData = (locale: string, token: string): Promise<{ person6Data?: PayInfo }> => apiRequest({
url: '/home/person6',
method: 'post',
locale,
token
});
export const setPayData = (locale: string, token: string, data: PayInfo): Promise<PayInfo> => apiRequest({
url: '/home/applyperson6',
method: 'post',
data,
locale,
token
});

13
src/actions/upload.ts Normal file
View File

@ -0,0 +1,13 @@
import { ExpertDocument } from '../types/file';
import { apiRequest } from './helpers';
export const setUploadFile = (locale: string, token: string, data: any): Promise<ExpertDocument> => apiRequest({
url: '/home/uploadfile',
method: 'post',
data,
locale,
token,
headers: {
'Content-Type': 'multipart/form-data'
}
});

View File

@ -7,12 +7,12 @@ export default function AddOffer() {
<>
<ol className="breadcrumb">
<li className="breadcrumb-item">
<Link href={'/account/work-with-us' as any}>
<Link href={'/account/expert-profile' as any}>
Work With Us
</Link>
</li>
<li className="breadcrumb-item">
<Link href={'/account/work-with-us/coaching' as any}>
<Link href={'/account/expert-profile/coaching' as any}>
Coaching
</Link>
</li>

View File

@ -7,7 +7,7 @@ export default function NewTopic() {
<>
<ol className="breadcrumb">
<li className="breadcrumb-item">
<Link href={'/account/work-with-us' as any}>
<Link href={'/account/expert-profile' as any}>
Work With Us
</Link>
</li>

View File

@ -0,0 +1,66 @@
'use client'
import { useEffect, useState } from 'react';
import { message } from 'antd';
// import { unstable_setRequestLocale } from 'next-intl/server';
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 { ExpertProfile } from '../../../../../components/ExpertProfile';
import { Loader } from '../../../../../components/view/Loader';
export default function ExpertProfilePage({ params: { locale } }: { params: { locale: string } }) {
// unstable_setRequestLocale(locale);
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
const [loading, setLoading] = useState<boolean>(false);
const [data, setData] = useState<ExpertData | undefined>();
useEffect(() => {
if (jwt) {
setLoading(true);
Promise.all([
getPersonalData(locale, jwt),
getEducation(locale, jwt),
getTags(locale, jwt),
getPractice(locale, jwt),
getSchedule(locale, jwt),
getPayData(locale, jwt)
])
.then(([person, education, tags, practice, schedule, payData]) => {
console.log('person', person);
console.log('education', education);
console.log('tags', tags);
console.log('practice', practice);
console.log('schedule', schedule);
console.log('payData', payData);
setData({
person,
education,
tags,
practice,
schedule,
payData
});
})
.catch(() => {
message.error('Не удалось загрузить данные эксперта');
})
.finally(() => {
setLoading(false);
})
}
}, [jwt]);
return (
<Loader isLoading={loading}>
{data && (
<ExpertProfile
locale={locale}
data={data}
updateData={setData}
/>
)}
</Loader>
);
};

View File

@ -1,18 +1,10 @@
import React, { Suspense } from 'react';
import type { Metadata } from 'next';
import { unstable_setRequestLocale } from 'next-intl/server';
import { useTranslations } from 'next-intl';
import { ProfileSettings } from '../../../../../components/Account';
import { i18nText } from '../../../../../i18nKeys';
export const metadata: Metadata = {
title: 'Bbuddy - Account - Profile Settings',
description: 'Bbuddy desc Profile settings'
};
export default function Settings({ params: { locale } }: { params: { locale: string } }) {
unstable_setRequestLocale(locale);
const t = useTranslations('Account.Settings');
return (
<>

View File

@ -1,133 +0,0 @@
import React from 'react';
import { unstable_setRequestLocale } from 'next-intl/server';
import { Link } from '../../../../../../navigation';
import { i18nText } from '../../../../../../i18nKeys';
export default function Coaching({ params: { locale } }: { params: { locale: string } }) {
unstable_setRequestLocale(locale);
return (
<>
<ol className="breadcrumb">
<li className="breadcrumb-item">
<Link href={'/account/work-with-us' as any}>
Work With Us
</Link>
</li>
<li className="breadcrumb-item active" aria-current="page">Coaching</li>
</ol>
<div className="coaching-info">
<div className="card-profile">
<div className="card-profile__header">
<div className="card-profile__header__portrait">
<img src="/images/person.png" className="" alt="" />
</div>
<div className="card-profile__header__inner">
<div className="card-profile__header__name">
David
</div>
<div className="card-profile__header__title">
12 Practice hours
</div>
<div className="card-profile__header__title ">
15 Supervision per year
</div>
</div>
</div>
</div>
<div className="coaching-info__wrap-btn">
<a href="#" className="btn-edit">Edit</a>
<a href="#" className="btn-apply">Add Offer</a>
</div>
</div>
<div className="coaching-section">
<h2 className="title-h2">
My Offers
</h2>
<div className="coaching-section__desc">
<div className="coaching-offer">
<div className="coaching-offer__header">
<div className="coaching-offer__title">
Senior Software Engineer
</div>
<div className="coaching-offer__wrap-btn">
<a href="#" className="link-edit">Edit</a>
<a href="#" className="link-remove">Remove</a>
</div>
</div>
<div className="coaching-offer__price">
45$ <span>/ 45min</span>
</div>
<div className="skills__list">
<div className="skills__list__item">Engineering & Data</div>
<div className="skills__list__item">Engineering & Data</div>
<div className="skills__list__more">+6</div>
</div>
<div className="coaching-offer__desc">
I have worked across a variety of organizations, lead teams, and delivered quality software
for 8 years. In that time I've worked as an independent consultant, at agencies as a team
lead, and as a senior engineer at Auth0. I also host a podcast
https://anchor.fm/work-in-programming where I break down how …
</div>
</div>
</div>
</div>
<div className="coaching-section">
<h2 className="title-h2">
About Coach
</h2>
<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 className="coaching-section">
<h2 className="title-h2">
Education
</h2>
<div className="coaching-section__desc">
<h3 className="title-h3">Psychologist </h3>
<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 className="sertific">
<img src="/images/sertific.png" className="" alt="" />
</div>
</div>
</div>
<div className="coaching-section">
<h2 className="title-h2">{i18nText('profCertification', locale)}</h2>
<div className="coaching-section__desc">
<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 className="coaching-section">
<h2 className="title-h2">
Trainings | Seminars | Courses
</h2>
<div className="coaching-section__desc">
<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 className="coaching-section">
<h2 className="title-h2">
MBA Information
</h2>
<div className="coaching-section__desc">
<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>
</>
);
}

View File

@ -1,25 +0,0 @@
import React from 'react';
import { unstable_setRequestLocale } from 'next-intl/server';
import { i18nText } from '../../../../../i18nKeys';
export default function WorkWithUs({ params: { locale } }: { params: { locale: string } }) {
unstable_setRequestLocale(locale);
return (
<>
<ol className="breadcrumb">
<li className="breadcrumb-item active" aria-current="page">{i18nText('accountMenu.work-with-us', locale)}</li>
</ol>
<div className="b-work">
<div className="image-info">
<img className="" src="/images/info.png" alt="" />
</div>
<div className="b-work__description">
<div className="b-work__text">{i18nText('insertInfo', locale)}</div>
<div className="b-work__text">{i18nText('changeUserData', locale)}</div>
<button className="btn-apply">{i18nText('getStarted', locale)}</button>
</div>
</div>
</>
);
}

View File

@ -6,4 +6,4 @@ export default function Loading() {
...loading
</div>
);
}
};

View File

@ -1,68 +1,103 @@
'use client';
import React, { FC, useEffect, useState } from 'react';
import { Form, Upload } from 'antd';
import type { UploadFile, UploadProps } from 'antd';
import { Button, Form, message, Upload } from 'antd';
import type { GetProp, UploadFile, UploadProps } from 'antd';
import ImgCrop from 'antd-img-crop';
import { CameraOutlined, DeleteOutlined } from '@ant-design/icons';
import { useRouter } from '../../navigation';
import { i18nText } from '../../i18nKeys';
import { Profile } from '../../types/profile';
import { ProfileRequest } from '../../types/profile';
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 { DeleteAccountModal } from "../Modals/DeleteAccountModal";
import { DeleteAccountModal } from '../Modals/DeleteAccountModal';
import { Loader } from '../view/Loader';
type ProfileSettingsProps = {
locale: string;
};
// type FileType = Parameters<GetProp<UploadProps, 'beforeUpload'>>[0];
type FileType = Parameters<GetProp<UploadProps, 'beforeUpload'>>[0];
export const ProfileSettings: FC<ProfileSettingsProps> = ({ locale }) => {
const [form] = Form.useForm<Profile>();
const { profileSettings } = useProfileSettings(locale);
const [form] = Form.useForm<ProfileRequest>();
const { profileSettings, fetchProfileSettings, save, fetchLoading } = useProfileSettings(locale);
const [showDeleteModal, setShowDeleteModal] = useState<boolean>(false);
const [saveLoading, setSaveLoading] = useState<boolean>(false);
const [photo, setPhoto] = useState<UploadFile | undefined>();
const router = useRouter();
useEffect(() => {
fetchProfileSettings()
}, []);
useEffect(() => {
if (profileSettings) {
form.setFieldsValue(profileSettings);
}
}, [profileSettings]);
const saveProfileSettings = () => {
const onSaveProfile = () => {
form.validateFields()
.then(({ login, surname, username }) => {
const { phone, role, languagesLinks } = profileSettings;
const newProfile: ProfileRequest = {
phone,
role,
login,
surname,
username,
isPasswordKeepExisting: true,
isFaceImageKeepExisting: true,
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;
// }
console.log(newProfile);
setSaveLoading(true);
save(newProfile)
.then(() => {
console.log('success')
fetchProfileSettings();
})
.catch(() => {
message.error('Не удалось сохранить изменения');
})
.finally(() => {
setSaveLoading(false);
})
})
}
const [fileList, setFileList] = useState<UploadFile[]>();
const beforeCrop = (file: UploadFile) => {
return validateImage(file, true);
}
const onChange: UploadProps['onChange'] = ({ fileList: newFileList }) => {
setFileList(newFileList);
};
const beforeUpload = (file: UploadFile) => {
const isValid = validateImage(file);
const onPreview = async (file: UploadFile) => {
// let src = file.url as string;
// if (!src) {
// src = await new Promise((resolve) => {
// const reader = new FileReader();
// reader.readAsDataURL(file.originFileObj as FileType);
// reader.onload = () => resolve(reader.result as string);
// });
// }
// const image = new Image();
// image.src = src;
// const imgWindow = window.open(src);
// imgWindow?.document.write(image.outerHTML);
};
if (isValid) {
setPhoto(file);
}
return false;
}
const onDeleteAccount = () => setShowDeleteModal(true);
return (
<Loader isLoading={fetchLoading} refresh={fetchProfileSettings}>
<Form form={form} className="form-settings">
<div className="user-avatar">
<div className="user-avatar__edit" style={profileSettings?.faceImageUrl ? { backgroundImage: `url(${profileSettings.faceImageUrl})` } : undefined}>
@ -71,19 +106,38 @@ export const ProfileSettings: FC<ProfileSettingsProps> = ({ locale }) => {
</div>
<div className="user-avatar__text">{i18nText('photoDesc', locale)}</div>
</div>
{/* <ImgCrop rotationSlider>
<Upload
action="https://run.mocky.io/v3/435e224c-44fb-4773-9faf-380c5e6a2188"
fileList={fileList}
onChange={onChange}
onPreview={onPreview}
<ImgCrop
modalTitle="Редактировать"
modalOk="Сохранить"
modalCancel="Отмена"
beforeCrop={beforeCrop}
>
<Upload
fileList={photo ? [photo] : profileSettings?.faceImageUrl ? [
{
uid: profileSettings.faceImageUrl,
name: profileSettings.faceImageUrl,
status: 'done',
url: profileSettings.faceImageUrl
}
] : undefined}
accept=".jpg,.jpeg,.png,.gif"
beforeUpload={beforeUpload}
multiple={false}
showUploadList={false}
>
{photo && <img height={100} width={100} src={URL.createObjectURL(photo)} />}
<Button icon={<CameraOutlined />}>Click to Upload</Button>
</Upload>
</ImgCrop> */}
</ImgCrop>
<div className="form-fieldset">
<div className="form-field">
<Form.Item name="username">
<Form.Item name="username" rules={[
{
required: true,
message: 'Поле не должно быть пустым'
}
]}>
<CustomInput placeholder={i18nText('name', locale)} />
</Form.Item>
</div>
@ -98,13 +152,23 @@ export const ProfileSettings: FC<ProfileSettingsProps> = ({ locale }) => {
</Form.Item>
</div> */}
<div className="form-field">
<Form.Item name="login">
<Form.Item name="login" rules={[
{
required: true,
message: 'Поле не должно быть пустым'
}
]}>
<CustomInput type="email" placeholder="E-mail" />
</Form.Item>
</div>
</div>
<div className="form-actions">
<FilledYellowButton onClick={saveProfileSettings}>{i18nText('save', locale)}</FilledYellowButton>
<FilledYellowButton
onClick={onSaveProfile}
loading={saveLoading}
>
{i18nText('save', locale)}
</FilledYellowButton>
<OutlinedButton onClick={() => router.push('change-password')}>
{i18nText('changePass', locale)}
</OutlinedButton>
@ -121,5 +185,6 @@ export const ProfileSettings: FC<ProfileSettingsProps> = ({ locale }) => {
handleCancel={() => setShowDeleteModal(false)}
/>
</Form>
</Loader>
);
};

View File

@ -283,7 +283,7 @@ export const SessionDetailsContent = ({ session, locale, activeType, startSessio
))}
{(isCoach ? session?.clientComments : session?.coachComments)?.length > 0 && (
<div className="card-detail__comments_title">
{isCoach ? 'Client Comments' : 'Coach Comments'}
{isCoach ? i18nText('session.clientComments', locale) : i18nText('session.coachComments', locale)}
</div>
)}
{(isCoach ? session?.clientComments : session?.coachComments)?.map(({ id , comment }) => (

View File

@ -0,0 +1,19 @@
import { i18nText } from '../../i18nKeys';
export const EmptyExpertProfile = ({ locale }: { locale: string }) => (
<>
<ol className="breadcrumb">
<li className="breadcrumb-item active" aria-current="page">{i18nText('accountMenu.expert-profile', locale)}</li>
</ol>
<div className="b-work">
<div className="image-info">
<img className="" src="/images/info.png" alt="" />
</div>
<div className="b-work__description">
<div className="b-work__text">{i18nText('insertInfo', locale)}</div>
<div className="b-work__text">{i18nText('changeUserData', locale)}</div>
<button className="btn-apply">{i18nText('getStarted', locale)}</button>
</div>
</div>
</>
);

View File

@ -0,0 +1,98 @@
'use client'
import { useState } from 'react';
import { message } from 'antd';
import { EditOutlined } from '@ant-design/icons';
import { i18nText } from '../../i18nKeys';
import { ExpertData } from '../../types/profile';
import { AUTH_TOKEN_KEY } from '../../constants/common';
import { useLocalStorage } from '../../hooks/useLocalStorage';
import { getTags } 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';
type ExpertProfileProps = {
locale: string;
data: ExpertData;
updateData: (data: ExpertData) => void;
};
export const ExpertProfile = ({ locale, data, updateData }: ExpertProfileProps) => {
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
const [loading, setLoading] = useState<(keyof ExpertData)[]>([]);
const updateExpert = (key: keyof ExpertData) => {
switch (key) {
case 'tags':
setLoading([key]);
getTags(locale, jwt)
.then((tags) => {
updateData({
...data,
tags
});
})
.catch(() => message.error('Не удалось обновить направления'))
.finally(() => setLoading([]));
break;
default:
break;
}
};
return (
<>
<ol className="breadcrumb">
<li className="breadcrumb-item active" aria-current="page">{i18nText('coaching', locale)}</li>
</ol>
<div className="coaching-info">
<div className="coaching-profile">
<div className="coaching-profile__portrait">
<img src="/images/person.png" className="" alt="" />
</div>
<div className="coaching-profile__inner">
<div className="coaching-profile__name">
David
</div>
</div>
</div>
<div className="coaching-section__wrap">
<div className="coaching-section">
<div className="coaching-section__title">
<h2 className="title-h2">{i18nText('aboutCoach', locale)}</h2>
<h2 className="title-h2">person1 + person4</h2>
<LinkButton
type="link"
icon={<EditOutlined />}
/>
</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>
<Loader isLoading={loading.includes('tags')}>
<ExpertTags
locale={locale}
data={data?.tags}
updateExpert={updateExpert}
/>
</Loader>
<ExpertSchedule locale={locale} data={data?.schedule} />
<ExpertEducation locale={locale} data={data?.education} />
<ExpertPayData locale={locale} data={data?.payData?.person6Data} />
</div>
</>
)
};

View File

@ -0,0 +1,34 @@
export const MyOffers = () => (
<div className="coaching-section">
<h2 className="title-h2">
My Offers
</h2>
<div className="coaching-section__desc">
<div className="coaching-offer">
<div className="coaching-offer__header">
<div className="coaching-offer__title">
Senior Software Engineer
</div>
<div className="coaching-offer__wrap-btn">
<a href="#" className="link-edit">Edit</a>
<a href="#" className="link-remove">Remove</a>
</div>
</div>
<div className="coaching-offer__price">
45$ <span>/ 45min</span>
</div>
<div className="skills__list">
<div className="skills__list__item">Engineering & Data</div>
<div className="skills__list__item">Engineering & Data</div>
<div className="skills__list__more">+6</div>
</div>
<div className="coaching-offer__desc">
I have worked across a variety of organizations, lead teams, and delivered quality software
for 8 years. In that time I've worked as an independent consultant, at agencies as a team
lead, and as a senior engineer at Auth0. I also host a podcast
https://anchor.fm/work-in-programming where I break down how …
</div>
</div>
</div>
</div>
);

View File

@ -0,0 +1,65 @@
import { EditOutlined } from '@ant-design/icons';
import { EducationDTO } from '../../../types/education';
import { i18nText } from '../../../i18nKeys';
import { LinkButton } from '../../view/LinkButton';
type ExpertEducationProps = {
locale: string;
data?: EducationDTO;
};
export const ExpertEducation = ({ locale, data }: ExpertEducationProps) => {
return (
<div className="coaching-section__wrap">
<div className="coaching-section">
<div className="coaching-section__title">
<h2 className="title-h2">{i18nText('education', locale)}</h2>
<h2 className="title-h2">person2</h2>
<LinkButton
type="link"
icon={<EditOutlined />}
/>
</div>
<div className="coaching-section__desc">
<h3 className="title-h3">Psychologist</h3>
<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 className="sertific">
<img src="/images/sertific.png" className="" alt="" />
</div>
</div>
</div>
<div className="coaching-section">
<h2 className="title-h2">{i18nText('profCertification', locale)}</h2>
<div className="coaching-section__desc">
<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 className="coaching-section">
<h2 className="title-h2">
{`${i18nText('trainings', locale)} | ${i18nText('seminars', locale)} | ${i18nText('courses', locale)}`}
</h2>
<div className="coaching-section__desc">
<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 className="coaching-section">
<h2 className="title-h2">{i18nText('mba', locale)}</h2>
<div className="coaching-section__desc">
<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>
);
};

View File

@ -0,0 +1,28 @@
import { EditOutlined } from '@ant-design/icons';
import { i18nText } from '../../../i18nKeys';
import { PayInfo } from '../../../types/profile';
import { LinkButton } from '../../view/LinkButton';
type ExpertPayDataProps = {
locale: string;
data?: PayInfo
};
export const ExpertPayData = ({ locale, data }: ExpertPayDataProps) => {
return (
<div className="coaching-section__wrap">
<div className="coaching-section">
<div className="coaching-section__title">
<h2 className="title-h2">Card data - person6</h2>
<LinkButton
type="link"
icon={<EditOutlined />}
/>
</div>
<div className="base-text">
Card
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,28 @@
import { EditOutlined } from '@ant-design/icons';
import { ScheduleDTO } from '../../../types/schedule';
import { i18nText } from '../../../i18nKeys';
import { LinkButton } from '../../view/LinkButton';
type ExpertScheduleProps = {
locale: string;
data?: ScheduleDTO;
};
export const ExpertSchedule = ({ locale, data }: ExpertScheduleProps) => {
return (
<div className="coaching-section__wrap">
<div className="coaching-section">
<div className="coaching-section__title">
<h2 className="title-h2">Schedule - person51</h2>
<LinkButton
type="link"
icon={<EditOutlined />}
/>
</div>
<div className="base-text">
Schedule
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,47 @@
'use client'
import { useState } from 'react';
import { Tag } from 'antd';
import { EditOutlined } from '@ant-design/icons';
import { i18nText } from '../../../i18nKeys';
import { ExpertsTags } from '../../../types/tags';
import { ExpertData } from '../../../types/profile';
import { LinkButton } from '../../view/LinkButton';
import { EditExpertTagsModal } from '../../Modals/EditExpertTagsModal';
type ExpertTagsProps = {
locale: string;
data?: ExpertsTags;
updateExpert: (key: keyof ExpertData) => void;
}
export const ExpertTags = ({ locale, data, updateExpert }: ExpertTagsProps) => {
const [showEdit, setShowEdit] = useState<boolean>(false);
return (
<div className="coaching-section__wrap">
<div className="coaching-section">
<div className="coaching-section__title">
<h2 className="title-h2">{i18nText('direction', locale)}</h2>
<LinkButton
type="link"
icon={<EditOutlined />}
onClick={() => setShowEdit(true)}
/>
</div>
<div className="skills__list">
{data?.themesTags && data.themesTags?.length > 0 && data.themesTags
.filter(({ isActive, isSelected }) => isActive && isSelected)
.map(({ id, name }) => <Tag key={id} className="skills__list__item">{name}</Tag>)}
</div>
</div>
<EditExpertTagsModal
locale={locale}
open={showEdit}
data={data}
handleCancel={() => setShowEdit(false)}
refresh={() => updateExpert('tags')}
/>
</div>
);
};

View File

@ -0,0 +1,5 @@
'use client'
export * from './EmptyExpertProfile';
export * from './ExpertProfile';
export * from './MyOffers';

View File

@ -98,7 +98,7 @@ export const ExpertPractice: FC<ExpertDetailsProps> = ({ expert, locale }) => {
return practiceCases?.length > 0 ? (
<div>
<h3 className="title-h3">Successful Cases From Practice</h3>
<h3 className="title-h3">{i18nText('successfulCase', locale)}</h3>
{practiceCases?.map(({ id, description, themesGroupIds }) => {
const filtered = themesGroups?.filter(({ id }) => themesGroupIds?.includes(+id));

View File

@ -0,0 +1,114 @@
'use client';
import React, {FC, useCallback, useEffect, useState} from 'react';
import { Modal, Button, List, message } from 'antd';
import { CloseOutlined } from '@ant-design/icons';
import { i18nText } from '../../i18nKeys';
import { useLocalStorage } from '../../hooks/useLocalStorage';
import { AUTH_TOKEN_KEY } from '../../constants/common';
import { ExpertsTags, Tag } from '../../types/tags';
import { CustomSwitch } from '../view/CustomSwitch';
import { setTags } from '../../actions/profile';
type EditExpertTagsModalProps = {
open: boolean;
handleCancel: () => void;
locale: string;
data?: ExpertsTags;
refresh: () => void;
};
export const EditExpertTagsModal: FC<EditExpertTagsModalProps> = ({
open,
handleCancel,
locale,
data,
refresh
}) => {
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
const [loading, setLoading] = useState<boolean>(false);
const [expertTags, setExpertTags] = useState<Tag[]>([]);
useEffect(() => {
setExpertTags(data?.themesTags || []);
}, [data]);
const onSaveTags = () => {
setLoading(true);
setTags(locale, jwt, { themesGroups: data?.themesGroups, themesTags: expertTags })
.then(() => {
handleCancel();
refresh();
})
.catch(() => {
message.error('Не удалось сохранить направления');
})
.finally(() => {
setLoading(false);
})
};
const updateTag = useCallback((id: number, isSelected: boolean) => {
setExpertTags(expertTags.map((tag) => {
if (tag.id === id) {
tag.isSelected = isSelected;
}
return tag;
}))
}, [expertTags, setExpertTags]);
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('direction', locale)}</div>
<div className="b-modal__expert__inner">
{data?.themesGroups && data.themesGroups.filter(({ isActive }) => isActive).map(({ id, name }) => (
<div key={`group_${id}`}>
<h3 className="title-h4">{name}</h3>
{expertTags?.length > 0 ? (
<div className="b-filter__inner">
<List
itemLayout="vertical"
size="small"
dataSource={expertTags.filter(({ isActive, groupId }) => (isActive && groupId == id)) || []}
split={false}
style={{ width: '100%' }}
renderItem={({ id, name, isSelected }) => (
<List.Item key={`tag_${id}`} style={{ padding: 0 }}>
<div className="b-filter__item">
<div className="b-filter__title">{name}</div>
<CustomSwitch
defaultChecked={isSelected || false}
onChange={(checked: boolean) => updateTag(id, checked)}
/>
</div>
</List.Item>
)}
/>
</div>
) : <div>No tags</div>}
</div>
))}
</div>
<div className="b-modal__expert__button">
<Button
className="card-detail__apply"
onClick={onSaveTags}
loading={loading}
>
{i18nText('save', locale)}
</Button>
</div>
</div>
</Modal>
);
};

View File

@ -4,9 +4,9 @@ export default {
notifications: 'Benachrichtigung',
support: 'Hilfe & Support',
information: 'Rechtliche Informationen',
settings: 'Profileinstellungen',
settings: 'Kontoeinstellungen',
messages: 'Nachrichten',
'work-with-us': 'Arbeite mit uns'
'expert-profile': 'Expertenprofil'
},
menu: {
'bb-client': 'Mit BB wachsen',
@ -41,6 +41,8 @@ export default {
myComments: 'Meine Kommentare',
addComment: 'Neuen Kommentar hinzufügen',
commentPlaceholder: 'Ihr Kommentar',
clientComments: 'Kundenkommentare',
coachComments: 'Trainerkommentare'
},
room: {
upcoming: 'Zukünftige Räume',
@ -84,6 +86,7 @@ export default {
fromTo: 'von $ bis $',
apply: 'Anwenden',
save: 'Speichern',
edit: 'Bearbeiten',
changePass: 'Passwort ändern',
resetPass: 'Passwort zurücksetzen',
getStarted: 'Loslegen',
@ -98,9 +101,18 @@ export default {
supervisionCount: 'Supervision pro Jahr',
outOf: 'von',
schedule: 'Zeitplan',
successfulCase: 'Erfolgreiche Fälle aus der Praxis',
signUp: 'Jetzt anmelden',
noData: 'Keine Daten',
notFound: 'Nicht gefunden',
trainings: 'Trainings',
seminars: 'Seminare',
courses: 'Kurse',
mba: 'MBA-Information',
aboutCoach: 'Über Coach',
education: 'Bildung',
coaching: 'Coaching',
errors: {
invalidEmail: 'Die E-Mail-Adresse ist ungültig',
emptyEmail: 'Bitte geben Sie Ihre E-Mail ein',

View File

@ -4,9 +4,9 @@ export default {
notifications: 'Notification',
support: 'Help & Support',
information: 'Legal Information',
settings: 'Profile Settings',
settings: 'Account Settings',
messages: 'Messages',
'work-with-us': 'Work With Us'
'expert-profile': 'Expert profile'
},
menu: {
'bb-client': 'Start grow with BB',
@ -41,6 +41,8 @@ export default {
myComments: 'My comments',
addComment: 'Add new',
commentPlaceholder: 'Your comment',
clientComments: 'Client Comments',
coachComments: 'Coach Comments'
},
room: {
upcoming: 'Upcoming Rooms',
@ -84,6 +86,7 @@ export default {
fromTo: 'from $ to $',
apply: 'Apply',
save: 'Save',
edit: 'Edit',
changePass: 'Change password',
resetPass: 'Reset password',
getStarted: 'Get started',
@ -98,9 +101,17 @@ export default {
supervisionCount: 'Supervision per year',
outOf: 'out of',
schedule: 'Schedule',
successfulCase: 'Successful Cases From Practice',
signUp: 'Sign up now',
noData: 'No data',
notFound: 'Not found',
trainings: 'Trainings',
seminars: 'Seminars',
courses: 'Courses',
mba: 'MBA Information',
aboutCoach: 'About Coach',
education: 'Education',
coaching: 'Coaching',
errors: {
invalidEmail: 'The email address is not valid',
emptyEmail: 'Please enter your E-mail',

View File

@ -4,9 +4,9 @@ export default {
notifications: 'Notificación',
support: 'Ayuda y asistencia',
information: 'Información jurídica',
settings: 'Ajustes del perfil',
settings: 'Ajustes de cuenta',
messages: 'Mensajes',
'work-with-us': 'Trabaja con nosotros'
'expert-profile': 'Perfil del experto'
},
menu: {
'bb-client': 'Empieza a crecer con BB',
@ -41,6 +41,8 @@ export default {
myComments: 'Mis comentarios',
addComment: 'Añadir nuevo comentario',
commentPlaceholder: 'Tu comentario',
clientComments: 'Comentarios del cliente',
coachComments: 'Comentarios del entrenador'
},
room: {
upcoming: 'Próximas salas',
@ -84,6 +86,7 @@ export default {
fromTo: 'de $ a $',
apply: 'Solicitar',
save: 'Guardar',
edit: 'Editar',
changePass: 'Cambiar contraseña',
resetPass: 'Restablecer contraseña',
getStarted: 'Empieza',
@ -94,13 +97,22 @@ export default {
courseInfo: 'Información del curso',
expertBackground: 'Antecedentes del experto',
profCertification: 'Certificación profesional',
practiceHours: 'horas de práctica',
supervisionCount: 'supervisiones anuales',
practiceHours: 'Horas de práctica',
supervisionCount: 'Supervisiones anuales',
outOf: 'de',
schedule: 'Horario',
successfulCase: 'Casos de éxito de la práctica',
signUp: 'Regístrate ahora',
noData: 'Sin datos',
notFound: 'No encontrado',
trainings: 'Formación',
seminars: 'Seminarios',
courses: 'Cursos',
mba: 'Información sobre máster en ADE (MBA)',
aboutCoach: 'Sobre el coach',
education: 'Educación',
coaching: 'Coaching',
errors: {
invalidEmail: 'La dirección de correo electrónico no es válida',
emptyEmail: 'Introduce tu correo electrónico',

View File

@ -4,9 +4,9 @@ export default {
notifications: 'Notification',
support: 'Aide et support',
information: 'Informations légales',
settings: 'Paramètres du profil',
settings: 'Paramètres du compte',
messages: 'Messages',
'work-with-us': 'Travaillez avec nous'
'expert-profile': 'Profil de l\'expert'
},
menu: {
'bb-client': 'Commencez à vous développer avec BB',
@ -41,6 +41,8 @@ export default {
myComments: 'Mes commentaires',
addComment: 'Ajouter un nouveau commentaire',
commentPlaceholder: 'Votre commentaire',
clientComments: 'Commentaires du client',
coachComments: 'Commentaires du coach'
},
room: {
upcoming: 'Salles futures',
@ -84,6 +86,7 @@ export default {
fromTo: 'de $ à $',
apply: 'Appliquer',
save: 'Sauvegarder',
edit: 'Modifier',
changePass: 'Modifier le mot de passe',
resetPass: 'Réinitialiser le mot de passe',
getStarted: 'Commencer',
@ -94,13 +97,22 @@ export default {
courseInfo: 'Infos sur le cours',
expertBackground: 'Antécédents de l\'expert',
profCertification: 'Certification professionnelle',
practiceHours: 'heures de pratique',
practiceHours: 'Heures de pratique',
supervisionCount: 'Supervision par an',
outOf: 'sur',
schedule: 'Programme',
successfulCase: 'Cas réussis de la pratique',
signUp: 'Inscrivez-vous maintenant',
noData: 'Aucune donnée',
notFound: 'Non trouvé',
trainings: 'Formations',
seminars: 'Séminaires',
courses: 'Cours',
mba: 'Infos Maîtrise en gestion',
aboutCoach: 'À propos du coach',
education: 'Éducation',
coaching: 'Coaching',
errors: {
invalidEmail: 'L\'adresse e-mail n\'est pas valide',
emptyEmail: 'Veuillez saisir votre e-mail',

View File

@ -4,9 +4,9 @@ export default {
notifications: 'Notifica',
support: 'Assistenza e supporto',
information: 'Informazioni legali',
settings: 'Impostazioni profilo',
settings: 'Impostazioni account',
messages: 'Messaggi',
'work-with-us': 'Lavora con noi'
'expert-profile': 'Profilo dell\'esperto'
},
menu: {
'bb-client': 'Inizia a crescere con BB',
@ -41,6 +41,8 @@ export default {
myComments: 'I miei commenti',
addComment: 'Aggiungi nuovo commento',
commentPlaceholder: 'Il tuo commento',
clientComments: 'Commenti del cliente',
coachComments: 'Commenti dell\'allenatore'
},
room: {
upcoming: 'Prossime sale',
@ -84,6 +86,7 @@ export default {
fromTo: 'da $ a $',
apply: 'Applica',
save: 'Salva',
edit: 'Modifica',
changePass: 'Cambia password',
resetPass: 'Reimposta password',
getStarted: 'Inizia',
@ -94,13 +97,22 @@ export default {
courseInfo: 'Informazioni sul corso',
expertBackground: 'Background esperto',
profCertification: 'Certificazione professionale',
practiceHours: 'ore di pratica',
supervisionCount: 'supervisioni per anno',
practiceHours: 'Ore di pratica',
supervisionCount: 'Supervisioni per anno',
outOf: 'su',
schedule: 'Programma',
successfulCase: 'Casi di successo dalla pratica',
signUp: 'Iscriviti ora',
noData: 'Nessun dato',
notFound: 'Non trovato',
trainings: 'Training',
seminars: 'Seminari',
courses: 'Corsi',
mba: 'Info sull\'MBA',
aboutCoach: 'Informazioni sul coach',
education: 'Istruzione',
coaching: 'Coaching',
errors: {
invalidEmail: 'L\'indirizzo e-mail non è valido',
emptyEmail: 'Inserisci l\'e-mail',

View File

@ -4,9 +4,9 @@ export default {
notifications: 'Уведомления',
support: 'Служба поддержки',
information: 'Юридическая информация',
settings: 'Настройки профиля',
settings: 'Настройки учетной записи',
messages: 'Сообщения',
'work-with-us': 'Сотрудничество'
'expert-profile': 'Профиль эксперта'
},
menu: {
'bb-client': 'Начните свой рост с BB',
@ -41,6 +41,8 @@ export default {
myComments: 'Мои комментарии',
addComment: 'Добавить новый',
commentPlaceholder: 'Ваш комментарий',
clientComments: 'Комментарии клиента',
coachComments: 'Комментарии коуча'
},
room: {
upcoming: 'Предстоящие комнаты',
@ -84,6 +86,7 @@ export default {
fromTo: 'от $ до $',
apply: 'Применить',
save: 'Сохранить',
edit: 'Редактировать',
changePass: 'Изменить пароль',
resetPass: 'Сбросить пароль',
getStarted: 'Начать работу',
@ -94,13 +97,22 @@ export default {
courseInfo: 'Информация о курсе',
expertBackground: 'Профессиональный опыт эксперта',
profCertification: 'Профессиональная сертификация',
practiceHours: 'часов практики',
supervisionCount: 'часов супервизии в год',
practiceHours: 'Часов практики',
supervisionCount: 'Часов супервизии в год',
outOf: 'из',
schedule: 'Расписание',
successfulCase: 'Успешные случаи из практики',
signUp: 'Записаться сейчас',
noData: 'Нет данных',
notFound: 'Не найдено',
trainings: 'Тренинги',
seminars: 'Семинары',
courses: 'Курсы',
mba: 'Информация о MBA',
aboutCoach: 'О коуче',
education: 'Образование',
coaching: 'Коучинг',
errors: {
invalidEmail: 'Адрес электронной почты недействителен',
emptyEmail: 'Пожалуйста, введите ваш E-mail',

View File

@ -12,7 +12,10 @@ export const onSuccessRequestCallback = (config: InternalAxiosRequestConfig) =>
// if (IS_DEV && !newConfig.headers.Authorization && getAuthToken()) {
// newConfig.headers.Authorization = `Bearer ${getAuthToken()}`;
// }
if (!newConfig.headers['Content-Type']) {
newConfig.headers['Content-Type'] = 'application/json';
}
return newConfig;
};

View File

@ -8,6 +8,21 @@ body{
--font: var(--font-comfortaa), -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-family: var(--font);
background-color: #ffffff;
& * {
&::-webkit-scrollbar {
width: 8px;
height: 8px;
}
&::-webkit-scrollbar-track {
background: #C4DFE6;
border-radius: 8px;
}
&::-webkit-scrollbar-thumb {
background-color: #2C7873;
border-radius: 8px;
}
}
}
*::selection {

View File

@ -32,6 +32,55 @@
padding: 44px 40px;
gap: 24px;
}
&__expert {
&__title {
color: #003B46;
@include rem(20);
font-style: normal;
font-weight: 600;
line-height: 133.333%;
}
&__button {
width: 100%;
button {
width: 100% !important;
}
}
&__inner {
height: 60vh;
overflow-y: auto;
& > div {
display: flex;
flex-direction: column;
padding-right: 20px;
& > * {
width: 100%;
}
}
}
&__content {
display: flex;
flex-direction: column;
padding: 40px;
gap: 24px;
.title-h4 {
color: #003B46;
@include rem(16);
font-style: normal;
font-weight: 700;
line-height: 150%;
padding-bottom: 16px;
}
}
}
}
.ant-modal-mask {

View File

@ -1140,15 +1140,13 @@
height: 146px;
object-fit: cover;
}
}
.coaching-info {
display: flex;
flex-flow: column;
justify-content: space-between;
gap: 24px;
margin-bottom: 24px;
gap: 16px;
.card-profile {
border: none !important;
@ -1172,9 +1170,6 @@
}
@media (min-width: 768px) {
flex-flow: nowrap;
gap: 10px;
&__wrap-btn {
display: flex;
gap: 10px;
@ -1183,8 +1178,66 @@
}
}
.coaching-profile {
display: flex;
gap: 16px;
align-items: flex-start;
&__portrait {
width: 86px;
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;
img {
object-fit: cover;
width: 100%;
height: 100%;
display: block;
border-radius: 16px;
}
}
&__inner {
padding-top: 10px;
}
&__name {
color: #003B46;
@include rem(18);
font-weight: 600;
line-height: 150%;
}
}
.coaching-section {
margin-bottom: 24px;
&__wrap {
border-top: 1px solid #C4DFE6;
padding-top: 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
&__title {
display: flex;
width: 100%;
justify-content: space-between;
align-self: center;
.b-button__link {
height: 24px !important;
}
}
.title-h2 {
color: #003B46;
@include rem(18);
line-height: 24px;
}
.base-text {
margin-bottom: 0;

44
src/types/education.ts Normal file
View File

@ -0,0 +1,44 @@
import { ExpertDocument } from './file';
export type Details = {
id: number;
userId?:number;
title?: string;
description?: string;
document?: ExpertDocument;
};
export type Certificate = {
id: number;
userId?: number;
associationLevelId?: number;
document?: ExpertDocument;
};
export type Experience = {
id: number,
userId?: number,
title?: string,
description?: string
};
export type Association = {
id: number;
name?: string;
};
export type AssociationLevel = Association & { associationId?: number };
export type EducationData = {
certificates?: Certificate[],
educations?: Details[],
trainings?: Details[],
mbas?: Details[],
experiences?: Experience[]
};
export interface EducationDTO {
person2Data?: EducationData,
associations?: Association[],
associationLevels?: AssociationLevel[]
}

View File

@ -1,4 +1,5 @@
import { Tag, ThemeGroups } from './tags';
import { Association, AssociationLevel, EducationData } from './education';
export type GeneralFilter = Filter & AdditionalFilter;
@ -19,34 +20,6 @@ export type AdditionalFilter = {
coachSort?: string;
};
export type File = {
id: number;
fileType: string;
url: string;
};
export interface ExpertDocument {
fileName: string;
original?: File;
preview?: File;
fullSize?: File;
}
export type Details = {
id: number;
userId?:number;
title?: string;
description?: string;
document?: ExpertDocument;
};
export type Certificate = {
id: number;
userId?: number;
associationLevelId?: number;
document?: ExpertDocument;
};
export type Practice = {
id: number;
userId?: number;
@ -61,16 +34,6 @@ export type ThemeGroup = {
canDeleted?: boolean;
};
export type Association = {
id: number;
name: string;
};
export type AssociationLevel = {
id: number;
associationId: number;
name: string;
};
export interface ExpertItem {
id: number;
@ -98,14 +61,9 @@ export type ExpertsData = {
};
export type ExpertDetails = {
publicCoachDetails: ExpertItem & {
publicCoachDetails: ExpertItem & EducationData & {
practiceHours?: number;
supervisionPerYearId?: number;
educations?: Details[];
certificates?: Certificate[];
trainings?: Details[];
mbas?: Details[];
experiences?: Details[];
practiceCases?: Practice[];
themesGroups?: ThemeGroup[];
};

12
src/types/file.ts Normal file
View File

@ -0,0 +1,12 @@
export type File = {
id: number;
fileType: string;
url: string;
};
export interface ExpertDocument {
fileName: string;
original?: File;
preview?: File;
fullSize?: File;
}

29
src/types/practice.ts Normal file
View File

@ -0,0 +1,29 @@
import { ExpertsThemesGroups } from './tags';
export type Supervision = {
id: number,
name: string
};
export type PracticeCase = {
id: number,
userId?: number,
description?: string,
themesGroupIds?: number[]
};
export type PracticeData = {
practiceHours?: number,
supervisionPerYearId?: number,
sessionDuration?: number,
sessionCost?: number,
practiceCases?: PracticeCase[]
}
export interface PracticeDTO {
person4Data: PracticeData & {
themesGroups?: ExpertsThemesGroups[],
supervisionPerYears?: Supervision[],
sessionCosts?: number[]
}
}

View File

@ -1,5 +1,10 @@
export type Profile = {
id: number;
import { UploadFile } from 'antd';
import {EducationDTO} from "./education";
import {ExpertsTags} from "./tags";
import {PracticeDTO} from "./practice";
import {ScheduleDTO} from "./schedule";
export type ProfileData = {
username?: string;
surname?: string;
fillProgress?: string;
@ -9,4 +14,35 @@ export type Profile = {
hasPassword?: boolean;
hasExternalLogin?: boolean;
isTestMode?: boolean;
phone?: string;
languagesLinks?: { language: { id: number, code: string, nativeSpelling: string }, languageId: number }[]
}
export type Profile = ProfileData & { id: number };
export type ProfileRequest = {
login?: string;
password?: string;
isPasswordKeepExisting?: boolean;
languagesLinks?: { languageId: number }[];
username?: string;
surname?: string;
faceImage?: UploadFile;
isFaceImageKeepExisting?: boolean;
phone?: string;
};
export type PayInfo = {
beneficiaryName?: string,
iban?: string,
bicOrSwift?: string
};
export interface ExpertData {
person?: ProfileData,
education?: EducationDTO,
tags?: ExpertsTags,
practice?: PracticeDTO,
schedule?: ScheduleDTO,
payData?: { person6Data?: PayInfo },
}

10
src/types/schedule.ts Normal file
View File

@ -0,0 +1,10 @@
export type WorkingTime = {
startDayOfWeekUtc?: string,
startTimeUtc?: number,
endDayOfWeekUtc?: string,
endTimeUtc?: number
}
export interface ScheduleDTO {
workingTimes?: WorkingTime[]
}

View File

@ -4,6 +4,9 @@ export type Tag = {
name: string
couchCount?: number;
group?: string | null;
isActive?: boolean,
isSelected?: boolean,
canDeleted?: boolean
};
export type ThemeGroups = {
@ -27,3 +30,15 @@ export type Language = {
}
export type Languages = Language[];
export type ExpertsThemesGroups = {
id: number,
name: string,
isActive?: boolean,
canDeleted?: boolean
};
export interface ExpertsTags {
themesGroups?: ExpertsThemesGroups[],
themesTags?: Tag[]
}

View File

@ -1,6 +1,8 @@
import { message } from 'antd';
import type { UploadFile } from 'antd';
import { i18nText } from '../i18nKeys';
const ROUTES = ['sessions', 'notifications', 'support', 'information', 'settings', 'messages', 'work-with-us'];
const ROUTES = ['sessions', 'notifications', 'support', 'information', 'settings', 'messages', 'expert-profile'];
const COUNTS: Record<string, number> = {
sessions: 12,
notifications: 5,
@ -12,3 +14,17 @@ export const getMenuConfig = (locale: string) => ROUTES.map((path) => ({
title: i18nText(`accountMenu.${path}`, locale),
count: COUNTS[path] || undefined
}));
export const validateImage = (file: UploadFile, showMessage?: boolean): boolean => {
const isImage = file.type === 'image/jpg' || file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'image/gif';
if (!isImage && showMessage) {
message.error('You can only upload JPG/PNG file');
}
const isLt5M = file.size / 1024 / 1024 <= 5;
if (!isLt5M && showMessage) {
message.error('Image must smaller than 5MB');
}
return isImage && isLt5M;
};