main
alex 2024-08-31 10:28:01 +02:00
parent 5b19897740
commit 12467edda7
18 changed files with 432 additions and 75 deletions

7
commit_and_push.sh Executable file
View File

@ -0,0 +1,7 @@
git add *
read -p "Commit message: " commit_message
git commit -m "$commit_message"
git push -u origin main

65
package-lock.json generated
View File

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

View File

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

View File

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

View File

@ -1,4 +1,3 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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