init project
commit
c3268bd71b
|
@ -0,0 +1,4 @@
|
|||
FROM nginx:latest
|
||||
|
||||
COPY ./build /usr/share/nginx/html
|
||||
COPY ./nginx.conf /etc/nginx/conf.d/default.conf
|
|
@ -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
|
|
@ -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"
|
|
@ -0,0 +1,7 @@
|
|||
git add *
|
||||
|
||||
read -p "Commit message: " commit_message
|
||||
|
||||
git commit -m "$commit_message"
|
||||
|
||||
git push -u origin main
|
|
@ -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;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
|
@ -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>
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 9.4 KiB |
|
@ -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"
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
*/
|
|
@ -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();
|
||||
});
|
||||
*/
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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 }} />;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
*/
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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
|
@ -0,0 +1,7 @@
|
|||
export default function Calendar() {
|
||||
return (
|
||||
<>
|
||||
<h1>Calendar</h1>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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;
|
|
@ -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()} />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
export default function Feedback() {
|
||||
return (
|
||||
<>
|
||||
<h1>Feedback</h1>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
export default function Services() {
|
||||
return (
|
||||
<>
|
||||
<h1>Services</h1>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
export default function Support() {
|
||||
return (
|
||||
<>
|
||||
<h1>Support</h1>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
|
@ -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;
|
||||
*/
|
|
@ -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";
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue