added questions, reply and like functionality"
parent
679ee5bf28
commit
9be79b1481
|
@ -24,7 +24,7 @@
|
||||||
work correctly both with client-side routing and a non-root public URL.
|
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`.
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
-->
|
-->
|
||||||
<title>React App</title>
|
<title>App</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from "@reduxjs/toolkit";
|
||||||
|
import { CachedUser } from "core/types/userProfile";
|
||||||
|
|
||||||
export const appSlice = createSlice({
|
export const appSlice = createSlice({
|
||||||
name: 'app',
|
name: "app",
|
||||||
initialState: {
|
initialState: {
|
||||||
darkMode: false,
|
darkMode: false,
|
||||||
userAuthenticated: null,
|
userAuthenticated: null,
|
||||||
userProfilePictureUrl: null,
|
userProfilePictureUrl: null,
|
||||||
primaryColor: '#111',
|
primaryColor: "#111",
|
||||||
logoUrl: null,
|
logoUrl: null,
|
||||||
bannerUrl: null,
|
bannerUrl: null,
|
||||||
|
cachedUsers: {} as Record<string, CachedUser>,
|
||||||
},
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
setDarkMode: (state, action) => {
|
setDarkMode: (state, action) => {
|
||||||
|
@ -29,6 +31,9 @@ export const appSlice = createSlice({
|
||||||
setBannerUrl: (state, action) => {
|
setBannerUrl: (state, action) => {
|
||||||
state.bannerUrl = action.payload;
|
state.bannerUrl = action.payload;
|
||||||
},
|
},
|
||||||
|
addUserToCache: (state, action) => {
|
||||||
|
state.cachedUsers[action.payload.id] = action.payload;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
selectors: {
|
selectors: {
|
||||||
darkMode: (state) => state.darkMode,
|
darkMode: (state) => state.darkMode,
|
||||||
|
@ -37,9 +42,26 @@ export const appSlice = createSlice({
|
||||||
primaryColor: (state) => state.primaryColor,
|
primaryColor: (state) => state.primaryColor,
|
||||||
logoUrl: (state) => state.logoUrl,
|
logoUrl: (state) => state.logoUrl,
|
||||||
bannerUrl: (state) => state.bannerUrl,
|
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;
|
||||||
|
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import {
|
||||||
LessonContent,
|
LessonContent,
|
||||||
LessonQuestion,
|
LessonQuestion,
|
||||||
LessonSettings,
|
LessonSettings,
|
||||||
|
QuestionsResponse,
|
||||||
UpdateLessonPreviewThumbnail,
|
UpdateLessonPreviewThumbnail,
|
||||||
} from "core/types/lesson";
|
} from "core/types/lesson";
|
||||||
|
|
||||||
|
@ -94,12 +95,42 @@ export const lessonsApi = createApi({
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
getQuestions: builder.query<LessonQuestion[], string>({
|
getLessonQuestions: builder.query<QuestionsResponse, string>({
|
||||||
query: (lessonId) => ({
|
query: (lessonId) => ({
|
||||||
url: `lessons/${lessonId}/questions`,
|
url: `lessons/${lessonId}/questions`,
|
||||||
method: "GET",
|
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,
|
useUpdateLessonContentMutation,
|
||||||
useUpdateLessonContentPositionMutation,
|
useUpdateLessonContentPositionMutation,
|
||||||
useDeleteLessonContentMutation,
|
useDeleteLessonContentMutation,
|
||||||
|
useGetLessonQuestionsQuery,
|
||||||
|
useCreateLessonQuestionMutation,
|
||||||
|
useCreateLessonQuestionReplyMutation,
|
||||||
|
useLikeQuestionMutation,
|
||||||
|
useDislikeQuestionMutation,
|
||||||
} = lessonsApi;
|
} = lessonsApi;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { createApi } from "@reduxjs/toolkit/query/react";
|
import { createApi } from "@reduxjs/toolkit/query/react";
|
||||||
import { baseQueryWithErrorHandling } from "core/helper/api";
|
import { baseQueryWithErrorHandling } from "core/helper/api";
|
||||||
import { UserProfile } from "core/types/userProfile";
|
import { CachedUser, UserProfile } from "core/types/userProfile";
|
||||||
|
|
||||||
export const userProfileApi = createApi({
|
export const userProfileApi = createApi({
|
||||||
reducerPath: "userProfileApi",
|
reducerPath: "userProfileApi",
|
||||||
|
@ -12,7 +12,13 @@ export const userProfileApi = createApi({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
getUser: builder.query<CachedUser, string>({
|
||||||
|
query: (userId) => ({
|
||||||
|
url: `user/${userId}`,
|
||||||
|
method: "GET",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { useGetUserProfileQuery } = userProfileApi;
|
export const { useGetUserProfileQuery, useGetUserQuery } = userProfileApi;
|
||||||
|
|
|
@ -1,9 +1,19 @@
|
||||||
import { Dispatch } from '@reduxjs/toolkit';
|
import { Dispatch } from "@reduxjs/toolkit";
|
||||||
import { setBannerUrl, setLogoUrl, setPrimaryColor, setUserProfilePictureUrl } from 'core/reducers/appSlice';
|
import {
|
||||||
import { store } from 'core/store/store';
|
setBannerUrl,
|
||||||
import { BrowserTabSession, Constants } from 'core/utils/utils';
|
setLogoUrl,
|
||||||
import { WebSocketReceivedMessagesCmds } from 'core/utils/webSocket';
|
setPrimaryColor,
|
||||||
import { addLessonPageContent, deleteLessonPageContent, updateLessonPageContent, updateLessonPageContentPosition } from 'features/Lessons/LessonPage/lessonPageSlice';
|
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 {
|
import {
|
||||||
addLessonContent,
|
addLessonContent,
|
||||||
deleteLessonContent,
|
deleteLessonContent,
|
||||||
|
@ -12,10 +22,26 @@ import {
|
||||||
setPageEditorLessonState,
|
setPageEditorLessonState,
|
||||||
updateLessonContent,
|
updateLessonContent,
|
||||||
updateLessonContentPosition,
|
updateLessonContentPosition,
|
||||||
} from 'features/Lessons/LessonPageEditor/lessonPageEditorSlice';
|
} from "features/Lessons/LessonPageEditor/lessonPageEditorSlice";
|
||||||
import { addLesson, updateLessonPreviewThumbnail, updateLessonPreviewTitle, updateLessonState } from 'features/Lessons/lessonsSlice';
|
import {
|
||||||
import { addTeamMember, deleteTeamMember, updateTeamMemberRole } from 'features/Team/teamSlice';
|
addLesson,
|
||||||
import { setProfilePictureUrl } from 'features/UserProfile/userProfileSlice';
|
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 {
|
interface WebSocketMessage {
|
||||||
Cmd: number;
|
Cmd: number;
|
||||||
|
@ -29,7 +55,9 @@ class WebSocketService {
|
||||||
private offlineQueue: WebSocketMessage[] = [];
|
private offlineQueue: WebSocketMessage[] = [];
|
||||||
private firstConnect: boolean = true;
|
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) {
|
constructor(url: string) {
|
||||||
this.url = url;
|
this.url = url;
|
||||||
|
@ -38,10 +66,14 @@ class WebSocketService {
|
||||||
private dispatch: Dispatch | null = null;
|
private dispatch: Dispatch | null = null;
|
||||||
|
|
||||||
public connect(): void {
|
public connect(): void {
|
||||||
this.socket = new WebSocket(`${this.url}?auth=${localStorage.getItem('session')}&bts=${BrowserTabSession}`);
|
this.socket = new WebSocket(
|
||||||
|
`${this.url}?auth=${localStorage.getItem(
|
||||||
|
"session"
|
||||||
|
)}&bts=${BrowserTabSession}`
|
||||||
|
);
|
||||||
|
|
||||||
this.socket.onopen = () => {
|
this.socket.onopen = () => {
|
||||||
console.log('WebSocket connected', this.firstConnect);
|
console.log("WebSocket connected", this.firstConnect);
|
||||||
|
|
||||||
// Send all messages from the offline queue
|
// Send all messages from the offline queue
|
||||||
|
|
||||||
|
@ -63,21 +95,24 @@ class WebSocketService {
|
||||||
if (this.messageHandler) {
|
if (this.messageHandler) {
|
||||||
this.messageHandler(data, this.dispatch!);
|
this.messageHandler(data, this.dispatch!);
|
||||||
} else {
|
} else {
|
||||||
console.error('No handler defined for WebSocket messages');
|
console.error("No handler defined for WebSocket messages");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.socket.onclose = () => {
|
this.socket.onclose = () => {
|
||||||
console.log('WebSocket disconnected. Reconnecting...');
|
console.log("WebSocket disconnected. Reconnecting...");
|
||||||
setTimeout(() => this.connect(), this.reconnectInterval);
|
setTimeout(() => this.connect(), this.reconnectInterval);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.socket.onerror = (error: Event) => {
|
this.socket.onerror = (error: Event) => {
|
||||||
console.error('WebSocket error:', error);
|
console.error("WebSocket error:", error);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public setHandler(handler: (message: WebSocketMessage, dispatch: Dispatch) => void, dispatch: Dispatch): void {
|
public setHandler(
|
||||||
|
handler: (message: WebSocketMessage, dispatch: Dispatch) => void,
|
||||||
|
dispatch: Dispatch
|
||||||
|
): void {
|
||||||
this.messageHandler = handler;
|
this.messageHandler = handler;
|
||||||
this.dispatch = dispatch;
|
this.dispatch = dispatch;
|
||||||
}
|
}
|
||||||
|
@ -102,10 +137,10 @@ class WebSocketService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const webSocketConnectionEventName = 'WebSocketConnectionEvent';
|
const webSocketConnectionEventName = "WebSocketConnectionEvent";
|
||||||
|
|
||||||
const webSocketConnectionEvent = new CustomEvent(webSocketConnectionEventName, {
|
const webSocketConnectionEvent = new CustomEvent(webSocketConnectionEventName, {
|
||||||
detail: 'wsReconnect',
|
detail: "wsReconnect",
|
||||||
});
|
});
|
||||||
|
|
||||||
export function addWebSocketReconnectListener(callback: () => void): void {
|
export function addWebSocketReconnectListener(callback: () => void): void {
|
||||||
|
@ -119,10 +154,13 @@ export function removeWebSocketReconnectListener(callback: () => void): void {
|
||||||
const webSocketService = new WebSocketService(Constants.WS_ADDRESS);
|
const webSocketService = new WebSocketService(Constants.WS_ADDRESS);
|
||||||
export default webSocketService;
|
export default webSocketService;
|
||||||
|
|
||||||
export function WebSocketMessageHandler(message: WebSocketMessage, dispatch: Dispatch) {
|
export function WebSocketMessageHandler(
|
||||||
|
message: WebSocketMessage,
|
||||||
|
dispatch: Dispatch
|
||||||
|
) {
|
||||||
const { Cmd, Body } = message;
|
const { Cmd, Body } = message;
|
||||||
|
|
||||||
console.log('WebSocketMessageHandler', Cmd, Body);
|
console.log("WebSocketMessageHandler", Cmd, Body);
|
||||||
|
|
||||||
switch (Cmd) {
|
switch (Cmd) {
|
||||||
case WebSocketReceivedMessagesCmds.SettingsUpdated:
|
case WebSocketReceivedMessagesCmds.SettingsUpdated:
|
||||||
|
@ -135,9 +173,11 @@ export function WebSocketMessageHandler(message: WebSocketMessage, dispatch: Dis
|
||||||
dispatch(setBannerUrl(Body));
|
dispatch(setBannerUrl(Body));
|
||||||
break;
|
break;
|
||||||
case WebSocketReceivedMessagesCmds.SettingsUpdatedSubdomain:
|
case WebSocketReceivedMessagesCmds.SettingsUpdatedSubdomain:
|
||||||
localStorage.removeItem('session');
|
localStorage.removeItem("session");
|
||||||
|
|
||||||
window.location.href = `${window.location.protocol}//${Body}.${window.location.hostname.split('.').slice(1).join('.')}`;
|
window.location.href = `${
|
||||||
|
window.location.protocol
|
||||||
|
}//${Body}.${window.location.hostname.split(".").slice(1).join(".")}`;
|
||||||
break;
|
break;
|
||||||
case WebSocketReceivedMessagesCmds.TeamAddedMember:
|
case WebSocketReceivedMessagesCmds.TeamAddedMember:
|
||||||
dispatch(addTeamMember(Body));
|
dispatch(addTeamMember(Body));
|
||||||
|
@ -249,7 +289,24 @@ export function WebSocketMessageHandler(message: WebSocketMessage, dispatch: Dis
|
||||||
dispatch(setProfilePictureUrl(Body));
|
dispatch(setProfilePictureUrl(Body));
|
||||||
dispatch(setUserProfilePictureUrl(Body));
|
dispatch(setUserProfilePictureUrl(Body));
|
||||||
break;
|
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:
|
default:
|
||||||
console.error('Unknown message type:', Cmd);
|
console.error("Unknown message type:", Cmd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { lessonsSlice } from "features/Lessons/lessonsSlice";
|
||||||
import { lessonPageSlice } from "features/Lessons/LessonPage/lessonPageSlice";
|
import { lessonPageSlice } from "features/Lessons/LessonPage/lessonPageSlice";
|
||||||
import { userProfileApi } from "core/services/userProfile";
|
import { userProfileApi } from "core/services/userProfile";
|
||||||
import { userProfileSlice } from "features/UserProfile/userProfileSlice";
|
import { userProfileSlice } from "features/UserProfile/userProfileSlice";
|
||||||
|
import { lessonQuestionsSlice } from "features/Lessons/Questions/lessonQuestionSlice";
|
||||||
|
|
||||||
const makeStore = (/* preloadedState */) => {
|
const makeStore = (/* preloadedState */) => {
|
||||||
const store = configureStore({
|
const store = configureStore({
|
||||||
|
@ -21,6 +22,7 @@ const makeStore = (/* preloadedState */) => {
|
||||||
[lessonsApi.reducerPath]: lessonsApi.reducer,
|
[lessonsApi.reducerPath]: lessonsApi.reducer,
|
||||||
[lessonsSlice.reducerPath]: lessonsSlice.reducer,
|
[lessonsSlice.reducerPath]: lessonsSlice.reducer,
|
||||||
[lessonPageSlice.reducerPath]: lessonPageSlice.reducer,
|
[lessonPageSlice.reducerPath]: lessonPageSlice.reducer,
|
||||||
|
[lessonQuestionsSlice.reducerPath]: lessonQuestionsSlice.reducer,
|
||||||
[organizationApi.reducerPath]: organizationApi.reducer,
|
[organizationApi.reducerPath]: organizationApi.reducer,
|
||||||
[teamSlice.reducerPath]: teamSlice.reducer,
|
[teamSlice.reducerPath]: teamSlice.reducer,
|
||||||
[userProfileApi.reducerPath]: userProfileApi.reducer,
|
[userProfileApi.reducerPath]: userProfileApi.reducer,
|
||||||
|
|
|
@ -36,20 +36,16 @@ export interface UpdateLessonPreviewThumbnail {
|
||||||
export interface LessonQuestion {
|
export interface LessonQuestion {
|
||||||
Id: string;
|
Id: string;
|
||||||
LessionId: string;
|
LessionId: string;
|
||||||
Question: string;
|
QuestionId?: string;
|
||||||
|
ReplyId?: string;
|
||||||
|
Message: string;
|
||||||
Likes: number;
|
Likes: number;
|
||||||
CreatorUserId: string;
|
CreatorUserId: string;
|
||||||
CreatedAt: string;
|
CreatedAt: string;
|
||||||
UpdatedAt: string;
|
UpdatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LessonQuestionReply {
|
export interface QuestionsResponse {
|
||||||
Id: string;
|
Questions: LessonQuestion[];
|
||||||
QuestionId: string;
|
LikedQuestions: string[];
|
||||||
ReplyId?: string;
|
|
||||||
Reply: string;
|
|
||||||
Likes: number;
|
|
||||||
CreatorUserId: string;
|
|
||||||
CreatedAt: string;
|
|
||||||
UpdatedAt: string;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,3 +5,10 @@ export interface UserProfile {
|
||||||
Email: string;
|
Email: string;
|
||||||
RoleId: string;
|
RoleId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CachedUser {
|
||||||
|
Id: string;
|
||||||
|
FirstName: string;
|
||||||
|
LastName: string;
|
||||||
|
ProfilePictureUrl: string;
|
||||||
|
}
|
||||||
|
|
|
@ -20,6 +20,11 @@ enum WebSocketReceivedMessagesCmds {
|
||||||
LessonContentUpdatedPosition = 15,
|
LessonContentUpdatedPosition = 15,
|
||||||
LessonContentFileUpdated = 16,
|
LessonContentFileUpdated = 16,
|
||||||
UserProfilePictureUpdated = 17,
|
UserProfilePictureUpdated = 17,
|
||||||
|
LessonQuestionCreated = 18,
|
||||||
|
LessonQuestionLiked = 19,
|
||||||
|
LessonQuestionCountUpLikes = 20,
|
||||||
|
LessonQuestionDisliked = 21,
|
||||||
|
LessonQuestionCountDownLikes = 22,
|
||||||
}
|
}
|
||||||
|
|
||||||
export { WebSocketSendMessagesCmds, WebSocketReceivedMessagesCmds };
|
export { WebSocketSendMessagesCmds, WebSocketReceivedMessagesCmds };
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Descriptions, Typography } from 'antd';
|
import { Descriptions, Typography } from "antd";
|
||||||
import MyMiddleCard from 'shared/components/MyMiddleCard';
|
import MyMiddleCard from "shared/components/MyMiddleCard";
|
||||||
import MyBanner from 'shared/components/MyBanner';
|
import MyBanner from "shared/components/MyBanner";
|
||||||
|
|
||||||
export default function ContactSupport() {
|
export default function ContactSupport() {
|
||||||
return (
|
return (
|
||||||
|
@ -8,10 +8,13 @@ export default function ContactSupport() {
|
||||||
<MyBanner title="Contact Support" />
|
<MyBanner title="Contact Support" />
|
||||||
|
|
||||||
<MyMiddleCard title="Support">
|
<MyMiddleCard title="Support">
|
||||||
<Typography.Paragraph>If you have any questions or need help, please contact us at the following e-mail address:</Typography.Paragraph>
|
<Typography.Paragraph>
|
||||||
|
If you have any questions or need help, please contact us at the
|
||||||
|
following e-mail address:
|
||||||
|
</Typography.Paragraph>
|
||||||
<Descriptions>
|
<Descriptions>
|
||||||
<Descriptions.Item label="E-Mail">
|
<Descriptions.Item label="E-Mail">
|
||||||
<a href="mailto:support@jannex.de">support@jannex.de</a>
|
<a href="mailto:support@.">support@.</a>
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
</MyMiddleCard>
|
</MyMiddleCard>
|
||||||
|
|
|
@ -79,7 +79,7 @@ export default function LessonPage() {
|
||||||
<MyMiddleCard
|
<MyMiddleCard
|
||||||
outOfCardChildren={
|
outOfCardChildren={
|
||||||
<div style={{ marginTop: 24 }}>
|
<div style={{ marginTop: 24 }}>
|
||||||
<Questions lessionID={"lessionID"} />
|
<Questions />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,267 +1,295 @@
|
||||||
import { HeartFilled, HeartOutlined } from "@ant-design/icons";
|
import { HeartFilled, HeartOutlined } from "@ant-design/icons";
|
||||||
import { Avatar, Button, Flex, Form, Input, InputRef, Typography } from "antd";
|
import { Button, Flex, Form, Input, InputRef, Typography } from "antd";
|
||||||
import { LessonQuestion, LessonQuestionReply } from "core/types/lesson";
|
import {
|
||||||
import { Constants } from "core/utils/utils";
|
useCreateLessonQuestionMutation,
|
||||||
import React, { useRef } from "react";
|
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 }) {
|
const CreateQuestionForm: React.FC = () => {
|
||||||
let questions: LessonQuestion[] = [
|
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 (
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={() => handleCreateLessonQuestion()}
|
||||||
|
requiredMark={false}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
label="Ask a question"
|
||||||
|
name="message"
|
||||||
|
rules={[{ required: true, message: "Please write a question" }]}
|
||||||
|
>
|
||||||
|
<Input.TextArea
|
||||||
|
placeholder={"Type something"}
|
||||||
|
autoSize={{ minRows: 2 }}
|
||||||
|
maxLength={5000}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item>
|
||||||
|
<Button type="primary" htmlType="submit" loading={isLoading}>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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",
|
refetchOnMountOrArgChange: true,
|
||||||
LessionId: "1",
|
}
|
||||||
Question: "What is the capital of Germany?",
|
);
|
||||||
Likes: 5,
|
|
||||||
CreatorUserId: "1",
|
useEffect(() => {
|
||||||
CreatedAt: "2021-09-01T12:00:00Z",
|
if (!data) return;
|
||||||
UpdatedAt: "2021-09-01T12:00:00Z",
|
|
||||||
},
|
dispatch(setQuestions(data.Questions));
|
||||||
{
|
dispatch(setLikedQuestions(data.LikedQuestions));
|
||||||
Id: "2",
|
}, [data]);
|
||||||
LessionId: "1",
|
|
||||||
Question: "What is the capital of France?",
|
useEffect(() => {
|
||||||
Likes: 3,
|
addWebSocketReconnectListener(refetch);
|
||||||
CreatorUserId: "2",
|
|
||||||
CreatedAt: "2021-09-01T12:00:00Z",
|
return () => removeWebSocketReconnectListener(refetch);
|
||||||
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 (
|
return (
|
||||||
<Flex justify="center">
|
<Flex justify="center">
|
||||||
<Flex style={{ width: 800, maxWidth: 800 * 0.9 }} vertical>
|
<Flex style={{ width: 800, maxWidth: 800 * 0.9 }} vertical>
|
||||||
<Typography.Title level={3}>Questions</Typography.Title>
|
<Typography.Title level={3}>Questions</Typography.Title>
|
||||||
<Form layout="vertical">
|
|
||||||
<Form.Item label="Ask a question">
|
<CreateQuestionForm />
|
||||||
<Input.TextArea placeholder={"Type something"} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item>
|
|
||||||
<Button type="primary">Submit</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
<Flex vertical style={{}}>
|
<Flex vertical style={{}}>
|
||||||
{questions.map((question) => (
|
{error ? (
|
||||||
<QuestionItem key={question.Id} question={question} />
|
<MyErrorResult />
|
||||||
))}
|
) : isLoading ? (
|
||||||
</Flex>
|
<MyCenteredSpin height="100px" />
|
||||||
</Flex>
|
) : (
|
||||||
</Flex>
|
dataQuestions
|
||||||
);
|
.filter((question) => question.QuestionId === "")
|
||||||
}
|
.sort((b, a) => a.CreatedAt.localeCompare(b.CreatedAt))
|
||||||
|
.map((question) => (
|
||||||
type HandleReplyFunction = (text: string, replyID?: string) => Promise<void>;
|
<QuestionItem
|
||||||
|
key={question.Id}
|
||||||
export function QuestionItem({ question }: { question: LessonQuestion }) {
|
lessonId={lessonId as string}
|
||||||
const [showReplies, setShowReplies] = React.useState(1);
|
question={question}
|
||||||
|
replies={dataQuestions.filter(
|
||||||
let user = {
|
(reply) => reply.QuestionId === question.Id
|
||||||
Id: "132154153613",
|
)}
|
||||||
FirstName: "Anja",
|
likedQuestions={dataLikedQuestions}
|
||||||
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",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
async function handleReply(text: string, replyID?: string) {
|
|
||||||
console.log("reply", text);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<QuestionUIRaw
|
|
||||||
userID={user.Id}
|
|
||||||
text={question.Question}
|
|
||||||
childContent={
|
|
||||||
<div>
|
|
||||||
{(() => {
|
|
||||||
let nodes = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < questionsReplys.length; i++) {
|
|
||||||
if (i > showReplies - 1) {
|
|
||||||
nodes.push(
|
|
||||||
<Button
|
|
||||||
key="showMore"
|
|
||||||
type="link"
|
|
||||||
color="primary"
|
|
||||||
onClick={() => setShowReplies(showReplies + 3)}
|
|
||||||
style={{ marginLeft: 64 }}
|
|
||||||
>
|
|
||||||
Show more
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
nodes.push(
|
|
||||||
<QuestionReplyItem
|
|
||||||
key={"reply_" + questionsReplys[i].Id}
|
|
||||||
question={questionsReplys[i]}
|
|
||||||
handleReply={handleReply}
|
|
||||||
/>
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return nodes;
|
type HandleReplyFunction = (message: string, replyId?: string) => Promise<void>;
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
likes={question.Likes}
|
|
||||||
onReply={handleReply}
|
|
||||||
onLike={() => {}}
|
|
||||||
replyID={undefined}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function QuestionReplyItem({
|
export function QuestionItem({
|
||||||
|
lessonId,
|
||||||
question,
|
question,
|
||||||
handleReply,
|
replies,
|
||||||
|
likedQuestions,
|
||||||
}: {
|
}: {
|
||||||
question: LessonQuestionReply;
|
lessonId: string;
|
||||||
handleReply: HandleReplyFunction;
|
question: LessonQuestion;
|
||||||
|
replies: LessonQuestion[];
|
||||||
|
likedQuestions: string[];
|
||||||
}) {
|
}) {
|
||||||
let user = {
|
const [showReplies, setShowReplies] = useState(1);
|
||||||
Id: "132154153613",
|
const { success, error } = useMessage();
|
||||||
FirstName: "Anja",
|
|
||||||
LastName: "Blasinstroment",
|
const [reqCreateQuestionReply] = useCreateLessonQuestionReplyMutation();
|
||||||
};
|
const [reqLikeQuestion] = useLikeQuestionMutation();
|
||||||
|
const [reqDislikeQuestion] = useDislikeQuestionMutation();
|
||||||
|
|
||||||
|
async function handleReply(message: string, replyId?: string) {
|
||||||
|
try {
|
||||||
|
await reqCreateQuestionReply({
|
||||||
|
lessonId: lessonId as string,
|
||||||
|
questionId: question.Id,
|
||||||
|
message: message,
|
||||||
|
}).unwrap();
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<QuestionUIRaw
|
<QuestionUIRaw
|
||||||
userID={user.Id}
|
userId={question.CreatorUserId}
|
||||||
text={question.Reply}
|
message={question.Message}
|
||||||
|
childContent={
|
||||||
|
<>
|
||||||
|
{replies
|
||||||
|
.sort((a, b) => a.CreatedAt.localeCompare(b.CreatedAt))
|
||||||
|
.map((reply) => (
|
||||||
|
<QuestionUIRaw
|
||||||
|
key={reply.Id}
|
||||||
|
userId={reply.CreatorUserId}
|
||||||
|
message={reply.Message}
|
||||||
childContent={<></>}
|
childContent={<></>}
|
||||||
|
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}
|
likes={question.Likes}
|
||||||
onReply={handleReply}
|
onReply={handleReply}
|
||||||
onLike={() => {}}
|
onLike={() => handleLike(question.Id)}
|
||||||
replyID={question.Id}
|
onDislike={() => handleDislike(question.Id)}
|
||||||
|
replyId={undefined}
|
||||||
|
hasLiked={likedQuestions.some((likeId) => likeId === question.Id)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function QuestionUIRaw({
|
export function QuestionUIRaw({
|
||||||
userID,
|
userId,
|
||||||
text,
|
message,
|
||||||
childContent,
|
childContent,
|
||||||
likes,
|
likes,
|
||||||
replyID,
|
replyId,
|
||||||
onReply,
|
onReply,
|
||||||
onLike,
|
onLike,
|
||||||
|
onDislike,
|
||||||
|
hasLiked,
|
||||||
}: {
|
}: {
|
||||||
userID: string;
|
userId: string;
|
||||||
text: string;
|
message: string;
|
||||||
childContent: React.ReactNode;
|
childContent: ReactNode;
|
||||||
likes: number;
|
likes: number;
|
||||||
replyID?: string;
|
replyId?: string;
|
||||||
onReply: HandleReplyFunction;
|
onReply: HandleReplyFunction;
|
||||||
onLike: () => void;
|
onLike: () => void;
|
||||||
|
onDislike: () => void;
|
||||||
|
hasLiked: boolean;
|
||||||
}) {
|
}) {
|
||||||
const [hasLiked, setHasLiked] = React.useState(false);
|
//const [hasLiked, setHasLiked] = useState(false);
|
||||||
|
|
||||||
const [replyFormVisible, setReplyFormVisible] = React.useState(false);
|
const [replyFormVisible, setReplyFormVisible] = useState(false);
|
||||||
const [replyText, setReplyText] = React.useState<null | string>(null);
|
const [replyMessage, setReplyMessage] = useState<null | string>(null);
|
||||||
const [isSendingReply, setIsSendingReply] = React.useState(false);
|
const [isSendingReply, setIsSendingReply] = useState(false);
|
||||||
|
|
||||||
let user = {
|
const { user, error, isLoading } = useCachedUser(userId);
|
||||||
Id: "132154153613",
|
|
||||||
FirstName: "Anja",
|
|
||||||
LastName: "Blasinstroment",
|
|
||||||
};
|
|
||||||
|
|
||||||
const userAt = `@${user.FirstName} ${user.LastName} `;
|
|
||||||
|
|
||||||
async function toggleLike() {
|
|
||||||
setHasLiked(!hasLiked);
|
|
||||||
}
|
|
||||||
|
|
||||||
// useref to focus on the input field
|
// useref to focus on the input field
|
||||||
const inputRef = useRef<InputRef>(null);
|
const inputRef = useRef<InputRef>(null);
|
||||||
|
|
||||||
|
const userAt =
|
||||||
|
isLoading || user === undefined
|
||||||
|
? ""
|
||||||
|
: `@${user.FirstName} ${user.LastName} `;
|
||||||
|
|
||||||
|
if (error) return <MyErrorResult />;
|
||||||
|
|
||||||
|
if (isLoading) return <MyCenteredSpin height="80px" />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Flex gap={16}>
|
<Flex gap={16}>
|
||||||
<Avatar
|
<MyUserAvatar
|
||||||
src={`${Constants.STATIC_CONTENT_ADDRESS}/demo/lesson_thumbnail.webp`}
|
profilePictureUrl={user.ProfilePictureUrl}
|
||||||
size={56}
|
firstName={user.FirstName}
|
||||||
|
disableCursorPointer
|
||||||
/>
|
/>
|
||||||
<Flex vertical style={{ width: "100%" }}>
|
<Flex vertical style={{ width: "100%" }}>
|
||||||
<Typography style={{ fontSize: 24, fontWeight: 800 }}>
|
<Typography style={{ fontSize: 24, fontWeight: 800 }}>
|
||||||
{user.FirstName} {user.LastName}
|
{user.FirstName} {user.LastName}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography style={{ fontSize: 18, fontWeight: 500 }}>
|
<Typography style={{ fontSize: 18, fontWeight: 500 }}>
|
||||||
{text}
|
{message}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Flex gap={0} align="center">
|
<Flex gap={0} align="center">
|
||||||
<Button
|
<Button
|
||||||
|
@ -272,9 +300,9 @@ export function QuestionUIRaw({
|
||||||
style={{
|
style={{
|
||||||
color: hasLiked ? "red" : undefined,
|
color: hasLiked ? "red" : undefined,
|
||||||
transform: hasLiked ? "scale(1.2)" : "scale(1)",
|
transform: hasLiked ? "scale(1.2)" : "scale(1)",
|
||||||
transition: "all 0.3s ease-in-out",
|
transition: "all 0.2s ease-in-out",
|
||||||
}}
|
}}
|
||||||
onClick={toggleLike}
|
onClick={() => (hasLiked ? onDislike() : onLike())}
|
||||||
></Button>
|
></Button>
|
||||||
|
|
||||||
<Typography
|
<Typography
|
||||||
|
@ -285,7 +313,7 @@ export function QuestionUIRaw({
|
||||||
<Button
|
<Button
|
||||||
type={replyFormVisible ? "link" : "text"}
|
type={replyFormVisible ? "link" : "text"}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (replyText === null) setReplyText(userAt);
|
if (replyMessage === null) setReplyMessage(userAt);
|
||||||
setReplyFormVisible(!replyFormVisible);
|
setReplyFormVisible(!replyFormVisible);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -301,14 +329,17 @@ export function QuestionUIRaw({
|
||||||
</Flex>
|
</Flex>
|
||||||
{replyFormVisible ? (
|
{replyFormVisible ? (
|
||||||
<Form
|
<Form
|
||||||
|
initialValues={{
|
||||||
|
reply: replyMessage ? replyMessage : userAt,
|
||||||
|
}}
|
||||||
disabled={isSendingReply}
|
disabled={isSendingReply}
|
||||||
onFinish={async () => {
|
onFinish={async () => {
|
||||||
setIsSendingReply(true);
|
setIsSendingReply(true);
|
||||||
await onReply(replyText ? replyText : "", replyID);
|
await onReply(replyMessage ? replyMessage : "", replyId);
|
||||||
|
|
||||||
setIsSendingReply(false);
|
setIsSendingReply(false);
|
||||||
setReplyFormVisible(false);
|
setReplyFormVisible(false);
|
||||||
setReplyText(null);
|
setReplyMessage(null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
@ -317,10 +348,11 @@ export function QuestionUIRaw({
|
||||||
>
|
>
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
defaultValue={replyText ? replyText : userAt}
|
value={replyMessage ? replyMessage : userAt}
|
||||||
value={replyText ? replyText : userAt}
|
|
||||||
placeholder="Write a reply"
|
placeholder="Write a reply"
|
||||||
onChange={(e) => setReplyText(e.target.value)}
|
onChange={(e) => setReplyMessage(e.target.value)}
|
||||||
|
autoSize={{ minRows: 2 }}
|
||||||
|
maxLength={5000}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
|
|
|
@ -1,34 +1,65 @@
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from "@reduxjs/toolkit";
|
||||||
import { LessonContent, LessonState } from 'core/types/lesson';
|
import { LessonQuestion } from "core/types/lesson";
|
||||||
|
|
||||||
interface AddLessonContentAction {
|
export const lessonQuestionsSlice = createSlice({
|
||||||
type: string;
|
name: "questions",
|
||||||
payload: LessonContent;
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
export const lessonPageEditorSlice = createSlice({
|
|
||||||
name: 'lessonQuestions',
|
|
||||||
initialState: {
|
initialState: {
|
||||||
editorActive: false,
|
questions: [] as LessonQuestion[],
|
||||||
currentLessonId: '', // required in sideMenu because has no access to useParams
|
likedQuestions: [] as string[],
|
||||||
lessonThumbnail: {
|
|
||||||
img: '',
|
|
||||||
title: 'Tesdt',
|
|
||||||
},
|
|
||||||
lessonContents: [] as LessonContent[],
|
|
||||||
lessonState: LessonState.Draft,
|
|
||||||
},
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
setEditorActive: (state, action) => {
|
setQuestions: (state, action) => {
|
||||||
state.editorActive = action.payload;
|
state.questions = action.payload as LessonQuestion[];
|
||||||
|
},
|
||||||
|
addQuestion: (state, action) => {
|
||||||
|
state.questions.push(action.payload as LessonQuestion);
|
||||||
|
},
|
||||||
|
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: {
|
selectors: {
|
||||||
editorActive: (state) => state.editorActive,
|
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;
|
||||||
*/
|
|
||||||
|
|
Loading…
Reference in New Issue