fetching lessons, update lesson preview and page editor drag and drop
|
@ -10,6 +10,8 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^5.2.6",
|
"@ant-design/icons": "^5.2.6",
|
||||||
"@dnd-kit/core": "^6.1.0",
|
"@dnd-kit/core": "^6.1.0",
|
||||||
|
"@dnd-kit/modifiers": "^7.0.0",
|
||||||
|
"@dnd-kit/sortable": "^8.0.0",
|
||||||
"@reduxjs/toolkit": "^2.2.7",
|
"@reduxjs/toolkit": "^2.2.7",
|
||||||
"@testing-library/jest-dom": "^5.17.0",
|
"@testing-library/jest-dom": "^5.17.0",
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
|
@ -19,6 +21,7 @@
|
||||||
"@types/react": "^18.3.4",
|
"@types/react": "^18.3.4",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"antd": "^5.20.3",
|
"antd": "^5.20.3",
|
||||||
|
"antd-img-crop": "^4.23.0",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"i18next": "^23.7.6",
|
"i18next": "^23.7.6",
|
||||||
"i18next-browser-languagedetector": "^7.2.0",
|
"i18next-browser-languagedetector": "^7.2.0",
|
||||||
|
@ -29,7 +32,11 @@
|
||||||
"react-router-dom": "^6.19.0",
|
"react-router-dom": "^6.19.0",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"typescript": "^4.9.5",
|
"typescript": "^4.9.5",
|
||||||
|
"uuid": "^10.0.0",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/uuid": "^10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@adobe/css-tools": {
|
"node_modules/@adobe/css-tools": {
|
||||||
|
@ -2584,6 +2591,34 @@
|
||||||
"react-dom": ">=16.8.0"
|
"react-dom": ">=16.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@dnd-kit/modifiers": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-BG/ETy3eBjFap7+zIti53f0PCLGDzNXyTmn6fSdrudORf+OH04MxrW4p5+mPu4mgMk9kM41iYONjc3DOUWTcfg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dnd-kit/core": "^6.1.0",
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/sortable": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dnd-kit/core": "^6.1.0",
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@dnd-kit/utilities": {
|
"node_modules/@dnd-kit/utilities": {
|
||||||
"version": "3.2.2",
|
"version": "3.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
|
||||||
|
@ -4925,6 +4960,13 @@
|
||||||
"integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==",
|
"integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/uuid": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/ws": {
|
"node_modules/@types/ws": {
|
||||||
"version": "8.5.12",
|
"version": "8.5.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz",
|
||||||
|
@ -5646,6 +5688,21 @@
|
||||||
"react-dom": ">=16.9.0"
|
"react-dom": ">=16.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/antd-img-crop": {
|
||||||
|
"version": "4.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/antd-img-crop/-/antd-img-crop-4.23.0.tgz",
|
||||||
|
"integrity": "sha512-JtQoUmR3GqXoG+hsYXRxCBC60AgUKbbvArbnd8/5UmmuyVcQzBnumfoQTdC9wczWQuxRIpkPwsdOge6CCeepqg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"react-easy-crop": "^5.0.8",
|
||||||
|
"tslib": "^2.6.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"antd": ">=4.0.0",
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/any-promise": {
|
"node_modules/any-promise": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
|
||||||
|
@ -14140,6 +14197,12 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/normalize-wheel": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/normalize-wheel/-/normalize-wheel-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/npm-run-path": {
|
"node_modules/npm-run-path": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
|
||||||
|
@ -17107,6 +17170,20 @@
|
||||||
"react": "^18.3.1"
|
"react": "^18.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-easy-crop": {
|
||||||
|
"version": "5.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-easy-crop/-/react-easy-crop-5.0.8.tgz",
|
||||||
|
"integrity": "sha512-KjulxXhR5iM7+ATN2sGCum/IyDxGw7xT0dFoGcqUP+ysaPU5Ka7gnrDa2tUHFHUoMNyPrVZ05QA+uvMgC5ym/g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"normalize-wheel": "^1.0.1",
|
||||||
|
"tslib": "^2.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.4.0",
|
||||||
|
"react-dom": ">=16.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-error-overlay": {
|
"node_modules/react-error-overlay": {
|
||||||
"version": "6.0.11",
|
"version": "6.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz",
|
||||||
|
@ -18262,6 +18339,15 @@
|
||||||
"websocket-driver": "^0.7.4"
|
"websocket-driver": "^0.7.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sockjs/node_modules/uuid": {
|
||||||
|
"version": "8.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||||
|
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-list-map": {
|
"node_modules/source-list-map": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
|
||||||
|
@ -19786,9 +19872,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/uuid": {
|
"node_modules/uuid": {
|
||||||
"version": "8.3.2",
|
"version": "10.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
||||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"uuid": "dist/bin/uuid"
|
"uuid": "dist/bin/uuid"
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^5.2.6",
|
"@ant-design/icons": "^5.2.6",
|
||||||
"@dnd-kit/core": "^6.1.0",
|
"@dnd-kit/core": "^6.1.0",
|
||||||
|
"@dnd-kit/modifiers": "^7.0.0",
|
||||||
|
"@dnd-kit/sortable": "^8.0.0",
|
||||||
"@reduxjs/toolkit": "^2.2.7",
|
"@reduxjs/toolkit": "^2.2.7",
|
||||||
"@testing-library/jest-dom": "^5.17.0",
|
"@testing-library/jest-dom": "^5.17.0",
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
|
@ -14,6 +16,7 @@
|
||||||
"@types/react": "^18.3.4",
|
"@types/react": "^18.3.4",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"antd": "^5.20.3",
|
"antd": "^5.20.3",
|
||||||
|
"antd-img-crop": "^4.23.0",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"i18next": "^23.7.6",
|
"i18next": "^23.7.6",
|
||||||
"i18next-browser-languagedetector": "^7.2.0",
|
"i18next-browser-languagedetector": "^7.2.0",
|
||||||
|
@ -24,6 +27,7 @@
|
||||||
"react-router-dom": "^6.19.0",
|
"react-router-dom": "^6.19.0",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"typescript": "^4.9.5",
|
"typescript": "^4.9.5",
|
||||||
|
"uuid": "^10.0.0",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -49,5 +53,8 @@
|
||||||
"last 1 firefox version",
|
"last 1 firefox version",
|
||||||
"last 1 safari version"
|
"last 1 safari version"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/uuid": "^10.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
After Width: | Height: | Size: 13 KiB |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="30" viewBox="0 0 24 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0.71804 29.5V0.40909H4.24077V13.3636H19.7521V0.40909H23.2749V29.5H19.7521V16.4886H4.24077V29.5H0.71804Z" fill="black"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 233 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="58" height="31" viewBox="0 0 58 31" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0 4.5C0 2.29086 1.79086 0.5 4 0.5H54C56.2091 0.5 58 2.29086 58 4.5V26.5C58 28.7091 56.2091 30.5 54 30.5H4C1.79086 30.5 0 28.7091 0 26.5V4.5Z" fill="white"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 270 B |
After Width: | Height: | Size: 17 KiB |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="22" height="30" viewBox="0 0 22 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0.099787 3.53409V0.40909H21.918V3.53409H12.7702V29.5H9.24751V3.53409H0.099787Z" fill="black"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 208 B |
After Width: | Height: | Size: 3.2 KiB |
|
@ -4,12 +4,13 @@ import {
|
||||||
darkMode,
|
darkMode,
|
||||||
setUserAuthenticated,
|
setUserAuthenticated,
|
||||||
userAuthenticated,
|
userAuthenticated,
|
||||||
} from "./core/store/appSlice";
|
} from "./core/reducers/appSlice";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import SignIn from "./features/Auth/SignIn";
|
import SignIn from "./features/Auth/SignIn";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { myFetch } from "./core/utils/utils";
|
import { myFetch } from "./core/utils/utils";
|
||||||
import MyCenteredSpin from "./shared/components/MyCenteredSpin";
|
import MyCenteredSpin from "./shared/components/MyCenteredSpin";
|
||||||
|
import webSocketService from "core/services/websocketService";
|
||||||
|
|
||||||
const { defaultAlgorithm, darkAlgorithm } = theme;
|
const { defaultAlgorithm, darkAlgorithm } = theme;
|
||||||
|
|
||||||
|
@ -44,6 +45,12 @@ function App() {
|
||||||
}
|
}
|
||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
webSocketService.connect();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
webSocketService.disconnect();
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { closestCenter, DndContext, DragOverlay } from "@dnd-kit/core";
|
||||||
|
import { DraggableCreateComponent } from "../SideMenu";
|
||||||
|
import { componentsGroups } from "features/Lessons/components";
|
||||||
|
|
||||||
|
function MyDndContext({ children }: { children: React.ReactNode }) {
|
||||||
|
return <DndContext collisionDetection={closestCenter}>{children}
|
||||||
|
</DndContext>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default MyDndContext;
|
|
@ -7,6 +7,8 @@ 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";
|
||||||
|
|
||||||
|
|
||||||
const { useBreakpoint } = Grid;
|
const { useBreakpoint } = Grid;
|
||||||
|
|
||||||
|
@ -44,6 +46,7 @@ export function SideMenu() {
|
||||||
|
|
||||||
export default function DashboardLayout() {
|
export default function DashboardLayout() {
|
||||||
return (
|
return (
|
||||||
|
<MyDndContext>
|
||||||
<Layout style={{ minHeight: "100vh" }}>
|
<Layout style={{ minHeight: "100vh" }}>
|
||||||
<Layout>
|
<Layout>
|
||||||
<SideMenu />
|
<SideMenu />
|
||||||
|
@ -51,5 +54,6 @@ export default function DashboardLayout() {
|
||||||
<PageContent />
|
<PageContent />
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
</MyDndContext>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ import {
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { darkMode, setDarkMode } from "../../store/appSlice";
|
import { darkMode, setDarkMode } from "../../reducers/appSlice";
|
||||||
import styles from "./styles.module.css";
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
type HeaderBarProps = {
|
type HeaderBarProps = {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {
|
||||||
TeamOutlined,
|
TeamOutlined,
|
||||||
WalletOutlined,
|
WalletOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { Divider, Flex, Menu } from "antd";
|
import { Divider, Flex, Menu, Typography } from "antd";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
@ -17,10 +17,19 @@ import {
|
||||||
sideMenuComponentFirstRender,
|
sideMenuComponentFirstRender,
|
||||||
} from "./sideMenuSlice";
|
} from "./sideMenuSlice";
|
||||||
import { ItemType, MenuItemType } from "antd/es/menu/interface";
|
import { ItemType, MenuItemType } from "antd/es/menu/interface";
|
||||||
import { BreakpointLgWidth, Constants } from "../../utils/utils";
|
import { BreakpointLgWidth, Constants } from "core/utils/utils";
|
||||||
import Search from "antd/es/input/Search";
|
import Search from "antd/es/input/Search";
|
||||||
import { MyContainer } from "../../../shared/components/MyContainer";
|
import { MyContainer } from "shared/components/MyContainer";
|
||||||
import { addLessonContent } from "../../../features/Lessons/LessonPageEditor/lessonPageEditorSlice";
|
import { addLessonContent } from "features/Lessons/LessonPageEditor/lessonPageEditorSlice";
|
||||||
|
|
||||||
|
import { Component, componentsGroups } from "features/Lessons/components";
|
||||||
|
|
||||||
|
import { darkMode } from "core/reducers/appSlice";
|
||||||
|
|
||||||
|
import { DndContext, DragOverlay, useDraggable } from "@dnd-kit/core";
|
||||||
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
|
import { snapCenterToCursor } from "@dnd-kit/modifiers";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
|
||||||
export function SideMenuContent() {
|
export function SideMenuContent() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
@ -199,35 +208,9 @@ export function SideMenuContent() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type ComponentGroup = {
|
|
||||||
category: string;
|
|
||||||
components: Component[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type Component = {
|
|
||||||
type: number;
|
|
||||||
name: string;
|
|
||||||
thumbnail?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const componentsGroups: ComponentGroup[] = [
|
|
||||||
{
|
|
||||||
category: "General",
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: 0,
|
|
||||||
name: "Header",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 1,
|
|
||||||
name: "Text",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export function SideMenuEditorContent() {
|
export function SideMenuEditorContent() {
|
||||||
const dispatch = useDispatch();
|
// create is dragging useState
|
||||||
|
const [isDragging, setIsDragging] = useState<String | null>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MyContainer>
|
<MyContainer>
|
||||||
|
@ -236,35 +219,130 @@ export function SideMenuEditorContent() {
|
||||||
style={{ paddingBottom: 16 }}
|
style={{ paddingBottom: 16 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{componentsGroups.map((group, i) => (
|
<DndContext
|
||||||
<div key={i}>
|
onDragStart={(event) => {
|
||||||
<span>{group.category}</span>
|
console.log("drag start", event.active.id);
|
||||||
|
|
||||||
<Flex gap={16} wrap style={{ paddingTop: 16 }}>
|
setIsDragging(event.active.id.toString());
|
||||||
{group.components.map((component, i) => (
|
}}
|
||||||
<Flex
|
onDragEnd={(evnet) => {
|
||||||
key={i}
|
console.log("drag end", evnet.active.id);
|
||||||
vertical
|
setIsDragging(null);
|
||||||
align="center"
|
}}
|
||||||
justify="center"
|
>
|
||||||
style={{
|
{componentsGroups.map((group, i) => (
|
||||||
backgroundColor: "#EBEBEB",
|
<div key={i}>
|
||||||
height: 80,
|
<span>{group.category}</span>
|
||||||
width: 80,
|
|
||||||
cursor: "pointer",
|
<Flex gap={16} wrap style={{ paddingTop: 16 }}>
|
||||||
}}
|
{group.components.map((component, i) => (
|
||||||
onClick={() => {
|
<DraggableCreateComponent key={i} component={component} />
|
||||||
console.log("insert component", component.type);
|
))}
|
||||||
dispatch(addLessonContent(component.type));
|
</Flex>
|
||||||
}}
|
</div>
|
||||||
>
|
))}
|
||||||
<div>Thumbnail</div>
|
{createPortal(
|
||||||
<div>{component.name}</div>
|
<DragOverlay>
|
||||||
</Flex>
|
{isDragging
|
||||||
))}
|
? (() => {
|
||||||
</Flex>
|
const comp = componentsGroups
|
||||||
</div>
|
.flatMap((group) => group.components)
|
||||||
))}
|
.find((comp) => "draggable_" + comp.name === isDragging);
|
||||||
|
console.log("dragging", comp);
|
||||||
|
if (!comp) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{}}>
|
||||||
|
<CreateComponent component={comp} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()
|
||||||
|
: null}
|
||||||
|
</DragOverlay>,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
|
</DndContext>
|
||||||
</MyContainer>
|
</MyContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function DraggableCreateComponent({
|
||||||
|
component,
|
||||||
|
}: {
|
||||||
|
component: Component;
|
||||||
|
}) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const { attributes, listeners, setNodeRef, transform, isDragging, active } =
|
||||||
|
useDraggable({
|
||||||
|
id: "draggable_" + component.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
ref={setNodeRef}
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
style={{
|
||||||
|
transition: !isDragging ? "all 0.3s ease-out" : "none",
|
||||||
|
|
||||||
|
boxShadow: isDragging ? "0px 0px 10px 0px #00000040" : "none",
|
||||||
|
opacity: isDragging ? 0.25 : 1,
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
console.log("insert component", component.type);
|
||||||
|
dispatch(addLessonContent(component.type));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CreateComponent component={component} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CreateComponent({ component }: { component: Component }) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const isDarkMode = useSelector(darkMode);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
vertical
|
||||||
|
align="center"
|
||||||
|
justify="center"
|
||||||
|
style={{
|
||||||
|
backgroundColor: !isDarkMode ? "#f0f2f5" : "#1f1f1f",
|
||||||
|
height: 80,
|
||||||
|
width: 80,
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
console.log("insert component", component.type);
|
||||||
|
dispatch(addLessonContent(component.type));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{component.thumbnail ? (
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
src={component.thumbnail}
|
||||||
|
style={{
|
||||||
|
width: 40,
|
||||||
|
filter:
|
||||||
|
isDarkMode && component.invertThumbnailAtDarkmode
|
||||||
|
? "invert(1)"
|
||||||
|
: "invert(0)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<Typography.Text style={{ fontSize: 12 }}>
|
||||||
|
{component.name}
|
||||||
|
</Typography.Text>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//console.log("insert component", component.type);
|
||||||
|
//dispatch(addLessonContent(component.type));
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { FetchArgs, fetchBaseQuery } from "@reduxjs/toolkit/query";
|
||||||
|
import { Constants, handleLogout } from "core/utils/utils";
|
||||||
|
|
||||||
|
export function getApiHeader() {
|
||||||
|
return {
|
||||||
|
"X-Authorization": localStorage.getItem("session") || "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseQuery = fetchBaseQuery({
|
||||||
|
baseUrl: Constants.API_ADDRESS,
|
||||||
|
prepareHeaders: (headers) => {
|
||||||
|
headers.set("X-Authorization", localStorage.getItem("session") || "");
|
||||||
|
return headers;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const baseQueryWithErrorHandling = async (
|
||||||
|
args: string | FetchArgs,
|
||||||
|
api: any,
|
||||||
|
extraOptions: any
|
||||||
|
) => {
|
||||||
|
const result = await baseQuery(args, api, extraOptions);
|
||||||
|
if (result.error && result.error.status === 401) {
|
||||||
|
console.error("Unauthorized. Please log in again.");
|
||||||
|
|
||||||
|
handleLogout();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { createApi } from "@reduxjs/toolkit/query/react";
|
||||||
|
import { baseQueryWithErrorHandling } from "core/helper/api";
|
||||||
|
import {
|
||||||
|
Lesson,
|
||||||
|
LessonPreview,
|
||||||
|
UpdateLessonPreviewThumbnail,
|
||||||
|
} from "core/types/lesson";
|
||||||
|
import { LessonContent } from "features/Lessons/LessonPage";
|
||||||
|
|
||||||
|
export const lessonsApi = createApi({
|
||||||
|
reducerPath: "lessonsApi",
|
||||||
|
baseQuery: baseQueryWithErrorHandling,
|
||||||
|
endpoints: (builder) => ({
|
||||||
|
getLessons: builder.query<Lesson[], undefined>({
|
||||||
|
query: () => ({
|
||||||
|
url: "lessons",
|
||||||
|
method: "GET",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
createLesson: builder.mutation({
|
||||||
|
query: () => ({
|
||||||
|
url: "lessons",
|
||||||
|
method: "POST",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
getLessonPreview: builder.query<LessonPreview, string>({
|
||||||
|
query: (lessonId) => ({
|
||||||
|
url: `lessons/${lessonId}/preview`,
|
||||||
|
method: "GET",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
getLessonContents: builder.query<LessonContent[], string>({
|
||||||
|
query: (lessonId) => ({
|
||||||
|
url: `lessons/${lessonId}/contents`,
|
||||||
|
method: "GET",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
updateLessonPreviewTitle: builder.mutation({
|
||||||
|
query: ({ lessonId, newTitle }) => ({
|
||||||
|
url: `lessons/${lessonId}/preview/title`,
|
||||||
|
method: "PATCH",
|
||||||
|
body: { Title: newTitle },
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
updateLessonPreviewThumbnail: builder.mutation<
|
||||||
|
void,
|
||||||
|
UpdateLessonPreviewThumbnail
|
||||||
|
>({
|
||||||
|
query: ({ lessonId, formData }) => ({
|
||||||
|
url: `lessons/${lessonId}/preview/thumbnail`,
|
||||||
|
method: "PATCH",
|
||||||
|
body: formData,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const {
|
||||||
|
useGetLessonsQuery,
|
||||||
|
useCreateLessonMutation,
|
||||||
|
useGetLessonPreviewQuery,
|
||||||
|
useGetLessonContentsQuery,
|
||||||
|
useUpdateLessonPreviewTitleMutation,
|
||||||
|
useUpdateLessonPreviewThumbnailMutation,
|
||||||
|
} = lessonsApi;
|
|
@ -0,0 +1,68 @@
|
||||||
|
import { BrowserTabSession, Constants } from "core/utils/utils";
|
||||||
|
|
||||||
|
interface WebSocketMessage {
|
||||||
|
type: string;
|
||||||
|
payload: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
class WebSocketService {
|
||||||
|
private url: string;
|
||||||
|
private socket: WebSocket | null = null;
|
||||||
|
private reconnectInterval: number = 10000; // 5 Sekunden
|
||||||
|
private handlers: Record<string, (payload: any) => void> = {};
|
||||||
|
|
||||||
|
constructor(url: string) {
|
||||||
|
this.url = `${url}?auth=${localStorage.getItem(
|
||||||
|
"session"
|
||||||
|
)}&bts=${BrowserTabSession}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public connect(): void {
|
||||||
|
this.socket = new WebSocket(this.url);
|
||||||
|
|
||||||
|
this.socket.onopen = () => {
|
||||||
|
console.log("WebSocket connected");
|
||||||
|
};
|
||||||
|
|
||||||
|
this.socket.onmessage = (event: MessageEvent) => {
|
||||||
|
const data: WebSocketMessage = JSON.parse(event.data);
|
||||||
|
this.handleMessage(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.socket.onclose = () => {
|
||||||
|
console.log("WebSocket disconnected. Reconnecting...");
|
||||||
|
setTimeout(() => this.connect(), this.reconnectInterval);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.socket.onerror = (error: Event) => {
|
||||||
|
console.error("WebSocket error:", error);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleMessage(data: WebSocketMessage): void {
|
||||||
|
const { type, payload } = data;
|
||||||
|
if (this.handlers[type]) {
|
||||||
|
this.handlers[type](payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public onMessage(type: string, handler: (payload: any) => void): void {
|
||||||
|
this.handlers[type] = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public send(type: string, payload: any): void {
|
||||||
|
const message: WebSocketMessage = { type, payload };
|
||||||
|
if (this.socket) {
|
||||||
|
this.socket.send(JSON.stringify(message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public disconnect(): void {
|
||||||
|
if (this.socket) {
|
||||||
|
this.socket.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const webSocketService = new WebSocketService(Constants.WS_ADDRESS);
|
||||||
|
export default webSocketService;
|
|
@ -2,17 +2,22 @@ import { configureStore } from "@reduxjs/toolkit";
|
||||||
import { setupListeners } from "@reduxjs/toolkit/query";
|
import { setupListeners } from "@reduxjs/toolkit/query";
|
||||||
import { sideMenuSlice } from "../components/SideMenu/sideMenuSlice";
|
import { sideMenuSlice } from "../components/SideMenu/sideMenuSlice";
|
||||||
import { lessonPageEditorSlice } from "../../features/Lessons/LessonPageEditor/lessonPageEditorSlice";
|
import { lessonPageEditorSlice } from "../../features/Lessons/LessonPageEditor/lessonPageEditorSlice";
|
||||||
import { appSlice } from "./appSlice";
|
import { appSlice } from "../reducers/appSlice";
|
||||||
|
import { lessonsApi } from "core/services/lessons";
|
||||||
|
|
||||||
export const makeStore = (/* preloadedState */) => {
|
const makeStore = (/* preloadedState */) => {
|
||||||
const store = configureStore({
|
const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
app: appSlice.reducer,
|
app: appSlice.reducer,
|
||||||
sideMenu: sideMenuSlice.reducer,
|
sideMenu: sideMenuSlice.reducer,
|
||||||
lessonPageEditor: lessonPageEditorSlice.reducer,
|
lessonPageEditor: lessonPageEditorSlice.reducer,
|
||||||
// counter: counterSlice.reducer,
|
[lessonsApi.reducerPath]: lessonsApi.reducer,
|
||||||
},
|
},
|
||||||
// preloadedState,
|
// preloadedState,
|
||||||
|
middleware: (getDefaultMiddleware) =>
|
||||||
|
getDefaultMiddleware().concat(
|
||||||
|
lessonsApi.middleware
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
setupListeners(store.dispatch);
|
setupListeners(store.dispatch);
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
export interface Lesson {
|
||||||
|
Id: string;
|
||||||
|
State: number;
|
||||||
|
Title: string;
|
||||||
|
ThumbnailUrl: string;
|
||||||
|
CreatorUserId: string;
|
||||||
|
CreatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// used for the preview card on /lessions page and on the lesson editor
|
||||||
|
export interface LessonPreview {
|
||||||
|
Title: string;
|
||||||
|
ThumbnailUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// used on lesson page and on the lesson editor
|
||||||
|
export interface LessonContent {
|
||||||
|
Id: string;
|
||||||
|
Title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateLessonPreviewThumbnail {
|
||||||
|
lessonId: string;
|
||||||
|
formData: FormData;
|
||||||
|
}
|
|
@ -1,7 +1,12 @@
|
||||||
import { Buffer } from "buffer";
|
import { Buffer } from "buffer";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
const wssProtocol = window.location.protocol === "https:" ? "wss://" : "ws://";
|
||||||
|
|
||||||
export const Constants = {
|
export const Constants = {
|
||||||
API_ADDRESS: `${window.location.protocol}//${window.location.hostname}/api/v1`,
|
API_ADDRESS: `${window.location.protocol}//${window.location.hostname}/api/v1`,
|
||||||
|
WS_ADDRESS: `${wssProtocol}${window.location.hostname}/apiws`,
|
||||||
|
STATIC_CONTENT_ADDRESS: `${window.location.protocol}//${window.location.hostname}/api/static`,
|
||||||
ROUTE_PATHS: {
|
ROUTE_PATHS: {
|
||||||
LESSIONS: {
|
LESSIONS: {
|
||||||
ROOT: "/lessons",
|
ROOT: "/lessons",
|
||||||
|
@ -18,11 +23,33 @@ export const Constants = {
|
||||||
STYLES: {
|
STYLES: {
|
||||||
BLACK: "#000",
|
BLACK: "#000",
|
||||||
},
|
},
|
||||||
|
MAX_IMAGE_SIZE: 25 * 1024 * 1024, // 25MB
|
||||||
|
ACCEPTED_IMAGE_FILE_TYPES: [
|
||||||
|
"image/png",
|
||||||
|
"image/jpeg",
|
||||||
|
"image/jpg",
|
||||||
|
"image/webp",
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
// used for sideMenu
|
// used for sideMenu
|
||||||
export const BreakpointLgWidth = 992;
|
export const BreakpointLgWidth = 992;
|
||||||
|
|
||||||
|
export function GetUuid() {
|
||||||
|
return uuidv4();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getImageUrl(imageName: string) {
|
||||||
|
return `${Constants.STATIC_CONTENT_ADDRESS}/${imageName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// needed for a user who uses multiple tabs in the browser
|
||||||
|
// with the same session id because otherwise the last browser
|
||||||
|
// tab would subscribe to the topic and the other tabs would
|
||||||
|
// not receive any messages
|
||||||
|
// used for topic subscription
|
||||||
|
export const BrowserTabSession = GetUuid();
|
||||||
|
|
||||||
export function getUserSessionFromLocalStorage() {
|
export function getUserSessionFromLocalStorage() {
|
||||||
return localStorage.getItem("session");
|
return localStorage.getItem("session");
|
||||||
}
|
}
|
||||||
|
@ -45,8 +72,7 @@ export const myFetchContentType = {
|
||||||
FORM_DATA: 1,
|
FORM_DATA: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Method = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
|
||||||
type Method = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
|
||||||
|
|
||||||
interface MyFetchOptions<TRequest = any, TResponse = any> {
|
interface MyFetchOptions<TRequest = any, TResponse = any> {
|
||||||
url?: string;
|
url?: string;
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {
|
||||||
} from "../../../core/utils/utils";
|
} from "../../../core/utils/utils";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useDispatch } from "react-redux";
|
import { useDispatch } from "react-redux";
|
||||||
import { setUserAuthenticated } from "../../../core/store/appSlice";
|
import { setUserAuthenticated } from "../../../core/reducers/appSlice";
|
||||||
|
|
||||||
type FieldType = {
|
type FieldType = {
|
||||||
email: string;
|
email: string;
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import { Button, Card, Flex, Typography } from "antd";
|
import { Button, Card, Flex, Typography } from "antd";
|
||||||
|
|
||||||
import img from "./pexels-photo-302902.webp";
|
|
||||||
import { MyContainer } from "../../../shared/components/MyContainer";
|
import { MyContainer } from "../../../shared/components/MyContainer";
|
||||||
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 } 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 MySpin from "shared/components/MySpin";
|
||||||
|
import MyErrorResult from "shared/components/MyResult";
|
||||||
|
import MyEmpty from "shared/components/MyEmpty";
|
||||||
|
import { useGetLessonContentsQuery } from "core/services/lessons";
|
||||||
|
|
||||||
export type LessonContent = {
|
export type LessonContent = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -14,6 +17,7 @@ export type LessonContent = {
|
||||||
data: string;
|
data: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
const LessonContents = [
|
const LessonContents = [
|
||||||
{
|
{
|
||||||
id: "0",
|
id: "0",
|
||||||
|
@ -39,7 +43,7 @@ const LessonContents = [
|
||||||
type: 1,
|
type: 1,
|
||||||
data: "Think a moment in silence. What makes you really happy? Are you the only one with this? Probably you could sell this knowledge to others! Think about it",
|
data: "Think a moment in silence. What makes you really happy? Are you the only one with this? Probably you could sell this knowledge to others! Think about it",
|
||||||
},
|
},
|
||||||
] as LessonContent[];
|
] as LessonContent[]; */
|
||||||
|
|
||||||
export function Converter({
|
export function Converter({
|
||||||
mode,
|
mode,
|
||||||
|
@ -108,17 +112,42 @@ export function Converter({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LessonContents: React.FC = () => {
|
||||||
|
const { lessonId } = useParams();
|
||||||
|
|
||||||
|
const { data, error, isLoading } = useGetLessonContentsQuery(
|
||||||
|
lessonId as string,
|
||||||
|
{
|
||||||
|
refetchOnMountOrArgChange: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isLoading) return <MySpin />;
|
||||||
|
if (error) return <MyErrorResult />;
|
||||||
|
|
||||||
|
if (!data || data.length === 0) return <MyEmpty />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{data.map((lessonContent) => (
|
||||||
|
<div key={lessonContent.id} style={{ paddingBottom: 6 }}>
|
||||||
|
<Converter mode="view" lessonContent={lessonContent} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default function LessonPage() {
|
export default function LessonPage() {
|
||||||
const location = useLocation()
|
const location = useLocation();
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<HeaderBar
|
<HeaderBar
|
||||||
theme="light"
|
theme="light"
|
||||||
backTo={Constants.ROUTE_PATHS.LESSIONS.ROOT}
|
backTo={Constants.ROUTE_PATHS.LESSIONS.ROOT}
|
||||||
onEdit={() =>
|
onEdit={() => navigate(`${location.pathname}/editor`)}
|
||||||
navigate(`${location.pathname}/editor`)}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MyContainer>
|
<MyContainer>
|
||||||
|
@ -129,11 +158,7 @@ export default function LessonPage() {
|
||||||
maxWidth: 800,
|
maxWidth: 800,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{LessonContents.map((lessonContent) => (
|
<LessonContents />
|
||||||
<div key={lessonContent.id} style={{ paddingBottom: 6 }}>
|
|
||||||
<Converter mode="view" lessonContent={lessonContent} />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<Flex justify="right">
|
<Flex justify="right">
|
||||||
<Button type="primary" icon={<CheckOutlined />}>
|
<Button type="primary" icon={<CheckOutlined />}>
|
||||||
|
|
Before Width: | Height: | Size: 47 KiB |
|
@ -0,0 +1,52 @@
|
||||||
|
import { DndContext, DragEndEvent, useDroppable } from "@dnd-kit/core";
|
||||||
|
import { verticalListSortingStrategy, SortableContext } from "@dnd-kit/sortable";
|
||||||
|
import SortableEditorItem from "./SortableEditorItem";
|
||||||
|
import React from "react";
|
||||||
|
import { LessonContent } from "../LessonPage";
|
||||||
|
import { store } from "core/store/store";
|
||||||
|
|
||||||
|
import {
|
||||||
|
restrictToVerticalAxis,
|
||||||
|
restrictToWindowEdges,
|
||||||
|
} from "@dnd-kit/modifiers";
|
||||||
|
import { lessonContents, onDragHandler } from "./lessonPageEditorSlice";
|
||||||
|
|
||||||
|
const Droppable = ({ items }: { items: LessonContent[] }) => {
|
||||||
|
const droppableID = "editorComponentArea";
|
||||||
|
const { setNodeRef } = useDroppable({ id: droppableID });
|
||||||
|
|
||||||
|
|
||||||
|
const itemIDs = items.map((item) => item.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DndContext modifiers={[restrictToVerticalAxis, restrictToWindowEdges]} onDragEnd={handleDragEnd}>
|
||||||
|
<SortableContext
|
||||||
|
id={droppableID}
|
||||||
|
items={itemIDs}
|
||||||
|
strategy={verticalListSortingStrategy}
|
||||||
|
>
|
||||||
|
<div ref={setNodeRef} >
|
||||||
|
{items.map((item) => (
|
||||||
|
<SortableEditorItem key={`${droppableID}_${item.id}`} item={item} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleDragEnd(event: DragEndEvent) {
|
||||||
|
console.log("drag end",event);
|
||||||
|
|
||||||
|
if(!event.over) return;
|
||||||
|
|
||||||
|
const activeId = event.active.id;
|
||||||
|
const overId = event.over.id;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
store.dispatch(onDragHandler({activeId, overId}));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default Droppable;
|
|
@ -0,0 +1,57 @@
|
||||||
|
import React from "react";
|
||||||
|
import { defaultAnimateLayoutChanges, useSortable } from "@dnd-kit/sortable";
|
||||||
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
|
import { Converter, LessonContent } from "../LessonPage";
|
||||||
|
import { HolderOutlined, DeleteOutlined } from "@ant-design/icons";
|
||||||
|
import { Flex } from "antd";
|
||||||
|
import { setLessonContent, deleteLessonContent } from "./lessonPageEditorSlice";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
|
||||||
|
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
|
||||||
|
} = useSortable({ id: lnContent.id, animateLayoutChanges });
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
transform: CSS.Translate.toString(transform),
|
||||||
|
transition
|
||||||
|
}} ref={setNodeRef} {...attributes} >
|
||||||
|
<Flex key={lnContent.id} >
|
||||||
|
<HolderOutlined style={{paddingLeft:8,paddingRight:8, touchAction: "none", cursor: "move"}}{...listeners}/>
|
||||||
|
<Converter
|
||||||
|
mode="edititable"
|
||||||
|
lessonContent={lnContent}
|
||||||
|
onEdit={(data) =>
|
||||||
|
dispatch(
|
||||||
|
setLessonContent({
|
||||||
|
id: lnContent.id,
|
||||||
|
data: data,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DeleteOutlined
|
||||||
|
onClick={() => {
|
||||||
|
console.log("delete", lnContent.id);
|
||||||
|
dispatch(deleteLessonContent(lnContent.id));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SortableEditorItem;
|
|
@ -1,20 +1,65 @@
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
deleteLessonContent,
|
|
||||||
lessonContents,
|
lessonContents,
|
||||||
lessonThumbnail,
|
lessonThumbnail,
|
||||||
setEditorActive,
|
setEditorActive,
|
||||||
setLessonContent,
|
|
||||||
setLessonThumbnailTitle,
|
|
||||||
} from "./lessonPageEditorSlice";
|
} from "./lessonPageEditorSlice";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { Converter, LessonContent } from "../LessonPage";
|
import { LessonContent } from "../LessonPage";
|
||||||
import { Card, Flex, Typography } from "antd";
|
import { Card, Flex } from "antd";
|
||||||
import { DeleteOutlined } from "@ant-design/icons";
|
|
||||||
import { Constants } from "../../../core/utils/utils";
|
import { Constants } from "../../../core/utils/utils";
|
||||||
import HeaderBar from "../../../core/components/Header";
|
import HeaderBar from "../../../core/components/Header";
|
||||||
import img from "../LessonPage/pexels-photo-302902.webp";
|
import Droppable from "./Droppable";
|
||||||
|
import LessonPreviewCard from "../../../shared/components/MyLessonPreviewCard";
|
||||||
|
import {
|
||||||
|
useGetLessonPreviewQuery,
|
||||||
|
useUpdateLessonPreviewTitleMutation,
|
||||||
|
} from "core/services/lessons";
|
||||||
|
import MyErrorResult from "shared/components/MyResult";
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
const PreviewCard: React.FC = () => {
|
||||||
|
const { lessonId } = useParams();
|
||||||
|
|
||||||
|
const { data, error, isLoading, refetch } = useGetLessonPreviewQuery(
|
||||||
|
lessonId as string,
|
||||||
|
{
|
||||||
|
refetchOnMountOrArgChange: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const [updateLessonPreviewTitle, {}] = useUpdateLessonPreviewTitleMutation();
|
||||||
|
|
||||||
|
if (error) return <MyErrorResult />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LessonPreviewCard
|
||||||
|
mode="editable"
|
||||||
|
lessonId={lessonId as string}
|
||||||
|
loading={isLoading}
|
||||||
|
lessonPreview={{
|
||||||
|
Title: data?.Title || "",
|
||||||
|
ThumbnailUrl: data?.ThumbnailUrl || "",
|
||||||
|
}}
|
||||||
|
onEditTitle={async (newTitle) => {
|
||||||
|
try {
|
||||||
|
const res = await updateLessonPreviewTitle({
|
||||||
|
lessonId: lessonId as string,
|
||||||
|
newTitle: newTitle,
|
||||||
|
}).unwrap();
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onThumbnailChanged={refetch}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default function LessonPageEditor() {
|
export default function LessonPageEditor() {
|
||||||
const { lessonId } = useParams();
|
const { lessonId } = useParams();
|
||||||
|
@ -51,65 +96,17 @@ export default function LessonPageEditor() {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Flex justify="center" style={{ paddingTop: 24 }}>
|
<Flex justify="center" style={{ paddingTop: 24 }}>
|
||||||
<Flex justify="center" vertical gap={16}>
|
<Flex
|
||||||
<Card
|
justify="center"
|
||||||
style={{
|
vertical
|
||||||
width: 800,
|
gap={16}
|
||||||
maxWidth: 800,
|
className={styles.cardContainer}
|
||||||
}}
|
>
|
||||||
>
|
<PreviewCard />
|
||||||
<Flex vertical={false} style={{ gap: 16 }}>
|
|
||||||
<img src={img} alt="img1" style={{ height: 140 }} />
|
|
||||||
|
|
||||||
<Flex align="center" style={{ width: "100%" }}>
|
<Card>
|
||||||
<Typography.Title
|
|
||||||
level={2}
|
|
||||||
editable={{
|
|
||||||
triggerType: "text" as any,
|
|
||||||
tooltip: "click to edit text",
|
|
||||||
onChange: (event) =>
|
|
||||||
dispatch(setLessonThumbnailTitle(event)),
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{lnThumbnail.title}
|
|
||||||
</Typography.Title>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card
|
|
||||||
style={{
|
|
||||||
width: 800,
|
|
||||||
maxWidth: 800,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Flex vertical gap={16}>
|
<Flex vertical gap={16}>
|
||||||
{lnContents.map((lnContent) => (
|
<Droppable items={lnContents} />
|
||||||
<Flex key={lnContent.id}>
|
|
||||||
<Converter
|
|
||||||
mode="edititable"
|
|
||||||
lessonContent={lnContent}
|
|
||||||
onEdit={(data) =>
|
|
||||||
dispatch(
|
|
||||||
setLessonContent({
|
|
||||||
id: lnContent.id,
|
|
||||||
data: data,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DeleteOutlined
|
|
||||||
onClick={() => {
|
|
||||||
console.log("delete", lnContent.id);
|
|
||||||
dispatch(deleteLessonContent(lnContent.id));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
))}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Card>
|
</Card>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -120,7 +117,7 @@ export default function LessonPageEditor() {
|
||||||
|
|
||||||
export function StandardEditorCompontent(type: number): LessonContent {
|
export function StandardEditorCompontent(type: number): LessonContent {
|
||||||
return {
|
return {
|
||||||
id: Math.floor(Math.random() * 100).toString(),
|
id: Math.floor(Math.random() * 10000).toString(),
|
||||||
position: 1,
|
position: 1,
|
||||||
type: type,
|
type: type,
|
||||||
data: "Some data",
|
data: "Some data",
|
||||||
|
|
|
@ -34,6 +34,27 @@ export const lessonPageEditorSlice = createSlice({
|
||||||
setLessonThumbnailTitle: (state, action) => {
|
setLessonThumbnailTitle: (state, action) => {
|
||||||
state.lessonThumbnail.title = action.payload;
|
state.lessonThumbnail.title = action.payload;
|
||||||
},
|
},
|
||||||
|
onDragHandler: (state, action) => {
|
||||||
|
const { activeId, overId } = action.payload as {
|
||||||
|
activeId: string;
|
||||||
|
overId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (activeId !== overId) {
|
||||||
|
let oldIndex = state.lessonContents.findIndex(
|
||||||
|
(item) => item.id === activeId
|
||||||
|
);
|
||||||
|
let newIndex = state.lessonContents.findIndex(
|
||||||
|
(item) => item.id === overId
|
||||||
|
);
|
||||||
|
|
||||||
|
state.lessonContents.splice(
|
||||||
|
newIndex,
|
||||||
|
0,
|
||||||
|
state.lessonContents.splice(oldIndex, 1)[0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
selectors: {
|
selectors: {
|
||||||
editorActive: (state) => state.editorActive,
|
editorActive: (state) => state.editorActive,
|
||||||
|
@ -48,6 +69,7 @@ export const {
|
||||||
deleteLessonContent,
|
deleteLessonContent,
|
||||||
setLessonContent,
|
setLessonContent,
|
||||||
setLessonThumbnailTitle,
|
setLessonThumbnailTitle,
|
||||||
|
onDragHandler,
|
||||||
} = lessonPageEditorSlice.actions;
|
} = lessonPageEditorSlice.actions;
|
||||||
|
|
||||||
export const { editorActive, lessonContents, lessonThumbnail } =
|
export const { editorActive, lessonContents, lessonThumbnail } =
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
.cardContainer {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 750px) {
|
||||||
|
.cardContainer {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
// Desc: This file contains the list of components that are used in the Lessons
|
||||||
|
|
||||||
|
type ComponentGroup = {
|
||||||
|
category: string;
|
||||||
|
components: Component[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Component = {
|
||||||
|
type: number;
|
||||||
|
name: string;
|
||||||
|
thumbnail?: string;
|
||||||
|
invertThumbnailAtDarkmode?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const componentsGroups: ComponentGroup[] = [
|
||||||
|
{
|
||||||
|
category: "Common",
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: 0,
|
||||||
|
name: "Header",
|
||||||
|
thumbnail: "/editor/thumbnails/component_thumbnail_header.svg",
|
||||||
|
invertThumbnailAtDarkmode: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 1,
|
||||||
|
name: "Text",
|
||||||
|
thumbnail: "/editor/thumbnails/component_thumbnail_text.svg",
|
||||||
|
invertThumbnailAtDarkmode: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Media",
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: 0,
|
||||||
|
name: "Image",
|
||||||
|
thumbnail: "/editor/thumbnails/component_thumbnail_image.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 1,
|
||||||
|
name: "YouTube",
|
||||||
|
thumbnail: "/editor/thumbnails/component_thumbnail_youtube.png",
|
||||||
|
invertThumbnailAtDarkmode: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 1,
|
||||||
|
name: "Video",
|
||||||
|
thumbnail: "/editor/thumbnails/component_thumbnail_youtube.png",
|
||||||
|
invertThumbnailAtDarkmode: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "HTML",
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: 0,
|
||||||
|
name: "Iframe",
|
||||||
|
thumbnail: "/editor/thumbnails/component_thumbnail_iframe.svg",
|
||||||
|
invertThumbnailAtDarkmode: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Special",
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: 0,
|
||||||
|
name: "Banner",
|
||||||
|
thumbnail: "/editor/thumbnails/component_thumbnail_banner.png",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export { componentsGroups };
|
|
@ -1,63 +1,83 @@
|
||||||
import { Button, Card, Flex, Segmented } from "antd";
|
import { Button, Flex, Segmented } from "antd";
|
||||||
import MyBanner from "../../shared/components/MyBanner";
|
import MyBanner from "../../shared/components/MyBanner";
|
||||||
import { MyContainer } from "../../shared/components/MyContainer";
|
import { MyContainer } from "../../shared/components/MyContainer";
|
||||||
|
|
||||||
import img1 from "./pexels-photo-1181625.webp";
|
|
||||||
import img2 from "./pexels-photo-302894.webp";
|
|
||||||
import {
|
import {
|
||||||
AppstoreOutlined,
|
AppstoreOutlined,
|
||||||
BarsOutlined,
|
BarsOutlined,
|
||||||
CommentOutlined,
|
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import Search, { SearchProps } from "antd/es/input/Search";
|
import Search, { SearchProps } from "antd/es/input/Search";
|
||||||
import { Link } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import HeaderBar from "../../core/components/Header";
|
import HeaderBar from "../../core/components/Header";
|
||||||
|
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";
|
||||||
|
|
||||||
|
const CreateLessonButton: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [createLesson, { isLoading }] = useCreateLessonMutation();
|
||||||
|
|
||||||
|
const handleCreateLesson = async () => {
|
||||||
|
try {
|
||||||
|
const res = await createLesson({}).unwrap();
|
||||||
|
|
||||||
|
if (res && res.Id) {
|
||||||
|
navigate(
|
||||||
|
Constants.ROUTE_PATHS.LESSIONS.PAGE_EDITOR.replace(
|
||||||
|
":lessonId",
|
||||||
|
res.Id
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function ListItem({ img, title }: { img: string; title: string }) {
|
|
||||||
return (
|
return (
|
||||||
<Link to={"/lessons/aksdmaskdmsad"}>
|
<Button
|
||||||
<Card>
|
icon={<PlusOutlined />}
|
||||||
<Flex vertical={false} style={{ gap: 16 }}>
|
onClick={handleCreateLesson}
|
||||||
<img src={img} alt="img1" style={{ height: 140 }} />
|
loading={isLoading}
|
||||||
|
>
|
||||||
<Flex vertical justify="center">
|
Create
|
||||||
<div
|
</Button>
|
||||||
style={{
|
|
||||||
fontWeight: "bold",
|
|
||||||
fontSize: 36,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<CommentOutlined /> 12 comments
|
|
||||||
</div>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
</Card>
|
|
||||||
</Link>
|
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const data = [
|
const LessonList: React.FC = () => {
|
||||||
{
|
const { data, error, isLoading } = useGetLessonsQuery(undefined, {
|
||||||
img: img1,
|
refetchOnMountOrArgChange: true,
|
||||||
title: "How to clean the coffee machine",
|
});
|
||||||
},
|
|
||||||
{
|
if (isLoading) return <MySpin />;
|
||||||
img: img2,
|
if (error) return <MyErrorResult />;
|
||||||
title: "How to clean the coffee machine",
|
|
||||||
},
|
if (!data || data.length === 0) return <MyEmpty />;
|
||||||
{
|
|
||||||
img: img1,
|
return (
|
||||||
title: "How to clean the coffee machine",
|
<>
|
||||||
},
|
{data.map((item, index) => (
|
||||||
{
|
<LessonPreviewCard
|
||||||
img: img2,
|
key={index}
|
||||||
title: "How to clean the coffee machine",
|
mode="view"
|
||||||
},
|
lessonId={item.Id}
|
||||||
];
|
loading={false}
|
||||||
|
lessonPreview={{
|
||||||
|
Title: item.Title,
|
||||||
|
ThumbnailUrl: item.ThumbnailUrl,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default function Lessons() {
|
export default function Lessons() {
|
||||||
const onSearch: SearchProps["onSearch"] = (value, _e, info) =>
|
const onSearch: SearchProps["onSearch"] = (value, _e, info) =>
|
||||||
|
@ -76,7 +96,7 @@ export default function Lessons() {
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button icon={<PlusOutlined />}>Create</Button>
|
<CreateLessonButton />
|
||||||
|
|
||||||
<Search
|
<Search
|
||||||
placeholder="Search..."
|
placeholder="Search..."
|
||||||
|
@ -87,11 +107,8 @@ export default function Lessons() {
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Flex vertical gap={16}>
|
<Flex vertical gap={16}>
|
||||||
{data.map((item, index) => (
|
<LessonList />
|
||||||
<ListItem key={index} img={item.img} title={item.title} />
|
|
||||||
))}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
</MyContainer>
|
</MyContainer>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
Before Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 62 KiB |
|
@ -1,4 +1,4 @@
|
||||||
import img from "./pexels-photo-269077.jpeg";
|
import { Constants } from "core/utils/utils";
|
||||||
import styles from "./styles.module.css";
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
export default function MyBanner({
|
export default function MyBanner({
|
||||||
|
@ -17,7 +17,7 @@ export default function MyBanner({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={img}
|
src={`${Constants.STATIC_CONTENT_ADDRESS}/demo/organization_banner.jpeg`}
|
||||||
alt="banner"
|
alt="banner"
|
||||||
style={{
|
style={{
|
||||||
height: 228,
|
height: 228,
|
||||||
|
|
Before Width: | Height: | Size: 124 KiB |
|
@ -0,0 +1,5 @@
|
||||||
|
import { Empty } from "antd";
|
||||||
|
|
||||||
|
export default function MyEmpty() {
|
||||||
|
return <Empty />
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
import { CommentOutlined } from "@ant-design/icons";
|
||||||
|
import { Card, Flex, Typography } from "antd";
|
||||||
|
import { LessonPreview } from "core/types/lesson";
|
||||||
|
import { Constants, getImageUrl } from "core/utils/utils";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import MyUpload from "shared/components/MyUpload";
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
export default function MyLessonPreviewCard({
|
||||||
|
mode = "view",
|
||||||
|
lessonId,
|
||||||
|
loading = false,
|
||||||
|
lessonPreview,
|
||||||
|
onEditTitle,
|
||||||
|
onThumbnailChanged,
|
||||||
|
}: {
|
||||||
|
mode: "view" | "editable";
|
||||||
|
lessonId: string;
|
||||||
|
loading?: boolean;
|
||||||
|
lessonPreview: LessonPreview;
|
||||||
|
onEditTitle?: (newTitle: string) => void;
|
||||||
|
onThumbnailChanged?: () => void;
|
||||||
|
}) {
|
||||||
|
const LinkWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
if (mode === "editable") return <>{children}</>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
to={Constants.ROUTE_PATHS.LESSIONS.PAGE.replace(":lessonId", lessonId)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const UploadWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
if (mode === "view") return <>{children}</>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MyUpload
|
||||||
|
action={`/lessons/${lessonId}/preview/thumbnail`}
|
||||||
|
onChange={(info) => {
|
||||||
|
if (info.file.status === "done") {
|
||||||
|
onThumbnailChanged?.();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
imgCropProps={{
|
||||||
|
aspect: 5 / 4,
|
||||||
|
children: <></>,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</MyUpload>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LinkWrapper>
|
||||||
|
<Card loading={loading}>
|
||||||
|
<div className={styles.card}>
|
||||||
|
<UploadWrapper>
|
||||||
|
<img
|
||||||
|
src={
|
||||||
|
lessonPreview.ThumbnailUrl === ""
|
||||||
|
? `${Constants.STATIC_CONTENT_ADDRESS}/demo/lesson_thumbnail.webp`
|
||||||
|
: getImageUrl(lessonPreview.ThumbnailUrl)
|
||||||
|
}
|
||||||
|
alt="lesson thumbnail"
|
||||||
|
className={styles.img}
|
||||||
|
/>
|
||||||
|
</UploadWrapper>
|
||||||
|
|
||||||
|
<Flex vertical justify="center" style={{ width: "100%" }}>
|
||||||
|
{mode === "view" ? (
|
||||||
|
<div>
|
||||||
|
<div className={styles.title}>{lessonPreview.Title}</div>
|
||||||
|
<CommentOutlined /> 12 comments
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Typography.Title
|
||||||
|
level={2}
|
||||||
|
editable={{
|
||||||
|
triggerType: "text" as any,
|
||||||
|
tooltip: "click to edit text",
|
||||||
|
onChange: (event) => onEditTitle?.(event),
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{lessonPreview.Title}
|
||||||
|
</Typography.Title>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</LinkWrapper>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
.card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img {
|
||||||
|
height: 240px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 20px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 750px) {
|
||||||
|
.card {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img {
|
||||||
|
height: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 36px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { Result } from "antd";
|
||||||
|
|
||||||
|
export default function MyErrorResult() {
|
||||||
|
return (
|
||||||
|
<Result
|
||||||
|
status="error"
|
||||||
|
title="Something went wrong"
|
||||||
|
subTitle="Please try again later."
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
import ImgCrop, { ImgCropProps } from "antd-img-crop";
|
||||||
|
import Upload from "antd/es/upload/Upload";
|
||||||
|
import { getApiHeader } from "core/helper/api";
|
||||||
|
import { Constants } from "core/utils/utils";
|
||||||
|
|
||||||
|
export default function MyUpload({
|
||||||
|
children,
|
||||||
|
imgCropProps,
|
||||||
|
accept = Constants.ACCEPTED_IMAGE_FILE_TYPES.join(","),
|
||||||
|
maxCount = 1,
|
||||||
|
showUploadList = false,
|
||||||
|
headers = getApiHeader(),
|
||||||
|
action,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
imgCropProps?: ImgCropProps;
|
||||||
|
accept?: string;
|
||||||
|
maxCount?: number;
|
||||||
|
showUploadList?: boolean;
|
||||||
|
headers?: any;
|
||||||
|
action?: string;
|
||||||
|
onChange?: (info: any) => void;
|
||||||
|
}) {
|
||||||
|
const beforeUpload = (file: File) => {
|
||||||
|
if (!Constants.ACCEPTED_IMAGE_FILE_TYPES.includes(file.type)) {
|
||||||
|
console.error("File typ not allowed!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.size > Constants.MAX_IMAGE_SIZE) {
|
||||||
|
console.error("Image is to large!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ImgCrop
|
||||||
|
{...imgCropProps}
|
||||||
|
rotationSlider
|
||||||
|
>
|
||||||
|
<Upload
|
||||||
|
accept={accept}
|
||||||
|
maxCount={maxCount}
|
||||||
|
showUploadList={showUploadList}
|
||||||
|
headers={headers}
|
||||||
|
action={`${Constants.API_ADDRESS}${action}`}
|
||||||
|
onChange={onChange}
|
||||||
|
beforeUpload={beforeUpload}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Upload>
|
||||||
|
</ImgCrop>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,11 +1,8 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"baseUrl": "src",
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
"lib": [
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"esnext"
|
|
||||||
],
|
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
|
@ -20,7 +17,5 @@
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx"
|
"jsx": "react-jsx"
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["src"]
|
||||||
"src"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|