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
|
@ -1,2 +1,2 @@
|
||||
VITE_APP_NAME=App Production
|
||||
# Config avaliable on production
|
||||
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/)
|
||||
Toolstack for my UI projects. I try to use `bun`.
|
||||
> It was challenging stuff to configure, but now it works like a charm... I think.
|
||||
|
||||
<img style="width: 256px; height: 256px; margin-left: 50%; translate: -50%;" src="lupa2Logo.webp"/>
|
||||
|
||||
Used technologies:
|
||||
|
||||
| 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)
|
||||
- [Tailwind Elements](https://tw-elements.com/) - main styling
|
||||
- [react-icons](https://react-icons.github.io/react-icons/) - big icon library
|
||||
- [recharts](https://echarts.apache.org/) - charts library
|
||||
- [wouter](https://github.com/molefrog/wouter) - router library
|
||||
- [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
|
||||
- [@tanstack/react-table](https://tanstack.com/table/v8/docs/adapters/react-table) - advanced table library
|
||||
|
||||
## Usage
|
||||
|
||||
@ -41,8 +14,6 @@ ToDo:
|
||||
2. run: `bun run dev`
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
[notes](docs/notes.md)
|
||||
|
||||
> UI inspiration: <https://demo.themesberg.com/windster-pro/#>
|
||||
|
@ -12,15 +12,14 @@
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.3.2",
|
||||
"@tanstack/react-table": "^8.10.7",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"axios": "^1.6.0",
|
||||
"echarts": "^5.4.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.47.0",
|
||||
"react-router-dom": "^6.17.0",
|
||||
"recharts": "^2.9.2",
|
||||
"tw-elements": "^1.0.0",
|
||||
"uuid": "^9.0.1",
|
||||
"wouter": "^2.12.1",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"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;
|
||||
|
||||
// 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
|
||||
export const main = {
|
||||
program_name: viteEnv.VITE_APP_NAME,
|
||||
@ -129,5 +9,3 @@ 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);
|
||||
|
@ -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 { useEffect, useState } from "react";
|
||||
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";
|
||||
import { main } from "../configure";
|
||||
import Debugger from "./Debugger";
|
||||
|
||||
/** 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);
|
||||
};
|
||||
|
||||
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 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>
|
||||
<h1>App</h1>
|
||||
<p>App</p>
|
||||
<Debugger />
|
||||
<p>
|
||||
{main.program_name} v{main.program_version}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { flatRoutes } from "../configure";
|
||||
import { useLocation } from "wouter";
|
||||
|
||||
function Debugger() {
|
||||
const windowLocation = window.location.pathname;
|
||||
@ -10,7 +9,6 @@ function Debugger() {
|
||||
<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>
|
||||
@ -22,7 +20,6 @@ function Debugger() {
|
||||
</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>
|
||||
);
|
||||
|
@ -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