Compare commits

...

22 Commits

Author SHA1 Message Date
SD be7efc0d32 0.0.4 2024-10-16 21:48:56 +04:00
SD 4b429c8655 0.0.3 2024-09-10 20:49:39 +04:00
SD 3ed78c0e45 fix: fix main filter point, fix save profile pictures 2024-09-10 20:49:19 +04:00
SD 3345a533d2 feat: add appVersion in browser 2024-09-06 15:22:55 +04:00
SD 76ffdc4094 0.0.2 2024-09-06 14:40:19 +04:00
SD b52096b3bc feat: add edit modal for expert's schedule 2024-09-05 20:06:00 +04:00
dzfelix cda91b9ea9 breadcrumbs & author link 2024-08-28 12:28:30 +03:00
dzfelix 28f5babf22 fix meta anf first page 2024-08-27 17:01:29 +03:00
dzfelix 80f53e871d Merge branch 'refs/heads/develop' into blog 2024-08-27 16:54:31 +03:00
dzfelix 77d3c8f66b main fix 2024-08-26 23:29:30 +04:00
dzfelix f7fe427aae fix lock 2024-08-22 18:15:36 +03:00
dzfelix 5844bd9e7c Merge branch 'refs/heads/blog' into develop 2024-08-22 17:28:23 +03:00
dzfelix dbb74b9ccd Merge remote-tracking branch 'origin/blog' into blog 2024-08-22 17:19:35 +03:00
dzfelix 8ee52bc834 blog fix cat link 2024-08-22 17:19:22 +03:00
norton81 c563818e91 Merge remote-tracking branch 'origin/blog' into develop 2024-08-21 09:44:07 +03:00
norton81 2f2d9db82a Merge branch 'master' into blog 2024-08-20 13:28:40 +03:00
norton81 1461c4948e Merge branch 'master' into develop 2024-08-20 13:25:46 +03:00
dzfelix 74d93541a3 blog pagination & sitemap 2024-08-17 23:26:12 +04:00
SD f92810d320 feat: add expert profile 2024-08-17 03:51:27 +04:00
SD 6f5c3738b7 Merge branch 'master' into develop 2024-08-17 03:50:01 +04:00
Dasha 526e703d9a Merge pull request 'blog' (#1) from blog into develop
Reviewed-on: #1
2024-08-16 23:49:20 +00:00
dzfelix ed756d0646 blog contentful 2024-08-16 14:43:07 +03:00
79 changed files with 4128 additions and 913 deletions

3
.env
View File

@ -1,3 +1,6 @@
NEXT_PUBLIC_SERVER_BASE_URL=https://api.bbuddy.expert/api NEXT_PUBLIC_SERVER_BASE_URL=https://api.bbuddy.expert/api
NEXT_PUBLIC_AGORA_APPID=ed90c9dc42634e5687d4e2e0766b363f NEXT_PUBLIC_AGORA_APPID=ed90c9dc42634e5687d4e2e0766b363f
NEXT_PUBLIC_CONTENTFUL_SPACE_ID = voxpxjq7y7vf
NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN = s99GWKfpDKkNwiEJ3pN7US_tmqsGvDlaex-sOJwpzuc
NEXT_PUBLIC_CONTENTFUL_PREVIEW_ACCESS_TOKEN = Z9WOKpLDbKNj7xVOmT_VXYNLH0AZwISFvQsq0PQlHfE

View File

@ -1,6 +1,7 @@
// @ts-check // @ts-check
const withNextIntl = require('next-intl/plugin')(); const withNextIntl = require('next-intl/plugin')();
const path = require('path'); const path = require('path');
const json = require('./package.json');
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
@ -14,6 +15,9 @@ const nextConfig = {
sassOptions: { sassOptions: {
includePaths: [path.join(__dirname, 'styles')], includePaths: [path.join(__dirname, 'styles')],
}, },
env: {
version: json.version
},
typescript: { typescript: {
// !! WARN !! // !! WARN !!
// Dangerously allow production builds to successfully complete even if // Dangerously allow production builds to successfully complete even if
@ -30,8 +34,7 @@ const nextConfig = {
}, },
// output: 'standalone', // output: 'standalone',
poweredByHeader: false, poweredByHeader: false,
productionBrowserSourceMaps: true, productionBrowserSourceMaps: true
trailingSlash: true
}; };
module.exports = withNextIntl(nextConfig); module.exports = withNextIntl(nextConfig);

735
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "bbuddy-ui", "name": "bbuddy-ui",
"version": "0.0.1", "version": "0.0.4",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 4200", "dev": "next dev -p 4200",
@ -12,11 +12,13 @@
"@ant-design/cssinjs": "^1.18.1", "@ant-design/cssinjs": "^1.18.1",
"@ant-design/icons": "^5.2.6", "@ant-design/icons": "^5.2.6",
"@ant-design/nextjs-registry": "^1.0.0", "@ant-design/nextjs-registry": "^1.0.0",
"@contentful/rich-text-react-renderer": "^15.22.9",
"agora-rtc-react": "^2.1.0", "agora-rtc-react": "^2.1.0",
"agora-rtc-sdk-ng": "^4.20.2", "agora-rtc-sdk-ng": "^4.20.2",
"antd": "^5.12.1", "antd": "^5.12.1",
"antd-img-crop": "^4.21.0", "antd-img-crop": "^4.21.0",
"axios": "^1.6.5", "axios": "^1.6.5",
"contentful": "^10.13.3",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"next": "14.0.3", "next": "14.0.3",

View File

@ -1,71 +1,40 @@
import React from 'react'; import React from 'react';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { unstable_setRequestLocale } from 'next-intl/server'; import {getTranslations, unstable_setRequestLocale} from 'next-intl/server';
import { i18nText } from '../../../../i18nKeys'; import { i18nText } from '../../../../i18nKeys';
import {fetchBlogPosts} from "../../../../lib/contentful/blogPosts";
import Link from "next/link";
export default function News({ params: { locale } }: { params: { locale: string }}) { export default async function News({params: {locale}}: { params: { locale: string } }) {
unstable_setRequestLocale(locale); unstable_setRequestLocale(locale);
const t = useTranslations('Main'); const t = await getTranslations('Main');
const {data, total} = await fetchBlogPosts({preview: false, sticky: true})
return ( return (
<div className="main-articles"> <div className="main-articles">
<div className="b-inner"> <div className="b-inner">
<h2 className="title-h2">{t('news')}</h2> <h2 className="title-h2">{t('news')}</h2>
<div className="row"> <div className="row">
<div className="col-lg-4 col-md-6 col-sm-6"> {data.map((item, i) => (
<div className="b-article"> <div className="col-lg-4 col-md-6 col-sm-6" key={'news' + i}>
<div className="b-article__image"> <div className="b-article">
<img className="" src="/images/article.png" alt=""/> <div className="b-article__image">
</div> <img className="" src={item.listImage?.src} alt={item.listImage?.alt}/>
<div className="b-article__inner"> </div>
<div className="b-article__title">News Headline</div> <div className="b-article__inner">
<div className="b-article__text"> <div className="b-article__title">{item.title}</div>
The program not only focuses on a financial perspective, but allows you to study <div className="b-article__text">
performance from many angles, such as human resources management, IT, operations {item.excerpt}
management, risks etc. </div>
<Link href={`/${locale}/blog/${item.slug}`} className="b-article__link">
{i18nText('readMore', locale)}
<img className="" src="/images/chevron-forward.svg" alt=""/>
</Link>
</div> </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 className="col-lg-4 d-none d-lg-block">
<div className="b-article">
<div className="b-article__image">
<img className="" src="/images/article.png" alt=""/>
</div>
<div className="b-article__inner">
<div className="b-article__title">News Headline</div>
<div className="b-article__text">
The program not only focuses on a financial perspective, but allows you to study
performance from many angles, such as human resources management, IT, operations
management, risks etc.
</div>
<a href="#" className="b-article__link">{i18nText('readMore', locale)}
<img className="" src="/images/chevron-forward.svg" alt=""/>
</a>
</div>
</div>
</div>
<div className="col-lg-4 d-none d-lg-block">
<div className="b-article">
<div className="b-article__image">
<img className="" src="/images/article.png" alt=""/>
</div>
<div className="b-article__inner">
<div className="b-article__title">News Headline</div>
<div className="b-article__text">
The program not only focuses on a financial perspective, but allows you to study
performance from many angles, such as human resources management, IT, operations
management, risks etc.
</div>
<a href="#" className="b-article__link">{i18nText('readMore', locale)}
<img className="" src="/images/chevron-forward.svg" alt=""/>
</a>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

@ -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 />
Im excited to kick off this series of newsletters where Ill 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, Ive 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 fathers
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 mothers 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 doesnt
scare me as much. Never be afraid to start from zero again!
</p>
</div>
</div>
</div>
);
};

View File

@ -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>
&nbsp;>&nbsp;
<Link href={`/${params.locale}/blog/category/${item.categorySlug}`}>
{item.category}
</Link>
&nbsp;>&nbsp;
<span>
{item.title}
</span>
</nav>
</div>
</div>
);
};

View File

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

View File

@ -1,213 +1,43 @@
import React from 'react'; import React from 'react';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import * as Util from "node:util";
import {fetchBlogPosts} from "../../../lib/contentful/blogPosts";
import {unstable_setRequestLocale} from "next-intl/server";
import Link from "next/link";
import {fetchBlogPostCategories} from "../../../lib/contentful/blogPostsCategories";
import {CustomPagination} from "../../../components/view/CustomPagination";
import {DEFAULT_PAGE_SIZE} from "../../../constants/common";
import {BlogPosts} from "../../../components/BlogPosts/BlogPosts";
export const metadata: Metadata = {
title: 'Bbuddy - Blog',
description: 'Bbuddy desc blog'
};
export default function Blog() { interface BlogPostPageParams {
slug: string
}
interface BlogPostPageProps {
params: BlogPostPageParams
}
export async function generateStaticParams(): Promise<BlogPostPageParams[]> {
const blogPosts = await fetchBlogPosts({ preview: false })
return blogPosts.data.map((post) => ({ slug: post.slug }))
}
export default async function Blog({ params: { locale }, searchParams }: { params: { locale: string }, searhParams?: {page: number} }) {
unstable_setRequestLocale(locale);
const pageSize = DEFAULT_PAGE_SIZE
const page = searchParams.page || undefined
// BlogPosts('/'+locale+'/blog/', locale, pageSize)
return ( return (
<div className="b-news">
<div className="b-news__header"> <BlogPosts
<div className="b-inner"> basePath={'/'+locale+'/blog/'}
<h1 className="title-h1"> locale={locale}
Mentorship, Career <br /> pageSize={pageSize}
Development & Coaching. page={page}
</h1> >
<div className="wrap-text"> </BlogPosts>
<p className="">The ins-and-outs of building a career in tech, gaining <br /> experience</p>
<p className="">from a mentor, and getting your feet wet with coaching.</p>
</div>
<div className="b-news__header__img">
<img className="" src="/images/news-top.png" alt="" />
</div>
</div>
</div>
<div className="b-news__filter ">
<div className="b-inner">
<div className="wrap-filter">
<a href="#" className="filter-item">Leadership & Management</a>
<a href="#" className="filter-item">Professional Development</a>
<a href="#" className="filter-item">Research & Insights</a>
<a href="#" className="filter-item">Well-Being</a>
<a href="#" className="filter-item">Diversity & Inclusion</a>
<a href="#" className="filter-item">Culture</a>
<a href="#" className="filter-item">Sales</a>
<a href="#" className="filter-item">Collaboration</a>
<a href="#" className="filter-item">Hiring</a>
<a href="#" className="filter-item active">BBuddy product</a>
<a href="#" className="filter-item">Customer Stories</a>
<a href="#" className="filter-item">Coaching</a>
</div>
</div>
</div>
<div className="b-news__result-list">
<div className="b-inner">
<div className="news-list">
<a href="#" className="news-item">
<div className="news-item__image">
<img className="" src="/images/news.png" alt="" />
</div>
<div className="news-item__inner">
<div className="">
<div className="news-item__title">
6 learnings from Shivpuri to Silicon Valley
</div>
<div className="news-item__badge">Leadership & Management</div>
<div className="news-item__text">
Im excited to kick off this series of newsletters where Ill 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">
Im excited to kick off this series of newsletters where Ill 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">
Im excited to kick off this series of newsletters where Ill 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">
Im excited to kick off this series of newsletters where Ill 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>
); );
} }

View File

@ -9,7 +9,7 @@ import {
ExpertInformation, ExpertInformation,
ExpertPractice ExpertPractice
} from '../../../../components/Experts/ExpertDetails'; } from '../../../../components/Experts/ExpertDetails';
import { Details } from '../../../../types/experts'; import { Details } from '../../../../types/education';
import { BackButton } from '../../../../components/view/BackButton'; import { BackButton } from '../../../../components/view/BackButton';
import { i18nText } from '../../../../i18nKeys'; import { i18nText } from '../../../../i18nKeys';
@ -36,7 +36,6 @@ export default async function ExpertItem({ params: { expertId = '', locale } }:
if (!expertId) notFound(); if (!expertId) notFound();
const expert = await getExpertById(expertId, locale); const expert = await getExpertById(expertId, locale);
console.log(expert);
const getAssociationLevel = (accLevelId?: number) => { const getAssociationLevel = (accLevelId?: number) => {
if (accLevelId) { if (accLevelId) {
@ -112,7 +111,11 @@ export default async function ExpertItem({ params: { expertId = '', locale } }:
{expert?.publicCoachDetails?.trainings && expert.publicCoachDetails.trainings?.map(generateDescription)} {expert?.publicCoachDetails?.trainings && expert.publicCoachDetails.trainings?.map(generateDescription)}
{expert?.publicCoachDetails?.mbas && expert.publicCoachDetails.mbas?.map(generateDescription)} {expert?.publicCoachDetails?.mbas && expert.publicCoachDetails.mbas?.map(generateDescription)}
{expert?.publicCoachDetails?.experiences && expert.publicCoachDetails.experiences?.map(generateDescription)} {expert?.publicCoachDetails?.experiences && expert.publicCoachDetails.experiences?.map(generateDescription)}
<ExpertPractice expert={expert} locale={locale} /> <ExpertPractice
themes={expert?.publicCoachDetails?.themesGroups}
cases={expert?.publicCoachDetails?.practiceCases}
locale={locale}
/>
{/* <h2 className="title-h2">All Offers by this Expert</h2> {/* <h2 className="title-h2">All Offers by this Expert</h2>
<div className="offers-list"> <div className="offers-list">

View File

@ -1,4 +1,4 @@
import React, { ReactNode } from 'react'; import React, { ReactNode, Suspense } from 'react';
import { Metadata } from 'next'; import { Metadata } from 'next';
import { unstable_setRequestLocale } from 'next-intl/server'; import { unstable_setRequestLocale } from 'next-intl/server';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
@ -6,7 +6,7 @@ import { ConfigProvider } from 'antd';
import { AntdRegistry } from '@ant-design/nextjs-registry'; import { AntdRegistry } from '@ant-design/nextjs-registry';
import theme from '../../constants/theme'; import theme from '../../constants/theme';
import { ALLOWED_LOCALES } from '../../constants/locale'; import { ALLOWED_LOCALES } from '../../constants/locale';
import { Header, Footer } from '../../components/Page'; import { Header, Footer, AppConfig } from '../../components/Page';
type LayoutProps = { type LayoutProps = {
children: ReactNode; children: ReactNode;
@ -30,6 +30,9 @@ export default function LocaleLayout({ children, params: { locale } }: LayoutPro
<AntdRegistry> <AntdRegistry>
<ConfigProvider theme={theme}> <ConfigProvider theme={theme}>
<div className="b-wrapper"> <div className="b-wrapper">
<Suspense fallback={null}>
<AppConfig />
</Suspense>
<div className="b-content"> <div className="b-content">
<Header locale={locale} /> <Header locale={locale} />
{children} {children}

27
src/app/sitemap.jsx Normal file
View File

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

View File

@ -12,9 +12,10 @@ import { validateImage } from '../../utils/account';
import { useProfileSettings } from '../../actions/hooks/useProfileSettings'; import { useProfileSettings } from '../../actions/hooks/useProfileSettings';
import { CustomInput } from '../view/CustomInput'; import { CustomInput } from '../view/CustomInput';
import { OutlinedButton } from '../view/OutlinedButton'; import { OutlinedButton } from '../view/OutlinedButton';
import { FilledYellowButton } from '../view/FilledButton'; import {FilledButton, FilledSquareButton, FilledYellowButton} from '../view/FilledButton';
import { DeleteAccountModal } from '../Modals/DeleteAccountModal'; import { DeleteAccountModal } from '../Modals/DeleteAccountModal';
import { Loader } from '../view/Loader'; import { Loader } from '../view/Loader';
import {ButtonProps} from "antd/es/button/button";
type ProfileSettingsProps = { type ProfileSettingsProps = {
locale: string; locale: string;
@ -40,6 +41,20 @@ export const ProfileSettings: FC<ProfileSettingsProps> = ({ locale }) => {
} }
}, [profileSettings]); }, [profileSettings]);
const onSave = (newProfile: ProfileRequest) => {
setSaveLoading(true);
save(newProfile)
.then(() => {
fetchProfileSettings();
})
.catch(() => {
message.error('Не удалось сохранить изменения');
})
.finally(() => {
setSaveLoading(false);
})
}
const onSaveProfile = () => { const onSaveProfile = () => {
form.validateFields() form.validateFields()
.then(({ login, surname, username }) => { .then(({ login, surname, username }) => {
@ -55,28 +70,19 @@ export const ProfileSettings: FC<ProfileSettingsProps> = ({ locale }) => {
languagesLinks: languagesLinks?.map(({ languageId }) => ({ languageId })) || [] languagesLinks: languagesLinks?.map(({ languageId }) => ({ languageId })) || []
}; };
// if (photo) { if (photo) {
// console.log(photo); const reader = new FileReader();
// const formData = new FormData(); reader.readAsDataURL(photo as File);
// formData.append('file', photo as FileType); reader.onloadend = () => {
// const newReg = new RegExp('data:image/(png|jpg|jpeg);base64,')
// newProfile.faceImage = photo; newProfile.faceImage = reader.result.replace(newReg, '');
// newProfile.isFaceImageKeepExisting = false; newProfile.isFaceImageKeepExisting = false;
// }
console.log(newProfile); onSave(newProfile);
}
setSaveLoading(true); } else {
save(newProfile) onSave(newProfile);
.then(() => { }
fetchProfileSettings();
})
.catch(() => {
message.error('Не удалось сохранить изменения');
})
.finally(() => {
setSaveLoading(false);
})
}) })
} }
@ -99,17 +105,14 @@ export const ProfileSettings: FC<ProfileSettingsProps> = ({ locale }) => {
return ( return (
<Loader isLoading={fetchLoading} refresh={fetchProfileSettings}> <Loader isLoading={fetchLoading} refresh={fetchProfileSettings}>
<Form form={form} className="form-settings"> <Form form={form} className="form-settings">
<div className="user-avatar">
<div className="user-avatar__edit" style={profileSettings?.faceImageUrl ? { backgroundImage: `url(${profileSettings.faceImageUrl})` } : undefined}>
<input className="" type="file" id="input-file" />
<label htmlFor="input-file" className="form-label" />
</div>
<div className="user-avatar__text">{i18nText('photoDesc', locale)}</div>
</div>
<ImgCrop <ImgCrop
modalTitle="Редактировать" modalTitle="Редактировать"
modalOk="Сохранить" modalOk="Сохранить"
modalCancel="Отмена" modalCancel="Отмена"
modalProps={{
okButtonProps: { className: 'b-button__filled_yellow' },
cancelButtonProps: { className: 'b-button__outlined' }
}}
beforeCrop={beforeCrop} beforeCrop={beforeCrop}
> >
<Upload <Upload
@ -121,13 +124,22 @@ export const ProfileSettings: FC<ProfileSettingsProps> = ({ locale }) => {
url: profileSettings.faceImageUrl url: profileSettings.faceImageUrl
} }
] : undefined} ] : undefined}
accept=".jpg,.jpeg,.png,.gif" accept=".jpg,.jpeg,.png"
beforeUpload={beforeUpload} beforeUpload={beforeUpload}
multiple={false} multiple={false}
showUploadList={false} showUploadList={false}
> >
{photo && <img height={100} width={100} src={URL.createObjectURL(photo)} />} <div className="user-avatar">
<Button icon={<CameraOutlined />}>Click to Upload</Button> <div className="user-avatar__edit" style={photo
? { backgroundImage: `url(${URL.createObjectURL(photo)})` }
: profileSettings?.faceImageUrl ? { backgroundImage: `url(${profileSettings.faceImageUrl})`} : undefined }>
<FilledSquareButton
type="primary"
icon={<CameraOutlined style={{ fontSize: 28 }} />}
/>
</div>
<div className="user-avatar__text">{i18nText('photoDesc', locale)}</div>
</div>
</Upload> </Upload>
</ImgCrop> </ImgCrop>
<div className="form-fieldset"> <div className="form-fieldset">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,28 +1,56 @@
import { useState } from 'react';
import { Tag } from 'antd';
import { EditOutlined } from '@ant-design/icons'; import { EditOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import { ScheduleDTO } from '../../../types/schedule'; import { ScheduleDTO } from '../../../types/schedule';
import { i18nText } from '../../../i18nKeys'; import { i18nText } from '../../../i18nKeys';
import { getCurrentTime, getTimeString } from '../../../utils/time';
import { ExpertData } from '../../../types/profile';
import { LinkButton } from '../../view/LinkButton'; import { LinkButton } from '../../view/LinkButton';
import { EditExpertScheduleModal } from '../../Modals/EditExpertScheduleModal';
type ExpertScheduleProps = { type ExpertScheduleProps = {
locale: string; locale: string;
data?: ScheduleDTO; data?: ScheduleDTO;
updateExpert: (key: keyof ExpertData) => void;
}; };
export const ExpertSchedule = ({ locale, data }: ExpertScheduleProps) => { export const ExpertSchedule = ({ locale, data, updateExpert }: ExpertScheduleProps) => {
const [showEdit, setShowEdit] = useState<boolean>(false);
return ( return (
<div className="coaching-section__wrap"> <div className="coaching-section__wrap">
<div className="coaching-section"> <div className="coaching-section">
<div className="coaching-section__title"> <div className="coaching-section__title">
<h2 className="title-h2">Schedule - person51</h2> <h2 className="title-h2">{i18nText('schedule', locale)}</h2>
<LinkButton <LinkButton
type="link" type="link"
icon={<EditOutlined />} icon={<EditOutlined />}
onClick={() => setShowEdit(true)}
/> />
</div> </div>
<div className="base-text"> <div className="b-schedule-list">
Schedule {data && data?.workingTimes?.map((date, index) => {
const { startDay, startTimeMin, endTimeMin } = getCurrentTime(date, dayjs().format('Z'));
return (
<div key={`date_${index}`}>
<Tag className="skills__list__item">{i18nText(startDay, locale)}</Tag>
<div>{startTimeMin ? getTimeString(startTimeMin) : '00:00'}</div>
<span>-</span>
<div>{endTimeMin ? getTimeString(endTimeMin) : '00:00'}</div>
</div>
)
})}
</div> </div>
</div> </div>
<EditExpertScheduleModal
open={showEdit}
handleCancel={() => setShowEdit(false)}
locale={locale}
data={data}
refresh={() => updateExpert('schedule')}
/>
</div> </div>
); );
}; };

View File

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

View File

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

View File

@ -114,6 +114,7 @@ export const ExpertsFilter = ({
...getObjectByAdditionalFilter(searchParams) ...getObjectByAdditionalFilter(searchParams)
}; };
const search = getSearchParamsString(newFilter); const search = getSearchParamsString(newFilter);
console.log('basePath', basePath);
router.push(search ? `${basePath}?${search}#filter` : `${basePath}#filter`); router.push(search ? `${basePath}?${search}#filter` : `${basePath}#filter`);
@ -162,18 +163,15 @@ export const ExpertsFilter = ({
), [filter, searchParams, searchData]); ), [filter, searchParams, searchData]);
const getLangList = () => { const getLangList = () => {
const reg = searchLang ? new RegExp(searchLang, 'ig') : ''; const langList = searchLang ? (languages || []).filter(({ code, nativeSpelling }) => code.indexOf(searchLang) !== -1 || nativeSpelling.indexOf(searchLang) !== -1) : languages;
const langList = reg ? (languages || []).filter(({ code, nativeSpelling }) => reg.test(code) || reg.test(nativeSpelling)) : languages;
return langList?.length return langList?.length
? getList('userLanguages', langList.map(({ code, nativeSpelling }) => ({ id: code, name: nativeSpelling }))) ? getList('userLanguages', langList.map(({ code, nativeSpelling }) => ({ id: code, name: nativeSpelling })))
: null; : null;
}; };
const getTagsList = () => { const getTagsList = () => {
const reg = searchTags ? new RegExp(searchTags, 'ig') : ''; if (searchTags) {
const tagsList = filteredTags.filter(({ name, group }) => name.indexOf(searchTags) !== -1 || group.indexOf(searchTags) !== -1);
if (reg) {
const tagsList = filteredTags.filter(({ name, group }) => reg.test(name) || reg.test(group));
return getList('themesTagIds', tagsList); return getList('themesTagIds', tagsList);
} }

View File

@ -0,0 +1,254 @@
'use client';
import React, { FC, useEffect, useState } from 'react';
import { Modal, Button, message, Form, Input } from 'antd';
import { CloseOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons';
import { i18nText } from '../../i18nKeys';
import { ProfileData, ProfileRequest } from '../../types/profile';
import { PracticePersonData, PracticeDTO, PracticeData, PracticeCase } from '../../types/practice';
import { AUTH_TOKEN_KEY } from '../../constants/common';
import { useLocalStorage } from '../../hooks/useLocalStorage';
import { setPersonData, setPractice } from '../../actions/profile';
import { CustomInput } from '../view/CustomInput';
import { CustomMultiSelect } from '../view/CustomMultiSelect';
import { CustomSelect } from '../view/CustomSelect';
import { LinkButton } from '../view/LinkButton';
type EditExpertAboutModalProps = {
open: boolean;
handleCancel: () => void;
locale: string;
practice?: PracticeDTO;
person?: ProfileData;
refreshPractice: () => void;
refreshPerson: () => void;
};
type FormPerson = PracticePersonData & {
sessionLang: number[];
};
export const EditExpertAboutModal: FC<EditExpertAboutModalProps> = ({
open,
handleCancel,
locale,
practice,
person,
refreshPerson,
refreshPractice
}) => {
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
const [loading, setLoading] = useState<boolean>(false);
const [form] = Form.useForm<FormPerson>();
const [practiceCases, setPracticeCases] = useState<PracticeCase[]>([]);
useEffect(() => {
if (open) {
if (practice?.person4Data) {
form.setFieldsValue(practice.person4Data);
setPracticeCases(practice.person4Data?.practiceCases || []);
}
if (person?.languagesLinks) {
form.setFieldValue('sessionLang', person.languagesLinks.map(({ languageId }) => languageId))
}
}
}, [open, practice?.person4Data]);
const addPracticeCase = () => {
setPracticeCases([
...practiceCases,
{
description: '',
themesGroupIds: []
}
]);
};
const deletePracticeCase = (index: number) => {
setPracticeCases([...practiceCases].filter((cases, i) => i !== index));
};
const onChangePracticeDescription = (value: string, index: number) => {
setPracticeCases(practiceCases.map((cases, i) => {
if (i === index) {
return {
...cases,
description: value
}
}
return cases;
}));
};
const onChangePracticeThemes = (value: number[], index: number) => {
setPracticeCases(practiceCases.map((cases, i) => {
if (i === index) {
return {
...cases,
themesGroupIds: value
}
}
return cases;
}));
};
const onSave = () => {
form.validateFields().then((values) => {
const newPersonData: ProfileRequest = {
login: person?.login,
isPasswordKeepExisting: true,
username: person?.username,
surname: person?.surname,
isFaceImageKeepExisting: true,
phone: person?.phone,
languagesLinks: values?.sessionLang?.map((id) => ({ languageId: +id })) || []
};
const newPracticeData: PracticeData = {
practiceHours: values?.practiceHours,
supervisionPerYearId: values?.supervisionPerYearId,
sessionDuration: values?.sessionDuration ? (isNaN(Number(values.sessionDuration)) ? 0 : Number(values.sessionDuration)) : 0,
sessionCost: values?.sessionCost ? (isNaN(Number(values.sessionCost)) ? 0 : Number(values.sessionCost)) : 0,
practiceCases: practiceCases ? practiceCases : practice?.person4Data?.practiceCases
}
setLoading(true);
Promise.all([
setPractice(locale, jwt, newPracticeData),
setPersonData(newPersonData, locale, jwt)
])
.then(() => {
handleCancel();
refreshPractice();
refreshPerson();
})
.catch(() => {
message.error('Не удалось сохранить данные');
})
.finally(() => {
setLoading(false);
})
})
};
return (
<Modal
className="b-modal"
open={open}
title={undefined}
onOk={undefined}
onCancel={handleCancel}
footer={false}
width={498}
closeIcon={<CloseOutlined style={{ fontSize: 20, color: '#000' }}/>}
>
<div className="b-modal__expert__content">
<div className="b-modal__expert__title">{i18nText('aboutCoach', locale)}</div>
<div className="b-modal__expert__inner">
<Form form={form} style={{ width: '100%', display: 'flex', gap: 16, flexDirection: 'column' }}>
<Form.Item
name="sessionLang"
noStyle
>
<CustomMultiSelect
label={i18nText('sessionLang', locale)}
options={person?.allLanguages?.map(({ id, nativeSpelling }) => ({ value: id, label: nativeSpelling })) || []}
/>
</Form.Item>
<Form.Item
name="sessionDuration"
noStyle
>
<CustomInput
size="small"
placeholder={i18nText('sessionDuration', locale)}
autoComplete="off"
addonAfter={locale === 'ru' ? 'мин' : 'min'}
/>
</Form.Item>
<Form.Item
name="sessionCost"
noStyle
>
<CustomSelect
label={i18nText('sessionCost', locale)}
options={practice?.person4Data?.sessionCosts?.map((cost) => ({ value: cost, label: cost })) || []}
/>
</Form.Item>
<Form.Item
name="practiceHours"
noStyle
>
<CustomInput
size="small"
placeholder={i18nText('experienceHours', locale)}
autoComplete="off"
addonAfter={locale === 'ru' ? 'часов' : 'hours'}
/>
</Form.Item>
<Form.Item
name="supervisionPerYearId"
noStyle
>
<CustomSelect
label={i18nText('supervisionCount', locale)}
options={practice?.person4Data?.supervisionPerYears?.map(({ id, name }) => ({ value: id, label: name })) || []}
/>
</Form.Item>
</Form>
</div>
<div className="b-practice-cases">
<div className="b-practice-case__header">
<div>{i18nText('successfulCase', locale)}</div>
<LinkButton
type="link"
icon={<PlusOutlined />}
onClick={addPracticeCase}
/>
</div>
{practiceCases.map(({ description, themesGroupIds }, index) => (
<div key={index} className="b-practice-case__item">
<div className="b-practice-case__content">
<div>
<CustomMultiSelect
value={themesGroupIds || []}
label={i18nText('topics', locale)}
options={practice?.person4Data?.themesGroups?.map(({ id, name }) => ({ value: id, label: name })) || []}
onChange={(val) => onChangePracticeThemes(val, index)}
/>
</div>
<div>
<Input.TextArea
value={description}
className="b-textarea"
rows={2}
placeholder={i18nText('description', locale)}
onChange={(e) => onChangePracticeDescription(e.target.value, index)}
/>
</div>
</div>
<LinkButton
type="link"
danger
icon={<DeleteOutlined />}
onClick={() => deletePracticeCase(index)}
/>
</div>
))}
</div>
<div className="b-modal__expert__button">
<Button
className="card-detail__apply"
onClick={onSave}
loading={loading}
>
{i18nText('save', locale)}
</Button>
</div>
</div>
</Modal>
);
};

View File

@ -0,0 +1,152 @@
'use client';
import React, { FC, useEffect, useState } from 'react';
import {Modal, Button, message, Form, Collapse, GetProp, UploadProps} from 'antd';
import type { CollapseProps } from 'antd';
import { CloseOutlined } from '@ant-design/icons';
import { i18nText } from '../../i18nKeys';
import { PracticePersonData, PracticeDTO, PracticeData, PracticeCase } from '../../types/practice';
import { AUTH_TOKEN_KEY } from '../../constants/common';
import { useLocalStorage } from '../../hooks/useLocalStorage';
import {setEducation} from '../../actions/profile';
import {Certificate, Details, EducationData, EducationDTO, Experience} from "../../types/education";
import {CertificatesContent} from "./educationModalContent/Certificates";
import {EducationsContent} from "./educationModalContent/Educations";
import {TrainingsContent} from "./educationModalContent/Trainings";
import {MbasContent} from "./educationModalContent/Mbas";
import {ExperiencesContent} from "./educationModalContent/Experiences";
type EditExpertEducationModalProps = {
open: boolean;
handleCancel: () => void;
locale: string;
data?: EducationDTO;
refresh: () => void;
};
type FormPerson = PracticePersonData & {
sessionLang: number[];
};
export const EditExpertEducationModal: FC<EditExpertEducationModalProps> = ({
open,
handleCancel,
locale,
data,
refresh
}) => {
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
const [loading, setLoading] = useState<boolean>(false);
const [form] = Form.useForm<FormPerson>();
const [editedData, setEditedData] = useState<EducationData>(data?.person2Data as EducationData);
const onSave = () => {
setLoading(true);
setEducation(locale, jwt, editedData)
.then(() => {
handleCancel();
refresh();
})
.catch(() => {
message.error('Не удалось сохранить образование');
})
.finally(() => {
setLoading(false);
})
};
const items: CollapseProps['items'] = [
{
key: 'certificates',
label: i18nText('profCertification', locale),
children: (
<CertificatesContent
certificates={editedData?.certificates}
update={(certificates) => setEditedData({ ...editedData, certificates })}
locale={locale}
associationLevels={data?.associationLevels}
associations={data?.associations}
/>
),
},
{
key: 'educations',
label: i18nText('education', locale),
children: (
<EducationsContent
educations={editedData?.educations}
update={(educations) => setEditedData({ ...editedData, educations })}
locale={locale}
/>
),
},
{
key: 'trainings',
label: `${i18nText('trainings', locale)} | ${i18nText('seminars', locale)} | ${i18nText('courses', locale)}`,
children: (
<TrainingsContent
trainings={editedData?.trainings}
update={(trainings) => setEditedData({ ...editedData, trainings })}
locale={locale}
/>
),
},
{
key: 'mbas',
label: i18nText('mba', locale),
children: (
<MbasContent
mbas={editedData?.mbas}
update={(mbas) => setEditedData({ ...editedData, mbas })}
locale={locale}
/>
),
},
{
key: 'experiences',
label: i18nText('mExperiences', locale),
children: (
<ExperiencesContent
experiences={editedData?.experiences}
update={(experiences) => setEditedData({ ...editedData, experiences })}
locale={locale}
/>
),
},
];
return (
<Modal
className="b-modal"
open={open}
title={undefined}
onOk={undefined}
onCancel={handleCancel}
footer={false}
width={498}
closeIcon={<CloseOutlined style={{ fontSize: 20, color: '#000' }}/>}
>
<div className="b-modal__expert__content">
<div className="b-modal__expert__title">{i18nText('skillsInfo', locale)}</div>
<div className="b-modal__expert__inner" style={{ paddingRight: 12 }}>
<Form form={form} style={{ width: '100%' }}>
<Collapse
ghost
expandIconPosition="end"
items={items}
/>
</Form>
</div>
<div className="b-modal__expert__button">
<Button
className="card-detail__apply"
onClick={onSave}
loading={loading}
>
{i18nText('save', locale)}
</Button>
</div>
</div>
</Modal>
);
};

View File

@ -0,0 +1,118 @@
'use client';
import React, { FC, useEffect, useState } from 'react';
import { Modal, Button, message, Form } from 'antd';
import { CloseOutlined } from '@ant-design/icons';
import { i18nText } from '../../i18nKeys';
import { PayInfo } from '../../types/profile';
import { AUTH_TOKEN_KEY } from '../../constants/common';
import { useLocalStorage } from '../../hooks/useLocalStorage';
import { setPayData } from '../../actions/profile';
import { CustomInput } from '../view/CustomInput';
type EditExpertPayDataModalProps = {
open: boolean;
handleCancel: () => void;
locale: string;
data?: PayInfo;
refresh: () => void;
};
export const EditExpertPayDataModal: FC<EditExpertPayDataModalProps> = ({
open,
handleCancel,
locale,
data,
refresh
}) => {
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
const [loading, setLoading] = useState<boolean>(false);
const [form] = Form.useForm<PayInfo>();
useEffect(() => {
if (open) {
if (data) {
form.setFieldsValue(data);
} else {
form.resetFields();
}
}
}, [open, data]);
const onSavePayData = () => {
form.validateFields().then(({ beneficiaryName, bicOrSwift, iban }) => {
setLoading(true);
setPayData(locale, jwt, { beneficiaryName, bicOrSwift, iban })
.then(() => {
handleCancel();
refresh();
})
.catch(() => {
message.error('Не удалось сохранить платежную информацию');
})
.finally(() => {
setLoading(false);
})
})
};
return (
<Modal
className="b-modal"
open={open}
title={undefined}
onOk={undefined}
onCancel={handleCancel}
footer={false}
width={498}
closeIcon={<CloseOutlined style={{ fontSize: 20, color: '#000' }}/>}
>
<div className="b-modal__expert__content">
<div className="b-modal__expert__title">{i18nText('payInfo', locale)}</div>
<div className="b-modal__expert__inner">
<Form form={form} style={{ width: '100%', display: 'flex', gap: 16, flexDirection: 'column' }}>
<Form.Item
name="beneficiaryName"
noStyle
>
<CustomInput
size="small"
placeholder={i18nText('beneficiaryName', locale)}
autoComplete="off"
/>
</Form.Item>
<Form.Item
name="bicOrSwift"
noStyle
>
<CustomInput
size="small"
placeholder={i18nText('bicOrSwift', locale)}
autoComplete="off"
/>
</Form.Item>
<Form.Item
name="iban"
noStyle
>
<CustomInput
size="small"
placeholder="IBAN"
autoComplete="off"
/>
</Form.Item>
</Form>
</div>
<div className="b-modal__expert__button">
<Button
className="card-detail__apply"
onClick={onSavePayData}
loading={loading}
>
{i18nText('save', locale)}
</Button>
</div>
</div>
</Modal>
);
};

View File

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

View File

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

View File

@ -0,0 +1,205 @@
import { Upload, UploadFile } from 'antd';
import { DeleteOutlined } from '@ant-design/icons';
import { Association, AssociationLevel, Certificate } from '../../../types/education';
import { CustomSelect } from '../../view/CustomSelect';
import { LinkButton } from '../../view/LinkButton';
import { OutlinedButton } from '../../view/OutlinedButton';
import { i18nText } from '../../../i18nKeys';
import { validateDoc } from '../../../utils/account';
import { useLocalStorage } from '../../../hooks/useLocalStorage';
import { AUTH_TOKEN_KEY } from '../../../constants/common';
type CertificatesContentProps = {
certificates?: Certificate[];
update: (cert?: Certificate[]) => void;
associations?: Association[];
associationLevels?: AssociationLevel[];
locale: string;
};
export const CertificatesContent = ({
certificates,
update,
associations,
associationLevels,
locale
}: CertificatesContentProps) => {
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
const addCertificate = () => {
const cert = {
associationLevelId: undefined,
document: null
};
update(certificates?.length > 0
? [
...certificates,
cert
]
: [cert]);
};
const deleteCertificate = (index: number) => {
update([...certificates].filter((cert, i) => i !== index));
};
const beforeUpload = (file: UploadFile) => {
const isValid = validateDoc(file);
if (!isValid) {
return Upload.LIST_IGNORE;
}
return true;
}
const onRemoveFile = (index: number) => {
update(certificates?.map((cert, i) => {
if (i === index) {
return {
...cert,
document: null,
}
}
return cert;
}));
};
const onChangeAssociation = (val: number, index: number) => {
update(certificates?.map((cert, i) => {
if (i === index) {
return {
...cert,
associationId: val,
associationLevelId: undefined
}
}
return cert;
}));
};
const onChangeLevel = (val: number, index: number) => {
update(certificates?.map((cert, i) => {
if (i === index) {
return {
...cert,
associationLevelId: val
}
}
return cert;
}));
};
const onChange = (file: any, index: number) => {
if (file?.response) {
update([...certificates].map((cert, i) => {
if (i === index) {
return {
...cert,
document: file?.response || null,
}
}
return cert;
}));
}
};
return (
<div className="b-edu-content">
<div className="b-edu-list">
{certificates?.map(({ associationId, associationLevelId, document: file }, index) => {
let cAssociationId = associationId;
if (!cAssociationId) {
const [cAssLvl] = associationLevels ? associationLevels.filter(({ id }) => id === associationLevelId) : [];
if (cAssLvl?.associationId) {
cAssociationId = associations ? associations.filter(({ id }) => id === cAssLvl.associationId)[0]?.id : undefined;
}
}
return (
<div className="b-edu-list__item" key={`cert_${index}`}>
<div>
<CustomSelect
value={cAssociationId}
label={i18nText('association', locale)}
options={associations?.map(({ id, name }) => ({ value: id, label: name })) || []}
onChange={(val) => onChangeAssociation(val, index)}
style={{ maxWidth: 320, minWidth: 320 }}
/>
<CustomSelect
value={associationLevelId}
label={i18nText('level', locale)}
options={associationLevels && associationLevels.length > 0
? associationLevels
.filter(({ associationId }) => associationId === cAssociationId)
.map(({ id, name }) => ({ value: id, label: name }))
: []}
onChange={(val) => onChangeLevel(val, index)}
/>
{/*<Upload
fileList={tmpFile ? [tmpFile] : file ? [
{
uid: file.original?.id,
name: file.fileName,
status: 'done',
url: file.original?.url
}
] : undefined}
accept=".jpg,.jpeg,.png,.pdf"
beforeUpload={(file) => beforeUpload(file as UploadFile, index)}
multiple={false}
onRemove={() => onRemoveFile(index)}
>
<LinkButton type="link">{i18nText('addDiploma', locale)}</LinkButton>
</Upload>*/}
<Upload
fileList={file ? [
{
uid: file.original?.id,
name: file.fileName,
status: 'done',
url: file.original?.url
}
] : undefined}
accept=".jpg,.jpeg,.png,.pdf"
beforeUpload={beforeUpload}
multiple={false}
onRemove={() => onRemoveFile(index)}
action="https://api.bbuddy.expert/api/home/uploadfile"
method="POST"
headers={{
authorization: `Bearer ${jwt}`,
'X-User-Language': locale,
'X-Referrer-Channel': 'site',
}}
onChange={(obj) => onChange(obj.file, index)}
>
<LinkButton type="link">{i18nText('addDiploma', locale)}</LinkButton>
</Upload>
</div>
<LinkButton
type="link"
danger
icon={<DeleteOutlined />}
onClick={() => deleteCertificate(index)}
/>
</div>
)
})}
</div>
<OutlinedButton
type="link"
onClick={addCertificate}
>
{i18nText('addNew', locale)}
</OutlinedButton>
</div>
);
};

View File

@ -0,0 +1,166 @@
import { DeleteOutlined } from '@ant-design/icons';
import { CustomInput } from '../../view/CustomInput';
import { LinkButton } from '../../view/LinkButton';
import { OutlinedButton } from '../../view/OutlinedButton';
import { Details } from '../../../types/education';
import { i18nText } from '../../../i18nKeys';
import { Upload, UploadFile } from 'antd';
import { validateDoc } from '../../../utils/account';
import { useLocalStorage } from '../../../hooks/useLocalStorage';
import { AUTH_TOKEN_KEY } from '../../../constants/common';
type EducationsContentProps = {
educations?: Details[];
update: (edu?: Details[]) => void;
locale: string;
};
export const EducationsContent = ({
educations,
update,
locale
}: EducationsContentProps) => {
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
const addEdu = () => {
const edu = {
title: undefined,
description: undefined,
document: null
};
update(educations?.length > 0
? [
...educations,
edu
]
: [edu]);
};
const deleteEdu = (index: number) => {
update([...educations].filter((ed, i) => i !== index));
};
const beforeUpload = (file: UploadFile) => {
const isValid = validateDoc(file);
if (!isValid) {
return Upload.LIST_IGNORE;
}
return true;
}
const onRemoveFile = (index: number) => {
update(educations?.map((edu, i) => {
if (i === index) {
return {
...edu,
document: null,
}
}
return edu;
}));
};
const onChange = (file: any, index: number) => {
if (file?.response) {
update([...educations].map((edu, i) => {
if (i === index) {
return {
...edu,
document: file?.response || null,
}
}
return edu;
}));
}
};
const onChangeUniversity = (val: string, index: number) => {
update(educations?.map((edu, i) => {
if (i === index) {
return {
...edu,
title: val,
}
}
return edu;
}));
};
const onChangeDesc = (val: string, index: number) => {
update(educations?.map((edu, i) => {
if (i === index) {
return {
...edu,
description: val,
}
}
return edu;
}));
};
return (
<div className="b-edu-content">
<div className="b-edu-list">
{educations?.map(({ title, description, document: file}, index) => (
<div className="b-edu-list__item" key={`edu_${index}`}>
<div>
<CustomInput
value={title}
placeholder={i18nText('university', locale)}
onChange={(e) => onChangeUniversity(e?.target?.value, index)}
/>
<CustomInput
value={description}
placeholder={i18nText('description', locale)}
onChange={(e) => onChangeDesc(e?.target?.value, index)}
/>
<Upload
fileList={file ? [
{
uid: file.original?.id,
name: file.fileName,
status: 'done',
url: file.original?.url
}
] : undefined}
accept=".jpg,.jpeg,.png,.pdf"
beforeUpload={beforeUpload}
multiple={false}
onRemove={() => onRemoveFile(index)}
action="https://api.bbuddy.expert/api/home/uploadfile"
method="POST"
headers={{
authorization: `Bearer ${jwt}`,
'X-User-Language': locale,
'X-Referrer-Channel': 'site',
}}
onChange={(obj) => onChange(obj.file, index)}
>
<LinkButton type="link">{i18nText('addDiploma', locale)}</LinkButton>
</Upload>
</div>
<LinkButton
type="link"
danger
icon={<DeleteOutlined />}
onClick={() => deleteEdu(index)}
/>
</div>
))}
</div>
<OutlinedButton
type="link"
onClick={addEdu}
>
{i18nText('addNew', locale)}
</OutlinedButton>
</div>
);
};

View File

@ -0,0 +1,101 @@
import { DeleteOutlined } from '@ant-design/icons';
import { CustomInput } from '../../view/CustomInput';
import { LinkButton } from '../../view/LinkButton';
import { OutlinedButton } from '../../view/OutlinedButton';
import { Experience } from '../../../types/education';
import { i18nText } from '../../../i18nKeys';
import {useLocalStorage} from "../../../hooks/useLocalStorage";
import {AUTH_TOKEN_KEY} from "../../../constants/common";
type ExperiencesContentProps = {
experiences?: Experience[];
update: (ex?: Experience[]) => void;
locale: string;
};
export const ExperiencesContent = ({
experiences,
update,
locale
}: ExperiencesContentProps) => {
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
const addExperience = () => {
const ex = {
title: undefined,
description: undefined,
};
update(experiences?.length > 0
? [
...experiences,
ex
]
: [ex]);
};
const deleteExperience = (index: number) => {
update([...experiences].filter((ex, i) => i !== index));
};
const onChangeName = (val: string, index: number) => {
update(experiences?.map((ex, i) => {
if (i === index) {
return {
...ex,
title: val,
}
}
return ex;
}));
};
const onChangeDesc = (val: string, index: number) => {
update(experiences?.map((ex, i) => {
if (i === index) {
return {
...ex,
description: val,
}
}
return ex;
}));
};
return (
<div className="b-edu-content">
<div className="b-edu-list">
{experiences?.map(({ title, description}, index) => (
<div className="b-edu-list__item" key={`ex_${index}`}>
<div>
<CustomInput
value={title}
placeholder={i18nText('name', locale)}
onChange={(e) => onChangeName(e?.target?.value, index)}
/>
<CustomInput
value={description}
placeholder={i18nText('description', locale)}
onChange={(e) => onChangeDesc(e?.target?.value, index)}
/>
</div>
<LinkButton
type="link"
danger
icon={<DeleteOutlined />}
onClick={() => deleteExperience(index)}
/>
</div>
))}
</div>
<OutlinedButton
type="link"
onClick={addExperience}
>
{i18nText('addNew', locale)}
</OutlinedButton>
</div>
);
};

View File

@ -0,0 +1,166 @@
import { DeleteOutlined } from '@ant-design/icons';
import { CustomInput } from '../../view/CustomInput';
import { LinkButton } from '../../view/LinkButton';
import { OutlinedButton } from '../../view/OutlinedButton';
import { Details } from '../../../types/education';
import { i18nText } from '../../../i18nKeys';
import { Upload, UploadFile } from 'antd';
import { validateDoc } from '../../../utils/account';
import { useLocalStorage } from '../../../hooks/useLocalStorage';
import { AUTH_TOKEN_KEY } from '../../../constants/common';
type MbasContentProps = {
mbas?: Details[];
update: (mba?: Details[]) => void;
locale: string;
};
export const MbasContent = ({
mbas,
update,
locale
}: MbasContentProps) => {
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
const addMba = () => {
const mba = {
title: undefined,
description: undefined,
document: null
};
update(mbas?.length > 0
? [
...mbas,
mba
]
: [mba]);
};
const deleteMba = (index: number) => {
update([...mbas].filter((mba, i) => i !== index));
};
const beforeUpload = (file: UploadFile) => {
const isValid = validateDoc(file);
if (!isValid) {
return Upload.LIST_IGNORE;
}
return true;
}
const onRemoveFile = (index: number) => {
update(mbas?.map((mb, i) => {
if (i === index) {
return {
...mb,
document: null,
}
}
return mb;
}));
};
const onChange = (file: any, index: number) => {
if (file?.response) {
update([...mbas].map((mb, i) => {
if (i === index) {
return {
...mb,
document: file?.response || null,
}
}
return mb;
}));
}
};
const onChangeName = (val: string, index: number) => {
update(mbas?.map((mb, i) => {
if (i === index) {
return {
...mb,
title: val,
}
}
return mb;
}));
};
const onChangeDesc = (val: string, index: number) => {
update(mbas?.map((mb, i) => {
if (i === index) {
return {
...mb,
description: val,
}
}
return mb;
}));
};
return (
<div className="b-edu-content">
<div className="b-edu-list">
{mbas?.map(({ title, description, document: file}, index) => (
<div className="b-edu-list__item" key={`mba_${index}`}>
<div>
<CustomInput
value={title}
placeholder={i18nText('university', locale)}
onChange={(e) => onChangeName(e?.target?.value, index)}
/>
<CustomInput
value={description}
placeholder={i18nText('description', locale)}
onChange={(e) => onChangeDesc(e?.target?.value, index)}
/>
<Upload
fileList={file ? [
{
uid: file.original?.id,
name: file.fileName,
status: 'done',
url: file.original?.url
}
] : undefined}
accept=".jpg,.jpeg,.png,.pdf"
beforeUpload={beforeUpload}
multiple={false}
onRemove={() => onRemoveFile(index)}
action="https://api.bbuddy.expert/api/home/uploadfile"
method="POST"
headers={{
authorization: `Bearer ${jwt}`,
'X-User-Language': locale,
'X-Referrer-Channel': 'site',
}}
onChange={(obj) => onChange(obj.file, index)}
>
<LinkButton type="link">{i18nText('addDiploma', locale)}</LinkButton>
</Upload>
</div>
<LinkButton
type="link"
danger
icon={<DeleteOutlined />}
onClick={() => deleteMba(index)}
/>
</div>
))}
</div>
<OutlinedButton
type="link"
onClick={addMba}
>
{i18nText('addNew', locale)}
</OutlinedButton>
</div>
);
};

View File

@ -0,0 +1,166 @@
import { DeleteOutlined } from '@ant-design/icons';
import { CustomInput } from '../../view/CustomInput';
import { LinkButton } from '../../view/LinkButton';
import { OutlinedButton } from '../../view/OutlinedButton';
import { Details } from '../../../types/education';
import { i18nText } from '../../../i18nKeys';
import { Upload, UploadFile } from 'antd';
import { validateDoc } from '../../../utils/account';
import { useLocalStorage } from '../../../hooks/useLocalStorage';
import { AUTH_TOKEN_KEY } from '../../../constants/common';
type TrainingsContentProps = {
trainings?: Details[];
update: (tr?: Details[]) => void;
locale: string;
};
export const TrainingsContent = ({
trainings,
update,
locale
}: TrainingsContentProps) => {
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
const addTrainings = () => {
const training = {
title: undefined,
description: undefined,
document: null
};
update(trainings?.length > 0
? [
...trainings,
training
]
: [training]);
};
const deleteTrainings = (index: number) => {
update([...trainings].filter((tr, i) => i !== index));
};
const beforeUpload = (file: UploadFile) => {
const isValid = validateDoc(file);
if (!isValid) {
return Upload.LIST_IGNORE;
}
return true;
}
const onRemoveFile = (index: number) => {
update(trainings?.map((tr, i) => {
if (i === index) {
return {
...tr,
document: null,
}
}
return tr;
}));
};
const onChange = (file: any, index: number) => {
if (file?.response) {
update([...trainings].map((tr, i) => {
if (i === index) {
return {
...tr,
document: file?.response || null,
}
}
return tr;
}));
}
};
const onChangeName = (val: string, index: number) => {
update(trainings?.map((tr, i) => {
if (i === index) {
return {
...tr,
title: val,
}
}
return tr;
}));
};
const onChangeDesc = (val: string, index: number) => {
update(trainings?.map((tr, i) => {
if (i === index) {
return {
...tr,
description: val,
}
}
return tr;
}));
};
return (
<div className="b-edu-content">
<div className="b-edu-list">
{trainings?.map(({ title, description, document: file}, index) => (
<div className="b-edu-list__item" key={`training_${index}`}>
<div>
<CustomInput
value={title}
placeholder={i18nText('name', locale)}
onChange={(e) => onChangeName(e?.target?.value, index)}
/>
<CustomInput
value={description}
placeholder={i18nText('description', locale)}
onChange={(e) => onChangeDesc(e?.target?.value, index)}
/>
<Upload
fileList={file ? [
{
uid: file.original?.id,
name: file.fileName,
status: 'done',
url: file.original?.url
}
] : undefined}
accept=".jpg,.jpeg,.png,.pdf"
beforeUpload={beforeUpload}
multiple={false}
onRemove={() => onRemoveFile(index)}
action="https://api.bbuddy.expert/api/home/uploadfile"
method="POST"
headers={{
authorization: `Bearer ${jwt}`,
'X-User-Language': locale,
'X-Referrer-Channel': 'site',
}}
onChange={(obj) => onChange(obj.file, index)}
>
<LinkButton type="link">{i18nText('addDiploma', locale)}</LinkButton>
</Upload>
</div>
<LinkButton
type="link"
danger
icon={<DeleteOutlined />}
onClick={() => deleteTrainings(index)}
/>
</div>
))}
</div>
<OutlinedButton
type="link"
onClick={addTrainings}
>
{i18nText('addNew', locale)}
</OutlinedButton>
</div>
);
};

View File

@ -0,0 +1,11 @@
'use client'
import { useEffect } from 'react';
export const AppConfig = () => {
useEffect(() => {
console.log('AppVersion', process.env.version);
}, []);
return null;
};

View File

@ -1,3 +1,4 @@
export * from './Header'; export * from './Header';
export * from './Footer'; export * from './Footer';
export * from './GeneralTopSection'; export * from './GeneralTopSection';
export * from './AppConfig';

View File

@ -30,7 +30,9 @@ export const CustomMultiSelect = (props: any) => {
return ( return (
<div className={`b-multiselect-wrap ${isActiveLabel ? 'b-multiselect__active' : ''}`}> <div className={`b-multiselect-wrap ${isActiveLabel ? 'b-multiselect__active' : ''}`}>
<div className="b-multiselect-label">{label}</div> <div className="b-multiselect-label">
<span>{label}</span>
</div>
<Select <Select
className="b-multiselect" className="b-multiselect"
mode="multiple" mode="multiple"

View File

@ -4,7 +4,7 @@ import React, { useEffect, useState } from 'react';
import { Select } from 'antd'; import { Select } from 'antd';
export const CustomSelect = (props: any) => { export const CustomSelect = (props: any) => {
const { label, value, ...other } = props; const { label, value, style, ...other } = props;
const [isActiveLabel, setIsActiveLabel] = useState<boolean>(false); const [isActiveLabel, setIsActiveLabel] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
@ -16,8 +16,10 @@ export const CustomSelect = (props: any) => {
}, [value]); }, [value]);
return ( return (
<div className={`b-select-wrap ${isActiveLabel ? 'b-select__active' : ''}`}> <div className={`b-select-wrap ${isActiveLabel ? 'b-select__active' : ''}`} style={style}>
<div className="b-select-label">{label}</div> <div className="b-select-label">
<span>{label}</span>
</div>
<Select <Select
className="b-select" className="b-select"
value={value} value={value}

View File

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

View File

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

View File

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

37
src/constants/time.ts Normal file
View File

@ -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'
];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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),
}
}

View File

@ -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])
}

