init project

master
alex 2024-01-10 22:29:33 +01:00
commit c3268bd71b
56 changed files with 27401 additions and 0 deletions

4
Dockerfile Normal file
View File

@ -0,0 +1,4 @@
FROM nginx:latest
COPY ./build /usr/share/nginx/html
COPY ./nginx.conf /etc/nginx/conf.d/default.conf

15
README.md Normal file
View File

@ -0,0 +1,15 @@
# Jannex Admin Dashboard
With this system you can easily run your Python scripts and visualize them in the user interface to manage them more easily. You can see who started the task and who worked on it later in the task view. When a group task is started, the backend server starts the Python script and sends the result to the web UI. The web UI can then be used to start the other tasks or to repeat or undo the current task.
## Features
- Dynamic Group Task System
- Equipment Documentation System
- Advanced log system with log viewer and log filter
- Robot control system
- Role System
- Scanner integration to use mobile or pc devices as qrcode scanners
- User login system
- User deactivation system
- User API key system

26
build-docker.sh Executable file
View File

@ -0,0 +1,26 @@
DOCKER_REGISTRY_URL="dockreg.ex.umbach.dev"
DOCKER_IMAGE_NAME="jnx-admin-dashboard-proxy"
# only allow to run this script as root
if [ "$EUID" -ne 0 ]
then echo "Please run as root"
exit
fi
echo "Starting static web build of $DOCKER_IMAGE_NAME"
npm run build
echo "Finished static web build of $DOCKER_IMAGE_NAME"
# rm images
docker image rm $DOCKER_IMAGE_NAME
# build images
docker build -t $DOCKER_IMAGE_NAME .
# tag images
docker image tag $DOCKER_IMAGE_NAME:latest $DOCKER_REGISTRY_URL/$DOCKER_IMAGE_NAME:latest
# push to self-hosted docker registry
docker push $DOCKER_REGISTRY_URL/$DOCKER_IMAGE_NAME
echo "Uploaded $DOCKER_IMAGE_NAME to $DOCKER_REGISTRY_URL"

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

48
nginx.conf Normal file
View File

@ -0,0 +1,48 @@
server {
listen 80;
location /api/ { # api server
client_max_body_size 0;
proxy_http_version 1.0;
proxy_pass http://jnx-admin-dashboard-server/;
}
location /ws { # websocket server
proxy_read_timeout 10800s;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_pass http://jnx-admin-dashboard-server/ws/;
proxy_http_version 1.1;
}
location /lm/ { # log manager server
client_max_body_size 0;
proxy_http_version 1.0;
proxy_set_header Connection ""; # needed for sse
proxy_pass http://jnx-log-manager-server/;
}
location /tm/ { # telegram bot manager
client_max_body_size 0;
proxy_http_version 1.0;
proxy_set_header Connection ""; # needed for sse
proxy_pass http://jnx-telegram-bot-manager/;
}
location /rcm/ { # robot control manager
proxy_read_timeout 10800s;
client_max_body_size 0;
proxy_http_version 1.0;
proxy_set_header Connection ""; # needed for sse
proxy_set_header X-Real-IP $remote_addr; # needed to get the robot ip address
proxy_pass http://jnx-robot-control-manager/;
}
location / { # frontend
root /usr/share/nginx/html;
index index.html;
try_files $uri /index.html;
}
}

22326
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

59
package.json Normal file
View File

