diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..4aff1c3 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @basmasking @petermasking diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..b498248 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,6 @@ +Fixes # + +Changes proposed in this pull request: +- +- +- diff --git a/.github/workflows/nodejsci.yml b/.github/workflows/nodejsci.yml new file mode 100644 index 0000000..038601a --- /dev/null +++ b/.github/workflows/nodejsci.yml @@ -0,0 +1,37 @@ +name: Node.js CI + +on: + pull_request: + branches: [ main ] + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [lts/*] + + steps: + - name: ⚙️ Checkout Repository + uses: actions/checkout@v6 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node-version }} + cache: npm + cache-dependency-path: '**/package-lock.json' + + - name: 📦 Install Dependencies + run: npm ci + + - name: 🚀 Build Package + run: npm run build + + - name: ✅ Lint Package + run: npm run lint \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..be639ff --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,30 @@ +on: + push: + branches: + - main + +permissions: + id-token: write + contents: read + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: ⚙️ Checkout Repository + uses: actions/checkout@v6 + + - name: 🛠️ Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: '24.x' + registry-url: 'https://registry.npmjs.org/' + + - name: 📦 Install Dependencies + run: npm ci + + - name: 🚀 Publish Package + run: npm publish + + - name: ✅ Publish Complete + run: echo "Package published successfully." \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..28fd778 --- /dev/null +++ b/README.md @@ -0,0 +1,165 @@ +# React Toolkit + +A set of reusables for React mainly used by ourselves. + +## Components + +### ErrorBoundary + +Catches errors and passes it to the provided element. + +Usage: + +```tsx +import { ErrorBoundary } from '@maskingtech/react-toolkit'; + +function ErrorHandler({ error }: { error: unknown }) +{ + return <>Oops...; +} + + + {/* Content goes here */} +; +``` + +## Hooks + +### useDebouncedValue + +Delays value updates for a period of time. + +Usage: + +```tsx +import { useDebouncedValue } from '@maskingtech/react-toolkit'; + +function MyComponent() +{ + const initialValue: number = 0; // required + const onChange = (debouncedValue: number) => { }; // optional + const delay = 300; // optional, default 500 + + const [debouncedValue, setValue] = useDebouncedValue(initialValue, onChange, delay); + + return

+ {debouncedValue} + +

