diff --git a/.eslintrc.js b/.eslintrc.cjs similarity index 95% rename from .eslintrc.js rename to .eslintrc.cjs index f7472b4..d29b926 100644 --- a/.eslintrc.js +++ b/.eslintrc.cjs @@ -1,4 +1,4 @@ -export default { +module.exports = { env: { browser: true, es2020: true, node: true }, extends: [ "eslint:recommended", diff --git a/.prettierrc.js b/.prettierrc.cjs similarity index 91% rename from .prettierrc.js rename to .prettierrc.cjs index 5223e66..eaf4b83 100644 --- a/.prettierrc.js +++ b/.prettierrc.cjs @@ -1,4 +1,4 @@ -export default { +module.exports = { printWidth: 80, trailingComma: "all", singleQuote: false, diff --git a/README.md b/README.md index 68f56dd..feb6fce 100644 --- a/README.md +++ b/README.md @@ -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: +> UI inspirations: +> +> - +> - diff --git a/bun.lockb b/bun.lockb index cdb38c8..c71a4fe 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/docs/servimainUI.excalidraw b/docs/servimainUI.excalidraw index b0d6fa4..a7d653a 100644 --- a/docs/servimainUI.excalidraw +++ b/docs/servimainUI.excalidraw @@ -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": { diff --git a/package.json b/package.json index ae3cd6e..cf34dfd 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App.tsx b/src/App.tsx index 99d2f39..23c27ec 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 (
- diff --git a/src/components/DevControlPanel.tsx b/src/components/DevControlPanel.tsx index 12e3b69..eb23ea1 100644 --- a/src/components/DevControlPanel.tsx +++ b/src/components/DevControlPanel.tsx @@ -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) => {
{/* Rendering the dynamically generated route navigation buttons */}
{routesCrawler(routesTree)}
+
+

{t("welcome")}

+

Lang: {i18n.language}

+
+ ); }; diff --git a/src/components/ThemeButton.tsx b/src/components/ThemeButton.tsx index 2289c9c..bddad40 100644 --- a/src/components/ThemeButton.tsx +++ b/src/components/ThemeButton.tsx @@ -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 = () => { inDebug(() => console.log("Theme changed"))} + onChange={() => inDev(() => console.log("Theme changed"))} onClick={toggleTheme} type="checkbox" /> diff --git a/src/configure.tsx b/src/configure.tsx index 846c144..79f64b4 100644 --- a/src/configure.tsx +++ b/src/configure.tsx @@ -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:

🇬🇧

, + action: () => { + console.log("English"); + setLanguage("eng"); + }, + }, + { + label: "Polski", + icon:

🇵🇱

, + 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: }, -]; -export const floatingLanguageMenuStructure: MenuStructure[] = [ { - label: "English", - icon:

🇬🇧

, - action: () => console.log("English"), - }, - { - label: "Polski", - icon:

🇵🇱

, - action: () => console.log("Polski"), + label: LANGUAGE, + element: ( + + ), }, ]; diff --git a/src/consts.ts b/src/consts.ts index 2ab1074..47eb5ed 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -14,3 +14,4 @@ export const ROLES = "roles"; export const USERS = "users"; // -- BUTTONS -- export const THEME = "theme"; +export const LANGUAGE = "language"; diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts index 629d1c3..e3a6e72 100644 --- a/src/hooks/useLocalStorage.ts +++ b/src/hooks/useLocalStorage.ts @@ -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. diff --git a/src/hooks/useSessionStorage.ts b/src/hooks/useSessionStorage.ts new file mode 100644 index 0000000..7fc5aeb --- /dev/null +++ b/src/hooks/useSessionStorage.ts @@ -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 = ( + key: string, + initialValue: T, +): [T, (value: T) => void] => { + const [storedValue, setStoredValue] = useState(() => { + 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; diff --git a/src/hooks/useTheme.ts b/src/hooks/useTheme.ts index dceb815..b75726f 100644 --- a/src/hooks/useTheme.ts +++ b/src/hooks/useTheme.ts @@ -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]); diff --git a/src/locales/eng/general.json b/src/locales/eng/general.json new file mode 100644 index 0000000..e7a35f9 --- /dev/null +++ b/src/locales/eng/general.json @@ -0,0 +1,7 @@ +{ + "welcome": "Welcome!", + "login_button": "Login", + "login_username": "Username", + "login_password": "Password", + "login_accept_terms": "I accept the terms and conditions" +} diff --git a/src/locales/localesConfig.ts b/src/locales/localesConfig.ts new file mode 100644 index 0000000..79575e8 --- /dev/null +++ b/src/locales/localesConfig.ts @@ -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", + }); diff --git a/src/locales/pol/general.json b/src/locales/pol/general.json new file mode 100644 index 0000000..a2e514e --- /dev/null +++ b/src/locales/pol/general.json @@ -0,0 +1,7 @@ +{ + "welcome": "Witaj!", + "login_button": "Zaloguj", + "login_username": "Nazwa użytkownika", + "login_password": "Hasło", + "login_accept_terms": "Akceptuję regulamin" +} diff --git a/src/main.tsx b/src/main.tsx index a4276ef..06efb39 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -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 ( diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx index 86a4cda..18783c9 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -1,8 +1,9 @@ +import { useTranslation } from "react-i18next"; import { main } from "../configure"; const LoginPage = () => { + const { t } = useTranslation(); return ( - // hero daisyui login page
@@ -15,21 +16,21 @@ const LoginPage = () => {
@@ -37,12 +38,16 @@ const LoginPage = () => {
- +
diff --git a/src/routes/SwitchRouteGenerator.tsx b/src/routes/SwitchRouteGenerator.tsx index f0d0321..bca3208 100644 --- a/src/routes/SwitchRouteGenerator.tsx +++ b/src/routes/SwitchRouteGenerator.tsx @@ -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 ( diff --git a/src/utils/ObjectUtils.ts b/src/utils/ObjectUtils.ts index 47b3177..b19cfd2 100644 --- a/src/utils/ObjectUtils.ts +++ b/src/utils/ObjectUtils.ts @@ -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>, -) => { +): 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(); }; diff --git a/src/utils/StringTransformationUtils.ts b/src/utils/StringTransformationUtils.ts index b96728d..e389c68 100644 --- a/src/utils/StringTransformationUtils.ts +++ b/src/utils/StringTransformationUtils.ts @@ -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, ""); }; diff --git a/src/utils/inDebug.ts b/src/utils/inDev.ts similarity index 67% rename from src/utils/inDebug.ts rename to src/utils/inDev.ts index 744805a..064adb5 100644 --- a/src/utils/inDebug.ts +++ b/src/utils/inDev.ts @@ -1,11 +1,11 @@ /** * Execute whatever you pass only in development (not production) */ -const inDebug = (callback: () => T): T | null => { +const inDev = (callback: () => T): T | null => { if (process.env.NODE_ENV === "development") { return callback(); } return null; }; -export default inDebug; +export default inDev; diff --git a/tsconfig.json b/tsconfig.json index 14d2753..d710eeb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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