@ -0,0 +1,59 @@
{
"name": "admin-dashboard",
"version": "0.1.0",
"private": true,
"dependencies": {
"@ant-design/icons": "^5.0.1",
"@mdxeditor/editor": "^1.13.2",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@yudiel/react-qr-scanner": "^1.1.10",
"antd": "^5.10.3",
"buffer": "^6.0.3",
"i18next": "^23.2.3",
"i18next-browser-languagedetector": "^7.1.0",
"i18next-http-backend": "^2.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^13.0.1",
"react-qr-scanner": "^1.0.0-alpha.11",
"react-router-dom": "^6.10.0",
"react-scripts": "5.0.1",
"react-stl-viewer": "^2.2.5",
"react-virtuoso": "^4.5.1",
"react-webcam": "^7.1.1",
"uuid": "^9.0.0",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "BROWSER=none HOST=localhost PORT=50151 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"
]
},
"overrides": {
"react-qr-reader": {
"react": "$react",
"react-dom": "$react-dom"
}
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

33
public/index.html Normal file
View File

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Dashboard</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

View File

@ -0,0 +1,32 @@
{
"common": {
"button": {
"cancel": "Abbrechen",
"close": "Schließen",
"save": "Speichern",
"delete": "Löschen",
"confirm": "Bestätigen",
"create": "Erstellen"
},
"contactAdmin": "Bitte kontaktieren Sie einen Administrator"
},
"pageNotFound": {
"title": "Seite nicht gefunden",
"subTitle": "Die Seite, die Sie besucht haben, existiert leider nicht.",
"buttonBackHome": "Zurück zur Startseite"
},
"sideMenu": {
"overview": "Übersicht",
"website": {
"title": "Website",
"colorPalette": "Farbpalette",
"banner": "Banner",
"socials": "Soziale Netzwerke"
},
"employees": "Mitarbeiter",
"services": "Dienstleistungen",
"calendar": "Kalender",
"support": "Unterstützung",
"feedback": "Feedback"
}
}

View File

@ -0,0 +1,32 @@
{
"common": {
"button": {
"cancel": "Cancel",
"close": "Close",
"save": "Save",
"delete": "Delete",
"confirm": "Confirm",
"create": "Create"
},
"contactAdmin": "Please contact an administrator"
},
"pageNotFound": {
"title": "Page Not Found",
"subTitle": "The page you visited does not exist.",
"buttonBackHome": "Back to Home"
},
"sideMenu": {
"overview": "Overview",
"website": {
"title": "Website",
"colorPalette": "Color palette",
"banner": "Banner",
"socials": "Socials"
},
"employees": "Employees",
"services": "Services",
"calendar": "Calendar",
"support": "Support",
"feedback": "Feedback"
}
}

BIN
public/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

25
public/manifest.json Normal file
View File

@ -0,0 +1,25 @@
{
"short_name": "Admin Dashboard",
"name": "Admin Dashboard - Janex",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

3
public/robots.txt Normal file
View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

73
src/App.css Normal file
View File

@ -0,0 +1,73 @@
.CompanyNameContainer {
padding: 12px 12px 0 12px;
color: #e67e22;
font-weight: bold;
font-size: 24px;
text-align: center;
letter-spacing: 6px;
}
.CompanyNameContainer span {
animation: light 10s infinite;
}
@keyframes light {
20% {
color: #e67e22;
}
25% {
color: #ffd700;
}
30% {
color: #e67e22;
}
}
.CompanyNameContainer span:nth-child(1) {
animation-delay: 200ms;
}
.CompanyNameContainer span:nth-child(2) {
animation-delay: 400ms;
}
.CompanyNameContainer span:nth-child(3) {
animation-delay: 600ms;
}
.CompanyNameContainer span:nth-child(4) {
animation-delay: 800ms;
}
.CompanyNameContainer span:nth-child(5) {
animation-delay: 1000ms;
}
.Subtitle {
text-align: center;
color: #9b59b6;
font-weight: bold;
font-size: 14px;
padding-bottom: 20px;
}
/* full screen ant modal for an screen size smaller than 576px */
@media (max-width: 575px) {
.ant-modal {
height: -webkit-fill-available;
max-width: 100vw;
}
.ant-modal-content {
width: 100vw;
height: 100vh;
top: 0;
overflow: auto;
}
.ant-modal-centered::before {
content: unset;
}
}
.mdx-editor {
background: #f4f5f4;
}

165
src/App.js Normal file
View File

@ -0,0 +1,165 @@
import "antd/dist/reset.css";
import "./App.css";
import Login from "./Pages/Login";
import { Layout, Spin } from "antd";
import { UseUserSession, myFetch } from "./utils";
import DashboardLayout from "./Components/DashboardLayout";
import SideBarProvider from "./Contexts/SideBarContext";
import { AppProvider } from "./Contexts/AppContext";
import { UserProfileProvider } from "./Contexts/UserProfileContext";
import { UsersProvider } from "./Contexts/UsersContext";
import HeaderProvider from "./Contexts/HeaderContext";
import { useEffect, useState } from "react";
export default function App() {
/*const [notificationApi, notificationContextHolder] =
notification.useNotification();
*/
const { userSession, setUserSession } = UseUserSession();
//const [isWebSocketReady, setIsWebSocketReady] = useState(false);
const [appUserData, setAppUserData] = useState(null);
useEffect(() => {
if (!userSession) return;
console.log("userprofile");
myFetch("/user", "GET")
.then((data) => {
console.log(data);
setAppUserData(data);
})
.catch((errStatus) => {
setUserSession();
window.location.href = "/";
});
}, []);
if (!userSession) {
return <Login />;
}
if (appUserData === null) {
return (
<div
style={{
height: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 40,
}}
className="App"
>
<Spin size="large" />
<span
style={{
fontWeight: "bold",
fontSize: 24,
letterSpacing: 2,
}}
>
Lade Daten...
</span>
</div>
);
}
console.info(
"\n %c Customer Dashboard %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"
);
return (
<Layout style={{ minHeight: "100vh" }}>
<HeaderProvider>
<SideBarProvider _username={appUserData.username}>
<UserProfileProvider>
<UsersProvider>
<AppProvider>
<DashboardLayout
userSession={userSession}
setUserSession={setUserSession}
/>
</AppProvider>
</UsersProvider>
</UserProfileProvider>
</SideBarProvider>
</HeaderProvider>
</Layout>
);
}
/*
<WebSocketProvider
userSession={userSession}
setUserSession={setUserSession}
isWebSocketReady={isWebSocketReady}
setIsWebSocketReady={setIsWebSocketReady}
notificationApi={notificationApi}
>
<ReconnectingView isWebSocketReady={isWebSocketReady} />
<DashboardLayout
userSession={userSession}
setUserSession={setUserSession}
/>
</WebSocketProvider>
*/
/*
const ReconnectingView = ({ isWebSocketReady }) => {
return (
<div
style={{
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "rgba(0, 0, 0, 0.8)",
display: isWebSocketReady ? "none" : "block",
justifyContent: "center",
alignItems: "center",
zIndex: 9999,
}}
>
<svg id="loading-reconnecting" className="loading" viewBox="0 0 1350 600">
<text x="50%" y="50%" fill="transparent" textAnchor="middle">
C O M P A N Y
</text>
</svg>
<div
style={{
height: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span
style={{
marginTop: 80,
fontWeight: "bold",
fontSize: 24,
letterSpacing: 2,
}}
>
Connecting...
</span>
</div>
</div>
);
}; */
// TODO: Undo this
/*
<text x="50%" y="50%" fill="transparent" textAnchor="middle">
J A N N E X
</text>
*/

9
src/App.test.js Normal file
View File

@ -0,0 +1,9 @@
/*import { render, screen } from "@testing-library/react";
import App from "./App";
test("renders learn react link", () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
*/

View File

@ -0,0 +1,98 @@
import { Route, Routes } from "react-router-dom";
import { Constants } from "../../utils";
import { useAppContext } from "../../Contexts/AppContext";
import { lazy } from "react";
import { MySupsenseFallback } from "../MySupsenseFallback";
// Lazy-loaded components
const Dashboard = lazy(() => import("../../Pages/Dashboard"));
const PageNotFound = lazy(() => import("../../Pages/PageNotFound"));
const Employees = lazy(() => import("../../Pages/Employees"));
const Services = lazy(() => import("../../Pages/Services"));
const Calendar = lazy(() => import("../../Pages/Calendar"));
const Support = lazy(() => import("../../Pages/Support"));
const Feedback = lazy(() => import("../../Pages/Feedback"));
const UserProfile = lazy(() => import("../../Pages/UserProfile"));
export default function AppRoutes({ userSession, setUserSession }) {
//const appContext = useAppContext();
return (
<Routes>
<Route
path={Constants.ROUTE_PATHS.OVERVIEW}
element={
<MySupsenseFallback>
<Dashboard />
</MySupsenseFallback>
}
/>
<Route
path={Constants.ROUTE_PATHS.EMPLOYEES}
element={
<MySupsenseFallback>
<Employees />
</MySupsenseFallback>
}
/>
<Route
path={Constants.ROUTE_PATHS.SERVICES}
element={
<MySupsenseFallback>
<Services />
</MySupsenseFallback>
}
/>
<Route
path={Constants.ROUTE_PATHS.CALENDAR}
element={
<MySupsenseFallback>
<Calendar />
</MySupsenseFallback>
}
/>
<Route
path={Constants.ROUTE_PATHS.SUPPORT}
element={
<MySupsenseFallback>
<Support />
</MySupsenseFallback>
}
/>
<Route
path={Constants.ROUTE_PATHS.FEEDBACK}
element={
<MySupsenseFallback>
<Feedback />
</MySupsenseFallback>
}
/>
<Route
path={Constants.ROUTE_PATHS.USER_PROFILE}
element={
<MySupsenseFallback>
<UserProfile
userSession={userSession}
setUserSession={setUserSession}
/>
</MySupsenseFallback>
}
/>
<Route
path="*"
element={
<MySupsenseFallback>
<PageNotFound />
</MySupsenseFallback>
}
/>
</Routes>
);
}

View File

@ -0,0 +1,86 @@
import { Grid, Layout } from "antd";
import { SideMenuContent } from "../SideMenu";
import PageContent from "../PageContent";
import { memo, useRef, useState } from "react";
import SideMenuMobile from "../SideMenu/Mobile";
import SideMenuDesktop from "../SideMenu/Desktop";
const { useBreakpoint } = Grid;
const Content = memo(
({
isSideMenuCollapsed,
setIsSideMenuCollapsed,
setUserSession,
userSession,
contentFirstRender,
}) => {
return (
<SideMenuContent
isSideMenuCollapsed={isSideMenuCollapsed}
setIsSideMenuCollapsed={setIsSideMenuCollapsed}
setUserSession={setUserSession}
userSession={userSession}
contentFirstRender={contentFirstRender}
/>
);
}
);
export function SideMenu({ isSideMenuCollapsed, setIsSideMenuCollapsed }) {
const screenBreakpoint = useBreakpoint();
// used to prevent auto close the sideMenu on mobile on the first render
const contentFirstRender = useRef(true);
return (
<>
{screenBreakpoint.lg ? (
<SideMenuDesktop
isSideMenuCollapsed={screenBreakpoint.lg ? isSideMenuCollapsed : true}
setIsSideMenuCollapsed={setIsSideMenuCollapsed}
>
<Content
setIsSideMenuCollapsed={setIsSideMenuCollapsed}
contentFirstRender={contentFirstRender}
/>
</SideMenuDesktop>
) : (
<SideMenuMobile
isSideMenuCollapsed={isSideMenuCollapsed}
setIsSideMenuCollapsed={setIsSideMenuCollapsed}
>
<Content
setIsSideMenuCollapsed={setIsSideMenuCollapsed}
contentFirstRender={contentFirstRender}
/>
</SideMenuMobile>
)}
</>
);
}
export default function DashboardLayout({ userSession, setUserSession }) {
// open on desktop, closed on mobile on page open
const [isSideMenuCollapsed, setIsSideMenuCollapsed] = useState(
window.innerWidth < 992
);
return (
<Layout style={{ minHeight: "100vh" }}>
<Layout>
<SideMenu
isSideMenuCollapsed={isSideMenuCollapsed}
setIsSideMenuCollapsed={setIsSideMenuCollapsed}
/>
<PageContent
isSideMenuCollapsed={isSideMenuCollapsed}
setIsSideMenuCollapsed={setIsSideMenuCollapsed}
userSession={userSession}
setUserSession={setUserSession}
/>
</Layout>
</Layout>
);
}

View File

@ -0,0 +1,196 @@
import {
BellOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
CloseOutlined,
DeleteOutlined,
ExclamationCircleOutlined,
InboxOutlined,
InfoCircleOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
QuestionCircleOutlined,
} from "@ant-design/icons";
import { Badge, Button, Drawer, List, Popconfirm, Typography } from "antd";
import { Header } from "antd/es/layout/layout";
import { useEffect, useState } from "react";
import { useHeaderContext } from "../../Contexts/HeaderContext";
import { myFetch } from "../../utils";
import { useWebSocketContext } from "../../Contexts/WebSocketContext";
import { SentMessagesCommands } from "../../Handlers/WebSocketMessageHandler";
import { useTranslation } from "react-i18next";
import LiveTimeAgo from "../LiveTimeAgo";
import MyPagination from "../MyPagination";
export default function HeaderMenu({
isSideMenuCollapsed,
setIsSideMenuCollapsed,
}) {
//const webSocketContext = useWebSocketContext();
//const headerContext = useHeaderContext();
//const { t } = useTranslation();
const [isNotificationDrawerOpen, setIsNotificationDrawerOpen] =
useState(false);
/*
const fetchNotifications = (page = 1) => {
myFetch(`/notifications?page=${page}`, "GET").then((data) =>
headerContext.setNotificationResponse(data)
);
};
const onPaginationChange = (page) => {
headerContext.setPaginationPage(page);
headerContext.paginationPageRef.current = page;
}; */
/*
useEffect(() => {
// fetch will only be called if the drawer is open and there are no notifications
// further notifications will be fetched by the websocket
if (!isNotificationDrawerOpen || headerContext.notficationResponse !== null)
return;
fetchNotifications(1);
}, [isNotificationDrawerOpen]);
useEffect(() => {
if (!isNotificationDrawerOpen) return;
fetchNotifications(headerContext.paginationPage);
}, [headerContext.paginationPage]); */
return (
<Header
style={{
position: "sticky",
top: 0,
zIndex: 10,
width: "100%",
display: "flex",
alignItems: "center",
padding: 0,
background: "#fff",
justifyContent: "space-between",
}}
>
<Button
type="text"
icon={
isSideMenuCollapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />
}
onClick={() => setIsSideMenuCollapsed(!isSideMenuCollapsed)}
style={{ fontSize: "16px", width: 64, height: 64 }}
/>
</Header>
);
}
/*
<Button
type="text"
icon={
<Badge count={headerContext.totalNotifications} offset={[2, -2]}>
<BellOutlined style={{ fontSize: "16px" }} />
</Badge>
}
onClick={() => setIsNotificationDrawerOpen(true)}
style={{ fontSize: "16px", width: 64, height: 64 }}
/>
<Drawer
title={t("header.notificationDrawer.title")}
placement="right"
open={isNotificationDrawerOpen}
onClose={() => setIsNotificationDrawerOpen(false)}
extra={
headerContext.totalNotifications > 0 && (
<Popconfirm
title={t("header.notificationDrawer.deleteAllPopconfirm.title")}
okText={t("common.button.confirm")}
cancelText={t("common.button.cancel")}
onConfirm={() => {
webSocketContext.SendSocketMessage(
SentMessagesCommands.DeleteAllNotifications,
{}
);
setIsNotificationDrawerOpen(false);
}}
>
<Button type="link" icon={<DeleteOutlined />}>
{t("header.notificationDrawer.deleteAllButtonText")}
</Button>
</Popconfirm>
)
}
>
{isNotificationDrawerOpen && (
<>
{headerContext.totalNotifications === 0 ||
headerContext.notficationResponse === null ? (
<div style={{ textAlign: "center" }}>
<InboxOutlined style={{ fontSize: 32, marginBottom: 10 }} />
<Typography.Title level={5}>
{t("header.notificationDrawer.noNotifications")}
</Typography.Title>
</div>
) : (
<List
dataSource={headerContext.notficationResponse.Notifications.sort(
(a, b) => {
return new Date(b.CreatedAt) - new Date(a.CreatedAt);
}
)}
footer={
<MyPagination
paginationPage={headerContext.paginationPage}
setPaginationPage={(page) => onPaginationChange(page)}
totalPages={headerContext.notficationResponse.TotalPages}
size="small"
/>
}
renderItem={(item) => (
<List.Item>
<List.Item.Meta
avatar={<NotificationTypeIcon type={item.Type} />}
title={item.Title}
description={<LiveTimeAgo startTime={item.CreatedAt} />}
/>
<CloseOutlined
onClick={() => {
/*webSocketContext.SendSocketMessage(
SentMessagesCommands.DeleteOneNotification,
{
notificationId: item.Id,
}
)
}}
/>
</List.Item>
)}
/>
)}
</>
)}
</Drawer>
*/
function NotificationTypeIcon({ type }) {
switch (type) {
case 1:
return <CheckCircleOutlined color="#33a834" style={{ fontSize: 16 }} />;
case 2:
return <InfoCircleOutlined color="#0c69d7" style={{ fontSize: 16 }} />;
case 3:
return (
<ExclamationCircleOutlined color="#dd9433" style={{ fontSize: 16 }} />
);
case 4:
return <CloseCircleOutlined color="#e5444b" style={{ fontSize: 16 }} />;
default:
return <QuestionCircleOutlined color="#fff" style={{ fontSize: 16 }} />;
}
}

View File

@ -0,0 +1,61 @@
import { Tooltip } from "antd";
import { useEffect, useState } from "react";
import { FormatDatetime } from "../../utils";
import { useTranslation } from "react-i18next";
// Calculate elapsed time in seconds, minutes, hours, or days
function calculatedTimeAgo(startTime, t) {
const currentTime = new Date();
const elapsedMilliseconds = currentTime - new Date(startTime);
let timeAgoText;
if (elapsedMilliseconds < 60000) {
const secondsAgo = Math.floor(elapsedMilliseconds / 1000);
timeAgoText =
secondsAgo === 0
? "just now"
: secondsAgo === 1
? t("liveTimeAgo.second", { count: secondsAgo })
: t("liveTimeAgo.second_plural", { count: secondsAgo });
} else if (elapsedMilliseconds < 3600000) {
const minutesAgo = Math.floor(elapsedMilliseconds / 60000);
timeAgoText =
minutesAgo === 1
? t("liveTimeAgo.minute", { count: minutesAgo })
: t("liveTimeAgo.minute_plural", { count: minutesAgo });
} else if (elapsedMilliseconds < 86400000) {
const hoursAgo = Math.floor(elapsedMilliseconds / 3600000);
timeAgoText =
hoursAgo === 1
? t("liveTimeAgo.hour", { count: hoursAgo })
: t("liveTimeAgo.hour_plural", { count: hoursAgo });
} else {
const daysAgo = Math.floor(elapsedMilliseconds / 86400000);
timeAgoText =
daysAgo === 1
? t("liveTimeAgo.day", { count: daysAgo })
: t("liveTimeAgo.day_plural", { count: daysAgo });
}
return timeAgoText;
}
export default function LiveTimeAgo({ startTime }) {
const { t } = useTranslation();
const [timeAgo, setTimeAgo] = useState(calculatedTimeAgo(startTime, t));
useEffect(() => {
const interval = setInterval(() => {
setTimeAgo(calculatedTimeAgo(startTime, t));
}, 1000); // Update every second
return () => clearInterval(interval); // Clean up on component unmount
}, [startTime]);
return (
<Tooltip title={FormatDatetime(startTime)}>
<span>{timeAgo}</span>
</Tooltip>
);
}

View File

@ -0,0 +1,50 @@
import { UserOutlined } from "@ant-design/icons";
import { Avatar, Tooltip } from "antd";
import { Constants } from "../../utils";
export function MyAvatar({
avatarWidth,
avatar,
tooltip,
tooltipTitle,
allUsers,
userId,
}) {
if (!avatar) {
if (allUsers && userId) {
const user = allUsers.find((u) => u.Id === userId);
if (user) {
avatar = user.Avatar;
tooltipTitle = user.Username;
}
}
}
const avatarContent = avatar ? (
<Avatar
size={avatarWidth}
src={Constants.STATIC_CONTENT_ADDRESS + "avatars/" + avatar}
/>
) : (
<Avatar size={avatarWidth} icon={<UserOutlined />} />
);
return tooltip ? (
<Tooltip placement="top" trigger="hover" title={tooltipTitle}>
{avatarContent}
</Tooltip>
) : (
avatarContent
);
}
export function MyUserAvatar({ avatar, size = "default" }) {
if (avatar === "") return <Avatar icon={<UserOutlined />} size={size} />;
return (
<Avatar
src={Constants.STATIC_CONTENT_ADDRESS + "avatars/" + avatar}
size={size}
/>
);
}

View File

@ -0,0 +1,50 @@
import {
CopyOutlined,
EyeInvisibleOutlined,
EyeOutlined,
ReloadOutlined,
} from "@ant-design/icons";
import { Tooltip } from "antd";
import { useTranslation } from "react-i18next";
export function MyCopyIcon({ text, notificationApi }) {
const { t } = useTranslation();
return (
<Tooltip title={t("common.text.copyToClipboard")}>
<CopyOutlined
onClick={() => {
navigator.clipboard.writeText(text);
notificationApi["info"]({
message: t("common.text.copiedToClipboard"),
});
}}
/>
</Tooltip>
);
}
export function MyShowHiddenIcon({ setIsHidden, isHidden }) {
const { t } = useTranslation();
return (
<Tooltip title={isHidden ? t("common.text.hide") : t("common.text.show")}>
{isHidden ? (
<EyeInvisibleOutlined onClick={() => setIsHidden(false)} />
) : (
<EyeOutlined onClick={() => setIsHidden(true)} />
)}
</Tooltip>
);
}
export function MyReloadIcon({ onClick }) {
const { t } = useTranslation();
return (
<Tooltip title={t("common.text.reload")}>
<ReloadOutlined onClick={onClick} />
</Tooltip>
);
}

View File

@ -0,0 +1,31 @@
import { Spin } from "antd";
import { useState } from "react";
export default function MyImage({ src, width, style }) {
const [loaded, setLoaded] = useState(false);
return (
<div>
{loaded ? null : (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: 200,
}}
>
<Spin />
</div>
)}
<img
src={src}
width={width}
style={loaded ? style : { display: "none" }}
alt="Image"
onLoad={() => setLoaded(true)}
/>
</div>
);
}