; +} +``` + +### useFocusOnMount + +Gives a form element focus after mount. + +Usage: + +```tsx +import { useFocusOnMount } from '@maskingtech/react-toolkit'; + +function MyComponent() +{ + const ref = useFocusOnMount(); + + return ; +} +``` + +### useForm + +Provides access to the data of a form after submitting. + +Usage: + +```tsx +import { useForm } from '@maskingtech/react-toolkit'; + +function MyComponent() +{ + const submitHandler = (data: FormData) { console.log(data.get('name')); }; + + const [ref, state, handleSubmit] = useForm(submitHandler); + + // states: 'pristine' | 'dirty' | 'submitting' + + return
+ + +
; +} +``` + +### useFormData + +Provides access to the data of a form without submitting. + +Usage: + +```tsx +import { useFormData } from '@maskingtech/react-toolkit'; + +function MyComponent() +{ + const dataHandler = (data: FormData) { console.log(data.get('name')); }; + + const [ref, state, handleData] = useFormData(dataHandler); + + // states: 'idle' | 'working' + + return
+ + +
; +} +``` + +### useLoadData + +Provides helpers for loading data. + +Usage: + +```tsx +import { useLoadData } from '@maskingtech/react-toolkit'; + +async function getData() { /* get data here */ } + +function MyComponent() +{ + const [data, isLoading, refresh, setData] = useLoadData(getData); + + if (isLoading) return <>Loading...; + + return

+ {data} + +

; +} +``` + +### usePagination + +Provides helpers for loading paginated data. + +Usage: + +```tsx +import { useLoadData } from '@maskingtech/react-toolkit'; + +async function getPageData(page: number) { /* get data here */ } + +function MyComponent() +{ + const [data, isLoading, isFinished, next, previous, reset, setData] = useLoadData(useLoadData); + + if (isLoading) return <>Loading...; + + return

+ {data} + + + +

; +} +``` diff --git a/package-lock.json b/package-lock.json index 8eeb38c..2d7f067 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@maskingtech/react-toolkit", - "version": "0.0.3", + "version": "0.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@maskingtech/react-toolkit", - "version": "0.0.3", + "version": "0.0.2", "devDependencies": { "@eslint/js": "9.39.1", "@types/node": "25.0.1", @@ -51,6 +51,7 @@ "version": "7.28.5", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -750,7 +751,6 @@ "version": "0.21.1", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", @@ -764,7 +764,6 @@ "version": "0.4.2", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@eslint/core": "^0.17.0" }, @@ -776,7 +775,6 @@ "version": "0.17.0", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@types/json-schema": "^7.0.15" }, @@ -788,7 +786,6 @@ "version": "3.3.3", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -822,7 +819,6 @@ "version": "2.1.7", "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } @@ -831,7 +827,6 @@ "version": "0.4.1", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" @@ -844,7 +839,6 @@ "version": "0.19.1", "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=18.18.0" } @@ -853,7 +847,6 @@ "version": "0.16.7", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" @@ -866,7 +859,6 @@ "version": "1.0.1", "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=12.22" }, @@ -879,7 +871,6 @@ "version": "0.4.3", "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=18.18" }, @@ -1469,6 +1460,7 @@ "version": "8.13.0", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", @@ -1689,13 +1681,13 @@ "node_modules/@types/json-schema": { "version": "7.0.15", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/node": { "version": "25.0.1", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1747,6 +1739,7 @@ "version": "8.49.0", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.49.0", "@typescript-eslint/types": "8.49.0", @@ -2075,6 +2068,7 @@ "version": "8.15.0", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2086,7 +2080,6 @@ "version": "5.3.2", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -2095,7 +2088,6 @@ "version": "6.12.6", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -2152,7 +2144,6 @@ "version": "4.3.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -2166,8 +2157,7 @@ "node_modules/argparse": { "version": "2.0.1", "dev": true, - "license": "Python-2.0", - "peer": true + "license": "Python-2.0" }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", @@ -2355,6 +2345,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2417,7 +2408,6 @@ "version": "3.1.0", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -2445,7 +2435,6 @@ "version": "4.1.2", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -2461,7 +2450,6 @@ "version": "2.0.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -2472,8 +2460,7 @@ "node_modules/color-name": { "version": "1.1.4", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/compare-versions": { "version": "6.1.1", @@ -2499,7 +2486,6 @@ "version": "7.0.6", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -2586,8 +2572,7 @@ "node_modules/deep-is": { "version": "0.1.4", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/define-data-property": { "version": "1.1.4", @@ -2882,7 +2867,6 @@ "version": "4.0.0", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -2984,7 +2968,6 @@ "version": "8.4.0", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -3011,7 +2994,6 @@ "version": "9.39.2", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -3023,7 +3005,6 @@ "version": "10.4.0", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", @@ -3040,7 +3021,6 @@ "version": "1.6.0", "dev": true, "license": "BSD-3-Clause", - "peer": true, "dependencies": { "estraverse": "^5.1.0" }, @@ -3052,7 +3032,6 @@ "version": "4.3.0", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "estraverse": "^5.2.0" }, @@ -3094,14 +3073,12 @@ "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/fast-uri": { "version": "3.1.0", @@ -3138,7 +3115,6 @@ "version": "8.0.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "flat-cache": "^4.0.0" }, @@ -3150,7 +3126,6 @@ "version": "5.0.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -3166,7 +3141,6 @@ "version": "4.0.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" @@ -3178,8 +3152,7 @@ "node_modules/flatted": { "version": "3.3.3", "dev": true, - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/for-each": { "version": "0.3.5", @@ -3342,7 +3315,6 @@ "version": "6.0.2", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "is-glob": "^4.0.3" }, @@ -3368,7 +3340,6 @@ "version": "14.0.0", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -3499,7 +3470,6 @@ "version": "5.3.2", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 4" } @@ -3508,7 +3478,6 @@ "version": "3.3.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -3532,7 +3501,6 @@ "version": "0.1.4", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.8.19" } @@ -3673,7 +3641,6 @@ "version": "2.1.1", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -3714,7 +3681,6 @@ "version": "4.0.3", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -3894,8 +3860,7 @@ "node_modules/isexe": { "version": "2.0.0", "dev": true, - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/iterator.prototype": { "version": "1.1.5", @@ -3927,7 +3892,6 @@ "version": "4.1.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "argparse": "^2.0.1" }, @@ -3949,20 +3913,17 @@ "node_modules/json-buffer": { "version": "3.0.1", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/json5": { "version": "2.2.3", @@ -4004,7 +3965,6 @@ "version": "4.5.4", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "json-buffer": "3.0.1" } @@ -4018,7 +3978,6 @@ "version": "0.4.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -4047,7 +4006,6 @@ "version": "6.0.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "p-locate": "^5.0.0" }, @@ -4066,8 +4024,7 @@ "node_modules/lodash.merge": { "version": "4.6.2", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/loose-envify": { "version": "1.4.0", @@ -4284,7 +4241,6 @@ "version": "0.9.4", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -4317,7 +4273,6 @@ "version": "3.1.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "yocto-queue": "^0.1.0" }, @@ -4332,7 +4287,6 @@ "version": "5.0.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "p-limit": "^3.0.2" }, @@ -4352,7 +4306,6 @@ "version": "1.0.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "callsites": "^3.0.0" }, @@ -4369,7 +4322,6 @@ "version": "4.0.0", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -4378,7 +4330,6 @@ "version": "3.1.1", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -4425,6 +4376,7 @@ "version": "4.0.3", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -4481,7 +4433,6 @@ "version": "1.2.1", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8.0" } @@ -4608,7 +4559,6 @@ "version": "4.0.0", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=4" } @@ -4789,7 +4739,6 @@ "version": "2.0.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -4801,7 +4750,6 @@ "version": "3.0.0", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -5018,7 +4966,6 @@ "version": "7.2.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -5067,7 +5014,6 @@ "version": "0.4.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "prelude-ls": "^1.2.1" }, @@ -5149,6 +5095,7 @@ "version": "5.9.3", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5255,6 +5202,7 @@ "version": "7.2.7", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -5358,7 +5306,6 @@ "version": "2.0.2", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "isexe": "^2.0.0" }, @@ -5454,7 +5401,6 @@ "version": "1.2.5", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -5468,7 +5414,6 @@ "version": "0.1.0", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, diff --git a/package.json b/package.json index 18a6add..6d51f22 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,11 @@ { "name": "@maskingtech/react-toolkit", "private": false, - "version": "0.0.3", + "version": "0.0.2", "type": "module", + "repository": { + "url": "https://github.com/MaskingTechnology/react-toolkit" + }, "scripts": { "build": "vite build", "clean": "rimraf dist", diff --git a/src/hooks/useDebouncedValue.ts b/src/hooks/useDebouncedValue.ts index 516af9e..e166071 100644 --- a/src/hooks/useDebouncedValue.ts +++ b/src/hooks/useDebouncedValue.ts @@ -3,7 +3,7 @@ import { useEffect, useState } from 'react'; type ChangeHandler = (value: T) => void; -export function useDebouncedValue(initialValue: T, onChange: ChangeHandler, delay = 500) +export function useDebouncedValue(initialValue: T, onChange?: ChangeHandler, delay = 500) { const [value, setValue] = useState(initialValue); const [debouncedValue, setDebouncedValue] = useState(initialValue); @@ -18,7 +18,10 @@ export function useDebouncedValue(initialValue: T, onChange: ChangeHandler useEffect(() => { - onChange(debouncedValue); + if (onChange !== undefined) + { + onChange(debouncedValue); + } }, [debouncedValue, onChange]); diff --git a/src/hooks/useFocusOnMount.ts b/src/hooks/useFocusOnMount.ts index 333249b..84dfd12 100644 --- a/src/hooks/useFocusOnMount.ts +++ b/src/hooks/useFocusOnMount.ts @@ -2,7 +2,7 @@ import type { RefObject } from 'react'; import { useEffect, useRef } from 'react'; -export function useFocusOnMount(ref?: RefObject) +export function useFocusOnMount(ref?: RefObject) { const elementRef = ref ?? useRef(null); diff --git a/src/hooks/useFormData.ts b/src/hooks/useFormData.ts index 577d7ce..e76f775 100644 --- a/src/hooks/useFormData.ts +++ b/src/hooks/useFormData.ts @@ -2,7 +2,7 @@ import type { RefObject } from 'react'; import { useCallback, useState, useRef } from 'react'; -type States = 'idle' | 'submitting'; +type States = 'idle' | 'working'; type DataHandler = (data: FormData) => Promise; type FormRef = RefObject; @@ -18,7 +18,7 @@ export function useFormData(handler: DataHandler, ref?: FormRef) if (form === null) return; - setState('submitting'); + setState('working'); await handler(new FormData(form)); diff --git a/src/hooks/usePagination.ts b/src/hooks/usePagination.ts index d660646..4c3884c 100644 --- a/src/hooks/usePagination.ts +++ b/src/hooks/usePagination.ts @@ -50,12 +50,18 @@ export function usePagination(getData: GetData, limit: number, deps: Depen }, []); - const nextPage = useCallback(() => + const next = useCallback(() => { setPage(page => page + 1); }, []); + const previous = useCallback(() => + { + setPage(page => page - 1); + + }, []); + const reset = useCallback(() => { resetData(); @@ -67,5 +73,5 @@ export function usePagination(getData: GetData, limit: number, deps: Depen useEffect(loadData, [getData, loadData, page]); - return [data, isLoading, isFinished, nextPage, reset, setData] as const; + return [data, isLoading, isFinished, next, previous, reset, setData] as const; }