View File

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

View File

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

View File

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

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

@ -0,0 +1,35 @@
.b-edu {
&-content {
display: flex;
flex-direction: column;
gap: 16px;
}
&-list {
display: flex;
flex-direction: column;
gap: 12px;
width: 100%;
&__item {
padding-top: 12px;
border-top: 1px solid #C4DFE6;
display: flex;
gap: 8px;
justify-content: space-between;
align-items: flex-start;
&:first-child {
padding-top: 0;
border-top: none;
}
& > div {
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
}
}
}
}

View File

@ -109,10 +109,11 @@ textarea {
} }
.user-avatar { .user-avatar {
display: flex; display: grid;
gap: 16px; gap: 16px;
align-items: center; align-items: center;
margin-bottom: 4px; margin-bottom: 4px;
grid-template-columns: 100px auto;
&__edit { &__edit {
position: relative; position: relative;

View File

@ -51,7 +51,8 @@
} }
&__inner { &__inner {
height: 60vh; height: auto;
max-height: 60vh;
overflow-y: auto; overflow-y: auto;
& > div { & > div {
@ -86,3 +87,13 @@
.ant-modal-mask { .ant-modal-mask {
background-color: rgba(0, 59, 70, 0.4) !important; background-color: rgba(0, 59, 70, 0.4) !important;
} }
.ant-upload-list-item-name {
max-width: 280px;
}
.ant-upload-list-item {
&:hover {
background-color: transparent !important;
}
}

View File

@ -1188,7 +1188,6 @@
height: 86px; height: 86px;
border-radius: 16px; border-radius: 16px;
border: 2px solid #FFF; border: 2px solid #FFF;
background: lightgray 50%;
box-shadow: 0 8px 16px 0 rgba(102, 165, 173, 0.32); box-shadow: 0 8px 16px 0 rgba(102, 165, 173, 0.32);
overflow: hidden; overflow: hidden;
@ -1210,6 +1209,7 @@
@include rem(18); @include rem(18);
font-weight: 600; font-weight: 600;
line-height: 150%; line-height: 150%;
margin-bottom: 16px;
} }
} }
@ -1233,6 +1233,61 @@
} }
} }
&__info {
display: flex;
flex-direction: column;
gap: 8px;
.title-h3 {
color: #003B46;
@include rem(16);
line-height: 18px;
}
.case-list {
margin-top: 8px;
p {
margin-top: 8px;
}
}
}
&__practice {
color: #2C7873;
@include rem(16);
line-height: 18px;
}
&__lang {
display: flex;
flex-direction: column;
gap: 8px;
& > div {
color: #003B46;
@include rem(16);
line-height: 18px;
}
}
&__list {
display: flex;
flex-direction: column;
gap: 8px;
& > div {
display: flex;
gap: 12px;
}
}
&__item {
color: #003B46;
@include rem(16);
line-height: 18px;
}
.title-h2 { .title-h2 {
color: #003B46; color: #003B46;
@include rem(18); @include rem(18);
@ -1243,7 +1298,7 @@
margin-bottom: 0; margin-bottom: 0;
} }
&__desc { &__desc, &__desc > div {
border-radius: 16px; border-radius: 16px;
background: #EFFCFF; background: #EFFCFF;
padding: 16px; padding: 16px;
@ -1340,6 +1395,7 @@
background-position: 99% 50%; background-position: 99% 50%;
background-repeat: no-repeat; background-repeat: no-repeat;
padding: 16px; padding: 16px;
border-radius: 8px;
&__title { &__title {
color: #FFBD00; color: #FFBD00;
@ -1366,6 +1422,16 @@
background: #C4DFE6; background: #C4DFE6;
} }
&-item {
display: grid;
gap: 8px;
grid-template-columns: 80px 1fr 1fr 32px;
&__single {
grid-template-columns: 80px 1fr 1fr;
}
}
&__inner{ &__inner{
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -1382,6 +1448,7 @@
&__wrap { &__wrap {
display: flex; display: flex;
gap: 8px; gap: 8px;
flex-direction: column;
.btn-cancel, .btn-cancel,
.btn-edit { .btn-edit {
@ -1443,8 +1510,17 @@
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
} }
} }
} }
}
.pay-data-list {
display: flex;
flex-direction: column;
gap: 8px;
& > div {
display: flex;
gap: 16px;
}
} }

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