View File

@ -0,0 +1,145 @@
import { Button, Grid, Modal, Result } from "antd";
import { useTranslation } from "react-i18next";
const { useBreakpoint } = Grid;
export default function MyModal({
children,
isOpen,
onCancel,
footer = <MyModalOnlyCloseButtonFooter onCancel={onCancel} />,
title,
}) {
const screenBreakpoint = useBreakpoint();
return (
<Modal
open={isOpen}
width={screenBreakpoint.xs ? "100vw" : "70vw"}
maskClosable={false}
onCancel={onCancel}
footer={footer}
centered={screenBreakpoint.xs}
title={title}
>
{children}
</Modal>
);
}
// This modal will show a loading indicator until the data is loaded.
// If the data is loaded and no data is found, it will show a not found modal.
// If the data is loaded and data is found, it will show the children.
/*export function MyLazyLoadingModal({
isOpen,
onCancel,
children,
resultTitleNoDataFound,
setFoundData,
fetchUrl,
fetchType,
}) {
// for the loading indicator
const [isDataLoaded, setIsDataLoaded] = useState(false);
// response data by the fetch
const [noDataFound, setNoDataFound] = useState(false);
useEffect(() => {
if (!isOpen) return;
if (isDataLoaded) {
setIsDataLoaded(false);
setNoDataFound(false);
}
myFetch(fetchUrl, fetchType).then((data) => {
setIsDataLoaded(true);
if (!data) {
setNoDataFound(true);
return;
}
setFoundData(data);
});
}, [isOpen]);
return (
<MyModal isOpen={isOpen} onCancel={onCancel}>
{noDataFound ? (
<MyNotFoundModalContent resultTitle={resultTitleNoDataFound} />
) : isDataLoaded ? (
children
) : (
<div
style={{
display: "flex",
height: "48.3vh", // set the loading modal height to the same height as the MyNotFoundModalContent result
justifyContent: "center",
alignItems: "center",
}}
>
<Spin size="large" />
</div>
)}
</MyModal>
);
} */
export function MyNotFoundModal({ isOpen, onCancel, resultTitle }) {
return (
<MyModal isOpen={isOpen} onCancel={onCancel}>
<MyNotFoundModalContent resultTitle={resultTitle} />
</MyModal>
);
}
export function MyNotFoundModalContent({ resultTitle }) {
return <Result status="500" title={resultTitle} />;
}
export function MyModalOnlyCloseButtonFooter({ onCancel }) {
const { t } = useTranslation();
return <Button onClick={onCancel}>{t("common.button.close")}</Button>;
}
export function MyModalCloseSaveButtonFooter({
onCancel,
onSave,
isSaveButtonLoading,
}) {
const { t } = useTranslation();
return (
<>
<Button onClick={onCancel}>{t("common.button.close")}</Button>
<Button onClick={onSave} type="primary" loading={isSaveButtonLoading}>
{t("common.button.save")}
</Button>
</>
);
}
export function MyModalCloseCreateButtonFooter({
onCancel,
onCreate,
isCreateButtonDisabled,
isCreateButtonLoading,
}) {
const { t } = useTranslation();
return (
<>
<Button onClick={onCancel}>{t("common.button.close")}</Button>
<Button
onClick={onCreate}
type="primary"
disabled={isCreateButtonDisabled}
loading={isCreateButtonLoading}
>
{t("common.button.create")}
</Button>
</>
);
}

