diff --git a/package-lock.json b/package-lock.json index 8195a89..8854cb5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1164,6 +1164,16 @@ "picomatch": "^2.2.2" } }, + "@socket.io/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@socket.io/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-dOlCBKnDw4iShaIsH/bxujKTM18+2TOAsYz+KSc11Am38H4q5Xw8Bbz97ZYdrVNM+um3p7w86Bvvmcn9q+5+eQ==" + }, + "@socket.io/component-emitter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.0.0.tgz", + "integrity": "sha512-2pTGuibAXJswAPJjaKisthqS/NOK5ypG4LYT6tEAV0S/mxW0zOIvYvGK0V8w8+SHxAm6vRMSjqSalFXeBAqs+Q==" + }, "@types/history": { "version": "4.7.9", "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.9.tgz", @@ -1582,6 +1592,11 @@ "resolve": "^1.12.0" } }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1730,7 +1745,6 @@ "version": "4.3.2", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "dev": true, "requires": { "ms": "2.1.2" } @@ -1790,6 +1804,30 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "engine.io-client": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.1.1.tgz", + "integrity": "sha512-V05mmDo4gjimYW+FGujoGmmmxRaDsrVr7AXA3ZIfa04MWM1jOfZfUwou0oNqhNwy/votUDvGDt4JA4QF4e0b4g==", + "requires": { + "@socket.io/component-emitter": "~3.0.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.0", + "has-cors": "1.1.0", + "parseqs": "0.0.6", + "parseuri": "0.0.6", + "ws": "~8.2.3", + "xmlhttprequest-ssl": "~2.0.0", + "yeast": "0.1.2" + } + }, + "engine.io-parser": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.3.tgz", + "integrity": "sha512-BtQxwF27XUNnSafQLvDi0dQ8s3i6VgzSoQMJacpIcGNrlUdfHSKbgm3jmjCVvQluGzqwujQMPAoMai3oYSTurg==", + "requires": { + "@socket.io/base64-arraybuffer": "~1.0.2" + } + }, "enquirer": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", @@ -2519,6 +2557,11 @@ "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", "dev": true }, + "has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" + }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -2973,8 +3016,7 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "nanoclone": { "version": "0.2.1", @@ -3122,6 +3164,16 @@ "lines-and-columns": "^1.1.6" } }, + "parseqs": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz", + "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==" + }, + "parseuri": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz", + "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==" + }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -3713,6 +3765,28 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, + "socket.io-client": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.4.1.tgz", + "integrity": "sha512-N5C/L5fLNha5Ojd7Yeb/puKcPWWcoB/A09fEjjNsg91EDVr5twk/OEyO6VT9dlLSUNY85NpW6KBhVMvaLKQ3vQ==", + "requires": { + "@socket.io/component-emitter": "~3.0.0", + "backo2": "~1.0.2", + "debug": "~4.3.2", + "engine.io-client": "~6.1.1", + "parseuri": "0.0.6", + "socket.io-parser": "~4.1.1" + } + }, + "socket.io-parser": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.1.1.tgz", + "integrity": "sha512-USQVLSkDWE5nbcY760ExdKaJxCE65kcsG/8k5FDGZVVxpD1pA7hABYXYkCUvxUuYYh/+uQw0N/fvBzfT8o07KA==", + "requires": { + "@socket.io/component-emitter": "~3.0.0", + "debug": "~4.3.1" + } + }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -4009,6 +4083,16 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, + "ws": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==" + }, + "xmlhttprequest-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==" + }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -4020,6 +4104,11 @@ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" }, + "yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" + }, "yup": { "version": "0.32.11", "resolved": "https://registry.npmjs.org/yup/-/yup-0.32.11.tgz", diff --git a/package.json b/package.json index d331997..f5f1821 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "react-router-dom": "^5.2.0", "react-spinners": "^0.11.0", "react-virtuoso": "^2.2.8", + "socket.io-client": "^4.4.1", "validator": "^13.6.0", "yup": "^0.32.9" }, diff --git a/src/contexts/index.ts b/src/contexts/index.ts index 1bc8b83..3b545c7 100644 --- a/src/contexts/index.ts +++ b/src/contexts/index.ts @@ -1,9 +1,14 @@ import { createContext } from 'react'; import { ChangeEmailPageContextType, CoursesPageContextType, UsersPageContextType } from '@educt/types'; import RootStore from '../stores/RootStore'; +import { Socket } from 'socket.io-client'; export const StoreContext = createContext(undefined); +export const SocketContext = createContext<{ socket: Socket | null }>({ + socket: null, +}); + export const ChangeEmailPageContext = createContext({ isCodeSent: false, setIsCodeSent: () => null, diff --git a/src/hooks/useSocket.ts b/src/hooks/useSocket.ts new file mode 100644 index 0000000..631385d --- /dev/null +++ b/src/hooks/useSocket.ts @@ -0,0 +1,18 @@ +import { io, Socket } from 'socket.io-client'; +import { useEffect, useState } from 'react'; + +export const useSocket = (url: string) => { + const [socket, setSocket] = useState(null); + + useEffect(() => { + const socketClient = io(url, { autoConnect: true }); + + setSocket(socketClient); + + return () => { + socket?.disconnect(); + }; + }, []); + + return socket; +}; diff --git a/src/hooks/useSocketEvent.ts b/src/hooks/useSocketEvent.ts new file mode 100644 index 0000000..c8ba89e --- /dev/null +++ b/src/hooks/useSocketEvent.ts @@ -0,0 +1,18 @@ +import { useContext, useEffect } from 'react'; +import { SocketContext } from '@educt/contexts'; + +export const useSocketEvent = (event: string, callback: () => void) => { + const { socket } = useContext(SocketContext); + + useEffect(() => { + if (!socket) { + return; + } + + socket.on(event, callback); + + return () => { + socket.off(event, callback); + }; + }, [callback, event, socket]); +}; diff --git a/src/index.tsx b/src/index.tsx index 085d093..ed4dd9d 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,14 +3,16 @@ import ReactDOM from 'react-dom'; import { ChakraProvider, ColorModeScript } from '@chakra-ui/react'; import App from './App'; import { theme } from './theme'; -import { RootStoreProvider } from './providers'; +import { RootStoreProvider, SocketContextProvider } from './providers'; ReactDOM.render( <> - + + + , diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 45f73d7..cb74b60 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -13,6 +13,7 @@ export interface IAppConfig { } export interface IToken { + userId: string; token: string; type: string; expires_at: string; diff --git a/src/pages/main/main.tsx b/src/pages/main/main.tsx index 04970dd..b1c533f 100644 --- a/src/pages/main/main.tsx +++ b/src/pages/main/main.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { Flex, Box, Text, Link, SimpleGrid, Divider } from '@chakra-ui/react'; import { Link as ReactRouterLink } from 'react-router-dom'; import { IPageProps } from '@educt/interfaces'; @@ -32,6 +32,20 @@ const MainPage: React.FC = () => { } = useRootStore(); const { data: courses } = useFetchCourses({ limit: 3 }); const { data: stat } = useFetchStat(); + const [online, setOnline] = useState(0); + + // useEffect(() => { + // const onUpdateOnline = users => { + // console.log(users); + // setOnline(users.length); + // }; + + // socket.on('user:online', onUpdateOnline); + + // return () => { + // socket.off('user:online', onUpdateOnline); + // }; + // }, [setOnline, socket]); if (me === null || courses === null || stat === null) return ; @@ -43,7 +57,7 @@ const MainPage: React.FC = () => { Online - 24 + {online} diff --git a/src/providers/index.tsx b/src/providers/index.tsx index a9daa1b..8f5d43d 100644 --- a/src/providers/index.tsx +++ b/src/providers/index.tsx @@ -1,9 +1,16 @@ import React, { useState, ReactNode } from 'react'; -import { ChangeEmailPageContext, CoursesPageContext, StoreContext, UsersPageContext } from '@educt/contexts'; +import { + ChangeEmailPageContext, + CoursesPageContext, + SocketContext, + StoreContext, + UsersPageContext, +} from '@educt/contexts'; import { ConfirmEmailDataType, SearchingRoleStateType } from '@educt/types'; import RootStore from '@educt/stores/RootStore'; -import { ICategory, ICourse, IUser } from '@educt/interfaces'; +import { ICategory } from '@educt/interfaces'; import { CourseStatusEnum } from '@educt/enums'; +import { useSocket } from '@educt/hooks/useSocket'; /** * Root store context provider @@ -14,6 +21,15 @@ export const RootStoreProvider = ({ children }: { children: ReactNode }) => { return {children}; }; +/** + * Socket context provider + */ +export const SocketContextProvider: React.FC = ({ children }) => { + const socket = useSocket(''); + + return {children}; +}; + /** * Change email page context provider */ diff --git a/vite.config.ts b/vite.config.ts index 6f1fbde..e5404ba 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -33,6 +33,10 @@ export default defineConfig({ target: constants.BACKEND_URL, changeOrigin: true, }, + '/socket.io': { + target: constants.BACKEND_URL, + changeOrigin: true, + }, }, }, resolve: {