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

View File

@ -1,5 +1,5 @@
import React from 'react';
import type { Metadata } from 'next';
import type {Metadata, ResolvingMetadata} from 'next';
import { draftMode } from 'next/headers'
import { notFound } from 'next/navigation';
import {fetchBlogPost, fetchBlogPosts, Widget} from "../../../../lib/contentful/blogPosts";
@ -23,7 +23,7 @@ export async function generateMetadata({ params }: BlogPostPageProps, parent: Re
return {
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?.mbas && expert.publicCoachDetails.mbas?.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>
<div className="offers-list">

View File

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

View File

@ -1,43 +1,96 @@
'use client'
import { useState } from 'react';
import { message } from 'antd';
import { EditOutlined } from '@ant-design/icons';
import React, { useState } from 'react';
import {Alert, message} from 'antd';
import Image from 'next/image';
import { i18nText } from '../../i18nKeys';
import { ExpertData } from '../../types/profile';
import { ExpertData, PayInfo, ProfileData } from '../../types/profile';
import { ExpertsTags } from '../../types/tags';
import { PracticeDTO } from '../../types/practice';
import { EducationDTO } from '../../types/education';
import { ScheduleDTO } from '../../types/schedule';
import { AUTH_TOKEN_KEY } from '../../constants/common';
import { useLocalStorage } from '../../hooks/useLocalStorage';
import { getTags } from '../../actions/profile';
import { getTags, getPayData, getEducation, getPractice, getSchedule, getPersonalData } from '../../actions/profile';
import { Loader } from '../view/Loader';
import { LinkButton } from '../view/LinkButton';
import { ExpertTags } from './content/ExpertTags';
import { ExpertSchedule } from './content/ExpertSchedule';
import { ExpertPayData } from './content/ExpertPayData';
import { ExpertEducation } from './content/ExpertEducation';
import { ExpertAbout } from './content/ExpertAbout';
type ExpertProfileProps = {
locale: string;
data: ExpertData;
updateData: (data: ExpertData) => void;
isFull: boolean;
};
export const ExpertProfile = ({ locale, data, updateData }: ExpertProfileProps) => {
type NewDataPartProps<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 [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) => {
switch (key) {
case 'tags':
setLoading([key]);
getTags(locale, jwt)
.then((tags) => {
updateData({
...data,
tags
getNewPartData<ExpertsTags>({
key,
getNewData: getTags,
errorMessage: 'Не удалось получить направления'
});
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;
default:
break;
@ -52,36 +105,39 @@ export const ExpertProfile = ({ locale, data, updateData }: ExpertProfileProps)
<div className="coaching-info">
<div className="coaching-profile">
<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 className="coaching-profile__inner">
<div className="coaching-profile__inner" style={{ flex: 1 }}>
<div className="coaching-profile__name">
David
{`${data?.person?.username} ${data?.person?.surname || ''}`}
</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 />}
{!isFull && (
<Alert
message="Проверьте заполненность блоков"
description={(
<ul className="b-rules-list">
<li>о себе</li>
<li>темы сессии</li>
<li>рабочее расписание</li>
<li>информация об образовании</li>
<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>
<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')}>
<ExpertTags
locale={locale}
@ -90,9 +146,21 @@ export const ExpertProfile = ({ locale, data, updateData }: ExpertProfileProps)
/>
</Loader>
<ExpertSchedule locale={locale} data={data?.schedule} />
<ExpertEducation locale={locale} data={data?.education} />
<ExpertPayData locale={locale} data={data?.payData?.person6Data} />
<Loader isLoading={loading.includes('education')}>
<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>
</>
)
);
};

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 { EducationDTO } from '../../../types/education';
import { i18nText } from '../../../i18nKeys';
import { LinkButton } from '../../view/LinkButton';
import {ExpertCertificate} from "../../Experts/ExpertDetails";
import {useState} from "react";
import {ExpertData} from "../../../types/profile";
import {EditExpertEducationModal} from "../../Modals/EditExpertEducationModal";
type ExpertEducationProps = {
locale: string;
data?: EducationDTO;
updateExpert: (key: keyof ExpertData) => void;
};
export const ExpertEducation = ({ locale, data, 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 '';
};
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>
<h2 className="title-h2">{i18nText('skillsInfo', locale)}</h2>
<LinkButton
type="link"
icon={<EditOutlined />}
onClick={() => setShowEdit(true)}
/>
</div>
{data?.person2Data?.educations?.length > 0 && (
<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>
{data?.person2Data?.educations?.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">
<img src="/images/sertific.png" className="" alt="" />
<ExpertCertificate document={document} />
</div>
)}
</div>
))}
</div>
)}
</div>
{data?.person2Data?.certificates?.length > 0 && (
<div className="coaching-section">
<h2 className="title-h2">{i18nText('profCertification', locale)}</h2>
<div className="coaching-section__desc">
{data?.person2Data?.certificates?.map((cert) => (
<div key={cert.id}>
<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>
{`${getAssociationLevel(cert?.associationLevelId)} ${getAssociation(cert?.associationLevelId)}`}
</div>
{cert.document && (
<div className="sertific">
<ExpertCertificate document={cert.document} />
</div>
)}
</div>
))}
</div>
</div>
)}
{data?.person2Data?.trainings?.length > 0 && (
<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>
{data?.person2Data?.trainings?.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>
)}
{data?.person2Data?.mbas?.length > 0 && (
<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.
{data?.person2Data?.mbas?.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>
)}
{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>
)}
<EditExpertEducationModal
open={showEdit}
handleCancel={() => setShowEdit(false)}
locale={locale}
data={data}
refresh={() => updateExpert('education')}
/>
</div>
);
};

View File

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

View File

@ -1,7 +1,11 @@
import { EditOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import { ScheduleDTO } from '../../../types/schedule';
import { i18nText } from '../../../i18nKeys';
import { LinkButton } from '../../view/LinkButton';
import {useState} from "react";
import {Tag} from "antd";
import {getCurrentTime} from "../../../utils/time";
type ExpertScheduleProps = {
locale: string;
@ -9,18 +13,34 @@ type ExpertScheduleProps = {
};
export const ExpertSchedule = ({ locale, data }: ExpertScheduleProps) => {
const [showEdit, setShowEdit] = useState<boolean>(false);
// person51
return (
<div className="coaching-section__wrap">
<div className="coaching-section">
<div className="coaching-section__title">
<h2 className="title-h2">Schedule - person51</h2>
<LinkButton
<h2 className="title-h2">{i18nText('schedule', locale)}</h2>
{/*<LinkButton
type="link"
icon={<EditOutlined />}
/>
onClick={() => setShowEdit(true)}
/>*/}
</div>
<div className="base-text">
Schedule
<div className="b-schedule-list">
{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>

View File

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

View File

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

View File

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

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' }}/>}
>
<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">
{data?.themesGroups && data.themesGroups.filter(({ isActive }) => isActive).map(({ id, name }) => (
<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}
</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';
export const LinkButton = (props: any) => (
<Button className="b-button__link" {...props}>
<Button className={`b-button__link${props?.danger ? ' danger': ''}`} {...props}>
{props.children}
</Button>
);

View File

@ -105,6 +105,7 @@ export default {
signUp: 'Jetzt anmelden',
noData: 'Keine Daten',
notFound: 'Nicht gefunden',
skillsInfo: 'Fähigkeiten-Infos',
trainings: 'Trainings',
seminars: 'Seminare',
courses: 'Kurse',
@ -112,7 +113,38 @@ export default {
aboutCoach: 'Über Coach',
education: 'Bildung',
coaching: 'Coaching',
experiences: 'Praktische Erfahrung',
payInfo: 'Zahlungsdaten',
sessionDuration: 'Sitzungsdauer',
experienceHours: 'Gesamtstunden praktischer Erfahrung',
topics: 'Themen',
selectTopic: 'Thema auswählen',
title: 'Titel',
description: 'Beschreibung',
sessionCost: 'Sitzungskosten in Euro',
yourTimezone: 'Deine Zeitzone',
workTime: 'Arbeitszeit',
startAt: 'Beginn um',
finishAt: 'Ende um',
addWorkingHours: 'Arbeitszeiten hinzufügen',
specialisation: 'Spezialisierung',
selectSpecialisation: 'Wählen Sie Ihre Spezialisierung, um fortzufahren',
fillWeeklySchedule: 'Trage Sachen in deinen Wochenplan ein',
beneficiaryName: 'Name des Empfängers',
bicOrSwift: 'BIC/Swift-Code',
association: 'Verband',
level: 'Stufe',
addDiploma: 'Zertifikat hinzufügen',
university: 'Institution',
sunday: 'So',
monday: 'Mo',
tuesday: 'Di',
wednesday: 'Mi',
thursday: 'Do',
friday: 'Fr',
saturday: 'Sa',
addNew: 'Neu hinzufügen',
mExperiences: 'Führungserfahrung',
errors: {
invalidEmail: 'Die E-Mail-Adresse ist ungültig',
emptyEmail: 'Bitte geben Sie Ihre E-Mail ein',

View File

@ -110,8 +110,41 @@ export default {
courses: 'Courses',
mba: 'MBA Information',
aboutCoach: 'About Coach',
skillsInfo: 'Skills Info',
education: 'Education',
coaching: 'Coaching',
experiences: 'Practical experience',
payInfo: 'Payment Info',
sessionDuration: 'Session duration',
experienceHours: 'Total hours of practical experience',
topics: 'Topics',
selectTopic: 'Select Topic',
title: 'Title',
description: 'Description',
sessionCost: 'Session cost in euro',
yourTimezone: 'Your timezone',
workTime: 'Work time',
startAt: 'Start at',
finishAt: 'Finish at',
addWorkingHours: 'Add working hours',
specialisation: 'Specialisation',
selectSpecialisation: 'Select your specialisation to proceed',
fillWeeklySchedule: 'Fill up your weekly schedule',
beneficiaryName: 'Beneficiary Name',
bicOrSwift: 'BIC/Swift code',
association: 'Association',
level: 'Level',
addDiploma: 'Add Diploma',
university: 'Institution',
sunday: 'Su',
monday: 'Mo',
tuesday: 'Tu',
wednesday: 'We',
thursday: 'Th',
friday: 'Fr',
saturday: 'Sa',
addNew: 'Add New',
mExperiences: 'Managerial Experience',
errors: {
invalidEmail: 'The email address is not valid',
emptyEmail: 'Please enter your E-mail',

View File

@ -105,6 +105,7 @@ export default {
signUp: 'Regístrate ahora',
noData: 'Sin datos',
notFound: 'No encontrado',
skillsInfo: 'Información',
trainings: 'Formación',
seminars: 'Seminarios',
courses: 'Cursos',
@ -112,7 +113,38 @@ export default {
aboutCoach: 'Sobre el coach',
education: 'Educación',
coaching: 'Coaching',
experiences: 'Experiencia práctica',
payInfo: 'Información de pago',
sessionDuration: 'Duración de la sesión',
experienceHours: 'Total de horas de experiencia práctica',
topics: 'Temas',
selectTopic: 'Seleccione el tema',
title: 'Título',
description: 'Descripción',
sessionCost: 'Coste de la sesión en euros',
yourTimezone: 'Tu zona horaria',
workTime: 'Tiempo de trabajo',
startAt: 'Empieza a las',
finishAt: 'Termina a las',
addWorkingHours: 'Añadir horas de trabajo',
specialisation: 'Especialización',
selectSpecialisation: 'Selecciona tu especialización para continuar',
fillWeeklySchedule: 'Rellena tu agenda semanal',
beneficiaryName: 'Nombre del beneficiario',
bicOrSwift: 'Código BIC/Swift',
association: 'Asociación',
level: 'Nivel',
addDiploma: 'Añadir diploma',
university: 'Institución',
sunday: 'D',
monday: 'L',
tuesday: 'M',
wednesday: 'X',
thursday: 'J',
friday: 'V',
saturday: 'S',
addNew: 'Añadir nuevo',
mExperiences: 'Experiencia de dirección',
errors: {
invalidEmail: 'La dirección de correo electrónico no es válida',
emptyEmail: 'Introduce tu correo electrónico',

View File

@ -105,6 +105,7 @@ export default {
signUp: 'Inscrivez-vous maintenant',
noData: 'Aucune donnée',
notFound: 'Non trouvé',
skillsInfo: 'Infos sur les compétences',
trainings: 'Formations',
seminars: 'Séminaires',
courses: 'Cours',
@ -112,7 +113,38 @@ export default {
aboutCoach: 'À propos du coach',
education: 'Éducation',
coaching: 'Coaching',
experiences: 'Expérience pratique',
payInfo: 'Infos sur le paiement',
sessionDuration: 'Durée de la session',
experienceHours: 'Heures totales d\'expérience pratique',
topics: 'Sujets',
selectTopic: 'Sélectionnez un sujet',
title: 'Titre',
description: 'Description',
sessionCost: 'Coût de la session en euros',
yourTimezone: 'Votre fuseau horaire',
workTime: 'Heures de travail',
startAt: 'Commencer à',
finishAt: 'Finir à',
addWorkingHours: 'Ajouter des heures de travail',
specialisation: 'Spécialisation',
selectSpecialisation: 'Sélectionnez votre spécialisation pour continuer',
fillWeeklySchedule: 'Remplissez votre emploi du temps hebdomadaire',
beneficiaryName: 'Nom du bénéficiaire',
bicOrSwift: 'Code BIC/Swift',
association: 'Association',
level: 'Niveau',
addDiploma: 'Ajouter un diplôme',
university: 'Institution',
sunday: 'Di',
monday: 'Lu',
tuesday: 'Ma',
wednesday: 'Me',
thursday: 'Je',
friday: 'Ve',
saturday: 'Sa',
addNew: 'Ajouter un nouveau',
mExperiences: 'Expérience en gestion',
errors: {
invalidEmail: 'L\'adresse e-mail n\'est pas valide',
emptyEmail: 'Veuillez saisir votre e-mail',

View File

@ -105,6 +105,7 @@ export default {
signUp: 'Iscriviti ora',
noData: 'Nessun dato',
notFound: 'Non trovato',
skillsInfo: 'Info su competenze',
trainings: 'Training',
seminars: 'Seminari',
courses: 'Corsi',
@ -112,7 +113,38 @@ export default {
aboutCoach: 'Informazioni sul coach',
education: 'Istruzione',
coaching: 'Coaching',
experiences: 'Esperienza pratica',
payInfo: 'Info pagamento',
sessionDuration: 'Durata della sessione',
experienceHours: 'Totale ore di esperienza pratica',
topics: 'Argomenti',
selectTopic: 'Seleziona l\'argomento',
title: 'Titolo',
description: 'Descrizione',
sessionCost: 'Costo della sessione in euro',
yourTimezone: 'Il tuo fuso orario',
workTime: 'Orario di lavoro',
startAt: 'Inizia alle',
finishAt: 'Termina alle',
addWorkingHours: 'Aggiungi ore lavorative',
specialisation: 'Specializzazione',
selectSpecialisation: 'Seleziona la tua specializzazione per continuare',
fillWeeklySchedule: 'Compila la tua agenda settimanale',
beneficiaryName: 'Nome del beneficiario',
bicOrSwift: 'BIC/codice Swift',
association: 'Associazione',
level: 'Livello',
addDiploma: 'Aggiungi diploma',
university: 'Istituto',
sunday: 'Do',
monday: 'Lu',
tuesday: 'Ma',
wednesday: 'Me',
thursday: 'Gi',
friday: 'Ve',
saturday: 'Sa',
addNew: 'Aggiungi nuovo',
mExperiences: 'Esperienza manageriale',
errors: {
invalidEmail: 'L\'indirizzo e-mail non è valido',
emptyEmail: 'Inserisci l\'e-mail',

View File

@ -98,21 +98,53 @@ export default {
expertBackground: 'Профессиональный опыт эксперта',
profCertification: 'Профессиональная сертификация',
practiceHours: 'Часов практики',
supervisionCount: 'Часов супервизии в год',
supervisionCount: 'Супервизий в год',
outOf: 'из',
schedule: 'Расписание',
successfulCase: 'Успешные случаи из практики',
signUp: 'Записаться сейчас',
noData: 'Нет данных',
notFound: 'Не найдено',
skillsInfo: 'Навыки',
trainings: 'Тренинги',
seminars: 'Семинары',
courses: 'Курсы',
mba: 'Информация о MBA',
experiences: 'Практический опыт',
aboutCoach: 'О коуче',
education: 'Образование',
coaching: 'Коучинг',
payInfo: 'Платежная информация',
sessionDuration: 'Продолжительность сессии',
experienceHours: 'Общее количество часов практического опыта',
topics: 'Темы',
selectTopic: 'Выберите тему',
title: 'Название',
description: 'Описание',
sessionCost: 'Стоимость сессии в евро',
yourTimezone: 'Ваш часовой пояс',
workTime: 'Рабочее время',
startAt: 'Начало в',
finishAt: 'Завершение в',
addWorkingHours: 'Добавить рабочие часы',
specialisation: 'Специализация',
selectSpecialisation: 'Выберите свою специализацию для продолжения',
fillWeeklySchedule: 'Заполните свое недельное расписание',
beneficiaryName: 'Имя получателя',
bicOrSwift: 'BIC/Swift код',
association: 'Ассоциация',
level: 'Уровень',
addDiploma: 'Добавить диплом',
university: 'ВУЗ',
sunday: 'Вс',
monday: 'Пн',
tuesday: 'Вт',
wednesday: 'Ср',
thursday: 'Чт',
friday: 'Пт',
saturday: 'Сб',
addNew: 'Добавить',
mExperiences: 'Управленческий опыт',
errors: {
invalidEmail: 'Адрес электронной почты недействителен',
emptyEmail: 'Пожалуйста, введите ваш E-mail',

View File

@ -77,7 +77,7 @@ export async function fetchBlogPosts({ preview, category, page, sticky }: FetchB
const contentful = contentfulClient({ preview })
const query = {
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'],
}
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 {
height: 60vh;
height: auto;
max-height: 60vh;
overflow-y: auto;
& > div {
@ -86,3 +87,13 @@
.ant-modal-mask {
background-color: rgba(0, 59, 70, 0.4) !important;
}
.ant-upload-list-item-name {
max-width: 280px;
}
.ant-upload-list-item {
&:hover {
background-color: transparent !important;
}
}

View File

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

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 "_auth-modal.scss";
@import "_modal.scss";
@import "_edu.scss";
@import "_schedule.scss";
@import "./view/style.scss";
@import "./sessions/style.scss";

View File

@ -17,6 +17,19 @@
padding: 4px 24px !important;
}
&_square {
width: 42px !important;
height: 42px !important;
background: #66A5AD !important;
font-size: 15px !important;
border-radius: 8px !important;
box-shadow: 0px 2px 4px 0px rgba(102, 165, 173, 0.32) !important;
position: absolute !important;
right: -8px !important;
bottom: -8px !important;
cursor: pointer;
}
&.danger {
background: #D93E5C !important;
box-shadow: none !important;
@ -28,6 +41,10 @@
font-size: 15px !important;
height: auto !important;
padding: 0 !important;
&.danger {
color: #D93E5C !important;
}
}
&__outlined {

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 {
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 {

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 "_switch.scss";
@import "_buttons.scss";
@import "_practice.scss";
@import "_collapse.scss";

View File

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

View File

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

View File

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

View File

@ -15,7 +15,8 @@ export type ProfileData = {
hasExternalLogin?: boolean;
isTestMode?: boolean;
phone?: string;
languagesLinks?: { language: { id: number, code: string, nativeSpelling: string }, languageId: number }[]
languagesLinks?: { language: { id: number, code: string, nativeSpelling: string }, languageId: number }[];
allLanguages?: { id: number, code: string, nativeSpelling: string }[]
}
export type Profile = ProfileData & { id: number };

View File

@ -28,3 +28,19 @@ export const validateImage = (file: UploadFile, showMessage?: boolean): boolean
return isImage && isLt5M;
};
export const validateDoc = (file: UploadFile): boolean => {
const isDoc = file.type === 'image/jpg' || file.type === 'image/jpeg'
|| file.type === 'image/png' || file.type === 'image/gif' || file.type === 'application/pdf';
if (!isDoc) {
message.error('You can only upload JPG/PNG/PDF file');
}
const isLt5M = file.size / 1024 / 1024 <= 5;
if (!isLt5M) {
message.error('Image must smaller than 5MB');
}
return isDoc && isLt5M;
};

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