From 5ad94b7b25f5848ac74c74f1a4c675a184c07690 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Sun, 23 Jan 2022 21:00:42 +0300 Subject: [PATCH 1/7] feat: added layout --- src/components/Stat/index.tsx | 45 +++++++++ .../queries/category/useUpdateCategory.ts | 1 + src/pages/main/components/index.tsx | 94 ++++++++++++++++++ src/pages/main/main.tsx | 96 ++++++++++++++++++- src/pages/profile/components/index.tsx | 2 +- 5 files changed, 232 insertions(+), 6 deletions(-) create mode 100644 src/components/Stat/index.tsx create mode 100644 src/pages/main/components/index.tsx diff --git a/src/components/Stat/index.tsx b/src/components/Stat/index.tsx new file mode 100644 index 0000000..8275d14 --- /dev/null +++ b/src/components/Stat/index.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import { + Stat as ChakraStat, + StatProps, + useColorModeValue, + StatLabel as ChakraStatLabel, + StatLabel as ChakraStatNumber, + StatLabelProps, + Icon, + FlexProps, + Flex, +} from '@chakra-ui/react'; +import { IconType } from 'react-icons'; + +export const Stat: React.FC = (props: StatProps) => ( + +); + +export const StatLabel: React.FC = (props: StatLabelProps) => ( + +); + +export const StatNumber: React.FC = (props: StatLabelProps) => ( + +); + +type StatIconPropsType = { + icon: IconType; +}; +export const StatIcon: React.FC = props => ( + +); + +export const StatContent: React.FC = props => ( + + {props.children} + +); diff --git a/src/hooks/queries/category/useUpdateCategory.ts b/src/hooks/queries/category/useUpdateCategory.ts index c48890f..c8296f0 100644 --- a/src/hooks/queries/category/useUpdateCategory.ts +++ b/src/hooks/queries/category/useUpdateCategory.ts @@ -13,6 +13,7 @@ const useUpdateCategory = () => { const update = async (id: string, data: UpdateCategoryParamsType) => { try { const result = await CategoryServiceInstance.update(id, data); + toast({ title: 'Category updated.', status: 'info' }); return result.data; } catch (error) { if (axios.isAxiosError(error)) { diff --git a/src/pages/main/components/index.tsx b/src/pages/main/components/index.tsx new file mode 100644 index 0000000..8833f00 --- /dev/null +++ b/src/pages/main/components/index.tsx @@ -0,0 +1,94 @@ +import React from 'react'; +import { + Box, + BoxProps, + TextProps, + Text, + FlexProps, + Flex, + Tag, + TagLabel, + Icon, + Button, + useColorModeValue, + SimpleGrid, + SimpleGridProps, +} from '@chakra-ui/react'; +import { Card } from '@educt/components/Card'; +import { MdOutlineVideoLibrary, MdOutlineGroup, MdOutlineThumbUpOffAlt, MdOutlinePlayCircle } from 'react-icons/md'; +import { useHistory } from 'react-router-dom'; + +export const CourseCardList: React.FC = props => ( + +); +export const CourseCard: React.FC = props => ; +export const CourseCardHeading: React.FC = props => ( + +); +export const CourseCardTitle: React.FC = props => ; +export const CourseCardCategory: React.FC = props => ( + + + {props.children} + + +); +export const CourseCardContent: React.FC = props => ; +export const CourseCardCreated: React.FC<{ created: string }> = props => ( + + + Created + + + {props.created} + + +); +export const CourseCardLessons: React.FC<{ count: string | number }> = props => ( + + + + + + Total lessons + + {props.count} + + + +); +export const CourseCardFooter: React.FC = props => ( + +); + +export const CourseCardStat: React.FC<{ students: string | number; likes: string | number }> = props => ( + + + + {props.students} + + + + {props.likes} + + +); +export const CourseCardWatch: React.FC<{ id: string }> = props => { + const history = useHistory(); + + return ( + + + + ); +}; diff --git a/src/pages/main/main.tsx b/src/pages/main/main.tsx index ff9b74b..691c4b5 100644 --- a/src/pages/main/main.tsx +++ b/src/pages/main/main.tsx @@ -1,16 +1,102 @@ import React from 'react'; -import { Box, Heading } from '@chakra-ui/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'; +import { PageContent, PageHeading, PageWrapper } from '@educt/components/PageElements'; +import { Stat, StatContent, StatIcon, StatLabel, StatNumber } from '@educt/components/Stat'; +import { MdOutlineCollectionsBookmark, MdOutlineGroup, MdOutlineMessage, MdOutlineVideoLibrary } from 'react-icons/md'; +import { useRootStore } from '@educt/hooks/useRootStore'; +import LoadingPage from '@educt/components/LoadingPage'; +import { + CourseCard, + CourseCardCategory, + CourseCardCreated, + CourseCardHeading, + CourseCardContent, + CourseCardLessons, + CourseCardTitle, + CourseCardFooter, + CourseCardWatch, + CourseCardStat, + CourseCardList, +} from './components'; /** * Main page */ const MainPage: React.FC = () => { + const { + userStore: { me }, + } = useRootStore(); + + if (me === null) return ; + return ( - - Main page - + + + + + + Unread messages + + 3 + + + + + + Registered students + + 24 + + + + + + Courses + + 7 + + + + + + Lessons + + 42 + + + + + + + Recently added + + See all + + + + + + Javascript in a nutshell + Web Developing + + + + + + + + + + + + + + + + + ); }; diff --git a/src/pages/profile/components/index.tsx b/src/pages/profile/components/index.tsx index bd1d76c..1f89cb7 100644 --- a/src/pages/profile/components/index.tsx +++ b/src/pages/profile/components/index.tsx @@ -69,7 +69,7 @@ export const ProfileDescription: React.FC = props => { ); }; -export const ProfileSignOutButton: React.FC = props => { +export const ProfileSignOutButton: React.FC = () => { const { logout } = useLogout(); return ( From 2c6534648a592b2a2b64dc52c5a4140845c2f3e6 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Sun, 23 Jan 2022 23:13:17 +0300 Subject: [PATCH 2/7] feat: rendering courses --- src/hooks/queries/course/useFetchCourses.ts | 33 ++++++++++++++++ src/hooks/queries/index.ts | 2 + src/pages/main/components/index.tsx | 5 ++- src/pages/main/main.tsx | 43 +++++++++++++-------- src/services/CourseService.ts | 1 + src/types/index.d.ts | 8 +--- 6 files changed, 66 insertions(+), 26 deletions(-) create mode 100644 src/hooks/queries/course/useFetchCourses.ts diff --git a/src/hooks/queries/course/useFetchCourses.ts b/src/hooks/queries/course/useFetchCourses.ts new file mode 100644 index 0000000..430ed5a --- /dev/null +++ b/src/hooks/queries/course/useFetchCourses.ts @@ -0,0 +1,33 @@ +import { ICourse } from '@educt/interfaces'; +import { CourseServiceInstance } from '@educt/services'; +import { FetchCoursesParams } from '@educt/types'; +import useAsync from '@educt/hooks/useAsync'; +import { useEffect } from 'react'; +import { useErrorHandler } from 'react-error-boundary'; + +const useFetchCourses = (params?: FetchCoursesParams | undefined) => { + const handleError = useErrorHandler(); + + const fetchCourses = async (params?: FetchCoursesParams | undefined) => { + try { + const result = await CourseServiceInstance.fetchAll(params); + return result.data; + } catch (error) { + handleError(error); + return Promise.reject(error); + } + }; + + const { execute: fetch, ...state } = useAsync< + Omit[], + Parameters + >(fetchCourses); + + useEffect(() => { + fetch(params); + }, []); + + return { ...state }; +}; + +export { useFetchCourses }; diff --git a/src/hooks/queries/index.ts b/src/hooks/queries/index.ts index 7da175a..9928bdb 100644 --- a/src/hooks/queries/index.ts +++ b/src/hooks/queries/index.ts @@ -14,6 +14,7 @@ import { useUpdateLesson } from './lesson/useUpdateLesson'; /** * Course */ +import { useFetchCourses } from './course/useFetchCourses'; import { useCreateCourse } from './course/useCreateCourse'; import { useUpdateCourse } from './course/useUpdateCourse'; import { useFetchCourse } from './course/useFetchCourse'; @@ -40,6 +41,7 @@ import { useUpdateCategory } from './category/useUpdateCategory'; export { useLogin, useLogout, + useFetchCourses, useCreateCourse, useUpdateCourse, useFetchCourse, diff --git a/src/pages/main/components/index.tsx b/src/pages/main/components/index.tsx index 8833f00..d16e58b 100644 --- a/src/pages/main/components/index.tsx +++ b/src/pages/main/components/index.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import moment from 'moment'; import { Box, BoxProps, @@ -48,7 +49,7 @@ export const CourseCardCreated: React.FC<{ created: string }> = props => ( Created - {props.created} + {moment(props.created).format('ll')} ); @@ -86,7 +87,7 @@ export const CourseCardWatch: React.FC<{ id: string }> = props => { return ( - diff --git a/src/pages/main/main.tsx b/src/pages/main/main.tsx index 691c4b5..899f0b7 100644 --- a/src/pages/main/main.tsx +++ b/src/pages/main/main.tsx @@ -20,6 +20,8 @@ import { CourseCardStat, CourseCardList, } from './components'; +import { observer } from 'mobx-react'; +import { useFetchCourses } from '@educt/hooks/queries'; /** * Main page @@ -28,8 +30,9 @@ const MainPage: React.FC = () => { const { userStore: { me }, } = useRootStore(); + const { data: courses } = useFetchCourses({ limit: 3 }); - if (me === null) return ; + if (me === null || courses === null) return ; return ( @@ -39,7 +42,11 @@ const MainPage: React.FC = () => { Unread messages - 3 + + + 3 + + @@ -76,22 +83,24 @@ const MainPage: React.FC = () => { - - - Javascript in a nutshell - Web Developing - + {courses.map(course => ( + + + {course.title} + {course.category.title} + - - - - + + + + - - - - - + + + + + + ))} @@ -100,4 +109,4 @@ const MainPage: React.FC = () => { ); }; -export default MainPage; +export default observer(MainPage); diff --git a/src/services/CourseService.ts b/src/services/CourseService.ts index a6470d6..4ae127d 100644 --- a/src/services/CourseService.ts +++ b/src/services/CourseService.ts @@ -24,6 +24,7 @@ class CourseService { params: { status: params?.status, category_id: params?.category_id, + limit: params?.limit, }, }); return result.data; diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 8abc3ef..3211751 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -109,15 +109,9 @@ export type CoursesPageContextType = { setCourseStatus: React.Dispatch>; }; export type FetchCoursesParams = { - /** - * Get courses by status - */ status?: CourseStatusEnum; - - /** - * Get courses with category by id - */ category_id?: string; + limit?: number; }; export type FetchUsersParamsType = { page?: number; From d07879a7002554311f3ccf3f0e995bfc7fcef083 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Mon, 24 Jan 2022 22:39:17 +0300 Subject: [PATCH 3/7] feat: rendering basic stat info --- src/hooks/queries/index.ts | 6 +++++ src/hooks/queries/stat/useFetchStat.ts | 29 +++++++++++++++++++++ src/pages/main/main.tsx | 35 +++++++++++++------------- src/services/StatService.ts | 24 ++++++++++++++++++ src/types/index.d.ts | 5 ++++ 5 files changed, 82 insertions(+), 17 deletions(-) create mode 100644 src/hooks/queries/stat/useFetchStat.ts create mode 100644 src/services/StatService.ts diff --git a/src/hooks/queries/index.ts b/src/hooks/queries/index.ts index 9928bdb..d573ed1 100644 --- a/src/hooks/queries/index.ts +++ b/src/hooks/queries/index.ts @@ -38,6 +38,11 @@ import { useCreateCategory } from './category/useCreateCategory'; import { useDeleteCategory } from './category/useDeleteCategory'; import { useUpdateCategory } from './category/useUpdateCategory'; +/** + * Stat + */ +import { useFetchStat } from './stat/useFetchStat'; + export { useLogin, useLogout, @@ -59,4 +64,5 @@ export { useDeleteCategory, useUpdateCategory, useFetchCategories, + useFetchStat, }; diff --git a/src/hooks/queries/stat/useFetchStat.ts b/src/hooks/queries/stat/useFetchStat.ts new file mode 100644 index 0000000..9ca0808 --- /dev/null +++ b/src/hooks/queries/stat/useFetchStat.ts @@ -0,0 +1,29 @@ +import { StatServiceInstance } from '@educt/services/StatService'; +import { StatInfoType } from '@educt/types'; +import { useEffect } from 'react'; +import useAsync from '@educt/hooks/useAsync'; +import { useErrorHandler } from 'react-error-boundary'; + +const useFetchStat = () => { + const handleError = useErrorHandler(); + + const fetchStat = async () => { + try { + const result = await StatServiceInstance.fetchStat(); + return result.data; + } catch (error) { + handleError(error); + return Promise.reject(error); + } + }; + + const { execute: fetch, ...state } = useAsync>(fetchStat); + + useEffect(() => { + fetch(); + }, []); + + return { ...state }; +}; + +export { useFetchStat }; diff --git a/src/pages/main/main.tsx b/src/pages/main/main.tsx index 899f0b7..04970dd 100644 --- a/src/pages/main/main.tsx +++ b/src/pages/main/main.tsx @@ -21,7 +21,7 @@ import { CourseCardList, } from './components'; import { observer } from 'mobx-react'; -import { useFetchCourses } from '@educt/hooks/queries'; +import { useFetchCourses, useFetchStat } from '@educt/hooks/queries'; /** * Main page @@ -31,8 +31,9 @@ const MainPage: React.FC = () => { userStore: { me }, } = useRootStore(); const { data: courses } = useFetchCourses({ limit: 3 }); + const { data: stat } = useFetchStat(); - if (me === null || courses === null) return ; + if (me === null || courses === null || stat === null) return ; return ( @@ -40,29 +41,29 @@ const MainPage: React.FC = () => { - Unread messages - - - - 3 - - - - - - - - Registered students + Online 24 + + Unread messages + + + + 1 + + + + + + Courses - 7 + {stat.courses_count} @@ -70,7 +71,7 @@ const MainPage: React.FC = () => { Lessons - 42 + {stat.lessons_count} diff --git a/src/services/StatService.ts b/src/services/StatService.ts new file mode 100644 index 0000000..dae67f7 --- /dev/null +++ b/src/services/StatService.ts @@ -0,0 +1,24 @@ +import { AxiosInstance } from 'axios'; +import { ApiServiceInstance } from '.'; +import { IApiRespose } from '@educt/interfaces'; +import { StatInfoType } from '@educt/types'; + +class StatService { + public api: AxiosInstance; + + constructor(api: AxiosInstance) { + this.api = api; + } + + /** + * Get basic stat info about system + * + * @returns Stat info + */ + public async fetchStat(): Promise> { + const result = await this.api.get('/v1/stat'); + return result.data; + } +} + +export const StatServiceInstance = new StatService(ApiServiceInstance.api); diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 3211751..d314bdb 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -40,6 +40,11 @@ export type LessonProgress = { } | null; }; +export type StatInfoType = { + courses_count: string; + lessons_count: string; +}; + export type LessonVideoType = { id: number; client_name: string; From 283fc683c85b2ab94b1a36d232cd5474e1a653d8 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Tue, 25 Jan 2022 23:10:28 +0300 Subject: [PATCH 4/7] feat: added some socket hooks --- package-lock.json | 95 +++++++++++++++++++++++++++++++++++-- package.json | 1 + src/contexts/index.ts | 5 ++ src/hooks/useSocket.ts | 18 +++++++ src/hooks/useSocketEvent.ts | 18 +++++++ src/index.tsx | 6 ++- src/interfaces/index.ts | 1 + src/pages/main/main.tsx | 18 ++++++- src/providers/index.tsx | 20 +++++++- vite.config.ts | 4 ++ 10 files changed, 177 insertions(+), 9 deletions(-) create mode 100644 src/hooks/useSocket.ts create mode 100644 src/hooks/useSocketEvent.ts 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: { From e049ce7f71be69d03e782209497b20e9ebb02527 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Thu, 27 Jan 2022 00:01:22 +0300 Subject: [PATCH 5/7] fix: socket connection --- src/App.tsx | 5 +++++ src/components/Layout/Layout.tsx | 17 ++++++++++++++++- src/hooks/queries/auth/useLogin.ts | 4 ++++ src/hooks/queries/auth/useLogout.ts | 8 ++++++++ src/hooks/useSocket.ts | 4 ++-- src/hooks/useSocketEvent.ts | 2 +- src/pages/main/main.tsx | 26 +++++++++++++++----------- src/providers/index.tsx | 2 +- src/types/index.d.ts | 1 + vite.config.ts | 4 ---- 10 files changed, 53 insertions(+), 20 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index ec5e270..4a49dae 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,12 +12,17 @@ import { useRootStore } from '@educt/hooks/useRootStore'; */ import PrivateRoute from '@educt/components/PrivateRoute'; import PublicRoute from '@educt/components/PublicRoute'; +import { useSocketEvent } from './hooks/useSocketEvent'; const App = () => { const { uiStore: { history }, } = useRootStore(); + useSocketEvent('connect', () => console.info('[WebSocket]: Connected.')); + useSocketEvent('disconnect', () => console.info('[WebSocket]: Disconnected.')); + useSocketEvent('connect_error', err => console.error(err)); + return ( diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx index 8d28180..691d871 100644 --- a/src/components/Layout/Layout.tsx +++ b/src/components/Layout/Layout.tsx @@ -1,4 +1,4 @@ -import React, { Suspense } from 'react'; +import React, { Suspense, useContext } from 'react'; import { observer } from 'mobx-react'; import { Redirect } from 'react-router'; import { useErrorHandler } from 'react-error-boundary'; @@ -23,6 +23,11 @@ import NavDesktop from '@educt/components/Nav/Desktop'; import { useEffect } from 'react'; import { useRootStore } from '@educt/hooks/useRootStore'; +/** + * Contexts + */ +import { SocketContext } from '@educt/contexts'; + type LayoutPropsType = { roles: UserRoleEnum[] | undefined }; /** @@ -30,6 +35,7 @@ type LayoutPropsType = { roles: UserRoleEnum[] | undefined }; */ const Layout: React.FC = ({ children, roles }) => { const { userStore } = useRootStore(); + const { socket } = useContext(SocketContext); const handleError = useErrorHandler(); /** @@ -39,6 +45,15 @@ const Layout: React.FC = ({ children, roles }) => { userStore.loadCurrentUserData().catch(error => handleError(error)); }, [handleError, userStore]); + /** + * Connect to websocket server + */ + useEffect(() => { + if (!socket?.connected) { + socket?.connect(); + } + }, [socket]); + return ( <>
diff --git a/src/hooks/queries/auth/useLogin.ts b/src/hooks/queries/auth/useLogin.ts index f9f311f..1e4a9d2 100644 --- a/src/hooks/queries/auth/useLogin.ts +++ b/src/hooks/queries/auth/useLogin.ts @@ -1,5 +1,7 @@ import { useToast } from '@chakra-ui/react'; +import { SocketContext } from '@educt/contexts'; import { IToken } from '@educt/interfaces'; +import { useContext } from 'react'; import { useErrorHandler } from 'react-error-boundary'; import useAsync from '../../useAsync'; import { useRootStore } from '../../useRootStore'; @@ -8,6 +10,7 @@ type LoginResultDataType = IToken; const useLogin = () => { const { authStore } = useRootStore(); + const { socket } = useContext(SocketContext); const toast = useToast(); const handleError = useErrorHandler(); @@ -20,6 +23,7 @@ const useLogin = () => { isClosable: true, status: 'success', }); + socket?.connect(); return result.data; } catch (error: any) { if (error.response) { diff --git a/src/hooks/queries/auth/useLogout.ts b/src/hooks/queries/auth/useLogout.ts index 040fedf..ff122fa 100644 --- a/src/hooks/queries/auth/useLogout.ts +++ b/src/hooks/queries/auth/useLogout.ts @@ -3,11 +3,18 @@ import { useHistory } from 'react-router-dom'; import { useRootStore } from '../../useRootStore'; import { useErrorHandler } from 'react-error-boundary'; import useAsync from '../../useAsync'; +import { useContext } from 'react'; + +/** + * Contexts + */ +import { SocketContext } from '@educt/contexts'; type LogoutResultDataType = Record; const useLogout = () => { const { authStore } = useRootStore(); + const { socket } = useContext(SocketContext); const history = useHistory(); const toast = useToast(); const handleError = useErrorHandler(); @@ -17,6 +24,7 @@ const useLogout = () => { const result = await authStore.logout(); history.push('/auth'); toast({ title: 'You are logged out.', isClosable: true, status: 'info' }); + socket?.disconnect(); return result.data; } catch (error) { handleError(error); diff --git a/src/hooks/useSocket.ts b/src/hooks/useSocket.ts index 631385d..9a39ea7 100644 --- a/src/hooks/useSocket.ts +++ b/src/hooks/useSocket.ts @@ -5,12 +5,12 @@ export const useSocket = (url: string) => { const [socket, setSocket] = useState(null); useEffect(() => { - const socketClient = io(url, { autoConnect: true }); + const socketClient = io(url, { autoConnect: false, withCredentials: true }); setSocket(socketClient); return () => { - socket?.disconnect(); + socket?.close(); }; }, []); diff --git a/src/hooks/useSocketEvent.ts b/src/hooks/useSocketEvent.ts index c8ba89e..272597b 100644 --- a/src/hooks/useSocketEvent.ts +++ b/src/hooks/useSocketEvent.ts @@ -1,7 +1,7 @@ import { useContext, useEffect } from 'react'; import { SocketContext } from '@educt/contexts'; -export const useSocketEvent = (event: string, callback: () => void) => { +export const useSocketEvent = (event: string, callback: (...args: any) => void) => { const { socket } = useContext(SocketContext); useEffect(() => { diff --git a/src/pages/main/main.tsx b/src/pages/main/main.tsx index b1c533f..75bd3c2 100644 --- a/src/pages/main/main.tsx +++ b/src/pages/main/main.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { 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'; @@ -22,6 +22,8 @@ import { } from './components'; import { observer } from 'mobx-react'; import { useFetchCourses, useFetchStat } from '@educt/hooks/queries'; +import { useSocketEvent } from '@educt/hooks/useSocketEvent'; +import { useEffect } from 'react'; /** * Main page @@ -34,18 +36,20 @@ const MainPage: React.FC = () => { const { data: stat } = useFetchStat(); const [online, setOnline] = useState(0); - // useEffect(() => { - // const onUpdateOnline = users => { - // console.log(users); - // setOnline(users.length); - // }; + /** + * Update online state on socket event + */ + useSocketEvent('user:online', online => setOnline(online)); - // socket.on('user:online', onUpdateOnline); + useSocketEvent('user:connected', user => { + console.table(user); + }); - // return () => { - // socket.off('user:online', onUpdateOnline); - // }; - // }, [setOnline, socket]); + useEffect(() => { + if (stat) { + setOnline(stat.online); + } + }, [stat?.online]); if (me === null || courses === null || stat === null) return ; diff --git a/src/providers/index.tsx b/src/providers/index.tsx index 8f5d43d..1e9c225 100644 --- a/src/providers/index.tsx +++ b/src/providers/index.tsx @@ -25,7 +25,7 @@ export const RootStoreProvider = ({ children }: { children: ReactNode }) => { * Socket context provider */ export const SocketContextProvider: React.FC = ({ children }) => { - const socket = useSocket(''); + const socket = useSocket('ws://localhost:3333'); return {children}; }; diff --git a/src/types/index.d.ts b/src/types/index.d.ts index d314bdb..2fe9f43 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -43,6 +43,7 @@ export type LessonProgress = { export type StatInfoType = { courses_count: string; lessons_count: string; + online: number; }; export type LessonVideoType = { diff --git a/vite.config.ts b/vite.config.ts index e5404ba..6f1fbde 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -33,10 +33,6 @@ export default defineConfig({ target: constants.BACKEND_URL, changeOrigin: true, }, - '/socket.io': { - target: constants.BACKEND_URL, - changeOrigin: true, - }, }, }, resolve: { From bfbf0e80649f4aa5d0cc917ed16c3197901e8d3b Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Thu, 27 Jan 2022 23:59:30 +0300 Subject: [PATCH 6/7] feat: saving session id of socket in localstorage --- src/App.tsx | 13 ++++++++++++- src/components/Layout/Layout.tsx | 8 +++++++- src/hooks/queries/auth/useLogout.ts | 1 + src/pages/main/main.tsx | 4 +--- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 4a49dae..ca529cf 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useContext } from 'react'; import { Router, Switch } from 'react-router-dom'; import { routes } from './routes.config'; @@ -13,15 +13,26 @@ import { useRootStore } from '@educt/hooks/useRootStore'; import PrivateRoute from '@educt/components/PrivateRoute'; import PublicRoute from '@educt/components/PublicRoute'; import { useSocketEvent } from './hooks/useSocketEvent'; +import { SocketContext } from './contexts'; const App = () => { const { uiStore: { history }, } = useRootStore(); + const { socket } = useContext(SocketContext); useSocketEvent('connect', () => console.info('[WebSocket]: Connected.')); useSocketEvent('disconnect', () => console.info('[WebSocket]: Disconnected.')); useSocketEvent('connect_error', err => console.error(err)); + useSocketEvent('user:session', data => { + console.log(data); + if (socket) { + const { sessionId, userId } = data; + socket.auth = { sessionId }; + window.localStorage.setItem('sessionId', sessionId); + socket.userId = userId; + } + }); return ( diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx index 691d871..355dcad 100644 --- a/src/components/Layout/Layout.tsx +++ b/src/components/Layout/Layout.tsx @@ -50,7 +50,13 @@ const Layout: React.FC = ({ children, roles }) => { */ useEffect(() => { if (!socket?.connected) { - socket?.connect(); + if (socket) { + const sessionId = window.localStorage.getItem('sessionId'); + if (sessionId) { + socket.auth = { sessionId }; + } + socket.connect(); + } } }, [socket]); diff --git a/src/hooks/queries/auth/useLogout.ts b/src/hooks/queries/auth/useLogout.ts index ff122fa..1d8f93f 100644 --- a/src/hooks/queries/auth/useLogout.ts +++ b/src/hooks/queries/auth/useLogout.ts @@ -24,6 +24,7 @@ const useLogout = () => { const result = await authStore.logout(); history.push('/auth'); toast({ title: 'You are logged out.', isClosable: true, status: 'info' }); + // window.localStorage.removeItem('sessionId'); socket?.disconnect(); return result.data; } catch (error) { diff --git a/src/pages/main/main.tsx b/src/pages/main/main.tsx index 75bd3c2..7dd5df8 100644 --- a/src/pages/main/main.tsx +++ b/src/pages/main/main.tsx @@ -41,9 +41,7 @@ const MainPage: React.FC = () => { */ useSocketEvent('user:online', online => setOnline(online)); - useSocketEvent('user:connected', user => { - console.table(user); - }); + useSocketEvent('user:connected', user => console.table(user)); useEffect(() => { if (stat) { From cdce2c5512f1bcf43cff8a97b2c08ba9f0ef01c3 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Sat, 29 Jan 2022 17:57:34 +0300 Subject: [PATCH 7/7] feat: online status --- src/hooks/queries/auth/useLogout.ts | 22 +++++++++++++++++++--- src/pages/main/main.tsx | 2 +- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/hooks/queries/auth/useLogout.ts b/src/hooks/queries/auth/useLogout.ts index 1d8f93f..3d763d4 100644 --- a/src/hooks/queries/auth/useLogout.ts +++ b/src/hooks/queries/auth/useLogout.ts @@ -13,7 +13,10 @@ import { SocketContext } from '@educt/contexts'; type LogoutResultDataType = Record; const useLogout = () => { - const { authStore } = useRootStore(); + const { + authStore, + userStore: { me }, + } = useRootStore(); const { socket } = useContext(SocketContext); const history = useHistory(); const toast = useToast(); @@ -21,11 +24,24 @@ const useLogout = () => { const logout = async () => { try { + const sessionId = window.localStorage.getItem('sessionId'); const result = await authStore.logout(); + history.push('/auth'); toast({ title: 'You are logged out.', isClosable: true, status: 'info' }); - // window.localStorage.removeItem('sessionId'); - socket?.disconnect(); + + /** + * Destroy session + */ + if (sessionId) { + if (socket) { + socket.auth = { sessionId: undefined }; + // socket.emit('user:logout', { sessionId }); + socket.disconnect(); + } + window.localStorage.removeItem('sessionId'); + } + return result.data; } catch (error) { handleError(error); diff --git a/src/pages/main/main.tsx b/src/pages/main/main.tsx index 7dd5df8..bedec1c 100644 --- a/src/pages/main/main.tsx +++ b/src/pages/main/main.tsx @@ -47,7 +47,7 @@ const MainPage: React.FC = () => { if (stat) { setOnline(stat.online); } - }, [stat?.online]); + }, [stat]); if (me === null || courses === null || stat === null) return ;