@ -0,0 +1,15 @@
.b-schedule-list {
display: flex;
flex-direction: column;
gap: 12px;
color: #003B46;
@include rem(16);
font-style: normal;
font-weight: 500;
line-height: 150%;
& > div {
display: flex;
gap: 8px;
}
}

View File

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

View File

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

View File

@ -0,0 +1,15 @@
.ant-collapse-header {
padding: 12px 0 !important;
}
.ant-collapse-header-text {
color: #003B46;
@include rem(16);
font-style: normal;
font-weight: 600;
line-height: 133.333%;
}
.ant-collapse-content-box {
padding: 12px 0 !important;
}

View File

@ -8,6 +8,14 @@
input { input {
background-color: transparent !important; background-color: transparent !important;
border-color: transparent !important;
box-shadow: none !important;
}
.ant-input-group-addon {
background-color: transparent !important;
border-color: transparent !important;
box-shadow: none !important;
} }
&:focus, &:hover, &:focus-within { &:focus, &:hover, &:focus-within {

View File

@ -0,0 +1,29 @@
.b-practice {
&-cases {
display: flex;
flex-direction: column;
gap: 16px;
}
&-case {
&__header {
display: flex;
justify-content: space-between;
align-items: center;
}
&__item {
display: flex;
justify-content: space-between;
gap: 8px;
align-items: flex-start;
}
&__content {
display: flex;
flex-direction: column;
gap: 16px;
flex: 1;
}
}
}

View File

@ -53,8 +53,15 @@
position: absolute; position: absolute;
left: 16px; left: 16px;
top: 15px; top: 15px;
right: 22px;
z-index: 1; z-index: 1;
transition: all .1s ease; transition: all .1s ease;
overflow: hidden;
text-overflow: ellipsis;
span {
white-space: nowrap;
}
} }
} }
@ -110,7 +117,14 @@
position: absolute; position: absolute;
left: 16px; left: 16px;
top: 15px; top: 15px;
right: 22px;
z-index: 1; z-index: 1;
transition: all .1s ease; transition: all .1s ease;
overflow: hidden;
text-overflow: ellipsis;
span {
white-space: nowrap;
}
} }
} }

