init project

main
alex 2024-08-30 19:02:46 +02:00
commit 5b19897740
50 changed files with 22576 additions and 0 deletions

1
README.md Normal file
View File

@ -0,0 +1 @@
https://master--5fc05e08a4a65d0021ae0bf2.chromatic.com/?path=/story/presets-sortable-grid--basic-setup

20786
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

52
package.json Normal file
View File

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

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

43
public/index.html Normal file
View File

@ -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>

BIN
public/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

25
public/manifest.json Normal file
View File

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

3
public/robots.txt Normal file
View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

38
src/App.css Normal file
View File

@ -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);
}
}

9
src/App.test.tsx Normal file
View File

@ -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();
});

32
src/App.tsx Normal file
View File

@ -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;

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
); */
}

View File

@ -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;
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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;

View File

@ -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;

22
src/core/store/store.tsx Normal file
View File

@ -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();

18
src/core/utils/utils.tsx Normal file
View File

@ -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;

View File

@ -0,0 +1,7 @@
export default function ContactSupport() {
return (
<>
<h1>ContactSupport</h1>
</>
);
}

View File

@ -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>
</>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -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",
};
}

View File

@ -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;

View File

@ -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>
</>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@ -0,0 +1,7 @@
export default function PageNotFound() {
return (
<>
<h1>PageNotFound</h1>
</>
);
}

View File

@ -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>
</>
);
}

View File

@ -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>
</>
);
}

View File

@ -0,0 +1,7 @@
export default function SuggestFeature() {
return (
<>
<h1>SuggestFeature</h1>
</>
);
}

View File

@ -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>
</>
);
}

View File

@ -0,0 +1,7 @@
export default function WhatsNew() {
return (
<>
<h1>WhatsNew</h1>
</>
);
}

13
src/index.css Normal file
View File

@ -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;
}

24
src/index.tsx Normal file
View File

@ -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();

1
src/react-app-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="react-scripts" />

15
src/reportWebVitals.ts Normal file
View File

@ -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;

5
src/setupTests.ts Normal file
View File

@ -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';

View File

@ -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>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

View File

@ -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;
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

26
tsconfig.json Normal file
View File

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