diff --git a/public/index.html b/public/index.html
index aa069f2..b4fc862 100644
--- a/public/index.html
+++ b/public/index.html
@@ -24,7 +24,7 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
-
React App
+ App
You need to enable JavaScript to run this app.
diff --git a/src/core/reducers/appSlice.tsx b/src/core/reducers/appSlice.tsx
index fbddf49..e9e2e4e 100644
--- a/src/core/reducers/appSlice.tsx
+++ b/src/core/reducers/appSlice.tsx
@@ -1,45 +1,67 @@
-import { createSlice } from '@reduxjs/toolkit';
+import { createSlice } from "@reduxjs/toolkit";
+import { CachedUser } from "core/types/userProfile";
export const appSlice = createSlice({
- name: 'app',
- initialState: {
- darkMode: false,
- userAuthenticated: null,
- userProfilePictureUrl: null,
- primaryColor: '#111',
- logoUrl: null,
- bannerUrl: null,
+ name: "app",
+ initialState: {
+ darkMode: false,
+ userAuthenticated: null,
+ userProfilePictureUrl: null,
+ primaryColor: "#111",
+ logoUrl: null,
+ bannerUrl: null,
+ cachedUsers: {} as Record,
+ },
+ reducers: {
+ setDarkMode: (state, action) => {
+ state.darkMode = action.payload;
},
- reducers: {
- setDarkMode: (state, action) => {
- state.darkMode = action.payload;
- },
- setUserAuthenticated: (state, action) => {
- state.userAuthenticated = action.payload;
- },
- setUserProfilePictureUrl: (state, action) => {
- state.userProfilePictureUrl = action.payload;
- },
- setPrimaryColor: (state, action) => {
- state.primaryColor = action.payload;
- },
- setLogoUrl: (state, action) => {
- state.logoUrl = action.payload;
- },
- setBannerUrl: (state, action) => {
- state.bannerUrl = action.payload;
- },
+ setUserAuthenticated: (state, action) => {
+ state.userAuthenticated = action.payload;
},
- selectors: {
- darkMode: (state) => state.darkMode,
- userAuthenticated: (state) => state.userAuthenticated,
- userProfilePictureUrl: (state) => state.userProfilePictureUrl,
- primaryColor: (state) => state.primaryColor,
- logoUrl: (state) => state.logoUrl,
- bannerUrl: (state) => state.bannerUrl,
+ setUserProfilePictureUrl: (state, action) => {
+ state.userProfilePictureUrl = action.payload;
},
+ setPrimaryColor: (state, action) => {
+ state.primaryColor = action.payload;
+ },
+ setLogoUrl: (state, action) => {
+ state.logoUrl = action.payload;
+ },
+ setBannerUrl: (state, action) => {
+ state.bannerUrl = action.payload;
+ },
+ addUserToCache: (state, action) => {
+ state.cachedUsers[action.payload.id] = action.payload;
+ },
+ },
+ selectors: {
+ darkMode: (state) => state.darkMode,
+ userAuthenticated: (state) => state.userAuthenticated,
+ userProfilePictureUrl: (state) => state.userProfilePictureUrl,
+ primaryColor: (state) => state.primaryColor,
+ logoUrl: (state) => state.logoUrl,
+ bannerUrl: (state) => state.bannerUrl,
+ cachedUsers: (state) => state.cachedUsers,
+ },
});
-export const { setDarkMode, setUserAuthenticated, setUserProfilePictureUrl, setPrimaryColor, setLogoUrl, setBannerUrl } = appSlice.actions;
+export const {
+ setDarkMode,
+ setUserAuthenticated,
+ setUserProfilePictureUrl,
+ setPrimaryColor,
+ setLogoUrl,
+ setBannerUrl,
+ addUserToCache,
+} = appSlice.actions;
-export const { darkMode, userAuthenticated, userProfilePictureUrl, primaryColor, logoUrl, bannerUrl } = appSlice.selectors;
+export const {
+ darkMode,
+ userAuthenticated,
+ userProfilePictureUrl,
+ primaryColor,
+ logoUrl,
+ bannerUrl,
+ cachedUsers,
+} = appSlice.selectors;
diff --git a/src/core/services/cachedUser.ts b/src/core/services/cachedUser.ts
new file mode 100644
index 0000000..923ba91
--- /dev/null
+++ b/src/core/services/cachedUser.ts
@@ -0,0 +1,30 @@
+import { useDispatch, useSelector } from "react-redux";
+import { useGetUserQuery } from "./userProfile";
+import { addUserToCache, cachedUsers } from "core/reducers/appSlice";
+import { CachedUser } from "core/types/userProfile";
+import { useEffect } from "react";
+
+export function useCachedUser(userId: string) {
+ const dispatch = useDispatch();
+ const cachedUser = useSelector(cachedUsers)[userId] as CachedUser;
+
+ const {
+ data: user,
+ error,
+ isLoading,
+ } = useGetUserQuery(userId, {
+ skip: !!cachedUser, // Skip the query if the user is already cached
+ });
+
+ useEffect(() => {
+ if (user) {
+ dispatch(addUserToCache(user));
+ }
+ }, [user]);
+
+ return {
+ user: cachedUser || user,
+ error,
+ isLoading,
+ };
+}
diff --git a/src/core/services/lessons.ts b/src/core/services/lessons.ts
index 479dbcd..7b3f44a 100644
--- a/src/core/services/lessons.ts
+++ b/src/core/services/lessons.ts
@@ -5,6 +5,7 @@ import {
LessonContent,
LessonQuestion,
LessonSettings,
+ QuestionsResponse,
UpdateLessonPreviewThumbnail,
} from "core/types/lesson";
@@ -94,12 +95,42 @@ export const lessonsApi = createApi({
method: "DELETE",
}),
}),
- getQuestions: builder.query({
+ getLessonQuestions: builder.query({
query: (lessonId) => ({
url: `lessons/${lessonId}/questions`,
method: "GET",
}),
}),
+ createLessonQuestion: builder.mutation({
+ query: ({ lessonId, message }) => ({
+ url: `lessons/${lessonId}/questions`,
+ method: "POST",
+ body: {
+ Message: message,
+ },
+ }),
+ }),
+ createLessonQuestionReply: builder.mutation({
+ query: ({ lessonId, questionId, message }) => ({
+ url: `lessons/${lessonId}/questions/${questionId}/replies`,
+ method: "POST",
+ body: {
+ Message: message,
+ },
+ }),
+ }),
+ likeQuestion: builder.mutation({
+ query: ({ lessonId, questionId }) => ({
+ url: `lessons/${lessonId}/questions/${questionId}/likes`,
+ method: "POST",
+ }),
+ }),
+ dislikeQuestion: builder.mutation({
+ query: ({ lessonId, questionId }) => ({
+ url: `lessons/${lessonId}/questions/${questionId}/likes`,
+ method: "DELETE",
+ }),
+ }),
}),
});
@@ -115,4 +146,9 @@ export const {
useUpdateLessonContentMutation,
useUpdateLessonContentPositionMutation,
useDeleteLessonContentMutation,
+ useGetLessonQuestionsQuery,
+ useCreateLessonQuestionMutation,
+ useCreateLessonQuestionReplyMutation,
+ useLikeQuestionMutation,
+ useDislikeQuestionMutation,
} = lessonsApi;
diff --git a/src/core/services/userProfile.ts b/src/core/services/userProfile.ts
index 79aa09f..a2d75ef 100644
--- a/src/core/services/userProfile.ts
+++ b/src/core/services/userProfile.ts
@@ -1,6 +1,6 @@
import { createApi } from "@reduxjs/toolkit/query/react";
import { baseQueryWithErrorHandling } from "core/helper/api";
-import { UserProfile } from "core/types/userProfile";
+import { CachedUser, UserProfile } from "core/types/userProfile";
export const userProfileApi = createApi({
reducerPath: "userProfileApi",
@@ -12,7 +12,13 @@ export const userProfileApi = createApi({
method: "GET",
}),
}),
+ getUser: builder.query({
+ query: (userId) => ({
+ url: `user/${userId}`,
+ method: "GET",
+ }),
+ }),
}),
});
-export const { useGetUserProfileQuery } = userProfileApi;
+export const { useGetUserProfileQuery, useGetUserQuery } = userProfileApi;
diff --git a/src/core/services/websocketService.ts b/src/core/services/websocketService.ts
index 28d3b84..b287582 100644
--- a/src/core/services/websocketService.ts
+++ b/src/core/services/websocketService.ts
@@ -1,255 +1,312 @@
-import { Dispatch } from '@reduxjs/toolkit';
-import { setBannerUrl, setLogoUrl, setPrimaryColor, setUserProfilePictureUrl } from 'core/reducers/appSlice';
-import { store } from 'core/store/store';
-import { BrowserTabSession, Constants } from 'core/utils/utils';
-import { WebSocketReceivedMessagesCmds } from 'core/utils/webSocket';
-import { addLessonPageContent, deleteLessonPageContent, updateLessonPageContent, updateLessonPageContentPosition } from 'features/Lessons/LessonPage/lessonPageSlice';
+import { Dispatch } from "@reduxjs/toolkit";
import {
- addLessonContent,
- deleteLessonContent,
- setLessonThumbnailTitle,
- setLessonThumbnailUrl,
- setPageEditorLessonState,
- updateLessonContent,
- updateLessonContentPosition,
-} from 'features/Lessons/LessonPageEditor/lessonPageEditorSlice';
-import { addLesson, updateLessonPreviewThumbnail, updateLessonPreviewTitle, updateLessonState } from 'features/Lessons/lessonsSlice';
-import { addTeamMember, deleteTeamMember, updateTeamMemberRole } from 'features/Team/teamSlice';
-import { setProfilePictureUrl } from 'features/UserProfile/userProfileSlice';
+ setBannerUrl,
+ setLogoUrl,
+ setPrimaryColor,
+ setUserProfilePictureUrl,
+} from "core/reducers/appSlice";
+import { store } from "core/store/store";
+import { BrowserTabSession, Constants } from "core/utils/utils";
+import { WebSocketReceivedMessagesCmds } from "core/utils/webSocket";
+import {
+ addLessonPageContent,
+ deleteLessonPageContent,
+ updateLessonPageContent,
+ updateLessonPageContentPosition,
+} from "features/Lessons/LessonPage/lessonPageSlice";
+import {
+ addLessonContent,
+ deleteLessonContent,
+ setLessonThumbnailTitle,
+ setLessonThumbnailUrl,
+ setPageEditorLessonState,
+ updateLessonContent,
+ updateLessonContentPosition,
+} from "features/Lessons/LessonPageEditor/lessonPageEditorSlice";
+import {
+ addLesson,
+ updateLessonPreviewThumbnail,
+ updateLessonPreviewTitle,
+ updateLessonState,
+} from "features/Lessons/lessonsSlice";
+import {
+ addLikedQuestion,
+ addQuestion,
+ countDownLikedQuestion,
+ countUpLikedQuestion,
+ deleteLikedQuestion,
+} from "features/Lessons/Questions/lessonQuestionSlice";
+import {
+ addTeamMember,
+ deleteTeamMember,
+ updateTeamMemberRole,
+} from "features/Team/teamSlice";
+import { setProfilePictureUrl } from "features/UserProfile/userProfileSlice";
interface WebSocketMessage {
- Cmd: number;
- Body: any;
+ Cmd: number;
+ Body: any;
}
class WebSocketService {
- private url: string;
- private socket: WebSocket | null = null;
- private reconnectInterval: number = 2000; // in ms
- private offlineQueue: WebSocketMessage[] = [];
- private firstConnect: boolean = true;
+ private url: string;
+ private socket: WebSocket | null = null;
+ private reconnectInterval: number = 2000; // in ms
+ private offlineQueue: WebSocketMessage[] = [];
+ private firstConnect: boolean = true;
- private messageHandler: ((message: WebSocketMessage, dispatch: Dispatch) => void) | null = null;
+ private messageHandler:
+ | ((message: WebSocketMessage, dispatch: Dispatch) => void)
+ | null = null;
- constructor(url: string) {
- this.url = url;
+ constructor(url: string) {
+ this.url = url;
+ }
+
+ private dispatch: Dispatch | null = null;
+
+ public connect(): void {
+ this.socket = new WebSocket(
+ `${this.url}?auth=${localStorage.getItem(
+ "session"
+ )}&bts=${BrowserTabSession}`
+ );
+
+ this.socket.onopen = () => {
+ console.log("WebSocket connected", this.firstConnect);
+
+ // Send all messages from the offline queue
+
+ this.offlineQueue.forEach((message) => this.send(message));
+ this.offlineQueue = [];
+
+ // Dispatch event to notify that the WebSocket connection is established
+
+ if (!this.firstConnect) {
+ document.dispatchEvent(webSocketConnectionEvent);
+ } else {
+ this.firstConnect = false;
+ }
+ };
+
+ this.socket.onmessage = (event: MessageEvent) => {
+ const data: WebSocketMessage = JSON.parse(event.data);
+
+ if (this.messageHandler) {
+ this.messageHandler(data, this.dispatch!);
+ } else {
+ console.error("No handler defined for WebSocket messages");
+ }
+ };
+
+ this.socket.onclose = () => {
+ console.log("WebSocket disconnected. Reconnecting...");
+ setTimeout(() => this.connect(), this.reconnectInterval);
+ };
+
+ this.socket.onerror = (error: Event) => {
+ console.error("WebSocket error:", error);
+ };
+ }
+
+ public setHandler(
+ handler: (message: WebSocketMessage, dispatch: Dispatch) => void,
+ dispatch: Dispatch
+ ): void {
+ this.messageHandler = handler;
+ this.dispatch = dispatch;
+ }
+
+ public send(message: WebSocketMessage): void {
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
+ this.socket.send(
+ JSON.stringify({
+ Cmd: message.Cmd,
+ Body: message.Body,
+ })
+ );
+ } else {
+ this.offlineQueue.push(message);
}
+ }
- private dispatch: Dispatch | null = null;
-
- public connect(): void {
- this.socket = new WebSocket(`${this.url}?auth=${localStorage.getItem('session')}&bts=${BrowserTabSession}`);
-
- this.socket.onopen = () => {
- console.log('WebSocket connected', this.firstConnect);
-
- // Send all messages from the offline queue
-
- this.offlineQueue.forEach((message) => this.send(message));
- this.offlineQueue = [];
-
- // Dispatch event to notify that the WebSocket connection is established
-
- if (!this.firstConnect) {
- document.dispatchEvent(webSocketConnectionEvent);
- } else {
- this.firstConnect = false;
- }
- };
-
- this.socket.onmessage = (event: MessageEvent) => {
- const data: WebSocketMessage = JSON.parse(event.data);
-
- if (this.messageHandler) {
- this.messageHandler(data, this.dispatch!);
- } else {
- console.error('No handler defined for WebSocket messages');
- }
- };
-
- this.socket.onclose = () => {
- console.log('WebSocket disconnected. Reconnecting...');
- setTimeout(() => this.connect(), this.reconnectInterval);
- };
-
- this.socket.onerror = (error: Event) => {
- console.error('WebSocket error:', error);
- };
- }
-
- public setHandler(handler: (message: WebSocketMessage, dispatch: Dispatch) => void, dispatch: Dispatch): void {
- this.messageHandler = handler;
- this.dispatch = dispatch;
- }
-
- public send(message: WebSocketMessage): void {
- if (this.socket && this.socket.readyState === WebSocket.OPEN) {
- this.socket.send(
- JSON.stringify({
- Cmd: message.Cmd,
- Body: message.Body,
- })
- );
- } else {
- this.offlineQueue.push(message);
- }
- }
-
- public disconnect(): void {
- if (this.socket) {
- this.socket.close();
- }
+ public disconnect(): void {
+ if (this.socket) {
+ this.socket.close();
}
+ }
}
-const webSocketConnectionEventName = 'WebSocketConnectionEvent';
+const webSocketConnectionEventName = "WebSocketConnectionEvent";
const webSocketConnectionEvent = new CustomEvent(webSocketConnectionEventName, {
- detail: 'wsReconnect',
+ detail: "wsReconnect",
});
export function addWebSocketReconnectListener(callback: () => void): void {
- document.addEventListener(webSocketConnectionEventName, callback);
+ document.addEventListener(webSocketConnectionEventName, callback);
}
export function removeWebSocketReconnectListener(callback: () => void): void {
- document.removeEventListener(webSocketConnectionEventName, callback);
+ document.removeEventListener(webSocketConnectionEventName, callback);
}
const webSocketService = new WebSocketService(Constants.WS_ADDRESS);
export default webSocketService;
-export function WebSocketMessageHandler(message: WebSocketMessage, dispatch: Dispatch) {
- const { Cmd, Body } = message;
+export function WebSocketMessageHandler(
+ message: WebSocketMessage,
+ dispatch: Dispatch
+) {
+ const { Cmd, Body } = message;
- console.log('WebSocketMessageHandler', Cmd, Body);
+ console.log("WebSocketMessageHandler", Cmd, Body);
- switch (Cmd) {
- case WebSocketReceivedMessagesCmds.SettingsUpdated:
- dispatch(setPrimaryColor(Body.PrimaryColor));
- break;
- case WebSocketReceivedMessagesCmds.SettingsUpdatedLogo:
- dispatch(setLogoUrl(Body));
- break;
- case WebSocketReceivedMessagesCmds.SettingsUpdatedBanner:
- dispatch(setBannerUrl(Body));
- break;
- case WebSocketReceivedMessagesCmds.SettingsUpdatedSubdomain:
- localStorage.removeItem('session');
+ switch (Cmd) {
+ case WebSocketReceivedMessagesCmds.SettingsUpdated:
+ dispatch(setPrimaryColor(Body.PrimaryColor));
+ break;
+ case WebSocketReceivedMessagesCmds.SettingsUpdatedLogo:
+ dispatch(setLogoUrl(Body));
+ break;
+ case WebSocketReceivedMessagesCmds.SettingsUpdatedBanner:
+ dispatch(setBannerUrl(Body));
+ break;
+ case WebSocketReceivedMessagesCmds.SettingsUpdatedSubdomain:
+ localStorage.removeItem("session");
- window.location.href = `${window.location.protocol}//${Body}.${window.location.hostname.split('.').slice(1).join('.')}`;
- break;
- case WebSocketReceivedMessagesCmds.TeamAddedMember:
- dispatch(addTeamMember(Body));
- break;
- case WebSocketReceivedMessagesCmds.TeamUpdatedMemberRole:
- dispatch(updateTeamMemberRole(Body));
- break;
- case WebSocketReceivedMessagesCmds.TeamDeletedMember:
- dispatch(deleteTeamMember(Body));
- break;
- case WebSocketReceivedMessagesCmds.LessonCreated:
- dispatch(addLesson(Body));
- break;
- case WebSocketReceivedMessagesCmds.LessonPreviewTitleUpdated:
- dispatch(updateLessonPreviewTitle(Body));
+ window.location.href = `${
+ window.location.protocol
+ }//${Body}.${window.location.hostname.split(".").slice(1).join(".")}`;
+ break;
+ case WebSocketReceivedMessagesCmds.TeamAddedMember:
+ dispatch(addTeamMember(Body));
+ break;
+ case WebSocketReceivedMessagesCmds.TeamUpdatedMemberRole:
+ dispatch(updateTeamMemberRole(Body));
+ break;
+ case WebSocketReceivedMessagesCmds.TeamDeletedMember:
+ dispatch(deleteTeamMember(Body));
+ break;
+ case WebSocketReceivedMessagesCmds.LessonCreated:
+ dispatch(addLesson(Body));
+ break;
+ case WebSocketReceivedMessagesCmds.LessonPreviewTitleUpdated:
+ dispatch(updateLessonPreviewTitle(Body));
- if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) {
- dispatch(setLessonThumbnailTitle(Body.Title));
- }
- break;
- case WebSocketReceivedMessagesCmds.LessonPreviewThumbnailUpdated:
- dispatch(updateLessonPreviewThumbnail(Body));
+ if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) {
+ dispatch(setLessonThumbnailTitle(Body.Title));
+ }
+ break;
+ case WebSocketReceivedMessagesCmds.LessonPreviewThumbnailUpdated:
+ dispatch(updateLessonPreviewThumbnail(Body));
- if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) {
- dispatch(setLessonThumbnailUrl(Body.ThumbnailUrl));
- }
- break;
- case WebSocketReceivedMessagesCmds.LessonStateUpdated:
- dispatch(updateLessonState(Body));
+ if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) {
+ dispatch(setLessonThumbnailUrl(Body.ThumbnailUrl));
+ }
+ break;
+ case WebSocketReceivedMessagesCmds.LessonStateUpdated:
+ dispatch(updateLessonState(Body));
- if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) {
- dispatch(setPageEditorLessonState(Body.State));
- }
- break;
- case WebSocketReceivedMessagesCmds.LessonAddedContent:
- if (Body.LessonId === store.getState().lessonPage.currentLessonId) {
- dispatch(addLessonPageContent(Body));
- }
- if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) {
- dispatch(addLessonContent(Body));
- }
- break;
- case WebSocketReceivedMessagesCmds.LessonDeletedContent:
- if (Body.LessonId === store.getState().lessonPage.currentLessonId) {
- dispatch(deleteLessonPageContent(Body.ContentId));
- }
- if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) {
- dispatch(deleteLessonContent(Body.ContentId));
- }
- break;
- case WebSocketReceivedMessagesCmds.LessonContentUpdated:
- if (Body.LessonId === store.getState().lessonPage.currentLessonId) {
- dispatch(
- updateLessonPageContent({
- id: Body.ContentId,
- data: Body.Data,
- })
- );
- }
+ if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) {
+ dispatch(setPageEditorLessonState(Body.State));
+ }
+ break;
+ case WebSocketReceivedMessagesCmds.LessonAddedContent:
+ if (Body.LessonId === store.getState().lessonPage.currentLessonId) {
+ dispatch(addLessonPageContent(Body));
+ }
+ if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) {
+ dispatch(addLessonContent(Body));
+ }
+ break;
+ case WebSocketReceivedMessagesCmds.LessonDeletedContent:
+ if (Body.LessonId === store.getState().lessonPage.currentLessonId) {
+ dispatch(deleteLessonPageContent(Body.ContentId));
+ }
+ if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) {
+ dispatch(deleteLessonContent(Body.ContentId));
+ }
+ break;
+ case WebSocketReceivedMessagesCmds.LessonContentUpdated:
+ if (Body.LessonId === store.getState().lessonPage.currentLessonId) {
+ dispatch(
+ updateLessonPageContent({
+ id: Body.ContentId,
+ data: Body.Data,
+ })
+ );
+ }
- if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) {
- dispatch(
- updateLessonContent({
- id: Body.ContentId,
- data: Body.Data,
- })
- );
- }
- break;
- case WebSocketReceivedMessagesCmds.LessonContentUpdatedPosition:
- if (Body.LessonId === store.getState().lessonPage.currentLessonId) {
- dispatch(
- updateLessonPageContentPosition({
- contentId: Body.ContentId,
- position: Body.Position,
- })
- );
- }
+ if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) {
+ dispatch(
+ updateLessonContent({
+ id: Body.ContentId,
+ data: Body.Data,
+ })
+ );
+ }
+ break;
+ case WebSocketReceivedMessagesCmds.LessonContentUpdatedPosition:
+ if (Body.LessonId === store.getState().lessonPage.currentLessonId) {
+ dispatch(
+ updateLessonPageContentPosition({
+ contentId: Body.ContentId,
+ position: Body.Position,
+ })
+ );
+ }
- if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) {
- dispatch(
- updateLessonContentPosition({
- contentId: Body.ContentId,
- position: Body.Position,
- })
- );
- }
- break;
- case WebSocketReceivedMessagesCmds.LessonContentFileUpdated:
- if (Body.LessonId === store.getState().lessonPage.currentLessonId) {
- dispatch(
- updateLessonPageContent({
- id: Body.ContentId,
- data: Body.Data,
- })
- );
- }
+ if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) {
+ dispatch(
+ updateLessonContentPosition({
+ contentId: Body.ContentId,
+ position: Body.Position,
+ })
+ );
+ }
+ break;
+ case WebSocketReceivedMessagesCmds.LessonContentFileUpdated:
+ if (Body.LessonId === store.getState().lessonPage.currentLessonId) {
+ dispatch(
+ updateLessonPageContent({
+ id: Body.ContentId,
+ data: Body.Data,
+ })
+ );
+ }
- if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) {
- dispatch(
- updateLessonContent({
- id: Body.ContentId,
- data: Body.Data,
- })
- );
- }
- break;
- case WebSocketReceivedMessagesCmds.UserProfilePictureUpdated:
- dispatch(setProfilePictureUrl(Body));
- dispatch(setUserProfilePictureUrl(Body));
- break;
- default:
- console.error('Unknown message type:', Cmd);
- }
+ if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) {
+ dispatch(
+ updateLessonContent({
+ id: Body.ContentId,
+ data: Body.Data,
+ })
+ );
+ }
+ break;
+ case WebSocketReceivedMessagesCmds.UserProfilePictureUpdated:
+ dispatch(setProfilePictureUrl(Body));
+ dispatch(setUserProfilePictureUrl(Body));
+ break;
+ case WebSocketReceivedMessagesCmds.LessonQuestionCreated:
+ if (Body.LessonId === store.getState().lessonPage.currentLessonId) {
+ dispatch(addQuestion(Body));
+ }
+ break;
+ case WebSocketReceivedMessagesCmds.LessonQuestionLiked:
+ dispatch(addLikedQuestion(Body));
+ break;
+ case WebSocketReceivedMessagesCmds.LessonQuestionCountUpLikes:
+ dispatch(countUpLikedQuestion(Body));
+ break;
+ case WebSocketReceivedMessagesCmds.LessonQuestionDisliked:
+ dispatch(deleteLikedQuestion(Body));
+ break;
+ case WebSocketReceivedMessagesCmds.LessonQuestionCountDownLikes:
+ dispatch(countDownLikedQuestion(Body));
+ break;
+ default:
+ console.error("Unknown message type:", Cmd);
+ }
}
diff --git a/src/core/store/store.tsx b/src/core/store/store.tsx
index 6c5a7b5..80198de 100644
--- a/src/core/store/store.tsx
+++ b/src/core/store/store.tsx
@@ -10,6 +10,7 @@ import { lessonsSlice } from "features/Lessons/lessonsSlice";
import { lessonPageSlice } from "features/Lessons/LessonPage/lessonPageSlice";
import { userProfileApi } from "core/services/userProfile";
import { userProfileSlice } from "features/UserProfile/userProfileSlice";
+import { lessonQuestionsSlice } from "features/Lessons/Questions/lessonQuestionSlice";
const makeStore = (/* preloadedState */) => {
const store = configureStore({
@@ -21,6 +22,7 @@ const makeStore = (/* preloadedState */) => {
[lessonsApi.reducerPath]: lessonsApi.reducer,
[lessonsSlice.reducerPath]: lessonsSlice.reducer,
[lessonPageSlice.reducerPath]: lessonPageSlice.reducer,
+ [lessonQuestionsSlice.reducerPath]: lessonQuestionsSlice.reducer,
[organizationApi.reducerPath]: organizationApi.reducer,
[teamSlice.reducerPath]: teamSlice.reducer,
[userProfileApi.reducerPath]: userProfileApi.reducer,
diff --git a/src/core/types/lesson.ts b/src/core/types/lesson.ts
index 8ad1a9f..4fdee43 100644
--- a/src/core/types/lesson.ts
+++ b/src/core/types/lesson.ts
@@ -36,20 +36,16 @@ export interface UpdateLessonPreviewThumbnail {
export interface LessonQuestion {
Id: string;
LessionId: string;
- Question: string;
+ QuestionId?: string;
+ ReplyId?: string;
+ Message: string;
Likes: number;
CreatorUserId: string;
CreatedAt: string;
UpdatedAt: string;
}
-export interface LessonQuestionReply {
- Id: string;
- QuestionId: string;
- ReplyId?: string;
- Reply: string;
- Likes: number;
- CreatorUserId: string;
- CreatedAt: string;
- UpdatedAt: string;
+export interface QuestionsResponse {
+ Questions: LessonQuestion[];
+ LikedQuestions: string[];
}
diff --git a/src/core/types/userProfile.ts b/src/core/types/userProfile.ts
index 1bde012..faf1fe5 100644
--- a/src/core/types/userProfile.ts
+++ b/src/core/types/userProfile.ts
@@ -5,3 +5,10 @@ export interface UserProfile {
Email: string;
RoleId: string;
}
+
+export interface CachedUser {
+ Id: string;
+ FirstName: string;
+ LastName: string;
+ ProfilePictureUrl: string;
+}
diff --git a/src/core/utils/webSocket.ts b/src/core/utils/webSocket.ts
index 8839833..8149b7b 100644
--- a/src/core/utils/webSocket.ts
+++ b/src/core/utils/webSocket.ts
@@ -20,6 +20,11 @@ enum WebSocketReceivedMessagesCmds {
LessonContentUpdatedPosition = 15,
LessonContentFileUpdated = 16,
UserProfilePictureUpdated = 17,
+ LessonQuestionCreated = 18,
+ LessonQuestionLiked = 19,
+ LessonQuestionCountUpLikes = 20,
+ LessonQuestionDisliked = 21,
+ LessonQuestionCountDownLikes = 22,
}
export { WebSocketSendMessagesCmds, WebSocketReceivedMessagesCmds };
diff --git a/src/features/ContactSupport/index.tsx b/src/features/ContactSupport/index.tsx
index 974eb0b..09fd718 100644
--- a/src/features/ContactSupport/index.tsx
+++ b/src/features/ContactSupport/index.tsx
@@ -1,20 +1,23 @@
-import { Descriptions, Typography } from 'antd';
-import MyMiddleCard from 'shared/components/MyMiddleCard';
-import MyBanner from 'shared/components/MyBanner';
+import { Descriptions, Typography } from "antd";
+import MyMiddleCard from "shared/components/MyMiddleCard";
+import MyBanner from "shared/components/MyBanner";
export default function ContactSupport() {
- return (
- <>
-
+ return (
+ <>
+
-
- If you have any questions or need help, please contact us at the following e-mail address:
-
-
- support@jannex.de
-
-
-
- >
- );
+
+
+ If you have any questions or need help, please contact us at the
+ following e-mail address:
+
+
+
+ support@.
+
+
+
+ >
+ );
}
diff --git a/src/features/Lessons/LessonPage/index.tsx b/src/features/Lessons/LessonPage/index.tsx
index d0e1b66..6dea7f6 100644
--- a/src/features/Lessons/LessonPage/index.tsx
+++ b/src/features/Lessons/LessonPage/index.tsx
@@ -79,7 +79,7 @@ export default function LessonPage() {
-
+
}
>
diff --git a/src/features/Lessons/Questions/index.tsx b/src/features/Lessons/Questions/index.tsx
index d2dc7f1..54b5e11 100644
--- a/src/features/Lessons/Questions/index.tsx
+++ b/src/features/Lessons/Questions/index.tsx
@@ -1,267 +1,295 @@
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";
+import { Button, Flex, Form, Input, InputRef, Typography } from "antd";
+import {
+ useCreateLessonQuestionMutation,
+ useCreateLessonQuestionReplyMutation,
+ useDislikeQuestionMutation,
+ useGetLessonQuestionsQuery,
+ useLikeQuestionMutation,
+} from "core/services/lessons";
+import { LessonQuestion } from "core/types/lesson";
+import { ReactNode, useEffect, useRef, useState } from "react";
+import { useDispatch, useSelector } from "react-redux";
+import { useParams } from "react-router-dom";
+import {
+ likedQuestions,
+ questions,
+ setLikedQuestions,
+ setQuestions,
+} from "./lessonQuestionSlice";
+import MyCenteredSpin from "shared/components/MyCenteredSpin";
+import MyErrorResult from "shared/components/MyResult";
+import { useMessage } from "core/context/MessageContext";
+import { useForm } from "antd/es/form/Form";
+import {
+ addWebSocketReconnectListener,
+ removeWebSocketReconnectListener,
+} from "core/services/websocketService";
+import { useCachedUser } from "core/services/cachedUser";
+import MyUserAvatar from "shared/components/MyUserAvatar";
-export default function Questions({ lessionID }: { lessionID: string }) {
- let questions: LessonQuestion[] = [
+const CreateQuestionForm: React.FC = () => {
+ const { lessonId } = useParams();
+ const [form] = useForm();
+
+ const [createQuestion, { isLoading }] = useCreateLessonQuestionMutation();
+ const { success, error } = useMessage();
+
+ const handleCreateLessonQuestion = async () => {
+ try {
+ await createQuestion({
+ lessonId: lessonId as string,
+ message: form.getFieldValue("message"),
+ }).unwrap();
+
+ success("Question created successfully");
+ form.resetFields();
+ } catch (err) {
+ console.error(err);
+ error("Failed to create question");
+ }
+ };
+
+ return (
+
+
+
+
+
+
+ Submit
+
+
+
+ );
+};
+
+export default function Questions() {
+ const dispatch = useDispatch();
+ const { lessonId } = useParams();
+
+ const dataQuestions = useSelector(questions);
+ const dataLikedQuestions = useSelector(likedQuestions);
+
+ const { data, error, isLoading, refetch } = useGetLessonQuestionsQuery(
+ lessonId as string,
{
- 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",
- },
- ];
+ refetchOnMountOrArgChange: true,
+ }
+ );
+
+ useEffect(() => {
+ if (!data) return;
+
+ dispatch(setQuestions(data.Questions));
+ dispatch(setLikedQuestions(data.LikedQuestions));
+ }, [data]);
+
+ useEffect(() => {
+ addWebSocketReconnectListener(refetch);
+
+ return () => removeWebSocketReconnectListener(refetch);
+ }, []);
return (
Questions
-
-
-
-
- Submit
-
-
+
+
+
- {questions.map((question) => (
-
- ))}
+ {error ? (
+
+ ) : isLoading ? (
+
+ ) : (
+ dataQuestions
+ .filter((question) => question.QuestionId === "")
+ .sort((b, a) => a.CreatedAt.localeCompare(b.CreatedAt))
+ .map((question) => (
+ reply.QuestionId === question.Id
+ )}
+ likedQuestions={dataLikedQuestions}
+ />
+ ))
+ )}
);
}
-type HandleReplyFunction = (text: string, replyID?: string) => Promise;
+type HandleReplyFunction = (message: string, replyId?: string) => Promise;
-export function QuestionItem({ question }: { question: LessonQuestion }) {
- const [showReplies, setShowReplies] = React.useState(1);
+export function QuestionItem({
+ lessonId,
+ question,
+ replies,
+ likedQuestions,
+}: {
+ lessonId: string;
+ question: LessonQuestion;
+ replies: LessonQuestion[];
+ likedQuestions: string[];
+}) {
+ const [showReplies, setShowReplies] = useState(1);
+ const { success, error } = useMessage();
- let user = {
- Id: "132154153613",
- FirstName: "Anja",
- LastName: "Blasinstroment",
- };
+ const [reqCreateQuestionReply] = useCreateLessonQuestionReplyMutation();
+ const [reqLikeQuestion] = useLikeQuestionMutation();
+ const [reqDislikeQuestion] = useDislikeQuestionMutation();
- 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(message: string, replyId?: string) {
+ try {
+ await reqCreateQuestionReply({
+ lessonId: lessonId as string,
+ questionId: question.Id,
+ message: message,
+ }).unwrap();
- async function handleReply(text: string, replyID?: string) {
- console.log("reply", text);
- await new Promise((resolve) => setTimeout(resolve, 1000));
+ success("Question created successfully");
+
+ await new Promise((resolve) => setTimeout(resolve, 0));
+ } catch (err) {
+ console.error(err);
+ error("Failed to create question");
+ await new Promise((resolve) => setTimeout(resolve, 0));
+ }
+ }
+
+ async function handleLike(questionId: string) {
+ try {
+ await reqLikeQuestion({
+ lessonId: lessonId as string,
+ questionId: questionId,
+ }).unwrap();
+ } catch (err) {
+ console.error(err);
+ error("Failed to like");
+ }
+ }
+
+ async function handleDislike(questionId: string) {
+ try {
+ await reqDislikeQuestion({
+ lessonId: lessonId as string,
+ questionId: questionId,
+ }).unwrap();
+ } catch (err) {
+ console.error(err);
+ error("Failed to dislike");
+ }
}
return (
- {(() => {
- let nodes = [];
-
- for (let i = 0; i < questionsReplys.length; i++) {
- if (i > showReplies - 1) {
- nodes.push(
- setShowReplies(showReplies + 3)}
- style={{ marginLeft: 64 }}
- >
- Show more
-
- );
- break;
- }
-
- nodes.push(
-
- );
- }
-
- return nodes;
- })()}
-
+ <>
+ {replies
+ .sort((a, b) => a.CreatedAt.localeCompare(b.CreatedAt))
+ .map((reply) => (
+ >}
+ likes={reply.Likes}
+ onReply={handleReply}
+ onLike={() => handleLike(reply.Id)}
+ onDislike={() => handleDislike(reply.Id)}
+ replyId={reply.Id}
+ hasLiked={likedQuestions.some((likeId) => likeId === reply.Id)}
+ />
+ ))}
+ >
}
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",
- };
-
- return (
- >}
- likes={question.Likes}
- onReply={handleReply}
- onLike={() => {}}
- replyID={question.Id}
+ onLike={() => handleLike(question.Id)}
+ onDislike={() => handleDislike(question.Id)}
+ replyId={undefined}
+ hasLiked={likedQuestions.some((likeId) => likeId === question.Id)}
/>
);
}
export function QuestionUIRaw({
- userID,
- text,
+ userId,
+ message,
childContent,
likes,
- replyID,
+ replyId,
onReply,
onLike,
+ onDislike,
+ hasLiked,
}: {
- userID: string;
- text: string;
- childContent: React.ReactNode;
+ userId: string;
+ message: string;
+ childContent: ReactNode;
likes: number;
- replyID?: string;
+ replyId?: string;
onReply: HandleReplyFunction;
onLike: () => void;
+ onDislike: () => void;
+ hasLiked: boolean;
}) {
- const [hasLiked, setHasLiked] = React.useState(false);
+ //const [hasLiked, setHasLiked] = useState(false);
- const [replyFormVisible, setReplyFormVisible] = React.useState(false);
- const [replyText, setReplyText] = React.useState(null);
- const [isSendingReply, setIsSendingReply] = React.useState(false);
+ const [replyFormVisible, setReplyFormVisible] = useState(false);
+ const [replyMessage, setReplyMessage] = useState(null);
+ const [isSendingReply, setIsSendingReply] = useState(false);
- let user = {
- Id: "132154153613",
- FirstName: "Anja",
- LastName: "Blasinstroment",
- };
-
- const userAt = `@${user.FirstName} ${user.LastName} `;
-
- async function toggleLike() {
- setHasLiked(!hasLiked);
- }
+ const { user, error, isLoading } = useCachedUser(userId);
// useref to focus on the input field
const inputRef = useRef(null);
+ const userAt =
+ isLoading || user === undefined
+ ? ""
+ : `@${user.FirstName} ${user.LastName} `;
+
+ if (error) return ;
+
+ if (isLoading) return ;
+
return (
<>
-
{user.FirstName} {user.LastName}
- {text}
+ {message}
(hasLiked ? onDislike() : onLike())}
>
{
- if (replyText === null) setReplyText(userAt);
+ if (replyMessage === null) setReplyMessage(userAt);
setReplyFormVisible(!replyFormVisible);
setTimeout(() => {
@@ -301,14 +329,17 @@ export function QuestionUIRaw({
{replyFormVisible ? (
setReplyText(e.target.value)}
+ onChange={(e) => setReplyMessage(e.target.value)}
+ autoSize={{ minRows: 2 }}
+ maxLength={5000}
/>
diff --git a/src/features/Lessons/Questions/lessonQuestionSlice.ts b/src/features/Lessons/Questions/lessonQuestionSlice.ts
index 166a138..d73133d 100644
--- a/src/features/Lessons/Questions/lessonQuestionSlice.ts
+++ b/src/features/Lessons/Questions/lessonQuestionSlice.ts
@@ -1,34 +1,65 @@
-import { createSlice } from '@reduxjs/toolkit';
-import { LessonContent, LessonState } from 'core/types/lesson';
+import { createSlice } from "@reduxjs/toolkit";
+import { LessonQuestion } from "core/types/lesson";
-interface AddLessonContentAction {
- type: string;
- payload: LessonContent;
-}
-/*
-export const lessonPageEditorSlice = createSlice({
- name: 'lessonQuestions',
- initialState: {
- editorActive: false,
- currentLessonId: '', // required in sideMenu because has no access to useParams
- lessonThumbnail: {
- img: '',
- title: 'Tesdt',
- },
- lessonContents: [] as LessonContent[],
- lessonState: LessonState.Draft,
+export const lessonQuestionsSlice = createSlice({
+ name: "questions",
+ initialState: {
+ questions: [] as LessonQuestion[],
+ likedQuestions: [] as string[],
+ },
+ reducers: {
+ setQuestions: (state, action) => {
+ state.questions = action.payload as LessonQuestion[];
},
- reducers: {
- setEditorActive: (state, action) => {
- state.editorActive = action.payload;
- },
+ addQuestion: (state, action) => {
+ state.questions.push(action.payload as LessonQuestion);
},
- selectors: {
- editorActive: (state) => state.editorActive,
+ setLikedQuestions: (state, action) => {
+ state.likedQuestions = action.payload as string[];
},
+ addLikedQuestion: (state, action) => {
+ state.likedQuestions.push(action.payload as string);
+ },
+ deleteLikedQuestion: (state, action) => {
+ const questionId = action.payload as string;
+
+ state.likedQuestions = state.likedQuestions.filter(
+ (qId) => qId !== questionId
+ );
+ },
+ countUpLikedQuestion: (state, action) => {
+ const questionId = action.payload as string;
+
+ const question = state.questions.find((q) => q.Id === questionId);
+
+ if (question) {
+ question.Likes++;
+ }
+ },
+ countDownLikedQuestion: (state, action) => {
+ const questionId = action.payload as string;
+
+ const question = state.questions.find((q) => q.Id === questionId);
+
+ if (question) {
+ question.Likes--;
+ }
+ },
+ },
+ selectors: {
+ questions: (state) => state.questions,
+ likedQuestions: (state) => state.likedQuestions,
+ },
});
-export const { setEditorActive } = lessonPageEditorSlice.actions;
+export const {
+ setQuestions,
+ addQuestion,
+ setLikedQuestions,
+ addLikedQuestion,
+ deleteLikedQuestion,
+ countUpLikedQuestion,
+ countDownLikedQuestion,
+} = lessonQuestionsSlice.actions;
-export const { editorActive } = lessonPageEditorSlice.selectors;
-*/
+export const { questions, likedQuestions } = lessonQuestionsSlice.selectors;