From 615cc777e3477da5c8c94c02ad20065079a81eca Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Wed, 21 Jan 2026 22:57:59 +0100 Subject: [PATCH] implement numora config and numora input --- apps/frontend/package.json | 1 + .../src/components/NumericInput/helpers.ts | 88 ------------------- .../src/components/NumericInput/index.tsx | 32 +++---- bun.lock | 3 + 4 files changed, 18 insertions(+), 106 deletions(-) delete mode 100644 apps/frontend/src/components/NumericInput/helpers.ts diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 5f2dbfce0..e3981f805 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -50,6 +50,7 @@ "i18next": "^24.2.3", "lottie-react": "^2.4.1", "motion": "^12.0.3", + "numora-react": "^3.0.2", "qrcode.react": "^4.2.0", "react": "=19.2.0", "react-dom": "=19.2.0", diff --git a/apps/frontend/src/components/NumericInput/helpers.ts b/apps/frontend/src/components/NumericInput/helpers.ts deleted file mode 100644 index 46a5d7e00..000000000 --- a/apps/frontend/src/components/NumericInput/helpers.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { ChangeEvent, ClipboardEvent } from "react"; - -const removeNonNumericCharacters = (value: string): string => value.replace(/[^0-9.]/g, ""); - -const removeExtraDots = (value: string): string => value.replace(/(\..*?)\./g, "$1"); - -function sanitizeNumericInput(value: string): string { - return removeExtraDots(removeNonNumericCharacters(value)); -} - -export function trimToMaxDecimals(value: string, maxDecimals: number): string { - const [integer, decimal] = value.split("."); - return decimal ? `${integer}.${decimal.slice(0, maxDecimals)}` : value; -} - -const replaceCommasWithDots = (value: string): string => value.replace(/,/g, "."); - -/** - * Handles the input change event to ensure the value does not exceed the maximum number of decimal places, - * replaces commas with dots, and removes invalid non-numeric characters. - * - * @param e - The keyboard event triggered by the input. - * @param maxDecimals - The maximum number of decimal places allowed. - */ -export function handleOnChangeNumericInput(e: ChangeEvent, maxDecimals: number): void { - const target = e.target as HTMLInputElement; - - target.value = replaceCommasWithDots(target.value); - - target.value = sanitizeNumericInput(target.value); - - target.value = trimToMaxDecimals(target.value, maxDecimals); - - target.value = handleLeadingZeros(target.value); - - target.value = replaceInvalidOrEmptyString(target.value); -} - -function replaceInvalidOrEmptyString(value: string): string { - if (value === "" || value === ".") { - return "0"; - } - return value; -} - -function handleLeadingZeros(value: string): string { - if (Number(value) >= 1) { - return value.replace(/^0+/, ""); - } - - // Add leading zeros for numbers < 1 that don't start with '0' - if (Number(value) < 1 && value[0] !== "0") { - return "0" + value; - } - - // No more than one leading zero - return value.replace(/^0+/, "0"); -} - -/** - * Handles the paste event to ensure the value does not exceed the maximum number of decimal places, - * replaces commas with dots, and removes invalid non-numeric characters. - * - * @param e - The clipboard event triggered by the input. - * @param maxDecimals - The maximum number of decimal places allowed. - * @returns The sanitized value after the paste event. - */ - -export function handleOnPasteNumericInput(e: ClipboardEvent, maxDecimals: number): string { - const inputElement = e.target as HTMLInputElement; - const { value, selectionStart, selectionEnd } = inputElement; - - const clipboardData = sanitizeNumericInput(e.clipboardData?.getData("text/plain") || ""); - - const combinedValue = value.slice(0, selectionStart || 0) + clipboardData + value.slice(selectionEnd || 0); - - const [integerPart, ...decimalParts] = combinedValue.split("."); - const sanitizedValue = integerPart + (decimalParts.length > 0 ? "." + decimalParts.join("") : ""); - - e.preventDefault(); - inputElement.value = trimToMaxDecimals(sanitizedValue, maxDecimals); - inputElement.value = handleLeadingZeros(inputElement.value); - - const newCursorPosition = (selectionStart || 0) + clipboardData.length - (combinedValue.length - sanitizedValue.length); - inputElement.setSelectionRange(newCursorPosition, newCursorPosition); - - return trimToMaxDecimals(sanitizedValue, maxDecimals); -} diff --git a/apps/frontend/src/components/NumericInput/index.tsx b/apps/frontend/src/components/NumericInput/index.tsx index c2d58b5c7..2edaee095 100644 --- a/apps/frontend/src/components/NumericInput/index.tsx +++ b/apps/frontend/src/components/NumericInput/index.tsx @@ -1,7 +1,7 @@ +import { NumoraInput } from "numora-react"; import { ChangeEvent, useEffect, useRef } from "react"; import { UseFormRegisterReturn, useFormContext, useWatch } from "react-hook-form"; import { cn } from "../../helpers/cn"; -import { handleOnChangeNumericInput, handleOnPasteNumericInput, trimToMaxDecimals } from "./helpers"; interface NumericInputProps { register: UseFormRegisterReturn; @@ -15,6 +15,11 @@ interface NumericInputProps { onChange?: (e: ChangeEvent) => void; } +function trimToMaxDecimals(value: string, maxDecimals: number): string { + const [integer, decimal] = value.split("."); + return decimal ? `${integer}.${decimal.slice(0, maxDecimals)}` : value; +} + export const NumericInput = ({ register, readOnly = false, @@ -30,21 +35,19 @@ export const NumericInput = ({ const inputValue = useWatch({ name: fieldName }); const prevMaxDecimals = useRef(maxDecimals); - function handleOnChange(e: ChangeEvent): void { - handleOnChangeNumericInput(e, maxDecimals); + function handleChange(e: ChangeEvent): void { const value = e.target.value; setValue(fieldName, value, { shouldDirty: true, shouldValidate: true }); - if (onChange) onChange(e); register.onChange(e); + if (onChange) onChange(e); } // Watch for maxDecimals changes and trim value if needed useEffect(() => { - if (prevMaxDecimals.current > maxDecimals) { + if (prevMaxDecimals.current > maxDecimals && inputValue) { const trimmed = trimToMaxDecimals(inputValue, maxDecimals); if (trimmed !== inputValue) { setValue(fieldName, trimmed, { shouldDirty: true, shouldValidate: true }); - // Create a synthetic event for register.onChange const syntheticEvent = { target: { value: trimmed } } as ChangeEvent; register.onChange(syntheticEvent); } @@ -54,8 +57,7 @@ export const NumericInput = ({ return (
- handleOnPasteNumericInput(event, maxDecimals)} - pattern="^[0-9]*[.,]?[0-9]*$" + maxDecimals={maxDecimals} + name={fieldName} + onChange={handleChange} placeholder="0.0" readOnly={readOnly} spellCheck={false} - step="any" - type="text" value={inputValue ?? ""} /> - {loading && ( - - )} + {loading && }
); }; diff --git a/bun.lock b/bun.lock index a17c19278..6c861303d 100644 --- a/bun.lock +++ b/bun.lock @@ -151,6 +151,7 @@ "i18next": "^24.2.3", "lottie-react": "^2.4.1", "motion": "^12.0.3", + "numora-react": "^3.0.2", "qrcode.react": "^4.2.0", "react": "=19.2.0", "react-dom": "=19.2.0", @@ -3168,6 +3169,8 @@ "nullthrows": ["nullthrows@1.1.1", "", {}, "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw=="], + "numora-react": ["numora-react@3.0.2", "", {}, "sha512-PsWjhFsvk1AxB4C2jA8Lj4pNJ5fvCEIjJtop48gxJjZyPjfOZBqLzdt4UwA6+rogOc/gYeMhCzS9uyjGKQBpjA=="], + "obj-multiplex": ["obj-multiplex@1.0.0", "", { "dependencies": { "end-of-stream": "^1.4.0", "once": "^1.4.0", "readable-stream": "^2.3.3" } }, "sha512-0GNJAOsHoBHeNTvl5Vt6IWnpUEcc3uSRxzBri7EDyIcMgYvnY2JL2qdeV5zTMjWQX5OHcD5amcW2HFfDh0gjIA=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],