merge develop

This commit is contained in:
SD 2024-09-11 15:05:09 +04:00
commit b141a6ad44
144 changed files with 6668 additions and 2500 deletions

3
.env
View File

@ -4,3 +4,6 @@ NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51LVB3LK5pVGxNPeKk4gedt5NW4cb8k7BVXvg
STRIPE_SECRET_KEY=sk_test_51LVB3LK5pVGxNPeK6j0wCsPqYMoGfcuwf1LpwGEBsr1dUx4NngukyjYL2oMZer5EOlW3lqnVEPjNDruN0OkUohIf00fWFUHN5O STRIPE_SECRET_KEY=sk_test_51LVB3LK5pVGxNPeK6j0wCsPqYMoGfcuwf1LpwGEBsr1dUx4NngukyjYL2oMZer5EOlW3lqnVEPjNDruN0OkUohIf00fWFUHN5O
STRIPE_PAYMENT_DESCRIPTION='BBuddy services' STRIPE_PAYMENT_DESCRIPTION='BBuddy services'
NEXT_PUBLIC_CONTENTFUL_SPACE_ID = voxpxjq7y7vf
NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN = s99GWKfpDKkNwiEJ3pN7US_tmqsGvDlaex-sOJwpzuc
NEXT_PUBLIC_CONTENTFUL_PREVIEW_ACCESS_TOKEN = Z9WOKpLDbKNj7xVOmT_VXYNLH0AZwISFvQsq0PQlHfE

16
Jenkinsfile vendored
View File

