ai chat and websocket
parent
9176e80363
commit
73aad4727d
File diff suppressed because it is too large
Load Diff
117
package.json
117
package.json
|
@ -1,61 +1,60 @@
|
|||
{
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.2.6",
|
||||
"@dnd-kit/core": "^6.1.0",
|
||||
"@dnd-kit/modifiers": "^7.0.0",
|
||||
"@dnd-kit/sortable": "^8.0.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",
|
||||
"@vidstack/react": "^1.12.9",
|
||||
"antd": "^5.20.3",
|
||||
"antd-img-crop": "^4.23.0",
|
||||
"buffer": "^6.0.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",
|
||||
"uuid": "^10.0.0",
|
||||
"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"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/uuid": "^10.0.0"
|
||||
}
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.2.6",
|
||||
"@dnd-kit/core": "^6.1.0",
|
||||
"@dnd-kit/modifiers": "^7.0.0",
|
||||
"@dnd-kit/sortable": "^8.0.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",
|
||||
"@vidstack/react": "^1.12.9",
|
||||
"antd": "^5.20.3",
|
||||
"antd-img-crop": "^4.23.0",
|
||||
"buffer": "^6.0.3",
|
||||
"deep-chat-react": "^2.0.1",
|
||||
"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",
|
||||
"uuid": "^10.0.0",
|
||||
"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"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.5",
|
||||
"@types/uuid": "^10.0.0"
|
||||
}
|
||||
}
|
||||
|
|
81
src/App.tsx
81
src/App.tsx
|
@ -14,11 +14,14 @@ import SignIn from "./features/Auth/SignIn";
|
|||
import { useEffect } from "react";
|
||||
import { myFetch } from "./core/utils/utils";
|
||||
import MyCenteredSpin from "./shared/components/MyCenteredSpin";
|
||||
import webSocketService from "core/services/websocketService";
|
||||
import webSocketService, {
|
||||
WebSocketMessageHandler,
|
||||
} from "core/services/websocketService";
|
||||
import { MessageProvider } from "core/context/MessageContext";
|
||||
|
||||
const { defaultAlgorithm, darkAlgorithm } = theme;
|
||||
|
||||
function App() {
|
||||
export default function App() {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const isDarkMode = useSelector(darkMode);
|
||||
|
@ -33,33 +36,41 @@ function App() {
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (uAuthenticated) {
|
||||
(async () => {
|
||||
try {
|
||||
const response = await myFetch({
|
||||
url: "/app",
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
if (response) {
|
||||
dispatch(setUserAuthenticated(true));
|
||||
dispatch(setPrimaryColor(`#${response.Organization.PrimaryColor}`));
|
||||
dispatch(setLogoUrl(response.Organization.LogoUrl));
|
||||
dispatch(setBannerUrl(response.Organization.BannerUrl));
|
||||
}
|
||||
} catch (error) {}
|
||||
})();
|
||||
|
||||
webSocketService.connect();
|
||||
webSocketService.setHandler(WebSocketMessageHandler, dispatch);
|
||||
|
||||
return () => {
|
||||
webSocketService.disconnect();
|
||||
};
|
||||
}
|
||||
}, [uAuthenticated]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("App mounted");
|
||||
|
||||
if (!localStorage.getItem("session")) {
|
||||
dispatch(setUserAuthenticated(false));
|
||||
return;
|
||||
} else {
|
||||
dispatch(setUserAuthenticated(true));
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const response = await myFetch({
|
||||
url: "/app",
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
if (response) {
|
||||
dispatch(setUserAuthenticated(true));
|
||||
dispatch(setPrimaryColor(`#${response.Organization.PrimaryColor}`));
|
||||
dispatch(setLogoUrl(response.Organization.LogoUrl));
|
||||
dispatch(setBannerUrl(response.Organization.BannerUrl));
|
||||
}
|
||||
} catch (error) {}
|
||||
})();
|
||||
|
||||
webSocketService.connect();
|
||||
|
||||
return () => {
|
||||
webSocketService.disconnect();
|
||||
};
|
||||
}, []);
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<Layout style={{ minHeight: "100vh" }}>
|
||||
|
@ -71,16 +82,16 @@ function App() {
|
|||
},
|
||||
}}
|
||||
>
|
||||
{uAuthenticated == null ? (
|
||||
<MyCenteredSpin />
|
||||
) : uAuthenticated ? (
|
||||
<DashboardLayout />
|
||||
) : (
|
||||
<SignIn />
|
||||
)}
|
||||
<MessageProvider>
|
||||
{uAuthenticated == null ? (
|
||||
<MyCenteredSpin />
|
||||
) : uAuthenticated ? (
|
||||
<DashboardLayout />
|
||||
) : (
|
||||
<SignIn />
|
||||
)}
|
||||
</MessageProvider>
|
||||
</ConfigProvider>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
|
|
@ -13,10 +13,20 @@ import LessonPage from "../../../features/Lessons/LessonPage";
|
|||
import LessonPageEditor from "../../../features/Lessons/LessonPageEditor";
|
||||
import TeamCreateUser from "features/Team/CreateUser";
|
||||
import AccountSettings from "features/AccountSettings";
|
||||
import Board from "features/Board";
|
||||
|
||||
export default function AppRoutes() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route
|
||||
path={Constants.ROUTE_PATHS.BOARD}
|
||||
element={
|
||||
<MySupsenseFallback>
|
||||
<Board />
|
||||
</MySupsenseFallback>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={Constants.ROUTE_PATHS.LESSIONS.ROOT}
|
||||
element={
|
||||
|
@ -71,7 +81,7 @@ export default function AppRoutes() {
|
|||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
<Route
|
||||
path={Constants.ROUTE_PATHS.ORGANIZATION_SETTINGS}
|
||||
element={
|
||||
<MySupsenseFallback>
|
||||
|
@ -80,7 +90,7 @@ export default function AppRoutes() {
|
|||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
<Route
|
||||
path={Constants.ROUTE_PATHS.ACCOUNT_SETTINGS}
|
||||
element={
|
||||
<MySupsenseFallback>
|
||||
|
|
|
@ -1,59 +1,60 @@
|
|||
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";
|
||||
import MyDndContext from "./MyDndContext";
|
||||
|
||||
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';
|
||||
import MyDndContext from './MyDndContext';
|
||||
import AiChat from 'features/AiChat';
|
||||
|
||||
const { useBreakpoint } = Grid;
|
||||
|
||||
export function SideMenu() {
|
||||
const screenBreakpoint = useBreakpoint();
|
||||
const screenBreakpoint = useBreakpoint();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const isEditorActive = useSelector(editorActive);
|
||||
const isEditorActive = useSelector(editorActive);
|
||||
|
||||
console.log("isEditorActive", isEditorActive);
|
||||
console.log('isEditorActive', isEditorActive);
|
||||
|
||||
const Content = () => {
|
||||
return isEditorActive ? <SideMenuEditorContent /> : <SideMenuContent />;
|
||||
};
|
||||
const Content = () => {
|
||||
return isEditorActive ? <SideMenuEditorContent /> : <SideMenuContent />;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setIsSideMenuCollapsed(!screenBreakpoint.lg));
|
||||
}, [screenBreakpoint]);
|
||||
useEffect(() => {
|
||||
dispatch(setIsSideMenuCollapsed(!screenBreakpoint.lg));
|
||||
}, [screenBreakpoint]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{screenBreakpoint.lg ? (
|
||||
<SideMenuDesktop>
|
||||
<Content />
|
||||
</SideMenuDesktop>
|
||||
) : (
|
||||
<SideMenuMobile>
|
||||
<Content />
|
||||
</SideMenuMobile>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<AiChat />
|
||||
{screenBreakpoint.lg ? (
|
||||
<SideMenuDesktop>
|
||||
<Content />
|
||||
</SideMenuDesktop>
|
||||
) : (
|
||||
<SideMenuMobile>
|
||||
<Content />
|
||||
</SideMenuMobile>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DashboardLayout() {
|
||||
return (
|
||||
<MyDndContext>
|
||||
<Layout style={{ minHeight: "100vh" }}>
|
||||
<Layout>
|
||||
<SideMenu />
|
||||
return (
|
||||
<MyDndContext>
|
||||
<Layout style={{ minHeight: '100vh' }}>
|
||||
<Layout>
|
||||
<SideMenu />
|
||||
|
||||
<PageContent />
|
||||
</Layout>
|
||||
</Layout>
|
||||
</MyDndContext>
|
||||
);
|
||||
<PageContent />
|
||||
</Layout>
|
||||
</Layout>
|
||||
</MyDndContext>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,118 +1,168 @@
|
|||
import { Avatar, Dropdown, Flex } from 'antd';
|
||||
import { isSideMenuCollapsed, setIsSideMenuCollapsed } from '../SideMenu/sideMenuSlice';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { EditOutlined, EyeOutlined, LeftOutlined, LogoutOutlined, MenuFoldOutlined, MenuUnfoldOutlined, MoonOutlined, SunOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { darkMode, setDarkMode } from '../../reducers/appSlice';
|
||||
import styles from './styles.module.css';
|
||||
import { Constants } from 'core/utils/utils';
|
||||
import { Avatar, Dropdown, Flex } from "antd";
|
||||
import {
|
||||
isSideMenuCollapsed,
|
||||
setIsSideMenuCollapsed,
|
||||
} from "../SideMenu/sideMenuSlice";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
EditOutlined,
|
||||
EyeOutlined,
|
||||
LeftOutlined,
|
||||
LogoutOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
MoonOutlined,
|
||||
SunOutlined,
|
||||
UserOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import {
|
||||
darkMode,
|
||||
setDarkMode,
|
||||
setUserAuthenticated,
|
||||
} from "../../reducers/appSlice";
|
||||
import styles from "./styles.module.css";
|
||||
import { Constants } from "core/utils/utils";
|
||||
import webSocketService from "core/services/websocketService";
|
||||
|
||||
type HeaderBarProps = {
|
||||
theme?: 'light' | 'dark';
|
||||
onView?: () => void;
|
||||
onEdit?: () => void;
|
||||
backTo?: string;
|
||||
theme?: "light" | "dark";
|
||||
onView?: () => void;
|
||||
onEdit?: () => void;
|
||||
backTo?: string;
|
||||
};
|
||||
|
||||
export default function HeaderBar(props: HeaderBarProps = { theme: 'light' }) {
|
||||
const dispatch = useDispatch();
|
||||
export default function HeaderBar(props: HeaderBarProps = { theme: "light" }) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const isCollpased = useSelector(isSideMenuCollapsed);
|
||||
const isDarkMode = useSelector(darkMode);
|
||||
const isCollpased = useSelector(isSideMenuCollapsed);
|
||||
const isDarkMode = useSelector(darkMode);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Flex
|
||||
justify="space-between"
|
||||
align="center"
|
||||
style={{
|
||||
paddingTop: 12,
|
||||
paddingLeft: 12,
|
||||
paddingRight: 12,
|
||||
}}
|
||||
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 }}
|
||||
>
|
||||
<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}
|
||||
{isCollpased ? (
|
||||
<div
|
||||
className={styles.iconContainer}
|
||||
onClick={() => dispatch(setIsSideMenuCollapsed(false))}
|
||||
>
|
||||
{props.onView && (
|
||||
<div className={styles.iconContainer} onClick={props.onView}>
|
||||
<EyeOutlined className={styles.icon} />
|
||||
</div>
|
||||
)}
|
||||
<MenuUnfoldOutlined className={styles.icon} />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={styles.iconContainer}
|
||||
onClick={() => dispatch(setIsSideMenuCollapsed(true))}
|
||||
>
|
||||
<MenuFoldOutlined className={styles.icon} />
|
||||
</div>
|
||||
)}
|
||||
</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>
|
||||
)}
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: '1',
|
||||
label: 'Profile',
|
||||
icon: <UserOutlined />,
|
||||
onClick: () => navigate(Constants.ROUTE_PATHS.ACCOUNT_SETTINGS),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: 'Logout',
|
||||
icon: <LogoutOutlined />,
|
||||
danger: true,
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
<Avatar size="default" icon={<UserOutlined />} />
|
||||
</Dropdown>
|
||||
{props.backTo && (
|
||||
<Link to={props.backTo}>
|
||||
<Flex gap={4}>
|
||||
<LeftOutlined />
|
||||
<span>Back</span>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
</Link>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
/* return (
|
||||
<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>
|
||||
)}
|
||||
<Dropdown
|
||||
overlayStyle={{ minWidth: 150 }}
|
||||
trigger={["click"]}
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: "1",
|
||||
label: "Profile",
|
||||
icon: <UserOutlined />,
|
||||
onClick: () => navigate(Constants.ROUTE_PATHS.ACCOUNT_SETTINGS),
|
||||
},
|
||||
{
|
||||
key: "2",
|
||||
label: "Logout",
|
||||
icon: <LogoutOutlined />,
|
||||
danger: true,
|
||||
onClick: () => {
|
||||
webSocketService.disconnect();
|
||||
window.localStorage.removeItem("session");
|
||||
window.location.href = "/";
|
||||
},
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
size="default"
|
||||
icon={<UserOutlined />}
|
||||
style={{ cursor: "pointer" }}
|
||||
/>
|
||||
</Dropdown>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
/* return (
|
||||
<Header
|
||||
style={{
|
||||
position: "sticky",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
ControlOutlined,
|
||||
FundProjectionScreenOutlined,
|
||||
MessageOutlined,
|
||||
QuestionCircleOutlined,
|
||||
SettingOutlined,
|
||||
|
@ -17,7 +18,11 @@ import {
|
|||
sideMenuComponentFirstRender,
|
||||
} from "./sideMenuSlice";
|
||||
import { ItemType, MenuItemType } from "antd/es/menu/interface";
|
||||
import { BreakpointLgWidth, Constants } from "core/utils/utils";
|
||||
import {
|
||||
BreakpointLgWidth,
|
||||
BrowserTabSession,
|
||||
Constants,
|
||||
} from "core/utils/utils";
|
||||
import Search from "antd/es/input/Search";
|
||||
import { MyContainer } from "shared/components/MyContainer";
|
||||
import {
|
||||
|
@ -27,7 +32,7 @@ import {
|
|||
} from "features/Lessons/LessonPageEditor/lessonPageEditorSlice";
|
||||
import { Component, componentsGroups } from "features/Lessons/components";
|
||||
import { darkMode, logoUrl } from "core/reducers/appSlice";
|
||||
import { DndContext, DragOverlay, useDraggable } from "@dnd-kit/core";
|
||||
import { DndContext, DragOverlay } from "@dnd-kit/core";
|
||||
import { createPortal } from "react-dom";
|
||||
import { LessonState } from "core/types/lesson";
|
||||
import { useForm } from "antd/es/form/Form";
|
||||
|
@ -35,6 +40,11 @@ import {
|
|||
useAddLessonContentMutation,
|
||||
useUpdateLessonStateMutation,
|
||||
} from "core/services/lessons";
|
||||
import webSocketService, {
|
||||
addWebSocketReconnectListener,
|
||||
removeWebSocketReconnectListener,
|
||||
} from "core/services/websocketService";
|
||||
import { WebSocketSendMessagesCmds } from "core/utils/webSocket";
|
||||
|
||||
export function SideMenuContent() {
|
||||
const location = useLocation();
|
||||
|
@ -60,6 +70,12 @@ export function SideMenuContent() {
|
|||
};
|
||||
|
||||
if (overviewGroup.children) {
|
||||
overviewGroup.children.push({
|
||||
key: Constants.ROUTE_PATHS.BOARD,
|
||||
label: "Board",
|
||||
icon: <FundProjectionScreenOutlined />,
|
||||
});
|
||||
|
||||
overviewGroup.children.push({
|
||||
key: Constants.ROUTE_PATHS.LESSIONS.ROOT,
|
||||
label: "Lessons",
|
||||
|
@ -138,6 +154,20 @@ export function SideMenuContent() {
|
|||
|
||||
setSelectedKeys(pathname);
|
||||
|
||||
const subscribeTopicMessage = () => {
|
||||
webSocketService.send({
|
||||
Cmd: WebSocketSendMessagesCmds.SubscribeToTopic,
|
||||
Body: {
|
||||
topic: pathname,
|
||||
browserTabSession: BrowserTabSession,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
subscribeTopicMessage();
|
||||
|
||||
addWebSocketReconnectListener(subscribeTopicMessage);
|
||||
|
||||
let path = pathname.split("/");
|
||||
|
||||
if (path.length > 2) {
|
||||
|
@ -152,6 +182,8 @@ export function SideMenuContent() {
|
|||
} else if (document.body.clientWidth < BreakpointLgWidth) {
|
||||
dispatch(setIsSideMenuCollapsed(true));
|
||||
}
|
||||
|
||||
return () => removeWebSocketReconnectListener(subscribeTopicMessage);
|
||||
}, [location.pathname]);
|
||||
|
||||
return (
|
||||
|
@ -183,7 +215,9 @@ export function SideMenuContent() {
|
|||
<div>
|
||||
<Flex justify="center" style={{ paddingBottom: 24, width: "100%" }}>
|
||||
<img
|
||||
src={`${Constants.STATIC_CONTENT_ADDRESS}${appLogoUrl}`}
|
||||
src={`${Constants.STATIC_CONTENT_ADDRESS}${
|
||||
appLogoUrl || Constants.DEMO_LOGO_URL
|
||||
}`}
|
||||
alt="logo"
|
||||
style={{ height: 80 }}
|
||||
/>
|
||||
|
@ -234,8 +268,6 @@ export function SideMenuEditorContent() {
|
|||
|
||||
const [updateLessonState] = useUpdateLessonStateMutation();
|
||||
|
||||
console.log("lesson state", lnState);
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue({
|
||||
state: lnState,
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
import React, { createContext, useContext } from "react";
|
||||
import { message } from "antd";
|
||||
|
||||
interface MessageContextType {
|
||||
info: (content: string) => void;
|
||||
success: (content: string) => void;
|
||||
error: (content: string) => void;
|
||||
warning: (content: string) => void;
|
||||
}
|
||||
|
||||
const MessageContext = createContext<MessageContextType | null>(null);
|
||||
|
||||
export const MessageProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
|
||||
const info = (content: string) => {
|
||||
messageApi.info(content);
|
||||
};
|
||||
|
||||
const success = (content: string) => {
|
||||
messageApi.success(content);
|
||||
};
|
||||
|
||||
const error = (content: string) => {
|
||||
messageApi.error(content);
|
||||
};
|
||||
|
||||
const warning = (content: string) => {
|
||||
messageApi.warning(content);
|
||||
};
|
||||
|
||||
return (
|
||||
<MessageContext.Provider value={{ info, success, error, warning }}>
|
||||
{contextHolder}
|
||||
{children}
|
||||
</MessageContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useMessage = () => {
|
||||
const context = useContext(MessageContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useMessage must be used within a MessageProvider");
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
|
@ -1,9 +1,10 @@
|
|||
import { FetchArgs, fetchBaseQuery } from "@reduxjs/toolkit/query";
|
||||
import { Constants, handleLogout } from "core/utils/utils";
|
||||
import { BrowserTabSession, Constants, handleLogout } from "core/utils/utils";
|
||||
|
||||
export function getApiHeader() {
|
||||
return {
|
||||
"X-Authorization": localStorage.getItem("session") || "",
|
||||
"Browser-Tab-Session": BrowserTabSession,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -11,6 +12,7 @@ const baseQuery = fetchBaseQuery({
|
|||
baseUrl: Constants.API_ADDRESS,
|
||||
prepareHeaders: (headers) => {
|
||||
headers.set("X-Authorization", localStorage.getItem("session") || "");
|
||||
headers.set("Browser-Tab-Session", BrowserTabSession);
|
||||
return headers;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -5,7 +5,7 @@ export const appSlice = createSlice({
|
|||
initialState: {
|
||||
darkMode: false,
|
||||
userAuthenticated: null,
|
||||
primaryColor: "#1677FF",
|
||||
primaryColor: "#111",
|
||||
logoUrl: "",
|
||||
bannerUrl: "",
|
||||
},
|
||||
|
|
|
@ -3,7 +3,7 @@ import { baseQueryWithErrorHandling } from "core/helper/api";
|
|||
import {
|
||||
TeamMember,
|
||||
OrganizationSettings,
|
||||
IsSubdomainAvailableResponse,
|
||||
Roles,
|
||||
} from "core/types/organization";
|
||||
|
||||
export const organizationApi = createApi({
|
||||
|
@ -16,6 +16,19 @@ export const organizationApi = createApi({
|
|||
method: "GET",
|
||||
}),
|
||||
}),
|
||||
createTeamMember: builder.mutation({
|
||||
query: ({ firstName, lastName, email, roleId, password }) => ({
|
||||
url: "organization/team/members",
|
||||
method: "POST",
|
||||
body: {
|
||||
FirstName: firstName,
|
||||
LastName: lastName,
|
||||
Email: email,
|
||||
RoleId: roleId,
|
||||
Password: password,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
getOrganizationSettings: builder.query<OrganizationSettings, undefined>({
|
||||
query: () => ({
|
||||
url: "organization/settings",
|
||||
|
@ -41,13 +54,31 @@ export const organizationApi = createApi({
|
|||
method: "PATCH",
|
||||
}),
|
||||
}),
|
||||
getRoles: builder.query<Roles, undefined>({
|
||||
query: () => ({
|
||||
url: "organization/roles",
|
||||
method: "GET",
|
||||
}),
|
||||
}),
|
||||
/* createRole: builder.mutation({
|
||||
query: (name) => ({
|
||||
url: "organization/roles",
|
||||
method: "POST",
|
||||
body: {
|
||||
Name: name,
|
||||
},
|
||||
}),
|
||||
}), */
|
||||
}),
|
||||
});
|
||||
|
||||
export const {
|
||||
useGetTeamQuery,
|
||||
useCreateTeamMemberMutation,
|
||||
useGetOrganizationSettingsQuery,
|
||||
useUpdateOrganizationSettingsMutation,
|
||||
useIsSubdomainAvailableMutation,
|
||||
useUpdateSubdomainMutation,
|
||||
useGetRolesQuery,
|
||||
// useCreateRoleMutation,
|
||||
} = organizationApi;
|
||||
|
|
|
@ -1,32 +1,68 @@
|
|||
import { Dispatch } from "@reduxjs/toolkit";
|
||||
import {
|
||||
setBannerUrl,
|
||||
setLogoUrl,
|
||||
setPrimaryColor,
|
||||
} from "core/reducers/appSlice";
|
||||
import { BrowserTabSession, Constants } from "core/utils/utils";
|
||||
import { WebSocketReceivedMessagesCmds } from "core/utils/webSocket";
|
||||
import { addTeamMember } from "features/Team/teamSlice";
|
||||
import { useDispatch } from "react-redux";
|
||||
|
||||
interface WebSocketMessage {
|
||||
type: string;
|
||||
payload: any;
|
||||
Cmd: number;
|
||||
Body: any;
|
||||
}
|
||||
|
||||
class WebSocketService {
|
||||
private url: string;
|
||||
private socket: WebSocket | null = null;
|
||||
private reconnectInterval: number = 10000; // 5 Sekunden
|
||||
private handlers: Record<string, (payload: any) => void> = {};
|
||||
private reconnectInterval: number = 1000; // in ms
|
||||
private offlineQueue: WebSocketMessage[] = [];
|
||||
private firstConnect: boolean = true;
|
||||
|
||||
private messageHandler:
|
||||
| ((message: WebSocketMessage, dispatch: Dispatch) => void)
|
||||
| null = null;
|
||||
|
||||
constructor(url: string) {
|
||||
this.url = `${url}?auth=${localStorage.getItem(
|
||||
"session"
|
||||
)}&bts=${BrowserTabSession}`;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
private dispatch: Dispatch | null = null;
|
||||
|
||||
public connect(): void {
|
||||
this.socket = new WebSocket(this.url);
|
||||
this.socket = new WebSocket(
|
||||
`${this.url}?auth=${localStorage.getItem(
|
||||
"session"
|
||||
)}&bts=${BrowserTabSession}`
|
||||
);
|
||||
|
||||
this.socket.onopen = () => {
|
||||
console.log("WebSocket connected");
|
||||
console.log("WebSocket connected", this.firstConnect);
|
||||
|
||||
// Send all messages from the offline queue
|
||||
|
||||
this.offlineQueue.forEach((message) => this.send(message));
|
||||
this.offlineQueue = [];
|
||||
|
||||
// Dispatch event to notify that the WebSocket connection is established
|
||||
|
||||
if (!this.firstConnect) {
|
||||
document.dispatchEvent(webSocketConnectionEvent);
|
||||
} else {
|
||||
this.firstConnect = false;
|
||||
}
|
||||
};
|
||||
|
||||
this.socket.onmessage = (event: MessageEvent) => {
|
||||
const data: WebSocketMessage = JSON.parse(event.data);
|
||||
this.handleMessage(data);
|
||||
|
||||
if (this.messageHandler) {
|
||||
this.messageHandler(data, this.dispatch!);
|
||||
} else {
|
||||
console.error("No handler defined for WebSocket messages");
|
||||
}
|
||||
};
|
||||
|
||||
this.socket.onclose = () => {
|
||||
|
@ -39,21 +75,24 @@ class WebSocketService {
|
|||
};
|
||||
}
|
||||
|
||||
private handleMessage(data: WebSocketMessage): void {
|
||||
const { type, payload } = data;
|
||||
if (this.handlers[type]) {
|
||||
this.handlers[type](payload);
|
||||
}
|
||||
public setHandler(
|
||||
handler: (message: WebSocketMessage, dispatch: Dispatch) => void,
|
||||
dispatch: Dispatch
|
||||
): void {
|
||||
this.messageHandler = handler;
|
||||
this.dispatch = dispatch;
|
||||
}
|
||||
|
||||
public onMessage(type: string, handler: (payload: any) => void): void {
|
||||
this.handlers[type] = handler;
|
||||
}
|
||||
|
||||
public send(type: string, payload: any): void {
|
||||
const message: WebSocketMessage = { type, payload };
|
||||
if (this.socket) {
|
||||
this.socket.send(JSON.stringify(message));
|
||||
public send(message: WebSocketMessage): void {
|
||||
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
||||
this.socket.send(
|
||||
JSON.stringify({
|
||||
Cmd: message.Cmd,
|
||||
Body: message.Body,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
this.offlineQueue.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -64,5 +103,52 @@ class WebSocketService {
|
|||
}
|
||||
}
|
||||
|
||||
const webSocketConnectionEventName = "WebSocketConnectionEvent";
|
||||
|
||||
const webSocketConnectionEvent = new CustomEvent(webSocketConnectionEventName, {
|
||||
detail: "wsReconnect",
|
||||
});
|
||||
|
||||
export function addWebSocketReconnectListener(callback: () => void): void {
|
||||
document.addEventListener(webSocketConnectionEventName, callback);
|
||||
}
|
||||
|
||||
export function removeWebSocketReconnectListener(callback: () => void): void {
|
||||
document.removeEventListener(webSocketConnectionEventName, callback);
|
||||
}
|
||||
|
||||
const webSocketService = new WebSocketService(Constants.WS_ADDRESS);
|
||||
export default webSocketService;
|
||||
|
||||
export function WebSocketMessageHandler(
|
||||
message: WebSocketMessage,
|
||||
dispatch: Dispatch
|
||||
) {
|
||||
const { Cmd, Body } = message;
|
||||
|
||||
console.log("WebSocketMessageHandler", Cmd, Body);
|
||||
|
||||
switch (Cmd) {
|
||||
case WebSocketReceivedMessagesCmds.SettingsUpdated:
|
||||
dispatch(setPrimaryColor(Body.PrimaryColor));
|
||||
break;
|
||||
case WebSocketReceivedMessagesCmds.SettingsUpdatedLogo:
|
||||
dispatch(setLogoUrl(Body));
|
||||
break;
|
||||
case WebSocketReceivedMessagesCmds.SettingsUpdatedBanner:
|
||||
dispatch(setBannerUrl(Body));
|
||||
break;
|
||||
case WebSocketReceivedMessagesCmds.SettingsUpdatedSubdomain:
|
||||
localStorage.removeItem("session");
|
||||
|
||||
window.location.href = `${
|
||||
window.location.protocol
|
||||
}//${Body}.${window.location.hostname.split(".").slice(1).join(".")}`;
|
||||
break;
|
||||
case WebSocketReceivedMessagesCmds.TeamAddedMember:
|
||||
dispatch(addTeamMember(Body));
|
||||
break;
|
||||
default:
|
||||
console.error("Unknown message type:", Cmd);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,26 +1,33 @@
|
|||
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 '../reducers/appSlice';
|
||||
import { lessonsApi } from 'core/services/lessons';
|
||||
import { organizationApi } from 'core/services/organization';
|
||||
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 "../reducers/appSlice";
|
||||
import { lessonsApi } from "core/services/lessons";
|
||||
import { organizationApi } from "core/services/organization";
|
||||
import { teamSlice } from "features/Team/teamSlice";
|
||||
|
||||
const makeStore = (/* preloadedState */) => {
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
app: appSlice.reducer,
|
||||
sideMenu: sideMenuSlice.reducer,
|
||||
lessonPageEditor: lessonPageEditorSlice.reducer,
|
||||
[lessonsApi.reducerPath]: lessonsApi.reducer,
|
||||
[organizationApi.reducerPath]: organizationApi.reducer,
|
||||
},
|
||||
// preloadedState,
|
||||
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(lessonsApi.middleware, organizationApi.middleware),
|
||||
});
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
app: appSlice.reducer,
|
||||
sideMenu: sideMenuSlice.reducer,
|
||||
lessonPageEditor: lessonPageEditorSlice.reducer,
|
||||
teamSlice: teamSlice.reducer,
|
||||
[lessonsApi.reducerPath]: lessonsApi.reducer,
|
||||
[organizationApi.reducerPath]: organizationApi.reducer,
|
||||
[teamSlice.reducerPath]: teamSlice.reducer,
|
||||
},
|
||||
// preloadedState,
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware().concat(
|
||||
lessonsApi.middleware,
|
||||
organizationApi.middleware
|
||||
),
|
||||
});
|
||||
|
||||
setupListeners(store.dispatch);
|
||||
return store;
|
||||
setupListeners(store.dispatch);
|
||||
return store;
|
||||
};
|
||||
|
||||
export const store = makeStore();
|
||||
|
|
|
@ -3,7 +3,7 @@ export interface TeamMember {
|
|||
FirstName: string;
|
||||
LastName: string;
|
||||
Email: string;
|
||||
Role: string;
|
||||
RoleId: string;
|
||||
Status: string;
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,18 @@ export interface OrganizationSettings {
|
|||
BannerUrl: string;
|
||||
}
|
||||
|
||||
export interface IsSubdomainAvailableResponse {
|
||||
Available: boolean;
|
||||
interface RoleUser {
|
||||
FirstName: string;
|
||||
LastName: string;
|
||||
ProfilePictureUrl: string;
|
||||
}
|
||||
|
||||
export interface Role {
|
||||
Id: string;
|
||||
Permissions: number[];
|
||||
Users: RoleUser[];
|
||||
}
|
||||
|
||||
export interface Roles {
|
||||
Roles: Role[];
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ export const Constants = {
|
|||
WS_ADDRESS: `${wssProtocol}${window.location.hostname}/apiws`,
|
||||
STATIC_CONTENT_ADDRESS: `${window.location.protocol}//${window.location.hostname}/api/static`,
|
||||
ROUTE_PATHS: {
|
||||
BOARD: "/",
|
||||
LESSIONS: {
|
||||
ROOT: "/lessons",
|
||||
PAGE: "/lessons/:lessonId",
|
||||
|
@ -16,7 +17,7 @@ export const Constants = {
|
|||
ORGANIZATION_TEAM: "/team",
|
||||
ORGANIZATION_TEAM_CREATE_USER: "/team/create-user",
|
||||
ORGANIZATION_ROLES: "/roles",
|
||||
ORGANIZATION_SETTINGS: "/organization",
|
||||
ORGANIZATION_SETTINGS: "/settings",
|
||||
ACCOUNT_SETTINGS: "/account",
|
||||
WHATS_NEW: "/whats-new",
|
||||
SUGGEST_FEATURE: "/suggest-feature",
|
||||
|
@ -36,6 +37,20 @@ export const Constants = {
|
|||
GLOBALS: {
|
||||
MIN_SUBDOMAIN_LENGTH: 3,
|
||||
MAX_SUBDOMAIN_LENGTH: 32,
|
||||
MIN_FIRST_NAME_LENGTH: 2,
|
||||
MAX_FIRST_NAME_LENGTH: 32,
|
||||
MIN_LAST_NAME_LENGTH: 2,
|
||||
MAX_LAST_NAME_LENGTH: 32,
|
||||
MIN_PASSWORD_LENGTH: 8,
|
||||
MAX_PASSWORD_LENGTH: 32,
|
||||
},
|
||||
DEMO_LOGO_URL: "/demo/logo.png",
|
||||
DEMO_BANNER_URL: "/demo/organization_banner.jpeg",
|
||||
PERMISSIONS: {
|
||||
TEAM_INVITE_NEW_MEMBER: 1,
|
||||
TEAM_REMOVE_MEMBER: 2,
|
||||
ROLES_CREATE: 3,
|
||||
ROLES_EDIT: 4,
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
enum WebSocketSendMessagesCmds {
|
||||
SubscribeToTopic = 1,
|
||||
}
|
||||
|
||||
enum WebSocketReceivedMessagesCmds {
|
||||
SettingsUpdated = 1,
|
||||
SettingsUpdatedLogo = 2,
|
||||
SettingsUpdatedBanner = 3,
|
||||
SettingsUpdatedSubdomain = 4,
|
||||
TeamAddedMember = 5,
|
||||
}
|
||||
|
||||
export { WebSocketSendMessagesCmds, WebSocketReceivedMessagesCmds };
|
|
@ -0,0 +1,38 @@
|
|||
import React from 'react';
|
||||
import { FloatButton } from 'antd';
|
||||
import { CommentOutlined } from '@ant-design/icons';
|
||||
import { DeepChat } from 'deep-chat-react';
|
||||
|
||||
function AiChat() {
|
||||
const [visible, setVisible] = React.useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
{visible ? (
|
||||
<div style={{ position: 'fixed', bottom: 100, right: 10, zIndex: 10000, maxWidth: '95vw', width: 500, height: 1000, maxHeight: 'calc(100vh - 165px)' }}>
|
||||
<DeepChat
|
||||
style={{ width: '100%', height: '100%', borderRadius: 10, boxShadow: '0 0 10px rgba(0,0,0,0.1)', bottom: 0, position: 'absolute' }}
|
||||
history={[
|
||||
{ text: 'Show me a modern city', role: 'user' },
|
||||
{ files: [{ src: 'https://test.ex.umbach.dev/api/statico/809fe37e-8c41-4a44-98d1-d9247affd531/67c763b6-ea67-4b49-9621-2f78b85eb180.png', type: 'image' }], role: 'ai' },
|
||||
{ text: 'Whats on your mind?', role: 'user' },
|
||||
{ text: 'Peace and tranquility', role: 'ai' },
|
||||
]}
|
||||
></DeepChat>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<FloatButton
|
||||
icon={<CommentOutlined />}
|
||||
type="primary"
|
||||
onClick={() => console.log('onClick')}
|
||||
style={{ zIndex: 10000 }}
|
||||
onClickCapture={() => {
|
||||
setVisible(!visible);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default AiChat;
|
|
@ -0,0 +1,10 @@
|
|||
import HeaderBar from "core/components/Header";
|
||||
import MyBanner from "shared/components/MyBanner";
|
||||
|
||||
export default function Board() {
|
||||
return (
|
||||
<>
|
||||
<MyBanner title="Board" headerBar={<HeaderBar />} />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,7 +1,20 @@
|
|||
import { Descriptions, Typography } from 'antd';
|
||||
import MyMiddleCard from 'shared/components/MyMiddleCard';
|
||||
import MyBanner from 'shared/components/MyBanner';
|
||||
|
||||
export default function ContactSupport() {
|
||||
return (
|
||||
<>
|
||||
<h1>ContactSupport</h1>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<MyBanner title="Contact Support" />
|
||||
|
||||
<MyMiddleCard title="Support">
|
||||
<Typography.Paragraph>If you have any questions or need help, please contact us at the following e-mail address:</Typography.Paragraph>
|
||||
<Descriptions>
|
||||
<Descriptions.Item label="E-Mail">
|
||||
<a href="mailto:support@jannex.de">support@jannex.de</a>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</MyMiddleCard>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,13 +4,14 @@ import { Button, Input, Typography, Flex } from 'antd';
|
|||
import { useUpdateLessonContentMutation } from 'core/services/lessons';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { currentLessonId } from './LessonPageEditor/lessonPageEditorSlice';
|
||||
import { useRef, useEffect } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import MyUpload from 'shared/components/MyUpload';
|
||||
import { Constants } from 'core/utils/utils';
|
||||
import { MediaPlayer, MediaProvider } from '@vidstack/react';
|
||||
import { defaultLayoutIcons, DefaultVideoLayout } from '@vidstack/react/player/layouts/default';
|
||||
import '@vidstack/react/player/styles/default/theme.css';
|
||||
import '@vidstack/react/player/styles/default/layouts/video.css';
|
||||
import { darkMode } from 'core/reducers/appSlice';
|
||||
|
||||
const extractVideoId = (url: string) => {
|
||||
// regex to extract video id from youtube url
|
||||
|
@ -22,6 +23,8 @@ const extractVideoId = (url: string) => {
|
|||
export function Converter({ mode, lessonContent, onEdit }: { mode: 'view' | 'edititable'; lessonContent: LessonContent; onEdit?: (newData: string) => void }) {
|
||||
const lessonId = useSelector(currentLessonId);
|
||||
|
||||
const isDarkMode = useSelector(darkMode);
|
||||
|
||||
const [reqUpdateLessonContent] = useUpdateLessonContentMutation();
|
||||
|
||||
const debounceRef = useRef<null | NodeJS.Timeout>(null);
|
||||
|
@ -63,7 +66,19 @@ export function Converter({ mode, lessonContent, onEdit }: { mode: 'view' | 'edi
|
|||
if (mode === 'view') {
|
||||
const formattedText = lessonContent.Data.split('\n').map((line, index) => <li key={index}>{line || '\u00A0'}</li>);
|
||||
|
||||
return <ul style={{ fontSize: 16, wordBreak: 'break-all', padding: 0, margin: 0, listStyleType: 'none' }}>{formattedText}</ul>;
|
||||
return (
|
||||
<ul
|
||||
style={{
|
||||
fontSize: 16,
|
||||
wordBreak: 'break-all',
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
listStyleType: 'none',
|
||||
}}
|
||||
>
|
||||
{formattedText}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -106,7 +121,9 @@ export function Converter({ mode, lessonContent, onEdit }: { mode: 'view' | 'edi
|
|||
position: 'relative',
|
||||
height: 120,
|
||||
width: '100%',
|
||||
backgroundColor: '#EBEBEB',
|
||||
backgroundColor: isDarkMode ? '#222' : '#EBEBEB',
|
||||
marginTop: 4,
|
||||
borderRadius: 4,
|
||||
marginRight: 8,
|
||||
}}
|
||||
>
|
||||
|
@ -151,7 +168,8 @@ export function Converter({ mode, lessonContent, onEdit }: { mode: 'view' | 'edi
|
|||
position: 'relative',
|
||||
height: 120,
|
||||
width: '100%',
|
||||
backgroundColor: '#EBEBEB',
|
||||
backgroundColor: isDarkMode ? '#222' : '#EBEBEB',
|
||||
borderRadius: 4,
|
||||
margin: '12px 12px 12px 0',
|
||||
}}
|
||||
>
|
||||
|
@ -179,7 +197,15 @@ export function Converter({ mode, lessonContent, onEdit }: { mode: 'view' | 'edi
|
|||
<img src={`${Constants.STATIC_CONTENT_ADDRESS}/${lessonContent.Data}`} alt="img" style={{ width: '100%' }} />
|
||||
|
||||
{mode === 'edititable' && (
|
||||
<Flex style={{ width: '100%', backgroundColor: '#EBEBEB' }} justify="center">
|
||||
<Flex
|
||||
style={{
|
||||
width: '100%',
|
||||
backgroundColor: isDarkMode ? '#222' : '#EBEBEB',
|
||||
marginTop: 4,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
justify="center"
|
||||
>
|
||||
<div>
|
||||
<span>Choose another image from</span>
|
||||
<GalleryUpload />
|
||||
|
@ -247,7 +273,9 @@ export function Converter({ mode, lessonContent, onEdit }: { mode: 'view' | 'edi
|
|||
position: 'relative',
|
||||
height: 120,
|
||||
width: '100%',
|
||||
backgroundColor: '#EBEBEB',
|
||||
backgroundColor: isDarkMode ? '#222' : '#EBEBEB',
|
||||
marginTop: 4,
|
||||
borderRadius: 4,
|
||||
marginRight: 8,
|
||||
}}
|
||||
>
|
||||
|
@ -289,7 +317,8 @@ export function Converter({ mode, lessonContent, onEdit }: { mode: 'view' | 'edi
|
|||
position: 'relative',
|
||||
height: 120,
|
||||
width: '100%',
|
||||
backgroundColor: '#EBEBEB',
|
||||
backgroundColor: isDarkMode ? '#222' : '#EBEBEB',
|
||||
borderRadius: 4,
|
||||
margin: '12px 12px 12px 0',
|
||||
}}
|
||||
>
|
||||
|
@ -320,7 +349,17 @@ export function Converter({ mode, lessonContent, onEdit }: { mode: 'view' | 'edi
|
|||
</MediaPlayer>
|
||||
|
||||
{mode === 'edititable' && (
|
||||
<Flex style={{ width: '100%', backgroundColor: '#EBEBEB', height: 48 }} justify="center" align="center">
|
||||
<Flex
|
||||
style={{
|
||||
width: '100%',
|
||||
backgroundColor: isDarkMode ? '#222' : '#EBEBEB',
|
||||
margin: '4px 0',
|
||||
borderRadius: 4,
|
||||
height: 48,
|
||||
}}
|
||||
justify="center"
|
||||
align="center"
|
||||
>
|
||||
<div>
|
||||
<span>Choose another video from</span>
|
||||
<VideoUpload />
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
import { Button, Divider, Flex, Segmented } from "antd";
|
||||
import { Button, Divider, Flex } from "antd";
|
||||
import MyBanner from "shared/components/MyBanner";
|
||||
import { MyContainer } from "shared/components/MyContainer";
|
||||
import {
|
||||
AppstoreOutlined,
|
||||
BarsOutlined,
|
||||
PlusOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { PlusOutlined } from "@ant-design/icons";
|
||||
import Search, { SearchProps } from "antd/es/input/Search";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import HeaderBar from "core/components/Header";
|
||||
|
@ -61,8 +57,8 @@ const LessonList: React.FC = () => {
|
|||
|
||||
if (!data || data.length === 0) return <MyEmpty />;
|
||||
|
||||
const publishedItems = data.filter(item => item.State === 1);
|
||||
const unpublishedItems = data.filter(item => item.State === 2);
|
||||
const publishedItems = data.filter((item) => item.State === 1);
|
||||
const unpublishedItems = data.filter((item) => item.State === 2);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -85,7 +81,9 @@ const LessonList: React.FC = () => {
|
|||
|
||||
{unpublishedItems.length > 0 && (
|
||||
<>
|
||||
<Divider orientation="left" style={{marginBottom: 0}}>Unpublished</Divider>
|
||||
<Divider orientation="left" style={{ marginBottom: 0 }}>
|
||||
Unpublished
|
||||
</Divider>
|
||||
|
||||
{unpublishedItems.map((item, index) => (
|
||||
<LessonPreviewCard
|
||||
|
@ -115,13 +113,6 @@ export default function Lessons() {
|
|||
|
||||
<MyContainer>
|
||||
<Flex justify="right" gap={16} style={{ paddingBottom: 16 }}>
|
||||
<Segmented
|
||||
options={[
|
||||
{ value: "List", icon: <BarsOutlined /> },
|
||||
{ value: "Kanban", icon: <AppstoreOutlined /> },
|
||||
]}
|
||||
/>
|
||||
|
||||
<CreateLessonButton />
|
||||
|
||||
<Search
|
||||
|
|
|
@ -1,17 +1,339 @@
|
|||
import { Checkbox, Collapse, Form } from "antd";
|
||||
import HeaderBar from "../../core/components/Header";
|
||||
import MyBanner from "../../shared/components/MyBanner";
|
||||
import { MyContainer } from "../../shared/components/MyContainer";
|
||||
import MyErrorResult from "shared/components/MyResult";
|
||||
import MyEmpty from "shared/components/MyEmpty";
|
||||
import MyCenteredSpin from "shared/components/MyCenteredSpin";
|
||||
import { Role } from "core/types/organization";
|
||||
import { useGetRolesQuery } from "core/services/organization";
|
||||
|
||||
export default function Roles() {
|
||||
const { data, error, isLoading } = useGetRolesQuery(undefined, {
|
||||
refetchOnMountOrArgChange: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<MyBanner title="Roles" subtitle="MANAGE" headerBar={
|
||||
<HeaderBar />
|
||||
} />
|
||||
<MyBanner title="Roles" subtitle="MANAGE" headerBar={<HeaderBar />} />
|
||||
|
||||
<MyContainer>
|
||||
<h1>Roles</h1>
|
||||
<MyContainer
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
{error ? (
|
||||
<MyErrorResult />
|
||||
) : isLoading ? (
|
||||
<MyCenteredSpin height="240px" />
|
||||
) : data === undefined ? (
|
||||
<MyEmpty />
|
||||
) : (
|
||||
<>
|
||||
{data.Roles.map((role, index) => (
|
||||
<RoleComponent key={index} role={role} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</MyContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface Permission {
|
||||
id: number;
|
||||
category: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
// test data
|
||||
const tmpI18nObj = [
|
||||
{
|
||||
id: 1,
|
||||
category: "Team",
|
||||
title: "Invite new team member",
|
||||
description:
|
||||
"Permission to invite a member. An email will be sent to the new member.",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
category: "Team",
|
||||
title: "Remove team member",
|
||||
description: "Permission to remove a member.",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
category: "Roles",
|
||||
title: "Create new role",
|
||||
description:
|
||||
"Permission to invite a member. An email will be sent to the new member.",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
category: "Roles",
|
||||
title: "Delete role",
|
||||
description:
|
||||
"Permission to invite a member. An email will be sent to the new member.",
|
||||
},
|
||||
];
|
||||
|
||||
export const tmpRoleNames = {
|
||||
"d0f0fa0d-3f3b-438b-a76f-7febeb8aab57": "Admin",
|
||||
"b7359e12-359e-423b-b39c-f0d4069adebc": "Moderator",
|
||||
"a1f084ad-d501-4015-b326-4c5c46fd1c5e": "User",
|
||||
} as any;
|
||||
|
||||
function RoleComponent({ role }: { role: Role }) {
|
||||
const teamPermissions = tmpI18nObj.filter(
|
||||
(permission) => permission.category === "Team"
|
||||
);
|
||||
const rolePermissions = tmpI18nObj.filter(
|
||||
(permission) => permission.category === "Roles"
|
||||
);
|
||||
|
||||
const MyCheckbox = ({ permission }: { permission: Permission }) => {
|
||||
return (
|
||||
<Form.Item extra={permission.description}>
|
||||
<Checkbox disabled checked={role.Permissions.includes(permission.id)}>
|
||||
{permission.title}
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<Collapse
|
||||
defaultActiveKey={["1"]}
|
||||
items={[
|
||||
{
|
||||
key: "1",
|
||||
label: tmpRoleNames[role.Id],
|
||||
children: (
|
||||
<Collapse
|
||||
defaultActiveKey={["1"]}
|
||||
ghost
|
||||
items={[
|
||||
{
|
||||
key: "1",
|
||||
label: "Team",
|
||||
children: (
|
||||
<>
|
||||
{teamPermissions.map((permission, index) => (
|
||||
<MyCheckbox key={index} permission={permission} />
|
||||
))}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "2",
|
||||
label: "Roles",
|
||||
children: (
|
||||
<>
|
||||
{rolePermissions.map((permission, index) => (
|
||||
<MyCheckbox key={index} permission={permission} />
|
||||
))}
|
||||
</>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
function RoleComponent({ role }: { role: Role }) {
|
||||
const [expandIconPosition, setExpandIconPosition] = useState<ExpandIconPosition>('start');
|
||||
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
const [form] = useForm();
|
||||
|
||||
const onPositionChange = (newExpandIconPosition: ExpandIconPosition) => {
|
||||
setExpandIconPosition(newExpandIconPosition);
|
||||
};
|
||||
|
||||
const onChange = (key: string | string[]) => {};
|
||||
|
||||
const teamPermissions = [
|
||||
{
|
||||
id: 1,
|
||||
label: 'Invite new team member',
|
||||
description: 'Permission to invite a member. An email will be sent to the new member.',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
label: 'Remove team member',
|
||||
description: 'Permission to invite a member. An email will be sent to the new member.',
|
||||
},
|
||||
];
|
||||
|
||||
const rolePermissions = [
|
||||
{
|
||||
id: 3,
|
||||
label: 'Invite new team member',
|
||||
description: 'Permission to invite a member. An email will be sent to the new member.',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
label: 'Invite new team member',
|
||||
description: 'Permission to invite a member. An email will be sent to the new member.',
|
||||
},
|
||||
];
|
||||
|
||||
const MyCheckbox = ({ name, label, description }: { name: number; label: string; description: string }) => {
|
||||
return (
|
||||
<FormItem name={name} valuePropName="checked" extra={description}>
|
||||
<Checkbox disabled={role.Master || (!role.Master && !editMode)}>{label}</Checkbox>
|
||||
</FormItem>
|
||||
);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
console.log(form.getFieldsValue());
|
||||
};
|
||||
|
||||
/*
|
||||
useEffect(() => {
|
||||
// set all checkboxes to true if role is master
|
||||
if (role.Master) {
|
||||
const obj = {} as any;
|
||||
const objPermissionValues = Object.values(Constants.PERMISSIONS);
|
||||
|
||||
for (let i = 1; i <= Object.keys(Constants.PERMISSIONS).length; i++) {
|
||||
obj[objPermissionValues[i - 1]] = true;
|
||||
}
|
||||
|
||||
form.setFieldsValue(obj);
|
||||
}
|
||||
}, []);
|
||||
*/
|
||||
/*
|
||||
|
||||
return (
|
||||
<Form form={form}>
|
||||
<Collapse
|
||||
defaultActiveKey={['1']}
|
||||
collapsible={editMode ? 'icon' : 'header'}
|
||||
onChange={onChange}
|
||||
expandIconPosition={expandIconPosition}
|
||||
items={[
|
||||
{
|
||||
key: '1',
|
||||
label: editMode ? <Input placeholder="Role name" value={'Admin'} size="small" /> : role.Name,
|
||||
children: (
|
||||
<>
|
||||
{role.Master && (
|
||||
<Typography.Text type="secondary" italic>
|
||||
Permissions for this role cannot be changed as it the master role.
|
||||
</Typography.Text>
|
||||
)}
|
||||
|
||||
<Collapse
|
||||
defaultActiveKey={['1']}
|
||||
ghost
|
||||
onChange={onChange}
|
||||
expandIconPosition={expandIconPosition}
|
||||
items={[
|
||||
{
|
||||
key: '1',
|
||||
label: 'Team',
|
||||
children: (
|
||||
<>
|
||||
{teamPermissions.map((permission) => (
|
||||
<MyCheckbox key={permission.id} name={permission.id} label={permission.label} description={permission.description} />
|
||||
))}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: 'Roles',
|
||||
children: (
|
||||
<>
|
||||
{rolePermissions.map((permission) => (
|
||||
<MyCheckbox key={permission.id} name={permission.id} label={permission.label} description={permission.description} />
|
||||
))}
|
||||
</>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
extra: (
|
||||
<Space style={{ paddingLeft: 12 }}>
|
||||
{!editMode ? (
|
||||
<>
|
||||
{role.Users.length > 0 && (
|
||||
<Avatar.Group
|
||||
size="small"
|
||||
max={{
|
||||
count: 2,
|
||||
}}
|
||||
>
|
||||
{role.Users.map((user) => (
|
||||
<Tooltip title={`${user.FirstName} ${user.LastName}`} placement="top">
|
||||
<Avatar style={{ backgroundColor: '#f56a00' }}>{user.FirstName[0]}</Avatar>
|
||||
</Tooltip>
|
||||
))}
|
||||
</Avatar.Group>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<DeleteOutlined />
|
||||
<SaveOutlined
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
setEditMode(false);
|
||||
|
||||
handleSave();
|
||||
}}
|
||||
/>
|
||||
<CloseOutlined
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
setEditMode(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
const CreateRoleButton: React.FC = () => {
|
||||
const [createRole, { isLoading }] = useCreateRoleMutation();
|
||||
|
||||
const handleCreateRole = async () => {
|
||||
try {
|
||||
createRole('New Role');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button icon={<PlusOutlined />} onClick={handleCreateRole} loading={isLoading}>
|
||||
Create role
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
*/
|
||||
|
|
|
@ -25,6 +25,7 @@ import {
|
|||
} from "core/reducers/appSlice";
|
||||
import MyMiddleCard from "shared/components/MyMiddleCard";
|
||||
import { OrganizationSettings } from "core/types/organization";
|
||||
import { useMessage } from "core/context/MessageContext";
|
||||
|
||||
type GeneralFieldType = {
|
||||
primaryColor: string | AggregationColor;
|
||||
|
@ -47,7 +48,7 @@ export default function Settings() {
|
|||
|
||||
<GeneralCard data={data} isLoading={isLoading} />
|
||||
|
||||
<MediaCard data={data} isLoading={isLoading} />
|
||||
<MediaCard isLoading={isLoading} />
|
||||
|
||||
<SubdomainCard data={data} isLoading={isLoading} />
|
||||
</>
|
||||
|
@ -59,6 +60,7 @@ function GeneralCard({
|
|||
isLoading,
|
||||
}: { data?: OrganizationSettings; isLoading?: boolean } = {}) {
|
||||
const [form] = useForm<GeneralFieldType>();
|
||||
const { success } = useMessage();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const debounceRef = useRef<null | NodeJS.Timeout>(null);
|
||||
|
@ -67,19 +69,21 @@ function GeneralCard({
|
|||
const [updateOrganizationSettings, { isLoading: isUpdateSettingsLoading }] =
|
||||
useUpdateOrganizationSettingsMutation();
|
||||
|
||||
const handleSave = (values: GeneralFieldType) => {
|
||||
const handleSave = async (values: GeneralFieldType) => {
|
||||
const hexColor =
|
||||
typeof values.primaryColor === "string"
|
||||
? values.primaryColor
|
||||
: values.primaryColor.toHexString().split("#")[1];
|
||||
|
||||
try {
|
||||
updateOrganizationSettings({
|
||||
await updateOrganizationSettings({
|
||||
primaryColor: hexColor,
|
||||
companyName: values.companyName,
|
||||
});
|
||||
}).unwrap();
|
||||
|
||||
currentPrimaryColor.current = hexColor;
|
||||
|
||||
success("Settings updated successfully!");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
@ -148,7 +152,7 @@ function GeneralCard({
|
|||
</Form.Item>
|
||||
|
||||
<Form.Item<GeneralFieldType> name="companyName" label="Company name">
|
||||
<Input defaultValue="Jannex" />
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
</MyMiddleCard>
|
||||
|
@ -157,9 +161,9 @@ function GeneralCard({
|
|||
}
|
||||
|
||||
function MediaCard({
|
||||
data,
|
||||
isLoading,
|
||||
}: { data?: OrganizationSettings; isLoading?: boolean } = {}) {
|
||||
const { success } = useMessage();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const appLogoUrl = useSelector(logoUrl);
|
||||
|
@ -174,6 +178,8 @@ function MediaCard({
|
|||
onChange={(info) => {
|
||||
if (info.file.status === "done" && info.file.response.Data) {
|
||||
dispatch(setLogoUrl(info.file.response.Data));
|
||||
|
||||
success("Logo updated successfully!");
|
||||
}
|
||||
}}
|
||||
imgCropProps={{
|
||||
|
@ -182,7 +188,9 @@ function MediaCard({
|
|||
}}
|
||||
>
|
||||
<img
|
||||
src={`${Constants.STATIC_CONTENT_ADDRESS}${appLogoUrl}`}
|
||||
src={`${Constants.STATIC_CONTENT_ADDRESS}${
|
||||
appLogoUrl || Constants.DEMO_LOGO_URL
|
||||
}`}
|
||||
alt="Company Logo"
|
||||
style={{
|
||||
width: 128,
|
||||
|
@ -199,8 +207,13 @@ function MediaCard({
|
|||
<MyUpload
|
||||
action="/organization/file/banner"
|
||||
onChange={(info) => {
|
||||
console.log("Banner updated1!", info.file.status);
|
||||
if (info.file.status === "done" && info.file.response.Data) {
|
||||
dispatch(setBannerUrl(info.file.response.Data));
|
||||
|
||||
console.log("Banner updated!");
|
||||
|
||||
success("Banner updated successfully!");
|
||||
}
|
||||
}}
|
||||
imgCropProps={{
|
||||
|
@ -209,7 +222,9 @@ function MediaCard({
|
|||
}}
|
||||
>
|
||||
<img
|
||||
src={`${Constants.STATIC_CONTENT_ADDRESS}${appBannerUrl}`}
|
||||
src={`${Constants.STATIC_CONTENT_ADDRESS}${
|
||||
appBannerUrl || Constants.DEMO_BANNER_URL
|
||||
}`}
|
||||
alt="Banner"
|
||||
style={{
|
||||
width: "100%",
|
||||
|
@ -234,6 +249,7 @@ function SubdomainCard({
|
|||
isLoading,
|
||||
}: { data?: OrganizationSettings; isLoading?: boolean } = {}) {
|
||||
const [form] = useForm();
|
||||
const { success, info } = useMessage();
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [reqIsSubdomainAvailable] = useIsSubdomainAvailableMutation();
|
||||
|
@ -287,13 +303,16 @@ function SubdomainCard({
|
|||
centered
|
||||
onCancel={() => setIsModalOpen(false)}
|
||||
okText="Change"
|
||||
onOk={() => {
|
||||
onOk={async () => {
|
||||
try {
|
||||
reqUpdateSubdomain(form.getFieldValue("subdomain"));
|
||||
await reqUpdateSubdomain(form.getFieldValue("subdomain"));
|
||||
|
||||
success("Subdomain updated successfully!");
|
||||
info("You will be redirected to the new subdomain!");
|
||||
/*
|
||||
window.location.href = `https://${form.getFieldValue(
|
||||
"subdomain"
|
||||
)}.${window.location.hostname.split(".").slice(1).join(".")}`;
|
||||
)}.${window.location.hostname.split(".").slice(1).join(".")}`; */
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import { PlusOutlined } from "@ant-design/icons";
|
||||
import { Button, Card, Checkbox, Flex, Form, Input } from "antd";
|
||||
import { Button, Flex, Form, Input, Select } from "antd";
|
||||
import HeaderBar from "core/components/Header";
|
||||
import { Constants } from "core/utils/utils";
|
||||
import { useMessage } from "core/context/MessageContext";
|
||||
import { useCreateTeamMemberMutation } from "core/services/organization";
|
||||
import { Constants, EncodeStringToBase64 } from "core/utils/utils";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import MyMiddleCard from "shared/components/MyMiddleCard";
|
||||
|
||||
type FieldType = {
|
||||
|
@ -14,6 +17,11 @@ type FieldType = {
|
|||
};
|
||||
|
||||
export default function TeamCreateUser() {
|
||||
const navigate = useNavigate();
|
||||
const { success } = useMessage();
|
||||
|
||||
const [reqCreateTeamMember, { isLoading }] = useCreateTeamMemberMutation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeaderBar
|
||||
|
@ -22,21 +30,72 @@ export default function TeamCreateUser() {
|
|||
/>
|
||||
|
||||
<MyMiddleCard title="Create User">
|
||||
<Form layout="vertical" requiredMark={false}>
|
||||
<Form
|
||||
layout="vertical"
|
||||
requiredMark={false}
|
||||
initialValues={{
|
||||
roleId: "a1f084ad-d501-4015-b326-4c5c46fd1c5e",
|
||||
}}
|
||||
onFinish={async (values) => {
|
||||
console.log(values);
|
||||
|
||||
try {
|
||||
await reqCreateTeamMember({
|
||||
firstName: values.firstName,
|
||||
lastName: values.lastName,
|
||||
email: values.email,
|
||||
roleId: values.roleId,
|
||||
password: EncodeStringToBase64(values.password),
|
||||
}).unwrap();
|
||||
|
||||
success("User created successfully!");
|
||||
|
||||
navigate(Constants.ROUTE_PATHS.ORGANIZATION_TEAM);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Form.Item<FieldType>
|
||||
label="First Name"
|
||||
name="firstName"
|
||||
rules={[{ required: true, message: "Please input first name!" }]}
|
||||
rules={[
|
||||
{ required: true, message: "Please input first name!" },
|
||||
{
|
||||
min: Constants.GLOBALS.MIN_FIRST_NAME_LENGTH,
|
||||
message: `First name must be at least ${Constants.GLOBALS.MIN_FIRST_NAME_LENGTH} characters long!`,
|
||||
},
|
||||
{
|
||||
max: Constants.GLOBALS.MAX_FIRST_NAME_LENGTH,
|
||||
message: `First name must be at most ${Constants.GLOBALS.MAX_FIRST_NAME_LENGTH} characters long!`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="First Name" />
|
||||
<Input
|
||||
placeholder="First Name"
|
||||
maxLength={Constants.GLOBALS.MAX_FIRST_NAME_LENGTH}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<FieldType>
|
||||
label="Last Name"
|
||||
name="lastName"
|
||||
rules={[{ required: true, message: "Please input last name!" }]}
|
||||
rules={[
|
||||
{ required: true, message: "Please input last name!" },
|
||||
{
|
||||
min: Constants.GLOBALS.MIN_LAST_NAME_LENGTH,
|
||||
message: `Last name must be at least ${Constants.GLOBALS.MIN_LAST_NAME_LENGTH} characters long!`,
|
||||
},
|
||||
{
|
||||
max: Constants.GLOBALS.MAX_LAST_NAME_LENGTH,
|
||||
message: `Last name must be at most ${Constants.GLOBALS.MAX_LAST_NAME_LENGTH} characters long!`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="Last Name" />
|
||||
<Input
|
||||
placeholder="Last Name"
|
||||
maxLength={Constants.GLOBALS.MAX_LAST_NAME_LENGTH}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<FieldType>
|
||||
|
@ -49,30 +108,48 @@ export default function TeamCreateUser() {
|
|||
<Input placeholder="Email" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<FieldType>
|
||||
name="changePasswordOnFirstLogin"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Checkbox>Change Password on First Login</Checkbox>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<FieldType>
|
||||
label="Password"
|
||||
name="password"
|
||||
rules={[{ required: true, message: "Please input password!" }]}
|
||||
rules={[
|
||||
{ required: true, message: "Please input password!" },
|
||||
{
|
||||
min: Constants.GLOBALS.MIN_PASSWORD_LENGTH,
|
||||
message: `Password must be at least ${Constants.GLOBALS.MIN_PASSWORD_LENGTH} characters long!`,
|
||||
},
|
||||
{
|
||||
max: Constants.GLOBALS.MAX_PASSWORD_LENGTH,
|
||||
message: `Password must be at most ${Constants.GLOBALS.MAX_PASSWORD_LENGTH} characters long!`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input.Password placeholder="Password" />
|
||||
<Input.Password
|
||||
placeholder="Password"
|
||||
maxLength={Constants.GLOBALS.MAX_PASSWORD_LENGTH}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<FieldType>
|
||||
name="sendInvitationEmail"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Checkbox>Send an invitation email to the user</Checkbox>
|
||||
<Form.Item name="roleId" label="Role">
|
||||
<Select>
|
||||
<Select.Option value="d0f0fa0d-3f3b-438b-a76f-7febeb8aab57">
|
||||
Admin
|
||||
</Select.Option>
|
||||
<Select.Option value="b7359e12-359e-423b-b39c-f0d4069adebc">
|
||||
Moderator
|
||||
</Select.Option>
|
||||
<Select.Option value="a1f084ad-d501-4015-b326-4c5c46fd1c5e">
|
||||
User
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Flex justify="end">
|
||||
<Button type="primary" icon={<PlusOutlined />}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
htmlType="submit"
|
||||
loading={isLoading}
|
||||
>
|
||||
Create User
|
||||
</Button>
|
||||
</Flex>
|
||||
|
@ -81,3 +158,12 @@ export default function TeamCreateUser() {
|
|||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
<Form.Item<FieldType>
|
||||
name="sendInvitationEmail"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Checkbox>Send an invitation email to the user</Checkbox>
|
||||
</Form.Item>
|
||||
*/
|
||||
|
|
|
@ -1,100 +1,120 @@
|
|||
import MyTable from 'shared/components/MyTable';
|
||||
import HeaderBar from '../../core/components/Header';
|
||||
import MyBanner from '../../shared/components/MyBanner';
|
||||
import { MyContainer } from '../../shared/components/MyContainer';
|
||||
import { Button, Flex } from 'antd';
|
||||
import { UserAddOutlined } from '@ant-design/icons';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Constants } from 'core/utils/utils';
|
||||
import { useGetTeamQuery } from 'core/services/organization';
|
||||
import MyErrorResult from 'shared/components/MyResult';
|
||||
import MyTable from "shared/components/MyTable";
|
||||
import HeaderBar from "core/components/Header";
|
||||
import MyBanner from "shared/components/MyBanner";
|
||||
import { MyContainer } from "shared/components/MyContainer";
|
||||
import { Button, Flex } from "antd";
|
||||
import { UserAddOutlined } from "@ant-design/icons";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Constants } from "core/utils/utils";
|
||||
import { useGetTeamQuery } from "core/services/organization";
|
||||
import MyErrorResult from "shared/components/MyResult";
|
||||
import { useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { setTeamMembers, teamMembers } from "./teamSlice";
|
||||
import { tmpRoleNames } from "features/Roles";
|
||||
|
||||
const TeamList: React.FC = () => {
|
||||
const { data, error, isLoading } = useGetTeamQuery(undefined, {
|
||||
refetchOnMountOrArgChange: true,
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const dataTeamMembers = useSelector(teamMembers);
|
||||
|
||||
const { data, error, isLoading } = useGetTeamQuery(undefined, {
|
||||
refetchOnMountOrArgChange: true,
|
||||
});
|
||||
|
||||
const getTableContent = () => {
|
||||
let items = [
|
||||
{
|
||||
title: "First name",
|
||||
dataIndex: "firstName",
|
||||
key: "firstName",
|
||||
},
|
||||
{
|
||||
title: "Last name",
|
||||
dataIndex: "lastName",
|
||||
key: "lastName",
|
||||
},
|
||||
{
|
||||
title: "Email",
|
||||
dataIndex: "email",
|
||||
key: "email",
|
||||
},
|
||||
{
|
||||
title: "Role",
|
||||
dataIndex: "role",
|
||||
key: "role",
|
||||
},
|
||||
{
|
||||
title: "Status",
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
},
|
||||
{
|
||||
title: "Actions",
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
},
|
||||
];
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
const getTableItems = () => {
|
||||
let items = [] as any[];
|
||||
|
||||
if (!dataTeamMembers) return items;
|
||||
|
||||
dataTeamMembers.forEach((item) => {
|
||||
items.push({
|
||||
key: item.Id,
|
||||
firstName: item.FirstName,
|
||||
lastName: item.LastName,
|
||||
email: item.Email,
|
||||
role: tmpRoleNames[item.RoleId],
|
||||
status: item.Status,
|
||||
});
|
||||
});
|
||||
|
||||
const getTableContent = () => {
|
||||
let items = [
|
||||
{
|
||||
title: 'First name',
|
||||
dataIndex: 'firstName',
|
||||
key: 'firstName',
|
||||
},
|
||||
{
|
||||
title: 'Last name',
|
||||
dataIndex: 'lastName',
|
||||
key: 'lastName',
|
||||
},
|
||||
{
|
||||
title: 'Email',
|
||||
dataIndex: 'email',
|
||||
key: 'email',
|
||||
},
|
||||
{
|
||||
title: 'Role',
|
||||
dataIndex: 'role',
|
||||
key: 'role',
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
},
|
||||
{
|
||||
title: 'Actions',
|
||||
dataIndex: 'actions',
|
||||
key: 'actions',
|
||||
},
|
||||
];
|
||||
return items;
|
||||
};
|
||||
|
||||
return items;
|
||||
};
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
|
||||
const getTableItems = () => {
|
||||
let items = [] as any[];
|
||||
dispatch(setTeamMembers(data));
|
||||
}, [data]);
|
||||
|
||||
if (!data) return items;
|
||||
if (error) return <MyErrorResult />;
|
||||
|
||||
data.forEach((item) => {
|
||||
items.push({
|
||||
key: item.Id,
|
||||
firstName: item.FirstName,
|
||||
lastName: item.LastName,
|
||||
email: item.Email,
|
||||
role: item.Role,
|
||||
status: item.Status,
|
||||
});
|
||||
});
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
if (error) return <MyErrorResult />;
|
||||
|
||||
return <MyTable loading={isLoading} columns={getTableContent()} dataSource={getTableItems()} pagination={false} />;
|
||||
return (
|
||||
<MyTable
|
||||
loading={isLoading}
|
||||
columns={getTableContent()}
|
||||
dataSource={getTableItems()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default function Team() {
|
||||
return (
|
||||
<>
|
||||
<MyBanner title="Team" subtitle="MANAGE" headerBar={<HeaderBar />} />
|
||||
return (
|
||||
<>
|
||||
<MyBanner title="Team" subtitle="MANAGE" headerBar={<HeaderBar />} />
|
||||
|
||||
<MyContainer
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
<Flex justify="end">
|
||||
<Link to={Constants.ROUTE_PATHS.ORGANIZATION_TEAM_CREATE_USER}>
|
||||
<Button icon={<UserAddOutlined />}>Invite new member</Button>
|
||||
</Link>
|
||||
</Flex>
|
||||
<MyContainer
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
<Flex justify="end">
|
||||
<Link to={Constants.ROUTE_PATHS.ORGANIZATION_TEAM_CREATE_USER}>
|
||||
<Button icon={<UserAddOutlined />}>Invite new member</Button>
|
||||
</Link>
|
||||
</Flex>
|
||||
|
||||
<TeamList />
|
||||
</MyContainer>
|
||||
</>
|
||||
);
|
||||
<TeamList />
|
||||
</MyContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import { TeamMember } from "core/types/organization";
|
||||
|
||||
interface AddTeamMemberAction {
|
||||
type: string;
|
||||
payload: TeamMember;
|
||||
}
|
||||
|
||||
export const teamSlice = createSlice({
|
||||
name: "team",
|
||||
initialState: {
|
||||
teamMembers: [] as TeamMember[],
|
||||
},
|
||||
reducers: {
|
||||
addTeamMember: (state, action: AddTeamMemberAction) => {
|
||||
state.teamMembers.push(action.payload);
|
||||
},
|
||||
setTeamMembers: (state, action) => {
|
||||
state.teamMembers = action.payload;
|
||||
},
|
||||
},
|
||||
selectors: {
|
||||
teamMembers: (state) => state.teamMembers,
|
||||
},
|
||||
});
|
||||
|
||||
export const { addTeamMember, setTeamMembers } = teamSlice.actions;
|
||||
|
||||
export const { teamMembers } = teamSlice.selectors;
|
|
@ -1,7 +1,7 @@
|
|||
export default function WhatsNew() {
|
||||
return (
|
||||
<>
|
||||
<h1>WhatsNew</h1>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<h1>WhatsNew</h1>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ export default function MyBanner({
|
|||
>
|
||||
<img
|
||||
src={`${Constants.STATIC_CONTENT_ADDRESS}${
|
||||
appBannerUrl || "/demo/organization_banner.jpeg"
|
||||
appBannerUrl || Constants.DEMO_BANNER_URL
|
||||
}`}
|
||||
alt="banner"
|
||||
style={{
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import { MyCenteredContainer } from "../MyContainer";
|
||||
import { MyCenteredContainer, MyCenteredContainerProps } from "../MyContainer";
|
||||
import MySpin from "../MySpin";
|
||||
|
||||
export default function MyCenteredSpin({ fullHeight = false }) {
|
||||
const MyCenteredSpin: React.FC<MyCenteredContainerProps> = (props) => {
|
||||
return (
|
||||
<MyCenteredContainer fullHeight>
|
||||
<MyCenteredContainer {...props}>
|
||||
<MySpin />
|
||||
</MyCenteredContainer>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default MyCenteredSpin;
|
||||
|
|
|
@ -10,15 +10,15 @@ export function MyContainer({
|
|||
return <Content style={{ padding: 12, ...style }}>{children}</Content>;
|
||||
}
|
||||
|
||||
export interface MyCenteredContainerProps {
|
||||
children?: React.ReactNode;
|
||||
height?: string;
|
||||
}
|
||||
|
||||
export function MyCenteredContainer({
|
||||
children,
|
||||
fullHeight = false,
|
||||
height = "100vh",
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
fullHeight?: boolean;
|
||||
height?: string;
|
||||
}) {
|
||||
height,
|
||||
}: MyCenteredContainerProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
|
@ -27,7 +27,7 @@ export function MyCenteredContainer({
|
|||
justifyContent: "center",
|
||||
alignContent: "center",
|
||||
alignItems: "center",
|
||||
height: fullHeight ? height : "85.3vh",
|
||||
height: height || "100vh",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { LoadingOutlined } from "@ant-design/icons";
|
||||
import { Spin } from "antd";
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Spin } from 'antd';
|
||||
|
||||
export default function MySpin() {
|
||||
return <Spin size="large" indicator={<LoadingOutlined spin />} />;
|
||||
return <Spin size="large" indicator={<LoadingOutlined spin />} />;
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ export function MySupsenseFallback({
|
|||
<Suspense
|
||||
fallback={
|
||||
spinnerCentered ? (
|
||||
<MyCenteredSpin fullHeight />
|
||||
<MyCenteredSpin height="100vh" />
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
|
|
|
@ -2,8 +2,6 @@ import ImgCrop, { ImgCropProps } from "antd-img-crop";
|
|||
import Upload from "antd/es/upload/Upload";
|
||||
import { getApiHeader } from "core/helper/api";
|
||||
import { Constants } from "core/utils/utils";
|
||||
import { Fragment, useState } from "react";
|
||||
import MySpin from "../MySpin";
|
||||
|
||||
export default function MyUpload({
|
||||
children,
|
||||
|
@ -26,8 +24,6 @@ export default function MyUpload({
|
|||
onChange?: (info: any) => void;
|
||||
fileType?: "image" | "video";
|
||||
}) {
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
const beforeUpload = (file: File) => {
|
||||
if (!accept.includes(file.type)) {
|
||||
console.error("File typ not allowed!");
|
||||
|
@ -54,17 +50,7 @@ export default function MyUpload({
|
|||
headers={headers}
|
||||
action={`${Constants.API_ADDRESS}${action}`}
|
||||
onChange={(info) => {
|
||||
if (onChange) {
|
||||
console.log("call");
|
||||
onChange(info);
|
||||
}
|
||||
|
||||
if (info.file.status === "uploading") {
|
||||
setUploading(true);
|
||||
} else if (info.file.status === "done") {
|
||||
console.log("done2");
|
||||
setUploading(false);
|
||||
}
|
||||
if (onChange) onChange(info);
|
||||
}}
|
||||
beforeUpload={beforeUpload}
|
||||
>
|
||||
|
|
|
@ -1,21 +1,21 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src",
|
||||
"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"]
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src",
|
||||
"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": false,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue