Compare commits
	
		
			5 Commits
		
	
	
		
			dbd5eaa014
			...
			eff29677dc
		
	
	| Author | SHA1 | Date | 
|---|---|---|
|  | eff29677dc | |
|  | 2da77f7347 | |
|  | 87b14e8716 | |
|  | 52fba3a879 | |
|  | 61de5c81e7 | 
							
								
								
									
										1
									
								
								.env
								
								
								
								
							
							
						
						
									
										1
									
								
								.env
								
								
								
								
							|  | @ -7,3 +7,4 @@ STRIPE_PAYMENT_DESCRIPTION='BBuddy services' | |||
| NEXT_PUBLIC_CONTENTFUL_SPACE_ID = voxpxjq7y7vf | ||||
| NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN = s99GWKfpDKkNwiEJ3pN7US_tmqsGvDlaex-sOJwpzuc | ||||
| NEXT_PUBLIC_CONTENTFUL_PREVIEW_ACCESS_TOKEN = Z9WOKpLDbKNj7xVOmT_VXYNLH0AZwISFvQsq0PQlHfE | ||||
| 
 | ||||
|  |  | |||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -13,6 +13,7 @@ | |||
|     "@ant-design/icons": "^5.2.6", | ||||
|     "@ant-design/nextjs-registry": "^1.0.0", | ||||
|     "@contentful/rich-text-react-renderer": "^15.22.9", | ||||
|     "@microsoft/signalr": "^8.0.7", | ||||
|     "@stripe/react-stripe-js": "^2.7.3", | ||||
|     "@stripe/stripe-js": "^4.1.0", | ||||
|     "agora-rtc-react": "2.1.0", | ||||
|  | @ -28,6 +29,7 @@ | |||
|     "next-intl": "^3.3.1", | ||||
|     "react": "^18", | ||||
|     "react-dom": "^18", | ||||
|     "react-signalr": "^0.2.24", | ||||
|     "react-slick": "^0.29.0", | ||||
|     "react-stripe-js": "^1.1.5", | ||||
|     "slick-carousel": "^1.8.1", | ||||
|  |  | |||
|  | @ -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 | ||||
| }); | ||||
|  | @ -23,22 +23,22 @@ export const apiRequest = async <T = any, K = any>( | |||
| 
 | ||||
|         return response.data; | ||||
|     } catch (err) { | ||||
|         // const {
 | ||||
|         //     response: {
 | ||||
|         //         status: responseCode = null,
 | ||||
|         //         statusText = '',
 | ||||
|         //         data: { message = '', status: errorKey = '' } = {},
 | ||||
|         //     } = {},
 | ||||
|         //     code: statusCode = '',
 | ||||
|         // } = err as AxiosError;
 | ||||
|         //
 | ||||
|         // throw new Error(
 | ||||
|         //     JSON.stringify({
 | ||||
|         //         statusCode,
 | ||||
|         //         statusMessage: message || statusText,
 | ||||
|         //         responseCode,
 | ||||
|         //         errorKey,
 | ||||
|         //     }),
 | ||||
|         // );
 | ||||
|         const { | ||||
|             response: { | ||||
|                 status: responseCode = null, | ||||
|                 statusText = '', | ||||
|                 data, | ||||
|             } = {}, | ||||
|             code: statusCode = '', | ||||
|         } = err as AxiosError; | ||||
| 
 | ||||
|         throw new Error( | ||||
|             JSON.stringify({ | ||||
|                 statusCode, | ||||
|                 statusMessage: statusText, | ||||
|                 responseCode, | ||||
|                 details: data | ||||
|             }), | ||||
|         ); | ||||
|     } | ||||
| }; | ||||
|  |  | |||
|  | @ -1,8 +1,9 @@ | |||
| 'use client' | ||||
| import React from 'react'; | ||||
| import { unstable_setRequestLocale } from 'next-intl/server'; | ||||
| import { Link } from '../../../../../../navigation'; | ||||
| import { i18nText } from '../../../../../../i18nKeys'; | ||||
| 
 | ||||
| import {ChatMessages} from "../../../../../../components/Chat/ChatMessages"; | ||||
| /* | ||||
| export function generateStaticParams({ | ||||
|     params: { locale }, | ||||
| }: { params: { locale: string } }) { | ||||
|  | @ -15,56 +16,22 @@ export function generateStaticParams({ | |||
| 
 | ||||
|     return result; | ||||
| } | ||||
| * | ||||
|  */ | ||||
| 
 | ||||
| export default function Message({ params }: { params: { locale: string, textId: string } }) { | ||||
|     unstable_setRequestLocale(params.locale); | ||||
| export default function Message({ params: { locale, textId } }: { params: { locale: string, textId: string } }) { | ||||
| 
 | ||||
|     return ( | ||||
|         <> | ||||
|             <ol className="breadcrumb"> | ||||
|                 <li className="breadcrumb-item"> | ||||
|                     <Link href={'/account/messages' as any}> | ||||
|                         {i18nText('accountMenu.messages', params.locale)} | ||||
|                         {i18nText('accountMenu.messages', locale)} | ||||
|                     </Link> | ||||
|                 </li> | ||||
|                 <li className="breadcrumb-item active" aria-current="page">{`Person ${params.textId}`}</li> | ||||
|             </ol> | ||||
| 
 | ||||
|             <div className="b-message"> | ||||
|                 <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> | ||||
|             <ChatMessages locale={locale} groupId={parseInt(textId)} /> | ||||
|         </> | ||||
|     ); | ||||
| }; | ||||
|  |  | |||
|  | @ -1,12 +1,10 @@ | |||
| 'use client' | ||||
| import React, { Suspense } from 'react'; | ||||
| import { unstable_setRequestLocale } from 'next-intl/server'; | ||||
| import { Link } from '../../../../../navigation'; | ||||
| import { CustomInput } from '../../../../../components/view/CustomInput'; | ||||
| import { i18nText } from '../../../../../i18nKeys'; | ||||
| import {ChatList} from "../../../../../components/Chat/ChatList"; | ||||
| 
 | ||||
| export default function  Messages({ params: { locale } }: { params: { locale: string } }) { | ||||
|     unstable_setRequestLocale(locale); | ||||
| 
 | ||||
|     return ( | ||||
|         <> | ||||
|             <ol className="breadcrumb"> | ||||
|  | @ -15,74 +13,7 @@ export default function Messages({ params: { locale } }: { params: { locale: str | |||
|             <Suspense> | ||||
|                 <CustomInput placeholder={i18nText('name', locale)} /> | ||||
|             </Suspense> | ||||
|             <div className="messages-session"> | ||||
|                 <Link | ||||
|                     className="card-profile" | ||||
|                     href={'messages/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={'messages/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={'messages/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> | ||||
|             <ChatList  locale={locale}/> | ||||
|         </> | ||||
|     ); | ||||
| }; | ||||
|  |  | |||
|  | @ -4,21 +4,44 @@ import { Button } from 'antd'; | |||
| import { useSelectedLayoutSegment, usePathname } from 'next/navigation'; | ||||
| import { Link } from '../../navigation'; | ||||
| 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 { getMenuConfig } from '../../utils/account'; | ||||
| import {useEffect, useState} from "react"; | ||||
| import {getChatList} from "../../actions/chat/groups"; | ||||
| 
 | ||||
| export const AccountMenu = ({ locale }: { locale: string }) => { | ||||
|     const selectedLayoutSegment = useSelectedLayoutSegment(); | ||||
|     const pathname = selectedLayoutSegment || ''; | ||||
|     const paths = usePathname(); | ||||
|     const menu: { path: string, title: string, count?: number }[] = getMenuConfig(locale); | ||||
|     const [counts, setCounts] = useState<any>({}); | ||||
| 
 | ||||
|     const onLogout = () => { | ||||
|         deleteStorageKey(AUTH_TOKEN_KEY); | ||||
|         deleteStorageKey(AUTH_USER); | ||||
|         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 ( | ||||
|         <ul className="list-sidebar"> | ||||
|  | @ -26,8 +49,8 @@ export const AccountMenu = ({ locale }: { locale: string }) => { | |||
|                 <li key={path} className="list-sidebar__item"> | ||||
|                     <Link href={`/account/${path}` as any} className={path === pathname ? 'active' : ''}> | ||||
|                         {title} | ||||
|                         {count ? ( | ||||
|                             <span className="count">{count}</span> | ||||
|                         {getterCount(path, count) ? ( | ||||
|                             <span className="count">{getterCount(path, count)}</span> | ||||
|                         ) : null} | ||||
|                     </Link> | ||||
|                 </li> | ||||
|  |  | |||
|  | @ -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 "../../navigation"; | ||||
| 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,200 @@ | |||
| '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 { newMessage, joinChat, readMessages, addListener } = SignalrConnection(); | ||||
|     const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, ''); | ||||
|     //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); | ||||
| 
 | ||||
|                 }) | ||||
|                 .catch((e) => { | ||||
|                     console.log(e) | ||||
|                     message.error('Не удалось загрузить данные'); | ||||
|                 }) | ||||
|                 .finally(() => { | ||||
|                     setLoading(false); | ||||
|                 }) | ||||
|         } | ||||
|     },[jwt]) | ||||
| 
 | ||||
| 
 | ||||
|     const onConnected = (flag: boolean) =>{ | ||||
|         if (flag) { | ||||
|             joinChat(groupId) | ||||
|             readUreaded() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     const readUreaded = () => { | ||||
|         const msgs = [] as number[]; | ||||
|         messages.forEach((message: any) => { | ||||
|             if (!message.seen && message.sentByMe === false){ | ||||
|                 msgs.push(message.id) | ||||
|             } | ||||
|         }) | ||||
|         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([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(() => { | ||||
|         addListener('onConnected', onConnected) | ||||
|         addListener('ReceiveMessage', onReceiveMessage) | ||||
|         addListener('MessageWasRead', onOpponentRead) | ||||
|     }, [messages, me, setMessages]); | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|     const handleSendMessages = () => { | ||||
|         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> | ||||
| 
 | ||||
|     ) | ||||
| } | ||||
|  | @ -15,6 +15,9 @@ 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 '../../navigation'; | ||||
| import { useLocalStorage } from '../../hooks/useLocalStorage'; | ||||
| 
 | ||||
| type ExpertDetailsProps = { | ||||
|     expert: ExpertDetails; | ||||
|  | @ -32,8 +35,29 @@ export const ExpertCard: FC<ExpertDetailsProps> = ({ expert, locale, expertId }) | |||
|     const { publicCoachDetails } = expert || {}; | ||||
|     const [showSchedulerModal, setShowSchedulerModal] = useState<boolean>(false); | ||||
|     const [mode, setMode] = useState<'data' | 'time' | 'pay' | 'finish'>('data'); | ||||
|     const { publicCoachDetails: { tags = [], sessionCost = 0, sessionDuration = 0, coachLanguages = [] } } = expert || {}; | ||||
|     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(); | ||||
|     const router = useRouter(); | ||||
| 
 | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         document?.addEventListener('show_pay_form', handleShowPayForm); | ||||
| 
 | ||||
|         return () => { | ||||
|             closeConnection(); | ||||
|             document?.removeEventListener('show_pay_form', handleShowPayForm); | ||||
|         } | ||||
|     }, []); | ||||
| 
 | ||||
|     const handleJoinChat = (id?: number) => { | ||||
|         if (id) { | ||||
|             joinChatPerson(id).then((res: any) => { | ||||
|                 router.push(`/account/messages/${res.id}` as string); | ||||
|             }) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     const checkSession = (data?: SignupSessionData) => { | ||||
|         if (data?.startAtUtc && data?.tagId) { | ||||
|  | @ -44,7 +68,7 @@ export const ExpertCard: FC<ExpertDetailsProps> = ({ expert, locale, expertId }) | |||
|             } else { | ||||
|                 setShowSchedulerModal(false); | ||||
|                 const showAuth = new Event('show_auth_enter'); | ||||
|                 document.dispatchEvent(showAuth); | ||||
|                 document?.dispatchEvent(showAuth); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | @ -54,13 +78,6 @@ export const ExpertCard: FC<ExpertDetailsProps> = ({ expert, locale, expertId }) | |||
|         setMode('pay'); | ||||
|     } | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         document.addEventListener('show_pay_form', handleShowPayForm); | ||||
|         return () => { | ||||
|             document.removeEventListener('show_pay_form', handleShowPayForm); | ||||
|         }; | ||||
|     }, []); | ||||
| 
 | ||||
|     const onSchedulerHandle = () => { | ||||
|         setMode('data'); | ||||
|         setShowSchedulerModal(true); | ||||
|  | @ -86,10 +103,17 @@ export const ExpertCard: FC<ExpertDetailsProps> = ({ expert, locale, expertId }) | |||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 {jwt && ( | ||||
|                     <div className="expert-card__wrap-btn"> | ||||
|                     <Button className="btn-apply" onClick={onSchedulerHandle}> | ||||
|                         <img src="/images/calendar-outline.svg" className="" alt="" /> | ||||
|                         {i18nText('schedule', locale)} | ||||
|                         <Button className="btn-apply" onClick={() => handleJoinChat(id)}> | ||||
|                             {i18nText('chat.join', locale)} | ||||
|                         </Button> | ||||
|                         <Button | ||||
|                             className={`btn-apply${!botUserId ? ' btn-disabled' : ''}`} | ||||
|                             disabled={!botUserId} | ||||
|                             onClick={botUserId ? () => handleJoinChat(botUserId) : undefined} | ||||
|                         > | ||||
|                             {i18nText('chat.joinAI', locale)} | ||||
|                         </Button> | ||||
|                         {/* | ||||
|                             <a href="#" className="btn-video"> | ||||
|  | @ -98,6 +122,7 @@ export const ExpertCard: FC<ExpertDetailsProps> = ({ expert, locale, expertId }) | |||
|                             </a> | ||||
|                         */} | ||||
|                     </div> | ||||
|                 )} | ||||
|             </div> | ||||
|             <div className="expert-info"> | ||||
|                 {/* <h2 className="title-h2">{}</h2> */} | ||||
|  |  | |||
|  | @ -185,6 +185,10 @@ export default { | |||
|     sessionWishes: 'Schreiben Sie Ihre Wünsche zur Sitzung', | ||||
|     successPayment: 'Erfolgreiche Zahlung', | ||||
|     errorPayment: 'Zahlungsfehler', | ||||
|     chat: { | ||||
|         join: 'Chat starten', | ||||
|         joinAI: 'Start AI chat' | ||||
|     }, | ||||
|     errors: { | ||||
|         invalidEmail: 'Die E-Mail-Adresse ist ungültig', | ||||
|         emptyEmail: 'Bitte geben Sie Ihre E-Mail ein', | ||||
|  |  | |||
|  | @ -185,6 +185,10 @@ export default { | |||
|     sessionWishes: 'Write your wishes about the session', | ||||
|     successPayment: 'Successful Payment', | ||||
|     errorPayment: 'Payment Error', | ||||
|     chat: { | ||||
|         join: 'Start chat', | ||||
|         joinAI: 'Start AI chat' | ||||
|     }, | ||||
|     errors: { | ||||
|         invalidEmail: 'The email address is not valid', | ||||
|         emptyEmail: 'Please enter your E-mail', | ||||
|  |  | |||
|  | @ -185,6 +185,10 @@ export default { | |||
|     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: { | ||||
|         invalidEmail: 'La dirección de correo electrónico no es válida', | ||||
|         emptyEmail: 'Introduce tu correo electrónico', | ||||
|  |  | |||
|  | @ -185,6 +185,10 @@ export default { | |||
|     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: { | ||||
|         invalidEmail: 'L\'adresse e-mail n\'est pas valide', | ||||
|         emptyEmail: 'Veuillez saisir votre e-mail', | ||||
|  |  | |||
|  | @ -185,6 +185,10 @@ export default { | |||
|     sessionWishes: 'Scrivi i tuoi desideri riguardo alla sessione', | ||||
|     successPayment: 'Pagamento Riuscito', | ||||
|     errorPayment: 'Errore di Pagamento', | ||||
|     chat: { | ||||
|         join: 'Avvia chat', | ||||
|         joinAI: 'Start AI chat' | ||||
|     }, | ||||
|     errors: { | ||||
|         invalidEmail: 'L\'indirizzo e-mail non è valido', | ||||
|         emptyEmail: 'Inserisci l\'e-mail', | ||||
|  |  | |||
|  | @ -185,6 +185,10 @@ export default { | |||
|     sessionWishes: 'Напишите свои пожелания по поводу сессии', | ||||
|     successPayment: 'Успешная оплата', | ||||
|     errorPayment: 'Ошибка оплаты', | ||||
|     chat: { | ||||
|         join: 'Начать чат', | ||||
|         joinAI: 'Начать чат с ИИ' | ||||
|     }, | ||||
|     errors: { | ||||
|         invalidEmail: 'Адрес электронной почты недействителен', | ||||
|         emptyEmail: 'Пожалуйста, введите ваш E-mail', | ||||
|  |  | |||
|  | @ -0,0 +1,79 @@ | |||
| import { HubConnection, HubConnectionBuilder, LogLevel } from '@microsoft/signalr'; | ||||
| import { IHttpConnectionOptions } from "@microsoft/signalr/src/IHttpConnectionOptions"; | ||||
| import { AUTH_TOKEN_KEY, BASE_URL } from '../constants/common'; | ||||
| import { IChatMessage } from '../types/chat'; | ||||
| 
 | ||||
| const chatMessageMethodName = 'ReceiveMessage'; | ||||
| const chatUserStatusChangeMethodName = 'chatUserStatusChange'; | ||||
| const chatSeenMethodName = 'MessageWasRead'; | ||||
| const chatTypingMethodName = 'directTyping'; | ||||
| const sendChatTextMethodName = 'SendMessage'; | ||||
| const sendChatTypingMethodName = 'DirectIsTyping'; | ||||
| const serverError = 'Error'; | ||||
| 
 | ||||
| const sendChatSeenMethodName = 'ConfirmMessageRead'; | ||||
| const sendChatMessagesSeenMethodName = 'ConfirmMessagesRead'; | ||||
| 
 | ||||
| 
 | ||||
| const joinChatMethodName = 'JoinChat'; | ||||
| const leaveChatMethodName = 'LeaveChat'; | ||||
| 
 | ||||
| const joinChatsMethodName = 'JoinChats'; | ||||
| const leaveChatsMethodName = 'LeaveChats'; | ||||
| const chatsReceiveMessageMethodName = 'ChatsMessageCreated'; | ||||
| 
 | ||||
| class SignalConnector { | ||||
|     private connection: HubConnection; | ||||
|     private events ={} as any; | ||||
|     static instance: SignalConnector; | ||||
|     constructor() { | ||||
|         const options = { | ||||
|             accessTokenFactory: () => localStorage.getItem(AUTH_TOKEN_KEY) | ||||
|         } as IHttpConnectionOptions; | ||||
|         this.connection = new HubConnectionBuilder() | ||||
|             .withUrl(`${BASE_URL}/hubs/chat`, options) | ||||
|             .withAutomaticReconnect() | ||||
|             .configureLogging(LogLevel.Debug) | ||||
|             .build(); | ||||
|         this.connection.start().then(()=>{ | ||||
|             this.events?.onConnected(true) | ||||
|             for (const k in this.events) { | ||||
|                 if (k != 'onConnected'){ | ||||
|                     this.connection.on(k, (payload: any) => { | ||||
|                         this.events[k](payload); | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
|         }).catch(err => console.log('SignalR Error', err)); | ||||
|     } | ||||
|     public addListener = (name: string, func: any)=> { | ||||
|         this.events[name] = func; | ||||
|     } | ||||
| 
 | ||||
|     public closeConnection = () => { | ||||
|         this.connection.stop(); | ||||
|     } | ||||
| 
 | ||||
|     public newMessage = (message: IChatMessage) => { | ||||
|         this.connection.invoke(sendChatTextMethodName, message).then(x => console.log('NewMsg',x)) | ||||
|     } | ||||
|     public joinChat = (groupId: number) => { | ||||
|         this.connection.invoke(joinChatMethodName, groupId, null).then(x => console.log(joinChatMethodName, x)) | ||||
|     } | ||||
| 
 | ||||
|     public joinChatPerson = (accId: number) => { | ||||
|         return this.connection.invoke(joinChatMethodName, 0, accId) | ||||
|     } | ||||
| 
 | ||||
|     public readMessages = (messagesId: number[]) => { | ||||
|         this.connection.invoke(sendChatMessagesSeenMethodName, messagesId).then(x => console.log(sendChatMessagesSeenMethodName, x)) | ||||
|     } | ||||
| 
 | ||||
|     public static getInstance(): SignalConnector { | ||||
|         if (!SignalConnector.instance) | ||||
|             SignalConnector.instance = new SignalConnector(); | ||||
|         return SignalConnector.instance; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export default SignalConnector.getInstance; | ||||
|  | @ -563,6 +563,10 @@ a { | |||
|   } | ||||
| } | ||||
| 
 | ||||
| .btn-disabled { | ||||
|   opacity: .4 !important; | ||||
| } | ||||
| 
 | ||||
| .btn-back { | ||||
|   user-select: none; | ||||
|   outline: none; | ||||
|  | @ -823,6 +827,7 @@ a { | |||
|     flex: 0 0 100%; | ||||
|     display: flex; | ||||
|     gap: 16px; | ||||
|     flex-direction: column; | ||||
| 
 | ||||
|     .btn-apply, | ||||
|     .btn-video { | ||||
|  |  | |||
|  | @ -13,6 +13,8 @@ | |||
|   } | ||||
| 
 | ||||
|   &__inner { | ||||
|     display: flex; | ||||
|     flex-direction: column-reverse; | ||||
|     flex-grow: 1; | ||||
|     height: 0; | ||||
|     position: relative; | ||||
|  | @ -94,7 +96,8 @@ | |||
|     align-items: center; | ||||
| 
 | ||||
|     textarea { | ||||
|       width: calc(100% - 136px); | ||||
|       resize: none; | ||||
|       width: calc(100% - 40px); | ||||
|       height: 24px; | ||||
|       padding: 0 8px; | ||||
|       border-radius: 0; | ||||
|  |  | |||
|  | @ -0,0 +1,12 @@ | |||
| 
 | ||||
| export interface IChatMessage { | ||||
|     GroupId: string; | ||||
|     CreatorId: number; | ||||
|     Content: string; | ||||
| } | ||||
| 
 | ||||
| export interface IChatJoin { | ||||
|     GroupId: string; | ||||
|     CreatorId: number; | ||||
|     Content: string; | ||||
| } | ||||
|  | @ -37,6 +37,7 @@ export type ThemeGroup = { | |||
| 
 | ||||
| export interface ExpertItem { | ||||
|     id: number; | ||||
|     botUserId?: number; | ||||
|     name: string; | ||||
|     surname?: string; | ||||
|     faceImageUrl?: string; | ||||
|  |  | |||
|  | @ -5,8 +5,7 @@ import { i18nText } from '../i18nKeys'; | |||
| const ROUTES = ['sessions', 'rooms', 'notifications', 'support', 'information', 'settings', 'messages', 'expert-profile']; | ||||
| const COUNTS: Record<string, number> = { | ||||
|     sessions: 12, | ||||
|     notifications: 5, | ||||
|     messages: 113 | ||||
|     notifications: 5 | ||||
| }; | ||||
| 
 | ||||
| export const getMenuConfig = (locale: string) => ROUTES.map((path) => ({ | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue