From 59ab9790e2f03046ca7fef31a6db883e1ec31075 Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Sat, 21 Feb 2026 23:46:01 -0600 Subject: modernize site and migrate to TypeScript --- assets/ts/app.ts | 152 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 assets/ts/app.ts (limited to 'assets/ts') diff --git a/assets/ts/app.ts b/assets/ts/app.ts new file mode 100644 index 0000000..9b29604 --- /dev/null +++ b/assets/ts/app.ts @@ -0,0 +1,152 @@ +declare const Plotly: { + newPlot( + el: string | HTMLElement, + data: object[], + layout: object, + config?: object + ): void; +}; + +interface GraphColors { + bgColor: string; + fgColor: string; + gridColor: string; + lineColor: string; +} + +function formatMoney(num: number): string { + return num.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, "$&,"); +} + +function getInputValue(id: string): number { + const el = document.getElementById(id) as HTMLInputElement | null; + return el ? parseFloat(el.value) : NaN; +} + +function getThemeColors(): GraphColors { + const dark = window.matchMedia("(prefers-color-scheme: dark)").matches; + return dark + ? { bgColor: "#1a1d27", fgColor: "#e8eaf0", gridColor: "#2d3141", lineColor: "#3d4258" } + : { bgColor: "#ffffff", fgColor: "#1a1d23", gridColor: "#e2e6ea", lineColor: "#c8cdd5" }; +} + +function buildAxisLayout(title: string, colors: GraphColors): object { + const { bgColor, fgColor, gridColor, lineColor } = colors; + return { + title: { text: title, font: { color: fgColor, size: 13 } }, + paper_bgcolor: bgColor, + plot_bgcolor: bgColor, + font: { color: fgColor, family: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" }, + margin: { t: 40, r: 16, b: 48, l: 60 }, + yaxis: { gridcolor: gridColor, zerolinecolor: gridColor, linecolor: lineColor }, + xaxis: { title: { text: "Months", font: { size: 11 } }, gridcolor: gridColor, zerolinecolor: gridColor, linecolor: lineColor }, + }; +} + +class Data { + balance: number; + contribution: number; + returnRate: number; + inflationRate: number; + + constructor(balance: number, contribution: number, returnRate: number, inflationRate: number) { + this.balance = balance; + this.contribution = contribution; + this.returnRate = returnRate; + this.inflationRate = inflationRate; + } + + summaryRow(): string { + return ` + $${formatMoney(this.balance)} + $${formatMoney(this.contribution)} + ${this.returnRate.toFixed(2)}% + ${this.inflationRate.toFixed(2)}% + `; + } + + adjustedRate(): number { + return (1 + this.returnRate / 100) / (1 + this.inflationRate / 100) - 1; + } +} + +function showResults(data: Data, monthsArr: number[], balanceArr: number[], interestArr: number[]): void { + const infoTbody = document.querySelector("#infoTable tbody") as HTMLTableSectionElement; + infoTbody.innerHTML = data.summaryRow(); + + document.querySelectorAll(".table-section").forEach((el) => (el.style.display = "block")); + + const graphCard = document.getElementById("graphCard") as HTMLElement; + graphCard.style.display = "block"; + + const colors = getThemeColors(); + const balanceTrace = { x: monthsArr, y: balanceArr, type: "scatter", line: { color: "#4f6ef7", width: 2 } }; + const interestTrace = { x: monthsArr, y: interestArr, type: "scatter", line: { color: "#22c55e", width: 2 } }; + + Plotly.newPlot("balChartContainer", [balanceTrace], buildAxisLayout("Total Balance", colors), { responsive: true }); + Plotly.newPlot("intChartContainer", [interestTrace], buildAxisLayout("Accrued Interest", colors), { responsive: true }); +} + +function runCalculation(data: Data, stopCondition: (balance: number, month: number) => boolean): void { + document.querySelectorAll("tbody").forEach((el) => (el.innerHTML = "")); + + const resultsTbody = document.querySelector(".resultsTable tbody") as HTMLTableSectionElement; + const adjustedRate = data.adjustedRate(); + const monthlyContr = data.contribution; + + const monthsArr: number[] = []; + const balanceArr: number[] = []; + const interestArr: number[] = []; + + let pVal = data.balance; + let i = 0; + + while (!stopCondition(pVal, i)) { + const month = i + 1; + const interest = pVal * (adjustedRate / 12); + const newBalance = pVal + interest + monthlyContr; + + const row = document.createElement("tr"); + row.innerHTML = `${month}$${formatMoney(interest)}$${formatMoney(monthlyContr)}$${formatMoney(newBalance)}`; + resultsTbody.appendChild(row); + + if (month === 1 || month % 12 === 0) { + interestArr.push(Math.round(interest * 100) / 100); + balanceArr.push(Math.round(newBalance * 100) / 100); + monthsArr.push(month); + } + + pVal = newBalance; + i++; + } + + showResults(data, monthsArr, balanceArr, interestArr); +} + +function retirementYears(): void { + const data = new Data( + getInputValue("begBalance"), + getInputValue("monthlyContr"), + getInputValue("returnRate"), + getInputValue("inflationRate") + ); + const years = getInputValue("years"); + const totalMonths = years * 12; + runCalculation(data, (_bal, i) => i >= totalMonths); + document.getElementById("graphCard")?.scrollIntoView({ behavior: "smooth" }); +} + +function retirementMoney(): void { + const data = new Data( + getInputValue("begBalance"), + getInputValue("monthlyContr"), + getInputValue("returnRate"), + getInputValue("inflationRate") + ); + const target = getInputValue("money"); + runCalculation(data, (bal) => bal >= target); + document.getElementById("graphCard")?.scrollIntoView({ behavior: "smooth" }); +} + +(window as Window & typeof globalThis & { retirementYears: () => void; retirementMoney: () => void }).retirementYears = retirementYears; +(window as Window & typeof globalThis & { retirementYears: () => void; retirementMoney: () => void }).retirementMoney = retirementMoney; -- cgit v1.2.3