Completly changed startup template
This commit is contained in:
parent
e6e7eb868f
commit
ff97e04ea6
@ -1,2 +1,2 @@
|
|||||||
VITE_APP_NAME=App Development
|
# Config avaliable on development environment
|
||||||
VITE_API_URL=http://localhost:7082
|
VITE_API_URL=http://localhost:7082
|
@ -1,2 +1,2 @@
|
|||||||
VITE_APP_NAME=App Production
|
# Config avaliable on production
|
||||||
VITE_API_URL=http://192.168.179.36:7082
|
VITE_API_URL=http://192.168.179.36:7082
|
47
README.md
47
README.md
@ -1,39 +1,12 @@
|
|||||||
# DaisyUI-React-Starter
|
# TailwindElements-React-Starter
|
||||||
|
|
||||||
[](https://jenkins.bigoscloud.com/job/LuPa2/lastBuild/)
|
- [Tailwind Elements](https://tw-elements.com/) - main styling
|
||||||
Toolstack for my UI projects. I try to use `bun`.
|
- [react-icons](https://react-icons.github.io/react-icons/) - big icon library
|
||||||
> It was challenging stuff to configure, but now it works like a charm... I think.
|
- [recharts](https://echarts.apache.org/) - charts library
|
||||||
|
- [wouter](https://github.com/molefrog/wouter) - router library
|
||||||
<img style="width: 256px; height: 256px; margin-left: 50%; translate: -50%;" src="lupa2Logo.webp"/>
|
- [react-hook-form](https://github.com/react-hook-form/resolvers#zod) with [zod](https://zod.dev/) resolver - forms library
|
||||||
|
- [axios](https://axios-http.com/) - http request library
|
||||||
Used technologies:
|
- [@tanstack/react-table](https://tanstack.com/table/v8/docs/adapters/react-table) - advanced table library
|
||||||
|
|
||||||
| Name | Description |
|
|
||||||
|--------|---|
|
|
||||||
| [TypeScript](https://www.typescriptlang.org/) | Main Language |
|
|
||||||
| [Vite](https://vitejs.dev/) | Bundler |
|
|
||||||
| [React](https://reactjs.org/) | Framework |
|
|
||||||
| [TailwindCSS](https://tailwindcss.com/) | CSS Framework |
|
|
||||||
| [PostCSS](https://postcss.org/) | CSS Processor |
|
|
||||||
| [DaisyUI](https://daisyui.com/) | A tool for transforming CSS with JavaScript |
|
|
||||||
| [RadixUI](https://www.radix-ui.com/) | Unstyled, accessible components for building high‑quality design systems and web apps in React |
|
|
||||||
| [Zod](https://zod.dev/) | TypeScript-first schema validation with static type inference |
|
|
||||||
| [React Router](https://reactrouter.com/) | Routing. Docs are lame, use [github](https://github.com/remix-run/react-router/tree/main) |
|
|
||||||
> Planned: [React Hook Form](https://react-hook-form.com/) Forms
|
|
||||||
|
|
||||||
Linting, formatting and code editor:
|
|
||||||
|
|
||||||
- [VSCode](https://code.visualstudio.com/)
|
|
||||||
- [ESlint](https://eslint.org/)
|
|
||||||
- [Prettier](https://prettier.io/)
|
|
||||||
|
|
||||||
> Always up-to-date tools rather than stable old.
|
|
||||||
> It's not intended to be shared, but you can use it if you want.
|
|
||||||
|
|
||||||
ToDo:
|
|
||||||
[ ] Add tests
|
|
||||||
[?] Add CI/CD
|
|
||||||
[?] Add SSR (Server Side Rendering)
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@ -41,8 +14,6 @@ ToDo:
|
|||||||
2. run: `bun run dev`
|
2. run: `bun run dev`
|
||||||
3. To build for production, run: `bun run build`
|
3. To build for production, run: `bun run build`
|
||||||
|
|
||||||
> Use <https://daisyui.com/components> for components styling and <https://www.radix-ui.com/docs/primitives/overview/introduction> for components
|
|
||||||
|
|
||||||
### Contact
|
### Contact
|
||||||
|
|
||||||
If you have any suggestions/opinions, please let me know in issues
|
If you have any suggestions/opinions, please let me know in issues
|
||||||
@ -50,3 +21,5 @@ If you have any suggestions/opinions, please let me know in issues
|
|||||||
#### Dev notes
|
#### Dev notes
|
||||||
|
|
||||||
[notes](docs/notes.md)
|
[notes](docs/notes.md)
|
||||||
|
|
||||||
|
> UI inspiration: <https://demo.themesberg.com/windster-pro/#>
|
||||||
|
@ -12,15 +12,14 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^3.3.2",
|
"@hookform/resolvers": "^3.3.2",
|
||||||
"@tanstack/react-table": "^8.10.7",
|
"@tanstack/react-table": "^8.10.7",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
|
"echarts": "^5.4.3",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "^7.47.0",
|
"react-hook-form": "^7.47.0",
|
||||||
"react-router-dom": "^6.17.0",
|
|
||||||
"recharts": "^2.9.2",
|
|
||||||
"tw-elements": "^1.0.0",
|
"tw-elements": "^1.0.0",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
|
"wouter": "^2.12.1",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -1,72 +0,0 @@
|
|||||||
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;
|
|
@ -1,35 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
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;
|
|
@ -1,39 +0,0 @@
|
|||||||
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;
|
|
@ -1,52 +0,0 @@
|
|||||||
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;
|
|
@ -1,23 +0,0 @@
|
|||||||
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;
|
|
@ -1,17 +0,0 @@
|
|||||||
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;
|
|
@ -1,109 +0,0 @@
|
|||||||
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;
|
|
@ -1,79 +0,0 @@
|
|||||||
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;
|
|
@ -1,136 +0,0 @@
|
|||||||
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;
|
|
@ -1,37 +0,0 @@
|
|||||||
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;
|
|
@ -1,25 +0,0 @@
|
|||||||
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;
|
|
@ -1,9 +0,0 @@
|
|||||||
export type NotificationStatus = "success" | "error" | "warning" | "info";
|
|
||||||
export interface INotificationBase {
|
|
||||||
message: string;
|
|
||||||
status: NotificationStatus;
|
|
||||||
duration?: number;
|
|
||||||
}
|
|
||||||
export interface INotification extends INotificationBase {
|
|
||||||
id: number;
|
|
||||||
}
|
|
@ -1,2 +0,0 @@
|
|||||||
# Notification
|
|
||||||
I wrote own notification module but its little bit laggy and buggy so I don't use it.
|
|
@ -1,34 +0,0 @@
|
|||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,124 +1,4 @@
|
|||||||
/* 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 App from "./features/App";
|
|
||||||
import LoginPage from "./features/Login/LoginPage";
|
|
||||||
import { flatternRoutingTable } from "./utils/RoutingTableUtils";
|
|
||||||
import Debugger from "./features/Debugger";
|
|
||||||
//----
|
|
||||||
const HomePageLazy = lazy(() => import("./features/Home/HomePage"));
|
|
||||||
|
|
||||||
export const viteEnv = import.meta.env;
|
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// 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
|
//Main configuration, static data, mostly used here
|
||||||
export const main = {
|
export const main = {
|
||||||
program_name: viteEnv.VITE_APP_NAME,
|
program_name: viteEnv.VITE_APP_NAME,
|
||||||
@ -129,5 +9,3 @@ export const about = {
|
|||||||
program_description: `This is a ${main.program_name} for <purpose>.`,
|
program_description: `This is a ${main.program_name} for <purpose>.`,
|
||||||
program_authors: [{ name: "Author Name", email: "email@example.com" }],
|
program_authors: [{ name: "Author Name", email: "email@example.com" }],
|
||||||
};
|
};
|
||||||
//----
|
|
||||||
export const flatRoutes = flatternRoutingTable(navigation);
|
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
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";
|
|
@ -1,32 +0,0 @@
|
|||||||
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;
|
|
@ -1,94 +1,15 @@
|
|||||||
import { HamburgerMenuIcon, SunIcon } from "@radix-ui/react-icons";
|
import { main } from "../configure";
|
||||||
import { useEffect, useState } from "react";
|
import Debugger from "./Debugger";
|
||||||
import { Outlet } from "react-router-dom";
|
|
||||||
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() {
|
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ConfirmationDialog />
|
<h1>App</h1>
|
||||||
{/* Root drawer container */}
|
<p>App</p>
|
||||||
{/* Drawer opening is controlled directly via css prop and next via local useState variable */}
|
<Debugger />
|
||||||
<div className={"drawer " + (openDrawer ? "lg:drawer-open" : "")}>
|
<p>
|
||||||
{/* A hidden checkbox to toggle the visibility of the drawer */}
|
{main.program_name} v{main.program_version}
|
||||||
<input
|
</p>
|
||||||
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 HERE
|
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "wouter";
|
||||||
import { flatRoutes } from "../configure";
|
|
||||||
|
|
||||||
function Debugger() {
|
function Debugger() {
|
||||||
const windowLocation = window.location.pathname;
|
const windowLocation = window.location.pathname;
|
||||||
@ -10,7 +9,6 @@ function Debugger() {
|
|||||||
<summary className="collapse-title text-xl font-medium">
|
<summary className="collapse-title text-xl font-medium">
|
||||||
Flat Routes
|
Flat Routes
|
||||||
</summary>
|
</summary>
|
||||||
<code className="collapse-content">{JSON.stringify(flatRoutes)}</code>
|
|
||||||
</details>
|
</details>
|
||||||
<details className="collapse bg-base-200 mb-4">
|
<details className="collapse bg-base-200 mb-4">
|
||||||
<summary className="collapse-title text-xl font-medium">Locations</summary>
|
<summary className="collapse-title text-xl font-medium">Locations</summary>
|
||||||
@ -22,7 +20,6 @@ function Debugger() {
|
|||||||
</details>
|
</details>
|
||||||
<details className="collapse bg-base-200 mb-4">
|
<details className="collapse bg-base-200 mb-4">
|
||||||
<summary className="collapse-title text-xl font-medium">Routes</summary>
|
<summary className="collapse-title text-xl font-medium">Routes</summary>
|
||||||
<code className="collapse-content">{JSON.stringify(flatRoutes)}</code>
|
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
function HomePage() {
|
|
||||||
return <>HOME</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default HomePage;
|
|
@ -1,102 +0,0 @@
|
|||||||
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;
|
|
@ -1,18 +0,0 @@
|
|||||||
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;
|
|
Loading…
x
Reference in New Issue
Block a user