diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..e90b41a --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,15 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +polar: # Replace with a single Polar username +buy_me_a_coffee: ryanlynch # Replace with a single Buy Me a Coffee username +thanks_dev: # Replace with a single thanks.dev username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/index.html b/index.html index 39a9464..5a3e25d 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + Irish Mortgage Calculator diff --git a/src/components/MortgageComparison/MortgageComparison.tsx b/src/components/MortgageComparison/MortgageComparison.tsx index 9b7b010..f1aa550 100644 --- a/src/components/MortgageComparison/MortgageComparison.tsx +++ b/src/components/MortgageComparison/MortgageComparison.tsx @@ -1,4 +1,11 @@ -import { Icon, Link, PrimaryButton, Stack, Text } from "@fluentui/react"; +import { + Checkbox, + Icon, + Link, + PrimaryButton, + Stack, + Text, +} from "@fluentui/react"; import React, { useEffect } from "react"; import { InterestRate } from "../InterestRate/InterestRate"; import { MaxLoanInput } from "../MaxLoanInput/MaxLoanInput"; @@ -10,163 +17,187 @@ import { FeesPanel } from "../FeesPanel/FeesPanel"; export interface MortgageComparisonProps {} export const MortgageComparison: React.FC = () => { - const savedMortgageDetails = localStorage.getItem("mortgageDetails"); - const parsedMortgageDetails = JSON.parse(savedMortgageDetails ?? "{}"); - - const [interestRate, setInterestRate] = React.useState( - parsedMortgageDetails.interestRate ?? 4.0 - ); - const [useGlobalInterestRate, setUseGlobalInterestRate] = React.useState( - parsedMortgageDetails.useGlobalInterestRate ?? true - ); - - const [maxLoan, setMaxLoan] = React.useState( - parsedMortgageDetails.maxLoan ?? 0 - ); - - const [term, setTerm] = React.useState(parsedMortgageDetails.term ?? 35); - - const [fees, setFees] = React.useState( - parsedMortgageDetails.fees ?? { - valuationFee: 185, - surveyFee: 600, - // legalFee: 3382.5, - legalFee: 2800, - searchFee: 250, - registerOfDeedsFee: 100, - landRegistryFee: 975, - } - ); - - const [isPanelOpen, setIsPanelOpen] = React.useState(false); - - const containerStackStyles = { - root: { alignItems: "center" }, - }; - const containerStackTokens = { childrenGap: 30 }; - const comparisonStackTokens = { childrenGap: 40 }; - - useEffect(() => { - localStorage.setItem( - "mortgageDetails", - JSON.stringify({ - fees, - interestRate, - useGlobalInterestRate, - maxLoan, - term, - }) - ); - }, [fees, interestRate, useGlobalInterestRate, maxLoan, term]); - - return ( - -

Mortgage Comparison

