diff --git a/package-lock.json b/package-lock.json index b7b1ae8..39228d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1514,9 +1514,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1534,9 +1531,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1554,9 +1548,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1574,9 +1565,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3337,9 +3325,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3361,9 +3346,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3385,9 +3367,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3409,9 +3388,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ diff --git a/src/App.css b/src/App.css deleted file mode 100644 index b9d355d..0000000 --- a/src/App.css +++ /dev/null @@ -1,42 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/src/App.tsx b/src/App.tsx index 3313571..258eb43 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,7 +3,7 @@ * ================================================== */ import { useState, useEffect } from "react"; -import { styled, themeClasses, globalStyles } from "../stitches.config"; +import { styled, themeClasses } from "../stitches.config"; import { ThemeSelector } from "./components/ThemeSwitcher"; import StateIOPopover from "./components/sidebar/StateIOPopover"; import SubdomainRouter from "./components/SubdomainRouter"; @@ -64,8 +64,7 @@ const defaultState: TH4State = { monthlyWithdrawalB: DEFAULT_MONTHLY_WITHDRAWAL, withdrawalStartYearA: DEFAULT_WITHDRAWAL_START_YEAR, withdrawalStartYearB: DEFAULT_WITHDRAWAL_START_YEAR, - yearlyInflationA: DEFAULT_INFLATION_RATE, - yearlyInflationB: DEFAULT_INFLATION_RATE, + yearlyInflation: DEFAULT_INFLATION_RATE, }, inputs: { currentAmountA: String(DEFAULT_INITIAL_AMOUNT), @@ -92,13 +91,6 @@ export default function App() { const [inputs, setInputs] = useState(defaultState.inputs); const [toggles, setToggles] = useState(defaultState.toggles); - /** - * Initialize global styles on component mount - */ - useEffect(() => { - globalStyles(); - }, []); - /** * Apply theme changes to document body */ diff --git a/src/components/InvestmentCalculatorModern.tsx b/src/components/InvestmentCalculatorModern.tsx index 1cf3e1a..284cfd1 100644 --- a/src/components/InvestmentCalculatorModern.tsx +++ b/src/components/InvestmentCalculatorModern.tsx @@ -1,4 +1,6 @@ -// src/components/InvestmentCalculatorRadixModern.tsx +/* ================================================== + * Investment Calculator Component + * ================================================== */ import type { Dispatch, SetStateAction } from "react"; import * as Popover from "@radix-ui/react-popover"; import * as Slider from "@radix-ui/react-slider"; @@ -8,6 +10,18 @@ import { InvestmentCalculator } from "../common/helpers/investment-growth-calcul import DateAmountTable from "./date-amount-table"; import { InvestmentLineChart } from "./investment-line-chart"; import { addYears } from "date-fns"; +import { + DEFAULT_INITIAL_AMOUNT, + DEFAULT_PROJECTED_GAIN, + DEFAULT_YEARS_OF_GROWTH, + DEFAULT_INFLATION_RATE, + MAX_PROJECTED_GAIN, + MAX_YEARS_OF_GROWTH, + MAX_MONTHLY_CONTRIBUTION, + MAX_MONTHLY_WITHDRAWAL, + MAX_INFLATION_RATE, + MIN_VALUE, +} from "../common/constants/app-constants"; /* ---------------- Styles & Animations ---------------- */ const fadeInUp = keyframes({ @@ -147,6 +161,11 @@ const InfoRow = styled("div", { fontWeight: 500, color: "$comment", }); +const SwitchRow = styled("div", { + display: "flex", + gap: "12px", + alignItems: "center", +}); /* ---------------- Helpers ---------------- */ function CurrencyInput({ @@ -247,72 +266,68 @@ export default function InvestmentCalculatorRadixModern({ // ---------------- Investment A ---------------- const invAProps = { - currentAmount: inputs.currentAmountA || "10000", - projectedGain: sliders.projectedGainA || 10, - yearsOfGrowth: sliders.yearsOfGrowthA || 30, - monthlyContribution: sliders.monthlyContributionA || 0, - monthlyWithdrawal: sliders.monthlyWithdrawalA || 0, + currentAmount: inputs.currentAmountA || String(DEFAULT_INITIAL_AMOUNT), + projectedGain: sliders.projectedGainA || DEFAULT_PROJECTED_GAIN, + yearsOfGrowth: sliders.yearsOfGrowthA || DEFAULT_YEARS_OF_GROWTH, + monthlyContribution: sliders.monthlyContributionA || MIN_VALUE, + monthlyWithdrawal: sliders.monthlyWithdrawalA || MIN_VALUE, yearContributionsStop: sliders.contributionStopYearA ?? sliders.yearsOfGrowthA, - yearWithdrawalsBegin: sliders.withdrawalStartYearA || 0, + yearWithdrawalsBegin: sliders.withdrawalStartYearA || MIN_VALUE, advanced: toggles.advanced, - depreciationRate: sliders.yearlyInflation || 2.5, + depreciationRate: sliders.yearlyInflation || DEFAULT_INFLATION_RATE, rollOver: false, investmentId: "investmentA", growthMatrix: [], setCurrentAmount: (v: string | undefined) => - updateInput("currentAmountA", v ?? ""), // Added default value for undefined - setProjectedGain: (v: number) => updateSlider("projectedGainA", v ?? 0), // Added default value for undefined - setYearsOfGrowth: (v: number) => updateSlider("yearsOfGrowthA", v ?? 0), + updateInput("currentAmountA", v ?? ""), + setProjectedGain: (v: number) => updateSlider("projectedGainA", v), + setYearsOfGrowth: (v: number) => updateSlider("yearsOfGrowthA", v), setMonthlyContribution: (v: number) => - updateSlider("monthlyContributionA", v ?? 0), - setMonthlyWithdrawal: (v: number) => - updateSlider("monthlyWithdrawalA", v ?? 0), + updateSlider("monthlyContributionA", v), + setMonthlyWithdrawal: (v: number) => updateSlider("monthlyWithdrawalA", v), setYearWithdrawalsBegin: (v: number) => - updateSlider("withdrawalStartYearA", v ?? 0), + updateSlider("withdrawalStartYearA", v), setYearContributionsStop: (v: number | undefined) => - updateSlider("contributionStopYearA", v ?? 0), // Added default value for undefined - maxMonthlyWithdrawal: 10000, // max limit set for the withdrawal + updateSlider("contributionStopYearA", v ?? 0), + maxMonthlyWithdrawal: MAX_MONTHLY_WITHDRAWAL, }; const calcA = new InvestmentCalculator(invAProps); - calcA.getGrowthMatrix(); const totalA = parseInt( calcA.calculateGrowth(toggles.showInflation).replace(/[^0-9.-]+/g, ""), ); // ---------------- Investment B ---------------- const invBProps = { - currentAmount: inputs.currentAmountB || "10000", - projectedGain: sliders.projectedGainB || 10, - yearsOfGrowth: sliders.yearsOfGrowthB || 30, - monthlyContribution: sliders.monthlyContributionB || 0, - monthlyWithdrawal: sliders.monthlyWithdrawalB || 0, + currentAmount: inputs.currentAmountB || String(DEFAULT_INITIAL_AMOUNT), + projectedGain: sliders.projectedGainB || DEFAULT_PROJECTED_GAIN, + yearsOfGrowth: sliders.yearsOfGrowthB || DEFAULT_YEARS_OF_GROWTH, + monthlyContribution: sliders.monthlyContributionB || MIN_VALUE, + monthlyWithdrawal: sliders.monthlyWithdrawalB || MIN_VALUE, yearContributionsStop: sliders.contributionStopYearB ?? sliders.yearsOfGrowthB, - yearWithdrawalsBegin: sliders.withdrawalStartYearB || 0, + yearWithdrawalsBegin: sliders.withdrawalStartYearB || MIN_VALUE, advanced: toggles.advanced, - depreciationRate: sliders.yearlyInflation || 2.5, + depreciationRate: sliders.yearlyInflation || DEFAULT_INFLATION_RATE, rollOver: toggles.rollover, investmentToRoll: toggles.rollover ? totalA : 0, yearOfRollover: toggles.rollover ? sliders.yearsOfGrowthA : undefined, investmentId: "investmentB", growthMatrix: [], setCurrentAmount: (v: string | undefined) => - updateInput("currentAmountB", v ?? ""), // Added default value for undefined - setProjectedGain: (v: number) => updateSlider("projectedGainB", v ?? 0), // Added default value for undefined - setYearsOfGrowth: (v: number) => updateSlider("yearsOfGrowthB", v ?? 0), + updateInput("currentAmountB", v ?? ""), + setProjectedGain: (v: number) => updateSlider("projectedGainB", v), + setYearsOfGrowth: (v: number) => updateSlider("yearsOfGrowthB", v), setMonthlyContribution: (v: number) => - updateSlider("monthlyContributionB", v ?? 0), - setMonthlyWithdrawal: (v: number) => - updateSlider("monthlyWithdrawalB", v ?? 0), + updateSlider("monthlyContributionB", v), + setMonthlyWithdrawal: (v: number) => updateSlider("monthlyWithdrawalB", v), setYearWithdrawalsBegin: (v: number) => - updateSlider("withdrawalStartYearB", v ?? 0), + updateSlider("withdrawalStartYearB", v), setYearContributionsStop: (v: number | undefined) => - updateSlider("contributionStopYearB", v ?? 0), // Added default value for undefined - maxMonthlyWithdrawal: 10000, // max limit set for the withdrawal + updateSlider("contributionStopYearB", v ?? 0), + maxMonthlyWithdrawal: MAX_MONTHLY_WITHDRAWAL, }; const calcB = new InvestmentCalculator(invBProps); - calcB.getGrowthMatrix(); const totalB = parseInt( calcB.calculateGrowth(toggles.showInflation).replace(/[^0-9.-]+/g, ""), ); @@ -358,7 +373,7 @@ export default function InvestmentCalculatorRadixModern({ label: "Rollover Amount", value: toggles.rollover ? `$${totalA.toLocaleString()}` : "N/A", }, - { label: "Inflation Rate", value: `${sliders.yearlyInflation || 2.5}%` }, + { label: "Inflation Rate", value: `${sliders.yearlyInflation || DEFAULT_INFLATION_RATE}%` }, ]; return ( @@ -373,15 +388,15 @@ export default function InvestmentCalculatorRadixModern({ updateSlider("projectedGainA", v)} /> updateSlider("yearsOfGrowthA", v)} /> {toggles.advanced && ( @@ -389,28 +404,28 @@ export default function InvestmentCalculatorRadixModern({ updateSlider("monthlyContributionA", v)} /> updateSlider("contributionStopYearA", v)} /> updateSlider("monthlyWithdrawalA", v)} /> updateSlider("withdrawalStartYearA", v)} /> @@ -428,42 +443,42 @@ export default function InvestmentCalculatorRadixModern({ updateSlider("projectedGainB", v)} /> updateSlider("yearsOfGrowthB", v)} /> updateSlider("monthlyContributionB", v)} /> updateSlider("contributionStopYearB", v)} /> updateSlider("monthlyWithdrawalB", v)} /> updateSlider("withdrawalStartYearB", v)} /> @@ -472,32 +487,32 @@ export default function InvestmentCalculatorRadixModern({ {/* Info / Global Settings Panel */} -
+ updateToggle("advanced", v)} /> -
-
+ + updateToggle("rollover", v)} /> -
-
- + + + updateToggle("showInflation", v)} /> -
+ updateSlider("yearlyInflation", v)} /> diff --git a/src/components/ThemePopover.tsx b/src/components/ThemePopover.tsx deleted file mode 100644 index c634879..0000000 --- a/src/components/ThemePopover.tsx +++ /dev/null @@ -1,139 +0,0 @@ -// src/components/sidebar/ThemePopover.tsx -import { useState } from "react"; -import * as Popover from "@radix-ui/react-popover"; -import { styled, keyframes, themeObjects } from "../../stitches.config"; -import * as Icons from "@radix-ui/react-icons"; - -// ======================= -// Animations -// ======================= -const slideRightAndFade = keyframes({ - "0%": { opacity: 0, transform: "translateX(-6px)" }, - "100%": { opacity: 1, transform: "translateX(0)" }, -}); - -// ======================= -// Styled Components -// ======================= -const PopoverContent = styled(Popover.Content, { - backgroundColor: "$currentLine", - color: "$foreground", - borderRadius: "12px", - padding: "12px", - minWidth: "180px", - display: "flex", - flexDirection: "column", - gap: "8px", - boxShadow: "0 10px 20px rgba(0,0,0,0.2)", - animation: `${slideRightAndFade} 0.25s ease-out`, -}); - -const PopoverArrow = styled(Popover.Arrow, { - fill: "$currentLine", -}); - -const ScrollArea = styled("div", { - display: "flex", - flexDirection: "column", - gap: "8px", - maxHeight: "calc(4 * 48px + 16px)", // 4 buttons + padding - overflowY: "auto", - paddingRight: "4px", // prevent scrollbar overlapping -}); - -export const ThemeButton = styled("button", { - all: "unset", - display: "flex", - alignItems: "center", - gap: "10px", - padding: "10px 14px", - borderRadius: "9999px", - cursor: "pointer", - fontSize: "0.95rem", - fontWeight: 500, - backgroundColor: "$currentLine", - transition: "all 0.2s ease", - "&:hover": { - backgroundColor: "$purple", - transform: "translateY(-1px)", - boxShadow: "0 4px 12px rgba(0,0,0,0.25)", - }, - "&:active": { - transform: "translateY(0)", - boxShadow: "0 2px 6px rgba(0,0,0,0.2)", - }, -}); - -const ColorSwatch = styled("span", { - width: "18px", - height: "18px", - borderRadius: "50%", - flexShrink: 0, - border: "2px solid $foreground", -}); - -const TriggerButton = styled("button", { - all: "unset", - cursor: "pointer", - color: "inherit", - display: "flex", - alignItems: "center", - justifyContent: "center", - width: "36px", - height: "36px", -}); - -// ======================= -// Component -// ======================= -export function ThemePopover() { - const themeKeys = Object.keys(themeObjects) as Array< - keyof typeof themeObjects - >; - const [activeTheme, setActiveTheme] = useState(() => { - const current = themeKeys.find((key) => - document.body.classList.contains(`${key}-theme`), - ); - return current ?? themeKeys[0]; - }); - - const switchTheme = (themeKey: string) => { - const body = document.body; - themeKeys.forEach((key) => body.classList.remove(`${key}-theme`)); - body.classList.add(`${themeKey}-theme`); - setActiveTheme(themeKey); - }; - - return ( - - - - - - - - - - {themeKeys.map((key) => { - const theme = themeObjects[key]; - const isActive = activeTheme === key; - return ( - switchTheme(key)} - > - - {theme.name.charAt(0).toUpperCase() + theme.name.slice(1)} - - ); - })} - - - - - - ); -} diff --git a/src/components/ThemeSwitcher.tsx b/src/components/ThemeSwitcher.tsx index d898461..9cf5c9b 100644 --- a/src/components/ThemeSwitcher.tsx +++ b/src/components/ThemeSwitcher.tsx @@ -67,6 +67,14 @@ const ThemeButton = styled(DropdownMenu.Item, { outline: "none", backgroundColor: "$comment", }, + variants: { + active: { + true: { + backgroundColor: "$purple", + color: "$background", + }, + }, + }, }); const ColorSwatch = styled("span", { @@ -143,10 +151,7 @@ export function ThemeSelector({ onThemeChange }: ThemeSelectorProps) { switchTheme(key)} - style={{ - backgroundColor: isActive ? "$purple" : undefined, - color: isActive ? "$background" : undefined, - }} + active={isActive} > - {matrix.map((entry, idx) => { + {matrix.map((entry) => { if (!entry || entry.y === undefined) return null; const year = new Date(entry.x).getFullYear(); @@ -101,7 +101,7 @@ export default function DateAmountTable({ initial !== 0 ? ((nominal - initial) / initial) * 100 : 0; return ( - + {year} ${nominal.toLocaleString()} diff --git a/src/components/sidebar/theme-button.tsx b/src/components/sidebar/theme-button.tsx deleted file mode 100644 index dc1d11e..0000000 --- a/src/components/sidebar/theme-button.tsx +++ /dev/null @@ -1,110 +0,0 @@ -// ThemeSelector.tsx -import * as Popover from "@radix-ui/react-popover"; -import { styled, keyframes } from "../../../stitches.config"; -import * as Icons from "@radix-ui/react-icons"; - -// ======================= -// Animations -// ======================= -const slideRightAndFade = keyframes({ - "0%": { opacity: 0, transform: "translateX(-6px)" }, - "100%": { opacity: 1, transform: "translateX(0)" }, -}); - -// ======================= -// Styled Components -// ======================= -const PopoverContent = styled(Popover.Content, { - backgroundColor: "$currentLine", - color: "$foreground", - borderRadius: "12px", - padding: "12px", - minWidth: "160px", - display: "flex", - flexDirection: "column", - gap: "10px", - boxShadow: "0 10px 20px rgba(0,0,0,0.2)", - animation: `${slideRightAndFade} 0.25s ease-out`, -}); - -const PopoverArrow = styled(Popover.Arrow, { - fill: "$currentLine", -}); - -const ThemeButton = styled("button", { - all: "unset", - display: "flex", - alignItems: "center", - gap: "10px", - padding: "10px 14px", - borderRadius: "9999px", - cursor: "pointer", - fontSize: "0.95rem", - fontWeight: 500, - backgroundColor: "$currentLine", - transition: "all 0.2s ease", - "&:hover": { - backgroundColor: "$purple", - transform: "translateY(-1px)", - boxShadow: "0 4px 12px rgba(0,0,0,0.25)", - }, - "&:active": { - transform: "translateY(0)", - boxShadow: "0 2px 6px rgba(0,0,0,0.2)", - }, -}); - -const ColorSwatch = styled("span", { - width: "18px", - height: "18px", - borderRadius: "50%", - flexShrink: 0, - border: "1px solid $foreground", - boxShadow: "0 1px 3px rgba(0,0,0,0.2)", -}); - -const SidebarButton = styled("button", { - all: "unset", - color: "$foreground", - padding: "0.75rem", - marginBottom: "0.5rem", - cursor: "pointer", - borderRadius: 5, - display: "flex", - alignItems: "center", - justifyContent: "center", - "&:hover": { - backgroundColor: "$purple", - }, -}); - -// ======================= -// Component -// ======================= -type ThemeSelectorProps = { - onThemeChange: (theme: "dracula" | "gruvbox") => void; -}; - -export function ThemeSelector({ onThemeChange }: ThemeSelectorProps) { - return ( - - - - - - - - - onThemeChange("dracula")}> - - Dracula - - onThemeChange("gruvbox")}> - - Gruvbox Dark - - - - - ); -} diff --git a/src/global.ts b/src/global.ts deleted file mode 100644 index c78a36a..0000000 --- a/src/global.ts +++ /dev/null @@ -1,11 +0,0 @@ -// src/global.ts -import { globalCss } from "../stitches.config"; - -export const globalStyles = globalCss({ - "*": { margin: 0, padding: 0, boxSizing: "border-box" }, - body: { - fontFamily: "$body", - backgroundColor: "$background", - color: "$foreground", - }, -}); diff --git a/src/index.css b/src/index.css deleted file mode 100644 index 08a3ac9..0000000 --- a/src/index.css +++ /dev/null @@ -1,68 +0,0 @@ -:root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } -} diff --git a/src/index.tsx b/src/index.tsx deleted file mode 100644 index 4e443b9..0000000 --- a/src/index.tsx +++ /dev/null @@ -1,13 +0,0 @@ -// src/main.tsx -import React from "react"; -import ReactDOM from "react-dom/client"; -import App from "./App"; -import { globalStyles } from "./global"; - -globalStyles(); - -ReactDOM.createRoot(document.getElementById("root")!).render( - - - , -); diff --git a/src/main.tsx b/src/main.tsx index c7dd3ee..98bc398 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,16 +2,16 @@ * Application Entry Point * ================================================== */ -import React from "react"; +import { StrictMode } from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; import { globalStyles } from "../stitches.config"; -// Apply global styles +// Apply global styles before first render globalStyles(); ReactDOM.createRoot(document.getElementById("root")!).render( - + - , + , );