View File

@ -0,0 +1,20 @@
import { Pagination } from "antd";
import { AppStyle } from "../../utils";
export default function MyPagination({
paginationPage,
setPaginationPage,
totalPages,
size,
}) {
return (
<Pagination
size={size}
style={{ marginTop: AppStyle.app.margin, textAlign: "right" }}
showSizeChanger={false}
current={paginationPage}
onChange={setPaginationPage}
total={totalPages * 10}
/>
);
}

View File

@ -0,0 +1,26 @@
import { Spin } from "antd";
import { Suspense } from "react";
export function MySupsenseFallback({ children }) {
return (
<Suspense
fallback={
<div
style={{
display: "flex",
flexDirection: "row",
justifyContent: "center",
alignContent: "center",
alignItems: "center",
textAlign: "center",
height: "98.3vh",
}}
>
<Spin size="large" />
</div>
}
>
{children}
</Suspense>
);
}

View File

@ -0,0 +1,37 @@
import { EditOutlined } from "@ant-design/icons";
import { Input, Typography } from "antd";
import { useState } from "react";
export default function MyTypography({ value, setValue, maxLength }) {
const [editing, setEditing] = useState(false);
return (
<div
style={{
display: "flex",
flexDirection: "row",
gap: 10,
marginRight: 26,
alignItems: "center",
}}
>
{editing ? (
<Input
value={value}
onChange={(e) => setValue(e.target.value)}
maxLength={maxLength}
showCount
/>
) : (
<Typography.Title level={3} style={{ margin: 0 }}>
{value}
</Typography.Title>
)}
<EditOutlined
style={{ fontSize: 24 }}
onClick={() => setEditing(!editing)}
/>
</div>
);
}

View File