- - - - - - - setIsPanelOpen(true)}> - View/Edit Fees - - - - - - - - - - -
- - - Disclaimer: This is a simple mortgage - comparison tool that calculates monthly payments based on the - interest rate, loan amount, and term. The numbers are all - estimates based on my own experience and research. Always - consult with a financial advisor before making any decisions. - - - - If you found this useful, consider{" "} - - buying me a coffee! {" "} - - - - -
+ const savedMortgageDetails = localStorage.getItem("mortgageDetails"); + const parsedMortgageDetails = JSON.parse(savedMortgageDetails ?? "{}"); + + const [firstTimeBuyer, setFirstTimeBuyer] = React.useState(true); + + const [interestRate, setInterestRate] = React.useState( + parsedMortgageDetails.interestRate ?? 4.0 + ); + const [useGlobalInterestRate, setUseGlobalInterestRate] = React.useState( + parsedMortgageDetails.useGlobalInterestRate ?? true + ); + + const [maxLoan, setMaxLoan] = React.useState( + parsedMortgageDetails.maxLoan ?? 0 + ); + + const [term, setTerm] = React.useState(parsedMortgageDetails.term ?? 35); + + const [fees, setFees] = React.useState( + parsedMortgageDetails.fees ?? { + valuationFee: 185, + surveyFee: 600, + // legalFee: 3382.5, + legalFee: 2800, + searchFee: 250, + registerOfDeedsFee: 100, + landRegistryFee: 975, + } + ); + + const [isPanelOpen, setIsPanelOpen] = React.useState(false); + + const containerStackStyles = { + root: { alignItems: "center" }, + }; + const containerStackTokens = { childrenGap: 30 }; + const comparisonStackTokens = { childrenGap: 40 }; + + useEffect(() => { + localStorage.setItem( + "mortgageDetails", + JSON.stringify({ + fees, + interestRate, + useGlobalInterestRate, + maxLoan, + term, + }) ); + }, [fees, interestRate, useGlobalInterestRate, maxLoan, term]); + + return ( + +

Mortgage Comparison

+ + { + if (checked === undefined) { + return; + } + setFirstTimeBuyer(checked); + }} + /> + + + + + + + + setIsPanelOpen(true)}> + View/Edit Fees + + + + + + + + + + +
+ + + Disclaimer: This is a simple mortgage comparison tool + that calculates monthly payments based on the interest rate, loan + amount, and term. The numbers are all estimates based on my own + experience and research. Always consult with a financial advisor before + making any decisions. + + + + If you found this useful, consider{" "} + + buying me a coffee! {" "} + + + + +
+ ); }; interface MortgageOptionProps { - fees: MortgageFees; - id: number; - interestRate: number | undefined; - useGlobalInterestRate: boolean; - maxLoan: number; - term: number; + fees: MortgageFees; + firstTimeBuyer: boolean; + id: number; + interestRate: number | undefined; + useGlobalInterestRate: boolean; + maxLoan: number; + term: number; } const MortgageOption: React.FC = ( - props: MortgageOptionProps + props: MortgageOptionProps ) => { - const { fees, id, interestRate, useGlobalInterestRate, maxLoan, term } = - props; - - return ( - - -

Option {id}

- - -
-
- ); + const { + fees, + firstTimeBuyer, + id, + interestRate, + useGlobalInterestRate, + maxLoan, + term, + } = props; + + return ( + + +

Option {id}

+ + +
+
+ ); }; diff --git a/src/components/MortgageDetails/MortgageDetails.mapper.ts b/src/components/MortgageDetails/MortgageDetails.mapper.ts index 7f031d3..ee98f10 100644 --- a/src/components/MortgageDetails/MortgageDetails.mapper.ts +++ b/src/components/MortgageDetails/MortgageDetails.mapper.ts @@ -1,62 +1,118 @@ export interface MortgageFees { - valuationFee: number; - surveyFee: number; - legalFee: number; - stampDuty?: number; - searchFee: number; - registerOfDeedsFee: number; - landRegistryFee: number; + valuationFee: number; + surveyFee: number; + legalFee: number; + stampDuty?: number; + searchFee: number; + registerOfDeedsFee: number; + landRegistryFee: number; } -export const savingsRequired = (houseValue: number, deposit: number, fees: MortgageFees) => { - return deposit + fees.valuationFee + fees.surveyFee + fees.legalFee + (fees.stampDuty ?? (houseValue / 100)) + fees.searchFee + fees.registerOfDeedsFee + fees.landRegistryFee; +export const savingsRequired = ( + houseValue: number, + deposit: number, + fees: MortgageFees +) => { + return ( + deposit + + fees.valuationFee + + fees.surveyFee + + fees.legalFee + + (fees.stampDuty ?? houseValue / 100) + + fees.searchFee + + fees.registerOfDeedsFee + + fees.landRegistryFee + ); }; export const formatter = new Intl.NumberFormat("en-IE", { - style: "currency", - currency: "EUR", - minimumFractionDigits: 2, + style: "currency", + currency: "EUR", + minimumFractionDigits: 2, }); export const cleanLoanAmount = ( - housePrice: number, - loanAmount: number, - maxLoanAmount: number | undefined + firstTimeBuyer: boolean, + housePrice: number, + loanAmount: number, + maxLoanAmount: number | undefined ): number => { - if (loanAmount < 0) { - return 0; - } else if (loanAmount > housePrice * 0.9) { - return cleanLoanAmount(housePrice, housePrice * 0.9, maxLoanAmount); - } else if (maxLoanAmount && loanAmount > maxLoanAmount) { - return maxLoanAmount; + if (loanAmount < 0) { + return 0; + } else if ( + (firstTimeBuyer && loanAmount > housePrice * 0.9) || + (!firstTimeBuyer && loanAmount > housePrice * 0.8) + ) { + if (firstTimeBuyer) { + return cleanLoanAmount(true, housePrice, housePrice * 0.9, maxLoanAmount); } else { - return parseFloat(loanAmount.toFixed(2)); + return cleanLoanAmount( + false, + housePrice, + housePrice * 0.8, + maxLoanAmount + ); } + } else if (maxLoanAmount && loanAmount > maxLoanAmount) { + return maxLoanAmount; + } else { + return parseFloat(loanAmount.toFixed(2)); + } }; export const getMonthlyPayment = ( - loanAmount: number, - interestRate: number, - loanTerm: number + loanAmount: number, + interestRate: number, + loanTerm: number ) => { - if (isNaN(loanAmount) || isNaN(interestRate) || isNaN(loanTerm)) { - return 0; - } + if (isNaN(loanAmount) || isNaN(interestRate) || isNaN(loanTerm)) { + return 0; + } - const monthlyInterestRate = interestRate / 100 / 12; - const numberOfPayments = loanTerm * 12; - const numerator = - loanAmount * - monthlyInterestRate * - (1 + monthlyInterestRate) ** numberOfPayments; - const denominator = (1 + monthlyInterestRate) ** numberOfPayments - 1; - return parseFloat((numerator / denominator).toFixed(2)); + const monthlyInterestRate = interestRate / 100 / 12; + const numberOfPayments = loanTerm * 12; + const numerator = + loanAmount * + monthlyInterestRate * + (1 + monthlyInterestRate) ** numberOfPayments; + const denominator = (1 + monthlyInterestRate) ** numberOfPayments - 1; + return parseFloat((numerator / denominator).toFixed(2)); }; -export const setLoanAmountToMax = (houseValue: number, maxLoanAmount: number, setLoanAmount: (newValue: number) => void) => { - if (houseValue * 0.9 > maxLoanAmount && maxLoanAmount > 0) { - setLoanAmount(maxLoanAmount); - } else { - setLoanAmount(houseValue * 0.9); - } -} \ No newline at end of file +export const setLoanAmountToMax = ( + firstTimeBuyer: boolean, + houseValue: number, + maxLoanAmount: number, + setLoanAmount: (newValue: number) => void +) => { + if ( + // if first time buyer and max loan amount is greater than 90% of house value and max loan amount is set + firstTimeBuyer && + houseValue * 0.9 > maxLoanAmount && + maxLoanAmount > 0 + ) { + setLoanAmount(maxLoanAmount); + console.log("if, maxLoanAmount", maxLoanAmount); + } else if ( + // if not first time buyer and max loan amount is greater than 80% of house value and max loan amount is set + !firstTimeBuyer && + houseValue * 0.8 > maxLoanAmount && + maxLoanAmount > 0 + ) { + setLoanAmount(maxLoanAmount); + console.log("else if, maxLoanAmount", maxLoanAmount); + } else { + // if first time buyer set loan amount to 90% of house value, otherwise set to 80% of house value + firstTimeBuyer + ? setLoanAmount(houseValue * 0.9) + : setLoanAmount(houseValue * 0.8); + console.log( + "else, houseValue", + houseValue, + "firstTimeBuyer", + firstTimeBuyer, + "maxLoanAmount", + maxLoanAmount + ); + } +}; diff --git a/src/components/MortgageDetails/MortgageDetails.tsx b/src/components/MortgageDetails/MortgageDetails.tsx index 094a7b3..37be6ab 100644 --- a/src/components/MortgageDetails/MortgageDetails.tsx +++ b/src/components/MortgageDetails/MortgageDetails.tsx @@ -1,227 +1,219 @@ import { - Icon, - PrimaryButton, - Stack, - Text, - TextField, - TooltipHost, + Icon, + PrimaryButton, + Stack, + Text, + TextField, + TooltipHost, } from "@fluentui/react"; import React, { useEffect } from "react"; import { - MortgageFees, - formatter, - getMonthlyPayment, - savingsRequired, - setLoanAmountToMax, + MortgageFees, + formatter, + getMonthlyPayment, + savingsRequired, + setLoanAmountToMax, } from "./MortgageDetails.mapper"; export interface MortgageDetailsProps { - id: number; - fees: MortgageFees; - interestRate: number | undefined; - maxLoan: number; - term: number; + id: number; + fees: MortgageFees; + firstTimeBuyer: boolean; + interestRate: number | undefined; + maxLoan: number; + term: number; } export const MortgageDetails: React.FC = ( - props: MortgageDetailsProps + props: MortgageDetailsProps ) => { - const { id, fees, interestRate, maxLoan, term } = props; - - const localStorageData = localStorage.getItem("mortgageOption" + id); - - const [localInterestRate, setLocalInterestRate] = React.useState( - localStorageData - ? JSON.parse(localStorageData).interestRate - : interestRate ?? 4.0 - ); - const [houseValue, setHouseValue] = React.useState( - localStorageData ? JSON.parse(localStorageData).houseValue : 0 - ); - const [loanAmount, setLoanAmount] = React.useState( - localStorageData ? JSON.parse(localStorageData).loanAmount : 0 + const { id, fees, firstTimeBuyer, interestRate, maxLoan, term } = props; + + const localStorageData = localStorage.getItem("mortgageOption" + id); + + const [localInterestRate, setLocalInterestRate] = React.useState( + localStorageData + ? JSON.parse(localStorageData).interestRate + : interestRate ?? 4.0 + ); + const [houseValue, setHouseValue] = React.useState( + localStorageData ? JSON.parse(localStorageData).houseValue : 0 + ); + const [loanAmount, setLoanAmount] = React.useState( + localStorageData ? JSON.parse(localStorageData).loanAmount : 0 + ); + + const containerStackStyles = { + root: { alignItems: "center" }, + }; + const containerStackTokens = { childrenGap: 30 }; + + const handleLoanAmountChange = (newValue: string | undefined) => { + if (newValue === undefined) { + return; + } else if (newValue === "") { + setLoanAmount(0); + return; + } else if (isNaN(parseFloat(newValue))) { + return; + } else if (parseFloat(newValue) > maxLoan && maxLoan > 0) { + setLoanAmount(maxLoan); + return; + } else if (parseFloat(newValue) < 0) { + setLoanAmount(0); + return; + } else { + setLoanAmount(parseFloat(newValue)); + } + }; + + useEffect(() => { + localStorage.setItem( + "mortgageOption" + id, + JSON.stringify({ houseValue, loanAmount, localInterestRate }) ); - - const containerStackStyles = { - root: { alignItems: "center" }, - }; - const containerStackTokens = { childrenGap: 30 }; - - const handleLoanAmountChange = (newValue: string | undefined) => { - if (newValue === undefined) { - return; - } else if (newValue === "") { - setLoanAmount(0); - return; - } else if (isNaN(parseFloat(newValue))) { - return; - } else if (parseFloat(newValue) > maxLoan && maxLoan > 0) { - setLoanAmount(maxLoan); - return; - } else if (parseFloat(newValue) < 0) { - setLoanAmount(0); - return; - } else { - setLoanAmount(parseFloat(newValue)); - } - }; - - useEffect(() => { - localStorage.setItem( - "mortgageOption" + id, - JSON.stringify({ houseValue, loanAmount, localInterestRate }) - ); - }, [houseValue, loanAmount, localInterestRate, id]); - - return ( - - - { - if (newValue === undefined) { - return; - } else if (isNaN(parseFloat(newValue))) { - return; - } else { - setHouseValue(parseFloat(newValue)); - } - }} - prefix="€" - type="number" - value={houseValue.toString()} - /> - - - {interestRate === undefined && ( - - { - setLocalInterestRate( - newValue === undefined - ? 0 - : parseFloat(newValue) - ); - }} - suffix="%" - type="number" - value={localInterestRate?.toString()} - /> - - )} - - - { - handleLoanAmountChange(newValue); - }} - prefix="€" - type="number" - value={loanAmount.toString()} - /> - - - - OR - - - - - setLoanAmountToMax(houseValue, maxLoan, setLoanAmount) - } - > - Set loan to max - {" "} - - - - - - - Savings Required: - - - - ( - - - Deposit:{" "} - {formatter.format(houseValue - loanAmount)} - - - Fees:{" "} - {formatter.format( - savingsRequired(0, 0, fees) - )} - - - Stamp Duty:{" "} - {formatter.format(houseValue * 0.01)} - - - ), - }} - > - - {formatter.format( - savingsRequired( - houseValue, - houseValue - loanAmount, - fees - ) - )}{" "} - - - - - - - Monthly Payment: - - - - - {formatter.format( - getMonthlyPayment( - loanAmount, - interestRate ?? localInterestRate, - term - ) - )} + }, [houseValue, loanAmount, localInterestRate, id]); + + return ( + + + { + if (newValue === undefined) { + return; + } else if (isNaN(parseFloat(newValue))) { + return; + } else { + setHouseValue(parseFloat(newValue)); + } + }} + prefix="€" + type="number" + value={houseValue.toString()} + /> + + + {interestRate === undefined && ( + + { + setLocalInterestRate( + newValue === undefined ? 0 : parseFloat(newValue) + ); + }} + suffix="%" + type="number" + value={localInterestRate?.toString()} + /> + + )} + + + { + handleLoanAmountChange(newValue); + }} + prefix="€" + type="number" + value={loanAmount.toString()} + /> + + + + OR + + + + + setLoanAmountToMax( + firstTimeBuyer, + houseValue, + maxLoan, + setLoanAmount + ) + } + > + Set loan to max + {" "} + + + + + + + Savings Required: + + + + ( + + + Deposit: {formatter.format(houseValue - loanAmount)} - - - ); + + Fees: {formatter.format(savingsRequired(0, 0, fees))} + + + Stamp Duty: {formatter.format(houseValue * 0.01)} + + + ), + }} + > + + {formatter.format( + savingsRequired(houseValue, houseValue - loanAmount, fees) + )}{" "} + + + + + + + Monthly Payment: + + + + + {formatter.format( + getMonthlyPayment( + loanAmount, + interestRate ?? localInterestRate, + term + ) + )} + + + + ); };