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"
|
||||||
|
]
|
||||||
|
}
|