From 691a03a0c51ca6f6883950d37064ce511c0c8544 Mon Sep 17 00:00:00 2001 From: 28raining Date: Sat, 15 Nov 2025 14:21:59 -0800 Subject: [PATCH 1/2] Add mode toggle component with improved styling - Extract toggle buttons and input fields into new ModeToggle component - Move home value and monthly payment inputs inline with toggle buttons - Update color scheme to align with #cff4fc (cyan theme) - Fix hover state readability with proper contrast - Improve button styling with rounded corners and shadows - Show calculated values on same row as input fields --- src/App.jsx | 24 +++--- src/LoanForm.jsx | 39 +--------- src/ModeToggle.jsx | 181 +++++++++++++++++++++++++++++++++++++++++++++ src/index.css | 60 +++++++++++++++ 4 files changed, 253 insertions(+), 51 deletions(-) create mode 100644 src/ModeToggle.jsx diff --git a/src/App.jsx b/src/App.jsx index 4ebc21c..c6416e0 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,6 +1,7 @@ import { useState } from "react"; import LoanForm from "./LoanForm.jsx"; import { loanMaths, isNumber } from "./loanMaths.js"; +import ModeToggle from "./ModeToggle.jsx"; import LoanPlot from "./LoanPlot.jsx"; import LoanStats from "./LoanStats.jsx"; import EventsForm from "./EventsForm.jsx"; @@ -568,21 +569,18 @@ function App() {
-
-
-
-
-

- An easy to use mortgage calculator to find out exactly how much it will cost to buy a house. Or, enter a monthly budget. How much can you - afford? This tool supports unlimited overpayment, re-finance and recast events. Also, try adding inflation. -

