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
+ )
+ )}
+
+
+
+ );
};