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