-
-
-
-
+ +
- updateUserInput(f, v)} /> + updateUserInput(f, v)} chosenInput={chosenInput} />
diff --git a/src/LoanForm.jsx b/src/LoanForm.jsx index d59e718..b8b7381 100644 --- a/src/LoanForm.jsx +++ b/src/LoanForm.jsx @@ -26,7 +26,7 @@ function FbComp({ x }) { } } -function LoanForm({ displayState, flash, updateUserInput, valid }) { +function LoanForm({ displayState, flash, updateUserInput, valid, chosenInput }) { const [show, setShow] = useState(false); const feeOptions = ["$ / year", "$ / month", "% / year", "% / month"]; const additionalPayments = @@ -97,43 +97,6 @@ function LoanForm({ displayState, flash, updateUserInput, valid }) { return (
-
-
- - updateIfChanged(displayState["homeVal"], e.target.value, "homeVal")} - /> - - {additionalPayments ? ( - - ) : null} -
-
or
-
- - updateIfChanged(displayState["monthlyPayment"], e.target.value, "monthlyPayment")} - value={cashFormat(displayState["monthlyPayment"])} - /> - - - {additionalPayments ? ( - - ) : null} -
-
diff --git a/src/ModeToggle.jsx b/src/ModeToggle.jsx new file mode 100644 index 0000000..5ce744c --- /dev/null +++ b/src/ModeToggle.jsx @@ -0,0 +1,181 @@ +import { cashFormat, isNumber } from "./loanMaths.js"; + +function ModeToggle({ chosenInput, displayState, valid, flash, userInput, updateUserInput }) { + const handleHomeValClick = () => { + if (chosenInput !== "homeVal") { + updateUserInput("homeVal", userInput.homeVal); + } + }; + + const handleMonthlyPaymentClick = () => { + if (chosenInput !== "monthlyPayment") { + const monthlyPaymentValue = userInput.monthlyPayment !== "0" ? userInput.monthlyPayment : displayState.monthlyPayment; + updateUserInput("monthlyPayment", monthlyPaymentValue); + } + }; + + const updateIfChanged = (oldVal, newVal, name) => { + var parsedNewVal = newVal.replace(/[^0-9.]+/g, ""); + if (oldVal !== parsedNewVal) { + var noLeading0 = oldVal === "0" && isNumber(parsedNewVal) ? parseFloat(parsedNewVal).toString() : parsedNewVal; + if (newVal.slice(-1) === "." && noLeading0.slice(-1) !== ".") { + noLeading0 = `${noLeading0}.`; + } + updateUserInput(name, noLeading0); + } + }; + + const additionalPayments = + Number(displayState["propertyTax"]) > 0 || + Number(displayState["hoa"]) > 0 || + Number(displayState["insurance"]) > 0 || + Number(displayState["pmi"]) || + Number(displayState["maintenance"]) > 0 || + Number(displayState["utilities"]) > 0; + + return ( + <> +
+
+
+
I want to start with:
+ +
+ + + +
+
+
+
+ {chosenInput && ( +
+
+
+
+ {chosenInput === "homeVal" ? ( + <> +
+ + updateIfChanged(displayState["homeVal"], e.target.value, "homeVal")} + /> + {valid["homeVal"] !== null && ( +
+ {valid["homeVal"]} +
+ )} +
+
+ +
+
+ + + {additionalPayments && ( + + )} +
+ + ) : ( + <> +
+ + updateIfChanged(displayState["monthlyPayment"], e.target.value, "monthlyPayment")} + /> + {valid["monthlyPayment"] !== null && ( +
+ {valid["monthlyPayment"]} +
+ )} + {additionalPayments && ( + + )} +
+
+ +
+
+ + +
+ + )} +
+
+
+
+ )} + + ); +} + +export default ModeToggle; + diff --git a/src/index.css b/src/index.css index 66b4b10..9b5f0d7 100644 --- a/src/index.css +++ b/src/index.css @@ -122,3 +122,63 @@ body { --bs-tooltip-bg: var(--bd-violet-bg); --bs-tooltip-color: var(--bs-white); } + +.mode-toggle-btn { + position: relative; + border: 1px solid #0dcaf0 !important; +} + +.mode-toggle-btn:first-child { + border-top-right-radius: 0 !important; + border-bottom-right-radius: 0 !important; +} + +.mode-toggle-btn:last-child { + border-top-left-radius: 0 !important; + border-bottom-left-radius: 0 !important; +} + +.mode-toggle-active { + background-color: #0cbbde !important; + border-color: #0cbbde !important; + color: #fff !important; + z-index: 1; + box-shadow: 0 2px 4px rgba(13, 202, 240, 0.3); +} + +.mode-toggle-active:hover { + background-color: #0aa2c0 !important; + border-color: #0aa2c0 !important; + color: #fff !important; + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(13, 202, 240, 0.4); +} + +.mode-toggle-inactive { + background-color: #fff !important; + border-color: #0dcaf0 !important; + color: #000 !important; +} + +.mode-toggle-inactive:hover { + background-color: #cff4fc !important; + border-color: #0dcaf0 !important; + color: #000 !important; + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(13, 202, 240, 0.2); +} + +.mode-toggle-btn:active { + transform: translateY(0); +} + +@media (width <= 768px) { + .mode-toggle-btn { + min-width: 140px !important; + padding: 0.75rem 1rem !important; + } + + .mode-toggle-btn small { + font-size: 0.7rem !important; + } +} From 5964a6c41d41fdc447686a638715998954d39a7c Mon Sep 17 00:00:00 2001 From: 28raining Date: Sun, 23 Nov 2025 15:12:36 -0800 Subject: [PATCH 2/2] fix big toggle button mode --- src/App.jsx | 81 +++++++---------- src/LoanForm.jsx | 9 +- src/LoanPlot.jsx | 23 ++--- src/ModeToggle.jsx | 214 +++++++++++++++++++++++++-------------------- src/Rent.jsx | 4 +- src/index.css | 33 +++++-- 6 files changed, 193 insertions(+), 171 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index c6416e0..dfa64fc 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -204,7 +204,7 @@ if (searchParams.has("downPayCash")) initialUserSetDownPercent = false; function App() { const [loanEvent, setLoanEvent] = useState(initialEvents); - const [chosenInput, setChosenInput] = useState("homeVal"); + const [chosenInput, setChosenInput] = useState("monthlyPayment" in initialOverride ? "monthlyPayment" : "homeVal"); const [userSetDownPercent, setUserSetDownPercent] = useState(initialUserSetDownPercent); const [showURLToast, setShowURLToast] = useState(gotStuffFromURL); const [rentSim, setRentSim] = useState({ ...initialRentSimulation, ...overrideRentSimulation }); @@ -569,25 +569,18 @@ function App() {
- + -
-
+
+
updateUserInput(f, v)} chosenInput={chosenInput} />
-
-
-
+
+
+
-
+
-
-
- -
-
-
-
- setRentSim(r)} - /> -
+ +
+ setRentSim(r)} + />
diff --git a/src/LoanForm.jsx b/src/LoanForm.jsx index b8b7381..194f6b6 100644 --- a/src/LoanForm.jsx +++ b/src/LoanForm.jsx @@ -26,16 +26,9 @@ function FbComp({ x }) { } } -function LoanForm({ displayState, flash, updateUserInput, valid, chosenInput }) { +function LoanForm({ displayState, flash, updateUserInput, valid }) { const [show, setShow] = useState(false); const feeOptions = ["$ / year", "$ / month", "% / year", "% / month"]; - const additionalPayments = - Number(displayState["propertyTax"]) > 0 || - Number(displayState["hoa"]) > 0 || - Number(displayState["insurance"]) > 0 || - Number(displayState["pmi"]) || - Number(displayState["maintenance"]) > 0 || - Number(displayState["utilities"]) > 0; //builds class for each input based on flash (whether it changed and should flash) and valid (if user input is valid) const startDateOptions = { month: "short", year: "numeric" }; diff --git a/src/LoanPlot.jsx b/src/LoanPlot.jsx index 282b03d..f27faa3 100644 --- a/src/LoanPlot.jsx +++ b/src/LoanPlot.jsx @@ -249,23 +249,16 @@ function LoanPlot({ maxMonthly, loanRes, loanMonths, propertyTax, hoa, pmi, util }; return ( -
+
-
-
-
- -
-
+
+
-
- Show: -
- {["Monthly Breakdown", "Yearly Breakdown"].map((x) => ( -
-
+
+ {["Monthly Breakdown", "Yearly Breakdown"].map((x) => ( +
-
- ))} + ))} +
diff --git a/src/ModeToggle.jsx b/src/ModeToggle.jsx index 5ce744c..5cd2aa2 100644 --- a/src/ModeToggle.jsx +++ b/src/ModeToggle.jsx @@ -35,52 +35,63 @@ function ModeToggle({ chosenInput, displayState, valid, flash, userInput, update return ( <> -
+
I want to start with:
-
+
@@ -88,94 +99,111 @@ function ModeToggle({ chosenInput, displayState, valid, flash, userInput, update
- {chosenInput && ( -
-
-
-
- {chosenInput === "homeVal" ? ( - <> -
- - updateIfChanged(displayState["homeVal"], e.target.value, "homeVal")} - /> - {valid["homeVal"] !== null && ( -
- {valid["homeVal"]} -
- )} -
-
- -
-
- - - {additionalPayments && ( - - )} -
- - ) : ( - <> -
- - updateIfChanged(displayState["monthlyPayment"], e.target.value, "monthlyPayment")} - /> - {valid["monthlyPayment"] !== null && ( -
- {valid["monthlyPayment"]} -
- )} - {additionalPayments && ( - - )} -
-
- -
-
- - -
- - )} -
+
+
+
+
+ {chosenInput === "homeVal" ? ( + <> +
+ + updateIfChanged(userInput["homeVal"], e.target.value, "homeVal")} + /> + {valid["homeVal"] !== null && ( +
+ {valid["homeVal"]} +
+ )} +
+
+ + → + + + → + +
+
+ + + {additionalPayments && ( + + )} +
+ + ) : ( + <> +
+ + + updateIfChanged( + userInput["monthlyPayment"] !== "0" ? userInput["monthlyPayment"] : displayState["monthlyPayment"], + e.target.value, + "monthlyPayment" + ) + } + /> + {valid["monthlyPayment"] !== null && ( +
+ {valid["monthlyPayment"]} +
+ )} + {additionalPayments && ( + + )} +
+
+ + → + + + → + +
+
+ + +
+ + )}
- )} +
); } export default ModeToggle; - diff --git a/src/Rent.jsx b/src/Rent.jsx index 3351a49..8db46ce 100644 --- a/src/Rent.jsx +++ b/src/Rent.jsx @@ -89,7 +89,7 @@ function Rent({ loanMonths, inflation, downPayment, equity, homeVal, monthlyPaym // Render chart with explanatory paragraph return ( -
+ <>
Instead of buying a home, what if that money was invested in the stock market?
    @@ -173,7 +173,7 @@ function Rent({ loanMonths, inflation, downPayment, equity, homeVal, monthlyPaym
-
+ ); } diff --git a/src/index.css b/src/index.css index 9b5f0d7..0bbb742 100644 --- a/src/index.css +++ b/src/index.css @@ -22,11 +22,11 @@ } .anim1 { - animation-name: "example2"; + animation-name: example2; animation-duration: 1.5s; } .anim2 { - animation-name: "example3"; + animation-name: example3; animation-duration: 1.5s; } @@ -108,6 +108,12 @@ .outputLabelWidth { width: auto; } + /* Prevent horizontal overflow */ + .container-xxl, + .row { + max-width: 100%; + overflow-x: hidden; + } /* … */ } @@ -174,11 +180,26 @@ body { @media (width <= 768px) { .mode-toggle-btn { - min-width: 140px !important; - padding: 0.75rem 1rem !important; + padding: 0.75rem 0.5rem !important; + font-size: 0.875rem !important; + } + + .mode-toggle-btn small { + font-size: 0.65rem !important; + } + + .mode-toggle-btn .fw-semibold { + font-size: 0.875rem !important; + } +} + +@media (width <= 500px) { + .mode-toggle-btn { + padding: 0.5rem 0.25rem !important; } - + .mode-toggle-btn small { - font-size: 0.7rem !important; + font-size: 0.6rem !important; + line-height: 1.2 !important; } }