@ -0,0 +1,79 @@
import { Content } from "antd/es/layout/layout";
import AppRoutes from "../AppRoutes";
import { Layout } from "antd";
import HeaderMenu from "../Header";
import { BreakpointLgWidth } from "../../utils";
import { memo } from "react";
const PageContent = memo(
({
isSideMenuCollapsed,
setIsSideMenuCollapsed,
userSession,
setUserSession,
}) => {
return (
<Layout
style={{
marginLeft:
isSideMenuCollapsed ||
window.document.body.clientWidth < BreakpointLgWidth
? 0
: 200,
}}
>
<HeaderMenu
isSideMenuCollapsed={isSideMenuCollapsed}
setIsSideMenuCollapsed={setIsSideMenuCollapsed}
/>
<Content
style={{
padding: 12,
}}
>
<AppRoutes
userSession={userSession}
setUserSession={setUserSession}
/>
</Content>
</Layout>
);
}
);
export default PageContent;
/*
export default function PageContent({
isSideMenuCollapsed,
setIsSideMenuCollapsed,
userSession,
setUserSession,
}) {
return (
<Layout
style={{
marginLeft:
isSideMenuCollapsed ||
window.document.body.clientWidth < BreakpointLgWidth
? 0
: 200,
}}
>
<HeaderMenu
isSideMenuCollapsed={isSideMenuCollapsed}
setIsSideMenuCollapsed={setIsSideMenuCollapsed}
/>
<Content
style={{
padding: 12,
}}
>
<AppRoutes userSession={userSession} setUserSession={setUserSession} />
</Content>
</Layout>
);
}
*/

View File

@ -0,0 +1,27 @@
import Sider from "antd/es/layout/Sider";
export default function SideMenuDesktop({
children,
isSideMenuCollapsed,
setIsSideMenuCollapsed,
}) {
return (
<Sider
theme="light"
style={{
overflow: "auto",
height: "100vh",
position: "fixed",
left: 0,
top: 0,
bottom: 0,
}}
breakpoint="lg"
collapsedWidth={1}
collapsed={isSideMenuCollapsed}
onCollapse={(collapsed) => setIsSideMenuCollapsed(collapsed)}
>
{children}
</Sider>
);
}

View File

@ -0,0 +1,19 @@
import { Drawer } from "antd";
export default function SideMenuMobile({
children,
isSideMenuCollapsed,
setIsSideMenuCollapsed,
}) {
return (
<Drawer
open={!isSideMenuCollapsed}
onClose={() => setIsSideMenuCollapsed(true)}
placement="left"
styles={{ body: { padding: 0 } }}
width={200}
>
{children}
</Drawer>
);
}

View File

@ -0,0 +1,245 @@
import {
AppstoreOutlined,
BgColorsOutlined,
BookOutlined,
CalendarOutlined,
ClusterOutlined,
ControlOutlined,
DesktopOutlined,
EditOutlined,
FileImageOutlined,
FileTextOutlined,
HistoryOutlined,
MessageOutlined,
QuestionCircleOutlined,
RobotOutlined,
ScanOutlined,
ScissorOutlined,
SettingOutlined,
SnippetsOutlined,
TeamOutlined,
UserOutlined,
UsergroupAddOutlined,
} from "@ant-design/icons";
import { Badge, Divider, Menu } from "antd";
import { useEffect, useRef, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import {
BreakpointLgWidth,
BrowserTabSession,
Constants,
hasOnePermission,
hasOneXYPermission,
hasPermission,
wsConnectionCustomEventName,
} from "../../utils";
import { useTranslation } from "react-i18next";
import { useSideBarContext } from "../../Contexts/SideBarContext";
import { useAppContext } from "../../Contexts/AppContext";
export function SideMenuContent({
setIsSideMenuCollapsed,
contentFirstRender,
}) {
const appContext = useAppContext();
const sideBarContext = useSideBarContext();
const location = useLocation();
const [selectedKeys, setSelectedKeys] = useState("/");
const { t } = useTranslation();
//const lastSubscribedTopic = useRef("");
const navigate = useNavigate();
const getFirstMenuItems = () => {
let items = [
{
label: t("sideMenu.overview"),
icon: <AppstoreOutlined />,
key: Constants.ROUTE_PATHS.OVERVIEW,
},
];
// website
let groupWebsite = {
label: t("sideMenu.website.title"),
icon: <EditOutlined />,
children: [],
key: "/website",
};
groupWebsite.children.push({
label: t("sideMenu.website.colorPalette"),
icon: <BgColorsOutlined />,
key: Constants.ROUTE_PATHS.WEBSITE_COLOR_PALETTE,
});
groupWebsite.children.push({
label: t("sideMenu.website.banner"),
icon: <FileImageOutlined />,
key: Constants.ROUTE_PATHS.WEBSITE_BANNER,
});
groupWebsite.children.push({
label: t("sideMenu.website.socials"),
icon: <ClusterOutlined />,
key: Constants.ROUTE_PATHS.WEBSITE_SOCIALS,
});
items.push(groupWebsite);
// employees
items.push({
label: t("sideMenu.employees"),
icon: <TeamOutlined />,
key: Constants.ROUTE_PATHS.EMPLOYEES,
});
// services
items.push({
label: t("sideMenu.services"),
icon: <ScissorOutlined />,
key: Constants.ROUTE_PATHS.SERVICES,
});
// calendar
items.push({
label: t("sideMenu.calendar"),
icon: <CalendarOutlined />,
key: Constants.ROUTE_PATHS.CALENDAR,
});
return items;
};
const getSecondMenuItems = () => {
let items = [];
// connection status, userprofile, logout
items.push(
{
label: "Support",
icon: <QuestionCircleOutlined />,
key: Constants.ROUTE_PATHS.SUPPORT,
},
{
label: "Feedback",
icon: <MessageOutlined />,
key: Constants.ROUTE_PATHS.FEEDBACK,
},
{
label: ` ${sideBarContext.username}`,
icon: <UserOutlined />,
key: Constants.ROUTE_PATHS.USER_PROFILE,
}
);
return items;
};
useEffect(() => {
const pathname = location.pathname;
setSelectedKeys(pathname);
//lastSubscribedTopic.current = pathname;
/*
const subscribeTopicMessage = () =>
webSocketContext.SendSocketMessage(
SentMessagesCommands.SubscribeToTopic,
{
topic: pathname,
browserTabSession: BrowserTabSession,
}
);
subscribeTopicMessage();
const handleSubscribeTopicMessage = () => subscribeTopicMessage();
document.addEventListener(
wsConnectionCustomEventName,
handleSubscribeTopicMessage
); */
// auto close sideMenu on mobile
// this will prevent to auto close sideMenu on first render as the useEffects will be called after the first render
if (contentFirstRender.current) {
contentFirstRender.current = false;
//return;
} else if (
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
) ||
document.body.clientWidth < BreakpointLgWidth
) {
setIsSideMenuCollapsed(true);
}
/*
return () =>
document.removeEventListener(
wsConnectionCustomEventName,
handleSubscribeTopicMessage
); */
}, [location.pathname]);
return (
<div
style={{
display: "flex",
justifyContent: "space-between",
flexDirection: "column",
height: "100%",
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
overflowY: "hidden",
}}
>
<div>
<div className="CompanyNameContainer">
<span>C</span>
<span>O</span>
<span>M</span>
<span>P</span>
<span>A</span>
<span>N</span>
<span>Y</span>
</div>
<div className="Subtitle">Dashboard</div>
</div>
<div style={{ overflowY: "scroll" }}>
<Menu
mode="inline"
onClick={(item) => navigate(item.key)}
theme="light"
selectedKeys={[selectedKeys]}
items={getFirstMenuItems()}
defaultOpenKeys={["/website"]}
/>
</div>
</div>
<div>
<Divider style={{ margin: 0 }} />
<Menu
selectable={true}
selectedKeys={[selectedKeys]}
mode="vertical"
onClick={(item) => navigate(item.key)}
items={getSecondMenuItems()}
/>
</div>
</div>
);
}

View File

@ -0,0 +1,34 @@
import { createContext, useContext, useEffect, useRef, useState } from "react";
import { myFetch, myFetchContentType } from "../utils";
const preview = {
userId: "",
userPermissions: [],
users: [],
};
const AppContext = createContext(preview);
export const useAppContext = () => useContext(AppContext);
export function AppProvider({ children }) {
const userId = useRef(""); // used for some conditions in webSocket message handler
const [userPermissions, setUserPermissions] = useState([]);
// used for avatars and the tooltip for the avatar
const [users, setUsers] = useState([]); // { Id, Username, Avatar}
return (
<AppContext.Provider
value={{
userId,
userPermissions,
setUserPermissions,
users,
setUsers,
}}
>
{children}
</AppContext.Provider>
);
}

View File

