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_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
|
||||
const withNextIntl = require('next-intl/plugin')();
|
||||
const path = require('path');
|
||||
const json = require('./package.json');
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
|
@ -14,6 +15,9 @@ const nextConfig = {
|
|||
sassOptions: {
|
||||
includePaths: [path.join(__dirname, 'styles')],
|
||||
},
|
||||
env: {
|
||||
version: json.version
|
||||
},
|
||||
typescript: {
|
||||
// !! WARN !!
|
||||
// Dangerously allow production builds to successfully complete even if
|
||||
|
@ -30,8 +34,7 @@ const nextConfig = {
|
|||
},
|
||||
// output: 'standalone',
|
||||
poweredByHeader: false,
|
||||
productionBrowserSourceMaps: true,
|
||||
trailingSlash: true
|
||||
productionBrowserSourceMaps: true
|
||||
};
|
||||
|
||||
module.exports = withNextIntl(nextConfig);
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "bbuddy-ui",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.4",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 4200",
|
||||
|
@ -12,11 +12,13 @@
|
|||
"@ant-design/cssinjs": "^1.18.1",
|
||||
"@ant-design/icons": "^5.2.6",
|
||||
"@ant-design/nextjs-registry": "^1.0.0",
|
||||
"@contentful/rich-text-react-renderer": "^15.22.9",
|
||||
"agora-rtc-react": "^2.1.0",
|
||||
"agora-rtc-sdk-ng": "^4.20.2",
|
||||
"antd": "^5.12.1",
|
||||
"antd-img-crop": "^4.21.0",
|
||||
"axios": "^1.6.5",
|
||||
"contentful": "^10.13.3",
|
||||
"dayjs": "^1.11.10",
|
||||
"lodash": "^4.17.21",
|
||||
"next": "14.0.3",
|
||||
|
|
|
@ -1,71 +1,40 @@
|
|||
import React from 'react';
|
||||
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 {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);
|
||||
const t = useTranslations('Main');
|
||||
const t = await getTranslations('Main');
|
||||
const {data, total} = await fetchBlogPosts({preview: false, sticky: true})
|
||||
|
||||
return (
|
||||
<div className="main-articles">
|
||||
<div className="b-inner">
|
||||
<h2 className="title-h2">{t('news')}</h2>
|
||||
<div className="row">
|
||||
<div className="col-lg-4 col-md-6 col-sm-6">
|
||||
<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.
|
||||
{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__image">
|
||||
<img className="" src={item.listImage?.src} alt={item.listImage?.alt}/>
|
||||
</div>
|
||||
<div className="b-article__inner">
|
||||
<div className="b-article__title">{item.title}</div>
|
||||
<div className="b-article__text">
|
||||
{item.excerpt}
|
||||
</div>
|
||||
<Link href={`/${locale}/blog/${item.slug}`} className="b-article__link">
|
||||
{i18nText('readMore', locale)}
|
||||
<img className="" src="/images/chevron-forward.svg" alt=""/>
|
||||
</Link>
|
||||
</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 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>
|
||||
|
|
|
@ -6,7 +6,15 @@ import { message } from 'antd';
|
|||
import { ExpertData } from '../../../../../types/profile';
|
||||
import { AUTH_TOKEN_KEY } from '../../../../../constants/common';
|
||||
import { useLocalStorage } from '../../../../../hooks/useLocalStorage';
|
||||
import { getEducation, getPersonalData, getTags, getPractice, getSchedule, getPayData } from '../../../../../actions/profile';
|
||||
import {
|
||||
getEducation,
|
||||
getPersonalData,
|
||||
getTags,
|
||||
getPractice,
|
||||
getSchedule,
|
||||
getPayData,
|
||||
getUserData
|
||||
} from '../../../../../actions/profile';
|
||||
import { ExpertProfile } from '../../../../../components/ExpertProfile';
|
||||
import { Loader } from '../../../../../components/view/Loader';
|
||||
|
||||
|
@ -15,11 +23,13 @@ export default function ExpertProfilePage({ params: { locale } }: { params: { lo
|
|||
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [data, setData] = useState<ExpertData | undefined>();
|
||||
const [isFull, setIsFull] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (jwt) {
|
||||
setLoading(true);
|
||||
Promise.all([
|
||||
getUserData(locale, jwt),
|
||||
getPersonalData(locale, jwt),
|
||||
getEducation(locale, jwt),
|
||||
getTags(locale, jwt),
|
||||
|
@ -27,13 +37,8 @@ export default function ExpertProfilePage({ params: { locale } }: { params: { lo
|
|||
getSchedule(locale, jwt),
|
||||
getPayData(locale, jwt)
|
||||
])
|
||||
.then(([person, education, tags, practice, schedule, payData]) => {
|
||||
console.log('person', person);
|
||||
console.log('education', education);
|
||||
console.log('tags', tags);
|
||||
console.log('practice', practice);
|
||||
console.log('schedule', schedule);
|
||||
console.log('payData', payData);
|
||||
.then(([profile, person, education, tags, practice, schedule, payData]) => {
|
||||
setIsFull(profile.fillProgress === 'full');
|
||||
setData({
|
||||
person,
|
||||
education,
|
||||
|
@ -54,13 +59,12 @@ export default function ExpertProfilePage({ params: { locale } }: { params: { lo
|
|||
|
||||
return (
|
||||
<Loader isLoading={loading}>
|
||||
{data && (
|
||||
<ExpertProfile
|
||||
locale={locale}
|
||||
data={data}
|
||||
updateData={setData}
|
||||
/>
|
||||
)}
|
||||
<ExpertProfile
|
||||
isFull={isFull}
|
||||
locale={locale}
|
||||
data={data}
|
||||
updateData={setData}
|
||||
/>
|
||||
</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 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 (
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<BlogPosts
|
||||
basePath={'/'+locale+'/blog/'}
|
||||
locale={locale}
|
||||
pageSize={pageSize}
|
||||
page={page}
|
||||
>
|
||||
</BlogPosts>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
ExpertInformation,
|
||||
ExpertPractice
|
||||
} from '../../../../components/Experts/ExpertDetails';
|
||||
import { Details } from '../../../../types/experts';
|
||||
import { Details } from '../../../../types/education';
|
||||
import { BackButton } from '../../../../components/view/BackButton';
|
||||
import { i18nText } from '../../../../i18nKeys';
|
||||
|
||||
|
@ -36,7 +36,6 @@ export default async function ExpertItem({ params: { expertId = '', locale } }:
|
|||
if (!expertId) notFound();
|
||||
|
||||
const expert = await getExpertById(expertId, locale);
|
||||
console.log(expert);
|
||||
|
||||
const getAssociationLevel = (accLevelId?: number) => {
|
||||
if (accLevelId) {
|
||||
|
@ -112,7 +111,11 @@ export default async function ExpertItem({ params: { expertId = '', locale } }:
|
|||
{expert?.publicCoachDetails?.trainings && expert.publicCoachDetails.trainings?.map(generateDescription)}
|
||||
{expert?.publicCoachDetails?.mbas && expert.publicCoachDetails.mbas?.map(generateDescription)}
|
||||
{expert?.publicCoachDetails?.experiences && expert.publicCoachDetails.experiences?.map(generateDescription)}
|
||||
<ExpertPractice expert={expert} locale={locale} />
|
||||
<ExpertPractice
|
||||
themes={expert?.publicCoachDetails?.themesGroups}
|
||||
cases={expert?.publicCoachDetails?.practiceCases}
|
||||
locale={locale}
|
||||
/>
|
||||
|
||||
{/* <h2 className="title-h2">All Offers by this Expert</h2>
|
||||
<div className="offers-list">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { ReactNode } from 'react';
|
||||
import React, { ReactNode, Suspense } from 'react';
|
||||
import { Metadata } from 'next';
|
||||
import { unstable_setRequestLocale } from 'next-intl/server';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
@ -6,7 +6,7 @@ import { ConfigProvider } from 'antd';
|
|||
import { AntdRegistry } from '@ant-design/nextjs-registry';
|
||||
import theme from '../../constants/theme';
|
||||
import { ALLOWED_LOCALES } from '../../constants/locale';
|
||||
import { Header, Footer } from '../../components/Page';
|
||||
import { Header, Footer, AppConfig } from '../../components/Page';
|
||||
|
||||
type LayoutProps = {
|
||||
children: ReactNode;
|
||||
|
@ -30,6 +30,9 @@ export default function LocaleLayout({ children, params: { locale } }: LayoutPro
|
|||
<AntdRegistry>
|
||||
<ConfigProvider theme={theme}>
|
||||
<div className="b-wrapper">
|
||||
<Suspense fallback={null}>
|
||||
<AppConfig />
|
||||
</Suspense>
|
||||
<div className="b-content">
|
||||
<Header locale={locale} />
|
||||
{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 { CustomInput } from '../view/CustomInput';
|
||||
import { OutlinedButton } from '../view/OutlinedButton';
|
||||
import { FilledYellowButton } from '../view/FilledButton';
|
||||
import {FilledButton, FilledSquareButton, FilledYellowButton} from '../view/FilledButton';
|
||||
import { DeleteAccountModal } from '../Modals/DeleteAccountModal';
|
||||
import { Loader } from '../view/Loader';
|
||||
import {ButtonProps} from "antd/es/button/button";
|
||||
|
||||
type ProfileSettingsProps = {
|
||||
locale: string;
|
||||
|
@ -40,6 +41,20 @@ export const ProfileSettings: FC<ProfileSettingsProps> = ({ locale }) => {
|
|||
}
|
||||
}, [profileSettings]);
|
||||
|
||||
const onSave = (newProfile: ProfileRequest) => {
|
||||
setSaveLoading(true);
|
||||
save(newProfile)
|
||||
.then(() => {
|
||||
fetchProfileSettings();
|
||||
})
|
||||
.catch(() => {
|
||||
message.error('Не удалось сохранить изменения');
|
||||
})
|
||||
.finally(() => {
|
||||
setSaveLoading(false);
|
||||
})
|
||||
}
|
||||
|
||||
const onSaveProfile = () => {
|
||||
form.validateFields()
|
||||
.then(({ login, surname, username }) => {
|
||||
|
@ -55,28 +70,19 @@ export const ProfileSettings: FC<ProfileSettingsProps> = ({ locale }) => {
|
|||
languagesLinks: languagesLinks?.map(({ languageId }) => ({ languageId })) || []
|
||||
};
|
||||
|
||||
// if (photo) {
|
||||
// console.log(photo);
|
||||
// const formData = new FormData();
|
||||
// formData.append('file', photo as FileType);
|
||||
//
|
||||
// newProfile.faceImage = photo;
|
||||
// newProfile.isFaceImageKeepExisting = false;
|
||||
// }
|
||||
if (photo) {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(photo as File);
|
||||
reader.onloadend = () => {
|
||||
const newReg = new RegExp('data:image/(png|jpg|jpeg);base64,')
|
||||
newProfile.faceImage = reader.result.replace(newReg, '');
|
||||
newProfile.isFaceImageKeepExisting = false;
|
||||
|
||||
console.log(newProfile);
|
||||
|
||||
setSaveLoading(true);
|
||||
save(newProfile)
|
||||
.then(() => {
|
||||
fetchProfileSettings();
|
||||
})
|
||||
.catch(() => {
|
||||
message.error('Не удалось сохранить изменения');
|
||||
})
|
||||
.finally(() => {
|
||||
setSaveLoading(false);
|
||||
})
|
||||
onSave(newProfile);
|
||||
}
|
||||
} else {
|
||||
onSave(newProfile);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -99,17 +105,14 @@ export const ProfileSettings: FC<ProfileSettingsProps> = ({ locale }) => {
|
|||
return (
|
||||
<Loader isLoading={fetchLoading} refresh={fetchProfileSettings}>
|
||||
<Form form={form} className="form-settings">
|
||||
<div className="user-avatar">
|
||||
<div className="user-avatar__edit" style={profileSettings?.faceImageUrl ? { backgroundImage: `url(${profileSettings.faceImageUrl})` } : undefined}>
|
||||
<input className="" type="file" id="input-file" />
|
||||
<label htmlFor="input-file" className="form-label" />
|
||||
</div>
|
||||
<div className="user-avatar__text">{i18nText('photoDesc', locale)}</div>
|
||||
</div>
|
||||
<ImgCrop
|
||||
modalTitle="Редактировать"
|
||||
modalOk="Сохранить"
|
||||
modalCancel="Отмена"
|
||||
modalProps={{
|
||||
okButtonProps: { className: 'b-button__filled_yellow' },
|
||||
cancelButtonProps: { className: 'b-button__outlined' }
|
||||
}}
|
||||
beforeCrop={beforeCrop}
|
||||
>
|
||||
<Upload
|
||||
|
@ -121,13 +124,22 @@ export const ProfileSettings: FC<ProfileSettingsProps> = ({ locale }) => {
|
|||
url: profileSettings.faceImageUrl
|
||||
}
|
||||
] : undefined}
|
||||
accept=".jpg,.jpeg,.png,.gif"
|
||||
accept=".jpg,.jpeg,.png"
|
||||
beforeUpload={beforeUpload}
|
||||
multiple={false}
|
||||
showUploadList={false}
|
||||
>
|
||||
{photo && <img height={100} width={100} src={URL.createObjectURL(photo)} />}
|
||||
<Button icon={<CameraOutlined />}>Click to Upload</Button>
|
||||
<div className="user-avatar">
|
||||
<div className="user-avatar__edit" style={photo
|
||||
? { backgroundImage: `url(${URL.createObjectURL(photo)})` }
|
||||
: profileSettings?.faceImageUrl ? { backgroundImage: `url(${profileSettings.faceImageUrl})`} : undefined }>
|
||||
<FilledSquareButton
|
||||
type="primary"
|
||||
icon={<CameraOutlined style={{ fontSize: 28 }} />}
|
||||
/>
|
||||
</div>
|
||||
<div className="user-avatar__text">{i18nText('photoDesc', locale)}</div>
|
||||
</div>
|
||||
</Upload>
|
||||
</ImgCrop>
|
||||
<div className="form-fieldset">
|
||||
|
|
|
@ -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'
|
||||
|
||||
import { useState } from 'react';
|
||||
import { message } from 'antd';
|
||||
import { EditOutlined } from '@ant-design/icons';
|
||||
import React, { useState } from 'react';
|
||||
import {Alert, message} from 'antd';
|
||||
import Image from 'next/image';
|
||||
import { i18nText } from '../../i18nKeys';
|
||||
import { ExpertData } from '../../types/profile';
|
||||
import { ExpertData, PayInfo, ProfileData } from '../../types/profile';
|
||||
import { ExpertsTags } from '../../types/tags';
|
||||
import { PracticeDTO } from '../../types/practice';
|
||||
import { EducationDTO } from '../../types/education';
|
||||
import { ScheduleDTO } from '../../types/schedule';
|
||||
import { AUTH_TOKEN_KEY } from '../../constants/common';
|
||||
import { useLocalStorage } from '../../hooks/useLocalStorage';
|
||||
import { getTags } from '../../actions/profile';
|
||||
import { getTags, getPayData, getEducation, getPractice, getSchedule, getPersonalData } from '../../actions/profile';
|
||||
import { Loader } from '../view/Loader';
|
||||
import { LinkButton } from '../view/LinkButton';
|
||||
import { ExpertTags } from './content/ExpertTags';
|
||||
import { ExpertSchedule } from './content/ExpertSchedule';
|
||||
import { ExpertPayData } from './content/ExpertPayData';
|
||||
import { ExpertEducation } from './content/ExpertEducation';
|
||||
import { ExpertAbout } from './content/ExpertAbout';
|
||||
|
||||
type ExpertProfileProps = {
|
||||
locale: string;
|
||||
data: ExpertData;
|
||||
updateData: (data: ExpertData) => void;
|
||||
isFull: boolean;
|
||||
};
|
||||
|
||||
export const ExpertProfile = ({ locale, data, updateData }: ExpertProfileProps) => {
|
||||
type NewDataPartProps<T> = {
|
||||
key: keyof ExpertData,
|
||||
getNewData: (locale: string, token: string) => Promise<T>,
|
||||
errorMessage?: string;
|
||||
};
|
||||
|
||||
export const ExpertProfile = ({ locale, data, updateData, isFull }: ExpertProfileProps) => {
|
||||
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
|
||||
const [loading, setLoading] = useState<(keyof ExpertData)[]>([]);
|
||||
|
||||
function getNewPartData <T>({ key, getNewData, errorMessage = 'Не удалось получить данные' }: NewDataPartProps<T>) {
|
||||
setLoading([key]);
|
||||
getNewData(locale, jwt)
|
||||
.then((newData) => {
|
||||
updateData({
|
||||
...data,
|
||||
[key]: newData
|
||||
});
|
||||
})
|
||||
.catch(() => message.error(errorMessage))
|
||||
.finally(() => setLoading([]));
|
||||
}
|
||||
|
||||
const updateExpert = (key: keyof ExpertData) => {
|
||||
switch (key) {
|
||||
case 'tags':
|
||||
setLoading([key]);
|
||||
getTags(locale, jwt)
|
||||
.then((tags) => {
|
||||
updateData({
|
||||
...data,
|
||||
tags
|
||||
});
|
||||
})
|
||||
.catch(() => message.error('Не удалось обновить направления'))
|
||||
.finally(() => setLoading([]));
|
||||
getNewPartData<ExpertsTags>({
|
||||
key,
|
||||
getNewData: getTags,
|
||||
errorMessage: 'Не удалось получить направления'
|
||||
});
|
||||
break;
|
||||
case 'practice':
|
||||
getNewPartData<PracticeDTO>({
|
||||
key,
|
||||
getNewData: getPractice
|
||||
});
|
||||
break;
|
||||
case 'education':
|
||||
getNewPartData<EducationDTO>({
|
||||
key,
|
||||
getNewData: getEducation,
|
||||
errorMessage: 'Не удалось получить информацию об образовании'
|
||||
});
|
||||
break;
|
||||
case 'schedule':
|
||||
getNewPartData<ScheduleDTO>({
|
||||
key,
|
||||
getNewData: getSchedule,
|
||||
errorMessage: 'Не удалось получить расписание'
|
||||
});
|
||||
break;
|
||||
case 'person':
|
||||
getNewPartData<ProfileData>({
|
||||
key,
|
||||
getNewData: getPersonalData,
|
||||
errorMessage: 'Не удалось получить информацию о пользователе'
|
||||
});
|
||||
break;
|
||||
case 'payData':
|
||||
getNewPartData<{ person6Data?: PayInfo }>({
|
||||
key,
|
||||
getNewData: getPayData,
|
||||
errorMessage: 'Не удалось получить платежную информацию'
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
@ -52,36 +105,39 @@ export const ExpertProfile = ({ locale, data, updateData }: ExpertProfileProps)
|
|||
<div className="coaching-info">
|
||||
<div className="coaching-profile">
|
||||
<div className="coaching-profile__portrait">
|
||||
<img src="/images/person.png" className="" alt="" />
|
||||
<Image src={data?.person?.faceImageUrl || '/images/user-avatar.png'} width={216} height={216} alt="" />
|
||||
</div>
|
||||
<div className="coaching-profile__inner">
|
||||
<div className="coaching-profile__inner" style={{ flex: 1 }}>
|
||||
<div className="coaching-profile__name">
|
||||
David
|
||||
{`${data?.person?.username} ${data?.person?.surname || ''}`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="coaching-section__wrap">
|
||||
<div className="coaching-section">
|
||||
<div className="coaching-section__title">
|
||||
<h2 className="title-h2">{i18nText('aboutCoach', locale)}</h2>
|
||||
<h2 className="title-h2">person1 + person4</h2>
|
||||
<LinkButton
|
||||
type="link"
|
||||
icon={<EditOutlined />}
|
||||
{!isFull && (
|
||||
<Alert
|
||||
message="Проверьте заполненность блоков"
|
||||
description={(
|
||||
<ul className="b-rules-list">
|
||||
<li>о себе</li>
|
||||
<li>темы сессии</li>
|
||||
<li>рабочее расписание</li>
|
||||
<li>информация об образовании</li>
|
||||
<li>платежная информация</li>
|
||||
</ul>
|
||||
)}
|
||||
type="warning"
|
||||
showIcon
|
||||
/>
|
||||
</div>
|
||||
<div className="card-profile__header__title">
|
||||
{`12 ${i18nText('practiceHours', locale)}`}
|
||||
</div>
|
||||
<div className="card-profile__header__title ">
|
||||
{`15 ${i18nText('supervisionCount', locale)}`}
|
||||
</div>
|
||||
<div className="base-text">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam aliquet, lectus nec viverra
|
||||
malesuada, ligula sem tempor risus, non posuere urna diam a libero.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Loader isLoading={loading.includes('practice') || loading.includes('person')}>
|
||||
<ExpertAbout
|
||||
locale={locale}
|
||||
practice={data?.practice}
|
||||
person={data?.person}
|
||||
updateExpert={updateExpert}
|
||||
/>
|
||||
</Loader>
|
||||
<Loader isLoading={loading.includes('tags')}>
|
||||
<ExpertTags
|
||||
locale={locale}
|
||||
|
@ -89,10 +145,28 @@ export const ExpertProfile = ({ locale, data, updateData }: ExpertProfileProps)
|
|||
updateExpert={updateExpert}
|
||||
/>
|
||||
</Loader>
|
||||
<ExpertSchedule locale={locale} data={data?.schedule} />
|
||||
<ExpertEducation locale={locale} data={data?.education} />
|
||||
<ExpertPayData locale={locale} data={data?.payData?.person6Data} />
|
||||
<Loader isLoading={loading.includes('schedule')}>
|
||||
<ExpertSchedule
|
||||
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>
|
||||
</>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 { EducationDTO } from '../../../types/education';
|
||||
import { i18nText } from '../../../i18nKeys';
|
||||
import { LinkButton } from '../../view/LinkButton';
|
||||
import {ExpertCertificate} from "../../Experts/ExpertDetails";
|
||||
import {useState} from "react";
|
||||
import {ExpertData} from "../../../types/profile";
|
||||
import {EditExpertEducationModal} from "../../Modals/EditExpertEducationModal";
|
||||
|
||||
type ExpertEducationProps = {
|
||||
locale: string;
|
||||
data?: EducationDTO;
|
||||
updateExpert: (key: keyof ExpertData) => void;
|
||||
};
|
||||
|
||||
export const ExpertEducation = ({ locale, data }: ExpertEducationProps) => {
|
||||
export const ExpertEducation = ({ locale, data, updateExpert }: ExpertEducationProps) => {
|
||||
const [showEdit, setShowEdit] = useState<boolean>(false);
|
||||
|
||||
const getAssociationLevel = (accLevelId?: number) => {
|
||||
if (accLevelId) {
|
||||
const [cur] = (data?.associationLevels || []).filter(({ id }) => id === accLevelId) || [];
|
||||
|
||||
return cur?.name || '';
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const getAssociation = (accLevelId?: number) => {
|
||||
if (accLevelId) {
|
||||
const [curLevel] = (data?.associationLevels || []).filter(({ id }) => id === accLevelId) || [];
|
||||
if (curLevel) {
|
||||
const [cur] = (data?.associations || []).filter(({ id }) => id === curLevel.associationId) || [];
|
||||
return cur?.name || '';
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="coaching-section__wrap">
|
||||
<div className="coaching-section">
|
||||
<div className="coaching-section__title">
|
||||
<h2 className="title-h2">{i18nText('education', locale)}</h2>
|
||||
<h2 className="title-h2">person2</h2>
|
||||
<h2 className="title-h2">{i18nText('skillsInfo', locale)}</h2>
|
||||
<LinkButton
|
||||
type="link"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => setShowEdit(true)}
|
||||
/>
|
||||
</div>
|
||||
<div className="coaching-section__desc">
|
||||
<h3 className="title-h3">Psychologist</h3>
|
||||
<div className="base-text">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam aliquet, lectus nec viverra
|
||||
malesuada, ligula sem tempor risus, non posuere urna diam a libero.
|
||||
{data?.person2Data?.educations?.length > 0 && (
|
||||
<div className="coaching-section__desc">
|
||||
{data?.person2Data?.educations?.map(({ id, title, description, document }) => (
|
||||
<div key={id}>
|
||||
<h3 className="title-h3">{title}</h3>
|
||||
{description && <div className="base-text">{description}</div>}
|
||||
{document && (
|
||||
<div className="sertific">
|
||||
<ExpertCertificate document={document} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="sertific">
|
||||
<img src="/images/sertific.png" className="" alt="" />
|
||||
)}
|
||||
</div>
|
||||
{data?.person2Data?.certificates?.length > 0 && (
|
||||
<div className="coaching-section">
|
||||
<h2 className="title-h2">{i18nText('profCertification', locale)}</h2>
|
||||
<div className="coaching-section__desc">
|
||||
{data?.person2Data?.certificates?.map((cert) => (
|
||||
<div key={cert.id}>
|
||||
<div className="base-text">
|
||||
{`${getAssociationLevel(cert?.associationLevelId)} ${getAssociation(cert?.associationLevelId)}`}
|
||||
</div>
|
||||
{cert.document && (
|
||||
<div className="sertific">
|
||||
<ExpertCertificate document={cert.document} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="coaching-section">
|
||||
<h2 className="title-h2">{i18nText('profCertification', locale)}</h2>
|
||||
<div className="coaching-section__desc">
|
||||
<div className="base-text">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam aliquet, lectus nec viverra
|
||||
malesuada, ligula sem tempor risus, non posuere urna diam a libero.
|
||||
)}
|
||||
{data?.person2Data?.trainings?.length > 0 && (
|
||||
<div className="coaching-section">
|
||||
<h2 className="title-h2">
|
||||
{`${i18nText('trainings', locale)} | ${i18nText('seminars', locale)} | ${i18nText('courses', locale)}`}
|
||||
</h2>
|
||||
<div className="coaching-section__desc">
|
||||
{data?.person2Data?.trainings?.map(({ id, title, description, document }) => (
|
||||
<div key={id}>
|
||||
<h3 className="title-h3">{title}</h3>
|
||||
{description && <div className="base-text">{description}</div>}
|
||||
{document && (
|
||||
<div className="sertific">
|
||||
<ExpertCertificate document={document} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="coaching-section">
|
||||
<h2 className="title-h2">
|
||||
{`${i18nText('trainings', locale)} | ${i18nText('seminars', locale)} | ${i18nText('courses', locale)}`}
|
||||
</h2>
|
||||
<div className="coaching-section__desc">
|
||||
<div className="base-text">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam aliquet, lectus nec viverra
|
||||
malesuada, ligula sem tempor risus, non posuere urna diam a libero.
|
||||
)}
|
||||
{data?.person2Data?.mbas?.length > 0 && (
|
||||
<div className="coaching-section">
|
||||
<h2 className="title-h2">{i18nText('mba', locale)}</h2>
|
||||
<div className="coaching-section__desc">
|
||||
{data?.person2Data?.mbas?.map(({ id, title, description, document }) => (
|
||||
<div key={id}>
|
||||
<h3 className="title-h3">{title}</h3>
|
||||
{description && <div className="base-text">{description}</div>}
|
||||
{document && (
|
||||
<div className="sertific">
|
||||
<ExpertCertificate document={document} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="coaching-section">
|
||||
<h2 className="title-h2">{i18nText('mba', locale)}</h2>
|
||||
<div className="coaching-section__desc">
|
||||
<div className="base-text">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam aliquet, lectus nec viverra
|
||||
malesuada, ligula sem tempor risus, non posuere urna diam a libero.
|
||||
)}
|
||||
{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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,28 +1,65 @@
|
|||
'use client'
|
||||
|
||||
import { useState } from 'react';
|
||||
import { EditOutlined } from '@ant-design/icons';
|
||||
import { i18nText } from '../../../i18nKeys';
|
||||
import { PayInfo } from '../../../types/profile';
|
||||
import { ExpertData, PayInfo } from '../../../types/profile';
|
||||
import { LinkButton } from '../../view/LinkButton';
|
||||
import { EditExpertPayDataModal } from '../../Modals/EditExpertPayDataModal';
|
||||
|
||||
type ExpertPayDataProps = {
|
||||
locale: string;
|
||||
data?: PayInfo
|
||||
data?: PayInfo;
|
||||
updateExpert: (key: keyof ExpertData) => void;
|
||||
};
|
||||
|
||||
export const ExpertPayData = ({ locale, data }: ExpertPayDataProps) => {
|
||||
export const ExpertPayData = ({ locale, data, updateExpert }: ExpertPayDataProps) => {
|
||||
const [showEdit, setShowEdit] = useState<boolean>(false);
|
||||
|
||||
const hide = (str?: string) => {
|
||||
const reg = new RegExp('(.)(?=.*....)', 'gi');
|
||||
return str ? str.replace(reg, '*') : '';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="coaching-section__wrap">
|
||||
<div className="coaching-section">
|
||||
<div className="coaching-section__title">
|
||||
<h2 className="title-h2">Card data - person6</h2>
|
||||
<h2 className="title-h2">{i18nText('payInfo', locale)}</h2>
|
||||
<LinkButton
|
||||
type="link"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => setShowEdit(true)}
|
||||
/>
|
||||
</div>
|
||||
<div className="base-text">
|
||||
Card
|
||||
<div className="base-text pay-data-list">
|
||||
{data?.beneficiaryName && (
|
||||
<div>
|
||||
<div>{i18nText('beneficiaryName', locale)}</div>
|
||||
<div>{data.beneficiaryName}</div>
|
||||
</div>
|
||||
)}
|
||||
{data?.bicOrSwift && (
|
||||
<div>
|
||||
<div>{i18nText('bicOrSwift', locale)}</div>
|
||||
<div>{hide(data.bicOrSwift)}</div>
|
||||
</div>
|
||||
)}
|
||||
{data?.iban && (
|
||||
<div>
|
||||
<div>IBAN</div>
|
||||
<div>{hide(data.iban)}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<EditExpertPayDataModal
|
||||
locale={locale}
|
||||
open={showEdit}
|
||||
data={data}
|
||||
handleCancel={() => setShowEdit(false)}
|
||||
refresh={() => updateExpert('payData')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,28 +1,56 @@
|
|||
import { useState } from 'react';
|
||||
import { Tag } from 'antd';
|
||||
import { EditOutlined } from '@ant-design/icons';
|
||||
import dayjs from 'dayjs';
|
||||
import { ScheduleDTO } from '../../../types/schedule';
|
||||
import { i18nText } from '../../../i18nKeys';
|
||||
import { getCurrentTime, getTimeString } from '../../../utils/time';
|
||||
import { ExpertData } from '../../../types/profile';
|
||||
import { LinkButton } from '../../view/LinkButton';
|
||||
import { EditExpertScheduleModal } from '../../Modals/EditExpertScheduleModal';
|
||||
|
||||
type ExpertScheduleProps = {
|
||||
locale: string;
|
||||
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 (
|
||||
<div className="coaching-section__wrap">
|
||||
<div className="coaching-section">
|
||||
<div className="coaching-section__title">
|
||||
<h2 className="title-h2">Schedule - person51</h2>
|
||||
<h2 className="title-h2">{i18nText('schedule', locale)}</h2>
|
||||
<LinkButton
|
||||
type="link"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => setShowEdit(true)}
|
||||
/>
|
||||
</div>
|
||||
<div className="base-text">
|
||||
Schedule
|
||||
<div className="b-schedule-list">
|
||||
{data && data?.workingTimes?.map((date, index) => {
|
||||
const { startDay, 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>
|
||||
<EditExpertScheduleModal
|
||||
open={showEdit}
|
||||
handleCancel={() => setShowEdit(false)}
|
||||
locale={locale}
|
||||
data={data}
|
||||
refresh={() => updateExpert('schedule')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -22,7 +22,7 @@ export const ExpertTags = ({ locale, data, updateExpert }: ExpertTagsProps) => {
|
|||
<div className="coaching-section__wrap">
|
||||
<div className="coaching-section">
|
||||
<div className="coaching-section__title">
|
||||
<h2 className="title-h2">{i18nText('direction', locale)}</h2>
|
||||
<h2 className="title-h2">{i18nText('topics', locale)}</h2>
|
||||
<LinkButton
|
||||
type="link"
|
||||
icon={<EditOutlined />}
|
||||
|
|
|
@ -4,7 +4,8 @@ import React, { FC } from 'react';
|
|||
import Image from 'next/image';
|
||||
import { Tag, Image as AntdImage, Space } from 'antd';
|
||||
import { ZoomInOutlined, ZoomOutOutlined, StarFilled } from '@ant-design/icons';
|
||||
import { ExpertDetails, ExpertDocument } from '../../types/experts';
|
||||
import { ExpertDetails, Practice, ThemeGroup } from '../../types/experts';
|
||||
import { ExpertDocument } from '../../types/file';
|
||||
import { Locale } from '../../types/locale';
|
||||
import { CustomRate } from '../view/CustomRate';
|
||||
import { i18nText } from '../../i18nKeys';
|
||||
|
@ -15,6 +16,12 @@ type ExpertDetailsProps = {
|
|||
locale?: string;
|
||||
};
|
||||
|
||||
type ExpertPracticeProps = {
|
||||
cases?: Practice[];
|
||||
themes?: ThemeGroup[];
|
||||
locale?: string;
|
||||
};
|
||||
|
||||
export const ExpertCard: FC<ExpertDetailsProps> = ({ expert, locale }) => {
|
||||
const { publicCoachDetails } = expert || {};
|
||||
|
||||
|
@ -62,10 +69,10 @@ export const ExpertInformation: FC<ExpertDetailsProps> = ({ expert, locale }) =>
|
|||
<div className="expert-info">
|
||||
{/* <h2 className="title-h2">{}</h2> */}
|
||||
<div className="skills__list">
|
||||
{coachLanguages?.map((skill) => <Tag key={skill} className="skills__list__item">{skill}</Tag>)}
|
||||
{coachLanguages?.map((lang) => <Tag key={lang} className="skills__list__item">{lang}</Tag>)}
|
||||
</div>
|
||||
</div>
|
||||
<p className="base-text">
|
||||
{/* <p className="base-text">
|
||||
Hello, my name is Marcelo. I am a Senior UX Designer with more than 6 years of experience working
|
||||
with the largest companies in the world such as Disney, Globant and currently IBM.
|
||||
During my career, I have helped organizations solve complex problems using aesthetically pleasing
|
||||
|
@ -79,7 +86,7 @@ export const ExpertInformation: FC<ExpertDetailsProps> = ({ expert, locale }) =>
|
|||
Strategic thinking <br /><br />
|
||||
|
||||
Oh, and I also speak Spanish!
|
||||
</p>
|
||||
</p> */}
|
||||
<div className="skills__list">
|
||||
{tags?.map((skill) => <Tag key={skill?.id} className="skills__list__item">{skill?.name}</Tag>)}
|
||||
</div>
|
||||
|
@ -93,14 +100,12 @@ export const ExpertInformation: FC<ExpertDetailsProps> = ({ expert, locale }) =>
|
|||
);
|
||||
};
|
||||
|
||||
export const ExpertPractice: FC<ExpertDetailsProps> = ({ expert, locale }) => {
|
||||
const { publicCoachDetails: { practiceCases = [], themesGroups = [] } } = expert || {};
|
||||
|
||||
return practiceCases?.length > 0 ? (
|
||||
export const ExpertPractice: FC<ExpertPracticeProps> = ({ themes = [], cases = [], locale }) => {
|
||||
return cases?.length > 0 ? (
|
||||
<div>
|
||||
<h3 className="title-h3">{i18nText('successfulCase', locale)}</h3>
|
||||
{practiceCases?.map(({ id, description, themesGroupIds }) => {
|
||||
const filtered = themesGroups?.filter(({ id }) => themesGroupIds?.includes(+id));
|
||||
{cases?.map(({ id, description, themesGroupIds }) => {
|
||||
const filtered = themes ? themes.filter(({ id }) => themesGroupIds?.includes(+id)) : [];
|
||||
|
||||
return (
|
||||
<div key={id} className="case-list">
|
||||
|
|
|
@ -114,6 +114,7 @@ export const ExpertsFilter = ({
|
|||
...getObjectByAdditionalFilter(searchParams)
|
||||
};
|
||||
const search = getSearchParamsString(newFilter);
|
||||
console.log('basePath', basePath);
|
||||
|
||||
router.push(search ? `${basePath}?${search}#filter` : `${basePath}#filter`);
|
||||
|
||||
|
@ -162,18 +163,15 @@ export const ExpertsFilter = ({
|
|||
), [filter, searchParams, searchData]);
|
||||
|
||||
const getLangList = () => {
|
||||
const reg = searchLang ? new RegExp(searchLang, 'ig') : '';
|
||||
const langList = reg ? (languages || []).filter(({ code, nativeSpelling }) => reg.test(code) || reg.test(nativeSpelling)) : languages;
|
||||
const langList = searchLang ? (languages || []).filter(({ code, nativeSpelling }) => code.indexOf(searchLang) !== -1 || nativeSpelling.indexOf(searchLang) !== -1) : languages;
|
||||
return langList?.length
|
||||
? getList('userLanguages', langList.map(({ code, nativeSpelling }) => ({ id: code, name: nativeSpelling })))
|
||||
: null;
|
||||
};
|
||||
|
||||
const getTagsList = () => {
|
||||
const reg = searchTags ? new RegExp(searchTags, 'ig') : '';
|
||||
|
||||
if (reg) {
|
||||
const tagsList = filteredTags.filter(({ name, group }) => reg.test(name) || reg.test(group));
|
||||
if (searchTags) {
|
||||
const tagsList = filteredTags.filter(({ name, group }) => name.indexOf(searchTags) !== -1 || group.indexOf(searchTags) !== -1);
|
||||
return getList('themesTagIds', tagsList);
|
||||
}
|
||||
|
||||
|
|
|
@ -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' }}/>}
|
||||
>
|
||||
<div className="b-modal__expert__content">
|
||||
<div className="b-modal__expert__title">{i18nText('direction', locale)}</div>
|
||||
<div className="b-modal__expert__title">{i18nText('selectTopic', locale)}</div>
|
||||
<div className="b-modal__expert__inner">
|
||||
{data?.themesGroups && data.themesGroups.filter(({ isActive }) => isActive).map(({ id, name }) => (
|
||||
<div key={`group_${id}`}>
|
||||
|
|
|
@ -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 './Footer';
|
||||
export * from './GeneralTopSection';
|
||||
export * from './AppConfig';
|
||||
|
|
|
@ -30,7 +30,9 @@ export const CustomMultiSelect = (props: any) => {
|
|||
|
||||
return (
|
||||
<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
|
||||
className="b-multiselect"
|
||||
mode="multiple"
|
||||
|
|
|
@ -4,7 +4,7 @@ import React, { useEffect, useState } from 'react';
|
|||
import { Select } from 'antd';
|
||||
|
||||
export const CustomSelect = (props: any) => {
|
||||
const { label, value, ...other } = props;
|
||||
const { label, value, style, ...other } = props;
|
||||
const [isActiveLabel, setIsActiveLabel] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -16,8 +16,10 @@ export const CustomSelect = (props: any) => {
|
|||
}, [value]);
|
||||
|
||||
return (
|
||||
<div className={`b-select-wrap ${isActiveLabel ? 'b-select__active' : ''}`}>
|
||||
<div className="b-select-label">{label}</div>
|
||||
<div className={`b-select-wrap ${isActiveLabel ? 'b-select__active' : ''}`} style={style}>
|
||||
<div className="b-select-label">
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
<Select
|
||||
className="b-select"
|
||||
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}
|
||||
</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';
|
||||
|
||||
export const LinkButton = (props: any) => (
|
||||
<Button className="b-button__link" {...props}>
|
||||
<Button className={`b-button__link${props?.danger ? ' danger': ''}`} {...props}>
|
||||
{props.children}
|
||||
</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',
|
||||
noData: 'Keine Daten',
|
||||
notFound: 'Nicht gefunden',
|
||||
skillsInfo: 'Fähigkeiten-Infos',
|
||||
trainings: 'Trainings',
|
||||
seminars: 'Seminare',
|
||||
courses: 'Kurse',
|
||||
|
@ -112,7 +113,39 @@ export default {
|
|||
aboutCoach: 'Über Coach',
|
||||
education: 'Bildung',
|
||||
coaching: 'Coaching',
|
||||
|
||||
experiences: 'Praktische Erfahrung',
|
||||
payInfo: 'Zahlungsdaten',
|
||||
sessionDuration: 'Sitzungsdauer',
|
||||
experienceHours: 'Gesamtstunden praktischer Erfahrung',
|
||||
topics: 'Themen',
|
||||
selectTopic: 'Thema auswählen',
|
||||
title: 'Titel',
|
||||
description: 'Beschreibung',
|
||||
sessionCost: 'Sitzungskosten in Euro',
|
||||
yourTimezone: 'Deine Zeitzone',
|
||||
workTime: 'Arbeitszeit',
|
||||
startAt: 'Beginn um',
|
||||
finishAt: 'Ende um',
|
||||
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: {
|
||||
invalidEmail: 'Die E-Mail-Adresse ist ungültig',
|
||||
emptyEmail: 'Bitte geben Sie Ihre E-Mail ein',
|
||||
|
|
|
@ -110,8 +110,42 @@ export default {
|
|||
courses: 'Courses',
|
||||
mba: 'MBA Information',
|
||||
aboutCoach: 'About Coach',
|
||||
skillsInfo: 'Skills Info',
|
||||
education: 'Education',
|
||||
coaching: 'Coaching',
|
||||
experiences: 'Practical experience',
|
||||
payInfo: 'Payment Info',
|
||||
sessionDuration: 'Session duration',
|
||||
experienceHours: 'Total hours of practical experience',
|
||||
topics: 'Topics',
|
||||
selectTopic: 'Select Topic',
|
||||
title: 'Title',
|
||||
description: 'Description',
|
||||
sessionCost: 'Session cost in euro',
|
||||
yourTimezone: 'Your timezone',
|
||||
workTime: 'Work time',
|
||||
startAt: 'Start at',
|
||||
finishAt: 'Finish at',
|
||||
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: {
|
||||
invalidEmail: 'The email address is not valid',
|
||||
emptyEmail: 'Please enter your E-mail',
|
||||
|
|
|
@ -105,6 +105,7 @@ export default {
|
|||
signUp: 'Regístrate ahora',
|
||||
noData: 'Sin datos',
|
||||
notFound: 'No encontrado',
|
||||
skillsInfo: 'Información',
|
||||
trainings: 'Formación',
|
||||
seminars: 'Seminarios',
|
||||
courses: 'Cursos',
|
||||
|
@ -112,7 +113,39 @@ export default {
|
|||
aboutCoach: 'Sobre el coach',
|
||||
education: 'Educación',
|
||||
coaching: 'Coaching',
|
||||
|
||||
experiences: 'Experiencia práctica',
|
||||
payInfo: 'Información de pago',
|
||||
sessionDuration: 'Duración de la sesión',
|
||||
experienceHours: 'Total de horas de experiencia práctica',
|
||||
topics: 'Temas',
|
||||
selectTopic: 'Seleccione el tema',
|
||||
title: 'Título',
|
||||
description: 'Descripción',
|
||||
sessionCost: 'Coste de la sesión en euros',
|
||||
yourTimezone: 'Tu zona horaria',
|
||||
workTime: 'Tiempo de trabajo',
|
||||
startAt: 'Empieza a las',
|
||||
finishAt: 'Termina a las',
|
||||
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: {
|
||||
invalidEmail: 'La dirección de correo electrónico no es válida',
|
||||
emptyEmail: 'Introduce tu correo electrónico',
|
||||
|
|
|
@ -105,6 +105,7 @@ export default {
|
|||
signUp: 'Inscrivez-vous maintenant',
|
||||
noData: 'Aucune donnée',
|
||||
notFound: 'Non trouvé',
|
||||
skillsInfo: 'Infos sur les compétences',
|
||||
trainings: 'Formations',
|
||||
seminars: 'Séminaires',
|
||||
courses: 'Cours',
|
||||
|
@ -112,7 +113,39 @@ export default {
|
|||
aboutCoach: 'À propos du coach',
|
||||
education: 'Éducation',
|
||||
coaching: 'Coaching',
|
||||
|
||||
experiences: 'Expérience pratique',
|
||||
payInfo: 'Infos sur le paiement',
|
||||
sessionDuration: 'Durée de la session',
|
||||
experienceHours: 'Heures totales d\'expérience pratique',
|
||||
topics: 'Sujets',
|
||||
selectTopic: 'Sélectionnez un sujet',
|
||||
title: 'Titre',
|
||||
description: 'Description',
|
||||
sessionCost: 'Coût de la session en euros',
|
||||
yourTimezone: 'Votre fuseau horaire',
|
||||
workTime: 'Heures de travail',
|
||||
startAt: 'Commencer à',
|
||||
finishAt: 'Finir à',
|
||||
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: {
|
||||
invalidEmail: 'L\'adresse e-mail n\'est pas valide',
|
||||
emptyEmail: 'Veuillez saisir votre e-mail',
|
||||
|
|
|
@ -105,6 +105,7 @@ export default {
|
|||
signUp: 'Iscriviti ora',
|
||||
noData: 'Nessun dato',
|
||||
notFound: 'Non trovato',
|
||||
skillsInfo: 'Info su competenze',
|
||||
trainings: 'Training',
|
||||
seminars: 'Seminari',
|
||||
courses: 'Corsi',
|
||||
|
@ -112,7 +113,39 @@ export default {
|
|||
aboutCoach: 'Informazioni sul coach',
|
||||
education: 'Istruzione',
|
||||
coaching: 'Coaching',
|
||||
|
||||
experiences: 'Esperienza pratica',
|
||||
payInfo: 'Info pagamento',
|
||||
sessionDuration: 'Durata della sessione',
|
||||
experienceHours: 'Totale ore di esperienza pratica',
|
||||
topics: 'Argomenti',
|
||||
selectTopic: 'Seleziona l\'argomento',
|
||||
title: 'Titolo',
|
||||
description: 'Descrizione',
|
||||
sessionCost: 'Costo della sessione in euro',
|
||||
yourTimezone: 'Il tuo fuso orario',
|
||||
workTime: 'Orario di lavoro',
|
||||
startAt: 'Inizia alle',
|
||||
finishAt: 'Termina alle',
|
||||
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: {
|
||||
invalidEmail: 'L\'indirizzo e-mail non è valido',
|
||||
emptyEmail: 'Inserisci l\'e-mail',
|
||||
|
|
|
@ -98,21 +98,54 @@ export default {
|
|||
expertBackground: 'Профессиональный опыт эксперта',
|
||||
profCertification: 'Профессиональная сертификация',
|
||||
practiceHours: 'Часов практики',
|
||||
supervisionCount: 'Часов супервизии в год',
|
||||
supervisionCount: 'Супервизий в год',
|
||||
outOf: 'из',
|
||||
schedule: 'Расписание',
|
||||
successfulCase: 'Успешные случаи из практики',
|
||||
signUp: 'Записаться сейчас',
|
||||
noData: 'Нет данных',
|
||||
notFound: 'Не найдено',
|
||||
skillsInfo: 'Навыки',
|
||||
trainings: 'Тренинги',
|
||||
seminars: 'Семинары',
|
||||
courses: 'Курсы',
|
||||
mba: 'Информация о MBA',
|
||||
experiences: 'Практический опыт',
|
||||
aboutCoach: 'О коуче',
|
||||
education: 'Образование',
|
||||
coaching: 'Коучинг',
|
||||
|
||||
payInfo: 'Платежная информация',
|
||||
sessionDuration: 'Продолжительность сессии',
|
||||
experienceHours: 'Общее количество часов практического опыта',
|
||||
topics: 'Темы',
|
||||
selectTopic: 'Выберите тему',
|
||||
title: 'Название',
|
||||
description: 'Описание',
|
||||
sessionCost: 'Стоимость сессии в евро',
|
||||
yourTimezone: 'Ваш часовой пояс',
|
||||
workTime: 'Рабочее время',
|
||||
startAt: 'Начало в',
|
||||
finishAt: 'Завершение в',
|
||||
day: 'День',
|
||||
addWorkingHours: 'Добавить рабочие часы',
|
||||
specialisation: 'Специализация',
|
||||
selectSpecialisation: 'Выберите свою специализацию для продолжения',
|
||||
fillWeeklySchedule: 'Заполните свое недельное расписание',
|
||||
beneficiaryName: 'Имя получателя',
|
||||
bicOrSwift: 'BIC/Swift код',
|
||||
association: 'Ассоциация',
|
||||
level: 'Уровень',
|
||||
addDiploma: 'Добавить диплом',
|
||||
university: 'ВУЗ',
|
||||
sunday: 'Вс',
|
||||
monday: 'Пн',
|
||||
tuesday: 'Вт',
|
||||
wednesday: 'Ср',
|
||||
thursday: 'Чт',
|
||||
friday: 'Пт',
|
||||
saturday: 'Сб',
|
||||
addNew: 'Добавить',
|
||||
mExperiences: 'Управленческий опыт',
|
||||
errors: {
|
||||
invalidEmail: 'Адрес электронной почты недействителен',
|
||||
emptyEmail: 'Пожалуйста, введите ваш E-mail',
|
||||
|
|
|
@ -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 {
|
||||
display: flex;
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
grid-template-columns: 100px auto;
|
||||
|
||||
&__edit {
|
||||
position: relative;
|
||||
|
|
|
@ -51,7 +51,8 @@
|
|||
}
|
||||
|
||||
&__inner {
|
||||
height: 60vh;
|
||||
height: auto;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
|
||||
& > div {
|
||||
|
@ -86,3 +87,13 @@
|
|||
.ant-modal-mask {
|
||||
background-color: rgba(0, 59, 70, 0.4) !important;
|
||||
}
|
||||
|
||||
.ant-upload-list-item-name {
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
.ant-upload-list-item {
|
||||
&:hover {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1188,7 +1188,6 @@
|
|||
height: 86px;
|
||||
border-radius: 16px;
|
||||
border: 2px solid #FFF;
|
||||
background: lightgray 50%;
|
||||
box-shadow: 0 8px 16px 0 rgba(102, 165, 173, 0.32);
|
||||
overflow: hidden;
|
||||
|
||||
|
@ -1210,6 +1209,7 @@
|
|||
@include rem(18);
|
||||
font-weight: 600;
|
||||
line-height: 150%;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1233,6 +1233,61 @@
|
|||
}
|
||||
}
|
||||
|
||||
&__info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.title-h3 {
|
||||
color: #003B46;
|
||||
@include rem(16);
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.case-list {
|
||||
margin-top: 8px;
|
||||
|
||||
p {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__practice {
|
||||
color: #2C7873;
|
||||
@include rem(16);
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
&__lang {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
& > div {
|
||||
color: #003B46;
|
||||
@include rem(16);
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
&__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
& > div {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&__item {
|
||||
color: #003B46;
|
||||
@include rem(16);
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.title-h2 {
|
||||
color: #003B46;
|
||||
@include rem(18);
|
||||
|
@ -1243,7 +1298,7 @@
|
|||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&__desc {
|
||||
&__desc, &__desc > div {
|
||||
border-radius: 16px;
|
||||
background: #EFFCFF;
|
||||
padding: 16px;
|
||||
|
@ -1340,6 +1395,7 @@
|
|||
background-position: 99% 50%;
|
||||
background-repeat: no-repeat;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
|
||||
&__title {
|
||||
color: #FFBD00;
|
||||
|
@ -1366,6 +1422,16 @@
|
|||
background: #C4DFE6;
|
||||
}
|
||||
|
||||
&-item {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
grid-template-columns: 80px 1fr 1fr 32px;
|
||||
|
||||
&__single {
|
||||
grid-template-columns: 80px 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
&__inner{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -1382,6 +1448,7 @@
|
|||
&__wrap {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-direction: column;
|
||||
|
||||
.btn-cancel,
|
||||
.btn-edit {
|
||||
|
@ -1443,8 +1510,17 @@
|
|||
top: 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 "_auth-modal.scss";
|
||||
@import "_modal.scss";
|
||||
@import "_edu.scss";
|
||||
@import "_schedule.scss";
|
||||
|
||||
@import "./view/style.scss";
|
||||
@import "./sessions/style.scss";
|
||||
|
|
|
@ -17,6 +17,19 @@
|
|||
padding: 4px 24px !important;
|
||||
}
|
||||
|
||||
&_square {
|
||||
width: 42px !important;
|
||||
height: 42px !important;
|
||||
background: #66A5AD !important;
|
||||
font-size: 15px !important;
|
||||
border-radius: 8px !important;
|
||||
box-shadow: 0px 2px 4px 0px rgba(102, 165, 173, 0.32) !important;
|
||||
position: absolute !important;
|
||||
right: -8px !important;
|
||||
bottom: -8px !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
background: #D93E5C !important;
|
||||
box-shadow: none !important;
|
||||
|
@ -28,6 +41,10 @@
|
|||
font-size: 15px !important;
|
||||
height: auto !important;
|
||||
padding: 0 !important;
|
||||
|
||||
&.danger {
|
||||
color: #D93E5C !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__outlined {
|
||||
|
|
|
@ -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 {
|
||||
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 {
|
||||
|
|
|
@ -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;
|
||||
left: 16px;
|
||||
top: 15px;
|
||||
right: 22px;
|
||||
z-index: 1;
|
||||
transition: all .1s ease;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
span {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -110,7 +117,14 @@
|
|||
position: absolute;
|
||||
left: 16px;
|
||||
top: 15px;
|
||||
right: 22px;
|
||||
z-index: 1;
|
||||
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 "_switch.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';
|
||||
|
||||
export type Details = {
|
||||
id: number;
|
||||
userId?:number;
|
||||
id?: number;
|
||||
userId?: number;
|
||||
title?: string;
|
||||
description?: string;
|
||||
document?: ExpertDocument;
|
||||
document?: ExpertDocument | null;
|
||||
};
|
||||
|
||||
export type Certificate = {
|
||||
id: number;
|
||||
id?: number;
|
||||
userId?: number;
|
||||
associationLevelId?: number;
|
||||
document?: ExpertDocument;
|
||||
associationId?: number;
|
||||
document?: ExpertDocument | null;
|
||||
};
|
||||
|
||||
export type Experience = {
|
||||
id: number,
|
||||
id?: number,
|
||||
userId?: number,
|
||||
title?: string,
|
||||
description?: string
|
||||
|
@ -24,10 +25,10 @@ export type Experience = {
|
|||
|
||||
export type Association = {
|
||||
id: number;
|
||||
name?: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type AssociationLevel = Association & { associationId?: number };
|
||||
export type AssociationLevel = Association & { associationId: number };
|
||||
|
||||
export type EducationData = {
|
||||
certificates?: Certificate[],
|
||||
|
|
|
@ -6,7 +6,7 @@ export type Supervision = {
|
|||
};
|
||||
|
||||
export type PracticeCase = {
|
||||
id: number,
|
||||
id?: number,
|
||||
userId?: number,
|
||||
description?: string,
|
||||
themesGroupIds?: number[]
|
||||
|
@ -18,12 +18,14 @@ export type PracticeData = {
|
|||
sessionDuration?: number,
|
||||
sessionCost?: number,
|
||||
practiceCases?: PracticeCase[]
|
||||
}
|
||||
};
|
||||
|
||||
export type PracticePersonData = PracticeData & {
|
||||
themesGroups?: ExpertsThemesGroups[],
|
||||
supervisionPerYears?: Supervision[],
|
||||
sessionCosts?: number[]
|
||||
};
|
||||
|
||||
export interface PracticeDTO {
|
||||
person4Data: PracticeData & {
|
||||
themesGroups?: ExpertsThemesGroups[],
|
||||
supervisionPerYears?: Supervision[],
|
||||
sessionCosts?: number[]
|
||||
}
|
||||
person4Data: PracticePersonData
|
||||
}
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { UploadFile } from 'antd';
|
||||
import {EducationDTO} from "./education";
|
||||
import {ExpertsTags} from "./tags";
|
||||
import {PracticeDTO} from "./practice";
|
||||
import {ScheduleDTO} from "./schedule";
|
||||
import { EducationDTO } from './education';
|
||||
import { ExpertsTags } from './tags';
|
||||
import { PracticeDTO } from './practice';
|
||||
import { ScheduleDTO } from './schedule';
|
||||
|
||||
export type ProfileData = {
|
||||
username?: string;
|
||||
|
@ -15,7 +14,8 @@ export type ProfileData = {
|
|||
hasExternalLogin?: boolean;
|
||||
isTestMode?: boolean;
|
||||
phone?: string;
|
||||
languagesLinks?: { language: { id: number, code: string, nativeSpelling: string }, languageId: number }[]
|
||||
languagesLinks?: { language: { id: number, code: string, nativeSpelling: string }, languageId: number }[];
|
||||
allLanguages?: { id: number, code: string, nativeSpelling: string }[]
|
||||
}
|
||||
|
||||
export type Profile = ProfileData & { id: number };
|
||||
|
@ -27,7 +27,7 @@ export type ProfileRequest = {
|
|||
languagesLinks?: { languageId: number }[];
|
||||
username?: string;
|
||||
surname?: string;
|
||||
faceImage?: UploadFile;
|
||||
faceImage?: any;
|
||||
isFaceImageKeepExisting?: boolean;
|
||||
phone?: string;
|
||||
};
|
||||
|
|
|
@ -3,8 +3,15 @@ export type WorkingTime = {
|
|||
startTimeUtc?: number,
|
||||
endDayOfWeekUtc?: string,
|
||||
endTimeUtc?: number
|
||||
}
|
||||
};
|
||||
|
||||
export interface ScheduleDTO {
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
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