Updated with LuPa2 changes
This commit is contained in:
parent
d13e6bab44
commit
961bac2b0e
2
.env.development
Normal file
2
.env.development
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
VITE_APP_NAME=App Development
|
||||||
|
VITE_API_URL=http://localhost:7082
|
2
.env.production
Normal file
2
.env.production
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
VITE_APP_NAME=App Production
|
||||||
|
VITE_API_URL=http://192.168.179.36:7082
|
16
.eslintrc.cjs
Normal file
16
.eslintrc.cjs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
module.exports = {
|
||||||
|
env: { browser: true, es2020: true, node: true },
|
||||||
|
extends: [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:react-hooks/recommended",
|
||||||
|
"prettier",
|
||||||
|
],
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
parserOptions: { ecmaVersion: "latest", sourceType: "module" },
|
||||||
|
plugins: ["react-refresh", "prettier"],
|
||||||
|
rules: {
|
||||||
|
"react-refresh/only-export-components": "warn",
|
||||||
|
"prettier/prettier": "error",
|
||||||
|
},
|
||||||
|
};
|
12
.prettierrc.cjs
Normal file
12
.prettierrc.cjs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
module.exports = {
|
||||||
|
printWidth: 80,
|
||||||
|
trailingComma: "all",
|
||||||
|
singleQuote: false,
|
||||||
|
semi: true,
|
||||||
|
arrowParens: "always",
|
||||||
|
jsxSingleQuote: false,
|
||||||
|
bracketSameLine: false,
|
||||||
|
endOfLine: "lf",
|
||||||
|
useTabs: true,
|
||||||
|
tabWidth: 1,
|
||||||
|
};
|
24
Jenkinsfile
vendored
Normal file
24
Jenkinsfile
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
pipeline {
|
||||||
|
agent any
|
||||||
|
|
||||||
|
stages {
|
||||||
|
stage('Checkout') {
|
||||||
|
steps {
|
||||||
|
checkout scm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('Hello World') {
|
||||||
|
steps {
|
||||||
|
dir('frontend') {
|
||||||
|
sh 'echo "Hello World!"'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
// Cleanup the workspace
|
||||||
|
cleanWs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
daisyui.config.js
Normal file
31
daisyui.config.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
export const themes = [
|
||||||
|
"light",
|
||||||
|
"dark",
|
||||||
|
"cupcake",
|
||||||
|
"bumblebee",
|
||||||
|
"emerald",
|
||||||
|
"corporate",
|
||||||
|
"synthwave",
|
||||||
|
"retro",
|
||||||
|
"cyberpunk",
|
||||||
|
"valentine",
|
||||||
|
"halloween",
|
||||||
|
"garden",
|
||||||
|
"forest",
|
||||||
|
"aqua",
|
||||||
|
"lofi",
|
||||||
|
"pastel",
|
||||||
|
"fantasy",
|
||||||
|
"wireframe",
|
||||||
|
"black",
|
||||||
|
"luxury",
|
||||||
|
"dracula",
|
||||||
|
"cmyk",
|
||||||
|
"autumn",
|
||||||
|
"business",
|
||||||
|
"acid",
|
||||||
|
"lemonade",
|
||||||
|
"night",
|
||||||
|
"coffee",
|
||||||
|
"winter",
|
||||||
|
];
|
10
frontend.code-workspace
Normal file
10
frontend.code-workspace
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib"
|
||||||
|
}
|
||||||
|
}
|
12
index.html
Normal file
12
index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="icon" href="/icon.png" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
46
package.json
Normal file
46
package.json
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"name": "app",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --host",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"preview": "vite preview --host"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@hookform/resolvers": "^3.3.2",
|
||||||
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
|
"@types/react-router-dom": "^5.3.3",
|
||||||
|
"axios": "^1.5.1",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-hook-form": "^7.47.0",
|
||||||
|
"react-router-dom": "^6.17.0",
|
||||||
|
"uuid": "^9.0.1",
|
||||||
|
"zod": "^3.22.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/typography": "^0.5.10",
|
||||||
|
"@types/node": "^20.8.6",
|
||||||
|
"@types/react": "^18.2.28",
|
||||||
|
"@types/react-dom": "^18.2.13",
|
||||||
|
"@types/uuid": "^9.0.5",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.8.0",
|
||||||
|
"@typescript-eslint/parser": "^6.8.0",
|
||||||
|
"@vitejs/plugin-react": "^4.1.0",
|
||||||
|
"autoprefixer": "^10.4.16",
|
||||||
|
"daisyui": "^3.9.3",
|
||||||
|
"eslint": "^8.51.0",
|
||||||
|
"eslint-config-prettier": "^9.0.0",
|
||||||
|
"eslint-plugin-prettier": "^5.0.1",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.3",
|
||||||
|
"postcss": "^8.4.31",
|
||||||
|
"prettier": "^3.0.3",
|
||||||
|
"tailwindcss": "^3.3.3",
|
||||||
|
"typescript": "^5.2.2",
|
||||||
|
"vite": "^4.4.11"
|
||||||
|
}
|
||||||
|
}
|
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
BIN
public/icon.png
Executable file
BIN
public/icon.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 56 KiB |
98
src/api/AxiosService.ts
Normal file
98
src/api/AxiosService.ts
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
// TODO: Add calls for notifications on varius states of the request
|
||||||
|
|
||||||
|
export const setupAxiosInterceptors = () => {
|
||||||
|
axios.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
// Do something before request is sent
|
||||||
|
return Promise.resolve(config);
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
// Do something with request error
|
||||||
|
return Promise.reject(error);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
axios.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
// Do something with response data
|
||||||
|
const status = response.status;
|
||||||
|
if (status === 200 && response.data) {
|
||||||
|
console.error(response.data);
|
||||||
|
}
|
||||||
|
// Check if the status code is 401 (requires user authentication)
|
||||||
|
if (status === 401) {
|
||||||
|
console.error(response.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(response);
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
// Do something with response error
|
||||||
|
return Promise.reject(error);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAxiosError = (error: any, callbackFunction = null) => {
|
||||||
|
if (error.message == "canceled") {
|
||||||
|
callbackFunction && console.debug(callbackFunction);
|
||||||
|
console.debug("Request Aborted");
|
||||||
|
} else if (error.response) {
|
||||||
|
// Request made and server responded
|
||||||
|
console.log(error.response.data);
|
||||||
|
console.log(error.response.status);
|
||||||
|
console.log(error.response.headers);
|
||||||
|
} else if (error.request) {
|
||||||
|
// The request was made but no response was received
|
||||||
|
console.log(error.request);
|
||||||
|
} else {
|
||||||
|
// Something happened in setting up the request that triggered an Error
|
||||||
|
console.log("Error", error.message);
|
||||||
|
}
|
||||||
|
console.log(error.config);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const axiosGet = async (url: string) => {
|
||||||
|
return axios
|
||||||
|
.get(url)
|
||||||
|
.then((response) => {
|
||||||
|
return response.data;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
handleAxiosError(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const axiosPost = async (url: string, data: any) => {
|
||||||
|
return axios
|
||||||
|
.post(url, data)
|
||||||
|
.then((response) => {
|
||||||
|
return response.data;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
handleAxiosError(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const axiosPut = async (url: string, data: any) => {
|
||||||
|
return axios
|
||||||
|
.put(url, data)
|
||||||
|
.then((response) => {
|
||||||
|
return response.data;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
handleAxiosError(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const axiosDelete = async (url: string) => {
|
||||||
|
return axios
|
||||||
|
.delete(url)
|
||||||
|
.then((response) => {
|
||||||
|
return response.data;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
handleAxiosError(error);
|
||||||
|
});
|
||||||
|
};
|
72
src/components/Breadcrumbs.tsx
Normal file
72
src/components/Breadcrumbs.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { Link, useLocation, useParams } from "react-router-dom";
|
||||||
|
import { findElementInFlatRoutes } from "../utils/RoutingTableUtils";
|
||||||
|
import { capitalizeFirstLetter } from "../utils/StringTransformationUtils";
|
||||||
|
|
||||||
|
const BREADCRUMB_BAR_CLASSES =
|
||||||
|
"text-sm breadcrumbs max-w pl-6 shadow-neutral-focus shadow-sm sticky top-2 z-10 bg-neutral transition-all rounded-md m-2 w-auto";
|
||||||
|
|
||||||
|
const Breadcrumbs = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
const pathnames = location.pathname.split("/").filter((x) => x);
|
||||||
|
const pathParams = useParams();
|
||||||
|
|
||||||
|
const hideBreadcrumbBar = (path: string) => {
|
||||||
|
// if (Object.keys(pathParams).length !== 0) return true;
|
||||||
|
const _route = findElementInFlatRoutes(path);
|
||||||
|
return _route?.disableBreadcrumbBar || false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const findRouteNameByPath = (path: string) => {
|
||||||
|
const _route = findElementInFlatRoutes(path);
|
||||||
|
return _route?.name || path;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderLinkBreadcrumb = (
|
||||||
|
value: string,
|
||||||
|
index: number,
|
||||||
|
pathnames: string[],
|
||||||
|
) => {
|
||||||
|
if (value === pathParams.id) return null;
|
||||||
|
const to = `/${pathnames.slice(0, index + 1).join("/")}`;
|
||||||
|
const routeName = capitalizeFirstLetter(findRouteNameByPath(value));
|
||||||
|
return (
|
||||||
|
<li key={to} className="text-neutral-content">
|
||||||
|
<Link to={to}>{routeName}</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderTextBreadcrumb = (value: string) => {
|
||||||
|
if (value === pathParams.id) return null;
|
||||||
|
const routeName = capitalizeFirstLetter(findRouteNameByPath(value));
|
||||||
|
return (
|
||||||
|
<li key={value} className="text-neutral-content">
|
||||||
|
{routeName}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (hideBreadcrumbBar(location.pathname)) {
|
||||||
|
return (
|
||||||
|
<div className={BREADCRUMB_BAR_CLASSES}>{pathnames[0].toUpperCase()}</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={BREADCRUMB_BAR_CLASSES}>
|
||||||
|
<ul>
|
||||||
|
<li className="text-neutral-content">
|
||||||
|
<Link to="/">Home</Link>
|
||||||
|
</li>
|
||||||
|
{pathnames.map((value, index) => {
|
||||||
|
const isLast = index === pathnames.length - 1;
|
||||||
|
return isLast
|
||||||
|
? renderTextBreadcrumb(value)
|
||||||
|
: renderLinkBreadcrumb(value, index, pathnames);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Breadcrumbs;
|
35
src/components/Card.tsx
Normal file
35
src/components/Card.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
interface CardProps {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
bgImage?: string;
|
||||||
|
link: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Card component
|
||||||
|
* @param {string} title - Card title
|
||||||
|
* @param {string} description - Card description
|
||||||
|
* @param {string} bgImage - Background image
|
||||||
|
* @param {string} link - Link to open on button click
|
||||||
|
*/
|
||||||
|
export function Card({ title, description, bgImage, link }: CardProps) {
|
||||||
|
return (
|
||||||
|
<div className="card min-w-[230px] bg-neutral text-neutral-content">
|
||||||
|
{!!bgImage && (
|
||||||
|
<figure>
|
||||||
|
<img src={bgImage} alt={title + "_bgImage"} />
|
||||||
|
</figure>
|
||||||
|
)}
|
||||||
|
<div className="card-body items-center text-center">
|
||||||
|
<h2 className="card-title">{title}</h2>
|
||||||
|
<p>{description}</p>
|
||||||
|
<div className="card-actions justify-end">
|
||||||
|
<Link to={link || "/"} className="btn btn-primary">
|
||||||
|
Go
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
13
src/components/CardGrid.tsx
Normal file
13
src/components/CardGrid.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
const CardGrid = ({ children }: { children: ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center p-8">
|
||||||
|
<div className="grid gap-8 xl:grid-cols-4 lg:grid-cols-3 md:grid-cols-2 sm:grid-cols-1">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CardGrid;
|
39
src/components/ConfirmationDialog/ConfirmationDialog.tsx
Normal file
39
src/components/ConfirmationDialog/ConfirmationDialog.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import Modal from "../Modal";
|
||||||
|
import useConfirmationDialog from "./useConfirmationDialog";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description ConfirmationDialog component displays a confirmation dialog/modal with a message and two buttons: Yes and No.
|
||||||
|
* Actions of buttons are definded via hook.
|
||||||
|
* State of dialog is based on react context.
|
||||||
|
* @returns ConfirmationDialog component
|
||||||
|
*/
|
||||||
|
const ConfirmationDialog = () => {
|
||||||
|
const {
|
||||||
|
isConfirmationDialogVisible,
|
||||||
|
handleConfirm,
|
||||||
|
handleCancel,
|
||||||
|
customModalBoxStyles,
|
||||||
|
customModalStyles,
|
||||||
|
} = useConfirmationDialog();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isConfirmationDialogVisible && (
|
||||||
|
<Modal
|
||||||
|
modalId={"confirmation_dialog"}
|
||||||
|
customModalBoxStyles={customModalBoxStyles}
|
||||||
|
customModalStyles={customModalStyles}
|
||||||
|
>
|
||||||
|
<button className="btn btn-primary" onClick={handleConfirm}>
|
||||||
|
Yes
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-secondary" onClick={handleCancel}>
|
||||||
|
No
|
||||||
|
</button>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConfirmationDialog;
|
@ -0,0 +1,52 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { ConfirmationDialogContext } from "../../contexts/ConfirmationDialogContext";
|
||||||
|
|
||||||
|
export const ConfirmationDialogProvider = ({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
const [isConfirmationDialogVisible, setIsConfirmationDialogVisible] =
|
||||||
|
useState(false);
|
||||||
|
const [customModalBoxStyles, setCustomModalBoxStyles] = useState("");
|
||||||
|
const [customModalStyles, setCustomModalStyles] = useState("");
|
||||||
|
|
||||||
|
const showDialog = () => {
|
||||||
|
setIsConfirmationDialogVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hideDialog = () => {
|
||||||
|
setIsConfirmationDialogVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
// Logic for when the user confirms
|
||||||
|
// You can call external functions or dispatch actions here
|
||||||
|
hideDialog();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
// Logic for when the user cancels
|
||||||
|
hideDialog();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfirmationDialogContext.Provider
|
||||||
|
value={{
|
||||||
|
isConfirmationDialogVisible,
|
||||||
|
showDialog,
|
||||||
|
hideDialog,
|
||||||
|
handleConfirm,
|
||||||
|
handleCancel,
|
||||||
|
customModalBoxStyles,
|
||||||
|
customModalStyles,
|
||||||
|
setCustomModalBoxStyles,
|
||||||
|
setCustomModalStyles,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ConfirmationDialogContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConfirmationDialogProvider;
|
23
src/components/ConfirmationDialog/useConfirmationDialog.ts
Normal file
23
src/components/ConfirmationDialog/useConfirmationDialog.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { useContext } from "react";
|
||||||
|
import {
|
||||||
|
ConfirmationDialogContext,
|
||||||
|
ConfirmationDialogContextType,
|
||||||
|
} from "../../contexts/ConfirmationDialogContext";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Hook to manage the confirmation dialog, only passes the controls to specyfic dialog component
|
||||||
|
* @param onConfirm Function to be called when the user confirms the action
|
||||||
|
* @param onCancel Function to be called when the user cancels the action
|
||||||
|
* @returns Hook controls
|
||||||
|
*/
|
||||||
|
export const useConfirmationDialog = (): ConfirmationDialogContextType => {
|
||||||
|
const context = useContext(ConfirmationDialogContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
"useConfirmationDialog must be used within a ConfirmationDialogProvider",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useConfirmationDialog;
|
17
src/components/Loader.tsx
Normal file
17
src/components/Loader.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
function Loader() {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className="loading loading-ring loading-lg"
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "300%",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translate(-50%, -50%)",
|
||||||
|
height: "128px",
|
||||||
|
width: "128px",
|
||||||
|
}}
|
||||||
|
></span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Loader;
|
109
src/components/Modal.tsx
Normal file
109
src/components/Modal.tsx
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import { useCallback, useEffect } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import useConfirmationDialog from "./ConfirmationDialog/useConfirmationDialog";
|
||||||
|
import { Cross1Icon } from "@radix-ui/react-icons";
|
||||||
|
|
||||||
|
interface ModalProps {
|
||||||
|
modalId: string;
|
||||||
|
title?: string;
|
||||||
|
subTitle?: string;
|
||||||
|
content?: string;
|
||||||
|
navigatePathOnClose?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
customModalStyles?: string;
|
||||||
|
customModalBoxStyles?: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @description A modal component that can be used to display a fairly custom modal/dialog
|
||||||
|
* @param modalId The id of the modal
|
||||||
|
* @param title The title of the modal
|
||||||
|
* @param subTitle The subtitle of the modal
|
||||||
|
* @param content The content of the modal
|
||||||
|
* @param navigatePathOnClose The path to navigate to when the modal is closed
|
||||||
|
* @param children The children of the modal
|
||||||
|
* @param customModalStyles Custom styles for the modal
|
||||||
|
* @param customModalBoxStyles Custom styles for the modal box
|
||||||
|
* @returns A modal component
|
||||||
|
*/
|
||||||
|
export const Modal = ({
|
||||||
|
modalId,
|
||||||
|
navigatePathOnClose,
|
||||||
|
title,
|
||||||
|
subTitle,
|
||||||
|
content,
|
||||||
|
children,
|
||||||
|
customModalStyles,
|
||||||
|
customModalBoxStyles,
|
||||||
|
}: ModalProps) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { isConfirmationDialogVisible, hideDialog } = useConfirmationDialog();
|
||||||
|
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
if (navigatePathOnClose) navigate(navigatePathOnClose);
|
||||||
|
if (isConfirmationDialogVisible)
|
||||||
|
(document.getElementById(modalId) as HTMLDialogElement).close();
|
||||||
|
hideDialog();
|
||||||
|
}, [
|
||||||
|
hideDialog,
|
||||||
|
isConfirmationDialogVisible,
|
||||||
|
modalId,
|
||||||
|
navigate,
|
||||||
|
navigatePathOnClose,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleKeyPress = useCallback(
|
||||||
|
(event: KeyboardEvent) => {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
handleClose();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[handleClose],
|
||||||
|
);
|
||||||
|
// Handle modal open
|
||||||
|
useEffect(() => {
|
||||||
|
if (!modalId) return;
|
||||||
|
(document.getElementById(modalId) as HTMLDialogElement).showModal();
|
||||||
|
document.addEventListener("keydown", handleKeyPress);
|
||||||
|
// Handle modal close
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("keydown", handleKeyPress);
|
||||||
|
handleClose;
|
||||||
|
};
|
||||||
|
}, [handleClose, handleKeyPress, modalId]);
|
||||||
|
|
||||||
|
const composeModalStyles = useCallback(() => {
|
||||||
|
const _baseStyles = "modal modal-bottom sm:modal-middle w-screen z-50";
|
||||||
|
if (customModalStyles) return _baseStyles + " " + customModalStyles;
|
||||||
|
return _baseStyles;
|
||||||
|
}, [customModalStyles]);
|
||||||
|
const comopseModalBoxStyles = useCallback(() => {
|
||||||
|
const _baseStyles = "modal-box";
|
||||||
|
if (customModalBoxStyles) return _baseStyles + " " + customModalBoxStyles;
|
||||||
|
return _baseStyles;
|
||||||
|
}, [customModalBoxStyles]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<dialog id={modalId} className={composeModalStyles()}>
|
||||||
|
<div className={comopseModalBoxStyles()}>
|
||||||
|
<h3 className="font-bold text-lg">{title ?? "Hello!"}</h3>
|
||||||
|
<p className="py-4">
|
||||||
|
{subTitle ?? "Press ESC key or click the button below to close"}
|
||||||
|
</p>
|
||||||
|
<p>{content}</p>
|
||||||
|
<div className="modal-action">
|
||||||
|
<form method="dialog">
|
||||||
|
<button
|
||||||
|
className="btn btn-circle btn-sm absolute top-2 right-2"
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
|
<Cross1Icon />
|
||||||
|
</button>
|
||||||
|
{children}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Modal;
|
79
src/components/NavigationTree.tsx
Normal file
79
src/components/NavigationTree.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { CustomRouteObject } from "../configure";
|
||||||
|
import { Link, useLocation } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
clearMultiplePathSlashes,
|
||||||
|
trimPathOfParameters,
|
||||||
|
} from "../utils/StringTransformationUtils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns Navigation tree elements, require to be used like in example below
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* <ul className="menu bg-base-200 w-56 h-full">
|
||||||
|
* <NavigationTree routes={navigation} />
|
||||||
|
* </ul>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
function NavigationTree(props: {
|
||||||
|
routes: CustomRouteObject[];
|
||||||
|
}): React.JSX.Element {
|
||||||
|
const locationHook = useLocation(); // Used to highlight active link in navigation tree
|
||||||
|
|
||||||
|
const GenerateNavigationEntries = (
|
||||||
|
routes: CustomRouteObject[],
|
||||||
|
parentPath?: string,
|
||||||
|
): React.ReactNode => {
|
||||||
|
return (
|
||||||
|
routes.map((route) => {
|
||||||
|
// Prepare path for links
|
||||||
|
let combinedPath = undefined;
|
||||||
|
if (parentPath !== undefined && route.path !== undefined)
|
||||||
|
combinedPath = trimPathOfParameters(
|
||||||
|
clearMultiplePathSlashes(`/${parentPath}/${route.path}`),
|
||||||
|
);
|
||||||
|
else combinedPath = route.path;
|
||||||
|
// Does it have children and enabled? Make entry with `/{parent.path}/{route.path}`
|
||||||
|
if (route.children && !route.additionalProps.disableInNavbar) {
|
||||||
|
return (
|
||||||
|
<ul key={route.path}>
|
||||||
|
<li key={route.path}>
|
||||||
|
<Link
|
||||||
|
to={combinedPath || "/"}
|
||||||
|
className={locationHook.pathname === combinedPath ? "active" : ""}
|
||||||
|
>
|
||||||
|
{route.additionalProps.name}
|
||||||
|
</Link>
|
||||||
|
{route.children ? (
|
||||||
|
<ul>{GenerateNavigationEntries(route.children, combinedPath)}</ul>
|
||||||
|
) : null}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Does it have children and not visible? Skip this entry and call this function for children passing path.
|
||||||
|
else if (route.children && route.additionalProps.disableInNavbar) {
|
||||||
|
return GenerateNavigationEntries(route.children, combinedPath);
|
||||||
|
} else if (route.additionalProps.disableInNavbar) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Make entry with `/{route.path}`
|
||||||
|
else {
|
||||||
|
return (
|
||||||
|
<li key={route.path}>
|
||||||
|
<Link
|
||||||
|
to={combinedPath || "/"}
|
||||||
|
className={locationHook.pathname === combinedPath ? "active" : ""}
|
||||||
|
>
|
||||||
|
{route.additionalProps.name}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}) || <>empty navigation tree</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return <div className="h-fit">{GenerateNavigationEntries(props.routes)}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NavigationTree;
|
136
src/components/Notification/Notification.tsx
Normal file
136
src/components/Notification/Notification.tsx
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { INotification, NotificationStatus } from "./NotificationType";
|
||||||
|
|
||||||
|
enum NotificationColorsClasses {
|
||||||
|
"info" = "alert alert-info",
|
||||||
|
"success" = "alert alert-success",
|
||||||
|
"warning" = "alert alert-warning",
|
||||||
|
"error" = "alert alert-error",
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectIcon = (status: NotificationStatus) => {
|
||||||
|
switch (status) {
|
||||||
|
case "error":
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="stroke-current shrink-0 h-6 w-6"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
case "success":
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="stroke-current shrink-0 h-6 w-6"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "info":
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
className="stroke-current shrink-0 w-6 h-6"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "warning":
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="stroke-current shrink-0 h-6 w-6"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
interface NotificationProps {
|
||||||
|
notification: INotification;
|
||||||
|
removeNotification: (id: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Notification({ notification, removeNotification }: NotificationProps) {
|
||||||
|
const [counter, setCounter] = useState<number | undefined>(
|
||||||
|
notification.duration,
|
||||||
|
);
|
||||||
|
const isCloseable = !!!notification.duration;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Skip if counter is undefined
|
||||||
|
if (counter === undefined) return;
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
setCounter(counter - 1);
|
||||||
|
if (counter === 0) {
|
||||||
|
removeNotification(notification.id);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
return () => {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
};
|
||||||
|
}, [counter]);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={NotificationColorsClasses[notification.status]}
|
||||||
|
key={notification.id}
|
||||||
|
>
|
||||||
|
{selectIcon(notification.status)}
|
||||||
|
<span>{notification.message}</span>
|
||||||
|
{isCloseable ? (
|
||||||
|
<button
|
||||||
|
className="btn btn-sm"
|
||||||
|
onClick={() => removeNotification(notification.id)}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
{isCloseable ? null : (
|
||||||
|
<div className="flex flex-col p-2 bg-neutral rounded-box text-neutral-content">
|
||||||
|
<span className="countdown">
|
||||||
|
<span style={{ "--value": counter } as React.CSSProperties}></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Notification;
|
37
src/components/Notification/NotificationProvider.tsx
Normal file
37
src/components/Notification/NotificationProvider.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { INotification } from "./NotificationType";
|
||||||
|
|
||||||
|
interface INotificationStore {
|
||||||
|
notifications: INotification[] | [];
|
||||||
|
setNotifications: React.Dispatch<React.SetStateAction<[] | INotification[]>>;
|
||||||
|
duration?: number;
|
||||||
|
}
|
||||||
|
// Store for notifications
|
||||||
|
export const NotificationContext = React.createContext<INotificationStore>({
|
||||||
|
notifications: [],
|
||||||
|
setNotifications: () => {},
|
||||||
|
duration: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Just store for notifications, logic is in useNotification hook
|
||||||
|
export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
// Queue for notifications
|
||||||
|
const [notifications, setNotifications] = useState<Array<INotification> | []>(
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
NotificationContext.displayName = "Notifications";
|
||||||
|
return (
|
||||||
|
<NotificationContext.Provider
|
||||||
|
value={{
|
||||||
|
notifications,
|
||||||
|
setNotifications,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</NotificationContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotificationProvider;
|
25
src/components/Notification/NotificationStack.tsx
Normal file
25
src/components/Notification/NotificationStack.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import Notification from "./Notification";
|
||||||
|
import { useNotification } from "./useNotification";
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
// Component that renders notification stack
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
function NotificationStack() {
|
||||||
|
const { notifications, removeNotification } = useNotification();
|
||||||
|
|
||||||
|
// It takes notification array from hook and renders it
|
||||||
|
const renderNotifications = () => {
|
||||||
|
return notifications.map((notification) => {
|
||||||
|
return (
|
||||||
|
<Notification
|
||||||
|
key={notification.id}
|
||||||
|
notification={notification}
|
||||||
|
removeNotification={removeNotification}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return <div className="toast toast-end">{renderNotifications()}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NotificationStack;
|
9
src/components/Notification/NotificationType.ts
Normal file
9
src/components/Notification/NotificationType.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export type NotificationStatus = "success" | "error" | "warning" | "info";
|
||||||
|
export interface INotificationBase {
|
||||||
|
message: string;
|
||||||
|
status: NotificationStatus;
|
||||||
|
duration?: number;
|
||||||
|
}
|
||||||
|
export interface INotification extends INotificationBase {
|
||||||
|
id: number;
|
||||||
|
}
|
2
src/components/Notification/README.md
Normal file
2
src/components/Notification/README.md
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Notification
|
||||||
|
I wrote own notification module but its little bit laggy and buggy so I don't use it.
|
34
src/components/Notification/useNotification.ts
Normal file
34
src/components/Notification/useNotification.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { useContext } from "react";
|
||||||
|
import { NotificationContext } from "./NotificationProvider";
|
||||||
|
import { INotification, NotificationStatus } from "./NotificationType";
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
// Hook for adding notifications to the queue in local storage
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
export function useNotification() {
|
||||||
|
const { notifications, setNotifications } = useContext(NotificationContext);
|
||||||
|
|
||||||
|
const addNotificationToQueue = (
|
||||||
|
message: string,
|
||||||
|
status: NotificationStatus,
|
||||||
|
duration?: number,
|
||||||
|
) => {
|
||||||
|
const id = notifications.length + 1;
|
||||||
|
const notification: INotification = { id, message, status, duration };
|
||||||
|
setNotifications([...notifications, notification]);
|
||||||
|
};
|
||||||
|
const removeNotificationFromQueue = (id: number) => {
|
||||||
|
setNotifications(
|
||||||
|
notifications.filter((notification) => notification.id !== id),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const clearNotificationsQueue = () => {
|
||||||
|
setNotifications([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
notifications: notifications,
|
||||||
|
addNotification: addNotificationToQueue,
|
||||||
|
removeNotification: removeNotificationFromQueue,
|
||||||
|
clearNotifications: clearNotificationsQueue,
|
||||||
|
};
|
||||||
|
}
|
191
src/configure.tsx
Normal file
191
src/configure.tsx
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
/* eslint-disable react-refresh/only-export-components */
|
||||||
|
import { Suspense, lazy } from "react";
|
||||||
|
import { RouteObject } from "react-router-dom";
|
||||||
|
import Loader from "./components/Loader";
|
||||||
|
import AboutPage from "./features/About/AboutPage";
|
||||||
|
import PersonManager from "./features/Administration/PersonManager";
|
||||||
|
import App from "./features/App/App";
|
||||||
|
import LoginPage from "./features/Login/LoginPage";
|
||||||
|
import { flatternRoutingTable } from "./utils/RoutingTableUtils";
|
||||||
|
import AssetManagement from "./features/Administration/AssetManagement";
|
||||||
|
import Debugger from "./features/Debugger";
|
||||||
|
//----
|
||||||
|
const AdministrationLazy = lazy(
|
||||||
|
() => import("./features/Administration/Administration"),
|
||||||
|
);
|
||||||
|
const HomePageLazy = lazy(() => import("./features/Home/HomePage"));
|
||||||
|
const MaintenanceLazy = lazy(
|
||||||
|
() => import("./features/Maintenance/Maintenance"),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const viteEnv = import.meta.env;
|
||||||
|
|
||||||
|
// Based on https://reactrouter.com/en/main/start/overview#nested-routes with additional props
|
||||||
|
export const navigation: CustomRouteObject[] = [
|
||||||
|
// root page, mainframe for all pages, skipped in navigation tree generation (inly childs are used)
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
element: <App />,
|
||||||
|
additionalProps: {
|
||||||
|
name: "Root", // used for breadcrumbs and navigation tree
|
||||||
|
disableRedirect: false, //entry will not be included in the react-router redirect list, will cut out childs
|
||||||
|
disableInNavbar: true, //entry will not be rendered in the navbar
|
||||||
|
disableBreadcrumbBar: false, //entry will be directly under root in routing table
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
// home page
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
index: true, //if true will be used as a default route for parent, dont declare path if use this
|
||||||
|
element: (
|
||||||
|
<Suspense fallback={<Loader />}>
|
||||||
|
<HomePageLazy />
|
||||||
|
</Suspense>
|
||||||
|
),
|
||||||
|
additionalProps: {
|
||||||
|
name: "Home",
|
||||||
|
disableRedirect: false,
|
||||||
|
disableInNavbar: false,
|
||||||
|
disableBreadcrumbBar: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "maintenance",
|
||||||
|
element: (
|
||||||
|
<Suspense fallback={<Loader />}>
|
||||||
|
<MaintenanceLazy />
|
||||||
|
</Suspense>
|
||||||
|
),
|
||||||
|
additionalProps: {
|
||||||
|
name: "Maintenance",
|
||||||
|
disableRedirect: false,
|
||||||
|
disableInNavbar: false,
|
||||||
|
disableBreadcrumbBar: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "administration",
|
||||||
|
element: (
|
||||||
|
<Suspense fallback={<Loader />}>
|
||||||
|
<AdministrationLazy />
|
||||||
|
</Suspense>
|
||||||
|
),
|
||||||
|
additionalProps: {
|
||||||
|
name: "Administration",
|
||||||
|
disableRedirect: false,
|
||||||
|
disableInNavbar: false,
|
||||||
|
disableBreadcrumbBar: false,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "asset_management/:state?/:id?",
|
||||||
|
element: <AssetManagement />,
|
||||||
|
additionalProps: {
|
||||||
|
name: "Asset Management",
|
||||||
|
disableRedirect: false,
|
||||||
|
disableInNavbar: false,
|
||||||
|
disableBreadcrumbBar: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "person_management",
|
||||||
|
element: <PersonManager />,
|
||||||
|
additionalProps: {
|
||||||
|
name: "Person Management",
|
||||||
|
disableRedirect: false,
|
||||||
|
disableInNavbar: false,
|
||||||
|
disableBreadcrumbBar: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// about page
|
||||||
|
{
|
||||||
|
path: "about",
|
||||||
|
element: <AboutPage />,
|
||||||
|
additionalProps: {
|
||||||
|
name: "About",
|
||||||
|
disableRedirect: false,
|
||||||
|
disableInNavbar: false,
|
||||||
|
disableBreadcrumbBar: false,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "child",
|
||||||
|
element: <div>DUPA CHILD</div>,
|
||||||
|
additionalProps: {
|
||||||
|
name: "About Child",
|
||||||
|
disableRedirect: false,
|
||||||
|
disableInNavbar: false,
|
||||||
|
disableBreadcrumbBar: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// login page
|
||||||
|
{
|
||||||
|
path: "login",
|
||||||
|
element: <LoginPage />,
|
||||||
|
additionalProps: {
|
||||||
|
name: "Login",
|
||||||
|
disableRedirect: false,
|
||||||
|
disableInNavbar: false,
|
||||||
|
disableBreadcrumbBar: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
// ---
|
||||||
|
|
||||||
|
if (viteEnv.DEV) {
|
||||||
|
// Add Test page
|
||||||
|
// Test page is outside of mainframe, will be rendered in root (withius App where is entire navigation frame)
|
||||||
|
navigation.push({
|
||||||
|
path: "/Test",
|
||||||
|
element: <div>Test</div>,
|
||||||
|
additionalProps: {
|
||||||
|
name: "Test",
|
||||||
|
disableRedirect: false,
|
||||||
|
disableInNavbar: false,
|
||||||
|
disableBreadcrumbBar: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// Add debugger page
|
||||||
|
navigation[0].children?.push({
|
||||||
|
path: "debug_variables",
|
||||||
|
element: <Debugger />,
|
||||||
|
additionalProps: {
|
||||||
|
name: "Debug Variables",
|
||||||
|
disableRedirect: false,
|
||||||
|
disableInNavbar: false,
|
||||||
|
disableBreadcrumbBar: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---
|
||||||
|
//Custom Route Object for handling custom behaviours on app navigation
|
||||||
|
export interface RouteObjectAdditionalProps {
|
||||||
|
name: string;
|
||||||
|
disableRedirect: boolean;
|
||||||
|
disableInNavbar: boolean;
|
||||||
|
disableBreadcrumbBar: boolean;
|
||||||
|
}
|
||||||
|
export interface CustomRouteObject extends Omit<RouteObject, "children"> {
|
||||||
|
additionalProps: RouteObjectAdditionalProps;
|
||||||
|
children?: CustomRouteObject[];
|
||||||
|
}
|
||||||
|
//----
|
||||||
|
//Main configuration, static data, mostly used here
|
||||||
|
export const main = {
|
||||||
|
program_name: viteEnv.VITE_APP_NAME,
|
||||||
|
program_version: "1.0.0",
|
||||||
|
};
|
||||||
|
//About page configuration
|
||||||
|
export const about = {
|
||||||
|
program_description: `This is a ${main.program_name} for <purpose>.`,
|
||||||
|
program_authors: [{ name: "Author Name", email: "email@example.com" }],
|
||||||
|
};
|
||||||
|
//----
|
||||||
|
export const flatRoutes = flatternRoutingTable(navigation);
|
19
src/contexts/ConfirmationDialogContext.tsx
Normal file
19
src/contexts/ConfirmationDialogContext.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { createContext } from "react";
|
||||||
|
|
||||||
|
// Define the shape of the context
|
||||||
|
export type ConfirmationDialogContextType = {
|
||||||
|
isConfirmationDialogVisible: boolean;
|
||||||
|
showDialog: () => void;
|
||||||
|
hideDialog: () => void;
|
||||||
|
handleConfirm: () => void;
|
||||||
|
handleCancel: () => void;
|
||||||
|
customModalStyles: string;
|
||||||
|
customModalBoxStyles: string;
|
||||||
|
setCustomModalBoxStyles: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
setCustomModalStyles: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
};
|
||||||
|
// Create the context with default values
|
||||||
|
export const ConfirmationDialogContext = createContext<
|
||||||
|
ConfirmationDialogContextType | undefined
|
||||||
|
>(undefined);
|
||||||
|
ConfirmationDialogContext.displayName = "ConfirmationDialog";
|
32
src/features/About/AboutPage.tsx
Normal file
32
src/features/About/AboutPage.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { about } from "../../configure";
|
||||||
|
|
||||||
|
/** Here is located about page where you can find information about this application:
|
||||||
|
* - short description of this application
|
||||||
|
* - how to use it
|
||||||
|
* - information about persons responsible for maintaining this application
|
||||||
|
*/
|
||||||
|
function AboutPage() {
|
||||||
|
return (
|
||||||
|
<div className="hero ">
|
||||||
|
<div className="hero-content text-center">
|
||||||
|
<div className="max-w-md">
|
||||||
|
<h1 className="text-5xl font-bold">About</h1>
|
||||||
|
<p className="py-6">{about.program_description}</p>
|
||||||
|
<h2 className="text-2xl font-bold pt-10">Authors</h2>
|
||||||
|
<div className="py-6">
|
||||||
|
{about.program_authors.map((author) => (
|
||||||
|
<div key={author.name}>
|
||||||
|
{author.name} -{" "}
|
||||||
|
<a href={"mailto:" + author.email} className="link">
|
||||||
|
{author.email}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AboutPage;
|
108
src/features/App/App.tsx
Normal file
108
src/features/App/App.tsx
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import { HamburgerMenuIcon, SunIcon } from "@radix-ui/react-icons";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Outlet } from "react-router-dom";
|
||||||
|
import { themes } from "../../../daisyui.config.js";
|
||||||
|
import { main, navigation } from "../../configure.tsx";
|
||||||
|
import NavigationTree from "../../components/NavigationTree.tsx";
|
||||||
|
import Breadcrumbs from "../../components/Breadcrumbs.tsx";
|
||||||
|
import ConfirmationDialog from "../../components/ConfirmationDialog/ConfirmationDialog.tsx";
|
||||||
|
|
||||||
|
/** Here is located global wrapper for entire application, here you canfind:
|
||||||
|
* - Drawer - contains navigation buttons
|
||||||
|
* - Navbar - contains hamburger menu and theme selector
|
||||||
|
* - Outlet - contains active page content
|
||||||
|
* - App theme controll
|
||||||
|
*/
|
||||||
|
function App() {
|
||||||
|
const [theme, setTheme] = useState<string>("adient");
|
||||||
|
const [openDrawer, setOpenDrawer] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.querySelector("html")?.setAttribute("data-theme", theme);
|
||||||
|
// To resolve this issue, you can use the import.meta.env object instead of process.env. The import.meta.env object is provided by Vite.js and allows you to access environment variables in your code.
|
||||||
|
document.title = import.meta.env.VITE_APP_NAME;
|
||||||
|
}, [theme, openDrawer]);
|
||||||
|
|
||||||
|
// Function on click drawer hamburger button
|
||||||
|
const handleDrawerStatus = () => {
|
||||||
|
setOpenDrawer(!openDrawer);
|
||||||
|
};
|
||||||
|
// Function on click theme on themes list
|
||||||
|
const handleThemeChange = (theme: string) => {
|
||||||
|
setTheme(theme);
|
||||||
|
console.log("Theme changed to " + theme, "info", 10);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ConfirmationDialog />
|
||||||
|
{/* Root drawer container */}
|
||||||
|
{/* Drawer opening is controlled directly via css prop and next via local useState variable */}
|
||||||
|
<div className={"drawer " + (openDrawer ? "lg:drawer-open" : "")}>
|
||||||
|
{/* A hidden checkbox to toggle the visibility of the drawer */}
|
||||||
|
<input
|
||||||
|
id="my-drawer"
|
||||||
|
type="checkbox"
|
||||||
|
placeholder="drawer-checkbox"
|
||||||
|
className="drawer-toggle"
|
||||||
|
checked={openDrawer}
|
||||||
|
onChange={handleDrawerStatus}
|
||||||
|
/>
|
||||||
|
{/* The actual drawer content */}
|
||||||
|
<div className="drawer-content">
|
||||||
|
{/* Navbar */}
|
||||||
|
<div className="navbar bg-neutral text-neutral-content w-auto">
|
||||||
|
{/* Left side navbar */}
|
||||||
|
<div className="flex-1">
|
||||||
|
<label
|
||||||
|
htmlFor="my-drawer"
|
||||||
|
className="btn btn-circle btn-neutral drawer-button"
|
||||||
|
>
|
||||||
|
<HamburgerMenuIcon className="w-6 h-6" />
|
||||||
|
</label>
|
||||||
|
<p className="text-lg font-bold ml-8">{main.program_name}</p>
|
||||||
|
</div>
|
||||||
|
{/* Right side navbar */}
|
||||||
|
<div className="flex-none gap-2">
|
||||||
|
<div className="dropdown dropdown-bottom dropdown-end">
|
||||||
|
<button tabIndex={0} className="btn btn-circle m-1">
|
||||||
|
<SunIcon className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
<ul
|
||||||
|
tabIndex={0}
|
||||||
|
className="menu dropdown-content z-[12] p-2 shadow bg-base-100 rounded-box w-fit mt-4 max-h-24 sm:max-h-96 flex flex-col items-center flex-nowrap overflow-y-auto"
|
||||||
|
>
|
||||||
|
{themes.map((theme) => (
|
||||||
|
<li key={theme}>
|
||||||
|
<a className="prose" onClick={() => handleThemeChange(theme)}>
|
||||||
|
{theme}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<button className="btn m-1">Log out</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* App/active_drawer content */}
|
||||||
|
<div className="drawer-content">
|
||||||
|
{/*Automatically generated breadcrumbs based on routing table from configuration file and active path*/}
|
||||||
|
{/*Get active route and find it in routing file*/}
|
||||||
|
<Breadcrumbs />
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Drawer sidebar wrapper */}
|
||||||
|
<div className="drawer-side z-20">
|
||||||
|
{/* Dark overlay on mobile devices, clickable to close drawer */}
|
||||||
|
<label htmlFor="my-drawer" className="drawer-overlay"></label>
|
||||||
|
<ul className="menu bg-base-200 w-56 h-full overflow-auto">
|
||||||
|
<NavigationTree routes={navigation} />
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
31
src/features/Debugger.tsx
Normal file
31
src/features/Debugger.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
import { flatRoutes } from "../configure";
|
||||||
|
|
||||||
|
function Debugger() {
|
||||||
|
const windowLocation = window.location.pathname;
|
||||||
|
const locationHook = useLocation();
|
||||||
|
return (
|
||||||
|
<div className="m-auto p-4">
|
||||||
|
<details className="collapse bg-base-200 mb-4">
|
||||||
|
<summary className="collapse-title text-xl font-medium">
|
||||||
|
Flat Routes
|
||||||
|
</summary>
|
||||||
|
<code className="collapse-content">{JSON.stringify(flatRoutes)}</code>
|
||||||
|
</details>
|
||||||
|
<details className="collapse bg-base-200 mb-4">
|
||||||
|
<summary className="collapse-title text-xl font-medium">Locations</summary>
|
||||||
|
<div className="collapse-content">
|
||||||
|
<code>Location hook: {JSON.stringify(locationHook)}</code>
|
||||||
|
<br />
|
||||||
|
<code>Location window: {JSON.stringify(windowLocation)}</code>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
<details className="collapse bg-base-200 mb-4">
|
||||||
|
<summary className="collapse-title text-xl font-medium">Routes</summary>
|
||||||
|
<code className="collapse-content">{JSON.stringify(flatRoutes)}</code>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Debugger;
|
5
src/features/Home/HomePage.tsx
Normal file
5
src/features/Home/HomePage.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
function HomePage() {
|
||||||
|
return <>HOME</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HomePage;
|
102
src/features/Login/LoginPage.tsx
Normal file
102
src/features/Login/LoginPage.tsx
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
type Input = {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
const InputSchema = z.object({
|
||||||
|
username: z.string().min(4, "Username must be at least 4 characters"),
|
||||||
|
password: z
|
||||||
|
.string()
|
||||||
|
.regex(/[A-Za-z\d@$!%*#?&]{4,}/, "Minimum four characters")
|
||||||
|
.regex(/(?=.*[A-Z])/, "At least one big letter")
|
||||||
|
.regex(/(?=.*\d)/, "At least one number")
|
||||||
|
.regex(/(?=.*[@$!%*#?&])/, "At least one special character"),
|
||||||
|
});
|
||||||
|
|
||||||
|
function LoginPage() {
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<Input>({
|
||||||
|
resolver: zodResolver(InputSchema),
|
||||||
|
});
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const onSubmit: SubmitHandler<Input> = (data) => {
|
||||||
|
if (data.username === "admin" && data.password === "A0m!n") {
|
||||||
|
console.log("Login successful!", 3);
|
||||||
|
navigate("/products");
|
||||||
|
} else {
|
||||||
|
console.log("Wrong username or password");
|
||||||
|
}
|
||||||
|
setIsSubmitting(false);
|
||||||
|
};
|
||||||
|
const onError: SubmitErrorHandler<Input> = () => {
|
||||||
|
console.log("Errors in form fields");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-4">
|
||||||
|
<form onSubmit={handleSubmit(onSubmit, onError)}>
|
||||||
|
<fieldset className="flex flex-col content-around justify-center items-center space-y-4">
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="input-group">
|
||||||
|
<span className="w-32 flex-none">Username</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="username"
|
||||||
|
autoComplete="username"
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
{...register("username")}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
{errors.username && (
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text-alt">{errors.username?.message}</span>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="input-group">
|
||||||
|
<span className="w-32 flex-none">Password</span>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
placeholder="password"
|
||||||
|
autoComplete="current-password"
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
{...register("password")}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
{errors.password && (
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text-alt">
|
||||||
|
Password must have: {errors.password?.message}{" "}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary btn-wide"
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting && (
|
||||||
|
<span className="loading loading-spinner loading-lg"></span>
|
||||||
|
)}{" "}
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoginPage;
|
3
src/main.css
Normal file
3
src/main.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
21
src/main.tsx
Normal file
21
src/main.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import { RouterProvider } from "react-router-dom";
|
||||||
|
import { setupAxiosInterceptors } from "./api/AxiosService";
|
||||||
|
import "./main.css";
|
||||||
|
import router from "./routes";
|
||||||
|
import ConfirmationDialogProvider from "./components/ConfirmationDialog/ConfirmationDialogProvider";
|
||||||
|
|
||||||
|
setupAxiosInterceptors();
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
{/* fallbackElement - use when routing table is quite big to load, it will display desired element in convention of loading screen */}
|
||||||
|
<ConfirmationDialogProvider>
|
||||||
|
<RouterProvider
|
||||||
|
router={router}
|
||||||
|
fallbackElement={<p>Loading route table...</p>}
|
||||||
|
/>
|
||||||
|
</ConfirmationDialogProvider>
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
69
src/routes.tsx
Normal file
69
src/routes.tsx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
/**
|
||||||
|
* This file is used to configure react-router-dom.
|
||||||
|
* Do not change anything here, unless you know what you are doing.
|
||||||
|
* Every path and route is configured in `configure.tsx` file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createBrowserRouter, RouteObject } from "react-router-dom";
|
||||||
|
import { CustomRouteObject, navigation } from "./configure.tsx";
|
||||||
|
import Redirect from "./utils/Redirect.tsx";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is used to convert `CustomRouteObject` to `RouteObject` and process all additionalProps
|
||||||
|
*/
|
||||||
|
const createRoutingFromCustomRouteObject = (
|
||||||
|
routes: CustomRouteObject[],
|
||||||
|
): RouteObject[] => {
|
||||||
|
const navigationRoutes: RouteObject[] = []; //Root routes that will be used for navigation
|
||||||
|
routes.forEach((route) => {
|
||||||
|
// Extract everything except additionalProps and children
|
||||||
|
const { additionalProps, children, ...routeNative } = route;
|
||||||
|
//Filter routes that are disabled for navigation
|
||||||
|
const isRouteDisabled = additionalProps.disableRedirect;
|
||||||
|
if (isRouteDisabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//Add route to navigationRoutes, array that will be returned
|
||||||
|
const isRouteWithChildren = children !== undefined && children.length > 0;
|
||||||
|
if (!isRouteWithChildren) {
|
||||||
|
navigationRoutes.push({
|
||||||
|
...routeNative,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
navigationRoutes.push({
|
||||||
|
path: route.path,
|
||||||
|
element: route.element,
|
||||||
|
children: createRoutingFromCustomRouteObject(children),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
//In case of empty routes, add a default route with message
|
||||||
|
if (navigationRoutes.length === 0) {
|
||||||
|
navigationRoutes.push({
|
||||||
|
path: "/",
|
||||||
|
element: <div>Empty routes</div>,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//Always add redirect unmached route to root at the end
|
||||||
|
return navigationRoutes;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** It is main variable that handle all navigation rules and paths */
|
||||||
|
export const routes: RouteObject[] = [
|
||||||
|
...createRoutingFromCustomRouteObject(navigation),
|
||||||
|
{
|
||||||
|
path: "*",
|
||||||
|
element: <Redirect to="/" />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/** It is variable that have routes for react-router-dom, final form */
|
||||||
|
export const router = createBrowserRouter(routes, {
|
||||||
|
basename: "/",
|
||||||
|
future: {
|
||||||
|
// Normalize `useNavigation()`/`useFetcher()` `formMethod` to uppercase
|
||||||
|
v7_normalizeFormMethod: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
9
src/utils/ObjectUtils.ts
Normal file
9
src/utils/ObjectUtils.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export const rollThroughObj = (
|
||||||
|
obj: Readonly<Partial<{ state: string; id: string }>>,
|
||||||
|
) => {
|
||||||
|
let result = "";
|
||||||
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
|
result += ` -${key}: ${value}`;
|
||||||
|
}
|
||||||
|
return result.trim();
|
||||||
|
};
|
18
src/utils/Redirect.tsx
Normal file
18
src/utils/Redirect.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Redirects to the given page
|
||||||
|
* @param {string} to - The page to redirect to
|
||||||
|
*/
|
||||||
|
function Redirect({ to }: { to: string }) {
|
||||||
|
// The navigate function from useNavigate is used to navigate to the given page
|
||||||
|
const navigate = useNavigate();
|
||||||
|
// useEffect is used to navigate to the given page when the component mounts
|
||||||
|
useEffect(() => {
|
||||||
|
navigate(to);
|
||||||
|
});
|
||||||
|
// A null element is returned because the component does not need to render anything
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
export default Redirect;
|
79
src/utils/RoutingTableUtils.ts
Normal file
79
src/utils/RoutingTableUtils.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import {
|
||||||
|
CustomRouteObject,
|
||||||
|
RouteObjectAdditionalProps,
|
||||||
|
flatRoutes,
|
||||||
|
} from "../configure";
|
||||||
|
import {
|
||||||
|
clearMultiplePathSlashes,
|
||||||
|
trimPathOfParameters,
|
||||||
|
} from "./StringTransformationUtils";
|
||||||
|
|
||||||
|
interface FlatternRoutingTableElement extends RouteObjectAdditionalProps {
|
||||||
|
path: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
//! WARNING: This function will generate error if paths aren't unique, disableInNavbar or disableRedirect to prevent this. It's a useful feature to prevent duplicate path in navbar
|
||||||
|
/**
|
||||||
|
* @description Convert a existing routing table as a flat array
|
||||||
|
*/
|
||||||
|
export const flatternRoutingTable = (
|
||||||
|
routes: CustomRouteObject[],
|
||||||
|
previousPath = "undefined",
|
||||||
|
): FlatternRoutingTableElement[] => {
|
||||||
|
const result: FlatternRoutingTableElement[] = [];
|
||||||
|
routes.forEach((route: CustomRouteObject) => {
|
||||||
|
if (route.additionalProps.disableRedirect) return; // Skip if disable redirect, children are skipped too
|
||||||
|
if (
|
||||||
|
typeof route.path !== "undefined" &&
|
||||||
|
typeof previousPath === "undefined" &&
|
||||||
|
!route.additionalProps.disableInNavbar
|
||||||
|
) {
|
||||||
|
result.push({
|
||||||
|
path: trimPathOfParameters(route.path),
|
||||||
|
name: route.additionalProps.name,
|
||||||
|
disableBreadcrumbBar: route.additionalProps.disableBreadcrumbBar,
|
||||||
|
disableInNavbar: route.additionalProps.disableInNavbar,
|
||||||
|
disableRedirect: route.additionalProps.disableRedirect,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
typeof route.path !== "undefined" &&
|
||||||
|
typeof previousPath !== "undefined" &&
|
||||||
|
!route.additionalProps.disableInNavbar
|
||||||
|
) {
|
||||||
|
result.push({
|
||||||
|
path: trimPathOfParameters(
|
||||||
|
clearMultiplePathSlashes(`/${previousPath}/${route.path}`),
|
||||||
|
),
|
||||||
|
name: route.additionalProps.name,
|
||||||
|
disableBreadcrumbBar: route.additionalProps.disableBreadcrumbBar,
|
||||||
|
disableInNavbar: route.additionalProps.disableInNavbar,
|
||||||
|
disableRedirect: route.additionalProps.disableRedirect,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (route.children && typeof previousPath === "undefined") {
|
||||||
|
result.push(...flatternRoutingTable(route.children));
|
||||||
|
}
|
||||||
|
if (route.children && typeof previousPath !== "undefined") {
|
||||||
|
result.push(...flatternRoutingTable(route.children, route.path));
|
||||||
|
}
|
||||||
|
// Errors handling
|
||||||
|
if (typeof route.path === "undefined")
|
||||||
|
console.error(`Route ${route.additionalProps.name} is missing path`);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* @description Function to find element in flattern routes array by LAST path ex: /admin/MAINTENANCE
|
||||||
|
*/
|
||||||
|
export const findElementInFlatRoutes = (
|
||||||
|
path: string,
|
||||||
|
): FlatternRoutingTableElement | undefined => {
|
||||||
|
// Split path string into array, split by '/', then get the last element
|
||||||
|
const _route = flatRoutes.find((route) => {
|
||||||
|
const pathArray = route.path.split("/");
|
||||||
|
if (pathArray[pathArray.length - 1] === path) return true;
|
||||||
|
else return false;
|
||||||
|
});
|
||||||
|
return _route;
|
||||||
|
};
|
22
src/utils/StringTransformationUtils.ts
Normal file
22
src/utils/StringTransformationUtils.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
export function capitalizeFirstLetter(string: string): string {
|
||||||
|
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @description Function to clear multiple path slashes
|
||||||
|
* @example clearMultiplePathSlashes("/admin//MAINTENANCE") => "/admin/MAINTENANCE"
|
||||||
|
*/
|
||||||
|
export const clearMultiplePathSlashes = (path: string): string => {
|
||||||
|
return path.replace(/\/{2,}/g, "/");
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Trims parameters and queries from a URL path that start with ':' or '?'.
|
||||||
|
*
|
||||||
|
* @param {string} path - The URL path to trim.
|
||||||
|
* @returns {string} The URL path without parameters and queries.
|
||||||
|
* @example
|
||||||
|
* // returns "/path"
|
||||||
|
* trimPathOfParameters("/path/:param1/:param2:param3/?param4")
|
||||||
|
*/
|
||||||
|
export const trimPathOfParameters = (path: string) => {
|
||||||
|
return path.replace(/\/:[^/]*|\?[^/]*/g, "");
|
||||||
|
};
|
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
11
tailwind.config.js
Normal file
11
tailwind.config.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
import { themes } from "./daisyui.config.js";
|
||||||
|
export default {
|
||||||
|
content: ["./src/**/*.{js,ts,jsx,tsx}"],
|
||||||
|
// safelist is used to allow classes to not be purged by tailwind
|
||||||
|
safelist: ["alert-info", "alert-success", "alert-warning", "alert-error"],
|
||||||
|
daisyui: {
|
||||||
|
themes: themes,
|
||||||
|
},
|
||||||
|
plugins: [require("daisyui"), require("@tailwindcss/typography")],
|
||||||
|
};
|
49
tsconfig.json
Normal file
49
tsconfig.json
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"lib": [
|
||||||
|
"DOM",
|
||||||
|
"DOM.Iterable",
|
||||||
|
"ESNext"
|
||||||
|
],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noErrorTruncation": true,
|
||||||
|
/* "react-jsx" and "react-jsxdev": These are new options available from TypeScript 4.1 onwards. "react-jsx" transforms JSX into calls to a function that will be imported from react/jsx-runtime. "react-jsxdev" does the same, but for development builds. These options are useful if you're using React 17 or later, which introduced a new JSX Transform.
|
||||||
|
Remember that to use these options, you also need to set the module compiler option to esnext or commonjs, because the output will contain import statements.*/
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
/* Vite handle emiting */
|
||||||
|
"noEmit": true,
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
/*
|
||||||
|
"strict": true in TypeScript's tsconfig.json is an overarching setting that turns on a number of strict type-checking options. Some of these could overlap with ESLint rules related to good practices in JavaScript and TypeScript coding. However, since ESLint and TypeScript serve different purposes (ESLint for style and syntax, TypeScript for type checking), there usually isn't a direct conflict.
|
||||||
|
"noUnusedLocals": true and "noUnusedParameters": true in TypeScript could overlap with ESLint's no-unused-vars rule, which flags declared variables or arguments that are not used anywhere in the code.
|
||||||
|
"noFallthroughCasesInSwitch": true could overlap with ESLint's no-fallthrough rule, which disallows fallthrough behavior in switch statements, a common error in JavaScript. */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"types": [
|
||||||
|
"@types/node"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
],
|
||||||
|
// "references": [
|
||||||
|
// {
|
||||||
|
// "path": "./tsconfig.node.json"
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
}
|
15
vite.config.ts
Normal file
15
vite.config.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { defineConfig, loadEnv } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default ({ mode }) => {
|
||||||
|
process.env = { ...process.env, ...loadEnv(mode, process.cwd()) };
|
||||||
|
return defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
// resolve: {
|
||||||
|
// alias: {
|
||||||
|
// "tailwind.config.js": path.resolve(__dirname, "tailwind.config.js"),
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
});
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user