+ );
+ }
+ // 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 (
+
+ {/*Automatically generated breadcrumbs based on routing table from configuration file and active path*/}
+ {/*Get active route and find it in routing file*/}
+
+
+
+
+ {/* Drawer sidebar wrapper */}
+
+ {/* Dark overlay on mobile devices, clickable to close drawer */}
+
+
+ );
+}
+
+export default Debugger;
diff --git a/src/features/Home/HomePage.tsx b/src/features/Home/HomePage.tsx
new file mode 100644
index 0000000..2d4d6bf
--- /dev/null
+++ b/src/features/Home/HomePage.tsx
@@ -0,0 +1,5 @@
+function HomePage() {
+ return <>HOME>;
+}
+
+export default HomePage;
diff --git a/src/features/Login/LoginPage.tsx b/src/features/Login/LoginPage.tsx
new file mode 100644
index 0000000..770d52b
--- /dev/null
+++ b/src/features/Login/LoginPage.tsx
@@ -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({
+ resolver: zodResolver(InputSchema),
+ });
+ const navigate = useNavigate();
+ const onSubmit: SubmitHandler = (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 = () => {
+ console.log("Errors in form fields");
+ };
+
+ return (
+
+
+
+ );
+}
+
+export default LoginPage;
diff --git a/src/main.css b/src/main.css
new file mode 100644
index 0000000..bd6213e
--- /dev/null
+++ b/src/main.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
\ No newline at end of file
diff --git a/src/main.tsx b/src/main.tsx
new file mode 100644
index 0000000..702fa7b
--- /dev/null
+++ b/src/main.tsx
@@ -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(
+
+ {/* fallbackElement - use when routing table is quite big to load, it will display desired element in convention of loading screen */}
+
+ Loading route table...}
+ />
+
+ ,
+);
diff --git a/src/routes.tsx b/src/routes.tsx
new file mode 100644
index 0000000..642a91a
--- /dev/null
+++ b/src/routes.tsx
@@ -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:
Empty routes
,
+ });
+ }
+ //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: ,
+ },
+];
+
+/** 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;
diff --git a/src/utils/ObjectUtils.ts b/src/utils/ObjectUtils.ts
new file mode 100644
index 0000000..47b3177
--- /dev/null
+++ b/src/utils/ObjectUtils.ts
@@ -0,0 +1,9 @@
+export const rollThroughObj = (
+ obj: Readonly>,
+) => {
+ let result = "";
+ for (const [key, value] of Object.entries(obj)) {
+ result += ` -${key}: ${value}`;
+ }
+ return result.trim();
+};
diff --git a/src/utils/Redirect.tsx b/src/utils/Redirect.tsx
new file mode 100644
index 0000000..64a35d7
--- /dev/null
+++ b/src/utils/Redirect.tsx
@@ -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;
diff --git a/src/utils/RoutingTableUtils.ts b/src/utils/RoutingTableUtils.ts
new file mode 100644
index 0000000..c778233
--- /dev/null
+++ b/src/utils/RoutingTableUtils.ts
@@ -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;
+};
diff --git a/src/utils/StringTransformationUtils.ts b/src/utils/StringTransformationUtils.ts
new file mode 100644
index 0000000..b96728d
--- /dev/null
+++ b/src/utils/StringTransformationUtils.ts
@@ -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, "");
+};
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/tailwind.config.js b/tailwind.config.js
new file mode 100644
index 0000000..a22a179
--- /dev/null
+++ b/tailwind.config.js
@@ -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")],
+};
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..f468454
--- /dev/null
+++ b/tsconfig.json
@@ -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"
+ // }
+ // ]
+}
\ No newline at end of file
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..f5de01d
--- /dev/null
+++ b/vite.config.ts
@@ -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"),
+ // },
+ // },
+ });
+};