diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 00000000..66ff6a67 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,2 @@ +npm run lint +npm run build diff --git a/package-lock.json b/package-lock.json index 1aa69d33..4a583dbd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "axios": "^1.12.2", "clsx": "^2.1.1", "formik": "^2.4.6", + "inputmask": "^5.0.9", "moment": "^2.30.1", "next": "15.5.3", "react": "19.1.0", @@ -29,12 +30,14 @@ "@eslint/eslintrc": "^3", "@iconify/react": "^6.0.2", "@tailwindcss/postcss": "^4", + "@types/inputmask": "^5.0.7", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", "daisyui": "^5.1.12", "eslint": "^9", "eslint-config-next": "15.5.3", + "husky": "^9.1.7", "tailwindcss": "^4", "typescript": "^5" } @@ -1639,6 +1642,13 @@ "@types/react": "*" } }, + "node_modules/@types/inputmask": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@types/inputmask/-/inputmask-5.0.7.tgz", + "integrity": "sha512-uojbVPWzBQ/n/0jc/d16fLqmGasFIptbrLD2WrCPWArlk+5PgblOqH4EDkI3AoobXLAlOK5yF01V8jMmvMG5qg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1674,6 +1684,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz", "integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1743,6 +1754,7 @@ "integrity": "sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.43.0", "@typescript-eslint/types": "8.43.0", @@ -2260,6 +2272,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2828,7 +2841,8 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/daisyui": { "version": "5.1.12", @@ -3262,6 +3276,7 @@ "integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3436,6 +3451,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -4176,6 +4192,22 @@ "react-is": "^16.7.0" } }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4212,6 +4244,12 @@ "node": ">=0.8.19" } }, + "node_modules/inputmask": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/inputmask/-/inputmask-5.0.9.tgz", + "integrity": "sha512-s0lUfqcEbel+EQXtehXqwCJGShutgieOaIImFKC/r4reYNvX3foyrChl6LOEvaEgxEbesePIrw1Zi2jhZaDZbQ==", + "license": "MIT" + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -5749,6 +5787,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -5758,6 +5797,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -6574,6 +6614,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -6741,6 +6782,7 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 8adf6787..70e5737f 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "dev": "eslint && next dev --turbopack", "build": "next build --turbopack", "start": "next start", - "lint": "eslint" + "lint": "eslint", + "prepare": "husky" }, "dependencies": { "@tanstack/match-sorter-utils": "^8.19.4", @@ -14,6 +15,7 @@ "axios": "^1.12.2", "clsx": "^2.1.1", "formik": "^2.4.6", + "inputmask": "^5.0.9", "moment": "^2.30.1", "next": "15.5.3", "react": "19.1.0", @@ -30,12 +32,14 @@ "@eslint/eslintrc": "^3", "@iconify/react": "^6.0.2", "@tailwindcss/postcss": "^4", + "@types/inputmask": "^5.0.7", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", "daisyui": "^5.1.12", "eslint": "^9", "eslint-config-next": "15.5.3", + "husky": "^9.1.7", "tailwindcss": "^4", "typescript": "^5" } diff --git a/src/components/input/CheckboxInput.tsx b/src/components/input/CheckboxInput.tsx index 8ee202db..fb0c95c7 100644 --- a/src/components/input/CheckboxInput.tsx +++ b/src/components/input/CheckboxInput.tsx @@ -1,278 +1,87 @@ 'use client'; -import { ChangeEventHandler, FocusEventHandler, ReactNode, useId } from 'react'; - +import { HTMLProps, useEffect, useRef } from 'react'; import { cn } from '@/lib/helper'; -import FieldMessage from '@/components/helper/FieldMessage'; -export interface CheckboxInputProps { - // Basic Props +interface CheckboxInputProps extends HTMLProps { name: string; label?: string; - bottomLabel?: string; - checked?: boolean; - value?: string | number; indeterminate?: boolean; - naked?: boolean; // New prop for checkbox-only mode - - // Styling Props - className?: { + classNames?: { wrapper?: string; - label?: string; + inputWrapper?: string; checkbox?: string; - input?: string; + label?: string; }; - - // State Props isError?: boolean; isValid?: boolean; errorMessage?: string; - disabled?: boolean; - readOnly?: boolean; - required?: boolean; - isLoading?: boolean; - - // Adornment Props - startAdornment?: ReactNode; - endAdornment?: ReactNode; - - // Event Handlers - onChange?: ChangeEventHandler; - onBlur?: FocusEventHandler; - onFocus?: FocusEventHandler; - - // Additional Props - tooltip?: string; - description?: string; - size?: 'sm' | 'md' | 'lg'; - variant?: - | 'default' - | 'primary' - | 'secondary' - | 'success' - | 'warning' - | 'info' - | 'error'; } const CheckboxInput = ({ + indeterminate, name, label, - bottomLabel, - checked = false, - value, - indeterminate = false, - naked = false, className, + classNames, + isValid, isError, errorMessage, - disabled = false, - readOnly = false, - required = false, - isLoading = false, - startAdornment, - endAdornment, - onChange, - onBlur, - onFocus, - tooltip, - description, - size = 'md', - variant = 'default', + ...rest }: CheckboxInputProps) => { - const showErrorMessage = Boolean(isError && errorMessage); - const feedbackMessage = showErrorMessage ? errorMessage : bottomLabel; + const ref = useRef(null!); - // Size classes - const sizeClasses = { - sm: 'checkbox-sm', - md: 'checkbox-md', - lg: 'checkbox-lg', - }; - - // Variant classes - const variantClasses = { - default: '', - primary: 'checkbox-primary', - secondary: 'checkbox-secondary', - success: 'checkbox-success', - warning: 'checkbox-warning', - info: 'checkbox-info', - error: 'checkbox-error', - }; - - // Generate unique ID for accessibility using React's useId hook for SSR compatibility - const generatedId = useId(); - const checkboxId = `checkbox-${name}-${generatedId}`; - - // Naked mode - only checkbox, no wrapper structure - if (naked) { - return ( -
- { - if (input) { - input.indeterminate = indeterminate; - } - }} - /> - - {/* Loading State */} - {isLoading && ( -
- -
- )} -
- ); - } + useEffect(() => { + if (typeof indeterminate === 'boolean') { + ref.current.indeterminate = !rest.checked && indeterminate; + } + }, [ref, indeterminate]); return (
- {/* Label with Tooltip Support */} - {label && ( -
- - )} - - {/* Description */} - {description && ( -

- {description} -

- )} - - {/* Field Message */} - + {errorMessage && {errorMessage}} ); }; diff --git a/src/components/input/FileInput.tsx b/src/components/input/FileInput.tsx index 285a3f42..6218970c 100644 --- a/src/components/input/FileInput.tsx +++ b/src/components/input/FileInput.tsx @@ -2,7 +2,6 @@ import { Ref } from 'react'; import { cn } from '@/lib/helper'; import { TextInputProps } from '@/components/input/TextInput'; -import FieldMessage from '@/components/helper/FieldMessage'; interface FileInputProps extends Omit< @@ -38,9 +37,6 @@ const FileInput = ({ onBlur, readOnly = false, }: FileInputProps) => { - const showErrorMessage = Boolean(isError && errorMessage); - const feedbackMessage = showErrorMessage ? errorMessage : bottomLabel; - return (
- + {bottomLabel && ( +

{bottomLabel}

+ )} + + {isError &&

{errorMessage}

}
); }; diff --git a/src/components/input/NumberInput.tsx b/src/components/input/NumberInput.tsx index 4d6a7393..5b2188ee 100644 --- a/src/components/input/NumberInput.tsx +++ b/src/components/input/NumberInput.tsx @@ -6,24 +6,55 @@ import { FocusEventHandler, ReactNode, useEffect, + useRef, useState, } from 'react'; -import { Icon } from '@iconify/react'; import { cn } from '@/lib/helper'; -import FieldMessage from '@/components/helper/FieldMessage'; +import Inputmask from 'inputmask'; -export type MaskType = 'currency' | 'weight' | 'decimal' | 'number'; +const createInputMask = ( + maskType: MaskType, + decimals: number, + thousandSeparator: string, + decimalSeparator: string, + allowNegative: boolean, + oncomplete?: () => void, + onincomplete?: () => void, + oncleared?: () => void +): Inputmask.Instance => { + const options: Inputmask.Options = { + alias: 'numeric', + groupSeparator: thousandSeparator, + radixPoint: decimalSeparator, + digits: decimals, + allowMinus: allowNegative, + rightAlign: false, + insertMode: true, + autoUnmask: false, + clearMaskOnLostFocus: false, + digitsOptional: decimals > 0, + placeholder: '0', + numericInput: false, + positionCaretOnClick: 'radixFocus', + greedy: true, + oncomplete, + onincomplete, + oncleared + }; + + return new Inputmask(options); +}; + +export type MaskType = 'currency' | 'weight' | 'decimal' | 'number' | 'text'; export interface NumberInputProps { - // Basic Props label?: string; bottomLabel?: string; name: string; value?: number | string; placeholder?: string; - // Styling Props className?: { wrapper?: string; label?: string; @@ -31,7 +62,6 @@ export interface NumberInputProps { input?: string; }; - // State Props isError?: boolean; isValid?: boolean; errorMessage?: string; @@ -40,16 +70,13 @@ export interface NumberInputProps { required?: boolean; isLoading?: boolean; - // Adornment Props startAdornment?: ReactNode; endAdornment?: ReactNode; - // Event Handlers onChange?: ChangeEventHandler; onBlur?: FocusEventHandler; onFocus?: FocusEventHandler; - // Masking Options maskType?: MaskType; decimals?: number; thousandSeparator?: string; @@ -57,131 +84,50 @@ export interface NumberInputProps { currencyPrefix?: string; weightUnit?: string; - // Validation Props min?: number; max?: number; allowNegative?: boolean; - // Stepper Options - showSteppers?: boolean; - step?: number; + oncomplete?: () => void; + onincomplete?: () => void; + oncleared?: () => void; } -// UTILITY FUNCTIONS -/** - * Core number formatting function - * Formats number with thousand separator and decimal separator - */ -const formatNumber = ( - value: number | string, - decimals: number = 0, - thousandSeparator: string = '.', - decimalSeparator: string = ',' -): string => { - if (value === '' || value === null || value === undefined) return ''; - - const numValue = typeof value === 'string' ? parseFloat(value) : value; - if (isNaN(numValue)) return ''; - - const parts = numValue.toFixed(decimals).split('.'); - const integerPart = parts[0].replace( - /\B(?=(\d{3})+(?!\d))/g, - thousandSeparator - ); - const decimalPart = parts[1]; - - return decimals > 0 && decimalPart - ? `${integerPart}${decimalSeparator}${decimalPart}` - : integerPart; -}; - -/** - * Parse formatted string to number - * Converts formatted input back to raw number for processing - */ -const parseNumber = ( - value: string, - thousandSeparator: string = '.', - decimalSeparator: string = ',' -): number => { - if (!value) return 0; - - // Remove thousand separators and replace decimal separator with dot - const cleaned = value - .replace(new RegExp(`\\${thousandSeparator}`, 'g'), '') - .replace(decimalSeparator, '.'); - - const parsed = parseFloat(cleaned); - return isNaN(parsed) ? 0 : parsed; -}; - -/** - * Clean and validate numeric input while typing - * Ensures only valid characters are allowed - */ -const cleanNumericInput = ( - value: string, - allowDecimal: boolean = false, - decimalSeparator: string = ',' -): string => { - // Only allow numbers, decimal separator (if allowed), and minus sign at the start - let cleaned = value.replace(/[^\d,.-]/g, ''); - - // Handle decimal separator - if (allowDecimal) { - const parts = cleaned.split(decimalSeparator); - if (parts.length > 2) { - // Keep only first decimal separator - cleaned = parts[0] + decimalSeparator + parts.slice(1).join(''); - } - } else { - cleaned = cleaned.replace(new RegExp(decimalSeparator, 'g'), ''); - } - - // Handle minus sign (only at start) - const hasMinusAtStart = cleaned.startsWith('-'); - cleaned = cleaned.replace(/-/g, ''); - if (hasMinusAtStart) cleaned = '-' + cleaned; - - return cleaned; -}; - const NumberInput = ({ - label, - bottomLabel, - name, - value, - placeholder, - className, - isError, - isValid, - errorMessage, - startAdornment, - endAdornment, - disabled = false, - required = false, - onChange, - onBlur, - onFocus, - readOnly = false, - isLoading = false, - maskType = 'number', - decimals = 0, - thousandSeparator = '.', - decimalSeparator = ',', - currencyPrefix = 'Rp ', - weightUnit = 'kg', - min, - max, - allowNegative = false, - showSteppers = false, - step = 1, -}: NumberInputProps) => { - const [displayValue, setDisplayValue] = useState(''); - - // CONFIG & HELPERS - const allowDecimal = - maskType === 'decimal' || maskType === 'weight' || decimals > 0; + label, + bottomLabel, + name, + value, + placeholder, + className, + isError, + isValid, + errorMessage, + startAdornment, + endAdornment, + disabled = false, + required = false, + onChange, + onBlur, + onFocus, + readOnly = false, + isLoading = false, + maskType = 'number', + decimals = 0, + thousandSeparator = ',', + decimalSeparator = '.', + currencyPrefix = 'Rp ', + weightUnit = 'kg', + allowNegative = false, + oncomplete, + onincomplete, + oncleared, + }: NumberInputProps) => { + const inputRef = useRef(null); + const inputmaskRef = useRef(null); + const [maskComplete, setMaskComplete] = useState(false); + const [maskIncomplete, setMaskIncomplete] = useState(false); + const [maskCleared, setMaskCleared] = useState(false); const getInputPrefix = (): string => { switch (maskType) { @@ -201,165 +147,93 @@ const NumberInput = ({ } }; - const getFormattedValue = (rawValue: number | string): string => { - if (rawValue === '' || rawValue === null || rawValue === undefined) - return ''; - - switch (maskType) { - case 'currency': - case 'weight': - case 'decimal': - case 'number': - return formatNumber( - rawValue, - decimals, - thousandSeparator, - decimalSeparator - ); - default: - return String(rawValue); - } - }; - - // EFFECTS useEffect(() => { - setDisplayValue(getFormattedValue(value || '')); + if (inputRef.current && !readOnly && !disabled) { + if (inputmaskRef.current) { + try { + inputmaskRef.current.remove(); + } catch (error) { + console.warn('Error removing Inputmask:', error); + } + } + + const handleComplete = () => { + setMaskComplete(true); + setMaskIncomplete(false); + setMaskCleared(false); + if (oncomplete) oncomplete(); + }; + + const handleIncomplete = () => { + setMaskIncomplete(true); + setMaskComplete(false); + setMaskCleared(false); + if (onincomplete) onincomplete(); + }; + + const handleCleared = () => { + setMaskCleared(true); + setMaskComplete(false); + setMaskIncomplete(false); + if (oncleared) oncleared(); + }; + + const im = createInputMask( + maskType, + decimals, + ',', + '.', + allowNegative, + handleComplete, + handleIncomplete, + handleCleared + ); + + try { + im.mask(inputRef.current); + inputmaskRef.current = im; + } catch (error) { + console.warn('Error applying Inputmask:', error); + inputmaskRef.current = null; + } + } + + return () => { + if (inputmaskRef.current) { + try { + inputmaskRef.current.remove(); + } catch (error) { + console.warn('Error removing Inputmask on cleanup:', error); + } + } + }; + }, [maskType, decimals, thousandSeparator, decimalSeparator, allowNegative, readOnly, disabled, oncomplete, onincomplete, oncleared]); + + useEffect(() => { + if (inputRef.current && value !== undefined) { + if (value === null || value === '') { + inputRef.current.value = ''; + } else { + inputRef.current.value = String(value); + } + } }, [value]); - // EVENT HANDLERS - const handleInputChange = (e: ChangeEvent) => { - const inputValue = e.target.value; + const handleKeyUp = (e: React.KeyboardEvent) => { + const currentValue = (e.currentTarget as HTMLInputElement).value; + console.log('✅ After format:', currentValue); - // Clean input - const cleaned = cleanNumericInput( - inputValue, - allowDecimal, - decimalSeparator - ); - - // Parse to number - let numericValue = parseNumber( - cleaned, - thousandSeparator, - decimalSeparator - ); - - // Apply validation - if (!allowNegative && numericValue < 0) { - numericValue = 0; - } - if (min !== undefined && numericValue < min) { - numericValue = min; - } - if (max !== undefined && numericValue > max) { - numericValue = max; - } - - // Update display value - const formattedForDisplay = formatNumber( - numericValue, - decimals, - thousandSeparator, - decimalSeparator - ); - - setDisplayValue(formattedForDisplay); - - // Call onChange with modified event - if (onChange) { - const syntheticEvent = { - ...e, - target: { - ...e.target, - name, - value: numericValue.toString(), - }, - } as ChangeEvent; - - onChange(syntheticEvent); - } - }; - - const handleIncrement = () => { - if (disabled || readOnly) return; - - const currentValue = parseNumber( - displayValue, - thousandSeparator, - decimalSeparator - ); - let newValue = currentValue + step; - - // Apply max validation - if (max !== undefined && newValue > max) { - newValue = max; - } - - // Update display - const formattedForDisplay = formatNumber( - newValue, - decimals, - thousandSeparator, - decimalSeparator - ); - setDisplayValue(formattedForDisplay); - - // Call onChange with synthetic event if (onChange) { const syntheticEvent = { target: { name, - value: newValue.toString(), + value: currentValue, }, } as ChangeEvent; - onChange(syntheticEvent); } }; - const handleDecrement = () => { - if (disabled || readOnly) return; - - const currentValue = parseNumber( - displayValue, - thousandSeparator, - decimalSeparator - ); - let newValue = currentValue - step; - - // Apply min validation (prevent negative if not allowed) - if (!allowNegative && newValue < 0) { - newValue = 0; - } - if (min !== undefined && newValue < min) { - newValue = min; - } - - // Update display - const formattedForDisplay = formatNumber( - newValue, - decimals, - thousandSeparator, - decimalSeparator - ); - setDisplayValue(formattedForDisplay); - - // Call onChange with synthetic event - if (onChange) { - const syntheticEvent = { - target: { - name, - value: newValue.toString(), - }, - } as ChangeEvent; - - onChange(syntheticEvent); - } - }; - - // RENDER CALCULATIONS - const showErrorMessage = Boolean(isError && errorMessage); - const feedbackMessage = showErrorMessage ? errorMessage : bottomLabel; const inputPrefix = getInputPrefix(); const inputSuffix = getInputSuffix(); @@ -393,9 +267,7 @@ const NumberInput = ({ )} - {/* Input Container */}
- {/* Prefix Block */} {inputPrefix && (
)} - {/* Input Wrapper */}
- {/* Stepper Buttons */} - {showSteppers && ( - - )} - - {/* Start Adornment */} {startAdornment && startAdornment} - {/* Main Input */} - {/* End Adornment & Loading */} {(isLoading || endAdornment) && (
{isLoading && } {endAdornment && endAdornment}
)} - - {/* Increment Button */} - {showSteppers && ( - - )}
- {/* Suffix Block */} {inputSuffix && (
- {/* Field Message */} - + {(maskType === 'text' || (oncomplete || onincomplete || oncleared)) && ( +
+ + Complete + + + Incomplete + + + Cleared + +
+ )} + + {!isError && bottomLabel && ( +

{bottomLabel}

+ )} + {isError && errorMessage && ( +

{errorMessage}

+ )}
); }; export default NumberInput; + diff --git a/src/components/input/RadioInput.tsx b/src/components/input/RadioInput.tsx index 65589258..71a731aa 100644 --- a/src/components/input/RadioInput.tsx +++ b/src/components/input/RadioInput.tsx @@ -2,7 +2,6 @@ import { ChangeEventHandler, ReactNode } from 'react'; import { cn } from '@/lib/helper'; -import FieldMessage from '@/components/helper/FieldMessage'; export interface RadioOption { label: string; @@ -48,8 +47,6 @@ const RadioInput = ({ onChange, onBlur, }: RadioInputProps) => { - const showErrorMessage = Boolean(isError && errorMessage); - const feedbackMessage = showErrorMessage ? errorMessage : bottomLabel; return (
{/* Label atas */} @@ -100,11 +97,15 @@ const RadioInput = ({ ))}
- + {/* Label bawah */} + {!isError && bottomLabel && ( +

{bottomLabel}

+ )} + + {/* Pesan error */} + {isError && errorMessage && ( +

{errorMessage}

+ )}
); }; diff --git a/src/components/input/SelectInput.tsx b/src/components/input/SelectInput.tsx index 9caf2e17..28eb9786 100644 --- a/src/components/input/SelectInput.tsx +++ b/src/components/input/SelectInput.tsx @@ -12,7 +12,6 @@ import CreatableSelect from 'react-select/creatable'; import makeAnimated from 'react-select/animated'; import { useDebounce } from 'use-debounce'; import { cn } from '@/lib/helper'; -import FieldMessage from '@/components/helper/FieldMessage'; export interface OptionType { value: string | number; @@ -118,9 +117,6 @@ const SelectInput = (props: SelectInputProps) => { } }; - const showErrorMessage = Boolean(isError && errorMessage); - const feedbackMessage = showErrorMessage ? errorMessage : bottomLabel; - return (
(props: SelectInputProps) => { }} /> - + {isError &&

{errorMessage}

} + {!isError && bottomLabel && ( +

{bottomLabel}

+ )}
); }; diff --git a/src/components/input/TagInput.tsx b/src/components/input/TagInput.tsx index 11407ada..a14b2f63 100644 --- a/src/components/input/TagInput.tsx +++ b/src/components/input/TagInput.tsx @@ -2,7 +2,6 @@ import React, { useState, KeyboardEvent, ChangeEvent, useEffect } from 'react'; import { cn } from '@/lib/helper'; -import FieldMessage from '@/components/helper/FieldMessage'; export interface TagInputProps { label?: string; @@ -74,9 +73,6 @@ const TagInput: React.FC = ({ setInputValue(e.target.value); }; - const showErrorMessage = Boolean(isError && errorMessage); - const feedbackMessage = showErrorMessage ? errorMessage : bottomLabel; - return (
= ({ )}
- + {/* Bottom label or error message */} + {!isError && bottomLabel && ( +

{bottomLabel}

+ )} + {isError &&

{errorMessage}

}
); }; diff --git a/src/components/input/TextArea.tsx b/src/components/input/TextArea.tsx index 34644021..03bf7c0b 100644 --- a/src/components/input/TextArea.tsx +++ b/src/components/input/TextArea.tsx @@ -3,7 +3,6 @@ import { ChangeEventHandler, FocusEventHandler, ReactNode } from 'react'; import { cn } from '@/lib/helper'; -import FieldMessage from '@/components/helper/FieldMessage'; export interface TextAreaProps { label?: string; @@ -51,9 +50,6 @@ const TextArea = ({ isLoading = false, rows = 3, }: TextAreaProps) => { - const showErrorMessage = Boolean(isError && errorMessage); - const feedbackMessage = showErrorMessage ? errorMessage : bottomLabel; - return (
)} - + {!isError && bottomLabel && ( +

{bottomLabel}

+ )} + {isError &&

{errorMessage}

}
); }; diff --git a/src/components/input/TextInput.tsx b/src/components/input/TextInput.tsx index 1bd154b6..43797637 100644 --- a/src/components/input/TextInput.tsx +++ b/src/components/input/TextInput.tsx @@ -8,7 +8,6 @@ import { } from 'react'; import { cn } from '@/lib/helper'; -import FieldMessage from '@/components/helper/FieldMessage'; export interface TextInputProps { type?: HTMLInputTypeAttribute; @@ -56,9 +55,6 @@ const TextInput = ({ readOnly = false, isLoading = false, }: TextInputProps) => { - const showErrorMessage = Boolean(isError && errorMessage); - const feedbackMessage = showErrorMessage ? errorMessage : bottomLabel; - return (
- + {!isError && bottomLabel && ( +

{bottomLabel}

+ )} + {isError && errorMessage && ( +

{errorMessage}

+ )}
); }; diff --git a/src/components/pages/inventory/movement/form/MovementForm.tsx b/src/components/pages/inventory/movement/form/MovementForm.tsx index 57d0a585..d2c91168 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.tsx +++ b/src/components/pages/inventory/movement/form/MovementForm.tsx @@ -29,7 +29,6 @@ import { SupplierApi, WarehouseApi } from '@/services/api/master-data'; import { ProductWarehouseApi } from '@/services/api/inventory'; import { toast } from 'react-hot-toast'; import FileInput from '@/components/input/FileInput'; -import FieldMessage from '@/components/helper/FieldMessage'; import CheckboxInput from '@/components/input/CheckboxInput'; interface MovementFormProps { @@ -848,7 +847,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { selectedProducts.length && formik.values.products?.length > 0 } - onChange={(e) => { + onChange={( + e: React.ChangeEvent + ) => { if (e.target.checked) { setSelectedProducts( formik.values.products?.map( @@ -859,8 +860,10 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { setSelectedProducts([]); } }} - naked={true} - size='sm' + classNames={{ + wrapper: 'flex justify-center', + checkbox: 'checkbox checkbox-sm', + }} /> @@ -891,11 +894,13 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { {type !== 'detail' && ( -
+
{ + onChange={( + e: React.ChangeEvent + ) => { if (e.target.checked) { setSelectedProducts([ ...selectedProducts, @@ -907,10 +912,11 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { ); } }} - naked={true} - size='sm' + classNames={{ + wrapper: 'flex justify-center', + checkbox: 'checkbox checkbox-sm', + }} /> -
)} @@ -1006,7 +1012,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { height={24} /> -
)} @@ -1064,7 +1069,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { selectedDeliveries.length && formik.values.deliveries?.length > 0 } - onChange={(e) => { + onChange={( + e: React.ChangeEvent + ) => { if (e.target.checked) { setSelectedDeliveries( formik.values.deliveries?.map( @@ -1075,8 +1082,10 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { setSelectedDeliveries([]); } }} - naked={true} - size='sm' + classNames={{ + wrapper: 'flex justify-center', + checkbox: 'checkbox checkbox-sm', + }} /> @@ -1153,11 +1162,13 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { {type !== 'detail' && ( -
+
{ + onChange={( + e: React.ChangeEvent + ) => { if (e.target.checked) { setSelectedDeliveries([ ...selectedDeliveries, @@ -1171,10 +1182,11 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { ); } }} - naked={true} - size='sm' + classNames={{ + wrapper: 'flex justify-center', + checkbox: 'checkbox checkbox-sm', + }} /> -
)} @@ -1323,10 +1335,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { '-' )} -
) : ( @@ -1444,7 +1452,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { height={24} /> - )} diff --git a/src/styles/daisyui.css b/src/styles/daisyui.css index 9a148fb4..dadaa2fa 100644 --- a/src/styles/daisyui.css +++ b/src/styles/daisyui.css @@ -8,4 +8,8 @@ --step-bg: var(--color-error); --step-fg: var(--color-error-content); } + + .table :where(th, td) { + vertical-align: top; + } }