Updated based on my other project

This commit is contained in:
Igor Barcik 2024-01-24 09:54:32 +01:00
parent ea2629b7eb
commit acebe04456
Signed by: biggy
GPG Key ID: EA4CE0D1E2A6DC98
24 changed files with 289 additions and 130 deletions

View File

@ -1,4 +1,4 @@
export default {
module.exports = {
env: { browser: true, es2020: true, node: true },
extends: [
"eslint:recommended",

View File

@ -1,4 +1,4 @@
export default {
module.exports = {
printWidth: 80,
trailingComma: "all",
singleQuote: false,

View File

@ -10,22 +10,20 @@
## Usage
1. Setup `.env.{mode}` files
```ini
VITE_APP_NAME=Universal React Starter
VITE_BASE_PATH=/
```
2. run: `bun run dev`
3. To build for production, run: `bun run build`
_bun can be replaced by packet manager of your choice_
1. Setup `.env` files
1. `.env` - for production and development
2. `.env.development` - for development
3. `.env.production` - for production
2. Build or run dev server
3. Enjoy 🎉
### 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.
#### Dev notes
> UI inspiration: <https://demo.themesberg.com/windster-pro/#>
> UI inspirations:
>
> - <https://demo.themesberg.com/windster-pro/#>
> - <https://tamagui.dev/>

BIN
bun.lockb

Binary file not shown.

View File

@ -29,7 +29,7 @@
"roundness": {
"type": 3
},
"boundElements": null,
"boundElements": [],
"updated": 1705396978531,
"link": null,
"locked": false
@ -94,7 +94,7 @@
],
"frameId": null,
"roundness": null,
"boundElements": null,
"boundElements": [],
"updated": 1705397026573,
"link": null,
"locked": false,
@ -106,7 +106,7 @@
"containerId": "2zx0egnLluNOg2aagGFf5",
"originalText": "Username",
"lineHeight": 1.25,
"baseline": 18
"baseline": 19
},
{
"type": "rectangle",
@ -168,7 +168,7 @@
],
"frameId": null,
"roundness": null,
"boundElements": null,
"boundElements": [],
"updated": 1705397072409,
"link": null,
"locked": false,
@ -180,7 +180,7 @@
"containerId": "I0Xg4QaaO7XRxm7w1P_0a",
"originalText": "Password",
"lineHeight": 1.25,
"baseline": 18
"baseline": 19
},
{
"type": "rectangle",
@ -242,7 +242,7 @@
],
"frameId": null,
"roundness": null,
"boundElements": null,
"boundElements": [],
"updated": 1705397026573,
"link": null,
"locked": false,
@ -254,12 +254,12 @@
"containerId": "ZikkdqWQ7aOQyqTYogsSg",
"originalText": "Login",
"lineHeight": 1.25,
"baseline": 18
"baseline": 19
},
{
"type": "text",
"version": 768,
"versionNonce": 851740953,
"version": 770,
"versionNonce": 402387348,
"isDeleted": false,
"id": "97fLf76gEdKS_GvQ6ALpU",
"fillStyle": "solid",
@ -280,8 +280,8 @@
],
"frameId": null,
"roundness": null,
"boundElements": null,
"updated": 1705396978532,
"boundElements": [],
"updated": 1705762591927,
"link": null,
"locked": false,
"fontSize": 36,
@ -295,40 +295,40 @@
"baseline": 32
},
{
"id": "no67OXeP6ZJsJ51TOHxJy",
"type": "text",
"x": 719.7241633880242,
"y": 107.53011605520807,
"width": 70.11228942871094,
"height": 35,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"version": 36,
"versionNonce": 1797562796,
"isDeleted": false,
"id": "no67OXeP6ZJsJ51TOHxJy",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 719.7241633880242,
"y": 107.53011605520807,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"width": 70.11228942871094,
"height": 35,
"seed": 1759430041,
"groupIds": [],
"frameId": null,
"roundness": null,
"seed": 1759430041,
"version": 34,
"versionNonce": 764198007,
"isDeleted": false,
"boundElements": null,
"updated": 1705397088770,
"boundElements": [],
"updated": 1705762591928,
"link": null,
"locked": false,
"text": "/login",
"fontSize": 28,
"fontFamily": 1,
"text": "/login",
"textAlign": "left",
"verticalAlign": "top",
"baseline": 25,
"containerId": null,
"originalText": "/login",
"lineHeight": 1.25
"lineHeight": 1.25,
"baseline": 25
},
{
"type": "rectangle",
@ -362,69 +362,69 @@
"locked": false
},
{
"id": "wApCZR8vnuIdRuwfVeC5x",
"type": "rectangle",
"x": 953.3863135555557,
"y": 244.88073477777772,
"width": 468.8888888888887,
"height": 264.44444444444457,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"version": 109,
"versionNonce": 1334885687,
"isDeleted": false,
"id": "wApCZR8vnuIdRuwfVeC5x",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 953.3863135555557,
"y": 244.88073477777772,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"width": 468.8888888888887,
"height": 264.44444444444457,
"seed": 902925239,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 3
},
"seed": 902925239,
"version": 109,
"versionNonce": 1334885687,
"isDeleted": false,
"boundElements": null,
"boundElements": [],
"updated": 1705396987119,
"link": null,
"locked": false
},
{
"id": "Gmr1u54szmiUkN9YuVUAY",
"type": "text",
"x": 1686.3908303880241,
"y": 107.53011605520807,
"width": 111.60848999023438,
"height": 35,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"version": 134,
"versionNonce": 1076928276,
"isDeleted": false,
"id": "Gmr1u54szmiUkN9YuVUAY",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1686.3908303880241,
"y": 107.53011605520807,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"width": 111.60848999023438,
"height": 35,
"seed": 858001817,
"groupIds": [],
"frameId": null,
"roundness": null,
"seed": 858001817,
"version": 132,
"versionNonce": 1615445527,
"isDeleted": false,
"boundElements": null,
"updated": 1705397104893,
"boundElements": [],
"updated": 1705762591928,
"link": null,
"locked": false,
"text": "/; /home",
"fontSize": 28,
"fontFamily": 1,
"text": "/; /home",
"textAlign": "left",
"verticalAlign": "top",
"baseline": 25,
"containerId": null,
"originalText": "/; /home",
"lineHeight": 1.25
"lineHeight": 1.25,
"baseline": 25
}
],
"appState": {

View File

@ -17,9 +17,12 @@
"axios": "^1.6.1",
"daisyui": "latest",
"echarts": "^5.4.3",
"i18next": "^23.7.18",
"i18next-browser-languagedetector": "^7.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.48.2",
"react-i18next": "^14.0.1",
"react-icons": "^5.0.1",
"uuid": "^9.0.1",
"wouter": "next",

View File

@ -1,25 +1,18 @@
import { IconContext } from "react-icons/lib";
import DevControlPanel from "./components/DevControlPanel";
import FloatingMenu from "./components/FloatingMenu";
import {
floatingLanguageMenuStructure,
floatingMenuStructure,
} from "./configure";
import { floatingMenuStructure } from "./configure";
import SwitchRouteGenerator from "./routes/SwitchRouteGenerator";
import routesTree from "./routes/routes";
// TODO: - [x] Rewrite new switch generating function based on custom routes object
// TODO: - [x] Make condition for base path in Router, for "/" in main.base_path don't add to Router
// TODO: - [ ] Rewrite DevControlPanel to use custom routes object
// TODO: - [x] Rewrite DevControlPanel to use custom routes object
function App() {
return (
<div>
<IconContext.Provider value={{ className: "size-5" }}>
<FloatingMenu menuStructure={floatingMenuStructure} />
<FloatingMenu
menuStructure={floatingLanguageMenuStructure}
className="flex gap-2 absolute left-4 top-4 flex-col"
tooltipClassName="tooltip tooltip-right"
/>
</IconContext.Provider>
<SwitchRouteGenerator routesTree={routesTree} />
<DevControlPanel routesTree={routesTree} />

View File

@ -1,9 +1,12 @@
import { useLocation } from "wouter";
import { main } from "../configure";
import { floatingLanguageMenuStructure, main } from "../configure";
import { DevControlPanelProps } from "../types/devControlPanelTypes";
import { RoutingTree } from "../types/routesTypes";
import inDebug from "../utils/inDebug";
import inDev from "../utils/inDev";
import ThemeButton from "./ThemeButton";
import { clearMultiplePathSlashes } from "../utils/StringTransformationUtils";
import { useTranslation } from "react-i18next";
import FloatingMenu from "./FloatingMenu";
/**
* Development Control Panel Component
@ -16,10 +19,11 @@ import ThemeButton from "./ThemeButton";
*/
const DevControlPanel = ({ routesTree }: DevControlPanelProps) => {
const [, setLocation] = useLocation();
const { t, i18n } = useTranslation();
// Function to update the current location, with debug logging
const _setLocation = (targetLocation: string) => {
inDebug(() => console.log("DevControlPanel_setLocation", targetLocation));
inDev(() => console.log("DevControlPanel_setLocation", targetLocation));
setLocation(targetLocation);
};
@ -30,12 +34,12 @@ const DevControlPanel = ({ routesTree }: DevControlPanelProps) => {
): JSX.Element[] => {
return routesTree.map((route): JSX.Element => {
// Constructing path for the route button
const _path = (
parentPath ? "/" + parentPath + "/" + route.path : "/" + route.path
).replace(/\/\//g, "/");
const _path = clearMultiplePathSlashes(
parentPath ? "/" + parentPath + "/" + route.path : "/" + route.path,
);
// Debug logging for route information
inDebug(() =>
inDev(() =>
console.log(
"%croutesCrawler_routes %s %s",
"color: lightblue",
@ -86,6 +90,15 @@ const DevControlPanel = ({ routesTree }: DevControlPanelProps) => {
</div>
{/* Rendering the dynamically generated route navigation buttons */}
<div className="space-y-2 space-x-2 mb-6">{routesCrawler(routesTree)}</div>
<div className="px-5 mt-6">
<p>{t("welcome")}</p>
<p>Lang: {i18n.language}</p>
</div>
<FloatingMenu
menuStructure={floatingLanguageMenuStructure}
className="flex gap-2 flex-col"
tooltipClassName="tooltip tooltip-right"
/>
</div>
);
};

View File

@ -1,5 +1,5 @@
import useTheme from "../hooks/useTheme";
import inDebug from "../utils/inDebug";
import inDev from "../utils/inDev";
const ThemeButton = () => {
const [isDark, toggleTheme] = useTheme();
@ -8,7 +8,7 @@ const ThemeButton = () => {
<input
checked={isDark}
className="hidden"
onChange={() => inDebug(() => console.log("Theme changed"))}
onChange={() => inDev(() => console.log("Theme changed"))}
onClick={toggleTheme}
type="checkbox"
/>

View File

@ -1,8 +1,10 @@
import { FiHome, FiLogIn } from "react-icons/fi";
import { setLocation } from "./components/FloatingMenu";
import ThemeButton from "./components/ThemeButton";
import { HOME, LOGIN, THEME } from "./consts";
import { HOME, LANGUAGE, LOGIN, THEME } from "./consts";
import { MenuStructure } from "./types/floatingMenuTypes";
import { setLanguage } from "./main";
import { HiMiniLanguage } from "react-icons/hi2";
/* INSTRUCTIONS:
* Here you can configure:
* - program version
@ -27,6 +29,24 @@ export const main = {
// sizes in pixels
export const topbarSize = 64;
export const sidebarSize = 256;
export const floatingLanguageMenuStructure: MenuStructure[] = [
{
label: "English",
icon: <p>🇬🇧</p>,
action: () => {
console.log("English");
setLanguage("eng");
},
},
{
label: "Polski",
icon: <p>🇵🇱</p>,
action: () => {
console.log("Polski");
setLanguage("pol");
},
},
];
export const floatingMenuStructure: MenuStructure[] = [
{
label: HOME,
@ -39,16 +59,34 @@ export const floatingMenuStructure: MenuStructure[] = [
action: () => setLocation(`/${LOGIN}`),
},
{ label: THEME, element: <ThemeButton /> },
];
export const floatingLanguageMenuStructure: MenuStructure[] = [
{
label: "English",
icon: <p>🇬🇧</p>,
action: () => console.log("English"),
},
{
label: "Polski",
icon: <p>🇵🇱</p>,
action: () => console.log("Polski"),
label: LANGUAGE,
element: (
<div className="dropdown dropdown-left">
<div tabIndex={0} role="button" className="btn btn-circle btn-outline">
<HiMiniLanguage />
</div>
<ul
tabIndex={0}
className="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52 mr-1"
>
{floatingLanguageMenuStructure.map((element) => {
return (
<li>
<a
onClick={() => {
if ("action" in element && typeof element.action === "function")
element.action();
else console.log("No action");
}}
>
{element.label} {"icon" in element ? element.icon : null}
</a>
</li>
);
})}
</ul>
</div>
),
},
];

View File

@ -14,3 +14,4 @@ export const ROLES = "roles";
export const USERS = "users";
// -- BUTTONS --
export const THEME = "theme";
export const LANGUAGE = "language";

View File

@ -1,11 +1,11 @@
import { useState } from "react";
/**
* Custom hook for persisting state in local storage.
*
*
* This hook works similarly to the standard `useState` hook but also stores the state in local storage,
* allowing the state to persist across browser sessions. The state is initialized from local storage
* if it exists; otherwise, it falls back to the provided initial value.
*
*
* @template T The type of the value to be stored.
* @param {string} key The key under which the value is stored in local storage.
* @param {T} initialValue The initial value to be used if there is no item in local storage with the given key.

View File

@ -0,0 +1,35 @@
import { useState } from "react";
/**
* Works like useState but stores the value in session storage
* @param key Create a key to store the value in session storage
* @param initialValue Assign an initial value to the key
* @returns [storedValue, setValue] Returns the stored value and a function to set the value
*/
const useSessionStorage = <T>(
key: string,
initialValue: T,
): [T, (value: T) => void] => {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = sessionStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.log(error);
return initialValue;
}
});
const setValue = (value: T) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
sessionStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.log(error);
}
};
return [storedValue, setValue];
};
export default useSessionStorage;

View File

@ -1,6 +1,6 @@
import { useEffect } from "react";
import useLocalStorage from "./useLocalStorage";
import inDebug from "../utils/inDebug";
import inDev from "../utils/inDev";
/**
* Custom hook for managing theme state.
*
@ -20,14 +20,14 @@ const useTheme = (): [boolean, () => void] => {
const isDark = theme === "dark";
const toggleTheme = () => {
inDebug(() => console.log("toggleTheme called"));
inDev(() => console.log("toggleTheme called"));
const newTheme = theme === "light" ? "dark" : "light";
setTheme(newTheme);
document.documentElement.dataset.theme = newTheme;
};
useEffect(() => {
inDebug(() => console.log("useEffect called"));
inDev(() => console.log("useEffect called"));
document.documentElement.dataset.theme = theme;
}, [theme]);

View File

@ -0,0 +1,7 @@
{
"welcome": "Welcome!",
"login_button": "Login",
"login_username": "Username",
"login_password": "Password",
"login_accept_terms": "I accept the terms and conditions"
}

View File

@ -0,0 +1,24 @@
import i18n from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import { initReactI18next } from "react-i18next";
import generalEng from "./eng/general.json";
import generalPol from "./pol/general.json";
i18n
.use(initReactI18next)
.use(LanguageDetector)
.init({
resources: {
eng: {
general: generalEng,
},
pol: {
general: generalPol,
},
},
fallbackLng: "eng",
interpolation: {
escapeValue: false,
},
defaultNS: "general",
});

View File

@ -0,0 +1,7 @@
{
"welcome": "Witaj!",
"login_button": "Zaloguj",
"login_username": "Nazwa użytkownika",
"login_password": "Hasło",
"login_accept_terms": "Akceptuję regulamin"
}

View File

@ -1,17 +1,26 @@
import { Global, css } from "@emotion/react";
import React from "react";
import React, { useEffect } from "react";
import ReactDOM from "react-dom/client";
import { Router } from "wouter";
import App from "./App";
import { setupAxiosInterceptors } from "./api/AxiosService";
import { main, viteEnv } from "./configure";
import inDebug from "./utils/inDebug";
import useLocalStorage from "./hooks/useLocalStorage";
import "./locales/localesConfig";
import inDev from "./utils/inDev";
import "/style.css"; // Global tailwind styles
import { Router } from "wouter";
import { useTranslation } from "react-i18next";
setupAxiosInterceptors();
inDebug(() => console.log(viteEnv));
//! Important, defining base as '/' isn't equal to not defining it at all. That way is easier 😁
inDev(() => console.log(viteEnv));
export let language: string, setLanguage: (value: string) => void;
const _App = () => {
[language, setLanguage] = useLocalStorage("language", "eng");
const { i18n } = useTranslation();
useEffect(() => {
i18n.changeLanguage(language);
}, [language]);
if (main.base_path !== "/") {
return (
<Router base={main.base_path}>

View File

@ -1,8 +1,9 @@
import { useTranslation } from "react-i18next";
import { main } from "../configure";
const LoginPage = () => {
const { t } = useTranslation();
return (
// hero daisyui login page
<div className="hero min-h-screen bg-base-200">
<div className="flex-col justify-center hero-content lg:flex-row-reverse">
<div className="text-center lg:text-left">
@ -15,21 +16,21 @@ const LoginPage = () => {
<form>
<div className="form-control">
<label className="label">
<span className="label-text">Login</span>
<span className="label-text">{t("login_username")}</span>
</label>
<input
type="text"
placeholder="login"
placeholder={t("login_username")}
className="input input-bordered"
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">Password</span>
<span className="label-text">{t("login_password")}</span>
</label>
<input
type="password"
placeholder="password"
placeholder={t("login_password")}
className="input input-bordered"
/>
</div>
@ -37,12 +38,16 @@ const LoginPage = () => {
<label className="cursor-pointer label justify-start gap-4">
<input type="checkbox" className="checkbox checkbox-success" />
<span className="label-text justify-start">
Agree to the terms and policy
{t("login_accept_terms")}
</span>
</label>
</div>
<div className="form-control mt-6">
<input type="submit" value="Login" className="btn btn-primary" />
<input
type="submit"
value={t("login_button")}
className="btn btn-primary"
/>
</div>
</form>
</div>

View File

@ -1,7 +1,8 @@
import { Route, Switch } from "wouter";
import inDebug from "../utils/inDebug";
import inDev from "../utils/inDev";
import { RoutingTree } from "../types/routesTypes";
import { SwitchRouteGeneratorProps } from "../types/switchRouteGeneratorTypes";
import { clearMultiplePathSlashes } from "../utils/StringTransformationUtils";
const routesCrawler = (
routesTree: RoutingTree,
@ -10,11 +11,11 @@ const routesCrawler = (
return routesTree.map((route): JSX.Element => {
// -----
if (route.path) {
const _path = (
parentPath && parentPath !== "/" ? parentPath + route.path : route.path
).replace("//", "/");
const _path = clearMultiplePathSlashes(
parentPath && parentPath !== "/" ? parentPath + route.path : route.path,
);
// Debug log shows generated routes
inDebug(() => console.log("routesCrawler_routes", route.name, _path));
inDev(() => console.log("routesCrawler_routes", route.name, _path));
// -----
if (route.nest) {
return (

View File

@ -1,9 +1,22 @@
/**
* Rolls through each entry in a given object and creates a formatted string.
* The function expects an object with optional 'state' and 'id' properties.
*
* @param obj A readonly and partially optional object with 'state' and 'id' properties.
* @returns A string concatenating each key-value pair from the object.
*/
export const rollThroughObj = (
obj: Readonly<Partial<{ state: string; id: string }>>,
) => {
): string => {
// Initialize an empty result string.
let result = "";
// Iterate over each entry in the object.
for (const [key, value] of Object.entries(obj)) {
// Append the key-value pair to the result string in a formatted way.
result += ` -${key}: ${value}`;
}
// Return the trimmed result.
return result.trim();
};

View File

@ -1,13 +1,24 @@
/**
* Capitalizes the first letter of a given string.
*
* @param {string} string - The string to capitalize.
* @returns {string} The string with the first letter capitalized.
*/
export function capitalizeFirstLetter(string: string): string {
return string.charAt(0).toUpperCase() + string.slice(1);
}
/**
* @description Function to clear multiple path slashes
* Clears multiple consecutive slashes in a path string.
*
* @param {string} path - The path string to be formatted.
* @returns {string} The path with consecutive slashes reduced to a single slash.
* @example clearMultiplePathSlashes("/admin//MAINTENANCE") => "/admin/MAINTENANCE"
*/
export const clearMultiplePathSlashes = (path: string): string => {
return path.replace(/\/{2,}/g, "/");
};
/**
* Trims parameters and queries from a URL path that start with ':' or '?'.
*
@ -17,6 +28,6 @@ export const clearMultiplePathSlashes = (path: string): string => {
* // returns "/path"
* trimPathOfParameters("/path/:param1/:param2:param3/?param4")
*/
export const trimPathOfParameters = (path: string) => {
export const trimPathOfParameters = (path: string): string => {
return path.replace(/\/:[^/]*|\?[^/]*/g, "");
};

View File

@ -1,11 +1,11 @@
/**
* Execute whatever you pass only in development (not production)
*/
const inDebug = <T>(callback: () => T): T | null => {
const inDev = <T>(callback: () => T): T | null => {
if (process.env.NODE_ENV === "development") {
return callback();
}
return null;
};
export default inDebug;
export default inDev;

View File

@ -17,6 +17,7 @@
"jsx": "react-jsx", // Transform JSX for React 17+ JSX Transform
// Strengthening type-checking and ensuring consistency
"strict": true, // Enable all strict type-checking options
"strictNullChecks": true, // Enable strict null checks
"noUnusedLocals": true, // Disallow unused local variables
"noUnusedParameters": true, // Disallow unused function parameters
"noFallthroughCasesInSwitch": true, // Prevent fallthrough cases in switch statements