init project
|
@ -0,0 +1 @@
|
|||
https://master--5fc05e08a4a65d0021ae0bf2.chromatic.com/?path=/story/presets-sortable-grid--basic-setup
|
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.2.6",
|
||||
"@dnd-kit/core": "^6.1.0",
|
||||
"@reduxjs/toolkit": "^2.2.7",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^16.18.106",
|
||||
"@types/react": "^18.3.4",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"antd": "^5.20.3",
|
||||
"i18next": "^23.7.6",
|
||||
"i18next-browser-languagedetector": "^7.2.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-i18next": "^13.5.0",
|
||||
"react-redux": "^9.1.2",
|
||||
"react-router-dom": "^6.19.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"typescript": "^4.9.5",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "BROWSER=none PORT=50261 react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 3.8 KiB |
|
@ -0,0 +1,43 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 9.4 KiB |
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
|
@ -0,0 +1,38 @@
|
|||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
|
@ -0,0 +1,32 @@
|
|||
import React from "react";
|
||||
import logo from "./logo.svg";
|
||||
import "./App.css";
|
||||
import { Button, ConfigProvider, Layout, theme } from "antd";
|
||||
import DashboardLayout from "./core/components/DashboardLayout";
|
||||
import { darkMode } from "./core/store/appSlice";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
const { defaultAlgorithm, darkAlgorithm } = theme;
|
||||
|
||||
function App() {
|
||||
const isDarkMode = useSelector(darkMode)
|
||||
|
||||
console.info(
|
||||
"\n %c LMS %c v0.1.0 %c \n",
|
||||
"background-color: #555;color: #fff;padding: 3px 2px 3px 3px;border-radius: 3px 0 0 3px;font-family: DejaVu Sans,Verdana,Geneva,sans-serif;text-shadow: 0 1px 0 rgba(1, 1, 1, 0.3)",
|
||||
"background-color: #bc81e0;background-image: linear-gradient(90deg, #e67e22, #9b59b6);color: #fff;padding: 3px 3px 3px 2px;border-radius: 0 3px 3px 0;font-family: DejaVu Sans,Verdana,Geneva,sans-serif;text-shadow: 0 1px 0 rgba(1, 1, 1, 0.3)",
|
||||
"background-color: transparent"
|
||||
);
|
||||
|
||||
return (
|
||||
<Layout style={{ minHeight: "100vh" }}>
|
||||
<ConfigProvider theme={{
|
||||
algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm,
|
||||
}}>
|
||||
<DashboardLayout />
|
||||
</ConfigProvider>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
|
@ -0,0 +1,109 @@
|
|||
import { Route, Routes } from "react-router-dom";
|
||||
import { MySupsenseFallback } from "../../../shared/components/MySupsenseFallback";
|
||||
import { Constants } from "../../utils/utils";
|
||||
import Team from "../../../features/Team";
|
||||
import Roles from "../../../features/Roles";
|
||||
import WhatsNew from "../../../features/WhatsNew";
|
||||
import SuggestFeature from "../../../features/SuggestFeature";
|
||||
import ContactSupport from "../../../features/ContactSupport";
|
||||
import Lessons from "../../../features/Lessons";
|
||||
import Settings from "../../../features/Settings";
|
||||
import PageNotFound from "../../../features/PageNotFound";
|
||||
import LessonPage from "../../../features/Lessons/LessonPage";
|
||||
import LessonPageEditor from "../../../features/Lessons/LessonPageEditor";
|
||||
|
||||
export default function AppRoutes() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route
|
||||
path={Constants.ROUTE_PATHS.LESSIONS.ROOT}
|
||||
element={
|
||||
<MySupsenseFallback>
|
||||
<Lessons />
|
||||
</MySupsenseFallback>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={Constants.ROUTE_PATHS.LESSIONS.PAGE}
|
||||
element={
|
||||
<MySupsenseFallback>
|
||||
<LessonPage />
|
||||
</MySupsenseFallback>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={Constants.ROUTE_PATHS.LESSIONS.PAGE_EDITOR}
|
||||
element={
|
||||
<MySupsenseFallback>
|
||||
<LessonPageEditor />
|
||||
</MySupsenseFallback>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={Constants.ROUTE_PATHS.ORGANIZATION_TEAM}
|
||||
element={
|
||||
<MySupsenseFallback>
|
||||
<Team />
|
||||
</MySupsenseFallback>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={Constants.ROUTE_PATHS.ORGANIZATION_ROLES}
|
||||
element={
|
||||
<MySupsenseFallback>
|
||||
<Roles />
|
||||
</MySupsenseFallback>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={Constants.ROUTE_PATHS.ORGANIZATION_SETTINGS}
|
||||
element={
|
||||
<MySupsenseFallback>
|
||||
<Settings />
|
||||
</MySupsenseFallback>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={Constants.ROUTE_PATHS.WHATS_NEW}
|
||||
element={
|
||||
<MySupsenseFallback>
|
||||
<WhatsNew />
|
||||
</MySupsenseFallback>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={Constants.ROUTE_PATHS.SUGGEST_FEATURE}
|
||||
element={
|
||||
<MySupsenseFallback>
|
||||
<SuggestFeature />
|
||||
</MySupsenseFallback>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={Constants.ROUTE_PATHS.CONTACT_SUPPORT}
|
||||
element={
|
||||
<MySupsenseFallback>
|
||||
<ContactSupport />
|
||||
</MySupsenseFallback>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="*"
|
||||
element={
|
||||
<MySupsenseFallback>
|
||||
<PageNotFound />
|
||||
</MySupsenseFallback>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
import { Grid, Layout } from "antd";
|
||||
import PageContent from "../PageContent";
|
||||
import SideMenuDesktop from "../SideMenu/Desktop";
|
||||
import SideMenuMobile from "../SideMenu/Mobile";
|
||||
import { SideMenuContent, SideMenuEditorContent } from "../SideMenu";
|
||||
import { useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { setIsSideMenuCollapsed } from "../SideMenu/sideMenuSlice";
|
||||
import { editorActive } from "../../../features/Lessons/LessonPageEditor/lessonPageEditorSlice";
|
||||
|
||||
const { useBreakpoint } = Grid;
|
||||
|
||||
export function SideMenu() {
|
||||
const screenBreakpoint = useBreakpoint();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const isEditorActive = useSelector(editorActive);
|
||||
|
||||
console.log("isEditorActive", isEditorActive);
|
||||
|
||||
const Content = () => {
|
||||
return isEditorActive ? <SideMenuEditorContent /> : <SideMenuContent />;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setIsSideMenuCollapsed(!screenBreakpoint.lg));
|
||||
}, [screenBreakpoint]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{screenBreakpoint.lg ? (
|
||||
<SideMenuDesktop>
|
||||
<Content />
|
||||
</SideMenuDesktop>
|
||||
) : (
|
||||
<SideMenuMobile>
|
||||
<Content />
|
||||
</SideMenuMobile>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DashboardLayout() {
|
||||
return (
|
||||
<Layout style={{ minHeight: "100vh" }}>
|
||||
<Layout>
|
||||
<SideMenu />
|
||||
|
||||
<PageContent />
|
||||
</Layout>
|
||||
</Layout>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,189 @@
|
|||
import { Avatar, Flex } from "antd";
|
||||
import {
|
||||
isSideMenuCollapsed,
|
||||
setIsSideMenuCollapsed,
|
||||
} from "../SideMenu/sideMenuSlice";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
EditOutlined,
|
||||
EyeOutlined,
|
||||
LeftOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
MoonOutlined,
|
||||
SunOutlined,
|
||||
UserOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { editorActive } from "../../../features/Lessons/LessonPageEditor/lessonPageEditorSlice";
|
||||
import { Link } from "react-router-dom";
|
||||
import { darkMode, setDarkMode } from "../../store/appSlice";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
type HeaderBarProps = {
|
||||
theme?: "light" | "dark";
|
||||
onView?: () => void;
|
||||
onEdit?: () => void;
|
||||
backTo?: string;
|
||||
};
|
||||
|
||||
export default function HeaderBar(props: HeaderBarProps = { theme: "light" }) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const isCollpased = useSelector(isSideMenuCollapsed);
|
||||
const isDarkMode = useSelector(darkMode);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
justify="space-between"
|
||||
align="center"
|
||||
style={{
|
||||
paddingTop: 12,
|
||||
paddingLeft: 12,
|
||||
paddingRight: 12,
|
||||
}}
|
||||
>
|
||||
<Flex align="center" gap={16}>
|
||||
<div
|
||||
className={
|
||||
props.theme === "light"
|
||||
? styles.containerLight
|
||||
: styles.containerDark
|
||||
}
|
||||
style={{ borderRadius: 28, padding: 4 }}
|
||||
>
|
||||
{isCollpased ? (
|
||||
<div
|
||||
className={styles.iconContainer}
|
||||
onClick={() => dispatch(setIsSideMenuCollapsed(false))}
|
||||
>
|
||||
<MenuUnfoldOutlined className={styles.icon} />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={styles.iconContainer}
|
||||
onClick={() => dispatch(setIsSideMenuCollapsed(true))}
|
||||
>
|
||||
<MenuFoldOutlined className={styles.icon} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{props.backTo && (
|
||||
<Link to={props.backTo}>
|
||||
<Flex gap={4}>
|
||||
<LeftOutlined />
|
||||
<span>Back</span>
|
||||
</Flex>
|
||||
</Link>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
align="center"
|
||||
className={
|
||||
props.theme === "light" ? styles.containerLight : styles.containerDark
|
||||
}
|
||||
style={{
|
||||
borderRadius: 28,
|
||||
paddingLeft: 6,
|
||||
paddingRight: 6,
|
||||
paddingTop: 4,
|
||||
paddingBottom: 4,
|
||||
}}
|
||||
gap={8}
|
||||
>
|
||||
{props.onView && (
|
||||
<div className={styles.iconContainer} onClick={props.onView}>
|
||||
<EyeOutlined className={styles.icon} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{props.onEdit && (
|
||||
<div className={styles.iconContainer} onClick={props.onEdit}>
|
||||
<EditOutlined className={styles.icon} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isDarkMode ? (
|
||||
<div
|
||||
className={styles.iconContainer}
|
||||
onClick={() => dispatch(setDarkMode(false))}
|
||||
>
|
||||
<SunOutlined className={styles.icon} />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={styles.iconContainer}
|
||||
onClick={() => dispatch(setDarkMode(true))}
|
||||
>
|
||||
<MoonOutlined className={styles.icon} />
|
||||
</div>
|
||||
)}
|
||||
<Avatar size="default" icon={<UserOutlined />} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
/* return (
|
||||
<Header
|
||||
style={{
|
||||
position: "sticky",
|
||||
top: 0,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: 0,
|
||||
background: "#fff",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
icon={isCollpased ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||||
onClick={() =>
|
||||
dispatch(setIsSideMenuCollapsed(!isCollpased))
|
||||
}
|
||||
style={{ fontSize: 16, width: 64, height: 64 }}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Link to={Constants.ROUTE_PATHS.LESSIONS.PAGE_EDITOR}>
|
||||
<EditOutlined
|
||||
style={{
|
||||
fontSize: 20,
|
||||
width: 64,
|
||||
height: 64,
|
||||
justifyContent: "center",
|
||||
}}
|
||||
onClick={() => dispatch(setEditorActive(!isEditorActive))}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
{isDarkMode ? (
|
||||
<SunOutlined
|
||||
style={{
|
||||
fontSize: 20,
|
||||
width: 64,
|
||||
height: 64,
|
||||
justifyContent: "center",
|
||||
}}
|
||||
onClick={() => dispatch(setDarkMode(false))}
|
||||
/>
|
||||
) : (
|
||||
<MoonOutlined
|
||||
style={{
|
||||
fontSize: 20,
|
||||
width: 64,
|
||||
height: 64,
|
||||
justifyContent: "center",
|
||||
}}
|
||||
onClick={() => {
|
||||
dispatch(setDarkMode(true));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Header>
|
||||
); */
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
.containerLight {
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.containerDark {
|
||||
background-color: rgba(0, 0, 0, 0.22);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.iconContainer {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
padding: 6px;
|
||||
border-radius: 28px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.iconContainer:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 20px;
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import { Layout } from "antd";
|
||||
import { Content } from "antd/es/layout/layout";
|
||||
import HeaderMenu from "../Header";
|
||||
import AppRoutes from "../AppRoutes";
|
||||
import { useSelector } from "react-redux";
|
||||
import { isSideMenuCollapsed } from "../SideMenu/sideMenuSlice";
|
||||
import { BreakpointLgWidth } from "../../utils/utils";
|
||||
|
||||
export default function PageContent() {
|
||||
const isCollpased = useSelector(isSideMenuCollapsed);
|
||||
|
||||
return (
|
||||
<Layout
|
||||
style={{
|
||||
marginLeft:
|
||||
isCollpased || window.document.body.clientWidth < BreakpointLgWidth
|
||||
? 0
|
||||
: 200,
|
||||
}}
|
||||
>
|
||||
|
||||
|
||||
<AppRoutes />
|
||||
</Layout>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
import Sider from "antd/es/layout/Sider";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { isSideMenuCollapsed, setIsSideMenuCollapsed } from "../sideMenuSlice";
|
||||
|
||||
export default function SideMenuDesktop({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
const isCollpased = useSelector(isSideMenuCollapsed);
|
||||
|
||||
return (
|
||||
<Sider
|
||||
theme="light"
|
||||
style={{
|
||||
overflow: "auto",
|
||||
height: "100vh",
|
||||
position: "fixed",
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
}}
|
||||
breakpoint="lg"
|
||||
collapsedWidth={1}
|
||||
collapsed={isCollpased}
|
||||
onCollapse={(collapsed) => dispatch(setIsSideMenuCollapsed(collapsed))}
|
||||
>
|
||||
{children}
|
||||
</Sider>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import { Drawer } from "antd";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { isSideMenuCollapsed, setIsSideMenuCollapsed } from "../sideMenuSlice";
|
||||
|
||||
export default function SideMenuMobile({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
const isCollpased = useSelector(isSideMenuCollapsed);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
open={!isCollpased}
|
||||
onClose={() => dispatch(setIsSideMenuCollapsed(true))}
|
||||
placement="left"
|
||||
styles={{ body: { padding: 0 } }}
|
||||
width={200}
|
||||
>
|
||||
{children}
|
||||
</Drawer>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,275 @@
|
|||
import {
|
||||
ControlOutlined,
|
||||
GroupOutlined,
|
||||
MessageOutlined,
|
||||
PieChartOutlined,
|
||||
QuestionCircleOutlined,
|
||||
SettingOutlined,
|
||||
SnippetsOutlined,
|
||||
TeamOutlined,
|
||||
WalletOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { Divider, Flex, Menu } from "antd";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
setIsSideMenuCollapsed,
|
||||
setSideMenuComponentFirstRender,
|
||||
sideMenuComponentFirstRender,
|
||||
} from "./sideMenuSlice";
|
||||
import { ItemType, MenuItemType } from "antd/es/menu/interface";
|
||||
import { BreakpointLgWidth, Constants } from "../../utils/utils";
|
||||
import { useDraggable, useDroppable } from "@dnd-kit/core";
|
||||
import Search from "antd/es/input/Search";
|
||||
import { MyContainer } from "../../../shared/components/MyContainer";
|
||||
import { addLessonContent } from "../../../features/Lessons/LessonPageEditor/lessonPageEditorSlice";
|
||||
|
||||
export function SideMenuContent() {
|
||||
const location = useLocation();
|
||||
const [selectedKeys, setSelectedKeys] = useState("/");
|
||||
const [openKeys, setOpenKeys] = useState([""]);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const componentFirstRender = useSelector(sideMenuComponentFirstRender);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const getFirstMenuItems = (): ItemType<MenuItemType>[] => {
|
||||
let items: ItemType<MenuItemType>[] = [];
|
||||
|
||||
// overview
|
||||
|
||||
let overviewGroup: ItemType<MenuItemType> = {
|
||||
key: "overviewGroup",
|
||||
label: "OVERVIEW",
|
||||
type: "group",
|
||||
children: [],
|
||||
};
|
||||
|
||||
if (overviewGroup.children) {
|
||||
overviewGroup.children.push({
|
||||
key: Constants.ROUTE_PATHS.LESSIONS.ROOT,
|
||||
label: "Lessons",
|
||||
icon: <SnippetsOutlined />,
|
||||
});
|
||||
}
|
||||
|
||||
items.push(overviewGroup);
|
||||
|
||||
// organization
|
||||
|
||||
let organizationGroup: ItemType<MenuItemType> = {
|
||||
key: "organizationGroup",
|
||||
label: "ORGANIZATION",
|
||||
type: "group",
|
||||
children: [],
|
||||
};
|
||||
|
||||
if (organizationGroup.children) {
|
||||
organizationGroup.children.push({
|
||||
key: Constants.ROUTE_PATHS.ORGANIZATION_TEAM,
|
||||
label: "Team",
|
||||
icon: <TeamOutlined />,
|
||||
});
|
||||
|
||||
organizationGroup.children.push({
|
||||
key: Constants.ROUTE_PATHS.ORGANIZATION_ROLES,
|
||||
label: "Roles",
|
||||
icon: <ControlOutlined />,
|
||||
});
|
||||
|
||||
organizationGroup.children.push({
|
||||
key: Constants.ROUTE_PATHS.ORGANIZATION_SETTINGS,
|
||||
label: "Settings",
|
||||
icon: <SettingOutlined />,
|
||||
});
|
||||
}
|
||||
|
||||
items.push(organizationGroup);
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
const getSecondMenuItems = (): ItemType<MenuItemType>[] => {
|
||||
let items: ItemType<MenuItemType>[] = [];
|
||||
|
||||
// support
|
||||
|
||||
items.push({
|
||||
key: Constants.ROUTE_PATHS.WHATS_NEW,
|
||||
label: "What's New",
|
||||
icon: <QuestionCircleOutlined />,
|
||||
});
|
||||
|
||||
// feedback
|
||||
|
||||
items.push({
|
||||
key: Constants.ROUTE_PATHS.SUGGEST_FEATURE,
|
||||
label: "Suggest a Feature",
|
||||
icon: <MessageOutlined />,
|
||||
});
|
||||
|
||||
// payment plan
|
||||
|
||||
items.push({
|
||||
key: Constants.ROUTE_PATHS.CONTACT_SUPPORT,
|
||||
label: "Contact Support",
|
||||
icon: <WalletOutlined />,
|
||||
});
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const pathname = location.pathname;
|
||||
|
||||
setSelectedKeys(pathname);
|
||||
|
||||
let path = pathname.split("/");
|
||||
|
||||
if (path.length > 2) {
|
||||
// /store/:storeId/:subPage - open the store menu
|
||||
setOpenKeys([`/${path[1]}/${path[2]}`]);
|
||||
}
|
||||
|
||||
// auto close sideMenu on mobile
|
||||
// this will prevent to auto close sideMenu on first render as the useEffects will be called after the first render
|
||||
if (componentFirstRender) {
|
||||
dispatch(setSideMenuComponentFirstRender(false));
|
||||
} else if (document.body.clientWidth < BreakpointLgWidth) {
|
||||
dispatch(setIsSideMenuCollapsed(true));
|
||||
}
|
||||
}, [location.pathname]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
flexDirection: "column",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
overflowY: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
padding: 10,
|
||||
}}
|
||||
></div>
|
||||
|
||||
<div style={{ overflowY: "scroll" }}>
|
||||
<Menu
|
||||
mode="inline"
|
||||
onClick={(item) => navigate(item.key)}
|
||||
theme="light"
|
||||
selectedKeys={[selectedKeys]}
|
||||
items={getFirstMenuItems()}
|
||||
openKeys={openKeys}
|
||||
onOpenChange={(openKeys) =>
|
||||
setOpenKeys(
|
||||
openKeys[openKeys.length - 1]
|
||||
? [openKeys[openKeys.length - 1]]
|
||||
: []
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Divider style={{ margin: 0 }} />
|
||||
|
||||
<Menu
|
||||
selectable={true}
|
||||
selectedKeys={[selectedKeys]}
|
||||
mode="vertical"
|
||||
onClick={(item) => navigate(item.key)}
|
||||
items={getSecondMenuItems()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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() {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
return (
|
||||
<MyContainer>
|
||||
<Search
|
||||
placeholder="What would you like to insert?"
|
||||
style={{ paddingBottom: 16 }}
|
||||
/>
|
||||
|
||||
{componentsGroups.map((group, i) => (
|
||||
<div key={i}>
|
||||
<span>{group.category}</span>
|
||||
|
||||
<Flex gap={16} wrap style={{ paddingTop: 16 }}>
|
||||
{group.components.map((component, i) => (
|
||||
<Flex
|
||||
key={i}
|
||||
vertical
|
||||
align="center"
|
||||
justify="center"
|
||||
style={{
|
||||
backgroundColor: "#EBEBEB",
|
||||
height: 80,
|
||||
width: 80,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => {
|
||||
console.log("insert component", component.type);
|
||||
dispatch(addLessonContent(component.type));
|
||||
}}
|
||||
>
|
||||
<div>Thumbnail</div>
|
||||
<div>{component.name}</div>
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
</div>
|
||||
))}
|
||||
</MyContainer>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import { BreakpointLgWidth } from "../../utils/utils";
|
||||
|
||||
export const sideMenuSlice = createSlice({
|
||||
name: "sideMenu",
|
||||
initialState: {
|
||||
isSideMenuCollapsed: window.innerWidth < BreakpointLgWidth,
|
||||
sideMenuComponentFirstRender: true,
|
||||
},
|
||||
reducers: {
|
||||
setIsSideMenuCollapsed: (state, action) => {
|
||||
state.isSideMenuCollapsed = action.payload;
|
||||
},
|
||||
setSideMenuComponentFirstRender: (state, action) => {
|
||||
state.sideMenuComponentFirstRender = action.payload;
|
||||
},
|
||||
},
|
||||
selectors: {
|
||||
isSideMenuCollapsed: (state) => state.isSideMenuCollapsed,
|
||||
sideMenuComponentFirstRender: (state) => state.sideMenuComponentFirstRender,
|
||||
},
|
||||
});
|
||||
|
||||
export const { setIsSideMenuCollapsed, setSideMenuComponentFirstRender } =
|
||||
sideMenuSlice.actions;
|
||||
|
||||
export const { isSideMenuCollapsed, sideMenuComponentFirstRender } =
|
||||
sideMenuSlice.selectors;
|
|
@ -0,0 +1,20 @@
|
|||
import { createSlice } from "@reduxjs/toolkit";
|
||||
|
||||
export const appSlice = createSlice({
|
||||
name: "app",
|
||||
initialState: {
|
||||
darkMode: false,
|
||||
},
|
||||
reducers: {
|
||||
setDarkMode: (state, action) => {
|
||||
state.darkMode = action.payload;
|
||||
},
|
||||
},
|
||||
selectors: {
|
||||
darkMode: (state) => state.darkMode,
|
||||
},
|
||||
})
|
||||
|
||||
export const { setDarkMode } = appSlice.actions;
|
||||
|
||||
export const { darkMode } = appSlice.selectors;
|
|
@ -0,0 +1,22 @@
|
|||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import { setupListeners } from "@reduxjs/toolkit/query";
|
||||
import { sideMenuSlice } from "../components/SideMenu/sideMenuSlice";
|
||||
import { lessonPageEditorSlice } from "../../features/Lessons/LessonPageEditor/lessonPageEditorSlice";
|
||||
import { appSlice } from "./appSlice";
|
||||
|
||||
export const makeStore = (/* preloadedState */) => {
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
app: appSlice.reducer,
|
||||
sideMenu: sideMenuSlice.reducer,
|
||||
lessonPageEditor: lessonPageEditorSlice.reducer,
|
||||
// counter: counterSlice.reducer,
|
||||
},
|
||||
// preloadedState,
|
||||
});
|
||||
|
||||
setupListeners(store.dispatch);
|
||||
return store;
|
||||
};
|
||||
|
||||
export const store = makeStore();
|
|
@ -0,0 +1,18 @@
|
|||
export const Constants = {
|
||||
ROUTE_PATHS: {
|
||||
LESSIONS: {
|
||||
ROOT: "/lessons",
|
||||
PAGE: "/lessons/:lessonId",
|
||||
PAGE_EDITOR: "/lessons/:lessonId/editor",
|
||||
},
|
||||
ORGANIZATION_TEAM: "/team",
|
||||
ORGANIZATION_ROLES: "/roles",
|
||||
ORGANIZATION_SETTINGS: "/organization",
|
||||
WHATS_NEW: "/whats-new",
|
||||
SUGGEST_FEATURE: "/suggest-feature",
|
||||
CONTACT_SUPPORT: "/contact-support",
|
||||
},
|
||||
};
|
||||
|
||||
// used for sideMenu
|
||||
export const BreakpointLgWidth = 992;
|
|
@ -0,0 +1,7 @@
|
|||
export default function ContactSupport() {
|
||||
return (
|
||||
<>
|
||||
<h1>ContactSupport</h1>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,150 @@
|
|||
import { Button, Card, Flex, Typography } from "antd";
|
||||
|
||||
import img from "./pexels-photo-302902.webp";
|
||||
import { MyContainer } from "../../../shared/components/MyContainer";
|
||||
import { CheckOutlined } from "@ant-design/icons";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { lessonContents } from "../LessonPageEditor/lessonPageEditorSlice";
|
||||
import HeaderBar from "../../../core/components/Header";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { Constants } from "../../../core/utils/utils";
|
||||
|
||||
export type LessonContent = {
|
||||
id: string;
|
||||
position: number;
|
||||
type: number;
|
||||
data: string;
|
||||
};
|
||||
|
||||
const LessonContents = [
|
||||
{
|
||||
id: "0",
|
||||
position: 1,
|
||||
type: 0,
|
||||
data: "How to clean the coffee machine",
|
||||
},
|
||||
{
|
||||
id: "1",
|
||||
position: 1,
|
||||
type: 2,
|
||||
data: img,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
position: 2,
|
||||
type: 1,
|
||||
data: "The proper cleaning of the coffee machine",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
position: 3,
|
||||
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",
|
||||
},
|
||||
] as LessonContent[];
|
||||
|
||||
export function Converter({
|
||||
mode,
|
||||
lessonContent,
|
||||
onEdit,
|
||||
}: {
|
||||
mode: "view" | "edititable";
|
||||
lessonContent: LessonContent;
|
||||
onEdit?: (newData: string) => void;
|
||||
}) {
|
||||
// const dispatch = useDispatch();
|
||||
// const contents = useSelector(lessonContents);
|
||||
|
||||
switch (lessonContent.type) {
|
||||
case 0:
|
||||
return mode === "view" ? (
|
||||
<div style={{ fontWeight: "bold", fontSize: 24 }}>
|
||||
{lessonContent.data}
|
||||
</div>
|
||||
) : (
|
||||
<Typography.Title
|
||||
editable={{
|
||||
triggerType: "text" as any,
|
||||
onChange: (event) => onEdit?.(event),
|
||||
}}
|
||||
level={1}
|
||||
style={{
|
||||
margin: 0,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{lessonContent.data}
|
||||
</Typography.Title>
|
||||
);
|
||||
case 1:
|
||||
return mode === "view" ? (
|
||||
<div style={{ fontSize: 16 }}>{lessonContent.data}</div>
|
||||
) : (
|
||||
<Typography.Text
|
||||
editable={{
|
||||
triggerType: "text" as any,
|
||||
onChange: (event) => onEdit?.(event),
|
||||
}}
|
||||
style={{
|
||||
margin: 0,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{lessonContent.data}
|
||||
</Typography.Text>
|
||||
);
|
||||
case 2:
|
||||
return (
|
||||
<img src={lessonContent.data} alt="img" style={{ width: "100%" }} />
|
||||
);
|
||||
case 3:
|
||||
return (
|
||||
<div style={{ fontWeight: "700", fontSize: 20 }}>
|
||||
{lessonContent.data}
|
||||
</div>
|
||||
);
|
||||
case 3:
|
||||
return <div style={{ fontSize: 14 }}>{lessonContent.data}</div>;
|
||||
default:
|
||||
return <div>Unknown type</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export default function LessonPage() {
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeaderBar
|
||||
theme="light"
|
||||
backTo={Constants.ROUTE_PATHS.LESSIONS.ROOT}
|
||||
onEdit={() =>
|
||||
navigate(`${location.pathname}/editor`)}
|
||||
/>
|
||||
|
||||
<MyContainer>
|
||||
<Flex justify="center">
|
||||
<Card
|
||||
style={{
|
||||
width: 800,
|
||||
maxWidth: 800,
|
||||
}}
|
||||
>
|
||||
{LessonContents.map((lessonContent) => (
|
||||
<div key={lessonContent.id} style={{ paddingBottom: 6 }}>
|
||||
<Converter mode="view" lessonContent={lessonContent} />
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Flex justify="right">
|
||||
<Button type="primary" icon={<CheckOutlined />}>
|
||||
Finish lesson
|
||||
</Button>
|
||||
</Flex>
|
||||
</Card>
|
||||
</Flex>
|
||||
</MyContainer>
|
||||
</>
|
||||
);
|
||||
}
|
After Width: | Height: | Size: 47 KiB |
|
@ -0,0 +1,128 @@
|
|||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import {
|
||||
deleteLessonContent,
|
||||
lessonContents,
|
||||
lessonThumbnail,
|
||||
setEditorActive,
|
||||
setLessonContent,
|
||||
setLessonThumbnailTitle,
|
||||
} from "./lessonPageEditorSlice";
|
||||
import { useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { Converter, LessonContent } from "../LessonPage";
|
||||
import { Card, Flex, Typography } from "antd";
|
||||
import { DeleteOutlined } from "@ant-design/icons";
|
||||
import { Constants } from "../../../core/utils/utils";
|
||||
import HeaderBar from "../../../core/components/Header";
|
||||
import img from "../LessonPage/pexels-photo-302902.webp";
|
||||
|
||||
export default function LessonPageEditor() {
|
||||
const { lessonId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const lnContents = useSelector(lessonContents);
|
||||
const lnThumbnail = useSelector(lessonThumbnail);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setEditorActive(true));
|
||||
|
||||
return () => {
|
||||
dispatch(setEditorActive(false));
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeaderBar
|
||||
theme="light"
|
||||
backTo={Constants.ROUTE_PATHS.LESSIONS.PAGE.replace(
|
||||
":lessonId",
|
||||
lessonId as string
|
||||
)}
|
||||
onView={() =>
|
||||
navigate(
|
||||
Constants.ROUTE_PATHS.LESSIONS.PAGE.replace(
|
||||
":lessonId",
|
||||
lessonId as string
|
||||
)
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<Flex justify="center" style={{ paddingTop: 24 }}>
|
||||
<Flex justify="center" vertical gap={16}>
|
||||
<Card
|
||||
style={{
|
||||
width: 800,
|
||||
maxWidth: 800,
|
||||
}}
|
||||
>
|
||||
<Flex vertical={false} style={{ gap: 16 }}>
|
||||
<img src={img} alt="img1" style={{ height: 140 }} />
|
||||
|
||||
<Flex align="center" style={{ width: "100%" }}>
|
||||
<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}>
|
||||
{lnContents.map((lnContent) => (
|
||||
<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>
|
||||
</Card>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function StandardEditorCompontent(type: number): LessonContent {
|
||||
return {
|
||||
id: Math.floor(Math.random() * 100).toString(),
|
||||
position: 1,
|
||||
type: type,
|
||||
data: "Some data",
|
||||
};
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import { LessonContent } from "../LessonPage";
|
||||
import { StandardEditorCompontent } from ".";
|
||||
|
||||
export const lessonPageEditorSlice = createSlice({
|
||||
name: "lessonPageEditor",
|
||||
initialState: {
|
||||
editorActive: false,
|
||||
lessonThumbnail: {
|
||||
img: "",
|
||||
title: "Test",
|
||||
},
|
||||
lessonContents: [] as LessonContent[],
|
||||
},
|
||||
reducers: {
|
||||
setEditorActive: (state, action) => {
|
||||
state.editorActive = action.payload;
|
||||
},
|
||||
addLessonContent: (state, action) => {
|
||||
state.lessonContents.push(StandardEditorCompontent(action.payload));
|
||||
},
|
||||
deleteLessonContent: (state, action) => {
|
||||
state.lessonContents = state.lessonContents.filter(
|
||||
(content) => content.id !== action.payload
|
||||
);
|
||||
},
|
||||
setLessonContent: (state, action) => {
|
||||
const index = state.lessonContents.findIndex(
|
||||
(content) => content.id === action.payload.id
|
||||
);
|
||||
|
||||
state.lessonContents[index].data = action.payload.data;
|
||||
},
|
||||
setLessonThumbnailTitle: (state, action) => {
|
||||
state.lessonThumbnail.title = action.payload;
|
||||
},
|
||||
},
|
||||
selectors: {
|
||||
editorActive: (state) => state.editorActive,
|
||||
lessonContents: (state) => state.lessonContents,
|
||||
lessonThumbnail: (state) => state.lessonThumbnail,
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
setEditorActive,
|
||||
addLessonContent,
|
||||
deleteLessonContent,
|
||||
setLessonContent,
|
||||
setLessonThumbnailTitle,
|
||||
} = lessonPageEditorSlice.actions;
|
||||
|
||||
export const { editorActive, lessonContents, lessonThumbnail } =
|
||||
lessonPageEditorSlice.selectors;
|
|
@ -0,0 +1,98 @@
|
|||
import { Button, Card, Flex, Segmented } from "antd";
|
||||
import MyBanner from "../../shared/components/MyBanner";
|
||||
import { MyContainer } from "../../shared/components/MyContainer";
|
||||
|
||||
import img1 from "./pexels-photo-1181625.webp";
|
||||
import img2 from "./pexels-photo-302894.webp";
|
||||
import {
|
||||
AppstoreOutlined,
|
||||
BarsOutlined,
|
||||
CommentOutlined,
|
||||
PlusOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import Search, { SearchProps } from "antd/es/input/Search";
|
||||
import { Link } from "react-router-dom";
|
||||
import HeaderBar from "../../core/components/Header";
|
||||
|
||||
function ListItem({ img, title }: { img: string; title: string }) {
|
||||
return (
|
||||
<Link to={"/lessons/aksdmaskdmsad"}>
|
||||
<Card>
|
||||
<Flex vertical={false} style={{ gap: 16 }}>
|
||||
<img src={img} alt="img1" style={{ height: 140 }} />
|
||||
|
||||
<Flex vertical justify="center">
|
||||
<div
|
||||
style={{
|
||||
fontWeight: "bold",
|
||||
fontSize: 36,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
<div>
|
||||
<CommentOutlined /> 12 comments
|
||||
</div>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
const data = [
|
||||
{
|
||||
img: img1,
|
||||
title: "How to clean the coffee machine",
|
||||
},
|
||||
{
|
||||
img: img2,
|
||||
title: "How to clean the coffee machine",
|
||||
},
|
||||
{
|
||||
img: img1,
|
||||
title: "How to clean the coffee machine",
|
||||
},
|
||||
{
|
||||
img: img2,
|
||||
title: "How to clean the coffee machine",
|
||||
},
|
||||
];
|
||||
|
||||
export default function Lessons() {
|
||||
const onSearch: SearchProps["onSearch"] = (value, _e, info) =>
|
||||
console.log(info?.source, value);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MyBanner title="Lessons" headerBar={<HeaderBar />} />
|
||||
|
||||
<MyContainer>
|
||||
<Flex justify="right" gap={16} style={{ paddingBottom: 16 }}>
|
||||
<Segmented
|
||||
options={[
|
||||
{ value: "List", icon: <BarsOutlined /> },
|
||||
{ value: "Kanban", icon: <AppstoreOutlined /> },
|
||||
]}
|
||||
/>
|
||||
|
||||
<Button icon={<PlusOutlined />}>Create</Button>
|
||||
|
||||
<Search
|
||||
placeholder="Search..."
|
||||
onSearch={onSearch}
|
||||
style={{ width: 300 }}
|
||||
allowClear
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Flex vertical gap={16}>
|
||||
{data.map((item, index) => (
|
||||
<ListItem key={index} img={item.img} title={item.title} />
|
||||
))}
|
||||
</Flex>
|
||||
|
||||
</MyContainer>
|
||||
</>
|
||||
);
|
||||
}
|
After Width: | Height: | Size: 61 KiB |
After Width: | Height: | Size: 62 KiB |
|
@ -0,0 +1,7 @@
|
|||
export default function PageNotFound() {
|
||||
return (
|
||||
<>
|
||||
<h1>PageNotFound</h1>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import HeaderBar from "../../core/components/Header";
|
||||
import MyBanner from "../../shared/components/MyBanner";
|
||||
import { MyContainer } from "../../shared/components/MyContainer";
|
||||
|
||||
export default function Roles() {
|
||||
return (
|
||||
<>
|
||||
<MyBanner title="Roles" subtitle="MANAGE" headerBar={
|
||||
<HeaderBar />
|
||||
} />
|
||||
|
||||
<MyContainer>
|
||||
<h1>Roles</h1>
|
||||
</MyContainer>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import HeaderBar from "../../core/components/Header";
|
||||
import MyBanner from "../../shared/components/MyBanner";
|
||||
import { MyContainer } from "../../shared/components/MyContainer";
|
||||
|
||||
export default function Settings() {
|
||||
return (
|
||||
<>
|
||||
<MyBanner title="Settings" subtitle="MANAGE" headerBar={
|
||||
<HeaderBar />
|
||||
} />
|
||||
|
||||
<MyContainer>
|
||||
<h1>Settings</h1>
|
||||
</MyContainer>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
export default function SuggestFeature() {
|
||||
return (
|
||||
<>
|
||||
<h1>SuggestFeature</h1>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import HeaderBar from "../../core/components/Header";
|
||||
import MyBanner from "../../shared/components/MyBanner";
|
||||
import { MyContainer } from "../../shared/components/MyContainer";
|
||||
|
||||
export default function Team() {
|
||||
return (
|
||||
<>
|
||||
<MyBanner title="Team" subtitle="MANAGE" headerBar={
|
||||
<HeaderBar />
|
||||
} />
|
||||
|
||||
<MyContainer>
|
||||
<h1>Team</h1>
|
||||
</MyContainer>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
export default function WhatsNew() {
|
||||
return (
|
||||
<>
|
||||
<h1>WhatsNew</h1>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import "./index.css";
|
||||
import App from "./App";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import { Provider } from "react-redux";
|
||||
import { store } from "./core/store/store";
|
||||
// import reportWebVitals from './reportWebVitals';
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById("root") as HTMLElement
|
||||
);
|
||||
root.render(
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
// reportWebVitals();
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="react-scripts" />
|
|
@ -0,0 +1,15 @@
|
|||
import { ReportHandler } from 'web-vitals';
|
||||
|
||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
|
@ -0,0 +1,5 @@
|
|||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
|
@ -0,0 +1,71 @@
|
|||
import img from "./pexels-photo-269077.jpeg";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
export default function MyBanner({
|
||||
title,
|
||||
subtitle,
|
||||
headerBar,
|
||||
}: {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
headerBar?: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={img}
|
||||
alt="banner"
|
||||
style={{
|
||||
height: 228,
|
||||
width: "100%",
|
||||
objectFit: "cover",
|
||||
userSelect: "none",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className={styles.gradientContainer}></div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
zIndex: 1,
|
||||
width: "100%",
|
||||
height: 228,
|
||||
}}
|
||||
>
|
||||
{headerBar}
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
paddingLeft: 24,
|
||||
paddingRight: 24,
|
||||
bottom: 24,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{subtitle && (
|
||||
<div style={{ fontSize: 12, color: "#A4A4A4" }}>{subtitle}</div>
|
||||
)}
|
||||
<div style={{ fontWeight: "bold", fontSize: 36, color: "#fff" }}>
|
||||
{title}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 2,
|
||||
marginTop: 16,
|
||||
background: "rgba(253, 253, 253, 0.3)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
After Width: | Height: | Size: 124 KiB |
|
@ -0,0 +1,26 @@
|
|||
.gradientContainer {
|
||||
position: absolute;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
background: -moz-linear-gradient(
|
||||
56deg,
|
||||
rgba(0, 0, 0, 1) 0%,
|
||||
rgba(0, 0, 0, 0.8) 22%,
|
||||
rgba(0, 0, 0, 0.2) 100%
|
||||
);
|
||||
background: -webkit-linear-gradient(
|
||||
56deg,
|
||||
rgba(0, 0, 0, 1) 0%,
|
||||
rgba(0, 0, 0, 0.8) 22%,
|
||||
rgba(0, 0, 0, 0.2) 100%
|
||||
);
|
||||
background: linear-gradient(
|
||||
56deg,
|
||||
rgba(0, 0, 0, 1) 0%,
|
||||
rgba(0, 0, 0, 0.8) 22%,
|
||||
rgba(0, 0, 0, 0.2) 100%
|
||||
);
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 228px;
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import { Spin } from "antd";
|
||||
import { MyCenteredContainer } from "../MyContainer";
|
||||
|
||||
export default function MyCenteredSpin({ fullHeight = false }) {
|
||||
return (
|
||||
<MyCenteredContainer fullHeight>
|
||||
<Spin size="large" />
|
||||
</MyCenteredContainer>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
import { Content } from "antd/es/layout/layout";
|
||||
|
||||
export function MyContainer({ children }: { children: React.ReactNode }) {
|
||||
return <Content style={{ padding: 12 }}>{children}</Content>;
|
||||
}
|
||||
|
||||
export function MyCenteredContainer({
|
||||
children,
|
||||
fullHeight = false,
|
||||
height = "100vh",
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
fullHeight?: boolean;
|
||||
height?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
alignContent: "center",
|
||||
alignItems: "center",
|
||||
height: fullHeight ? height : "85.3vh",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import { Spin } from "antd";
|
||||
import { Suspense } from "react";
|
||||
import MyCenteredSpin from "../MyCenteredSpin";
|
||||
|
||||
export function MySupsenseFallback({
|
||||
children,
|
||||
spinnerCentered = true,
|
||||
}: {
|
||||
children: any;
|
||||
spinnerCentered?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
spinnerCentered ? (
|
||||
<MyCenteredSpin fullHeight />
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingTop: 50,
|
||||
}}
|
||||
>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|