websocket events for live editing
parent
61d01eedc7
commit
3bba897454
|
@ -63,8 +63,6 @@ export default function App() {
|
|||
}, [uAuthenticated]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("App mounted");
|
||||
|
||||
if (!localStorage.getItem("session")) {
|
||||
dispatch(setUserAuthenticated(false));
|
||||
} else {
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import { Grid, Layout } from 'antd';
|
||||
import PageContent from '../PageContent';
|
||||
import SideMenuDesktop from '../SideMenu/Desktop';
|
||||
import SideMenuMobile from '../SideMenu/Mobile';
|
||||
import { SideMenuContent, SideMenuEditorContent } from '../SideMenu';
|
||||
import { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { setIsSideMenuCollapsed } from '../SideMenu/sideMenuSlice';
|
||||
import { editorActive } from '../../../features/Lessons/LessonPageEditor/lessonPageEditorSlice';
|
||||
import MyDndContext from './MyDndContext';
|
||||
import AiChat from 'features/AiChat';
|
||||
import { Grid, Layout } from "antd";
|
||||
import PageContent from "../PageContent";
|
||||
import SideMenuDesktop from "../SideMenu/Desktop";
|
||||
import SideMenuMobile from "../SideMenu/Mobile";
|
||||
import { SideMenuContent, SideMenuEditorContent } from "../SideMenu";
|
||||
import { useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { setIsSideMenuCollapsed } from "../SideMenu/sideMenuSlice";
|
||||
import { editorActive } from "../../../features/Lessons/LessonPageEditor/lessonPageEditorSlice";
|
||||
import MyDndContext from "./MyDndContext";
|
||||
import AiChat from "features/AiChat";
|
||||
|
||||
const { useBreakpoint } = Grid;
|
||||
|
||||
|
@ -19,8 +19,6 @@ export function SideMenu() {
|
|||
|
||||
const isEditorActive = useSelector(editorActive);
|
||||
|
||||
console.log('isEditorActive', isEditorActive);
|
||||
|
||||
const Content = () => {
|
||||
return isEditorActive ? <SideMenuEditorContent /> : <SideMenuContent />;
|
||||
};
|
||||
|
@ -31,7 +29,6 @@ export function SideMenu() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<AiChat />
|
||||
{screenBreakpoint.lg ? (
|
||||
<SideMenuDesktop>
|
||||
<Content />
|
||||
|
@ -48,11 +45,13 @@ export function SideMenu() {
|
|||
export default function DashboardLayout() {
|
||||
return (
|
||||
<MyDndContext>
|
||||
<Layout style={{ minHeight: '100vh' }}>
|
||||
<Layout style={{ minHeight: "100vh" }}>
|
||||
<Layout>
|
||||
<SideMenu />
|
||||
|
||||
<PageContent />
|
||||
|
||||
<AiChat />
|
||||
</Layout>
|
||||
</Layout>
|
||||
</MyDndContext>
|
||||
|
|
|
@ -23,7 +23,6 @@ import {
|
|||
BrowserTabSession,
|
||||
Constants,
|
||||
} from "core/utils/utils";
|
||||
import Search from "antd/es/input/Search";
|
||||
import { MyContainer } from "shared/components/MyContainer";
|
||||
import {
|
||||
addLessonContent,
|
||||
|
@ -45,6 +44,8 @@ import webSocketService, {
|
|||
removeWebSocketReconnectListener,
|
||||
} from "core/services/websocketService";
|
||||
import { WebSocketSendMessagesCmds } from "core/utils/webSocket";
|
||||
import { useMessage } from "core/context/MessageContext";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
export function SideMenuContent() {
|
||||
const location = useLocation();
|
||||
|
@ -150,10 +151,9 @@ export function SideMenuContent() {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
// subscribe to the current page
|
||||
const pathname = location.pathname;
|
||||
|
||||
setSelectedKeys(pathname);
|
||||
|
||||
const subscribeTopicMessage = () => {
|
||||
webSocketService.send({
|
||||
Cmd: WebSocketSendMessagesCmds.SubscribeToTopic,
|
||||
|
@ -165,14 +165,18 @@ export function SideMenuContent() {
|
|||
};
|
||||
|
||||
subscribeTopicMessage();
|
||||
|
||||
addWebSocketReconnectListener(subscribeTopicMessage);
|
||||
|
||||
// set selected keys
|
||||
|
||||
let path = pathname.split("/");
|
||||
|
||||
if (path.length > 2) {
|
||||
setSelectedKeys(`/${path[1]}`);
|
||||
// /store/:storeId/:subPage - open the store menu
|
||||
setOpenKeys([`/${path[1]}/${path[2]}`]);
|
||||
//setOpenKeys([`/${path[1]}/${path[2]}`]);
|
||||
} else {
|
||||
setSelectedKeys(pathname);
|
||||
}
|
||||
|
||||
// auto close sideMenu on mobile
|
||||
|
@ -259,16 +263,37 @@ export function SideMenuContent() {
|
|||
}
|
||||
|
||||
export function SideMenuEditorContent() {
|
||||
// create is dragging useState
|
||||
const location = useLocation();
|
||||
const [isDragging, setIsDragging] = useState<String | null>(null);
|
||||
const { lessonId } = useParams();
|
||||
const { success, error } = useMessage();
|
||||
|
||||
const [form] = useForm();
|
||||
|
||||
const lnState = useSelector(lessonState);
|
||||
const currentLnId = useSelector(currentLessonId);
|
||||
|
||||
const [updateLessonState] = useUpdateLessonStateMutation();
|
||||
const [reqUpdateLessonState] = useUpdateLessonStateMutation();
|
||||
|
||||
useEffect(() => {
|
||||
// subscribe to the current page
|
||||
const pathname = location.pathname;
|
||||
|
||||
const subscribeTopicMessage = () => {
|
||||
webSocketService.send({
|
||||
Cmd: WebSocketSendMessagesCmds.SubscribeToTopic,
|
||||
Body: {
|
||||
topic: pathname,
|
||||
browserTabSession: BrowserTabSession,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
subscribeTopicMessage();
|
||||
addWebSocketReconnectListener(subscribeTopicMessage);
|
||||
|
||||
return () => removeWebSocketReconnectListener(subscribeTopicMessage);
|
||||
}, [location.pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue({
|
||||
|
@ -276,14 +301,11 @@ export function SideMenuEditorContent() {
|
|||
});
|
||||
}, [lnState]);
|
||||
|
||||
// <Search placeholder="What would you like to insert?" />;
|
||||
|
||||
return (
|
||||
<Flex justify="space-between" vertical style={{ height: "100%" }}>
|
||||
<MyContainer>
|
||||
<Search
|
||||
placeholder="What would you like to insert?"
|
||||
style={{ paddingBottom: 16 }}
|
||||
/>
|
||||
|
||||
<DndContext
|
||||
onDragStart={(event) => {
|
||||
console.log("drag start", event.active.id);
|
||||
|
@ -296,10 +318,14 @@ export function SideMenuEditorContent() {
|
|||
}}
|
||||
>
|
||||
{componentsGroups.map((group, i) => (
|
||||
<div key={i}>
|
||||
<div key={i} style={{ paddingTop: 16 }}>
|
||||
<span>{group.category}</span>
|
||||
|
||||
<Flex gap={16} wrap style={{ paddingTop: 16 }}>
|
||||
<Flex
|
||||
gap={16}
|
||||
wrap
|
||||
style={{ paddingTop: 16, userSelect: "none" }}
|
||||
>
|
||||
{group.components.map((component, i) => (
|
||||
<DraggableCreateComponent key={i} component={component} />
|
||||
))}
|
||||
|
@ -340,12 +366,15 @@ export function SideMenuEditorContent() {
|
|||
console.log("state changed", value, lessonId);
|
||||
|
||||
try {
|
||||
await updateLessonState({
|
||||
await reqUpdateLessonState({
|
||||
lessonId: currentLnId,
|
||||
newState: value,
|
||||
}).unwrap();
|
||||
|
||||
success("Lesson state updated successfully");
|
||||
} catch (err) {
|
||||
console.log("error", err);
|
||||
error("Failed to update lesson state");
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
@ -366,7 +395,7 @@ export function DraggableCreateComponent({
|
|||
}: {
|
||||
component: Component;
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
// const dispatch = useDispatch();
|
||||
|
||||
/*const { attributes, listeners, setNodeRef, transform, isDragging, active } =
|
||||
useDraggable({
|
||||
|
@ -394,9 +423,10 @@ export function DraggableCreateComponent({
|
|||
}
|
||||
|
||||
function CreateComponent({ component }: { component: Component }) {
|
||||
const { error } = useMessage();
|
||||
const dispatch = useDispatch();
|
||||
const isDarkMode = useSelector(darkMode);
|
||||
|
||||
const isDarkMode = useSelector(darkMode);
|
||||
const currentLnId = useSelector(currentLessonId);
|
||||
|
||||
const [reqAddLessonContent] = useAddLessonContentMutation();
|
||||
|
@ -406,15 +436,16 @@ function CreateComponent({ component }: { component: Component }) {
|
|||
vertical
|
||||
align="center"
|
||||
justify="center"
|
||||
className={styles.createComponentContainer}
|
||||
style={{
|
||||
backgroundColor: !isDarkMode ? "#f0f2f5" : "#1f1f1f",
|
||||
height: 80,
|
||||
width: 80,
|
||||
cursor: "pointer",
|
||||
borderRadius: 4,
|
||||
transition: "background-color 0.2s",
|
||||
}}
|
||||
onClick={async () => {
|
||||
console.log("insert component", component.type);
|
||||
|
||||
try {
|
||||
const res = await reqAddLessonContent({
|
||||
lessonId: currentLnId,
|
||||
|
@ -422,8 +453,6 @@ function CreateComponent({ component }: { component: Component }) {
|
|||
data: component.defaultData || "",
|
||||
}).unwrap();
|
||||
|
||||
console.log("add content", component);
|
||||
|
||||
dispatch(
|
||||
addLessonContent({
|
||||
Id: res.Id,
|
||||
|
@ -435,15 +464,18 @@ function CreateComponent({ component }: { component: Component }) {
|
|||
);
|
||||
} catch (err) {
|
||||
console.log("error", err);
|
||||
error("Failed to add content");
|
||||
}
|
||||
}}
|
||||
>
|
||||
{component.thumbnail ? (
|
||||
<div>
|
||||
<img
|
||||
draggable={false}
|
||||
src={component.thumbnail}
|
||||
style={{
|
||||
width: 40,
|
||||
maxHeight: 40,
|
||||
filter:
|
||||
isDarkMode && component.invertThumbnailAtDarkmode
|
||||
? "invert(1)"
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
.createComponentContainer:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
|
@ -4,8 +4,30 @@ import {
|
|||
setLogoUrl,
|
||||
setPrimaryColor,
|
||||
} from "core/reducers/appSlice";
|
||||
import { store } from "core/store/store";
|
||||
import { BrowserTabSession, Constants } from "core/utils/utils";
|
||||
import { WebSocketReceivedMessagesCmds } from "core/utils/webSocket";
|
||||
import {
|
||||
addLessonPageContent,
|
||||
deleteLessonPageContent,
|
||||
updateLessonPageContent,
|
||||
updateLessonPageContentPosition,
|
||||
} from "features/Lessons/LessonPage/lessonPageSlice";
|
||||
import {
|
||||
addLessonContent,
|
||||
deleteLessonContent,
|
||||
setLessonThumbnailTitle,
|
||||
setLessonThumbnailUrl,
|
||||
setPageEditorLessonState,
|
||||
updateLessonContent,
|
||||
updateLessonContentPosition,
|
||||
} from "features/Lessons/LessonPageEditor/lessonPageEditorSlice";
|
||||
import {
|
||||
addLesson,
|
||||
updateLessonPreviewThumbnail,
|
||||
updateLessonPreviewTitle,
|
||||
updateLessonState,
|
||||
} from "features/Lessons/lessonsSlice";
|
||||
import {
|
||||
addTeamMember,
|
||||
deleteTeamMember,
|
||||
|
@ -20,7 +42,7 @@ interface WebSocketMessage {
|
|||
class WebSocketService {
|
||||
private url: string;
|
||||
private socket: WebSocket | null = null;
|
||||
private reconnectInterval: number = 1000; // in ms
|
||||
private reconnectInterval: number = 2000; // in ms
|
||||
private offlineQueue: WebSocketMessage[] = [];
|
||||
private firstConnect: boolean = true;
|
||||
|
||||
|
@ -157,6 +179,103 @@ export function WebSocketMessageHandler(
|
|||
case WebSocketReceivedMessagesCmds.TeamDeletedMember:
|
||||
dispatch(deleteTeamMember(Body));
|
||||
break;
|
||||
case WebSocketReceivedMessagesCmds.LessonCreated:
|
||||
dispatch(addLesson(Body));
|
||||
break;
|
||||
case WebSocketReceivedMessagesCmds.LessonPreviewTitleUpdated:
|
||||
dispatch(updateLessonPreviewTitle(Body));
|
||||
|
||||
if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) {
|
||||
dispatch(setLessonThumbnailTitle(Body.Title));
|
||||
}
|
||||
break;
|
||||
case WebSocketReceivedMessagesCmds.LessonPreviewThumbnailUpdated:
|
||||
dispatch(updateLessonPreviewThumbnail(Body));
|
||||
|
||||
if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) {
|
||||
dispatch(setLessonThumbnailUrl(Body.ThumbnailUrl));
|
||||
}
|
||||
break;
|
||||
case WebSocketReceivedMessagesCmds.LessonStateUpdated:
|
||||
dispatch(updateLessonState(Body));
|
||||
|
||||
if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) {
|
||||
dispatch(setPageEditorLessonState(Body.State));
|
||||
}
|
||||
break;
|
||||
case WebSocketReceivedMessagesCmds.LessonAddedContent:
|
||||
if (Body.LessonId === store.getState().lessonPage.currentLessonId) {
|
||||
dispatch(addLessonPageContent(Body));
|
||||
}
|
||||
if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) {
|
||||
dispatch(addLessonContent(Body));
|
||||
}
|
||||
break;
|
||||
case WebSocketReceivedMessagesCmds.LessonDeletedContent:
|
||||
if (Body.LessonId === store.getState().lessonPage.currentLessonId) {
|
||||
dispatch(deleteLessonPageContent(Body.ContentId));
|
||||
}
|
||||
if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) {
|
||||
dispatch(deleteLessonContent(Body.ContentId));
|
||||
}
|
||||
break;
|
||||
case WebSocketReceivedMessagesCmds.LessonContentUpdated:
|
||||
if (Body.LessonId === store.getState().lessonPage.currentLessonId) {
|
||||
dispatch(
|
||||
updateLessonPageContent({
|
||||
id: Body.ContentId,
|
||||
data: Body.Data,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) {
|
||||
dispatch(
|
||||
updateLessonContent({
|
||||
id: Body.ContentId,
|
||||
data: Body.Data,
|
||||
})
|
||||
);
|
||||
}
|
||||
break;
|
||||
case WebSocketReceivedMessagesCmds.LessonContentUpdatedPosition:
|
||||
if (Body.LessonId === store.getState().lessonPage.currentLessonId) {
|
||||
dispatch(
|
||||
updateLessonPageContentPosition({
|
||||
contentId: Body.ContentId,
|
||||
position: Body.Position,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) {
|
||||
dispatch(
|
||||
updateLessonContentPosition({
|
||||
contentId: Body.ContentId,
|
||||
position: Body.Position,
|
||||
})
|
||||
);
|
||||
}
|
||||
break;
|
||||
case WebSocketReceivedMessagesCmds.LessonContentFileUpdated:
|
||||
if (Body.LessonId === store.getState().lessonPage.currentLessonId) {
|
||||
dispatch(
|
||||
updateLessonPageContent({
|
||||
id: Body.ContentId,
|
||||
data: Body.Data,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) {
|
||||
dispatch(
|
||||
updateLessonContent({
|
||||
id: Body.ContentId,
|
||||
data: Body.Data,
|
||||
})
|
||||
);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.error("Unknown message type:", Cmd);
|
||||
}
|
||||
|
|
|
@ -6,6 +6,8 @@ import { appSlice } from "../reducers/appSlice";
|
|||
import { lessonsApi } from "core/services/lessons";
|
||||
import { organizationApi } from "core/services/organization";
|
||||
import { teamSlice } from "features/Team/teamSlice";
|
||||
import { lessonsSlice } from "features/Lessons/lessonsSlice";
|
||||
import { lessonPageSlice } from "features/Lessons/LessonPage/lessonPageSlice";
|
||||
|
||||
const makeStore = (/* preloadedState */) => {
|
||||
const store = configureStore({
|
||||
|
@ -15,6 +17,8 @@ const makeStore = (/* preloadedState */) => {
|
|||
lessonPageEditor: lessonPageEditorSlice.reducer,
|
||||
teamSlice: teamSlice.reducer,
|
||||
[lessonsApi.reducerPath]: lessonsApi.reducer,
|
||||
[lessonsSlice.reducerPath]: lessonsSlice.reducer,
|
||||
[lessonPageSlice.reducerPath]: lessonPageSlice.reducer,
|
||||
[organizationApi.reducerPath]: organizationApi.reducer,
|
||||
[teamSlice.reducerPath]: teamSlice.reducer,
|
||||
},
|
||||
|
|
|
@ -10,6 +10,15 @@ enum WebSocketReceivedMessagesCmds {
|
|||
TeamAddedMember = 5,
|
||||
TeamUpdatedMemberRole = 6,
|
||||
TeamDeletedMember = 7,
|
||||
LessonCreated = 8,
|
||||
LessonPreviewTitleUpdated = 9,
|
||||
LessonPreviewThumbnailUpdated = 10,
|
||||
LessonStateUpdated = 11,
|
||||
LessonAddedContent = 12,
|
||||
LessonDeletedContent = 13,
|
||||
LessonContentUpdated = 14,
|
||||
LessonContentUpdatedPosition = 15,
|
||||
LessonContentFileUpdated = 16,
|
||||
}
|
||||
|
||||
export { WebSocketSendMessagesCmds, WebSocketReceivedMessagesCmds };
|
||||
|
|
|
@ -1,32 +1,61 @@
|
|||
import { Button, Flex } from 'antd';
|
||||
import { CheckOutlined } from '@ant-design/icons';
|
||||
import HeaderBar from '../../../core/components/Header';
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import { Constants } from '../../../core/utils/utils';
|
||||
import React from 'react';
|
||||
import MySpin from 'shared/components/MySpin';
|
||||
import MyErrorResult from 'shared/components/MyResult';
|
||||
import MyEmpty from 'shared/components/MyEmpty';
|
||||
import { useGetLessonContentsQuery } from 'core/services/lessons';
|
||||
import MyMiddleCard from 'shared/components/MyMiddleCard';
|
||||
import { Converter } from '../converter';
|
||||
import Questions from '../Questions';
|
||||
import { Button, Flex } from "antd";
|
||||
import { CheckOutlined } from "@ant-design/icons";
|
||||
import HeaderBar from "core/components/Header";
|
||||
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
||||
import { Constants } from "core/utils/utils";
|
||||
import React, { useEffect } from "react";
|
||||
import MyErrorResult from "shared/components/MyResult";
|
||||
import MyEmpty from "shared/components/MyEmpty";
|
||||
import { useGetLessonContentsQuery } from "core/services/lessons";
|
||||
import MyMiddleCard from "shared/components/MyMiddleCard";
|
||||
import { Converter } from "../converter";
|
||||
import Questions from "../Questions";
|
||||
import {
|
||||
addWebSocketReconnectListener,
|
||||
removeWebSocketReconnectListener,
|
||||
} from "core/services/websocketService";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
lessonPageContents,
|
||||
setLessonPageContents,
|
||||
setLessonPageCurrentLessonId,
|
||||
} from "./lessonPageSlice";
|
||||
import MyCenteredSpin from "shared/components/MyCenteredSpin";
|
||||
|
||||
const LessonContents: React.FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
const { lessonId } = useParams();
|
||||
|
||||
const { data, error, isLoading } = useGetLessonContentsQuery(lessonId as string, {
|
||||
refetchOnMountOrArgChange: true,
|
||||
});
|
||||
const lessonContents = useSelector(lessonPageContents);
|
||||
|
||||
if (isLoading) return <MySpin />;
|
||||
const { data, error, isLoading, refetch } = useGetLessonContentsQuery(
|
||||
lessonId as string,
|
||||
{
|
||||
refetchOnMountOrArgChange: true,
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setLessonPageCurrentLessonId(lessonId as string));
|
||||
addWebSocketReconnectListener(refetch);
|
||||
|
||||
return () => removeWebSocketReconnectListener(refetch);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
|
||||
dispatch(setLessonPageContents(data));
|
||||
}, [data]);
|
||||
|
||||
if (isLoading) return <MyCenteredSpin height="140px" />;
|
||||
if (error) return <MyErrorResult />;
|
||||
|
||||
if (!data || data.length === 0) return <MyEmpty />;
|
||||
if (!lessonContents || lessonContents.length === 0) return <MyEmpty />;
|
||||
|
||||
return (
|
||||
<>
|
||||
{data.map((lessonContent) => (
|
||||
{lessonContents.map((lessonContent) => (
|
||||
<div key={lessonContent.Id} style={{ paddingBottom: 6 }}>
|
||||
<Converter mode="view" lessonContent={lessonContent} />
|
||||
</div>
|
||||
|
@ -41,12 +70,16 @@ export default function LessonPage() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<HeaderBar theme="light" backTo={Constants.ROUTE_PATHS.LESSIONS.ROOT} onEdit={() => navigate(`${location.pathname}/editor`)} />
|
||||
<HeaderBar
|
||||
theme="light"
|
||||
backTo={Constants.ROUTE_PATHS.LESSIONS.ROOT}
|
||||
onEdit={() => navigate(`${location.pathname}/editor`)}
|
||||
/>
|
||||
|
||||
<MyMiddleCard
|
||||
outOfCardChildren={
|
||||
<div style={{ marginTop: 24 }}>
|
||||
<Questions lessionID={'lessionID'} />
|
||||
<Questions lessionID={"lessionID"} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import { LessonContent } from "core/types/lesson";
|
||||
|
||||
export const lessonPageSlice = createSlice({
|
||||
name: "lessonPage",
|
||||
initialState: {
|
||||
currentLessonId: "",
|
||||
lessonContents: [] as LessonContent[],
|
||||
},
|
||||
reducers: {
|
||||
setLessonPageCurrentLessonId: (state, action) => {
|
||||
state.currentLessonId = action.payload;
|
||||
},
|
||||
setLessonPageContents: (state, action) => {
|
||||
state.lessonContents = action.payload;
|
||||
},
|
||||
addLessonPageContent: (state, action) => {
|
||||
state.lessonContents.push(action.payload);
|
||||
},
|
||||
deleteLessonPageContent: (state, action) => {
|
||||
state.lessonContents = state.lessonContents.filter(
|
||||
(content) => content.Id !== action.payload
|
||||
);
|
||||
},
|
||||
updateLessonPageContent: (state, action) => {
|
||||
const index = state.lessonContents.findIndex(
|
||||
(content) => content.Id === action.payload.id
|
||||
);
|
||||
|
||||
if (index !== -1) {
|
||||
state.lessonContents[index].Data = action.payload.data;
|
||||
}
|
||||
},
|
||||
updateLessonPageContentPosition: (state, action) => {
|
||||
// change only by contentId and new position
|
||||
const content = state.lessonContents.find(
|
||||
(content) => content.Id === action.payload.contentId
|
||||
);
|
||||
|
||||
if (content) {
|
||||
const index = state.lessonContents.indexOf(content);
|
||||
state.lessonContents.splice(index, 1);
|
||||
state.lessonContents.splice(action.payload.position - 1, 0, content);
|
||||
}
|
||||
},
|
||||
},
|
||||
selectors: {
|
||||
lessonPageCurrentLessonId: (state) => state.currentLessonId,
|
||||
lessonPageContents: (state) => state.lessonContents,
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
setLessonPageCurrentLessonId,
|
||||
setLessonPageContents,
|
||||
addLessonPageContent,
|
||||
deleteLessonPageContent,
|
||||
updateLessonPageContent,
|
||||
updateLessonPageContentPosition,
|
||||
} = lessonPageSlice.actions;
|
||||
|
||||
export const { lessonPageCurrentLessonId, lessonPageContents } =
|
||||
lessonPageSlice.selectors;
|
|
@ -1,20 +1,37 @@
|
|||
import { defaultAnimateLayoutChanges, useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { HolderOutlined, DeleteOutlined, CameraOutlined, FolderOpenOutlined } from '@ant-design/icons';
|
||||
import { Flex } from 'antd';
|
||||
import { currentLessonId, deleteLessonContent, updateLessonContent } from './lessonPageEditorSlice';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { getComponentByType } from '../components';
|
||||
import { LessonContent } from 'core/types/lesson';
|
||||
import './styles.module.css';
|
||||
import { Converter } from '../converter';
|
||||
import { useDeleteLessonContentMutation } from 'core/services/lessons';
|
||||
import { defaultAnimateLayoutChanges, useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import {
|
||||
HolderOutlined,
|
||||
DeleteOutlined,
|
||||
CameraOutlined,
|
||||
FolderOpenOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { Flex } from "antd";
|
||||
import {
|
||||
currentLessonId,
|
||||
deleteLessonContent,
|
||||
updateLessonContent,
|
||||
} from "./lessonPageEditorSlice";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { getComponentByType } from "../components";
|
||||
import { LessonContent } from "core/types/lesson";
|
||||
import "./styles.module.css";
|
||||
import { Converter } from "../converter";
|
||||
import { useDeleteLessonContentMutation } from "core/services/lessons";
|
||||
|
||||
const animateLayoutChanges = (args: any) => (args.isSorting || args.wasDragging ? defaultAnimateLayoutChanges(args) : true);
|
||||
const animateLayoutChanges = (args: any) =>
|
||||
args.isSorting || args.wasDragging ? defaultAnimateLayoutChanges(args) : true;
|
||||
|
||||
const SortableEditorItem = (props: { item: LessonContent }) => {
|
||||
const lnContent = props.item;
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: lnContent.Id });
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: lnContent.Id });
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
|
@ -42,19 +59,26 @@ const SortableEditorItem = (props: { item: LessonContent }) => {
|
|||
style={{
|
||||
paddingLeft: 8,
|
||||
paddingRight: 8,
|
||||
touchAction: 'none',
|
||||
cursor: 'grab',
|
||||
touchAction: "none",
|
||||
cursor: "grab",
|
||||
opacity: isDragging ? 0 : 1,
|
||||
}}
|
||||
{...listeners}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex style={{ overflow: 'hidden', width: '100%', transition: '', boxShadow: isDragging ? 'rgba(0, 0, 0, 0.35) 0px 5px 15px;' : '' }}>
|
||||
<Flex
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
width: "100%",
|
||||
transition: "",
|
||||
boxShadow: isDragging ? "rgba(0, 0, 0, 0.08) 0px 5px 15px" : "",
|
||||
}}
|
||||
>
|
||||
<Converter
|
||||
mode="edititable"
|
||||
lessonContent={lnContent}
|
||||
onEdit={(data) => {
|
||||
console.log('edit', lnContent.Id, data);
|
||||
console.log("edit", lnContent.Id, data);
|
||||
|
||||
dispatch(
|
||||
updateLessonContent({
|
||||
|
@ -71,7 +95,7 @@ const SortableEditorItem = (props: { item: LessonContent }) => {
|
|||
<DeleteOutlined
|
||||
className="EditorActionIcon"
|
||||
onClick={() => {
|
||||
console.log('delete', lnContent.Id);
|
||||
console.log("delete", lnContent.Id);
|
||||
dispatch(deleteLessonContent(lnContent.Id));
|
||||
|
||||
try {
|
||||
|
|
|
@ -1,29 +1,55 @@
|
|||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { lessonContents, lessonThumbnail, setCurrentLessonId, setEditorActive, setLessonContents, setLessonState } from './lessonPageEditorSlice';
|
||||
import { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Card, Flex } from 'antd';
|
||||
import { Constants } from 'core/utils/utils';
|
||||
import HeaderBar from 'core/components/Header';
|
||||
import Droppable from './Droppable';
|
||||
import LessonPreviewCard from 'shared/components/MyLessonPreviewCard';
|
||||
import { useGetLessonContentsQuery, useGetLessonSettingsQuery, useUpdateLessonPreviewTitleMutation } from 'core/services/lessons';
|
||||
import MyErrorResult from 'shared/components/MyResult';
|
||||
import styles from './styles.module.css';
|
||||
import MyEmpty from 'shared/components/MyEmpty';
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import {
|
||||
lessonContents,
|
||||
lessonThumbnail,
|
||||
setCurrentLessonId,
|
||||
setEditorActive,
|
||||
setLessonContents,
|
||||
setLessonThumbnailTitle,
|
||||
setLessonThumbnailUrl,
|
||||
setPageEditorLessonState,
|
||||
} from "./lessonPageEditorSlice";
|
||||
import { useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { Card, Flex } from "antd";
|
||||
import { Constants } from "core/utils/utils";
|
||||
import HeaderBar from "core/components/Header";
|
||||
import Droppable from "./Droppable";
|
||||
import LessonPreviewCard from "shared/components/MyLessonPreviewCard";
|
||||
import {
|
||||
useGetLessonContentsQuery,
|
||||
useGetLessonSettingsQuery,
|
||||
useUpdateLessonPreviewTitleMutation,
|
||||
} from "core/services/lessons";
|
||||
import MyErrorResult from "shared/components/MyResult";
|
||||
import styles from "./styles.module.css";
|
||||
import MyEmpty from "shared/components/MyEmpty";
|
||||
import {
|
||||
addWebSocketReconnectListener,
|
||||
removeWebSocketReconnectListener,
|
||||
} from "core/services/websocketService";
|
||||
|
||||
const PreviewCard: React.FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
const { lessonId } = useParams();
|
||||
|
||||
const { data, error, isLoading, refetch } = useGetLessonSettingsQuery(lessonId as string, {
|
||||
const dataLessonThumbnail = useSelector(lessonThumbnail);
|
||||
|
||||
const { data, error, isLoading, refetch } = useGetLessonSettingsQuery(
|
||||
lessonId as string,
|
||||
{
|
||||
refetchOnMountOrArgChange: true,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const [updateLessonPreviewTitle] = useUpdateLessonPreviewTitleMutation();
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.State) dispatch(setLessonState(data.State));
|
||||
if (!data) return;
|
||||
|
||||
dispatch(setPageEditorLessonState(data.State));
|
||||
dispatch(setLessonThumbnailTitle(data.Title));
|
||||
dispatch(setLessonThumbnailUrl(data.ThumbnailUrl));
|
||||
}, [data]);
|
||||
|
||||
if (error) return <MyErrorResult />;
|
||||
|
@ -34,24 +60,21 @@ const PreviewCard: React.FC = () => {
|
|||
lessonId={lessonId as string}
|
||||
loading={isLoading}
|
||||
lessonSettings={{
|
||||
Title: data?.Title || '',
|
||||
ThumbnailUrl: data?.ThumbnailUrl || '',
|
||||
Title: dataLessonThumbnail.title || "",
|
||||
ThumbnailUrl: dataLessonThumbnail.url || "",
|
||||
}}
|
||||
onEditTitle={async (newTitle) => {
|
||||
try {
|
||||
const res = await updateLessonPreviewTitle({
|
||||
await updateLessonPreviewTitle({
|
||||
lessonId: lessonId as string,
|
||||
newTitle: newTitle,
|
||||
}).unwrap();
|
||||
|
||||
if (res) {
|
||||
refetch();
|
||||
}
|
||||
dispatch(setLessonThumbnailTitle(newTitle));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}}
|
||||
onThumbnailChanged={refetch}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -60,9 +83,12 @@ const LessonContentComponents: React.FC = () => {
|
|||
const { lessonId } = useParams();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { data, error, isLoading } = useGetLessonContentsQuery(lessonId as string, {
|
||||
const { data, error, isLoading, refetch } = useGetLessonContentsQuery(
|
||||
lessonId as string,
|
||||
{
|
||||
refetchOnMountOrArgChange: true,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const lnContents = useSelector(lessonContents);
|
||||
|
||||
|
@ -72,12 +98,22 @@ const LessonContentComponents: React.FC = () => {
|
|||
dispatch(setLessonContents(data));
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
addWebSocketReconnectListener(refetch);
|
||||
|
||||
return () => removeWebSocketReconnectListener(refetch);
|
||||
}, []);
|
||||
|
||||
if (error) return <MyErrorResult />;
|
||||
|
||||
return (
|
||||
<Card loading={isLoading}>
|
||||
<Flex vertical gap={16}>
|
||||
{!lnContents || lnContents.length == 0 ? <MyEmpty /> : <Droppable items={lnContents} />}
|
||||
{!lnContents || lnContents.length == 0 ? (
|
||||
<MyEmpty />
|
||||
) : (
|
||||
<Droppable items={lnContents} />
|
||||
)}
|
||||
</Flex>
|
||||
</Card>
|
||||
);
|
||||
|
@ -88,8 +124,6 @@ export default function LessonPageEditor() {
|
|||
const navigate = useNavigate();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const lnContents = useSelector(lessonContents);
|
||||
const lnThumbnail = useSelector(lessonThumbnail);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setEditorActive(true));
|
||||
|
@ -104,12 +138,27 @@ export default function LessonPageEditor() {
|
|||
<>
|
||||
<HeaderBar
|
||||
theme="light"
|
||||
backTo={Constants.ROUTE_PATHS.LESSIONS.PAGE.replace(':lessonId', lessonId as string)}
|
||||
onView={() => navigate(Constants.ROUTE_PATHS.LESSIONS.PAGE.replace(':lessonId', lessonId as string))}
|
||||
backTo={Constants.ROUTE_PATHS.LESSIONS.PAGE.replace(
|
||||
":lessonId",
|
||||
lessonId as string
|
||||
)}
|
||||
onView={() =>
|
||||
navigate(
|
||||
Constants.ROUTE_PATHS.LESSIONS.PAGE.replace(
|
||||
":lessonId",
|
||||
lessonId as string
|
||||
)
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<Flex justify="center" style={{ paddingTop: 24 }}>
|
||||
<Flex justify="center" vertical gap={16} className={styles.cardContainer}>
|
||||
<Flex
|
||||
justify="center"
|
||||
vertical
|
||||
gap={16}
|
||||
className={styles.cardContainer}
|
||||
>
|
||||
<PreviewCard />
|
||||
|
||||
<LessonContentComponents />
|
||||
|
|
|
@ -12,8 +12,8 @@ export const lessonPageEditorSlice = createSlice({
|
|||
editorActive: false,
|
||||
currentLessonId: "", // required in sideMenu because has no access to useParams
|
||||
lessonThumbnail: {
|
||||
img: "",
|
||||
title: "Test",
|
||||
url: "",
|
||||
},
|
||||
lessonContents: [] as LessonContent[],
|
||||
lessonState: LessonState.Draft,
|
||||
|
@ -48,6 +48,9 @@ export const lessonPageEditorSlice = createSlice({
|
|||
setLessonThumbnailTitle: (state, action) => {
|
||||
state.lessonThumbnail.title = action.payload;
|
||||
},
|
||||
setLessonThumbnailUrl: (state, action) => {
|
||||
state.lessonThumbnail.url = action.payload;
|
||||
},
|
||||
onDragHandler: (state, action) => {
|
||||
state.lessonContents.splice(
|
||||
action.payload.newIndex,
|
||||
|
@ -55,9 +58,21 @@ export const lessonPageEditorSlice = createSlice({
|
|||
state.lessonContents.splice(action.payload.oldIndex, 1)[0]
|
||||
);
|
||||
},
|
||||
setLessonState: (state, action) => {
|
||||
setPageEditorLessonState: (state, action) => {
|
||||
state.lessonState = action.payload;
|
||||
},
|
||||
updateLessonContentPosition: (state, action) => {
|
||||
// change only by contentId and new position
|
||||
const content = state.lessonContents.find(
|
||||
(content) => content.Id === action.payload.contentId
|
||||
);
|
||||
|
||||
if (content) {
|
||||
const index = state.lessonContents.indexOf(content);
|
||||
state.lessonContents.splice(index, 1);
|
||||
state.lessonContents.splice(action.payload.position - 1, 0, content);
|
||||
}
|
||||
},
|
||||
},
|
||||
selectors: {
|
||||
editorActive: (state) => state.editorActive,
|
||||
|
@ -76,8 +91,10 @@ export const {
|
|||
setLessonContents,
|
||||
updateLessonContent,
|
||||
setLessonThumbnailTitle,
|
||||
setLessonThumbnailUrl,
|
||||
onDragHandler,
|
||||
setLessonState,
|
||||
setPageEditorLessonState,
|
||||
updateLessonContentPosition,
|
||||
} = lessonPageEditorSlice.actions;
|
||||
|
||||
export const {
|
||||
|
|
|
@ -1,39 +1,37 @@
|
|||
import { DownOutlined, HeartFilled, HeartOutlined } from '@ant-design/icons';
|
||||
import { Avatar, Button, Card, Collapse, Divider, Flex, Form, Input, InputRef, Typography } from 'antd';
|
||||
import Meta from 'antd/es/card/Meta';
|
||||
import TextArea from 'antd/es/input/TextArea';
|
||||
import { LessonQuestion, LessonQuestionReply } from 'core/types/lesson';
|
||||
import { Constants } from 'core/utils/utils';
|
||||
import React, { useRef } from 'react';
|
||||
import { HeartFilled, HeartOutlined } from "@ant-design/icons";
|
||||
import { Avatar, Button, Flex, Form, Input, InputRef, Typography } from "antd";
|
||||
import { LessonQuestion, LessonQuestionReply } from "core/types/lesson";
|
||||
import { Constants } from "core/utils/utils";
|
||||
import React, { useRef } from "react";
|
||||
|
||||
export default function Questions({ lessionID }: { lessionID: string }) {
|
||||
let questions: LessonQuestion[] = [
|
||||
{
|
||||
Id: '1',
|
||||
LessionId: '1',
|
||||
Question: 'What is the capital of Germany?',
|
||||
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',
|
||||
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?',
|
||||
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',
|
||||
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?',
|
||||
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',
|
||||
CreatorUserId: "3",
|
||||
CreatedAt: "2021-09-01T12:00:00Z",
|
||||
UpdatedAt: "2021-09-01T12:00:00Z",
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -43,7 +41,7 @@ export default function Questions({ lessionID }: { lessionID: string }) {
|
|||
<Typography.Title level={3}>Questions</Typography.Title>
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="Ask a question">
|
||||
<Input.TextArea placeholder={'Type something'} />
|
||||
<Input.TextArea placeholder={"Type something"} />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary">Submit</Button>
|
||||
|
@ -65,79 +63,79 @@ export function QuestionItem({ question }: { question: LessonQuestion }) {
|
|||
const [showReplies, setShowReplies] = React.useState(1);
|
||||
|
||||
let user = {
|
||||
Id: '132154153613',
|
||||
FirstName: 'Anja',
|
||||
LastName: 'Blasinstroment',
|
||||
Id: "132154153613",
|
||||
FirstName: "Anja",
|
||||
LastName: "Blasinstroment",
|
||||
};
|
||||
|
||||
let questionsReplys: LessonQuestionReply[] = [
|
||||
{
|
||||
Id: '1',
|
||||
QuestionId: '1',
|
||||
Reply: 'Berlin',
|
||||
Id: "1",
|
||||
QuestionId: "1",
|
||||
Reply: "Berlin",
|
||||
Likes: 5,
|
||||
CreatorUserId: '1',
|
||||
CreatedAt: '2021-09-01T12:00:00Z',
|
||||
UpdatedAt: '2021-09-01T12:00:00Z',
|
||||
CreatorUserId: "1",
|
||||
CreatedAt: "2021-09-01T12:00:00Z",
|
||||
UpdatedAt: "2021-09-01T12:00:00Z",
|
||||
},
|
||||
{
|
||||
Id: '2',
|
||||
QuestionId: '1',
|
||||
Reply: 'Munich',
|
||||
Id: "2",
|
||||
QuestionId: "1",
|
||||
Reply: "Munich",
|
||||
Likes: 3,
|
||||
CreatorUserId: '2',
|
||||
CreatedAt: '2021-09-01T12:00:00Z',
|
||||
UpdatedAt: '2021-09-01T12:00:00Z',
|
||||
CreatorUserId: "2",
|
||||
CreatedAt: "2021-09-01T12:00:00Z",
|
||||
UpdatedAt: "2021-09-01T12:00:00Z",
|
||||
},
|
||||
{
|
||||
Id: '3',
|
||||
QuestionId: '1',
|
||||
Reply: 'Hamburg',
|
||||
Id: "3",
|
||||
QuestionId: "1",
|
||||
Reply: "Hamburg",
|
||||
Likes: 2,
|
||||
CreatorUserId: '3',
|
||||
CreatedAt: '2021-09-01T12:00:00Z',
|
||||
UpdatedAt: '2021-09-01T12:00:00Z',
|
||||
CreatorUserId: "3",
|
||||
CreatedAt: "2021-09-01T12:00:00Z",
|
||||
UpdatedAt: "2021-09-01T12:00:00Z",
|
||||
},
|
||||
{
|
||||
Id: '4',
|
||||
QuestionId: '1',
|
||||
Reply: 'Cologne',
|
||||
Id: "4",
|
||||
QuestionId: "1",
|
||||
Reply: "Cologne",
|
||||
Likes: 0,
|
||||
CreatorUserId: '3',
|
||||
CreatedAt: '2021-09-01T12:00:00Z',
|
||||
UpdatedAt: '2021-09-01T12:00:00Z',
|
||||
CreatorUserId: "3",
|
||||
CreatedAt: "2021-09-01T12:00:00Z",
|
||||
UpdatedAt: "2021-09-01T12:00:00Z",
|
||||
},
|
||||
{
|
||||
Id: '5',
|
||||
QuestionId: '1',
|
||||
Reply: 'Frankfurt',
|
||||
Id: "5",
|
||||
QuestionId: "1",
|
||||
Reply: "Frankfurt",
|
||||
Likes: 0,
|
||||
CreatorUserId: '3',
|
||||
CreatedAt: '2021-09-01T12:00:00Z',
|
||||
UpdatedAt: '2021-09-01T12:00:00Z',
|
||||
CreatorUserId: "3",
|
||||
CreatedAt: "2021-09-01T12:00:00Z",
|
||||
UpdatedAt: "2021-09-01T12:00:00Z",
|
||||
},
|
||||
{
|
||||
Id: '6',
|
||||
QuestionId: '1',
|
||||
Reply: 'Stuttgart',
|
||||
Id: "6",
|
||||
QuestionId: "1",
|
||||
Reply: "Stuttgart",
|
||||
Likes: 2,
|
||||
CreatorUserId: '3',
|
||||
CreatedAt: '2021-09-01T12:00:00Z',
|
||||
UpdatedAt: '2021-09-01T12:00:00Z',
|
||||
CreatorUserId: "3",
|
||||
CreatedAt: "2021-09-01T12:00:00Z",
|
||||
UpdatedAt: "2021-09-01T12:00:00Z",
|
||||
},
|
||||
{
|
||||
Id: '7',
|
||||
QuestionId: '1',
|
||||
Reply: 'Düsseldorf',
|
||||
Id: "7",
|
||||
QuestionId: "1",
|
||||
Reply: "Düsseldorf",
|
||||
Likes: 10,
|
||||
CreatorUserId: '3',
|
||||
CreatedAt: '2021-09-01T12:00:00Z',
|
||||
UpdatedAt: '2021-09-01T12:00:00Z',
|
||||
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);
|
||||
console.log("reply", text);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
|
@ -153,14 +151,26 @@ export function QuestionItem({ question }: { question: LessonQuestion }) {
|
|||
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 }}>
|
||||
<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} />);
|
||||
nodes.push(
|
||||
<QuestionReplyItem
|
||||
key={"reply_" + questionsReplys[i].Id}
|
||||
question={questionsReplys[i]}
|
||||
handleReply={handleReply}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return nodes;
|
||||
|
@ -175,14 +185,30 @@ export function QuestionItem({ question }: { question: LessonQuestion }) {
|
|||
);
|
||||
}
|
||||
|
||||
export function QuestionReplyItem({ question, handleReply }: { question: LessonQuestionReply; handleReply: HandleReplyFunction }) {
|
||||
export function QuestionReplyItem({
|
||||
question,
|
||||
handleReply,
|
||||
}: {
|
||||
question: LessonQuestionReply;
|
||||
handleReply: HandleReplyFunction;
|
||||
}) {
|
||||
let user = {
|
||||
Id: '132154153613',
|
||||
FirstName: 'Anja',
|
||||
LastName: 'Blasinstroment',
|
||||
Id: "132154153613",
|
||||
FirstName: "Anja",
|
||||
LastName: "Blasinstroment",
|
||||
};
|
||||
|
||||
return <QuestionUIRaw userID={user.Id} text={question.Reply} childContent={<></>} likes={question.Likes} onReply={handleReply} onLike={() => {}} replyID={question.Id} />;
|
||||
return (
|
||||
<QuestionUIRaw
|
||||
userID={user.Id}
|
||||
text={question.Reply}
|
||||
childContent={<></>}
|
||||
likes={question.Likes}
|
||||
onReply={handleReply}
|
||||
onLike={() => {}}
|
||||
replyID={question.Id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function QuestionUIRaw({
|
||||
|
@ -209,9 +235,9 @@ export function QuestionUIRaw({
|
|||
const [isSendingReply, setIsSendingReply] = React.useState(false);
|
||||
|
||||
let user = {
|
||||
Id: '132154153613',
|
||||
FirstName: 'Anja',
|
||||
LastName: 'Blasinstroment',
|
||||
Id: "132154153613",
|
||||
FirstName: "Anja",
|
||||
LastName: "Blasinstroment",
|
||||
};
|
||||
|
||||
const userAt = `@${user.FirstName} ${user.LastName} `;
|
||||
|
@ -226,25 +252,38 @@ export function QuestionUIRaw({
|
|||
return (
|
||||
<>
|
||||
<Flex gap={16}>
|
||||
<Avatar src={`${Constants.STATIC_CONTENT_ADDRESS}/demo/lesson_thumbnail.webp`} size={56} />
|
||||
<Flex vertical style={{ width: '100%' }}>
|
||||
<Avatar
|
||||
src={`${Constants.STATIC_CONTENT_ADDRESS}/demo/lesson_thumbnail.webp`}
|
||||
size={56}
|
||||
/>
|
||||
<Flex vertical style={{ width: "100%" }}>
|
||||
<Typography style={{ fontSize: 24, fontWeight: 800 }}>
|
||||
{user.FirstName} {user.LastName}
|
||||
</Typography>
|
||||
<Typography style={{ fontSize: 18, fontWeight: 500 }}>{text}</Typography>
|
||||
<Typography style={{ fontSize: 18, fontWeight: 500 }}>
|
||||
{text}
|
||||
</Typography>
|
||||
<Flex gap={0} align="center">
|
||||
<Button
|
||||
type="text"
|
||||
icon={hasLiked ? <HeartFilled /> : <HeartOutlined />}
|
||||
shape="circle"
|
||||
size="large"
|
||||
style={{ color: hasLiked ? 'red' : undefined, transform: hasLiked ? 'scale(1.2)' : 'scale(1)', transition: 'all 0.3s ease-in-out' }}
|
||||
style={{
|
||||
color: hasLiked ? "red" : undefined,
|
||||
transform: hasLiked ? "scale(1.2)" : "scale(1)",
|
||||
transition: "all 0.3s ease-in-out",
|
||||
}}
|
||||
onClick={toggleLike}
|
||||
></Button>
|
||||
|
||||
<Typography style={{ fontSize: 16, fontWeight: 400, pointerEvents: 'none' }}>{likes >= 1 ? likes : ' '}</Typography>
|
||||
<Typography
|
||||
style={{ fontSize: 16, fontWeight: 400, pointerEvents: "none" }}
|
||||
>
|
||||
{likes >= 1 ? likes : " "}
|
||||
</Typography>
|
||||
<Button
|
||||
type={replyFormVisible ? 'link' : 'text'}
|
||||
type={replyFormVisible ? "link" : "text"}
|
||||
onClick={() => {
|
||||
if (replyText === null) setReplyText(userAt);
|
||||
setReplyFormVisible(!replyFormVisible);
|
||||
|
@ -252,12 +291,12 @@ export function QuestionUIRaw({
|
|||
setTimeout(() => {
|
||||
if (inputRef.current) {
|
||||
const input = inputRef.current;
|
||||
input.focus({ cursor: 'end' });
|
||||
input.focus({ cursor: "end" });
|
||||
}
|
||||
}, 100);
|
||||
}}
|
||||
>
|
||||
{replyFormVisible ? 'Hide' : 'Reply'}
|
||||
{replyFormVisible ? "Hide" : "Reply"}
|
||||
</Button>
|
||||
</Flex>
|
||||
{replyFormVisible ? (
|
||||
|
@ -265,14 +304,17 @@ export function QuestionUIRaw({
|
|||
disabled={isSendingReply}
|
||||
onFinish={async () => {
|
||||
setIsSendingReply(true);
|
||||
await onReply(replyText ? replyText : '', replyID);
|
||||
await onReply(replyText ? replyText : "", replyID);
|
||||
|
||||
setIsSendingReply(false);
|
||||
setReplyFormVisible(false);
|
||||
setReplyText(null);
|
||||
}}
|
||||
>
|
||||
<Form.Item name="reply" rules={[{ required: true, message: 'Please write a reply' }]}>
|
||||
<Form.Item
|
||||
name="reply"
|
||||
rules={[{ required: true, message: "Please write a reply" }]}
|
||||
>
|
||||
<Input.TextArea
|
||||
ref={inputRef}
|
||||
defaultValue={replyText ? replyText : userAt}
|
||||
|
@ -282,7 +324,11 @@ export function QuestionUIRaw({
|
|||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" loading={isSendingReply} htmlType="submit">
|
||||
<Button
|
||||
type="primary"
|
||||
loading={isSendingReply}
|
||||
htmlType="submit"
|
||||
>
|
||||
Reply
|
||||
</Button>
|
||||
</Form.Item>
|
||||
|
|
|
@ -17,44 +17,44 @@ export type Component = {
|
|||
|
||||
const componentsGroups: ComponentGroup[] = [
|
||||
{
|
||||
category: 'Common',
|
||||
category: "Common",
|
||||
components: [
|
||||
{
|
||||
type: 0,
|
||||
name: 'Header',
|
||||
thumbnail: '/editor/thumbnails/component_thumbnail_header.svg',
|
||||
name: "Header",
|
||||
thumbnail: "/editor/thumbnails/component_thumbnail_header.svg",
|
||||
invertThumbnailAtDarkmode: true,
|
||||
defaultData: 'Header',
|
||||
defaultData: "Header",
|
||||
},
|
||||
{
|
||||
type: 1,
|
||||
name: 'Text',
|
||||
thumbnail: '/editor/thumbnails/component_thumbnail_text.svg',
|
||||
name: "Text",
|
||||
thumbnail: "/editor/thumbnails/component_thumbnail_text.svg",
|
||||
invertThumbnailAtDarkmode: true,
|
||||
defaultData: 'Text',
|
||||
defaultData: "Text",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: 'Media',
|
||||
category: "Media",
|
||||
components: [
|
||||
{
|
||||
type: 2,
|
||||
name: 'Image',
|
||||
thumbnail: '/editor/thumbnails/component_thumbnail_image.png',
|
||||
name: "Image",
|
||||
thumbnail: "/editor/thumbnails/component_thumbnail_image.png",
|
||||
uploadImage: true,
|
||||
uploadFileTypes: ['image/*'],
|
||||
uploadFileTypes: ["image/*"],
|
||||
},
|
||||
{
|
||||
type: 3,
|
||||
name: 'YouTube',
|
||||
thumbnail: '/editor/thumbnails/component_thumbnail_youtube.png',
|
||||
name: "YouTube",
|
||||
thumbnail: "/editor/thumbnails/component_thumbnail_youtube.png",
|
||||
invertThumbnailAtDarkmode: true,
|
||||
},
|
||||
{
|
||||
type: 4,
|
||||
name: 'Video',
|
||||
thumbnail: '/editor/thumbnails/component_thumbnail_youtube.png',
|
||||
name: "Video",
|
||||
thumbnail: "/editor/thumbnails/component_thumbnail_youtube.png",
|
||||
invertThumbnailAtDarkmode: true,
|
||||
},
|
||||
],
|
||||
|
|
|
@ -9,21 +9,32 @@ import {
|
|||
useCreateLessonMutation,
|
||||
useGetLessonsQuery,
|
||||
} from "core/services/lessons";
|
||||
import MySpin from "shared/components/MySpin";
|
||||
import { Constants } from "core/utils/utils";
|
||||
import MyEmpty from "shared/components/MyEmpty";
|
||||
import MyErrorResult from "shared/components/MyResult";
|
||||
import LessonPreviewCard from "shared/components/MyLessonPreviewCard";
|
||||
import { useMessage } from "core/context/MessageContext";
|
||||
import { useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { lessons, setLessons } from "./lessonsSlice";
|
||||
import {
|
||||
addWebSocketReconnectListener,
|
||||
removeWebSocketReconnectListener,
|
||||
} from "core/services/websocketService";
|
||||
import MyCenteredSpin from "shared/components/MyCenteredSpin";
|
||||
|
||||
const CreateLessonButton: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [createLesson, { isLoading }] = useCreateLessonMutation();
|
||||
const { success, error } = useMessage();
|
||||
|
||||
const handleCreateLesson = async () => {
|
||||
try {
|
||||
const res = await createLesson({}).unwrap();
|
||||
|
||||
if (res && res.Id) {
|
||||
success("Lesson created successfully ");
|
||||
|
||||
navigate(
|
||||
Constants.ROUTE_PATHS.LESSIONS.PAGE_EDITOR.replace(
|
||||
":lessonId",
|
||||
|
@ -33,6 +44,7 @@ const CreateLessonButton: React.FC = () => {
|
|||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
error("Failed to create lesson");
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -48,17 +60,32 @@ const CreateLessonButton: React.FC = () => {
|
|||
};
|
||||
|
||||
const LessonList: React.FC = () => {
|
||||
const { data, error, isLoading } = useGetLessonsQuery(undefined, {
|
||||
const dispatch = useDispatch();
|
||||
const dataLessons = useSelector(lessons);
|
||||
|
||||
const { data, error, isLoading, refetch } = useGetLessonsQuery(undefined, {
|
||||
refetchOnMountOrArgChange: true,
|
||||
});
|
||||
|
||||
if (isLoading) return <MySpin />;
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
|
||||
dispatch(setLessons(data));
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
addWebSocketReconnectListener(refetch);
|
||||
|
||||
return () => removeWebSocketReconnectListener(refetch);
|
||||
}, []);
|
||||
|
||||
if (isLoading) return <MyCenteredSpin height="240px" />;
|
||||
if (error) return <MyErrorResult />;
|
||||
|
||||
if (!data || data.length === 0) return <MyEmpty />;
|
||||
if (!dataLessons || dataLessons.length === 0) return <MyEmpty />;
|
||||
|
||||
const publishedItems = data.filter((item) => item.State === 1);
|
||||
const unpublishedItems = data.filter((item) => item.State === 2);
|
||||
const publishedItems = dataLessons.filter((item) => item.State === 1);
|
||||
const unpublishedItems = dataLessons.filter((item) => item.State === 2);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import { Lesson } from "core/types/lesson";
|
||||
|
||||
interface AddLessonAction {
|
||||
type: string;
|
||||
payload: Lesson;
|
||||
}
|
||||
|
||||
export const lessonsSlice = createSlice({
|
||||
name: "lessons",
|
||||
initialState: {
|
||||
lessons: [] as Lesson[],
|
||||
},
|
||||
reducers: {
|
||||
setLessons: (state, action) => {
|
||||
state.lessons = action.payload;
|
||||
},
|
||||
addLesson: (state, action: AddLessonAction) => {
|
||||
state.lessons.push(action.payload);
|
||||
},
|
||||
updateLessonPreviewTitle: (state, action) => {
|
||||
const lesson = state.lessons.find(
|
||||
(lesson) => lesson.Id === action.payload.LessonId
|
||||
);
|
||||
|
||||
if (lesson) {
|
||||
lesson.Title = action.payload.Title;
|
||||
}
|
||||
},
|
||||
updateLessonPreviewThumbnail: (state, action) => {
|
||||
const lesson = state.lessons.find(
|
||||
(lesson) => lesson.Id === action.payload.LessonId
|
||||
);
|
||||
|
||||
if (lesson) {
|
||||
lesson.ThumbnailUrl = action.payload.ThumbnailUrl;
|
||||
}
|
||||
},
|
||||
updateLessonState: (state, action) => {
|
||||
const lesson = state.lessons.find(
|
||||
(lesson) => lesson.Id === action.payload.LessonId
|
||||
);
|
||||
|
||||
if (lesson) {
|
||||
lesson.State = action.payload.State;
|
||||
}
|
||||
},
|
||||
},
|
||||
selectors: {
|
||||
lessons: (state) => state.lessons,
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
setLessons,
|
||||
addLesson,
|
||||
updateLessonPreviewTitle,
|
||||
updateLessonPreviewThumbnail,
|
||||
updateLessonState,
|
||||
} = lessonsSlice.actions;
|
||||
|
||||
export const { lessons } = lessonsSlice.selectors;
|
|
@ -7,12 +7,23 @@ import MyEmpty from "shared/components/MyEmpty";
|
|||
import MyCenteredSpin from "shared/components/MyCenteredSpin";
|
||||
import { Role } from "core/types/organization";
|
||||
import { useGetRolesQuery } from "core/services/organization";
|
||||
import { useEffect } from "react";
|
||||
import {
|
||||
addWebSocketReconnectListener,
|
||||
removeWebSocketReconnectListener,
|
||||
} from "core/services/websocketService";
|
||||
|
||||
export default function Roles() {
|
||||
const { data, error, isLoading } = useGetRolesQuery(undefined, {
|
||||
const { data, error, isLoading, refetch } = useGetRolesQuery(undefined, {
|
||||
refetchOnMountOrArgChange: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
addWebSocketReconnectListener(refetch);
|
||||
|
||||
return () => removeWebSocketReconnectListener(refetch);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MyBanner title="Roles" subtitle="MANAGE" headerBar={<HeaderBar />} />
|
||||
|
|
|
@ -26,6 +26,10 @@ import {
|
|||
import MyMiddleCard from "shared/components/MyMiddleCard";
|
||||
import { OrganizationSettings } from "core/types/organization";
|
||||
import { useMessage } from "core/context/MessageContext";
|
||||
import {
|
||||
addWebSocketReconnectListener,
|
||||
removeWebSocketReconnectListener,
|
||||
} from "core/services/websocketService";
|
||||
|
||||
type GeneralFieldType = {
|
||||
primaryColor: string | AggregationColor;
|
||||
|
@ -33,13 +37,19 @@ type GeneralFieldType = {
|
|||
};
|
||||
|
||||
export default function Settings() {
|
||||
const { data, error, isLoading } = useGetOrganizationSettingsQuery(
|
||||
const { data, error, isLoading, refetch } = useGetOrganizationSettingsQuery(
|
||||
undefined,
|
||||
{
|
||||
refetchOnMountOrArgChange: true,
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
addWebSocketReconnectListener(refetch);
|
||||
|
||||
return () => removeWebSocketReconnectListener(refetch);
|
||||
}, []);
|
||||
|
||||
if (error) return <MyErrorResult />;
|
||||
|
||||
return (
|
||||
|
|
Loading…
Reference in New Issue