Compare commits
67 Commits
socialnetw
...
master
Author | SHA1 | Date |
---|---|---|
SD | 08d12cd89e | |
SD | 332595fd39 | |
SD | 46b0c5b747 | |
SD | d866ee2f62 | |
SD | 60a35db46b | |
SD | 0222335694 | |
SD | 9a3aa98158 | |
SD | c0feea48e5 | |
SD | 5b8ba1b5c4 | |
SD | cd44c9f1a1 | |
SD | 5712cbcf56 | |
SD | b31d2cf700 | |
SD | a39f53c57d | |
SD | 4ac2740942 | |
SD | be7efc0d32 | |
SD | d5808e96db | |
dzfelix | 59de68d611 | |
SD | b141a6ad44 | |
SD | 4b429c8655 | |
SD | 3ed78c0e45 | |
SD | 3345a533d2 | |
SD | 76ffdc4094 | |
SD | b52096b3bc | |
dzfelix | cda91b9ea9 | |
dzfelix | 28f5babf22 | |
dzfelix | 80f53e871d | |
dzfelix | 77d3c8f66b | |
dzfelix | f7fe427aae | |
dzfelix | 5844bd9e7c | |
dzfelix | dbb74b9ccd | |
dzfelix | 8ee52bc834 | |
norton81 | c563818e91 | |
norton81 | 2f2d9db82a | |
norton81 | 1461c4948e | |
norton81 | 44674a1910 | |
dzfelix | 74d93541a3 | |
SD | f92810d320 | |
SD | 6f5c3738b7 | |
Dasha | 526e703d9a | |
dzfelix | ed756d0646 | |
dzfelix | 3b2241892f | |
dzfelix | ee4dcb58cc | |
SD | ff74e5ba49 | |
SD | abf04b4c5b | |
Witalij Poljatchek | 4a00d715df | |
Witalij Poljatchek | ec5fb6d443 | |
Witalij Poljatchek | b36efa0ddf | |
Witalij Poljatchek | 020ba600d9 | |
Witalij Poljatchek | a9387b1f28 | |
Witalij Poljatchek | e926d4cb4a | |
dzfelix | bdbe4f4b04 | |
dzfelix | 3a6c7bd88c | |
SD | f3d2cfd55a | |
SD | 347ac0113c | |
SD | aa027466e2 | |
SD | f4de114fb3 | |
e510734 | f6e0f7dcdb | |
SD | 456e2a7cdf | |
SD | 035286823e | |
SD | a1204cea91 | |
SD | ab5bd5fe2b | |
SD | 5303bb895d | |
SD | 6c875cdf39 | |
SD | 0828e944b4 | |
SD | 8f00a5c41c | |
SD | c2e29cbef3 | |
SD | ffbbeadb40 |
|
@ -0,0 +1,8 @@
|
||||||
|
node_modules/
|
||||||
|
.next
|
||||||
|
.idea
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
npm-debug.log
|
||||||
|
README.md
|
||||||
|
.git
|
|
@ -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
|
27
Dockerfile
27
Dockerfile
|
@ -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"]
|
||||||
|
|
|
@ -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
|
|
@ -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,10 +20,10 @@ 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}
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
|
@ -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": {
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"applinks": {
|
||||||
|
"apps": [],
|
||||||
|
"details": [
|
||||||
|
{
|
||||||
|
"appID": "GTYAM4FYH3.com.bbuddy.whistle",
|
||||||
|
"paths": ["/en/experts/*"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 |
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
|
@ -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
|
||||||
|
});
|
||||||
|
|
|
@ -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,
|
||||||
|
// }),
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
};
|
|
@ -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
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
|
||||||
|
};
|
||||||
|
};
|
|
@ -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
|
||||||
|
};
|
||||||
|
};
|
|
@ -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
|
||||||
|
};
|
||||||
|
};
|
|
@ -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
|
||||||
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
});
|
|
@ -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
|
||||||
|
});
|
|
@ -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);
|
|
@ -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;
|
|
||||||
};
|
|
||||||
|
|
|
@ -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'
|
||||||
|
}
|
||||||
|
});
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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;
|
||||||
|
};
|
|
@ -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 (
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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 (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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">
|
||||||
|
|
|
@ -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 (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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();
|
||||||
|
};
|
|
@ -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;
|
||||||
|
};
|
|
@ -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();
|
||||||
|
};
|
|
@ -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;
|
||||||
|
};
|
|
@ -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;
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -1,150 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import type { Metadata } from 'next';
|
|
||||||
import { notFound } from 'next/navigation';
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: 'Bbuddy - Blog item',
|
|
||||||
description: 'Bbuddy desc blog item'
|
|
||||||
};
|
|
||||||
|
|
||||||
export function generateStaticParams() {
|
|
||||||
return [{ blogId: 'news-1' }, { blogId: 'news-2' }];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function BlogItem({ params }: { params: { blogId: string } }) {
|
|
||||||
if (!params?.blogId) notFound();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="b-news-page">
|
|
||||||
<div className="b-inner">
|
|
||||||
<h1 className="b-news-page__title">6 learnings from Shivpuri to Silicon Valley</h1>
|
|
||||||
<div className="news-item__badge">Leadership & Management</div>
|
|
||||||
<div className="b-news-page__text">
|
|
||||||
{`news id ${params.blogId}`}<br />
|
|
||||||
I’m excited to kick off this series of newsletters where I’ll be sharing my experiences, learnings,
|
|
||||||
and best practices which helped me to grow both in my personal and professional life. My hope is to
|
|
||||||
give back to the community and help anyone connect directly with me who may have got impacted with
|
|
||||||
recent layoffs, dealing with immigration challenges.
|
|
||||||
</div>
|
|
||||||
<div className="b-news-page__image">
|
|
||||||
<img className="" src="/images/news1.png" alt="" />
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info">
|
|
||||||
<div className="news-item__info__author">
|
|
||||||
<img className="" src="/images/author.png" alt="" />
|
|
||||||
<div className="news-item__info__author__inner">
|
|
||||||
<div className="news-item__info__name">Sonali Garg</div>
|
|
||||||
<div className="news-item__info__date">February 6th, 2023</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info__counter">
|
|
||||||
<div className="news-item__info__like">
|
|
||||||
<img className="" src="/images/heart-outline.svg" alt="" />
|
|
||||||
165
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info__share">
|
|
||||||
<img className="" src="/images/share-social.svg" alt="" />
|
|
||||||
Share
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="b-news-page__inner">
|
|
||||||
<h2 className="title-h2">
|
|
||||||
This is not about layoffs, it's about living with whatever life throws at you..
|
|
||||||
</h2>
|
|
||||||
<p className="b-news-page__text">
|
|
||||||
Over the past few months, as the macro-economic events have unfolded, I have heard voices filled
|
|
||||||
with anxiety, helplessness and general lack of confidence to deal with this ambiguity from my
|
|
||||||
mentees, colleagues, friends and family. I was laid off from Meta last November and I firmly
|
|
||||||
believe this is nothing but a bump in the road that might seem like a steep climb in the
|
|
||||||
short-term. I may not have all the answers but this has inspired me to share my story. If you
|
|
||||||
are looking for a sob story, you can stop reading now. Ever wondered what it takes for a girl
|
|
||||||
born into a conservative family in a small sleepy town in India, who lost one of her parents at
|
|
||||||
age 17, earned her living while pursuing engineering, moved to the UK by herself and ended up
|
|
||||||
working in big tech in Silicon valley? My goal with this series of posts is to inspire and share
|
|
||||||
my mental models that helped me throughout my professional and personal life.
|
|
||||||
</p>
|
|
||||||
<p className="b-news-page__text">
|
|
||||||
After completing my engineering, I started my career at a small software company in Bhopal and
|
|
||||||
then worked for TCS(Tata Consultancy Services), one of the largest IT-outsourcing companies in
|
|
||||||
the world for almost 5 years. Over the past 14 years, I have worked for big tech companies like
|
|
||||||
Meta (Facebook) and Google, wore multiple hats, led strategic programs, scaled multi
|
|
||||||
billion-dollar businesses, built teams and helped achieve business operational excellence.
|
|
||||||
Throughout my career, I’ve dealt with several challenges from execution to scale to building a
|
|
||||||
high performance team. A lot of my early struggles were about how to assimilate in a new
|
|
||||||
culture, create a network in a new environment, earn trust, create and nurture work
|
|
||||||
relationships into fruitful friendships and so on.
|
|
||||||
</p>
|
|
||||||
<p className="b-news-page__text">
|
|
||||||
I was born in a conservative family in a small town called ‘Shivpuri’, also known as ‘Mini
|
|
||||||
Kashmir’ because of its natural beauty. My father was a civil engineer working on Madikheda Dam
|
|
||||||
on Sindh river and was a strict disciplinarian. He was gone from dawn to dusk and was always
|
|
||||||
focused. My mother was a teacher in a school that was about 30 kms from our home. We (me and my
|
|
||||||
sister) would often be left with neighbors to be taken care of and this led us to become
|
|
||||||
independent at an early age. Our otherwise slow paced, simple life with only a few families
|
|
||||||
around in the government quarters that were set up to support construction of the dam was filled
|
|
||||||
with natural beauty, wildlife and a community of close friends. Our lives were balanced and
|
|
||||||
while my parents worked hard to provide basic needs, we were satisfied. There were only a few
|
|
||||||
schools with Hindi being the prevalent language as the medium of teaching. There were no
|
|
||||||
colleges for advanced studies and most girls did not go to college often married off by their
|
|
||||||
18th birthday. Generally speaking, we had a joyous childhood with just the basics. While most
|
|
||||||
folks we interacted with were not highly educated nor ambitious, earned lower middle class
|
|
||||||
salaries and lacked exposure to the outside world but there was plenty to learn from them.
|
|
||||||
People had learnt to stick together in good and bad times. They embodied the old school
|
|
||||||
qualities of hard work, dedication and commitment. Be willing to give it all- hard work,
|
|
||||||
dedication and commitment.
|
|
||||||
</p>
|
|
||||||
<p className="b-news-page__text">
|
|
||||||
In 2003, my father passed away suddenly and we found ourselves in crisis. My mother was a
|
|
||||||
teacher and she did not have time to deal with her grief. Rather, she was struggling to garner
|
|
||||||
support to get transferred to a school in Bhopal, capital of Madhya Pradesh to be closer to our
|
|
||||||
maternal grandparents. As we uprooted ourselves from Shivpuri to Bhopal, one of my father’s
|
|
||||||
loyal friends came to help load the moving truck. While he had nothing to gain out of us, he
|
|
||||||
continued to serve us until the last day in Shivpuri. Remember, in crisis your team matters more
|
|
||||||
than any other time. Advocate for them ruthlessly in good and bad times, they will come through
|
|
||||||
in crisis.
|
|
||||||
</p>
|
|
||||||
<p className="b-news-page__text">
|
|
||||||
Eventually we found our footing, my mother’s job was transferred to a local school in Bhopal and
|
|
||||||
I got admission in a government engineering college. My sister was still attending high school
|
|
||||||
and both of us were teaching tuition classes to middle school students in the evenings to make
|
|
||||||
ends meet. I also started a tiffin service for a few out of town students while attending
|
|
||||||
college to pay for my transportation and cost of supplies. We refused to give up. Persevere when
|
|
||||||
all else fails.
|
|
||||||
</p>
|
|
||||||
<p className="b-news-page__text">
|
|
||||||
Our 5 years went by quickly in Bhopal as we worked towards improving our financial situation and
|
|
||||||
I completed my Bachelors in Computer Science. This was the time I first stepped out to live in a
|
|
||||||
metropolitan city, Mumbai for my job at TCS. This was a paradigm shift from Bhopal and I was
|
|
||||||
blown away to meet so many talented folks in Mumbai. In my head, I did not belong in this place.
|
|
||||||
I had imposter syndrome and felt like an outsider trying to make it in a new city. Most people I
|
|
||||||
met were fluent in more than 1 language, well-dressed, communicated openly and with confidence,
|
|
||||||
and presented themselves well. I was always in a dilemma when it came to adopting values. It
|
|
||||||
took me a while to adjust to it but I was still not confident about my work and communication
|
|
||||||
while my hard skills that I learnt in engineering were top notch. I kept questioning my
|
|
||||||
abilities but persisted. This was not the first time I was out of my comfort zone. Persist, when
|
|
||||||
in discomfort.
|
|
||||||
</p>
|
|
||||||
<p className="b-news-page__text">
|
|
||||||
I worked with multiple global companies who were clients of TCS and was presented an opportunity
|
|
||||||
to move to Scotland, UK for an year to work for GE, who was also a client. This was my first
|
|
||||||
opportunity to explore a different culture, food, music, languages etc. I remember working on my
|
|
||||||
english when in Mumbai, in preparation for my UK trip. It was really difficult to understand the
|
|
||||||
accent in the UK, even though language was not a barrier. I still remember certain words would
|
|
||||||
just not get across no matter how hard some of my colleagues tried and they would end up using
|
|
||||||
signs to convey. Be prepared, opportunities come to those who are prepared.
|
|
||||||
</p>
|
|
||||||
<p className="b-news-page__text">
|
|
||||||
In 2013, I came to the US on a dependent visa after marriage and quickly realized the curse of
|
|
||||||
H4 visa. I paved my path by going back to school at UC Berkeley and then jumped back into
|
|
||||||
building my career from scratch. While working in the US over the past years, I realized college
|
|
||||||
degrees with good grades and certifications definitely help you to get your foot in the door but
|
|
||||||
are not enough to be successful in your career. As I was again starting from scratch in a new
|
|
||||||
culture, determined to do whatever it takes, having done this a few times before, it doesn’t
|
|
||||||
scare me as much. Never be afraid to start from zero again!
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -0,0 +1,112 @@
|
||||||
|
import React from 'react';
|
||||||
|
import type {Metadata, ResolvingMetadata} from 'next';
|
||||||
|
import { draftMode } from 'next/headers'
|
||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
import {fetchBlogPost, fetchBlogPosts, Widget} from "../../../../lib/contentful/blogPosts";
|
||||||
|
import Util from "node:util";
|
||||||
|
import RichText from "../../../../lib/contentful/RichText";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
interface BlogPostPageParams {
|
||||||
|
slug: string
|
||||||
|
locale: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BlogPostPageProps {
|
||||||
|
params: BlogPostPageParams
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({ params }: BlogPostPageProps, parent: ResolvingMetadata): Promise<Metadata> {
|
||||||
|
const blogPost = await fetchBlogPost({ slug: params.slug, preview: draftMode().isEnabled })
|
||||||
|
|
||||||
|
if (!blogPost) {
|
||||||
|
return notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: blogPost.title,
|
||||||
|
description: blogPost.metaDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function renderWidget (widget: Widget, index: number) {
|
||||||
|
switch (widget.type){
|
||||||
|
case 'widgetParagraph':
|
||||||
|
return (
|
||||||
|
<div key={'widget'+index} >
|
||||||
|
<h2 className="title-h2">
|
||||||
|
{widget.widget.subTitle}
|
||||||
|
</h2>
|
||||||
|
<RichText document={widget.widget.body} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
case 'widgetMedia':
|
||||||
|
return (
|
||||||
|
<img key={'widget'+index} src={widget.widget.file?.src}/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function BlogItem({params}: { params: BlogPostPageParams }) {
|
||||||
|
const item = await fetchBlogPost({slug: params.slug, preview: draftMode().isEnabled })
|
||||||
|
console.log('BLOG POST')
|
||||||
|
console.log(Util.inspect(item, {showHidden: false, depth: null, colors: true}))
|
||||||
|
if (!item) notFound();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="b-news-page">
|
||||||
|
<div className="b-inner">
|
||||||
|
<h1 className="b-news-page__title">{item.title}</h1>
|
||||||
|
<div className="news-item__badge">{item.category}</div>
|
||||||
|
<div className="b-news-page__text">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div className="b-news-page__image">
|
||||||
|
<img className="" src="/images/news1.png" alt=""/>
|
||||||
|
</div>
|
||||||
|
<div className="news-item__info">
|
||||||
|
<div className="news-item__info__author">
|
||||||
|
<Link href={`/${params.locale}/experts/${item.author?.expertId}`} className="news-item">
|
||||||
|
<img className="" src={item.author.avatar.src} alt=""/>
|
||||||
|
<div className="news-item__info__author__inner">
|
||||||
|
<div className="news-item__info__name">{item.author?.name}</div>
|
||||||
|
<div className="news-item__info__date">{item.createdAt}</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="news-item__info__counter">
|
||||||
|
<div className="news-item__info__like">
|
||||||
|
<img className="" src="/images/heart-outline.svg" alt=""/>
|
||||||
|
165
|
||||||
|
</div>
|
||||||
|
<div className="news-item__info__share">
|
||||||
|
<img className="" src="/images/share-social.svg" alt=""/>
|
||||||
|
Share
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="b-news-page__inner">
|
||||||
|
{item.body.map(renderWidget)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="b-inner" style={ {marginTop: '40px'}}>
|
||||||
|
<nav className="min-h-6 pb-6">
|
||||||
|
<Link href='/'>
|
||||||
|
Home
|
||||||
|
</Link>
|
||||||
|
>
|
||||||
|
<Link href={`/${params.locale}/blog/category/${item.categorySlug}`}>
|
||||||
|
{item.category}
|
||||||
|
</Link>
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{item.title}
|
||||||
|
</span>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,29 @@
|
||||||
|
import React from 'react';
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
import { draftMode } from 'next/headers'
|
||||||
|
import {unstable_setRequestLocale} from "next-intl/server";
|
||||||
|
import Link from "next/link";
|
||||||
|
import {fetchBlogPosts} from "../../../../../lib/contentful/blogPosts";
|
||||||
|
import {fetchBlogPostCategories} from "../../../../../lib/contentful/blogPostsCategories";
|
||||||
|
import {BlogPosts} from "../../../../../components/BlogPosts/BlogPosts";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Bbuddy - Blog',
|
||||||
|
description: 'Bbuddy desc blog'
|
||||||
|
};
|
||||||
|
|
||||||
|
interface BlogPostPageParams {
|
||||||
|
slug: string
|
||||||
|
locale: string
|
||||||
|
}
|
||||||
|
interface BlogPostPageProps {
|
||||||
|
params: BlogPostPageParams
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function Blog({params, searchParams}: { params: BlogPostPageParams, searhParams?: {page: number} }) {
|
||||||
|
unstable_setRequestLocale(params.locale);
|
||||||
|
const page = searchParams.page || undefined
|
||||||
|
return (
|
||||||
|
<BlogPosts basePath={'/'+params.locale+'/blog/'} locale={params.locale} currentCat={params.slug} page={page}/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,213 +1,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">
|
|
||||||
I’m excited to kick off this series of newsletters where I’ll be sharing my
|
|
||||||
experiences,
|
|
||||||
learnings, and best practices which helped me to grow both in my personal and
|
|
||||||
professional life. My hope is to give back to the community and help anyone
|
|
||||||
connect directly with me who may have got impacted with recent layoffs,
|
|
||||||
dealing with immigration challenges.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info">
|
|
||||||
<div className="news-item__info__author">
|
|
||||||
<img className="" src="/images/author.png" alt="" />
|
|
||||||
<div className="news-item__info__author__inner">
|
|
||||||
<div className="news-item__info__name">Sonali Garg</div>
|
|
||||||
<div className="news-item__info__date">February 6th, 2023</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info__counter">
|
|
||||||
<div className="news-item__info__like">
|
|
||||||
<img className="" src="/images/heart-outline.svg" alt="" />
|
|
||||||
165
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info__share">
|
|
||||||
<img className="" src="/images/share-social.svg" alt="" />
|
|
||||||
Share
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a href="#" className="news-item">
|
|
||||||
<div className="news-item__image">
|
|
||||||
<img className="" src="/images/news.png" alt="" />
|
|
||||||
</div>
|
|
||||||
<div className="news-item__inner">
|
|
||||||
<div className="">
|
|
||||||
<div className="news-item__title">
|
|
||||||
6 learnings from Shivpuri to Silicon Valley
|
|
||||||
</div>
|
|
||||||
<div className="news-item__badge">Leadership & Management</div>
|
|
||||||
<div className="news-item__text">
|
|
||||||
I’m excited to kick off this series of newsletters where I’ll be sharing my
|
|
||||||
experiences,
|
|
||||||
learnings, and best practices which helped me to grow both in my personal and
|
|
||||||
professional life. My hope is to give back to the community and help anyone
|
|
||||||
connect directly with me who may have got impacted with recent layoffs,
|
|
||||||
dealing with immigration challenges.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info">
|
|
||||||
<div className="news-item__info__author">
|
|
||||||
<img className="" src="/images/author.png" alt="" />
|
|
||||||
<div className="news-item__info__author__inner">
|
|
||||||
<div className="news-item__info__name">Sonali Garg</div>
|
|
||||||
<div className="news-item__info__date">February 6th, 2023</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info__counter">
|
|
||||||
<div className="news-item__info__like">
|
|
||||||
<img className="" src="/images/heart-outline.svg" alt="" />
|
|
||||||
165
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info__share">
|
|
||||||
<img className="" src="/images/share-social.svg" alt="" />
|
|
||||||
Share
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a href="#" className="news-item">
|
|
||||||
<div className="news-item__image">
|
|
||||||
<img className="" src="/images/news.png" alt="" />
|
|
||||||
</div>
|
|
||||||
<div className="news-item__inner">
|
|
||||||
<div className="">
|
|
||||||
<div className="news-item__title">
|
|
||||||
6 learnings from Shivpuri to Silicon Valley
|
|
||||||
</div>
|
|
||||||
<div className="news-item__badge">Leadership & Management</div>
|
|
||||||
<div className="news-item__text">
|
|
||||||
I’m excited to kick off this series of newsletters where I’ll be sharing my
|
|
||||||
experiences,
|
|
||||||
learnings, and best practices which helped me to grow both in my personal and
|
|
||||||
professional life. My hope is to give back to the community and help anyone
|
|
||||||
connect directly with me who may have got impacted with recent layoffs,
|
|
||||||
dealing with immigration challenges.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info">
|
|
||||||
<div className="news-item__info__author">
|
|
||||||
<img className="" src="/images/author.png" alt="" />
|
|
||||||
<div className="news-item__info__author__inner">
|
|
||||||
<div className="news-item__info__name">Sonali Garg</div>
|
|
||||||
<div className="news-item__info__date">February 6th, 2023</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info__counter">
|
|
||||||
<div className="news-item__info__like">
|
|
||||||
<img className="" src="/images/heart-outline.svg" alt="" />
|
|
||||||
165
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info__share">
|
|
||||||
<img className="" src="/images/share-social.svg" alt="" />
|
|
||||||
Share
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a href="#" className="news-item">
|
|
||||||
<div className="news-item__image">
|
|
||||||
<img className="" src="/images/news.png" alt="" />
|
|
||||||
</div>
|
|
||||||
<div className="news-item__inner">
|
|
||||||
<div className="">
|
|
||||||
<div className="news-item__title">
|
|
||||||
6 learnings from Shivpuri to Silicon Valley
|
|
||||||
</div>
|
|
||||||
<div className="news-item__badge">Leadership & Management</div>
|
|
||||||
<div className="news-item__text">
|
|
||||||
I’m excited to kick off this series of newsletters where I’ll be sharing my
|
|
||||||
experiences,
|
|
||||||
learnings, and best practices which helped me to grow both in my personal and
|
|
||||||
professional life. My hope is to give back to the community and help anyone
|
|
||||||
connect directly with me who may have got impacted with recent layoffs,
|
|
||||||
dealing with immigration challenges.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info">
|
|
||||||
<div className="news-item__info__author">
|
|
||||||
<img className="" src="/images/author.png" alt="" />
|
|
||||||
<div className="news-item__info__author__inner">
|
|
||||||
<div className="news-item__info__name">Sonali Garg</div>
|
|
||||||
<div className="news-item__info__date">February 6th, 2023</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info__counter">
|
|
||||||
<div className="news-item__info__like">
|
|
||||||
<img className="" src="/images/heart-outline.svg" alt="" />
|
|
||||||
165
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info__share">
|
|
||||||
<img className="" src="/images/share-social.svg" alt="" />
|
|
||||||
Share
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
|
@ -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 });
|
||||||
|
}
|
|
@ -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}`}>
|
||||||
|
|
|
@ -6,4 +6,4 @@ export default function Loading() {
|
||||||
...loading
|
...loading
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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} />;
|
||||||
|
};
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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;
|
||||||
|
};
|
|
@ -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} />;
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
|
@ -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" />
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
export * from './RemoteVideoPlayer';
|
||||||
|
export * from './UserCover';
|
||||||
|
export * from './RemoteUsers';
|
||||||
|
export * from './LocalUserPanel';
|
||||||
|
export * from './RemoteUserPanel';
|
||||||
|
export * from './UsersGroupPanel';
|
|
@ -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} />
|
||||||
|
);
|
|
@ -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;
|
||||||
|
};
|
|
@ -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>
|
||||||
|
);
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./MediaControl";
|
|
@ -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';
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
|
};
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,6 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
export * from './RoomDetails';
|
||||||
|
export * from './RoomsTabs';
|
||||||
|
export * from './RoomDetailsContent';
|
||||||
|
export * from './CreateRoom';
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
|
@ -0,0 +1,5 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
export * from './SessionDetails';
|
||||||
|
export * from './SessionsTabs';
|
||||||
|
export * from './SessionDetailsContent';
|
|
@ -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
Loading…
Reference in New Issue