Compare commits
22 Commits
44674a1910
...
be7efc0d32
Author | SHA1 | Date |
---|---|---|
SD | be7efc0d32 | |
SD | 4b429c8655 | |
SD | 3ed78c0e45 | |
SD | 3345a533d2 | |
SD | 76ffdc4094 | |
SD | b52096b3bc | |
dzfelix | cda91b9ea9 | |
dzfelix | 28f5babf22 | |
dzfelix | 80f53e871d | |
dzfelix | 77d3c8f66b | |
dzfelix | f7fe427aae | |
dzfelix | 5844bd9e7c | |
dzfelix | dbb74b9ccd | |
dzfelix | 8ee52bc834 | |
norton81 | c563818e91 | |
norton81 | 2f2d9db82a | |
norton81 | 1461c4948e | |
dzfelix | 74d93541a3 | |
SD | f92810d320 | |
SD | 6f5c3738b7 | |
Dasha | 526e703d9a | |
dzfelix | ed756d0646 |
3
.env
3
.env
|
@ -1,3 +1,6 @@
|
||||||
NEXT_PUBLIC_SERVER_BASE_URL=https://api.bbuddy.expert/api
|
NEXT_PUBLIC_SERVER_BASE_URL=https://api.bbuddy.expert/api
|
||||||
NEXT_PUBLIC_AGORA_APPID=ed90c9dc42634e5687d4e2e0766b363f
|
NEXT_PUBLIC_AGORA_APPID=ed90c9dc42634e5687d4e2e0766b363f
|
||||||
|
|
||||||
|
NEXT_PUBLIC_CONTENTFUL_SPACE_ID = voxpxjq7y7vf
|
||||||
|
NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN = s99GWKfpDKkNwiEJ3pN7US_tmqsGvDlaex-sOJwpzuc
|
||||||
|
NEXT_PUBLIC_CONTENTFUL_PREVIEW_ACCESS_TOKEN = Z9WOKpLDbKNj7xVOmT_VXYNLH0AZwISFvQsq0PQlHfE
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
const withNextIntl = require('next-intl/plugin')();
|
const withNextIntl = require('next-intl/plugin')();
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const json = require('./package.json');
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
|
@ -14,6 +15,9 @@ const nextConfig = {
|
||||||
sassOptions: {
|
sassOptions: {
|
||||||
includePaths: [path.join(__dirname, 'styles')],
|
includePaths: [path.join(__dirname, 'styles')],
|
||||||
},
|
},
|
||||||
|
env: {
|
||||||
|
version: json.version
|
||||||
|
},
|
||||||
typescript: {
|
typescript: {
|
||||||
// !! WARN !!
|
// !! WARN !!
|
||||||
// Dangerously allow production builds to successfully complete even if
|
// Dangerously allow production builds to successfully complete even if
|
||||||
|
@ -30,8 +34,7 @@ const nextConfig = {
|
||||||
},
|
},
|
||||||
// output: 'standalone',
|
// output: 'standalone',
|
||||||
poweredByHeader: false,
|
poweredByHeader: false,
|
||||||
productionBrowserSourceMaps: true,
|
productionBrowserSourceMaps: true
|
||||||
trailingSlash: true
|
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = withNextIntl(nextConfig);
|
module.exports = withNextIntl(nextConfig);
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "bbuddy-ui",
|
"name": "bbuddy-ui",
|
||||||
"version": "0.0.1",
|
"version": "0.0.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -p 4200",
|
"dev": "next dev -p 4200",
|
||||||
|
@ -12,11 +12,13 @@
|
||||||
"@ant-design/cssinjs": "^1.18.1",
|
"@ant-design/cssinjs": "^1.18.1",
|
||||||
"@ant-design/icons": "^5.2.6",
|
"@ant-design/icons": "^5.2.6",
|
||||||
"@ant-design/nextjs-registry": "^1.0.0",
|
"@ant-design/nextjs-registry": "^1.0.0",
|
||||||
|
"@contentful/rich-text-react-renderer": "^15.22.9",
|
||||||
"agora-rtc-react": "^2.1.0",
|
"agora-rtc-react": "^2.1.0",
|
||||||
"agora-rtc-sdk-ng": "^4.20.2",
|
"agora-rtc-sdk-ng": "^4.20.2",
|
||||||
"antd": "^5.12.1",
|
"antd": "^5.12.1",
|
||||||
"antd-img-crop": "^4.21.0",
|
"antd-img-crop": "^4.21.0",
|
||||||
"axios": "^1.6.5",
|
"axios": "^1.6.5",
|
||||||
|
"contentful": "^10.13.3",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"next": "14.0.3",
|
"next": "14.0.3",
|
||||||
|
|
|
@ -1,71 +1,40 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { unstable_setRequestLocale } from 'next-intl/server';
|
import {getTranslations, unstable_setRequestLocale} from 'next-intl/server';
|
||||||
import { i18nText } from '../../../../i18nKeys';
|
import { i18nText } from '../../../../i18nKeys';
|
||||||
|
import {fetchBlogPosts} from "../../../../lib/contentful/blogPosts";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
export default function News({ params: { locale } }: { params: { locale: string }}) {
|
export default async function News({params: {locale}}: { params: { locale: string } }) {
|
||||||
unstable_setRequestLocale(locale);
|
unstable_setRequestLocale(locale);
|
||||||
const t = useTranslations('Main');
|
const t = await getTranslations('Main');
|
||||||
|
const {data, total} = await fetchBlogPosts({preview: false, sticky: true})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="main-articles">
|
<div className="main-articles">
|
||||||
<div className="b-inner">
|
<div className="b-inner">
|
||||||
<h2 className="title-h2">{t('news')}</h2>
|
<h2 className="title-h2">{t('news')}</h2>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-lg-4 col-md-6 col-sm-6">
|
{data.map((item, i) => (
|
||||||
|
<div className="col-lg-4 col-md-6 col-sm-6" key={'news' + i}>
|
||||||
<div className="b-article">
|
<div className="b-article">
|
||||||
<div className="b-article__image">
|
<div className="b-article__image">
|
||||||
<img className="" src="/images/article.png" alt=""/>
|
<img className="" src={item.listImage?.src} alt={item.listImage?.alt}/>
|
||||||
</div>
|
</div>
|
||||||
<div className="b-article__inner">
|
<div className="b-article__inner">
|
||||||
<div className="b-article__title">News Headline</div>
|
<div className="b-article__title">{item.title}</div>
|
||||||
<div className="b-article__text">
|
<div className="b-article__text">
|
||||||
The program not only focuses on a financial perspective, but allows you to study
|
{item.excerpt}
|
||||||
performance from many angles, such as human resources management, IT, operations
|
|
||||||
management, risks etc.
|
|
||||||
</div>
|
</div>
|
||||||
<a href="#" className="b-article__link">{i18nText('readMore', locale)}
|
<Link href={`/${locale}/blog/${item.slug}`} className="b-article__link">
|
||||||
|
{i18nText('readMore', locale)}
|
||||||
<img className="" src="/images/chevron-forward.svg" alt=""/>
|
<img className="" src="/images/chevron-forward.svg" alt=""/>
|
||||||
</a>
|
</Link>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-lg-4 d-none d-lg-block">
|
|
||||||
<div className="b-article">
|
|
||||||
<div className="b-article__image">
|
|
||||||
<img className="" src="/images/article.png" alt=""/>
|
|
||||||
</div>
|
|
||||||
<div className="b-article__inner">
|
|
||||||
<div className="b-article__title">News Headline</div>
|
|
||||||
<div className="b-article__text">
|
|
||||||
The program not only focuses on a financial perspective, but allows you to study
|
|
||||||
performance from many angles, such as human resources management, IT, operations
|
|
||||||
management, risks etc.
|
|
||||||
</div>
|
|
||||||
<a href="#" className="b-article__link">{i18nText('readMore', locale)}
|
|
||||||
<img className="" src="/images/chevron-forward.svg" alt=""/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-lg-4 d-none d-lg-block">
|
|
||||||
<div className="b-article">
|
|
||||||
<div className="b-article__image">
|
|
||||||
<img className="" src="/images/article.png" alt=""/>
|
|
||||||
</div>
|
|
||||||
<div className="b-article__inner">
|
|
||||||
<div className="b-article__title">News Headline</div>
|
|
||||||
<div className="b-article__text">
|
|
||||||
The program not only focuses on a financial perspective, but allows you to study
|
|
||||||
performance from many angles, such as human resources management, IT, operations
|
|
||||||
management, risks etc.
|
|
||||||
</div>
|
|
||||||
<a href="#" className="b-article__link">{i18nText('readMore', locale)}
|
|
||||||
<img className="" src="/images/chevron-forward.svg" alt=""/>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,7 +6,15 @@ import { message } from 'antd';
|
||||||
import { ExpertData } from '../../../../../types/profile';
|
import { ExpertData } from '../../../../../types/profile';
|
||||||
import { AUTH_TOKEN_KEY } from '../../../../../constants/common';
|
import { AUTH_TOKEN_KEY } from '../../../../../constants/common';
|
||||||
import { useLocalStorage } from '../../../../../hooks/useLocalStorage';
|
import { useLocalStorage } from '../../../../../hooks/useLocalStorage';
|
||||||
import { getEducation, getPersonalData, getTags, getPractice, getSchedule, getPayData } from '../../../../../actions/profile';
|
import {
|
||||||
|
getEducation,
|
||||||
|
getPersonalData,
|
||||||
|
getTags,
|
||||||
|
getPractice,
|
||||||
|
getSchedule,
|
||||||
|
getPayData,
|
||||||
|
getUserData
|
||||||
|
} from '../../../../../actions/profile';
|
||||||
import { ExpertProfile } from '../../../../../components/ExpertProfile';
|
import { ExpertProfile } from '../../../../../components/ExpertProfile';
|
||||||
import { Loader } from '../../../../../components/view/Loader';
|
import { Loader } from '../../../../../components/view/Loader';
|
||||||
|
|
||||||
|
@ -15,11 +23,13 @@ export default function ExpertProfilePage({ params: { locale } }: { params: { lo
|
||||||
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
|
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const [data, setData] = useState<ExpertData | undefined>();
|
const [data, setData] = useState<ExpertData | undefined>();
|
||||||
|
const [isFull, setIsFull] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (jwt) {
|
if (jwt) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
Promise.all([
|
Promise.all([
|
||||||
|
getUserData(locale, jwt),
|
||||||
getPersonalData(locale, jwt),
|
getPersonalData(locale, jwt),
|
||||||
getEducation(locale, jwt),
|
getEducation(locale, jwt),
|
||||||
getTags(locale, jwt),
|
getTags(locale, jwt),
|
||||||
|
@ -27,13 +37,8 @@ export default function ExpertProfilePage({ params: { locale } }: { params: { lo
|
||||||
getSchedule(locale, jwt),
|
getSchedule(locale, jwt),
|
||||||
getPayData(locale, jwt)
|
getPayData(locale, jwt)
|
||||||
])
|
])
|
||||||
.then(([person, education, tags, practice, schedule, payData]) => {
|
.then(([profile, person, education, tags, practice, schedule, payData]) => {
|
||||||
console.log('person', person);
|
setIsFull(profile.fillProgress === 'full');
|
||||||
console.log('education', education);
|
|
||||||
console.log('tags', tags);
|
|
||||||
console.log('practice', practice);
|
|
||||||
console.log('schedule', schedule);
|
|
||||||
console.log('payData', payData);
|
|
||||||
setData({
|
setData({
|
||||||
person,
|
person,
|
||||||
education,
|
education,
|
||||||
|
@ -54,13 +59,12 @@ export default function ExpertProfilePage({ params: { locale } }: { params: { lo
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Loader isLoading={loading}>
|
<Loader isLoading={loading}>
|
||||||
{data && (
|
|
||||||
<ExpertProfile
|
<ExpertProfile
|
||||||
|
isFull={isFull}
|
||||||
locale={locale}
|
locale={locale}
|
||||||
data={data}
|
data={data}
|
||||||
updateData={setData}
|
updateData={setData}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</Loader>
|
</Loader>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,150 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import type { Metadata } from 'next';
|
|
||||||
import { notFound } from 'next/navigation';
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: 'Bbuddy - Blog item',
|
|
||||||
description: 'Bbuddy desc blog item'
|
|
||||||
};
|
|
||||||
|
|
||||||
export function generateStaticParams() {
|
|
||||||
return [{ blogId: 'news-1' }, { blogId: 'news-2' }];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function BlogItem({ params }: { params: { blogId: string } }) {
|
|
||||||
if (!params?.blogId) notFound();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="b-news-page">
|
|
||||||
<div className="b-inner">
|
|
||||||
<h1 className="b-news-page__title">6 learnings from Shivpuri to Silicon Valley</h1>
|
|
||||||
<div className="news-item__badge">Leadership & Management</div>
|
|
||||||
<div className="b-news-page__text">
|
|
||||||
{`news id ${params.blogId}`}<br />
|
|
||||||
I’m excited to kick off this series of newsletters where I’ll be sharing my experiences, learnings,
|
|
||||||
and best practices which helped me to grow both in my personal and professional life. My hope is to
|
|
||||||
give back to the community and help anyone connect directly with me who may have got impacted with
|
|
||||||
recent layoffs, dealing with immigration challenges.
|
|
||||||
</div>
|
|
||||||
<div className="b-news-page__image">
|
|
||||||
<img className="" src="/images/news1.png" alt="" />
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info">
|
|
||||||
<div className="news-item__info__author">
|
|
||||||
<img className="" src="/images/author.png" alt="" />
|
|
||||||
<div className="news-item__info__author__inner">
|
|
||||||
<div className="news-item__info__name">Sonali Garg</div>
|
|
||||||
<div className="news-item__info__date">February 6th, 2023</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info__counter">
|
|
||||||
<div className="news-item__info__like">
|
|
||||||
<img className="" src="/images/heart-outline.svg" alt="" />
|
|
||||||
165
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info__share">
|
|
||||||
<img className="" src="/images/share-social.svg" alt="" />
|
|
||||||
Share
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="b-news-page__inner">
|
|
||||||
<h2 className="title-h2">
|
|
||||||
This is not about layoffs, it's about living with whatever life throws at you..
|
|
||||||
</h2>
|
|
||||||
<p className="b-news-page__text">
|
|
||||||
Over the past few months, as the macro-economic events have unfolded, I have heard voices filled
|
|
||||||
with anxiety, helplessness and general lack of confidence to deal with this ambiguity from my
|
|
||||||
mentees, colleagues, friends and family. I was laid off from Meta last November and I firmly
|
|
||||||
believe this is nothing but a bump in the road that might seem like a steep climb in the
|
|
||||||
short-term. I may not have all the answers but this has inspired me to share my story. If you
|
|
||||||
are looking for a sob story, you can stop reading now. Ever wondered what it takes for a girl
|
|
||||||
born into a conservative family in a small sleepy town in India, who lost one of her parents at
|
|
||||||
age 17, earned her living while pursuing engineering, moved to the UK by herself and ended up
|
|
||||||
working in big tech in Silicon valley? My goal with this series of posts is to inspire and share
|
|
||||||
my mental models that helped me throughout my professional and personal life.
|
|
||||||
</p>
|
|
||||||
<p className="b-news-page__text">
|
|
||||||
After completing my engineering, I started my career at a small software company in Bhopal and
|
|
||||||
then worked for TCS(Tata Consultancy Services), one of the largest IT-outsourcing companies in
|
|
||||||
the world for almost 5 years. Over the past 14 years, I have worked for big tech companies like
|
|
||||||
Meta (Facebook) and Google, wore multiple hats, led strategic programs, scaled multi
|
|
||||||
billion-dollar businesses, built teams and helped achieve business operational excellence.
|
|
||||||
Throughout my career, I’ve dealt with several challenges from execution to scale to building a
|
|
||||||
high performance team. A lot of my early struggles were about how to assimilate in a new
|
|
||||||
culture, create a network in a new environment, earn trust, create and nurture work
|
|
||||||
relationships into fruitful friendships and so on.
|
|
||||||
</p>
|
|
||||||
<p className="b-news-page__text">
|
|
||||||
I was born in a conservative family in a small town called ‘Shivpuri’, also known as ‘Mini
|
|
||||||
Kashmir’ because of its natural beauty. My father was a civil engineer working on Madikheda Dam
|
|
||||||
on Sindh river and was a strict disciplinarian. He was gone from dawn to dusk and was always
|
|
||||||
focused. My mother was a teacher in a school that was about 30 kms from our home. We (me and my
|
|
||||||
sister) would often be left with neighbors to be taken care of and this led us to become
|
|
||||||
independent at an early age. Our otherwise slow paced, simple life with only a few families
|
|
||||||
around in the government quarters that were set up to support construction of the dam was filled
|
|
||||||
with natural beauty, wildlife and a community of close friends. Our lives were balanced and
|
|
||||||
while my parents worked hard to provide basic needs, we were satisfied. There were only a few
|
|
||||||
schools with Hindi being the prevalent language as the medium of teaching. There were no
|
|
||||||
colleges for advanced studies and most girls did not go to college often married off by their
|
|
||||||
18th birthday. Generally speaking, we had a joyous childhood with just the basics. While most
|
|
||||||
folks we interacted with were not highly educated nor ambitious, earned lower middle class
|
|
||||||
salaries and lacked exposure to the outside world but there was plenty to learn from them.
|
|
||||||
People had learnt to stick together in good and bad times. They embodied the old school
|
|
||||||
qualities of hard work, dedication and commitment. Be willing to give it all- hard work,
|
|
||||||
dedication and commitment.
|
|
||||||
</p>
|
|
||||||
<p className="b-news-page__text">
|
|
||||||
In 2003, my father passed away suddenly and we found ourselves in crisis. My mother was a
|
|
||||||
teacher and she did not have time to deal with her grief. Rather, she was struggling to garner
|
|
||||||
support to get transferred to a school in Bhopal, capital of Madhya Pradesh to be closer to our
|
|
||||||
maternal grandparents. As we uprooted ourselves from Shivpuri to Bhopal, one of my father’s
|
|
||||||
loyal friends came to help load the moving truck. While he had nothing to gain out of us, he
|
|
||||||
continued to serve us until the last day in Shivpuri. Remember, in crisis your team matters more
|
|
||||||
than any other time. Advocate for them ruthlessly in good and bad times, they will come through
|
|
||||||
in crisis.
|
|
||||||
</p>
|
|
||||||
<p className="b-news-page__text">
|
|
||||||
Eventually we found our footing, my mother’s job was transferred to a local school in Bhopal and
|
|
||||||
I got admission in a government engineering college. My sister was still attending high school
|
|
||||||
and both of us were teaching tuition classes to middle school students in the evenings to make
|
|
||||||
ends meet. I also started a tiffin service for a few out of town students while attending
|
|
||||||
college to pay for my transportation and cost of supplies. We refused to give up. Persevere when
|
|
||||||
all else fails.
|
|
||||||
</p>
|
|
||||||
<p className="b-news-page__text">
|
|
||||||
Our 5 years went by quickly in Bhopal as we worked towards improving our financial situation and
|
|
||||||
I completed my Bachelors in Computer Science. This was the time I first stepped out to live in a
|
|
||||||
metropolitan city, Mumbai for my job at TCS. This was a paradigm shift from Bhopal and I was
|
|
||||||
blown away to meet so many talented folks in Mumbai. In my head, I did not belong in this place.
|
|
||||||
I had imposter syndrome and felt like an outsider trying to make it in a new city. Most people I
|
|
||||||
met were fluent in more than 1 language, well-dressed, communicated openly and with confidence,
|
|
||||||
and presented themselves well. I was always in a dilemma when it came to adopting values. It
|
|
||||||
took me a while to adjust to it but I was still not confident about my work and communication
|
|
||||||
while my hard skills that I learnt in engineering were top notch. I kept questioning my
|
|
||||||
abilities but persisted. This was not the first time I was out of my comfort zone. Persist, when
|
|
||||||
in discomfort.
|
|
||||||
</p>
|
|
||||||
<p className="b-news-page__text">
|
|
||||||
I worked with multiple global companies who were clients of TCS and was presented an opportunity
|
|
||||||
to move to Scotland, UK for an year to work for GE, who was also a client. This was my first
|
|
||||||
opportunity to explore a different culture, food, music, languages etc. I remember working on my
|
|
||||||
english when in Mumbai, in preparation for my UK trip. It was really difficult to understand the
|
|
||||||
accent in the UK, even though language was not a barrier. I still remember certain words would
|
|
||||||
just not get across no matter how hard some of my colleagues tried and they would end up using
|
|
||||||
signs to convey. Be prepared, opportunities come to those who are prepared.
|
|
||||||
</p>
|
|
||||||
<p className="b-news-page__text">
|
|
||||||
In 2013, I came to the US on a dependent visa after marriage and quickly realized the curse of
|
|
||||||
H4 visa. I paved my path by going back to school at UC Berkeley and then jumped back into
|
|
||||||
building my career from scratch. While working in the US over the past years, I realized college
|
|
||||||
degrees with good grades and certifications definitely help you to get your foot in the door but
|
|
||||||
are not enough to be successful in your career. As I was again starting from scratch in a new
|
|
||||||
culture, determined to do whatever it takes, having done this a few times before, it doesn’t
|
|
||||||
scare me as much. Never be afraid to start from zero again!
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -0,0 +1,112 @@
|
||||||
|
import React from 'react';
|
||||||
|
import type {Metadata, ResolvingMetadata} from 'next';
|
||||||
|
import { draftMode } from 'next/headers'
|
||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
import {fetchBlogPost, fetchBlogPosts, Widget} from "../../../../lib/contentful/blogPosts";
|
||||||
|
import Util from "node:util";
|
||||||
|
import RichText from "../../../../lib/contentful/RichText";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
interface BlogPostPageParams {
|
||||||
|
slug: string
|
||||||
|
locale: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BlogPostPageProps {
|
||||||
|
params: BlogPostPageParams
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({ params }: BlogPostPageProps, parent: ResolvingMetadata): Promise<Metadata> {
|
||||||
|
const blogPost = await fetchBlogPost({ slug: params.slug, preview: draftMode().isEnabled })
|
||||||
|
|
||||||
|
if (!blogPost) {
|
||||||
|
return notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: blogPost.title,
|
||||||
|
description: blogPost.metaDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function renderWidget (widget: Widget, index: number) {
|
||||||
|
switch (widget.type){
|
||||||
|
case 'widgetParagraph':
|
||||||
|
return (
|
||||||
|
<div key={'widget'+index} >
|
||||||
|
<h2 className="title-h2">
|
||||||
|
{widget.widget.subTitle}
|
||||||
|
</h2>
|
||||||
|
<RichText document={widget.widget.body} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
case 'widgetMedia':
|
||||||
|
return (
|
||||||
|
<img key={'widget'+index} src={widget.widget.file?.src}/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function BlogItem({params}: { params: BlogPostPageParams }) {
|
||||||
|
const item = await fetchBlogPost({slug: params.slug, preview: draftMode().isEnabled })
|
||||||
|
console.log('BLOG POST')
|
||||||
|
console.log(Util.inspect(item, {showHidden: false, depth: null, colors: true}))
|
||||||
|
if (!item) notFound();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="b-news-page">
|
||||||
|
<div className="b-inner">
|
||||||
|
<h1 className="b-news-page__title">{item.title}</h1>
|
||||||
|
<div className="news-item__badge">{item.category}</div>
|
||||||
|
<div className="b-news-page__text">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div className="b-news-page__image">
|
||||||
|
<img className="" src="/images/news1.png" alt=""/>
|
||||||
|
</div>
|
||||||
|
<div className="news-item__info">
|
||||||
|
<div className="news-item__info__author">
|
||||||
|
<Link href={`/${params.locale}/experts/${item.author?.expertId}`} className="news-item">
|
||||||
|
<img className="" src={item.author.avatar.src} alt=""/>
|
||||||
|
<div className="news-item__info__author__inner">
|
||||||
|
<div className="news-item__info__name">{item.author?.name}</div>
|
||||||
|
<div className="news-item__info__date">{item.createdAt}</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="news-item__info__counter">
|
||||||
|
<div className="news-item__info__like">
|
||||||
|
<img className="" src="/images/heart-outline.svg" alt=""/>
|
||||||
|
165
|
||||||
|
</div>
|
||||||
|
<div className="news-item__info__share">
|
||||||
|
<img className="" src="/images/share-social.svg" alt=""/>
|
||||||
|
Share
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="b-news-page__inner">
|
||||||
|
{item.body.map(renderWidget)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="b-inner" style={ {marginTop: '40px'}}>
|
||||||
|
<nav className="min-h-6 pb-6">
|
||||||
|
<Link href='/'>
|
||||||
|
Home
|
||||||
|
</Link>
|
||||||
|
>
|
||||||
|
<Link href={`/${params.locale}/blog/category/${item.categorySlug}`}>
|
||||||
|
{item.category}
|
||||||
|
</Link>
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{item.title}
|
||||||
|
</span>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,29 @@
|
||||||
|
import React from 'react';
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
import { draftMode } from 'next/headers'
|
||||||
|
import {unstable_setRequestLocale} from "next-intl/server";
|
||||||
|
import Link from "next/link";
|
||||||
|
import {fetchBlogPosts} from "../../../../../lib/contentful/blogPosts";
|
||||||
|
import {fetchBlogPostCategories} from "../../../../../lib/contentful/blogPostsCategories";
|
||||||
|
import {BlogPosts} from "../../../../../components/BlogPosts/BlogPosts";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Bbuddy - Blog',
|
||||||
|
description: 'Bbuddy desc blog'
|
||||||
|
};
|
||||||
|
|
||||||
|
interface BlogPostPageParams {
|
||||||
|
slug: string
|
||||||
|
locale: string
|
||||||
|
}
|
||||||
|
interface BlogPostPageProps {
|
||||||
|
params: BlogPostPageParams
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function Blog({params, searchParams}: { params: BlogPostPageParams, searhParams?: {page: number} }) {
|
||||||
|
unstable_setRequestLocale(params.locale);
|
||||||
|
const page = searchParams.page || undefined
|
||||||
|
return (
|
||||||
|
<BlogPosts basePath={'/'+params.locale+'/blog/'} locale={params.locale} currentCat={params.slug} page={page}/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,213 +1,43 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
import * as Util from "node:util";
|
||||||
|
import {fetchBlogPosts} from "../../../lib/contentful/blogPosts";
|
||||||
|
import {unstable_setRequestLocale} from "next-intl/server";
|
||||||
|
import Link from "next/link";
|
||||||
|
import {fetchBlogPostCategories} from "../../../lib/contentful/blogPostsCategories";
|
||||||
|
import {CustomPagination} from "../../../components/view/CustomPagination";
|
||||||
|
import {DEFAULT_PAGE_SIZE} from "../../../constants/common";
|
||||||
|
import {BlogPosts} from "../../../components/BlogPosts/BlogPosts";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: 'Bbuddy - Blog',
|
|
||||||
description: 'Bbuddy desc blog'
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Blog() {
|
interface BlogPostPageParams {
|
||||||
|
slug: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BlogPostPageProps {
|
||||||
|
params: BlogPostPageParams
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateStaticParams(): Promise<BlogPostPageParams[]> {
|
||||||
|
const blogPosts = await fetchBlogPosts({ preview: false })
|
||||||
|
|
||||||
|
return blogPosts.data.map((post) => ({ slug: post.slug }))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default async function Blog({ params: { locale }, searchParams }: { params: { locale: string }, searhParams?: {page: number} }) {
|
||||||
|
unstable_setRequestLocale(locale);
|
||||||
|
const pageSize = DEFAULT_PAGE_SIZE
|
||||||
|
const page = searchParams.page || undefined
|
||||||
|
// BlogPosts('/'+locale+'/blog/', locale, pageSize)
|
||||||
return (
|
return (
|
||||||
<div className="b-news">
|
|
||||||
<div className="b-news__header">
|
<BlogPosts
|
||||||
<div className="b-inner">
|
basePath={'/'+locale+'/blog/'}
|
||||||
<h1 className="title-h1">
|
locale={locale}
|
||||||
Mentorship, Career <br />
|
pageSize={pageSize}
|
||||||
Development & Coaching.
|
page={page}
|
||||||
</h1>
|
>
|
||||||
<div className="wrap-text">
|
</BlogPosts>
|
||||||
<p className="">The ins-and-outs of building a career in tech, gaining <br /> experience</p>
|
|
||||||
<p className="">from a mentor, and getting your feet wet with coaching.</p>
|
|
||||||
</div>
|
|
||||||
<div className="b-news__header__img">
|
|
||||||
<img className="" src="/images/news-top.png" alt="" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="b-news__filter ">
|
|
||||||
<div className="b-inner">
|
|
||||||
<div className="wrap-filter">
|
|
||||||
<a href="#" className="filter-item">Leadership & Management</a>
|
|
||||||
<a href="#" className="filter-item">Professional Development</a>
|
|
||||||
<a href="#" className="filter-item">Research & Insights</a>
|
|
||||||
<a href="#" className="filter-item">Well-Being</a>
|
|
||||||
<a href="#" className="filter-item">Diversity & Inclusion</a>
|
|
||||||
<a href="#" className="filter-item">Culture</a>
|
|
||||||
<a href="#" className="filter-item">Sales</a>
|
|
||||||
<a href="#" className="filter-item">Collaboration</a>
|
|
||||||
<a href="#" className="filter-item">Hiring</a>
|
|
||||||
<a href="#" className="filter-item active">BBuddy product</a>
|
|
||||||
<a href="#" className="filter-item">Customer Stories</a>
|
|
||||||
<a href="#" className="filter-item">Coaching</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="b-news__result-list">
|
|
||||||
<div className="b-inner">
|
|
||||||
<div className="news-list">
|
|
||||||
<a href="#" className="news-item">
|
|
||||||
<div className="news-item__image">
|
|
||||||
<img className="" src="/images/news.png" alt="" />
|
|
||||||
</div>
|
|
||||||
<div className="news-item__inner">
|
|
||||||
<div className="">
|
|
||||||
<div className="news-item__title">
|
|
||||||
6 learnings from Shivpuri to Silicon Valley
|
|
||||||
</div>
|
|
||||||
<div className="news-item__badge">Leadership & Management</div>
|
|
||||||
<div className="news-item__text">
|
|
||||||
I’m excited to kick off this series of newsletters where I’ll be sharing my
|
|
||||||
experiences,
|
|
||||||
learnings, and best practices which helped me to grow both in my personal and
|
|
||||||
professional life. My hope is to give back to the community and help anyone
|
|
||||||
connect directly with me who may have got impacted with recent layoffs,
|
|
||||||
dealing with immigration challenges.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info">
|
|
||||||
<div className="news-item__info__author">
|
|
||||||
<img className="" src="/images/author.png" alt="" />
|
|
||||||
<div className="news-item__info__author__inner">
|
|
||||||
<div className="news-item__info__name">Sonali Garg</div>
|
|
||||||
<div className="news-item__info__date">February 6th, 2023</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info__counter">
|
|
||||||
<div className="news-item__info__like">
|
|
||||||
<img className="" src="/images/heart-outline.svg" alt="" />
|
|
||||||
165
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info__share">
|
|
||||||
<img className="" src="/images/share-social.svg" alt="" />
|
|
||||||
Share
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a href="#" className="news-item">
|
|
||||||
<div className="news-item__image">
|
|
||||||
<img className="" src="/images/news.png" alt="" />
|
|
||||||
</div>
|
|
||||||
<div className="news-item__inner">
|
|
||||||
<div className="">
|
|
||||||
<div className="news-item__title">
|
|
||||||
6 learnings from Shivpuri to Silicon Valley
|
|
||||||
</div>
|
|
||||||
<div className="news-item__badge">Leadership & Management</div>
|
|
||||||
<div className="news-item__text">
|
|
||||||
I’m excited to kick off this series of newsletters where I’ll be sharing my
|
|
||||||
experiences,
|
|
||||||
learnings, and best practices which helped me to grow both in my personal and
|
|
||||||
professional life. My hope is to give back to the community and help anyone
|
|
||||||
connect directly with me who may have got impacted with recent layoffs,
|
|
||||||
dealing with immigration challenges.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info">
|
|
||||||
<div className="news-item__info__author">
|
|
||||||
<img className="" src="/images/author.png" alt="" />
|
|
||||||
<div className="news-item__info__author__inner">
|
|
||||||
<div className="news-item__info__name">Sonali Garg</div>
|
|
||||||
<div className="news-item__info__date">February 6th, 2023</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info__counter">
|
|
||||||
<div className="news-item__info__like">
|
|
||||||
<img className="" src="/images/heart-outline.svg" alt="" />
|
|
||||||
165
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info__share">
|
|
||||||
<img className="" src="/images/share-social.svg" alt="" />
|
|
||||||
Share
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a href="#" className="news-item">
|
|
||||||
<div className="news-item__image">
|
|
||||||
<img className="" src="/images/news.png" alt="" />
|
|
||||||
</div>
|
|
||||||
<div className="news-item__inner">
|
|
||||||
<div className="">
|
|
||||||
<div className="news-item__title">
|
|
||||||
6 learnings from Shivpuri to Silicon Valley
|
|
||||||
</div>
|
|
||||||
<div className="news-item__badge">Leadership & Management</div>
|
|
||||||
<div className="news-item__text">
|
|
||||||
I’m excited to kick off this series of newsletters where I’ll be sharing my
|
|
||||||
experiences,
|
|
||||||
learnings, and best practices which helped me to grow both in my personal and
|
|
||||||
professional life. My hope is to give back to the community and help anyone
|
|
||||||
connect directly with me who may have got impacted with recent layoffs,
|
|
||||||
dealing with immigration challenges.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info">
|
|
||||||
<div className="news-item__info__author">
|
|
||||||
<img className="" src="/images/author.png" alt="" />
|
|
||||||
<div className="news-item__info__author__inner">
|
|
||||||
<div className="news-item__info__name">Sonali Garg</div>
|
|
||||||
<div className="news-item__info__date">February 6th, 2023</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info__counter">
|
|
||||||
<div className="news-item__info__like">
|
|
||||||
<img className="" src="/images/heart-outline.svg" alt="" />
|
|
||||||
165
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info__share">
|
|
||||||
<img className="" src="/images/share-social.svg" alt="" />
|
|
||||||
Share
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a href="#" className="news-item">
|
|
||||||
<div className="news-item__image">
|
|
||||||
<img className="" src="/images/news.png" alt="" />
|
|
||||||
</div>
|
|
||||||
<div className="news-item__inner">
|
|
||||||
<div className="">
|
|
||||||
<div className="news-item__title">
|
|
||||||
6 learnings from Shivpuri to Silicon Valley
|
|
||||||
</div>
|
|
||||||
<div className="news-item__badge">Leadership & Management</div>
|
|
||||||
<div className="news-item__text">
|
|
||||||
I’m excited to kick off this series of newsletters where I’ll be sharing my
|
|
||||||
experiences,
|
|
||||||
learnings, and best practices which helped me to grow both in my personal and
|
|
||||||
professional life. My hope is to give back to the community and help anyone
|
|
||||||
connect directly with me who may have got impacted with recent layoffs,
|
|
||||||
dealing with immigration challenges.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info">
|
|
||||||
<div className="news-item__info__author">
|
|
||||||
<img className="" src="/images/author.png" alt="" />
|
|
||||||
<div className="news-item__info__author__inner">
|
|
||||||
<div className="news-item__info__name">Sonali Garg</div>
|
|
||||||
<div className="news-item__info__date">February 6th, 2023</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info__counter">
|
|
||||||
<div className="news-item__info__like">
|
|
||||||
<img className="" src="/images/heart-outline.svg" alt="" />
|
|
||||||
165
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info__share">
|
|
||||||
<img className="" src="/images/share-social.svg" alt="" />
|
|
||||||
Share
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {
|
||||||
ExpertInformation,
|
ExpertInformation,
|
||||||
ExpertPractice
|
ExpertPractice
|
||||||
} from '../../../../components/Experts/ExpertDetails';
|
} from '../../../../components/Experts/ExpertDetails';
|
||||||
import { Details } from '../../../../types/experts';
|
import { Details } from '../../../../types/education';
|
||||||
import { BackButton } from '../../../../components/view/BackButton';
|
import { BackButton } from '../../../../components/view/BackButton';
|
||||||
import { i18nText } from '../../../../i18nKeys';
|
import { i18nText } from '../../../../i18nKeys';
|
||||||
|
|
||||||
|
@ -36,7 +36,6 @@ export default async function ExpertItem({ params: { expertId = '', locale } }:
|
||||||
if (!expertId) notFound();
|
if (!expertId) notFound();
|
||||||
|
|
||||||
const expert = await getExpertById(expertId, locale);
|
const expert = await getExpertById(expertId, locale);
|
||||||
console.log(expert);
|
|
||||||
|
|
||||||
const getAssociationLevel = (accLevelId?: number) => {
|
const getAssociationLevel = (accLevelId?: number) => {
|
||||||
if (accLevelId) {
|
if (accLevelId) {
|
||||||
|
@ -112,7 +111,11 @@ export default async function ExpertItem({ params: { expertId = '', locale } }:
|
||||||
{expert?.publicCoachDetails?.trainings && expert.publicCoachDetails.trainings?.map(generateDescription)}
|
{expert?.publicCoachDetails?.trainings && expert.publicCoachDetails.trainings?.map(generateDescription)}
|
||||||
{expert?.publicCoachDetails?.mbas && expert.publicCoachDetails.mbas?.map(generateDescription)}
|
{expert?.publicCoachDetails?.mbas && expert.publicCoachDetails.mbas?.map(generateDescription)}
|
||||||
{expert?.publicCoachDetails?.experiences && expert.publicCoachDetails.experiences?.map(generateDescription)}
|
{expert?.publicCoachDetails?.experiences && expert.publicCoachDetails.experiences?.map(generateDescription)}
|
||||||
<ExpertPractice expert={expert} locale={locale} />
|
<ExpertPractice
|
||||||
|
themes={expert?.publicCoachDetails?.themesGroups}
|
||||||
|
cases={expert?.publicCoachDetails?.practiceCases}
|
||||||
|
locale={locale}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* <h2 className="title-h2">All Offers by this Expert</h2>
|
{/* <h2 className="title-h2">All Offers by this Expert</h2>
|
||||||
<div className="offers-list">
|
<div className="offers-list">
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode, Suspense } from 'react';
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
import { unstable_setRequestLocale } from 'next-intl/server';
|
import { unstable_setRequestLocale } from 'next-intl/server';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
|
@ -6,7 +6,7 @@ import { ConfigProvider } from 'antd';
|
||||||
import { AntdRegistry } from '@ant-design/nextjs-registry';
|
import { AntdRegistry } from '@ant-design/nextjs-registry';
|
||||||
import theme from '../../constants/theme';
|
import theme from '../../constants/theme';
|
||||||
import { ALLOWED_LOCALES } from '../../constants/locale';
|
import { ALLOWED_LOCALES } from '../../constants/locale';
|
||||||
import { Header, Footer } from '../../components/Page';
|
import { Header, Footer, AppConfig } from '../../components/Page';
|
||||||
|
|
||||||
type LayoutProps = {
|
type LayoutProps = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
@ -30,6 +30,9 @@ export default function LocaleLayout({ children, params: { locale } }: LayoutPro
|
||||||
<AntdRegistry>
|
<AntdRegistry>
|
||||||
<ConfigProvider theme={theme}>
|
<ConfigProvider theme={theme}>
|
||||||
<div className="b-wrapper">
|
<div className="b-wrapper">
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<AppConfig />
|
||||||
|
</Suspense>
|
||||||
<div className="b-content">
|
<div className="b-content">
|
||||||
<Header locale={locale} />
|
<Header locale={locale} />
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { fetchBlogPosts } from '../lib/contentful/blogPosts';
|
||||||
|
|
||||||
|
export default async function sitemap() {
|
||||||
|
const paths = [
|
||||||
|
{
|
||||||
|
url: process.env.NEXT_PUBLIC_HOST,
|
||||||
|
lastModified: new Date(),
|
||||||
|
changeFrequency: "monthly",
|
||||||
|
priority: 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
const blogPosts = await fetchBlogPosts({ preview: false })
|
||||||
|
|
||||||
|
blogPosts.data.forEach((item) => {
|
||||||
|
|
||||||
|
paths.push({
|
||||||
|
url: `${process.env.NEXT_PUBLIC_HOST}${item.slug}`,
|
||||||
|
lastModified: item.createdAt.split('T')[0],
|
||||||
|
changeFrequency: 'daily',
|
||||||
|
priority: '1.0'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return paths
|
||||||
|
}
|
|
@ -12,9 +12,10 @@ import { validateImage } from '../../utils/account';
|
||||||
import { useProfileSettings } from '../../actions/hooks/useProfileSettings';
|
import { useProfileSettings } from '../../actions/hooks/useProfileSettings';
|
||||||
import { CustomInput } from '../view/CustomInput';
|
import { CustomInput } from '../view/CustomInput';
|
||||||
import { OutlinedButton } from '../view/OutlinedButton';
|
import { OutlinedButton } from '../view/OutlinedButton';
|
||||||
import { FilledYellowButton } from '../view/FilledButton';
|
import {FilledButton, FilledSquareButton, FilledYellowButton} from '../view/FilledButton';
|
||||||
import { DeleteAccountModal } from '../Modals/DeleteAccountModal';
|
import { DeleteAccountModal } from '../Modals/DeleteAccountModal';
|
||||||
import { Loader } from '../view/Loader';
|
import { Loader } from '../view/Loader';
|
||||||
|
import {ButtonProps} from "antd/es/button/button";
|
||||||
|
|
||||||
type ProfileSettingsProps = {
|
type ProfileSettingsProps = {
|
||||||
locale: string;
|
locale: string;
|
||||||
|
@ -40,6 +41,20 @@ export const ProfileSettings: FC<ProfileSettingsProps> = ({ locale }) => {
|
||||||
}
|
}
|
||||||
}, [profileSettings]);
|
}, [profileSettings]);
|
||||||
|
|
||||||
|
const onSave = (newProfile: ProfileRequest) => {
|
||||||
|
setSaveLoading(true);
|
||||||
|
save(newProfile)
|
||||||
|
.then(() => {
|
||||||
|
fetchProfileSettings();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
message.error('Не удалось сохранить изменения');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setSaveLoading(false);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const onSaveProfile = () => {
|
const onSaveProfile = () => {
|
||||||
form.validateFields()
|
form.validateFields()
|
||||||
.then(({ login, surname, username }) => {
|
.then(({ login, surname, username }) => {
|
||||||
|
@ -55,28 +70,19 @@ export const ProfileSettings: FC<ProfileSettingsProps> = ({ locale }) => {
|
||||||
languagesLinks: languagesLinks?.map(({ languageId }) => ({ languageId })) || []
|
languagesLinks: languagesLinks?.map(({ languageId }) => ({ languageId })) || []
|
||||||
};
|
};
|
||||||
|
|
||||||
// if (photo) {
|
if (photo) {
|
||||||
// console.log(photo);
|
const reader = new FileReader();
|
||||||
// const formData = new FormData();
|
reader.readAsDataURL(photo as File);
|
||||||
// formData.append('file', photo as FileType);
|
reader.onloadend = () => {
|
||||||
//
|
const newReg = new RegExp('data:image/(png|jpg|jpeg);base64,')
|
||||||
// newProfile.faceImage = photo;
|
newProfile.faceImage = reader.result.replace(newReg, '');
|
||||||
// newProfile.isFaceImageKeepExisting = false;
|
newProfile.isFaceImageKeepExisting = false;
|
||||||
// }
|
|
||||||
|
|
||||||
console.log(newProfile);
|
onSave(newProfile);
|
||||||
|
}
|
||||||
setSaveLoading(true);
|
} else {
|
||||||
save(newProfile)
|
onSave(newProfile);
|
||||||
.then(() => {
|
}
|
||||||
fetchProfileSettings();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
message.error('Не удалось сохранить изменения');
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setSaveLoading(false);
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,17 +105,14 @@ export const ProfileSettings: FC<ProfileSettingsProps> = ({ locale }) => {
|
||||||
return (
|
return (
|
||||||
<Loader isLoading={fetchLoading} refresh={fetchProfileSettings}>
|
<Loader isLoading={fetchLoading} refresh={fetchProfileSettings}>
|
||||||
<Form form={form} className="form-settings">
|
<Form form={form} className="form-settings">
|
||||||
<div className="user-avatar">
|
|
||||||
<div className="user-avatar__edit" style={profileSettings?.faceImageUrl ? { backgroundImage: `url(${profileSettings.faceImageUrl})` } : undefined}>
|
|
||||||
<input className="" type="file" id="input-file" />
|
|
||||||
<label htmlFor="input-file" className="form-label" />
|
|
||||||
</div>
|
|
||||||
<div className="user-avatar__text">{i18nText('photoDesc', locale)}</div>
|
|
||||||
</div>
|
|
||||||
<ImgCrop
|
<ImgCrop
|
||||||
modalTitle="Редактировать"
|
modalTitle="Редактировать"
|
||||||
modalOk="Сохранить"
|
modalOk="Сохранить"
|
||||||
modalCancel="Отмена"
|
modalCancel="Отмена"
|
||||||
|
modalProps={{
|
||||||
|
okButtonProps: { className: 'b-button__filled_yellow' },
|
||||||
|
cancelButtonProps: { className: 'b-button__outlined' }
|
||||||
|
}}
|
||||||
beforeCrop={beforeCrop}
|
beforeCrop={beforeCrop}
|
||||||
>
|
>
|
||||||
<Upload
|
<Upload
|
||||||
|
@ -121,13 +124,22 @@ export const ProfileSettings: FC<ProfileSettingsProps> = ({ locale }) => {
|
||||||
url: profileSettings.faceImageUrl
|
url: profileSettings.faceImageUrl
|
||||||
}
|
}
|
||||||
] : undefined}
|
] : undefined}
|
||||||
accept=".jpg,.jpeg,.png,.gif"
|
accept=".jpg,.jpeg,.png"
|
||||||
beforeUpload={beforeUpload}
|
beforeUpload={beforeUpload}
|
||||||
multiple={false}
|
multiple={false}
|
||||||
showUploadList={false}
|
showUploadList={false}
|
||||||
>
|
>
|
||||||
{photo && <img height={100} width={100} src={URL.createObjectURL(photo)} />}
|
<div className="user-avatar">
|
||||||
<Button icon={<CameraOutlined />}>Click to Upload</Button>
|
<div className="user-avatar__edit" style={photo
|
||||||
|
? { backgroundImage: `url(${URL.createObjectURL(photo)})` }
|
||||||
|
: profileSettings?.faceImageUrl ? { backgroundImage: `url(${profileSettings.faceImageUrl})`} : undefined }>
|
||||||
|
<FilledSquareButton
|
||||||
|
type="primary"
|
||||||
|
icon={<CameraOutlined style={{ fontSize: 28 }} />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="user-avatar__text">{i18nText('photoDesc', locale)}</div>
|
||||||
|
</div>
|
||||||
</Upload>
|
</Upload>
|
||||||
</ImgCrop>
|
</ImgCrop>
|
||||||
<div className="form-fieldset">
|
<div className="form-fieldset">
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
'use client';
|
||||||
|
import React, {useState} from "react";
|
||||||
|
import {Languages} from "../../types/tags";
|
||||||
|
import {BlogPostCategory} from "../../types/blogPostCategory";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
languages?: Languages;
|
||||||
|
basePath: string;
|
||||||
|
locale: string;
|
||||||
|
cats: BlogPostCategory[],
|
||||||
|
slug: string
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BlogPostCategories = ({ basePath = '/', cats = [], slug = '' }: Props) => {
|
||||||
|
const [currentCat, setCurrentCat] = useState<String>(slug);
|
||||||
|
return (
|
||||||
|
<div className="b-news__filter ">
|
||||||
|
<div className="b-inner">
|
||||||
|
<div className="wrap-filter">
|
||||||
|
{
|
||||||
|
cats.map((cat, i)=>(
|
||||||
|
<Link
|
||||||
|
href={ basePath+'category/'+cat.slug} key={'blogCat'+i}
|
||||||
|
className={"filter-item"+(cat.slug === currentCat ? ' active' : '')}
|
||||||
|
>
|
||||||
|
{cat.title}
|
||||||
|
</Link>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { DEFAULT_PAGE_SIZE } from '../../constants/common';
|
||||||
|
import {getLanguages} from "../../actions/tags";
|
||||||
|
import {fetchBlogPosts} from "../../lib/contentful/blogPosts";
|
||||||
|
import {fetchBlogPostCategories} from "../../lib/contentful/blogPostsCategories";
|
||||||
|
import {BlogPostsList} from "./BlogPostsList";
|
||||||
|
import {BlogPostCategories} from "./BlogPostCategories";
|
||||||
|
|
||||||
|
type PostsProps = {
|
||||||
|
basePath: string;
|
||||||
|
locale: string;
|
||||||
|
pageSize?: number;
|
||||||
|
currentCat: string;
|
||||||
|
page?: number
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BlogPosts = async ({ basePath = '/', locale, pageSize = DEFAULT_PAGE_SIZE, currentCat = '', page = 1 }: PostsProps) => {
|
||||||
|
const languages = await getLanguages(locale);
|
||||||
|
const {data, total} = await fetchBlogPosts({preview: false, category: currentCat, page: page})
|
||||||
|
const cats = await fetchBlogPostCategories(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="b-news">
|
||||||
|
<div className="b-news__header">
|
||||||
|
<div className="b-inner">
|
||||||
|
<h1 className="title-h1">
|
||||||
|
Mentorship, Career <br/>
|
||||||
|
Development & Coaching
|
||||||
|
</h1>
|
||||||
|
<div className="wrap-text">
|
||||||
|
<p className="">The ins-and-outs of building a career in tech, gaining <br/> experience</p>
|
||||||
|
<p className="">from a mentor, and getting your feet wet with coaching.</p>
|
||||||
|
</div>
|
||||||
|
<div className="b-news__header__img">
|
||||||
|
<img className="" src="/images/news-top.png" alt=""/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<BlogPostCategories
|
||||||
|
slug={currentCat}
|
||||||
|
cats={cats}
|
||||||
|
basePath={basePath}
|
||||||
|
locale={locale}
|
||||||
|
/>
|
||||||
|
<BlogPostsList
|
||||||
|
data={data}
|
||||||
|
total={total}
|
||||||
|
basePath={basePath}
|
||||||
|
locale={locale}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { DEFAULT_PAGE_SIZE } from '../../constants/common';
|
||||||
|
import {Languages, SearchData} from "../../types/tags";
|
||||||
|
import {BlogPost} from "../../types/blogPost";
|
||||||
|
import Link from "next/link";
|
||||||
|
import {CustomPagination} from "../view/CustomPagination";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
searchData?: SearchData;
|
||||||
|
languages?: Languages;
|
||||||
|
basePath: string;
|
||||||
|
locale: string;
|
||||||
|
data: BlogPost[],
|
||||||
|
total: number,
|
||||||
|
pageSize: number
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BlogPostsList = ({ basePath = '/', locale, pageSize = DEFAULT_PAGE_SIZE, data = [], total= 0 }: Props) => {
|
||||||
|
const currentPage = 1
|
||||||
|
const onChangePage = (page: number) => {
|
||||||
|
router.push(page === 1 ? basePath : basePath+'?page='+page);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="b-news__result-list">
|
||||||
|
<div className="b-inner">
|
||||||
|
<div className="news-list">
|
||||||
|
{data.map((item, i) => (
|
||||||
|
<li key={'blog'+i} className="list-sidebar__item">
|
||||||
|
<Link href={`/${locale}/blog/${item.slug}`} className="news-item">
|
||||||
|
<div className="news-item__image">
|
||||||
|
<img className="" src={item.listImage?.src} alt={item.listImage?.alt}/>
|
||||||
|
</div>
|
||||||
|
<div className="news-item__inner">
|
||||||
|
<div className="">
|
||||||
|
<div className="news-item__title">
|
||||||
|
{item.title}
|
||||||
|
</div>
|
||||||
|
<div className="news-item__badge">{item.category}</div>
|
||||||
|
<div className="news-item__text">
|
||||||
|
{item.excerpt}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="news-item__info">
|
||||||
|
<Link href={`/${locale}/experts/${item.author?.expertId}`} className="news-item">
|
||||||
|
<div className="news-item__info__author">
|
||||||
|
<img className="" src={item.author.avatar.src} alt=""/>
|
||||||
|
<div className="news-item__info__author__inner">
|
||||||
|
<div className="news-item__info__name">{item.author.name}</div>
|
||||||
|
<div className="news-item__info__date">{item.createdAt}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
<div className="news-item__info__counter">
|
||||||
|
<div className="news-item__info__like">
|
||||||
|
<img className="" src="/images/heart-outline.svg" alt=""/>
|
||||||
|
165
|
||||||
|
</div>
|
||||||
|
<div className="news-item__info__share">
|
||||||
|
<img className="" src="/images/share-social.svg" alt=""/>
|
||||||
|
Share
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{total > pageSize && (
|
||||||
|
<CustomPagination
|
||||||
|
total={total}
|
||||||
|
pageSize={pageSize}
|
||||||
|
onChange={onChangePage}
|
||||||
|
current={currentPage}
|
||||||
|
/>)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,43 +1,96 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { message } from 'antd';
|
import {Alert, message} from 'antd';
|
||||||
import { EditOutlined } from '@ant-design/icons';
|
import Image from 'next/image';
|
||||||
import { i18nText } from '../../i18nKeys';
|
import { i18nText } from '../../i18nKeys';
|
||||||
import { ExpertData } from '../../types/profile';
|
import { ExpertData, PayInfo, ProfileData } from '../../types/profile';
|
||||||
|
import { ExpertsTags } from '../../types/tags';
|
||||||
|
import { PracticeDTO } from '../../types/practice';
|
||||||
|
import { EducationDTO } from '../../types/education';
|
||||||
|
import { ScheduleDTO } from '../../types/schedule';
|
||||||
import { AUTH_TOKEN_KEY } from '../../constants/common';
|
import { AUTH_TOKEN_KEY } from '../../constants/common';
|
||||||
import { useLocalStorage } from '../../hooks/useLocalStorage';
|
import { useLocalStorage } from '../../hooks/useLocalStorage';
|
||||||
import { getTags } from '../../actions/profile';
|
import { getTags, getPayData, getEducation, getPractice, getSchedule, getPersonalData } from '../../actions/profile';
|
||||||
import { Loader } from '../view/Loader';
|
import { Loader } from '../view/Loader';
|
||||||
import { LinkButton } from '../view/LinkButton';
|
|
||||||
import { ExpertTags } from './content/ExpertTags';
|
import { ExpertTags } from './content/ExpertTags';
|
||||||
import { ExpertSchedule } from './content/ExpertSchedule';
|
import { ExpertSchedule } from './content/ExpertSchedule';
|
||||||
import { ExpertPayData } from './content/ExpertPayData';
|
import { ExpertPayData } from './content/ExpertPayData';
|
||||||
import { ExpertEducation } from './content/ExpertEducation';
|
import { ExpertEducation } from './content/ExpertEducation';
|
||||||
|
import { ExpertAbout } from './content/ExpertAbout';
|
||||||
|
|
||||||
type ExpertProfileProps = {
|
type ExpertProfileProps = {
|
||||||
locale: string;
|
locale: string;
|
||||||
data: ExpertData;
|
data: ExpertData;
|
||||||
updateData: (data: ExpertData) => void;
|
updateData: (data: ExpertData) => void;
|
||||||
|
isFull: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ExpertProfile = ({ locale, data, updateData }: ExpertProfileProps) => {
|
type NewDataPartProps<T> = {
|
||||||
|
key: keyof ExpertData,
|
||||||
|
getNewData: (locale: string, token: string) => Promise<T>,
|
||||||
|
errorMessage?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ExpertProfile = ({ locale, data, updateData, isFull }: ExpertProfileProps) => {
|
||||||
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
|
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
|
||||||
const [loading, setLoading] = useState<(keyof ExpertData)[]>([]);
|
const [loading, setLoading] = useState<(keyof ExpertData)[]>([]);
|
||||||
|
|
||||||
|
function getNewPartData <T>({ key, getNewData, errorMessage = 'Не удалось получить данные' }: NewDataPartProps<T>) {
|
||||||
|
setLoading([key]);
|
||||||
|
getNewData(locale, jwt)
|
||||||
|
.then((newData) => {
|
||||||
|
updateData({
|
||||||
|
...data,
|
||||||
|
[key]: newData
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => message.error(errorMessage))
|
||||||
|
.finally(() => setLoading([]));
|
||||||
|
}
|
||||||
|
|
||||||
const updateExpert = (key: keyof ExpertData) => {
|
const updateExpert = (key: keyof ExpertData) => {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'tags':
|
case 'tags':
|
||||||
setLoading([key]);
|
getNewPartData<ExpertsTags>({
|
||||||
getTags(locale, jwt)
|
key,
|
||||||
.then((tags) => {
|
getNewData: getTags,
|
||||||
updateData({
|
errorMessage: 'Не удалось получить направления'
|
||||||
...data,
|
});
|
||||||
tags
|
break;
|
||||||
|
case 'practice':
|
||||||
|
getNewPartData<PracticeDTO>({
|
||||||
|
key,
|
||||||
|
getNewData: getPractice
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'education':
|
||||||
|
getNewPartData<EducationDTO>({
|
||||||
|
key,
|
||||||
|
getNewData: getEducation,
|
||||||
|
errorMessage: 'Не удалось получить информацию об образовании'
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'schedule':
|
||||||
|
getNewPartData<ScheduleDTO>({
|
||||||
|
key,
|
||||||
|
getNewData: getSchedule,
|
||||||
|
errorMessage: 'Не удалось получить расписание'
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'person':
|
||||||
|
getNewPartData<ProfileData>({
|
||||||
|
key,
|
||||||
|
getNewData: getPersonalData,
|
||||||
|
errorMessage: 'Не удалось получить информацию о пользователе'
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'payData':
|
||||||
|
getNewPartData<{ person6Data?: PayInfo }>({
|
||||||
|
key,
|
||||||
|
getNewData: getPayData,
|
||||||
|
errorMessage: 'Не удалось получить платежную информацию'
|
||||||
});
|
});
|
||||||
})
|
|
||||||
.catch(() => message.error('Не удалось обновить направления'))
|
|
||||||
.finally(() => setLoading([]));
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
@ -52,36 +105,39 @@ export const ExpertProfile = ({ locale, data, updateData }: ExpertProfileProps)
|
||||||
<div className="coaching-info">
|
<div className="coaching-info">
|
||||||
<div className="coaching-profile">
|
<div className="coaching-profile">
|
||||||
<div className="coaching-profile__portrait">
|
<div className="coaching-profile__portrait">
|
||||||
<img src="/images/person.png" className="" alt="" />
|
<Image src={data?.person?.faceImageUrl || '/images/user-avatar.png'} width={216} height={216} alt="" />
|
||||||
</div>
|
</div>
|
||||||
<div className="coaching-profile__inner">
|
<div className="coaching-profile__inner" style={{ flex: 1 }}>
|
||||||
<div className="coaching-profile__name">
|
<div className="coaching-profile__name">
|
||||||
David
|
{`${data?.person?.username} ${data?.person?.surname || ''}`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{!isFull && (
|
||||||
</div>
|
<Alert
|
||||||
<div className="coaching-section__wrap">
|
message="Проверьте заполненность блоков"
|
||||||
<div className="coaching-section">
|
description={(
|
||||||
<div className="coaching-section__title">
|
<ul className="b-rules-list">
|
||||||
<h2 className="title-h2">{i18nText('aboutCoach', locale)}</h2>
|
<li>о себе</li>
|
||||||
<h2 className="title-h2">person1 + person4</h2>
|
<li>темы сессии</li>
|
||||||
<LinkButton
|
<li>рабочее расписание</li>
|
||||||
type="link"
|
<li>информация об образовании</li>
|
||||||
icon={<EditOutlined />}
|
<li>платежная информация</li>
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
type="warning"
|
||||||
|
showIcon
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
<div className="card-profile__header__title">
|
|
||||||
{`12 ${i18nText('practiceHours', locale)}`}
|
|
||||||
</div>
|
|
||||||
<div className="card-profile__header__title ">
|
|
||||||
{`15 ${i18nText('supervisionCount', locale)}`}
|
|
||||||
</div>
|
|
||||||
<div className="base-text">
|
|
||||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam aliquet, lectus nec viverra
|
|
||||||
malesuada, ligula sem tempor risus, non posuere urna diam a libero.
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Loader isLoading={loading.includes('practice') || loading.includes('person')}>
|
||||||
|
<ExpertAbout
|
||||||
|
locale={locale}
|
||||||
|
practice={data?.practice}
|
||||||
|
person={data?.person}
|
||||||
|
updateExpert={updateExpert}
|
||||||
|
/>
|
||||||
|
</Loader>
|
||||||
<Loader isLoading={loading.includes('tags')}>
|
<Loader isLoading={loading.includes('tags')}>
|
||||||
<ExpertTags
|
<ExpertTags
|
||||||
locale={locale}
|
locale={locale}
|
||||||
|
@ -89,10 +145,28 @@ export const ExpertProfile = ({ locale, data, updateData }: ExpertProfileProps)
|
||||||
updateExpert={updateExpert}
|
updateExpert={updateExpert}
|
||||||
/>
|
/>
|
||||||
</Loader>
|
</Loader>
|
||||||
<ExpertSchedule locale={locale} data={data?.schedule} />
|
<Loader isLoading={loading.includes('schedule')}>
|
||||||
<ExpertEducation locale={locale} data={data?.education} />
|
<ExpertSchedule
|
||||||
<ExpertPayData locale={locale} data={data?.payData?.person6Data} />
|
locale={locale}
|
||||||
|
data={data?.schedule}
|
||||||
|
updateExpert={updateExpert}
|
||||||
|
/>
|
||||||
|
</Loader>
|
||||||
|
<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>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Tag } from 'antd';
|
||||||
|
import { EditOutlined } from '@ant-design/icons';
|
||||||
|
import { ExpertData, ProfileData } from '../../../types/profile';
|
||||||
|
import { i18nText } from '../../../i18nKeys';
|
||||||
|
import { PracticeDTO } from '../../../types/practice';
|
||||||
|
import { LinkButton } from '../../view/LinkButton';
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,65 +1,154 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
import { EditOutlined } from '@ant-design/icons';
|
import { EditOutlined } from '@ant-design/icons';
|
||||||
import { EducationDTO } from '../../../types/education';
|
import { EducationDTO } from '../../../types/education';
|
||||||
import { i18nText } from '../../../i18nKeys';
|
import { i18nText } from '../../../i18nKeys';
|
||||||
import { LinkButton } from '../../view/LinkButton';
|
import { LinkButton } from '../../view/LinkButton';
|
||||||
|
import {ExpertCertificate} from "../../Experts/ExpertDetails";
|
||||||
|
import {useState} from "react";
|
||||||
|
import {ExpertData} from "../../../types/profile";
|
||||||
|
import {EditExpertEducationModal} from "../../Modals/EditExpertEducationModal";
|
||||||
|
|
||||||
type ExpertEducationProps = {
|
type ExpertEducationProps = {
|
||||||
locale: string;
|
locale: string;
|
||||||
data?: EducationDTO;
|
data?: EducationDTO;
|
||||||
|
updateExpert: (key: keyof ExpertData) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ExpertEducation = ({ locale, data, 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 (
|
return (
|
||||||
<div className="coaching-section__wrap">
|
<div className="coaching-section__wrap">
|
||||||
<div className="coaching-section">
|
<div className="coaching-section">
|
||||||
<div className="coaching-section__title">
|
<div className="coaching-section__title">
|
||||||
<h2 className="title-h2">{i18nText('education', locale)}</h2>
|
<h2 className="title-h2">{i18nText('skillsInfo', locale)}</h2>
|
||||||
<h2 className="title-h2">person2</h2>
|
|
||||||
<LinkButton
|
<LinkButton
|
||||||
type="link"
|
type="link"
|
||||||
icon={<EditOutlined />}
|
icon={<EditOutlined />}
|
||||||
|
onClick={() => setShowEdit(true)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{data?.person2Data?.educations?.length > 0 && (
|
||||||
<div className="coaching-section__desc">
|
<div className="coaching-section__desc">
|
||||||
<h3 className="title-h3">Psychologist</h3>
|
{data?.person2Data?.educations?.map(({ id, title, description, document }) => (
|
||||||
<div className="base-text">
|
<div key={id}>
|
||||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam aliquet, lectus nec viverra
|
<h3 className="title-h3">{title}</h3>
|
||||||
malesuada, ligula sem tempor risus, non posuere urna diam a libero.
|
{description && <div className="base-text">{description}</div>}
|
||||||
</div>
|
{document && (
|
||||||
<div className="sertific">
|
<div className="sertific">
|
||||||
<img src="/images/sertific.png" className="" alt="" />
|
<ExpertCertificate document={document} />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{data?.person2Data?.certificates?.length > 0 && (
|
||||||
<div className="coaching-section">
|
<div className="coaching-section">
|
||||||
<h2 className="title-h2">{i18nText('profCertification', locale)}</h2>
|
<h2 className="title-h2">{i18nText('profCertification', locale)}</h2>
|
||||||
<div className="coaching-section__desc">
|
<div className="coaching-section__desc">
|
||||||
|
{data?.person2Data?.certificates?.map((cert) => (
|
||||||
|
<div key={cert.id}>
|
||||||
<div className="base-text">
|
<div className="base-text">
|
||||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam aliquet, lectus nec viverra
|
{`${getAssociationLevel(cert?.associationLevelId)} ${getAssociation(cert?.associationLevelId)}`}
|
||||||
malesuada, ligula sem tempor risus, non posuere urna diam a libero.
|
</div>
|
||||||
</div>
|
{cert.document && (
|
||||||
|
<div className="sertific">
|
||||||
|
<ExpertCertificate document={cert.document} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
{data?.person2Data?.trainings?.length > 0 && (
|
||||||
<div className="coaching-section">
|
<div className="coaching-section">
|
||||||
<h2 className="title-h2">
|
<h2 className="title-h2">
|
||||||
{`${i18nText('trainings', locale)} | ${i18nText('seminars', locale)} | ${i18nText('courses', locale)}`}
|
{`${i18nText('trainings', locale)} | ${i18nText('seminars', locale)} | ${i18nText('courses', locale)}`}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="coaching-section__desc">
|
<div className="coaching-section__desc">
|
||||||
<div className="base-text">
|
{data?.person2Data?.trainings?.map(({ id, title, description, document }) => (
|
||||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam aliquet, lectus nec viverra
|
<div key={id}>
|
||||||
malesuada, ligula sem tempor risus, non posuere urna diam a libero.
|
<h3 className="title-h3">{title}</h3>
|
||||||
</div>
|
{description && <div className="base-text">{description}</div>}
|
||||||
|
{document && (
|
||||||
|
<div className="sertific">
|
||||||
|
<ExpertCertificate document={document} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
{data?.person2Data?.mbas?.length > 0 && (
|
||||||
<div className="coaching-section">
|
<div className="coaching-section">
|
||||||
<h2 className="title-h2">{i18nText('mba', locale)}</h2>
|
<h2 className="title-h2">{i18nText('mba', locale)}</h2>
|
||||||
<div className="coaching-section__desc">
|
<div className="coaching-section__desc">
|
||||||
<div className="base-text">
|
{data?.person2Data?.mbas?.map(({ id, title, description, document }) => (
|
||||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam aliquet, lectus nec viverra
|
<div key={id}>
|
||||||
malesuada, ligula sem tempor risus, non posuere urna diam a libero.
|
<h3 className="title-h3">{title}</h3>
|
||||||
|
{description && <div className="base-text">{description}</div>}
|
||||||
|
{document && (
|
||||||
|
<div className="sertific">
|
||||||
|
<ExpertCertificate document={document} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
{data?.person2Data?.experiences?.length > 0 && (
|
||||||
|
<div className="coaching-section">
|
||||||
|
<h2 className="title-h2">{i18nText('mExperiences', locale)}</h2>
|
||||||
|
<div className="coaching-section__desc">
|
||||||
|
{data?.person2Data?.experiences?.map(({ id, title, description, document }) => (
|
||||||
|
<div key={id}>
|
||||||
|
<h3 className="title-h3">{title}</h3>
|
||||||
|
{description && <div className="base-text">{description}</div>}
|
||||||
|
{document && (
|
||||||
|
<div className="sertific">
|
||||||
|
<ExpertCertificate document={document} />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<EditExpertEducationModal
|
||||||
|
open={showEdit}
|
||||||
|
handleCancel={() => setShowEdit(false)}
|
||||||
|
locale={locale}
|
||||||
|
data={data}
|
||||||
|
refresh={() => updateExpert('education')}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,28 +1,65 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
import { EditOutlined } from '@ant-design/icons';
|
import { EditOutlined } from '@ant-design/icons';
|
||||||
import { i18nText } from '../../../i18nKeys';
|
import { i18nText } from '../../../i18nKeys';
|
||||||
import { PayInfo } from '../../../types/profile';
|
import { ExpertData, PayInfo } from '../../../types/profile';
|
||||||
import { LinkButton } from '../../view/LinkButton';
|
import { LinkButton } from '../../view/LinkButton';
|
||||||
|
import { EditExpertPayDataModal } from '../../Modals/EditExpertPayDataModal';
|
||||||
|
|
||||||
type ExpertPayDataProps = {
|
type ExpertPayDataProps = {
|
||||||
locale: string;
|
locale: string;
|
||||||
data?: PayInfo
|
data?: PayInfo;
|
||||||
|
updateExpert: (key: keyof ExpertData) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ExpertPayData = ({ locale, data }: ExpertPayDataProps) => {
|
export const ExpertPayData = ({ locale, data, updateExpert }: ExpertPayDataProps) => {
|
||||||
|
const [showEdit, setShowEdit] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const hide = (str?: string) => {
|
||||||
|
const reg = new RegExp('(.)(?=.*....)', 'gi');
|
||||||
|
return str ? str.replace(reg, '*') : '';
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="coaching-section__wrap">
|
<div className="coaching-section__wrap">
|
||||||
<div className="coaching-section">
|
<div className="coaching-section">
|
||||||
<div className="coaching-section__title">
|
<div className="coaching-section__title">
|
||||||
<h2 className="title-h2">Card data - person6</h2>
|
<h2 className="title-h2">{i18nText('payInfo', locale)}</h2>
|
||||||
<LinkButton
|
<LinkButton
|
||||||
type="link"
|
type="link"
|
||||||
icon={<EditOutlined />}
|
icon={<EditOutlined />}
|
||||||
|
onClick={() => setShowEdit(true)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="base-text">
|
<div className="base-text pay-data-list">
|
||||||
Card
|
{data?.beneficiaryName && (
|
||||||
|
<div>
|
||||||
|
<div>{i18nText('beneficiaryName', locale)}</div>
|
||||||
|
<div>{data.beneficiaryName}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{data?.bicOrSwift && (
|
||||||
|
<div>
|
||||||
|
<div>{i18nText('bicOrSwift', locale)}</div>
|
||||||
|
<div>{hide(data.bicOrSwift)}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{data?.iban && (
|
||||||
|
<div>
|
||||||
|
<div>IBAN</div>
|
||||||
|
<div>{hide(data.iban)}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<EditExpertPayDataModal
|
||||||
|
locale={locale}
|
||||||
|
open={showEdit}
|
||||||
|
data={data}
|
||||||
|
handleCancel={() => setShowEdit(false)}
|
||||||
|
refresh={() => updateExpert('payData')}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,28 +1,56 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Tag } from 'antd';
|
||||||
import { EditOutlined } from '@ant-design/icons';
|
import { EditOutlined } from '@ant-design/icons';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import { ScheduleDTO } from '../../../types/schedule';
|
import { ScheduleDTO } from '../../../types/schedule';
|
||||||
import { i18nText } from '../../../i18nKeys';
|
import { i18nText } from '../../../i18nKeys';
|
||||||
|
import { getCurrentTime, getTimeString } from '../../../utils/time';
|
||||||
|
import { ExpertData } from '../../../types/profile';
|
||||||
import { LinkButton } from '../../view/LinkButton';
|
import { LinkButton } from '../../view/LinkButton';
|
||||||
|
import { EditExpertScheduleModal } from '../../Modals/EditExpertScheduleModal';
|
||||||
|
|
||||||
type ExpertScheduleProps = {
|
type ExpertScheduleProps = {
|
||||||
locale: string;
|
locale: string;
|
||||||
data?: ScheduleDTO;
|
data?: ScheduleDTO;
|
||||||
|
updateExpert: (key: keyof ExpertData) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ExpertSchedule = ({ locale, data }: ExpertScheduleProps) => {
|
export const ExpertSchedule = ({ locale, data, updateExpert }: ExpertScheduleProps) => {
|
||||||
|
const [showEdit, setShowEdit] = useState<boolean>(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="coaching-section__wrap">
|
<div className="coaching-section__wrap">
|
||||||
<div className="coaching-section">
|
<div className="coaching-section">
|
||||||
<div className="coaching-section__title">
|
<div className="coaching-section__title">
|
||||||
<h2 className="title-h2">Schedule - person51</h2>
|
<h2 className="title-h2">{i18nText('schedule', locale)}</h2>
|
||||||
<LinkButton
|
<LinkButton
|
||||||
type="link"
|
type="link"
|
||||||
icon={<EditOutlined />}
|
icon={<EditOutlined />}
|
||||||
|
onClick={() => setShowEdit(true)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="base-text">
|
<div className="b-schedule-list">
|
||||||
Schedule
|
{data && data?.workingTimes?.map((date, index) => {
|
||||||
|
const { startDay, startTimeMin, endTimeMin } = getCurrentTime(date, dayjs().format('Z'));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={`date_${index}`}>
|
||||||
|
<Tag className="skills__list__item">{i18nText(startDay, locale)}</Tag>
|
||||||
|
<div>{startTimeMin ? getTimeString(startTimeMin) : '00:00'}</div>
|
||||||
|
<span>-</span>
|
||||||
|
<div>{endTimeMin ? getTimeString(endTimeMin) : '00:00'}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<EditExpertScheduleModal
|
||||||
|
open={showEdit}
|
||||||
|
handleCancel={() => setShowEdit(false)}
|
||||||
|
locale={locale}
|
||||||
|
data={data}
|
||||||
|
refresh={() => updateExpert('schedule')}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -22,7 +22,7 @@ export const ExpertTags = ({ locale, data, updateExpert }: ExpertTagsProps) => {
|
||||||
<div className="coaching-section__wrap">
|
<div className="coaching-section__wrap">
|
||||||
<div className="coaching-section">
|
<div className="coaching-section">
|
||||||
<div className="coaching-section__title">
|
<div className="coaching-section__title">
|
||||||
<h2 className="title-h2">{i18nText('direction', locale)}</h2>
|
<h2 className="title-h2">{i18nText('topics', locale)}</h2>
|
||||||
<LinkButton
|
<LinkButton
|
||||||
type="link"
|
type="link"
|
||||||
icon={<EditOutlined />}
|
icon={<EditOutlined />}
|
||||||
|
|
|
@ -4,7 +4,8 @@ import React, { FC } from 'react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { Tag, Image as AntdImage, Space } from 'antd';
|
import { Tag, Image as AntdImage, Space } from 'antd';
|
||||||
import { ZoomInOutlined, ZoomOutOutlined, StarFilled } from '@ant-design/icons';
|
import { ZoomInOutlined, ZoomOutOutlined, StarFilled } from '@ant-design/icons';
|
||||||
import { ExpertDetails, ExpertDocument } from '../../types/experts';
|
import { ExpertDetails, Practice, ThemeGroup } from '../../types/experts';
|
||||||
|
import { ExpertDocument } from '../../types/file';
|
||||||
import { Locale } from '../../types/locale';
|
import { Locale } from '../../types/locale';
|
||||||
import { CustomRate } from '../view/CustomRate';
|
import { CustomRate } from '../view/CustomRate';
|
||||||
import { i18nText } from '../../i18nKeys';
|
import { i18nText } from '../../i18nKeys';
|
||||||
|
@ -15,6 +16,12 @@ type ExpertDetailsProps = {
|
||||||
locale?: string;
|
locale?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ExpertPracticeProps = {
|
||||||
|
cases?: Practice[];
|
||||||
|
themes?: ThemeGroup[];
|
||||||
|
locale?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export const ExpertCard: FC<ExpertDetailsProps> = ({ expert, locale }) => {
|
export const ExpertCard: FC<ExpertDetailsProps> = ({ expert, locale }) => {
|
||||||
const { publicCoachDetails } = expert || {};
|
const { publicCoachDetails } = expert || {};
|
||||||
|
|
||||||
|
@ -62,10 +69,10 @@ export const ExpertInformation: FC<ExpertDetailsProps> = ({ expert, locale }) =>
|
||||||
<div className="expert-info">
|
<div className="expert-info">
|
||||||
{/* <h2 className="title-h2">{}</h2> */}
|
{/* <h2 className="title-h2">{}</h2> */}
|
||||||
<div className="skills__list">
|
<div className="skills__list">
|
||||||
{coachLanguages?.map((skill) => <Tag key={skill} className="skills__list__item">{skill}</Tag>)}
|
{coachLanguages?.map((lang) => <Tag key={lang} className="skills__list__item">{lang}</Tag>)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="base-text">
|
{/* <p className="base-text">
|
||||||
Hello, my name is Marcelo. I am a Senior UX Designer with more than 6 years of experience working
|
Hello, my name is Marcelo. I am a Senior UX Designer with more than 6 years of experience working
|
||||||
with the largest companies in the world such as Disney, Globant and currently IBM.
|
with the largest companies in the world such as Disney, Globant and currently IBM.
|
||||||
During my career, I have helped organizations solve complex problems using aesthetically pleasing
|
During my career, I have helped organizations solve complex problems using aesthetically pleasing
|
||||||
|
@ -79,7 +86,7 @@ export const ExpertInformation: FC<ExpertDetailsProps> = ({ expert, locale }) =>
|
||||||
Strategic thinking <br /><br />
|
Strategic thinking <br /><br />
|
||||||
|
|
||||||
Oh, and I also speak Spanish!
|
Oh, and I also speak Spanish!
|
||||||
</p>
|
</p> */}
|
||||||
<div className="skills__list">
|
<div className="skills__list">
|
||||||
{tags?.map((skill) => <Tag key={skill?.id} className="skills__list__item">{skill?.name}</Tag>)}
|
{tags?.map((skill) => <Tag key={skill?.id} className="skills__list__item">{skill?.name}</Tag>)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -93,14 +100,12 @@ export const ExpertInformation: FC<ExpertDetailsProps> = ({ expert, locale }) =>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ExpertPractice: FC<ExpertDetailsProps> = ({ expert, locale }) => {
|
export const ExpertPractice: FC<ExpertPracticeProps> = ({ themes = [], cases = [], locale }) => {
|
||||||
const { publicCoachDetails: { practiceCases = [], themesGroups = [] } } = expert || {};
|
return cases?.length > 0 ? (
|
||||||
|
|
||||||
return practiceCases?.length > 0 ? (
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="title-h3">{i18nText('successfulCase', locale)}</h3>
|
<h3 className="title-h3">{i18nText('successfulCase', locale)}</h3>
|
||||||
{practiceCases?.map(({ id, description, themesGroupIds }) => {
|
{cases?.map(({ id, description, themesGroupIds }) => {
|
||||||
const filtered = themesGroups?.filter(({ id }) => themesGroupIds?.includes(+id));
|
const filtered = themes ? themes.filter(({ id }) => themesGroupIds?.includes(+id)) : [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={id} className="case-list">
|
<div key={id} className="case-list">
|
||||||
|
|
|
@ -114,6 +114,7 @@ export const ExpertsFilter = ({
|
||||||
...getObjectByAdditionalFilter(searchParams)
|
...getObjectByAdditionalFilter(searchParams)
|
||||||
};
|
};
|
||||||
const search = getSearchParamsString(newFilter);
|
const search = getSearchParamsString(newFilter);
|
||||||
|
console.log('basePath', basePath);
|
||||||
|
|
||||||
router.push(search ? `${basePath}?${search}#filter` : `${basePath}#filter`);
|
router.push(search ? `${basePath}?${search}#filter` : `${basePath}#filter`);
|
||||||
|
|
||||||
|
@ -162,18 +163,15 @@ export const ExpertsFilter = ({
|
||||||
), [filter, searchParams, searchData]);
|
), [filter, searchParams, searchData]);
|
||||||
|
|
||||||
const getLangList = () => {
|
const getLangList = () => {
|
||||||
const reg = searchLang ? new RegExp(searchLang, 'ig') : '';
|
const langList = searchLang ? (languages || []).filter(({ code, nativeSpelling }) => code.indexOf(searchLang) !== -1 || nativeSpelling.indexOf(searchLang) !== -1) : languages;
|
||||||
const langList = reg ? (languages || []).filter(({ code, nativeSpelling }) => reg.test(code) || reg.test(nativeSpelling)) : languages;
|
|
||||||
return langList?.length
|
return langList?.length
|
||||||
? getList('userLanguages', langList.map(({ code, nativeSpelling }) => ({ id: code, name: nativeSpelling })))
|
? getList('userLanguages', langList.map(({ code, nativeSpelling }) => ({ id: code, name: nativeSpelling })))
|
||||||
: null;
|
: null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTagsList = () => {
|
const getTagsList = () => {
|
||||||
const reg = searchTags ? new RegExp(searchTags, 'ig') : '';
|
if (searchTags) {
|
||||||
|
const tagsList = filteredTags.filter(({ name, group }) => name.indexOf(searchTags) !== -1 || group.indexOf(searchTags) !== -1);
|
||||||
if (reg) {
|
|
||||||
const tagsList = filteredTags.filter(({ name, group }) => reg.test(name) || reg.test(group));
|
|
||||||
return getList('themesTagIds', tagsList);
|
return getList('themesTagIds', tagsList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,214 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { FC, useEffect, useState } from 'react';
|
||||||
|
import { Modal, Button, message } from 'antd';
|
||||||
|
import { CloseOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { i18nText } from '../../i18nKeys';
|
||||||
|
import { AUTH_TOKEN_KEY } from '../../constants/common';
|
||||||
|
import { UTC_LIST } from '../../constants/time';
|
||||||
|
import { MapWorkingTime, ScheduleDTO } from '../../types/schedule';
|
||||||
|
import {
|
||||||
|
WEEK_DAY,
|
||||||
|
formattedSchedule,
|
||||||
|
getNewTime,
|
||||||
|
getTimeZoneOffset,
|
||||||
|
getTimeString,
|
||||||
|
formattedTimeByOffset, formattedWorkList
|
||||||
|
} from '../../utils/time';
|
||||||
|
import { useLocalStorage } from '../../hooks/useLocalStorage';
|
||||||
|
import { setSchedule } from '../../actions/profile';
|
||||||
|
import { CustomSelect } from '../view/CustomSelect';
|
||||||
|
import { CustomTimePicker } from '../view/CustomTimePicker';
|
||||||
|
import { LinkButton } from '../view/LinkButton';
|
||||||
|
import { OutlinedButton } from '../view/OutlinedButton';
|
||||||
|
|
||||||
|
type EditExpertScheduleModalProps = {
|
||||||
|
open: boolean;
|
||||||
|
handleCancel: () => void;
|
||||||
|
locale: string;
|
||||||
|
data?: ScheduleDTO;
|
||||||
|
refresh: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_WORK: MapWorkingTime = { startDay: '' };
|
||||||
|
|
||||||
|
export const EditExpertScheduleModal: FC<EditExpertScheduleModalProps> = ({
|
||||||
|
open,
|
||||||
|
handleCancel,
|
||||||
|
locale,
|
||||||
|
data,
|
||||||
|
refresh,
|
||||||
|
}) => {
|
||||||
|
const defaultTimeZone = dayjs().format('Z');
|
||||||
|
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
|
||||||
|
const [timeZone, setTimeZone] = useState<string>(defaultTimeZone);
|
||||||
|
const [workList, setWorkList] = useState<MapWorkingTime[]>([DEFAULT_WORK]);
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open && data?.workingTimes && data.workingTimes.length > 0) {
|
||||||
|
setWorkList(formattedSchedule(data.workingTimes, timeZone));
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
const onSave = () => {
|
||||||
|
const workingTimes = formattedWorkList(workList, timeZone);
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setSchedule(locale, jwt, { workingTimes })
|
||||||
|
.then(() => {
|
||||||
|
handleCancel();
|
||||||
|
refresh();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
message.error('Не удалось сохранить расписание');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
const addWorkingHours = () => {
|
||||||
|
setWorkList([
|
||||||
|
...workList,
|
||||||
|
DEFAULT_WORK
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteWorkingHours = (index: number) => {
|
||||||
|
setWorkList(workList.filter((work, i) => i !== index));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChangeWeekDay = (val: string, index: number) => {
|
||||||
|
setWorkList(workList.map((work, i) => {
|
||||||
|
if (i === index) {
|
||||||
|
return {
|
||||||
|
...work,
|
||||||
|
startDay: val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return work;
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChangeTime = (time: string, index: number, start?: boolean) => {
|
||||||
|
setWorkList(workList.map((work, i) => {
|
||||||
|
if (i === index) {
|
||||||
|
const timeMin = getNewTime(time);
|
||||||
|
let res;
|
||||||
|
|
||||||
|
if (start) {
|
||||||
|
res = {
|
||||||
|
startTimeMin: timeMin
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res = {
|
||||||
|
endTimeMin: timeMin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...work,
|
||||||
|
...res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return work;
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChangeTimeZone = (newTimeZone: string) => {
|
||||||
|
const offset = getTimeZoneOffset(timeZone, newTimeZone);
|
||||||
|
setTimeZone(newTimeZone);
|
||||||
|
setWorkList(workList.map((work) => formattedTimeByOffset(work, offset)));
|
||||||
|
}
|
||||||
|
|
||||||
|
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('schedule', locale)}</div>
|
||||||
|
<div className="b-modal__expert__inner" style={{ paddingRight: 12 }}>
|
||||||
|
<div style={{ paddingRight: 0, paddingBottom: 1 }}>
|
||||||
|
<div className="schedule">
|
||||||
|
<div className="schedule__inner">
|
||||||
|
<div className="timezone">
|
||||||
|
<div className="timezone__title">{`${i18nText('yourTimezone', locale)}: ${defaultTimeZone}`}</div>
|
||||||
|
<div className="timezone__utc">
|
||||||
|
<CustomSelect
|
||||||
|
label="UTC"
|
||||||
|
value={timeZone}
|
||||||
|
options={UTC_LIST.map((value) => ({ value, label: value }))}
|
||||||
|
onChange={(val) => onChangeTimeZone(val)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3 className="title-h3">{i18nText('workTime', locale)}</h3>
|
||||||
|
<div className="schedule__wrap">
|
||||||
|
{workList.length === 1 ? workList.map(({ startDay, startTimeMin, endTimeMin }, index) => (
|
||||||
|
<div key={`day_${index}`} className="schedule-item__single">
|
||||||
|
<CustomSelect />
|
||||||
|
<CustomSelect label={i18nText('startAt', locale)} />
|
||||||
|
<CustomSelect label={i18nText('finishAt', locale)} />
|
||||||
|
</div>
|
||||||
|
)) : null}
|
||||||
|
{workList.length > 1 ? workList.map(({ startDay, startTimeMin, endTimeMin }, index) => (
|
||||||
|
<div key={`day_${index}`} className="schedule-item">
|
||||||
|
<CustomSelect
|
||||||
|
label={i18nText('day', locale)}
|
||||||
|
value={startDay || undefined}
|
||||||
|
options={WEEK_DAY.map((value) => ({ value, label: i18nText(value, locale) }))}
|
||||||
|
onChange={(val) => onChangeWeekDay(val, index)}
|
||||||
|
/>
|
||||||
|
<CustomTimePicker
|
||||||
|
label={i18nText('startAt', locale)}
|
||||||
|
value={startTimeMin ? dayjs(getTimeString(startTimeMin), 'HH:mm') : dayjs('00:00', 'HH:mm')}
|
||||||
|
onChange={(time, timeString) => onChangeTime(timeString, index, true)}
|
||||||
|
/>
|
||||||
|
<CustomTimePicker
|
||||||
|
label={i18nText('finishAt', locale)}
|
||||||
|
value={endTimeMin ? dayjs(getTimeString(endTimeMin), 'HH:mm') : dayjs('00:00', 'HH:mm')}
|
||||||
|
onChange={(time, timeString) => onChangeTime(timeString, index)}
|
||||||
|
/>
|
||||||
|
<LinkButton
|
||||||
|
type="link"
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
onClick={() => deleteWorkingHours(index)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)) : null}
|
||||||
|
</div>
|
||||||
|
<OutlinedButton
|
||||||
|
type="link"
|
||||||
|
onClick={addWorkingHours}
|
||||||
|
>
|
||||||
|
{i18nText('addWorkingHours', locale)}
|
||||||
|
</OutlinedButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="b-modal__expert__button">
|
||||||
|
<Button
|
||||||
|
className="card-detail__apply"
|
||||||
|
onClick={onSave}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
{i18nText('save', locale)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
|
@ -69,7 +69,7 @@ export const EditExpertTagsModal: FC<EditExpertTagsModalProps> = ({
|
||||||
closeIcon={<CloseOutlined style={{ fontSize: 20, color: '#000' }}/>}
|
closeIcon={<CloseOutlined style={{ fontSize: 20, color: '#000' }}/>}
|
||||||
>
|
>
|
||||||
<div className="b-modal__expert__content">
|
<div className="b-modal__expert__content">
|
||||||
<div className="b-modal__expert__title">{i18nText('direction', locale)}</div>
|
<div className="b-modal__expert__title">{i18nText('selectTopic', locale)}</div>
|
||||||
<div className="b-modal__expert__inner">
|
<div className="b-modal__expert__inner">
|
||||||
{data?.themesGroups && data.themesGroups.filter(({ isActive }) => isActive).map(({ id, name }) => (
|
{data?.themesGroups && data.themesGroups.filter(({ isActive }) => isActive).map(({ id, name }) => (
|
||||||
<div key={`group_${id}`}>
|
<div key={`group_${id}`}>
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,11 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
export const AppConfig = () => {
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('AppVersion', process.env.version);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
|
@ -1,3 +1,4 @@
|
||||||
export * from './Header';
|
export * from './Header';
|
||||||
export * from './Footer';
|
export * from './Footer';
|
||||||
export * from './GeneralTopSection';
|
export * from './GeneralTopSection';
|
||||||
|
export * from './AppConfig';
|
||||||
|
|
|
@ -30,7 +30,9 @@ export const CustomMultiSelect = (props: any) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`b-multiselect-wrap ${isActiveLabel ? 'b-multiselect__active' : ''}`}>
|
<div className={`b-multiselect-wrap ${isActiveLabel ? 'b-multiselect__active' : ''}`}>
|
||||||
<div className="b-multiselect-label">{label}</div>
|
<div className="b-multiselect-label">
|
||||||
|
<span>{label}</span>
|
||||||
|
</div>
|
||||||
<Select
|
<Select
|
||||||
className="b-multiselect"
|
className="b-multiselect"
|
||||||
mode="multiple"
|
mode="multiple"
|
||||||
|
|
|
@ -4,7 +4,7 @@ import React, { useEffect, useState } from 'react';
|
||||||
import { Select } from 'antd';
|
import { Select } from 'antd';
|
||||||
|
|
||||||
export const CustomSelect = (props: any) => {
|
export const CustomSelect = (props: any) => {
|
||||||
const { label, value, ...other } = props;
|
const { label, value, style, ...other } = props;
|
||||||
const [isActiveLabel, setIsActiveLabel] = useState<boolean>(false);
|
const [isActiveLabel, setIsActiveLabel] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -16,8 +16,10 @@ export const CustomSelect = (props: any) => {
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`b-select-wrap ${isActiveLabel ? 'b-select__active' : ''}`}>
|
<div className={`b-select-wrap ${isActiveLabel ? 'b-select__active' : ''}`} style={style}>
|
||||||
<div className="b-select-label">{label}</div>
|
<div className="b-select-label">
|
||||||
|
<span>{label}</span>
|
||||||
|
</div>
|
||||||
<Select
|
<Select
|
||||||
className="b-select"
|
className="b-select"
|
||||||
value={value}
|
value={value}
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { TimePicker } from 'antd';
|
||||||
|
import { DownOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
export const CustomTimePicker = (props: any) => {
|
||||||
|
const { label, value, ...other } = props;
|
||||||
|
const [isActiveLabel, setIsActiveLabel] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (label) {
|
||||||
|
setIsActiveLabel(!!value);
|
||||||
|
} else {
|
||||||
|
setIsActiveLabel(false);
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const onOpenChange = (open: boolean) => {
|
||||||
|
if (open) {
|
||||||
|
if (!isActiveLabel) setIsActiveLabel(true)
|
||||||
|
} else {
|
||||||
|
setIsActiveLabel(!!value)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`b-timepicker-wrap ${isActiveLabel ? 'b-timepicker__active' : ''}`}>
|
||||||
|
<div className="b-timepicker-label">
|
||||||
|
<span>{label}</span>
|
||||||
|
</div>
|
||||||
|
<TimePicker
|
||||||
|
className="b-timepicker"
|
||||||
|
format="HH:mm"
|
||||||
|
minuteStep={15}
|
||||||
|
value={value}
|
||||||
|
showNow={false}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
needConfirm={false}
|
||||||
|
placeholder=""
|
||||||
|
variant="filled"
|
||||||
|
allowClear={false}
|
||||||
|
suffixIcon={<DownOutlined style={{ color: '#2c7873', fontSize: 12 }} />}
|
||||||
|
{...other}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -12,3 +12,9 @@ export const FilledYellowButton = (props: any) => (
|
||||||
{props.children}
|
{props.children}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const FilledSquareButton = (props: any) => (
|
||||||
|
<Button className="b-button__filled_square" {...props}>
|
||||||
|
{props.children}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||||
import { Button } from 'antd';
|
import { Button } from 'antd';
|
||||||
|
|
||||||
export const LinkButton = (props: any) => (
|
export const LinkButton = (props: any) => (
|
||||||
<Button className="b-button__link" {...props}>
|
<Button className={`b-button__link${props?.danger ? ' danger': ''}`} {...props}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
export const UTC_LIST = [
|
||||||
|
'-12:00',
|
||||||
|
'-11:00',
|
||||||
|
'-10:00',
|
||||||
|
'-09:30',
|
||||||
|
'-09:00',
|
||||||
|
'-08:00',
|
||||||
|
'-07:00',
|
||||||
|
'-06:00',
|
||||||
|
'-05:00',
|
||||||
|
'-04:00',
|
||||||
|
'-03:30',
|
||||||
|
'-03:00',
|
||||||
|
'-02:00',
|
||||||
|
'-01:00',
|
||||||
|
'+00:00',
|
||||||
|
'+01:00',
|
||||||
|
'+02:00',
|
||||||
|
'+03:00',
|
||||||
|
'+03:30',
|
||||||
|
'+04:00',
|
||||||
|
'+04:30',
|
||||||
|
'+05:00',
|
||||||
|
'+05:30',
|
||||||
|
'+06:00',
|
||||||
|
'+06:30',
|
||||||
|
'+07:00',
|
||||||
|
'+08:00',
|
||||||
|
'+09:00',
|
||||||
|
'+09:30',
|
||||||
|
'+10:00',
|
||||||
|
'+10:30',
|
||||||
|
'+11:00',
|
||||||
|
'+12:00',
|
||||||
|
'+13:00',
|
||||||
|
'+14:00'
|
||||||
|
];
|
|
@ -105,6 +105,7 @@ export default {
|
||||||
signUp: 'Jetzt anmelden',
|
signUp: 'Jetzt anmelden',
|
||||||
noData: 'Keine Daten',
|
noData: 'Keine Daten',
|
||||||
notFound: 'Nicht gefunden',
|
notFound: 'Nicht gefunden',
|
||||||
|
skillsInfo: 'Fähigkeiten-Infos',
|
||||||
trainings: 'Trainings',
|
trainings: 'Trainings',
|
||||||
seminars: 'Seminare',
|
seminars: 'Seminare',
|
||||||
courses: 'Kurse',
|
courses: 'Kurse',
|
||||||
|
@ -112,7 +113,39 @@ export default {
|
||||||
aboutCoach: 'Über Coach',
|
aboutCoach: 'Über Coach',
|
||||||
education: 'Bildung',
|
education: 'Bildung',
|
||||||
coaching: 'Coaching',
|
coaching: 'Coaching',
|
||||||
|
experiences: 'Praktische Erfahrung',
|
||||||
|
payInfo: 'Zahlungsdaten',
|
||||||
|
sessionDuration: 'Sitzungsdauer',
|
||||||
|
experienceHours: 'Gesamtstunden praktischer Erfahrung',
|
||||||
|
topics: 'Themen',
|
||||||
|
selectTopic: 'Thema auswählen',
|
||||||
|
title: 'Titel',
|
||||||
|
description: 'Beschreibung',
|
||||||
|
sessionCost: 'Sitzungskosten in Euro',
|
||||||
|
yourTimezone: 'Deine Zeitzone',
|
||||||
|
workTime: 'Arbeitszeit',
|
||||||
|
startAt: 'Beginn um',
|
||||||
|
finishAt: 'Ende um',
|
||||||
|
day: 'Tag',
|
||||||
|
addWorkingHours: 'Arbeitszeiten hinzufügen',
|
||||||
|
specialisation: 'Spezialisierung',
|
||||||
|
selectSpecialisation: 'Wählen Sie Ihre Spezialisierung, um fortzufahren',
|
||||||
|
fillWeeklySchedule: 'Trage Sachen in deinen Wochenplan ein',
|
||||||
|
beneficiaryName: 'Name des Empfängers',
|
||||||
|
bicOrSwift: 'BIC/Swift-Code',
|
||||||
|
association: 'Verband',
|
||||||
|
level: 'Stufe',
|
||||||
|
addDiploma: 'Zertifikat hinzufügen',
|
||||||
|
university: 'Institution',
|
||||||
|
sunday: 'So',
|
||||||
|
monday: 'Mo',
|
||||||
|
tuesday: 'Di',
|
||||||
|
wednesday: 'Mi',
|
||||||
|
thursday: 'Do',
|
||||||
|
friday: 'Fr',
|
||||||
|
saturday: 'Sa',
|
||||||
|
addNew: 'Neu hinzufügen',
|
||||||
|
mExperiences: 'Führungserfahrung',
|
||||||
errors: {
|
errors: {
|
||||||
invalidEmail: 'Die E-Mail-Adresse ist ungültig',
|
invalidEmail: 'Die E-Mail-Adresse ist ungültig',
|
||||||
emptyEmail: 'Bitte geben Sie Ihre E-Mail ein',
|
emptyEmail: 'Bitte geben Sie Ihre E-Mail ein',
|
||||||
|
|
|
@ -110,8 +110,42 @@ export default {
|
||||||
courses: 'Courses',
|
courses: 'Courses',
|
||||||
mba: 'MBA Information',
|
mba: 'MBA Information',
|
||||||
aboutCoach: 'About Coach',
|
aboutCoach: 'About Coach',
|
||||||
|
skillsInfo: 'Skills Info',
|
||||||
education: 'Education',
|
education: 'Education',
|
||||||
coaching: 'Coaching',
|
coaching: 'Coaching',
|
||||||
|
experiences: 'Practical experience',
|
||||||
|
payInfo: 'Payment Info',
|
||||||
|
sessionDuration: 'Session duration',
|
||||||
|
experienceHours: 'Total hours of practical experience',
|
||||||
|
topics: 'Topics',
|
||||||
|
selectTopic: 'Select Topic',
|
||||||
|
title: 'Title',
|
||||||
|
description: 'Description',
|
||||||
|
sessionCost: 'Session cost in euro',
|
||||||
|
yourTimezone: 'Your timezone',
|
||||||
|
workTime: 'Work time',
|
||||||
|
startAt: 'Start at',
|
||||||
|
finishAt: 'Finish at',
|
||||||
|
day: 'Day',
|
||||||
|
addWorkingHours: 'Add working hours',
|
||||||
|
specialisation: 'Specialisation',
|
||||||
|
selectSpecialisation: 'Select your specialisation to proceed',
|
||||||
|
fillWeeklySchedule: 'Fill up your weekly schedule',
|
||||||
|
beneficiaryName: 'Beneficiary Name',
|
||||||
|
bicOrSwift: 'BIC/Swift code',
|
||||||
|
association: 'Association',
|
||||||
|
level: 'Level',
|
||||||
|
addDiploma: 'Add Diploma',
|
||||||
|
university: 'Institution',
|
||||||
|
sunday: 'Su',
|
||||||
|
monday: 'Mo',
|
||||||
|
tuesday: 'Tu',
|
||||||
|
wednesday: 'We',
|
||||||
|
thursday: 'Th',
|
||||||
|
friday: 'Fr',
|
||||||
|
saturday: 'Sa',
|
||||||
|
addNew: 'Add New',
|
||||||
|
mExperiences: 'Managerial Experience',
|
||||||
errors: {
|
errors: {
|
||||||
invalidEmail: 'The email address is not valid',
|
invalidEmail: 'The email address is not valid',
|
||||||
emptyEmail: 'Please enter your E-mail',
|
emptyEmail: 'Please enter your E-mail',
|
||||||
|
|
|
@ -105,6 +105,7 @@ export default {
|
||||||
signUp: 'Regístrate ahora',
|
signUp: 'Regístrate ahora',
|
||||||
noData: 'Sin datos',
|
noData: 'Sin datos',
|
||||||
notFound: 'No encontrado',
|
notFound: 'No encontrado',
|
||||||
|
skillsInfo: 'Información',
|
||||||
trainings: 'Formación',
|
trainings: 'Formación',
|
||||||
seminars: 'Seminarios',
|
seminars: 'Seminarios',
|
||||||
courses: 'Cursos',
|
courses: 'Cursos',
|
||||||
|
@ -112,7 +113,39 @@ export default {
|
||||||
aboutCoach: 'Sobre el coach',
|
aboutCoach: 'Sobre el coach',
|
||||||
education: 'Educación',
|
education: 'Educación',
|
||||||
coaching: 'Coaching',
|
coaching: 'Coaching',
|
||||||
|
experiences: 'Experiencia práctica',
|
||||||
|
payInfo: 'Información de pago',
|
||||||
|
sessionDuration: 'Duración de la sesión',
|
||||||
|
experienceHours: 'Total de horas de experiencia práctica',
|
||||||
|
topics: 'Temas',
|
||||||
|
selectTopic: 'Seleccione el tema',
|
||||||
|
title: 'Título',
|
||||||
|
description: 'Descripción',
|
||||||
|
sessionCost: 'Coste de la sesión en euros',
|
||||||
|
yourTimezone: 'Tu zona horaria',
|
||||||
|
workTime: 'Tiempo de trabajo',
|
||||||
|
startAt: 'Empieza a las',
|
||||||
|
finishAt: 'Termina a las',
|
||||||
|
day: 'Día',
|
||||||
|
addWorkingHours: 'Añadir horas de trabajo',
|
||||||
|
specialisation: 'Especialización',
|
||||||
|
selectSpecialisation: 'Selecciona tu especialización para continuar',
|
||||||
|
fillWeeklySchedule: 'Rellena tu agenda semanal',
|
||||||
|
beneficiaryName: 'Nombre del beneficiario',
|
||||||
|
bicOrSwift: 'Código BIC/Swift',
|
||||||
|
association: 'Asociación',
|
||||||
|
level: 'Nivel',
|
||||||
|
addDiploma: 'Añadir diploma',
|
||||||
|
university: 'Institución',
|
||||||
|
sunday: 'D',
|
||||||
|
monday: 'L',
|
||||||
|
tuesday: 'M',
|
||||||
|
wednesday: 'X',
|
||||||
|
thursday: 'J',
|
||||||
|
friday: 'V',
|
||||||
|
saturday: 'S',
|
||||||
|
addNew: 'Añadir nuevo',
|
||||||
|
mExperiences: 'Experiencia de dirección',
|
||||||
errors: {
|
errors: {
|
||||||
invalidEmail: 'La dirección de correo electrónico no es válida',
|
invalidEmail: 'La dirección de correo electrónico no es válida',
|
||||||
emptyEmail: 'Introduce tu correo electrónico',
|
emptyEmail: 'Introduce tu correo electrónico',
|
||||||
|
|
|
@ -105,6 +105,7 @@ export default {
|
||||||
signUp: 'Inscrivez-vous maintenant',
|
signUp: 'Inscrivez-vous maintenant',
|
||||||
noData: 'Aucune donnée',
|
noData: 'Aucune donnée',
|
||||||
notFound: 'Non trouvé',
|
notFound: 'Non trouvé',
|
||||||
|
skillsInfo: 'Infos sur les compétences',
|
||||||
trainings: 'Formations',
|
trainings: 'Formations',
|
||||||
seminars: 'Séminaires',
|
seminars: 'Séminaires',
|
||||||
courses: 'Cours',
|
courses: 'Cours',
|
||||||
|
@ -112,7 +113,39 @@ export default {
|
||||||
aboutCoach: 'À propos du coach',
|
aboutCoach: 'À propos du coach',
|
||||||
education: 'Éducation',
|
education: 'Éducation',
|
||||||
coaching: 'Coaching',
|
coaching: 'Coaching',
|
||||||
|
experiences: 'Expérience pratique',
|
||||||
|
payInfo: 'Infos sur le paiement',
|
||||||
|
sessionDuration: 'Durée de la session',
|
||||||
|
experienceHours: 'Heures totales d\'expérience pratique',
|
||||||
|
topics: 'Sujets',
|
||||||
|
selectTopic: 'Sélectionnez un sujet',
|
||||||
|
title: 'Titre',
|
||||||
|
description: 'Description',
|
||||||
|
sessionCost: 'Coût de la session en euros',
|
||||||
|
yourTimezone: 'Votre fuseau horaire',
|
||||||
|
workTime: 'Heures de travail',
|
||||||
|
startAt: 'Commencer à',
|
||||||
|
finishAt: 'Finir à',
|
||||||
|
day: 'Jour',
|
||||||
|
addWorkingHours: 'Ajouter des heures de travail',
|
||||||
|
specialisation: 'Spécialisation',
|
||||||
|
selectSpecialisation: 'Sélectionnez votre spécialisation pour continuer',
|
||||||
|
fillWeeklySchedule: 'Remplissez votre emploi du temps hebdomadaire',
|
||||||
|
beneficiaryName: 'Nom du bénéficiaire',
|
||||||
|
bicOrSwift: 'Code BIC/Swift',
|
||||||
|
association: 'Association',
|
||||||
|
level: 'Niveau',
|
||||||
|
addDiploma: 'Ajouter un diplôme',
|
||||||
|
university: 'Institution',
|
||||||
|
sunday: 'Di',
|
||||||
|
monday: 'Lu',
|
||||||
|
tuesday: 'Ma',
|
||||||
|
wednesday: 'Me',
|
||||||
|
thursday: 'Je',
|
||||||
|
friday: 'Ve',
|
||||||
|
saturday: 'Sa',
|
||||||
|
addNew: 'Ajouter un nouveau',
|
||||||
|
mExperiences: 'Expérience en gestion',
|
||||||
errors: {
|
errors: {
|
||||||
invalidEmail: 'L\'adresse e-mail n\'est pas valide',
|
invalidEmail: 'L\'adresse e-mail n\'est pas valide',
|
||||||
emptyEmail: 'Veuillez saisir votre e-mail',
|
emptyEmail: 'Veuillez saisir votre e-mail',
|
||||||
|
|
|
@ -105,6 +105,7 @@ export default {
|
||||||
signUp: 'Iscriviti ora',
|
signUp: 'Iscriviti ora',
|
||||||
noData: 'Nessun dato',
|
noData: 'Nessun dato',
|
||||||
notFound: 'Non trovato',
|
notFound: 'Non trovato',
|
||||||
|
skillsInfo: 'Info su competenze',
|
||||||
trainings: 'Training',
|
trainings: 'Training',
|
||||||
seminars: 'Seminari',
|
seminars: 'Seminari',
|
||||||
courses: 'Corsi',
|
courses: 'Corsi',
|
||||||
|
@ -112,7 +113,39 @@ export default {
|
||||||
aboutCoach: 'Informazioni sul coach',
|
aboutCoach: 'Informazioni sul coach',
|
||||||
education: 'Istruzione',
|
education: 'Istruzione',
|
||||||
coaching: 'Coaching',
|
coaching: 'Coaching',
|
||||||
|
experiences: 'Esperienza pratica',
|
||||||
|
payInfo: 'Info pagamento',
|
||||||
|
sessionDuration: 'Durata della sessione',
|
||||||
|
experienceHours: 'Totale ore di esperienza pratica',
|
||||||
|
topics: 'Argomenti',
|
||||||
|
selectTopic: 'Seleziona l\'argomento',
|
||||||
|
title: 'Titolo',
|
||||||
|
description: 'Descrizione',
|
||||||
|
sessionCost: 'Costo della sessione in euro',
|
||||||
|
yourTimezone: 'Il tuo fuso orario',
|
||||||
|
workTime: 'Orario di lavoro',
|
||||||
|
startAt: 'Inizia alle',
|
||||||
|
finishAt: 'Termina alle',
|
||||||
|
day: 'Giorno',
|
||||||
|
addWorkingHours: 'Aggiungi ore lavorative',
|
||||||
|
specialisation: 'Specializzazione',
|
||||||
|
selectSpecialisation: 'Seleziona la tua specializzazione per continuare',
|
||||||
|
fillWeeklySchedule: 'Compila la tua agenda settimanale',
|
||||||
|
beneficiaryName: 'Nome del beneficiario',
|
||||||
|
bicOrSwift: 'BIC/codice Swift',
|
||||||
|
association: 'Associazione',
|
||||||
|
level: 'Livello',
|
||||||
|
addDiploma: 'Aggiungi diploma',
|
||||||
|
university: 'Istituto',
|
||||||
|
sunday: 'Do',
|
||||||
|
monday: 'Lu',
|
||||||
|
tuesday: 'Ma',
|
||||||
|
wednesday: 'Me',
|
||||||
|
thursday: 'Gi',
|
||||||
|
friday: 'Ve',
|
||||||
|
saturday: 'Sa',
|
||||||
|
addNew: 'Aggiungi nuovo',
|
||||||
|
mExperiences: 'Esperienza manageriale',
|
||||||
errors: {
|
errors: {
|
||||||
invalidEmail: 'L\'indirizzo e-mail non è valido',
|
invalidEmail: 'L\'indirizzo e-mail non è valido',
|
||||||
emptyEmail: 'Inserisci l\'e-mail',
|
emptyEmail: 'Inserisci l\'e-mail',
|
||||||
|
|
|
@ -98,21 +98,54 @@ export default {
|
||||||
expertBackground: 'Профессиональный опыт эксперта',
|
expertBackground: 'Профессиональный опыт эксперта',
|
||||||
profCertification: 'Профессиональная сертификация',
|
profCertification: 'Профессиональная сертификация',
|
||||||
practiceHours: 'Часов практики',
|
practiceHours: 'Часов практики',
|
||||||
supervisionCount: 'Часов супервизии в год',
|
supervisionCount: 'Супервизий в год',
|
||||||
outOf: 'из',
|
outOf: 'из',
|
||||||
schedule: 'Расписание',
|
schedule: 'Расписание',
|
||||||
successfulCase: 'Успешные случаи из практики',
|
successfulCase: 'Успешные случаи из практики',
|
||||||
signUp: 'Записаться сейчас',
|
signUp: 'Записаться сейчас',
|
||||||
noData: 'Нет данных',
|
noData: 'Нет данных',
|
||||||
notFound: 'Не найдено',
|
notFound: 'Не найдено',
|
||||||
|
skillsInfo: 'Навыки',
|
||||||
trainings: 'Тренинги',
|
trainings: 'Тренинги',
|
||||||
seminars: 'Семинары',
|
seminars: 'Семинары',
|
||||||
courses: 'Курсы',
|
courses: 'Курсы',
|
||||||
mba: 'Информация о MBA',
|
mba: 'Информация о MBA',
|
||||||
|
experiences: 'Практический опыт',
|
||||||
aboutCoach: 'О коуче',
|
aboutCoach: 'О коуче',
|
||||||
education: 'Образование',
|
education: 'Образование',
|
||||||
coaching: 'Коучинг',
|
coaching: 'Коучинг',
|
||||||
|
payInfo: 'Платежная информация',
|
||||||
|
sessionDuration: 'Продолжительность сессии',
|
||||||
|
experienceHours: 'Общее количество часов практического опыта',
|
||||||
|
topics: 'Темы',
|
||||||
|
selectTopic: 'Выберите тему',
|
||||||
|
title: 'Название',
|
||||||
|
description: 'Описание',
|
||||||
|
sessionCost: 'Стоимость сессии в евро',
|
||||||
|
yourTimezone: 'Ваш часовой пояс',
|
||||||
|
workTime: 'Рабочее время',
|
||||||
|
startAt: 'Начало в',
|
||||||
|
finishAt: 'Завершение в',
|
||||||
|
day: 'День',
|
||||||
|
addWorkingHours: 'Добавить рабочие часы',
|
||||||
|
specialisation: 'Специализация',
|
||||||
|
selectSpecialisation: 'Выберите свою специализацию для продолжения',
|
||||||
|
fillWeeklySchedule: 'Заполните свое недельное расписание',
|
||||||
|
beneficiaryName: 'Имя получателя',
|
||||||
|
bicOrSwift: 'BIC/Swift код',
|
||||||
|
association: 'Ассоциация',
|
||||||
|
level: 'Уровень',
|
||||||
|
addDiploma: 'Добавить диплом',
|
||||||
|
university: 'ВУЗ',
|
||||||
|
sunday: 'Вс',
|
||||||
|
monday: 'Пн',
|
||||||
|
tuesday: 'Вт',
|
||||||
|
wednesday: 'Ср',
|
||||||
|
thursday: 'Чт',
|
||||||
|
friday: 'Пт',
|
||||||
|
saturday: 'Сб',
|
||||||
|
addNew: 'Добавить',
|
||||||
|
mExperiences: 'Управленческий опыт',
|
||||||
errors: {
|
errors: {
|
||||||
invalidEmail: 'Адрес электронной почты недействителен',
|
invalidEmail: 'Адрес электронной почты недействителен',
|
||||||
emptyEmail: 'Пожалуйста, введите ваш E-mail',
|
emptyEmail: 'Пожалуйста, введите ваш E-mail',
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { Document as RichTextDocument } from '@contentful/rich-text-types'
|
||||||
|
import { documentToReactComponents } from '@contentful/rich-text-react-renderer'
|
||||||
|
|
||||||
|
type RichTextProps = {
|
||||||
|
document: RichTextDocument | null
|
||||||
|
}
|
||||||
|
|
||||||
|
function RichText({ document }: RichTextProps) {
|
||||||
|
if (!document) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{documentToReactComponents(document)}</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RichText
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { parseContentfulContentImage } from './contentImage'
|
||||||
|
import {Author, AuthorEntry} from "../../types/author";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export function parseContentfulAuthor(authorEntry?: AuthorEntry<any, any>): Author | null {
|
||||||
|
if (!authorEntry) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: authorEntry.fields.name || '',
|
||||||
|
expertId: authorEntry.fields.expertId || '',
|
||||||
|
avatar: parseContentfulContentImage(authorEntry.fields.avatar),
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,124 @@
|
||||||
|
import { Entry } from 'contentful'
|
||||||
|
import contentfulClient from './contentfulClient'
|
||||||
|
import { parseContentfulContentImage } from './contentImage'
|
||||||
|
import {BlogPost, BlogPostEntry, BlogPostSkeleton} from "../../types/blogPost";
|
||||||
|
import {parseContentfulAuthor} from "./authors";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import {WidgetMedia, WidgetMediaEntry} from "../../types/blogWidgets/widgetMedia";
|
||||||
|
import {WidgetParagraph} from "../../types/blogWidgets/widgetParagraph";
|
||||||
|
import entry from "next/dist/server/typescript/rules/entry";
|
||||||
|
import Util from "node:util";
|
||||||
|
import {DEFAULT_PAGE_SIZE} from "../../constants/common";
|
||||||
|
|
||||||
|
const pageSize = DEFAULT_PAGE_SIZE
|
||||||
|
type PostEntry = BlogPostEntry<undefined, string>//Entry<BlogPostSkeleton, undefined, string>
|
||||||
|
type widgetEnum = WidgetParagraph | WidgetMedia
|
||||||
|
export type Widget = {
|
||||||
|
widget: widgetEnum
|
||||||
|
type: string
|
||||||
|
}
|
||||||
|
type WidgetEntry = WidgetMediaEntry<undefined, string> | WidgetParagraph<undefined, string>
|
||||||
|
function parseWidgets(entries?: Array<WidgetEntry>): Array<Widget> | null {
|
||||||
|
if (!entries || !entries.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return entries.map((entry: WidgetEntry) => {
|
||||||
|
const wType = entry.sys.contentType.sys.id
|
||||||
|
let wObj = {} as widgetEnum
|
||||||
|
switch (wType){
|
||||||
|
case 'widgetParagraph':
|
||||||
|
wObj = {
|
||||||
|
subTitle: entry.fields.subTitle,
|
||||||
|
body: entry.fields.body
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'widgetMedia':
|
||||||
|
wObj = {
|
||||||
|
decription: entry.fields.decription || '',
|
||||||
|
file: parseContentfulContentImage(entry.fields.file)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: wType,
|
||||||
|
widget: wObj
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseContentfulBlogPost(entry?: PostEntry): BlogPost | null {
|
||||||
|
if (!entry) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: entry.fields.title || '',
|
||||||
|
slug: entry.fields.slug,
|
||||||
|
excerpt: entry.fields.excerpt || '',
|
||||||
|
listImage: parseContentfulContentImage(entry.fields.listImage),
|
||||||
|
author: parseContentfulAuthor(entry.fields.author),
|
||||||
|
createdAt: dayjs(entry.sys.createdAt).format('MMM DD, YYYY'),
|
||||||
|
category: entry.fields.category.fields.title,
|
||||||
|
categorySlug: entry.fields.category.fields.slug,
|
||||||
|
body: parseWidgets(entry.fields.body) || []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FetchBlogPostsOptions {
|
||||||
|
preview: boolean
|
||||||
|
local?: string
|
||||||
|
category?: string
|
||||||
|
page?: number
|
||||||
|
sticky?: boolean
|
||||||
|
}
|
||||||
|
export async function fetchBlogPosts({ preview, category, page, sticky }: FetchBlogPostsOptions): Promise<{
|
||||||
|
total: number;
|
||||||
|
data: BlogPost[]
|
||||||
|
}> {
|
||||||
|
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', 'fields.metaDescription'],
|
||||||
|
order: ['sys.createdAt'],
|
||||||
|
}
|
||||||
|
if (category){
|
||||||
|
query['fields.category.fields.slug'] = category
|
||||||
|
query['fields.category.sys.contentType.sys.id']='blogPostCategory'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if(page){
|
||||||
|
query['limit'] = pageSize
|
||||||
|
query['skip'] = pageSize * (page - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sticky){ // только три для главной
|
||||||
|
query['fields.sticky'] = 1
|
||||||
|
query['limit'] = 3
|
||||||
|
query['skip'] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const blogPostsResult = await contentful.getEntries<BlogPostSkeleton>(query)
|
||||||
|
|
||||||
|
const data = blogPostsResult.items.map((blogPostEntry) => parseContentfulBlogPost(blogPostEntry) as BlogPost)
|
||||||
|
return {
|
||||||
|
total: blogPostsResult.total,
|
||||||
|
data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FetchBlogPostOptions {
|
||||||
|
slug: string
|
||||||
|
preview: boolean
|
||||||
|
}
|
||||||
|
export async function fetchBlogPost({ slug, preview }: FetchBlogPostOptions): Promise<BlogPost | null> {
|
||||||
|
const contentful = contentfulClient({ preview })
|
||||||
|
|
||||||
|
const blogPostsResult = await contentful.getEntries<BlogPostSkeleton>({
|
||||||
|
content_type: 'blogPost',
|
||||||
|
'fields.slug': slug,
|
||||||
|
include: 2,
|
||||||
|
})
|
||||||
|
|
||||||
|
return parseContentfulBlogPost(blogPostsResult.items[0])
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { Entry } from 'contentful'
|
||||||
|
import contentfulClient from './contentfulClient'
|
||||||
|
import { parseContentfulContentImage } from './contentImage'
|
||||||
|
import {BlogPost, BlogPostEntry, BlogPostSkeleton} from "../../types/blogPost";
|
||||||
|
import {parseContentfulAuthor} from "./authors";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import {BlogPostCategory, BlogPostCategoryEntry, BlogPostCategorySkeleton} from "../../types/blogPostCategory";
|
||||||
|
|
||||||
|
type ListEntry = BlogPostCategoryEntry<undefined, string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export function parseContentfulBlogPostCategory(entry?: ListEntry): BlogPostCategory | null {
|
||||||
|
if (!entry) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
title: entry.fields.title || '',
|
||||||
|
slug: entry.fields.slug || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FetchListOptions {
|
||||||
|
preview: boolean
|
||||||
|
local?: string
|
||||||
|
}
|
||||||
|
export async function fetchBlogPostCategories({ preview }: FetchListOptions): Promise<BlogPostCategory[]> {
|
||||||
|
const contentful = contentfulClient({ preview })
|
||||||
|
|
||||||
|
const results = await contentful.getEntries<BlogPostCategorySkeleton>({
|
||||||
|
content_type: 'blogPostCategory',
|
||||||
|
order: ['fields.title'],
|
||||||
|
})
|
||||||
|
|
||||||
|
return results.items.map((entry) => parseContentfulBlogPostCategory(entry) as BlogPostCategory)
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { Asset, AssetLink } from 'contentful'
|
||||||
|
|
||||||
|
export interface ContentImage {
|
||||||
|
src: string
|
||||||
|
alt: string
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseContentfulContentImage(
|
||||||
|
asset?: Asset<undefined, string> | { sys: AssetLink }
|
||||||
|
): ContentImage | null {
|
||||||
|
if (!asset) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('fields' in asset)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
src: asset.fields.file?.url || '',
|
||||||
|
alt: asset.fields.description || '',
|
||||||
|
width: asset.fields.file?.details.image?.width || 0,
|
||||||
|
height: asset.fields.file?.details.image?.height || 0,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { createClient } from 'contentful'
|
||||||
|
|
||||||
|
const { NEXT_PUBLIC_CONTENTFUL_SPACE_ID, NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN, NEXT_PUBLIC_CONTENTFUL_PREVIEW_ACCESS_TOKEN } = process.env
|
||||||
|
|
||||||
|
// This is the standard Contentful client. It fetches
|
||||||
|
// content that has been published.
|
||||||
|
const client = createClient({
|
||||||
|
space: NEXT_PUBLIC_CONTENTFUL_SPACE_ID!,
|
||||||
|
accessToken: NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN!,
|
||||||
|
})
|
||||||
|
|
||||||
|
// This is a Contentful client that's been configured
|
||||||
|
// to fetch drafts and unpublished content.
|
||||||
|
const previewClient = createClient({
|
||||||
|
space: NEXT_PUBLIC_CONTENTFUL_SPACE_ID!,
|
||||||
|
accessToken: NEXT_PUBLIC_CONTENTFUL_PREVIEW_ACCESS_TOKEN!,
|
||||||
|
host: 'preview.contentful.com',
|
||||||
|
})
|
||||||
|
|
||||||
|
// This little helper will let us switch between the two
|
||||||
|
// clients easily:
|
||||||
|
export default function contentfulClient({ preview = false }) {
|
||||||
|
if (preview) {
|
||||||
|
return previewClient
|
||||||
|
}
|
||||||
|
|
||||||
|
return client
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -109,10 +109,11 @@ textarea {
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-avatar {
|
.user-avatar {
|
||||||
display: flex;
|
display: grid;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
|
grid-template-columns: 100px auto;
|
||||||
|
|
||||||
&__edit {
|
&__edit {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
@ -51,7 +51,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&__inner {
|
&__inner {
|
||||||
height: 60vh;
|
height: auto;
|
||||||
|
max-height: 60vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
& > div {
|
& > div {
|
||||||
|
@ -86,3 +87,13 @@
|
||||||
.ant-modal-mask {
|
.ant-modal-mask {
|
||||||
background-color: rgba(0, 59, 70, 0.4) !important;
|
background-color: rgba(0, 59, 70, 0.4) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-upload-list-item-name {
|
||||||
|
max-width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-upload-list-item {
|
||||||
|
&:hover {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1188,7 +1188,6 @@
|
||||||
height: 86px;
|
height: 86px;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
border: 2px solid #FFF;
|
border: 2px solid #FFF;
|
||||||
background: lightgray 50%;
|
|
||||||
box-shadow: 0 8px 16px 0 rgba(102, 165, 173, 0.32);
|
box-shadow: 0 8px 16px 0 rgba(102, 165, 173, 0.32);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
@ -1210,6 +1209,7 @@
|
||||||
@include rem(18);
|
@include rem(18);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 150%;
|
line-height: 150%;
|
||||||
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1233,6 +1233,61 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.title-h3 {
|
||||||
|
color: #003B46;
|
||||||
|
@include rem(16);
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-list {
|
||||||
|
margin-top: 8px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__practice {
|
||||||
|
color: #2C7873;
|
||||||
|
@include rem(16);
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__lang {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
color: #003B46;
|
||||||
|
@include rem(16);
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item {
|
||||||
|
color: #003B46;
|
||||||
|
@include rem(16);
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
.title-h2 {
|
.title-h2 {
|
||||||
color: #003B46;
|
color: #003B46;
|
||||||
@include rem(18);
|
@include rem(18);
|
||||||
|
@ -1243,7 +1298,7 @@
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__desc {
|
&__desc, &__desc > div {
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
background: #EFFCFF;
|
background: #EFFCFF;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
@ -1340,6 +1395,7 @@
|
||||||
background-position: 99% 50%;
|
background-position: 99% 50%;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
&__title {
|
&__title {
|
||||||
color: #FFBD00;
|
color: #FFBD00;
|
||||||
|
@ -1366,6 +1422,16 @@
|
||||||
background: #C4DFE6;
|
background: #C4DFE6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-item {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
grid-template-columns: 80px 1fr 1fr 32px;
|
||||||
|
|
||||||
|
&__single {
|
||||||
|
grid-template-columns: 80px 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__inner{
|
&__inner{
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -1382,6 +1448,7 @@
|
||||||
&__wrap {
|
&__wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
.btn-cancel,
|
.btn-cancel,
|
||||||
.btn-edit {
|
.btn-edit {
|
||||||
|
@ -1443,8 +1510,17 @@
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pay-data-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,6 +15,8 @@
|
||||||
@import "_message.scss";
|
@import "_message.scss";
|
||||||
@import "_auth-modal.scss";
|
@import "_auth-modal.scss";
|
||||||
@import "_modal.scss";
|
@import "_modal.scss";
|
||||||
|
@import "_edu.scss";
|
||||||
|
@import "_schedule.scss";
|
||||||
|
|
||||||
@import "./view/style.scss";
|
@import "./view/style.scss";
|
||||||
@import "./sessions/style.scss";
|
@import "./sessions/style.scss";
|
||||||
|
|
|
@ -17,6 +17,19 @@
|
||||||
padding: 4px 24px !important;
|
padding: 4px 24px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&_square {
|
||||||
|
width: 42px !important;
|
||||||
|
height: 42px !important;
|
||||||
|
background: #66A5AD !important;
|
||||||
|
font-size: 15px !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
box-shadow: 0px 2px 4px 0px rgba(102, 165, 173, 0.32) !important;
|
||||||
|
position: absolute !important;
|
||||||
|
right: -8px !important;
|
||||||
|
bottom: -8px !important;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
&.danger {
|
&.danger {
|
||||||
background: #D93E5C !important;
|
background: #D93E5C !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
|
@ -28,6 +41,10 @@
|
||||||
font-size: 15px !important;
|
font-size: 15px !important;
|
||||||
height: auto !important;
|
height: auto !important;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
|
|
||||||
|
&.danger {
|
||||||
|
color: #D93E5C !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__outlined {
|
&__outlined {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -8,6 +8,14 @@
|
||||||
|
|
||||||
input {
|
input {
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
|
border-color: transparent !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-input-group-addon {
|
||||||
|
background-color: transparent !important;
|
||||||
|
border-color: transparent !important;
|
||||||
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus, &:hover, &:focus-within {
|
&:focus, &:hover, &:focus-within {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -53,8 +53,15 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 16px;
|
left: 16px;
|
||||||
top: 15px;
|
top: 15px;
|
||||||
|
right: 22px;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
transition: all .1s ease;
|
transition: all .1s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
span {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,7 +117,14 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 16px;
|
left: 16px;
|
||||||
top: 15px;
|
top: 15px;
|
||||||
|
right: 22px;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
transition: all .1s ease;
|
transition: all .1s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
span {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
.b-timepicker {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 54px !important;
|
||||||
|
|
||||||
|
&.ant-picker-filled {
|
||||||
|
background: transparent !important;
|
||||||
|
z-index: 1;
|
||||||
|
padding-top: 22px !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #2c7873 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-picker-input {
|
||||||
|
input {
|
||||||
|
font-size: 15px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-picker-suffix {
|
||||||
|
margin-top: -20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-wrap {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
background-color: #F8F8F7;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
&.b-timepicker__active .b-timepicker-label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 300;
|
||||||
|
line-height: 14px;
|
||||||
|
top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 24px;
|
||||||
|
color: #000;
|
||||||
|
opacity: .3;
|
||||||
|
position: absolute;
|
||||||
|
left: 16px;
|
||||||
|
top: 15px;
|
||||||
|
right: 22px;
|
||||||
|
z-index: 0;
|
||||||
|
transition: all .1s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
span {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,3 +6,6 @@
|
||||||
@import "_spin.scss";
|
@import "_spin.scss";
|
||||||
@import "_switch.scss";
|
@import "_switch.scss";
|
||||||
@import "_buttons.scss";
|
@import "_buttons.scss";
|
||||||
|
@import "_practice.scss";
|
||||||
|
@import "_collapse.scss";
|
||||||
|
@import "_timepicker.scss";
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
import type { ChainModifiers, Entry, EntryFieldTypes, EntrySkeletonType, LocaleCode } from 'contentful'
|
||||||
|
import {BlogPostFields} from "./blogPost";
|
||||||
|
import {ContentImage} from "../lib/contentful/contentImage";
|
||||||
|
|
||||||
|
export interface AuthorFields {
|
||||||
|
name: EntryFieldTypes.Symbol
|
||||||
|
avatar: EntryFieldTypes.AssetLink
|
||||||
|
expertId: EntryFieldTypes.AssetLink
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Author {
|
||||||
|
name: string
|
||||||
|
expertId: string
|
||||||
|
avatar: ContentImage | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AuthorSkeleton = EntrySkeletonType<AuthorFields, 'author'>
|
||||||
|
export type AuthorEntry<Modifiers extends ChainModifiers, Locales extends LocaleCode> = Entry<
|
||||||
|
AuthorSkeleton,
|
||||||
|
Modifiers,
|
||||||
|
Locales
|
||||||
|
>
|
|
@ -0,0 +1,42 @@
|
||||||
|
import type { ChainModifiers, Entry, EntryFieldTypes, EntrySkeletonType, LocaleCode } from 'contentful'
|
||||||
|
import {Author, AuthorSkeleton} from "./author";
|
||||||
|
import {ContentImage} from "../lib/contentful/contentImage";
|
||||||
|
import {BlogPostCategorySkeleton} from "./blogPostCategory";
|
||||||
|
import {WidgetMedia, WidgetMediaSkeleton} from "./blogWidgets/widgetMedia";
|
||||||
|
import {WidgetParagraph, WidgetParagraphSkeleton} from "./blogWidgets/widgetParagraph";
|
||||||
|
|
||||||
|
export interface BlogPostFields {
|
||||||
|
title?: EntryFieldTypes.Symbol
|
||||||
|
slug: EntryFieldTypes.Symbol
|
||||||
|
excerpt: EntryFieldTypes.Symbol
|
||||||
|
metaDescription: EntryFieldTypes.Symbol
|
||||||
|
listImage?: EntryFieldTypes.AssetLink
|
||||||
|
author?: AuthorSkeleton
|
||||||
|
category: BlogPostCategorySkeleton
|
||||||
|
createdAt?: EntryFieldTypes.Date
|
||||||
|
body?: Array<WidgetMediaSkeleton | WidgetParagraphSkeleton>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlogPostFields extends BlogPostFields{
|
||||||
|
body: Array<WidgetMediaSkeleton | WidgetParagraphSkeleton>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlogPost {
|
||||||
|
title: string
|
||||||
|
slug: string
|
||||||
|
excerpt: string
|
||||||
|
listImage: ContentImage | null
|
||||||
|
author: Author | null
|
||||||
|
category: string
|
||||||
|
categorySlug: string
|
||||||
|
createdAt: string
|
||||||
|
metaDescription: string
|
||||||
|
body: Array<WidgetMedia | WidgetParagraph>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BlogPostSkeleton = EntrySkeletonType<BlogPostFields, 'blogPost'>
|
||||||
|
export type BlogPostEntry<Modifiers extends ChainModifiers, Locales extends LocaleCode> = Entry<
|
||||||
|
BlogPostSkeleton,
|
||||||
|
Modifiers,
|
||||||
|
Locales
|
||||||
|
>
|
|
@ -0,0 +1,20 @@
|
||||||
|
import type { ChainModifiers, Entry, EntryFieldTypes, EntrySkeletonType, LocaleCode } from 'contentful'
|
||||||
|
import {BlogPostFields} from "./blogPost";
|
||||||
|
import {ContentImage} from "../lib/contentful/contentImage";
|
||||||
|
|
||||||
|
export interface BlogPostCategoryFields {
|
||||||
|
title: EntryFieldTypes.Symbol
|
||||||
|
slug: EntryFieldTypes.Symbol
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlogPostCategory {
|
||||||
|
title: string
|
||||||
|
slug: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BlogPostCategorySkeleton = EntrySkeletonType<BlogPostCategoryFields, 'blogPostCategory'>
|
||||||
|
export type BlogPostCategoryEntry<Modifiers extends ChainModifiers, Locales extends LocaleCode> = Entry<
|
||||||
|
BlogPostCategorySkeleton,
|
||||||
|
Modifiers,
|
||||||
|
Locales
|
||||||
|
>
|
|
@ -0,0 +1,20 @@
|
||||||
|
import type { ChainModifiers, Entry, EntryFieldTypes, EntrySkeletonType, LocaleCode } from 'contentful'
|
||||||
|
import {BlogPostFields} from "./blogPost";
|
||||||
|
import {ContentImage} from "../lib/contentful/contentImage";
|
||||||
|
|
||||||
|
export interface WidgetMediaFields {
|
||||||
|
decription?: EntryFieldTypes.Symbol
|
||||||
|
file?: EntryFieldTypes.AssetLink
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WidgetMedia {
|
||||||
|
file: ContentImage | null
|
||||||
|
decription: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WidgetMediaSkeleton = EntrySkeletonType<WidgetMediaFields, 'WidgetMedia'>
|
||||||
|
export type WidgetMediaEntry<Modifiers extends ChainModifiers, Locales extends LocaleCode> = Entry<
|
||||||
|
WidgetMediaSkeleton,
|
||||||
|
Modifiers,
|
||||||
|
Locales
|
||||||
|
>
|
|
@ -0,0 +1,20 @@
|
||||||
|
import type { ChainModifiers, Entry, EntryFieldTypes, EntrySkeletonType, LocaleCode } from 'contentful'
|
||||||
|
import {BlogPostFields} from "./blogPost";
|
||||||
|
import {ContentImage} from "../lib/contentful/contentImage";
|
||||||
|
import { Document as RichTextDocument } from '@contentful/rich-text-types'
|
||||||
|
export interface WidgetParagraphFields {
|
||||||
|
subTitle?: EntryFieldTypes.Symbol
|
||||||
|
body?: EntryFieldTypes.RichText
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WidgetParagraph {
|
||||||
|
subTitle: string
|
||||||
|
body: RichTextDocument | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WidgetParagraphSkeleton = EntrySkeletonType<WidgetParagraphFields, 'WidgetParagraph'>
|
||||||
|
export type WidgetParagraphEntry<Modifiers extends ChainModifiers, Locales extends LocaleCode> = Entry<
|
||||||
|
WidgetParagraphSkeleton,
|
||||||
|
Modifiers,
|
||||||
|
Locales
|
||||||
|
>
|
|
@ -1,22 +1,23 @@
|
||||||
import { ExpertDocument } from './file';
|
import { ExpertDocument } from './file';
|
||||||
|
|
||||||
export type Details = {
|
export type Details = {
|
||||||
id: number;
|
id?: number;
|
||||||
userId?: number;
|
userId?: number;
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
document?: ExpertDocument;
|
document?: ExpertDocument | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Certificate = {
|
export type Certificate = {
|
||||||
id: number;
|
id?: number;
|
||||||
userId?: number;
|
userId?: number;
|
||||||
associationLevelId?: number;
|
associationLevelId?: number;
|
||||||
document?: ExpertDocument;
|
associationId?: number;
|
||||||
|
document?: ExpertDocument | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Experience = {
|
export type Experience = {
|
||||||
id: number,
|
id?: number,
|
||||||
userId?: number,
|
userId?: number,
|
||||||
title?: string,
|
title?: string,
|
||||||
description?: string
|
description?: string
|
||||||
|
@ -24,10 +25,10 @@ export type Experience = {
|
||||||
|
|
||||||
export type Association = {
|
export type Association = {
|
||||||
id: number;
|
id: number;
|
||||||
name?: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AssociationLevel = Association & { associationId?: number };
|
export type AssociationLevel = Association & { associationId: number };
|
||||||
|
|
||||||
export type EducationData = {
|
export type EducationData = {
|
||||||
certificates?: Certificate[],
|
certificates?: Certificate[],
|
||||||
|
|
|
@ -6,7 +6,7 @@ export type Supervision = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PracticeCase = {
|
export type PracticeCase = {
|
||||||
id: number,
|
id?: number,
|
||||||
userId?: number,
|
userId?: number,
|
||||||
description?: string,
|
description?: string,
|
||||||
themesGroupIds?: number[]
|
themesGroupIds?: number[]
|
||||||
|
@ -18,12 +18,14 @@ export type PracticeData = {
|
||||||
sessionDuration?: number,
|
sessionDuration?: number,
|
||||||
sessionCost?: number,
|
sessionCost?: number,
|
||||||
practiceCases?: PracticeCase[]
|
practiceCases?: PracticeCase[]
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface PracticeDTO {
|
export type PracticePersonData = PracticeData & {
|
||||||
person4Data: PracticeData & {
|
|
||||||
themesGroups?: ExpertsThemesGroups[],
|
themesGroups?: ExpertsThemesGroups[],
|
||||||
supervisionPerYears?: Supervision[],
|
supervisionPerYears?: Supervision[],
|
||||||
sessionCosts?: number[]
|
sessionCosts?: number[]
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export interface PracticeDTO {
|
||||||
|
person4Data: PracticePersonData
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { UploadFile } from 'antd';
|
import { EducationDTO } from './education';
|
||||||
import {EducationDTO} from "./education";
|
import { ExpertsTags } from './tags';
|
||||||
import {ExpertsTags} from "./tags";
|
import { PracticeDTO } from './practice';
|
||||||
import {PracticeDTO} from "./practice";
|
import { ScheduleDTO } from './schedule';
|
||||||
import {ScheduleDTO} from "./schedule";
|
|
||||||
|
|
||||||
export type ProfileData = {
|
export type ProfileData = {
|
||||||
username?: string;
|
username?: string;
|
||||||
|
@ -15,7 +14,8 @@ export type ProfileData = {
|
||||||
hasExternalLogin?: boolean;
|
hasExternalLogin?: boolean;
|
||||||
isTestMode?: boolean;
|
isTestMode?: boolean;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
languagesLinks?: { language: { id: number, code: string, nativeSpelling: string }, languageId: number }[]
|
languagesLinks?: { language: { id: number, code: string, nativeSpelling: string }, languageId: number }[];
|
||||||
|
allLanguages?: { id: number, code: string, nativeSpelling: string }[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Profile = ProfileData & { id: number };
|
export type Profile = ProfileData & { id: number };
|
||||||
|
@ -27,7 +27,7 @@ export type ProfileRequest = {
|
||||||
languagesLinks?: { languageId: number }[];
|
languagesLinks?: { languageId: number }[];
|
||||||
username?: string;
|
username?: string;
|
||||||
surname?: string;
|
surname?: string;
|
||||||
faceImage?: UploadFile;
|
faceImage?: any;
|
||||||
isFaceImageKeepExisting?: boolean;
|
isFaceImageKeepExisting?: boolean;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,8 +3,15 @@ export type WorkingTime = {
|
||||||
startTimeUtc?: number,
|
startTimeUtc?: number,
|
||||||
endDayOfWeekUtc?: string,
|
endDayOfWeekUtc?: string,
|
||||||
endTimeUtc?: number
|
endTimeUtc?: number
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface ScheduleDTO {
|
export interface ScheduleDTO {
|
||||||
workingTimes?: WorkingTime[]
|
workingTimes?: WorkingTime[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MapWorkingTime = {
|
||||||
|
startDay?: string;
|
||||||
|
startTimeMin?: number;
|
||||||
|
endDay?: string;
|
||||||
|
endTimeMin?: number;
|
||||||
|
};
|
||||||
|
|
|
@ -28,3 +28,19 @@ export const validateImage = (file: UploadFile, showMessage?: boolean): boolean
|
||||||
|
|
||||||
return isImage && isLt5M;
|
return isImage && isLt5M;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const validateDoc = (file: UploadFile): boolean => {
|
||||||
|
const isDoc = file.type === 'image/jpg' || file.type === 'image/jpeg'
|
||||||
|
|| file.type === 'image/png' || file.type === 'image/gif' || file.type === 'application/pdf';
|
||||||
|
if (!isDoc) {
|
||||||
|
message.error('You can only upload JPG/PNG/PDF file');
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLt5M = file.size / 1024 / 1024 <= 5;
|
||||||
|
if (!isLt5M) {
|
||||||
|
message.error('Image must smaller than 5MB');
|
||||||
|
}
|
||||||
|
|
||||||
|
return isDoc && isLt5M;
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,222 @@
|
||||||
|
import { MapWorkingTime, WorkingTime } from '../types/schedule';
|
||||||
|
|
||||||
|
export const WEEK_DAY = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
|
||||||
|
const MAX_DAY_TIME = 24 * 60; // min
|
||||||
|
|
||||||
|
const addWeekDay = (weekDay?: string): string => {
|
||||||
|
const ind = weekDay ? WEEK_DAY.indexOf(weekDay) + 1 : 0;
|
||||||
|
return WEEK_DAY[ind >= WEEK_DAY.length ? 0 : ind];
|
||||||
|
}
|
||||||
|
|
||||||
|
const subWeekDay = (weekDay?: string): string => {
|
||||||
|
const ind = weekDay ? WEEK_DAY.indexOf(weekDay) - 1 : 0;
|
||||||
|
return WEEK_DAY[ind < 0 ? WEEK_DAY.length - 1 : ind];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCurrentTime = (data: WorkingTime, timeZone: string): MapWorkingTime => {
|
||||||
|
let startDay = data.startDayOfWeekUtc;
|
||||||
|
let endDay = data.endDayOfWeekUtc;
|
||||||
|
const startUtc = data.startTimeUtc / (1000 * 1000 * 60);
|
||||||
|
const endUtc = data.endTimeUtc / (1000 * 1000 * 60);
|
||||||
|
const matches = timeZone.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 = 0;
|
||||||
|
// 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 = 0;
|
||||||
|
// 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 = 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 = 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,
|
||||||
|
startTimeMin: startMin,
|
||||||
|
endDay: endDay,
|
||||||
|
endTimeMin: endMin
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTimeString = (min: number) => {
|
||||||
|
const timeH = `${(min - min % 60)/60}`;
|
||||||
|
|
||||||
|
return `${timeH.length === 1 ? `0${timeH}` : timeH}:${min % 60 || '00'}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const formattedSchedule = (workingTimes: WorkingTime[], timeZone: string): MapWorkingTime[] => (
|
||||||
|
workingTimes.map((time) => getCurrentTime(time, timeZone)) || []
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getNewTime = (time: string): number => {
|
||||||
|
const timeArr = time.split(':');
|
||||||
|
|
||||||
|
return Number(timeArr[0]) * 60 + Number(timeArr[1]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTimeZoneOffset = (oldTime: string, newTime: string): { sign: string, offset: number } => {
|
||||||
|
// старая таймзона
|
||||||
|
const matches1 = oldTime.match(/(\+|-)([0-9]{2}):([0-9]{2})/) || [];
|
||||||
|
const sign1 = matches1[1];
|
||||||
|
const min1 = Number(matches1[2]) * 60 + Number(matches1[3]);
|
||||||
|
// новая таймзона
|
||||||
|
const matches2 = newTime.match(/(\+|-)([0-9]{2}):([0-9]{2})/) || [];
|
||||||
|
const sign2 = matches2[1];
|
||||||
|
const min2 = Number(matches2[2]) * 60 + Number(matches2[3]);
|
||||||
|
|
||||||
|
if (sign1 === '+' && sign2 === '+') {
|
||||||
|
if (min1 < min2) {
|
||||||
|
return {
|
||||||
|
sign: '+',
|
||||||
|
offset: min2 - min1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
sign: '-',
|
||||||
|
offset: min1 - min2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sign1 === '-' && sign2 === '-') {
|
||||||
|
if (min1 < min2) {
|
||||||
|
return {
|
||||||
|
sign: '-',
|
||||||
|
offset: min2 - min1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
sign: '+',
|
||||||
|
offset: min1 - min2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sign1 === '+' && sign2 === '-') {
|
||||||
|
return {
|
||||||
|
sign: '-',
|
||||||
|
offset: min1 + min2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sign1 === '-' && sign2 === '+') {
|
||||||
|
return {
|
||||||
|
sign: '+',
|
||||||
|
offset: min1 + min2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const formattedTimeByOffset = (workTime: MapWorkingTime, objOffset: { sign: string, offset: number }): MapWorkingTime => {
|
||||||
|
if (objOffset.sign === '+') {
|
||||||
|
const resStartMin = workTime.startTimeMin + objOffset.offset;
|
||||||
|
const resEndMin = workTime.endTimeMin ? workTime.endTimeMin + objOffset.offset : workTime.endTimeMin;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...workTime,
|
||||||
|
startTimeMin: resStartMin >= MAX_DAY_TIME ? 0 : resStartMin,
|
||||||
|
endTimeMin: resEndMin >= MAX_DAY_TIME ? 0 : resEndMin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (objOffset.sign === '-') {
|
||||||
|
const resStartMin = workTime.startTimeMin - objOffset.offset;
|
||||||
|
const resEndMin = workTime.endTimeMin ? workTime.endTimeMin - objOffset.offset : workTime.endTimeMin;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...workTime,
|
||||||
|
startTimeMin: resStartMin < 0 ? 0 : resStartMin,
|
||||||
|
endTimeMin: resEndMin < 0 ? 0 : resEndMin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return workTime;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getResultTime = (data: MapWorkingTime, timeZone: string): WorkingTime => {
|
||||||
|
let startDayOfWeekUtc = data.startDay;
|
||||||
|
let endDayOfWeekUtc = data.startDay;
|
||||||
|
const matches = timeZone.match(/(\+|-)([0-9]{2}):([0-9]{2})/) || [];
|
||||||
|
const sign = matches[1];
|
||||||
|
const offset = (Number(matches[2]) * 60) + Number(matches[3]);
|
||||||
|
let startTime = data.startTimeMin;
|
||||||
|
let endTime = data.endTimeMin === 0 ? MAX_DAY_TIME : data.endTimeMin;
|
||||||
|
|
||||||
|
if (sign === '+') {
|
||||||
|
startTime = startTime - offset;
|
||||||
|
endTime = endTime - offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sign === '-') {
|
||||||
|
startTime = startTime + offset;
|
||||||
|
endTime = endTime + offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startTime >= MAX_DAY_TIME) {
|
||||||
|
startTime = startTime - MAX_DAY_TIME;
|
||||||
|
startDayOfWeekUtc = addWeekDay(startDayOfWeekUtc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endTime >= MAX_DAY_TIME) {
|
||||||
|
endTime = endTime - MAX_DAY_TIME;
|
||||||
|
endDayOfWeekUtc = addWeekDay(endDayOfWeekUtc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startTime < 0) {
|
||||||
|
startTime = MAX_DAY_TIME - Math.abs(startTime || 0);
|
||||||
|
startDayOfWeekUtc = subWeekDay(startDayOfWeekUtc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endTime < 0) {
|
||||||
|
endTime = MAX_DAY_TIME - Math.abs(endTime || 0);
|
||||||
|
endDayOfWeekUtc = subWeekDay(endDayOfWeekUtc);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
startDayOfWeekUtc,
|
||||||
|
startTimeUtc: startTime * 1000 * 1000 * 60,
|
||||||
|
endDayOfWeekUtc,
|
||||||
|
endTimeUtc: endTime * 1000 * 1000 * 60
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const formattedWorkList = (workingList: MapWorkingTime[], timeZone: string): WorkingTime[] => (
|
||||||
|
workingList
|
||||||
|
.filter(({ startTimeMin, endTimeMin }) => !(!startTimeMin && !endTimeMin))
|
||||||
|
.map((time) => getResultTime(time, timeZone)) || []
|
||||||
|
);
|
Loading…
Reference in New Issue