From cccce2c85442b6d99d81b07900a22d8d3c245c6d Mon Sep 17 00:00:00 2001
From: Ricearoni1245
Date: Wed, 11 Mar 2026 20:02:32 -0500
Subject: [PATCH] fixed inputs to accept decimals
---
web/src/components/CurrencyInput.tsx | 24 +++++++++++++-----
web/src/pages/OnboardingPage.tsx | 38 ++++++++++++++++++++++++----
2 files changed, 51 insertions(+), 11 deletions(-)
diff --git a/web/src/components/CurrencyInput.tsx b/web/src/components/CurrencyInput.tsx
index c8184d7..daa494b 100644
--- a/web/src/components/CurrencyInput.tsx
+++ b/web/src/components/CurrencyInput.tsx
@@ -1,4 +1,4 @@
-import React from "react";
+import React, { useEffect, useState } from "react";
type BaseProps = Omit<
React.InputHTMLAttributes,
@@ -39,21 +39,32 @@ export default function CurrencyInput({
if ("valueCents" in rest) {
const { valueCents, onChange, ...inputProps } = rest as CentsProps;
+ const [display, setDisplay] = useState("");
+
+ // keep display in sync when valueCents prop changes externally
+ useEffect(() => {
+ const asNumber = (valueCents ?? 0) / 100;
+ setDisplay(Number.isFinite(asNumber) ? asNumber.toString() : "");
+ }, [valueCents]);
+
const handleChange = (e: React.ChangeEvent) => {
const formatted = formatString(e.target.value);
- const parsed = Number.parseFloat(formatted || "0");
- onChange(Number.isFinite(parsed) ? Math.round(parsed * 100) : 0);
+ setDisplay(formatted);
+ const parsed = Number.parseFloat(formatted);
+ if (Number.isFinite(parsed)) {
+ onChange(Math.round(parsed * 100));
+ }
};
- const displayValue = (valueCents ?? 0) / 100;
- const value = Number.isFinite(displayValue) ? displayValue.toString() : "";
+
return (
);
}
@@ -70,6 +81,7 @@ export default function CurrencyInput({
placeholder={placeholder}
value={value}
onChange={handleChange}
+ pattern="[0-9]*[.,]?[0-9]*"
/>
);
}
diff --git a/web/src/pages/OnboardingPage.tsx b/web/src/pages/OnboardingPage.tsx
index 1ad8e04..dc47447 100644
--- a/web/src/pages/OnboardingPage.tsx
+++ b/web/src/pages/OnboardingPage.tsx
@@ -75,6 +75,14 @@ const defaultSchedule = (): AutoPaySchedule => ({
const DEFAULT_SAVINGS_PERCENT = 20;
const MIN_SAVINGS_PERCENT = DEFAULT_SAVINGS_PERCENT;
const normalizeCategoryName = (value: string) => value.trim().toLowerCase();
+const normalizePercentValue = (value: unknown) => {
+ const parsed =
+ typeof value === "number"
+ ? value
+ : Number.parseFloat(String(value ?? ""));
+ if (!Number.isFinite(parsed)) return 0;
+ return Math.max(0, Math.min(100, Math.round(parsed)));
+};
const createDefaultSavingsCategory = (): VariableCat => ({
id: crypto.randomUUID(),
name: "savings",
@@ -136,7 +144,13 @@ export default function OnboardingPage() {
if (s.budgetConservatism) setBudgetConservatism(s.budgetConservatism);
if (Number.isFinite(s.customFixedPercentage)) setCustomFixedPercentage(s.customFixedPercentage);
if (Array.isArray(s.vars) && s.vars.length > 0) {
- setVars(s.vars.map((v: VariableCat) => ({ ...v, name: normalizeCategoryName(v.name) })));
+ setVars(
+ s.vars.map((v: VariableCat) => ({
+ ...v,
+ name: normalizeCategoryName(v.name ?? ""),
+ percent: normalizePercentValue(v.percent),
+ }))
+ );
}
if (Array.isArray(s.fixeds))
setFixeds(
@@ -181,11 +195,15 @@ export default function OnboardingPage() {
// ── computed
const varsTotal = useMemo(
- () => vars.reduce((s, v) => s + (v.percent || 0), 0),
+ () => vars.reduce((s, v) => s + normalizePercentValue(v.percent), 0),
[vars]
);
const savingsTotal = useMemo(
- () => vars.reduce((s, v) => s + (v.isSavings ? v.percent || 0 : 0), 0),
+ () =>
+ vars.reduce(
+ (s, v) => s + (v.isSavings ? normalizePercentValue(v.percent) : 0),
+ 0
+ ),
[vars]
);
const varsRemaining = Math.max(0, 100 - varsTotal);
@@ -200,6 +218,10 @@ export default function OnboardingPage() {
.filter(([, count]) => count > 1)
.map(([name]) => name);
}, [vars]);
+ const hasMissingVarNames = useMemo(
+ () => vars.some((v) => !normalizeCategoryName(v.name)),
+ [vars]
+ );
const fixedTotal = useMemo(
() => fixeds.reduce((s, f) => s + (f.amountCents || 0), 0),
[fixeds]
@@ -241,7 +263,7 @@ export default function OnboardingPage() {
const canNext4 =
vars.length > 0 &&
varsTotal === 100 &&
- vars.every((v) => v.name.trim()) &&
+ !hasMissingVarNames &&
vars.some((v) => v.isSavings) &&
savingsTotal >= MIN_SAVINGS_PERCENT &&
duplicateVarNames.length === 0;
@@ -520,7 +542,7 @@ export default function OnboardingPage() {
dashboardSnapshot.variableCategories.map((c, i) => ({
id: c.id,
name: normalizeCategoryName(c.name),
- percent: c.percent,
+ percent: normalizePercentValue(c.percent),
priority: i + 1,
isSavings: !!c.isSavings,
}))
@@ -943,6 +965,12 @@ export default function OnboardingPage() {
)}
+
+ {hasMissingVarNames && (
+
+ Every category needs a name before you can continue.
+
+ )}
{varsTotal !== 100 && varsTotal > 0 && (