Completly changed startup template

This commit is contained in:
Igor Barcik 2023-11-09 13:10:22 +01:00
parent e6e7eb868f
commit ff97e04ea6
Signed by: biggy
GPG Key ID: EA4CE0D1E2A6DC98
28 changed files with 23 additions and 1113 deletions

View File

@ -1,2 +1,2 @@
VITE_APP_NAME=App Development
# Config avaliable on development environment
VITE_API_URL=http://localhost:7082

View File

@ -1,2 +1,2 @@
VITE_APP_NAME=App Production
# Config avaliable on production
VITE_API_URL=http://192.168.179.36:7082

View File

@ -1,39 +1,12 @@
# DaisyUI-React-Starter
# TailwindElements-React-Starter
[![Build Status](https://jenkins.bigoscloud.com/job/LuPa2/lastBuild/badge/icon)](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 highquality 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/#>

BIN
bun.lockb

Binary file not shown.

View File

@ -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": {

View File

@ -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;

View File

@ -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>
);
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;
}

View File

@ -1,2 +0,0 @@
# Notification
I wrote own notification module but its little bit laggy and buggy so I don't use it.

View File

@ -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,
};
}

View File

@ -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);

View File

@ -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";

View File

@ -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;

View File

@ -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>
);
}

View File

@ -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>
);

View File

@ -1,5 +0,0 @@
function HomePage() {
return <>HOME</>;
}
export default HomePage;

View File

@ -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;

View File

@ -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;