@ -1,14 +1,18 @@
pipeline { pipeline {
agent { label 'jenkins-nodejs-agent' } agent { label 'jenkins-nodejs-agent' }
environment {
RELEASE = "latest"
}
stages { stages {
stage('Build static content') { stage('Build static content') {
steps { steps {
sh ''' sh '''
docker build --progress=plain -t bbuddy/bbuddy_ui:latest . #npm install
#npm run build
#pwd
#echo
docker build --progress=plain -t bbuddy/bbuddy_ui:${RELEASE} .
''' '''
} }
} }
@ -16,8 +20,8 @@ pipeline {
steps { steps {
sh ''' sh '''
sudo docker login https://harbor-wtkp3fsbv6.vertexa.devbay.tech/ -u 'robot$jenkins' -p 'ZrzsVIAeueW1p0alpAnPfM5CDtaRVVKz' sudo docker login https://harbor-wtkp3fsbv6.vertexa.devbay.tech/ -u 'robot$jenkins' -p 'ZrzsVIAeueW1p0alpAnPfM5CDtaRVVKz'
sudo docker tag bbuddy/bbuddy_ui:latest harbor-wtkp3fsbv6.vertexa.devbay.tech/bbuddy/bbuddy_ui:latest sudo docker tag bbuddy/bbuddy_ui:${RELEASE} harbor-wtkp3fsbv6.vertexa.devbay.tech/bbuddy/bbuddy_ui:${RELEASE}
sudo docker push harbor-wtkp3fsbv6.vertexa.devbay.tech/bbuddy/bbuddy_ui:latest sudo docker push harbor-wtkp3fsbv6.vertexa.devbay.tech/bbuddy/bbuddy_ui:${RELEASE}
''' '''
} }
} }

View File

@ -1,20 +1,10 @@
{ {
"Header": {
"registration": "Registration",
"enter": "Enter",
"account": "My Account",
"menu": {
"bb-client": "Start grow with BB",
"bb-expert": "Become BB Expert",
"blog": "Blog&News"
}
},
"Main": { "Main": {
"title": "Bbuddy - Main", "title": "Bbuddy - Main",
"description": "Bbuddy desc", "description": "Bbuddy desc",
"header": "Mentorship, Career\nDevelopment & Coaching.", "header": "BBuddy: Plattform für persönlichen und beruflichen Erfolg",
"header-desc": "The ins-and-outs of building a career in tech, gaining experience from a mentor, and getting your feet wet with coaching.", "header-desc": "Erhalten Sie Beratungen von führenden Coaches und Mentoren auf BBuddy. Unsere Experten helfen Ihnen, sich zu entwickeln, zu lernen und Ihre persönlichen und beruflichen Ziele zu erreichen. Nutzen Sie unsere Web-Plattform und mobile App für professionelle Unterstützung und Wachstum.",
"news": "Professional Articles & Project News", "news": "Fachartikel & Projektneuigkeiten",
"popular": "Popular Topics" "popular": "Popular Topics"
}, },
"BbClient": { "BbClient": {
@ -89,7 +79,7 @@
} }
}, },
"Experts": { "Experts": {
"title": "Find a expert", "title": "Einen Experten finden",
"filter": { "filter": {
"price": "Price from {from}€ to {to}€", "price": "Price from {from}€ to {to}€",
"duration": "Duration from {from}min to {to}min", "duration": "Duration from {from}min to {to}min",

View File

@ -2,8 +2,8 @@
"Main": { "Main": {
"title": "Bbuddy - Main", "title": "Bbuddy - Main",
"description": "Bbuddy desc", "description": "Bbuddy desc",
"header": "Mentorship, Career\nDevelopment & Coaching.", "header": "BBuddy: Platform for Personal and Career Success",
"header-desc": "The ins-and-outs of building a career in tech, gaining experience from a mentor, and getting your feet wet with coaching.", "header-desc": "Receive consultations from leading coaches and mentors on BBuddy. Our experts will help you develop, learn, and achieve your personal and career goals. Use our web platform and mobile app for professional support and growth.",
"news": "Professional Articles & Project News", "news": "Professional Articles & Project News",
"popular": "Popular Topics" "popular": "Popular Topics"
}, },
@ -69,7 +69,7 @@
} }
}, },
"Experts": { "Experts": {
"title": "Find a expert", "title": "Find an expert",
"filter": { "filter": {
"price": "Price from {from}€ to {to}€", "price": "Price from {from}€ to {to}€",
"duration": "Duration from {from}min to {to}min", "duration": "Duration from {from}min to {to}min",

View File

@ -1,20 +1,10 @@
{ {
"Header": {
"registration": "Registration",
"enter": "Enter",
"account": "My Account",
"menu": {
"bb-client": "Start grow with BB",
"bb-expert": "Become BB Expert",
"blog": "Blog&News"
}
},
"Main": { "Main": {
"title": "Bbuddy - Main", "title": "Bbuddy - Main",
"description": "Bbuddy desc", "description": "Bbuddy desc",
"header": "Mentorship, Career\nDevelopment & Coaching.", "header": "BBuddy: Plataforma para el éxito personal y profesional",
"header-desc": "The ins-and-outs of building a career in tech, gaining experience from a mentor, and getting your feet wet with coaching.", "header-desc": "Reciba consultas de entrenadores y mentores líderes en BBuddy. Nuestros expertos le ayudarán a desarrollarse, aprender y alcanzar sus objetivos personales y profesionales. Utilice nuestra plataforma web y aplicación móvil para apoyo profesional y crecimiento.",
"news": "Professional Articles & Project News", "news": "Artículos profesionales y Noticias de proyectos",
"popular": "Popular Topics" "popular": "Popular Topics"
}, },
"BbClient": { "BbClient": {
@ -89,7 +79,7 @@
} }
}, },
"Experts": { "Experts": {
"title": "Find a expert", "title": "Encontrar un experto",
"filter": { "filter": {
"price": "Price from {from}€ to {to}€", "price": "Price from {from}€ to {to}€",
"duration": "Duration from {from}min to {to}min", "duration": "Duration from {from}min to {to}min",

View File

@ -1,20 +1,10 @@
{ {
"Header": {
"registration": "Registration",
"enter": "Enter",
"account": "My Account",
"menu": {
"bb-client": "Start grow with BB",
"bb-expert": "Become BB Expert",
"blog": "Blog&News"
}
},
"Main": { "Main": {
"title": "Bbuddy - Main", "title": "Bbuddy - Main",
"description": "Bbuddy desc", "description": "Bbuddy desc",
"header": "Mentorship, Career\nDevelopment & Coaching.", "header": "BBuddy: Plateforme pour le succès personnel et professionnel",
"header-desc": "The ins-and-outs of building a career in tech, gaining experience from a mentor, and getting your feet wet with coaching.", "header-desc": "Recevez des consultations de coachs et mentors de premier plan sur BBuddy. Nos experts vous aideront à développer, apprendre et atteindre vos objectifs personnels et professionnels. Utilisez notre plateforme web et notre application mobile pour un soutien professionnel et une croissance.",
"news": "Professional Articles & Project News", "news": "Articles professionnels et actualités des projets",
"popular": "Popular Topics" "popular": "Popular Topics"
}, },
"BbClient": { "BbClient": {
@ -89,7 +79,7 @@
} }
}, },
"Experts": { "Experts": {
"title": "Find a expert", "title": "Trouver un expert",
"filter": { "filter": {
"price": "Price from {from}€ to {to}€", "price": "Price from {from}€ to {to}€",
"duration": "Duration from {from}min to {to}min", "duration": "Duration from {from}min to {to}min",

View File

@ -1,20 +1,10 @@
{ {
"Header": {
"registration": "Registration",
"enter": "Enter",
"account": "My Account",
"menu": {
"bb-client": "Start grow with BB",
"bb-expert": "Become BB Expert",
"blog": "Blog&News"
}
},
"Main": { "Main": {
"title": "Bbuddy - Main", "title": "Bbuddy - Main",
"description": "Bbuddy desc", "description": "Bbuddy desc",
"header": "Mentorship, Career\nDevelopment & Coaching.", "header": "BBuddy: Piattaforma per il successo personale e professionale",
"header-desc": "The ins-and-outs of building a career in tech, gaining experience from a mentor, and getting your feet wet with coaching.", "header-desc": "Ricevi consulenze da coach e mentori leader su BBuddy. I nostri esperti ti aiuteranno a svilupparti, imparare e raggiungere i tuoi obiettivi personali e professionali. Usa la nostra piattaforma web e l'app mobile per supporto professionale e crescita.",
"news": "Professional Articles & Project News", "news": "Articoli professionali e novità sui progetti",
"popular": "Popular Topics" "popular": "Popular Topics"
}, },
"BbClient": { "BbClient": {
@ -89,7 +79,7 @@
} }
}, },
"Experts": { "Experts": {
"title": "Find a expert", "title": "Trova un esperto",
"filter": { "filter": {
"price": "Price from {from}€ to {to}€", "price": "Price from {from}€ to {to}€",
"duration": "Duration from {from}min to {to}min", "duration": "Duration from {from}min to {to}min",

View File

@ -1,20 +1,10 @@
{ {
"Header": {
"registration": "Регистрация",
"enter": "Вход",
"account": "Мой аккаунт",
"menu": {
"bb-client": "Начни вместе с BB",
"bb-expert": "Стань BB экспертом",
"blog": "Блог&Новости"
}
},
"Main": { "Main": {
"title": "Bbuddy - Главная", "title": "Bbuddy - Главная",
"description": "Bbuddy описание", "description": "Bbuddy описание",
"header": "Mentorship, Career\nDevelopment & Coaching.", "header": "BBuddy: Платформа для Личного и Карьерного Успеха",
"header-desc": "The ins-and-outs of building a career in tech, gaining experience from a mentor, and getting your feet wet with coaching.", "header-desc": "Получайте консультации от ведущих коучей и менторов в BBuddy. Наши эксперты помогут вам развиваться, обучаться и достигать личных и карьерных целей. Используйте нашу веб-платформу и мобильное приложение для получения профессиональной поддержки и роста.",
"news": "Professional Articles & Project News", "news": "Профессиональные статьи и новости проекта",
"popular": "Popular Topics" "popular": "Popular Topics"
}, },
"BbClient": { "BbClient": {

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

1488
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.3",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 4200", "dev": "next dev -p 4200",
@ -12,6 +12,7 @@
"@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",
"@stripe/react-stripe-js": "^2.7.3", "@stripe/react-stripe-js": "^2.7.3",
"@stripe/stripe-js": "^4.1.0", "@stripe/stripe-js": "^4.1.0",
"agora-rtc-react": "^2.1.0", "agora-rtc-react": "^2.1.0",
@ -20,6 +21,7 @@
"antd-img-crop": "^4.21.0", "antd-img-crop": "^4.21.0",
"antd-style": "^3.6.2", "antd-style": "^3.6.2",
"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",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -1,26 +1,14 @@
import { AxiosResponse } from 'axios'; import { apiRequest } from './helpers';
import { apiClient } from '../lib/apiClient';
export const getAuth = (locale: string, data: { login: string, password: string }): Promise<AxiosResponse<{ jwtToken: string }>> => ( export const getAuth = (locale: string, data: { login: string, password: string }): Promise<{ jwtToken: string }> => apiRequest({
apiClient.post( url: '/auth/login',
'/auth/login', method: 'post',
data, data,
{ locale
headers: { });
'X-User-Language': locale
}
}
)
);
export const getRegister = (locale: string): Promise<AxiosResponse<{ jwtToken: string }>> => ( export const getRegister = (locale: string): Promise<{ jwtToken: string }> => apiRequest({
apiClient.post( url: '/auth/register',
'/auth/register', method: 'post',
{}, locale
{ });
headers: {
'X-User-Language': locale
}
}
)
);

View File

@ -1,62 +1,16 @@
import { apiClient } from '../lib/apiClient'; import { GeneralFilter, ExpertsData, ExpertDetails } from '../types/experts';
import {GeneralFilter, ExpertsData, ExpertDetails, ExpertScheduler, ExpertSchedulerSession} from '../types/experts'; import { apiRequest } from './helpers';
import {useLocalStorage} from "../hooks/useLocalStorage";
import {AUTH_TOKEN_KEY} from "../constants/common";
export const getExpertsList = async (locale: string, filter?: GeneralFilter) => { export const getExpertsList = (locale: string, filter?: GeneralFilter): Promise<ExpertsData> => apiRequest({
const response = await apiClient.post( url: '/home/coachsearch1',
'/home/coachsearch1', method: 'post',
{ ...filter }, data: { ...filter },
{ locale
headers: { });
'X-User-Language': locale
}
}
);
return response.data as ExpertsData || null; export const getExpertById = (id: string, locale: string): Promise<ExpertDetails> => apiRequest({
}; url: '/home/coachdetails',
method: 'post',
export const getExpertById = async (id: string, locale: string) => { data: { id },
const response = await apiClient.post( locale
'/home/coachdetails', });
{ id },
{
headers: {
'X-User-Language': locale
}
}
);
return response.data as ExpertDetails || null;
};
export const getSchedulerByExpertId = async (expertId: string, locale: string, jwt: string) => {
const response = await apiClient.post(
'/home/sessionsignupdata',
{ id: expertId },
{
headers: {
'X-User-Language': locale,
Authorization: `Bearer ${jwt}`
}
}
);
return response.data as ExpertScheduler || null;
};
export const getSchedulerSession = async (data: { coachId: number, tagId: number, startAtUtc: string, clientComment: string }, locale: string, jwt: string) => {
const response = await apiClient.post(
'/home/sessionsignupsubmit',
data,
{
headers: {
'X-User-Language': locale,
Authorization: `Bearer ${jwt}`
}
}
);
return response.data as ExpertSchedulerSession || null;
};

44
src/actions/helpers.ts Normal file
View File

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

View File

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

View File

@ -18,8 +18,8 @@ export const useSessionDetails = (locale: string, sessionId: number) => {
setSession(undefined); setSession(undefined);
getSessionDetails(locale, jwt, sessionId) getSessionDetails(locale, jwt, sessionId)
.then(({ data }) => { .then((session) => {
setSession(data); setSession(session);
}) })
.catch((err) => { .catch((err) => {
setErrorData(err); setErrorData(err);

View File

@ -1,29 +1,103 @@
import { AxiosResponse } from 'axios'; import { PayInfo, Profile, ProfileRequest, ProfileData } from '../types/profile';
import { apiClient } from '../lib/apiClient'; import { ExpertsTags } from '../types/tags';
import { Profile } from '../types/profile'; import { EducationData, EducationDTO } from '../types/education';
import { PracticeData, PracticeDTO } from '../types/practice';
import { ScheduleDTO } from '../types/schedule';
import { apiRequest } from './helpers';
export const setPersonData = (person: { login: string, password: string, role: string, languagesLinks: any[] }, locale: string, jwt: string): Promise<AxiosResponse<{ userData: Profile }>> => ( export const getUserData = (locale: string, token: string): Promise<Profile> => apiRequest({
apiClient.post( url: '/home/userdata',
'/home/applyperson1', method: 'post',
{ ...person }, locale,
{ token
headers: { });
'X-User-Language': locale,
Authorization: `Bearer ${jwt}`
}
}
)
);
export const getPersonalData = (locale: string, jwt: string): Promise<AxiosResponse<Profile>> => ( export const getPersonalData = (locale: string, token: string): Promise<ProfileData> => apiRequest({
apiClient.post( url: '/home/person1',
'/home/userdata', method: 'post',
{}, locale,
{ token
headers: { });
'X-User-Language': locale,
Authorization: `Bearer ${jwt}` export const setPersonData = (data: ProfileRequest, locale: string, token: string): Promise<{ userData: Profile }> => apiRequest({
} url: '/home/applyperson1',
} method: 'post',
) data,
); locale,
token
});
export const getEducation = (locale: string, token: string): Promise<EducationDTO> => apiRequest({
url: '/home/person2',
method: 'post',
locale,
token
});
export const setEducation = (locale: string, token: string, data: EducationData): Promise<EducationData> => apiRequest({
url: '/home/applyperson2',
method: 'post',
data,
locale,
token
});
export const getTags = (locale: string, token: string): Promise<ExpertsTags> => apiRequest({
url: '/home/person3',
method: 'post',
locale,
token
});
export const setTags = (locale: string, token: string, data: ExpertsTags): Promise<ExpertsTags> => apiRequest({
url: '/home/applyperson3',
method: 'post',
data,
locale,
token
});
export const getPractice = (locale: string, token: string): Promise<PracticeDTO> => apiRequest({
url: '/home/person4',
method: 'post',
locale,
token
});
export const setPractice = (locale: string, token: string, data: PracticeData): Promise<PracticeDTO> => apiRequest({
url: '/home/applyperson4',
method: 'post',
data,
locale,
token
});
export const getSchedule = (locale: string, token: string): Promise<ScheduleDTO> => apiRequest({
url: '/home/person51',
method: 'post',
locale,
token
});
export const setSchedule = (locale: string, token: string, data: ScheduleDTO): Promise<ScheduleDTO> => apiRequest({
url: '/home/applyperson51',
method: 'post',
data,
locale,
token
});
export const getPayData = (locale: string, token: string): Promise<{ person6Data?: PayInfo }> => apiRequest({
url: '/home/person6',
method: 'post',
locale,
token
});
export const setPayData = (locale: string, token: string, data: PayInfo): Promise<PayInfo> => apiRequest({
url: '/home/applyperson6',
method: 'post',
data,
locale,
token
});

View File

@ -1,145 +1,93 @@
import { AxiosResponse } from 'axios';
import { apiClient } from '../lib/apiClient';
import { DeclineSessionData, Session, SessionsFilter, SessionCommentData } from '../types/sessions'; import { DeclineSessionData, Session, SessionsFilter, SessionCommentData } from '../types/sessions';
import { apiRequest } from './helpers';
export const getUpcomingSessions = (locale: string, jwt: string, filter?: SessionsFilter): Promise<AxiosResponse<Session[]>> => ( export const getUpcomingSessions = (locale: string, token: string, filter?: SessionsFilter): Promise<Session[]> => apiRequest({
apiClient.post( url: '/home/upcomingsessionsall',
'/home/upcomingsessionsall', method: 'post',
{ data: {
sessionType: 'session', sessionType: 'session',
...(filter || {}) ...(filter || {})
}, },
{ locale,
headers: { token
'X-User-Language': locale, });
Authorization: `Bearer ${jwt}`
}
}
)
);
export const getRequestedSessions = (locale: string, jwt: string): Promise<AxiosResponse<{ requestedSessions: Session[] }>> => ( export const getRequestedSessions = (locale: string, token: string): Promise<{ requestedSessions: Session[] }> => apiRequest({
apiClient.post( url: '/home/coachhomedata',
'/home/coachhomedata', method: 'post',
{}, locale,
{ token
headers: { });
'X-User-Language': locale,
Authorization: `Bearer ${jwt}`
}
}
)
);
export const getRecentSessions = (locale: string, jwt: string, filter?: SessionsFilter): Promise<AxiosResponse<Session[]>> => ( export const getRecentSessions = (locale: string, token: string, filter?: SessionsFilter): Promise<Session[]> => apiRequest({
apiClient.post( url: '/home/historicalmeetings',
'/home/historicalmeetings', method: 'post',
{ data: {
sessionType: 'session', sessionType: 'session',
...(filter || {}) ...(filter || {})
}, },
{ locale,
headers: { token
'X-User-Language': locale, });
Authorization: `Bearer ${jwt}`
}
}
)
);
export const getSessionDetails = (locale: string, jwt: string, id: number): Promise<AxiosResponse<Session>> => ( export const getSessionDetails = (locale: string, token: string, id: number): Promise<Session> => apiRequest({
apiClient.post( url: '/home/session',
'/home/session', method: 'post',
{ id }, data: { id },
{ locale,
headers: { token
'X-User-Language': locale, });
Authorization: `Bearer ${jwt}`
}
}
)
);
export const approveRequestedSession = (locale: string, jwt: string, sessionId: number): Promise<AxiosResponse> => ( export const approveRequestedSession = (locale: string, token: string, sessionId: number): Promise<any> => apiRequest({
apiClient.post( url: '/home/approverequestedsession',
'/home/approverequestedsession', method: 'post',
{ sessionId }, data: { sessionId },
{ locale,
headers: { token
'X-User-Language': locale, });
Authorization: `Bearer ${jwt}`
}
}
)
);
export const declineRequestedSession = (locale: string, jwt: string, { sessionId, reason }: DeclineSessionData): Promise<AxiosResponse> => ( export const declineRequestedSession = (locale: string, token: string, { sessionId, reason }: DeclineSessionData): Promise<any> => apiRequest({
apiClient.post( url: '/home/declinerequestedsession',
'/home/declinerequestedsession', method: 'post',
{ data: {
sessionId, sessionId,
coachDeclineReason: reason coachDeclineReason: reason
}, },
{ locale,
headers: { token
'X-User-Language': locale, });
Authorization: `Bearer ${jwt}`
}
}
)
);
export const cancelUpcomingSession = (locale: string, jwt: string, { sessionId, reason }: DeclineSessionData): Promise<AxiosResponse> => ( export const cancelUpcomingSession = (locale: string, token: string, { sessionId, reason }: DeclineSessionData): Promise<any> => apiRequest({
apiClient.post( url: '/home/cancelupcomingsession',
'/home/cancelupcomingsession', method: 'post',
{ data: {
sessionId, sessionId,
coachCancelReason: reason coachCancelReason: reason
}, },
{ locale,
headers: { token
'X-User-Language': locale, });
Authorization: `Bearer ${jwt}`
}
}
)
);
export const addSessionComment = (locale: string, jwt: string, data: SessionCommentData): Promise<AxiosResponse> => ( export const addSessionComment = (locale: string, token: string, data: SessionCommentData): Promise<any> => apiRequest({
apiClient.post( url: '/home/session_comment',
'/home/session_comment', method: 'post',
data, data,
{ locale,
headers: { token
'X-User-Language': locale, });
Authorization: `Bearer ${jwt}`
}
}
)
);
export const trackingStartSession = (locale: string, jwt: string, id: number): Promise<AxiosResponse> => ( export const trackingStartSession = (locale: string, token: string, id: number): Promise<any> => apiRequest({
apiClient.post( url: '/home/sessiontracking',
'/home/sessiontracking', method: 'post',
{ id }, data: { id },
{ locale,
headers: { token
'X-User-Language': locale, });
Authorization: `Bearer ${jwt}`
}
}
)
);
export const finishSession = (locale: string, jwt: string, sessionId: number): Promise<AxiosResponse> => ( export const finishSession = (locale: string, token: string, sessionId: number): Promise<any> => apiRequest({
apiClient.post( url: '/home/finishsession',
'/home/finishsession', method: 'post',
{ sessionId }, data: { sessionId },
{ locale,
headers: { token
'X-User-Language': locale, });
Authorization: `Bearer ${jwt}`
}
}
)
);

View File

@ -1,78 +0,0 @@
"use server";
import {PaymentIntentCreateParams, Stripe} from "stripe";
import { headers } from "next/headers";
import { formatAmountForStripe } from "../utils/stripe-helpers";
import { stripe } from "../lib/stripe";
export async function createCheckoutSession(
data: FormData,
): Promise<{ client_secret: string | null; url: string | null }> {
const ui_mode = data.get(
"uiMode",
) as Stripe.Checkout.SessionCreateParams.UiMode;
console.log('DATA', data)
const origin: string = headers().get("origin") as string;
const checkoutSession: Stripe.Checkout.Session =
await stripe.checkout.sessions.create({
mode: "payment",
submit_type: "donate",
line_items: [
{
quantity: 1,
price_data: {
currency: 'eur',
product_data: {
name: "Custom amount donation",
},
unit_amount: formatAmountForStripe(
Number(data.get("customDonation") as string),
'eur',
),
},
},
],
...(ui_mode === "hosted" && {
success_url: `${origin}/payment/with-checkout/result?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${origin}/with-checkout`,
}),
...(ui_mode === "embedded" && {
return_url: `${origin}/payment/with-embedded-checkout/result?session_id={CHECKOUT_SESSION_ID}`,
}),
ui_mode,
});
return {
client_secret: checkoutSession.client_secret,
url: checkoutSession.url,
};
}
export async function createPaymentIntent(
data: any,
): Promise<{ client_secret: string }> {
const params = {
amount: formatAmountForStripe(
Number(data['amount'] as string),
'eur',
),
automatic_payment_methods: { enabled: true },
currency: 'eur',
} as PaymentIntentCreateParams;
// additional params
if (data.sessionId){
params.metadata = {
sessionId : data.sessionId
}
}
const paymentIntent: Stripe.PaymentIntent =
await stripe.paymentIntents.create(params);
return { client_secret: paymentIntent.client_secret as string };
}

View File

@ -1,29 +1,14 @@
import { apiClient } from '../lib/apiClient';
import { SearchData, Languages } from '../types/tags'; import { SearchData, Languages } from '../types/tags';
import { apiRequest } from './helpers';
export const getTagList = async (locale: string) => { export const getTagList = (locale: string): Promise<SearchData> => apiRequest({
const response = await apiClient.post( url: '/home/searchdata',
'/home/searchdata', method: 'post',
{}, locale
{ });
headers: {
'X-User-Language': locale
}
}
);
return response.data as SearchData || null; export const getLanguages = (locale: string): Promise<Languages> => apiRequest({
}; url: '/home/languages',
method: 'get',
export const getLanguages = async (locale: string) => { locale
const response = await apiClient.get( });
'/home/languages',
{
headers: {
'X-User-Language': locale
}
}
);
return response.data as Languages || null;
};

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

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

View File

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

View File

@ -12,19 +12,16 @@ import React, { ReactNode } from 'react';
// }; // };
// } // }
export default function MainLayout({ children, news, directions, experts }: { export default function MainLayout({ children, news, experts }: {
children: ReactNode, children: ReactNode,
news: ReactNode, news: ReactNode,
directions: ReactNode, experts: ReactNode
experts: ReactNode,
payment: ReactNode
}) { }) {
return ( return (
<> <>
{children} {children}
{news} {news}
{directions}
{experts} {experts}
</> </>
); );
} };

View File

@ -1,18 +1,18 @@
import React from 'react'; import React from 'react';
import { Link } from '../../../../../../../navigation'; import { Link } from '../../../../../../navigation';
import { CustomSelect } from '../../../../../../../components/view/CustomSelect'; import { CustomSelect } from '../../../../../../components/view/CustomSelect';
export default function AddOffer() { export default function AddOffer() {
return ( return (
<> <>
<ol className="breadcrumb"> <ol className="breadcrumb">
<li className="breadcrumb-item"> <li className="breadcrumb-item">
<Link href={'/account/work-with-us' as any}> <Link href={'/account/expert-profile' as any}>
Work With Us Work With Us
</Link> </Link>
</li> </li>
<li className="breadcrumb-item"> <li className="breadcrumb-item">
<Link href={'/account/work-with-us/coaching' as any}> <Link href={'/account/expert-profile/coaching' as any}>
Coaching Coaching
</Link> </Link>
</li> </li>

View File

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

View File

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

View File

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

View File

@ -1,131 +0,0 @@
import React from 'react';
import { Link } from '../../../../../../navigation';
export default function Coaching() {
return (
<>
<ol className="breadcrumb">
<li className="breadcrumb-item">
<Link href={'/account/work-with-us' as any}>
Work With Us
</Link>
</li>
<li className="breadcrumb-item active" aria-current="page">Coaching</li>
</ol>
<div className="coaching-info">
<div className="card-profile">
<div className="card-profile__header">
<div className="card-profile__header__portrait">
<img src="/images/person.png" className="" alt="" />
</div>
<div className="card-profile__header__inner">
<div className="card-profile__header__name">
David
</div>
<div className="card-profile__header__title">
12 Practice hours
</div>
<div className="card-profile__header__title ">
15 Supervision per year
</div>
</div>
</div>
</div>
<div className="coaching-info__wrap-btn">
<a href="#" className="btn-edit">Edit</a>
<a href="#" className="btn-apply">Add Offer</a>
</div>
</div>
<div className="coaching-section">
<h2 className="title-h2">
My Offers
</h2>
<div className="coaching-section__desc">
<div className="coaching-offer">
<div className="coaching-offer__header">
<div className="coaching-offer__title">
Senior Software Engineer
</div>
<div className="coaching-offer__wrap-btn">
<a href="#" className="link-edit">Edit</a>
<a href="#" className="link-remove">Remove</a>
</div>
</div>
<div className="coaching-offer__price">
45$ <span>/ 45min</span>
</div>
<div className="skills__list">
<div className="skills__list__item">Engineering & Data</div>
<div className="skills__list__item">Engineering & Data</div>
<div className="skills__list__more">+6</div>
</div>
<div className="coaching-offer__desc">
I have worked across a variety of organizations, lead teams, and delivered quality software
for 8 years. In that time I've worked as an independent consultant, at agencies as a team
lead, and as a senior engineer at Auth0. I also host a podcast
https://anchor.fm/work-in-programming where I break down how …
</div>
</div>
</div>
</div>
<div className="coaching-section">
<h2 className="title-h2">
About Coach
</h2>
<div className="base-text">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam aliquet, lectus nec viverra
malesuada, ligula sem tempor risus, non posuere urna diam a libero.
</div>
</div>
<div className="coaching-section">
<h2 className="title-h2">
Education
</h2>
<div className="coaching-section__desc">
<h3 className="title-h3">Psychologist </h3>
<div className="base-text">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam aliquet, lectus nec viverra
malesuada, ligula sem tempor risus, non posuere urna diam a libero.
</div>
<div className="sertific">
<img src="/images/sertific.png" className="" alt="" />
</div>
</div>
</div>
<div className="coaching-section">
<h2 className="title-h2">
Professional Certification
</h2>
<div className="coaching-section__desc">
<div className="base-text">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam aliquet, lectus nec viverra
malesuada, ligula sem tempor risus, non posuere urna diam a libero.
</div>
</div>
</div>
<div className="coaching-section">
<h2 className="title-h2">
Trainings | Seminars | Courses
</h2>
<div className="coaching-section__desc">
<div className="base-text">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam aliquet, lectus nec viverra
malesuada, ligula sem tempor risus, non posuere urna diam a libero.
</div>
</div>
</div>
<div className="coaching-section">
<h2 className="title-h2">
MBA Information
</h2>
<div className="coaching-section__desc">
<div className="base-text">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam aliquet, lectus nec viverra
malesuada, ligula sem tempor risus, non posuere urna diam a libero.
</div>
</div>
</div>
</>
);
}

View File

@ -1,31 +0,0 @@
import React from 'react';
import type { Metadata } from 'next';
import { unstable_setRequestLocale } from 'next-intl/server';
import { useTranslations } from 'next-intl';
import { i18nText } from '../../../../../i18nKeys';
export const metadata: Metadata = {
title: 'Bbuddy - Account - Work with us',
description: 'Bbuddy desc work with us'
};
export default function WorkWithUs({ params: { locale } }: { params: { locale: string } }) {
unstable_setRequestLocale(locale);
const t = useTranslations('Account.WorkWithUs');
return (
<>
<ol className="breadcrumb">
<li className="breadcrumb-item active" aria-current="page">{i18nText('accountMenu.work-with-us', locale)}</li>
</ol>
<div className="b-info">
<div className="image-info">
<img className="" src="/images/info.png" alt="" />
</div>
<div className="b-info__title">{i18nText('insertInfo', locale)}</div>
<button className="btn-apply">{i18nText('getStarted', locale)}</button>
<div className="base-text">{i18nText('changeUserData', locale)}</div>
</div>
</>
);
}

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,9 +9,9 @@ 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 {SchedulerModal} from "../../../../components/Modals/SchedulerModal"; import { i18nText } from '../../../../i18nKeys';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Bbuddy - Experts item', title: 'Bbuddy - Experts item',
@ -32,7 +32,7 @@ export async function generateStaticParams({
return result; return result;
} }
export default async function ExpertItem({ params: { expertId = '', locale} }: { params: { expertId: string, locale: string } }) { export default async function ExpertItem({ params: { expertId = '', locale } }: { params: { expertId: string, locale: string } }) {
if (!expertId) notFound(); if (!expertId) notFound();
const expert = await getExpertById(expertId, locale); const expert = await getExpertById(expertId, locale);
@ -76,16 +76,16 @@ export default async function ExpertItem({ params: { expertId = '', locale} }: {
<div className="b-inner"> <div className="b-inner">
<div className="b-page__back"> <div className="b-page__back">
<Suspense> <Suspense>
<BackButton className="btn-back"> <BackButton className="btn-back btn-back__auto">
<img src="/images/arrow-back.svg" className="" alt="" /> <img src="/images/arrow-back.svg" className="" alt="" />
Back to experts list {i18nText('backToExperts', locale)}
</BackButton> </BackButton>
</Suspense> </Suspense>
</div> </div>
<ExpertCard expert={expert} locale={locale} expertId={expertId}/> <ExpertCard expert={expert} locale={locale} />
<ExpertInformation expert={expert} locale={locale} /> <ExpertInformation expert={expert} locale={locale} />
<h2 className="title-h2">Expert Background</h2> <h2 className="title-h2">{i18nText('expertBackground', locale)}</h2>
<p className="base-text"> <p className="base-text">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam aliquet, lectus nec viverra Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam aliquet, lectus nec viverra
malesuada, ligula sem tempor risus, non posuere urna diam a libero. malesuada, ligula sem tempor risus, non posuere urna diam a libero.
@ -93,7 +93,7 @@ export default async function ExpertItem({ params: { expertId = '', locale} }: {
{expert?.publicCoachDetails?.educations && expert.publicCoachDetails.educations?.map(generateDescription)} {expert?.publicCoachDetails?.educations && expert.publicCoachDetails.educations?.map(generateDescription)}
{expert?.publicCoachDetails?.certificates && expert.publicCoachDetails.certificates.length > 0 && ( {expert?.publicCoachDetails?.certificates && expert.publicCoachDetails.certificates.length > 0 && (
<div> <div>
<h3 className="title-h3">Professional Certification</h3> <h3 className="title-h3">{i18nText('profCertification', locale)}</h3>
{expert.publicCoachDetails.certificates?.map((cert) => ( {expert.publicCoachDetails.certificates?.map((cert) => (
<div key={cert.id}> <div key={cert.id}>
<p className="base-text"> <p className="base-text">
@ -111,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} /> <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}
@ -39,4 +42,4 @@ export default function LocaleLayout({ children, params: { locale } }: LayoutPro
</ConfigProvider> </ConfigProvider>
</AntdRegistry> </AntdRegistry>
); );
} };

View File

@ -1,20 +0,0 @@
import type { Metadata } from "next";
import {ElementsForm} from "../../../../components/stripe/ElementsForm";
export const metadata: Metadata = {
title: "Payment",
};
export default function PaymentElementPage({
searchParams,
}: {
searchParams?: { payment_intent_client_secret?: string };
}) {
return (
<div className="page-container">
<h1>Pay</h1>
<ElementsForm />
</div>
);
}

View File

@ -1,28 +0,0 @@
import React from 'react';
import type { Metadata } from 'next';
import { unstable_setRequestLocale } from 'next-intl/server';
import { useTranslations } from 'next-intl';
import { GeneralTopSection } from '../../../components/Page';
import PaymentElementPage from "./@payment/page";
export const metadata: Metadata = {
title: 'Bbuddy - Take the lead with BB',
description: 'Bbuddy desc Take the lead with BB'
};
export default function BbClientPage({ params: { locale } }: { params: { locale: string } }) {
unstable_setRequestLocale(locale);
const t = useTranslations('BbClient');
return (
<>
<GeneralTopSection
title={t('header')}
mainImage="banner-phone.png"
/>
<div className="main-articles bb-client">
<PaymentElementPage />
</div>
</>
);
};

View File

@ -1,19 +0,0 @@
import type { Metadata } from "next";
import React from "react";
export const metadata: Metadata = {
title: "Payment Intent Result",
};
export default function ResultLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="page-container">
<h1>Payment Intent Result</h1>
{children}
</div>
);
}

View File

@ -1,27 +0,0 @@
import type { Stripe } from "stripe";
import { stripe} from "../../../../lib/stripe";
import PrintObject from "../../../../components/stripe/PrintObject";
export default async function ResultPage({
searchParams,
}: {
searchParams: { payment_intent: string };
}) {
if (!searchParams.payment_intent)
throw new Error("Please provide a valid payment_intent (`pi_...`)");
const paymentIntent: Stripe.PaymentIntent =
await stripe.paymentIntents.retrieve(searchParams.payment_intent);
// Тут под идее тыкнуться в бек на тему того - прошла ли оплата. в зависимости от этого показать что все ок или нет
// также стоит расшить ссылкой КУДА переходить после того как показали что все ок.
return (
<>
<h2>Status: {paymentIntent.status}</h2>
<h3>Payment Intent response:</h3>
<PrintObject content={paymentIntent} />
</>
);
}

View File

@ -1,66 +0,0 @@
import type { Stripe } from "stripe";
import { NextResponse } from "next/server";
import { stripe } from "../../../lib/stripe";
export async function POST(req: Request) {
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
await (await req.blob()).text(),
req.headers.get("stripe-signature") as string,
process.env.STRIPE_WEBHOOK_SECRET as string,
);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "Unknown error";
// On error, log and return the error message.
if (err! instanceof Error) console.log(err);
console.log(`❌ Error message: ${errorMessage}`);
return NextResponse.json(
{ message: `Webhook Error: ${errorMessage}` },
{ status: 400 },
);
}
// Successfully constructed event.
console.log("✅ Success:", event.id);
const permittedEvents: string[] = [
"checkout.session.completed",
"payment_intent.succeeded",
"payment_intent.payment_failed",
];
if (permittedEvents.includes(event.type)) {
let data;
try {
switch (event.type) {
case "checkout.session.completed":
data = event.data.object as Stripe.Checkout.Session;
console.log(`💰 CheckoutSession status: ${data.payment_status}`);
break;
case "payment_intent.payment_failed":
data = event.data.object as Stripe.PaymentIntent;
console.log(`❌ Payment failed: ${data.last_payment_error?.message}`);
break;
case "payment_intent.succeeded":
data = event.data.object as Stripe.PaymentIntent;
console.log(`💰 PaymentIntent status: ${data.status}`);
break;
default:
throw new Error(`Unhandled event: ${event.type}`);
}
} catch (error) {
console.log(error);
return NextResponse.json(
{ message: "Webhook handler failed" },
{ status: 500 },
);
}
}
// Return a response to acknowledge receipt of the event.
return NextResponse.json({ message: "Received" }, { status: 200 });
}

View File

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

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

@ -1,20 +1,17 @@
'use client'; 'use client';
import React, { useState } from 'react';
import { Button } from 'antd'; import { Button } from 'antd';
import { useSelectedLayoutSegment, usePathname } from 'next/navigation'; import { useSelectedLayoutSegment, usePathname } from 'next/navigation';
import { Link } from '../../navigation'; import { Link } from '../../navigation';
import { AUTH_TOKEN_KEY, AUTH_USER } from '../../constants/common'; import { AUTH_TOKEN_KEY, AUTH_USER } from '../../constants/common';
import { deleteStorageKey } from '../../hooks/useLocalStorage'; import { deleteStorageKey } from '../../hooks/useLocalStorage';
import { i18nText } from '../../i18nKeys'; import { i18nText } from '../../i18nKeys';
import { DeleteAccountModal } from '../Modals/DeleteAccountModal';
import { getMenuConfig } from '../../utils/account'; import { getMenuConfig } from '../../utils/account';
export const AccountMenu = ({ locale }: { locale: string }) => { export const AccountMenu = ({ locale }: { locale: string }) => {
const selectedLayoutSegment = useSelectedLayoutSegment(); const selectedLayoutSegment = useSelectedLayoutSegment();
const pathname = selectedLayoutSegment || ''; const pathname = selectedLayoutSegment || '';
const paths = usePathname(); const paths = usePathname();
const [showDeleteModal, setShowDeleteModal] = useState<boolean>(false);
const menu: { path: string, title: string, count?: number }[] = getMenuConfig(locale); const menu: { path: string, title: string, count?: number }[] = getMenuConfig(locale);
const onLogout = () => { const onLogout = () => {
@ -23,8 +20,6 @@ export const AccountMenu = ({ locale }: { locale: string }) => {
window?.location?.replace(`/${paths.split('/')[1]}/`); window?.location?.replace(`/${paths.split('/')[1]}/`);
}; };
const onDeleteAccount = () => setShowDeleteModal(true);
return ( return (
<ul className="list-sidebar"> <ul className="list-sidebar">
{menu.map(({ path, title, count }) => ( {menu.map(({ path, title, count }) => (
@ -46,19 +41,6 @@ export const AccountMenu = ({ locale }: { locale: string }) => {
{i18nText('logout', locale)} {i18nText('logout', locale)}
</Button> </Button>
</li> </li>
<li className="list-sidebar__item">
<Button
type="link"
onClick={onDeleteAccount}
className="b-button__logout"
>
{i18nText('deleteAcc', locale)}
</Button>
<DeleteAccountModal
open={showDeleteModal}
handleCancel={() => setShowDeleteModal(false)}
/>
</li>
</ul> </ul>
); );
}; };

View File

@ -1,25 +1,39 @@
'use client'; 'use client';
import React, { FC, useEffect, useState } from 'react'; import React, { FC, useEffect, useState } from 'react';
import { Form, Upload, Button } from 'antd'; import { Button, Form, message, Upload } from 'antd';
import type { UploadFile, UploadProps } from 'antd'; import type { GetProp, UploadFile, UploadProps } from 'antd';
import ImgCrop from 'antd-img-crop'; import ImgCrop from 'antd-img-crop';
import { CameraOutlined } from '@ant-design/icons'; import { CameraOutlined, DeleteOutlined } from '@ant-design/icons';
import { Link } from '../../navigation'; import { useRouter } from '../../navigation';
import { CustomInput } from '../view/CustomInput';
import { Profile } from '../../types/profile';
import { useProfileSettings } from '../../actions/hooks/useProfileSettings';
import { i18nText } from '../../i18nKeys'; import { i18nText } from '../../i18nKeys';
import { ProfileRequest } from '../../types/profile';
import { validateImage } from '../../utils/account';
import { useProfileSettings } from '../../actions/hooks/useProfileSettings';
import { CustomInput } from '../view/CustomInput';
import { OutlinedButton } from '../view/OutlinedButton';
import {FilledButton, FilledSquareButton, FilledYellowButton} from '../view/FilledButton';
import { DeleteAccountModal } from '../Modals/DeleteAccountModal';
import { Loader } from '../view/Loader';
import {ButtonProps} from "antd/es/button/button";
type ProfileSettingsProps = { type ProfileSettingsProps = {
locale: string; locale: string;
}; };
// type FileType = Parameters<GetProp<UploadProps, 'beforeUpload'>>[0]; type FileType = Parameters<GetProp<UploadProps, 'beforeUpload'>>[0];
export const ProfileSettings: FC<ProfileSettingsProps> = ({ locale }) => { export const ProfileSettings: FC<ProfileSettingsProps> = ({ locale }) => {
const [form] = Form.useForm<Profile>(); const [form] = Form.useForm<ProfileRequest>();
const { profileSettings } = useProfileSettings(locale); const { profileSettings, fetchProfileSettings, save, fetchLoading } = useProfileSettings(locale);
const [showDeleteModal, setShowDeleteModal] = useState<boolean>(false);
const [saveLoading, setSaveLoading] = useState<boolean>(false);
const [photo, setPhoto] = useState<UploadFile | undefined>();
const router = useRouter();
useEffect(() => {
fetchProfileSettings()
}, []);
useEffect(() => { useEffect(() => {
if (profileSettings) { if (profileSettings) {
@ -27,48 +41,115 @@ export const ProfileSettings: FC<ProfileSettingsProps> = ({ locale }) => {
} }
}, [profileSettings]); }, [profileSettings]);
const [fileList, setFileList] = useState<UploadFile[]>(); const onSave = (newProfile: ProfileRequest) => {
setSaveLoading(true);
save(newProfile)
.then(() => {
fetchProfileSettings();
})
.catch(() => {
message.error('Не удалось сохранить изменения');
})
.finally(() => {
setSaveLoading(false);
})
}
const onChange: UploadProps['onChange'] = ({ fileList: newFileList }) => { const onSaveProfile = () => {
setFileList(newFileList); form.validateFields()
.then(({ login, surname, username }) => {
const { phone, role, languagesLinks } = profileSettings;
const newProfile: ProfileRequest = {
phone,
role,
login,
surname,
username,
isPasswordKeepExisting: true,
isFaceImageKeepExisting: true,
languagesLinks: languagesLinks?.map(({ languageId }) => ({ languageId })) || []
}; };
const onPreview = async (file: UploadFile) => { if (photo) {
// let src = file.url as string; const reader = new FileReader();
// if (!src) { reader.readAsDataURL(photo as File);
// src = await new Promise((resolve) => { reader.onloadend = () => {
// const reader = new FileReader(); const newReg = new RegExp('data:image/(png|jpg|jpeg);base64,')
// reader.readAsDataURL(file.originFileObj as FileType); newProfile.faceImage = reader.result.replace(newReg, '');
// reader.onload = () => resolve(reader.result as string); newProfile.isFaceImageKeepExisting = false;
// });
// } onSave(newProfile);
// const image = new Image(); }
// image.src = src; } else {
// const imgWindow = window.open(src); onSave(newProfile);
// imgWindow?.document.write(image.outerHTML); }
}; })
}
const beforeCrop = (file: UploadFile) => {
return validateImage(file, true);
}
const beforeUpload = (file: UploadFile) => {
const isValid = validateImage(file);
if (isValid) {
setPhoto(file);
}
return false;
}
const onDeleteAccount = () => setShowDeleteModal(true);
return ( return (
<Loader isLoading={fetchLoading} refresh={fetchProfileSettings}>
<Form form={form} className="form-settings"> <Form form={form} className="form-settings">
<ImgCrop
modalTitle="Редактировать"
modalOk="Сохранить"
modalCancel="Отмена"
modalProps={{
okButtonProps: { className: 'b-button__filled_yellow' },
cancelButtonProps: { className: 'b-button__outlined' }
}}
beforeCrop={beforeCrop}
>
<Upload
fileList={photo ? [photo] : profileSettings?.faceImageUrl ? [
{
uid: profileSettings.faceImageUrl,
name: profileSettings.faceImageUrl,
status: 'done',
url: profileSettings.faceImageUrl
}
] : undefined}
accept=".jpg,.jpeg,.png"
beforeUpload={beforeUpload}
multiple={false}
showUploadList={false}
>
<div className="user-avatar"> <div className="user-avatar">
<div className="user-avatar__edit" style={profileSettings?.faceImageUrl ? { backgroundImage: `url(${profileSettings.faceImageUrl})` } : undefined}> <div className="user-avatar__edit" style={photo
<input className="" type="file" id="input-file" /> ? { backgroundImage: `url(${URL.createObjectURL(photo)})` }
<label htmlFor="input-file" className="form-label" /> : profileSettings?.faceImageUrl ? { backgroundImage: `url(${profileSettings.faceImageUrl})`} : undefined }>
<FilledSquareButton
type="primary"
icon={<CameraOutlined style={{ fontSize: 28 }} />}
/>
</div> </div>
<div className="user-avatar__text">{i18nText('photoDesc', locale)}</div> <div className="user-avatar__text">{i18nText('photoDesc', locale)}</div>
</div> </div>
{/* <ImgCrop rotationSlider>
<Upload
action="https://run.mocky.io/v3/435e224c-44fb-4773-9faf-380c5e6a2188"
fileList={fileList}
onChange={onChange}
onPreview={onPreview}
>
<Button icon={<CameraOutlined />}>Click to Upload</Button>
</Upload> </Upload>
</ImgCrop> */} </ImgCrop>
<div className="form-fieldset">
<div className="form-field"> <div className="form-field">
<Form.Item name="username"> <Form.Item name="username" rules={[
{
required: true,
message: 'Поле не должно быть пустым'
}
]}>
<CustomInput placeholder={i18nText('name', locale)} /> <CustomInput placeholder={i18nText('name', locale)} />
</Form.Item> </Form.Item>
</div> </div>
@ -83,16 +164,39 @@ export const ProfileSettings: FC<ProfileSettingsProps> = ({ locale }) => {
</Form.Item> </Form.Item>
</div> */} </div> */}
<div className="form-field"> <div className="form-field">
<Form.Item name="login"> <Form.Item name="login" rules={[
{
required: true,
message: 'Поле не должно быть пустым'
}
]}>
<CustomInput type="email" placeholder="E-mail" /> <CustomInput type="email" placeholder="E-mail" />
</Form.Item> </Form.Item>
</div> </div>
<div className="form-link">
<Link href={'change-password' as any}>
{i18nText('changePass', locale)}
</Link>
</div> </div>
<button className="btn-apply">{i18nText('save', locale)}</button> <div className="form-actions">
<FilledYellowButton
onClick={onSaveProfile}
loading={saveLoading}
>
{i18nText('save', locale)}
</FilledYellowButton>
<OutlinedButton onClick={() => router.push('change-password')}>
{i18nText('changePass', locale)}
</OutlinedButton>
<OutlinedButton
onClick={onDeleteAccount}
icon={<DeleteOutlined />}
danger
>
{i18nText('deleteAcc', locale)}
</OutlinedButton>
</div>
<DeleteAccountModal
open={showDeleteModal}
handleCancel={() => setShowDeleteModal(false)}
/>
</Form> </Form>
</Loader>
); );
}; };

View File

@ -1,19 +1,19 @@
'use client' 'use client'
import React, {useState} from 'react'; import React, { useState } from 'react';
import {Button, Empty, notification, Tag} from 'antd'; import { Button, Empty, notification, Tag } from 'antd';
import {LeftOutlined, PlusOutlined, RightOutlined} from '@ant-design/icons'; import { LeftOutlined, PlusOutlined, RightOutlined } from '@ant-design/icons';
import Image from 'next/image'; import Image from 'next/image';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import {Link, useRouter} from '../../../navigation'; import { Link, useRouter } from '../../../navigation';
import {i18nText} from '../../../i18nKeys'; import { i18nText } from '../../../i18nKeys';
import {getDuration, getPrice} from '../../../utils/expert'; import { getDuration, getPrice } from '../../../utils/expert';
import {PublicUser, Session, SessionState, SessionType} from '../../../types/sessions'; import { PublicUser, Session, SessionState, SessionType } from '../../../types/sessions';
import {AUTH_TOKEN_KEY} from '../../../constants/common'; import { AUTH_TOKEN_KEY } from '../../../constants/common';
import {approveRequestedSession, finishSession} from '../../../actions/sessions'; import { approveRequestedSession, finishSession } from '../../../actions/sessions';
import {useLocalStorage} from '../../../hooks/useLocalStorage'; import { useLocalStorage } from '../../../hooks/useLocalStorage';
import {DeclineSessionModal} from '../../Modals/DeclineSessionModal'; import { DeclineSessionModal } from '../../Modals/DeclineSessionModal';
import {AddCommentModal} from '../../Modals/AddCommentModal'; import { AddCommentModal } from '../../Modals/AddCommentModal';
type SessionDetailsContentProps = { type SessionDetailsContentProps = {
locale: string; locale: string;
@ -43,7 +43,7 @@ export const SessionDetailsContent = ({ session, locale, activeType, startSessio
}) })
.catch((err) => { .catch((err) => {
notification.error({ notification.error({
message: 'Error approve session', message: i18nText('errors.approvingSession', locale),
description: err?.response?.data?.errMessage description: err?.response?.data?.errMessage
}); });
}) })
@ -64,7 +64,7 @@ export const SessionDetailsContent = ({ session, locale, activeType, startSessio
}) })
.catch((err) => { .catch((err) => {
notification.error({ notification.error({
message: 'Error finish session', message: i18nText('errors.finishingSession', locale),
description: err?.response?.data?.errMessage description: err?.response?.data?.errMessage
}); });
}) })
@ -166,7 +166,7 @@ export const SessionDetailsContent = ({ session, locale, activeType, startSessio
icon={<LeftOutlined />} icon={<LeftOutlined />}
onClick={goBack} onClick={goBack}
> >
Back {i18nText('back', locale)}
</Button> </Button>
</div> </div>
{Current} {Current}
@ -181,8 +181,8 @@ export const SessionDetailsContent = ({ session, locale, activeType, startSessio
disabled={finishLoading} disabled={finishLoading}
> >
{activeType === SessionType.UPCOMING {activeType === SessionType.UPCOMING
? (session?.state === SessionState.STARTED ? 'Join Session' : 'Start Session') ? (session?.state === SessionState.STARTED ? i18nText('session.join', locale) : i18nText('session.start', locale))
: 'Confirm Session'} : i18nText('session.confirm', locale)}
</Button> </Button>
{session?.state === SessionState.STARTED && isCoach && ( {session?.state === SessionState.STARTED && isCoach && (
<Button <Button
@ -190,7 +190,7 @@ export const SessionDetailsContent = ({ session, locale, activeType, startSessio
onClick={onFinishSession} onClick={onFinishSession}
loading={finishLoading} loading={finishLoading}
> >
Finish Session {i18nText('session.finish', locale)}
</Button> </Button>
)} )}
{session?.id && session?.state !== SessionState.STARTED && ( {session?.id && session?.state !== SessionState.STARTED && (
@ -200,7 +200,7 @@ export const SessionDetailsContent = ({ session, locale, activeType, startSessio
onClick={() => setOpenDeclineModal(true)} onClick={() => setOpenDeclineModal(true)}
disabled={approveLoading} disabled={approveLoading}
> >
Decline Session {i18nText('session.decline', locale)}
</Button> </Button>
<DeclineSessionModal <DeclineSessionModal
open={openDeclineModal} open={openDeclineModal}
@ -218,7 +218,7 @@ export const SessionDetailsContent = ({ session, locale, activeType, startSessio
<> <>
{activeType === SessionType.RECENT && ( {activeType === SessionType.RECENT && (
<> <>
<div className="card-detail__name">Course Info</div> <div className="card-detail__name">{i18nText('courseInfo', locale)}</div>
<div className="card-detail__inner"> <div className="card-detail__inner">
{/* <div className="card-detail__info"> {/* <div className="card-detail__info">
<div className="card-profile__subtitle">{current?.specialityDesc}</div> <div className="card-profile__subtitle">{current?.specialityDesc}</div>
@ -249,7 +249,9 @@ export const SessionDetailsContent = ({ session, locale, activeType, startSessio
<div className="card-detail__comments"> <div className="card-detail__comments">
<div className="card-detail__comments_header"> <div className="card-detail__comments_header">
<div className="card-detail__comments_title"> <div className="card-detail__comments_title">
{session?.clientComments?.length === 0 && session?.coachComments?.length === 0 ? 'Comments' : 'My Comments'} {session?.clientComments?.length === 0 && session?.coachComments?.length === 0
? i18nText('session.comments', locale)
: i18nText('session.myComments', locale)}
</div> </div>
{activeType === SessionType.UPCOMING && ( {activeType === SessionType.UPCOMING && (
<> <>
@ -260,7 +262,7 @@ export const SessionDetailsContent = ({ session, locale, activeType, startSessio
icon={<PlusOutlined style={{ fontSize: 18 }} />} icon={<PlusOutlined style={{ fontSize: 18 }} />}
onClick={() => setOpenAddCommentModal(true)} onClick={() => setOpenAddCommentModal(true)}
> >
Add new {i18nText('session.addComment', locale)}
</Button> </Button>
<AddCommentModal <AddCommentModal
open={openAddCommentModal} open={openAddCommentModal}
@ -281,7 +283,7 @@ export const SessionDetailsContent = ({ session, locale, activeType, startSessio
))} ))}
{(isCoach ? session?.clientComments : session?.coachComments)?.length > 0 && ( {(isCoach ? session?.clientComments : session?.coachComments)?.length > 0 && (
<div className="card-detail__comments_title"> <div className="card-detail__comments_title">
{isCoach ? 'Client Comments' : 'Coach Comments'} {isCoach ? i18nText('session.clientComments', locale) : i18nText('session.coachComments', locale)}
</div> </div>
)} )}
{(isCoach ? session?.clientComments : session?.coachComments)?.map(({ id , comment }) => ( {(isCoach ? session?.clientComments : session?.coachComments)?.map(({ id , comment }) => (

View File

@ -42,9 +42,9 @@ export const SessionsTabs = ({ locale, activeTab }: SessionsTabsProps) => {
]) ])
.then(([upcoming, requested, recent]) => { .then(([upcoming, requested, recent]) => {
setSessions({ setSessions({
[SessionType.UPCOMING]: upcoming.data || [], [SessionType.UPCOMING]: upcoming || [],
[SessionType.REQUESTED]: requested.data?.requestedSessions || [], [SessionType.REQUESTED]: requested?.requestedSessions || [],
[SessionType.RECENT]: recent.data || [] [SessionType.RECENT]: recent || []
}); });
}) })
.catch((err) => { .catch((err) => {
@ -115,7 +115,7 @@ export const SessionsTabs = ({ locale, activeTab }: SessionsTabsProps) => {
</div> </div>
) )
}) : ( }) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} /> <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={i18nText('noData', locale)} />
)} )}
</div> </div>
</> </>

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

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

View File

@ -0,0 +1,172 @@
'use client'
import React, { useState } from 'react';
import {Alert, message} from 'antd';
import Image from 'next/image';
import { i18nText } from '../../i18nKeys';
import { ExpertData, PayInfo, ProfileData } from '../../types/profile';
import { ExpertsTags } from '../../types/tags';
import { PracticeDTO } from '../../types/practice';
import { EducationDTO } from '../../types/education';
import { ScheduleDTO } from '../../types/schedule';
import { AUTH_TOKEN_KEY } from '../../constants/common';
import { useLocalStorage } from '../../hooks/useLocalStorage';
import { getTags, getPayData, getEducation, getPractice, getSchedule, getPersonalData } from '../../actions/profile';
import { Loader } from '../view/Loader';
import { ExpertTags } from './content/ExpertTags';
import { ExpertSchedule } from './content/ExpertSchedule';
import { ExpertPayData } from './content/ExpertPayData';
import { ExpertEducation } from './content/ExpertEducation';
import { ExpertAbout } from './content/ExpertAbout';
type ExpertProfileProps = {
locale: string;
data: ExpertData;
updateData: (data: ExpertData) => void;
isFull: boolean;
};
type NewDataPartProps<T> = {
key: keyof ExpertData,
getNewData: (locale: string, token: string) => Promise<T>,
errorMessage?: string;
};
export const ExpertProfile = ({ locale, data, updateData, isFull }: ExpertProfileProps) => {
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
const [loading, setLoading] = useState<(keyof ExpertData)[]>([]);
function getNewPartData <T>({ key, getNewData, errorMessage = 'Не удалось получить данные' }: NewDataPartProps<T>) {
setLoading([key]);
getNewData(locale, jwt)
.then((newData) => {
updateData({
...data,
[key]: newData
});
})
.catch(() => message.error(errorMessage))
.finally(() => setLoading([]));
}
const updateExpert = (key: keyof ExpertData) => {
switch (key) {
case 'tags':
getNewPartData<ExpertsTags>({
key,
getNewData: getTags,
errorMessage: 'Не удалось получить направления'
});
break;
case 'practice':
getNewPartData<PracticeDTO>({
key,
getNewData: getPractice
});
break;
case 'education':
getNewPartData<EducationDTO>({
key,
getNewData: getEducation,
errorMessage: 'Не удалось получить информацию об образовании'
});
break;
case 'schedule':
getNewPartData<ScheduleDTO>({
key,
getNewData: getSchedule,
errorMessage: 'Не удалось получить расписание'
});
break;
case 'person':
getNewPartData<ProfileData>({
key,
getNewData: getPersonalData,
errorMessage: 'Не удалось получить информацию о пользователе'
});
break;
case 'payData':
getNewPartData<{ person6Data?: PayInfo }>({
key,
getNewData: getPayData,
errorMessage: 'Не удалось получить платежную информацию'
});
break;
default:
break;
}
};
return (
<>
<ol className="breadcrumb">
<li className="breadcrumb-item active" aria-current="page">{i18nText('coaching', locale)}</li>
</ol>
<div className="coaching-info">
<div className="coaching-profile">
<div className="coaching-profile__portrait">
<Image src={data?.person?.faceImageUrl || '/images/user-avatar.png'} width={216} height={216} alt="" />
</div>
<div className="coaching-profile__inner" style={{ flex: 1 }}>
<div className="coaching-profile__name">
{`${data?.person?.username} ${data?.person?.surname || ''}`}
</div>
{!isFull && (
<Alert
message="Проверьте заполненность блоков"
description={(
<ul className="b-rules-list">
<li>о себе</li>
<li>темы сессии</li>
<li>рабочее расписание</li>
<li>информация об образовании</li>
<li>платежная информация</li>
</ul>
)}
type="warning"
showIcon
/>
)}
</div>
</div>
<Loader isLoading={loading.includes('practice') || loading.includes('person')}>
<ExpertAbout
locale={locale}
practice={data?.practice}
person={data?.person}
updateExpert={updateExpert}
/>
</Loader>
<Loader isLoading={loading.includes('tags')}>
<ExpertTags
locale={locale}
data={data?.tags}
updateExpert={updateExpert}
/>
</Loader>
<Loader isLoading={loading.includes('schedule')}>
<ExpertSchedule
locale={locale}
data={data?.schedule}
updateExpert={updateExpert}
/>
</Loader>
<Loader isLoading={loading.includes('education')}>
<ExpertEducation
locale={locale}
data={data?.education}
updateExpert={updateExpert}
/>
</Loader>
<Loader isLoading={loading.includes('payData')}>
<ExpertPayData
locale={locale}
data={data?.payData?.person6Data}
updateExpert={updateExpert}
/>
</Loader>
</div>
</>
);
};

View File

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

View File

@ -0,0 +1,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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,36 +1,29 @@
'use client'; 'use client';
import React, {FC, useEffect, useState} from 'react'; 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, ExpertScheduler} 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 {getSchedulerByExpertId} from "../../actions/experts"; import { i18nText } from '../../i18nKeys';
import {useLocalStorage} from "../../hooks/useLocalStorage"; import { FilledYellowButton } from '../view/FilledButton';
import {AUTH_TOKEN_KEY} from "../../constants/common";
import dayjs from "dayjs";
import {SchedulerModal} from "../Modals/SchedulerModal";
type ExpertDetailsProps = { type ExpertDetailsProps = {
expert: ExpertDetails; expert: ExpertDetails;
locale?: string; locale?: string;
expertId?: string;
}; };
export const ExpertCard: FC<ExpertDetailsProps> = ({ expert, locale, expertId }) => { type ExpertPracticeProps = {
const { publicCoachDetails } = expert || {}; cases?: Practice[];
const [showSchedulerModal, setShowSchedulerModal] = useState<boolean>(false); themes?: ThemeGroup[];
const [mode, setMode] = useState<'data' | 'time' | 'pay' | 'finish'>('data'); locale?: string;
const { publicCoachDetails: { tags = [], sessionCost = 0, sessionDuration = 0 } } = expert || {}; };
const onSchedulerHandle = async () => { export const ExpertCard: FC<ExpertDetailsProps> = ({ expert, locale }) => {
console.log('sessionCost', sessionCost); const { publicCoachDetails } = expert || {};
setMode('data');
setShowSchedulerModal(true)
// отмаппим.
}
return ( return (
<div className="expert-card"> <div className="expert-card">
@ -41,50 +34,45 @@ export const ExpertCard: FC<ExpertDetailsProps> = ({ expert, locale, expertId })
<div className="expert-card__inner"> <div className="expert-card__inner">
<h1 className="expert-card__title">{`${publicCoachDetails?.name} ${publicCoachDetails?.surname || ''}`}</h1> <h1 className="expert-card__title">{`${publicCoachDetails?.name} ${publicCoachDetails?.surname || ''}`}</h1>
<div className="expert-card__info"> <div className="expert-card__info">
<span>{`${publicCoachDetails?.practiceHours} Practice hours`}</span> <span>{`${publicCoachDetails?.practiceHours} ${i18nText('practiceHours', locale)}`}</span>
<i>|</i> <i>|</i>
<span>{`${publicCoachDetails?.supervisionPerYearId} Supervision per year`}</span> <span>{`${publicCoachDetails?.supervisionPerYearId} ${i18nText('supervisionCount', locale)}`}</span>
</div> </div>
<div className="expert-card__rating"> <div className="expert-card__rating">
<CustomRate defaultValue={4} character={<StarFilled style={{ fontSize: 32 }} />} disabled /> <CustomRate defaultValue={4} character={<StarFilled style={{ fontSize: 32 }} />} disabled />
<span>4/5 (out of 345)</span> <span>{`4/5 (${i18nText('outOf', locale)} 345)`}</span>
</div> </div>
</div> </div>
</div> </div>
<div className="expert-card__wrap-btn"> <div className="expert-card__wrap-btn">
<a href="#" className="btn-apply" onClick={onSchedulerHandle}> <a href="#" className="btn-apply">
<img src="/images/calendar-outline.svg" className="" alt="" /> <img src="/images/calendar-outline.svg" className="" alt="" />
Schedule {i18nText('schedule', locale)}
</a> </a>
{/*
<a href="#" className="btn-video"> <a href="#" className="btn-video">
<img src="/images/videocam-outline.svg" className="" alt="" /> <img src="/images/videocam-outline.svg" className="" alt=""/>
Video Video
</a> </a>
*/}
</div> </div>
<SchedulerModal
open={showSchedulerModal}
handleCancel={() => setShowSchedulerModal(false)}
updateMode={setMode}
mode={mode}
expertId={expertId as string}
locale={locale as string}
sessionCost={sessionCost}
/>
</div> </div>
); );
}; };
export const ExpertInformation: FC<ExpertDetailsProps> = ({ expert, locale }) => { export const ExpertInformation: FC<ExpertDetailsProps> = ({ expert, locale }) => {
const { publicCoachDetails: { tags = [], sessionCost = 0, sessionDuration = 0 } } = expert || {}; const { publicCoachDetails: { tags = [], sessionCost = 0, sessionDuration = 0, coachLanguages = [] } } = expert || {};
const isRus = locale === Locale.ru; const isRus = locale === Locale.ru;
return ( return (
<> <>
<h2 className="title-h2">Current Offer</h2> <div className="expert-info">
{/* <h2 className="title-h2">{}</h2> */}
<div className="skills__list"> <div className="skills__list">
{tags?.map((skill) => <Tag key={skill?.id} className="skills__list__item">{skill?.name}</Tag>)} {coachLanguages?.map((lang) => <Tag key={lang} className="skills__list__item">{lang}</Tag>)}
</div> </div>
<p className="base-text"> </div>
{/* <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
@ -98,11 +86,12 @@ 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">
{tags?.map((skill) => <Tag key={skill?.id} className="skills__list__item">{skill?.name}</Tag>)}
</div>
<div className="wrap-btn-prise"> <div className="wrap-btn-prise">
<a href="#" className="btn-apply"> <FilledYellowButton onClick={() => console.log('schedule')}>{i18nText('signUp', locale)}</FilledYellowButton>
Sign Up Now
</a>
<div className="wrap-btn-prise__text"> <div className="wrap-btn-prise__text">
{`${sessionCost}`} <span>/ {`${sessionDuration}${isRus ? 'мин' : 'min'}`}</span> {`${sessionCost}`} <span>/ {`${sessionDuration}${isRus ? 'мин' : 'min'}`}</span>
</div> </div>
@ -111,21 +100,19 @@ export const ExpertInformation: FC<ExpertDetailsProps> = ({ expert, locale }) =>
); );
}; };
export const ExpertPractice: FC<ExpertDetailsProps> = ({ expert }) => { 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">Successful Cases From Practice</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">
{themesGroupIds && ( {themesGroupIds && (
<div className="skills__list"> <div className="skills__list">
{filtered?.map(({ id, name }) => ( {filtered?.map(({ id, name }) => (
<div key={id} className="skills__list__item">{name}</div> <Tag key={id} className="skills__list__item">{name}</Tag>
))} ))}
</div> </div>
)} )}

View File

@ -84,6 +84,7 @@ export const ExpertsList = ({
size="large" size="large"
className="search-result" className="search-result"
dataSource={experts.coaches} dataSource={experts.coaches}
locale={{ emptyText: i18nText('notFound', locale) }}
renderItem={(item) => ( renderItem={(item) => (
<List.Item key={item?.id} className="card-profile"> <List.Item key={item?.id} className="card-profile">
<List.Item.Meta <List.Item.Meta

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,24 +163,21 @@ 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);
} }
return searchData?.themesGroups?.length ? searchData.themesGroups.map(({ id, name, tags }) => ( return searchData?.themesGroups?.length ? searchData.themesGroups.map(({ id, name, tags }) => (
<div key={id}> <div key={id}>
<h3 className="title-h5">{name}</h3> <h3 className="title-h4">{name}</h3>
{getList('themesTagIds', tags)} {getList('themesTagIds', tags)}
</div> </div>
)) : null; )) : null;
@ -214,7 +212,7 @@ export const ExpertsFilter = ({
key: 'themesTagIds', key: 'themesTagIds',
label: ( label: (
<> <>
<div className="b-filter__collapsed__title">Direction</div> <div className="b-filter__collapsed__title">{i18nText('direction', locale)}</div>
{!openedTabs.includes('themesTagIds') && filter?.themesTagIds?.length > 0 && ( {!openedTabs.includes('themesTagIds') && filter?.themesTagIds?.length > 0 && (
<div className="b-filter__collapsed__desc">{getSelectedTags()}</div> <div className="b-filter__collapsed__desc">{getSelectedTags()}</div>
)} )}

View File

@ -83,7 +83,7 @@ export const AddCommentModal: FC<AddCommentModalProps> = ({
rules={[ rules={[
{ {
required: true, required: true,
message: 'Please input your comment' message: i18nText('errors.emptyComment', locale)
} }
]} ]}
> >
@ -91,7 +91,7 @@ export const AddCommentModal: FC<AddCommentModalProps> = ({
className="b-textarea" className="b-textarea"
rows={4} rows={4}
maxLength={1000} maxLength={1000}
placeholder="Your comment" placeholder={i18nText('session.commentPlaceholder', locale)}
/> />
</Form.Item> </Form.Item>
</Form> </Form>
@ -101,7 +101,7 @@ export const AddCommentModal: FC<AddCommentModalProps> = ({
onClick={onAddComment} onClick={onAddComment}
loading={loading} loading={loading}
> >
Send {i18nText('send', locale)}
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -6,6 +6,7 @@ import Link from 'next/link';
import { Modal, Form } from 'antd'; import { Modal, Form } from 'antd';
import { CloseOutlined } from '@ant-design/icons'; import { CloseOutlined } from '@ant-design/icons';
import { RegisterContent, ResetContent, FinishContent, EnterContent } from './authModalContent'; import { RegisterContent, ResetContent, FinishContent, EnterContent } from './authModalContent';
import { i18nText } from '../../i18nKeys';
type AuthModalProps = { type AuthModalProps = {
open: boolean; open: boolean;
@ -13,6 +14,7 @@ type AuthModalProps = {
mode: 'enter' | 'register' | 'reset' | 'finish'; mode: 'enter' | 'register' | 'reset' | 'finish';
updateMode: (mode: 'enter' | 'register' | 'reset' | 'finish') => void; updateMode: (mode: 'enter' | 'register' | 'reset' | 'finish') => void;
updateToken: string | Dispatch<SetStateAction<string | undefined>> | undefined; updateToken: string | Dispatch<SetStateAction<string | undefined>> | undefined;
locale: string;
}; };
export const AuthModal: FC<AuthModalProps> = ({ export const AuthModal: FC<AuthModalProps> = ({
@ -20,7 +22,8 @@ export const AuthModal: FC<AuthModalProps> = ({
handleCancel, handleCancel,
mode, mode,
updateMode, updateMode,
updateToken updateToken,
locale
}) => { }) => {
const [form] = Form.useForm<{ login: string, password: string, confirmPassword: string }>(); const [form] = Form.useForm<{ login: string, password: string, confirmPassword: string }>();
const paths = usePathname().split('/'); const paths = usePathname().split('/');
@ -50,7 +53,7 @@ export const AuthModal: FC<AuthModalProps> = ({
onCancel={handleCancel} onCancel={handleCancel}
afterClose={onAfterClose} afterClose={onAfterClose}
footer={false} footer={false}
width={498} width={598}
closeIcon={<CloseOutlined style={{ fontSize: 20, color: '#000' }}/>} closeIcon={<CloseOutlined style={{ fontSize: 20, color: '#000' }}/>}
> >
<div className="b-modal__auth__content"> <div className="b-modal__auth__content">
@ -82,8 +85,8 @@ export const AuthModal: FC<AuthModalProps> = ({
<FinishContent locale={paths[1]} /> <FinishContent locale={paths[1]} />
)} )}
<div className="b-modal__auth__agreement"> <div className="b-modal__auth__agreement">
I have read and agree with the terms of the {`${i18nText('agreementText', locale)} `}
User Agreement, <Link href={'/docs/BBUDDY_privacy_policy_fin.docx' as any}>Privacy Policy</Link> <Link href={'/docs/BBUDDY_privacy_policy_fin.docx' as any}>{i18nText('privacyPolicy', locale)}</Link>
</div> </div>
</div> </div>
</Modal> </Modal>

View File

@ -7,7 +7,7 @@ import { SessionType } from '../../types/sessions';
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 { cancelUpcomingSession, declineRequestedSession } from '../../actions/sessions'; import { cancelUpcomingSession, declineRequestedSession } from '../../actions/sessions';
// import { i18nText } from '../../i18nKeys'; import { i18nText } from '../../i18nKeys';
import { FilledButton } from '../view/FilledButton'; import { FilledButton } from '../view/FilledButton';
type DeclineModalProps = { type DeclineModalProps = {
@ -79,7 +79,7 @@ export const DeclineSessionModal: FC<DeclineModalProps> = ({
<img className="" src="/images/decline-sign.svg" alt=""/> <img className="" src="/images/decline-sign.svg" alt=""/>
</div> </div>
<div className="b-modal__decline__title"> <div className="b-modal__decline__title">
Enter a reason for cancelling the session {i18nText('session.cancelReason', locale)}
</div> </div>
<Form form={form} style={{ width: '100%' }}> <Form form={form} style={{ width: '100%' }}>
<Form.Item <Form.Item
@ -88,14 +88,14 @@ export const DeclineSessionModal: FC<DeclineModalProps> = ({
rules={[ rules={[
{ {
required: true, required: true,
message: 'Please input the reason' message: i18nText('errors.emptyCancelReason', locale)
} }
]} ]}
> >
<Input.TextArea <Input.TextArea
className="b-textarea" className="b-textarea"
rows={1} rows={1}
placeholder="Describe the reason for the rejection" placeholder={i18nText('session.reasonPlaceholder', locale)}
/> />
</Form.Item> </Form.Item>
</Form> </Form>
@ -106,7 +106,7 @@ export const DeclineSessionModal: FC<DeclineModalProps> = ({
onClick={onDecline} onClick={onDecline}
loading={loading} loading={loading}
> >
Decline {i18nText('decline', locale)}
</FilledButton> </FilledButton>
</div> </div>
</div> </div>

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

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

View File

@ -1,290 +0,0 @@
'use client';
import React, {Dispatch, FC, SetStateAction, useEffect, useState} from 'react';
import { usePathname } from 'next/navigation';
import classNames from 'classnames';
import Link from 'next/link';
import {Modal, Form, Calendar, Radio } from 'antd';
import type { CalendarProps, RadioChangeEvent } from 'antd';
import { CloseOutlined } from '@ant-design/icons';
import { RegisterContent, ResetContent, FinishContent, EnterContent } from './authModalContent';
import dayjs, { Dayjs } from 'dayjs';
import {ExpertDetails, ExpertScheduler, Tags} from "../../types/experts";
import { createStyles } from 'antd-style';
import {useLocalStorage} from "../../hooks/useLocalStorage";
import {AUTH_TOKEN_KEY} from "../../constants/common";
import {getSchedulerByExpertId, getSchedulerSession} from "../../actions/experts";
import {ElementsForm} from "../stripe/ElementsForm";
type SchedulerModalProps = {
open: boolean;
handleCancel: () => void;
mode: 'data' | 'time' | 'pay' | 'finish';
updateMode: (mode: 'data' | 'time' | 'pay' | 'finish') => void;
sessionCost: number;
expertId: string;
locale: string;
};
const useStyle = createStyles(({ token, css, cx }) => {
const lunar = css`
color: ${token.colorTextTertiary};
font-size: ${token.fontSizeSM}px;
`;
return {
wrapper: css`
width: 450px;
border: 1px solid ${token.colorBorderSecondary};
border-radius: ${token.borderRadiusOuter};
padding: 5px;
`,
dateCell: css`
position: relative;
&:before {
content: '';
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
max-width: 40px;
max-height: 40px;
background: transparent;
transition: background 300ms;
border-radius: ${token.borderRadiusOuter}px;
border: 1px solid transparent;
box-sizing: border-box;
}
&:hover:before {
background: rgba(0, 0, 0, 0.04);
}
`,
today: css`
&:before {
border: 1px solid ${token.colorPrimary};
}
`,
text: css`
position: relative;
z-index: 1;
`,
lunar,
current: css`
color: ${token.colorTextLightSolid};
&:before {
background: ${token.colorPrimary};
}
&:hover:before {
background: ${token.colorPrimary};
opacity: 0.8;
}
.${cx(lunar)} {
color: ${token.colorTextLightSolid};
opacity: 0.9;
}
`,
monthCell: css`
width: 120px;
color: ${token.colorTextBase};
border-radius: ${token.borderRadiusOuter}px;
padding: 5px 0;
&:hover {
background: rgba(0, 0, 0, 0.04);
}
`,
monthCellCurrent: css`
color: ${token.colorTextLightSolid};
background: ${token.colorPrimary};
&:hover {
background: ${token.colorPrimary};
opacity: 0.8;
}
`,
weekend: css`
color: ${token.colorError};
&.gray {
opacity: 0.4;
}
`,
};
});
export const SchedulerModal: FC<SchedulerModalProps> = ({
open,
handleCancel,
mode,
updateMode,
sessionCost,
locale,
expertId,
}) => {
const { styles } = useStyle({ test: true });
const [selectDate, setSelectDate] = React.useState<Dayjs>(dayjs());
const [dates, setDates] = React.useState<any>();
const [tags, setTags] = React.useState<Tags[]>([]);
const [tag, setTag] = React.useState<number>(-1);
const [slot, setSlot] = React.useState<string>('');
const [sessionId, setSessionId] = React.useState<number>(-1);
const [rawScheduler, setRawScheduler] = useState<ExpertScheduler | null>(null);
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
useEffect( ()=> {
async function loadScheduler(){
const rawScheduler = await getSchedulerByExpertId(expertId as string, locale as string, jwt)
setRawScheduler(rawScheduler)
}
if (open) {
loadScheduler()
}
}, [open])
useEffect(() => {
const map = {} as any
rawScheduler?.availableSlots.forEach((el)=>{
const key = dayjs(el.startTime).format('YYYY-MM-DD');
if (!map[key]){
map[key] = []
}
map[key].push(el)
})
console.log(rawScheduler, map)
setDates(map)
setTags(rawScheduler?.tags)
}, [rawScheduler]);
const onPanelChange = (value: Dayjs, mode: CalendarProps<Dayjs>['mode']) => {
console.log(value.format('YYYY-MM-DD'), mode);
};
const onDateChange: CalendarProps<Dayjs>['onSelect'] = (value, selectInfo) => {
if (selectInfo.source === 'date') {
setSelectDate(value);
updateMode('time')
}
};
const disabledDate = (currentDate: Dayjs) => {
return !dates || !dates[currentDate.format('YYYY-MM-DD')]
}
const handleTimeslot = (e: RadioChangeEvent) => {
setSlot(e.target.value.startTime)
console.log('radio checked', e.target.value);
};
const handleTag = (e: RadioChangeEvent) => {
setTag(e.target.value)
console.log('tag radio checked', e.target.value);
};
const handleSingupSession = async () => {
const data = {coachId: expertId, tagId: tag, startAtUtc: slot, clientComment:''}
console.log(data)
const session = await getSchedulerSession({coachId: expertId, tagId: tag, startAtUtc: slot, clientComment:''}, locale, jwt)
console.log(session);
// тут должна быть проверка все ли с регистрацией сессии
setSessionId(session?.sessionId)
updateMode('pay')
}
const currentDay = dayjs()
const cellRender: CalendarProps<Dayjs>['fullCellRender'] = (date, info) => {
const isWeekend = date.day() === 6 || date.day() === 0;
return React.cloneElement(info.originNode, {
...info.originNode.props,
className: classNames(styles.dateCell, {
[styles.current]: selectDate.isSame(date, 'date'),
[styles.today]: date.isSame(dayjs(), 'date'),
}),
children: (
<div className={styles.text}>
<span
className={classNames({
[styles.weekend]: isWeekend,
})}
>
{date.get('date')}
</span>
</div>
),
});
}
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>
{tags && (
<Radio.Group name="radiogroupTags" onChange={handleTag}>
{tags?.map((tag)=>(
<Radio key={tag.id} value={tag.id}>{tag.name}</Radio>
)
)}
</Radio.Group>
)
}
</div>
{mode === 'data' && (
<Calendar
fullscreen={false}
onPanelChange={onPanelChange}
fullCellRender={cellRender}
onSelect={onDateChange}
disabledDate={disabledDate}
headerRender={({ value, type, onChange, onTypeChange }) => {
const start = 0;
const end = 12;
const monthOptions = [];
let current = currentDay.clone();
const localeData = value.locale();
const months = [];
for(let i=0; i<6; i++){
const m = current.clone()
months.push(m);
current = current.add(1,'month')
}
return (<>
{months.map((m, i)=>(
<button key={'SchedulerMonth'+i} onClick={()=> onChange(m)}>
{m.month()}
</button>
))}
</>)
}}
/>
)}
{mode === 'time' && (
<>
<div>
<button onClick={()=>{updateMode('data')}}>back</button>
</div>
<Radio.Group name="radiogroupSlots" onChange={handleTimeslot}>
{dates[selectDate.format('YYYY-MM-DD')].map( (el) => {
return (<Radio key={dayjs(el.startTime).format()} value={el}>{dayjs(el.startTime).format('hh-mm')} - {dayjs(el.endTime).format('hh-mm')}</Radio>)
})}
</Radio.Group>
<button onClick={handleSingupSession}>Записаться</button>
</>
)}
{mode === 'pay' && (
<ElementsForm amount={sessionCost}/>
)}
</Modal>
);
};

View File

@ -37,10 +37,10 @@ export const EnterContent: FC<EnterProps> = ({
const { login, password } = form.getFieldsValue(); const { login, password } = form.getFieldsValue();
setIsLoading(true); setIsLoading(true);
getAuth(locale, { login, password }) getAuth(locale, { login, password })
.then(({ data }) => { .then((data) => {
if (data.jwtToken) { if (data.jwtToken) {
getPersonalData(locale, data.jwtToken) getPersonalData(locale, data.jwtToken)
.then(({ data: profile }) => { .then((profile) => {
localStorage.setItem(AUTH_USER, JSON.stringify(profile)); localStorage.setItem(AUTH_USER, JSON.stringify(profile));
updateToken(data.jwtToken); updateToken(data.jwtToken);
handleCancel(); handleCancel();
@ -110,11 +110,11 @@ export const EnterContent: FC<EnterProps> = ({
rules={[ rules={[
{ {
type: 'email', type: 'email',
message: 'The input is not valid E-mail' message: i18nText('errors.validEmail', locale)
}, },
{ {
required: true, required: true,
message: 'Please input your E-mail' message: i18nText('error.emptyEmail', locale)
} }
]} ]}
> >
@ -129,7 +129,7 @@ export const EnterContent: FC<EnterProps> = ({
noStyle noStyle
rules={[{ rules={[{
required: true, required: true,
message: 'Please input your password' message: i18nText('errors.emptyPass', locale)
}]} }]}
> >
<CustomInputPassword <CustomInputPassword
@ -152,26 +152,26 @@ export const EnterContent: FC<EnterProps> = ({
type="link" type="link"
onClick={() => updateMode('reset')} onClick={() => updateMode('reset')}
> >
Forgot password? {`${i18nText('forgotPass', locale)}?`}
</LinkButton> </LinkButton>
<span>or</span> <span>{i18nText('or', locale)}</span>
<OutlinedButton <OutlinedButton
icon={<Image src="/images/facebook-logo.png" height={20} width={20} alt="" />} icon={<Image src="/images/facebook-logo.png" height={20} width={20} alt="" />}
onClick={() => onSocialEnter(Social.FACEBOOK)} onClick={() => onSocialEnter(Social.FACEBOOK)}
> >
Facebook account {i18nText('facebook', locale)}
</OutlinedButton> </OutlinedButton>
<OutlinedButton <OutlinedButton
icon={<Image src="/images/apple-logo.png" height={22} width={22} alt="" />} icon={<Image src="/images/apple-logo.png" height={22} width={22} alt="" />}
onClick={() => onSocialEnter(Social.APPLE)} onClick={() => onSocialEnter(Social.APPLE)}
> >
Apple account {i18nText('apple', locale)}
</OutlinedButton> </OutlinedButton>
<OutlinedButton <OutlinedButton
icon={<Image src="/images/google-logo.png" height={20} width={20} alt="" />} icon={<Image src="/images/google-logo.png" height={20} width={20} alt="" />}
onClick={() => onSocialEnter(Social.GOOGLE)} onClick={() => onSocialEnter(Social.GOOGLE)}
> >
Google account {i18nText('google', locale)}
</OutlinedButton> </OutlinedButton>
</> </>
); );

View File

@ -5,14 +5,10 @@ import { i18nText } from '../../../i18nKeys';
export const FinishContent = ({ locale }: { locale: string }) => ( export const FinishContent = ({ locale }: { locale: string }) => (
<> <>
<div className="b-modal__auth__agreement"> <div className="b-modal__auth__agreement">
A link to reset your password has been sent {i18nText('resetPassText', locale)}
<br />
to your email
</div> </div>
<FilledButton <FilledButton type="primary">
type="primary" {i18nText('enterAccount', locale)}
>
{i18nText('enter', locale)}
</FilledButton> </FilledButton>
</> </>
); );

View File

@ -36,10 +36,10 @@ export const RegisterContent: FC<RegisterProps> = ({
const { login, password } = form.getFieldsValue(); const { login, password } = form.getFieldsValue();
setIsLoading(true); setIsLoading(true);
getRegister(locale) getRegister(locale)
.then(({ data }) => { .then((data) => {
if (data.jwtToken) { if (data.jwtToken) {
setPersonData( { login, password, role: 'client', languagesLinks: [] }, locale, data.jwtToken) setPersonData( { login, password, role: 'client', languagesLinks: [] }, locale, data.jwtToken)
.then(({ data: profile }) => { .then((profile) => {
updateToken(data.jwtToken); updateToken(data.jwtToken);
localStorage.setItem(AUTH_USER, JSON.stringify(profile.userData)); localStorage.setItem(AUTH_USER, JSON.stringify(profile.userData));
handleCancel(); handleCancel();
@ -115,11 +115,11 @@ export const RegisterContent: FC<RegisterProps> = ({
rules={[ rules={[
{ {
type: 'email', type: 'email',
message: 'The input is not valid E-mail' message: i18nText('errors.validEmail', locale)
}, },
{ {
required: true, required: true,
message: 'Please input your E-mail' message: i18nText('error.emptyEmail', locale)
} }
]} ]}
> >
@ -134,7 +134,7 @@ export const RegisterContent: FC<RegisterProps> = ({
noStyle noStyle
rules={[{ rules={[{
required: true, required: true,
message: 'Please input your password' message: i18nText('errors.emptyPass', locale)
}]} }]}
> >
<CustomInputPassword <CustomInputPassword
@ -150,14 +150,14 @@ export const RegisterContent: FC<RegisterProps> = ({
rules={[ rules={[
{ {
required: true, required: true,
message: 'Please confirm your password', message: i18nText('errors.confirmPass', locale),
}, },
({ getFieldValue }) => ({ ({ getFieldValue }) => ({
validator(_, value) { validator(_, value) {
if (!value || getFieldValue('password') === value) { if (!value || getFieldValue('password') === value) {
return Promise.resolve(); return Promise.resolve();
} }
return Promise.reject(new Error('The new password that you entered do not match')); return Promise.reject(new Error(i18nText('errors.notMatchPass', locale)));
}, },
}), }),
]} ]}
@ -176,24 +176,24 @@ export const RegisterContent: FC<RegisterProps> = ({
{i18nText('registration', locale)} {i18nText('registration', locale)}
</FilledButton> </FilledButton>
<OutlinedButton onClick={() => updateMode('enter')}>{i18nText('enter', locale)}</OutlinedButton> <OutlinedButton onClick={() => updateMode('enter')}>{i18nText('enter', locale)}</OutlinedButton>
<span>or</span> <span>{i18nText('or', locale)}</span>
<OutlinedButton <OutlinedButton
icon={<Image src="/images/facebook-logo.png" height={20} width={20} alt="" />} icon={<Image src="/images/facebook-logo.png" height={20} width={20} alt="" />}
onClick={() => onSocialRegister(Social.FACEBOOK)} onClick={() => onSocialRegister(Social.FACEBOOK)}
> >
Facebook account {i18nText('facebook', locale)}
</OutlinedButton> </OutlinedButton>
<OutlinedButton <OutlinedButton
icon={<Image src="/images/apple-logo.png" height={22} width={22} alt="" />} icon={<Image src="/images/apple-logo.png" height={22} width={22} alt="" />}
onClick={() => onSocialRegister(Social.APPLE)} onClick={() => onSocialRegister(Social.APPLE)}
> >
Apple account {i18nText('apple', locale)}
</OutlinedButton> </OutlinedButton>
<OutlinedButton <OutlinedButton
icon={<Image src="/images/google-logo.png" height={20} width={20} alt="" />} icon={<Image src="/images/google-logo.png" height={20} width={20} alt="" />}
onClick={() => onSocialRegister(Social.GOOGLE)} onClick={() => onSocialRegister(Social.GOOGLE)}
> >
Google account {i18nText('google', locale)}
</OutlinedButton> </OutlinedButton>
</> </>
); );

View File

@ -29,11 +29,11 @@ export const ResetContent: FC<ResetProps> = ({
rules={[ rules={[
{ {
type: 'email', type: 'email',
message: 'The input is not valid E-mail' message: i18nText('errors.validEmail', locale)
}, },
{ {
required: true, required: true,
message: 'Please input your E-mail' message: i18nText('error.emptyEmail', locale)
} }
]} ]}
> >
@ -48,7 +48,7 @@ export const ResetContent: FC<ResetProps> = ({
type="primary" type="primary"
onClick={onResetPassword} onClick={onResetPassword}
> >
Reset Password {i18nText('resetPass', locale)}
</FilledButton> </FilledButton>
<LinkButton <LinkButton
type="link" type="link"

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

@ -12,7 +12,7 @@ export const GeneralTopSection = ({
mainImage mainImage
}: GeneralTopSectionProps) => ( }: GeneralTopSectionProps) => (
<div className="main-top"> <div className="main-top">
<div className="b-inner"> <div className="b-inner b-main-desc">
<h1 className="title-h1">{title}</h1> <h1 className="title-h1">{title}</h1>
<div className="main-top__wrap-text"> <div className="main-top__wrap-text">
{description && <p className="main-top__text">{description}</p>} {description && <p className="main-top__text">{description}</p>}

View File

@ -72,6 +72,7 @@ function HeaderAuthLinks ({
mode={mode} mode={mode}
updateMode={setMode} updateMode={setMode}
updateToken={setToken} updateToken={setToken}
locale={locale}
/> />
</> </>
); );

View File

@ -18,6 +18,7 @@ export const HeaderMenu = ({
}: HeaderMenuProps) => { }: HeaderMenuProps) => {
const selectedLayoutSegment = useSelectedLayoutSegment(); const selectedLayoutSegment = useSelectedLayoutSegment();
const pathname = selectedLayoutSegment || ''; const pathname = selectedLayoutSegment || '';
const url = pathname === '(main)' ? '' : pathname;
return ( return (
<div className="b-header__nav"> <div className="b-header__nav">
@ -25,7 +26,7 @@ export const HeaderMenu = ({
<ul className="b-header__nav__list"> <ul className="b-header__nav__list">
{linkConfig.map(({ path, title }) => ( {linkConfig.map(({ path, title }) => (
<li key={path}> <li key={path}>
<Link href={`/${path}` as any} className={pathname === path ? 'active' : ''}>{title}</Link> <Link href={`/${path}` as any} className={path === url ? 'active' : ''}>{title}</Link>
</li> </li>
))} ))}
<AuthLinks locale={locale} /> <AuthLinks locale={locale} />

View File

@ -13,7 +13,7 @@ type HeaderProps = {
export const Header: FC<HeaderProps> = ({ locale }) => { export const Header: FC<HeaderProps> = ({ locale }) => {
const routes: { path: string, title: string }[] = HEAD_ROUTES.map((item) => ({ const routes: { path: string, title: string }[] = HEAD_ROUTES.map((item) => ({
path: item, path: item,
title: i18nText(`menu.${item}`, locale) title: i18nText(item ? `menu.${item}` : 'menu.home', locale)
})); }));
return ( return (

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

@ -1,195 +0,0 @@
"use client";
import type { StripeError } from "@stripe/stripe-js";
import * as React from "react";
import {
useStripe,
useElements,
PaymentElement,
Elements,
} from "@stripe/react-stripe-js";
import StripeTestCards from "./StripeTestCards";
import getStripe from "../../utils/get-stripe";
import { createPaymentIntent} from "../../actions/stripe";
import {Form} from "antd";
import {Payment} from "../../types/payment";
import {CustomInput} from "../view/CustomInput";
import {i18nText} from "../../i18nKeys";
import {FC, useEffect} from "react";
import {getPersonalData} from "../../actions/profile";
type PaymentFormProps = {
amount: number,
sessionId?: string
}
export const CheckoutForm: FC<PaymentFormProps> = ({amount, sessionId}) => {
const [input, setInput] = React.useState<{
paySumm: number;
cardholderName: string;
}>({
paySumm: 1,
cardholderName: "",
});
const [form, ] = Form.useForm<Payment>();
const formAmount = Form.useWatch('amount', form);
const [paymentType, setPaymentType] = React.useState<string>("");
const [payment, setPayment] = React.useState<{
status: "initial" | "processing" | "error";
}>({ status: "initial" });
const [errorMessage, setErrorMessage] = React.useState<string>("");
const stripe = useStripe();
const elements = useElements();
const PaymentStatus = ({ status }: { status: string }) => {
switch (status) {
case "processing":
case "requires_payment_method":
case "requires_confirmation":
return <h2>Processing...</h2>;
case "requires_action":
return <h2>Authenticating...</h2>;
case "succeeded":
return <h2>Payment Succeeded 🥳</h2>;
case "error":
return (
<>
<h2>Error 😭</h2>
<p className="error-message">{errorMessage}</p>
</>
);
default:
return null;
}
};
useEffect(() => {
elements?.update({ amount: formAmount * 100 });
}, [formAmount]);
const handleInputChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
setInput({
...input,
[e.currentTarget.name]: e.currentTarget.value,
});
};
const onSubmit = async (data) => {
try {
if (!elements || !stripe) return;
setPayment({ status: "processing" });
const { error: submitError } = await elements.submit();
if (submitError) {
setPayment({ status: "error" });
setErrorMessage(submitError.message ?? "An unknown error occurred");
return;
}
// Create a PaymentIntent with the specified amount.
console.log('DATA', data);
const { client_secret: clientSecret } = await createPaymentIntent(
{amount: amount},
);
// Use your card Element with other Stripe.js APIs
const { error: confirmError } = await stripe!.confirmPayment({
elements,
clientSecret,
confirmParams: {
return_url: `${window.location.origin}/ru/payment/result`,
payment_method_data: {
allow_redisplay: 'limited',
billing_details: {
name: input.cardholderName,
},
},
},
});
if (confirmError) {
setPayment({ status: "error" });
setErrorMessage(confirmError.message ?? "An unknown error occurred");
}
} catch (err) {
const { message } = err as StripeError;
setPayment({ status: "error" });
setErrorMessage(message ?? "An unknown error occurred");
}
};
return (
<>
<Form form={form} onFinish={onSubmit}>
<fieldset className="elements-style">
<StripeTestCards/>
<legend>Your payment details:</legend>
{paymentType === "card" ? (
<input
placeholder="Cardholder name"
className="elements-style"
type="Text"
name="cardholderName"
onChange={handleInputChange}
required
/>
) : null}
<div className="FormRow elements-style">
<PaymentElement
onChange={(e) => {
setPaymentType(e.value.type);
}}
/>
</div>
</fieldset>
<button
className="elements-style-background"
type="submit"
disabled={
!["initial", "succeeded", "error"].includes(payment.status) ||
!stripe
}
>
Pay
</button>
</Form>
<PaymentStatus status={payment.status}/>
</>
);
}
export const ElementsForm: FC<PaymentFormProps> = ({amount, sessionId}) => {
return (
<Elements
stripe={getStripe()}
options={{
appearance: {
variables: {
colorIcon: "#6772e5",
fontFamily: "Roboto, Open Sans, Segoe UI, sans-serif",
},
},
currency: 'eur',
mode: "payment",
amount: amount*100,
}}
>
<CheckoutForm amount={amount} sessionId={sessionId}/>
</Elements>
)
}

View File

@ -1,10 +0,0 @@
import type { Stripe } from "stripe";
export default function PrintObject({
content,
}: {
content: Stripe.PaymentIntent | Stripe.Checkout.Session;
}): JSX.Element {
const formattedContent: string = JSON.stringify(content, null, 2);
return <pre>{formattedContent}</pre>;
}

View File

@ -1,19 +0,0 @@
export default function StripeTestCards(): JSX.Element {
return (
<div className="test-card-notice">
Use any of the{" "}
<a
href="https://stripe.com/docs/testing#cards"
target="_blank"
rel="noopener noreferrer"
>
Stripe test cards
</a>{" "}
for demo, e.g.{" "}
<div className="card-number">
4242<span></span>4242<span></span>4242<span></span>4242
</div>
.
</div>
);
}

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

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

View File

@ -2,7 +2,7 @@ import React from 'react';
import { Button } from 'antd'; 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>
); );

View File

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

View File

@ -1 +1,2 @@
export const HEAD_ROUTES = ['bb-client', 'bb-expert', 'blog']; // export const HEAD_ROUTES = ['bb-client', 'bb-expert', 'blog'];
export const HEAD_ROUTES = ['', 'blog'];

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

@ -4,19 +4,23 @@ export default {
notifications: 'Benachrichtigung', notifications: 'Benachrichtigung',
support: 'Hilfe & Support', support: 'Hilfe & Support',
information: 'Rechtliche Informationen', information: 'Rechtliche Informationen',
settings: 'Profileinstellungen', settings: 'Kontoeinstellungen',
messages: 'Nachrichten', messages: 'Nachrichten',
'work-with-us': 'Arbeite mit uns' 'expert-profile': 'Expertenprofil'
}, },
menu: { menu: {
'bb-client': 'Mit BB wachsen', 'bb-client': 'Mit BB wachsen',
'bb-expert': 'Werde BB-Experte', 'bb-expert': 'Werde BB-Experte',
home: 'Startseite',
blog: 'Blog&News' blog: 'Blog&News'
}, },
registration: 'Registrieren', registration: 'Registrieren',
enter: 'Anmelden', enter: 'Anmelden',
enterAccount: 'Konto anmelden',
account: 'Mein Konto', account: 'Mein Konto',
logout: 'Abmelden', logout: 'Abmelden',
decline: 'Ablehnen',
send: 'Senden',
deleteAcc: 'Konto löschen', deleteAcc: 'Konto löschen',
footer: { footer: {
faq: 'FAQ', faq: 'FAQ',
@ -25,8 +29,31 @@ export default {
session: { session: {
upcoming: 'Kommende Sitzungen', upcoming: 'Kommende Sitzungen',
requested: 'Angefragte Sitzungen', requested: 'Angefragte Sitzungen',
recent: 'Letzte Sitzungen' recent: 'Letzte Sitzungen',
cancelReason: 'Gib einen Grund für die Absage der Sitzung ein',
reasonPlaceholder: 'Beschreibe den Grund für die Ablehnung',
decline: 'Sitzung ablehnen',
confirm: 'Sitzung bestätigen',
join: 'Sitzung beitreten',
start: 'Sitzung starten',
finish: 'Sitzung abschließen',
comments: 'Kommentare',
myComments: 'Meine Kommentare',
addComment: 'Neuen Kommentar hinzufügen',
commentPlaceholder: 'Ihr Kommentar',
clientComments: 'Kundenkommentare',
coachComments: 'Trainerkommentare'
}, },
room: {
upcoming: 'Zukünftige Räume',
requested: 'Angeforderte Räume',
recent: 'Kürzliche Räume',
newRoom: 'Neuer Raum'
},
agreementText: 'Folgendes habe ich gelesen und erkläre mich damit einverstanden: Benutzervereinbarung,',
userAgreement: 'Benutzervereinbarung',
privacyPolicy: 'Datenschutzrichtlinie',
readMore: 'Mehr erfahren',
photoDesc: 'Füge ein echtes Foto hinzu, mit Gesicht wirkt es immer glaubwürdiger.', photoDesc: 'Füge ein echtes Foto hinzu, mit Gesicht wirkt es immer glaubwürdiger.',
dayStart: 'Tagesbeginn', dayStart: 'Tagesbeginn',
topic: 'Thema', topic: 'Thema',
@ -37,6 +64,12 @@ export default {
oldPass: 'Altes Passwort', oldPass: 'Altes Passwort',
newPass: 'Neues Passwort', newPass: 'Neues Passwort',
confirmPass: 'Passwort bestätigen', confirmPass: 'Passwort bestätigen',
forgotPass: 'Passwort vergessen',
resetPassText: 'Ein Link zum Zurücksetzen Ihres Passworts wurde an Ihre E-Mail gesendet',
or: 'oder',
facebook: 'Facebook-Konto',
apple: 'Apple-Konto',
google: 'Google-Konto',
becomeExpert: '', becomeExpert: '',
insertInfo: 'Füge deine persönlichen Informationen ein, um deine Reise als BBuddy-Experte zu beginnen', insertInfo: 'Füge deine persönlichen Informationen ein, um deine Reise als BBuddy-Experte zu beginnen',
changeUserData: 'Du kannst deine Angaben jederzeit ergänzen oder ändern\n', changeUserData: 'Du kannst deine Angaben jederzeit ergänzen oder ändern\n',
@ -49,11 +82,79 @@ export default {
sortPriceDesc: 'Nach Preis absteigend', sortPriceDesc: 'Nach Preis absteigend',
details: 'Details', details: 'Details',
sessionLang: 'Sitzungssprache', sessionLang: 'Sitzungssprache',
direction: 'Wegbeschreibung',
fromTo: 'von $ bis $', fromTo: 'von $ bis $',
apply: 'Anwenden', apply: 'Anwenden',
save: 'Speichern', save: 'Speichern',
edit: 'Bearbeiten',
changePass: 'Passwort ändern', changePass: 'Passwort ändern',
resetPass: 'Passwort zurücksetzen',
getStarted: 'Loslegen', getStarted: 'Loslegen',
delete: 'Löschen', delete: 'Löschen',
today: 'Heute' today: 'Heute',
back: 'Zurück',
backToExperts: 'Zurück zur Expertenliste',
courseInfo: 'Kursinfo',
expertBackground: 'Expertenhintergrund',
profCertification: 'Professionelle Zertifizierung',
practiceHours: 'Praxisstunden',
supervisionCount: 'Supervision pro Jahr',
outOf: 'von',
schedule: 'Zeitplan',
successfulCase: 'Erfolgreiche Fälle aus der Praxis',
signUp: 'Jetzt anmelden',
noData: 'Keine Daten',
notFound: 'Nicht gefunden',
skillsInfo: 'Fähigkeiten-Infos',
trainings: 'Trainings',
seminars: 'Seminare',
courses: 'Kurse',
mba: 'MBA-Information',
aboutCoach: 'Über Coach',
education: 'Bildung',
coaching: 'Coaching',
experiences: 'Praktische Erfahrung',
payInfo: 'Zahlungsdaten',
sessionDuration: 'Sitzungsdauer',
experienceHours: 'Gesamtstunden praktischer Erfahrung',
topics: 'Themen',
selectTopic: 'Thema auswählen',
title: 'Titel',
description: 'Beschreibung',
sessionCost: 'Sitzungskosten in Euro',
yourTimezone: 'Deine Zeitzone',
workTime: 'Arbeitszeit',
startAt: 'Beginn um',
finishAt: 'Ende um',
day: 'Tag',
addWorkingHours: 'Arbeitszeiten hinzufügen',
specialisation: 'Spezialisierung',
selectSpecialisation: 'Wählen Sie Ihre Spezialisierung, um fortzufahren',
fillWeeklySchedule: 'Trage Sachen in deinen Wochenplan ein',
beneficiaryName: 'Name des Empfängers',
bicOrSwift: 'BIC/Swift-Code',
association: 'Verband',
level: 'Stufe',
addDiploma: 'Zertifikat hinzufügen',
university: 'Institution',
sunday: 'So',
monday: 'Mo',
tuesday: 'Di',
wednesday: 'Mi',
thursday: 'Do',
friday: 'Fr',
saturday: 'Sa',
addNew: 'Neu hinzufügen',
mExperiences: 'Führungserfahrung',
errors: {
invalidEmail: 'Die E-Mail-Adresse ist ungültig',
emptyEmail: 'Bitte geben Sie Ihre E-Mail ein',
emptyPass: 'Bitte geben Sie Ihr Passwort ein',
confirmPass: 'Bitte bestätigen Sie Ihr Passwort',
notMatchPass: 'Die neuen Passwörter stimmen nicht überein',
emptyCancelReason: 'Bitte gib den Grund ein',
approvingSession: 'Fehler beim Genehmigen der Sitzung',
finishingSession: 'Fehler beim Beenden der Sitzung',
emptyComment: 'Bitte geben Sie Ihren Kommentar ein',
},
} }

View File

@ -4,19 +4,23 @@ export default {
notifications: 'Notification', notifications: 'Notification',
support: 'Help & Support', support: 'Help & Support',
information: 'Legal Information', information: 'Legal Information',
settings: 'Profile Settings', settings: 'Account Settings',
messages: 'Messages', messages: 'Messages',
'work-with-us': 'Work With Us' 'expert-profile': 'Expert profile'
}, },
menu: { menu: {
'bb-client': 'Start grow with BB', 'bb-client': 'Start grow with BB',
'bb-expert': 'Become BB Expert', 'bb-expert': 'Become BB Expert',
home: 'Home',
blog: 'Blog&News' blog: 'Blog&News'
}, },
registration: 'Registration', registration: 'Registration',
enter: 'Enter', enter: 'Enter',
enterAccount: 'Enter account',
account: 'My Account', account: 'My Account',
logout: 'Log out', logout: 'Log out',
decline: 'Decline',
send: 'Send',
deleteAcc: 'Delete account', deleteAcc: 'Delete account',
footer: { footer: {
faq: 'FAQ', faq: 'FAQ',
@ -25,8 +29,31 @@ export default {
session: { session: {
upcoming: 'Upcoming Sessions', upcoming: 'Upcoming Sessions',
requested: 'Sessions Requested', requested: 'Sessions Requested',
recent: 'Recent Sessions' recent: 'Recent Sessions',
cancelReason: 'Enter a reason for cancelling the session',
reasonPlaceholder: 'Describe the reason for the rejection',
decline: 'Decline session',
confirm: 'Confirm session',
join: 'Join session',
start: 'Start session',
finish: 'Finish session',
comments: 'Comments',
myComments: 'My comments',
addComment: 'Add new',
commentPlaceholder: 'Your comment',
clientComments: 'Client Comments',
coachComments: 'Coach Comments'
}, },
room: {
upcoming: 'Upcoming Rooms',
requested: 'Rooms Requested',
recent: 'Recent Rooms',
newRoom: 'New Room'
},
agreementText: 'I have read and agree with the terms of the User Agreement,',
userAgreement: 'User Agreement',
privacyPolicy: 'Privacy Policy',
readMore: 'Read more',
photoDesc: 'Add a real photo, as a person\'s face is always more credible.', photoDesc: 'Add a real photo, as a person\'s face is always more credible.',
dayStart: 'Day start', dayStart: 'Day start',
topic: 'Topic', topic: 'Topic',
@ -37,6 +64,12 @@ export default {
oldPass: 'Old Password', oldPass: 'Old Password',
newPass: 'New Password', newPass: 'New Password',
confirmPass: 'Confirm Password', confirmPass: 'Confirm Password',
forgotPass: 'Forgot password',
resetPassText: 'A link to reset your password has been sent to your email',
or: 'or',
facebook: 'Facebook account',
apple: 'Apple account',
google: 'Google account',
becomeExpert: '', becomeExpert: '',
insertInfo: 'Insert your personal information to start your journey as a BBuddy Expert', insertInfo: 'Insert your personal information to start your journey as a BBuddy Expert',
changeUserData: 'Your info can either be added or amended at anytime', changeUserData: 'Your info can either be added or amended at anytime',
@ -49,11 +82,79 @@ export default {
sortPriceDesc: 'By price descending', sortPriceDesc: 'By price descending',
details: 'Details', details: 'Details',
sessionLang: 'Session Language', sessionLang: 'Session Language',
direction: 'Direction',
fromTo: 'from $ to $', fromTo: 'from $ to $',
apply: 'Apply', apply: 'Apply',
save: 'Save', save: 'Save',
edit: 'Edit',
changePass: 'Change password', changePass: 'Change password',
resetPass: 'Reset password',
getStarted: 'Get started', getStarted: 'Get started',
delete: 'Delete', delete: 'Delete',
today: 'Today' today: 'Today',
back: 'Back',
backToExperts: 'Back to experts list',
courseInfo: 'Course Info',
expertBackground: 'Expert Background',
profCertification: 'Professional Certification',
practiceHours: 'Practice hours',
supervisionCount: 'Supervision per year',
outOf: 'out of',
schedule: 'Schedule',
successfulCase: 'Successful Cases From Practice',
signUp: 'Sign up now',
noData: 'No data',
notFound: 'Not found',
trainings: 'Trainings',
seminars: 'Seminars',
courses: 'Courses',
mba: 'MBA Information',
aboutCoach: 'About Coach',
skillsInfo: 'Skills Info',
education: 'Education',
coaching: 'Coaching',
experiences: 'Practical experience',
payInfo: 'Payment Info',
sessionDuration: 'Session duration',
experienceHours: 'Total hours of practical experience',
topics: 'Topics',
selectTopic: 'Select Topic',
title: 'Title',
description: 'Description',
sessionCost: 'Session cost in euro',
yourTimezone: 'Your timezone',
workTime: 'Work time',
startAt: 'Start at',
finishAt: 'Finish at',
day: 'Day',
addWorkingHours: 'Add working hours',
specialisation: 'Specialisation',
selectSpecialisation: 'Select your specialisation to proceed',
fillWeeklySchedule: 'Fill up your weekly schedule',
beneficiaryName: 'Beneficiary Name',
bicOrSwift: 'BIC/Swift code',
association: 'Association',
level: 'Level',
addDiploma: 'Add Diploma',
university: 'Institution',
sunday: 'Su',
monday: 'Mo',
tuesday: 'Tu',
wednesday: 'We',
thursday: 'Th',
friday: 'Fr',
saturday: 'Sa',
addNew: 'Add New',
mExperiences: 'Managerial Experience',
errors: {
invalidEmail: 'The email address is not valid',
emptyEmail: 'Please enter your E-mail',
emptyPass: 'Please enter your password',
confirmPass: 'Please confirm your password',
notMatchPass: 'The new passwords you entered do not match',
emptyCancelReason: 'Please enter the reason',
approvingSession: 'Error approving session',
finishingSession: 'Error finishing session',
emptyComment: 'Please enter your comment',
},
} }

View File

@ -4,19 +4,23 @@ export default {
notifications: 'Notificación', notifications: 'Notificación',
support: 'Ayuda y asistencia', support: 'Ayuda y asistencia',
information: 'Información jurídica', information: 'Información jurídica',
settings: 'Ajustes del perfil', settings: 'Ajustes de cuenta',
messages: 'Mensajes', messages: 'Mensajes',
'work-with-us': 'Trabaja con nosotros' 'expert-profile': 'Perfil del experto'
}, },
menu: { menu: {
'bb-client': 'Empieza a crecer con BB', 'bb-client': 'Empieza a crecer con BB',
'bb-expert': 'Conviértete en un experto en BB', 'bb-expert': 'Conviértete en un experto en BB',
home: 'Inicio',
blog: 'Blog y noticias' blog: 'Blog y noticias'
}, },
registration: 'Registro', registration: 'Registro',
enter: 'Entrar', enter: 'Entrar',
enterAccount: 'Introducir cuenta',
account: 'Mi cuenta', account: 'Mi cuenta',
logout: 'Cerrar sesión', logout: 'Cerrar sesión',
decline: 'Rechazar',
send: 'Enviar',
deleteAcc: 'Eliminar cuenta', deleteAcc: 'Eliminar cuenta',
footer: { footer: {
faq: 'Preguntas frecuentes', faq: 'Preguntas frecuentes',
@ -25,8 +29,31 @@ export default {
session: { session: {
upcoming: 'Próximas sesiones', upcoming: 'Próximas sesiones',
requested: 'Sesiones solicitadas', requested: 'Sesiones solicitadas',
recent: 'Sesiones recientes' recent: 'Sesiones recientes',
cancelReason: 'Introduce el motivo por el que has cancelado la sesión',
reasonPlaceholder: 'Describe el motivo del rechazo',
decline: 'Rechazar sesión',
confirm: 'Confirmar sesión',
join: 'Unirse a la sesión',
start: 'Iniciar sesión',
finish: 'Finalizar la sesión',
comments: 'Comentarios',
myComments: 'Mis comentarios',
addComment: 'Añadir nuevo comentario',
commentPlaceholder: 'Tu comentario',
clientComments: 'Comentarios del cliente',
coachComments: 'Comentarios del entrenador'
}, },
room: {
upcoming: 'Próximas salas',
requested: 'Salas solicitadas',
recent: 'Salas recientes',
newRoom: 'Nueva sala'
},
agreementText: 'He leído y acepto las condiciones del Acuerdo de usuario,',
userAgreement: 'Acuerdo de usuario',
privacyPolicy: 'Política de privacidad',
readMore: 'Seguir leyendo',
photoDesc: 'Añade una foto real, ya que la cara de una persona siempre es más creíble.', photoDesc: 'Añade una foto real, ya que la cara de una persona siempre es más creíble.',
dayStart: 'Inicio del día', dayStart: 'Inicio del día',
topic: 'Tema', topic: 'Tema',
@ -37,6 +64,12 @@ export default {
oldPass: 'Contraseña antigua', oldPass: 'Contraseña antigua',
newPass: 'Nueva contraseña', newPass: 'Nueva contraseña',
confirmPass: 'Confirmar contraseña', confirmPass: 'Confirmar contraseña',
forgotPass: 'Se te ha olvidado la contraseña',
resetPassText: 'Se ha enviado un enlace para restablecer la contraseña a tu correo electrónico',
or: 'o',
facebook: 'Cuenta de Facebook',
apple: 'Cuenta de Apple',
google: 'Cuenta de Google',
becomeExpert: '', becomeExpert: '',
insertInfo: 'Introduce tu información personal para comenzar tu viaje como experto en BBuddy', insertInfo: 'Introduce tu información personal para comenzar tu viaje como experto en BBuddy',
changeUserData: 'Tus datos pueden añadirse o modificarse en cualquier momento', changeUserData: 'Tus datos pueden añadirse o modificarse en cualquier momento',
@ -49,11 +82,79 @@ export default {
sortPriceDesc: 'Por precio descendiente', sortPriceDesc: 'Por precio descendiente',
details: 'Detalles', details: 'Detalles',
sessionLang: 'Idioma de la sesión', sessionLang: 'Idioma de la sesión',
direction: 'Dirección',
fromTo: 'de $ a $', fromTo: 'de $ a $',
apply: 'Solicitar', apply: 'Solicitar',
save: 'Guardar', save: 'Guardar',
edit: 'Editar',
changePass: 'Cambiar contraseña', changePass: 'Cambiar contraseña',
resetPass: 'Restablecer contraseña',
getStarted: 'Empieza', getStarted: 'Empieza',
delete: 'Eliminar', delete: 'Eliminar',
today: 'Hoy día' today: 'Hoy',
back: 'Volver',
backToExperts: 'Volver a la lista de expertos',
courseInfo: 'Información del curso',
expertBackground: 'Antecedentes del experto',
profCertification: 'Certificación profesional',
practiceHours: 'Horas de práctica',
supervisionCount: 'Supervisiones anuales',
outOf: 'de',
schedule: 'Horario',
successfulCase: 'Casos de éxito de la práctica',
signUp: 'Regístrate ahora',
noData: 'Sin datos',
notFound: 'No encontrado',
skillsInfo: 'Información',
trainings: 'Formación',
seminars: 'Seminarios',
courses: 'Cursos',
mba: 'Información sobre máster en ADE (MBA)',
aboutCoach: 'Sobre el coach',
education: 'Educación',
coaching: 'Coaching',
experiences: 'Experiencia práctica',
payInfo: 'Información de pago',
sessionDuration: 'Duración de la sesión',
experienceHours: 'Total de horas de experiencia práctica',
topics: 'Temas',
selectTopic: 'Seleccione el tema',
title: 'Título',
description: 'Descripción',
sessionCost: 'Coste de la sesión en euros',
yourTimezone: 'Tu zona horaria',
workTime: 'Tiempo de trabajo',
startAt: 'Empieza a las',
finishAt: 'Termina a las',
day: 'Día',
addWorkingHours: 'Añadir horas de trabajo',
specialisation: 'Especialización',
selectSpecialisation: 'Selecciona tu especialización para continuar',
fillWeeklySchedule: 'Rellena tu agenda semanal',
beneficiaryName: 'Nombre del beneficiario',
bicOrSwift: 'Código BIC/Swift',
association: 'Asociación',
level: 'Nivel',
addDiploma: 'Añadir diploma',
university: 'Institución',
sunday: 'D',
monday: 'L',
tuesday: 'M',
wednesday: 'X',
thursday: 'J',
friday: 'V',
saturday: 'S',
addNew: 'Añadir nuevo',
mExperiences: 'Experiencia de dirección',
errors: {
invalidEmail: 'La dirección de correo electrónico no es válida',
emptyEmail: 'Introduce tu correo electrónico',
emptyPass: 'Introduce tu contraseña',
confirmPass: 'Confirma tu contraseña',
notMatchPass: 'Las nuevas contraseñas que has introducido no coinciden',
emptyCancelReason: 'Introduce el motivo',
approvingSession: 'Error al aprobar la sesión',
finishingSession: 'Error al finalizar la sesión',
emptyComment: 'Introduce tu comentario',
},
} }

Some files were not shown because too many files have changed in this diff Show More