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.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
<title>App</title>
|
||||
</head>
|
||||
<body>
|
||||
<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({
|
||||
name: 'app',
|
||||
name: "app",
|
||||
initialState: {
|
||||
darkMode: false,
|
||||
userAuthenticated: null,
|
||||
userProfilePictureUrl: null,
|
||||
primaryColor: '#111',
|
||||
primaryColor: "#111",
|
||||
logoUrl: null,
|
||||
bannerUrl: null,
|
||||
cachedUsers: {} as Record<string, CachedUser>,
|
||||
},
|
||||
reducers: {
|
||||
setDarkMode: (state, action) => {
|
||||
|
@ -29,6 +31,9 @@ export const appSlice = createSlice({
|
|||
setBannerUrl: (state, action) => {
|
||||
state.bannerUrl = action.payload;
|
||||
},
|
||||
addUserToCache: (state, action) => {
|
||||
state.cachedUsers[action.payload.id] = action.payload;
|
||||
},
|
||||
},
|
||||
selectors: {
|
||||
darkMode: (state) => state.darkMode,
|
||||
|
@ -37,9 +42,26 @@ export const appSlice = createSlice({
|
|||
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;
|
||||
|
|
|
@ -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,
|
||||
LessonQuestion,
|
||||
LessonSettings,
|
||||
QuestionsResponse,
|
||||
UpdateLessonPreviewThumbnail,
|
||||
} from "core/types/lesson";
|
||||
|
||||
|
@ -94,12 +95,42 @@ export const lessonsApi = createApi({
|
|||
method: "DELETE",
|
||||
}),
|
||||
}),
|
||||
getQuestions: builder.query<LessonQuestion[], string>({
|
||||
getLessonQuestions: builder.query<QuestionsResponse, string>({
|
||||
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;
|
||||
|
|
|
@ -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<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 { 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 {
|
||||
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,
|
||||
|
@ -12,10 +22,26 @@ import {
|
|||
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';
|
||||
} 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;
|
||||
|
@ -29,7 +55,9 @@ class WebSocketService {
|
|||
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;
|
||||
|
@ -38,10 +66,14 @@ class WebSocketService {
|
|||
private dispatch: Dispatch | null = null;
|
||||
|
||||
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 = () => {
|
||||
console.log('WebSocket connected', this.firstConnect);
|
||||
console.log("WebSocket connected", this.firstConnect);
|
||||
|
||||
// Send all messages from the offline queue
|
||||
|
||||
|
@ -63,21 +95,24 @@ class WebSocketService {
|
|||
if (this.messageHandler) {
|
||||
this.messageHandler(data, this.dispatch!);
|
||||
} else {
|
||||
console.error('No handler defined for WebSocket messages');
|
||||
console.error("No handler defined for WebSocket messages");
|
||||
}
|
||||
};
|
||||
|
||||
this.socket.onclose = () => {
|
||||
console.log('WebSocket disconnected. Reconnecting...');
|
||||
console.log("WebSocket disconnected. Reconnecting...");
|
||||
setTimeout(() => this.connect(), this.reconnectInterval);
|
||||
};
|
||||
|
||||
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.dispatch = dispatch;
|
||||
}
|
||||
|
@ -102,10 +137,10 @@ class WebSocketService {
|
|||
}
|
||||
}
|
||||
|
||||
const webSocketConnectionEventName = 'WebSocketConnectionEvent';
|
||||
const webSocketConnectionEventName = "WebSocketConnectionEvent";
|
||||
|
||||
const webSocketConnectionEvent = new CustomEvent(webSocketConnectionEventName, {
|
||||
detail: 'wsReconnect',
|
||||
detail: "wsReconnect",
|
||||
});
|
||||
|
||||
export function addWebSocketReconnectListener(callback: () => void): void {
|
||||
|
@ -119,10 +154,13 @@ export function removeWebSocketReconnectListener(callback: () => void): void {
|
|||
const webSocketService = new WebSocketService(Constants.WS_ADDRESS);
|
||||
export default webSocketService;
|
||||
|
||||
export function WebSocketMessageHandler(message: WebSocketMessage, dispatch: Dispatch) {
|
||||
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:
|
||||
|
@ -135,9 +173,11 @@ export function WebSocketMessageHandler(message: WebSocketMessage, dispatch: Dis
|
|||
dispatch(setBannerUrl(Body));
|
||||
break;
|
||||
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;
|
||||
case WebSocketReceivedMessagesCmds.TeamAddedMember:
|
||||
dispatch(addTeamMember(Body));
|
||||
|
@ -249,7 +289,24 @@ export function WebSocketMessageHandler(message: WebSocketMessage, dispatch: Dis
|
|||
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);
|
||||
console.error("Unknown message type:", Cmd);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
@ -5,3 +5,10 @@ export interface UserProfile {
|
|||
Email: string;
|
||||
RoleId: string;
|
||||
}
|
||||
|
||||
export interface CachedUser {
|
||||
Id: string;
|
||||
FirstName: string;
|
||||
LastName: string;
|
||||
ProfilePictureUrl: string;
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
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 (
|
||||
|
@ -8,10 +8,13 @@ export default function ContactSupport() {
|
|||
<MyBanner title="Contact 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.Item label="E-Mail">
|
||||
<a href="mailto:support@jannex.de">support@jannex.de</a>
|
||||
<a href="mailto:support@.">support@.</a>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</MyMiddleCard>
|
||||
|
|
|
@ -79,7 +79,7 @@ export default function LessonPage() {
|
|||
<MyMiddleCard
|
||||
outOfCardChildren={
|
||||
<div style={{ marginTop: 24 }}>
|
||||
<Questions lessionID={"lessionID"} />
|
||||
<Questions />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
|
|
|
@ -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 (
|
||||
<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",
|
||||
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 (
|
||||
<Flex justify="center">
|
||||
<Flex style={{ width: 800, maxWidth: 800 * 0.9 }} vertical>
|
||||
<Typography.Title level={3}>Questions</Typography.Title>
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="Ask a question">
|
||||
<Input.TextArea placeholder={"Type something"} />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary">Submit</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<CreateQuestionForm />
|
||||
|
||||
<Flex vertical style={{}}>
|
||||
{questions.map((question) => (
|
||||
<QuestionItem key={question.Id} question={question} />
|
||||
))}
|
||||
{error ? (
|
||||
<MyErrorResult />
|
||||
) : isLoading ? (
|
||||
<MyCenteredSpin height="100px" />
|
||||
) : (
|
||||
dataQuestions
|
||||
.filter((question) => question.QuestionId === "")
|
||||
.sort((b, a) => a.CreatedAt.localeCompare(b.CreatedAt))
|
||||
.map((question) => (
|
||||
<QuestionItem
|
||||
key={question.Id}
|
||||
lessonId={lessonId as string}
|
||||
question={question}
|
||||
replies={dataQuestions.filter(
|
||||
(reply) => reply.QuestionId === question.Id
|
||||
)}
|
||||
likedQuestions={dataLikedQuestions}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
type HandleReplyFunction = (text: string, replyID?: string) => Promise<void>;
|
||||
type HandleReplyFunction = (message: string, replyId?: string) => Promise<void>;
|
||||
|
||||
export function QuestionItem({ question }: { question: LessonQuestion }) {
|
||||
const [showReplies, setShowReplies] = React.useState(1);
|
||||
|
||||
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",
|
||||
},
|
||||
];
|
||||
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return nodes;
|
||||
})()}
|
||||
</div>
|
||||
}
|
||||
likes={question.Likes}
|
||||
onReply={handleReply}
|
||||
onLike={() => {}}
|
||||
replyID={undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function QuestionReplyItem({
|
||||
export function QuestionItem({
|
||||
lessonId,
|
||||
question,
|
||||
handleReply,
|
||||
replies,
|
||||
likedQuestions,
|
||||
}: {
|
||||
question: LessonQuestionReply;
|
||||
handleReply: HandleReplyFunction;
|
||||
lessonId: string;
|
||||
question: LessonQuestion;
|
||||
replies: LessonQuestion[];
|
||||
likedQuestions: string[];
|
||||
}) {
|
||||
let user = {
|
||||
Id: "132154153613",
|
||||
FirstName: "Anja",
|
||||
LastName: "Blasinstroment",
|
||||
};
|
||||
const [showReplies, setShowReplies] = useState(1);
|
||||
const { success, error } = useMessage();
|
||||
|
||||
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 (
|
||||
<QuestionUIRaw
|
||||
userID={user.Id}
|
||||
text={question.Reply}
|
||||
userId={question.CreatorUserId}
|
||||
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={<></>}
|
||||
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={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 | string>(null);
|
||||
const [isSendingReply, setIsSendingReply] = React.useState(false);
|
||||
const [replyFormVisible, setReplyFormVisible] = useState(false);
|
||||
const [replyMessage, setReplyMessage] = useState<null | string>(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<InputRef>(null);
|
||||
|
||||
const userAt =
|
||||
isLoading || user === undefined
|
||||
? ""
|
||||
: `@${user.FirstName} ${user.LastName} `;
|
||||
|
||||
if (error) return <MyErrorResult />;
|
||||
|
||||
if (isLoading) return <MyCenteredSpin height="80px" />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex gap={16}>
|
||||
<Avatar
|
||||
src={`${Constants.STATIC_CONTENT_ADDRESS}/demo/lesson_thumbnail.webp`}
|
||||
size={56}
|
||||
<MyUserAvatar
|
||||
profilePictureUrl={user.ProfilePictureUrl}
|
||||
firstName={user.FirstName}
|
||||
disableCursorPointer
|
||||
/>
|
||||
<Flex vertical style={{ width: "100%" }}>
|
||||
<Typography style={{ fontSize: 24, fontWeight: 800 }}>
|
||||
{user.FirstName} {user.LastName}
|
||||
</Typography>
|
||||
<Typography style={{ fontSize: 18, fontWeight: 500 }}>
|
||||
{text}
|
||||
{message}
|
||||
</Typography>
|
||||
<Flex gap={0} align="center">
|
||||
<Button
|
||||
|
@ -272,9 +300,9 @@ export function QuestionUIRaw({
|
|||
style={{
|
||||
color: hasLiked ? "red" : undefined,
|
||||
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>
|
||||
|
||||
<Typography
|
||||
|
@ -285,7 +313,7 @@ export function QuestionUIRaw({
|
|||
<Button
|
||||
type={replyFormVisible ? "link" : "text"}
|
||||
onClick={() => {
|
||||
if (replyText === null) setReplyText(userAt);
|
||||
if (replyMessage === null) setReplyMessage(userAt);
|
||||
setReplyFormVisible(!replyFormVisible);
|
||||
|
||||
setTimeout(() => {
|
||||
|
@ -301,14 +329,17 @@ export function QuestionUIRaw({
|
|||
</Flex>
|
||||
{replyFormVisible ? (
|
||||
<Form
|
||||
initialValues={{
|
||||
reply: replyMessage ? replyMessage : userAt,
|
||||
}}
|
||||
disabled={isSendingReply}
|
||||
onFinish={async () => {
|
||||
setIsSendingReply(true);
|
||||
await onReply(replyText ? replyText : "", replyID);
|
||||
await onReply(replyMessage ? replyMessage : "", replyId);
|
||||
|
||||
setIsSendingReply(false);
|
||||
setReplyFormVisible(false);
|
||||
setReplyText(null);
|
||||
setReplyMessage(null);
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
|
@ -317,10 +348,11 @@ export function QuestionUIRaw({
|
|||
>
|
||||
<Input.TextArea
|
||||
ref={inputRef}
|
||||
defaultValue={replyText ? replyText : userAt}
|
||||
value={replyText ? replyText : userAt}
|
||||
value={replyMessage ? replyMessage : userAt}
|
||||
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>
|
||||
|
|
|
@ -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',
|
||||
export const lessonQuestionsSlice = createSlice({
|
||||
name: "questions",
|
||||
initialState: {
|
||||
editorActive: false,
|
||||
currentLessonId: '', // required in sideMenu because has no access to useParams
|
||||
lessonThumbnail: {
|
||||
img: '',
|
||||
title: 'Tesdt',
|
||||
},
|
||||
lessonContents: [] as LessonContent[],
|
||||
lessonState: LessonState.Draft,
|
||||
questions: [] as LessonQuestion[],
|
||||
likedQuestions: [] as string[],
|
||||
},
|
||||
reducers: {
|
||||
setEditorActive: (state, action) => {
|
||||
state.editorActive = action.payload;
|
||||
setQuestions: (state, action) => {
|
||||
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: {
|
||||
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