From 3bba8974549ba88b2af0abcb1bd0efdf2f0694e7 Mon Sep 17 00:00:00 2001 From: alex Date: Sun, 8 Sep 2024 19:29:13 +0200 Subject: [PATCH] websocket events for live editing --- src/App.tsx | 2 - src/core/components/DashboardLayout/index.tsx | 91 ++- src/core/components/SideMenu/index.tsx | 74 ++- .../components/SideMenu/styles.module.css | 3 + src/core/services/websocketService.ts | 121 +++- src/core/store/store.tsx | 4 + src/core/utils/webSocket.ts | 9 + src/features/Lessons/LessonPage/index.tsx | 133 ++-- .../Lessons/LessonPage/lessonPageSlice.ts | 63 ++ .../LessonPageEditor/SortableEditorItem.tsx | 178 +++--- .../Lessons/LessonPageEditor/index.tsx | 235 +++++--- .../LessonPageEditor/lessonPageEditorSlice.ts | 23 +- src/features/Lessons/Questions/index.tsx | 570 ++++++++++-------- src/features/Lessons/components.ts | 132 ++-- src/features/Lessons/index.tsx | 39 +- src/features/Lessons/lessonsSlice.ts | 62 ++ src/features/Roles/index.tsx | 13 +- src/features/Settings/index.tsx | 12 +- 18 files changed, 1135 insertions(+), 629 deletions(-) create mode 100644 src/core/components/SideMenu/styles.module.css create mode 100644 src/features/Lessons/LessonPage/lessonPageSlice.ts create mode 100644 src/features/Lessons/lessonsSlice.ts diff --git a/src/App.tsx b/src/App.tsx index 9f089d0..aa32e46 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -63,8 +63,6 @@ export default function App() { }, [uAuthenticated]); useEffect(() => { - console.log("App mounted"); - if (!localStorage.getItem("session")) { dispatch(setUserAuthenticated(false)); } else { diff --git a/src/core/components/DashboardLayout/index.tsx b/src/core/components/DashboardLayout/index.tsx index b886cf4..387b6c5 100644 --- a/src/core/components/DashboardLayout/index.tsx +++ b/src/core/components/DashboardLayout/index.tsx @@ -1,60 +1,59 @@ -import { Grid, Layout } from 'antd'; -import PageContent from '../PageContent'; -import SideMenuDesktop from '../SideMenu/Desktop'; -import SideMenuMobile from '../SideMenu/Mobile'; -import { SideMenuContent, SideMenuEditorContent } from '../SideMenu'; -import { useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { setIsSideMenuCollapsed } from '../SideMenu/sideMenuSlice'; -import { editorActive } from '../../../features/Lessons/LessonPageEditor/lessonPageEditorSlice'; -import MyDndContext from './MyDndContext'; -import AiChat from 'features/AiChat'; +import { Grid, Layout } from "antd"; +import PageContent from "../PageContent"; +import SideMenuDesktop from "../SideMenu/Desktop"; +import SideMenuMobile from "../SideMenu/Mobile"; +import { SideMenuContent, SideMenuEditorContent } from "../SideMenu"; +import { useEffect } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { setIsSideMenuCollapsed } from "../SideMenu/sideMenuSlice"; +import { editorActive } from "../../../features/Lessons/LessonPageEditor/lessonPageEditorSlice"; +import MyDndContext from "./MyDndContext"; +import AiChat from "features/AiChat"; const { useBreakpoint } = Grid; export function SideMenu() { - const screenBreakpoint = useBreakpoint(); + const screenBreakpoint = useBreakpoint(); - const dispatch = useDispatch(); + const dispatch = useDispatch(); - const isEditorActive = useSelector(editorActive); + const isEditorActive = useSelector(editorActive); - console.log('isEditorActive', isEditorActive); + const Content = () => { + return isEditorActive ? : ; + }; - const Content = () => { - return isEditorActive ? : ; - }; + useEffect(() => { + dispatch(setIsSideMenuCollapsed(!screenBreakpoint.lg)); + }, [screenBreakpoint]); - useEffect(() => { - dispatch(setIsSideMenuCollapsed(!screenBreakpoint.lg)); - }, [screenBreakpoint]); - - return ( - <> - - {screenBreakpoint.lg ? ( - - - - ) : ( - - - - )} - - ); + return ( + <> + {screenBreakpoint.lg ? ( + + + + ) : ( + + + + )} + + ); } export default function DashboardLayout() { - return ( - - - - + return ( + + + + - - - - - ); + + + + + + + ); } diff --git a/src/core/components/SideMenu/index.tsx b/src/core/components/SideMenu/index.tsx index 03a24f7..b7a4260 100644 --- a/src/core/components/SideMenu/index.tsx +++ b/src/core/components/SideMenu/index.tsx @@ -23,7 +23,6 @@ import { BrowserTabSession, Constants, } from "core/utils/utils"; -import Search from "antd/es/input/Search"; import { MyContainer } from "shared/components/MyContainer"; import { addLessonContent, @@ -45,6 +44,8 @@ import webSocketService, { removeWebSocketReconnectListener, } from "core/services/websocketService"; import { WebSocketSendMessagesCmds } from "core/utils/webSocket"; +import { useMessage } from "core/context/MessageContext"; +import styles from "./styles.module.css"; export function SideMenuContent() { const location = useLocation(); @@ -150,10 +151,9 @@ export function SideMenuContent() { }; useEffect(() => { + // subscribe to the current page const pathname = location.pathname; - setSelectedKeys(pathname); - const subscribeTopicMessage = () => { webSocketService.send({ Cmd: WebSocketSendMessagesCmds.SubscribeToTopic, @@ -165,14 +165,18 @@ export function SideMenuContent() { }; subscribeTopicMessage(); - addWebSocketReconnectListener(subscribeTopicMessage); + // set selected keys + let path = pathname.split("/"); if (path.length > 2) { + setSelectedKeys(`/${path[1]}`); // /store/:storeId/:subPage - open the store menu - setOpenKeys([`/${path[1]}/${path[2]}`]); + //setOpenKeys([`/${path[1]}/${path[2]}`]); + } else { + setSelectedKeys(pathname); } // auto close sideMenu on mobile @@ -259,16 +263,37 @@ export function SideMenuContent() { } export function SideMenuEditorContent() { - // create is dragging useState + const location = useLocation(); const [isDragging, setIsDragging] = useState(null); const { lessonId } = useParams(); + const { success, error } = useMessage(); const [form] = useForm(); const lnState = useSelector(lessonState); const currentLnId = useSelector(currentLessonId); - const [updateLessonState] = useUpdateLessonStateMutation(); + const [reqUpdateLessonState] = useUpdateLessonStateMutation(); + + useEffect(() => { + // subscribe to the current page + const pathname = location.pathname; + + const subscribeTopicMessage = () => { + webSocketService.send({ + Cmd: WebSocketSendMessagesCmds.SubscribeToTopic, + Body: { + topic: pathname, + browserTabSession: BrowserTabSession, + }, + }); + }; + + subscribeTopicMessage(); + addWebSocketReconnectListener(subscribeTopicMessage); + + return () => removeWebSocketReconnectListener(subscribeTopicMessage); + }, [location.pathname]); useEffect(() => { form.setFieldsValue({ @@ -276,14 +301,11 @@ export function SideMenuEditorContent() { }); }, [lnState]); + // ; + return ( - - { console.log("drag start", event.active.id); @@ -296,10 +318,14 @@ export function SideMenuEditorContent() { }} > {componentsGroups.map((group, i) => ( -
+
{group.category} - + {group.components.map((component, i) => ( ))} @@ -340,12 +366,15 @@ export function SideMenuEditorContent() { console.log("state changed", value, lessonId); try { - await updateLessonState({ + await reqUpdateLessonState({ lessonId: currentLnId, newState: value, }).unwrap(); + + success("Lesson state updated successfully"); } catch (err) { console.log("error", err); + error("Failed to update lesson state"); } }} > @@ -366,7 +395,7 @@ export function DraggableCreateComponent({ }: { component: Component; }) { - const dispatch = useDispatch(); + // const dispatch = useDispatch(); /*const { attributes, listeners, setNodeRef, transform, isDragging, active } = useDraggable({ @@ -394,9 +423,10 @@ export function DraggableCreateComponent({ } function CreateComponent({ component }: { component: Component }) { + const { error } = useMessage(); const dispatch = useDispatch(); - const isDarkMode = useSelector(darkMode); + const isDarkMode = useSelector(darkMode); const currentLnId = useSelector(currentLessonId); const [reqAddLessonContent] = useAddLessonContentMutation(); @@ -406,15 +436,16 @@ function CreateComponent({ component }: { component: Component }) { vertical align="center" justify="center" + className={styles.createComponentContainer} style={{ backgroundColor: !isDarkMode ? "#f0f2f5" : "#1f1f1f", height: 80, width: 80, cursor: "pointer", + borderRadius: 4, + transition: "background-color 0.2s", }} onClick={async () => { - console.log("insert component", component.type); - try { const res = await reqAddLessonContent({ lessonId: currentLnId, @@ -422,8 +453,6 @@ function CreateComponent({ component }: { component: Component }) { data: component.defaultData || "", }).unwrap(); - console.log("add content", component); - dispatch( addLessonContent({ Id: res.Id, @@ -435,15 +464,18 @@ function CreateComponent({ component }: { component: Component }) { ); } catch (err) { console.log("error", err); + error("Failed to add content"); } }} > {component.thumbnail ? (
{ const store = configureStore({ @@ -15,6 +17,8 @@ const makeStore = (/* preloadedState */) => { lessonPageEditor: lessonPageEditorSlice.reducer, teamSlice: teamSlice.reducer, [lessonsApi.reducerPath]: lessonsApi.reducer, + [lessonsSlice.reducerPath]: lessonsSlice.reducer, + [lessonPageSlice.reducerPath]: lessonPageSlice.reducer, [organizationApi.reducerPath]: organizationApi.reducer, [teamSlice.reducerPath]: teamSlice.reducer, }, diff --git a/src/core/utils/webSocket.ts b/src/core/utils/webSocket.ts index 6ef2078..9a33933 100644 --- a/src/core/utils/webSocket.ts +++ b/src/core/utils/webSocket.ts @@ -10,6 +10,15 @@ enum WebSocketReceivedMessagesCmds { TeamAddedMember = 5, TeamUpdatedMemberRole = 6, TeamDeletedMember = 7, + LessonCreated = 8, + LessonPreviewTitleUpdated = 9, + LessonPreviewThumbnailUpdated = 10, + LessonStateUpdated = 11, + LessonAddedContent = 12, + LessonDeletedContent = 13, + LessonContentUpdated = 14, + LessonContentUpdatedPosition = 15, + LessonContentFileUpdated = 16, } export { WebSocketSendMessagesCmds, WebSocketReceivedMessagesCmds }; diff --git a/src/features/Lessons/LessonPage/index.tsx b/src/features/Lessons/LessonPage/index.tsx index b519540..d0e1b66 100644 --- a/src/features/Lessons/LessonPage/index.tsx +++ b/src/features/Lessons/LessonPage/index.tsx @@ -1,63 +1,96 @@ -import { Button, Flex } from 'antd'; -import { CheckOutlined } from '@ant-design/icons'; -import HeaderBar from '../../../core/components/Header'; -import { useLocation, useNavigate, useParams } from 'react-router-dom'; -import { Constants } from '../../../core/utils/utils'; -import React from 'react'; -import MySpin from 'shared/components/MySpin'; -import MyErrorResult from 'shared/components/MyResult'; -import MyEmpty from 'shared/components/MyEmpty'; -import { useGetLessonContentsQuery } from 'core/services/lessons'; -import MyMiddleCard from 'shared/components/MyMiddleCard'; -import { Converter } from '../converter'; -import Questions from '../Questions'; +import { Button, Flex } from "antd"; +import { CheckOutlined } from "@ant-design/icons"; +import HeaderBar from "core/components/Header"; +import { useLocation, useNavigate, useParams } from "react-router-dom"; +import { Constants } from "core/utils/utils"; +import React, { useEffect } from "react"; +import MyErrorResult from "shared/components/MyResult"; +import MyEmpty from "shared/components/MyEmpty"; +import { useGetLessonContentsQuery } from "core/services/lessons"; +import MyMiddleCard from "shared/components/MyMiddleCard"; +import { Converter } from "../converter"; +import Questions from "../Questions"; +import { + addWebSocketReconnectListener, + removeWebSocketReconnectListener, +} from "core/services/websocketService"; +import { useDispatch, useSelector } from "react-redux"; +import { + lessonPageContents, + setLessonPageContents, + setLessonPageCurrentLessonId, +} from "./lessonPageSlice"; +import MyCenteredSpin from "shared/components/MyCenteredSpin"; const LessonContents: React.FC = () => { - const { lessonId } = useParams(); + const dispatch = useDispatch(); + const { lessonId } = useParams(); - const { data, error, isLoading } = useGetLessonContentsQuery(lessonId as string, { - refetchOnMountOrArgChange: true, - }); + const lessonContents = useSelector(lessonPageContents); - if (isLoading) return ; - if (error) return ; + const { data, error, isLoading, refetch } = useGetLessonContentsQuery( + lessonId as string, + { + refetchOnMountOrArgChange: true, + } + ); - if (!data || data.length === 0) return ; + useEffect(() => { + dispatch(setLessonPageCurrentLessonId(lessonId as string)); + addWebSocketReconnectListener(refetch); - return ( - <> - {data.map((lessonContent) => ( -
- -
- ))} - - ); + return () => removeWebSocketReconnectListener(refetch); + }, []); + + useEffect(() => { + if (!data) return; + + dispatch(setLessonPageContents(data)); + }, [data]); + + if (isLoading) return ; + if (error) return ; + + if (!lessonContents || lessonContents.length === 0) return ; + + return ( + <> + {lessonContents.map((lessonContent) => ( +
+ +
+ ))} + + ); }; export default function LessonPage() { - const location = useLocation(); - const navigate = useNavigate(); + const location = useLocation(); + const navigate = useNavigate(); - return ( - <> - navigate(`${location.pathname}/editor`)} /> + return ( + <> + navigate(`${location.pathname}/editor`)} + /> - - -
- } - > - + + +
+ } + > + - - - - - - ); + + + + + + ); } diff --git a/src/features/Lessons/LessonPage/lessonPageSlice.ts b/src/features/Lessons/LessonPage/lessonPageSlice.ts new file mode 100644 index 0000000..40fd8a5 --- /dev/null +++ b/src/features/Lessons/LessonPage/lessonPageSlice.ts @@ -0,0 +1,63 @@ +import { createSlice } from "@reduxjs/toolkit"; +import { LessonContent } from "core/types/lesson"; + +export const lessonPageSlice = createSlice({ + name: "lessonPage", + initialState: { + currentLessonId: "", + lessonContents: [] as LessonContent[], + }, + reducers: { + setLessonPageCurrentLessonId: (state, action) => { + state.currentLessonId = action.payload; + }, + setLessonPageContents: (state, action) => { + state.lessonContents = action.payload; + }, + addLessonPageContent: (state, action) => { + state.lessonContents.push(action.payload); + }, + deleteLessonPageContent: (state, action) => { + state.lessonContents = state.lessonContents.filter( + (content) => content.Id !== action.payload + ); + }, + updateLessonPageContent: (state, action) => { + const index = state.lessonContents.findIndex( + (content) => content.Id === action.payload.id + ); + + if (index !== -1) { + state.lessonContents[index].Data = action.payload.data; + } + }, + updateLessonPageContentPosition: (state, action) => { + // change only by contentId and new position + const content = state.lessonContents.find( + (content) => content.Id === action.payload.contentId + ); + + if (content) { + const index = state.lessonContents.indexOf(content); + state.lessonContents.splice(index, 1); + state.lessonContents.splice(action.payload.position - 1, 0, content); + } + }, + }, + selectors: { + lessonPageCurrentLessonId: (state) => state.currentLessonId, + lessonPageContents: (state) => state.lessonContents, + }, +}); + +export const { + setLessonPageCurrentLessonId, + setLessonPageContents, + addLessonPageContent, + deleteLessonPageContent, + updateLessonPageContent, + updateLessonPageContentPosition, +} = lessonPageSlice.actions; + +export const { lessonPageCurrentLessonId, lessonPageContents } = + lessonPageSlice.selectors; diff --git a/src/features/Lessons/LessonPageEditor/SortableEditorItem.tsx b/src/features/Lessons/LessonPageEditor/SortableEditorItem.tsx index e6b9a73..b8e6c48 100644 --- a/src/features/Lessons/LessonPageEditor/SortableEditorItem.tsx +++ b/src/features/Lessons/LessonPageEditor/SortableEditorItem.tsx @@ -1,94 +1,118 @@ -import { defaultAnimateLayoutChanges, useSortable } from '@dnd-kit/sortable'; -import { CSS } from '@dnd-kit/utilities'; -import { HolderOutlined, DeleteOutlined, CameraOutlined, FolderOpenOutlined } from '@ant-design/icons'; -import { Flex } from 'antd'; -import { currentLessonId, deleteLessonContent, updateLessonContent } from './lessonPageEditorSlice'; -import { useDispatch, useSelector } from 'react-redux'; -import { getComponentByType } from '../components'; -import { LessonContent } from 'core/types/lesson'; -import './styles.module.css'; -import { Converter } from '../converter'; -import { useDeleteLessonContentMutation } from 'core/services/lessons'; +import { defaultAnimateLayoutChanges, useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import { + HolderOutlined, + DeleteOutlined, + CameraOutlined, + FolderOpenOutlined, +} from "@ant-design/icons"; +import { Flex } from "antd"; +import { + currentLessonId, + deleteLessonContent, + updateLessonContent, +} from "./lessonPageEditorSlice"; +import { useDispatch, useSelector } from "react-redux"; +import { getComponentByType } from "../components"; +import { LessonContent } from "core/types/lesson"; +import "./styles.module.css"; +import { Converter } from "../converter"; +import { useDeleteLessonContentMutation } from "core/services/lessons"; -const animateLayoutChanges = (args: any) => (args.isSorting || args.wasDragging ? defaultAnimateLayoutChanges(args) : true); +const animateLayoutChanges = (args: any) => + args.isSorting || args.wasDragging ? defaultAnimateLayoutChanges(args) : true; const SortableEditorItem = (props: { item: LessonContent }) => { - const lnContent = props.item; - const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: lnContent.Id }); + const lnContent = props.item; + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: lnContent.Id }); - const dispatch = useDispatch(); + const dispatch = useDispatch(); - const component = getComponentByType(lnContent.Type); + const component = getComponentByType(lnContent.Type); - const [reqDeleteLessonContent] = useDeleteLessonContentMutation(); - const currentLnId = useSelector(currentLessonId); + const [reqDeleteLessonContent] = useDeleteLessonContentMutation(); + const currentLnId = useSelector(currentLessonId); - if (!component) { - return null; - } + if (!component) { + return null; + } - return ( -
+ + + + + - - - - - - { - console.log('edit', lnContent.Id, data); + { + console.log("edit", lnContent.Id, data); - dispatch( - updateLessonContent({ - id: lnContent.Id, - data: data, - }) - ); - }} - /> - + dispatch( + updateLessonContent({ + id: lnContent.Id, + data: data, + }) + ); + }} + /> + - -
- { - console.log('delete', lnContent.Id); - dispatch(deleteLessonContent(lnContent.Id)); + +
+ { + console.log("delete", lnContent.Id); + dispatch(deleteLessonContent(lnContent.Id)); - try { - reqDeleteLessonContent({ - lessonId: currentLnId, - contentId: lnContent.Id, - }); - } catch (err) { - console.error(err); - } - }} - /> -
-
- -
- ); + try { + reqDeleteLessonContent({ + lessonId: currentLnId, + contentId: lnContent.Id, + }); + } catch (err) { + console.error(err); + } + }} + /> +
+ + +
+ ); }; /* diff --git a/src/features/Lessons/LessonPageEditor/index.tsx b/src/features/Lessons/LessonPageEditor/index.tsx index 9c4ee46..b12a587 100644 --- a/src/features/Lessons/LessonPageEditor/index.tsx +++ b/src/features/Lessons/LessonPageEditor/index.tsx @@ -1,120 +1,169 @@ -import { useNavigate, useParams } from 'react-router-dom'; -import { lessonContents, lessonThumbnail, setCurrentLessonId, setEditorActive, setLessonContents, setLessonState } from './lessonPageEditorSlice'; -import { useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { Card, Flex } from 'antd'; -import { Constants } from 'core/utils/utils'; -import HeaderBar from 'core/components/Header'; -import Droppable from './Droppable'; -import LessonPreviewCard from 'shared/components/MyLessonPreviewCard'; -import { useGetLessonContentsQuery, useGetLessonSettingsQuery, useUpdateLessonPreviewTitleMutation } from 'core/services/lessons'; -import MyErrorResult from 'shared/components/MyResult'; -import styles from './styles.module.css'; -import MyEmpty from 'shared/components/MyEmpty'; +import { useNavigate, useParams } from "react-router-dom"; +import { + lessonContents, + lessonThumbnail, + setCurrentLessonId, + setEditorActive, + setLessonContents, + setLessonThumbnailTitle, + setLessonThumbnailUrl, + setPageEditorLessonState, +} from "./lessonPageEditorSlice"; +import { useEffect } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { Card, Flex } from "antd"; +import { Constants } from "core/utils/utils"; +import HeaderBar from "core/components/Header"; +import Droppable from "./Droppable"; +import LessonPreviewCard from "shared/components/MyLessonPreviewCard"; +import { + useGetLessonContentsQuery, + useGetLessonSettingsQuery, + useUpdateLessonPreviewTitleMutation, +} from "core/services/lessons"; +import MyErrorResult from "shared/components/MyResult"; +import styles from "./styles.module.css"; +import MyEmpty from "shared/components/MyEmpty"; +import { + addWebSocketReconnectListener, + removeWebSocketReconnectListener, +} from "core/services/websocketService"; const PreviewCard: React.FC = () => { - const dispatch = useDispatch(); - const { lessonId } = useParams(); + const dispatch = useDispatch(); + const { lessonId } = useParams(); - const { data, error, isLoading, refetch } = useGetLessonSettingsQuery(lessonId as string, { - refetchOnMountOrArgChange: true, - }); + const dataLessonThumbnail = useSelector(lessonThumbnail); - const [updateLessonPreviewTitle] = useUpdateLessonPreviewTitleMutation(); + const { data, error, isLoading, refetch } = useGetLessonSettingsQuery( + lessonId as string, + { + refetchOnMountOrArgChange: true, + } + ); - useEffect(() => { - if (data?.State) dispatch(setLessonState(data.State)); - }, [data]); + const [updateLessonPreviewTitle] = useUpdateLessonPreviewTitleMutation(); - if (error) return ; + useEffect(() => { + if (!data) return; - return ( - { - try { - const res = await updateLessonPreviewTitle({ - lessonId: lessonId as string, - newTitle: newTitle, - }).unwrap(); + dispatch(setPageEditorLessonState(data.State)); + dispatch(setLessonThumbnailTitle(data.Title)); + dispatch(setLessonThumbnailUrl(data.ThumbnailUrl)); + }, [data]); - if (res) { - refetch(); - } - } catch (err) { - console.error(err); - } - }} - onThumbnailChanged={refetch} - /> - ); + if (error) return ; + + return ( + { + try { + await updateLessonPreviewTitle({ + lessonId: lessonId as string, + newTitle: newTitle, + }).unwrap(); + + dispatch(setLessonThumbnailTitle(newTitle)); + } catch (err) { + console.error(err); + } + }} + /> + ); }; const LessonContentComponents: React.FC = () => { - const { lessonId } = useParams(); - const dispatch = useDispatch(); + const { lessonId } = useParams(); + const dispatch = useDispatch(); - const { data, error, isLoading } = useGetLessonContentsQuery(lessonId as string, { - refetchOnMountOrArgChange: true, - }); + const { data, error, isLoading, refetch } = useGetLessonContentsQuery( + lessonId as string, + { + refetchOnMountOrArgChange: true, + } + ); - const lnContents = useSelector(lessonContents); + const lnContents = useSelector(lessonContents); - useEffect(() => { - if (!data) return; + useEffect(() => { + if (!data) return; - dispatch(setLessonContents(data)); - }, [data]); + dispatch(setLessonContents(data)); + }, [data]); - if (error) return ; + useEffect(() => { + addWebSocketReconnectListener(refetch); - return ( - - - {!lnContents || lnContents.length == 0 ? : } - - - ); + return () => removeWebSocketReconnectListener(refetch); + }, []); + + if (error) return ; + + return ( + + + {!lnContents || lnContents.length == 0 ? ( + + ) : ( + + )} + + + ); }; export default function LessonPageEditor() { - const { lessonId } = useParams(); - const navigate = useNavigate(); + const { lessonId } = useParams(); + const navigate = useNavigate(); - const dispatch = useDispatch(); - const lnContents = useSelector(lessonContents); - const lnThumbnail = useSelector(lessonThumbnail); + const dispatch = useDispatch(); - useEffect(() => { - dispatch(setEditorActive(true)); - dispatch(setCurrentLessonId(lessonId as string)); + useEffect(() => { + dispatch(setEditorActive(true)); + dispatch(setCurrentLessonId(lessonId as string)); - return () => { - dispatch(setEditorActive(false)); - }; - }, []); + return () => { + dispatch(setEditorActive(false)); + }; + }, []); - return ( - <> - navigate(Constants.ROUTE_PATHS.LESSIONS.PAGE.replace(':lessonId', lessonId as string))} - /> + return ( + <> + + navigate( + Constants.ROUTE_PATHS.LESSIONS.PAGE.replace( + ":lessonId", + lessonId as string + ) + ) + } + /> - - - + + + - - - - - ); + + + + + ); } diff --git a/src/features/Lessons/LessonPageEditor/lessonPageEditorSlice.ts b/src/features/Lessons/LessonPageEditor/lessonPageEditorSlice.ts index c013869..f6734fd 100644 --- a/src/features/Lessons/LessonPageEditor/lessonPageEditorSlice.ts +++ b/src/features/Lessons/LessonPageEditor/lessonPageEditorSlice.ts @@ -12,8 +12,8 @@ export const lessonPageEditorSlice = createSlice({ editorActive: false, currentLessonId: "", // required in sideMenu because has no access to useParams lessonThumbnail: { - img: "", title: "Test", + url: "", }, lessonContents: [] as LessonContent[], lessonState: LessonState.Draft, @@ -48,6 +48,9 @@ export const lessonPageEditorSlice = createSlice({ setLessonThumbnailTitle: (state, action) => { state.lessonThumbnail.title = action.payload; }, + setLessonThumbnailUrl: (state, action) => { + state.lessonThumbnail.url = action.payload; + }, onDragHandler: (state, action) => { state.lessonContents.splice( action.payload.newIndex, @@ -55,9 +58,21 @@ export const lessonPageEditorSlice = createSlice({ state.lessonContents.splice(action.payload.oldIndex, 1)[0] ); }, - setLessonState: (state, action) => { + setPageEditorLessonState: (state, action) => { state.lessonState = action.payload; }, + updateLessonContentPosition: (state, action) => { + // change only by contentId and new position + const content = state.lessonContents.find( + (content) => content.Id === action.payload.contentId + ); + + if (content) { + const index = state.lessonContents.indexOf(content); + state.lessonContents.splice(index, 1); + state.lessonContents.splice(action.payload.position - 1, 0, content); + } + }, }, selectors: { editorActive: (state) => state.editorActive, @@ -76,8 +91,10 @@ export const { setLessonContents, updateLessonContent, setLessonThumbnailTitle, + setLessonThumbnailUrl, onDragHandler, - setLessonState, + setPageEditorLessonState, + updateLessonContentPosition, } = lessonPageEditorSlice.actions; export const { diff --git a/src/features/Lessons/Questions/index.tsx b/src/features/Lessons/Questions/index.tsx index da3c706..d2dc7f1 100644 --- a/src/features/Lessons/Questions/index.tsx +++ b/src/features/Lessons/Questions/index.tsx @@ -1,296 +1,342 @@ -import { DownOutlined, HeartFilled, HeartOutlined } from '@ant-design/icons'; -import { Avatar, Button, Card, Collapse, Divider, Flex, Form, Input, InputRef, Typography } from 'antd'; -import Meta from 'antd/es/card/Meta'; -import TextArea from 'antd/es/input/TextArea'; -import { LessonQuestion, LessonQuestionReply } from 'core/types/lesson'; -import { Constants } from 'core/utils/utils'; -import React, { useRef } from 'react'; +import { HeartFilled, HeartOutlined } from "@ant-design/icons"; +import { Avatar, Button, Flex, Form, Input, InputRef, Typography } from "antd"; +import { LessonQuestion, LessonQuestionReply } from "core/types/lesson"; +import { Constants } from "core/utils/utils"; +import React, { useRef } from "react"; export default function Questions({ lessionID }: { lessionID: string }) { - let questions: LessonQuestion[] = [ - { - Id: '1', - LessionId: '1', - Question: 'What is the capital of Germany?', - Likes: 5, - CreatorUserId: '1', - CreatedAt: '2021-09-01T12:00:00Z', - UpdatedAt: '2021-09-01T12:00:00Z', - }, - { - Id: '2', - LessionId: '1', - Question: 'What is the capital of France?', - Likes: 3, - CreatorUserId: '2', - CreatedAt: '2021-09-01T12:00:00Z', - UpdatedAt: '2021-09-01T12:00:00Z', - }, - { - Id: '3', - LessionId: '1', - Question: 'What is the capital of Italy?', - Likes: 2, - CreatorUserId: '3', - CreatedAt: '2021-09-01T12:00:00Z', - UpdatedAt: '2021-09-01T12:00:00Z', - }, - ]; + let questions: LessonQuestion[] = [ + { + Id: "1", + LessionId: "1", + Question: "What is the capital of Germany?", + Likes: 5, + CreatorUserId: "1", + CreatedAt: "2021-09-01T12:00:00Z", + UpdatedAt: "2021-09-01T12:00:00Z", + }, + { + Id: "2", + LessionId: "1", + Question: "What is the capital of France?", + Likes: 3, + CreatorUserId: "2", + CreatedAt: "2021-09-01T12:00:00Z", + UpdatedAt: "2021-09-01T12:00:00Z", + }, + { + Id: "3", + LessionId: "1", + Question: "What is the capital of Italy?", + Likes: 2, + CreatorUserId: "3", + CreatedAt: "2021-09-01T12:00:00Z", + UpdatedAt: "2021-09-01T12:00:00Z", + }, + ]; - return ( - - - Questions -
- - - - - - -
- - {questions.map((question) => ( - - ))} - -
+ return ( + + + Questions +
+ + + + + + +
+ + {questions.map((question) => ( + + ))} - ); +
+
+ ); } type HandleReplyFunction = (text: string, replyID?: string) => Promise; export function QuestionItem({ question }: { question: LessonQuestion }) { - const [showReplies, setShowReplies] = React.useState(1); + const [showReplies, setShowReplies] = React.useState(1); - let user = { - Id: '132154153613', - FirstName: 'Anja', - LastName: 'Blasinstroment', - }; + let user = { + Id: "132154153613", + FirstName: "Anja", + LastName: "Blasinstroment", + }; - let questionsReplys: LessonQuestionReply[] = [ - { - Id: '1', - QuestionId: '1', - Reply: 'Berlin', - Likes: 5, - CreatorUserId: '1', - CreatedAt: '2021-09-01T12:00:00Z', - UpdatedAt: '2021-09-01T12:00:00Z', - }, - { - Id: '2', - QuestionId: '1', - Reply: 'Munich', - Likes: 3, - CreatorUserId: '2', - CreatedAt: '2021-09-01T12:00:00Z', - UpdatedAt: '2021-09-01T12:00:00Z', - }, - { - Id: '3', - QuestionId: '1', - Reply: 'Hamburg', - Likes: 2, - CreatorUserId: '3', - CreatedAt: '2021-09-01T12:00:00Z', - UpdatedAt: '2021-09-01T12:00:00Z', - }, - { - Id: '4', - QuestionId: '1', - Reply: 'Cologne', - Likes: 0, - CreatorUserId: '3', - CreatedAt: '2021-09-01T12:00:00Z', - UpdatedAt: '2021-09-01T12:00:00Z', - }, - { - Id: '5', - QuestionId: '1', - Reply: 'Frankfurt', - Likes: 0, - CreatorUserId: '3', - CreatedAt: '2021-09-01T12:00:00Z', - UpdatedAt: '2021-09-01T12:00:00Z', - }, - { - Id: '6', - QuestionId: '1', - Reply: 'Stuttgart', - Likes: 2, - CreatorUserId: '3', - CreatedAt: '2021-09-01T12:00:00Z', - UpdatedAt: '2021-09-01T12:00:00Z', - }, - { - Id: '7', - QuestionId: '1', - Reply: 'Düsseldorf', - Likes: 10, - CreatorUserId: '3', - CreatedAt: '2021-09-01T12:00:00Z', - UpdatedAt: '2021-09-01T12:00:00Z', - }, - ]; + let questionsReplys: LessonQuestionReply[] = [ + { + Id: "1", + QuestionId: "1", + Reply: "Berlin", + Likes: 5, + CreatorUserId: "1", + CreatedAt: "2021-09-01T12:00:00Z", + UpdatedAt: "2021-09-01T12:00:00Z", + }, + { + Id: "2", + QuestionId: "1", + Reply: "Munich", + Likes: 3, + CreatorUserId: "2", + CreatedAt: "2021-09-01T12:00:00Z", + UpdatedAt: "2021-09-01T12:00:00Z", + }, + { + Id: "3", + QuestionId: "1", + Reply: "Hamburg", + Likes: 2, + CreatorUserId: "3", + CreatedAt: "2021-09-01T12:00:00Z", + UpdatedAt: "2021-09-01T12:00:00Z", + }, + { + Id: "4", + QuestionId: "1", + Reply: "Cologne", + Likes: 0, + CreatorUserId: "3", + CreatedAt: "2021-09-01T12:00:00Z", + UpdatedAt: "2021-09-01T12:00:00Z", + }, + { + Id: "5", + QuestionId: "1", + Reply: "Frankfurt", + Likes: 0, + CreatorUserId: "3", + CreatedAt: "2021-09-01T12:00:00Z", + UpdatedAt: "2021-09-01T12:00:00Z", + }, + { + Id: "6", + QuestionId: "1", + Reply: "Stuttgart", + Likes: 2, + CreatorUserId: "3", + CreatedAt: "2021-09-01T12:00:00Z", + UpdatedAt: "2021-09-01T12:00:00Z", + }, + { + Id: "7", + QuestionId: "1", + Reply: "Düsseldorf", + Likes: 10, + CreatorUserId: "3", + CreatedAt: "2021-09-01T12:00:00Z", + UpdatedAt: "2021-09-01T12:00:00Z", + }, + ]; - async function handleReply(text: string, replyID?: string) { - console.log('reply', text); - await new Promise((resolve) => setTimeout(resolve, 1000)); - } + async function handleReply(text: string, replyID?: string) { + console.log("reply", text); + await new Promise((resolve) => setTimeout(resolve, 1000)); + } - return ( - - {(() => { - let nodes = []; + return ( + + {(() => { + let nodes = []; - for (let i = 0; i < questionsReplys.length; i++) { - if (i > showReplies - 1) { - nodes.push( - - ); - break; - } + for (let i = 0; i < questionsReplys.length; i++) { + if (i > showReplies - 1) { + nodes.push( + + ); + break; + } - nodes.push(); - } - - return nodes; - })()} - + nodes.push( + + ); } - likes={question.Likes} - onReply={handleReply} - onLike={() => {}} - replyID={undefined} - /> - ); + + return nodes; + })()} + + } + likes={question.Likes} + onReply={handleReply} + onLike={() => {}} + replyID={undefined} + /> + ); } -export function QuestionReplyItem({ question, handleReply }: { question: LessonQuestionReply; handleReply: HandleReplyFunction }) { - let user = { - Id: '132154153613', - FirstName: 'Anja', - LastName: 'Blasinstroment', - }; +export function QuestionReplyItem({ + question, + handleReply, +}: { + question: LessonQuestionReply; + handleReply: HandleReplyFunction; +}) { + let user = { + Id: "132154153613", + FirstName: "Anja", + LastName: "Blasinstroment", + }; - return } likes={question.Likes} onReply={handleReply} onLike={() => {}} replyID={question.Id} />; + return ( + } + likes={question.Likes} + onReply={handleReply} + onLike={() => {}} + replyID={question.Id} + /> + ); } export function QuestionUIRaw({ - userID, - text, - childContent, - likes, - replyID, - onReply, - onLike, + userID, + text, + childContent, + likes, + replyID, + onReply, + onLike, }: { - userID: string; - text: string; - childContent: React.ReactNode; - likes: number; - replyID?: string; - onReply: HandleReplyFunction; - onLike: () => void; + userID: string; + text: string; + childContent: React.ReactNode; + likes: number; + replyID?: string; + onReply: HandleReplyFunction; + onLike: () => void; }) { - const [hasLiked, setHasLiked] = React.useState(false); + const [hasLiked, setHasLiked] = React.useState(false); - const [replyFormVisible, setReplyFormVisible] = React.useState(false); - const [replyText, setReplyText] = React.useState(null); - const [isSendingReply, setIsSendingReply] = React.useState(false); + const [replyFormVisible, setReplyFormVisible] = React.useState(false); + const [replyText, setReplyText] = React.useState(null); + const [isSendingReply, setIsSendingReply] = React.useState(false); - let user = { - Id: '132154153613', - FirstName: 'Anja', - LastName: 'Blasinstroment', - }; + let user = { + Id: "132154153613", + FirstName: "Anja", + LastName: "Blasinstroment", + }; - const userAt = `@${user.FirstName} ${user.LastName} `; + const userAt = `@${user.FirstName} ${user.LastName} `; - async function toggleLike() { - setHasLiked(!hasLiked); - } + async function toggleLike() { + setHasLiked(!hasLiked); + } - // useref to focus on the input field - const inputRef = useRef(null); + // useref to focus on the input field + const inputRef = useRef(null); - return ( - <> - - - - - {user.FirstName} {user.LastName} - - {text} - - + return ( + <> + + + + + {user.FirstName} {user.LastName} + + + {text} + + + - {likes >= 1 ? likes : ' '} - - - {replyFormVisible ? ( -
{ - setIsSendingReply(true); - await onReply(replyText ? replyText : '', replyID); + setTimeout(() => { + if (inputRef.current) { + const input = inputRef.current; + input.focus({ cursor: "end" }); + } + }, 100); + }} + > + {replyFormVisible ? "Hide" : "Reply"} + + + {replyFormVisible ? ( + { + setIsSendingReply(true); + await onReply(replyText ? replyText : "", replyID); - setIsSendingReply(false); - setReplyFormVisible(false); - setReplyText(null); - }} - > - - setReplyText(e.target.value)} - /> - - - - -
- ) : null} - {childContent} -
-
- - ); + setIsSendingReply(false); + setReplyFormVisible(false); + setReplyText(null); + }} + > + + setReplyText(e.target.value)} + /> + + + + + + ) : null} + {childContent} +
+
+ + ); } diff --git a/src/features/Lessons/components.ts b/src/features/Lessons/components.ts index cb5a2ed..2d6fca5 100644 --- a/src/features/Lessons/components.ts +++ b/src/features/Lessons/components.ts @@ -1,65 +1,65 @@ // Desc: This file contains the list of components that are used in the Lessons type ComponentGroup = { - category: string; - components: Component[]; + category: string; + components: Component[]; }; export type Component = { - type: number; - name: string; - thumbnail?: string; - invertThumbnailAtDarkmode?: boolean; - uploadFileTypes?: string[]; - uploadImage?: boolean; - defaultData?: string; + type: number; + name: string; + thumbnail?: string; + invertThumbnailAtDarkmode?: boolean; + uploadFileTypes?: string[]; + uploadImage?: boolean; + defaultData?: string; }; const componentsGroups: ComponentGroup[] = [ - { - category: 'Common', - components: [ - { - type: 0, - name: 'Header', - thumbnail: '/editor/thumbnails/component_thumbnail_header.svg', - invertThumbnailAtDarkmode: true, - defaultData: 'Header', - }, - { - type: 1, - name: 'Text', - thumbnail: '/editor/thumbnails/component_thumbnail_text.svg', - invertThumbnailAtDarkmode: true, - defaultData: 'Text', - }, - ], - }, - { - category: 'Media', - components: [ - { - type: 2, - name: 'Image', - thumbnail: '/editor/thumbnails/component_thumbnail_image.png', - uploadImage: true, - uploadFileTypes: ['image/*'], - }, - { - type: 3, - name: 'YouTube', - thumbnail: '/editor/thumbnails/component_thumbnail_youtube.png', - invertThumbnailAtDarkmode: true, - }, - { - type: 4, - name: 'Video', - thumbnail: '/editor/thumbnails/component_thumbnail_youtube.png', - invertThumbnailAtDarkmode: true, - }, - ], - }, - /* + { + category: "Common", + components: [ + { + type: 0, + name: "Header", + thumbnail: "/editor/thumbnails/component_thumbnail_header.svg", + invertThumbnailAtDarkmode: true, + defaultData: "Header", + }, + { + type: 1, + name: "Text", + thumbnail: "/editor/thumbnails/component_thumbnail_text.svg", + invertThumbnailAtDarkmode: true, + defaultData: "Text", + }, + ], + }, + { + category: "Media", + components: [ + { + type: 2, + name: "Image", + thumbnail: "/editor/thumbnails/component_thumbnail_image.png", + uploadImage: true, + uploadFileTypes: ["image/*"], + }, + { + type: 3, + name: "YouTube", + thumbnail: "/editor/thumbnails/component_thumbnail_youtube.png", + invertThumbnailAtDarkmode: true, + }, + { + type: 4, + name: "Video", + thumbnail: "/editor/thumbnails/component_thumbnail_youtube.png", + invertThumbnailAtDarkmode: true, + }, + ], + }, + /* { category: "HTML", components: [ @@ -84,30 +84,30 @@ const componentsGroups: ComponentGroup[] = [ ]; const componentsMap: { [key: string]: Component } = (() => { - const map: { [key: string]: Component } = {}; + const map: { [key: string]: Component } = {}; - for (const group of componentsGroups) { - for (const component of group.components) { - map[component.name] = component; - } + for (const group of componentsGroups) { + for (const component of group.components) { + map[component.name] = component; } - return map; + } + return map; })(); export function getTypeByName(name: string): number { - const component = componentsMap[name]; + const component = componentsMap[name]; - return component ? component.type : -1; + return component ? component.type : -1; } export function getComponentByType(type: number): Component | null { - for (const component of Object.values(componentsMap)) { - if (component.type === type) { - return component; - } + for (const component of Object.values(componentsMap)) { + if (component.type === type) { + return component; } + } - return null; + return null; } export { componentsGroups }; diff --git a/src/features/Lessons/index.tsx b/src/features/Lessons/index.tsx index fb69b5a..f82fefd 100644 --- a/src/features/Lessons/index.tsx +++ b/src/features/Lessons/index.tsx @@ -9,21 +9,32 @@ import { useCreateLessonMutation, useGetLessonsQuery, } from "core/services/lessons"; -import MySpin from "shared/components/MySpin"; import { Constants } from "core/utils/utils"; import MyEmpty from "shared/components/MyEmpty"; import MyErrorResult from "shared/components/MyResult"; import LessonPreviewCard from "shared/components/MyLessonPreviewCard"; +import { useMessage } from "core/context/MessageContext"; +import { useEffect } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { lessons, setLessons } from "./lessonsSlice"; +import { + addWebSocketReconnectListener, + removeWebSocketReconnectListener, +} from "core/services/websocketService"; +import MyCenteredSpin from "shared/components/MyCenteredSpin"; const CreateLessonButton: React.FC = () => { const navigate = useNavigate(); const [createLesson, { isLoading }] = useCreateLessonMutation(); + const { success, error } = useMessage(); const handleCreateLesson = async () => { try { const res = await createLesson({}).unwrap(); if (res && res.Id) { + success("Lesson created successfully "); + navigate( Constants.ROUTE_PATHS.LESSIONS.PAGE_EDITOR.replace( ":lessonId", @@ -33,6 +44,7 @@ const CreateLessonButton: React.FC = () => { } } catch (err) { console.error(err); + error("Failed to create lesson"); } }; @@ -48,17 +60,32 @@ const CreateLessonButton: React.FC = () => { }; const LessonList: React.FC = () => { - const { data, error, isLoading } = useGetLessonsQuery(undefined, { + const dispatch = useDispatch(); + const dataLessons = useSelector(lessons); + + const { data, error, isLoading, refetch } = useGetLessonsQuery(undefined, { refetchOnMountOrArgChange: true, }); - if (isLoading) return ; + useEffect(() => { + if (!data) return; + + dispatch(setLessons(data)); + }, [data]); + + useEffect(() => { + addWebSocketReconnectListener(refetch); + + return () => removeWebSocketReconnectListener(refetch); + }, []); + + if (isLoading) return ; if (error) return ; - if (!data || data.length === 0) return ; + if (!dataLessons || dataLessons.length === 0) return ; - const publishedItems = data.filter((item) => item.State === 1); - const unpublishedItems = data.filter((item) => item.State === 2); + const publishedItems = dataLessons.filter((item) => item.State === 1); + const unpublishedItems = dataLessons.filter((item) => item.State === 2); return ( <> diff --git a/src/features/Lessons/lessonsSlice.ts b/src/features/Lessons/lessonsSlice.ts new file mode 100644 index 0000000..a25438b --- /dev/null +++ b/src/features/Lessons/lessonsSlice.ts @@ -0,0 +1,62 @@ +import { createSlice } from "@reduxjs/toolkit"; +import { Lesson } from "core/types/lesson"; + +interface AddLessonAction { + type: string; + payload: Lesson; +} + +export const lessonsSlice = createSlice({ + name: "lessons", + initialState: { + lessons: [] as Lesson[], + }, + reducers: { + setLessons: (state, action) => { + state.lessons = action.payload; + }, + addLesson: (state, action: AddLessonAction) => { + state.lessons.push(action.payload); + }, + updateLessonPreviewTitle: (state, action) => { + const lesson = state.lessons.find( + (lesson) => lesson.Id === action.payload.LessonId + ); + + if (lesson) { + lesson.Title = action.payload.Title; + } + }, + updateLessonPreviewThumbnail: (state, action) => { + const lesson = state.lessons.find( + (lesson) => lesson.Id === action.payload.LessonId + ); + + if (lesson) { + lesson.ThumbnailUrl = action.payload.ThumbnailUrl; + } + }, + updateLessonState: (state, action) => { + const lesson = state.lessons.find( + (lesson) => lesson.Id === action.payload.LessonId + ); + + if (lesson) { + lesson.State = action.payload.State; + } + }, + }, + selectors: { + lessons: (state) => state.lessons, + }, +}); + +export const { + setLessons, + addLesson, + updateLessonPreviewTitle, + updateLessonPreviewThumbnail, + updateLessonState, +} = lessonsSlice.actions; + +export const { lessons } = lessonsSlice.selectors; diff --git a/src/features/Roles/index.tsx b/src/features/Roles/index.tsx index 5eb9780..c04aae1 100644 --- a/src/features/Roles/index.tsx +++ b/src/features/Roles/index.tsx @@ -7,12 +7,23 @@ import MyEmpty from "shared/components/MyEmpty"; import MyCenteredSpin from "shared/components/MyCenteredSpin"; import { Role } from "core/types/organization"; import { useGetRolesQuery } from "core/services/organization"; +import { useEffect } from "react"; +import { + addWebSocketReconnectListener, + removeWebSocketReconnectListener, +} from "core/services/websocketService"; export default function Roles() { - const { data, error, isLoading } = useGetRolesQuery(undefined, { + const { data, error, isLoading, refetch } = useGetRolesQuery(undefined, { refetchOnMountOrArgChange: true, }); + useEffect(() => { + addWebSocketReconnectListener(refetch); + + return () => removeWebSocketReconnectListener(refetch); + }, []); + return ( <> } /> diff --git a/src/features/Settings/index.tsx b/src/features/Settings/index.tsx index 9ea8de7..1437c8c 100644 --- a/src/features/Settings/index.tsx +++ b/src/features/Settings/index.tsx @@ -26,6 +26,10 @@ import { import MyMiddleCard from "shared/components/MyMiddleCard"; import { OrganizationSettings } from "core/types/organization"; import { useMessage } from "core/context/MessageContext"; +import { + addWebSocketReconnectListener, + removeWebSocketReconnectListener, +} from "core/services/websocketService"; type GeneralFieldType = { primaryColor: string | AggregationColor; @@ -33,13 +37,19 @@ type GeneralFieldType = { }; export default function Settings() { - const { data, error, isLoading } = useGetOrganizationSettingsQuery( + const { data, error, isLoading, refetch } = useGetOrganizationSettingsQuery( undefined, { refetchOnMountOrArgChange: true, } ); + useEffect(() => { + addWebSocketReconnectListener(refetch); + + return () => removeWebSocketReconnectListener(refetch); + }, []); + if (error) return ; return (