Compare commits

...

67 Commits

Author SHA1 Message Date
SD 08d12cd89e 0.2.3 2024-11-22 20:01:42 +04:00
SD 332595fd39 fix: fix styles for agora 2024-11-22 20:01:29 +04:00
SD 46b0c5b747 0.2.2 2024-11-22 16:03:20 +04:00
SD d866ee2f62 fix: fix agora version 2024-11-22 16:03:00 +04:00
SD 60a35db46b 0.2.1 2024-11-22 13:10:44 +04:00
SD 0222335694 feat: fix agora, fix creating room 2024-11-22 13:10:37 +04:00
SD 9a3aa98158 0.2.0 2024-11-21 17:04:17 +04:00
SD c0feea48e5 feat: create rooms 2024-11-21 17:03:49 +04:00
SD 5b8ba1b5c4 fix: fix account paths, fix local userData after login 2024-10-29 21:57:45 +04:00
SD cd44c9f1a1 0.1.0 2024-10-28 15:29:39 +04:00
SD 5712cbcf56 feat: add errors 2024-10-28 15:28:14 +04:00
SD b31d2cf700 feat: add styles for payment 2024-10-26 00:38:30 +04:00
SD a39f53c57d ::Merge branch 'develop' into stripe 2024-10-17 13:24:29 +04:00
SD 4ac2740942 fix: fix sessions path 2024-10-17 13:22:22 +04:00
SD be7efc0d32 0.0.4 2024-10-16 21:48:56 +04:00
SD d5808e96db feat: update views, styles, actions 2024-10-16 21:47:28 +04:00
dzfelix 59de68d611 stripe payment 2024-09-19 19:55:16 +04:00
SD b141a6ad44 merge develop 2024-09-11 15:05:09 +04:00
SD 4b429c8655 0.0.3 2024-09-10 20:49:39 +04:00
SD 3ed78c0e45 fix: fix main filter point, fix save profile pictures 2024-09-10 20:49:19 +04:00
SD 3345a533d2 feat: add appVersion in browser 2024-09-06 15:22:55 +04:00
SD 76ffdc4094 0.0.2 2024-09-06 14:40:19 +04:00
SD b52096b3bc feat: add edit modal for expert's schedule 2024-09-05 20:06:00 +04:00
dzfelix cda91b9ea9 breadcrumbs & author link 2024-08-28 12:28:30 +03:00
dzfelix 28f5babf22 fix meta anf first page 2024-08-27 17:01:29 +03:00
dzfelix 80f53e871d Merge branch 'refs/heads/develop' into blog 2024-08-27 16:54:31 +03:00
dzfelix 77d3c8f66b main fix 2024-08-26 23:29:30 +04:00
dzfelix f7fe427aae fix lock 2024-08-22 18:15:36 +03:00
dzfelix 5844bd9e7c Merge branch 'refs/heads/blog' into develop 2024-08-22 17:28:23 +03:00
dzfelix dbb74b9ccd Merge remote-tracking branch 'origin/blog' into blog 2024-08-22 17:19:35 +03:00
dzfelix 8ee52bc834 blog fix cat link 2024-08-22 17:19:22 +03:00
norton81 c563818e91 Merge remote-tracking branch 'origin/blog' into develop 2024-08-21 09:44:07 +03:00
norton81 2f2d9db82a Merge branch 'master' into blog 2024-08-20 13:28:40 +03:00
norton81 1461c4948e Merge branch 'master' into develop 2024-08-20 13:25:46 +03:00
norton81 44674a1910 fix master 2024-08-20 13:06:40 +03:00
dzfelix 74d93541a3 blog pagination & sitemap 2024-08-17 23:26:12 +04:00
SD f92810d320 feat: add expert profile 2024-08-17 03:51:27 +04:00
SD 6f5c3738b7 Merge branch 'master' into develop 2024-08-17 03:50:01 +04:00
Dasha 526e703d9a Merge pull request 'blog' (#1) from blog into develop
Reviewed-on: #1
2024-08-16 23:49:20 +00:00
dzfelix ed756d0646 blog contentful 2024-08-16 14:43:07 +03:00
dzfelix 3b2241892f stripe payment 2024-08-15 14:43:52 +03:00
dzfelix ee4dcb58cc stripe payment 2024-08-15 14:41:35 +03:00
SD ff74e5ba49 feat: add experts profile 2024-08-09 12:54:33 +04:00
SD abf04b4c5b feat: add more translate, fix styles 2024-07-29 23:57:04 +04:00
Witalij Poljatchek 4a00d715df release latest 2024-07-24 16:01:27 +02:00
Witalij Poljatchek ec5fb6d443 build tag 0.0.2 2024-07-24 15:53:32 +02:00
Witalij Poljatchek b36efa0ddf RELEASE = "latest" 2024-07-17 23:14:16 +02:00
Witalij Poljatchek 020ba600d9 v 0.0.1 2024-07-17 22:51:54 +02:00
Witalij Poljatchek a9387b1f28 RELEASE set to latest 2024-07-17 21:58:08 +02:00
Witalij Poljatchek e926d4cb4a add release to pipeline 2024-07-17 21:48:15 +02:00
dzfelix bdbe4f4b04 Revert "stripe payment"
This reverts commit 3a6c7bd88c.
2024-07-14 11:36:23 +03:00
dzfelix 3a6c7bd88c stripe payment 2024-07-13 13:13:58 +03:00
SD f3d2cfd55a fix: fix agora audioTrack 2024-07-08 17:55:36 +04:00
SD 347ac0113c feat: add .env file 2024-07-05 17:03:11 +04:00
SD aa027466e2 Merge branch 'master' of https://customer-vertexa-gitea.devbay.tech/Bbuddy/bbuddy-ui 2024-07-04 18:53:22 +04:00
SD f4de114fb3 fix: fix agora appid 2024-07-04 18:51:46 +04:00
e510734 f6e0f7dcdb EK: delete npm 2024-07-03 21:40:46 +02:00
SD 456e2a7cdf feat: fix next-intl bugs 2024-07-02 19:47:28 +04:00
SD 035286823e fix: fix styles 2024-06-29 17:55:59 +04:00
SD a1204cea91 fix: fix agora env variable 2024-06-28 19:36:13 +04:00
SD ab5bd5fe2b feat: add new dockerfile 2024-06-27 17:05:35 +04:00
SD 5303bb895d fix: fix ssr window 2024-06-22 20:03:56 +04:00
SD 6c875cdf39 feat: add agora session 2024-06-21 19:25:31 +04:00
SD 0828e944b4 feat: add sessions actions 2024-05-28 01:44:50 +04:00
SD 8f00a5c41c feat: add session styles, fix expert list 2024-05-22 03:26:20 +04:00
SD c2e29cbef3 feat: add sessions page, add delete modal, refactoring 2024-05-21 01:18:08 +04:00
SD ffbbeadb40 feat: fix filter scroll, add translate for auth modals 2024-04-12 20:34:18 +04:00
220 changed files with 13935 additions and 6288 deletions

8
.dockerignore Normal file
View File

@ -0,0 +1,8 @@
node_modules/
.next
.idea
Dockerfile
.dockerignore
npm-debug.log
README.md
.git

9
.env Normal file
View File

@ -0,0 +1,9 @@
NEXT_PUBLIC_SERVER_BASE_URL=https://api.bbuddy.expert/api
NEXT_PUBLIC_AGORA_APPID=ed90c9dc42634e5687d4e2e0766b363f
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51LVB3LK5pVGxNPeKk4gedt5NW4cb8k7BVXvgOMPTK4x1nnbGTD8BCqDqgInboT6N72YwrTl4tOsVz8rAjbUadX1m00y4Aq5qE8
STRIPE_SECRET_KEY=sk_test_51LVB3LK5pVGxNPeK6j0wCsPqYMoGfcuwf1LpwGEBsr1dUx4NngukyjYL2oMZer5EOlW3lqnVEPjNDruN0OkUohIf00fWFUHN5O
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

View File

@ -1,8 +1,23 @@
FROM node:bookworm AS build FROM node:20.10.0 as dependencies
RUN node -v WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --verbose
FROM node:20.10.0 as builder
WORKDIR /app WORKDIR /app
COPY . . COPY . .
FROM nginx COPY --from=dependencies /app/node_modules ./node_modules
COPY --from=build /app/dist /usr/share/nginx/html/ RUN npm run build --debug --verbose
RUN rm /etc/nginx/conf.d/default.conf
COPY _nginx/nginx.conf /etc/nginx/conf.d FROM node:20.10.0 as runner
WORKDIR /app
COPY . .
ENV NODE_ENV production
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/.next ./.next
COPY --from=dependencies /app/node_modules ./node_modules
EXPOSE 4200
CMD ["npm", "start"]

34
Dockerfile_standalone Normal file
View File

@ -0,0 +1,34 @@
FROM node:18-alpine AS base
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN yarn build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
RUN mkdir .next
RUN chown nextjs:nodejs .next
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 4200
ENV PORT 4200
CMD HOSTNAME="0.0.0.0" node server.js

20
Jenkinsfile vendored
View File

@ -1,18 +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 '''
npm install #npm install
npm run build #npm run build
pwd #pwd
echo #echo
docker build --progress=plain -t bbuddy/bbuddy_ui:latest . docker build --progress=plain -t bbuddy/bbuddy_ui:${RELEASE} .
''' '''
} }
} }
@ -20,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
@ -28,11 +32,9 @@ const nextConfig = {
taint: true, taint: true,
// typedRoutes: true // typedRoutes: true
}, },
output: 'export', // output: 'standalone',
distDir: 'dist',
poweredByHeader: false, poweredByHeader: false,
productionBrowserSourceMaps: true, productionBrowserSourceMaps: true
trailingSlash: true
}; };
module.exports = withNextIntl(nextConfig); module.exports = withNextIntl(nextConfig);

7711
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,20 +1,27 @@
{ {
"name": "bbuddy-ui", "name": "bbuddy-ui",
"version": "0.0.1", "version": "0.2.3",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 4200", "dev": "next dev -p 4200",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start -p 4200",
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@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/stripe-js": "^4.1.0",
"agora-rtc-react": "2.1.0",
"agora-rtc-sdk-ng": "^4.20.2",
"antd": "^5.12.1", "antd": "^5.12.1",
"antd-img-crop": "^4.21.0", "antd-img-crop": "^4.21.0",
"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",
@ -22,7 +29,9 @@
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",
"react-slick": "^0.29.0", "react-slick": "^0.29.0",
"react-stripe-js": "^1.1.5",
"slick-carousel": "^1.8.1", "slick-carousel": "^1.8.1",
"stripe": "^16.2.0",
"styled-components": "^6.1.1" "styled-components": "^6.1.1"
}, },
"devDependencies": { "devDependencies": {

View File

@ -0,0 +1,11 @@
{
"applinks": {
"apps": [],
"details": [
{
"appID": "GTYAM4FYH3.com.bbuddy.whistle",
"paths": ["/en/experts/*"]
}
]
}
}

View File

@ -0,0 +1,13 @@
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.bbuddy.whistle",
"sha256_cert_fingerprints": [
"87:A2:49:9A:F4:05:9C:06:3C:3D:F3:10:88:F5:49:6D:5F:F2:BC:1E:90:0D:F2:37:A5:BA:37:19:5C:A3:75:C2",
"86:42:FE:EA:44:22:9D:16:7F:FC:70:92:A6:39:9D:B1:C3:F1:DE:21:32:4A:45:8C:07:98:39:55:AF:47:32:66"
]
}
}
]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 106 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,30 +1,31 @@
import { apiClient } from '../lib/apiClient'; import { apiRequest } from './helpers';
import { GeneralFilter, ExpertsData, ExpertDetails } from '../types/experts'; import { GeneralFilter, ExpertsData, ExpertDetails, ExpertScheduler, ExpertSchedulerSession, SignupSessionData } from '../types/experts';
export const getExpertsList = async (filter: GeneralFilter, locale: string) => { 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',
data: { id },
locale
});
export const getExpertById = async (id: string, locale: string) => { export const getSchedulerByExpertId = (id: string, locale: string): Promise<ExpertScheduler> => apiRequest({
const response = await apiClient.post( url: '/home/sessionsignupdata',
'/home/coachdetails', method: 'post',
{ id }, data: { id },
{ locale
headers: { });
'X-User-Language': locale
}
}
);
return response.data as ExpertDetails || null; export const getSchedulerSession = (data: SignupSessionData, locale: string, token: string): Promise<ExpertSchedulerSession> => apiRequest({
}; url: '/home/sessionsignupsubmit',
method: 'post',
data,
locale,
token
});

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,40 +1,37 @@
'use client' 'use client'
import { useCallback, useEffect, useState } from 'react'; import { useCallback, 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) => {
console.log(err);
}) })
.finally(() => { .finally(() => {
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

@ -0,0 +1,42 @@
'use client'
import { useCallback, useEffect, useState } from 'react';
import { useLocalStorage } from '../../hooks/useLocalStorage';
import { AUTH_TOKEN_KEY } from '../../constants/common';
import { Room } from '../../types/rooms';
import { getRoomDetails } from '../rooms';
export const useRoomDetails = (locale: string, roomId: number) => {
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
const [room, setRoom] = useState<Room>();
const [errorData, setErrorData] = useState<any>();
const [loading, setLoading] = useState<boolean>(false);
const fetchData = useCallback(() => {
setLoading(true);
setErrorData(undefined);
setRoom(undefined);
getRoomDetails(locale, jwt, roomId)
.then((room) => {
setRoom(room);
})
.catch((err) => {
setErrorData(err);
})
.finally(() => {
setLoading(false);
})
}, []);
useEffect(() => {
fetchData();
}, []);
return {
fetchData,
loading,
room,
errorData
};
};

View File

@ -0,0 +1,42 @@
'use client'
import { useCallback, useEffect, useState } from 'react';
import { useLocalStorage } from '../../hooks/useLocalStorage';
import { AUTH_TOKEN_KEY } from '../../constants/common';
import { Session } from '../../types/sessions';
import { getSessionDetails } from '../sessions';
export const useSessionDetails = (locale: string, sessionId: number) => {
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
const [session, setSession] = useState<Session>();
const [errorData, setErrorData] = useState<any>();
const [loading, setLoading] = useState<boolean>(false);
const fetchData = useCallback(() => {
setLoading(true);
setErrorData(undefined);
setSession(undefined);
getSessionDetails(locale, jwt, sessionId)
.then((session) => {
setSession(session);
})
.catch((err) => {
setErrorData(err);
})
.finally(() => {
setLoading(false);
})
}, []);
useEffect(() => {
fetchData();
}, []);
return {
fetchData,
loading,
session,
errorData
};
};

View File

@ -0,0 +1,46 @@
'use client'
import { useCallback, useEffect, useRef } from 'react';
import { useLocalStorage } from '../../hooks/useLocalStorage';
import { AUTH_TOKEN_KEY } from '../../constants/common';
import { trackingStartSession } from '../sessions';
const DURATION = 30000;
export const useSessionTracking = (locale: string, sessionId: number) => {
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
const timer = useRef<number>();
const fetchData = useCallback(() => {
trackingStartSession(locale, jwt, sessionId)
.then(() => {
console.log('tracking success');
})
.catch((err) => {
console.log('tracking error', err);
})
}, []);
useEffect(() => {
return () => {
window.clearInterval(timer.current);
}
}, []);
const start = () => {
window.clearInterval(timer.current);
timer.current = window.setInterval(() => {
fetchData();
}, DURATION);
};
const stop = () => {
window.clearInterval(timer.current);
};
return {
start,
stop
};
};

View File

@ -1,75 +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 { Session, SessionsFilter } from '../types/sessions'; 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 getUpcomingSessions = (locale: string, jwt: string, filter?: SessionsFilter): Promise<AxiosResponse<Session[]>> => ( export const setPersonData = (data: ProfileRequest, locale: string, token: string): Promise<{ userData: Profile }> => apiRequest({
apiClient.post( url: '/home/applyperson1',
'/home/upcomingsessionsall', method: 'post',
{ data,
sessionType: 'session', locale,
...(filter || {}) token
}, });
{
headers: {
'X-User-Language': locale,
Authorization: `Bearer ${jwt}`
}
}
)
);
export const getRequestedSessions = (locale: string, jwt: string): Promise<AxiosResponse<{ requestedSessions: Session[] }>> => ( export const getEducation = (locale: string, token: string): Promise<EducationDTO> => apiRequest({
apiClient.post( url: '/home/person2',
'/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 setEducation = (locale: string, token: string, data: EducationData): Promise<EducationData> => apiRequest({
apiClient.post( url: '/home/applyperson2',
'/home/historicalmeetings ', method: 'post',
{ data,
sessionType: 'session', locale,
...(filter || {}) token
}, });
{
headers: { export const getTags = (locale: string, token: string): Promise<ExpertsTags> => apiRequest({
'X-User-Language': locale, url: '/home/person3',
Authorization: `Bearer ${jwt}` 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
});

109
src/actions/rooms.ts Normal file
View File

@ -0,0 +1,109 @@
import { apiRequest } from './helpers';
import {GetUsersForRooms, Room, RoomEdit, RoomEditDTO} from '../types/rooms';
export const getUpcomingRooms = (locale: string, token: string): Promise<Room[]> => apiRequest({
url: '/home/upcomingsessionsall',
method: 'post',
data: {
sessionType: 'room'
},
locale,
token
});
export const getRecentRooms = (locale: string, token: string): Promise<Room[]> => apiRequest({
url: '/home/historicalmeetings',
method: 'post',
data: {
sessionType: 'room'
},
locale,
token
});
export const getRoomDetails = (locale: string, token: string, id: number): Promise<Room> => apiRequest({
url: '/home/room',
method: 'post',
data: { id },
locale,
token
});
export const deleteRoomClient = (locale: string, token: string, data: { sessionId: number, clientUserId: number }): Promise<any> => apiRequest({
url: '/home/deleteclientfromroom',
method: 'post',
data,
locale,
token
});
export const deleteRoomSupervisor = (locale: string, token: string, data: { sessionId: number, supervisorUserId: number }): Promise<any> => apiRequest({
url: '/home/deletesupervisorfromroom',
method: 'post',
data,
locale,
token
});
export const becomeRoomClient = (locale: string, token: string, data: { sessionId: number, clientUserId: number }): Promise<any> => apiRequest({
url: '/home/becomeroomclient',
method: 'post',
data,
locale,
token
});
export const becomeRoomSupervisor = (locale: string, token: string, data: { sessionId: number, supervisorUserId: number }): Promise<any> => apiRequest({
url: '/home/becomeroomsupervisor',
method: 'post',
data,
locale,
token
});
export const getUsersList = (locale: string, token: string, data: { template: string }): Promise<GetUsersForRooms> => apiRequest({
url: '/home/findusersforroom',
method: 'post',
data,
locale,
token
});
export const addClient = (locale: string, token: string, data: { sessionId: number, clientUserId: number }): Promise<any> => apiRequest({
url: '/home/addclienttoroom',
method: 'post',
data,
locale,
token
});
export const addSupervisor = (locale: string, token: string, data: { sessionId: number, supervisorUserId: number }): Promise<any> => apiRequest({
url: '/home/addsupervisortoroom',
method: 'post',
data,
locale,
token
});
export const createRoom = (locale: string, token: string): Promise<any> => apiRequest({
url: '/home/createroom',
method: 'post',
locale,
token
});
export const updateRoom = (locale: string, token: string, data: RoomEdit): Promise<any> => apiRequest({
url: '/home/updateroom',
method: 'post',
data,
locale,
token
});
export const getRoomById = (locale: string, token: string, id: number): Promise<RoomEditDTO> => apiRequest({
url: '/home/getroomforedit',
method: 'post',
data: { id },
locale,
token
});

101
src/actions/sessions.ts Normal file
View File

@ -0,0 +1,101 @@
import { DeclineSessionData, Session, SessionsFilter, SessionCommentData } from '../types/sessions';
import { apiRequest } from './helpers';
export const getUpcomingSessions = (locale: string, token: string, filter?: SessionsFilter): Promise<Session[]> => apiRequest({
url: '/home/upcomingsessionsall',
method: 'post',
data: {
sessionType: 'session',
...(filter || {})
},
locale,
token
});
export const getRequestedSessions = (locale: string, token: string): Promise<{ requestedSessions: Session[] }> => apiRequest({
url: '/home/coachhomedata',
method: 'post',
locale,
token
});
export const getRecentSessions = (locale: string, token: string, filter?: SessionsFilter): Promise<Session[]> => apiRequest({
url: '/home/historicalmeetings',
method: 'post',
data: {
sessionType: 'session',
...(filter || {})
},
locale,
token
});
export const getSessionDetails = (locale: string, token: string, id: number): Promise<Session> => apiRequest({
url: '/home/session',
method: 'post',
data: { id },
locale,
token
});
export const approveRequestedSession = (locale: string, token: string, sessionId: number): Promise<any> => apiRequest({
url: '/home/approverequestedsession',
method: 'post',
data: { sessionId },
locale,
token
});
export const declineRequestedSession = (locale: string, token: string, { sessionId, reason }: DeclineSessionData): Promise<any> => apiRequest({
url: '/home/declinerequestedsession',
method: 'post',
data: {
sessionId,
coachDeclineReason: reason
},
locale,
token
});
export const cancelUpcomingSession = (locale: string, token: string, { sessionId, reason }: DeclineSessionData): Promise<any> => apiRequest({
url: '/home/cancelupcomingsession',
method: 'post',
data: {
sessionId,
coachCancelReason: reason
},
locale,
token
});
export const addSessionComment = (locale: string, token: string, data: SessionCommentData): Promise<any> => apiRequest({
url: '/home/session_comment',
method: 'post',
data,
locale,
token
});
export const trackingStartSession = (locale: string, token: string, id: number): Promise<any> => apiRequest({
url: '/home/sessiontracking',
method: 'post',
data: { id },
locale,
token
});
export const finishSession = (locale: string, token: string, sessionId: number): Promise<any> => apiRequest({
url: '/home/finishsession',
method: 'post',
data: { sessionId },
locale,
token
});
export const sessionPaymentConfirm = (locale: string, token: string, sessionId: number): Promise<Session> => apiRequest({
url: '/home/session_pay_confirm',
method: 'post',
data: { id: sessionId },
locale,
token
});

79
src/actions/stripe.ts Normal file
View File

@ -0,0 +1,79 @@
"use server";
import { 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: { amount: number, sessionId?: string },
): Promise<{ client_secret: string }> {
const params = {
amount: formatAmountForStripe(
data.amount,
'eur',
),
automatic_payment_methods: { enabled: true },
currency: 'eur',
} as Stripe.PaymentIntentCreateParams;
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 };
}
export const getStripePaymentStatus = async (payment_intent: string): Promise<Stripe.PaymentIntent> => await stripe.paymentIntents.retrieve(payment_intent);

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,6 +1,9 @@
import React from 'react'; import React from 'react';
import { unstable_setRequestLocale } from 'next-intl/server';
export default function Directions({ params: { locale } }: { params: { locale: string } }) {
unstable_setRequestLocale(locale);
export default function Directions() {
return ( return (
<div className="main-popular"> <div className="main-popular">
<div className="b-inner"> <div className="b-inner">

View File

@ -1,8 +1,10 @@
import React from 'react'; import React from 'react';
import { unstable_setRequestLocale } from 'next-intl/server';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { Experts } from '../../../../components/Experts/Experts'; import { Experts } from '../../../../components/Experts/Experts';
export default function ExpertsPage({ params }: { params: { locale: string } }) { export default function ExpertsPage({ params: { locale } }: { params: { locale: string } }) {
unstable_setRequestLocale(locale);
const t = useTranslations('Experts'); const t = useTranslations('Experts');
return ( return (
@ -14,7 +16,7 @@ export default function ExpertsPage({ params }: { params: { locale: string } })
<img src="/images/options-outline.svg" className="" alt=""/> <img src="/images/options-outline.svg" className="" alt=""/>
</div> </div>
</div> </div>
<Experts locale={params.locale} /> <Experts locale={locale} />
</div> </div>
</div> </div>
); );

View File

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

View File

@ -12,18 +12,17 @@ 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 data ? (
<Loader isLoading={loading}>
<ExpertProfile
isFull={isFull}
locale={locale}
data={data}
updateData={setData}
/>
</Loader>
) : null;
};

View File

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { unstable_setRequestLocale } from 'next-intl/server';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { i18nText } from '../../../../../i18nKeys'; import { i18nText } from '../../../../../i18nKeys';
@ -9,6 +10,7 @@ export const metadata: Metadata = {
}; };
export default function Information({ params: { locale } }: { params: { locale: string } }) { export default function Information({ params: { locale } }: { params: { locale: string } }) {
unstable_setRequestLocale(locale);
const t = useTranslations('Account.LegalInformation'); const t = useTranslations('Account.LegalInformation');
return ( return (

View File

@ -2,34 +2,19 @@
import React, { ReactNode } from 'react'; import React, { ReactNode } from 'react';
import { AccountMenu } from '../../../../components/Account'; import { AccountMenu } from '../../../../components/Account';
import { i18nText } from '../../../../i18nKeys';
type AccountInnerLayoutProps = { type AccountInnerLayoutProps = {
children: ReactNode; children: ReactNode;
params: { locale: string }; params: { locale: string };
}; };
const ROUTES = ['sessions', 'notifications', 'support', 'information', 'settings', 'messages', 'work-with-us'];
const COUNTS: Record<string, number> = {
sessions: 12,
notifications: 5,
messages: 113
};
export default function AccountInnerLayout({ children, params: { locale } }: AccountInnerLayoutProps) { export default function AccountInnerLayout({ children, params: { locale } }: AccountInnerLayoutProps) {
const getMenuConfig = () => ROUTES.map((path) => ({
path,
title: i18nText(`accountMenu.${path}`, locale),
count: COUNTS[path] || undefined
}));
return ( return (
<div className="page-account"> <div className="page-account">
<div className="b-inner"> <div className="b-inner">
<div className="row"> <div className="row">
<div className="col-xl-3 col-lg-4 d-none d-lg-block"> <div className="col-xl-3 col-lg-4 d-none d-lg-block">
<AccountMenu menu={getMenuConfig()} locale={locale} /> <AccountMenu locale={locale} />
</div> </div>
<div className="col-xl-9 col-lg-8 "> <div className="col-xl-9 col-lg-8 ">
<div className="page-account__inner"> <div className="page-account__inner">

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { useTranslations } from 'next-intl'; import { unstable_setRequestLocale } from 'next-intl/server';
import { Link } from '../../../../../../navigation'; import { Link } from '../../../../../../navigation';
import { i18nText } from '../../../../../../i18nKeys'; import { i18nText } from '../../../../../../i18nKeys';
@ -17,7 +17,7 @@ export function generateStaticParams({
} }
export default function Message({ params }: { params: { locale: string, textId: string } }) { export default function Message({ params }: { params: { locale: string, textId: string } }) {
const t = useTranslations('Account.Messages'); unstable_setRequestLocale(params.locale);
return ( return (
<> <>

View File

@ -1,17 +1,11 @@
import React, { Suspense } from 'react'; import React, { Suspense } from 'react';
import type { Metadata } from 'next'; import { unstable_setRequestLocale } from 'next-intl/server';
import { useTranslations } from 'next-intl';
import { Link } from '../../../../../navigation'; import { Link } from '../../../../../navigation';
import { CustomInput } from '../../../../../components/view/CustomInput'; import { CustomInput } from '../../../../../components/view/CustomInput';
import { i18nText } from '../../../../../i18nKeys'; import { i18nText } from '../../../../../i18nKeys';
export const metadata: Metadata = {
title: 'Bbuddy - Account - Messages',
description: 'Bbuddy desc messages'
};
export default function Messages({ params: { locale } }: { params: { locale: string } }) { export default function Messages({ params: { locale } }: { params: { locale: string } }) {
const t = useTranslations('Account.Messages'); unstable_setRequestLocale(locale);
return ( return (
<> <>
@ -24,7 +18,7 @@ export default function Messages({ params: { locale } }: { params: { locale: str
<div className="messages-session"> <div className="messages-session">
<Link <Link
className="card-profile" className="card-profile"
href={'1' as any} href={'messages/1' as any}
> >
<div className="card-profile__header"> <div className="card-profile__header">
<div className="card-profile__header__portrait"> <div className="card-profile__header__portrait">
@ -48,7 +42,7 @@ export default function Messages({ params: { locale } }: { params: { locale: str
</Link> </Link>
<Link <Link
className="card-profile" className="card-profile"
href={'2' as any} href={'messages/2' as any}
> >
<div className="card-profile__header"> <div className="card-profile__header">
<div className="card-profile__header__portrait"> <div className="card-profile__header__portrait">
@ -69,7 +63,7 @@ export default function Messages({ params: { locale } }: { params: { locale: str
</Link> </Link>
<Link <Link
className="card-profile" className="card-profile"
href={'3' as any} href={'messages/3' as any}
> >
<div className="card-profile__header"> <div className="card-profile__header">
<div className="card-profile__header__portrait"> <div className="card-profile__header__portrait">

View File

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { unstable_setRequestLocale } from 'next-intl/server';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import 'dayjs/locale/ru'; import 'dayjs/locale/ru';
import 'dayjs/locale/en'; import 'dayjs/locale/en';
@ -15,6 +16,7 @@ export const metadata: Metadata = {
}; };
export default function Notifications({ params: { locale } }: { params: { locale: string } }) { export default function Notifications({ params: { locale } }: { params: { locale: string } }) {
unstable_setRequestLocale(locale);
const date = dayjs('2022-05-22').locale(locale); const date = dayjs('2022-05-22').locale(locale);
return ( return (

View File

@ -1,21 +0,0 @@
import React, { Suspense } from 'react';
import type { Metadata } from 'next';
import { useTranslations } from 'next-intl';
import { SessionsTabs } from '../../../../../components/Account';
export const metadata: Metadata = {
title: 'Bbuddy - Account - Sessions',
description: 'Bbuddy desc sessions'
};
export default function Sessions({ params: { locale } }: { params: { locale: string } }) {
const t = useTranslations('Account.Sessions');
return (
<Suspense fallback={<p>Loading...</p>}>
<SessionsTabs
locale={locale}
/>
</Suspense>
);
}

View File

@ -1,8 +1,11 @@
import React from 'react'; import React from 'react';
import { unstable_setRequestLocale } from 'next-intl/server';
import { Link } from '../../../../../../navigation'; import { Link } from '../../../../../../navigation';
import { i18nText } from '../../../../../../i18nKeys/'; import { i18nText } from '../../../../../../i18nKeys/';
export default function ChangePassword({ params: { locale } }: { params: { locale: string } }) { export default function ChangePassword({ params: { locale } }: { params: { locale: string } }) {
unstable_setRequestLocale(locale);
return ( return (
<> <>
<ol className="breadcrumb"> <ol className="breadcrumb">

View File

@ -1,16 +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 { 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 } }) {
const t = useTranslations('Account.Settings'); unstable_setRequestLocale(locale);
return ( return (
<> <>

View File

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { unstable_setRequestLocale } from 'next-intl/server';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { i18nText } from '../../../../../i18nKeys'; import { i18nText } from '../../../../../i18nKeys';
@ -8,6 +9,8 @@ export const metadata: Metadata = {
}; };
export default function Support({ params: { locale } }: { params: { locale: string } }) { export default function Support({ params: { locale } }: { params: { locale: string } }) {
unstable_setRequestLocale(locale);
return ( return (
<> <>
<ol className="breadcrumb"> <ol className="breadcrumb">

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,29 +0,0 @@
import React from 'react';
import type { Metadata } from 'next';
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 } }) {
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

@ -0,0 +1,17 @@
import React, { ReactNode } from 'react';
type AccountSimpleLayoutProps = {
children: ReactNode;
};
export default function AccountSimpleLayout({ children }: AccountSimpleLayoutProps) {
return (
<div className="page-account">
<div className="b-inner">
<div className="row">
{children}
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,57 @@
import React, { Suspense } from 'react';
import { unstable_setRequestLocale } from 'next-intl/server';
import { notFound } from 'next/navigation';
import { AccountMenu, RoomDetails, RoomsTabs } from '../../../../../../components/Account';
import { RoomsType } from '../../../../../../types/rooms';
const ROOMS_ROUTES = [RoomsType.UPCOMING, RoomsType.RECENT, RoomsType.NEW];
export async function generateStaticParams({
params: { locale },
}: { params: { locale: string } }) {
return [{ locale, slug: [RoomsType.UPCOMING] }];
}
export default function RoomsDetailItem({ params: { locale, slug } }: { params: { locale: string, slug?: string[] } }) {
unstable_setRequestLocale(locale);
const roomType: string = slug?.length > 0 && slug[0] || '';
const roomId: number | null = slug?.length > 1 && Number(slug[1]) || null;
if (!slug?.length || slug?.length > 2) {
notFound();
}
if (ROOMS_ROUTES.includes(roomType as RoomsType) && Number.isInteger(roomId)) {
return (
<Suspense fallback={<p>Loading...</p>}>
<RoomDetails
locale={locale}
roomId={roomId || 0}
activeType={roomType as RoomsType}
/>
</Suspense>
);
}
if (ROOMS_ROUTES.includes(roomType as RoomsType) && !Number.isInteger(roomId)) {
return (
<>
<div className="col-xl-3 col-lg-4 d-none d-lg-block">
<AccountMenu locale={locale}/>
</div>
<div className="col-xl-9 col-lg-8 ">
<div className="page-account__inner">
<Suspense fallback={<p>Loading...</p>}>
<RoomsTabs
locale={locale}
activeTab={roomType as RoomsType}
/>
</Suspense>
</div>
</div>
</>
);
}
return notFound();
};

View File

@ -0,0 +1,12 @@
'use client';
import { redirect } from 'next/navigation';
import { useLocalStorage } from '../../../../../hooks/useLocalStorage';
import { AUTH_TOKEN_KEY } from '../../../../../constants/common';
import { RoomsType } from '../../../../../types/rooms';
export default function RoomsMainPage() {
const [token] = useLocalStorage(AUTH_TOKEN_KEY, '');
return token ? redirect(`rooms/${RoomsType.UPCOMING}`) : null;
};

View File

@ -0,0 +1,57 @@
import React, { Suspense } from 'react';
import { unstable_setRequestLocale } from 'next-intl/server';
import { notFound } from 'next/navigation';
import { AccountMenu, SessionDetails, SessionsTabs } from '../../../../../../components/Account';
import { SessionType } from '../../../../../../types/sessions';
const SESSION_ROUTES = [SessionType.UPCOMING, SessionType.REQUESTED, SessionType.RECENT];
export async function generateStaticParams({
params: { locale },
}: { params: { locale: string } }) {
return [{ locale, slug: [SessionType.UPCOMING] }];
}
export default function SessionDetailItem({ params: { locale, slug } }: { params: { locale: string, slug?: string[] } }) {
unstable_setRequestLocale(locale);
const sessionType: string = slug?.length > 0 && slug[0] || '';
const sessionId: number | null = slug?.length > 1 && Number(slug[1]) || null;
if (!slug?.length || slug?.length > 2) {
notFound();
}
if (SESSION_ROUTES.includes(sessionType as SessionType) && Number.isInteger(sessionId)) {
return (
<Suspense fallback={<p>Loading...</p>}>
<SessionDetails
locale={locale}
sessionId={sessionId as number}
activeType={sessionType as SessionType}
/>
</Suspense>
);
}
if (SESSION_ROUTES.includes(sessionType as SessionType) && !Number.isInteger(sessionId)) {
return (
<>
<div className="col-xl-3 col-lg-4 d-none d-lg-block">
<AccountMenu locale={locale}/>
</div>
<div className="col-xl-9 col-lg-8 ">
<div className="page-account__inner">
<Suspense fallback={<p>Loading...</p>}>
<SessionsTabs
locale={locale}
activeTab={sessionType as SessionType}
/>
</Suspense>
</div>
</div>
</>
);
}
return notFound();
};

View File

@ -0,0 +1,12 @@
'use client';
import { redirect } from 'next/navigation';
import { useLocalStorage } from '../../../../../hooks/useLocalStorage';
import { AUTH_TOKEN_KEY } from '../../../../../constants/common';
import { SessionType } from '../../../../../types/sessions';
export default function SessionsMainPage() {
const [token] = useLocalStorage(AUTH_TOKEN_KEY, '');
return token ? redirect(`sessions/${SessionType.UPCOMING}`) : null;
};

View File

@ -1,18 +1,14 @@
'use client'; 'use client';
import { ReactNode, useEffect } from 'react'; import { ReactNode } from 'react';
import { redirect, notFound } from 'next/navigation'; import dynamic from 'next/dynamic';
import { useLocalStorage } from '../../../hooks/useLocalStorage';
import { AUTH_TOKEN_KEY } from '../../../constants/common';
export default function AccountLayout({ children }: { children: ReactNode }) { const Account = dynamic(() => import('../../../components/Account/AccountWrapper'), { ssr: false });
const [token] = useLocalStorage(AUTH_TOKEN_KEY, '');
useEffect(() => { export default function AccountBaseLayout({ children }: { children: ReactNode }) {
if(!token){ return (
notFound(); <Account>
} {children}
}, []); </Account>
);
return children;
}; };

View File

@ -1,6 +1,5 @@
'use client'; 'use client';
import React from 'react';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
import { useLocalStorage } from '../../../hooks/useLocalStorage'; import { useLocalStorage } from '../../../hooks/useLocalStorage';
import { AUTH_TOKEN_KEY } from '../../../constants/common'; import { AUTH_TOKEN_KEY } from '../../../constants/common';

View File

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { unstable_setRequestLocale } from 'next-intl/server';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { GeneralTopSection } from '../../../components/Page'; import { GeneralTopSection } from '../../../components/Page';
@ -8,7 +9,8 @@ export const metadata: Metadata = {
description: 'Bbuddy desc Take the lead with BB' description: 'Bbuddy desc Take the lead with BB'
}; };
export default function BbClientPage() { export default function BbClientPage({ params: { locale } }: { params: { locale: string } }) {
unstable_setRequestLocale(locale);
const t = useTranslations('BbClient'); const t = useTranslations('BbClient');
return ( return (

View File

@ -1,15 +1,17 @@
import React from 'react'; import React from 'react';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { unstable_setRequestLocale } from 'next-intl/server';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { GeneralTopSection } from '../../../components/Page'; import { GeneralTopSection } from '../../../components/Page';
import { ScreenCarousel } from '../../../components/Page/ScreenCarousel/index'; import { ScreenCarousel } from '../../../components/Page/ScreenCarousel';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Bbuddy - Become a BB expert', title: 'Bbuddy - Become a BB expert',
description: 'Bbuddy desc Become a BB expert' description: 'Bbuddy desc Become a BB expert'
}; };
export default function BbExpertPage() { export default function BbExpertPage({ params: { locale } }: { params: { locale: string } }) {
unstable_setRequestLocale(locale);
const t = useTranslations('BbExpert'); const t = useTranslations('BbExpert');
return ( return (

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,42 @@
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 = { interface BlogPostPageParams {
title: 'Bbuddy - Blog', slug: string
description: 'Bbuddy desc blog' }
};
export default function Blog() { 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

@ -1,15 +1,16 @@
import React, { Suspense } from 'react'; import React, { Suspense } from 'react';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { unstable_setRequestLocale } from 'next-intl/server';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { getExpertById, getExpertsList } from '../../../../actions/experts'; import { getExpertById, getExpertsList } from '../../../../actions/experts';
import { import {
ExpertCard, ExpertCard,
ExpertCertificate, ExpertCertificate,
ExpertInformation,
ExpertPractice ExpertPractice
} from '../../../../components/Experts/ExpertDetails'; } from '../../../../components/Experts/ExpertDetails';
import { Details } from '../../../../types/experts'; import { Details } from '../../../../types/education';
import { BackButton } from '../../../../components/view/BackButton'; import { BackButton } from '../../../../components/view/BackButton';
import { i18nText } from '../../../../i18nKeys';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Bbuddy - Experts item', title: 'Bbuddy - Experts item',
@ -19,16 +20,9 @@ export const metadata: Metadata = {
export async function generateStaticParams({ export async function generateStaticParams({
params: { locale }, params: { locale },
}: { params: { locale: string } }) { }: { params: { locale: string } }) {
unstable_setRequestLocale(locale);
const result: { locale: string, expertId: string }[] = []; const result: { locale: string, expertId: string }[] = [];
const experts = await getExpertsList({ const experts = await getExpertsList(locale, { themesTagIds: [] });
"themesTagIds": [
1,2,3,4,5,6,7,8
],
"priceFrom": null,
"priceTo": null,
"durationFrom": null,
"durationTo": null
}, locale);
experts?.coaches?.forEach(({ id }) => { experts?.coaches?.forEach(({ id }) => {
result.push({ locale, expertId: id.toString() }); result.push({ locale, expertId: id.toString() });
@ -37,7 +31,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);
@ -81,16 +75,15 @@ 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} /> <ExpertCard expert={expert} locale={locale} expertId={expertId}/>
<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.
@ -98,7 +91,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">
@ -116,9 +109,13 @@ 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">
<div className="card-profile"> <div className="card-profile">
<div className="card-profile__skills"> <div className="card-profile__skills">
@ -164,7 +161,7 @@ export default async function ExpertItem({ params: { expertId = '', locale} }: {
</a> </a>
</div> </div>
</div> </div>
</div> </div> */}
</div> </div>
</div> </div>
); );

View File

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { unstable_setRequestLocale } from 'next-intl/server';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { Experts } from '../../../components/Experts/Experts'; import { Experts } from '../../../components/Experts/Experts';
@ -8,7 +9,8 @@ export const metadata: Metadata = {
description: 'Bbuddy desc experts' description: 'Bbuddy desc experts'
}; };
export default function ExpertsPage({ params }: { params: { locale: string } }) { export default function ExpertsPage({ params: { locale } }: { params: { locale: string } }) {
unstable_setRequestLocale(locale);
const t = useTranslations('Experts'); const t = useTranslations('Experts');
return ( return (
@ -22,7 +24,7 @@ export default function ExpertsPage({ params }: { params: { locale: string } })
</div> </div>
</div> </div>
<Experts <Experts
locale={params.locale} locale={locale}
basePath="/experts" basePath="/experts"
/> />
</div> </div>

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

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

@ -1,4 +1,5 @@
import React, { ReactNode } from 'react'; import React, { ReactNode } from 'react';
import type { Metadata } from 'next';
import { comfortaa, inter } from './fonts'; import { comfortaa, inter } from './fonts';
import '../styles/style.scss'; import '../styles/style.scss';
@ -7,6 +8,11 @@ type RootLayoutProps = {
params: { locale: string }; params: { locale: string };
}; };
export const metadata: Metadata = {
title: 'Bbuddy',
description: 'Bbuddy'
};
export default function RootLayout({ children, params: { locale } }: RootLayoutProps) { export default function RootLayout({ children, params: { locale } }: RootLayoutProps) {
return ( return (
<html lang={locale} className={`${comfortaa.variable} ${inter.variable}`}> <html lang={locale} className={`${comfortaa.variable} ${inter.variable}`}>

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,29 +1,18 @@
'use client'; 'use client';
import React from 'react';
import styled from 'styled-components';
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 { getMenuConfig } from '../../utils/account';
const Logout = styled(Button)` export const AccountMenu = ({ locale }: { locale: string }) => {
width: 100%;
height: 49px !important;
color: #D93E5C !important;
font-style: normal;
font-weight: 600 !important;
padding: 0 !important;
font-size: 1.125rem !important;
text-align: left !important;
`;
export const AccountMenu = ({ menu, locale }: { menu: { path: string, title: string, count?: number }[], locale: string }) => {
const selectedLayoutSegment = useSelectedLayoutSegment(); const selectedLayoutSegment = useSelectedLayoutSegment();
const pathname = selectedLayoutSegment || ''; const pathname = selectedLayoutSegment || '';
const paths = usePathname(); const paths = usePathname();
const menu: { path: string, title: string, count?: number }[] = getMenuConfig(locale);
const onLogout = () => { const onLogout = () => {
deleteStorageKey(AUTH_TOKEN_KEY); deleteStorageKey(AUTH_TOKEN_KEY);
@ -31,10 +20,6 @@ export const AccountMenu = ({ menu, locale }: { menu: { path: string, title: str
window?.location?.replace(`/${paths.split('/')[1]}/`); window?.location?.replace(`/${paths.split('/')[1]}/`);
}; };
const onDeleteAccount = () => {
console.log('delete');
};
return ( return (
<ul className="list-sidebar"> <ul className="list-sidebar">
{menu.map(({ path, title, count }) => ( {menu.map(({ path, title, count }) => (
@ -48,20 +33,13 @@ export const AccountMenu = ({ menu, locale }: { menu: { path: string, title: str
</li> </li>
))} ))}
<li className="list-sidebar__item"> <li className="list-sidebar__item">
<Logout <Button
type="link" type="link"
onClick={onLogout} onClick={onLogout}
className="b-button__logout"
> >
{i18nText('logout', locale)} {i18nText('logout', locale)}
</Logout> </Button>
</li>
<li className="list-sidebar__item">
<Logout
type="link"
onClick={onDeleteAccount}
>
{i18nText('deleteAcc', locale)}
</Logout>
</li> </li>
</ul> </ul>
); );

View File

@ -0,0 +1,20 @@
'use client'
import { ReactNode, useEffect } from 'react';
import { notFound } from 'next/navigation';
import { AUTH_TOKEN_KEY } from '../../constants/common';
import { useLocalStorage } from '../../hooks/useLocalStorage';
function AccountWrapper ({ children }: { children: ReactNode }) {
const [token] = useLocalStorage(AUTH_TOKEN_KEY, '');
useEffect(() => {
if(!token){
return notFound();
}
}, []);
return children;
}
export default AccountWrapper;

View File

@ -1,25 +1,36 @@
'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 { Form, message, Upload } from 'antd';
import type { UploadFile, UploadProps } from 'antd'; import type { UploadFile } 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 { FilledSquareButton, FilledYellowButton } from '../view/FilledButton';
import { DeleteAccountModal } from '../Modals/DeleteAccountModal';
import { Loader } from '../view/Loader';
type ProfileSettingsProps = { type ProfileSettingsProps = {
locale: string; locale: string;
}; };
// 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,72 +38,162 @@ 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 (
<Form form={form} className="form-settings"> <Loader isLoading={fetchLoading} refresh={fetchProfileSettings}>
<div className="user-avatar"> <Form form={form} className="form-settings">
<div className="user-avatar__edit" style={profileSettings?.faceImageUrl ? { backgroundImage: `url(${profileSettings.faceImageUrl})` } : undefined}> <ImgCrop
<input className="" type="file" id="input-file" /> modalTitle="Редактировать"
<label htmlFor="input-file" className="form-label" /> modalOk="Сохранить"
</div> modalCancel="Отмена"
<div className="user-avatar__text">{i18nText('photoDesc', locale)}</div> modalProps={{
</div> okButtonProps: { className: 'b-button__filled_yellow' },
{/* <ImgCrop rotationSlider> cancelButtonProps: { className: 'b-button__outlined' }
<Upload }}
action="https://run.mocky.io/v3/435e224c-44fb-4773-9faf-380c5e6a2188" beforeCrop={beforeCrop}
fileList={fileList}
onChange={onChange}
onPreview={onPreview}
> >
<Button icon={<CameraOutlined />}>Click to Upload</Button> <Upload
</Upload> fileList={photo ? [photo] : profileSettings?.faceImageUrl ? [
</ImgCrop> */} {
<div className="form-field"> uid: profileSettings.faceImageUrl,
<Form.Item name="username"> name: profileSettings.faceImageUrl,
<CustomInput placeholder={i18nText('name', locale)} /> status: 'done',
</Form.Item> url: profileSettings.faceImageUrl
</div> }
<div className="form-field"> ] : undefined}
<Form.Item name="surname"> accept=".jpg,.jpeg,.png"
<CustomInput placeholder={i18nText('surname', locale)} /> beforeUpload={beforeUpload}
</Form.Item> multiple={false}
</div> showUploadList={false}
{/* <div className="form-field"> >
<Form.Item name="birthday"> <div className="user-avatar">
<CustomInput placeholder={i18nText('birthday', locale)} /> <div className="user-avatar__edit" style={photo
</Form.Item> ? { backgroundImage: `url(${URL.createObjectURL(photo)})` }
</div> */} : profileSettings?.faceImageUrl ? { backgroundImage: `url(${profileSettings.faceImageUrl})`} : undefined }>
<div className="form-field"> <FilledSquareButton
<Form.Item name="login"> type="primary"
<CustomInput type="email" placeholder="E-mail" /> icon={<CameraOutlined style={{ fontSize: 28 }} />}
</Form.Item> />
</div> </div>
<div className="form-link"> <div className="user-avatar__text">{i18nText('photoDesc', locale)}</div>
<Link href={'change-password' as any}> </div>
{i18nText('changePass', locale)} </Upload>
</Link> </ImgCrop>
</div> <div className="form-fieldset">
<button className="btn-apply">{i18nText('save', locale)}</button> <div className="form-field">
</Form> <Form.Item name="username" rules={[
{
required: true,
message: 'Поле не должно быть пустым'
}
]}>
<CustomInput placeholder={i18nText('name', locale)} />
</Form.Item>
</div>
<div className="form-field">
<Form.Item name="surname">
<CustomInput placeholder={i18nText('surname', locale)} />
</Form.Item>
</div>
{/* <div className="form-field">
<Form.Item name="birthday">
<CustomInput placeholder={i18nText('birthday', locale)} />
</Form.Item>
</div> */}
<div className="form-field">
<Form.Item name="login" rules={[
{
required: true,
message: 'Поле не должно быть пустым'
}
]}>
<CustomInput type="email" placeholder="E-mail" />
</Form.Item>
</div>
</div>
<div className="form-actions">
<FilledYellowButton
onClick={onSaveProfile}
loading={saveLoading}
>
{i18nText('save', locale)}
</FilledYellowButton>
<OutlinedButton onClick={() => router.push('settings/change-password')}>
{i18nText('changePass', locale)}
</OutlinedButton>
<OutlinedButton
onClick={onDeleteAccount}
icon={<DeleteOutlined />}
danger
>
{i18nText('deleteAcc', locale)}
</OutlinedButton>
</div>
<DeleteAccountModal
open={showDeleteModal}
handleCancel={() => setShowDeleteModal(false)}
/>
</Form>
</Loader>
); );
}; };

View File

@ -0,0 +1,59 @@
'use client'
import { useJoin } from 'agora-rtc-react';
import { useEffect, useState } from 'react';
import { MediaControl } from './view';
import { LocalUserPanel, RemoteUserPanel } from './components';
import { PublicUser } from '../../../types/sessions';
type AgoraProps = {
sessionId: number;
secret?: string;
stopCalling: () => void;
remoteUser?: PublicUser;
};
export const Agora = ({ sessionId, secret, stopCalling, remoteUser }: AgoraProps) => {
const [calling, setCalling] = useState(false);
const [micOn, setMic] = useState(false);
const [cameraOn, setCamera] = useState(false);
useEffect(() => {
setCalling(true);
}, []);
useJoin(
{
appid: process.env.NEXT_PUBLIC_AGORA_APPID,
channel: `${sessionId}-${secret}`,
token: null,
},
calling,
);
const stop = () => {
stopCalling();
setCalling(false);
};
return (
<div className="b-agora__wrap b-agora__wrap__single">
<RemoteUserPanel calling={calling} user={remoteUser} />
<div className="b-agora__panel">
<MediaControl
calling={calling}
cameraOn={cameraOn}
micOn={micOn}
setCalling={stop}
setCamera={() => setCamera(a => !a)}
setMic={() => setMic(a => !a)}
/>
<LocalUserPanel
calling={calling}
cameraOn={cameraOn}
micOn={micOn}
/>
</div>
</div>
);
};

View File

@ -0,0 +1,54 @@
'use client'
import { useJoin } from 'agora-rtc-react';
import { useEffect, useState } from 'react';
import { MediaControl } from './view';
import { UsersGroupPanel } from './components';
type AgoraProps = {
roomId: number;
secret?: string;
stopCalling: () => void;
};
export const AgoraGroup = ({ roomId, secret, stopCalling }: AgoraProps) => {
const [calling, setCalling] = useState(false);
const [micOn, setMic] = useState(false);
const [cameraOn, setCamera] = useState(false);
useEffect(() => {
setCalling(true);
}, []);
useJoin(
{
appid: process.env.NEXT_PUBLIC_AGORA_APPID,
channel: `${roomId}-${secret}`,
token: null,
},
calling,
);
const stop = () => {
stopCalling();
setCalling(false);
};
return (
<>
<div className="b-agora__wrap">
<UsersGroupPanel calling={calling} micOn={micOn} cameraOn={cameraOn}/>
</div>
<div className="b-agora__panel_group">
<MediaControl
calling={calling}
cameraOn={cameraOn}
micOn={micOn}
setCalling={stop}
setCamera={() => setCamera(a => !a)}
setMic={() => setMic(a => !a)}
/>
</div>
</>
);
};

View File

@ -0,0 +1,30 @@
import { useEffect } from 'react';
import { ICameraVideoTrack, LocalVideoTrack, LocalVideoTrackProps, MaybePromiseOrNull } from 'agora-rtc-react';
import { useAwaited } from '../../../../utils/agora/tools';
interface CameraVideoTrackProps extends LocalVideoTrackProps {
/**
* A camera video track which can be created by `createCameraVideoTrack()`.
*/
readonly track?: MaybePromiseOrNull<ICameraVideoTrack>;
/**
* Device ID, which can be retrieved by calling `getDevices()`.
*/
readonly deviceId?: string;
}
export const CameraVideoTrack = ({
track: maybeTrack,
deviceId,
...props
}: CameraVideoTrackProps) => {
const track = useAwaited(maybeTrack);
useEffect(() => {
if (track && deviceId != null) {
track.setDevice(deviceId).catch(console.warn);
}
}, [deviceId, track]);
return <LocalVideoTrack track={maybeTrack} {...props} />;
};

View File

@ -0,0 +1,108 @@
import { HTMLProps, ReactNode } from 'react';
import { ICameraVideoTrack, IMicrophoneAudioTrack, MaybePromiseOrNull } from 'agora-rtc-react';
import { UserCover } from '../components';
import { MicrophoneAudioTrack } from './MicrophoneAudioTrack';
import { CameraVideoTrack } from './CameraVideoTrack';
interface LocalUserProps extends HTMLProps<HTMLDivElement> {
/**
* Whether to turn on the local user's microphone. Default false.
*/
readonly micOn?: boolean;
/**
* Whether to turn on the local user's camera. Default false.
*/
readonly cameraOn?: boolean;
/**
* A microphone audio track which can be created by `createMicrophoneAudioTrack()`.
*/
readonly audioTrack?: MaybePromiseOrNull<IMicrophoneAudioTrack>;
/**
* A camera video track which can be created by `createCameraVideoTrack()`.
*/
readonly videoTrack?: MaybePromiseOrNull<ICameraVideoTrack>;
/**
* Whether to play the local user's audio track. Default follows `micOn`.
*/
readonly playAudio?: boolean;
/**
* Whether to play the local user's video track. Default follows `cameraOn`.
*/
readonly playVideo?: boolean;
/**
* Device ID, which can be retrieved by calling `getDevices()`.
*/
readonly micDeviceId?: string;
/**
* Device ID, which can be retrieved by calling `getDevices()`.
*/
readonly cameraDeviceId?: string;
/**
* The volume. The value ranges from 0 (mute) to 1000 (maximum). A value of 100 is the current volume.
*/
readonly volume?: number;
/**
* Render cover image if playVideo is off.
*/
readonly cover?: string;
/**
* Children is rendered on top of the video canvas.
*/
readonly children?: ReactNode;
}
/**
* Play/Stop local user camera and microphone track.
*/
export function LocalUser({
micOn,
cameraOn,
audioTrack,
videoTrack,
playAudio = false,
playVideo,
micDeviceId,
cameraDeviceId,
volume,
cover,
children,
style,
...props
}: LocalUserProps) {
playVideo = playVideo ?? !!cameraOn;
playAudio = playAudio ?? !!micOn;
return (
<div {...props} style={{
position: "relative",
width: "100%",
height: "100%",
overflow: "hidden",
background: "#000",
...style
}}>
<CameraVideoTrack
deviceId={cameraDeviceId}
disabled={!cameraOn}
play={playVideo}
track={videoTrack}
/>
<MicrophoneAudioTrack
deviceId={micDeviceId}
disabled={!micOn}
play={playAudio}
track={audioTrack}
volume={volume}
/>
{cover && !cameraOn && <UserCover cover={cover} />}
<div style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
overflow: "hidden",
zIndex: 2,
}}>{children}</div>
</div>
);
};

View File

@ -0,0 +1,43 @@
import { useLocalMicrophoneTrack, useLocalCameraTrack, usePublish, useIsConnected } from 'agora-rtc-react';
import { UserOutlined } from '@ant-design/icons';
import { useLocalStorage } from '../../../../hooks/useLocalStorage';
import { AUTH_USER } from '../../../../constants/common';
import { LocalUser } from './LocalUser';
type LocalUserPanelProps = {
calling: boolean;
micOn: boolean;
cameraOn: boolean;
};
export const LocalUserPanel = ({
calling,
micOn,
cameraOn
}: LocalUserPanelProps) => {
const isConnected = useIsConnected();
const [userData] = useLocalStorage(AUTH_USER, '');
const { faceImageUrl: userImage = '' } = userData ? JSON.parse(userData) : {};
const { localMicrophoneTrack } = useLocalMicrophoneTrack(micOn);
const { localCameraTrack } = useLocalCameraTrack(cameraOn);
usePublish([localMicrophoneTrack, localCameraTrack]);
return calling && isConnected ? (
<div className="b-agora__local_user">
{!cameraOn && (
<div className="b-agora__local_base">
<div className="b-agora__call_avatar" style={userImage ? { backgroundImage: `url(${userImage})` } : undefined}>
{!userImage && (<UserOutlined style={{ fontSize: 40, color: '#2C7873' }}/>)}
</div>
</div>
)}
<LocalUser
audioTrack={localMicrophoneTrack}
cameraOn={cameraOn}
micOn={micOn}
videoTrack={localCameraTrack}
/>
</div>
) : null;
};

View File

@ -0,0 +1,32 @@
import { ReactNode, useEffect } from 'react';
import { IMicrophoneAudioTrack, LocalAudioTrack, LocalAudioTrackProps, MaybePromiseOrNull } from 'agora-rtc-react';
import { useAwaited } from '../../../../utils/agora/tools';
interface MicrophoneAudioTrackProps extends LocalAudioTrackProps {
/**
* A microphone audio track which can be created by `createMicrophoneAudioTrack()`.
*/
readonly track?: MaybePromiseOrNull<IMicrophoneAudioTrack>;
/**
* Device ID, which can be retrieved by calling `getDevices()`.
*/
readonly deviceId?: string;
readonly children?: ReactNode;
}
export const MicrophoneAudioTrack = ({
track: maybeTrack,
deviceId,
...props
}: MicrophoneAudioTrackProps) => {
const track = useAwaited(maybeTrack);
useEffect(() => {
if (track && deviceId != null) {
track.setDevice(deviceId).catch(console.warn);
}
}, [deviceId, track]);
return <LocalAudioTrack track={maybeTrack} {...props} />;
};

View File

@ -0,0 +1,36 @@
import { UserOutlined } from '@ant-design/icons';
import {
useIsConnected,
useRemoteAudioTracks,
useRemoteUsers,
useRemoteVideoTracks
} from 'agora-rtc-react';
import { RenderRemoteUsers } from './RemoteUsers';
import { PublicUser } from '../../../../types/sessions';
export const RemoteUserPanel = ({ calling, user }: { calling: boolean, user?: PublicUser }) => {
const isConnected = useIsConnected();
const remoteUsers = useRemoteUsers();
const { videoTracks } = useRemoteVideoTracks(remoteUsers);
const { audioTracks } = useRemoteAudioTracks(remoteUsers);
audioTracks.map(track => track.play());
return calling && isConnected ? (
<div className="b-agora__remote_user">
{videoTracks?.length > 0 ? (
<RenderRemoteUsers videoTracks={videoTracks} />
) : (
<div className="b-agora__remote_base">
{remoteUsers?.length === 0 && (
<div className="b-agora__remote_warning">
Ожидайте подключения собеседника
</div>
)}
<div className="b-agora__call_avatar" style={user?.faceImageUrl ? { backgroundImage: `url(${user.faceImageUrl})` } : undefined}>
{!user?.faceImageUrl && (<UserOutlined style={{ fontSize: 40, color: '#2C7873' }}/>)}
</div>
</div>
)}
</div>
) : null;
}

View File

@ -0,0 +1,12 @@
import type { IRemoteVideoTrack } from 'agora-rtc-react';
import { RemoteVideoPlayer } from './RemoteVideoPlayer';
export function RenderRemoteUsers({ videoTracks }: { videoTracks: IRemoteVideoTrack[] }) {
return (
<>
{videoTracks.map((track: IRemoteVideoTrack) => (
<RemoteVideoPlayer key={track.getUserId()} track={track} className="b-agora__video_remote" />
))}
</>
);
}

View File

@ -0,0 +1,68 @@
import type { HTMLProps, ReactNode } from 'react';
import type { IAgoraRTCClient, IRemoteVideoTrack } from 'agora-rtc-react';
import { RemoteVideoTrack, useRTCClient } from 'agora-rtc-react';
import { UserCover } from '../components';
export interface RemoteVideoPlayerProps extends HTMLProps<HTMLDivElement> {
/**
* A remote track
*/
readonly track?: IRemoteVideoTrack;
/**
* Whether to play the remote user's video track. Default follows `user.hasVideo`.
*/
readonly playVideo?: boolean;
/**
* Render cover image if playVideo is off.
*/
readonly cover?: string | (() => ReactNode);
/**
* Children is rendered on top of the video canvas.
*/
readonly children?: ReactNode;
/**
* client instance
*/
readonly client?: IAgoraRTCClient | null;
}
/**
* Subscribe and play remote user video track.
* An `IRemoteVideoTrack` can only be own by one `RemoteVideoPlayer`.
*/
export function RemoteVideoPlayer({
track,
playVideo,
cover,
client,
style,
children,
...props
}: RemoteVideoPlayerProps) {
const resolvedClient = useRTCClient(client);
const hasVideo = resolvedClient.remoteUsers?.find(user => user.uid === track?.getUserId())?.hasVideo;
playVideo = playVideo ?? hasVideo;
return (
<div {...props} style={{
position: "relative",
width: "100%",
height: "100%",
overflow: "hidden",
background: "#000",
...style
}}>
<RemoteVideoTrack play={playVideo} track={track} />
{cover && !playVideo && <UserCover cover={cover} />}
<div style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
overflow: "hidden",
zIndex: 2,
}}>{children}</div>
</div>
);
};

View File

@ -0,0 +1,56 @@
import type { CSSProperties, ReactNode } from 'react';
export const FloatBoxStyle: CSSProperties = {
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
overflow: "hidden",
zIndex: 2,
};
const CoverBlurStyle: CSSProperties = {
width: "100%",
height: "100%",
background: "#1a1e21 center/cover no-repeat",
filter: "blur(16px) brightness(0.4)",
};
const CoverImgStyle: CSSProperties = {
position: "absolute",
top: "50%",
left: "50%",
maxWidth: "50%",
maxHeight: "50%",
aspectRatio: "1",
transform: "translate(-50%, -50%)",
borderRadius: "50%",
overflow: "hidden",
objectFit: "cover",
};
export interface UserCoverProps {
/**
* Cover image url or a custom render function.
*/
cover: string | (() => ReactNode);
}
/**
* User Cover image with blur background
*/
export function UserCover({ cover }: UserCoverProps) {
return (
<div style={FloatBoxStyle}>
{typeof cover === "string" ? (
<>
<div style={{ ...CoverBlurStyle, backgroundImage: `url(${cover})` }} />
<img src={cover} style={CoverImgStyle} />
</>
) : (
cover()
)}
</div>
);
}

View File

@ -0,0 +1,44 @@
import {
useIsConnected, useLocalCameraTrack, useLocalMicrophoneTrack, usePublish,
useRemoteAudioTracks,
useRemoteUsers,
useRemoteVideoTracks
} from 'agora-rtc-react';
import { LocalUser } from './LocalUser';
import { RemoteVideoPlayer } from './RemoteVideoPlayer';
type UsersGroupPanelProps = {
calling: boolean;
micOn: boolean;
cameraOn: boolean;
};
export const UsersGroupPanel = ({ calling, micOn, cameraOn }: UsersGroupPanelProps) => {
const isConnected = useIsConnected();
const remoteUsers = useRemoteUsers();
const { localMicrophoneTrack } = useLocalMicrophoneTrack(micOn);
const { localCameraTrack } = useLocalCameraTrack(cameraOn);
const { videoTracks } = useRemoteVideoTracks(remoteUsers);
const { audioTracks } = useRemoteAudioTracks(remoteUsers);
usePublish([localMicrophoneTrack, localCameraTrack]);
audioTracks.map(track => track.play());
return calling && isConnected && remoteUsers ? (
<div className={`b-agora__remote_groups gr-${remoteUsers.length + 1}`}>
<div>
<LocalUser
audioTrack={localMicrophoneTrack}
cameraOn={cameraOn}
micOn={micOn}
videoTrack={localCameraTrack}
/>
</div>
{remoteUsers.length > 0 && remoteUsers.map((user) => (
<div key={user.uid}>
<RemoteVideoPlayer track={user.videoTrack} />
</div>
))}
</div>
) : null;
}

View File

@ -0,0 +1,6 @@
export * from './RemoteVideoPlayer';
export * from './UserCover';
export * from './RemoteUsers';
export * from './LocalUserPanel';
export * from './RemoteUserPanel';
export * from './UsersGroupPanel';

View File

@ -0,0 +1,61 @@
'use client'
import React from 'react';
import Icon from '@ant-design/icons';
import type { GetProps } from 'antd';
type CustomIconComponentProps = GetProps<typeof Icon>;
const MicOnSvg = () => (
<svg viewBox="0 0 24 24" width="1em" height="1em" fill="none">
<path d="M9 21H15M18 9.75V11.25C18 14.55 15.3 17.25 12 17.25C8.7 17.25 6 14.55 6 11.25V9.75M12 17.25V21" stroke="currentColor" strokeMiterlimit="10" strokeLinecap="square"/>
<path d="M12 15C11.505 14.9989 11.0153 14.8983 10.56 14.7042C10.1046 14.5102 9.6929 14.2266 9.34925 13.8703C8.64573 13.1608 8.2507 12.2023 8.25003 11.2032V6.00003C8.24811 5.50703 8.34379 5.01853 8.53157 4.5627C8.71934 4.10686 8.99549 3.6927 9.34409 3.34409C9.6927 2.99549 10.1069 2.71934 10.5627 2.53157C11.0185 2.34379 11.507 2.24811 12 2.25003C14.1028 2.25003 15.75 3.89722 15.75 6.00003V11.2032C15.75 13.2966 14.0677 15 12 15Z" fill="currentColor"/>
</svg>
);
const MicOffSvg = () => (
<svg width="1em" height="1em" viewBox="0 0 25 25" fill="none">
<path d="M10 22H16M19 10.75V12.25C19 15.55 16.3 18.25 13 18.25C9.7 18.25 7 15.55 7 12.25V10.75M13 18.25V22" stroke="currentColor" strokeMiterlimit="10" strokeLinecap="square"/>
<path d="M13 16C12.505 15.9989 12.0153 15.8983 11.56 15.7042C11.1046 15.5102 10.6929 15.2266 10.3492 14.8703C9.64573 14.1608 9.2507 13.2023 9.25003 12.2032V7.00003C9.24811 6.50703 9.34379 6.01853 9.53157 5.5627C9.71934 5.10686 9.99549 4.6927 10.3441 4.34409C10.6927 3.99549 11.1069 3.71934 11.5627 3.53157C12.0185 3.34379 12.507 3.24811 13 3.25003C15.1028 3.25003 16.75 4.89722 16.75 7.00003V12.2032C16.75 14.2966 15.0677 16 13 16Z" fill="currentColor"/>
<rect x="23.3135" y="0.476074" width="2" height="32" rx="1" transform="rotate(45 23.3135 0.476074)" fill="currentColor"/>
</svg>
);
const CameraOnSvg = () => (
<svg width="1em" height="1em" viewBox="0 0 24 14" fill="none">
<path d="M21.75 13.0183C21.54 13.0181 21.3324 12.9739 21.1406 12.8884C21.096 12.8686 21.0534 12.8444 21.0136 12.8162L17.1366 10.0872C16.9399 9.94878 16.7795 9.76513 16.6687 9.55172C16.5579 9.33831 16.5001 9.10139 16.5 8.86094V5.13906C16.5001 4.89861 16.5579 4.66169 16.6687 4.44828C16.7795 4.23487 16.9399 4.05122 17.1366 3.91281L21.0136 1.18375C21.0534 1.15555 21.096 1.13137 21.1406 1.11156C21.369 1.01003 21.6191 0.967168 21.8683 0.986876C22.1174 1.00658 22.3577 1.08824 22.5673 1.22441C22.7769 1.36059 22.9491 1.54697 23.0683 1.76663C23.1875 1.98629 23.25 2.23226 23.25 2.48219V11.5178C23.25 11.9156 23.092 12.2972 22.8107 12.5785C22.5294 12.8598 22.1478 13.0178 21.75 13.0178V13.0183ZM12.5625 13.75H3.9375C3.09239 13.7491 2.28214 13.413 1.68456 12.8154C1.08697 12.2179 0.750869 11.4076 0.75 10.5625V3.4375C0.750869 2.59239 1.08697 1.78214 1.68456 1.18456C2.28214 0.586973 3.09239 0.250869 3.9375 0.25H12.585C13.4241 0.250992 14.2286 0.584765 14.8219 1.1781C15.4152 1.77144 15.749 2.57589 15.75 3.415V10.5625C15.7491 11.4076 15.413 12.2179 14.8154 12.8154C14.2179 13.413 13.4076 13.7491 12.5625 13.75Z" fill="currentColor"/>
</svg>
);
const CameraOffSvg = () => (
<svg width="1em" height="1em" viewBox="0 0 25 25" fill="none">
<path d="M22.75 19.0183C22.54 19.0181 22.3324 18.9739 22.1406 18.8884C22.096 18.8686 22.0534 18.8444 22.0136 18.8162L18.1366 16.0872C17.9399 15.9488 17.7795 15.7651 17.6687 15.5517C17.5579 15.3383 17.5001 15.1014 17.5 14.8609V11.1391C17.5001 10.8986 17.5579 10.6617 17.6687 10.4483C17.7795 10.2349 17.9399 10.0512 18.1366 9.91281L22.0136 7.18375C22.0534 7.15555 22.096 7.13137 22.1406 7.11156C22.369 7.01003 22.6191 6.96717 22.8683 6.98688C23.1174 7.00658 23.3577 7.08824 23.5673 7.22441C23.7769 7.36059 23.9491 7.54697 24.0683 7.76663C24.1875 7.98629 24.25 8.23226 24.25 8.48219V17.5178C24.25 17.9156 24.092 18.2972 23.8107 18.5785C23.5294 18.8598 23.1478 19.0178 22.75 19.0178V19.0183ZM13.5625 19.75H4.9375C4.09239 19.7491 3.28214 19.413 2.68456 18.8154C2.08697 18.2179 1.75087 17.4076 1.75 16.5625V9.4375C1.75087 8.59239 2.08697 7.78214 2.68456 7.18456C3.28214 6.58697 4.09239 6.25087 4.9375 6.25H13.585C14.4241 6.25099 15.2286 6.58477 15.8219 7.1781C16.4152 7.77144 16.749 8.57589 16.75 9.415V16.5625C16.7491 17.4076 16.413 18.2179 15.8154 18.8154C15.2179 19.413 14.4076 19.7491 13.5625 19.75Z" fill="currentColor"/>
<rect x="23.3135" y="0.476074" width="2" height="32" rx="1" transform="rotate(45 23.3135 0.476074)" fill="currentColor"/>
</svg>
);
const PhoneSvg = () => (
<svg width="1em" height="1em" viewBox="0 0 26 11" fill="none">
<path d="M1.10074 4.05002C1.74774 3.40302 2.89061 2.72817 5.01193 2.12757C7.59496 1.40069 9.73186 0.97742 13.0196 0.973112C16.1926 0.972117 17.9918 1.22668 21.0395 2.11266C24.4813 3.11233 25.4373 4.48456 25.7021 5.21476C26.0196 6.08649 25.9663 6.85116 25.7906 7.73283C25.687 8.22986 25.5379 8.71628 25.3451 9.18594C25.3262 9.23334 25.309 9.27775 25.2937 9.31752C25.2036 9.55551 25.0667 9.9158 24.6325 10.1114C24.3435 10.2426 23.9944 10.2688 23.4107 10.2725C22.2145 10.2807 20.0687 9.7809 19.1154 9.43486C18.4737 9.20085 18.0498 9.04673 17.7253 8.72289C17.3457 8.34337 17.244 7.85978 17.1598 7.31221C17.1439 7.20946 17.1303 7.10969 17.117 7.01291C17.0378 6.42921 16.9974 6.27807 16.7899 6.1435C16.3723 5.87369 14.7978 5.4017 12.9881 5.40369C11.1783 5.40568 9.66822 5.88297 9.24959 6.15444C9.03315 6.29464 8.99305 6.4501 8.91217 7.05335C8.90057 7.13986 8.88897 7.22902 8.87505 7.3205C8.78456 7.93668 8.69142 8.40171 8.32085 8.77228L8.31886 8.77427C7.99636 9.09677 7.58038 9.233 6.89228 9.45972C5.99469 9.75538 3.81968 10.3056 2.61914 10.3003C2.03545 10.298 1.68643 10.2731 1.39739 10.1425C0.962193 9.94597 0.826959 9.586 0.734482 9.34702C0.719235 9.30725 0.702662 9.2635 0.683438 9.21577C0.492311 8.7456 0.344854 8.25885 0.242933 7.76167C0.0682546 6.88231 0.0172117 6.11731 0.335741 5.24393C0.497017 4.79315 0.7586 4.38491 1.10074 4.05002Z" fill="currentColor"/>
</svg>
);
export const MicOnIcon = (props: Partial<CustomIconComponentProps>) => (
<Icon component={MicOnSvg} {...props} />
);
export const MicOffIcon = (props: Partial<CustomIconComponentProps>) => (
<Icon component={MicOffSvg} {...props} />
);
export const CameraOnIcon = (props: Partial<CustomIconComponentProps>) => (
<Icon component={CameraOnSvg} {...props} />
);
export const CameraOffIcon = (props: Partial<CustomIconComponentProps>) => (
<Icon component={CameraOffSvg} {...props} />
);
export const PhoneIcon = (props: Partial<CustomIconComponentProps>) => (
<Icon component={PhoneSvg} {...props} />
);

View File

@ -0,0 +1,38 @@
'use client'
import AgoraRTC, { AgoraRTCProvider } from 'agora-rtc-react';
import { Session } from '../../../types/sessions';
import { Room } from '../../../types/rooms';
import { Agora } from './Agora';
import { AgoraGroup } from './AgoraGroup';
export const AgoraClient = ({ session, stopCalling, isCoach }: { session?: Session, stopCalling: () => void, isCoach: boolean }) => {
const remoteUser = isCoach ? (session?.clients?.length ? session?.clients[0] : undefined) : session?.coach;
return session ? (
<AgoraRTCProvider client={AgoraRTC.createClient({ mode: "rtc", codec: "vp8" })}>
{session && (
<Agora
sessionId={session.id}
secret={session.secret}
stopCalling={stopCalling}
remoteUser={remoteUser}
/>
)}
</AgoraRTCProvider>
) : null;
};
export const AgoraClientGroup = ({ room, stopCalling }: { room?: Room, stopCalling: () => void }) => {
return room ? (
<AgoraRTCProvider client={AgoraRTC.createClient({ mode: "rtc", codec: "vp8" })}>
{room && (
<AgoraGroup
roomId={room.id}
secret={room.secret}
stopCalling={stopCalling}
/>
)}
</AgoraRTCProvider>
) : null;
};

View File

@ -0,0 +1,49 @@
import { Button } from 'antd';
import { MicOffIcon, MicOnIcon, CameraOnIcon, CameraOffIcon, PhoneIcon } from '../icons';
interface MediaControlProps {
calling: boolean;
micOn: boolean;
cameraOn: boolean;
setMic: () => void;
setCamera: () => void;
setCalling: () => void;
}
export const MediaControl = ({
calling,
micOn,
cameraOn,
setMic,
setCamera,
setCalling,
}: MediaControlProps) => (
<div className="b-agora__controls">
<Button
className="b-agora__control"
type="primary"
shape="circle"
icon={micOn ? <MicOnIcon style={{ fontSize: 24, color: '#66A5AD' }} /> : <MicOffIcon style={{ fontSize: 24, color: '#fff' }} />}
danger={!micOn}
size="medium"
onClick={setMic}
/>
<Button
className="b-agora__control_big"
type="primary"
shape="circle"
icon={<PhoneIcon style={{ fontSize: 36, color: calling ? '#fff' : '#66A5AD' }} />}
danger={calling}
onClick={setCalling}
/>
<Button
className="b-agora__control"
type="primary"
shape="circle"
icon={cameraOn ? <CameraOnIcon style={{ fontSize: 24, color: '#66A5AD' }} /> : <CameraOffIcon style={{ fontSize: 24, color: '#fff' }} />}
danger={!cameraOn}
size="medium"
onClick={setCamera}
/>
</div>
);

View File

@ -0,0 +1 @@
export * from "./MediaControl";

View File

@ -1,5 +1,6 @@
'use client' 'use client'
export { AccountMenu } from './AccountMenu'; export { AccountMenu } from './AccountMenu';
export { SessionsTabs } from './SessionsTabs';
export { ProfileSettings } from './ProfileSettings'; export { ProfileSettings } from './ProfileSettings';
export * from './sessions';
export * from './rooms';

View File

@ -0,0 +1,45 @@
'use client'
import React, { useEffect, useState } from 'react';
import { EditRoomForm } from './EditRoomForm';
import debounce from 'lodash/debounce';
import { createRoom } from '../../../actions/rooms';
import { Loader } from '../../view/Loader';
import { useRouter } from '../../../navigation';
import { RoomsType } from '../../../types/rooms';
export const CreateRoom = ({ locale, jwt }: { locale: string, jwt: string }) => {
const [roomId, setRoomId] = useState<number>();
const [loading, setLoading] = useState<boolean>(false);
const router = useRouter();
const getRoom = debounce(() => {
createRoom(locale, jwt)
.then((data) => {
setRoomId(data);
})
.finally(() => {
setLoading(false);
})
}, 500);
useEffect(() => {
setLoading(true);
getRoom();
}, []);
return (
<Loader isLoading={loading}>
{roomId && (
<EditRoomForm
roomId={roomId}
locale={locale}
jwt={jwt}
mode="create"
afterSubmit={() => router.push(`/account/rooms/${RoomsType.UPCOMING}`)}
/>
)}
</Loader>
)
};

View File

@ -0,0 +1,220 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Button, Form, Input, notification } from 'antd';
import dayjs, { Dayjs } from 'dayjs';
import { i18nText } from '../../../i18nKeys';
import { Tag } from '../../../types/tags';
import { Slot } from '../../../types/experts';
import { RoomEdit, RoomEditDTO } from '../../../types/rooms';
import { getRoomById, updateRoom } from '../../../actions/rooms';
import { Loader } from '../../view/Loader';
import { CustomInput } from '../../view/CustomInput';
import { CustomSelect } from '../../view/CustomSelect';
import { CustomSwitch } from '../../view/CustomSwitch';
import { CustomMultiSelect } from '../../view/CustomMultiSelect';
import { CustomDatePicker } from '../../view/CustomDatePicker';
type EditRoomFormProps = {
roomId: number,
locale: string,
jwt: string,
mode: 'create' | 'edit';
afterSubmit?: () => void;
}
type RoomFormState = {
title?: string;
description?: string;
date?: Dayjs;
maxCount?: number;
startAt?: string;
supervisor?: boolean;
tags?: number[];
};
export const EditRoomForm = ({ roomId, locale, jwt, mode, afterSubmit }: EditRoomFormProps) => {
const [form] = Form.useForm<RoomFormState>();
const [editingRoom, setEditingRoom] = useState<RoomEditDTO>();
const dateValue = Form.useWatch('date', form);
const [loading, setLoading] = useState<boolean>(false);
const [fetchLoading, setFetchLoading] = useState<boolean>(false);
useEffect(() => {
setFetchLoading(true);
getRoomById(locale, jwt, roomId)
.then((data) => {
setEditingRoom(data);
const { item } = data || {};
if (mode === 'edit' && item) {
form.setFieldsValue({
title: item.title,
description: item.description,
date: item?.scheduledStartAtUtc ? dayjs(item.scheduledStartAtUtc) : undefined,
maxCount: item.maxClients,
startAt: item?.scheduledStartAtUtc,
supervisor: item.isNeedSupervisor,
tags: item.tagIds || undefined
})
}
})
.finally(() => {
setFetchLoading(false);
})
}, []);
const getAvailableSlots = useCallback((): string[] => {
const dateList = new Set<string>();
if (editingRoom?.availableSlots) {
editingRoom.availableSlots.forEach(({ startTime }) => {
const [date] = startTime.split('T');
dateList.add(dayjs(date).format('YYYY-MM-DD'));
});
return Array.from(dateList);
}
return [];
}, [editingRoom?.availableSlots]);
const getTimeOptions = (slots?: Slot[], curDate?: Dayjs) => {
const date = curDate ? curDate.format('YYYY-MM-DD') : '';
if (slots && slots?.length && date) {
return slots.filter(({ startTime }) => dayjs(startTime).format('YYYY-MM-DD') === date)
.map(({ startTime, endTime }) => ({ value: startTime, label: `${dayjs(startTime).format('HH:mm')} - ${dayjs(endTime).format('HH:mm')}` }));
}
return [];
}
const getTagsOptions = (tags?: Tag[]) => {
if (tags) {
return tags.map(({ id, name }) => ({ value: id, label: <span>{name}</span> })) || [];
}
return [];
}
const onSubmit = () => {
setLoading(true);
const { title, description, startAt, maxCount, tags, supervisor } = form.getFieldsValue();
const result: RoomEdit = {
...editingRoom,
id: roomId,
title,
scheduledStartAtUtc: startAt,
maxClients: maxCount,
isNeedSupervisor: supervisor,
tagIds: tags || []
};
if (description) {
result.description = description;
}
updateRoom(locale, jwt, result)
.then(() => {
afterSubmit && afterSubmit();
})
.catch((err) => {
notification.error({
message: 'Error',
description: err?.response?.data?.errMessage
});
})
.finally(() => {
setLoading(false)
});
}
const disabledDate = (current: Dayjs) => current && !getAvailableSlots().includes(current.format('YYYY-MM-DD'));
return (
<Loader isLoading={fetchLoading}>
<Form
form={form}
autoComplete="off"
style={{ display: 'flex', gap: 16, flexDirection: 'column' }}
onFinish={onSubmit}
className="b-room-form"
>
<Form.Item
name="title"
rules={[{ required: true }]}
noStyle
>
<CustomInput
size="small"
placeholder={i18nText('title', locale)}
/>
</Form.Item>
<Form.Item name="description">
<Input.TextArea
className="b-textarea"
rows={4}
maxLength={1000}
placeholder={i18nText('description', locale)}
/>
</Form.Item>
<div className="b-room-form__grid">
<Form.Item
name="date"
rules={[{ required: true }]}
noStyle
>
<CustomDatePicker
locale={locale}
label={i18nText('room.date', locale)}
disabledDate={disabledDate}
/>
</Form.Item>
<Form.Item
name="startAt"
rules={[{ required: true }]}
noStyle
>
<CustomSelect
label={i18nText('room.time', locale)}
options={getTimeOptions(editingRoom?.availableSlots, dateValue)}
disabled={!dateValue}
/>
</Form.Item>
<Form.Item
name="maxCount"
rules={[{ required: true }]}
noStyle
>
<CustomSelect
label={i18nText('room.maxParticipants', locale)}
options={Array.from({ length: 16 }).map((_, i) => ({ value: i+1, label: i+1 }))}
/>
</Form.Item>
<Form.Item
name="supervisor"
valuePropName="checked"
label={i18nText('room.presenceOfSupervisor', locale)}
className="b-room-switch"
>
<CustomSwitch />
</Form.Item>
</div>
<Form.Item
name="tags"
noStyle
>
<CustomMultiSelect
label={i18nText('topics', locale)}
options={getTagsOptions(editingRoom?.tags)}
/>
</Form.Item>
<Button
className="card-detail__apply"
htmlType="submit"
loading={loading}
disabled={loading}
>
{i18nText('room.save', locale)}
</Button>
</Form>
</Loader>
);
};

View File

@ -0,0 +1,66 @@
'use client'
import React, { useState, useEffect } from 'react';
import { RoomsType } from '../../../types/rooms';
import { useSessionTracking } from '../../../actions/hooks/useSessionTracking';
import { AccountMenu } from '../AccountMenu';
import { Loader } from '../../view/Loader';
import { RoomDetailsContent } from './RoomDetailsContent';
import { useRoomDetails } from '../../../actions/hooks/useRoomDetails';
import { AgoraClientGroup } from '../agora';
type RoomDetailsProps = {
locale: string;
roomId: number;
activeType: RoomsType;
};
export const RoomDetails = ({ roomId, locale, activeType }: RoomDetailsProps) => {
const { room, errorData, loading, fetchData } = useRoomDetails(locale, roomId);
const tracking = useSessionTracking(locale, roomId);
const [isCalling, setIsCalling] = useState<boolean>(false);
useEffect(() => {
if (isCalling) {
tracking.start();
} else {
tracking.stop();
}
}, [isCalling]);
const stopCalling = () => {
setIsCalling(false);
fetchData();
}
return isCalling
? (
<AgoraClientGroup
room={room}
stopCalling={stopCalling}
/>
) : (
<>
<div className="col-xl-3 col-lg-4 d-none d-lg-block">
<AccountMenu locale={locale} />
</div>
<div className="col-xl-9 col-lg-8 ">
<div className="page-account__inner">
<Loader
isLoading={loading}
errorData={errorData}
refresh={fetchData}
>
<RoomDetailsContent
locale={locale}
room={room}
activeType={activeType}
startRoom={() => setIsCalling(true)}
refresh={fetchData}
/>
</Loader>
</div>
</div>
</>
);
};

View File

@ -0,0 +1,355 @@
'use client'
import React, { useState } from 'react';
import { Button, notification, Tag } from 'antd';
import { DeleteOutlined, LeftOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import Image from 'next/image';
import { useRouter } from '../../../navigation';
import { Room, RoomsType } from '../../../types/rooms';
import { i18nText } from '../../../i18nKeys';
import { LinkButton } from '../../view/LinkButton';
import {
addClient,
addSupervisor,
becomeRoomClient,
becomeRoomSupervisor,
deleteRoomClient,
deleteRoomSupervisor
} from '../../../actions/rooms';
import { AUTH_TOKEN_KEY, AUTH_USER } from '../../../constants/common';
import { useLocalStorage } from '../../../hooks/useLocalStorage';
import { UserListModal } from '../../Modals/UsersListModal';
import { SessionState } from '../../../types/sessions';
import { EditRoomForm } from './EditRoomForm';
type RoomDetailsContentProps = {
locale: string;
activeType: RoomsType;
room?: Room;
startRoom: () => void;
refresh: () => void;
};
export const RoomDetailsContent = ({ room, startRoom, locale, activeType, refresh }: RoomDetailsContentProps) => {
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
const [userData] = useLocalStorage(AUTH_USER, '');
const { id: userId = 0 } = userData ? JSON.parse(userData) : {};
const router = useRouter();
const [showModal, setShowModal] = useState<boolean>(false);
const [forSupervisor, setForSupervisor] = useState<boolean>(false);
const startDate = room?.scheduledStartAtUtc ? dayjs(room?.scheduledStartAtUtc).locale(locale) : null;
const endDate = room?.scheduledEndAtUtc ? dayjs(room?.scheduledEndAtUtc).locale(locale) : null;
const today = startDate ? dayjs().format('YYYY-MM-DD') === startDate.format('YYYY-MM-DD') : false;
const isCreator = room?.coach && room.coach.id === +userId || false;
const isSupervisor = room?.supervisor && room.supervisor.id === +userId || false;
const isClient = room?.clients && room.clients.length > 0 && room.clients.map(({ id }) => id).includes(+userId) || false;
const isTimeBeforeStart = room?.scheduledStartAtUtc ? dayjs() < dayjs(room.scheduledStartAtUtc) : false;
const [isEdit, setIsEdit] = useState<boolean>(false);
const goBack = () => router.push(`/account/rooms/${activeType}`);
const checkUserApply = (): boolean => (!room?.supervisor || !isSupervisor) && (!room?.clients || room?.clients && room?.clients.length === 0 || !isClient);
const deleteClient = (clientUserId: number) => {
if (room?.id) {
deleteRoomClient(locale, jwt, { sessionId: room.id, clientUserId })
.then(() => {
refresh();
})
.catch((err) => {
notification.error({
message: 'Error',
description: err?.response?.data?.errMessage
});
});
}
};
const deleteSupervisor = (supervisorUserId?: number) => {
if (room?.id && supervisorUserId) {
deleteRoomSupervisor(locale, jwt, { sessionId: room.id, supervisorUserId })
.then(() => {
refresh();
})
.catch((err) => {
notification.error({
message: 'Error',
description: err?.response?.data?.errMessage
});
})
}
};
const becomeClient = () => {
if (room?.id && userId) {
becomeRoomClient(locale, jwt, { sessionId: room.id, clientUserId: +userId })
.then(() => {
refresh();
})
.catch((err) => {
notification.error({
message: 'Error',
description: err?.response?.data?.errMessage
});
});
}
};
const becomeSupervisor = () => {
if (room?.id && userId) {
becomeRoomSupervisor(locale, jwt, { sessionId: room.id, supervisorUserId: +userId })
.then(() => {
refresh();
})
.catch((err) => {
notification.error({
message: 'Error',
description: err?.response?.data?.errMessage
});
});
}
};
const onInviteSupervisor = () => {
setForSupervisor(true)
setShowModal(true);
};
const onAddUser = (id: number) => {
if (room?.id) {
setShowModal(false);
if (forSupervisor) {
addSupervisor(locale, jwt, { sessionId: room.id, supervisorUserId: id })
.then(() => {
refresh();
})
.catch((err) => {
notification.error({
message: 'Error',
description: err?.response?.data?.errMessage
});
});
} else {
addClient(locale, jwt, { sessionId: room.id, clientUserId: id })
.then(() => {
refresh();
})
.catch((err) => {
notification.error({
message: 'Error',
description: err?.response?.data?.errMessage
});
});
}
}
};
const afterEditing = () => {
setIsEdit(false);
refresh();
}
return !isEdit ? (
<div className="card-detail">
<div>
<Button
className="card-detail__back"
type="link"
icon={<LeftOutlined/>}
onClick={goBack}
>
{i18nText('back', locale)}
</Button>
</div>
<div className="card-detail__name">{room?.title || ''}</div>
<div
className={`card-detail__date${today ? ' chosen' : ''}${activeType === RoomsType.RECENT ? ' history' : ''}`}>
{today
? `${i18nText('today', locale)} ${startDate?.format('HH:mm')} - ${endDate?.format('HH:mm')}`
: `${startDate?.format('D MMMM')} ${startDate?.format('HH:mm')} - ${endDate?.format('HH:mm')}`}
</div>
{room?.themesTags && room.themesTags.length > 0 && (
<div className="card-detail__skills">
<div className="skills__list">
{room.themesTags.map((skill) => <Tag key={skill?.id}
className="skills__list__item">{skill?.name}</Tag>)}
</div>
</div>
)}
{room?.description && <div className="card-profile__desc">{room.description}</div>}
{activeType === RoomsType.UPCOMING && (isCreator || isSupervisor || isClient) && (
<div className="card-detail__actions">
{(isCreator || isClient || isSupervisor) && (
<Button
className="card-detail__apply"
onClick={startRoom}
>
{isCreator ? i18nText('session.start', locale) : i18nText('session.join', locale)}
</Button>
)}
{isCreator && isTimeBeforeStart && room?.state === SessionState.COACH_APPROVED && (
<Button
className="card-detail__filled"
onClick={() => setIsEdit(true)}
>
{i18nText('room.editRoom', locale)}
</Button>
)}
</div>
)}
<div className="card-detail__profile">
<div className="card-detail__profile_title">
<div>{i18nText('room.roomCreator', locale)}</div>
</div>
<div className="card-detail__profile_list">
<div className="card-detail__profile_item">
<div className="card-detail__portrait card-detail__portrait_small">
<Image src={room?.coach?.faceImageUrl || '/images/user-avatar.png'} width={86} height={86} alt=""/>
</div>
<div className="card-detail__inner">
<div className="card-detail__name">{`${room?.coach?.name} ${room?.coach?.surname || ''}`}</div>
</div>
</div>
</div>
</div>
{room?.isNeedSupervisor && (
<div className="card-detail__profile">
<div className="card-detail__profile_title">
<div>{i18nText('room.supervisor', locale)}</div>
</div>
{room?.supervisor && (
<div className="card-detail__profile_list">
<div className="card-detail__profile_item">
<div className="card-detail__portrait card-detail__portrait_small">
<Image src={room?.supervisor?.faceImageUrl || '/images/user-avatar.png'} width={86}
height={86}
alt=""/>
</div>
<div className="card-detail__inner">
<div
className="card-detail__name">{`${room?.supervisor?.name} ${room?.supervisor?.surname || ''}`}</div>
</div>
{isCreator && activeType === RoomsType.UPCOMING && isTimeBeforeStart && room?.state === SessionState.COACH_APPROVED && (
<LinkButton
type="link"
style={{alignSelf: 'flex-start'}}
danger
icon={<DeleteOutlined/>}
onClick={() => deleteSupervisor(room?.supervisor?.id)}
/>
)}
</div>
</div>
)}
{room?.supervisor && activeType === RoomsType.RECENT && (
<>
{room?.supervisorComment && (
<div className="card-detail__supervisor-comment">{room.supervisorComment}</div>
)}
</>
)}
{isTimeBeforeStart && room?.state === SessionState.COACH_APPROVED && !room?.supervisor && isCreator && activeType === RoomsType.UPCOMING && (
<Button
className="card-detail__filled"
onClick={onInviteSupervisor}
>
{i18nText('room.inviteSupervisor', locale)}
</Button>
)}
{isTimeBeforeStart && room?.state === SessionState.COACH_APPROVED && !room?.supervisor && !isCreator && activeType === RoomsType.UPCOMING && checkUserApply() && (
<Button
className="card-detail__apply"
onClick={becomeSupervisor}
>
{i18nText('room.joinSupervisor', locale)}
</Button>
)}
{!room?.supervisor && !isCreator && !checkUserApply() && (
<div className="card-profile__desc">{i18nText('noData', locale)}</div>
)}
</div>
)}
<div className="card-detail__profile">
<div className="card-detail__profile_title">
<div>{i18nText('room.participants', locale)}</div>
<div>{`${room?.clients?.length || 0}/${room?.maxClients}`}</div>
</div>
{room?.clients && room?.clients?.length > 0 && (
<div className="card-detail__profile_list">
{room.clients.map(({id, faceImageUrl, name, surname}) => (
<div key={id} className="card-detail__profile_item">
<div className="card-detail__portrait card-detail__portrait_small">
<Image src={faceImageUrl || '/images/user-avatar.png'} width={86}
height={86}
alt=""/>
</div>
<div className="card-detail__inner">
<div
className="card-detail__name">{`${name} ${surname || ''}`}</div>
</div>
{isCreator && room?.state === SessionState.COACH_APPROVED && activeType === RoomsType.UPCOMING && isTimeBeforeStart && (
<LinkButton
type="link"
style={{alignSelf: 'flex-start'}}
danger
icon={<DeleteOutlined/>}
onClick={() => deleteClient(id)}
/>
)}
</div>
))}
</div>
)}
{isTimeBeforeStart && room?.state === SessionState.COACH_APPROVED && isCreator && activeType === RoomsType.UPCOMING && (!room?.clients || (room?.clients && room?.clients?.length < room.maxClients)) && (
<Button
className="card-detail__filled"
onClick={() => setShowModal(true)}
>
{i18nText('room.inviteParticipant', locale)}
</Button>
)}
{isTimeBeforeStart && room?.state === SessionState.COACH_APPROVED && !isCreator && activeType === RoomsType.UPCOMING && (!room?.clients || (room?.clients && room?.clients?.length < room.maxClients)) && checkUserApply() && (
<Button
className="card-detail__apply"
onClick={becomeClient}
>
{i18nText('room.joinParticipant', locale)}
</Button>
)}
</div>
{room && (
<UserListModal
locale={locale}
jwt={jwt}
isOpen={showModal}
handleCancel={() => setShowModal(false)}
submit={onAddUser}
afterCloseModal={() => setForSupervisor(false)}
room={room}
/>
)}
</div>
) : (
<div className="card-detail">
<div>
<Button
className="card-detail__back"
type="link"
icon={<LeftOutlined/>}
onClick={() => setIsEdit(false)}
>
{i18nText('back', locale)}
</Button>
</div>
<EditRoomForm
roomId={room?.id || 0}
locale={locale}
jwt={jwt}
mode="edit"
afterSubmit={afterEditing}
/>
</div>
);
};

View File

@ -0,0 +1,173 @@
'use client';
import React, { MouseEvent, useCallback, useEffect, useState } from 'react';
import { Empty, Space } from 'antd';
import dayjs from 'dayjs';
import 'dayjs/locale/ru';
import 'dayjs/locale/en';
import 'dayjs/locale/de';
import 'dayjs/locale/it';
import 'dayjs/locale/fr';
import 'dayjs/locale/es';
import { RoomsType } from '../../../types/rooms';
import { getRecentRooms, getUpcomingRooms } from '../../../actions/rooms';
import { Loader } from '../../view/Loader';
import { useLocalStorage } from '../../../hooks/useLocalStorage';
import { AUTH_TOKEN_KEY } from '../../../constants/common';
import { usePathname, useRouter } from '../../../navigation';
import { i18nText } from '../../../i18nKeys';
import { CreateRoom } from './CreateRoom';
type RoomsTabsProps = {
locale: string;
activeTab: RoomsType;
};
export const RoomsTabs = ({ locale, activeTab }: RoomsTabsProps) => {
const [sort, setSort] = useState<string>();
const [rooms, setRooms] = useState<any>();
const [loading, setLoading] = useState<boolean>(true);
const [errorData, setErrorData] = useState<any>();
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
const router = useRouter();
const pathname = usePathname();
const fetchData = () => {
setErrorData(undefined);
setLoading(true);
Promise.all([
getUpcomingRooms(locale, jwt),
getRecentRooms(locale, jwt)
])
.then(([upcoming, recent]) => {
setRooms({
[RoomsType.UPCOMING]: upcoming || [],
[RoomsType.RECENT]: recent || []
});
})
.catch((err) => {
setErrorData(err);
})
.finally(() => {
setLoading(false);
});
};
useEffect(() => {
fetchData();
}, []);
const onChangeSort = useCallback((value: string) => {
setSort(value);
}, [sort]);
const onClickSession = (event: MouseEvent<HTMLDivElement>, id: number) => {
event.stopPropagation();
event.preventDefault();
router.push(`${pathname}/${id}`);
};
const getChildren = (list?: any[]) => (
<>
{/* <div className="filter-session">
<div className="filter-session__item">
<CustomSelect
label="Topic"
value={sort}
onChange={onChangeSort}
options={[
{ value: 'topic1', label: 'Topic 1' },
{ value: 'topic2', label: 'Topic 2' },
{ value: 'topic3', label: 'Topic 3' },
{ value: 'topic4', label: 'Topic 4' }
]}
/>
</div>
</div> */}
<div className="list-session">
{list && list?.length > 0 ? list?.map(({ id, scheduledStartAtUtc, scheduledEndAtUtc, title, coach, clients, supervisor, maxClients }) => {
const startDate = dayjs(scheduledStartAtUtc).locale(locale);
const endDate = dayjs(scheduledEndAtUtc).locale(locale);
const today = dayjs().format('YYYY-MM-DD') === startDate.format('YYYY-MM-DD');
return (
<div key={id} className="card-profile session__item" onClick={(e: MouseEvent<HTMLDivElement>) => onClickSession(e, id)}>
<div className="card-profile__header">
<div className="card-profile__header__portrait">
<img src={coach?.faceImageUrl || '/images/person.png'} className="" alt="" />
</div>
<div className="card-profile__header__inner">
<div>
<div className="card-profile__header__name">{`${coach?.name} ${coach?.surname || ''}`}</div>
<div className="card-profile__header__title">{title}</div>
<div className={`card-profile__header__date${activeTab === RoomsType.RECENT ? ' history' : (today ? ' chosen' : '')}`}>
{today
? `${i18nText('today', locale)} ${startDate.format('HH:mm')} - ${endDate.format('HH:mm')}`
: `${startDate.format('D MMMM')} ${startDate.format('HH:mm')} - ${endDate.format('HH:mm')}`}
</div>
<div className="card-room__details">
{supervisor && (
<>
<div>{i18nText('room.supervisor', locale)}</div>
<div>{`${supervisor?.name} ${supervisor?.surname || ''}`}</div>
</>
)}
<div>{i18nText('room.members', locale)}</div>
<div>{`${clients.length}/${maxClients}`}</div>
</div>
</div>
</div>
</div>
</div>
)
}) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={i18nText('noData', locale)} />
)}
</div>
</>
);
const tabs = [
{
key: RoomsType.UPCOMING,
label: (
<>
{i18nText('room.upcoming', locale)}
{rooms?.upcoming && rooms?.upcoming?.length > 0 ? (<span className="count">{rooms?.upcoming.length}</span>) : null}
</>
),
children: getChildren(rooms?.upcoming)
},
{
key: RoomsType.RECENT,
label: i18nText('room.recent', locale),
children: getChildren(rooms?.recent)
},
{
key: RoomsType.NEW,
label: i18nText('room.newRoom', locale),
children: <CreateRoom locale={locale} jwt={jwt} />
}
];
return (
<Loader
isLoading={loading}
errorData={errorData}
refresh={fetchData}
>
<div className="tabs-session">
{tabs.map(({ key, label }) => (
<Space
key={key}
className={`tabs-session__item ${key === activeTab ? 'active' : ''}`}
onClick={() => router.push(`/account/rooms/${key}`)}
>
{label}
</Space>
))}
</div>
{tabs.filter(({ key }) => key === activeTab)[0].children}
</Loader>
);
};

View File

@ -0,0 +1,6 @@
'use client'
export * from './RoomDetails';
export * from './RoomsTabs';
export * from './RoomDetailsContent';
export * from './CreateRoom';

View File

@ -0,0 +1,77 @@
'use client'
import React, { useState, useEffect } from 'react';
import { SessionType } from '../../../types/sessions';
import { AUTH_USER } from '../../../constants/common';
import { useLocalStorage } from '../../../hooks/useLocalStorage';
import { Loader } from '../../view/Loader';
import { useSessionDetails } from "../../../actions/hooks/useSessionDetails";
import { AgoraClient } from '../agora';
import { AccountMenu } from '../AccountMenu';
import { SessionDetailsContent } from './SessionDetailsContent';
import { useSessionTracking } from '../../../actions/hooks/useSessionTracking';
type SessionDetailsProps = {
locale: string;
sessionId: number;
activeType: SessionType;
};
export const SessionDetails = ({ sessionId, locale, activeType }: SessionDetailsProps) => {
const { session, errorData, loading, fetchData } = useSessionDetails(locale, sessionId);
const tracking = useSessionTracking(locale, sessionId);
const [isCalling, setIsCalling] = useState<boolean>(false);
const [userData] = useLocalStorage(AUTH_USER, '');
const { id: userId = 0 } = userData ? JSON.parse(userData) : {};
const client = session?.clients?.length ? session?.clients[0] : null;
const isCoach = +userId !== client?.id;
useEffect(() => {
if (isCalling) {
tracking.start();
} else {
tracking.stop();
}
}, [isCalling]);
const stopCalling = () => {
setIsCalling(false);
fetchData()
}
return isCalling
? (
<AgoraClient
session={session}
stopCalling={stopCalling}
isCoach={isCoach}
/>
) : (
<>
<div className="col-xl-3 col-lg-4 d-none d-lg-block">
<AccountMenu locale={locale} />
</div>
<div className="col-xl-9 col-lg-8 ">
<div className="page-account__inner">
<Loader
isLoading={loading}
errorData={errorData}
refresh={fetchData}
>
{session && (
<SessionDetailsContent
locale={locale}
session={session}
activeType={activeType}
startSession={() => setIsCalling(true)}
refresh={fetchData}
isCoach={isCoach}
/>
)}
</Loader>
</div>
</div>
</>
);
};

View File

@ -0,0 +1,301 @@
'use client'
import React, { useState } from 'react';
import { Button, Empty, notification, Tag } from 'antd';
import { LeftOutlined, PlusOutlined, RightOutlined } from '@ant-design/icons';
import Image from 'next/image';
import dayjs from 'dayjs';
import { Link, useRouter } from '../../../navigation';
import { i18nText } from '../../../i18nKeys';
import { getDuration, getPrice } from '../../../utils/expert';
import { PublicUser, Session, SessionState, SessionType } from '../../../types/sessions';
import { AUTH_TOKEN_KEY } from '../../../constants/common';
import { approveRequestedSession, finishSession } from '../../../actions/sessions';
import { useLocalStorage } from '../../../hooks/useLocalStorage';
import { DeclineSessionModal } from '../../Modals/DeclineSessionModal';
import { AddCommentModal } from '../../Modals/AddCommentModal';
type SessionDetailsContentProps = {
locale: string;
session: Session;
activeType: SessionType;
startSession: () => void;
refresh: () => void;
isCoach: boolean;
};
export const SessionDetailsContent = ({ session, locale, activeType, startSession, refresh, isCoach }: SessionDetailsContentProps) => {
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
const [approveLoading, setApproveLoading] = useState<boolean>(false);
const [finishLoading, setFinishLoading] = useState<boolean>(false);
const [openDeclineModal, setOpenDeclineModal] = useState<boolean>(false);
const [openAddCommentModal, setOpenAddCommentModal] = useState<boolean>(false);
const router = useRouter();
const goBack = () => router.push(`/account/sessions/${activeType}`);
const onApproveSession = () => {
if (activeType === SessionType.REQUESTED) {
setApproveLoading(true);
approveRequestedSession(locale, jwt, session.id)
.then(() => {
goBack();
})
.catch((err) => {
notification.error({
message: i18nText('errors.approvingSession', locale),
description: err?.response?.data?.errMessage
});
})
.finally(() => {
setApproveLoading(false);
});
} else {
startSession();
}
};
const onFinishSession = () => {
if (isCoach) {
setFinishLoading(true);
finishSession(locale, jwt, session.id)
.then(() => {
goBack();
})
.catch((err) => {
notification.error({
message: i18nText('errors.finishingSession', locale),
description: err?.response?.data?.errMessage
});
})
.finally(() => {
setFinishLoading(false);
})
}
};
const startDate = session?.scheduledStartAtUtc ? dayjs(session?.scheduledStartAtUtc).locale(locale) : null;
const endDate = session?.scheduledEndAtUtc ? dayjs(session?.scheduledEndAtUtc).locale(locale) : null;
const today = startDate ? dayjs().format('YYYY-MM-DD') === startDate.format('YYYY-MM-DD') : false;
const CoachCard = (coach?: PublicUser) => coach ? (
<div className="card-detail__expert">
<div className="card-detail__portrait">
<Image src={coach?.faceImageUrl || '/images/user-avatar.png'} width={140} height={140} alt="" />
</div>
<div className="card-detail__inner">
<Link href={`/experts/${coach?.id}` as any} target="_blank">
<div className="card-detail__name">{`${coach?.name} ${coach?.surname || ''}`}</div>
</Link>
{/* <div className="card-detail__info">
<div className="card-profile__subtitle">{coach?.specialityDesc}</div>
<div className="card-detail__lang">
{coach?.coachLanguages?.map((lang) => (
<Tag key={lang} className="skills__list__item">{lang}</Tag>
))}
</div>
</div> */}
<div className="card-detail__cost">
{getPrice(session?.cost)} <span>/ {getDuration(locale, session?.totalDuration)}</span>
</div>
<div className={`card-detail__date${today ? ' chosen': ''}${activeType === SessionType.RECENT ? ' history' : ''}`}>
{today
? `${i18nText('today', locale)} ${startDate?.format('HH:mm')} - ${endDate?.format('HH:mm')}`
: `${startDate?.format('D MMMM')} ${startDate?.format('HH:mm')} - ${endDate?.format('HH:mm')}`}
</div>
<div className="card-detail__skills">
<div className="skills__list">
{session?.themesTags?.slice(0, 2).map((skill) => <Tag key={skill?.id} className="skills__list__item">{skill?.name}</Tag>)}
{session?.themesTags && session?.themesTags?.length > 2
? (
<Tag className="skills__list__more">
<Link href={`/experts/${coach?.id}` as any} target="_blank">
{`+${session?.themesTags?.length - 2}`}
</Link>
</Tag>
) : null }
</div>
</div>
{/* <div className="card-profile__desc">{coach?.description}</div> */}
<Link href={`/experts/${coach?.id}` as any} target="_blank" className="card-detail__more">
{i18nText('details', locale)}
<RightOutlined style={{ fontSize: '10px', padding: '0 7px' }}/>
</Link>
</div>
</div>
) : null;
const StudentCard = (student?: PublicUser | null) => student ? (
<div className="card-detail__expert">
<div className="card-detail__portrait">
<Image src={student?.faceImageUrl || '/images/user-avatar.png'} width={140} height={140} alt="" />
</div>
<div className="card-detail__inner">
<div className="card-detail__name">{`${student?.name} ${student?.surname || ''}`}</div>
<div className={`card-detail__date${today ? ' chosen': ''}${activeType === SessionType.RECENT ? ' history' : ''}`}>
{today
? `${i18nText('today', locale)} ${startDate?.format('HH:mm')} - ${endDate?.format('HH:mm')}`
: `${startDate?.format('D MMMM')} ${startDate?.format('HH:mm')} - ${endDate?.format('HH:mm')}`}
</div>
<div className="card-detail__skills">
<div className="skills__list">
<Tag className="skills__list__item">{session?.themesTagName}</Tag>
</div>
</div>
{/* <div className="card-profile__desc">{student?.description}</div> */}
{activeType === SessionType.REQUESTED && session?.clientComment && (
<div className="card-detail__comments">
<div className="card-detail__comments_item">
{session.clientComment}
</div>
</div>
)}
</div>
</div>
) : null;
const client = session?.clients?.length ? session?.clients[0] : null;
const Current = isCoach ? StudentCard(client) : CoachCard(session?.coach);
return (
<div className="card-detail">
<div>
<Button
className="card-detail__back"
type="link"
icon={<LeftOutlined />}
onClick={goBack}
>
{i18nText('back', locale)}
</Button>
</div>
{Current}
{(activeType === SessionType.UPCOMING || activeType === SessionType.REQUESTED) &&
(session?.state === SessionState.CREATED || session?.state === SessionState.PAID
|| session?.state === SessionState.COACH_APPROVED || session?.state === SessionState.STARTED) && (
<div className="card-detail__actions">
<Button
className="card-detail__apply"
onClick={onApproveSession}
loading={approveLoading}
disabled={finishLoading}
>
{activeType === SessionType.UPCOMING
? (session?.state === SessionState.STARTED ? i18nText('session.join', locale) : i18nText('session.start', locale))
: i18nText('session.confirm', locale)}
</Button>
{session?.state === SessionState.STARTED && isCoach && (
<Button
className="card-detail__decline"
onClick={onFinishSession}
loading={finishLoading}
>
{i18nText('session.finish', locale)}
</Button>
)}
{session?.id && session?.state !== SessionState.STARTED && (
<>
<Button
className="card-detail__decline"
onClick={() => setOpenDeclineModal(true)}
disabled={approveLoading}
>
{i18nText('session.decline', locale)}
</Button>
<DeclineSessionModal
open={openDeclineModal}
handleCancel={() => setOpenDeclineModal(false)}
activeType={activeType}
locale={locale}
sessionId={session.id}
success={goBack}
/>
</>
)}
</div>
)}
{activeType !== SessionType.REQUESTED && (
<>
{activeType === SessionType.RECENT && (
<>
<div className="card-detail__name">{i18nText('courseInfo', locale)}</div>
<div className="card-detail__inner">
{/* <div className="card-detail__info">
<div className="card-profile__subtitle">{current?.specialityDesc}</div>
<div className="card-detail__lang">
{current?.coachLanguages?.map((lang) => (
<Tag key={lang} className="skills__list__item">{lang}</Tag>
))}
</div>
</div> */}
<div className="card-detail__cost">
{getPrice(session?.cost)} <span>/ {getDuration(locale, session?.totalDuration)}</span>
</div>
<div className="card-detail__skills">
<div className="skills__list">
{session?.themesTags?.slice(0, 2).map((skill) => <Tag key={skill?.id} className="skills__list__item">{skill?.name}</Tag>)}
{session?.themesTags?.length > 2
? (
<Tag className="skills__list__more">
{`+${session?.themesTags?.length - 2}`}
</Tag>
) : null }
</div>
</div>
{/* <div className="card-profile__desc">{current?.description}</div> */}
</div>
</>
)}
<div className="card-detail__comments">
<div className="card-detail__comments_header">
<div className="card-detail__comments_title">
{session?.clientComments?.length === 0 && session?.coachComments?.length === 0
? i18nText('session.comments', locale)
: i18nText('session.myComments', locale)}
</div>
{activeType === SessionType.UPCOMING && (
<>
<Button
className="card-detail__comments_add"
type="link"
iconPosition="end"
icon={<PlusOutlined style={{ fontSize: 18 }} />}
onClick={() => setOpenAddCommentModal(true)}
>
{i18nText('session.addComment', locale)}
</Button>
<AddCommentModal
open={openAddCommentModal}
handleCancel={() => setOpenAddCommentModal(false)}
locale={locale}
sessionId={session.id}
refresh={refresh}
/>
</>
)}
</div>
{(session?.clientComments?.length > 0 || session?.coachComments?.length > 0) ? (
<>
{(isCoach ? session?.coachComments : session?.clientComments)?.map(({ id, comment }) => (
<div key={`my_${id}`} className="card-detail__comments_item">
{comment}
</div>
))}
{(isCoach ? session?.clientComments : session?.coachComments)?.length > 0 && (
<div className="card-detail__comments_title">
{isCoach ? i18nText('session.clientComments', locale) : i18nText('session.coachComments', locale)}
</div>
)}
{(isCoach ? session?.clientComments : session?.coachComments)?.map(({ id , comment }) => (
<div key={`oth_${id}`} className="card-detail__comments_item">
{comment}
</div>
))}
</>
) : <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />}
</div>
</>
)}
</div>
);
};

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState, MouseEvent } from 'react';
import { Empty, Space } from 'antd'; import { Empty, Space } from 'antd';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import 'dayjs/locale/ru'; import 'dayjs/locale/ru';
@ -9,21 +9,29 @@ import 'dayjs/locale/de';
import 'dayjs/locale/it'; import 'dayjs/locale/it';
import 'dayjs/locale/fr'; import 'dayjs/locale/fr';
import 'dayjs/locale/es'; import 'dayjs/locale/es';
import { Loader } from '../view/Loader'; import { Loader } from '../../view/Loader';
import { useLocalStorage } from '../../hooks/useLocalStorage'; import { useLocalStorage } from '../../../hooks/useLocalStorage';
import { AUTH_TOKEN_KEY, AUTH_USER } from '../../constants/common'; import { AUTH_TOKEN_KEY, AUTH_USER } from '../../../constants/common';
import { getRecentSessions, getRequestedSessions, getUpcomingSessions } from '../../actions/profile'; import { getRecentSessions, getRequestedSessions, getUpcomingSessions } from '../../../actions/sessions';
import { Session, Sessions, SessionType } from '../../types/sessions'; import { Session, Sessions, SessionType } from '../../../types/sessions';
import { i18nText } from '../../i18nKeys'; import { useRouter, usePathname } from '../../../navigation';
import { i18nText } from '../../../i18nKeys';
export const SessionsTabs = ({ locale }: { locale: string }) => { type SessionsTabsProps = {
const [activeTab, setActiveTab] = useState<number>(0); locale: string;
activeTab: SessionType;
};
export const SessionsTabs = ({ locale, activeTab }: SessionsTabsProps) => {
const [sort, setSort] = useState<string>(); const [sort, setSort] = useState<string>();
const [sessions, setSessions] = useState<Sessions>(); const [sessions, setSessions] = useState<Sessions>();
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
const [errorData, setErrorData] = useState<any>(); const [errorData, setErrorData] = useState<any>();
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, ''); const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
const [userId] = useLocalStorage(AUTH_USER, ''); const [userData] = useLocalStorage(AUTH_USER, '');
const { id: userId = 0 } = userData ? JSON.parse(userData) : {};
const router = useRouter();
const pathname = usePathname();
const fetchData = () => { const fetchData = () => {
setErrorData(undefined); setErrorData(undefined);
@ -35,9 +43,9 @@ export const SessionsTabs = ({ locale }: { locale: string }) => {
]) ])
.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) => {
@ -56,6 +64,12 @@ export const SessionsTabs = ({ locale }: { locale: string }) => {
setSort(value); setSort(value);
}, [sort]); }, [sort]);
const onClickSession = (event: MouseEvent<HTMLDivElement>, id: number) => {
event.stopPropagation();
event.preventDefault();
router.push(`${pathname}/${id}`);
};
const getChildren = (list?: Session[]) => ( const getChildren = (list?: Session[]) => (
<> <>
{/* <div className="filter-session"> {/* <div className="filter-session">
@ -82,7 +96,7 @@ export const SessionsTabs = ({ locale }: { locale: string }) => {
const today = dayjs().format('YYYY-MM-DD') === startDate.format('YYYY-MM-DD'); const today = dayjs().format('YYYY-MM-DD') === startDate.format('YYYY-MM-DD');
return ( return (
<div key={id} className="card-profile session__item"> <div key={id} className="card-profile session__item" onClick={(e: MouseEvent<HTMLDivElement>) => onClickSession(e, id)}>
<div className="card-profile__header"> <div className="card-profile__header">
<div className="card-profile__header__portrait"> <div className="card-profile__header__portrait">
<img src={current?.faceImageUrl || '/images/person.png'} className="" alt="" /> <img src={current?.faceImageUrl || '/images/person.png'} className="" alt="" />
@ -102,7 +116,7 @@ export const SessionsTabs = ({ locale }: { locale: string }) => {
</div> </div>
) )
}) : ( }) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} /> <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={i18nText('noData', locale)} />
)} )}
</div> </div>
</> </>
@ -143,17 +157,17 @@ export const SessionsTabs = ({ locale }: { locale: string }) => {
refresh={fetchData} refresh={fetchData}
> >
<div className="tabs-session"> <div className="tabs-session">
{tabs.map((tab, index) => ( {tabs.map(({ key, label }) => (
<Space <Space
key={index} key={key}
className={`tabs-session__item ${index === activeTab ? 'active' : ''}`} className={`tabs-session__item ${key === activeTab ? 'active' : ''}`}
onClick={() => setActiveTab(index)} onClick={() => router.push(`/account/sessions/${key}`)}
> >
{tab.label} {label}
</Space> </Space>
))} ))}
</div> </div>
{tabs[activeTab].children} {tabs.filter(({ key }) => key === activeTab)[0].children}
</Loader> </Loader>
); );
}; };

View File

@ -0,0 +1,5 @@
'use client'
export * from './SessionDetails';
export * from './SessionsTabs';
export * from './SessionDetailsContent';

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

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