Compare commits
45 Commits
Author | SHA1 | Date |
---|---|---|
|
73229cbe19 | |
|
c89584dba0 | |
|
83965dd675 | |
|
a96a49d649 | |
|
f8c797abf3 | |
|
6b8cad1081 | |
|
ccc3e2cb79 | |
|
9ecb7d6981 | |
|
fc743dbb7d | |
|
28789e9054 | |
|
4e849b47a2 | |
|
33bf78ecc3 | |
|
42a4ae0c6c | |
|
fdb464ae68 | |
|
a6bba53dd2 | |
|
eff29677dc | |
|
2da77f7347 | |
|
87b14e8716 | |
|
52fba3a879 | |
|
dbd5eaa014 | |
|
3ab06523cc | |
|
a13eac0ac4 | |
|
79a133c3ca | |
|
08d12cd89e | |
|
332595fd39 | |
|
46b0c5b747 | |
|
d866ee2f62 | |
|
60a35db46b | |
|
0222335694 | |
|
9a3aa98158 | |
|
c0feea48e5 | |
|
5b8ba1b5c4 | |
|
61de5c81e7 | |
|
cd44c9f1a1 | |
|
5712cbcf56 | |
|
b31d2cf700 | |
|
a39f53c57d | |
|
4ac2740942 | |
|
be7efc0d32 | |
|
d5808e96db | |
|
59de68d611 | |
|
b141a6ad44 | |
|
3b2241892f | |
|
ee4dcb58cc | |
|
6a9bed479a |
5
.env
5
.env
|
@ -1,6 +1,11 @@
|
||||||
NEXT_PUBLIC_SERVER_BASE_URL=https://api.bbuddy.expert/api
|
NEXT_PUBLIC_SERVER_BASE_URL=https://api.bbuddy.expert/api
|
||||||
NEXT_PUBLIC_AGORA_APPID=ed90c9dc42634e5687d4e2e0766b363f
|
NEXT_PUBLIC_AGORA_APPID=ed90c9dc42634e5687d4e2e0766b363f
|
||||||
|
NEXT_PUBLIC_GOOGLE_CLIENT_ID=909563069647-03rivr8k1jmirf382bcfehegamthcfg4.apps.googleusercontent.com
|
||||||
|
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51LVB3LK5pVGxNPeKk4gedt5NW4cb8k7BVXvgOMPTK4x1nnbGTD8BCqDqgInboT6N72YwrTl4tOsVz8rAjbUadX1m00y4Aq5qE8
|
||||||
|
NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_test_51LVB3LK5pVGxNPeK6j0wCsPqYMoGfcuwf1LpwGEBsr1dUx4NngukyjYL2oMZer5EOlW3lqnVEPjNDruN0OkUohIf00fWFUHN5O
|
||||||
|
NEXT_PUBLIC_STRIPE_PAYMENT_DESCRIPTION='BBuddy services'
|
||||||
|
|
||||||
NEXT_PUBLIC_CONTENTFUL_SPACE_ID = voxpxjq7y7vf
|
NEXT_PUBLIC_CONTENTFUL_SPACE_ID = voxpxjq7y7vf
|
||||||
NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN = s99GWKfpDKkNwiEJ3pN7US_tmqsGvDlaex-sOJwpzuc
|
NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN = s99GWKfpDKkNwiEJ3pN7US_tmqsGvDlaex-sOJwpzuc
|
||||||
NEXT_PUBLIC_CONTENTFUL_PREVIEW_ACCESS_TOKEN = Z9WOKpLDbKNj7xVOmT_VXYNLH0AZwISFvQsq0PQlHfE
|
NEXT_PUBLIC_CONTENTFUL_PREVIEW_ACCESS_TOKEN = Z9WOKpLDbKNj7xVOmT_VXYNLH0AZwISFvQsq0PQlHfE
|
||||||
|
|
||||||
|
|
|
@ -38,3 +38,5 @@ yarn-error.log*
|
||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
|
certificates
|
|
@ -1,5 +1,6 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
const withNextIntl = require('next-intl/plugin')();
|
const createNextIntlPlugin = require('next-intl/plugin');
|
||||||
|
const withNextIntl = createNextIntlPlugin();
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const json = require('./package.json');
|
const json = require('./package.json');
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "bbuddy-ui",
|
"name": "bbuddy-ui",
|
||||||
"version": "0.0.3",
|
"version": "0.3.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -p 4200",
|
"dev": "next dev -p 4200",
|
||||||
|
@ -13,10 +13,15 @@
|
||||||
"@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",
|
"@contentful/rich-text-react-renderer": "^15.22.9",
|
||||||
"agora-rtc-react": "^2.1.0",
|
"@microsoft/signalr": "^8.0.7",
|
||||||
|
"@react-oauth/google": "^0.12.1",
|
||||||
|
"@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",
|
"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",
|
"contentful": "^10.13.3",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
|
@ -24,9 +29,13 @@
|
||||||
"next": "14.0.3",
|
"next": "14.0.3",
|
||||||
"next-intl": "^3.3.1",
|
"next-intl": "^3.3.1",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
|
"react-apple-login": "^1.1.6",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
|
"react-signalr": "^0.2.24",
|
||||||
"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,14 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"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",
|
||||||
|
"D0:28:97:E7:64:5D:ED:8D:7F:F1:41:B2:E8:F6:AB:7B:EE:FB:A3:1A:A2:D7:92:D4:C5:41:9A:3C:47:CE:EB:43",
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
|
@ -12,3 +12,39 @@ export const getRegister = (locale: string): Promise<{ jwtToken: string }> => ap
|
||||||
method: 'post',
|
method: 'post',
|
||||||
locale
|
locale
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const getRegisterByGoogle = (locale: string, accesstoken: string): Promise<{ jwtToken: string }> => apiRequest({
|
||||||
|
url: '/auth/registerexternal',
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
platform: 0,
|
||||||
|
provider: 4,
|
||||||
|
accesstoken
|
||||||
|
},
|
||||||
|
locale
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getLoginByGoogle = (locale: string, accesstoken: string): Promise<{ jwtToken: string }> => apiRequest({
|
||||||
|
url: '/auth/tryloginexternal',
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
platform: 0,
|
||||||
|
provider: 4,
|
||||||
|
accesstoken
|
||||||
|
},
|
||||||
|
locale
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getRegisterByApple = (locale: string, code: string): Promise<{ jwtToken: string }> => apiRequest({
|
||||||
|
url: '/auth/registerexternalappleweb',
|
||||||
|
method: 'post',
|
||||||
|
data: { code },
|
||||||
|
locale
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getLoginByApple = (locale: string, code: string): Promise<{ jwtToken: string }> => apiRequest({
|
||||||
|
url: '/auth/tryloginexternalappleweb',
|
||||||
|
method: 'post',
|
||||||
|
data: { code },
|
||||||
|
locale
|
||||||
|
});
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import {apiRequest} from "../helpers";
|
||||||
|
|
||||||
|
|
||||||
|
export const getChatList = (locale: string, token: string): Promise<any> => apiRequest({
|
||||||
|
url: '/chat/chatList',
|
||||||
|
method: 'get',
|
||||||
|
locale,
|
||||||
|
token
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getChatMessages = (locale: string, token: string, group: number): Promise<any> => apiRequest({
|
||||||
|
url: '/chat/chat_messages/'+group,
|
||||||
|
method: 'get',
|
||||||
|
locale,
|
||||||
|
token
|
||||||
|
});
|
|
@ -1,5 +1,5 @@
|
||||||
import { GeneralFilter, ExpertsData, ExpertDetails } from '../types/experts';
|
|
||||||
import { apiRequest } from './helpers';
|
import { apiRequest } from './helpers';
|
||||||
|
import { GeneralFilter, ExpertsData, ExpertDetails, ExpertScheduler, ExpertSchedulerSession, SignupSessionData } from '../types/experts';
|
||||||
|
|
||||||
export const getExpertsList = (locale: string, filter?: GeneralFilter): Promise<ExpertsData> => apiRequest({
|
export const getExpertsList = (locale: string, filter?: GeneralFilter): Promise<ExpertsData> => apiRequest({
|
||||||
url: '/home/coachsearch1',
|
url: '/home/coachsearch1',
|
||||||
|
@ -14,3 +14,18 @@ export const getExpertById = (id: string, locale: string): Promise<ExpertDetails
|
||||||
data: { id },
|
data: { id },
|
||||||
locale
|
locale
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const getSchedulerByExpertId = (id: string, locale: string): Promise<ExpertScheduler> => apiRequest({
|
||||||
|
url: '/home/sessionsignupdata',
|
||||||
|
method: 'post',
|
||||||
|
data: { id },
|
||||||
|
locale
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getSchedulerSession = (data: SignupSessionData, locale: string, token: string): Promise<ExpertSchedulerSession> => apiRequest({
|
||||||
|
url: '/home/sessionsignupsubmit',
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
locale,
|
||||||
|
token
|
||||||
|
});
|
||||||
|
|
|
@ -23,22 +23,22 @@ export const apiRequest = async <T = any, K = any>(
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// const {
|
const {
|
||||||
// response: {
|
response: {
|
||||||
// status: responseCode = null,
|
status: responseCode = null,
|
||||||
// statusText = '',
|
statusText = '',
|
||||||
// data: { message = '', status: errorKey = '' } = {},
|
data,
|
||||||
// } = {},
|
} = {},
|
||||||
// code: statusCode = '',
|
code: statusCode = '',
|
||||||
// } = err as AxiosError;
|
} = err as AxiosError;
|
||||||
//
|
|
||||||
// throw new Error(
|
throw new Error(
|
||||||
// JSON.stringify({
|
JSON.stringify({
|
||||||
// statusCode,
|
statusCode,
|
||||||
// statusMessage: message || statusText,
|
statusMessage: statusText,
|
||||||
// responseCode,
|
responseCode,
|
||||||
// errorKey,
|
details: data
|
||||||
// }),
|
}),
|
||||||
// );
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { ProfileData, ProfileRequest } from '../../types/profile';
|
import { ProfileData, ProfileRequest } from '../../types/profile';
|
||||||
import { getPersonalData, setPersonData } from '../profile';
|
import { getPersonalData, setPersonData } from '../profile';
|
||||||
import { useLocalStorage } from '../../hooks/useLocalStorage';
|
import { useLocalStorage } from '../../hooks/useLocalStorage';
|
||||||
|
@ -18,7 +18,7 @@ export const useProfileSettings = (locale: string) => {
|
||||||
setProfileSettings(data);
|
setProfileSettings(data);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setFetchLoading(false);
|
setFetchLoading(false);
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
'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';
|
||||||
|
import {SessionState} from "../../types/sessions";
|
||||||
|
|
||||||
|
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 [isStarted, setIsStarted] = useState(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();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (room?.state === SessionState.STARTED) {
|
||||||
|
setIsStarted(true);
|
||||||
|
}
|
||||||
|
}, [room?.state])
|
||||||
|
|
||||||
|
return {
|
||||||
|
fetchData,
|
||||||
|
loading,
|
||||||
|
room,
|
||||||
|
errorData,
|
||||||
|
isStarted
|
||||||
|
};
|
||||||
|
};
|
|
@ -23,20 +23,20 @@ export const useSessionTracking = (locale: string, sessionId: number) => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
window.clearInterval(timer.current);
|
window?.clearInterval(timer.current);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const start = () => {
|
const start = () => {
|
||||||
window.clearInterval(timer.current);
|
window?.clearInterval(timer.current);
|
||||||
|
|
||||||
timer.current = window.setInterval(() => {
|
timer.current = window?.setInterval(() => {
|
||||||
fetchData();
|
fetchData();
|
||||||
}, DURATION);
|
}, DURATION);
|
||||||
};
|
};
|
||||||
|
|
||||||
const stop = () => {
|
const stop = () => {
|
||||||
window.clearInterval(timer.current);
|
window?.clearInterval(timer.current);
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -0,0 +1,125 @@
|
||||||
|
import { apiRequest } from './helpers';
|
||||||
|
import {GetUsersForRooms, Report, ReportData, 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
|
||||||
|
});
|
||||||
|
|
||||||
|
// report
|
||||||
|
export const getReport = (locale: string, token: string, id: number): Promise<Report[]> => apiRequest({
|
||||||
|
url: `/home/getsessionsupervisorscores?sessionId=${id}`,
|
||||||
|
method: 'post',
|
||||||
|
locale,
|
||||||
|
token
|
||||||
|
});
|
||||||
|
|
||||||
|
export const saveReport = (locale: string, token: string, data: ReportData): Promise<any> => apiRequest({
|
||||||
|
url: '/home/setsessionsupervisorscores',
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
locale,
|
||||||
|
token
|
||||||
|
});
|
|
@ -91,3 +91,11 @@ export const finishSession = (locale: string, token: string, sessionId: number):
|
||||||
locale,
|
locale,
|
||||||
token
|
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,78 @@
|
||||||
|
"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;
|
||||||
|
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,8 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { unstable_setRequestLocale } from 'next-intl/server';
|
// import { unstable_setRequestLocale } from 'next-intl/server';
|
||||||
|
|
||||||
export default function Directions({ params: { locale } }: { params: { locale: string } }) {
|
export default function Directions() {
|
||||||
unstable_setRequestLocale(locale);
|
// unstable_setRequestLocale(locale);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="main-popular">
|
<div className="main-popular">
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { unstable_setRequestLocale } from 'next-intl/server';
|
// 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: { locale } }: { params: { locale: string } }) {
|
export default function ExpertsPage({ params: { locale } }: { params: { locale: string } }) {
|
||||||
unstable_setRequestLocale(locale);
|
// unstable_setRequestLocale(locale);
|
||||||
const t = useTranslations('Experts');
|
const t = useTranslations('Experts');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslations } from 'next-intl';
|
// import { useTranslations } from 'next-intl';
|
||||||
import {getTranslations, unstable_setRequestLocale} from 'next-intl/server';
|
import Link from 'next/link';
|
||||||
|
import { getTranslations } from 'next-intl/server';
|
||||||
import { i18nText } from '../../../../i18nKeys';
|
import { i18nText } from '../../../../i18nKeys';
|
||||||
import {fetchBlogPosts} from "../../../../lib/contentful/blogPosts";
|
import { fetchBlogPosts } from '../../../../lib/contentful/blogPosts';
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
export default async function News({params: {locale}}: { params: { locale: string } }) {
|
export default async function News({params: {locale}}: { params: { locale: string } }) {
|
||||||
unstable_setRequestLocale(locale);
|
// unstable_setRequestLocale(locale);
|
||||||
const t = await getTranslations('Main');
|
const t = await getTranslations('Main');
|
||||||
const {data, total} = await fetchBlogPosts({preview: false, sticky: true})
|
const { data, total } = await fetchBlogPosts({preview: false, sticky: true})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="main-articles">
|
<div className="main-articles">
|
||||||
|
|
|
@ -15,7 +15,8 @@ import React, { ReactNode } from 'react';
|
||||||
export default function MainLayout({ children, news, experts }: {
|
export default function MainLayout({ children, news, experts }: {
|
||||||
children: ReactNode,
|
children: ReactNode,
|
||||||
news: ReactNode,
|
news: ReactNode,
|
||||||
experts: ReactNode
|
experts: ReactNode,
|
||||||
|
payment: ReactNode
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -24,4 +25,4 @@ export default function MainLayout({ children, news, experts }: {
|
||||||
{experts}
|
{experts}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from '../../../../../../navigation';
|
import { Link } from '../../../../../../i18n/routing';
|
||||||
import { CustomSelect } from '../../../../../../components/view/CustomSelect';
|
import { CustomSelect } from '../../../../../../components/view/CustomSelect';
|
||||||
|
|
||||||
export default function AddOffer() {
|
export default function AddOffer() {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from '../../../../../../navigation';
|
import { Link } from '../../../../../../i18n/routing';
|
||||||
import { CustomSelect } from '../../../../../../components/view/CustomSelect';
|
import { CustomSelect } from '../../../../../../components/view/CustomSelect';
|
||||||
|
|
||||||
export default function NewTopic() {
|
export default function NewTopic() {
|
||||||
|
|
|
@ -57,7 +57,7 @@ export default function ExpertProfilePage({ params: { locale } }: { params: { lo
|
||||||
}
|
}
|
||||||
}, [jwt]);
|
}, [jwt]);
|
||||||
|
|
||||||
return (
|
return data ? (
|
||||||
<Loader isLoading={loading}>
|
<Loader isLoading={loading}>
|
||||||
<ExpertProfile
|
<ExpertProfile
|
||||||
isFull={isFull}
|
isFull={isFull}
|
||||||
|
@ -66,5 +66,5 @@ export default function ExpertProfilePage({ params: { locale } }: { params: { lo
|
||||||
updateData={setData}
|
updateData={setData}
|
||||||
/>
|
/>
|
||||||
</Loader>
|
</Loader>
|
||||||
);
|
) : null;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +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 { unstable_setRequestLocale } from 'next-intl/server';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { i18nText } from '../../../../../i18nKeys';
|
import { i18nText } from '../../../../../i18nKeys';
|
||||||
|
|
||||||
|
@ -10,7 +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);
|
// unstable_setRequestLocale(locale);
|
||||||
const t = useTranslations('Account.LegalInformation');
|
const t = useTranslations('Account.LegalInformation');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
|
'use client'
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { unstable_setRequestLocale } from 'next-intl/server';
|
import { Link } from '../../../../../../i18n/routing';
|
||||||
import { Link } from '../../../../../../navigation';
|
|
||||||
import { i18nText } from '../../../../../../i18nKeys';
|
import { i18nText } from '../../../../../../i18nKeys';
|
||||||
|
import {ChatMessages} from "../../../../../../components/Chat/ChatMessages";
|
||||||
|
/*
|
||||||
export function generateStaticParams({
|
export function generateStaticParams({
|
||||||
params: { locale },
|
params: { locale },
|
||||||
}: { params: { locale: string } }) {
|
}: { params: { locale: string } }) {
|
||||||
|
@ -15,56 +16,22 @@ export function generateStaticParams({
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
export default function Message({ params }: { params: { locale: string, textId: string } }) {
|
export default function Message({ params: { locale, textId } }: { params: { locale: string, textId: string } }) {
|
||||||
unstable_setRequestLocale(params.locale);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ol className="breadcrumb">
|
<ol className="breadcrumb">
|
||||||
<li className="breadcrumb-item">
|
<li className="breadcrumb-item">
|
||||||
<Link href={'/account/messages' as any}>
|
<Link href={'/account/messages' as any}>
|
||||||
{i18nText('accountMenu.messages', params.locale)}
|
{i18nText('accountMenu.messages', locale)}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li className="breadcrumb-item active" aria-current="page">{`Person ${params.textId}`}</li>
|
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
<div className="b-message">
|
<ChatMessages locale={locale} groupId={parseInt(textId)} />
|
||||||
<div className="b-message__inner">
|
|
||||||
<div className="b-message__list b-message__list--me">
|
|
||||||
<div className="b-message__item ">
|
|
||||||
<div className="b-message__avatar">
|
|
||||||
<img src="/images/person.png" className="" alt="" />
|
|
||||||
</div>
|
|
||||||
<div className="b-message__text">
|
|
||||||
🤩 It all for you!
|
|
||||||
|
|
||||||
<span className="date">07.09.2022</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="b-message__list">
|
|
||||||
<div className="b-message__item">
|
|
||||||
<div className="b-message__avatar">
|
|
||||||
<img src="/images/person.png" className="" alt="" />
|
|
||||||
</div>
|
|
||||||
<div className="b-message__text">
|
|
||||||
🤩 It all for you!
|
|
||||||
<span className="date">07.09.2022</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<form className="b-message__form" action="">
|
|
||||||
<textarea placeholder="Type your message here" name="" id="" />
|
|
||||||
<label className="b-message__upload-file">
|
|
||||||
<input type="file" required />
|
|
||||||
</label>
|
|
||||||
<div className="b-message__microphone" />
|
|
||||||
<button className="b-message__btn" type="submit" />
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
|
'use client'
|
||||||
import React, { Suspense } from 'react';
|
import React, { Suspense } from 'react';
|
||||||
import { unstable_setRequestLocale } from 'next-intl/server';
|
|
||||||
import { Link } from '../../../../../navigation';
|
|
||||||
import { CustomInput } from '../../../../../components/view/CustomInput';
|
import { CustomInput } from '../../../../../components/view/CustomInput';
|
||||||
import { i18nText } from '../../../../../i18nKeys';
|
import { i18nText } from '../../../../../i18nKeys';
|
||||||
|
import {ChatList} from "../../../../../components/Chat/ChatList";
|
||||||
|
|
||||||
export default function Messages({ params: { locale } }: { params: { locale: string } }) {
|
export default function Messages({ params: { locale } }: { params: { locale: string } }) {
|
||||||
unstable_setRequestLocale(locale);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ol className="breadcrumb">
|
<ol className="breadcrumb">
|
||||||
|
@ -15,74 +13,7 @@ export default function Messages({ params: { locale } }: { params: { locale: str
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<CustomInput placeholder={i18nText('name', locale)} />
|
<CustomInput placeholder={i18nText('name', locale)} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<div className="messages-session">
|
<ChatList locale={locale}/>
|
||||||
<Link
|
|
||||||
className="card-profile"
|
|
||||||
href={'1' as any}
|
|
||||||
>
|
|
||||||
<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 style={{ width: '100%' }}>
|
|
||||||
<div className="card-profile__header__name">
|
|
||||||
David
|
|
||||||
<span className="count">14</span>
|
|
||||||
</div>
|
|
||||||
<div className="card-profile__header__title">
|
|
||||||
Lorem ipsum dolor sit at, consecte...
|
|
||||||
</div>
|
|
||||||
<div className="card-profile__header__date ">
|
|
||||||
25 may
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
className="card-profile"
|
|
||||||
href={'2' as any}
|
|
||||||
>
|
|
||||||
<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 style={{ width: '100%' }}>
|
|
||||||
<div className="card-profile__header__name">David</div>
|
|
||||||
<div className="card-profile__header__title">
|
|
||||||
Lorem ipsum dolor sit at, consecte...
|
|
||||||
</div>
|
|
||||||
<div className="card-profile__header__date ">
|
|
||||||
25 may
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
className="card-profile"
|
|
||||||
href={'3' as any}
|
|
||||||
>
|
|
||||||
<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 style={{ width: '100%' }}>
|
|
||||||
<div className="card-profile__header__name">David</div>
|
|
||||||
<div className="card-profile__header__title">
|
|
||||||
Lorem ipsum dolor sit at, consecte...
|
|
||||||
</div>
|
|
||||||
<div className="card-profile__header__date ">
|
|
||||||
25 may
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +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 { 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';
|
||||||
|
@ -16,7 +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);
|
// unstable_setRequestLocale(locale);
|
||||||
const date = dayjs('2022-05-22').locale(locale);
|
const date = dayjs('2022-05-22').locale(locale);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { unstable_setRequestLocale } from 'next-intl/server';
|
// import { unstable_setRequestLocale } from 'next-intl/server';
|
||||||
import { Link } from '../../../../../../navigation';
|
import { Link } from '../../../../../../i18n/routing';
|
||||||
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);
|
// unstable_setRequestLocale(locale);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import React, { Suspense } from 'react';
|
import React, { Suspense } from 'react';
|
||||||
import { unstable_setRequestLocale } from 'next-intl/server';
|
// import { unstable_setRequestLocale } from 'next-intl/server';
|
||||||
import { ProfileSettings } from '../../../../../components/Account';
|
import { ProfileSettings } from '../../../../../components/Account';
|
||||||
import { i18nText } from '../../../../../i18nKeys';
|
import { i18nText } from '../../../../../i18nKeys';
|
||||||
|
|
||||||
export default function Settings({ params: { locale } }: { params: { locale: string } }) {
|
export default function Settings({ params: { locale } }: { params: { locale: string } }) {
|
||||||
unstable_setRequestLocale(locale);
|
// unstable_setRequestLocale(locale);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { unstable_setRequestLocale } from 'next-intl/server';
|
// 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';
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ 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);
|
// unstable_setRequestLocale(locale);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -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;
|
||||||
|
};
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { Suspense } from 'react';
|
import React, { Suspense } from 'react';
|
||||||
import { unstable_setRequestLocale } from 'next-intl/server';
|
// import { unstable_setRequestLocale } from 'next-intl/server';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
import { AccountMenu, SessionDetails, SessionsTabs } from '../../../../../../components/Account';
|
import { AccountMenu, SessionDetails, SessionsTabs } from '../../../../../../components/Account';
|
||||||
import { SessionType } from '../../../../../../types/sessions';
|
import { SessionType } from '../../../../../../types/sessions';
|
||||||
|
@ -13,7 +13,7 @@ export async function generateStaticParams({
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SessionDetailItem({ params: { locale, slug } }: { params: { locale: string, slug?: string[] } }) {
|
export default function SessionDetailItem({ params: { locale, slug } }: { params: { locale: string, slug?: string[] } }) {
|
||||||
unstable_setRequestLocale(locale);
|
// unstable_setRequestLocale(locale);
|
||||||
const sessionType: string = slug?.length > 0 && slug[0] || '';
|
const sessionType: string = slug?.length > 0 && slug[0] || '';
|
||||||
const sessionId: number | null = slug?.length > 1 && Number(slug[1]) || null;
|
const sessionId: number | null = slug?.length > 1 && Number(slug[1]) || null;
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ export default function SessionDetailItem({ params: { locale, slug } }: { params
|
||||||
<Suspense fallback={<p>Loading...</p>}>
|
<Suspense fallback={<p>Loading...</p>}>
|
||||||
<SessionDetails
|
<SessionDetails
|
||||||
locale={locale}
|
locale={locale}
|
||||||
sessionId={sessionId}
|
sessionId={sessionId as number}
|
||||||
activeType={sessionType as SessionType}
|
activeType={sessionType as SessionType}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
|
@ -8,5 +8,5 @@ import { SessionType } from '../../../../../types/sessions';
|
||||||
export default function SessionsMainPage() {
|
export default function SessionsMainPage() {
|
||||||
const [token] = useLocalStorage(AUTH_TOKEN_KEY, '');
|
const [token] = useLocalStorage(AUTH_TOKEN_KEY, '');
|
||||||
|
|
||||||
return token ? redirect(SessionType.UPCOMING) : null;
|
return token ? redirect(`sessions/${SessionType.UPCOMING}`) : null;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +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 { 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';
|
||||||
|
|
||||||
|
@ -9,8 +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({ params: { locale } }: { params: { locale: string } }) {
|
export default function BbClientPage() {
|
||||||
unstable_setRequestLocale(locale);
|
// unstable_setRequestLocale(locale);
|
||||||
const t = useTranslations('BbClient');
|
const t = useTranslations('BbClient');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,17 +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 { 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({ params: { locale } }: { params: { locale: string } }) {
|
export default function BbExpertPage() {
|
||||||
unstable_setRequestLocale(locale);
|
// unstable_setRequestLocale(locale);
|
||||||
const t = useTranslations('BbExpert');
|
const t = useTranslations('BbExpert');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -50,7 +50,6 @@ function renderWidget (widget: Widget, index: number) {
|
||||||
|
|
||||||
export default async function BlogItem({params}: { params: BlogPostPageParams }) {
|
export default async function BlogItem({params}: { params: BlogPostPageParams }) {
|
||||||
const item = await fetchBlogPost({slug: params.slug, preview: draftMode().isEnabled })
|
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}))
|
console.log(Util.inspect(item, {showHidden: false, depth: null, colors: true}))
|
||||||
if (!item) notFound();
|
if (!item) notFound();
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import { draftMode } from 'next/headers'
|
import { draftMode } from 'next/headers'
|
||||||
import {unstable_setRequestLocale} from "next-intl/server";
|
// import {unstable_setRequestLocale} from "next-intl/server";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {fetchBlogPosts} from "../../../../../lib/contentful/blogPosts";
|
import {fetchBlogPosts} from "../../../../../lib/contentful/blogPosts";
|
||||||
import {fetchBlogPostCategories} from "../../../../../lib/contentful/blogPostsCategories";
|
import {fetchBlogPostCategories} from "../../../../../lib/contentful/blogPostsCategories";
|
||||||
|
@ -20,9 +20,9 @@ interface BlogPostPageProps {
|
||||||
params: BlogPostPageParams
|
params: BlogPostPageParams
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function Blog({params, searchParams}: { params: BlogPostPageParams, searhParams?: {page: number} }) {
|
export default async function Blog({params, searchParams}: { params: BlogPostPageParams, searchParams?: {page: number} }) {
|
||||||
unstable_setRequestLocale(params.locale);
|
// unstable_setRequestLocale(params.locale);
|
||||||
const page = searchParams.page || undefined
|
const page = searchParams?.page || undefined
|
||||||
return (
|
return (
|
||||||
<BlogPosts basePath={'/'+params.locale+'/blog/'} locale={params.locale} currentCat={params.slug} page={page}/>
|
<BlogPosts basePath={'/'+params.locale+'/blog/'} locale={params.locale} currentCat={params.slug} page={page}/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,14 +2,13 @@ import React from 'react';
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import * as Util from "node:util";
|
import * as Util from "node:util";
|
||||||
import {fetchBlogPosts} from "../../../lib/contentful/blogPosts";
|
import {fetchBlogPosts} from "../../../lib/contentful/blogPosts";
|
||||||
import {unstable_setRequestLocale} from "next-intl/server";
|
// import {unstable_setRequestLocale} from "next-intl/server";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {fetchBlogPostCategories} from "../../../lib/contentful/blogPostsCategories";
|
import {fetchBlogPostCategories} from "../../../lib/contentful/blogPostsCategories";
|
||||||
import {CustomPagination} from "../../../components/view/CustomPagination";
|
import {CustomPagination} from "../../../components/view/CustomPagination";
|
||||||
import {DEFAULT_PAGE_SIZE} from "../../../constants/common";
|
import {DEFAULT_PAGE_SIZE} from "../../../constants/common";
|
||||||
import {BlogPosts} from "../../../components/BlogPosts/BlogPosts";
|
import {BlogPosts} from "../../../components/BlogPosts/BlogPosts";
|
||||||
|
|
||||||
|
|
||||||
interface BlogPostPageParams {
|
interface BlogPostPageParams {
|
||||||
slug: string
|
slug: string
|
||||||
}
|
}
|
||||||
|
@ -25,10 +24,10 @@ export async function generateStaticParams(): Promise<BlogPostPageParams[]> {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default async function Blog({ params: { locale }, searchParams }: { params: { locale: string }, searhParams?: {page: number} }) {
|
export default async function Blog({ params: { locale }, searchParams }: { params: { locale: string }, searchParams?: {page: number} }) {
|
||||||
unstable_setRequestLocale(locale);
|
// unstable_setRequestLocale(locale);
|
||||||
const pageSize = DEFAULT_PAGE_SIZE
|
const pageSize = DEFAULT_PAGE_SIZE
|
||||||
const page = searchParams.page || undefined
|
const page = searchParams?.page || undefined
|
||||||
// BlogPosts('/'+locale+'/blog/', locale, pageSize)
|
// BlogPosts('/'+locale+'/blog/', locale, pageSize)
|
||||||
return (
|
return (
|
||||||
|
|
||||||
|
@ -37,7 +36,6 @@ export default async function Blog({ params: { locale }, searchParams }: { param
|
||||||
locale={locale}
|
locale={locale}
|
||||||
pageSize={pageSize}
|
pageSize={pageSize}
|
||||||
page={page}
|
page={page}
|
||||||
>
|
/>
|
||||||
</BlogPosts>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
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 { 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/education';
|
import { Details } from '../../../../types/education';
|
||||||
|
@ -21,7 +20,7 @@ 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);
|
// unstable_setRequestLocale(locale);
|
||||||
const result: { locale: string, expertId: string }[] = [];
|
const result: { locale: string, expertId: string }[] = [];
|
||||||
const experts = await getExpertsList(locale, { themesTagIds: [] });
|
const experts = await getExpertsList(locale, { themesTagIds: [] });
|
||||||
|
|
||||||
|
@ -82,8 +81,7 @@ export default async function ExpertItem({ params: { expertId = '', locale } }:
|
||||||
</BackButton>
|
</BackButton>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
<ExpertCard expert={expert} locale={locale} />
|
<ExpertCard expert={expert} locale={locale} expertId={expertId}/>
|
||||||
<ExpertInformation expert={expert} locale={locale} />
|
|
||||||
|
|
||||||
<h2 className="title-h2">{i18nText('expertBackground', locale)}</h2>
|
<h2 className="title-h2">{i18nText('expertBackground', locale)}</h2>
|
||||||
<p className="base-text">
|
<p className="base-text">
|
||||||
|
|
|
@ -1,6 +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 { 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';
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ export const metadata: Metadata = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ExpertsPage({ params: { locale } }: { params: { locale: string } }) {
|
export default function ExpertsPage({ params: { locale } }: { params: { locale: string } }) {
|
||||||
unstable_setRequestLocale(locale);
|
// unstable_setRequestLocale(locale);
|
||||||
const t = useTranslations('Experts');
|
const t = useTranslations('Experts');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
import React, { ReactNode, Suspense } from 'react';
|
import React, { ReactNode, Suspense } from 'react';
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
import { unstable_setRequestLocale } from 'next-intl/server';
|
import { NextIntlClientProvider } from 'next-intl';
|
||||||
|
import { getMessages } from 'next-intl/server';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
import { ConfigProvider } from 'antd';
|
import { ConfigProvider } from 'antd';
|
||||||
import { AntdRegistry } from '@ant-design/nextjs-registry';
|
import { AntdRegistry } from '@ant-design/nextjs-registry';
|
||||||
|
import { GoogleOAuthProvider } from '@react-oauth/google';
|
||||||
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, AppConfig } from '../../components/Page';
|
import { Header, Footer, AppConfig } from '../../components/Page';
|
||||||
|
import { routing } from '../../i18n/routing';
|
||||||
|
|
||||||
type LayoutProps = {
|
type LayoutProps = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
@ -21,13 +24,17 @@ export const metadata: Metadata = {
|
||||||
title: 'Bbuddy'
|
title: 'Bbuddy'
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function LocaleLayout({ children, params: { locale } }: LayoutProps) {
|
export default async function LocaleLayout({ children, params: { locale } }: LayoutProps) {
|
||||||
if (!ALLOWED_LOCALES.includes(locale as any)) notFound();
|
if (!routing.locales.includes(locale as any)) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
unstable_setRequestLocale(locale);
|
const messages = await getMessages();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<NextIntlClientProvider messages={messages}>
|
||||||
<AntdRegistry>
|
<AntdRegistry>
|
||||||
|
<GoogleOAuthProvider clientId={process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || ''}>
|
||||||
<ConfigProvider theme={theme}>
|
<ConfigProvider theme={theme}>
|
||||||
<div className="b-wrapper">
|
<div className="b-wrapper">
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
|
@ -40,6 +47,8 @@ export default function LocaleLayout({ children, params: { locale } }: LayoutPro
|
||||||
<Footer locale={locale} />
|
<Footer locale={locale} />
|
||||||
</div>
|
</div>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
|
</GoogleOAuthProvider>
|
||||||
</AntdRegistry>
|
</AntdRegistry>
|
||||||
|
</NextIntlClientProvider>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { notification } from 'antd';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
import { CustomSpin } from '../../../../components/view/CustomSpin';
|
||||||
|
import { getLoginByApple } from '../../../../actions/auth';
|
||||||
|
import { getUserData } from '../../../../actions/profile';
|
||||||
|
import { AUTH_TOKEN_KEY, AUTH_USER } from '../../../../constants/common';
|
||||||
|
import { useLocalStorage } from '../../../../hooks/useLocalStorage';
|
||||||
|
import { useRouter } from '../../../../i18n/routing';
|
||||||
|
|
||||||
|
export default function AppleLoginPage({ params: { locale } }: { params: { locale: string } }) {
|
||||||
|
const params = useSearchParams();
|
||||||
|
const router = useRouter();
|
||||||
|
const [, setToken] = useLocalStorage(AUTH_TOKEN_KEY, '');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const code = params.get('code');
|
||||||
|
if (code) {
|
||||||
|
getLoginByApple(locale, code)
|
||||||
|
.then((data) => {
|
||||||
|
if (data.jwtToken) {
|
||||||
|
getUserData(locale, data.jwtToken)
|
||||||
|
.then((profile) => {
|
||||||
|
localStorage.setItem(AUTH_USER, JSON.stringify(profile));
|
||||||
|
setToken(data.jwtToken);
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
notification.error({
|
||||||
|
message: 'Error',
|
||||||
|
description: 'Access denied'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const err = error?.message ? JSON.parse(error.message) : {};
|
||||||
|
notification.error({
|
||||||
|
message: 'Error',
|
||||||
|
description: err?.details?.errMessage || undefined
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
router.push('/');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [params]);
|
||||||
|
|
||||||
|
return <CustomSpin />;
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { notification } from 'antd';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
import { CustomSpin } from '../../../../components/view/CustomSpin';
|
||||||
|
import { getRegisterByApple } from '../../../../actions/auth';
|
||||||
|
import { getUserData } from '../../../../actions/profile';
|
||||||
|
import { AUTH_TOKEN_KEY, AUTH_USER } from '../../../../constants/common';
|
||||||
|
import { useLocalStorage } from "../../../../hooks/useLocalStorage";
|
||||||
|
import { useRouter } from '../../../../i18n/routing';
|
||||||
|
|
||||||
|
export default function AppleRegisterPage({ params: { locale } }: { params: { locale: string } }) {
|
||||||
|
const params = useSearchParams();
|
||||||
|
const router = useRouter();
|
||||||
|
const [, setToken] = useLocalStorage(AUTH_TOKEN_KEY, '');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const code = params.get('code');
|
||||||
|
if (code) {
|
||||||
|
getRegisterByApple(locale, code)
|
||||||
|
.then((data) => {
|
||||||
|
if (data.jwtToken) {
|
||||||
|
getUserData(locale, data.jwtToken)
|
||||||
|
.then((profile) => {
|
||||||
|
localStorage.setItem(AUTH_USER, JSON.stringify(profile));
|
||||||
|
setToken(data.jwtToken);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const err = error?.message ? JSON.parse(error.message) : {};
|
||||||
|
notification.error({
|
||||||
|
message: 'Error',
|
||||||
|
description: err?.details?.errMessage || undefined
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
router.push('/');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [params]);
|
||||||
|
|
||||||
|
return <CustomSpin />;
|
||||||
|
}
|
|
@ -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 });
|
||||||
|
}
|
|
@ -10,7 +10,10 @@ type RootLayoutProps = {
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Bbuddy',
|
title: 'Bbuddy',
|
||||||
description: 'Bbuddy'
|
description: 'Bbuddy',
|
||||||
|
verification: {
|
||||||
|
google: 'UqmM7WbpuMetvvkMeyVRGKiSvXHEGyaaRuYbWxM-njs'
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({ children, params: { locale } }: RootLayoutProps) {
|
export default function RootLayout({ children, params: { locale } }: RootLayoutProps) {
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
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,24 +1,47 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
import { Button } from 'antd';
|
import { Button } from 'antd';
|
||||||
import { useSelectedLayoutSegment, usePathname } from 'next/navigation';
|
import { useSelectedLayoutSegment, usePathname } from 'next/navigation';
|
||||||
import { Link } from '../../navigation';
|
import { Link } from '../../i18n/routing';
|
||||||
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, useLocalStorage} from '../../hooks/useLocalStorage';
|
||||||
import { i18nText } from '../../i18nKeys';
|
import { i18nText } from '../../i18nKeys';
|
||||||
import { getMenuConfig } from '../../utils/account';
|
import { getMenuConfig } from '../../utils/account';
|
||||||
|
import { getChatList } from '../../actions/chat/groups';
|
||||||
|
|
||||||
export const AccountMenu = ({ locale }: { locale: string }) => {
|
export const AccountMenu = ({ locale }: { locale: string }) => {
|
||||||
const selectedLayoutSegment = useSelectedLayoutSegment();
|
const selectedLayoutSegment = useSelectedLayoutSegment();
|
||||||
const pathname = selectedLayoutSegment || '';
|
const pathname = selectedLayoutSegment || '';
|
||||||
const paths = usePathname();
|
const paths = usePathname();
|
||||||
const menu: { path: string, title: string, count?: number }[] = getMenuConfig(locale);
|
const menu: { path: string, title: string, count?: number }[] = getMenuConfig(locale);
|
||||||
|
const [counts, setCounts] = useState<any>({});
|
||||||
|
|
||||||
const onLogout = () => {
|
const onLogout = () => {
|
||||||
deleteStorageKey(AUTH_TOKEN_KEY);
|
deleteStorageKey(AUTH_TOKEN_KEY);
|
||||||
deleteStorageKey(AUTH_USER);
|
deleteStorageKey(AUTH_USER);
|
||||||
window?.location?.replace(`/${paths.split('/')[1]}/`);
|
window?.location?.replace(`/${paths.split('/')[1]}/`);
|
||||||
};
|
};
|
||||||
|
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
|
||||||
|
let init = false
|
||||||
|
useEffect(() => {
|
||||||
|
if (jwt && locale && !init) {
|
||||||
|
init = true;
|
||||||
|
getChatList(locale, jwt).then((payload: any)=> {
|
||||||
|
if (payload?.directs) {
|
||||||
|
let summ = 0;
|
||||||
|
payload?.directs.forEach((el: any) => {
|
||||||
|
summ = summ + el.newMessagesCount
|
||||||
|
})
|
||||||
|
setCounts({'messages': summ})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [jwt, locale])
|
||||||
|
|
||||||
|
const getterCount = (path: string, count: number)=> {
|
||||||
|
return counts[path]? counts[path] : count
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className="list-sidebar">
|
<ul className="list-sidebar">
|
||||||
|
@ -26,8 +49,8 @@ export const AccountMenu = ({ locale }: { locale: string }) => {
|
||||||
<li key={path} className="list-sidebar__item">
|
<li key={path} className="list-sidebar__item">
|
||||||
<Link href={`/account/${path}` as any} className={path === pathname ? 'active' : ''}>
|
<Link href={`/account/${path}` as any} className={path === pathname ? 'active' : ''}>
|
||||||
{title}
|
{title}
|
||||||
{count ? (
|
{getterCount(path, count) ? (
|
||||||
<span className="count">{count}</span>
|
<span className="count">{getterCount(path, count)}</span>
|
||||||
) : null}
|
) : null}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -1,28 +1,25 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { FC, useEffect, useState } from 'react';
|
import React, { FC, useEffect, useState } from 'react';
|
||||||
import { Button, Form, message, Upload } from 'antd';
|
import { Form, message, Upload } from 'antd';
|
||||||
import type { GetProp, UploadFile, UploadProps } from 'antd';
|
import type { UploadFile } from 'antd';
|
||||||
import ImgCrop from 'antd-img-crop';
|
import ImgCrop from 'antd-img-crop';
|
||||||
import { CameraOutlined, DeleteOutlined } from '@ant-design/icons';
|
import { CameraOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||||
import { useRouter } from '../../navigation';
|
import { useRouter } from '../../i18n/routing';
|
||||||
import { i18nText } from '../../i18nKeys';
|
import { i18nText } from '../../i18nKeys';
|
||||||
import { ProfileRequest } from '../../types/profile';
|
import { ProfileRequest } from '../../types/profile';
|
||||||
import { validateImage } from '../../utils/account';
|
import { validateImage } from '../../utils/account';
|
||||||
import { useProfileSettings } from '../../actions/hooks/useProfileSettings';
|
import { useProfileSettings } from '../../actions/hooks/useProfileSettings';
|
||||||
import { CustomInput } from '../view/CustomInput';
|
import { CustomInput } from '../view/CustomInput';
|
||||||
import { OutlinedButton } from '../view/OutlinedButton';
|
import { OutlinedButton } from '../view/OutlinedButton';
|
||||||
import {FilledButton, FilledSquareButton, FilledYellowButton} from '../view/FilledButton';
|
import { FilledSquareButton, FilledYellowButton } from '../view/FilledButton';
|
||||||
import { DeleteAccountModal } from '../Modals/DeleteAccountModal';
|
import { DeleteAccountModal } from '../Modals/DeleteAccountModal';
|
||||||
import { Loader } from '../view/Loader';
|
import { Loader } from '../view/Loader';
|
||||||
import {ButtonProps} from "antd/es/button/button";
|
|
||||||
|
|
||||||
type ProfileSettingsProps = {
|
type ProfileSettingsProps = {
|
||||||
locale: string;
|
locale: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type FileType = Parameters<GetProp<UploadProps, 'beforeUpload'>>[0];
|
|
||||||
|
|
||||||
export const ProfileSettings: FC<ProfileSettingsProps> = ({ locale }) => {
|
export const ProfileSettings: FC<ProfileSettingsProps> = ({ locale }) => {
|
||||||
const [form] = Form.useForm<ProfileRequest>();
|
const [form] = Form.useForm<ProfileRequest>();
|
||||||
const { profileSettings, fetchProfileSettings, save, fetchLoading } = useProfileSettings(locale);
|
const { profileSettings, fetchProfileSettings, save, fetchLoading } = useProfileSettings(locale);
|
||||||
|
@ -58,7 +55,7 @@ export const ProfileSettings: FC<ProfileSettingsProps> = ({ locale }) => {
|
||||||
const onSaveProfile = () => {
|
const onSaveProfile = () => {
|
||||||
form.validateFields()
|
form.validateFields()
|
||||||
.then(({ login, surname, username }) => {
|
.then(({ login, surname, username }) => {
|
||||||
const { phone, role, languagesLinks } = profileSettings;
|
const { phone, role, languagesLinks } = profileSettings || {};
|
||||||
const newProfile: ProfileRequest = {
|
const newProfile: ProfileRequest = {
|
||||||
phone,
|
phone,
|
||||||
role,
|
role,
|
||||||
|
@ -75,7 +72,7 @@ export const ProfileSettings: FC<ProfileSettingsProps> = ({ locale }) => {
|
||||||
reader.readAsDataURL(photo as File);
|
reader.readAsDataURL(photo as File);
|
||||||
reader.onloadend = () => {
|
reader.onloadend = () => {
|
||||||
const newReg = new RegExp('data:image/(png|jpg|jpeg);base64,')
|
const newReg = new RegExp('data:image/(png|jpg|jpeg);base64,')
|
||||||
newProfile.faceImage = reader.result.replace(newReg, '');
|
newProfile.faceImage = reader?.result?.replace(newReg, '');
|
||||||
newProfile.isFaceImageKeepExisting = false;
|
newProfile.isFaceImageKeepExisting = false;
|
||||||
|
|
||||||
onSave(newProfile);
|
onSave(newProfile);
|
||||||
|
@ -181,7 +178,7 @@ export const ProfileSettings: FC<ProfileSettingsProps> = ({ locale }) => {
|
||||||
>
|
>
|
||||||
{i18nText('save', locale)}
|
{i18nText('save', locale)}
|
||||||
</FilledYellowButton>
|
</FilledYellowButton>
|
||||||
<OutlinedButton onClick={() => router.push('change-password')}>
|
<OutlinedButton onClick={() => router.push('settings/change-password')}>
|
||||||
{i18nText('changePass', locale)}
|
{i18nText('changePass', locale)}
|
||||||
</OutlinedButton>
|
</OutlinedButton>
|
||||||
<OutlinedButton
|
<OutlinedButton
|
||||||
|
|
|
@ -37,7 +37,7 @@ export const Agora = ({ sessionId, secret, stopCalling, remoteUser }: AgoraProps
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="b-agora__wrap">
|
<div className="b-agora__wrap b-agora__wrap__single">
|
||||||
<RemoteUserPanel calling={calling} user={remoteUser} />
|
<RemoteUserPanel calling={calling} user={remoteUser} />
|
||||||
<div className="b-agora__panel">
|
<div className="b-agora__panel">
|
||||||
<MediaControl
|
<MediaControl
|
||||||
|
|
|
@ -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,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;
|
||||||
|
}
|
|
@ -3,3 +3,4 @@ export * from './UserCover';
|
||||||
export * from './RemoteUsers';
|
export * from './RemoteUsers';
|
||||||
export * from './LocalUserPanel';
|
export * from './LocalUserPanel';
|
||||||
export * from './RemoteUserPanel';
|
export * from './RemoteUserPanel';
|
||||||
|
export * from './UsersGroupPanel';
|
||||||
|
|
|
@ -2,7 +2,9 @@
|
||||||
|
|
||||||
import AgoraRTC, { AgoraRTCProvider } from 'agora-rtc-react';
|
import AgoraRTC, { AgoraRTCProvider } from 'agora-rtc-react';
|
||||||
import { Session } from '../../../types/sessions';
|
import { Session } from '../../../types/sessions';
|
||||||
|
import { Room } from '../../../types/rooms';
|
||||||
import { Agora } from './Agora';
|
import { Agora } from './Agora';
|
||||||
|
import { AgoraGroup } from './AgoraGroup';
|
||||||
|
|
||||||
export const AgoraClient = ({ session, stopCalling, isCoach }: { session?: Session, stopCalling: () => void, isCoach: boolean }) => {
|
export const AgoraClient = ({ session, stopCalling, isCoach }: { session?: Session, stopCalling: () => void, isCoach: boolean }) => {
|
||||||
const remoteUser = isCoach ? (session?.clients?.length ? session?.clients[0] : undefined) : session?.coach;
|
const remoteUser = isCoach ? (session?.clients?.length ? session?.clients[0] : undefined) : session?.coach;
|
||||||
|
@ -20,3 +22,17 @@ export const AgoraClient = ({ session, stopCalling, isCoach }: { session?: Sessi
|
||||||
</AgoraRTCProvider>
|
</AgoraRTCProvider>
|
||||||
) : null;
|
) : 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;
|
||||||
|
};
|
||||||
|
|
|
@ -3,3 +3,4 @@
|
||||||
export { AccountMenu } from './AccountMenu';
|
export { AccountMenu } from './AccountMenu';
|
||||||
export { ProfileSettings } from './ProfileSettings';
|
export { ProfileSettings } from './ProfileSettings';
|
||||||
export * from './sessions';
|
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 '../../../i18n/routing';
|
||||||
|
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,88 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import React, { useEffect, useState } 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';
|
||||||
|
import { SupervisorReportModal } from '../../Modals/SupervisorReportModal';
|
||||||
|
import { useLocalStorage } from '../../../hooks/useLocalStorage';
|
||||||
|
import { AUTH_USER } from '../../../constants/common';
|
||||||
|
|
||||||
|
type RoomDetailsProps = {
|
||||||
|
locale: string;
|
||||||
|
roomId: number;
|
||||||
|
activeType: RoomsType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RoomDetails = ({ roomId, locale, activeType }: RoomDetailsProps) => {
|
||||||
|
const { room, errorData, loading, fetchData, isStarted } = useRoomDetails(locale, roomId);
|
||||||
|
const tracking = useSessionTracking(locale, roomId);
|
||||||
|
const [isCalling, setIsCalling] = useState<boolean>(false);
|
||||||
|
const [isOpenReport, setIsOpenReport] = useState<boolean>(false);
|
||||||
|
const [userData] = useLocalStorage(AUTH_USER, '');
|
||||||
|
const { id: userId = 0 } = userData ? JSON.parse(userData) : {};
|
||||||
|
const isSupervisor = room?.supervisor && room.supervisor.id === +userId || false;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isCalling) {
|
||||||
|
tracking.start();
|
||||||
|
} else {
|
||||||
|
tracking.stop();
|
||||||
|
}
|
||||||
|
}, [isCalling]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSupervisor && isStarted) {
|
||||||
|
setIsOpenReport(true);
|
||||||
|
}
|
||||||
|
}, [isStarted]);
|
||||||
|
|
||||||
|
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>
|
||||||
|
{isSupervisor && room?.id && (
|
||||||
|
<SupervisorReportModal
|
||||||
|
open={isOpenReport}
|
||||||
|
handleCancel={() => setIsOpenReport(false)}
|
||||||
|
locale={locale}
|
||||||
|
refresh={fetchData}
|
||||||
|
roomId={room.id}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,377 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import React, { useState, useEffect } 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 '../../../i18n/routing';
|
||||||
|
import { Report, Room, RoomsType } from '../../../types/rooms';
|
||||||
|
import { i18nText } from '../../../i18nKeys';
|
||||||
|
import { LinkButton } from '../../view/LinkButton';
|
||||||
|
import {
|
||||||
|
addClient,
|
||||||
|
addSupervisor,
|
||||||
|
becomeRoomClient,
|
||||||
|
becomeRoomSupervisor,
|
||||||
|
deleteRoomClient,
|
||||||
|
deleteRoomSupervisor,
|
||||||
|
getReport
|
||||||
|
} 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 [report, setReport] = useState<Report[] | undefined>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (room?.id && room?.supervisor && activeType === RoomsType.RECENT) {
|
||||||
|
getReport(locale, jwt, room.id)
|
||||||
|
.then((data) => {
|
||||||
|
setReport(data);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [room])
|
||||||
|
|
||||||
|
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>
|
||||||
|
)}
|
||||||
|
{report && report.length > 0 && (
|
||||||
|
<div className="card-detail__report-list">
|
||||||
|
{report.map(({ key, score }) => (
|
||||||
|
<div key={key}>
|
||||||
|
<div>{i18nText(`room.rating_${key?.toLowerCase()}`, locale)}</div>
|
||||||
|
<div className="card-detail__report-list_divider" />
|
||||||
|
<div>{score || 0}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</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 '../../../i18n/routing';
|
||||||
|
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';
|
|
@ -5,7 +5,7 @@ import { Button, Empty, notification, Tag } from 'antd';
|
||||||
import { LeftOutlined, PlusOutlined, RightOutlined } from '@ant-design/icons';
|
import { LeftOutlined, PlusOutlined, RightOutlined } from '@ant-design/icons';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { Link, useRouter } from '../../../navigation';
|
import { Link, useRouter } from '../../../i18n/routing';
|
||||||
import { i18nText } from '../../../i18nKeys';
|
import { i18nText } from '../../../i18nKeys';
|
||||||
import { getDuration, getPrice } from '../../../utils/expert';
|
import { getDuration, getPrice } from '../../../utils/expert';
|
||||||
import { PublicUser, Session, SessionState, SessionType } from '../../../types/sessions';
|
import { PublicUser, Session, SessionState, SessionType } from '../../../types/sessions';
|
||||||
|
@ -81,7 +81,7 @@ export const SessionDetailsContent = ({ session, locale, activeType, startSessio
|
||||||
const CoachCard = (coach?: PublicUser) => coach ? (
|
const CoachCard = (coach?: PublicUser) => coach ? (
|
||||||
<div className="card-detail__expert">
|
<div className="card-detail__expert">
|
||||||
<div className="card-detail__portrait">
|
<div className="card-detail__portrait">
|
||||||
<Image src={coach?.faceImageUrl || '/images/person.png'} width={140} height={140} alt="" />
|
<Image src={coach?.faceImageUrl || '/images/user-avatar.png'} width={140} height={140} alt="" />
|
||||||
</div>
|
</div>
|
||||||
<div className="card-detail__inner">
|
<div className="card-detail__inner">
|
||||||
<Link href={`/experts/${coach?.id}` as any} target="_blank">
|
<Link href={`/experts/${coach?.id}` as any} target="_blank">
|
||||||
|
@ -106,7 +106,7 @@ export const SessionDetailsContent = ({ session, locale, activeType, startSessio
|
||||||
<div className="card-detail__skills">
|
<div className="card-detail__skills">
|
||||||
<div className="skills__list">
|
<div className="skills__list">
|
||||||
{session?.themesTags?.slice(0, 2).map((skill) => <Tag key={skill?.id} className="skills__list__item">{skill?.name}</Tag>)}
|
{session?.themesTags?.slice(0, 2).map((skill) => <Tag key={skill?.id} className="skills__list__item">{skill?.name}</Tag>)}
|
||||||
{session?.themesTags?.length > 2
|
{session?.themesTags && session?.themesTags?.length > 2
|
||||||
? (
|
? (
|
||||||
<Tag className="skills__list__more">
|
<Tag className="skills__list__more">
|
||||||
<Link href={`/experts/${coach?.id}` as any} target="_blank">
|
<Link href={`/experts/${coach?.id}` as any} target="_blank">
|
||||||
|
@ -128,7 +128,7 @@ export const SessionDetailsContent = ({ session, locale, activeType, startSessio
|
||||||
const StudentCard = (student?: PublicUser | null) => student ? (
|
const StudentCard = (student?: PublicUser | null) => student ? (
|
||||||
<div className="card-detail__expert">
|
<div className="card-detail__expert">
|
||||||
<div className="card-detail__portrait">
|
<div className="card-detail__portrait">
|
||||||
<Image src={student?.faceImageUrl || '/images/person.png'} width={140} height={140} alt="" />
|
<Image src={student?.faceImageUrl || '/images/user-avatar.png'} width={140} height={140} alt="" />
|
||||||
</div>
|
</div>
|
||||||
<div className="card-detail__inner">
|
<div className="card-detail__inner">
|
||||||
<div className="card-detail__name">{`${student?.name} ${student?.surname || ''}`}</div>
|
<div className="card-detail__name">{`${student?.name} ${student?.surname || ''}`}</div>
|
||||||
|
|
|
@ -14,7 +14,7 @@ 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/sessions';
|
import { getRecentSessions, getRequestedSessions, getUpcomingSessions } from '../../../actions/sessions';
|
||||||
import { Session, Sessions, SessionType } from '../../../types/sessions';
|
import { Session, Sessions, SessionType } from '../../../types/sessions';
|
||||||
import { useRouter } from '../../../navigation';
|
import { useRouter, usePathname } from '../../../i18n/routing';
|
||||||
import { i18nText } from '../../../i18nKeys';
|
import { i18nText } from '../../../i18nKeys';
|
||||||
|
|
||||||
type SessionsTabsProps = {
|
type SessionsTabsProps = {
|
||||||
|
@ -31,6 +31,7 @@ export const SessionsTabs = ({ locale, activeTab }: SessionsTabsProps) => {
|
||||||
const [userData] = useLocalStorage(AUTH_USER, '');
|
const [userData] = useLocalStorage(AUTH_USER, '');
|
||||||
const { id: userId = 0 } = userData ? JSON.parse(userData) : {};
|
const { id: userId = 0 } = userData ? JSON.parse(userData) : {};
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
const fetchData = () => {
|
const fetchData = () => {
|
||||||
setErrorData(undefined);
|
setErrorData(undefined);
|
||||||
|
@ -66,7 +67,7 @@ export const SessionsTabs = ({ locale, activeTab }: SessionsTabsProps) => {
|
||||||
const onClickSession = (event: MouseEvent<HTMLDivElement>, id: number) => {
|
const onClickSession = (event: MouseEvent<HTMLDivElement>, id: number) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
router.push(`${id}`);
|
router.push(`${pathname}/${id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getChildren = (list?: Session[]) => (
|
const getChildren = (list?: Session[]) => (
|
||||||
|
|
|
@ -10,7 +10,7 @@ type PostsProps = {
|
||||||
basePath: string;
|
basePath: string;
|
||||||
locale: string;
|
locale: string;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
currentCat: string;
|
currentCat?: string;
|
||||||
page?: number
|
page?: number
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
'use client'
|
||||||
|
import React, {useEffect, useState} from 'react';
|
||||||
|
import {AUTH_TOKEN_KEY} from '../../constants/common';
|
||||||
|
import {getChatList, getChatMessages} from "../../actions/chat/groups";
|
||||||
|
import {useLocalStorage} from "../../hooks/useLocalStorage";
|
||||||
|
import { Link } from "../../i18n/routing";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
|
import {message} from "antd";
|
||||||
|
import {Loader} from "../view/Loader";
|
||||||
|
|
||||||
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
|
type CompProps = {
|
||||||
|
locale: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ChatList = ({ locale }: CompProps) => {
|
||||||
|
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
|
||||||
|
const [chats, setСhats] = useState<any | undefined>();
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (jwt) {
|
||||||
|
setLoading(true);
|
||||||
|
Promise.all([
|
||||||
|
getChatList(locale, jwt),
|
||||||
|
])
|
||||||
|
.then(([_groups]) => {
|
||||||
|
setСhats(_groups)
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
message.error('Не удалось загрузить данные');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},[jwt])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="messages-session">
|
||||||
|
<Loader isLoading={loading}>
|
||||||
|
{chats?.directs.map((item: any, i: number) => (
|
||||||
|
<Link
|
||||||
|
key={'chat'+i}
|
||||||
|
className="card-profile"
|
||||||
|
href={'messages/'+item.group.id as any}
|
||||||
|
>
|
||||||
|
<div className="card-profile__header">
|
||||||
|
<div className="card-profile__header__portrait">
|
||||||
|
<img src={item.faceImageUrl} className="" alt="" />
|
||||||
|
</div>
|
||||||
|
<div className="card-profile__header__inner">
|
||||||
|
<div style={{ width: '100%' }}>
|
||||||
|
<div className="card-profile__header__name">
|
||||||
|
{item.firstName}
|
||||||
|
{item.newMessagesCount && (
|
||||||
|
<span className="count">{item.newMessagesCount}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="card-profile__header__title">
|
||||||
|
{item?.lastMessage?.text}
|
||||||
|
</div>
|
||||||
|
<div className="card-profile__header__date ">
|
||||||
|
{dayjs(item?.lastMessage?.sentAt).fromNow()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</Loader>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,203 @@
|
||||||
|
'use client'
|
||||||
|
import React, {useEffect, useState} from 'react';
|
||||||
|
import {AUTH_TOKEN_KEY} from '../../constants/common';
|
||||||
|
import {getChatList, getChatMessages} from "../../actions/chat/groups";
|
||||||
|
import {useLocalStorage} from "../../hooks/useLocalStorage";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
|
|
||||||
|
import {message} from "antd";
|
||||||
|
import {Loader} from "../view/Loader";
|
||||||
|
import SignalrConnection from "../../lib/signalr-connection";
|
||||||
|
import {CheckOutlined} from "@ant-design/icons";
|
||||||
|
|
||||||
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
|
type CompProps = {
|
||||||
|
locale: string;
|
||||||
|
groupId: number
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ChatMessages = ({ locale, groupId }: CompProps) => {
|
||||||
|
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
|
||||||
|
const { newMessage, joinChat, readMessages, addListener } = SignalrConnection({ jwt }) || {};
|
||||||
|
//const messages = await getChatMessages(locale, jwt, groupId)
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
const [text, setText] = useState('');
|
||||||
|
const [me, setMe] = useState<any | undefined>();
|
||||||
|
const [notMe, setNotMe] = useState<any | undefined>();
|
||||||
|
const [messages, setMessages] = useState<any[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (jwt) {
|
||||||
|
setLoading(true);
|
||||||
|
Promise.all([
|
||||||
|
getChatList(locale, jwt),
|
||||||
|
getChatMessages(locale, jwt, groupId),
|
||||||
|
])
|
||||||
|
.then(([_groups, _messages]) => {
|
||||||
|
//
|
||||||
|
const _group = _groups.directs.find( (el: any) => el.group.id === groupId)
|
||||||
|
if (_group.group.members[0].userId === parseInt(_group.userId)){
|
||||||
|
setMe(_group.group.members[0].user)
|
||||||
|
setNotMe(_group.group.members[1].user)
|
||||||
|
} else {
|
||||||
|
setMe(_group.group.members[1].user)
|
||||||
|
setNotMe(_group.group.members[0].user)
|
||||||
|
}
|
||||||
|
setMessages(_messages.messages);
|
||||||
|
joinChat(groupId)
|
||||||
|
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
message.error('Не удалось загрузить данные');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},[jwt])
|
||||||
|
|
||||||
|
|
||||||
|
const onConnected = (flag: boolean) =>{
|
||||||
|
if (flag && joinChat) {
|
||||||
|
joinChat(groupId)
|
||||||
|
readUreaded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const readUreaded = () => {
|
||||||
|
const msgs = [] as number[];
|
||||||
|
messages.forEach((message: any) => {
|
||||||
|
if (!message.seen && message.sentByMe === false){
|
||||||
|
msgs.push(message.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
readMessages && readMessages(msgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
const onReceiveMessage = (payload: any) => {
|
||||||
|
const _messages = [... messages];
|
||||||
|
let flag = false;
|
||||||
|
const msg = {
|
||||||
|
data : payload.data,
|
||||||
|
dataType: null,
|
||||||
|
id: payload.id,
|
||||||
|
receiverId:null,
|
||||||
|
seen: false,
|
||||||
|
senderId: payload.creatorId,
|
||||||
|
sentAt: payload.createdUtc+'.000Z',
|
||||||
|
sentByMe: payload.creatorId === me.id,
|
||||||
|
text: payload.content,
|
||||||
|
type:"text"
|
||||||
|
}
|
||||||
|
_messages.forEach((item: any, i) => {
|
||||||
|
if (item.id === msg.id){
|
||||||
|
_messages[i] = msg
|
||||||
|
flag = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
//console.log(payload, flag)
|
||||||
|
if (flag){
|
||||||
|
setMessages([..._messages]);
|
||||||
|
} else {
|
||||||
|
setMessages([msg, ..._messages]);
|
||||||
|
}
|
||||||
|
readMessages && readMessages([msg.id]);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const onOpponentRead = (payload: any) => {
|
||||||
|
console.log('onOpponentRead', payload)
|
||||||
|
const _messages = [... messages] as any[];
|
||||||
|
_messages.forEach((item: any, i) => {
|
||||||
|
if (item.id === payload.messageId){
|
||||||
|
_messages[i].seen = true
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setMessages([..._messages])
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (addListener) {
|
||||||
|
addListener('onConnected', onConnected);
|
||||||
|
addListener('ReceiveMessage', onReceiveMessage);
|
||||||
|
addListener('MessageWasRead', onOpponentRead);
|
||||||
|
}
|
||||||
|
}, [messages, me, setMessages]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const handleSendMessages = () => {
|
||||||
|
newMessage && newMessage({
|
||||||
|
GroupId: groupId.toString(),
|
||||||
|
CreatorId: me.id,
|
||||||
|
Content: text
|
||||||
|
})
|
||||||
|
setText('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const onEnterPress = (e: any) => {
|
||||||
|
if(e.keyCode == 13 && e.shiftKey == false) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSendMessages();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChangeMessage = (e: any) =>{
|
||||||
|
const { value } = e.target;
|
||||||
|
e.preventDefault();
|
||||||
|
setText(value);
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const messageRender = (message: any, i:number) => {
|
||||||
|
const item = message.sentByMe ? me : notMe
|
||||||
|
const date = dayjs(message.sentAt).fromNow()
|
||||||
|
const imgSrc = item?.faceImage?.descriptor ? 'http://static.bbuddy.expert/' + item.faceImage.descriptor.split(':')[1] : ''
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={message.sentByMe ? 'b-message__list b-message__list--me' : 'b-message__list'}
|
||||||
|
key={'message'+i}
|
||||||
|
>
|
||||||
|
<div className="b-message__item ">
|
||||||
|
<div className="b-message__avatar">
|
||||||
|
{imgSrc && (<img src={imgSrc} className="" alt=""/>)}
|
||||||
|
</div>
|
||||||
|
<div className="b-message__text" style={{minWidth: '150px'}}>
|
||||||
|
{message.text}
|
||||||
|
<span className="date">
|
||||||
|
<span className="checks" style={{color: message.seen ? 'green' : 'blue'}}>
|
||||||
|
<CheckOutlined />
|
||||||
|
{ message?.seen && (<CheckOutlined style={{marginLeft: '-10px'}} />)}
|
||||||
|
</span>
|
||||||
|
{date}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
|
||||||
|
<div className="b-message">
|
||||||
|
<Loader isLoading={loading}>
|
||||||
|
<div className="b-message__inner">
|
||||||
|
{messages.map((el, i)=> (messageRender(el, i)))}
|
||||||
|
</div>
|
||||||
|
</Loader>
|
||||||
|
<div className="b-message__form">
|
||||||
|
<textarea placeholder="Type your message here" onKeyDown={onEnterPress}
|
||||||
|
onChange={handleChangeMessage} value={text}/>
|
||||||
|
<button className="b-message__btn" type="submit" onClick={handleSendMessages}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import {Alert, message} from 'antd';
|
import { Alert, message } from 'antd';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { i18nText } from '../../i18nKeys';
|
import { i18nText } from '../../i18nKeys';
|
||||||
import { ExpertData, PayInfo, ProfileData } from '../../types/profile';
|
import { ExpertData, PayInfo, ProfileData } from '../../types/profile';
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { useRouter } from '../../navigation';
|
import { useRouter } from '../../i18n/routing';
|
||||||
import { AdditionalFilter } from '../../types/experts';
|
import { AdditionalFilter } from '../../types/experts';
|
||||||
import { getObjectByFilter, getObjectByAdditionalFilter, getSearchParamsString } from '../../utils/filter';
|
import { getObjectByFilter, getObjectByAdditionalFilter, getSearchParamsString } from '../../utils/filter';
|
||||||
import { CustomInput } from '../view/CustomInput';
|
import { CustomInput } from '../view/CustomInput';
|
||||||
|
@ -55,8 +55,6 @@ export const ExpertsAdditionalFilter = ({
|
||||||
};
|
};
|
||||||
const search = getSearchParamsString(newFilter);
|
const search = getSearchParamsString(newFilter);
|
||||||
|
|
||||||
console.log('here1');
|
|
||||||
|
|
||||||
router.push(search ? `${basePath}?${search}#filter` : `${basePath}#filter`);
|
router.push(search ? `${basePath}?${search}#filter` : `${basePath}#filter`);
|
||||||
|
|
||||||
// router.push({
|
// router.push({
|
||||||
|
|
|
@ -1,19 +1,28 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { FC } from 'react';
|
import React, { FC, useState, useEffect } from 'react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { Tag, Image as AntdImage, Space } from 'antd';
|
import { Tag, Image as AntdImage, Space, Button } from 'antd';
|
||||||
import { ZoomInOutlined, ZoomOutOutlined, StarFilled } from '@ant-design/icons';
|
import { ZoomInOutlined, ZoomOutOutlined, StarFilled } from '@ant-design/icons';
|
||||||
|
import { SignupSessionData } from '../../types/experts';
|
||||||
import { ExpertDetails, Practice, ThemeGroup } from '../../types/experts';
|
import { ExpertDetails, Practice, ThemeGroup } from '../../types/experts';
|
||||||
import { ExpertDocument } from '../../types/file';
|
import { ExpertDocument } from '../../types/file';
|
||||||
import { Locale } from '../../types/locale';
|
import { Locale } from '../../types/locale';
|
||||||
import { CustomRate } from '../view/CustomRate';
|
import { CustomRate } from '../view/CustomRate';
|
||||||
import { i18nText } from '../../i18nKeys';
|
import { i18nText } from '../../i18nKeys';
|
||||||
import { FilledYellowButton } from '../view/FilledButton';
|
import { FilledYellowButton } from '../view/FilledButton';
|
||||||
|
import { getStorageValue } from '../../hooks/useLocalStorage';
|
||||||
|
import { AUTH_TOKEN_KEY, SESSION_DATA } from '../../constants/common';
|
||||||
|
import { ScheduleModal } from '../Modals/ScheduleModal';
|
||||||
|
import { ScheduleModalResult } from '../Modals/ScheduleModalResult';
|
||||||
|
import SignalrConnection from '../../lib/signalr-connection';
|
||||||
|
import { useRouter } from '../../i18n/routing';
|
||||||
|
import { useLocalStorage } from '../../hooks/useLocalStorage';
|
||||||
|
|
||||||
type ExpertDetailsProps = {
|
type ExpertDetailsProps = {
|
||||||
expert: ExpertDetails;
|
expert: ExpertDetails;
|
||||||
locale?: string;
|
locale?: string;
|
||||||
|
expertId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ExpertPracticeProps = {
|
type ExpertPracticeProps = {
|
||||||
|
@ -22,10 +31,60 @@ type ExpertPracticeProps = {
|
||||||
locale?: string;
|
locale?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ExpertCard: FC<ExpertDetailsProps> = ({ expert, locale }) => {
|
export const ExpertCard: FC<ExpertDetailsProps> = ({ expert, locale, expertId }) => {
|
||||||
const { publicCoachDetails } = expert || {};
|
const { publicCoachDetails } = expert || {};
|
||||||
|
const [showSchedulerModal, setShowSchedulerModal] = useState<boolean>(false);
|
||||||
|
const [mode, setMode] = useState<'data' | 'time' | 'pay' | 'finish'>('data');
|
||||||
|
const isRus = locale === Locale.ru;
|
||||||
|
const { publicCoachDetails: { tags = [], sessionCost = 0, sessionDuration = 0, coachLanguages = [] , id, botUserId} } = expert || {};
|
||||||
|
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
|
||||||
|
const { joinChatPerson, closeConnection } = SignalrConnection({ jwt }) || {};
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document?.addEventListener('show_pay_form', handleShowPayForm);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// if (closeConnection) closeConnection();
|
||||||
|
document?.removeEventListener('show_pay_form', handleShowPayForm);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleJoinChat = (id?: number) => {
|
||||||
|
if (id && joinChatPerson) {
|
||||||
|
joinChatPerson(id).then((res: any) => {
|
||||||
|
router.push(`/account/messages/${res.id}` as string);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkSession = (data?: SignupSessionData) => {
|
||||||
|
if (data?.startAtUtc && data?.tagId) {
|
||||||
|
const jwt = getStorageValue(AUTH_TOKEN_KEY, '');
|
||||||
|
sessionStorage?.setItem(SESSION_DATA, JSON.stringify(data));
|
||||||
|
if (jwt) {
|
||||||
|
setMode('pay');
|
||||||
|
} else {
|
||||||
|
setShowSchedulerModal(false);
|
||||||
|
const showAuth = new Event('show_auth_enter');
|
||||||
|
document?.dispatchEvent(showAuth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleShowPayForm = () => {
|
||||||
|
setShowSchedulerModal(true);
|
||||||
|
setMode('pay');
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSchedulerHandle = () => {
|
||||||
|
setMode('data');
|
||||||
|
setShowSchedulerModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<div className="expert-card">
|
<div className="expert-card">
|
||||||
<div className="expert-card__wrap">
|
<div className="expert-card__wrap">
|
||||||
<div className="expert-card__avatar">
|
<div className="expert-card__avatar">
|
||||||
|
@ -44,11 +103,18 @@ export const ExpertCard: FC<ExpertDetailsProps> = ({ expert, locale }) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{jwt && (
|
||||||
<div className="expert-card__wrap-btn">
|
<div className="expert-card__wrap-btn">
|
||||||
<a href="#" className="btn-apply">
|
<Button className="btn-apply" onClick={() => handleJoinChat(id)}>
|
||||||
<img src="/images/calendar-outline.svg" className="" alt="" />
|
{i18nText('chat.join', locale)}
|
||||||
{i18nText('schedule', locale)}
|
</Button>
|
||||||
</a>
|
<Button
|
||||||
|
className={`btn-apply${!botUserId ? ' btn-disabled' : ''}`}
|
||||||
|
disabled={!botUserId}
|
||||||
|
onClick={botUserId ? () => handleJoinChat(botUserId) : undefined}
|
||||||
|
>
|
||||||
|
{i18nText('chat.joinAI', locale)}
|
||||||
|
</Button>
|
||||||
{/*
|
{/*
|
||||||
<a href="#" className="btn-video">
|
<a href="#" className="btn-video">
|
||||||
<img src="/images/videocam-outline.svg" className="" alt=""/>
|
<img src="/images/videocam-outline.svg" className="" alt=""/>
|
||||||
|
@ -56,16 +122,8 @@ export const ExpertCard: FC<ExpertDetailsProps> = ({ expert, locale }) => {
|
||||||
</a>
|
</a>
|
||||||
*/}
|
*/}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ExpertInformation: FC<ExpertDetailsProps> = ({ expert, locale }) => {
|
|
||||||
const { publicCoachDetails: { tags = [], sessionCost = 0, sessionDuration = 0, coachLanguages = [] } } = expert || {};
|
|
||||||
const isRus = locale === Locale.ru;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="expert-info">
|
<div className="expert-info">
|
||||||
{/* <h2 className="title-h2">{}</h2> */}
|
{/* <h2 className="title-h2">{}</h2> */}
|
||||||
<div className="skills__list">
|
<div className="skills__list">
|
||||||
|
@ -91,11 +149,22 @@ export const ExpertInformation: FC<ExpertDetailsProps> = ({ expert, locale }) =>
|
||||||
{tags?.map((skill) => <Tag key={skill?.id} className="skills__list__item">{skill?.name}</Tag>)}
|
{tags?.map((skill) => <Tag key={skill?.id} className="skills__list__item">{skill?.name}</Tag>)}
|
||||||
</div>
|
</div>
|
||||||
<div className="wrap-btn-prise">
|
<div className="wrap-btn-prise">
|
||||||
<FilledYellowButton onClick={() => console.log('schedule')}>{i18nText('signUp', locale)}</FilledYellowButton>
|
<FilledYellowButton onClick={onSchedulerHandle}>{i18nText('signUp', locale)}</FilledYellowButton>
|
||||||
<div className="wrap-btn-prise__text">
|
<div className="wrap-btn-prise__text">
|
||||||
{`${sessionCost}€`} <span>/ {`${sessionDuration}${isRus ? 'мин' : 'min'}`}</span>
|
{`${sessionCost}€`} <span>/ {`${sessionDuration}${isRus ? 'мин' : 'min'}`}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ScheduleModal
|
||||||
|
open={showSchedulerModal}
|
||||||
|
handleCancel={() => setShowSchedulerModal(false)}
|
||||||
|
updateMode={setMode}
|
||||||
|
mode={mode}
|
||||||
|
expertId={expertId as string}
|
||||||
|
locale={locale as string}
|
||||||
|
sessionCost={sessionCost}
|
||||||
|
checkSession={checkSession}
|
||||||
|
/>
|
||||||
|
<ScheduleModalResult locale={locale as string} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { List, Tag } from 'antd';
|
||||||
import { RightOutlined } from '@ant-design/icons';
|
import { RightOutlined } from '@ant-design/icons';
|
||||||
import isEqual from 'lodash/isEqual';
|
import isEqual from 'lodash/isEqual';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { Link, useRouter } from '../../navigation';
|
import { Link, useRouter } from '../../i18n/routing';
|
||||||
import { ExpertsData, Filter, GeneralFilter } from '../../types/experts';
|
import { ExpertsData, Filter, GeneralFilter } from '../../types/experts';
|
||||||
import { getObjectByFilter, getObjectByAdditionalFilter, getSearchParamsString } from '../../utils/filter';
|
import { getObjectByFilter, getObjectByAdditionalFilter, getSearchParamsString } from '../../utils/filter';
|
||||||
import { getDuration, getPrice } from '../../utils/expert';
|
import { getDuration, getPrice } from '../../utils/expert';
|
||||||
|
|
|
@ -4,7 +4,7 @@ import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { Button, Collapse, List } from 'antd';
|
import { Button, Collapse, List } from 'antd';
|
||||||
import type { CollapseProps } from 'antd';
|
import type { CollapseProps } from 'antd';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import { useRouter } from '../../navigation';
|
import { useRouter } from '../../i18n/routing';
|
||||||
import { Filter } from '../../types/experts';
|
import { Filter } from '../../types/experts';
|
||||||
import { Languages, SearchData, Tag } from '../../types/tags';
|
import { Languages, SearchData, Tag } from '../../types/tags';
|
||||||
import { getObjectByFilter, getObjectByAdditionalFilter, getSearchParamsString } from '../../utils/filter';
|
import { getObjectByFilter, getObjectByAdditionalFilter, getSearchParamsString } from '../../utils/filter';
|
||||||
|
@ -114,7 +114,6 @@ export const ExpertsFilter = ({
|
||||||
...getObjectByAdditionalFilter(searchParams)
|
...getObjectByAdditionalFilter(searchParams)
|
||||||
};
|
};
|
||||||
const search = getSearchParamsString(newFilter);
|
const search = getSearchParamsString(newFilter);
|
||||||
console.log('basePath', basePath);
|
|
||||||
|
|
||||||
router.push(search ? `${basePath}?${search}#filter` : `${basePath}#filter`);
|
router.push(search ? `${basePath}?${search}#filter` : `${basePath}#filter`);
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { Dispatch, FC, SetStateAction, useEffect } from 'react';
|
import React, { Dispatch, FC, SetStateAction, useEffect } from 'react';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname, useSearchParams } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Modal, Form } from 'antd';
|
import { Modal, Form, notification } from 'antd';
|
||||||
import { CloseOutlined } from '@ant-design/icons';
|
import { CloseOutlined } from '@ant-design/icons';
|
||||||
import { RegisterContent, ResetContent, FinishContent, EnterContent } from './authModalContent';
|
import { RegisterContent, ResetContent, FinishContent, EnterContent } from './authModalContent';
|
||||||
import { i18nText } from '../../i18nKeys';
|
import { i18nText } from '../../i18nKeys';
|
||||||
|
import { useRouter } from '../../i18n/routing';
|
||||||
|
import { AUTH_USER} from '../../constants/common';
|
||||||
|
import { getRegisterByApple, getLoginByApple } from '../../actions/auth';
|
||||||
|
import { getUserData } from '../../actions/profile';
|
||||||
|
|
||||||
type AuthModalProps = {
|
type AuthModalProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
@ -27,6 +31,47 @@ export const AuthModal: FC<AuthModalProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const [form] = Form.useForm<{ login: string, password: string, confirmPassword: string }>();
|
const [form] = Form.useForm<{ login: string, password: string, confirmPassword: string }>();
|
||||||
const paths = usePathname().split('/');
|
const paths = usePathname().split('/');
|
||||||
|
const params = useSearchParams();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const onUpdateToken = (token: string) => {
|
||||||
|
if (updateToken && typeof updateToken !== 'string') {
|
||||||
|
updateToken(token);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const code = params.get('code');
|
||||||
|
const type = params.get('state');
|
||||||
|
if (code && type) {
|
||||||
|
const appleFunc = type === 'bbregister' ? getRegisterByApple : getLoginByApple;
|
||||||
|
appleFunc(locale, code)
|
||||||
|
.then((data) => {
|
||||||
|
if (data.jwtToken) {
|
||||||
|
getUserData(locale, data.jwtToken)
|
||||||
|
.then((profile) => {
|
||||||
|
localStorage.setItem(AUTH_USER, JSON.stringify(profile));
|
||||||
|
onUpdateToken(data.jwtToken);
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
notification.error({
|
||||||
|
message: 'Error',
|
||||||
|
description: 'Access denied'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const err = error?.message ? JSON.parse(error.message) : {};
|
||||||
|
notification.error({
|
||||||
|
message: 'Error',
|
||||||
|
description: err?.details?.errMessage || undefined
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
router.push('/');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [params]);
|
||||||
|
|
||||||
const onAfterClose = () => {
|
const onAfterClose = () => {
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
|
@ -38,12 +83,6 @@ export const AuthModal: FC<AuthModalProps> = ({
|
||||||
}
|
}
|
||||||
}, [mode]);
|
}, [mode]);
|
||||||
|
|
||||||
const onUpdateToken = (token: string) => {
|
|
||||||
if (updateToken && typeof updateToken !== 'string') {
|
|
||||||
updateToken(token);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
className="b-modal"
|
className="b-modal"
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { FC, useEffect, useState } from 'react';
|
import React, { FC, useState } from 'react';
|
||||||
import {Modal, Button, message, Form, Collapse, GetProp, UploadProps} from 'antd';
|
import { Modal, Button, message, Form, Collapse } from 'antd';
|
||||||
import type { CollapseProps } from 'antd';
|
import type { CollapseProps } from 'antd';
|
||||||
import { CloseOutlined } from '@ant-design/icons';
|
import { CloseOutlined } from '@ant-design/icons';
|
||||||
import { i18nText } from '../../i18nKeys';
|
import { i18nText } from '../../i18nKeys';
|
||||||
import { PracticePersonData, PracticeDTO, PracticeData, PracticeCase } from '../../types/practice';
|
import { PracticePersonData } from '../../types/practice';
|
||||||
import { AUTH_TOKEN_KEY } from '../../constants/common';
|
import { AUTH_TOKEN_KEY } from '../../constants/common';
|
||||||
import { useLocalStorage } from '../../hooks/useLocalStorage';
|
import { useLocalStorage } from '../../hooks/useLocalStorage';
|
||||||
import {setEducation} from '../../actions/profile';
|
import { setEducation } from '../../actions/profile';
|
||||||
import {Certificate, Details, EducationData, EducationDTO, Experience} from "../../types/education";
|
import { EducationData, EducationDTO } from '../../types/education';
|
||||||
import {CertificatesContent} from "./educationModalContent/Certificates";
|
import { CertificatesContent } from './educationModalContent/Certificates';
|
||||||
import {EducationsContent} from "./educationModalContent/Educations";
|
import { EducationsContent } from './educationModalContent/Educations';
|
||||||
import {TrainingsContent} from "./educationModalContent/Trainings";
|
import { TrainingsContent } from './educationModalContent/Trainings';
|
||||||
import {MbasContent} from "./educationModalContent/Mbas";
|
import { MbasContent } from './educationModalContent/Mbas';
|
||||||
import {ExperiencesContent} from "./educationModalContent/Experiences";
|
import { ExperiencesContent } from './educationModalContent/Experiences';
|
||||||
|
|
||||||
type EditExpertEducationModalProps = {
|
type EditExpertEducationModalProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|
|
@ -0,0 +1,285 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, {FC, useEffect, useState} from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { Modal, Menu, Calendar, Radio, Button, Input, message, Form } from 'antd';
|
||||||
|
import type { CalendarProps, MenuProps } from 'antd';
|
||||||
|
import { ArrowLeftOutlined } from '@ant-design/icons';
|
||||||
|
import { CloseOutlined } from '@ant-design/icons';
|
||||||
|
import dayjs, { 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 { getLocale } from '../../utils/locale';
|
||||||
|
import { AUTH_TOKEN_KEY, SESSION_DATA } from '../../constants/common';
|
||||||
|
import { ExpertScheduler, SignupSessionData } from '../../types/experts';
|
||||||
|
import { Tag } from '../../types/tags';
|
||||||
|
import { getSchedulerByExpertId, getSchedulerSession } from '../../actions/experts';
|
||||||
|
import { StripeElementsForm } from '../stripe/StripeElementsForm';
|
||||||
|
import { i18nText } from '../../i18nKeys';
|
||||||
|
import { CustomSelect } from '../../components/view/CustomSelect';
|
||||||
|
import { Loader } from '../view/Loader';
|
||||||
|
import { getStorageValue } from '../../hooks/useLocalStorage';
|
||||||
|
|
||||||
|
type ScheduleModalProps = {
|
||||||
|
open: boolean;
|
||||||
|
handleCancel: () => void;
|
||||||
|
mode: 'data' | 'time' | 'pay' | 'finish';
|
||||||
|
updateMode: (mode: 'data' | 'time' | 'pay' | 'finish') => void;
|
||||||
|
sessionCost: number;
|
||||||
|
expertId: string;
|
||||||
|
locale: string;
|
||||||
|
checkSession: (data?: SignupSessionData) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MenuItem = Required<MenuProps>['items'][number];
|
||||||
|
|
||||||
|
const getCalendarMenu = (start: Dayjs): MenuItem[] => Array.from({ length: 3 })
|
||||||
|
.map((_: unknown, index: number) => {
|
||||||
|
const date = index ? start.add(index, 'M') : start.clone();
|
||||||
|
return {
|
||||||
|
label: <span className="b-calendar-month">{date.format('MMMM')}</span>,
|
||||||
|
key: date.format('YYYY-MM-DD')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ScheduleModal: FC<ScheduleModalProps> = ({
|
||||||
|
open,
|
||||||
|
handleCancel,
|
||||||
|
mode,
|
||||||
|
updateMode,
|
||||||
|
sessionCost,
|
||||||
|
locale,
|
||||||
|
expertId,
|
||||||
|
checkSession,
|
||||||
|
}) => {
|
||||||
|
const [selectDate, setSelectDate] = useState<Dayjs>(dayjs());
|
||||||
|
const [dates, setDates] = useState<Record<string, { startTime: string, endTime: string }[]> | undefined>();
|
||||||
|
const [tags, setTags] = useState<Tag[] | undefined>();
|
||||||
|
const [rawScheduler, setRawScheduler] = useState<ExpertScheduler | null>(null);
|
||||||
|
const [isPayLoading, setIsPayLoading] = useState<boolean>(false);
|
||||||
|
const [sessionId, setSessionId] = useState<string>('');
|
||||||
|
const [form] = Form.useForm<{ clientComment?: string, startAtUtc?: string, tagId?: number }>();
|
||||||
|
|
||||||
|
dayjs.locale(locale);
|
||||||
|
|
||||||
|
const signupSession = () => {
|
||||||
|
const data = sessionStorage?.getItem(SESSION_DATA);
|
||||||
|
const jwt = getStorageValue(AUTH_TOKEN_KEY, '');
|
||||||
|
|
||||||
|
if (jwt && data) {
|
||||||
|
const parseData = JSON.parse(data);
|
||||||
|
setIsPayLoading(true);
|
||||||
|
getSchedulerSession(parseData as SignupSessionData, locale || 'en', jwt)
|
||||||
|
.then((session) => {
|
||||||
|
setSessionId(session?.sessionId);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
message.error('Не удалось провести оплату')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
sessionStorage?.removeItem(SESSION_DATA);
|
||||||
|
setIsPayLoading(false);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(()=> {
|
||||||
|
if (open && mode !== 'pay') {
|
||||||
|
getSchedulerByExpertId(expertId as string, locale as string)
|
||||||
|
.then((data) => {
|
||||||
|
setRawScheduler(data);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!open) {
|
||||||
|
form.resetFields();
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open && mode === 'pay') {
|
||||||
|
signupSession();
|
||||||
|
}
|
||||||
|
}, [mode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const map = {} as any
|
||||||
|
rawScheduler?.availableSlots.forEach((el) => {
|
||||||
|
const key = dayjs(el.startTime).format('YYYY-MM-DD');
|
||||||
|
if (!map[key]){
|
||||||
|
map[key] = []
|
||||||
|
}
|
||||||
|
map[key].push(el);
|
||||||
|
})
|
||||||
|
setDates(map);
|
||||||
|
setTags(rawScheduler?.tags)
|
||||||
|
}, [rawScheduler]);
|
||||||
|
|
||||||
|
const onPanelChange = (value: Dayjs) => setSelectDate(value);
|
||||||
|
|
||||||
|
const onDateChange: CalendarProps<Dayjs>['onSelect'] = (value, selectInfo) => {
|
||||||
|
if (selectInfo.source === 'date') {
|
||||||
|
setSelectDate(value);
|
||||||
|
updateMode('time');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const disabledDate = (currentDate: Dayjs) => !dates || !dates[currentDate.format('YYYY-MM-DD')];
|
||||||
|
|
||||||
|
const cellRender: CalendarProps<Dayjs>['fullCellRender'] = (date, info) => {
|
||||||
|
const isWeekend = date.day() === 6 || date.day() === 0;
|
||||||
|
return React.cloneElement(info.originNode, {
|
||||||
|
...info.originNode.props,
|
||||||
|
className: classNames('b-calendar-cell', {
|
||||||
|
['b-calendar-cell__select']: selectDate.isSame(date, 'date'),
|
||||||
|
['b-calendar-cell__today']: date.isSame(dayjs(), 'date'),
|
||||||
|
['b-calendar-cell__weekend']: isWeekend,
|
||||||
|
}),
|
||||||
|
children: (
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
{date.get('date')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onValidate = () => {
|
||||||
|
form.validateFields()
|
||||||
|
.then((values) => {
|
||||||
|
checkSession({ coachId: +expertId, ...values });
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
className="b-modal"
|
||||||
|
open={open}
|
||||||
|
title={undefined}
|
||||||
|
onOk={undefined}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
footer={false}
|
||||||
|
width={498}
|
||||||
|
closeIcon={<CloseOutlined style={{ fontSize: 20, color: '#000' }}/>}
|
||||||
|
>
|
||||||
|
{mode === 'data' && (
|
||||||
|
<Calendar
|
||||||
|
className="b-calendar"
|
||||||
|
fullscreen={false}
|
||||||
|
onPanelChange={onPanelChange}
|
||||||
|
fullCellRender={cellRender}
|
||||||
|
onSelect={onDateChange}
|
||||||
|
value={selectDate}
|
||||||
|
disabledDate={disabledDate}
|
||||||
|
locale={getLocale(locale)}
|
||||||
|
validRange={[selectDate.startOf('M'), selectDate.endOf('M')]}
|
||||||
|
headerRender={({ onChange }) => {
|
||||||
|
const start = dayjs().startOf('M');
|
||||||
|
const [activeMonth, setActiveMonth] = useState<string>(start.format('YYYY-MM-DD'));
|
||||||
|
|
||||||
|
const onClick: MenuProps['onClick'] = (e) => {
|
||||||
|
setActiveMonth(e.key);
|
||||||
|
onChange(dayjs(e.key));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu
|
||||||
|
className="b-calendar-header"
|
||||||
|
onClick={onClick}
|
||||||
|
selectedKeys={[activeMonth]}
|
||||||
|
mode="horizontal"
|
||||||
|
items={getCalendarMenu(start)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{mode === 'time' && (
|
||||||
|
<div className="b-schedule-time">
|
||||||
|
<div className="b-schedule-time-header">
|
||||||
|
<Button
|
||||||
|
className="b-button-link-big"
|
||||||
|
type="link"
|
||||||
|
onClick={() => updateMode('data')}
|
||||||
|
icon={<ArrowLeftOutlined />}
|
||||||
|
iconPosition="start"
|
||||||
|
>
|
||||||
|
{selectDate.locale(locale).format('DD MMMM YYYY')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Form form={form}>
|
||||||
|
<div className="b-schedule-select-tag">
|
||||||
|
{tags && (
|
||||||
|
<Form.Item
|
||||||
|
name="tagId"
|
||||||
|
rules={[{
|
||||||
|
required: true,
|
||||||
|
message: ''
|
||||||
|
}]}
|
||||||
|
>
|
||||||
|
<CustomSelect
|
||||||
|
label={i18nText('selectTopic', locale)}
|
||||||
|
options={tags?.map(({id, name}) => ({value: id, label: name}))}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="b-schedule-radio-list">
|
||||||
|
<Form.Item
|
||||||
|
name="startAtUtc"
|
||||||
|
rules={[{
|
||||||
|
required: true,
|
||||||
|
message: ''
|
||||||
|
}]}
|
||||||
|
>
|
||||||
|
<Radio.Group>
|
||||||
|
{dates && dates[selectDate.format('YYYY-MM-DD')].map((el: any) => (
|
||||||
|
<Radio
|
||||||
|
key={el.startTime}
|
||||||
|
value={el.startTime}
|
||||||
|
>
|
||||||
|
{dayjs(el.startTime).format('HH:mm')} - {dayjs(el.endTime).format('HH:mm')}
|
||||||
|
</Radio>)
|
||||||
|
)}
|
||||||
|
</Radio.Group>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
<Form.Item name="clientComment">
|
||||||
|
<Input.TextArea
|
||||||
|
className="b-textarea"
|
||||||
|
rows={2}
|
||||||
|
placeholder={i18nText('sessionWishes', locale)}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
<Button
|
||||||
|
className="btn-apply"
|
||||||
|
onClick={onValidate}
|
||||||
|
>
|
||||||
|
{i18nText('pay', locale)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{mode === 'pay' && (
|
||||||
|
<div className="b-schedule-payment">
|
||||||
|
<Loader isLoading={isPayLoading}>
|
||||||
|
<StripeElementsForm
|
||||||
|
amount={sessionCost}
|
||||||
|
locale={locale}
|
||||||
|
sessionId={sessionId}
|
||||||
|
/>
|
||||||
|
</Loader>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,74 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Modal, Result } from 'antd';
|
||||||
|
import { CloseOutlined } from '@ant-design/icons';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
import { useRouter } from '../../i18n/routing';
|
||||||
|
import { Stripe } from 'stripe';
|
||||||
|
import { getStripePaymentStatus } from '../../actions/stripe';
|
||||||
|
import { sessionPaymentConfirm } from '../../actions/sessions';
|
||||||
|
import { getStorageValue } from '../../hooks/useLocalStorage';
|
||||||
|
import { AUTH_TOKEN_KEY } from '../../constants/common';
|
||||||
|
import { Session, SessionState } from '../../types/sessions';
|
||||||
|
import { i18nText } from '../../i18nKeys';
|
||||||
|
|
||||||
|
export const ScheduleModalResult = ({ locale }: { locale: string }) => {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const [paymentStatus, setPaymentStatus] = useState<Stripe.PaymentIntent.Status | undefined>();
|
||||||
|
const [session, setSession] = useState<Session | undefined>();
|
||||||
|
const [error, setError] = useState<any>();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setError(undefined);
|
||||||
|
const payment_intent = searchParams.get('payment_intent') || false;
|
||||||
|
if (payment_intent) {
|
||||||
|
getStripePaymentStatus(payment_intent)
|
||||||
|
.then((result) => {
|
||||||
|
setPaymentStatus(result?.status);
|
||||||
|
if (result?.status === 'succeeded' && result?.metadata?.sessionId) {
|
||||||
|
const jwt = getStorageValue(AUTH_TOKEN_KEY, '');
|
||||||
|
sessionPaymentConfirm(locale, jwt, +result.metadata.sessionId)
|
||||||
|
.then((session) => {
|
||||||
|
setSession(session);
|
||||||
|
})
|
||||||
|
.catch((err: any) => {
|
||||||
|
setError(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err: any) => {
|
||||||
|
setError(err);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [searchParams]);
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
const { origin, pathname } = window?.location || {};
|
||||||
|
|
||||||
|
router.push(`${origin}${pathname}`);
|
||||||
|
setPaymentStatus(undefined);
|
||||||
|
setSession(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
className="b-modal"
|
||||||
|
open={paymentStatus === 'succeeded' && session?.state === SessionState.PAID}
|
||||||
|
title={undefined}
|
||||||
|
onOk={undefined}
|
||||||
|
onCancel={onClose}
|
||||||
|
footer={false}
|
||||||
|
width={498}
|
||||||
|
closeIcon={<CloseOutlined style={{ fontSize: 20, color: '#000' }}/>}
|
||||||
|
>
|
||||||
|
<div className="b-schedule-payment-result">
|
||||||
|
<Result
|
||||||
|
status="success"
|
||||||
|
title={i18nText('successPayment', locale)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { FC, useEffect, useRef, useState } from 'react';
|
||||||
|
import { Modal, Button, message, Input } from 'antd';
|
||||||
|
import { CloseOutlined, StarFilled } from '@ant-design/icons';
|
||||||
|
import { i18nText } from '../../i18nKeys';
|
||||||
|
import { useLocalStorage } from '../../hooks/useLocalStorage';
|
||||||
|
import { AUTH_TOKEN_KEY } from '../../constants/common';
|
||||||
|
import { getReport, saveReport } from '../../actions/rooms';
|
||||||
|
import { Report, ReportData } from '../../types/rooms';
|
||||||
|
import { CustomRate } from '../view/CustomRate';
|
||||||
|
|
||||||
|
type SupervisorReportModalProps = {
|
||||||
|
open: boolean;
|
||||||
|
handleCancel: () => void;
|
||||||
|
locale: string;
|
||||||
|
refresh: () => void;
|
||||||
|
roomId: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SupervisorReportModal: FC<SupervisorReportModalProps> = ({
|
||||||
|
open,
|
||||||
|
handleCancel,
|
||||||
|
locale,
|
||||||
|
roomId,
|
||||||
|
refresh
|
||||||
|
}) => {
|
||||||
|
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
const [report, setReport] = useState<Report[] | undefined>();
|
||||||
|
const reasonRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getReport(locale, jwt, roomId)
|
||||||
|
.then((data) => {
|
||||||
|
setReport(data);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
message.error('Не удалось получить отчет');
|
||||||
|
})
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onSaveRate = () => {
|
||||||
|
const result: ReportData = {
|
||||||
|
sessionId: roomId,
|
||||||
|
sessionSupervisorScores: report || [],
|
||||||
|
supervisorComment: reasonRef?.current?.resizableTextArea?.textArea?.value || ''
|
||||||
|
};
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
saveReport(locale, jwt, result)
|
||||||
|
.then(() => {
|
||||||
|
handleCancel();
|
||||||
|
refresh();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
message.error('Не удалось сохранить отчет');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onChangeRate = (val: number, id: number) => {
|
||||||
|
setReport(report ? report.map((item) => {
|
||||||
|
if (item.evaluationCriteriaId === id) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
score: val
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}) : undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
className="b-modal"
|
||||||
|
open={open}
|
||||||
|
title={undefined}
|
||||||
|
onOk={undefined}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
footer={false}
|
||||||
|
width={498}
|
||||||
|
closeIcon={<CloseOutlined style={{ fontSize: 20, color: '#000' }}/>}
|
||||||
|
>
|
||||||
|
<div className="b-modal__report__content">
|
||||||
|
<div className="b-rate-list">
|
||||||
|
{report && report.length > 0 && report.map(({ key, evaluationCriteriaId, score }) => (
|
||||||
|
<div key={evaluationCriteriaId} className="b-rate-list__item">
|
||||||
|
<div className="b-rate-list__item_title">{i18nText(`room.rating_${key?.toLowerCase()}`, locale)}</div>
|
||||||
|
<CustomRate
|
||||||
|
defaultValue={score || 0}
|
||||||
|
character={<StarFilled style={{ fontSize: 32 }} />}
|
||||||
|
onChange={(val: number) => onChangeRate(val, evaluationCriteriaId)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Input.TextArea
|
||||||
|
ref={reasonRef}
|
||||||
|
className="b-textarea"
|
||||||
|
rows={1}
|
||||||
|
placeholder={i18nText('room.tellAboutReason', locale)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
className="btn-apply"
|
||||||
|
onClick={onSaveRate}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
{i18nText('room.rate', locale)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,113 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { Button, Modal, notification } from 'antd';
|
||||||
|
import { CloseOutlined } from '@ant-design/icons';
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { i18nText } from '../../i18nKeys';
|
||||||
|
import { getUsersList } from '../../actions/rooms';
|
||||||
|
import { PublicUser } from '../../types/sessions';
|
||||||
|
import { Room } from '../../types/rooms';
|
||||||
|
import { CustomInput } from '../view/CustomInput';
|
||||||
|
import { Loader } from '../view/Loader';
|
||||||
|
|
||||||
|
type UserListModalProps = {
|
||||||
|
room: Room;
|
||||||
|
isOpen: boolean;
|
||||||
|
locale: string;
|
||||||
|
handleCancel: () => void;
|
||||||
|
jwt: string;
|
||||||
|
submit: (id: number) => void;
|
||||||
|
afterCloseModal?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UserListModal = ({ room, isOpen, locale, handleCancel, jwt, submit, afterCloseModal }: UserListModalProps) => {
|
||||||
|
const [users, setUsers] = useState<PublicUser[] | undefined>();
|
||||||
|
const [loading, seLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const onSearch = useCallback(debounce((e: any) => {
|
||||||
|
if (e?.target?.value) {
|
||||||
|
seLoading(true);
|
||||||
|
getUsersList(locale, jwt, { template: e.target.value })
|
||||||
|
.then(({ items }) => {
|
||||||
|
const clients = room?.clients?.map(({ id }) => id);
|
||||||
|
setUsers(items
|
||||||
|
? items.filter(({ id }) => !(clients?.length && clients.includes(id) || id === room?.supervisor?.id || id === room?.coach?.id))
|
||||||
|
: undefined);
|
||||||
|
})
|
||||||
|
.catch((err: any) => {
|
||||||
|
notification.error({
|
||||||
|
message: 'Error',
|
||||||
|
description: err?.response?.data?.errMessage
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
seLoading(false);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setUsers(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
}, 300), []);
|
||||||
|
|
||||||
|
const onAfterClose = () => {
|
||||||
|
setUsers(undefined);
|
||||||
|
if (afterCloseModal) afterCloseModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
className="b-modal"
|
||||||
|
open={isOpen}
|
||||||
|
title={undefined}
|
||||||
|
onOk={undefined}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
footer={false}
|
||||||
|
width={498}
|
||||||
|
closeIcon={<CloseOutlined style={{ fontSize: 20, color: '#000' }}/>}
|
||||||
|
afterClose={onAfterClose}
|
||||||
|
>
|
||||||
|
<div className="b-modal__users-list__content">
|
||||||
|
<CustomInput
|
||||||
|
placeholder={i18nText('search', locale)}
|
||||||
|
onChange={onSearch}
|
||||||
|
allowClear
|
||||||
|
/>
|
||||||
|
{users && (
|
||||||
|
<div className="b-users-list__wrapper">
|
||||||
|
<Loader isLoading={loading}>
|
||||||
|
{users.length > 0 ? (
|
||||||
|
<div className="b-users-list">
|
||||||
|
{users.map(({ id, name, surname, faceImageUrl }) => (
|
||||||
|
<div className="b-users-list-item" key={id}>
|
||||||
|
<div>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
className="card-detail__filled"
|
||||||
|
onClick={() => submit(id)}
|
||||||
|
>
|
||||||
|
{i18nText('room.invite', locale)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="b-users-list__empty">{i18nText('noData', locale)}</div>
|
||||||
|
)}
|
||||||
|
</Loader>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,12 +1,11 @@
|
||||||
import React, { FC, useState } from 'react';
|
import React, { FC, useState } from 'react';
|
||||||
import { Form, FormInstance, notification } from 'antd';
|
import { Form, FormInstance, notification } from 'antd';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { Social } from '../../../types/social';
|
import { useGoogleLogin } from '@react-oauth/google';
|
||||||
|
import AppleLogin from 'react-apple-login';
|
||||||
import { AUTH_USER } from '../../../constants/common';
|
import { AUTH_USER } from '../../../constants/common';
|
||||||
import { SocialConfig } from '../../../constants/social';
|
import { getAuth, getLoginByGoogle } from '../../../actions/auth';
|
||||||
import { useOauthWindow } from '../../../hooks/useOauthWindow';
|
import { getUserData } from '../../../actions/profile';
|
||||||
import { getAuth } from '../../../actions/auth';
|
|
||||||
import { getPersonalData } from '../../../actions/profile';
|
|
||||||
import { CustomInput } from '../../view/CustomInput';
|
import { CustomInput } from '../../view/CustomInput';
|
||||||
import { CustomInputPassword } from '../../view/CustomInputPassword';
|
import { CustomInputPassword } from '../../view/CustomInputPassword';
|
||||||
import { FilledButton } from '../../view/FilledButton';
|
import { FilledButton } from '../../view/FilledButton';
|
||||||
|
@ -30,7 +29,6 @@ export const EnterContent: FC<EnterProps> = ({
|
||||||
handleCancel
|
handleCancel
|
||||||
}) => {
|
}) => {
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
const { openOauthWindow } = useOauthWindow();
|
|
||||||
|
|
||||||
const onLogin = () => {
|
const onLogin = () => {
|
||||||
form.validateFields().then(() => {
|
form.validateFields().then(() => {
|
||||||
|
@ -39,7 +37,7 @@ export const EnterContent: FC<EnterProps> = ({
|
||||||
getAuth(locale, { login, password })
|
getAuth(locale, { login, password })
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data.jwtToken) {
|
if (data.jwtToken) {
|
||||||
getPersonalData(locale, data.jwtToken)
|
getUserData(locale, data.jwtToken)
|
||||||
.then((profile) => {
|
.then((profile) => {
|
||||||
localStorage.setItem(AUTH_USER, JSON.stringify(profile));
|
localStorage.setItem(AUTH_USER, JSON.stringify(profile));
|
||||||
updateToken(data.jwtToken);
|
updateToken(data.jwtToken);
|
||||||
|
@ -59,47 +57,44 @@ export const EnterContent: FC<EnterProps> = ({
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSocialEnter = (type: Social) => {
|
|
||||||
const url = SocialConfig[type].oauthUrl;
|
|
||||||
|
|
||||||
if (!url) return;
|
const onGoogleLogin = useGoogleLogin({
|
||||||
|
onError: (err) => {
|
||||||
openOauthWindow(url, type, async (event: MessageEvent) => {
|
notification.error({
|
||||||
const { data: socialData } = event
|
message: err.error,
|
||||||
|
description: err.error_description
|
||||||
// примерная схема последующей обработки
|
});
|
||||||
|
},
|
||||||
// const socialErrors: string[] = [];
|
onSuccess: (tokenResponse) => {
|
||||||
// try {
|
setIsLoading(true);
|
||||||
// // отправляем запрос на бэк с данными из соц сети
|
getLoginByGoogle(locale, tokenResponse.access_token)
|
||||||
// const { data: { jwtToken } } = await query(socialData);
|
.then((data) => {
|
||||||
// // обновляем токен
|
if (data.jwtToken) {
|
||||||
// updateToken(jwtToken);
|
getUserData(locale, data.jwtToken)
|
||||||
// // получаем данные о пользователе
|
.then((profile) => {
|
||||||
// await getAuthUser()
|
localStorage.setItem(AUTH_USER, JSON.stringify(profile));
|
||||||
// } catch (error: any) {
|
updateToken(data.jwtToken);
|
||||||
// if (error.httpStatus === 449) {
|
handleCancel();
|
||||||
// // ошибка, когда отсутствует e-mail
|
|
||||||
//
|
|
||||||
// // какие-то дальнейшие действия после получения ошибки, например, закрываем окно и открываем модалку регистрации
|
|
||||||
// handleCancel();
|
|
||||||
// openSocialEmailRequestModal(socialData);
|
|
||||||
// } else if (error.httpStatus === 409) {
|
|
||||||
// // ошибка, когда по переданному email уже существует аккаунт
|
|
||||||
//
|
|
||||||
// // какие-то дальнейшие действия после получения ошибки, например, закрываем окно и открываем модалку с вводом пароля
|
|
||||||
// handleCancel();
|
|
||||||
// openSocialPasswordModal(socialData);
|
|
||||||
// } else {
|
|
||||||
// // в остальных случаях записываем ошибку в массив ошибок
|
|
||||||
// socialErrors.push(error.toString());
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // если все успешно, закрываем окно
|
|
||||||
// handleCancel();
|
|
||||||
})
|
})
|
||||||
};
|
} else {
|
||||||
|
notification.error({
|
||||||
|
message: 'Error',
|
||||||
|
description: 'Access denied'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const err = error?.message ? JSON.parse(error.message) : {};
|
||||||
|
notification.error({
|
||||||
|
message: 'Error',
|
||||||
|
description: err?.details?.errMessage || undefined
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -155,21 +150,24 @@ export const EnterContent: FC<EnterProps> = ({
|
||||||
{`${i18nText('forgotPass', locale)}?`}
|
{`${i18nText('forgotPass', locale)}?`}
|
||||||
</LinkButton>
|
</LinkButton>
|
||||||
<span>{i18nText('or', locale)}</span>
|
<span>{i18nText('or', locale)}</span>
|
||||||
<OutlinedButton
|
<AppleLogin
|
||||||
icon={<Image src="/images/facebook-logo.png" height={20} width={20} alt="" />}
|
clientId="bbuddy.expert"
|
||||||
onClick={() => onSocialEnter(Social.FACEBOOK)}
|
redirectURI="https://bbuddy.expert"
|
||||||
>
|
state="bblogin"
|
||||||
{i18nText('facebook', locale)}
|
responseType="code"
|
||||||
</OutlinedButton>
|
responseMode="query"
|
||||||
|
render={({ onClick }) => (
|
||||||
<OutlinedButton
|
<OutlinedButton
|
||||||
icon={<Image src="/images/apple-logo.png" height={22} width={22} alt="" />}
|
icon={<Image src="/images/apple-logo.png" height={22} width={22} alt="" />}
|
||||||
onClick={() => onSocialEnter(Social.APPLE)}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
{i18nText('apple', locale)}
|
{i18nText('apple', locale)}
|
||||||
</OutlinedButton>
|
</OutlinedButton>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<OutlinedButton
|
<OutlinedButton
|
||||||
icon={<Image src="/images/google-logo.png" height={20} width={20} alt="" />}
|
icon={<Image src="/images/google-logo.png" height={20} width={20} alt="" />}
|
||||||
onClick={() => onSocialEnter(Social.GOOGLE)}
|
onClick={onGoogleLogin}
|
||||||
>
|
>
|
||||||
{i18nText('google', locale)}
|
{i18nText('google', locale)}
|
||||||
</OutlinedButton>
|
</OutlinedButton>
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import React, { FC, useState } from 'react';
|
import React, { FC, useState } from 'react';
|
||||||
import { Form, FormInstance, notification } from 'antd';
|
import { Form, FormInstance, notification } from 'antd';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { Social } from '../../../types/social';
|
import { useGoogleLogin } from '@react-oauth/google';
|
||||||
|
import AppleLogin from 'react-apple-login';
|
||||||
import { AUTH_USER } from '../../../constants/common';
|
import { AUTH_USER } from '../../../constants/common';
|
||||||
import { SocialConfig } from '../../../constants/social';
|
import { getRegister, getRegisterByGoogle } from '../../../actions/auth';
|
||||||
import { getRegister } from '../../../actions/auth';
|
import { getUserData, setPersonData } from '../../../actions/profile';
|
||||||
import { setPersonData } from '../../../actions/profile';
|
|
||||||
import { useOauthWindow } from '../../../hooks/useOauthWindow';
|
|
||||||
import { CustomInput } from '../../view/CustomInput';
|
import { CustomInput } from '../../view/CustomInput';
|
||||||
import { CustomInputPassword } from '../../view/CustomInputPassword';
|
import { CustomInputPassword } from '../../view/CustomInputPassword';
|
||||||
import { FilledButton } from '../../view/FilledButton';
|
import { FilledButton } from '../../view/FilledButton';
|
||||||
|
@ -29,7 +28,6 @@ export const RegisterContent: FC<RegisterProps> = ({
|
||||||
handleCancel
|
handleCancel
|
||||||
}) => {
|
}) => {
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
const { openOauthWindow } = useOauthWindow();
|
|
||||||
|
|
||||||
const onRegister = () => {
|
const onRegister = () => {
|
||||||
form.validateFields().then(() => {
|
form.validateFields().then(() => {
|
||||||
|
@ -64,47 +62,38 @@ export const RegisterContent: FC<RegisterProps> = ({
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSocialRegister = (type: Social) => {
|
const onGoogleLogin = useGoogleLogin({
|
||||||
const url = SocialConfig[type].oauthUrl;
|
onError: (err) => {
|
||||||
|
notification.error({
|
||||||
if (!url) return;
|
message: err.error,
|
||||||
|
description: err.error_description
|
||||||
openOauthWindow(url, type, async (event: MessageEvent) => {
|
});
|
||||||
const { data: socialData } = event
|
},
|
||||||
|
onSuccess: (tokenResponse) => {
|
||||||
// примерная схема последующей обработки
|
setIsLoading(true);
|
||||||
|
getRegisterByGoogle(locale, tokenResponse.access_token)
|
||||||
// const socialErrors: string[] = [];
|
.then((data) => {
|
||||||
// try {
|
if (data.jwtToken) {
|
||||||
// // отправляем запрос на бэк с данными из соц сети
|
getUserData(locale, data.jwtToken)
|
||||||
// const { data: { jwtToken } } = await query(socialData);
|
.then((profile) => {
|
||||||
// // обновляем токен
|
localStorage.setItem(AUTH_USER, JSON.stringify(profile));
|
||||||
// updateToken(jwtToken);
|
updateToken(data.jwtToken);
|
||||||
// // получаем данные о пользователе
|
handleCancel();
|
||||||
// await getAuthUser()
|
|
||||||
// } catch (error: any) {
|
|
||||||
// if (error.httpStatus === 449) {
|
|
||||||
// // ошибка, когда отсутствует e-mail
|
|
||||||
//
|
|
||||||
// // какие-то дальнейшие действия после получения ошибки, например, закрываем окно и открываем модалку регистрации
|
|
||||||
// handleCancel();
|
|
||||||
// openSocialEmailRequestModal(socialData);
|
|
||||||
// } else if (error.httpStatus === 409) {
|
|
||||||
// // ошибка, когда по переданному email уже существует аккаунт
|
|
||||||
//
|
|
||||||
// // какие-то дальнейшие действия после получения ошибки, например, закрываем окно и открываем модалку с вводом пароля
|
|
||||||
// handleCancel();
|
|
||||||
// openSocialPasswordModal(socialData);
|
|
||||||
// } else {
|
|
||||||
// // в остальных случаях записываем ошибку в массив ошибок
|
|
||||||
// socialErrors.push(error.toString());
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // если все успешно, закрываем окно
|
|
||||||
// handleCancel();
|
|
||||||
})
|
})
|
||||||
};
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const err = error?.message ? JSON.parse(error.message) : {};
|
||||||
|
notification.error({
|
||||||
|
message: 'Error',
|
||||||
|
description: err?.details?.errMessage || undefined
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -177,21 +166,24 @@ export const RegisterContent: FC<RegisterProps> = ({
|
||||||
</FilledButton>
|
</FilledButton>
|
||||||
<OutlinedButton onClick={() => updateMode('enter')}>{i18nText('enter', locale)}</OutlinedButton>
|
<OutlinedButton onClick={() => updateMode('enter')}>{i18nText('enter', locale)}</OutlinedButton>
|
||||||
<span>{i18nText('or', locale)}</span>
|
<span>{i18nText('or', locale)}</span>
|
||||||
<OutlinedButton
|
<AppleLogin
|
||||||
icon={<Image src="/images/facebook-logo.png" height={20} width={20} alt="" />}
|
clientId="bbuddy.expert"
|
||||||
onClick={() => onSocialRegister(Social.FACEBOOK)}
|
redirectURI="https://bbuddy.expert"
|
||||||
>
|
state="bbregister"
|
||||||
{i18nText('facebook', locale)}
|
responseType="code"
|
||||||
</OutlinedButton>
|
responseMode="query"
|
||||||
|
render={({ onClick }) => (
|
||||||
<OutlinedButton
|
<OutlinedButton
|
||||||
icon={<Image src="/images/apple-logo.png" height={22} width={22} alt="" />}
|
icon={<Image src="/images/apple-logo.png" height={22} width={22} alt="" />}
|
||||||
onClick={() => onSocialRegister(Social.APPLE)}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
{i18nText('apple', locale)}
|
{i18nText('apple', locale)}
|
||||||
</OutlinedButton>
|
</OutlinedButton>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<OutlinedButton
|
<OutlinedButton
|
||||||
icon={<Image src="/images/google-logo.png" height={20} width={20} alt="" />}
|
icon={<Image src="/images/google-logo.png" height={20} width={20} alt="" />}
|
||||||
onClick={() => onSocialRegister(Social.GOOGLE)}
|
onClick={onGoogleLogin}
|
||||||
>
|
>
|
||||||
{i18nText('google', locale)}
|
{i18nText('google', locale)}
|
||||||
</OutlinedButton>
|
</OutlinedButton>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { Link as IntlLink } from '../../../navigation';
|
import { Link as IntlLink } from '../../../i18n/routing';
|
||||||
import { i18nText } from '../../../i18nKeys';
|
import { i18nText } from '../../../i18nKeys';
|
||||||
|
|
||||||
export const Footer = ({ locale }: { locale: string }) => {
|
export const Footer = ({ locale }: { locale: string }) => {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React, { FC, useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Button } from 'antd';
|
import { Button } from 'antd';
|
||||||
import { useSelectedLayoutSegment } from 'next/navigation';
|
import { useSelectedLayoutSegment } from 'next/navigation';
|
||||||
import { Link } from '../../../navigation';
|
import { Link } from '../../../i18n/routing';
|
||||||
import { AUTH_TOKEN_KEY } from '../../../constants/common';
|
import { AUTH_TOKEN_KEY } from '../../../constants/common';
|
||||||
import { useLocalStorage } from '../../../hooks/useLocalStorage';
|
import { useLocalStorage } from '../../../hooks/useLocalStorage';
|
||||||
import { AuthModal } from '../../Modals/AuthModal';
|
import { AuthModal } from '../../Modals/AuthModal';
|
||||||
|
@ -23,6 +23,31 @@ function HeaderAuthLinks ({
|
||||||
const selectedLayoutSegment = useSelectedLayoutSegment();
|
const selectedLayoutSegment = useSelectedLayoutSegment();
|
||||||
const pathname = selectedLayoutSegment || '';
|
const pathname = selectedLayoutSegment || '';
|
||||||
const [token, setToken] = useLocalStorage(AUTH_TOKEN_KEY, '');
|
const [token, setToken] = useLocalStorage(AUTH_TOKEN_KEY, '');
|
||||||
|
const [isPayPath, setIsPayPath] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const onOpen = (mode: 'enter' | 'register' | 'reset' | 'finish') => {
|
||||||
|
setMode(mode);
|
||||||
|
setIsOpenModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAuthRegister = () => {
|
||||||
|
setIsPayPath(true);
|
||||||
|
onOpen('register');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAuthEnter = () => {
|
||||||
|
setIsPayPath(true);
|
||||||
|
onOpen('enter');
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener('show_auth_register', handleAuthRegister);
|
||||||
|
document.addEventListener('show_auth_enter', handleAuthEnter);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('show_auth_register', handleAuthRegister);
|
||||||
|
document.removeEventListener('show_auth_enter', handleAuthEnter);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpenModal) {
|
if (!isOpenModal) {
|
||||||
|
@ -30,15 +55,22 @@ function HeaderAuthLinks ({
|
||||||
}
|
}
|
||||||
}, [isOpenModal]);
|
}, [isOpenModal]);
|
||||||
|
|
||||||
const onOpen = (mode: 'enter' | 'register' | 'reset' | 'finish') => {
|
useEffect(() => {
|
||||||
setMode(mode);
|
if (token && isPayPath) {
|
||||||
setIsOpenModal(true);
|
const showPayForm = new Event('show_pay_form');
|
||||||
|
document.dispatchEvent(showPayForm);
|
||||||
|
}
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
|
const addNewEvent = (name: 'show_auth_register' | 'show_auth_enter') => {
|
||||||
|
const evt = new Event(name);
|
||||||
|
document.dispatchEvent(evt);
|
||||||
};
|
};
|
||||||
|
|
||||||
return token
|
return token
|
||||||
? (
|
? (
|
||||||
<li>
|
<li>
|
||||||
<Link href={'/account/sessions/upcoming' as any} className={pathname === 'account' ? 'active' : ''}>
|
<Link href={'/account/sessions' as any} className={pathname === 'account' ? 'active' : ''}>
|
||||||
{i18nText('account', locale)}
|
{i18nText('account', locale)}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
|
@ -49,7 +81,7 @@ function HeaderAuthLinks ({
|
||||||
<Button
|
<Button
|
||||||
className="b-header__auth"
|
className="b-header__auth"
|
||||||
type="link"
|
type="link"
|
||||||
onClick={() => onOpen('register')}
|
onClick={() => addNewEvent('show_auth_register')}
|
||||||
>
|
>
|
||||||
{i18nText('registration', locale)}
|
{i18nText('registration', locale)}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -61,7 +93,7 @@ function HeaderAuthLinks ({
|
||||||
<Button
|
<Button
|
||||||
className="b-header__auth"
|
className="b-header__auth"
|
||||||
type="link"
|
type="link"
|
||||||
onClick={() => onOpen('enter')}
|
onClick={() => addNewEvent('show_auth_enter')}
|
||||||
>
|
>
|
||||||
{i18nText('enter', locale)}
|
{i18nText('enter', locale)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useSelectedLayoutSegment } from 'next/navigation';
|
import { useSelectedLayoutSegment } from 'next/navigation';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import { Link } from '../../../navigation';
|
import { Link } from '../../../i18n/routing';
|
||||||
|
|
||||||
type HeaderMenuProps = {
|
type HeaderMenuProps = {
|
||||||
locale: string;
|
locale: string;
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import React, { FC, useState } from 'react';
|
import React, { FC, useState } from 'react';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import { useSelectedLayoutSegment } from 'next/navigation';
|
import { useSelectedLayoutSegment } from 'next/navigation';
|
||||||
import { Link } from '../../../navigation';
|
import { Link } from '../../../i18n/routing';
|
||||||
|
|
||||||
type HeaderMenuMobileProps = {
|
type HeaderMenuMobileProps = {
|
||||||
locale: string;
|
locale: string;
|
||||||
|
|
|
@ -4,7 +4,7 @@ import React, { useTransition, useState } from 'react';
|
||||||
import { Dropdown } from 'antd';
|
import { Dropdown } from 'antd';
|
||||||
import type { MenuProps } from 'antd';
|
import type { MenuProps } from 'antd';
|
||||||
import { CaretDownOutlined, CaretUpOutlined } from '@ant-design/icons';
|
import { CaretDownOutlined, CaretUpOutlined } from '@ant-design/icons';
|
||||||
import { useRouter, usePathname } from '../../../navigation';
|
import { useRouter, usePathname } from '../../../i18n/routing';
|
||||||
import { LOCALES } from '../../../constants/locale';
|
import { LOCALES } from '../../../constants/locale';
|
||||||
import { Locale } from '../../../types/locale';
|
import { Locale } from '../../../types/locale';
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { HeaderMenu } from './HeaderMenu';
|
||||||
import { LanguageSwitcher } from './LanguageSwitcher';
|
import { LanguageSwitcher } from './LanguageSwitcher';
|
||||||
import { HeaderMobileMenu } from './HeaderMobileMenu';
|
import { HeaderMobileMenu } from './HeaderMobileMenu';
|
||||||
import { HEAD_ROUTES } from '../../../constants/routes';
|
import { HEAD_ROUTES } from '../../../constants/routes';
|
||||||
import { Link } from '../../../navigation';
|
import { Link } from '../../../i18n/routing';
|
||||||
import { i18nText } from '../../../i18nKeys';
|
import { i18nText } from '../../../i18nKeys';
|
||||||
|
|
||||||
type HeaderProps = {
|
type HeaderProps = {
|
||||||
|
|
|
@ -0,0 +1,165 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { FC, useEffect, useState } from 'react';
|
||||||
|
import type { StripeError } from '@stripe/stripe-js';
|
||||||
|
import {
|
||||||
|
useStripe,
|
||||||
|
useElements,
|
||||||
|
PaymentElement,
|
||||||
|
Elements,
|
||||||
|
} from '@stripe/react-stripe-js';
|
||||||
|
import { Form, Button, message } from 'antd';
|
||||||
|
import getStripe from '../../utils/get-stripe';
|
||||||
|
import { createPaymentIntent} from '../../actions/stripe';
|
||||||
|
import { Payment } from '../../types/payment';
|
||||||
|
import { i18nText } from '../../i18nKeys';
|
||||||
|
import { WithError } from '../view/WithError';
|
||||||
|
|
||||||
|
type PaymentFormProps = {
|
||||||
|
amount: number,
|
||||||
|
sessionId?: string,
|
||||||
|
locale: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaymentInfo = 'initial' | 'error' | 'processing' | 'requires_payment_method' | 'requires_confirmation' | 'requires_action' | 'succeeded';
|
||||||
|
|
||||||
|
const PaymentStatus = ({ status }: { status?: PaymentInfo }) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'processing':
|
||||||
|
case 'requires_payment_method':
|
||||||
|
case 'requires_confirmation':
|
||||||
|
return <h2>Processing...</h2>;
|
||||||
|
|
||||||
|
case 'requires_action':
|
||||||
|
return <h2>Authenticating...</h2>;
|
||||||
|
|
||||||
|
case 'succeeded':
|
||||||
|
return <h2>Payment Succeeded</h2>;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CheckoutForm: FC<PaymentFormProps> = ({ amount, sessionId, locale }) => {
|
||||||
|
const [form] = Form.useForm<Payment>();
|
||||||
|
const formAmount = Form.useWatch('amount', form);
|
||||||
|
const [paymentType, setPaymentType] = useState<string>('');
|
||||||
|
const [payment, setPayment] = useState<{
|
||||||
|
status: PaymentInfo
|
||||||
|
}>({ status: 'initial' });
|
||||||
|
const [errorData, setErrorData] = useState<any>();
|
||||||
|
const stripe = useStripe();
|
||||||
|
const elements = useElements();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
elements?.update({ amount: formAmount * 100 });
|
||||||
|
}, [formAmount]);
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
try {
|
||||||
|
if (!elements || !stripe) return;
|
||||||
|
|
||||||
|
setErrorData(undefined);
|
||||||
|
setPayment({ status: "processing" });
|
||||||
|
|
||||||
|
const { error: submitError } = await elements.submit();
|
||||||
|
|
||||||
|
if (submitError) {
|
||||||
|
if (submitError.message) {
|
||||||
|
message.error(submitError.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { client_secret: clientSecret } = await createPaymentIntent(
|
||||||
|
{ amount, sessionId }
|
||||||
|
);
|
||||||
|
|
||||||
|
const { error: confirmError } = await stripe!.confirmPayment({
|
||||||
|
elements,
|
||||||
|
clientSecret,
|
||||||
|
confirmParams: {
|
||||||
|
return_url: window?.location?.href || '',
|
||||||
|
payment_method_data: {
|
||||||
|
allow_redisplay: 'limited',
|
||||||
|
// billing_details: {
|
||||||
|
// name: input.cardholderName,
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (confirmError) {
|
||||||
|
setErrorData({
|
||||||
|
title: i18nText('errorPayment', locale),
|
||||||
|
message: confirmError.message ?? 'An unknown error occurred'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const { message } = err as StripeError;
|
||||||
|
setErrorData({
|
||||||
|
title: i18nText('errorPayment', locale),
|
||||||
|
message: message ?? 'An unknown error occurred'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WithError errorData={errorData}>
|
||||||
|
<Form form={form} onFinish={onSubmit} style={{ display: 'flex', overflow: 'hidden', flexDirection: 'column', gap: 16, justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<div style={{ width: '100%' }}>
|
||||||
|
<PaymentElement
|
||||||
|
onChange={(e) => {
|
||||||
|
setPaymentType(e.value.type);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<PaymentStatus status={payment.status}/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
className="btn-apply"
|
||||||
|
htmlType="submit"
|
||||||
|
disabled={
|
||||||
|
!["initial", "succeeded", "error"].includes(payment.status) ||
|
||||||
|
!stripe
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{`${i18nText('pay', locale)} ${amount}€`}
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</WithError>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StripeElementsForm: FC<PaymentFormProps> = ({ amount, sessionId, locale }) => {
|
||||||
|
return (
|
||||||
|
<Elements
|
||||||
|
stripe={getStripe()}
|
||||||
|
options={{
|
||||||
|
fonts: [{
|
||||||
|
cssSrc: 'https://fonts.googleapis.com/css2?family=Comfortaa&display=swap',
|
||||||
|
}],
|
||||||
|
appearance: {
|
||||||
|
variables: {
|
||||||
|
colorIcon: '#2c7873',
|
||||||
|
fontSizeBase: '16px',
|
||||||
|
colorPrimary: '#66A5AD',
|
||||||
|
colorBackground: '#F8F8F7',
|
||||||
|
colorText: '#000',
|
||||||
|
colorDanger: '#ff4d4f',
|
||||||
|
focusBoxShadow: 'none',
|
||||||
|
borderRadius: '8px'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
currency: 'eur',
|
||||||
|
mode: "payment",
|
||||||
|
amount: amount*100,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CheckoutForm amount={amount} sessionId={sessionId} locale={locale} />
|
||||||
|
</Elements>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,60 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { DatePicker } from 'antd';
|
||||||
|
import { CalendarOutlined } from '@ant-design/icons';
|
||||||
|
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 { getLocale } from '../../utils/locale';
|
||||||
|
|
||||||
|
export const CustomDatePicker = (props: any) => {
|
||||||
|
const { label, value, locale, ...other } = props;
|
||||||
|
const [isActiveLabel, setIsActiveLabel] = useState<boolean>(false);
|
||||||
|
|
||||||
|
dayjs.locale(locale);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (label) {
|
||||||
|
setIsActiveLabel(!!value);
|
||||||
|
} else {
|
||||||
|
setIsActiveLabel(false);
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const onOpenChange = (open: boolean) => {
|
||||||
|
if (open) {
|
||||||
|
if (!isActiveLabel) setIsActiveLabel(true)
|
||||||
|
} else {
|
||||||
|
setIsActiveLabel(!!value)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`b-datepicker-wrap ${isActiveLabel ? 'b-datepicker__active' : ''}`}>
|
||||||
|
<div className="b-datepicker-label">
|
||||||
|
<span>{label}</span>
|
||||||
|
</div>
|
||||||
|
<DatePicker
|
||||||
|
className="b-datepicker"
|
||||||
|
format="YYYY-MM-DD"
|
||||||
|
locale={getLocale(locale)}
|
||||||
|
value={value}
|
||||||
|
showNow={false}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
needConfirm={false}
|
||||||
|
placeholder=""
|
||||||
|
variant="filled"
|
||||||
|
allowClear={false}
|
||||||
|
popupClassName="b-datepicker-popup"
|
||||||
|
minDate={dayjs().startOf('month')}
|
||||||
|
suffixIcon={<CalendarOutlined style={{ color: '#2c7873', fontSize: 20 }} />}
|
||||||
|
{...other}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -16,8 +16,8 @@ export const WithError: FC<WithErrorProps> = ({
|
||||||
return (
|
return (
|
||||||
<Result
|
<Result
|
||||||
status="error"
|
status="error"
|
||||||
title="Submission Failed"
|
title={errorData?.title}
|
||||||
subTitle="Please check and modify the following information before resubmitting."
|
subTitle={errorData?.message}
|
||||||
extra={refresh ? (
|
extra={refresh ? (
|
||||||
<Button type="primary" onClick={refresh}>
|
<Button type="primary" onClick={refresh}>
|
||||||
Refresh page
|
Refresh page
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
export const BASE_URL = process.env.NEXT_PUBLIC_SERVER_BASE_URL || 'https://api.bbuddy.expert/api';
|
export const BASE_URL = process.env.NEXT_PUBLIC_SERVER_BASE_URL || 'https://api.bbuddy.expert/api';
|
||||||
export const AUTH_TOKEN_KEY = 'bbuddy_token';
|
export const AUTH_TOKEN_KEY = 'bbuddy_token';
|
||||||
export const AUTH_USER = 'bbuddy_auth_user';
|
export const AUTH_USER = 'bbuddy_auth_user';
|
||||||
|
export const SESSION_DATA = 'bbuddy_session_data';
|
||||||
|
|
||||||
export const DEFAULT_PAGE_SIZE = 5;
|
export const DEFAULT_PAGE_SIZE = 5;
|
||||||
export const DEFAULT_PAGE = 1;
|
export const DEFAULT_PAGE = 1;
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { Locale } from '../types/locale';
|
||||||
|
|
||||||
export const DEFAULT_LOCALE = Locale.en;
|
export const DEFAULT_LOCALE = Locale.en;
|
||||||
export const ALLOWED_LOCALES = [Locale.en, Locale.ru, Locale.de, Locale.it, Locale.es, Locale.fr] as const;
|
export const ALLOWED_LOCALES = [Locale.en, Locale.ru, Locale.de, Locale.it, Locale.es, Locale.fr] as const;
|
||||||
export const LOCALE_PREFIX = undefined;
|
|
||||||
|
|
||||||
export const LOCALES = {
|
export const LOCALES = {
|
||||||
[Locale.en]: 'En',
|
[Locale.en]: 'En',
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
function getStorageValue (key: string, defaultValue: any) {
|
export function getStorageValue (key: string, defaultValue: any) {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
const saved = localStorage.getItem(key);
|
const saved = localStorage?.getItem(key);
|
||||||
return saved || defaultValue;
|
return saved || defaultValue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export function deleteStorageKey (key: string) {
|
export function deleteStorageKey (key: string) {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
localStorage.removeItem(key);
|
localStorage?.removeItem(key);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ export const useLocalStorage = (key: string, defaultValue: any) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem(key, value);
|
localStorage?.setItem(key, value);
|
||||||
}, [key, value]);
|
}, [key, value]);
|
||||||
|
|
||||||
return [value, setValue];
|
return [value, setValue];
|
||||||
|
|
|
@ -14,21 +14,21 @@ export const useOauthWindow = () => {
|
||||||
|
|
||||||
if (data.messageType === 'oAuth') {
|
if (data.messageType === 'oAuth') {
|
||||||
messageHandler(event);
|
messageHandler(event);
|
||||||
window.removeEventListener('message', handler);
|
window?.removeEventListener('message', handler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.removeEventListener('message', handler);
|
window?.removeEventListener('message', handler);
|
||||||
|
|
||||||
if (!oauthWindow || oauthWindow.closed) {
|
if (!oauthWindow || oauthWindow.closed) {
|
||||||
// окно ещё не существует, либо было закрыто
|
// окно ещё не существует, либо было закрыто
|
||||||
oauthWindow = window.open(url, name, params)!;
|
oauthWindow = window?.open(url, name, params)!;
|
||||||
} else {
|
} else {
|
||||||
// окно уже существует
|
// окно уже существует
|
||||||
oauthWindow!.focus();
|
oauthWindow!.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('message', handler);
|
window?.addEventListener('message', handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
10
src/i18n.ts
10
src/i18n.ts
|
@ -1,10 +0,0 @@
|
||||||
import { getRequestConfig } from 'next-intl/server';
|
|
||||||
import { Locale } from './types/locale';
|
|
||||||
|
|
||||||
export default getRequestConfig(async ({ locale }) => ({
|
|
||||||
messages: (
|
|
||||||
await (locale === Locale.en
|
|
||||||
? import('../messages/en.json')
|
|
||||||
: import(`../messages/${locale}.json`))
|
|
||||||
).default
|
|
||||||
}));
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { getRequestConfig } from 'next-intl/server';
|
||||||
|
import { routing } from './routing';
|
||||||
|
|
||||||
|
export default getRequestConfig(async ({ requestLocale }) => {
|
||||||
|
let locale = await requestLocale;
|
||||||
|
|
||||||
|
if (!locale || !routing.locales.includes(locale as any)) {
|
||||||
|
locale = routing.defaultLocale;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
locale,
|
||||||
|
messages: (await import(`../../messages/${locale}.json`)).default
|
||||||
|
};
|
||||||
|
});
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { defineRouting } from 'next-intl/routing';
|
||||||
|
import { createNavigation } from 'next-intl/navigation';
|
||||||
|
import { ALLOWED_LOCALES, DEFAULT_LOCALE } from '../constants/locale';
|
||||||
|
|
||||||
|
export const routing = defineRouting({
|
||||||
|
locales: ALLOWED_LOCALES,
|
||||||
|
defaultLocale: DEFAULT_LOCALE
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { Link, redirect, usePathname, useRouter, getPathname } = createNavigation(routing);
|
|
@ -1,6 +1,7 @@
|
||||||
export default {
|
export default {
|
||||||
accountMenu: {
|
accountMenu: {
|
||||||
sessions: 'Kommende & letzte Sitzungen',
|
sessions: 'Kommende & letzte Sitzungen',
|
||||||
|
rooms: 'Zimmer',
|
||||||
notifications: 'Benachrichtigung',
|
notifications: 'Benachrichtigung',
|
||||||
support: 'Hilfe & Support',
|
support: 'Hilfe & Support',
|
||||||
information: 'Rechtliche Informationen',
|
information: 'Rechtliche Informationen',
|
||||||
|
@ -42,13 +43,47 @@ export default {
|
||||||
addComment: 'Neuen Kommentar hinzufügen',
|
addComment: 'Neuen Kommentar hinzufügen',
|
||||||
commentPlaceholder: 'Ihr Kommentar',
|
commentPlaceholder: 'Ihr Kommentar',
|
||||||
clientComments: 'Kundenkommentare',
|
clientComments: 'Kundenkommentare',
|
||||||
coachComments: 'Trainerkommentare'
|
coachComments: 'Expertenkommentare'
|
||||||
},
|
},
|
||||||
room: {
|
room: {
|
||||||
upcoming: 'Zukünftige Räume',
|
upcoming: 'Zukünftige Räume',
|
||||||
requested: 'Angeforderte Räume',
|
requested: 'Angeforderte Räume',
|
||||||
recent: 'Kürzliche Räume',
|
recent: 'Kürzliche Räume',
|
||||||
newRoom: 'Neuer Raum'
|
newRoom: 'Neuer Raum',
|
||||||
|
editRoom: 'Raum bearbeiten',
|
||||||
|
date: 'Datum',
|
||||||
|
time: 'Zeit',
|
||||||
|
maxParticipants: 'Max. erlaubte Teilnehmer',
|
||||||
|
presenceOfSupervisor: 'Anwesenheit eines Supervisors',
|
||||||
|
supervisor: 'Supervisor',
|
||||||
|
members: 'Mitglieder',
|
||||||
|
participants: 'Teilnehmer',
|
||||||
|
roomCreator: 'Raum-Ersteller',
|
||||||
|
inviteSupervisor: 'Supervisor einladen',
|
||||||
|
joinSupervisor: 'Als Supervisor beitreten',
|
||||||
|
inviteParticipant: 'Teilnehmer einladen',
|
||||||
|
joinParticipant: 'Als Teilnehmer beitreten',
|
||||||
|
rapport: 'Rapport',
|
||||||
|
invite: 'Invite',
|
||||||
|
save: 'Raum speichern',
|
||||||
|
rate: 'Bewerten',
|
||||||
|
tellAboutReason: 'Sag uns, was passiert ist',
|
||||||
|
rating_raport: 'Rapport',
|
||||||
|
rating_position_and_presence: 'Position oder Präsenz eines Coaches',
|
||||||
|
rating_balance_and_frustration: 'Balance zwischen Unterstützung und Frustration',
|
||||||
|
rating_agreement: 'Erstellung einer Coaching-Vereinbarung (Sitzungsvertrag)',
|
||||||
|
rating_planning_and_goals: 'Planung und Zielsetzung',
|
||||||
|
rating_reality: 'Klärung der Realität',
|
||||||
|
rating_opportunities: 'Neue Möglichkeiten gefunden',
|
||||||
|
rating_action_plan: 'Es wurde ein Aktionsplan erstellt',
|
||||||
|
rating_motivation: 'Motivationsquellen gefunden',
|
||||||
|
rating_next_session_stretch: 'Es ist noch Zeit bis zur nächsten Sitzung',
|
||||||
|
rating_relationship: 'Aufbau einer vertrauensvollen Beziehung zum Klienten',
|
||||||
|
rating_listening: 'Tiefes, aktives Zuhören',
|
||||||
|
rating_questions: 'Verwendung „starker“ Fragen',
|
||||||
|
rating_communication: 'Direkte Kommunikation',
|
||||||
|
rating_awareness: 'Entwicklung und Stimulierung des Bewusstseins',
|
||||||
|
rating_progress: 'Fortschritts- und Verantwortungsmanagement'
|
||||||
},
|
},
|
||||||
agreementText: 'Folgendes habe ich gelesen und erkläre mich damit einverstanden: Benutzervereinbarung,',
|
agreementText: 'Folgendes habe ich gelesen und erkläre mich damit einverstanden: Benutzervereinbarung,',
|
||||||
userAgreement: 'Benutzervereinbarung',
|
userAgreement: 'Benutzervereinbarung',
|
||||||
|
@ -110,9 +145,9 @@ export default {
|
||||||
seminars: 'Seminare',
|
seminars: 'Seminare',
|
||||||
courses: 'Kurse',
|
courses: 'Kurse',
|
||||||
mba: 'MBA-Information',
|
mba: 'MBA-Information',
|
||||||
aboutCoach: 'Über Coach',
|
aboutCoach: 'Über den Experten',
|
||||||
education: 'Bildung',
|
education: 'Bildung',
|
||||||
coaching: 'Coaching',
|
coaching: 'Expertenprofil',
|
||||||
experiences: 'Praktische Erfahrung',
|
experiences: 'Praktische Erfahrung',
|
||||||
payInfo: 'Zahlungsdaten',
|
payInfo: 'Zahlungsdaten',
|
||||||
sessionDuration: 'Sitzungsdauer',
|
sessionDuration: 'Sitzungsdauer',
|
||||||
|
@ -146,6 +181,14 @@ export default {
|
||||||
saturday: 'Sa',
|
saturday: 'Sa',
|
||||||
addNew: 'Neu hinzufügen',
|
addNew: 'Neu hinzufügen',
|
||||||
mExperiences: 'Führungserfahrung',
|
mExperiences: 'Führungserfahrung',
|
||||||
|
pay: 'Zahlung',
|
||||||
|
sessionWishes: 'Schreiben Sie Ihre Wünsche zur Sitzung',
|
||||||
|
successPayment: 'Erfolgreiche Zahlung',
|
||||||
|
errorPayment: 'Zahlungsfehler',
|
||||||
|
chat: {
|
||||||
|
join: 'Chat starten',
|
||||||
|
joinAI: 'Start AI chat'
|
||||||
|
},
|
||||||
errors: {
|
errors: {
|
||||||
invalidEmail: 'Die E-Mail-Adresse ist ungültig',
|
invalidEmail: 'Die E-Mail-Adresse ist ungültig',
|
||||||
emptyEmail: 'Bitte geben Sie Ihre E-Mail ein',
|
emptyEmail: 'Bitte geben Sie Ihre E-Mail ein',
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
export default {
|
export default {
|
||||||
accountMenu: {
|
accountMenu: {
|
||||||
sessions: 'Upcoming & Recent Sessions',
|
sessions: 'Upcoming & Recent Sessions',
|
||||||
|
rooms: 'Rooms',
|
||||||
notifications: 'Notification',
|
notifications: 'Notification',
|
||||||
support: 'Help & Support',
|
support: 'Help & Support',
|
||||||
information: 'Legal Information',
|
information: 'Legal Information',
|
||||||
|
@ -42,13 +43,47 @@ export default {
|
||||||
addComment: 'Add new',
|
addComment: 'Add new',
|
||||||
commentPlaceholder: 'Your comment',
|
commentPlaceholder: 'Your comment',
|
||||||
clientComments: 'Client Comments',
|
clientComments: 'Client Comments',
|
||||||
coachComments: 'Coach Comments'
|
coachComments: 'Expert Comments'
|
||||||
},
|
},
|
||||||
room: {
|
room: {
|
||||||
upcoming: 'Upcoming Rooms',
|
upcoming: 'Upcoming Rooms',
|
||||||
requested: 'Rooms Requested',
|
requested: 'Rooms Requested',
|
||||||
recent: 'Recent Rooms',
|
recent: 'Recent Rooms',
|
||||||
newRoom: 'New Room'
|
newRoom: 'New Room',
|
||||||
|
editRoom: 'Edit Room',
|
||||||
|
date: 'Date',
|
||||||
|
time: 'Time',
|
||||||
|
maxParticipants: 'Max Participants Allowed',
|
||||||
|
presenceOfSupervisor: 'Presence of a Supervisor',
|
||||||
|
supervisor: 'Supervisor',
|
||||||
|
members: 'Members',
|
||||||
|
participants: 'Participants',
|
||||||
|
roomCreator: 'Room Creator',
|
||||||
|
inviteSupervisor: 'Invite Supervisor',
|
||||||
|
joinSupervisor: 'Join As A Supervisor',
|
||||||
|
inviteParticipant: 'Invite Participant',
|
||||||
|
joinParticipant: 'Join as a participant',
|
||||||
|
rapport: 'Rapport',
|
||||||
|
invite: 'Invite',
|
||||||
|
save: 'Save room',
|
||||||
|
rate: 'Rate',
|
||||||
|
tellAboutReason: 'Tell us what happened',
|
||||||
|
rating_raport: 'Rapport',
|
||||||
|
rating_position_and_presence: 'Coaching position or coaching presence',
|
||||||
|
rating_balance_and_frustration: 'Balance of support and frustration',
|
||||||
|
rating_agreement: 'Creating a coaching agreement (session contract)',
|
||||||
|
rating_planning_and_goals: 'Planning and goal setting',
|
||||||
|
rating_reality: 'Clarifying reality',
|
||||||
|
rating_opportunities: 'New opportunities found',
|
||||||
|
rating_action_plan: 'An action plan has been drawn up',
|
||||||
|
rating_motivation: 'Sources of motivation found',
|
||||||
|
rating_next_session_stretch: 'There is a stretch for the next session',
|
||||||
|
rating_relationship: 'Establishing a trusting relationship with the client',
|
||||||
|
rating_listening: 'Deep, active listening',
|
||||||
|
rating_questions: 'Using "strong" questions',
|
||||||
|
rating_communication: 'Direct communication',
|
||||||
|
rating_awareness: 'Developing and stimulating awareness',
|
||||||
|
rating_progress: 'Progress and Responsibility Management'
|
||||||
},
|
},
|
||||||
agreementText: 'I have read and agree with the terms of the User Agreement,',
|
agreementText: 'I have read and agree with the terms of the User Agreement,',
|
||||||
userAgreement: 'User Agreement',
|
userAgreement: 'User Agreement',
|
||||||
|
@ -109,10 +144,10 @@ export default {
|
||||||
seminars: 'Seminars',
|
seminars: 'Seminars',
|
||||||
courses: 'Courses',
|
courses: 'Courses',
|
||||||
mba: 'MBA Information',
|
mba: 'MBA Information',
|
||||||
aboutCoach: 'About Coach',
|
aboutCoach: 'About Expert',
|
||||||
skillsInfo: 'Skills Info',
|
skillsInfo: 'Skills Info',
|
||||||
education: 'Education',
|
education: 'Education',
|
||||||
coaching: 'Coaching',
|
coaching: 'Expert profile',
|
||||||
experiences: 'Practical experience',
|
experiences: 'Practical experience',
|
||||||
payInfo: 'Payment Info',
|
payInfo: 'Payment Info',
|
||||||
sessionDuration: 'Session duration',
|
sessionDuration: 'Session duration',
|
||||||
|
@ -146,6 +181,14 @@ export default {
|
||||||
saturday: 'Sa',
|
saturday: 'Sa',
|
||||||
addNew: 'Add New',
|
addNew: 'Add New',
|
||||||
mExperiences: 'Managerial Experience',
|
mExperiences: 'Managerial Experience',
|
||||||
|
pay: 'Pay',
|
||||||
|
sessionWishes: 'Write your wishes about the session',
|
||||||
|
successPayment: 'Successful Payment',
|
||||||
|
errorPayment: 'Payment Error',
|
||||||
|
chat: {
|
||||||
|
join: 'Start chat',
|
||||||
|
joinAI: 'Start AI chat'
|
||||||
|
},
|
||||||
errors: {
|
errors: {
|
||||||
invalidEmail: 'The email address is not valid',
|
invalidEmail: 'The email address is not valid',
|
||||||
emptyEmail: 'Please enter your E-mail',
|
emptyEmail: 'Please enter your E-mail',
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
export default {
|
export default {
|
||||||
accountMenu: {
|
accountMenu: {
|
||||||
sessions: 'Próximas y recientes sesiones',
|
sessions: 'Próximas y recientes sesiones',
|
||||||
|
rooms: 'Habitaciones',
|
||||||
notifications: 'Notificación',
|
notifications: 'Notificación',
|
||||||
support: 'Ayuda y asistencia',
|
support: 'Ayuda y asistencia',
|
||||||
information: 'Información jurídica',
|
information: 'Información jurídica',
|
||||||
|
@ -42,13 +43,47 @@ export default {
|
||||||
addComment: 'Añadir nuevo comentario',
|
addComment: 'Añadir nuevo comentario',
|
||||||
commentPlaceholder: 'Tu comentario',
|
commentPlaceholder: 'Tu comentario',
|
||||||
clientComments: 'Comentarios del cliente',
|
clientComments: 'Comentarios del cliente',
|
||||||
coachComments: 'Comentarios del entrenador'
|
coachComments: 'Comentarios del experto'
|
||||||
},
|
},
|
||||||
room: {
|
room: {
|
||||||
upcoming: 'Próximas salas',
|
upcoming: 'Próximas salas',
|
||||||
requested: 'Salas solicitadas',
|
requested: 'Salas solicitadas',
|
||||||
recent: 'Salas recientes',
|
recent: 'Salas recientes',
|
||||||
newRoom: 'Nueva sala'
|
newRoom: 'Nueva sala',
|
||||||
|
editRoom: 'Editar la sala',
|
||||||
|
date: 'Fecha',
|
||||||
|
time: 'Tiempo',
|
||||||
|
maxParticipants: 'Máximo de participantes permitidos',
|
||||||
|
presenceOfSupervisor: 'Presencia de un supervisor',
|
||||||
|
supervisor: 'Supervisor',
|
||||||
|
members: 'Miembros',
|
||||||
|
participants: 'Participantes',
|
||||||
|
roomCreator: 'Creador de salas',
|
||||||
|
inviteSupervisor: 'Invitar al supervisor',
|
||||||
|
joinSupervisor: 'Unirse como supervisor',
|
||||||
|
inviteParticipant: 'Invitar a un participante',
|
||||||
|
joinParticipant: 'Unirse como participante',
|
||||||
|
rapport: 'Buena relación',
|
||||||
|
invite: 'Invitar',
|
||||||
|
save: 'Guardar sala',
|
||||||
|
rate: 'Valorar',
|
||||||
|
tellAboutReason: 'Cuéntanos qué ha pasado',
|
||||||
|
rating_raport: 'Buena relación',
|
||||||
|
rating_position_and_presence: 'Puesto de coach o presencia de coach',
|
||||||
|
rating_balance_and_frustration: 'Equilibrio entre apoyo y frustración',
|
||||||
|
rating_agreement: 'Crear un acuerdo de coaching (contrato de sesión)',
|
||||||
|
rating_planning_and_goals: 'Planear y establecer los objetivos',
|
||||||
|
rating_reality: 'Clarificar la realidad',
|
||||||
|
rating_opportunities: 'Nuevas oportunidades encontradas',
|
||||||
|
rating_action_plan: 'Se ha diseñado un plan de acción',
|
||||||
|
rating_motivation: 'Fuentes de motivación encontradas',
|
||||||
|
rating_next_session_stretch: 'Queda un poco para la siguiente sesión',
|
||||||
|
rating_relationship: 'Establecer una relación de confianza con el cliente',
|
||||||
|
rating_listening: 'Escucha activa y profunda',
|
||||||
|
rating_questions: 'Usar preguntas "contundentes"',
|
||||||
|
rating_communication: 'Comunicación directa',
|
||||||
|
rating_awareness: 'Desarrollar y estimular la conciencia',
|
||||||
|
rating_progress: 'Progreso y gestión de la responsabilidad'
|
||||||
},
|
},
|
||||||
agreementText: 'He leído y acepto las condiciones del Acuerdo de usuario,',
|
agreementText: 'He leído y acepto las condiciones del Acuerdo de usuario,',
|
||||||
userAgreement: 'Acuerdo de usuario',
|
userAgreement: 'Acuerdo de usuario',
|
||||||
|
@ -110,9 +145,9 @@ export default {
|
||||||
seminars: 'Seminarios',
|
seminars: 'Seminarios',
|
||||||
courses: 'Cursos',
|
courses: 'Cursos',
|
||||||
mba: 'Información sobre máster en ADE (MBA)',
|
mba: 'Información sobre máster en ADE (MBA)',
|
||||||
aboutCoach: 'Sobre el coach',
|
aboutCoach: 'Acerca del experto',
|
||||||
education: 'Educación',
|
education: 'Educación',
|
||||||
coaching: 'Coaching',
|
coaching: 'Perfil del experto',
|
||||||
experiences: 'Experiencia práctica',
|
experiences: 'Experiencia práctica',
|
||||||
payInfo: 'Información de pago',
|
payInfo: 'Información de pago',
|
||||||
sessionDuration: 'Duración de la sesión',
|
sessionDuration: 'Duración de la sesión',
|
||||||
|
@ -146,6 +181,14 @@ export default {
|
||||||
saturday: 'S',
|
saturday: 'S',
|
||||||
addNew: 'Añadir nuevo',
|
addNew: 'Añadir nuevo',
|
||||||
mExperiences: 'Experiencia de dirección',
|
mExperiences: 'Experiencia de dirección',
|
||||||
|
pay: 'Pago',
|
||||||
|
sessionWishes: 'Escribe tus deseos sobre la sesión',
|
||||||
|
successPayment: 'Pago Exitoso',
|
||||||
|
errorPayment: 'Error de Pago',
|
||||||
|
chat: {
|
||||||
|
join: 'Empezar un chat',
|
||||||
|
joinAI: 'Start AI chat'
|
||||||
|
},
|
||||||
errors: {
|
errors: {
|
||||||
invalidEmail: 'La dirección de correo electrónico no es válida',
|
invalidEmail: 'La dirección de correo electrónico no es válida',
|
||||||
emptyEmail: 'Introduce tu correo electrónico',
|
emptyEmail: 'Introduce tu correo electrónico',
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
export default {
|
export default {
|
||||||
accountMenu: {
|
accountMenu: {
|
||||||
sessions: 'Sessions futures et récentes',
|
sessions: 'Sessions futures et récentes',
|
||||||
|
rooms: 'Chambres',
|
||||||
notifications: 'Notification',
|
notifications: 'Notification',
|
||||||
support: 'Aide et support',
|
support: 'Aide et support',
|
||||||
information: 'Informations légales',
|
information: 'Informations légales',
|
||||||
|
@ -42,13 +43,47 @@ export default {
|
||||||
addComment: 'Ajouter un nouveau commentaire',
|
addComment: 'Ajouter un nouveau commentaire',
|
||||||
commentPlaceholder: 'Votre commentaire',
|
commentPlaceholder: 'Votre commentaire',
|
||||||
clientComments: 'Commentaires du client',
|
clientComments: 'Commentaires du client',
|
||||||
coachComments: 'Commentaires du coach'
|
coachComments: 'Commentaires de l\'expert'
|
||||||
},
|
},
|
||||||
room: {
|
room: {
|
||||||
upcoming: 'Salles futures',
|
upcoming: 'Salles futures',
|
||||||
requested: 'Salles demandées',
|
requested: 'Salles demandées',
|
||||||
recent: 'Salles récentes',
|
recent: 'Salles récentes',
|
||||||
newRoom: 'Nouvelle salle'
|
newRoom: 'Nouvelle salle',
|
||||||
|
editRoom: 'Modifier la salle',
|
||||||
|
date: 'Date',
|
||||||
|
time: 'Temps',
|
||||||
|
maxParticipants: 'Max de participants autorisés',
|
||||||
|
presenceOfSupervisor: 'Présence d\'un superviseur',
|
||||||
|
supervisor: 'Superviseur',
|
||||||
|
members: 'Membres',
|
||||||
|
participants: 'Participants',
|
||||||
|
roomCreator: 'Créateur de la salle',
|
||||||
|
inviteSupervisor: 'Inviter un superviseur',
|
||||||
|
joinSupervisor: 'Rejoindre en tant que superviseur',
|
||||||
|
inviteParticipant: 'Inviter un participant',
|
||||||
|
joinParticipant: 'Rejoindre en tant que participant',
|
||||||
|
rapport: 'Rapport',
|
||||||
|
invite: 'Inviter',
|
||||||
|
save: 'Sauvegarder la salle',
|
||||||
|
rate: 'Noter',
|
||||||
|
tellAboutReason: 'Dites-nous ce qui s\'est passé',
|
||||||
|
rating_raport: 'Rapport',
|
||||||
|
rating_position_and_presence: 'Poste de coach ou présence de coach',
|
||||||
|
rating_balance_and_frustration: 'Équilibre entre assistance et frustration',
|
||||||
|
rating_agreement: 'Création d\'un contrat de coaching (contrat de séance)',
|
||||||
|
rating_planning_and_goals: 'Planification et définition des objectifs',
|
||||||
|
rating_reality: 'Clarification de la réalité',
|
||||||
|
rating_opportunities: 'Nouvelles opportunités trouvées',
|
||||||
|
rating_action_plan: 'Un plan d\'action a été établi',
|
||||||
|
rating_motivation: 'Sources de motivation trouvées',
|
||||||
|
rating_next_session_stretch: 'Une période est présente pour la prochaine session',
|
||||||
|
rating_relationship: 'Établissement d\'une relation de confiance avec le client',
|
||||||
|
rating_listening: 'Écoute approfondie et active',
|
||||||
|
rating_questions: 'Utilisation de questions «fortes»',
|
||||||
|
rating_communication: 'Communication directe',
|
||||||
|
rating_awareness: 'Développement et stimulation de la prise de conscience',
|
||||||
|
rating_progress: 'Gestion de la progression et de la responsabilité'
|
||||||
},
|
},
|
||||||
agreementText: 'J\'ai lu et j\'accepte les dispositions de l\'Accord Utilisateur et de la',
|
agreementText: 'J\'ai lu et j\'accepte les dispositions de l\'Accord Utilisateur et de la',
|
||||||
userAgreement: '',
|
userAgreement: '',
|
||||||
|
@ -110,9 +145,9 @@ export default {
|
||||||
seminars: 'Séminaires',
|
seminars: 'Séminaires',
|
||||||
courses: 'Cours',
|
courses: 'Cours',
|
||||||
mba: 'Infos Maîtrise en gestion',
|
mba: 'Infos Maîtrise en gestion',
|
||||||
aboutCoach: 'À propos du coach',
|
aboutCoach: 'À propos de l\'expert',
|
||||||
education: 'Éducation',
|
education: 'Éducation',
|
||||||
coaching: 'Coaching',
|
coaching: 'Profil de l\'expert',
|
||||||
experiences: 'Expérience pratique',
|
experiences: 'Expérience pratique',
|
||||||
payInfo: 'Infos sur le paiement',
|
payInfo: 'Infos sur le paiement',
|
||||||
sessionDuration: 'Durée de la session',
|
sessionDuration: 'Durée de la session',
|
||||||
|
@ -146,6 +181,14 @@ export default {
|
||||||
saturday: 'Sa',
|
saturday: 'Sa',
|
||||||
addNew: 'Ajouter un nouveau',
|
addNew: 'Ajouter un nouveau',
|
||||||
mExperiences: 'Expérience en gestion',
|
mExperiences: 'Expérience en gestion',
|
||||||
|
pay: 'Paiement',
|
||||||
|
sessionWishes: 'Écrivez vos souhaits concernant la session',
|
||||||
|
successPayment: 'Paiement Réussi',
|
||||||
|
errorPayment: 'Erreur de Paiement',
|
||||||
|
chat: {
|
||||||
|
join: 'Commencer la discussion',
|
||||||
|
joinAI: 'Start AI chat'
|
||||||
|
},
|
||||||
errors: {
|
errors: {
|
||||||
invalidEmail: 'L\'adresse e-mail n\'est pas valide',
|
invalidEmail: 'L\'adresse e-mail n\'est pas valide',
|
||||||
emptyEmail: 'Veuillez saisir votre e-mail',
|
emptyEmail: 'Veuillez saisir votre e-mail',
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue