added questions, reply and like functionality"

main
alex 2024-09-12 20:06:10 +02:00
parent 679ee5bf28
commit 9be79b1481
14 changed files with 757 additions and 530 deletions

View File

@ -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>

View File

@ -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;

View File

@ -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,
};
}

View File

@ -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;

View File

@ -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;

View File

@ -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);
} }
} }

View File

@ -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,

View File

@ -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;
} }

View File

@ -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;
}

View File

@ -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 };

View File

@ -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>

View File

@ -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>
} }
> >

View File

@ -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>

View File

@ -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;
*/