diff --git a/commit_and_push.sh b/commit_and_push.sh
new file mode 100755
index 0000000..554786f
--- /dev/null
+++ b/commit_and_push.sh
@@ -0,0 +1,7 @@
+git add *
+
+read -p "Commit message: " commit_message
+
+git commit -m "$commit_message"
+
+git push -u origin main
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index ea43cbc..5baac9c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 38a26c2..7661372 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/App.css b/src/App.css
deleted file mode 100644
index 74b5e05..0000000
--- a/src/App.css
+++ /dev/null
@@ -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);
- }
-}
diff --git a/src/App.test.tsx b/src/App.test.tsx
index 2a68616..1f03afe 100644
--- a/src/App.test.tsx
+++ b/src/App.test.tsx
@@ -1,4 +1,3 @@
-import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
diff --git a/src/App.tsx b/src/App.tsx
index 4699d5e..8d01df5 100644
--- a/src/App.tsx
+++ b/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 (
-
-
+
+ {uAuthenticated == null ? (
+
+ ) : uAuthenticated ? (
+
+ ) : (
+
+ )}
);
diff --git a/src/core/components/Header/index.tsx b/src/core/components/Header/index.tsx
index e5ece8b..f6a2531 100644
--- a/src/core/components/Header/index.tsx
+++ b/src/core/components/Header/index.tsx
@@ -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";
diff --git a/src/core/components/PageContent/index.tsx b/src/core/components/PageContent/index.tsx
index 13f12e2..3a107b2 100644
--- a/src/core/components/PageContent/index.tsx
+++ b/src/core/components/PageContent/index.tsx
@@ -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,
}}
>
-
-
);
diff --git a/src/core/components/SideMenu/index.tsx b/src/core/components/SideMenu/index.tsx
index 8939f5f..44e6564 100644
--- a/src/core/components/SideMenu/index.tsx
+++ b/src/core/components/SideMenu/index.tsx
@@ -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);
diff --git a/src/core/store/appSlice.tsx b/src/core/store/appSlice.tsx
index 7664306..8cdc465 100644
--- a/src/core/store/appSlice.tsx
+++ b/src/core/store/appSlice.tsx
@@ -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;
\ No newline at end of file
+export const { darkMode, userAuthenticated } = appSlice.selectors;
\ No newline at end of file
diff --git a/src/core/utils/utils.tsx b/src/core/utils/utils.tsx
index 412fd9f..908ea56 100644
--- a/src/core/utils/utils.tsx
+++ b/src/core/utils/utils.tsx
@@ -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;
\ No newline at end of file
+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 {
+ url?: string;
+ method?: Method;
+ body?: TRequest | null;
+ headers?: Record;
+ 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({
+ url = "",
+ method = "GET",
+ body = null,
+ headers = {},
+ contentType = "JSON",
+ fetchUrl = Constants.API_ADDRESS,
+ ignoreUnauthorized = false,
+ notificationApi = null,
+ t = null,
+}: MyFetchOptions): Promise {
+ 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;
+ });
+}
diff --git a/src/features/Auth/SignIn/index.tsx b/src/features/Auth/SignIn/index.tsx
new file mode 100644
index 0000000..38f3644
--- /dev/null
+++ b/src/features/Auth/SignIn/index.tsx
@@ -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({
+ 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 (
+ <>
+
+
+
+
+
+ Sign In
+
+
+
+ label="Email"
+ name="email"
+ rules={[
+ {
+ required: true,
+ type: "email",
+ message: "Please input your email!",
+ },
+ ]}
+ >
+
+
+
+
+ label="Password"
+ name="password"
+ rules={[
+ { required: true, message: "Please input your password!" },
+ ]}
+ >
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/features/Auth/SignIn/pexels-photo-380769.webp b/src/features/Auth/SignIn/pexels-photo-380769.webp
new file mode 100644
index 0000000..ac077e4
Binary files /dev/null and b/src/features/Auth/SignIn/pexels-photo-380769.webp differ
diff --git a/src/features/Auth/SignIn/styles.module.css b/src/features/Auth/SignIn/styles.module.css
new file mode 100644
index 0000000..a85661c
--- /dev/null
+++ b/src/features/Auth/SignIn/styles.module.css
@@ -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%;
+}
diff --git a/src/features/Lessons/LessonPage/index.tsx b/src/features/Lessons/LessonPage/index.tsx
index 0d0847f..9c45e39 100644
--- a/src/features/Lessons/LessonPage/index.tsx
+++ b/src/features/Lessons/LessonPage/index.tsx
@@ -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}
);
- case 3:
+ case 4:
return {lessonContent.data}
;
default:
return Unknown type
;
diff --git a/src/index.tsx b/src/index.tsx
index 62699a1..e0aa384 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -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(
-
-
-
-
-
+ }>
+
+
+
+
+
+
);
// If you want to start measuring performance in your app, pass a function
diff --git a/src/shared/components/MyCenteredSpin/index.tsx b/src/shared/components/MyCenteredSpin/index.tsx
index b0212d8..e65dc93 100644
--- a/src/shared/components/MyCenteredSpin/index.tsx
+++ b/src/shared/components/MyCenteredSpin/index.tsx
@@ -1,10 +1,10 @@
-import { Spin } from "antd";
import { MyCenteredContainer } from "../MyContainer";
+import MySpin from "../MySpin";
export default function MyCenteredSpin({ fullHeight = false }) {
return (
-
+
);
}
diff --git a/src/shared/components/MySpin/index.tsx b/src/shared/components/MySpin/index.tsx
new file mode 100644
index 0000000..a13cf70
--- /dev/null
+++ b/src/shared/components/MySpin/index.tsx
@@ -0,0 +1,6 @@
+import { LoadingOutlined } from "@ant-design/icons";
+import { Spin } from "antd";
+
+export default function MySpin() {
+ return } />;
+}