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