import React, { useEffect, useState } from "react"; import { cn } from "@/lib/utils"; import { InputGroup, InputGroupAddon, InputGroupInput, } from "@/components/ui/input-group"; /** * Numeric input for interest rates with a "%" suffix. * Accepts 0–max with one decimal place. Strips the slider from the app's * InterestRateInput — use this wherever just the rate field is needed. * * Consumers should wrap in + / for label/error display. */ export type InterestRateInputProps = { /** Current rate value (e.g. 5.5 for 5.5%) */ value: number; /** Called on blur with the parsed, validated value */ onChange: (value: number) => void; /** HTML id forwarded to the inner */ id?: string; /** Minimum allowed value (default: 0) */ min?: number; /** Maximum allowed value (default: 10) */ max?: number; disabled?: boolean; /** When true, renders the InputGroup in its error visual state */ error?: boolean; className?: string; }; /** Allow one decimal place: 0–max (e.g. "5", "5.", "5.5", "10.0") */ function buildValidationRegex(max: number): RegExp { const intMax = Math.floor(max); // Matches: integer part 0–max, optional single decimal digit return new RegExp(`^(${intMax}(\\.\\d?)?|[0-9](\\.[0-9]?)?)$`); } function clamp(n: number, min: number, max: number): number { return Math.min(max, Math.max(min, n)); } export function InterestRateInput({ value, onChange, id, min = 0, max = 10, disabled = false, error = false, className, }: InterestRateInputProps) { const [displayValue, setDisplayValue] = useState(() => value > 0 ? String(value) : "", ); const [focused, setFocused] = useState(false); const validationRegex = buildValidationRegex(max); // Sync when value changes externally useEffect(() => { if (!focused) { setDisplayValue(value > 0 ? String(value) : ""); } }, [value, focused]); function handleChange(e: React.ChangeEvent) { const raw = e.target.value; // Allow empty string or values matching the pattern if (raw === "" || validationRegex.test(raw)) { setDisplayValue(raw); } } function handleFocus() { setFocused(true); } function handleBlur() { setFocused(false); // Strip trailing decimal point, then parse + clamp const cleaned = displayValue.replace(/\.$/, ""); const parsed = parseFloat(cleaned); const final = isNaN(parsed) ? min : clamp(parsed, min, max); onChange(final); setDisplayValue(final > 0 ? String(final) : ""); } return ( % ); }