auth
parent
5b19897740
commit
12467edda7
|
@ -0,0 +1,7 @@
|
|||
git add *
|
||||
|
||||
read -p "Commit message: " commit_message
|
||||
|
||||
git commit -m "$commit_message"
|
||||
|
||||
git push -u origin main
|
|
@ -19,6 +19,7 @@
|
|||
"@types/react": "^18.3.4",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"antd": "^5.20.3",
|
||||
"buffer": "^6.0.3",
|
||||
"i18next": "^23.7.6",
|
||||
"i18next-browser-languagedetector": "^7.2.0",
|
||||
"react": "^18.3.1",
|
||||
|
@ -6293,6 +6294,26 @@
|
|||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/batch": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
|
||||
|
@ -6487,6 +6508,30 @@
|
|||
"node-int64": "^0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
|
@ -10403,6 +10448,26 @@
|
|||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
"@types/react": "^18.3.4",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"antd": "^5.20.3",
|
||||
"buffer": "^6.0.3",
|
||||
"i18next": "^23.7.6",
|
||||
"i18next-browser-languagedetector": "^7.2.0",
|
||||
"react": "^18.3.1",
|
||||
|
|
38
src/App.css
38
src/App.css
|
@ -1,38 +0,0 @@
|
|||
.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);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
|
|
60
src/App.tsx
60
src/App.tsx
|
@ -1,29 +1,65 @@
|
|||
import React from "react";
|
||||
import logo from "./logo.svg";
|
||||
import "./App.css";
|
||||
import { Button, ConfigProvider, Layout, theme } from "antd";
|
||||
import { ConfigProvider, Layout, theme } from "antd";
|
||||
import DashboardLayout from "./core/components/DashboardLayout";
|
||||
import { darkMode } from "./core/store/appSlice";
|
||||
import { useSelector } from "react-redux";
|
||||
import {
|
||||
darkMode,
|
||||
setUserAuthenticated,
|
||||
userAuthenticated,
|
||||
} from "./core/store/appSlice";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import SignIn from "./features/Auth/SignIn";
|
||||
import { useEffect } from "react";
|
||||
import { myFetch } from "./core/utils/utils";
|
||||
import MyCenteredSpin from "./shared/components/MyCenteredSpin";
|
||||
|
||||
const { defaultAlgorithm, darkAlgorithm } = theme;
|
||||
|
||||
function App() {
|
||||
const isDarkMode = useSelector(darkMode)
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const isDarkMode = useSelector(darkMode);
|
||||
const uAuthenticated = useSelector(userAuthenticated);
|
||||
|
||||
console.info(
|
||||
"\n %c LMS %c v0.1.0 %c \n",
|
||||
"\n %c LMS %c v1.0.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"
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!localStorage.getItem("session")) {
|
||||
dispatch(setUserAuthenticated(false));
|
||||
return;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const response = await myFetch({
|
||||
url: "/user",
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
if (response) {
|
||||
dispatch(setUserAuthenticated(true))
|
||||
}
|
||||
} catch (error) {}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Layout style={{ minHeight: "100vh" }}>
|
||||
<ConfigProvider theme={{
|
||||
algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm,
|
||||
}}>
|
||||
<DashboardLayout />
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm,
|
||||
}}
|
||||
>
|
||||
{uAuthenticated == null ? (
|
||||
<MyCenteredSpin />
|
||||
) : uAuthenticated ? (
|
||||
<DashboardLayout />
|
||||
) : (
|
||||
<SignIn />
|
||||
)}
|
||||
</ConfigProvider>
|
||||
</Layout>
|
||||
);
|
||||
|
|
|
@ -14,7 +14,6 @@ import {
|
|||
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";
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
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";
|
||||
|
@ -18,8 +16,6 @@ export default function PageContent() {
|
|||
: 200,
|
||||
}}
|
||||
>
|
||||
|
||||
|
||||
<AppRoutes />
|
||||
</Layout>
|
||||
);
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import {
|
||||
ControlOutlined,
|
||||
GroupOutlined,
|
||||
MessageOutlined,
|
||||
PieChartOutlined,
|
||||
QuestionCircleOutlined,
|
||||
SettingOutlined,
|
||||
SnippetsOutlined,
|
||||
|
@ -12,7 +10,6 @@ import {
|
|||
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,
|
||||
|
@ -21,7 +18,6 @@ import {
|
|||
} 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";
|
||||
|
@ -30,7 +26,6 @@ export function SideMenuContent() {
|
|||
const location = useLocation();
|
||||
const [selectedKeys, setSelectedKeys] = useState("/");
|
||||
const [openKeys, setOpenKeys] = useState([""]);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const componentFirstRender = useSelector(sideMenuComponentFirstRender);
|
||||
|
|
|
@ -4,17 +4,22 @@ export const appSlice = createSlice({
|
|||
name: "app",
|
||||
initialState: {
|
||||
darkMode: false,
|
||||
userAuthenticated: null,
|
||||
},
|
||||
reducers: {
|
||||
setDarkMode: (state, action) => {
|
||||
state.darkMode = action.payload;
|
||||
},
|
||||
setUserAuthenticated: (state, action) => {
|
||||
state.userAuthenticated = action.payload;
|
||||
}
|
||||
},
|
||||
selectors: {
|
||||
darkMode: (state) => state.darkMode,
|
||||
userAuthenticated: (state) => state.userAuthenticated,
|
||||
},
|
||||
})
|
||||
|
||||
export const { setDarkMode } = appSlice.actions;
|
||||
export const { setDarkMode, setUserAuthenticated } = appSlice.actions;
|
||||
|
||||
export const { darkMode } = appSlice.selectors;
|
||||
export const { darkMode, userAuthenticated } = appSlice.selectors;
|
|
@ -1,4 +1,7 @@
|
|||
import { Buffer } from "buffer";
|
||||
|
||||
export const Constants = {
|
||||
API_ADDRESS: `${window.location.protocol}//${window.location.hostname}/api/v1`,
|
||||
ROUTE_PATHS: {
|
||||
LESSIONS: {
|
||||
ROOT: "/lessons",
|
||||
|
@ -12,7 +15,150 @@ export const Constants = {
|
|||
SUGGEST_FEATURE: "/suggest-feature",
|
||||
CONTACT_SUPPORT: "/contact-support",
|
||||
},
|
||||
STYLES: {
|
||||
BLACK: "#000",
|
||||
},
|
||||
};
|
||||
|
||||
// used for sideMenu
|
||||
export const BreakpointLgWidth = 992;
|
||||
export const BreakpointLgWidth = 992;
|
||||
|
||||
export function getUserSessionFromLocalStorage() {
|
||||
return localStorage.getItem("session");
|
||||
}
|
||||
|
||||
export function EncodeStringToBase64(value: string) {
|
||||
return Buffer.from(value).toString("base64");
|
||||
}
|
||||
|
||||
export function DecodedBase64ToString(value: string) {
|
||||
return Buffer.from(value, "base64").toString();
|
||||
}
|
||||
|
||||
export function handleLogout() {
|
||||
localStorage.removeItem("session");
|
||||
window.location.href = "/";
|
||||
}
|
||||
|
||||
export const myFetchContentType = {
|
||||
JSON: 0,
|
||||
FORM_DATA: 1,
|
||||
};
|
||||
|
||||
|
||||
type Method = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
||||
|
||||
interface MyFetchOptions<TRequest = any, TResponse = any> {
|
||||
url?: string;
|
||||
method?: Method;
|
||||
body?: TRequest | null;
|
||||
headers?: Record<string, string>;
|
||||
contentType?: "JSON" | "FORM_DATA";
|
||||
fetchUrl?: string;
|
||||
ignoreUnauthorized?: boolean;
|
||||
notificationApi?: any; // Passen Sie dies je nach Bedarf an
|
||||
t?: any; // Passen Sie dies je nach Bedarf an
|
||||
}
|
||||
|
||||
export function myFetch<TRequest = any, TResponse = any>({
|
||||
url = "",
|
||||
method = "GET",
|
||||
body = null,
|
||||
headers = {},
|
||||
contentType = "JSON",
|
||||
fetchUrl = Constants.API_ADDRESS,
|
||||
ignoreUnauthorized = false,
|
||||
notificationApi = null,
|
||||
t = null,
|
||||
}: MyFetchOptions<TRequest, TResponse>): Promise<TResponse> {
|
||||
const getContentType = () => {
|
||||
if (contentType === "JSON") return "application/json";
|
||||
|
||||
return "multipart/form-data";
|
||||
};
|
||||
|
||||
const getBody = () => {
|
||||
if (!body) return null;
|
||||
|
||||
if (contentType === "JSON") return JSON.stringify(body);
|
||||
|
||||
return body;
|
||||
};
|
||||
|
||||
// abort fetch if it takes to long
|
||||
const controller = new AbortController();
|
||||
const signal = controller.signal;
|
||||
|
||||
setTimeout(() => controller.abort(), 30000); // 30 seconds
|
||||
|
||||
const requestOptions = {
|
||||
method: method,
|
||||
headers: {
|
||||
"X-Authorization": getUserSessionFromLocalStorage() || "",
|
||||
"Content-Type": getContentType(),
|
||||
...headers,
|
||||
},
|
||||
body: getBody(),
|
||||
signal: signal,
|
||||
};
|
||||
|
||||
if (fetchUrl === "") {
|
||||
fetchUrl = Constants.API_ADDRESS;
|
||||
}
|
||||
|
||||
return fetch(`${fetchUrl}${url}`, requestOptions as RequestInit)
|
||||
.then(async (response) => {
|
||||
// if status is not in range 200-299
|
||||
if (!response.ok) {
|
||||
if (!ignoreUnauthorized && response.status === 401) {
|
||||
console.error("Unauthorized");
|
||||
// TODO: check here
|
||||
//setUserSessionToLocalStorage("");
|
||||
//window.location.href = "/";
|
||||
}
|
||||
|
||||
throw response.status;
|
||||
}
|
||||
|
||||
// check if response is json
|
||||
if (response.headers.get("content-type")?.includes("application/json")) {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
return response.text();
|
||||
})
|
||||
.catch(async (error) => {
|
||||
console.error("Error", error);
|
||||
|
||||
// ignore errors here as they are handled in the components
|
||||
if (error === 400) throw error;
|
||||
|
||||
if (error === 401) {
|
||||
if (ignoreUnauthorized) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
handleLogout();
|
||||
return;
|
||||
}
|
||||
|
||||
// show error notification for all other errors
|
||||
|
||||
if (notificationApi !== null && t !== null) {
|
||||
if (error === 500) {
|
||||
/* notificationApi["error"]({
|
||||
message: t("common.request.failed.title"),
|
||||
description: t("common.request.failed.description"),
|
||||
}); */
|
||||
} else {
|
||||
// other errors
|
||||
/* notificationApi["error"]({
|
||||
message: t("common.request.failedInternetProblem.title"),
|
||||
description: t("common.request.failedInternetProblem.description"),
|
||||
}); */
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
import { Button, Card, ConfigProvider, Flex, Form, Input } from "antd";
|
||||
import { useForm } from "antd/es/form/Form";
|
||||
import styles from "./styles.module.css";
|
||||
import {
|
||||
Constants,
|
||||
EncodeStringToBase64,
|
||||
myFetch,
|
||||
} from "../../../core/utils/utils";
|
||||
import { useState } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { setUserAuthenticated } from "../../../core/store/appSlice";
|
||||
|
||||
type FieldType = {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
interface SignInFetchResponse {
|
||||
Session: string;
|
||||
}
|
||||
|
||||
export default function SignIn() {
|
||||
const [form] = useForm();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [isRequesting, setIsRequesting] = useState(false);
|
||||
|
||||
const handleSignIn = async (values: FieldType) => {
|
||||
setIsRequesting(true);
|
||||
|
||||
try {
|
||||
const response = await myFetch<FieldType, SignInFetchResponse>({
|
||||
url: "/user/auth/login",
|
||||
method: "POST",
|
||||
body: {
|
||||
email: values.email,
|
||||
password: EncodeStringToBase64(values.password),
|
||||
},
|
||||
ignoreUnauthorized: true,
|
||||
});
|
||||
|
||||
setIsRequesting(false);
|
||||
|
||||
localStorage.setItem("session", response.Session);
|
||||
|
||||
dispatch(setUserAuthenticated(true));
|
||||
} catch (error) {
|
||||
console.log("here", error);
|
||||
setIsRequesting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.backgroundImage} />
|
||||
<div className={styles.darkenBackground} />
|
||||
|
||||
<Flex
|
||||
justify="center"
|
||||
align="center"
|
||||
style={{ position: "absolute", height: "100vh", width: "100%" }}
|
||||
>
|
||||
<Card style={{ width: 450, margin: 20 }}>
|
||||
<h1 style={{ marginTop: 0 }}>Sign In</h1>
|
||||
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: {
|
||||
colorPrimary: Constants.STYLES.BLACK,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
requiredMark={false}
|
||||
onFinish={handleSignIn}
|
||||
>
|
||||
<Form.Item<FieldType>
|
||||
label="Email"
|
||||
name="email"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
type: "email",
|
||||
message: "Please input your email!",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="Email" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<FieldType>
|
||||
label="Password"
|
||||
name="password"
|
||||
rules={[
|
||||
{ required: true, message: "Please input your password!" },
|
||||
]}
|
||||
>
|
||||
<Input.Password placeholder="Password" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item style={{ marginBottom: 0 }}>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
block
|
||||
loading={isRequesting}
|
||||
>
|
||||
Sign In
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</ConfigProvider>
|
||||
</Card>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 76 KiB |
|
@ -0,0 +1,18 @@
|
|||
.backgroundImage {
|
||||
position: absolute;
|
||||
background-image: url("pexels-photo-380769.webp");
|
||||
filter: blur(8px);
|
||||
-webkit-filter: blur(8px);
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.darkenBackground {
|
||||
position: absolute;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
|
@ -3,8 +3,6 @@ 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";
|
||||
|
@ -103,7 +101,7 @@ export function Converter({
|
|||
{lessonContent.data}
|
||||
</div>
|
||||
);
|
||||
case 3:
|
||||
case 4:
|
||||
return <div style={{ fontSize: 14 }}>{lessonContent.data}</div>;
|
||||
default:
|
||||
return <div>Unknown type</div>;
|
||||
|
|
|
@ -1,21 +1,25 @@
|
|||
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 { Suspense } from "react";
|
||||
import MyCenteredSpin from "./shared/components/MyCenteredSpin";
|
||||
// import reportWebVitals from './reportWebVitals';
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById("root") as HTMLElement
|
||||
);
|
||||
|
||||
root.render(
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
<Suspense fallback={<MyCenteredSpin />}>
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { Spin } from "antd";
|
||||
import { MyCenteredContainer } from "../MyContainer";
|
||||
import MySpin from "../MySpin";
|
||||
|
||||
export default function MyCenteredSpin({ fullHeight = false }) {
|
||||
return (
|
||||
<MyCenteredContainer fullHeight>
|
||||
<Spin size="large" />
|
||||
<MySpin />
|
||||
</MyCenteredContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
import { LoadingOutlined } from "@ant-design/icons";
|
||||
import { Spin } from "antd";
|
||||
|
||||
export default function MySpin() {
|
||||
return <Spin size="large" indicator={<LoadingOutlined spin />} />;
|
||||
}
|
Loading…
Reference in New Issue