@ -0,0 +1,35 @@
import { createContext, useContext, useRef, useState } from "react";
const preview = {
totalNotifications: 0,
notficationResponse: null,
paginationPage: 1,
paginationPageRef: null,
};
const HeaderContext = createContext(preview);
export const useHeaderContext = () => useContext(HeaderContext);
export default function HeaderProvider({ children }) {
const [totalNotifications, setTotalNotifications] = useState(0);
const [notficationResponse, setNotificationResponse] = useState(null);
const [paginationPage, setPaginationPage] = useState(1);
const paginationPageRef = useRef(paginationPage);
return (
<HeaderContext.Provider
value={{
totalNotifications,
setTotalNotifications,
notficationResponse,
setNotificationResponse,
paginationPage,
setPaginationPage,
paginationPageRef,
}}
>
{children}
</HeaderContext.Provider>
);
}

View File

@ -0,0 +1,45 @@
import { createContext, useContext, useState } from "react";
import { Constants } from "../utils";
const preview = {
connectionBadgeStatus: "",
connectedUsers: 0,
selectedScanner: "",
username: "",
avatar: "",
availableCategories: [],
};
const SideBarContext = createContext(preview);
export const useSideBarContext = () => useContext(SideBarContext);
export default function SideBarProvider({ _username, children }) {
const [connectionBadgeStatus, setConnectionBadgeStatus] = useState("error");
const [connectedUsers, setConnectedUsers] = useState(0);
const [selectedScanner, setSelectedScanner] = useState("");
const [username, setUsername] = useState(_username); //
const [avatar, setAvatar] = useState("");
const [availableCategories, setAvailableCategories] = useState([]);
return (
<SideBarContext.Provider
value={{
connectionBadgeStatus,
setConnectionBadgeStatus,
connectedUsers,
setConnectedUsers,
selectedScanner,
setSelectedScanner,
username,
setUsername,
avatar,
setAvatar,
availableCategories,
setAvailableCategories,
}}
>
{children}
</SideBarContext.Provider>
);
}

View File

@ -0,0 +1,34 @@
import { createContext, useContext, useEffect, useState } from "react";
import { Constants } from "../utils";
// userId, username is stored in appContext
const preview = {
email: Constants.LOADING,
sessions: [],
apiKeys: [],
};
const UserProfileContext = createContext(preview);
export const useUserProfileContext = () => useContext(UserProfileContext);
export function UserProfileProvider({ children }) {
const [email, setEmail] = useState("");
const [sessions, setSessions] = useState([]);
const [apiKeys, setApiKeys] = useState([]);
return (
<UserProfileContext.Provider
value={{
email,
setEmail,
sessions,
setSessions,
apiKeys,
setApiKeys,
}}
>
{children}
</UserProfileContext.Provider>
);
}

View File

@ -0,0 +1,32 @@
import { createContext, useContext, useState } from "react";
const preview = {
roleId: "",
users: [],
roles: [],
};
const UsersContext = createContext(preview);
export const useUsersContext = () => useContext(UsersContext);
export function UsersProvider({ children }) {
const [roleId, setRoleId] = useState("");
const [users, setUsers] = useState([]);
const [roles, setRoles] = useState([]);
return (
<UsersContext.Provider
value={{
roleId,
setRoleId,
users,
setUsers,
roles,
setRoles,
}}
>
{children}
</UsersContext.Provider>
);
}

View File