View File

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

View File

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

22
src/types/author.ts Normal file
View File

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

42
src/types/blogPost.ts Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,7 @@
import { UploadFile } from 'antd'; import { EducationDTO } from './education';
import {EducationDTO} from "./education"; import { ExpertsTags } from './tags';
import {ExpertsTags} from "./tags"; import { PracticeDTO } from './practice';
import {PracticeDTO} from "./practice"; import { ScheduleDTO } from './schedule';
import {ScheduleDTO} from "./schedule";
export type ProfileData = { export type ProfileData = {
username?: string; username?: string;
@ -15,7 +14,8 @@ export type ProfileData = {
hasExternalLogin?: boolean; hasExternalLogin?: boolean;
isTestMode?: boolean; isTestMode?: boolean;
phone?: string; phone?: string;
languagesLinks?: { language: { id: number, code: string, nativeSpelling: string }, languageId: number }[] languagesLinks?: { language: { id: number, code: string, nativeSpelling: string }, languageId: number }[];
allLanguages?: { id: number, code: string, nativeSpelling: string }[]
} }
export type Profile = ProfileData & { id: number }; export type Profile = ProfileData & { id: number };
@ -27,7 +27,7 @@ export type ProfileRequest = {
languagesLinks?: { languageId: number }[]; languagesLinks?: { languageId: number }[];
username?: string; username?: string;
surname?: string; surname?: string;
faceImage?: UploadFile; faceImage?: any;
isFaceImageKeepExisting?: boolean; isFaceImageKeepExisting?: boolean;
phone?: string; phone?: string;
}; };

View File

@ -3,8 +3,15 @@ export type WorkingTime = {
startTimeUtc?: number, startTimeUtc?: number,
endDayOfWeekUtc?: string, endDayOfWeekUtc?: string,
endTimeUtc?: number endTimeUtc?: number
} };
export interface ScheduleDTO { export interface ScheduleDTO {
workingTimes?: WorkingTime[] workingTimes?: WorkingTime[]
} }
export type MapWorkingTime = {
startDay?: string;
startTimeMin?: number;
endDay?: string;
endTimeMin?: number;
};

View File

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

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

@ -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)) || []
);