@ -0,0 +1,133 @@
import { createContext, useContext, useEffect, useRef } from "react";
import { wsConnectionCustomEventName } from "../utils";
const WebSocketContext = createContext(null);
export const useWebSocketContext = () => useContext(WebSocketContext);
let wsConnectionEvent = null;
let firstConnection = true;
export default function WebSocketProvider({
children,
userSession,
setUserSession,
isWebSocketReady,
setIsWebSocketReady,
notificationApi,
}) {
const ws = useRef(null);
const wsMessageCache = useRef([]);
if (wsConnectionEvent === null) {
wsConnectionEvent = new CustomEvent(wsConnectionCustomEventName, {
detail: "wsReconnect",
});
}
const connect = () => {
setIsWebSocketReady(true);
/*
ws.current = new WebSocket(
`${Constants.WS_ADDRESS}?auth=${userSession}&bts=${BrowserTabSession}`
);
ws.current.onopen = () => {
sideBarContext.setConnectionBadgeStatus("success");
setIsWebSocketReady(true);
if (firstConnection) {
firstConnection = false;
} else {
document.dispatchEvent(wsConnectionEvent);
}
myFetch("/user/", "GET").then((data) => {
appContext.userId.current = data.UserId;
appContext.setUserPermissions(
data.Permissions === null ? [] : data.Permissions
);
appContext.setUsers(data.Users);
headerContext.setTotalNotifications(data.TotalNotifications);
sideBarContext.setUsername(data.Username);
sideBarContext.setAvatar(data.Avatar);
sideBarContext.setAvailableCategories(
data.AvailableCategories === null ? [] : data.AvailableCategories
);
});
if (wsMessageCache.current.length > 0) {
// send cached messages
wsMessageCache.current.forEach((message) => {
ws.current.send(JSON.stringify(message));
});
wsMessageCache.current = [];
}
};
ws.current.onmessage = (event) => {
handleWebSocketMessage(
event,
navigate,
notificationApi,
sideBarContext,
appContext,
headerContext,
groupTasksContext,
userProfileContext,
adminAreaRolesContext,
usersContext,
consolesContext,
scannerContext,
crmContext
);
};
ws.current.onclose = (event) => {
setIsWebSocketReady(false);
sideBarContext.setConnectionBadgeStatus("error");
console.warn("closed", event);
// custom code defined by the backend server
if (event.code === 4001 || event.code === 4002) {
//Unauthorized || SessionClosed
setUserSession();
window.location.href = "/";
return;
}
if (event.reason.code === 1005) return;
console.warn("reconnecting...");
setTimeout(() => connect(), 1000);
}; */
};
const SendSocketMessage = (cmd, body) => {
if (
isWebSocketReady &&
ws.current !== null &&
ws.current.readyState === 1
) {
ws.current.send(JSON.stringify({ Cmd: cmd, Body: body }));
} else {
wsMessageCache.current.push({ Cmd: cmd, Body: body });
}
};
useEffect(() => {
connect();
return () => ws.current.close();
}, []);
return (
<WebSocketContext.Provider value={{ SendSocketMessage: SendSocketMessage }}>
{children}
</WebSocketContext.Provider>
);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
export default function Calendar() {
return (
<>
<h1>Calendar</h1>
</>
);
}

View File

@ -0,0 +1,120 @@
import { memo } from "react";
import { Card, Typography } from "antd";
import { useSideBarContext } from "../../Contexts/SideBarContext";
const randomGreeting = Math.floor(Math.random() * 18);
function getGreeting(name) {
const currentTime = new Date();
const currentHour = currentTime.getHours();
let greeting;
if (currentHour < 5) {
const nightGreetings = [
`Guten Morgen, ${name}! Ein weiterer Tag, um deinem Ziel näher zu kommen!`,
`Hab eine ruhige Nacht gehabt, ${name}? Du bist auf dem richtigen Weg!`,
`Hallo, ${name}! Bereit für einen neuen Tag voller Möglichkeiten? Du schaffst das!`,
`Schlaf gut, ${name}! Morgen ist ein neuer Tag, um Großartiges zu erreichen!`,
`Guten Morgen! Wie hast du geschlafen, ${name}? Du bist unaufhaltsam!`,
`Hallo, ${name}! Bereit für einen neuen Tag voller Chancen und Erfolge?`,
`Ein neuer Tag bricht an, ${name}! Du bist auf dem richtigen Pfad!`,
`Guten Morgen! Lass uns den Tag beginnen, ${name}! Du kannst alles schaffen!`,
`Hallo, ${name}! Wie geht es dir heute Morgen? Sei stolz auf deine Fortschritte!`,
`Ein herzliches "Guten Morgen" an dich, ${name}! Jeder Tag ist eine Chance zu wachsen!`,
`Hab eine erholsame Nacht gehabt, ${name}? Jetzt kannst du wieder Vollgas geben!`,
`Hallo, ${name}! Starte frisch in den Tag! Du bist auf dem richtigen Weg!`,
`Guten Morgen! Was steht auf deiner Agenda, ${name}? Setze deine Ziele hoch!`,
`Beginne den Tag mit einem Lächeln, ${name}! Du bist stark und voller Potential!`,
`Ich wünsche dir einen wunderbaren Morgen, ${name}! Glaube an dich und deine Träume!`,
`Hallo, ${name}! Bereit für neue Herausforderungen? Du wirst sie meistern!`,
`Guten Morgen! Wie war deine Nacht, ${name}? Du bist auf dem Weg zum Erfolg!`,
`Hallo, ${name}! Starte den Tag mit positiver Energie! Du bist unaufhaltsam!`,
];
//greeting = nightGreetings[Math.floor(Math.random() * nightGreetings.length)];
greeting = nightGreetings[randomGreeting];
} else if (currentHour < 12) {
const morningGreetings = [
`Guten Morgen, ${name}! Ein weiterer Tag, um deinem Ziel näher zu kommen!`,
`Ein strahlender Morgen erwartet dich, ${name}! Du hast das Zeug dazu!`,
`Hab einen motivierten Tag, ${name}! Du bist auf dem richtigen Weg!`,
`Guten Morgen! Lass dich nicht von deinen Träumen abhalten, ${name}!`,
`Starte den Tag mit positiver Energie, ${name}! Du bist unaufhaltsam!`,
`Hallo, ${name}! Nutze die Chancen des Tages und zeige, was in dir steckt!`,
`Ein neuer Tag voller Möglichkeiten erwartet dich, ${name}! Glaube an dich selbst!`,
`Guten Morgen! Du bist auf dem richtigen Weg, ${name}! Heute ist dein Tag!`,
`Hallo, ${name}! Zeige der Welt, was du heute erreichen kannst! Du bist großartig!`,
`Begrüße den Tag mit Begeisterung, ${name}! Du hast das Potenzial, Berge zu versetzen!`,
`Guten Morgen! Glaube an dich selbst und verfolge deine Träume, ${name}!`,
`Hallo, ${name}! Du bist ein Champion, also zeig es ihnen! Du schaffst das!`,
`Ein weiterer Tag, um deine Träume zu verwirklichen, ${name}! Gib alles!`,
`Guten Morgen! Lass dein Licht heute strahlen, ${name}! Du bist einzigartig!`,
`Hallo, ${name}! Du bist auf dem richtigen Weg zu großartigen Dingen! Glaube an dich!`,
`Starte den Tag mit einem Lächeln und einer positiven Einstellung, ${name}! Du bist stark!`,
`Guten Morgen! Glaube an deine Stärken und gehe mit Zuversicht voran, ${name}!`,
`Hallo, ${name}! Nutze den Tag, um dein Bestes zu geben! Du bist unaufhaltsam!`,
];
//greeting = morningGreetings[Math.floor(Math.random() * morningGreetings.length)];
greeting = morningGreetings[randomGreeting];
} else if (currentHour < 18) {
const afternoonGreetings = [
`Guten Tag, ${name}! Ein weiterer Moment, um deinem Ziel näher zu kommen!`,
`Schön, dich zu sehen, ${name}! Du bist auf dem richtigen Weg!`,
`Hoffentlich läuft dein Tag gut, ${name}! Du schaffst das!`,
`Hallo, ${name}! Wie geht's? Halte dich an deine Ziele!`,
`Genieße deinen Nachmittag, ${name}! Du bist auf dem richtigen Kurs!`,
`Hallo, ${name}! Alles klar bei dir? Bleibe fokussiert!`,
`Schönen Tag noch, ${name}! Verliere nicht aus den Augen, was dir wichtig ist!`,
`Hallo, ${name}! Was steht auf deiner Agenda? Verfolge deine Träume!`,
`Freut mich, dich zu sehen, ${name}! Glaube an dich selbst!`,
`Hoffentlich hattest du einen produktiven Tag, ${name}! Halte die Motivation hoch!`,
`Hallo, ${name}! Wie läuft's? Bleibe positiv und lass dich nicht entmutigen!`,
`Ich hoffe, du hast einen tollen Tag, ${name}! Vertraue auf deine Fähigkeiten!`,
`Hallo, ${name}! Bist du bereit für den Rest des Tages? Zeige, was in dir steckt!`,
`Schön, dich hier zu haben, ${name}! Gib dein Bestes und glaube an dich!`,
`Hallo, ${name}! Wie war dein Vormittag? Halte die Motivation hoch!`,
`Ich wünsche dir einen angenehmen Nachmittag, ${name}! Verfolge deine Ziele mit Leidenschaft!`,
`Hallo, ${name}! Wie geht es dir heute? Denke daran, wie weit du schon gekommen bist!`,
`Freut mich, dass du da bist, ${name}! Glaube an dich und gehe deinen Weg!`,
];
//greeting = afternoonGreetings[Math.floor(Math.random() * afternoonGreetings.length)];
greeting = afternoonGreetings[randomGreeting];
} else {
const eveningGreetings = [
`Guten Abend, ${name}! Ein weiterer Tag ist fast vorbei! Du hast viel erreicht!`,
`Schön, dass du da bist, ${name}! Du hast den Tag gemeistert!`,
`Hast du einen produktiven Tag gehabt, ${name}? Sei stolz auf dich!`,
`Hallo, ${name}! Wie war dein Tag? Du bist auf dem richtigen Weg zum Erfolg!`,
`Einen entspannten Abend wünsche ich dir, ${name}! Du hast es dir verdient!`,
`Guten Abend! Wie geht es dir, ${name}? Halte dich an deine Träume!`,
`Hallo, ${name}! Was steht bei dir am Abend an? Entspanne dich und lade deine Energie auf!`,
`Ein herzliches "Guten Abend" an dich, ${name}! Denke daran, wie weit du schon gekommen bist!`,
`Genieße die Ruhe des Abends, ${name}! Du hast heute viel erreicht!`,
`Hallo, ${name}! Wie war dein Tag heute? Feiere deine Erfolge!`,
`Guten Abend! Was hast du heute erlebt, ${name}? Bleibe stolz auf dich!`,
`Ein angenehmer Abend liegt vor dir, ${name}! Lade deine Batterien auf und träume groß!`,
`Hallo, ${name}! Wie geht es dir am Ende des Tages? Sei dankbar für deine Fortschritte!`,
`Schön, dich noch zu sehen, ${name}! Reflektiere über deinen Tag und freue dich auf morgen!`,
`Guten Abend! Was möchtest du heute Abend machen, ${name}? Nutze die Zeit für dich selbst!`,
`Ein gemütlicher Abend steht bevor, ${name}! Entspanne dich und genieße den Moment!`,
`Hallo, ${name}! Wie war deine Arbeit heute? Erhole dich und sei stolz auf dich!`,
`Guten Abend! Du bist auf dem richtigen Weg zum Erfolg, ${name}! Glaube an dich!`,
];
//greeting = eveningGreetings[Math.floor(Math.random() * eveningGreetings.length)];
greeting = eveningGreetings[randomGreeting];
}
return greeting;
}
const Dashboard = memo(() => {
const sideBarContext = useSideBarContext();
return (
<Card>
<Typography.Title level={4} style={{ margin: 0 }}>
{getGreeting(sideBarContext.username)}
</Typography.Title>
</Card>
);
});
export default Dashboard;

View File

@ -0,0 +1,67 @@
import { PlusOutlined } from "@ant-design/icons";
import { Button, Table } from "antd";
export default function Employees() {
const getTableColumns = () => {
return [
{
title: "Name",
dataIndex: "name",
key: "name",
},
{
title: "Age",
dataIndex: "age",
key: "age",
},
{
title: "Action",
dataIndex: "action",
key: "actions",
render: () => <a>Delete</a>,
},
];
};
const getTableItems = () => {
return [
{
key: "1",
name: "John Brown",
age: 32,
address: "New York No. 1 Lake Park",
},
{
key: "2",
name: "Jim Green",
age: 42,
address: "London No. 1 Lake Park",
},
{
key: "3",
name: "Joe Black",
age: 32,
address: "Sidney No. 1 Lake Park",
},
];
};
return (
<>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<h1>Employees</h1>
<Button type="primary" icon={<PlusOutlined />}>
Add employee
</Button>
</div>
<Table columns={getTableColumns()} dataSource={getTableItems()} />
</>
);
}

View File

@ -0,0 +1,7 @@
export default function Feedback() {
return (
<>
<h1>Feedback</h1>
</>
);
}

150
src/Pages/Login/index.js Normal file
View File

@ -0,0 +1,150 @@
import { LockOutlined, LoginOutlined, UserOutlined } from "@ant-design/icons";
import { Button, Form, Input, Modal, Tabs, notification } from "antd";
import {
Constants,
EncodeStringToBase64,
myFetch,
myFetchContentType,
setUserSessionToLocalStorage,
} from "../../utils";
import { useState } from "react";
export default function Login() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [api, contextHolder] = notification.useNotification();
const [selectedMethod, setSelectedMethod] = useState("1");
const showErrorNotification = (errStatus) => {
if (errStatus === 401) {
api["error"]({
message: "Account deactivated",
description: "Please contact an administrator",
});
return;
}
api["error"]({
message: "Login failed",
description: "Please check your username and password!",
});
};
const handleSubmit = () => {
if (
username.length > Constants.GLOBALS.MAX_USERNAME_LENGTH ||
username.length < Constants.GLOBALS.MIN_USERNAME_LENGTH ||
password.length > Constants.GLOBALS.MAX_PASSWORD_LENGTH ||
password.length < Constants.GLOBALS.MIN_PASSWORD_LENGTH
) {
showErrorNotification();
return;
}
myFetch(
`/user/auth/${selectedMethod === "1" ? "login" : "signup"}`,
"POST",
{
username: username,
password: EncodeStringToBase64(password),
},
{},
myFetchContentType.JSON,
"",
true
)
.then((data) => {
console.log(data.XAuthorization);
setUserSessionToLocalStorage(data.XAuthorization);
window.location.href = "/";
})
.catch((errStatus) => showErrorNotification(errStatus));
};
return (
<>
{contextHolder}
<Modal
open={true}
closable={false}
centered
keyboard={false}
footer={
<Button
type="primary"
htmlType="submit"
icon={<LoginOutlined />}
className="login-form-button"
onClick={() => handleSubmit()}
>
{selectedMethod === "1" ? "Anmelden" : "Registrieren"}
</Button>
}
>
<Tabs
defaultActiveKey="1"
items={[
{
key: "1",
label: "Anmelden",
},
{
key: "2",
label: "Registrieren",
},
]}
centered
onChange={(activeKey) => {
setSelectedMethod(activeKey);
}}
/>
<Form>
<Form.Item
name="username"
required
rules={[
{ required: true, message: "Please enter your username!" },
{
min: Constants.GLOBALS.MIN_USERNAME_LENGTH,
message: `Please enter a username length of at least ${Constants.GLOBALS.MIN_USERNAME_LENGTH}!`,
},
]}
>
<Input
prefix={<UserOutlined />}
placeholder="Benutzername"
onChange={(e) => setUsername(e.target.value)}
minLength={Constants.GLOBALS.MIN_USERNAME_LENGTH}
maxLength={Constants.GLOBALS.MAX_USERNAME_LENGTH}
/>
</Form.Item>
<Form.Item
name="password"
required
rules={[
{
required: true,
message: "Please enter your Password!",
},
{
min: Constants.GLOBALS.MIN_PASSWORD_LENGTH,
message: `Please enter a password length of at least ${Constants.GLOBALS.MIN_PASSWORD_LENGTH}!`,
},
]}
>
<Input.Password
prefix={<LockOutlined />}
placeholder="Passwort"
onChange={(e) => setPassword(e.target.value)}
minLength={Constants.GLOBALS.MIN_PASSWORD_LENGTH}
maxLength={Constants.GLOBALS.MAX_PASSWORD_LENGTH}
/>
</Form.Item>
</Form>
</Modal>
</>
);
}

View File

@ -0,0 +1,21 @@
import { Button, Result } from "antd";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { Constants } from "../../utils";
export default function PageNotFound() {
const { t } = useTranslation();
return (
<Result
status="404"
title={t("pageNotFound.title")}
subTitle={t("pageNotFound.subTitle")}
extra={
<Link to={Constants.ROUTE_PATHS.OVERVIEW}>
<Button type="primary">{t("pageNotFound.buttonBackHome")}</Button>
</Link>
}
/>
);
}

View File

@ -0,0 +1,7 @@
export default function Services() {
return (
<>
<h1>Services</h1>
</>
);
}

View File

@ -0,0 +1,7 @@
export default function Support() {
return (
<>
<h1>Support</h1>
</>
);
}

View File

@ -0,0 +1,52 @@
import { Button, Card, Select } from "antd";
import { Constants } from "../../utils";
import { useTranslation } from "react-i18next";
export default function UserProfile({ userSession, setUserSession }) {
const { t, i18n } = useTranslation();
return (
<>
<Card
title="Ihr Profil"
extra={
<Button
type="primary"
onClick={() => {
console.log("Logout");
setUserSession();
window.location.href = "/";
fetch(`${Constants.API_ADDRESS}/user/auth/logout`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
"X-Authorization": userSession,
},
}).catch(console.error);
}}
>
Logout
</Button>
}
>
<Select
style={{ width: "100%" }}
defaultValue={i18n.language}
options={[
{
value: "en",
label: "English",
},
{
value: "de",
label: "Deutsch",
},
]}
onChange={(e) => i18n.changeLanguage(e)}
/>
</Card>
</>
);
}

18
src/i18n.js Normal file
View File

@ -0,0 +1,18 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import Backend from "i18next-http-backend";
import LanguageDetector from "i18next-browser-languagedetector";
i18n
.use(initReactI18next) // passes i18n down to react-i18next
.use(LanguageDetector)
.use(Backend)
.init({
supportedLngs: ["en", "de"],
fallbackLng: "en",
interpolation: {
escapeValue: false, // react already safes from xss
},
});
export default i18n;

45
src/index.css Normal file
View File

@ -0,0 +1,45 @@
body {
margin: 0;
/*font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",sans-serif;*/
font-family: "Inter", "Helvetica Neue", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}
.loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.loading text {
stroke: #e67e22;
font-size: 300px;
font-weight: 700;
stroke-width: 10;
}
#loading-init text {
animation: textAnimate 0.5s;
}
#loading-reconnecting text {
animation: textAnimate 1s infinite;
}
@keyframes textAnimate {
0% {
stroke-dasharray: 0 50%;
}
100% {
stroke-dasharray: 50% 0;
}
}

38
src/index.js Normal file
View File

@ -0,0 +1,38 @@
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { BrowserRouter } from "react-router-dom";
import "./i18n";
//import reportWebVitals from './reportWebVitals';
const Loading = () => {
return (
<svg id="loading-init" className="loading" viewBox="0 0 1350 600">
<text x="50%" y="50%" fill="transparent" textAnchor="middle">
C O M P A N Y
</text>
</svg>
);
};
// TODO: Undo this
/*
<text x="50%" y="50%" fill="transparent" textAnchor="middle">
J A N N E X
</text>
*/
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.Suspense fallback={<Loading />}>
<BrowserRouter>
<App />
</BrowserRouter>
</React.Suspense>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
//reportWebVitals();

14
src/reportWebVitals.js Normal file
View File

@ -0,0 +1,14 @@
/*const reportWebVitals = (onPerfEntry) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;
*/

5
src/setupTests.js Normal file
View File

@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
//import "@testing-library/jest-dom";

1393
src/utils.js Normal file

File diff suppressed because it is too large Load Diff

2
start.sh Executable file
View File

@ -0,0 +1,2 @@
screen -dmS customer-dashboard | exit 0
screen -S customer-dashboard -p 0 -X stuff 'npm start\n'